Forge
csharp3407750f
1using System.Net.Http.Json;
2using System.Text.Json;
3
4namespace AgentForge.Tests;
5
6public sealed class ForgeProvenanceTests(ForgeWebApplicationFactory factory) : IClassFixture<ForgeWebApplicationFactory>
7{
8 private readonly HttpClient _client = factory.CreateClient();
9 private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true };
10
11 [Fact]
12 public async Task Git_push_with_provenance_stores_and_returns_metadata()
13 {
14 await _client.PostAsJsonAsync("/api/v1/repos", new { name = "prov-repo", driveMode = "agent-driven" });
15
16 var push = await _client.PostAsJsonAsync("/api/v1/git/push", new
17 {
18 repo = "prov-repo",
19 branch = "feat/x",
20 commit = "deadbeef",
21 provenance = new
22 {
23 actor = "Comet",
24 branch = "feat/x",
25 issueNumbers = new[] { 3 },
26 },
27 });
28 push.EnsureSuccessStatusCode();
29 var pushBody = await push.Content.ReadFromJsonAsync<JsonElement>(JsonOptions);
30 Assert.Equal("oar:Comet", pushBody.GetProperty("provenance").GetProperty("actor").GetString());
31
32 var get = await _client.GetFromJsonAsync<JsonElement>(
33 "/api/v1/repos/prov-repo/commits/deadbeef/provenance",
34 JsonOptions);
35 Assert.Equal("agent-driven", get.GetProperty("driveMode").GetString());
36 Assert.Equal(3, get.GetProperty("issueNumbers")[0].GetInt32());
37 }
38
39 [Fact]
40 public async Task Record_commit_provenance_is_idempotent_on_sha()
41 {
42 await _client.PostAsJsonAsync("/api/v1/repos", new { name = "idem-repo" });
43
44 var payload = new
45 {
46 sha = "abc123",
47 provenance = new
48 {
49 actor = "oar:LineA",
50 branch = "main",
51 },
52 };
53
54 var first = await _client.PostAsJsonAsync("/api/v1/repos/idem-repo/commits/provenance", payload);
55 first.EnsureSuccessStatusCode();
56 var second = await _client.PostAsJsonAsync("/api/v1/repos/idem-repo/commits/provenance", payload);
57 second.EnsureSuccessStatusCode();
58
59 var firstBody = await first.Content.ReadFromJsonAsync<JsonElement>(JsonOptions);
60 var secondBody = await second.Content.ReadFromJsonAsync<JsonElement>(JsonOptions);
61 Assert.Equal(
62 firstBody.GetProperty("recordedAt").GetString(),
63 secondBody.GetProperty("recordedAt").GetString());
64 }
65
66 [Fact]
67 public async Task Agent_driven_accept_allows_write_scope_without_accept_merge()
68 {
69 Environment.SetEnvironmentVariable("FORGE_REQUIRE_AUTH", "true");
70 Environment.SetEnvironmentVariable("FORGE_BOOTSTRAP_TOKEN", "bootstrap");
71 using var authFactory = new ForgeWebApplicationFactory();
72 using var client = authFactory.CreateClient();
73
74 var writeToken = await CreateBootstrapTokenAsync(client, "bootstrap", "read,write");
75 var auth = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", writeToken);
76
77 using (var createRepo = new HttpRequestMessage(HttpMethod.Post, "/api/v1/repos")
78 {
79 Content = JsonContent.Create(new { name = "agent-repo", driveMode = "agent-driven" }),
80 })
81 {
82 createRepo.Headers.Authorization = auth;
83 (await client.SendAsync(createRepo)).EnsureSuccessStatusCode();
84 }
85
86 authFactory.Git.CreateCommitOnBranch("agent-repo", "main", "README.md", "main", "main");
87 authFactory.Git.CreateCommitOnBranch("agent-repo", "feat", "a.txt", "a", "feat");
88
89 using (var createMr = new HttpRequestMessage(HttpMethod.Post, "/api/v1/repos/agent-repo/merge-requests")
90 {
91 Content = JsonContent.Create(new { title = "mr", sourceBranch = "feat" }),
92 })
93 {
94 createMr.Headers.Authorization = auth;
95 (await client.SendAsync(createMr)).EnsureSuccessStatusCode();
96 }
97
98 using var acceptRequest = new HttpRequestMessage(HttpMethod.Post, "/api/v1/repos/agent-repo/merge-requests/1/accept")
99 {
100 Content = JsonContent.Create(new { }),
101 };
102 acceptRequest.Headers.Authorization = auth;
103 (await client.SendAsync(acceptRequest)).EnsureSuccessStatusCode();
104
105 Environment.SetEnvironmentVariable("FORGE_REQUIRE_AUTH", null);
106 Environment.SetEnvironmentVariable("FORGE_BOOTSTRAP_TOKEN", null);
107 }
108
109 [Fact]
110 public async Task Human_driven_accept_rejects_write_only_scope()
111 {
112 Environment.SetEnvironmentVariable("FORGE_REQUIRE_AUTH", "true");
113 Environment.SetEnvironmentVariable("FORGE_BOOTSTRAP_TOKEN", "bootstrap");
114 using var authFactory = new ForgeWebApplicationFactory();
115 using var client = authFactory.CreateClient();
116
117 var writeToken = await CreateBootstrapTokenAsync(client, "bootstrap", "read,write");
118 var auth = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", writeToken);
119
120 using (var createRepo = new HttpRequestMessage(HttpMethod.Post, "/api/v1/repos")
121 {
122 Content = JsonContent.Create(new { name = "human-repo", driveMode = "human-driven" }),
123 })
124 {
125 createRepo.Headers.Authorization = auth;
126 (await client.SendAsync(createRepo)).EnsureSuccessStatusCode();
127 }
128
129 authFactory.Git.CreateCommitOnBranch("human-repo", "main", "README.md", "main", "main");
130 authFactory.Git.CreateCommitOnBranch("human-repo", "feat", "a.txt", "a", "feat");
131
132 using (var createMr = new HttpRequestMessage(HttpMethod.Post, "/api/v1/repos/human-repo/merge-requests")
133 {
134 Content = JsonContent.Create(new { title = "mr", sourceBranch = "feat" }),
135 })
136 {
137 createMr.Headers.Authorization = auth;
138 (await client.SendAsync(createMr)).EnsureSuccessStatusCode();
139 }
140
141 using var acceptRequest = new HttpRequestMessage(HttpMethod.Post, "/api/v1/repos/human-repo/merge-requests/1/accept")
142 {
143 Content = JsonContent.Create(new { }),
144 };
145 acceptRequest.Headers.Authorization = auth;
146 var response = await client.SendAsync(acceptRequest);
147 Assert.Equal(System.Net.HttpStatusCode.Forbidden, response.StatusCode);
148
149 Environment.SetEnvironmentVariable("FORGE_REQUIRE_AUTH", null);
150 Environment.SetEnvironmentVariable("FORGE_BOOTSTRAP_TOKEN", null);
151 }
152
153 private static async Task<string> CreateBootstrapTokenAsync(HttpClient client, string bootstrap, string scopes)
154 {
155 using var tokenRequest = new HttpRequestMessage(HttpMethod.Post, "/api/v1/admin/tokens")
156 {
157 Content = JsonContent.Create(new { name = "agent", scopes }),
158 };
159 tokenRequest.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", bootstrap);
160 var tokenResponse = await client.SendAsync(tokenRequest);
161 tokenResponse.EnsureSuccessStatusCode();
162 var tokenBody = await tokenResponse.Content.ReadFromJsonAsync<JsonElement>(JsonOptions);
163 return tokenBody.GetProperty("token").GetString()!;
164 }
165}
166
View only · write via MCP/CIDE