|
- using System.Net;
- using System.Net.Http.Headers;
- using System.Net.Http.Json;
- using Campaign_Tracker.Server.Audit;
- using Campaign_Tracker.Server.ElectionCycles;
- using Microsoft.Extensions.DependencyInjection;
-
- namespace Campaign_Tracker.Server.Tests;
-
- public sealed partial class ElectionCycleJobControllerTests
- {
- [Fact]
- public async Task CreateJob_ValidRequestWithExistingCycle_Returns201AndInSetupStatus_AC1_AC3()
- {
- await using var factory = new AuthIntegrationTestFactory();
- using var client = CreateClient(factory);
-
- var response = await client.PostAsJsonAsync(
- "/api/election-cycles/jobs",
- new { jCode = "FAIR01", cycleId = "2026-primary", cycleName = "2026 Primary" });
-
- Assert.Equal(HttpStatusCode.Created, response.StatusCode);
- var body = await response.Content.ReadFromJsonAsync<ElectionCycleJobDto>();
- Assert.NotNull(body);
- Assert.Equal("FAIR01", body.JCode);
- Assert.Equal("2026-primary", body.CycleId);
- Assert.Equal("2026 Primary", body.CycleName);
- Assert.Equal("In Setup", body.Status);
- Assert.Equal("cs@example.test", body.CreatedBy);
- }
-
- [Fact]
- public async Task CreateJob_NewCycleName_GeneratesCycleIdAndCreatesJob_AC1()
- {
- await using var factory = new AuthIntegrationTestFactory();
- using var client = CreateClient(factory);
-
- var response = await client.PostAsJsonAsync(
- "/api/election-cycles/jobs",
- new { jCode = "PINE03", cycleName = "2027 General" });
-
- Assert.Equal(HttpStatusCode.Created, response.StatusCode);
- var body = await response.Content.ReadFromJsonAsync<ElectionCycleJobDto>();
- Assert.NotNull(body);
- Assert.Equal("PINE03", body.JCode);
- Assert.Equal("2027-general", body.CycleId);
- Assert.Equal("2027 General", body.CycleName);
- Assert.Equal("In Setup", body.Status);
- }
-
- [Fact]
- public async Task CreateJob_MissingCycleSelection_Returns422WithValidationMessage_AC4()
- {
- await using var factory = new AuthIntegrationTestFactory();
- using var client = CreateClient(factory);
-
- var response = await client.PostAsJsonAsync(
- "/api/election-cycles/jobs",
- new { jCode = "FAIR01" });
-
- Assert.Equal(HttpStatusCode.UnprocessableEntity, response.StatusCode);
- var problem = await response.Content.ReadFromJsonAsync<ElectionCycleJobProblemDto>();
- Assert.NotNull(problem);
- Assert.Contains("required", problem.Error, StringComparison.OrdinalIgnoreCase);
- }
-
- [Fact]
- public async Task CreateJob_MissingJCode_ReturnsClientError_AC4()
- {
- await using var factory = new AuthIntegrationTestFactory();
- using var client = CreateClient(factory);
-
- var response = await client.PostAsJsonAsync(
- "/api/election-cycles/jobs",
- new { cycleId = "2026-primary", cycleName = "2026 Primary" });
-
- // Either 400 (model binding) or 422 (our validation) — both reject the request.
- Assert.True(response.StatusCode == HttpStatusCode.BadRequest ||
- response.StatusCode == HttpStatusCode.UnprocessableEntity);
- }
-
- [Fact]
- public async Task CreateJob_RejectsUnauthenticatedUser_AC5()
- {
- await using var factory = new AuthIntegrationTestFactory();
- using var noToken = factory.CreateClient();
-
- var response = await noToken.PostAsJsonAsync(
- "/api/election-cycles/jobs",
- new { jCode = "FAIR01", cycleId = "2026-primary", cycleName = "2026 Primary" });
-
- Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
- }
-
- [Fact]
- public async Task CreateJob_RejectsNonClientServicesRole_AC5()
- {
- await using var factory = new AuthIntegrationTestFactory();
- using var client = factory.CreateClient();
- client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
- "Bearer", AuthIntegrationTestFactory.CreateToken("prod@example.test", "production"));
-
- var response = await client.PostAsJsonAsync(
- "/api/election-cycles/jobs",
- new { jCode = "FAIR01", cycleId = "2026-primary", cycleName = "2026 Primary" });
-
- Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
- }
-
- [Fact]
- public async Task CreateJob_EmitsAuditEvent_AC5()
- {
- await using var factory = new AuthIntegrationTestFactory();
- using var client = CreateClient(factory);
-
- var response = await client.PostAsJsonAsync(
- "/api/election-cycles/jobs",
- new { jCode = "LAKE02", cycleId = "2026-primary", cycleName = "2026 Primary" });
-
- Assert.Equal(HttpStatusCode.Created, response.StatusCode);
-
- var auditService = factory.Services.GetRequiredService<IAuditService>();
- var events = auditService.GetRecent();
- Assert.Contains(events, e =>
- e.EventType == "ELECTION_CYCLE_JOB_CREATED" &&
- e.ActorIdentity == "cs@example.test" &&
- e.Outcome.Contains("LAKE02"));
- }
-
- [Fact]
- public async Task CreateJob_DoesNotWriteToLegacyTables_AC5()
- {
- // The repository is IElectionCycleJobRepository — an extension-layer write path.
- // ILegacyDataAccess has only Get* methods (read-only). Verify the interface contract.
- var legacyMethods = typeof(Campaign_Tracker.Server.LegacyData.ILegacyDataAccess).GetMethods();
- Assert.True(
- legacyMethods.All(m => m.Name.StartsWith("Get", StringComparison.Ordinal)),
- "ILegacyDataAccess must only expose read methods — no writes to legacy tables.");
- }
-
- [Fact]
- public async Task CreateJob_IdempotentForSameJCodeAndCycle()
- {
- await using var factory = new AuthIntegrationTestFactory();
- using var client = CreateClient(factory);
-
- var response1 = await client.PostAsJsonAsync(
- "/api/election-cycles/jobs",
- new { jCode = "FAIR01", cycleId = "2026-primary", cycleName = "2026 Primary" });
- var response2 = await client.PostAsJsonAsync(
- "/api/election-cycles/jobs",
- new { jCode = "FAIR01", cycleId = "2026-primary", cycleName = "2026 Primary" });
-
- Assert.Equal(HttpStatusCode.Created, response1.StatusCode);
- Assert.Equal(HttpStatusCode.Created, response2.StatusCode);
-
- var body1 = await response1.Content.ReadFromJsonAsync<ElectionCycleJobDto>();
- var body2 = await response2.Content.ReadFromJsonAsync<ElectionCycleJobDto>();
- Assert.NotNull(body1);
- Assert.NotNull(body2);
- Assert.Equal(body1.JobId, body2.JobId);
- }
-
- [Fact]
- public async Task GetById_UnknownJob_Returns404()
- {
- await using var factory = new AuthIntegrationTestFactory();
- using var client = CreateClient(factory);
-
- var response = await client.GetAsync("/api/election-cycles/jobs/does-not-exist");
- Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
- }
-
- private static HttpClient CreateClient(AuthIntegrationTestFactory factory)
- {
- var client = factory.CreateClient();
- client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
- "Bearer", AuthIntegrationTestFactory.CreateToken("cs@example.test", "client-services"));
- return client;
- }
-
- private sealed record ElectionCycleJobDto(
- string JobId,
- string JCode,
- string CycleId,
- string CycleName,
- string Status,
- string CreatedBy,
- string CreatedAt);
-
- private sealed record ElectionCycleJobProblemDto(string Error);
- }
|