using Centron.BusinessLogic.Administration.AccessTokens; using Centron.BusinessLogic.WebServices.Administration.AccessTokens; using JetBrains.Annotations; namespace Centron.Tests.BL.Administration.AccessTokens; /// /// Unit tests for AccessTokenWebServiceBL and AccessTokenBL. /// These tests focus on logic that can be tested without full database setup. /// For integration tests with rights checks, see the EndToEnd tests. /// [TestSubject(typeof(AccessTokenWebServiceBL))] public class AccessTokenWebServiceBLTest { #region Token Hash Tests [Fact] public void AccessTokenBL_HashToken_ReturnsDeterministicHash() { // Arrange var plainToken = "test-token-value"; // Act var hash1 = AccessTokenBL.HashToken(plainToken); var hash2 = AccessTokenBL.HashToken(plainToken); // Assert Assert.Equal(hash1, hash2); } [Fact] public void AccessTokenBL_HashToken_ReturnsDifferentHashForDifferentTokens() { // Arrange var token1 = "token-one"; var token2 = "token-two"; // Act var hash1 = AccessTokenBL.HashToken(token1); var hash2 = AccessTokenBL.HashToken(token2); // Assert Assert.NotEqual(hash1, hash2); } [Fact] public void AccessTokenBL_HashToken_ReturnsNonEmptyString() { // Arrange var plainToken = "test-token"; // Act var hash = AccessTokenBL.HashToken(plainToken); // Assert Assert.NotNull(hash); Assert.NotEmpty(hash); // SHA-256 produces 64 hex characters Assert.Equal(64, hash.Length); } [Fact] public void AccessTokenBL_HashToken_ReturnsUppercaseHex() { // Arrange var plainToken = "test-token"; // Act var hash = AccessTokenBL.HashToken(plainToken); // Assert Assert.Matches("^[A-F0-9]+$", hash); } [Fact] public void AccessTokenBL_HashToken_ConsistentAcrossRuns() { // This test verifies that the hash is deterministic and consistent // Arrange var plainToken = "consistent-test-token"; // Act var actualHash = AccessTokenBL.HashToken(plainToken); // Verify it's consistent across multiple calls var hash1 = AccessTokenBL.HashToken(plainToken); var hash2 = AccessTokenBL.HashToken(plainToken); // Assert Assert.Equal(hash1, hash2); Assert.Equal(64, actualHash.Length); // SHA-256 = 256 bits = 32 bytes = 64 hex chars } [Theory] [InlineData("")] [InlineData("a")] [InlineData("short")] [InlineData("this-is-a-longer-token-value-for-testing")] [InlineData("special!@#$%^&*()characters")] [InlineData("unicode-äöü-日本語-🔒")] public void AccessTokenBL_HashToken_HandlesVariousInputs(string plainToken) { // Act var hash = AccessTokenBL.HashToken(plainToken); // Assert Assert.NotNull(hash); Assert.Equal(64, hash.Length); Assert.Matches("^[A-F0-9]+$", hash); } #endregion #region Token Generation Tests [Fact] public void GenerateTestToken_ReturnsNonEmptyString() { // Act var token = GenerateTestToken(); // Assert Assert.NotNull(token); Assert.NotEmpty(token); } [Fact] public void GenerateTestToken_ReturnsUniqueTokens() { // Act var token1 = GenerateTestToken(); var token2 = GenerateTestToken(); // Assert Assert.NotEqual(token1, token2); } [Fact] public void GenerateTestToken_ReturnsUrlSafeString() { // Act var token = GenerateTestToken(); // Assert - URL-safe Base64 uses - and _ instead of + and / Assert.DoesNotContain("+", token); Assert.DoesNotContain("/", token); Assert.DoesNotContain("=", token); } [Fact] public void GenerateTestToken_HasSufficientLength() { // Act var token = GenerateTestToken(); // Assert - 32 bytes encoded in Base64 should be ~43 characters Assert.True(token.Length >= 32, $"Token length {token.Length} is too short for security"); } #endregion #region Hash Verification Tests [Fact] public void HashToken_VerificationWorkflow_MatchingToken_Succeeds() { // Arrange - Simulate the token creation and verification workflow var plainToken = GenerateTestToken(); var storedHash = AccessTokenBL.HashToken(plainToken); // Act - Later, when verifying var providedHash = AccessTokenBL.HashToken(plainToken); // Assert Assert.Equal(storedHash, providedHash); } [Fact] public void HashToken_VerificationWorkflow_WrongToken_Fails() { // Arrange - Simulate the token creation and verification workflow var originalToken = GenerateTestToken(); var storedHash = AccessTokenBL.HashToken(originalToken); var wrongToken = GenerateTestToken(); // Act - Attacker tries with wrong token var providedHash = AccessTokenBL.HashToken(wrongToken); // Assert Assert.NotEqual(storedHash, providedHash); } [Fact] public void HashToken_SimilarTokens_ProduceDifferentHashes() { // This tests that even minor changes produce completely different hashes // Arrange var token1 = "test-token-123"; var token2 = "test-token-124"; // One character different // Act var hash1 = AccessTokenBL.HashToken(token1); var hash2 = AccessTokenBL.HashToken(token2); // Assert - Hashes should be completely different (avalanche effect) Assert.NotEqual(hash1, hash2); // Count differing characters - should be significant var differences = hash1.Zip(hash2, (a, b) => a != b).Count(d => d); Assert.True(differences > 20, $"Hashes are too similar: only {differences} characters differ"); } #endregion #region Helper Methods /// /// Generates a test token similar to the internal AccessTokenBL method. /// private static string GenerateTestToken() { var bytes = new byte[32]; using (var rng = System.Security.Cryptography.RandomNumberGenerator.Create()) { rng.GetBytes(bytes); } return Convert.ToBase64String(bytes).Replace("+", "-").Replace("/", "_").TrimEnd('='); } #endregion }