tinyrelayfile
tinyrelay/tinygit/pr_repair_test.go/

tinygit/pr_repair_test.go raw
1package tinygit
2
3import (
4 "context"
5 "crypto/x509"
6 "encoding/pem"
7 "fmt"
8 "net/http"
9 "net/http/httptest"
10 "os"
11 "os/exec"
12 "path/filepath"
13 "strings"
14 "testing"
15
16 "github.com/FelineStateMachine/tinyrelay/internal/event"
17 "github.com/FelineStateMachine/tinyrelay/internal/policy"
18 "github.com/FelineStateMachine/tinyrelay/internal/storage"
19)
20
21func TestPullRequestTargetUsesEventIDForStandardNIP34(t *testing.T) {
22 secret := strings.Repeat("0", 63) + "1"
23 root := event.Event{Kind: event.KIND_GIT_PR, CreatedAt: 1, Content: "change", Tags: [][]string{{"a", "30617:owner:repo"}, {"c", strings.Repeat("a", 40)}, {"clone", "https://git.example/repo.git"}}}
24 if err := event.Sign(&root, secret); err != nil {
25 t.Fatal(err)
26 }
27 r, err := pullRequestTarget(root, event.KIND_GIT_PR)
28 if err != nil {
29 t.Fatal(err)
30 }
31 if r.Identifier != root.ID || r.Refs["refs/nostr/"+root.ID] != strings.Repeat("a", 40) {
32 t.Fatalf("standard PR target = %#v", r)
33 }
34}
35
36func TestPullRequestUpdateRequiresRootAuthor(t *testing.T) {
37 rootSecret := strings.Repeat("0", 63) + "1"
38 otherSecret := strings.Repeat("0", 63) + "2"
39 root := event.Event{Kind: event.KIND_GIT_PR, CreatedAt: 1, Tags: [][]string{{"a", "30617:owner:repo"}, {"c", strings.Repeat("a", 40)}, {"clone", "https://git.example/repo.git"}}}
40 if err := event.Sign(&root, rootSecret); err != nil {
41 t.Fatal(err)
42 }
43 update := event.Event{Kind: event.KIND_GIT_PR_UPDATE, CreatedAt: 2, Tags: [][]string{{"E", root.ID}, {"P", root.PubKey}, {"c", strings.Repeat("b", 40)}, {"clone", "https://git.example/repo.git"}}}
44 if err := event.Sign(&update, otherSecret); err != nil {
45 t.Fatal(err)
46 }
47 if err := validatePullRequestUpdate(root, update); err == nil {
48 t.Fatal("accepted update signed by another author")
49 }
50}
51
52func TestPullRequestRepairRejectsMissingCloneBeforeGit(t *testing.T) {
53 secret := strings.Repeat("0", 63) + "1"
54 root := event.Event{Kind: event.KIND_GIT_PR, CreatedAt: 1, Tags: [][]string{{"a", "30617:owner:repo"}, {"c", strings.Repeat("a", 40)}}}
55 owner, err := event.PublicKey(secret)
56 if err != nil {
57 t.Fatal(err)
58 }
59 root.Tags[0][1] = "30617:" + owner + ":repo"
60 if err := event.Sign(&root, secret); err != nil {
61 t.Fatal(err)
62 }
63 g := &GitRelay{}
64 hosted := Repository{Owner: root.PubKey, Identifier: "repo", Refs: map[string]string{}}
65 err = g.RepairPullRequestObjects(context.Background(), hosted, PullRequestRepair{Root: root})
66 if err == nil || !strings.Contains(err.Error(), "no clone source") {
67 t.Fatalf("missing clone error = %v", err)
68 }
69}
70
71func TestPullRequestRepairUsesHostedRepositoryRefs(t *testing.T) {
72 rootDir := t.TempDir()
73 remote := filepath.Join(rootDir, "remote", "repo.git")
74 work := filepath.Join(rootDir, "work")
75 prGitRun(t, "init", "--bare", remote)
76 prGitRun(t, "init", work)
77 prGitRun(t, "-C", work, "config", "user.email", "pr@example.test")
78 prGitRun(t, "-C", work, "config", "user.name", "PR")
79 if err := os.WriteFile(filepath.Join(work, "README"), []byte("one\n"), 0600); err != nil {
80 t.Fatal(err)
81 }
82 prGitRun(t, "-C", work, "add", "README")
83 prGitRun(t, "-C", work, "commit", "-m", "one")
84 first := strings.TrimSpace(prGitOutput(t, "-C", work, "rev-parse", "HEAD"))
85 prGitRun(t, "-C", work, "push", remote, "HEAD:refs/heads/main")
86 if err := os.WriteFile(filepath.Join(work, "NEXT"), []byte("two\n"), 0600); err != nil {
87 t.Fatal(err)
88 }
89 prGitRun(t, "-C", work, "add", "NEXT")
90 prGitRun(t, "-C", work, "commit", "-m", "two")
91 second := strings.TrimSpace(prGitOutput(t, "-C", work, "rev-parse", "HEAD"))
92 prGitRun(t, "-C", work, "push", remote, "HEAD:refs/heads/pr")
93
94 server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { serveGitBackend(t, w, r, filepath.Dir(remote)) }))
95 t.Cleanup(server.Close)
96 cert, err := x509.ParseCertificate(server.Certificate().Raw)
97 if err != nil {
98 t.Fatal(err)
99 }
100 ca := filepath.Join(rootDir, "ca.pem")
101 if err := os.WriteFile(ca, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}), 0600); err != nil {
102 t.Fatal(err)
103 }
104 t.Setenv("GIT_SSL_CAINFO", ca)
105 store, err := storage.Open(context.Background(), filepath.Join(rootDir, "relay.db"))
106 if err != nil {
107 t.Fatal(err)
108 }
109 t.Cleanup(func() { _ = store.Close() })
110 p := policy.Defaults("")
111 g, err := New(Config{Store: tinyStore(store), Root: filepath.Join(rootDir, "git"), Policy: func() Policy { return fromInternalPolicy(p) }, AllowPrivateRelays: true})
112 if err != nil {
113 t.Fatal(err)
114 }
115 secret := strings.Repeat("0", 63) + "1"
116 owner, err := event.PublicKey(secret)
117 if err != nil {
118 t.Fatal(err)
119 }
120 clone := server.URL + "/repo.git"
121 root := event.Event{Kind: event.KIND_GIT_PR, CreatedAt: 1, Content: "PR", Tags: [][]string{{"a", "30617:" + owner + ":repo"}, {"c", first}, {"clone", clone}}}
122 if err := event.Sign(&root, secret); err != nil {
123 t.Fatal(err)
124 }
125 update := event.Event{Kind: event.KIND_GIT_PR_UPDATE, CreatedAt: 2, Tags: [][]string{{"E", root.ID}, {"P", owner}, {"c", second}, {"clone", clone}}}
126 if err := event.Sign(&update, secret); err != nil {
127 t.Fatal(err)
128 }
129 hosted := Repository{Owner: owner, Identifier: "repo", Clone: []string{clone}, Refs: map[string]string{"refs/heads/main": first}}
130 if err := g.FetchMissing(context.Background(), hosted, hosted.Clone, hosted.Refs); err != nil {
131 t.Fatal(err)
132 }
133 if g.objectsPresent(context.Background(), hosted, []string{second}) {
134 t.Fatal("PR update was already present before repair; test must exercise a real fetch")
135 }
136 if err := g.RepairPullRequestObjects(context.Background(), hosted, PullRequestRepair{Root: root, Updates: []event.Event{update}}); err != nil {
137 t.Fatal(err)
138 }
139 gitDir := filepath.Join(rootDir, "git", owner, "repo.git")
140 for ref, want := range map[string]string{"refs/heads/main": first, "refs/nostr/" + root.ID: first, "refs/nostr/" + update.ID: second} {
141 got := strings.TrimSpace(prGitOutput(t, "--git-dir", gitDir, "rev-parse", ref))
142 if got != want {
143 t.Fatalf("%s = %s, want %s", ref, got, want)
144 }
145 }
146 if _, err := os.Stat(filepath.Join(gitDir, "tinyrelay.refs")); !os.IsNotExist(err) {
147 t.Fatalf("PR repair wrote signed state file: %v", err)
148 }
149 quarantines, err := filepath.Glob(filepath.Join(rootDir, "git", "prs", owner, "repo.repair-"+root.ID+"-*.git"))
150 if err != nil {
151 t.Fatal(err)
152 }
153 if len(quarantines) != 0 {
154 t.Fatalf("PR repair quarantine was not removed: %v", quarantines)
155 }
156 blockedRoot := root
157 blockedRoot.CreatedAt = 3
158 if err := event.Sign(&blockedRoot, secret); err != nil {
159 t.Fatal(err)
160 }
161 blockedUpdate := event.Event{Kind: event.KIND_GIT_PR_UPDATE, CreatedAt: 4, Tags: [][]string{{"E", blockedRoot.ID}, {"P", owner}, {"c", second}, {"clone", clone}}}
162 if err := event.Sign(&blockedUpdate, secret); err != nil {
163 t.Fatal(err)
164 }
165 // A ref-directory collision makes one update fail. The other signed ref
166 // must not appear, regardless of map iteration or transaction order.
167 blocker := "refs/nostr/" + blockedRoot.ID + "/child"
168 prGitRun(t, "--git-dir", gitDir, "update-ref", blocker, first)
169 if err := g.RepairPullRequestObjects(context.Background(), hosted, PullRequestRepair{Root: blockedRoot, Updates: []event.Event{blockedUpdate}}); err == nil {
170 t.Fatal("repair ignored conflicting ref")
171 }
172 if err := exec.Command("git", "--git-dir", gitDir, "show-ref", "--verify", "--quiet", "refs/nostr/"+blockedUpdate.ID).Run(); err == nil {
173 t.Fatal("failed repair partially promoted its signed refs")
174 }
175 if got := strings.TrimSpace(prGitOutput(t, "--git-dir", gitDir, "rev-parse", blocker)); got != first {
176 t.Fatalf("repair changed blocking ref: %s", got)
177 }
178 if out := prGitOutput(t, "--git-dir", gitDir, "for-each-ref", "refs/tinyrelay/pr-repair/"); strings.TrimSpace(out) != "" {
179 t.Fatalf("failed repair left temporary refs: %s", out)
180 }
181}
182
183func TestPullRequestRepairFailureLeavesHostedRefsUnchanged(t *testing.T) {
184 secret := strings.Repeat("0", 63) + "1"
185 owner, err := event.PublicKey(secret)
186 if err != nil {
187 t.Fatal(err)
188 }
189 rootDir := t.TempDir()
190 hosted := Repository{Owner: owner, Identifier: "repo", Refs: map[string]string{
191 "refs/heads/main": strings.Repeat("a", 40),
192 }}
193 g := &GitRelay{root: rootDir, allowPrivate: true, configured: make(map[string]struct{})}
194 root := event.Event{Kind: event.KIND_GIT_PR, CreatedAt: 1, PubKey: owner,
195 Tags: [][]string{{"a", "30617:" + owner + ":repo"}, {"c", strings.Repeat("b", 40)}, {"clone", "http://127.0.0.1:1/repo.git"}}}
196 if err := event.Sign(&root, secret); err != nil {
197 t.Fatal(err)
198 }
199 err = g.RepairPullRequestObjects(context.Background(), hosted, PullRequestRepair{Root: root})
200 if err == nil {
201 t.Fatal("repair unexpectedly succeeded from unavailable source")
202 }
203 if _, err := os.Stat(g.repoPath(hosted)); !os.IsNotExist(err) {
204 // ensureRepo is allowed to create the host, but no signed ref may be
205 // installed after a failed quarantine admission.
206 got, _ := exec.Command("git", "--git-dir", g.repoPath(hosted), "show-ref", "refs/nostr/"+root.ID).CombinedOutput()
207 if len(got) != 0 {
208 t.Fatalf("failed repair installed hosted PR ref: %s", got)
209 }
210 }
211}
212
213func TestPRQuarantineRejectsHistoryBeyondDepth(t *testing.T) {
214 root := t.TempDir()
215 work := filepath.Join(root, "work")
216 remote := filepath.Join(root, "remote.git")
217 prGitRun(t, "init", work)
218 prGitRun(t, "-C", work, "config", "user.email", "pr@example.test")
219 prGitRun(t, "-C", work, "config", "user.name", "PR")
220 for i := 0; i < prRepairDepth+1; i++ {
221 if err := os.WriteFile(filepath.Join(work, "history"), []byte(fmt.Sprintf("%d\n", i)), 0600); err != nil {
222 t.Fatal(err)
223 }
224 prGitRun(t, "-C", work, "add", "history")
225 prGitRun(t, "-C", work, "commit", "-m", fmt.Sprintf("commit %d", i))
226 }
227 tip := strings.TrimSpace(prGitOutput(t, "-C", work, "rev-parse", "HEAD"))
228 base := strings.TrimSpace(prGitOutput(t, "-C", work, "rev-parse", "HEAD~1"))
229 prGitRun(t, "init", "--bare", remote)
230 prGitRun(t, "-C", work, "push", remote, "HEAD:refs/heads/main")
231 server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { serveGitBackend(t, w, r, root) }))
232 t.Cleanup(server.Close)
233 ca := filepath.Join(root, "ca.pem")
234 if err := os.WriteFile(ca, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: server.Certificate().Raw}), 0600); err != nil {
235 t.Fatal(err)
236 }
237 t.Setenv("GIT_SSL_CAINFO", ca)
238 secret := strings.Repeat("0", 63) + "1"
239 owner, err := event.PublicKey(secret)
240 if err != nil {
241 t.Fatal(err)
242 }
243 hosted := Repository{Owner: owner, Identifier: "repo"}
244 g := &GitRelay{root: filepath.Join(root, "hosted"), allowPrivate: true, configured: make(map[string]struct{})}
245 pr := event.Event{Kind: event.KIND_GIT_PR, CreatedAt: 1, Tags: [][]string{{"a", "30617:" + owner + ":repo"}, {"c", tip}, {"clone", server.URL + "/remote.git"}}}
246 if err := event.Sign(&pr, secret); err != nil {
247 t.Fatal(err)
248 }
249 if err := g.RepairPullRequestObjects(context.Background(), hosted, PullRequestRepair{Root: pr}); err == nil || !strings.Contains(err.Error(), "object graph") {
250 t.Fatalf("expected incomplete object graph rejection, got %v", err)
251 }
252 if g.objectsPresent(context.Background(), hosted, []string{tip}) {
253 t.Fatal("rejected objects reached the hosted repository")
254 }
255 gitDir := g.repoPath(hosted)
256 if _, err := os.Stat(filepath.Join(gitDir, "shallow")); !os.IsNotExist(err) {
257 t.Fatalf("hosted shallow boundary changed: %v", err)
258 }
259 if out := prGitOutput(t, "--git-dir", gitDir, "for-each-ref", "refs/nostr/"); strings.TrimSpace(out) != "" {
260 t.Fatalf("rejected PR installed refs: %s", out)
261 }
262 // Once the existing base is hosted, the exact same PR must succeed even
263 // though its complete ancestry extends beyond the remote depth window.
264 prGitRun(t, "--git-dir", gitDir, "fetch", work, base+":refs/heads/main")
265 if err := g.RepairPullRequestObjects(context.Background(), hosted, PullRequestRepair{Root: pr}); err != nil {
266 t.Fatalf("PR with hosted base failed: %v", err)
267 }
268 if got := strings.TrimSpace(prGitOutput(t, "--git-dir", gitDir, "rev-parse", "refs/nostr/"+pr.ID)); got != tip {
269 t.Fatalf("PR tip = %s, want %s", got, tip)
270 }
271 prGitRun(t, "--git-dir", gitDir, "fsck", "--full", "--no-dangling")
272 if _, err := os.Stat(filepath.Join(gitDir, "shallow")); !os.IsNotExist(err) {
273 t.Fatalf("successful repair made host shallow: %v", err)
274 }
275}
276
277func TestPrivatePolicyDoesNotContactUnconfiguredRepairSource(t *testing.T) {
278 var requests int
279 server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
280 requests++
281 w.WriteHeader(http.StatusInternalServerError)
282 }))
283 t.Cleanup(server.Close)
284 root := t.TempDir()
285 cert, err := x509.ParseCertificate(server.Certificate().Raw)
286 if err != nil {
287 t.Fatal(err)
288 }
289 ca := filepath.Join(root, "ca.pem")
290 if err := os.WriteFile(ca, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}), 0600); err != nil {
291 t.Fatal(err)
292 }
293 t.Setenv("GIT_SSL_CAINFO", ca)
294 store, err := storage.Open(context.Background(), filepath.Join(root, "relay.db"))
295 if err != nil {
296 t.Fatal(err)
297 }
298 t.Cleanup(func() { _ = store.Close() })
299 p := policy.Defaults("")
300 p.Reads = "members"
301 g, err := New(Config{Store: tinyStore(store), Root: filepath.Join(root, "git"), Policy: func() Policy { return fromInternalPolicy(p) }, AllowPrivateRelays: true})
302 if err != nil {
303 t.Fatal(err)
304 }
305 repo := Repository{Owner: strings.Repeat("a", 64), Identifier: "private", Clone: []string{server.URL + "/repo.git"}, Refs: map[string]string{"refs/heads/main": strings.Repeat("b", 40)}}
306 err = g.FetchMissing(context.Background(), repo, repo.Clone, repo.Refs)
307 if err == nil {
308 t.Fatal("private-policy repair unexpectedly succeeded")
309 }
310 if requests != 0 {
311 t.Fatalf("private-policy repair contacted unconfigured source %d time(s)", requests)
312 }
313}
314
315func TestGRASPTickResumesAfterCanceledRepository(t *testing.T) {
316 p := policy.Policy{Features: policy.Features{Grasp: true, Grasp02: true}}
317 owner := strings.Repeat("a", 64)
318 first := Repository{Owner: owner, Identifier: "a", Refs: map[string]string{}}
319 second := Repository{Owner: owner, Identifier: "b", Refs: map[string]string{}}
320 var calls []string
321 var cancel context.CancelFunc
322 g := &GitRelay{
323 root: t.TempDir(),
324 policy: func() Policy { return fromInternalPolicy(p) },
325 repos: map[string]Repository{key(first.Owner, first.Identifier): first, key(second.Owner, second.Identifier): second},
326 gitSync: func(ctx context.Context, repo Repository) error {
327 calls = append(calls, repo.Identifier)
328 if len(calls) == 1 {
329 cancel()
330 return ctx.Err()
331 }
332 return nil
333 },
334 }
335 ctx, done := context.WithCancel(context.Background())
336 cancel = done
337 if err := g.GRASPService().Tick(ctx); err == nil {
338 t.Fatal("canceled GRASP tick unexpectedly succeeded")
339 }
340 status, err := g.GRASPService().SyncStatus(context.Background())
341 if err != nil {
342 t.Fatal(err)
343 }
344 if _, ok := status.Repositories[key(second.Owner, second.Identifier)]; ok {
345 t.Fatalf("unattempted repository received a status: %#v", status.Repositories)
346 }
347 if err := g.GRASPService().Tick(context.Background()); err != nil {
348 t.Fatal(err)
349 }
350 status, err = g.GRASPService().SyncStatus(context.Background())
351 if err != nil {
352 t.Fatal(err)
353 }
354 if status.Repositories[key(second.Owner, second.Identifier)].SucceededAt == 0 {
355 t.Fatalf("resumed repository was not attempted: %#v", status.Repositories)
356 }
357 if len(calls) != 2 || calls[1] != "b" {
358 t.Fatalf("resume order = %v, want [a b]", calls)
359 }
360}
361
362func prGitRun(t *testing.T, args ...string) {
363 t.Helper()
364 if out, err := exec.Command("git", args...).CombinedOutput(); err != nil {
365 t.Fatalf("git %v: %v: %s", args, err, out)
366 }
367}
368
369func prGitOutput(t *testing.T, args ...string) string {
370 t.Helper()
371 out, err := exec.Command("git", args...).Output()
372 if err != nil {
373 t.Fatal(err)
374 }
375 return string(out)
376}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs