using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using Xunit;
namespace TeamUp.IntegrationTests;
///
/// M3 BYOK acceptance: an owner adds an API config (key encrypted, never returned by any endpoint),
/// a connection test succeeds, and a non-owner Member cannot create or list configs.
///
public sealed class ByokTests(PostgresFixture postgres) : IClassFixture
{
private const string SecretKey = "sk-teamup-test-deadbeef-do-not-leak";
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 ApiConfigDto(Guid Id, string Name, string Provider, string Model, string? Endpoint);
private sealed record TestResultDto(bool Success, string? Error, long LatencyMs, string? Sample);
[Fact]
public async Task Owner_adds_config_key_never_returned_test_succeeds_member_forbidden()
{
await using var factory = new TeamUpWebFactory(postgres.ConnectionString);
using var anon = factory.CreateClient();
var owner = await Bootstrap(anon);
using var ownerClient = Authed(factory, owner.Token);
// Owner creates a config (stub provider, no network). The key must NOT appear in the response.
var createResponse = await ownerClient.PostAsJsonAsync("/api/integrations/api-configs", new
{
organizationId = owner.OrganizationId,
name = "Stub-Pro",
provider = "stub",
model = "test-model",
apiKey = SecretKey,
});
Assert.Equal(HttpStatusCode.OK, createResponse.StatusCode);
Assert.DoesNotContain(SecretKey, await createResponse.Content.ReadAsStringAsync());
var config = await createResponse.Content.ReadFromJsonAsync();
Assert.NotNull(config);
Assert.Equal("stub", config!.Provider);
// Listing returns the config but never the key.
var listResponse = await ownerClient.GetAsync($"/api/integrations/api-configs?organizationId={owner.OrganizationId}");
Assert.Equal(HttpStatusCode.OK, listResponse.StatusCode);
var listBody = await listResponse.Content.ReadAsStringAsync();
Assert.DoesNotContain(SecretKey, listBody);
Assert.Contains(config.Id.ToString(), listBody);
// The connection test succeeds (stub uses the decrypted key server-side; never echoes it).
var test = await ownerClient.PostAsync($"/api/integrations/api-configs/{config.Id}/test", content: null);
Assert.Equal(HttpStatusCode.OK, test.StatusCode);
var testBody = await test.Content.ReadAsStringAsync();
Assert.DoesNotContain(SecretKey, testBody);
var result = await test.Content.ReadFromJsonAsync();
Assert.True(result!.Success);
// A Member cannot manage or even list BYOK configs.
var member = await InviteMember(ownerClient, anon, owner.OrganizationId);
using var memberClient = Authed(factory, member.Token);
var memberCreate = await memberClient.PostAsJsonAsync("/api/integrations/api-configs", new
{
organizationId = owner.OrganizationId,
name = "Nope",
provider = "stub",
model = "x",
apiKey = "sk-nope",
});
Assert.Equal(HttpStatusCode.Forbidden, memberCreate.StatusCode);
var memberList = await memberClient.GetAsync($"/api/integrations/api-configs?organizationId={owner.OrganizationId}");
Assert.Equal(HttpStatusCode.Forbidden, memberList.StatusCode);
}
private static async Task Bootstrap(HttpClient client)
{
var response = await client.PostAsJsonAsync("/api/identity/bootstrap", new
{
organizationName = "AliaSaaS",
ownerEmail = "owner@alia.test",
ownerDisplayName = "Owner",
ownerPassword = "Passw0rd!",
});
var owner = await response.Content.ReadFromJsonAsync();
Assert.NotNull(owner);
return owner!;
}
private static async Task InviteMember(HttpClient ownerClient, HttpClient anon, Guid organizationId)
{
var invite = await ownerClient.PostAsJsonAsync("/api/identity/invitations", new
{
email = "dev@alia.test",
scopeType = "Organization",
scopeId = organizationId,
role = "Member",
organizationId,
});
var inviteResponse = await invite.Content.ReadFromJsonAsync();
var accept = await anon.PostAsJsonAsync("/api/identity/invitations/accept", new
{
token = inviteResponse!.Token,
displayName = "Dev",
password = "Passw0rd!",
});
var member = await accept.Content.ReadFromJsonAsync();
Assert.NotNull(member);
return member!;
}
private static HttpClient Authed(TeamUpWebFactory factory, string token)
{
var client = factory.CreateClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
return client;
}
}