using Xunit; namespace KArtSell.ModelOperations.UnitTests; /// /// VS-01: Identity and Roles - Domain Policy Tests /// Pure logic validation (no database, no infrastructure) /// Covers: Role hierarchy, permission grant, email validation /// public sealed class VS01_IdentityPolicyTests { // ============ Email Validation Policy ============ [Theory] [InlineData("alice@example.com")] [InlineData("bob.smith@company.co.uk")] [InlineData("user+tag@domain.org")] public void ValidateEmail_ValidFormats_AreAccepted(string email) { // Arrange & Act var isValid = EmailPolicy.IsValidFormat(email); // Assert Assert.True(isValid, $"Email '{email}' should be valid"); } [Theory] [InlineData("invalid@")] [InlineData("@domain.com")] [InlineData("alice@.com")] [InlineData("alice@@example.com")] [InlineData("alice.example.com")] public void ValidateEmail_InvalidFormats_AreRejected(string email) { // Arrange & Act var isValid = EmailPolicy.IsValidFormat(email); // Assert Assert.False(isValid, $"Email '{email}' should be invalid"); } [Fact] public void ValidateEmail_CaseSensitivity_IsNormalized() { // Policy: emails are case-insensitive, stored lowercase // Arrange var upper = "Alice@Example.COM"; var lower = EmailPolicy.Normalize(upper); // Act & Assert Assert.Equal("alice@example.com", lower); } // ============ Password Validation Policy ============ [Theory] [InlineData("TooShort", false)] // 8 chars < 12 [InlineData("ValidPassword123", true)] // 16 chars ≥ 12 [InlineData("123456789012", true)] // Exactly 12 chars public void ValidatePassword_LengthRequirement_Enforced(string password, bool expected) { // Arrange & Act var isValid = PasswordPolicy.IsValidLength(password); // Assert Assert.Equal(expected, isValid); } [Fact] public void ValidatePassword_EmptyPassword_Rejected() { // Arrange var password = ""; // Act var isValid = PasswordPolicy.IsValidLength(password); // Assert Assert.False(isValid); } // ============ Role Management Policy ============ [Fact] public void AssignRole_NewUserGetRole_IsSuccessful() { // Arrange var userId = Guid.NewGuid(); var role = "Analyst"; var user = new UserAggregate(userId, "alice@example.com"); // Act user.AssignRole(role); // Assert Assert.Contains(role, user.Roles); } [Fact] public void AssignRole_DuplicateRole_IsIdempotent() { // Arrange var userId = Guid.NewGuid(); var role = "Analyst"; var user = new UserAggregate(userId, "alice@example.com"); // Act user.AssignRole(role); var countAfterFirst = user.Roles.Count; user.AssignRole(role); // Same role again var countAfterSecond = user.Roles.Count; // Assert Assert.Equal(countAfterFirst, countAfterSecond, "Duplicate role assignment should not increase count"); } [Fact] public void RevokeRole_ActiveRole_IsRemoved() { // Arrange var userId = Guid.NewGuid(); var user = new UserAggregate(userId, "alice@example.com"); user.AssignRole("Analyst"); user.AssignRole("Trader"); // Act user.RevokeRole("Analyst"); // Assert Assert.DoesNotContain("Analyst", user.Roles); Assert.Contains("Trader", user.Roles); // Other roles unaffected } [Fact] public void RevokeRole_NonExistentRole_IsIdempotent() { // Arrange var user = new UserAggregate(Guid.NewGuid(), "alice@example.com"); // Act var threw = false; try { user.RevokeRole("NonExistentRole"); } catch { threw = true; } // Assert Assert.False(threw, "Revoking non-existent role should not throw"); } // ============ Permission Hierarchy Policy ============ [Theory] [InlineData("Admin", "read", true)] [InlineData("Admin", "write", true)] [InlineData("Admin", "approve", true)] [InlineData("Analyst", "read", true)] [InlineData("Analyst", "write", false)] [InlineData("Analyst", "approve", false)] [InlineData("Trader", "read", true)] [InlineData("Trader", "write", true)] [InlineData("Trader", "execute", true)] [InlineData("Viewer", "read", true)] [InlineData("Viewer", "write", false)] public void PermissionHierarchy_RoleActions_AreEnforced(string role, string action, bool expected) { // Arrange & Act var hasPermission = PermissionPolicy.CanPerform(role, action); // Assert Assert.Equal(expected, hasPermission, $"Role '{role}' should {'NOT ' if !expected:string.Empty}be able to '{action}'"); } [Fact] public void PermissionHierarchy_MultipleRoles_AreUnioned() { // Policy: If user has multiple roles, they can perform ANY of the role's actions // Arrange var roles = new[] { "Analyst", "Trader" }; // Act var canRead = roles.Any(r => PermissionPolicy.CanPerform(r, "read")); var canWrite = roles.Any(r => PermissionPolicy.CanPerform(r, "write")); var canApprove = roles.Any(r => PermissionPolicy.CanPerform(r, "approve")); // Assert Assert.True(canRead, "Should have read permission"); Assert.True(canWrite, "Should have write permission (from Trader)"); Assert.False(canApprove, "Should NOT have approve permission"); } // ============ User Status Transitions ============ [Theory] [InlineData("active", "inactive", true)] [InlineData("active", "suspended", true)] [InlineData("inactive", "active", true)] [InlineData("inactive", "suspended", true)] [InlineData("suspended", "active", false)] // Cannot reactivate from suspended [InlineData("suspended", "inactive", false)] // Cannot reactivate from suspended public void UserStatus_Transitions_AreValidated(string from, string to, bool valid) { // Arrange & Act var canTransition = UserStatusPolicy.CanTransition(from, to); // Assert Assert.Equal(valid, canTransition, $"Transition '{from}' → '{to}' should be {(valid ? "allowed" : "forbidden")}"); } [Fact] public void UserStatus_SuspendedUser_CannotLogin() { // Arrange var user = new UserAggregate(Guid.NewGuid(), "alice@example.com"); user.UpdateStatus("suspended"); // Act var canLogin = user.CanLogin(); // Assert Assert.False(canLogin, "Suspended user should not be able to login"); } // ============ Admin-Only Operations ============ [Fact] public void AdminOnly_CreateUser_RequiresAdminRole() { // Arrange var adminUser = new UserAggregate(Guid.NewGuid(), "admin@example.com"); adminUser.AssignRole("Admin"); var analystUser = new UserAggregate(Guid.NewGuid(), "analyst@example.com"); analystUser.AssignRole("Analyst"); var newUserEmail = "newuser@example.com"; // Act & Assert Assert.True(AdminPolicy.CanCreateUser(adminUser), "Admin should be able to create users"); Assert.False(AdminPolicy.CanCreateUser(analystUser), "Non-admin should NOT be able to create users"); } [Fact] public void AdminOnly_ModifyRoles_RequiresAdminRole() { // Arrange var admin = new UserAggregate(Guid.NewGuid(), "admin@example.com"); admin.AssignRole("Admin"); var analyst = new UserAggregate(Guid.NewGuid(), "analyst@example.com"); analyst.AssignRole("Analyst"); // Act & Assert Assert.True(AdminPolicy.CanModifyRoles(admin), "Admin should be able to modify roles"); Assert.False(AdminPolicy.CanModifyRoles(analyst), "Analyst should NOT be able to modify roles"); } // ============ Immutability Policy ============ [Fact] public void Immutability_Email_CannotBeChanged() { // Arrange var user = new UserAggregate(Guid.NewGuid(), "alice@example.com"); // Act var canChange = user.CanChangeEmail("newemail@example.com"); // Assert Assert.False(canChange, "Email should be immutable after creation"); } [Fact] public void Immutability_UserId_CannotBeChanged() { // Arrange var originalId = Guid.NewGuid(); var user = new UserAggregate(originalId, "alice@example.com"); // Act var canChange = user.CanChangeId(Guid.NewGuid()); // Assert Assert.False(canChange, "User ID should be immutable"); } // ============ Soft Delete Policy ============ [Fact] public void SoftDelete_InactiveUser_DoesNotAppearInLists() { // Arrange var activeUser = new UserAggregate(Guid.NewGuid(), "active@example.com"); var inactiveUser = new UserAggregate(Guid.NewGuid(), "inactive@example.com"); inactiveUser.UpdateStatus("inactive"); var users = new[] { activeUser, inactiveUser }; // Act var activeCount = users.Count(u => u.CanLogin()); // Assert Assert.Equal(1, activeCount, "Only active users should be counted"); } // ============ Consistency Checks ============ [Fact] public void Consistency_UserWithoutRoles_IsInvalid() { // Policy: Every user must have at least one role // Arrange var user = new UserAggregate(Guid.NewGuid(), "alice@example.com"); // Act var isValid = user.IsValid(); // Assert Assert.False(isValid, "User must have at least one role"); } [Fact] public void Consistency_UserWithValidRole_IsValid() { // Arrange var user = new UserAggregate(Guid.NewGuid(), "alice@example.com"); user.AssignRole("Analyst"); // Act var isValid = user.IsValid(); // Assert Assert.True(isValid, "User with valid role should be valid"); } [Fact] public void Consistency_UserWithInvalidRole_IsRejected() { // Arrange var user = new UserAggregate(Guid.NewGuid(), "alice@example.com"); // Act & Assert Assert.Throws(() => user.AssignRole("InvalidRole")); } } // ============ Helper Classes (Domain Policies) ============ public static class EmailPolicy { public static bool IsValidFormat(string email) { if (string.IsNullOrWhiteSpace(email)) return false; return System.Text.RegularExpressions.Regex.IsMatch( email, @"^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}$"); } public static string Normalize(string email) => email.ToLowerInvariant(); } public static class PasswordPolicy { public static bool IsValidLength(string password) => !string.IsNullOrEmpty(password) && password.Length >= 12; } public static class PermissionPolicy { private static readonly Dictionary RolePermissions = new() { { "Admin", new[] { "read", "write", "approve", "execute" } }, { "Analyst", new[] { "read" } }, { "Trader", new[] { "read", "write", "execute" } }, { "Viewer", new[] { "read" } }, }; public static bool CanPerform(string role, string action) { return RolePermissions.TryGetValue(role, out var permissions) && permissions.Contains(action); } } public static class UserStatusPolicy { public static bool CanTransition(string from, string to) { // Suspended users cannot be reactivated if (from == "suspended") return false; return from != to; } } public static class AdminPolicy { public static bool CanCreateUser(UserAggregate user) => user.Roles.Contains("Admin"); public static bool CanModifyRoles(UserAggregate user) => user.Roles.Contains("Admin"); } public class UserAggregate { public Guid Id { get; } public string Email { get; } public List Roles { get; } = new(); public string Status { get; set; } = "active"; public UserAggregate(Guid id, string email) { Id = id; Email = EmailPolicy.Normalize(email); } public void AssignRole(string role) { if (!new[] { "Admin", "Analyst", "Trader", "Viewer" }.Contains(role)) throw new ArgumentException($"Invalid role: {role}"); if (!Roles.Contains(role)) Roles.Add(role); } public void RevokeRole(string role) { Roles.Remove(role); } public bool CanLogin() => Status == "active"; public bool CanChangeEmail(string newEmail) => false; // Always immutable public bool CanChangeId(Guid newId) => false; // Always immutable public void UpdateStatus(string newStatus) => Status = newStatus; public bool IsValid() => Roles.Count > 0 && Roles.All(r => new[] { "Admin", "Analyst", "Trader", "Viewer" }.Contains(r)); }