QuellCode/CentronERP war nur als Gitlink (Submodul-Referenz auf 79c1142) getrackt, ohne .gitmodules und ohne erreichbares Remote. Der Untersuchungsgegenstand der Versuchsreihe war damit nicht reproduzierbar gesichert: Ein Klon haette ein leeres Verzeichnis erhalten, und die Belege der 3.287 Anforderungen waeren nicht ueberpruefbar gewesen. Umstellung: - Historie nach c:\DEV\CentronERP_git_snapshot_79c1142 ausgelagert (vollstaendig lesbar, enthaelt 79c1142 und Vorgaenger 89ccfd6) - Gitlink aus dem Index entfernt - Dateiinhalt aufgenommen: 24.557 Dateien, rund 333 MB Die verschachtelte .gitignore der Codebasis gilt weiter, Build-Artefakte bleiben ausgeschlossen. Details in Versuche/Versuch_01/_Codebasis-Nachweis.md
232 lines
6.4 KiB
C#
232 lines
6.4 KiB
C#
using Centron.BusinessLogic.Administration.AccessTokens;
|
|
using Centron.BusinessLogic.WebServices.Administration.AccessTokens;
|
|
using JetBrains.Annotations;
|
|
|
|
namespace Centron.Tests.BL.Administration.AccessTokens;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[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
|
|
|
|
/// <summary>
|
|
/// Generates a test token similar to the internal AccessTokenBL method.
|
|
/// </summary>
|
|
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
|
|
}
|