using ElectionSystem.Api.Infrastructure.Auditing; using ElectionSystem.Api.Infrastructure.Persistence; using ElectionSystem.Api.Common.Pagination; using ElectionSystem.Api.Modules.Identity.Domain; using ElectionSystem.Api.Modules.Organization; using ElectionSystem.Api.Modules.Organization.Domain; using ElectionSystem.Api.Modules.Voters.Domain; using System.Text; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; namespace ElectionSystem.Api.Tests; public sealed class PersistenceModelTests { [Fact] public async Task UserName_must_be_unique() { var options = new DbContextOptionsBuilder().UseInMemoryDatabase(Guid.NewGuid().ToString()).Options; await using var context = new ElectionDbContext(options); context.Users.Add(new ApplicationUser { UserName = "admin", DisplayName = "Administrator" }); await context.SaveChangesAsync(); var index = context.Model.FindEntityType(typeof(ApplicationUser))!.GetIndexes().Single(x => x.Properties.Single().Name == nameof(ApplicationUser.UserName)); Assert.True(index.IsUnique); } [Fact] public void Audit_entry_does_not_contain_vote_content_field() { var properties = typeof(AuditEntry).GetProperties().Select(property => property.Name); Assert.DoesNotContain(properties, property => property.Contains("Vote", StringComparison.OrdinalIgnoreCase)); } [Fact] public async Task Provincial_administrator_scope_includes_all_descendant_regions() { var options = new DbContextOptionsBuilder().UseInMemoryDatabase(Guid.NewGuid().ToString()).Options; await using var context = new ElectionDbContext(options); var province = new AdministrativeRegion { Code = "11", Name = "省", Level = AdministrativeRegionLevel.Province, Source = "test", SourceVersion = "test" }; var city = new AdministrativeRegion { Code = "1101", Name = "市", Level = AdministrativeRegionLevel.City, ParentId = province.Id, Source = "test", SourceVersion = "test" }; var village = new AdministrativeRegion { Code = "110101001001", Name = "村", Level = AdministrativeRegionLevel.Village, ParentId = city.Id, Source = "test", SourceVersion = "test" }; var admin = new ApplicationUser { UserName = "province-admin", DisplayName = "Province administrator" }; context.AddRange(province, city, village, admin); context.UserRegionalRoleAssignments.Add(new UserRegionalRoleAssignment { UserId = admin.Id, RegionId = province.Id, Role = RegionalRole.ProvinceAdministrator }); await context.SaveChangesAsync(); var scope = await new RegionScopeService(context).GetManagedRegionIdsAsync(admin.Id, CancellationToken.None); Assert.Equal(3, scope.Count); Assert.Contains(village.Id, scope); } [Fact] public void Voter_number_and_region_status_indexes_are_defined() { var options = new DbContextOptionsBuilder().UseInMemoryDatabase(Guid.NewGuid().ToString()).Options; using var context = new ElectionDbContext(options); var entity = context.Model.FindEntityType(typeof(VoterProfile))!; Assert.Contains(entity.GetIndexes(), x => x.IsUnique && x.Properties.Single().Name == nameof(VoterProfile.VoterNumber)); Assert.Contains(entity.GetIndexes(), x => x.Properties.Select(p => p.Name).SequenceEqual(new[] { nameof(VoterProfile.RegionId), nameof(VoterProfile.Status), nameof(VoterProfile.DisplayName) })); } [Fact] public async Task AreaCity_import_preserves_parent_chain_and_four_levels() { const string csv = "id,pid,deep,name\n11,0,0,北京市\n1101,11,1,市辖区\n110101,1101,2,东城区\n110101001,110101,3,东华门街道\n"; var options = new DbContextOptionsBuilder().UseInMemoryDatabase(Guid.NewGuid().ToString()).Options; await using var context = new ElectionDbContext(options); await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(csv)); var result = await new AreaCityCatalogImportService(context).ImportAsync(stream, "test-2026", CancellationToken.None); Assert.Equal(4, result.CreatedCount); var township = await context.AdministrativeRegions.SingleAsync(x => x.Code == "110101001"); var district = await context.AdministrativeRegions.SingleAsync(x => x.Code == "110101"); Assert.Equal(district.Id, township.ParentId); Assert.Equal(AdministrativeRegionLevel.Township, township.Level); } [Fact] public void Local_identity_password_hash_cannot_be_reused_as_plaintext() { var user = new ApplicationUser { UserName = "local-admin", DisplayName = "Local administrator" }; var hasher = new PasswordHasher(); var hash = hasher.HashPassword(user, "Correct-Horse-7!Battery"); Assert.NotEqual("Correct-Horse-7!Battery", hash); Assert.NotEqual(PasswordVerificationResult.Failed, hasher.VerifyHashedPassword(user, hash, "Correct-Horse-7!Battery")); Assert.Equal(PasswordVerificationResult.Failed, hasher.VerifyHashedPassword(user, hash, "wrong-password")); } [Fact] public void Cursor_token_round_trips_and_rejects_malformed_values() { var cursor = new TestCursor("张三", Guid.NewGuid()); var encoded = CursorToken.Encode(cursor); Assert.True(CursorToken.TryDecode(encoded, out var decoded)); Assert.Equal(cursor, decoded); Assert.False(CursorToken.TryDecode("not-a-cursor", out _)); } [Fact] public async Task Four_level_filter_returns_selected_subtree_and_rejects_cross_branch_selection() { var options = new DbContextOptionsBuilder().UseInMemoryDatabase(Guid.NewGuid().ToString()).Options; await using var context = new ElectionDbContext(options); var province = Region("11", "省", AdministrativeRegionLevel.Province); var city = Region("1101", "市", AdministrativeRegionLevel.City, province.Id); var district = Region("110101", "区", AdministrativeRegionLevel.District, city.Id); var township = Region("110101001", "镇", AdministrativeRegionLevel.Township, district.Id); var village = Region("110101001001", "村", AdministrativeRegionLevel.Village, township.Id); var otherCity = Region("1201", "另一市", AdministrativeRegionLevel.City, province.Id); context.AddRange(province, city, district, township, village, otherCity); await context.SaveChangesAsync(); var service = new RegionCascadeFilterService(context); var scope = new HashSet { province.Id, city.Id, district.Id, township.Id, village.Id, otherCity.Id }; var result = await service.ResolveAsync(scope, new AdministrativeRegionFilter(province.Id, city.Id, district.Id, township.Id), CancellationToken.None); Assert.Equal(new[] { township.Id, village.Id }.Order(), result.Order()); await Assert.ThrowsAsync(() => service.ResolveAsync(scope, new AdministrativeRegionFilter(province.Id, otherCity.Id, district.Id, null), CancellationToken.None)); } private static AdministrativeRegion Region(string code, string name, AdministrativeRegionLevel level, Guid? parentId = null) => new() { Code = code, Name = name, Level = level, ParentId = parentId, Source = "test", SourceVersion = "test" }; private sealed record TestCursor(string Name, Guid Id); }