clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

feat/tinyagent
tinyrelay / cmd / tinygit / main_integration_test.go
main_integration_test.go 5.4K
1package main23import (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"1718 "github.com/FelineStateMachine/tinyrelay/internal/event"19)2021// This is deliberately a subprocess acceptance test: it proves cmd/tinygit is22// 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.Stderr49 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 := false55 stop := func() {56 t.Helper()57 if stopped {58 return59 }60 stopped = true61 _ = 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 <-done70 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 string84 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, stop103 }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.Pending134 }135 git := func(dir string, wantSuccess bool, args ...string) string {136 t.Helper()137 cmd := exec.CommandContext(ctx, "git", args...)138 cmd.Dir = dir139 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}