| 1 | using System.Net.Http.Headers; |
| 2 | using System.Net.Http.Json; |
| 3 | using System.Text.Json; |
| 4 | using OtpNet; |
| 5 | |
| 6 | namespace OpenAgentRegistry.Tests; |
| 7 | |
| 8 | public sealed class RegistryApiTests(RegistryWebApplicationFactory factory) : IClassFixture<RegistryWebApplicationFactory> |
| 9 | { |
| 10 | private readonly HttpClient _client = factory.CreateClient(); |
| 11 | private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true }; |
| 12 | |
| 13 | [Fact] |
| 14 | public async Task Register_search_and_claim_via_email() |
| 15 | { |
| 16 | var reg = await _client.PostAsJsonAsync("/api/v1/agents/register", new |
| 17 | { |
| 18 | name = "ComposerCasa", |
| 19 | description = "CASA lab line", |
| 20 | skills = new[] { "casa", "python" }, |
| 21 | logical_line_id = "composer-cursor-2026", |
| 22 | contributor_lines = new[] { "Composer @ Cursor, 2026-06-09" }, |
| 23 | }); |
| 24 | reg.EnsureSuccessStatusCode(); |
| 25 | var regBody = await reg.Content.ReadFromJsonAsync<JsonElement>(JsonOptions); |
| 26 | var apiKey = regBody.GetProperty("apiKey").GetString()!; |
| 27 | var claimUrl = regBody.GetProperty("claimUrl").GetString()!; |
| 28 | var token = claimUrl.Split('/')[^1]; |
| 29 | |
| 30 | var searchBefore = await _client.GetFromJsonAsync<JsonElement>("/api/v1/agents/search?q=Composer"); |
| 31 | Assert.Equal(0, searchBefore.GetProperty("total").GetInt32()); |
| 32 | |
| 33 | var codeResp = await _client.PostAsJsonAsync($"/claim/{token}/request-code", new { email = "owner@example.com" }); |
| 34 | codeResp.EnsureSuccessStatusCode(); |
| 35 | var codeBody = await codeResp.Content.ReadFromJsonAsync<JsonElement>(JsonOptions); |
| 36 | var code = codeBody.TryGetProperty("devCode", out var devCode) |
| 37 | ? devCode.GetString()! |
| 38 | : codeBody.GetProperty("dev_code").GetString()!; |
| 39 | |
| 40 | var confirm = await _client.PostAsJsonAsync($"/claim/{token}/confirm", new { email = "owner@example.com", code }); |
| 41 | confirm.EnsureSuccessStatusCode(); |
| 42 | Assert.Equal("claimed", (await confirm.Content.ReadFromJsonAsync<JsonElement>(JsonOptions)).GetProperty("status").GetString()); |
| 43 | |
| 44 | using var meRequest = new HttpRequestMessage(HttpMethod.Get, "/api/v1/agents/me"); |
| 45 | meRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); |
| 46 | var me = await _client.SendAsync(meRequest); |
| 47 | me.EnsureSuccessStatusCode(); |
| 48 | var meBody = await me.Content.ReadFromJsonAsync<JsonElement>(JsonOptions); |
| 49 | Assert.True(meBody.GetProperty("isClaimed").GetBoolean()); |
| 50 | |
| 51 | var searchAfter = await _client.GetFromJsonAsync<JsonElement>("/api/v1/agents/search?q=CASA"); |
| 52 | Assert.Equal(1, searchAfter.GetProperty("total").GetInt32()); |
| 53 | } |
| 54 | |
| 55 | [Fact] |
| 56 | public async Task Claim_via_totp() |
| 57 | { |
| 58 | var reg = await _client.PostAsJsonAsync("/api/v1/agents/register", new { name = "TotpLine", description = "TOTP" }); |
| 59 | reg.EnsureSuccessStatusCode(); |
| 60 | var regBody = await reg.Content.ReadFromJsonAsync<JsonElement>(JsonOptions); |
| 61 | var token = regBody.GetProperty("claimUrl").GetString()!.Split('/')[^1]; |
| 62 | |
| 63 | var begin = await _client.PostAsJsonAsync($"/claim/{token}/begin", new { email = "totp@example.com", channel = "totp" }); |
| 64 | begin.EnsureSuccessStatusCode(); |
| 65 | var beginBody = await begin.Content.ReadFromJsonAsync<JsonElement>(JsonOptions); |
| 66 | var secret = beginBody.TryGetProperty("devTotpSecret", out var devSecret) |
| 67 | ? devSecret.GetString()! |
| 68 | : beginBody.GetProperty("dev_totp_secret").GetString()!; |
| 69 | var code = new Totp(Base32Encoding.ToBytes(secret)).ComputeTotp(); |
| 70 | |
| 71 | var confirm = await _client.PostAsJsonAsync($"/claim/{token}/confirm", new { email = "totp@example.com", code }); |
| 72 | confirm.EnsureSuccessStatusCode(); |
| 73 | |
| 74 | var profile = await _client.GetFromJsonAsync<JsonElement>("/api/v1/agents/TotpLine"); |
| 75 | Assert.True(profile.GetProperty("ownerHasTotp").GetBoolean()); |
| 76 | Assert.Equal("totp", profile.GetProperty("claimMethod").GetString()); |
| 77 | } |
| 78 | |
| 79 | [Fact] |
| 80 | public async Task Claim_2fa_flow() |
| 81 | { |
| 82 | await using var factory2fa = new RegistryWebApplicationFactory { Require2Fa = true }; |
| 83 | var client = factory2fa.CreateClient(); |
| 84 | |
| 85 | var reg = await client.PostAsJsonAsync("/api/v1/agents/register", new { name = "TwoFaLine", description = "2FA" }); |
| 86 | reg.EnsureSuccessStatusCode(); |
| 87 | var token = (await reg.Content.ReadFromJsonAsync<JsonElement>(JsonOptions)).GetProperty("claimUrl").GetString()!.Split('/')[^1]; |
| 88 | |
| 89 | var begin = await client.PostAsJsonAsync($"/claim/{token}/begin-2fa", new { email = "2fa@example.com" }); |
| 90 | begin.EnsureSuccessStatusCode(); |
| 91 | var beginBody = await begin.Content.ReadFromJsonAsync<JsonElement>(JsonOptions); |
| 92 | var emailCode = GetDevCode(beginBody); |
| 93 | |
| 94 | var step1 = await client.PostAsJsonAsync($"/claim/{token}/confirm-email", new { email = "2fa@example.com", code = emailCode }); |
| 95 | step1.EnsureSuccessStatusCode(); |
| 96 | Assert.Equal("email_verified", (await step1.Content.ReadFromJsonAsync<JsonElement>(JsonOptions)).GetProperty("status").GetString()); |
| 97 | |
| 98 | var setup = await client.PostAsync($"/claim/{token}/setup-totp", null); |
| 99 | setup.EnsureSuccessStatusCode(); |
| 100 | var setupBody = await setup.Content.ReadFromJsonAsync<JsonElement>(JsonOptions); |
| 101 | var secret = setupBody.TryGetProperty("devTotpSecret", out var devSecret) |
| 102 | ? devSecret.GetString()! |
| 103 | : setupBody.GetProperty("dev_totp_secret").GetString()!; |
| 104 | var totpCode = new Totp(Base32Encoding.ToBytes(secret)).ComputeTotp(); |
| 105 | |
| 106 | var step2 = await client.PostAsJsonAsync($"/claim/{token}/confirm-totp", new { email = "2fa@example.com", code = totpCode }); |
| 107 | step2.EnsureSuccessStatusCode(); |
| 108 | var step2Body = await step2.Content.ReadFromJsonAsync<JsonElement>(JsonOptions); |
| 109 | var method = step2Body.TryGetProperty("claimMethod", out var cm) ? cm.GetString() : step2Body.GetProperty("claim_method").GetString(); |
| 110 | Assert.Equal("2fa", method); |
| 111 | } |
| 112 | |
| 113 | private static string GetDevCode(JsonElement body) => |
| 114 | body.TryGetProperty("devCode", out var devCode) ? devCode.GetString()! : body.GetProperty("dev_code").GetString()!; |
| 115 | } |
| 116 | |