Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

193 lines
7.5KB

  1. using System.Net;
  2. using System.Net.Http.Headers;
  3. using System.Net.Http.Json;
  4. using Campaign_Tracker.Server.Audit;
  5. using Campaign_Tracker.Server.ElectionCycles;
  6. using Microsoft.Extensions.DependencyInjection;
  7. namespace Campaign_Tracker.Server.Tests;
  8. public sealed partial class ElectionCycleJobControllerTests
  9. {
  10. [Fact]
  11. public async Task CreateJob_ValidRequestWithExistingCycle_Returns201AndInSetupStatus_AC1_AC3()
  12. {
  13. await using var factory = new AuthIntegrationTestFactory();
  14. using var client = CreateClient(factory);
  15. var response = await client.PostAsJsonAsync(
  16. "/api/election-cycles/jobs",
  17. new { jCode = "FAIR01", cycleId = "2026-primary", cycleName = "2026 Primary" });
  18. Assert.Equal(HttpStatusCode.Created, response.StatusCode);
  19. var body = await response.Content.ReadFromJsonAsync<ElectionCycleJobDto>();
  20. Assert.NotNull(body);
  21. Assert.Equal("FAIR01", body.JCode);
  22. Assert.Equal("2026-primary", body.CycleId);
  23. Assert.Equal("2026 Primary", body.CycleName);
  24. Assert.Equal("In Setup", body.Status);
  25. Assert.Equal("cs@example.test", body.CreatedBy);
  26. }
  27. [Fact]
  28. public async Task CreateJob_NewCycleName_GeneratesCycleIdAndCreatesJob_AC1()
  29. {
  30. await using var factory = new AuthIntegrationTestFactory();
  31. using var client = CreateClient(factory);
  32. var response = await client.PostAsJsonAsync(
  33. "/api/election-cycles/jobs",
  34. new { jCode = "PINE03", cycleName = "2027 General" });
  35. Assert.Equal(HttpStatusCode.Created, response.StatusCode);
  36. var body = await response.Content.ReadFromJsonAsync<ElectionCycleJobDto>();
  37. Assert.NotNull(body);
  38. Assert.Equal("PINE03", body.JCode);
  39. Assert.Equal("2027-general", body.CycleId);
  40. Assert.Equal("2027 General", body.CycleName);
  41. Assert.Equal("In Setup", body.Status);
  42. }
  43. [Fact]
  44. public async Task CreateJob_MissingCycleSelection_Returns422WithValidationMessage_AC4()
  45. {
  46. await using var factory = new AuthIntegrationTestFactory();
  47. using var client = CreateClient(factory);
  48. var response = await client.PostAsJsonAsync(
  49. "/api/election-cycles/jobs",
  50. new { jCode = "FAIR01" });
  51. Assert.Equal(HttpStatusCode.UnprocessableEntity, response.StatusCode);
  52. var problem = await response.Content.ReadFromJsonAsync<ElectionCycleJobProblemDto>();
  53. Assert.NotNull(problem);
  54. Assert.Contains("required", problem.Error, StringComparison.OrdinalIgnoreCase);
  55. }
  56. [Fact]
  57. public async Task CreateJob_MissingJCode_ReturnsClientError_AC4()
  58. {
  59. await using var factory = new AuthIntegrationTestFactory();
  60. using var client = CreateClient(factory);
  61. var response = await client.PostAsJsonAsync(
  62. "/api/election-cycles/jobs",
  63. new { cycleId = "2026-primary", cycleName = "2026 Primary" });
  64. // Either 400 (model binding) or 422 (our validation) — both reject the request.
  65. Assert.True(response.StatusCode == HttpStatusCode.BadRequest ||
  66. response.StatusCode == HttpStatusCode.UnprocessableEntity);
  67. }
  68. [Fact]
  69. public async Task CreateJob_RejectsUnauthenticatedUser_AC5()
  70. {
  71. await using var factory = new AuthIntegrationTestFactory();
  72. using var noToken = factory.CreateClient();
  73. var response = await noToken.PostAsJsonAsync(
  74. "/api/election-cycles/jobs",
  75. new { jCode = "FAIR01", cycleId = "2026-primary", cycleName = "2026 Primary" });
  76. Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
  77. }
  78. [Fact]
  79. public async Task CreateJob_RejectsNonClientServicesRole_AC5()
  80. {
  81. await using var factory = new AuthIntegrationTestFactory();
  82. using var client = factory.CreateClient();
  83. client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
  84. "Bearer", AuthIntegrationTestFactory.CreateToken("prod@example.test", "production"));
  85. var response = await client.PostAsJsonAsync(
  86. "/api/election-cycles/jobs",
  87. new { jCode = "FAIR01", cycleId = "2026-primary", cycleName = "2026 Primary" });
  88. Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
  89. }
  90. [Fact]
  91. public async Task CreateJob_EmitsAuditEvent_AC5()
  92. {
  93. await using var factory = new AuthIntegrationTestFactory();
  94. using var client = CreateClient(factory);
  95. var response = await client.PostAsJsonAsync(
  96. "/api/election-cycles/jobs",
  97. new { jCode = "LAKE02", cycleId = "2026-primary", cycleName = "2026 Primary" });
  98. Assert.Equal(HttpStatusCode.Created, response.StatusCode);
  99. var auditService = factory.Services.GetRequiredService<IAuditService>();
  100. var events = auditService.GetRecent();
  101. Assert.Contains(events, e =>
  102. e.EventType == "ELECTION_CYCLE_JOB_CREATED" &&
  103. e.ActorIdentity == "cs@example.test" &&
  104. e.Outcome.Contains("LAKE02"));
  105. }
  106. [Fact]
  107. public async Task CreateJob_DoesNotWriteToLegacyTables_AC5()
  108. {
  109. // The repository is IElectionCycleJobRepository — an extension-layer write path.
  110. // ILegacyDataAccess has only Get* methods (read-only). Verify the interface contract.
  111. var legacyMethods = typeof(Campaign_Tracker.Server.LegacyData.ILegacyDataAccess).GetMethods();
  112. Assert.True(
  113. legacyMethods.All(m => m.Name.StartsWith("Get", StringComparison.Ordinal)),
  114. "ILegacyDataAccess must only expose read methods — no writes to legacy tables.");
  115. }
  116. [Fact]
  117. public async Task CreateJob_IdempotentForSameJCodeAndCycle()
  118. {
  119. await using var factory = new AuthIntegrationTestFactory();
  120. using var client = CreateClient(factory);
  121. var response1 = await client.PostAsJsonAsync(
  122. "/api/election-cycles/jobs",
  123. new { jCode = "FAIR01", cycleId = "2026-primary", cycleName = "2026 Primary" });
  124. var response2 = await client.PostAsJsonAsync(
  125. "/api/election-cycles/jobs",
  126. new { jCode = "FAIR01", cycleId = "2026-primary", cycleName = "2026 Primary" });
  127. Assert.Equal(HttpStatusCode.Created, response1.StatusCode);
  128. Assert.Equal(HttpStatusCode.Created, response2.StatusCode);
  129. var body1 = await response1.Content.ReadFromJsonAsync<ElectionCycleJobDto>();
  130. var body2 = await response2.Content.ReadFromJsonAsync<ElectionCycleJobDto>();
  131. Assert.NotNull(body1);
  132. Assert.NotNull(body2);
  133. Assert.Equal(body1.JobId, body2.JobId);
  134. }
  135. [Fact]
  136. public async Task GetById_UnknownJob_Returns404()
  137. {
  138. await using var factory = new AuthIntegrationTestFactory();
  139. using var client = CreateClient(factory);
  140. var response = await client.GetAsync("/api/election-cycles/jobs/does-not-exist");
  141. Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
  142. }
  143. private static HttpClient CreateClient(AuthIntegrationTestFactory factory)
  144. {
  145. var client = factory.CreateClient();
  146. client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
  147. "Bearer", AuthIntegrationTestFactory.CreateToken("cs@example.test", "client-services"));
  148. return client;
  149. }
  150. private sealed record ElectionCycleJobDto(
  151. string JobId,
  152. string JCode,
  153. string CycleId,
  154. string CycleName,
  155. string Status,
  156. string CreatedBy,
  157. string CreatedAt);
  158. private sealed record ElectionCycleJobProblemDto(string Error);
  159. }

Powered by TurnKey Linux.