using System.Net; using System.Net.Http.Json; using YesChef.Api.Entities; using YesChef.Api.Features.Families; using YesChef.Api.IntegrationTests.Infrastructure; namespace YesChef.Api.IntegrationTests.Features; public class FamilyEndpointsTests : AuthenticatedIntegrationTest { // The default registered user is the first member of the bootstrap family // and therefore the admin. Tests that need a non-admin register a second // user via the same invite code to get a Member. [Test] public async Task First_registrant_is_admin() { var family = await Client.GetFromJsonAsync("/api/family"); await Assert.That(family!.MyRole).IsEqualTo("Admin"); await Assert.That(family.Members.Count).IsEqualTo(1); await Assert.That(family.Members[0].Role).IsEqualTo("Admin"); } [Test] public async Task Second_registrant_is_member() { using var second = await Data.RegisterAsync("second-user"); var family = await second.Client.GetFromJsonAsync("/api/family"); await Assert.That(family!.MyRole).IsEqualTo("Member"); await Assert.That(family.Members.Count).IsEqualTo(2); } [Test] public async Task Get_family_hides_invite_code_from_members() { using var member = await Data.RegisterAsync("member"); var asAdmin = await Client.GetFromJsonAsync("/api/family"); var asMember = await member.Client.GetFromJsonAsync("/api/family"); await Assert.That(asAdmin!.InviteCode).IsNotNull(); await Assert.That(asMember!.InviteCode).IsNull(); } [Test] public async Task Regenerate_invite_code_replaces_code_and_returns_new_one() { var beforeCode = await UseDbAsync(db => db.Families.Select(f => f.InviteCode).FirstAsync()); var response = await Client.PostAsync("/api/family/invite-code/regenerate", content: null); await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.OK); var body = await response.Content.ReadFromJsonAsync(); await Assert.That(body!.InviteCode).IsNotEqualTo(beforeCode); await Assert.That(body.InviteCode.Length).IsEqualTo(10); var afterCode = await UseDbAsync(db => db.Families.Select(f => f.InviteCode).FirstAsync()); await Assert.That(afterCode).IsEqualTo(body.InviteCode); } [Test] public async Task Regenerate_invite_code_forbidden_for_member() { using var member = await Data.RegisterAsync("member"); var response = await member.Client.PostAsync("/api/family/invite-code/regenerate", content: null); await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.Forbidden); } [Test] public async Task Update_role_promotes_member_to_admin() { using var member = await Data.RegisterAsync("member"); var memberId = await UseDbAsync(db => db.Users.Where(u => u.Name == "member").Select(u => u.Id).SingleAsync()); var response = await Client.PutAsJsonAsync($"/api/family/members/{memberId}/role", new FamilyEndpoints.UpdateRoleRequest("Admin")); await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.NoContent); var role = await UseDbAsync(db => db.FamilyMemberships .Where(m => m.UserId == memberId).Select(m => m.Role).SingleAsync()); await Assert.That(role).IsEqualTo(FamilyRole.Admin); } [Test] public async Task Update_role_blocks_demoting_last_admin() { var response = await Client.PutAsJsonAsync($"/api/family/members/{User.Id}/role", new FamilyEndpoints.UpdateRoleRequest("Member")); await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.Conflict); } [Test] public async Task Update_role_allows_demoting_admin_when_other_admin_exists() { using var member = await Data.RegisterAsync("member"); var memberId = await UseDbAsync(db => db.Users.Where(u => u.Name == "member").Select(u => u.Id).SingleAsync()); // promote so we have two admins await Client.PutAsJsonAsync($"/api/family/members/{memberId}/role", new FamilyEndpoints.UpdateRoleRequest("Admin")); // demote self — should succeed because the other admin remains var response = await Client.PutAsJsonAsync($"/api/family/members/{User.Id}/role", new FamilyEndpoints.UpdateRoleRequest("Member")); await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.NoContent); } [Test] public async Task Update_role_forbidden_for_member() { using var member = await Data.RegisterAsync("member"); var memberId = await UseDbAsync(db => db.Users.Where(u => u.Name == "member").Select(u => u.Id).SingleAsync()); var response = await member.Client.PutAsJsonAsync($"/api/family/members/{memberId}/role", new FamilyEndpoints.UpdateRoleRequest("Admin")); await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.Forbidden); } [Test] public async Task Remove_member_deletes_membership() { using var member = await Data.RegisterAsync("member"); var memberId = await UseDbAsync(db => db.Users.Where(u => u.Name == "member").Select(u => u.Id).SingleAsync()); var response = await Client.DeleteAsync($"/api/family/members/{memberId}"); await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.NoContent); var stillMember = await UseDbAsync(db => db.FamilyMemberships.AnyAsync(m => m.UserId == memberId)); await Assert.That(stillMember).IsFalse(); } [Test] public async Task Remove_member_revokes_their_token() { using var member = await Data.RegisterAsync("member"); var memberId = await UseDbAsync(db => db.Users.Where(u => u.Name == "member").Select(u => u.Id).SingleAsync()); var removal = await Client.DeleteAsync($"/api/family/members/{memberId}"); await Assert.That(removal.StatusCode).IsEqualTo(HttpStatusCode.NoContent); // The kicked member's existing JWT must now be rejected. var meResponse = await member.Client.GetAsync("/api/auth/me"); await Assert.That(meResponse.StatusCode).IsEqualTo(HttpStatusCode.Unauthorized); } [Test] public async Task Remove_self_returns_409() { var response = await Client.DeleteAsync($"/api/family/members/{User.Id}"); await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.Conflict); } [Test] public async Task Member_demoted_loses_admin_access_on_next_request() { // Promote a second user to admin, then have the original admin demote // them. The original admin's already-issued token should still work // (still admin); the demoted user's token should no longer pass the // admin policy on the next request. using var second = await Data.RegisterAsync("second"); var secondId = await UseDbAsync(db => db.Users.Where(u => u.Name == "second").Select(u => u.Id).SingleAsync()); await Client.PutAsJsonAsync($"/api/family/members/{secondId}/role", new FamilyEndpoints.UpdateRoleRequest("Admin")); // confirm second can now regenerate (admin-only) var asAdmin = await second.Client.PostAsync("/api/family/invite-code/regenerate", content: null); await Assert.That(asAdmin.StatusCode).IsEqualTo(HttpStatusCode.OK); // demote second var demote = await Client.PutAsJsonAsync($"/api/family/members/{secondId}/role", new FamilyEndpoints.UpdateRoleRequest("Member")); await Assert.That(demote.StatusCode).IsEqualTo(HttpStatusCode.NoContent); // second's same token now fails the admin policy var afterDemote = await second.Client.PostAsync("/api/family/invite-code/regenerate", content: null); await Assert.That(afterDemote.StatusCode).IsEqualTo(HttpStatusCode.Forbidden); } }