Forge
csharpb1bb0b24
1using System.Net.Http.Headers;
2using System.Net.Http.Json;
3using System.Text.Json;
4using OtpNet;
5
6namespace OpenAgentRegistry.Tests;
7
8public 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
View only · write via MCP/CIDE