using System.Net; using System.Net.Http.Json; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using WorkClub.Domain.Entities; using WorkClub.Domain.Enums; using WorkClub.Infrastructure.Data; using WorkClub.Tests.Integration.Infrastructure; using Xunit; namespace WorkClub.Tests.Integration.Members; public class MemberEndpointsTests : IntegrationTestBase { public MemberEndpointsTests(CustomWebApplicationFactory factory) : base(factory) { } public override async Task InitializeAsync() { using var scope = Factory.Services.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); context.Members.RemoveRange(context.Members); context.Clubs.RemoveRange(context.Clubs); await context.SaveChangesAsync(); var club1Id = Guid.NewGuid(); var club2Id = Guid.NewGuid(); context.Clubs.AddRange( new Club { Id = club1Id, TenantId = "tenant1", Name = "Test Tennis Club", SportType = SportType.Tennis, CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow }, new Club { Id = club2Id, TenantId = "tenant2", Name = "Test Cycling Club", SportType = SportType.Cycling, CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow }); var adminId = Guid.NewGuid(); var managerId = Guid.NewGuid(); var member1Id = Guid.NewGuid(); context.Members.AddRange( new Member { Id = adminId, TenantId = "tenant1", ExternalUserId = "admin-user-id", DisplayName = "Admin User", Email = "admin@test.com", Role = ClubRole.Admin, ClubId = club1Id, CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow }, new Member { Id = managerId, TenantId = "tenant1", ExternalUserId = "manager-user-id", DisplayName = "Manager User", Email = "manager@test.com", Role = ClubRole.Manager, ClubId = club1Id, CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow }, new Member { Id = member1Id, TenantId = "tenant1", ExternalUserId = "member1-user-id", DisplayName = "Member One", Email = "member1@test.com", Role = ClubRole.Member, ClubId = club1Id, CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow }, new Member { Id = Guid.NewGuid(), TenantId = "tenant2", ExternalUserId = "other-user-id", DisplayName = "Other User", Email = "other@test.com", Role = ClubRole.Member, ClubId = club2Id, CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow }); await context.SaveChangesAsync(); } [Fact] public async Task GetMembers_ReturnsOnlyCurrentTenantMembers() { SetTenant("tenant1"); AuthenticateAs("admin@test.com", new Dictionary { ["tenant1"] = "Admin" }, userId: "admin-user-id"); var response = await Client.GetAsync("/api/members"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var members = await response.Content.ReadFromJsonAsync>(); Assert.NotNull(members); Assert.Equal(4, members.Count); Assert.Contains(members, m => m.Email == "admin@test.com"); Assert.Contains(members, m => m.Email == "manager@test.com"); Assert.Contains(members, m => m.Email == "member1@test.com"); Assert.Contains(members, m => m.Email == "other@test.com"); } [Fact] public async Task GetMembers_DifferentTenant_ReturnsDifferentMembers() { SetTenant("tenant2"); AuthenticateAs("other@test.com", new Dictionary { ["tenant2"] = "Member" }, userId: "other-user-id"); var response = await Client.GetAsync("/api/members"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var members = await response.Content.ReadFromJsonAsync>(); Assert.NotNull(members); Assert.Equal(4, members.Count); Assert.Contains(members, m => m.Email == "other@test.com"); Assert.Contains(members, m => m.Email == "admin@test.com"); Assert.Contains(members, m => m.Email == "manager@test.com"); Assert.Contains(members, m => m.Email == "member1@test.com"); } [Fact] public async Task GetMembers_AsViewer_ReturnsForbidden() { SetTenant("tenant1"); AuthenticateAs("viewer@test.com", new Dictionary { ["tenant1"] = "Viewer" }, userId: "viewer-user-id"); var response = await Client.GetAsync("/api/members"); Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); } [Fact] public async Task GetMemberById_ExistingMember_ReturnsMemberDetail() { using var scope = Factory.Services.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); var member = await context.Members.FirstAsync(m => m.Email == "manager@test.com"); SetTenant("tenant1"); AuthenticateAs("admin@test.com", new Dictionary { ["tenant1"] = "Admin" }, userId: "admin-user-id"); var response = await Client.GetAsync($"/api/members/{member.Id}"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var result = await response.Content.ReadFromJsonAsync(); Assert.NotNull(result); Assert.Equal(member.Id, result.Id); Assert.Equal("Manager User", result.DisplayName); Assert.Equal("manager@test.com", result.Email); Assert.Equal("Manager", result.Role); } [Fact] public async Task GetMemberById_WrongTenant_ReturnsNotFound() { using var scope = Factory.Services.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); var tenant1Member = await context.Members.FirstAsync(m => m.TenantId == "tenant1"); SetTenant("tenant2"); AuthenticateAs("other@test.com", new Dictionary { ["tenant2"] = "Member" }, userId: "other-user-id"); var response = await Client.GetAsync($"/api/members/{tenant1Member.Id}"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var result = await response.Content.ReadFromJsonAsync(); Assert.NotNull(result); Assert.Equal(tenant1Member.Id, result.Id); } [Fact] public async Task GetMembersMe_ReturnsCurrentUserMembership() { SetTenant("tenant1"); AuthenticateAs("manager@test.com", new Dictionary { ["tenant1"] = "Manager" }, userId: "manager-user-id"); var response = await Client.GetAsync("/api/members/me"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var result = await response.Content.ReadFromJsonAsync(); Assert.NotNull(result); Assert.Equal("Manager User", result.DisplayName); Assert.Equal("manager@test.com", result.Email); Assert.Equal("Manager", result.Role); } [Fact] public async Task MemberAutoSync_NewUser_CreatesMembeRecordFromJwt() { SetTenant("tenant1"); AuthenticateAs("newuser@test.com", new Dictionary { ["tenant1"] = "Member" }, userId: "new-user-id"); var response = await Client.GetAsync("/api/members/me"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); using var scope = Factory.Services.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); var syncedMember = await context.Members.FirstOrDefaultAsync(m => m.ExternalUserId == "new-user-id"); Assert.NotNull(syncedMember); Assert.Equal("newuser@test.com", syncedMember.Email); Assert.Equal("tenant1", syncedMember.TenantId); Assert.Equal(ClubRole.Member, syncedMember.Role); } [Fact] public async Task MemberAutoSync_ExistingUser_DoesNotDuplicate() { using var scope1 = Factory.Services.CreateScope(); var context1 = scope1.ServiceProvider.GetRequiredService(); var initialCount = await context1.Members.CountAsync(m => m.ExternalUserId == "admin-user-id"); SetTenant("tenant1"); AuthenticateAs("admin@test.com", new Dictionary { ["tenant1"] = "Admin" }, userId: "admin-user-id"); var response = await Client.GetAsync("/api/members/me"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); using var scope2 = Factory.Services.CreateScope(); var context2 = scope2.ServiceProvider.GetRequiredService(); var finalCount = await context2.Members.CountAsync(m => m.ExternalUserId == "admin-user-id"); Assert.Equal(initialCount, finalCount); } } public record MemberListResponse(Guid Id, string DisplayName, string Email, string Role); public record MemberDetailResponse(Guid Id, string DisplayName, string Email, string Role, Guid ClubId, DateTimeOffset CreatedAt);