using System.Net; using System.Net.Http.Headers; using System.Net.Http.Json; using Xunit; namespace TeamUp.IntegrationTests; /// /// The MCP server registry: owner-only to create (ManageApiKeys), team-owners may list (to bind), /// auth-header values are encrypted and never returned (only their names), and an unreachable server /// fails the test endpoint gracefully rather than throwing. /// public sealed class McpServerRegistryTests(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 McpServerDto(Guid Id, string Name, string Endpoint, bool Enabled, List HeaderNames); private sealed record McpTestResultDto(bool Success, string? Error, int ToolCount, List ToolNames); [Fact] public async Task Owner_registers_a_server_with_encrypted_headers_and_a_member_cannot() { 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); // Create with an auth header — the response exposes only the header NAME, never its value. var created = await PostOk(client, "/api/integrations/mcp-servers", new { organizationId = owner.OrganizationId, name = "GitHub MCP", endpoint = "https://mcp.example.com/mcp", headers = new Dictionary { ["Authorization"] = "Bearer super-secret-token" }, }); Assert.Equal("GitHub MCP", created.Name); Assert.True(created.Enabled); Assert.Equal(["Authorization"], created.HeaderNames); // Listing also never leaks the value. var list = await client.GetFromJsonAsync>( $"/api/integrations/mcp-servers?organizationId={owner.OrganizationId}"); var server = Assert.Single(list!); Assert.Equal(["Authorization"], server.HeaderNames); // A bad endpoint is rejected. var bad = await client.PostAsJsonAsync("/api/integrations/mcp-servers", new { organizationId = owner.OrganizationId, name = "Bad", endpoint = "not-a-url", headers = (object?)null, }); Assert.Equal(HttpStatusCode.BadRequest, bad.StatusCode); // Test endpoint on an unreachable server fails gracefully (no throw), reporting the reason. var test = await client.PostAsync($"/api/integrations/mcp-servers/{created.Id}/test", content: null); Assert.Equal(HttpStatusCode.OK, test.StatusCode); var result = await test.Content.ReadFromJsonAsync(); Assert.False(result!.Success); Assert.NotNull(result.Error); // A plain Member cannot register a server (ManageApiKeys is owner-only). var invite = await PostOk(client, "/api/identity/invitations", new { email = "dev@alia.test", scopeType = "Organization", scopeId = owner.OrganizationId, role = "Member", organizationId = owner.OrganizationId, }); var member = await PostOk(anon, "/api/identity/invitations/accept", new { token = invite.Token, displayName = "Dev", password = "Passw0rd!" }); using var memberClient = Authed(factory, member.Token); var forbidden = await memberClient.PostAsJsonAsync("/api/integrations/mcp-servers", new { organizationId = owner.OrganizationId, name = "Nope", endpoint = "https://mcp.example.com/mcp", headers = (object?)null, }); Assert.Equal(HttpStatusCode.Forbidden, forbidden.StatusCode); // The owner can delete it. var deleted = await client.DeleteAsync($"/api/integrations/mcp-servers/{created.Id}"); Assert.Equal(HttpStatusCode.NoContent, deleted.StatusCode); } 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!; } }