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
}