using System.Net; using System.Net.Http.Headers; using System.Net.Http.Json; using Campaign_Tracker.Server.ElectionCycles; using Campaign_Tracker.Server.ExtensionData; using Campaign_Tracker.Server.LegacyData; using Campaign_Tracker.Server.LegacyData.Models; using Campaign_Tracker.Server.Municipalities; using Microsoft.Extensions.DependencyInjection; namespace Campaign_Tracker.Server.Tests; public sealed class ElectionCycleKanbanReadModelTests { [Fact] public async Task GetKanban_GroupsActiveAssignmentsAndUnassignedMunicipalities_AC1_AC3() { await using var factory = new AuthIntegrationTestFactory(); using var client = CreateClient(factory, "client-services"); await CreateProfile(client, "FAIR01", "Fairview Display"); await CreateProfile(client, "LAKE02", null); await CreateProfile(client, "PINE03", null); var response = await client.GetAsync("/api/election-cycles/kanban"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var board = await response.Content.ReadFromJsonAsync(); Assert.NotNull(board); var primary = Assert.Single(board.Lanes, lane => lane.CycleId == "2026-primary"); Assert.Equal("2026 Primary", primary.CycleName); var fairview = Assert.Single(primary.Cards, card => card.JCode == "FAIR01"); Assert.Equal("Fairview Display", fairview.MunicipalityName); Assert.Equal("Ready", fairview.CycleJobStatus); Assert.Equal("FAIR01", fairview.LegacyJoinKey); Assert.Equal("/election-cycles/jobs/job-fair01-primary", fairview.QuickOpenHref); var unassigned = Assert.Single(board.Lanes, lane => lane.CycleId == ElectionCycleKanbanReadModel.UnassignedCycleId); Assert.Contains(unassigned.Cards, card => card.JCode == "PINE03" && card.CycleName == "Unassigned" && card.CycleJobStatus == "Unassigned"); } [Fact] public async Task ReadModel_ReturnsOneCardPerMunicipalityCyclePair_AC2() { var legacy = new InMemoryLegacyDataAccess(); var profiles = BuildProfileRepository(legacy); await profiles.CreateAsync("LAKE02", null, "test@example.test"); var jobs = new InMemoryElectionCycleJobRepository( [ new ElectionCycleJobAssignment( "job-lake-primary", "LAKE02", "2026-primary", "2026 Primary", "In progress", IsActive: true), new ElectionCycleJobAssignment( "job-lake-special", "LAKE02", "2026-special", "2026 Special", "Blocked", IsActive: true), ], TimeProvider.System); var sut = new ElectionCycleKanbanReadModel(profiles, jobs); var board = await sut.GetAsync(); var lakeCards = board.Lanes.SelectMany(lane => lane.Cards) .Where(card => card.JCode == "LAKE02") .ToArray(); Assert.Equal(2, lakeCards.Length); Assert.Contains(lakeCards, card => card.CycleId == "2026-primary"); Assert.Contains(lakeCards, card => card.CycleId == "2026-special"); } [Fact] public async Task ReadModel_ExcludesInactiveAssignmentsFromCycleLanes_AC1() { var legacy = new InMemoryLegacyDataAccess(); var profiles = BuildProfileRepository(legacy); await profiles.CreateAsync("FAIR01", null, "test@example.test"); var jobs = new InMemoryElectionCycleJobRepository( [ new ElectionCycleJobAssignment( "job-fair-old", "FAIR01", "2024-general", "2024 General", "Complete", IsActive: false), ], TimeProvider.System); var sut = new ElectionCycleKanbanReadModel(profiles, jobs); var board = await sut.GetAsync(); Assert.DoesNotContain(board.Lanes, lane => lane.CycleId == "2024-general"); var unassigned = Assert.Single(board.Lanes, lane => lane.CycleId == ElectionCycleKanbanReadModel.UnassignedCycleId); Assert.Contains(unassigned.Cards, card => card.JCode == "FAIR01"); } [Fact] public async Task GetKanban_RequiresClientServicesRole_AC1() { await using var factory = new AuthIntegrationTestFactory(); using var noToken = factory.CreateClient(); using var production = CreateClient(factory, "production"); var missingToken = await noToken.GetAsync("/api/election-cycles/kanban"); var wrongRole = await production.GetAsync("/api/election-cycles/kanban"); Assert.Equal(HttpStatusCode.Unauthorized, missingToken.StatusCode); Assert.Equal(HttpStatusCode.Forbidden, wrongRole.StatusCode); } [Fact] public async Task ReadModel_UsesReadOnlyLegacyAndExtensionRepositories_AC1() { ILegacyDataAccess legacy = new InMemoryLegacyDataAccess( jurisdictions: [ new("FAIR01", "Fairview Borough", "100 Main St", "Fairview, PA 16415", null, null), ]); var profiles = BuildProfileRepository(legacy); await profiles.CreateAsync("FAIR01", null, "test@example.test"); var jobs = new InMemoryElectionCycleJobRepository([], TimeProvider.System); var sut = new ElectionCycleKanbanReadModel(profiles, jobs); var board = await sut.GetAsync(); Assert.Contains(board.Lanes.Single(lane => lane.CycleId == ElectionCycleKanbanReadModel.UnassignedCycleId).Cards, card => card.JCode == "FAIR01"); Assert.True(typeof(ILegacyDataAccess).GetMethods().All(method => method.Name.StartsWith("Get", StringComparison.Ordinal))); } private static InMemoryMunicipalityProfileRepository BuildProfileRepository( ILegacyDataAccess legacy) => new( new LegacyLinkValidator(legacy), legacy, TimeProvider.System); private static HttpClient CreateClient(AuthIntegrationTestFactory factory, string role) { var client = factory.CreateClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Bearer", AuthIntegrationTestFactory.CreateToken("cs@example.test", role)); return client; } private static async Task CreateProfile( HttpClient client, string jCode, string? displayName) { var response = await client.PostAsJsonAsync("/api/municipalities/profiles", new { jCode, displayName, }); Assert.Equal(HttpStatusCode.OK, response.StatusCode); } private sealed record ElectionCycleKanbanDto(ElectionCycleKanbanLaneDto[] Lanes); private sealed record ElectionCycleKanbanLaneDto( string CycleId, string CycleName, ElectionCycleKanbanCardDto[] Cards); private sealed record ElectionCycleKanbanCardDto( string CardId, string MunicipalityName, string JCode, string CycleId, string CycleName, string CycleJobStatus, string LegacyJoinKey, string QuickOpenHref); }