tinyrelayfile
tinyrelay/cmd/tinygit/main_integration_test.go/

cmd/tinygit/main_integration_test.go raw
1package main
2
3import (
4 "bufio"
5 "bytes"
6 "context"
7 "encoding/json"
8 "fmt"
9 "io"
10 "net/http"
11 "os"
12 "os/exec"
13 "path/filepath"
14 "strings"
15 "testing"
16 "time"
17
18 "github.com/FelineStateMachine/tinyrelay/internal/event"
19)
20
21// This is deliberately a subprocess acceptance test: it proves cmd/tinygit is
22// a real standalone server, not only an embeddable HTTP handler.
23func TestStandaloneBinarySignedPushCloneAndRestart(t *testing.T) {
24 if testing.Short() {
25 t.Skip("builds and runs the standalone binary and native Git")
26 }
27 root := t.TempDir()
28 binary := filepath.Join(root, "tinygit")
29 build := exec.Command("go", "build", "-o", binary, ".")
30 if out, err := build.CombinedOutput(); err != nil {
31 t.Fatalf("build standalone binary: %v\n%s", err, out)
32 }
33 ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
34 defer cancel()
35 secret := strings.Repeat("0", 63) + "1"
36 owner, err := event.PublicKey(secret)
37 if err != nil {
38 t.Fatal(err)
39 }
40 data := filepath.Join(root, "data")
41 start := func() (string, func()) {
42 t.Helper()
43 cmd := exec.CommandContext(ctx, binary, "-listen", "127.0.0.1:0", "-data", data, "-owner", owner)
44 stdout, err := cmd.StdoutPipe()
45 if err != nil {
46 t.Fatal(err)
47 }
48 cmd.Stderr = os.Stderr
49 if err := cmd.Start(); err != nil {
50 t.Fatal(err)
51 }
52 done := make(chan error, 1)
53 go func() { done <- cmd.Wait() }()
54 stopped := false
55 stop := func() {
56 t.Helper()
57 if stopped {
58 return
59 }
60 stopped = true
61 _ = cmd.Process.Signal(os.Interrupt)
62 select {
63 case err := <-done:
64 if err != nil {
65 t.Errorf("server exit: %v", err)
66 }
67 case <-time.After(10 * time.Second):
68 _ = cmd.Process.Kill()
69 <-done
70 t.Error("server did not shut down")
71 }
72 }
73 t.Cleanup(stop)
74 line := make(chan string, 1)
75 go func() {
76 scanner := bufio.NewScanner(stdout)
77 if scanner.Scan() {
78 line <- scanner.Text()
79 } else {
80 line <- ""
81 }
82 }()
83 var address string
84 select {
85 case ready := <-line:
86 const prefix = "tinygit listening on "
87 if !strings.HasPrefix(ready, prefix) {
88 t.Fatalf("startup: %q", ready)
89 }
90 address = strings.TrimPrefix(ready, prefix)
91 case <-ctx.Done():
92 t.Fatal(ctx.Err())
93 }
94 res, err := http.Get(address + "/healthz")
95 if err != nil {
96 t.Fatal(err)
97 }
98 res.Body.Close()
99 if res.StatusCode != http.StatusOK {
100 t.Fatalf("health = %d", res.StatusCode)
101 }
102 return address, stop
103 }
104 address, stop := start()
105 post := func(e event.Event) bool {
106 t.Helper()
107 if err := event.Sign(&e, secret); err != nil {
108 t.Fatal(err)
109 }
110 body, err := json.Marshal(e)
111 if err != nil {
112 t.Fatal(err)
113 }
114 res, err := http.Post(address+"/events", "application/json", bytes.NewReader(body))
115 if err != nil {
116 t.Fatal(err)
117 }
118 defer res.Body.Close()
119 var result struct {
120 ID string `json:"id"`
121 Pending bool `json:"pending"`
122 }
123 if res.StatusCode != http.StatusOK {
124 body, _ := io.ReadAll(res.Body)
125 t.Fatalf("publish = %d: %s", res.StatusCode, body)
126 }
127 if err := json.NewDecoder(res.Body).Decode(&result); err != nil {
128 t.Fatal(err)
129 }
130 if result.ID != e.ID {
131 t.Fatalf("event ID = %q, want %q", result.ID, e.ID)
132 }
133 return result.Pending
134 }
135 git := func(dir string, wantSuccess bool, args ...string) string {
136 t.Helper()
137 cmd := exec.CommandContext(ctx, "git", args...)
138 cmd.Dir = dir
139 cmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0", "GIT_AUTHOR_NAME=test", "GIT_AUTHOR_EMAIL=test@example.invalid", "GIT_COMMITTER_NAME=test", "GIT_COMMITTER_EMAIL=test@example.invalid")
140 out, err := cmd.CombinedOutput()
141 if (err == nil) != wantSuccess {
142 t.Fatalf("git %v: %v\n%s", args, err, out)
143 }
144 return strings.TrimSpace(string(out))
145 }
146 work := filepath.Join(root, "work")
147 if err := os.Mkdir(work, 0700); err != nil {
148 t.Fatal(err)
149 }
150 git(work, true, "init", "-b", "main")
151 if err := os.WriteFile(filepath.Join(work, "hello.txt"), []byte("standalone Git\n"), 0600); err != nil {
152 t.Fatal(err)
153 }
154 git(work, true, "add", "hello.txt")
155 git(work, true, "commit", "-m", "first")
156 oid := git(work, true, "rev-parse", "HEAD")
157 post(event.Event{Kind: 30617, CreatedAt: 100, Tags: [][]string{{"d", "demo"}}})
158 cloneURL := func() string {
159 return address + "/npub10xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqpkge6d/demo.git"
160 }
161 git(work, false, "push", cloneURL(), "main")
162 state := event.Event{Kind: 30618, CreatedAt: 101, Tags: [][]string{{"d", "demo"}, {"HEAD", "ref: refs/heads/main"}, {"refs/heads/main", oid}}}
163 if !post(state) {
164 t.Fatal("missing objects were not staged as pending")
165 }
166 t.Log(git(work, true, "push", cloneURL(), "main"))
167 if post(state) {
168 t.Fatal("receive-pack did not promote pending state")
169 }
170 git(work, true, "commit", "--allow-empty", "-m", "unsigned change")
171 git(work, false, "push", cloneURL(), "main")
172 git(work, false, "push", cloneURL(), "HEAD:refs/heads/unauthorized")
173 verifyClone := func(name string) {
174 t.Helper()
175 dest := filepath.Join(root, name)
176 t.Log(git(root, true, "clone", cloneURL(), dest))
177 if got := git(dest, true, "rev-parse", "HEAD"); got != oid {
178 t.Fatalf("cloned HEAD = %s, want %s", got, oid)
179 }
180 body, err := os.ReadFile(filepath.Join(dest, "hello.txt"))
181 if err != nil || string(body) != "standalone Git\n" {
182 t.Fatalf("clone content = %q (%v)", body, err)
183 }
184 }
185 verifyClone("clone")
186 stop()
187 address, stop = start()
188 defer stop()
189 verifyClone("restarted-clone")
190 t.Log(fmt.Sprintf("verified signed push, rejected unsigned pushes, clone and durable restart at %s", oid))
191}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

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

Refs