| 1 | package tinygit |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "crypto/x509" |
| 6 | "encoding/pem" |
| 7 | "io" |
| 8 | "net/http" |
| 9 | "net/http/httptest" |
| 10 | "os" |
| 11 | "os/exec" |
| 12 | "path/filepath" |
| 13 | "strconv" |
| 14 | "strings" |
| 15 | "sync/atomic" |
| 16 | "testing" |
| 17 | |
| 18 | "github.com/FelineStateMachine/tinyrelay/internal/policy" |
| 19 | "github.com/FelineStateMachine/tinyrelay/internal/storage" |
| 20 | ) |
| 21 | |
| 22 | func TestFetchMissingHTTPSRepairAndNoop(t *testing.T) { |
| 23 | root := t.TempDir() |
| 24 | remoteRoot := filepath.Join(root, "remote" ) |
| 25 | work := filepath.Join(root, "work" ) |
| 26 | gitRepairRun(t, "init" , "--bare" , filepath.Join(remoteRoot, "repo.git" )) |
| 27 | gitRepairRun(t, "init" , work) |
| 28 | gitRepairRun(t, "-C" , work, "config" , "user.email" , "repair@example.test" ) |
| 29 | gitRepairRun(t, "-C" , work, "config" , "user.name" , "Repair" ) |
| 30 | if err := os.WriteFile(filepath.Join(work, "README" ), []byte("repair\n" ), 0600); err != nil { |
| 31 | t.Fatal(err) |
| 32 | } |
| 33 | gitRepairRun(t, "-C" , work, "add" , "README" ) |
| 34 | gitRepairRun(t, "-C" , work, "commit" , "-m" , "repair" ) |
| 35 | commit := strings.TrimSpace(gitRepairOutput(t, "-C" , work, "rev-parse" , "HEAD" )) |
| 36 | gitRepairRun(t, "-C" , work, "push" , filepath.Join(remoteRoot, "repo.git" ), "HEAD:refs/heads/main" ) |
| 37 | |
| 38 | var requests atomic.Int32 |
| 39 | server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 40 | requests.Add(1) |
| 41 | serveGitBackend(t, w, r, remoteRoot) |
| 42 | })) |
| 43 | t.Cleanup(server.Close) |
| 44 | caFile := filepath.Join(root, "ca.pem" ) |
| 45 | cert, err := x509.ParseCertificate(server.Certificate().Raw) |
| 46 | if err != nil { |
| 47 | t.Fatal(err) |
| 48 | } |
| 49 | if err := os.WriteFile(caFile, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE" , Bytes: cert.Raw}), 0600); err != nil { |
| 50 | t.Fatal(err) |
| 51 | } |
| 52 | t.Setenv("GIT_SSL_CAINFO" , caFile) |
| 53 | |
| 54 | store, err := storage.Open(context.Background(), filepath.Join(root, "relay.db" )) |
| 55 | if err != nil { |
| 56 | t.Fatal(err) |
| 57 | } |
| 58 | t.Cleanup(func() { _ = store.Close() }) |
| 59 | p := policy.Defaults("" ) |
| 60 | g, err := New(Config{Store: tinyStore(store), Root: filepath.Join(root, "git" ), Policy: func() Policy { return fromInternalPolicy(p) }, AllowPrivateRelays: true}) |
| 61 | if err != nil { |
| 62 | t.Fatal(err) |
| 63 | } |
| 64 | repo := Repository{Owner: strings.Repeat("a" , 64), Identifier: "repo" , Clone: []string{server.URL + "/repo.git" }, Refs: map[string]string{"refs/heads/main" : commit}} |
| 65 | if err := g.FetchMissing(context.Background(), repo, repo.Clone, repo.Refs); err != nil { |
| 66 | t.Fatal(err) |
| 67 | } |
| 68 | if requests.Load() == 0 || !gitRepairObject(t, filepath.Join(root, "git" , repo.Owner, "repo.git" ), commit) { |
| 69 | t.Fatalf("repair did not fetch commit: requests=%d" , requests.Load()) |
| 70 | } |
| 71 | firstRequests := requests.Load() |
| 72 | if err := g.FetchMissing(context.Background(), repo, repo.Clone, repo.Refs); err != nil { |
| 73 | t.Fatal(err) |
| 74 | } |
| 75 | if requests.Load() != firstRequests { |
| 76 | t.Fatalf("noop repair made network requests: before=%d after=%d" , firstRequests, requests.Load()) |
| 77 | } |
| 78 | } |
| 79 | |
| 80 | func serveGitBackend(t *testing.T, w http.ResponseWriter, r *http.Request, root string) { |
| 81 | t.Helper() |
| 82 | env := append(os.Environ(), "GIT_PROJECT_ROOT=" +root, "GIT_HTTP_EXPORT_ALL=1" , "PATH_INFO=" +r.URL.Path, "REQUEST_METHOD=" +r.Method, "QUERY_STRING=" +r.URL.RawQuery, "CONTENT_TYPE=" +r.Header.Get("Content-Type" ), "CONTENT_LENGTH=" +strconv.FormatInt(r.ContentLength, 10)) |
| 83 | cmd := exec.Command("git" , "http-backend" ) |
| 84 | cmd.Env = env |
| 85 | cmd.Stdin = r.Body |
| 86 | out, err := cmd.Output() |
| 87 | if err != nil { |
| 88 | http.Error(w, err.Error(), http.StatusBadGateway) |
| 89 | return |
| 90 | } |
| 91 | parts := strings.SplitN(string(out), "\r\n\r\n" , 2) |
| 92 | if len(parts) != 2 { |
| 93 | http.Error(w, "invalid git backend response" , http.StatusBadGateway) |
| 94 | return |
| 95 | } |
| 96 | for _, line := range strings.Split(parts[0], "\r\n" ) { |
| 97 | fields := strings.SplitN(line, ": " , 2) |
| 98 | if len(fields) == 2 { |
| 99 | w.Header().Set(fields[0], fields[1]) |
| 100 | } |
| 101 | } |
| 102 | _, _ = io.WriteString(w, parts[1]) |
| 103 | } |
| 104 | |
| 105 | func gitRepairRun(t *testing.T, args ...string) { |
| 106 | t.Helper() |
| 107 | if out, err := exec.Command("git" , args...).CombinedOutput(); err != nil { |
| 108 | t.Fatalf("git %v: %v: %s" , args, err, out) |
| 109 | } |
| 110 | } |
| 111 | func gitRepairOutput(t *testing.T, args ...string) string { |
| 112 | t.Helper() |
| 113 | out, err := exec.Command("git" , args...).Output() |
| 114 | if err != nil { |
| 115 | t.Fatal(err) |
| 116 | } |
| 117 | return string(out) |
| 118 | } |
| 119 | func gitRepairObject(t *testing.T, dir, object string) bool { |
| 120 | t.Helper() |
| 121 | return exec.Command("git" , "--git-dir" , dir, "cat-file" , "-e" , object).Run() == nil |
| 122 | } |