using System.Net; using System.Net.Http.Headers; using System.Net.Http.Json; using Campaign_Tracker.Server.Audit; using Microsoft.Extensions.DependencyInjection; namespace Campaign_Tracker.Server.Tests; public sealed class MunicipalityContactControllerTests { [Fact] public async Task AddContact_ValidRequest_Returns201WithContactTypeAndDetails_AC1() { await using var factory = new AuthIntegrationTestFactory(); using var client = CreateClient(factory); var profile = await CreateProfile(client); var response = await client.PostAsJsonAsync( $"/api/municipalities/{profile.ProfileId}/contacts", new { contactType = "Primary", name = "Ada Clerk", roleTitle = "Town Clerk", phone = "555-0100", email = "ada@example.test", }); Assert.Equal(HttpStatusCode.Created, response.StatusCode); var body = await response.Content.ReadFromJsonAsync(); Assert.NotNull(body); Assert.Equal("Primary", body.ContactType); Assert.Equal("Ada Clerk", body.Name); Assert.Equal("Town Clerk", body.RoleTitle); Assert.Equal("555-0100", body.Phone); Assert.Equal("ada@example.test", body.Email); } [Fact] public async Task GetContacts_DisplaysPrimaryAndSecondaryDesignations_AC2() { await using var factory = new AuthIntegrationTestFactory(); using var client = CreateClient(factory); var profile = await CreateProfile(client); await client.PostAsJsonAsync( $"/api/municipalities/{profile.ProfileId}/contacts", new { contactType = "Secondary", name = "Backup Clerk" }); await client.PostAsJsonAsync( $"/api/municipalities/{profile.ProfileId}/contacts", new { contactType = "Primary", name = "Main Clerk" }); var response = await client.GetAsync($"/api/municipalities/{profile.ProfileId}/contacts"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadFromJsonAsync(); Assert.NotNull(body); Assert.Equal(["Primary", "Secondary"], body.Select(c => c.ContactType).ToArray()); } [Fact] public async Task UpdateAndDeleteContact_RecordAuditEvents_AC3() { await using var factory = new AuthIntegrationTestFactory(); using var client = CreateClient(factory); var profile = await CreateProfile(client); var created = await (await client.PostAsJsonAsync( $"/api/municipalities/{profile.ProfileId}/contacts", new { contactType = "Primary", name = "Ada Clerk" })) .Content.ReadFromJsonAsync(); var updateResponse = await client.PutAsJsonAsync( $"/api/municipalities/{profile.ProfileId}/contacts/{created!.ContactId}", new { contactType = "Secondary", name = "Ada Updated", roleTitle = "Manager" }); var deleteResponse = await client.DeleteAsync( $"/api/municipalities/{profile.ProfileId}/contacts/{created.ContactId}"); Assert.Equal(HttpStatusCode.OK, updateResponse.StatusCode); Assert.Equal(HttpStatusCode.NoContent, deleteResponse.StatusCode); var auditService = factory.Services.GetRequiredService(); var events = auditService.GetRecent(); Assert.Contains(events, e => e.EventType == "MUNICIPALITY_CONTACT_UPDATED" && e.ActorIdentity == "cs@example.test"); Assert.Contains(events, e => e.EventType == "MUNICIPALITY_CONTACT_DELETED" && e.ActorIdentity == "cs@example.test"); } [Theory] [InlineData("", "Primary", "Name is required.")] [InlineData("Ada Clerk", "", "Contact type is required.")] public async Task AddContact_MissingRequiredFields_Returns422Problem_AC4( string name, string contactType, string expectedError) { await using var factory = new AuthIntegrationTestFactory(); using var client = CreateClient(factory); var profile = await CreateProfile(client); var response = await client.PostAsJsonAsync( $"/api/municipalities/{profile.ProfileId}/contacts", new { contactType, name }); Assert.Equal(HttpStatusCode.UnprocessableEntity, response.StatusCode); var problem = await response.Content.ReadFromJsonAsync(); Assert.NotNull(problem); Assert.Equal(expectedError, problem.Error); } [Fact] public async Task AddContact_UnknownProfile_Returns404() { await using var factory = new AuthIntegrationTestFactory(); using var client = CreateClient(factory); var response = await client.PostAsJsonAsync( "/api/municipalities/does-not-exist/contacts", new { contactType = "Primary", name = "Ada Clerk" }); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } [Fact] public async Task ContactRoutes_RejectContactFromDifferentProfile() { await using var factory = new AuthIntegrationTestFactory(); using var client = CreateClient(factory); var profileA = await CreateProfile(client, "FAIR01"); var profileB = await CreateProfile(client, "LAKE02"); var created = await (await client.PostAsJsonAsync( $"/api/municipalities/{profileA.ProfileId}/contacts", new { contactType = "Primary", name = "Ada Clerk" })) .Content.ReadFromJsonAsync(); var getResponse = await client.GetAsync( $"/api/municipalities/{profileB.ProfileId}/contacts/{created!.ContactId}"); var updateResponse = await client.PutAsJsonAsync( $"/api/municipalities/{profileB.ProfileId}/contacts/{created.ContactId}", new { contactType = "Secondary", name = "Wrong Profile" }); var deleteResponse = await client.DeleteAsync( $"/api/municipalities/{profileB.ProfileId}/contacts/{created.ContactId}"); Assert.Equal(HttpStatusCode.NotFound, getResponse.StatusCode); Assert.Equal(HttpStatusCode.NotFound, updateResponse.StatusCode); Assert.Equal(HttpStatusCode.NotFound, deleteResponse.StatusCode); var originalProfileResponse = await client.GetAsync( $"/api/municipalities/{profileA.ProfileId}/contacts/{created.ContactId}"); Assert.Equal(HttpStatusCode.OK, originalProfileResponse.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 static async Task CreateProfile( HttpClient client, string jCode = "FAIR01") { var created = await (await client.PostAsJsonAsync("/api/municipalities/profiles", new { jCode, displayName = $"{jCode} Profile", })).Content.ReadFromJsonAsync(); Assert.NotNull(created); return created; } private sealed record MunicipalityProfileDto(string ProfileId); private sealed record MunicipalityContactDto( string ContactId, string ProfileId, string ContactType, string Name, string? RoleTitle, string? Phone, string? Email, string UpdatedAt, string UpdatedBy); private sealed record MunicipalityContactProblemDto(string Error); }