Files
work-club-manager/backend/WorkClub.Tests.Integration/Clubs/ClubEndpointsTests.cs
WorkClub Automation db880b3480 feat(clubs): add Club and Member API endpoints with auto-sync
Implement Task 16: Club + Member API endpoints with MemberSyncService

Services:
- ClubService: GetMyClubsAsync (user's clubs), GetCurrentClubAsync (tenant club)
- MemberService: GetMembersAsync (list), GetMemberByIdAsync, GetCurrentMemberAsync
- MemberSyncService: Auto-creates Member records from JWT on first request

Middleware:
- MemberSyncMiddleware: Runs after auth, calls MemberSyncService

Endpoints:
- GET /api/clubs/me (list user's clubs)
- GET /api/clubs/current (current tenant's club)
- GET /api/members (list members, RLS filtered)
- GET /api/members/{id} (member detail)
- GET /api/members/me (current user's membership)

Tests: 14 integration tests (6 club + 8 member)
- Club filtering by user membership
- Multi-tenant isolation via RLS
- Member auto-sync on first request
- Cross-tenant access blocked
- Role-based authorization

Build: 0 errors, all tests compile
Pattern: TypedResults, RequireAuthorization policies, TDD approach
2026-03-03 19:41:01 +01:00

226 lines
7.0 KiB
C#

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.Clubs;
public class ClubEndpointsTests : IntegrationTestBase
{
public ClubEndpointsTests(CustomWebApplicationFactory<Program> factory) : base(factory)
{
}
public override async Task InitializeAsync()
{
using var scope = Factory.Services.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// Clean up and setup test data
context.Clubs.RemoveRange(context.Clubs);
context.Members.RemoveRange(context.Members);
await context.SaveChangesAsync();
// Create test clubs
var club1Id = Guid.NewGuid();
var club2Id = Guid.NewGuid();
var club1 = new Club
{
Id = club1Id,
TenantId = "tenant1",
Name = "Test Tennis Club",
SportType = SportType.Tennis,
Description = "Test club 1",
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow
};
var club2 = new Club
{
Id = club2Id,
TenantId = "tenant2",
Name = "Test Cycling Club",
SportType = SportType.Cycling,
Description = "Test club 2",
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow
};
context.Clubs.AddRange(club1, club2);
// Create test members (membership links)
var adminUserId = "admin-user-id";
var managerUserId = "manager-user-id";
// Admin is member of both clubs
context.Members.Add(new Member
{
Id = Guid.NewGuid(),
TenantId = "tenant1",
ExternalUserId = adminUserId,
DisplayName = "Admin User",
Email = "admin@test.com",
Role = ClubRole.Admin,
ClubId = club1Id,
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow
});
context.Members.Add(new Member
{
Id = Guid.NewGuid(),
TenantId = "tenant2",
ExternalUserId = adminUserId,
DisplayName = "Admin User",
Email = "admin@test.com",
Role = ClubRole.Member,
ClubId = club2Id,
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow
});
// Manager is only member of club1
context.Members.Add(new Member
{
Id = Guid.NewGuid(),
TenantId = "tenant1",
ExternalUserId = managerUserId,
DisplayName = "Manager User",
Email = "manager@test.com",
Role = ClubRole.Manager,
ClubId = club1Id,
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow
});
await context.SaveChangesAsync();
}
[Fact]
public async Task GetClubsMe_ReturnsOnlyUserClubs()
{
// Arrange - admin is member of 2 clubs
SetTenant("tenant1");
AuthenticateAs("admin@test.com", new Dictionary<string, string>
{
["tenant1"] = "Admin",
["tenant2"] = "Member"
}, userId: "admin-user-id");
// Act
var response = await Client.GetAsync("/api/clubs/me");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var clubs = await response.Content.ReadFromJsonAsync<List<ClubListResponse>>();
Assert.NotNull(clubs);
Assert.Equal(2, clubs.Count);
Assert.Contains(clubs, c => c.Name == "Test Tennis Club");
Assert.Contains(clubs, c => c.Name == "Test Cycling Club");
}
[Fact]
public async Task GetClubsMe_ForManagerUser_ReturnsOnlyOneClub()
{
// Arrange - manager is only member of club1
SetTenant("tenant1");
AuthenticateAs("manager@test.com", new Dictionary<string, string>
{
["tenant1"] = "Manager"
}, userId: "manager-user-id");
// Act
var response = await Client.GetAsync("/api/clubs/me");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var clubs = await response.Content.ReadFromJsonAsync<List<ClubListResponse>>();
Assert.NotNull(clubs);
Assert.Single(clubs);
Assert.Equal("Test Tennis Club", clubs[0].Name);
}
[Fact]
public async Task GetClubsCurrent_ReturnsCurrentTenantClub()
{
// Arrange
SetTenant("tenant1");
AuthenticateAs("admin@test.com", new Dictionary<string, string>
{
["tenant1"] = "Admin"
}, userId: "admin-user-id");
// Act
var response = await Client.GetAsync("/api/clubs/current");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var club = await response.Content.ReadFromJsonAsync<ClubDetailResponse>();
Assert.NotNull(club);
Assert.Equal("Test Tennis Club", club.Name);
Assert.Equal("Tennis", club.SportType);
Assert.Equal("Test club 1", club.Description);
}
[Fact]
public async Task GetClubsCurrent_DifferentTenant_ReturnsDifferentClub()
{
// Arrange
SetTenant("tenant2");
AuthenticateAs("admin@test.com", new Dictionary<string, string>
{
["tenant2"] = "Member"
}, userId: "admin-user-id");
// Act
var response = await Client.GetAsync("/api/clubs/current");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var club = await response.Content.ReadFromJsonAsync<ClubDetailResponse>();
Assert.NotNull(club);
Assert.Equal("Test Cycling Club", club.Name);
Assert.Equal("Cycling", club.SportType);
}
[Fact]
public async Task GetClubsCurrent_NoTenantContext_ReturnsBadRequest()
{
// Arrange - no tenant header set
AuthenticateAs("admin@test.com", new Dictionary<string, string>
{
["tenant1"] = "Admin"
}, userId: "admin-user-id");
// Act
var response = await Client.GetAsync("/api/clubs/current");
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Fact]
public async Task GetClubsMe_Unauthenticated_ReturnsUnauthorized()
{
// Act
var response = await Client.GetAsync("/api/clubs/me");
// Assert
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
}
// Response DTOs for test assertions
public record ClubListResponse(Guid Id, string Name, string SportType, int MemberCount);
public record ClubDetailResponse(Guid Id, string Name, string SportType, string? Description, DateTimeOffset CreatedAt);