using System.Net; using System.Net.Http.Headers; using System.Net.Http.Json; using Xunit; namespace TeamUp.IntegrationTests; /// /// The accountability surface: the member directory, the invitations list, work-item transitions, /// and the per-assignee performance metrics (pending load, done, worked hours, cycle time). /// public sealed class PerformanceTests(PostgresFixture postgres) : IClassFixture { private sealed record BootstrapResponse(string Token, Guid MemberId, Guid OrganizationId); private sealed record AuthResponse(string Token, Guid MemberId); private sealed record InviteResponse(Guid InvitationId, string Token); private sealed record TeamResponse(Guid Id, Guid OrganizationId, string Name); private sealed record TaskResponse( Guid Id, Guid TeamId, string Title, string? Description, string Type, string Status, string AssigneeKind, Guid? AssigneeId, Guid? ParentId); private sealed record MemberRow(Guid Id, string Email, string DisplayName, string? Role); private sealed record InvitationRow( Guid Id, string Email, string ScopeType, Guid ScopeId, string Role, string Status, string Token, DateTimeOffset CreatedAtUtc); private sealed record PerformanceRow( string AssigneeKind, Guid AssigneeId, string? Name, int Backlog, int InProgress, int InReview, int Done, double WorkedHours, double? AvgCycleHours); private sealed record PerformanceResponse(int UnassignedPending, List Rows); [Fact] public async Task Members_invitations_and_performance_metrics_work() { await using var factory = new TeamUpWebFactory(postgres.ConnectionString); using var anon = factory.CreateClient(); var owner = await PostOk(anon, "/api/identity/bootstrap", new { organizationName = "AliaSaaS", ownerEmail = "owner@alia.test", ownerDisplayName = "Owner", ownerPassword = "Passw0rd!", }); using var client = Authed(factory, owner.Token); await client.PostAsJsonAsync("/api/orgboard/organizations", new { organizationId = owner.OrganizationId, name = "AliaSaaS" }); var team = await PostOk(client, "/api/orgboard/teams", new { organizationId = owner.OrganizationId, name = "IPNOPS" }); // The member directory lists the owner with their org role. var members = await client.GetFromJsonAsync>( $"/api/identity/members?organizationId={owner.OrganizationId}"); var ownerRow = Assert.Single(members!); Assert.Equal("Owner", ownerRow.Role); // Invitations are listed (with the join token) for inviter-level callers… var invite = await PostOk(client, "/api/identity/invitations", new { email = "dev@alia.test", scopeType = "Organization", scopeId = owner.OrganizationId, role = "Member", organizationId = owner.OrganizationId, }); var invitations = await client.GetFromJsonAsync>( $"/api/identity/invitations?organizationId={owner.OrganizationId}"); Assert.Contains(invitations!, i => i.Id == invite.InvitationId && i.Status == "Pending" && i.Token.Length > 0); // …but a plain Member is 403'd from the invitations list and the performance view. var member = await PostOk(anon, "/api/identity/invitations/accept", new { token = invite.Token, displayName = "Dev", password = "Passw0rd!" }); using (var memberClient = Authed(factory, member.Token)) { Assert.Equal(HttpStatusCode.Forbidden, (await memberClient.GetAsync($"/api/identity/invitations?organizationId={owner.OrganizationId}")).StatusCode); Assert.Equal(HttpStatusCode.Forbidden, (await memberClient.GetAsync($"/api/orgboard/performance?organizationId={owner.OrganizationId}")).StatusCode); } // Work a task through the board: assign → InProgress → Done (transitions recorded). var task = await PostOk(client, "/api/orgboard/tasks", new { teamId = team.Id, title = "Ship the login screen", type = "Story", }); await PatchOk(client, $"/api/orgboard/tasks/{task.Id}/assign", new { memberId = owner.MemberId }); await PatchOk(client, $"/api/orgboard/tasks/{task.Id}/move", new { status = "InProgress" }); await PatchOk(client, $"/api/orgboard/tasks/{task.Id}/move", new { status = "Done" }); // A second task stays unassigned and pending. await PostOk(client, "/api/orgboard/tasks", new { teamId = team.Id, title = "Unowned chore", type = "Story", }); var performance = await client.GetFromJsonAsync( $"/api/orgboard/performance?organizationId={owner.OrganizationId}"); Assert.Equal(1, performance!.UnassignedPending); var row = Assert.Single(performance.Rows, r => r.AssigneeKind == "Member" && r.AssigneeId == owner.MemberId); Assert.Equal(1, row.Done); Assert.Equal(0, row.Backlog + row.InProgress + row.InReview); Assert.True(row.WorkedHours >= 0); Assert.NotNull(row.AvgCycleHours); // InProgress → Done was recorded via transitions Assert.Null(row.Name); // member names resolve client-side from the directory } private static HttpClient Authed(TeamUpWebFactory factory, string token) { var client = factory.CreateClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); return client; } private static async Task PostOk(HttpClient client, string url, object body) { var response = await client.PostAsJsonAsync(url, body); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var value = await response.Content.ReadFromJsonAsync(); Assert.NotNull(value); return value!; } private static async Task PatchOk(HttpClient client, string url, object body) { var response = await client.PatchAsJsonAsync(url, body); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var value = await response.Content.ReadFromJsonAsync(); Assert.NotNull(value); return value!; } }