Monorepo for Tangled
tangled.org
1package knotmirror
2
3import (
4 "context"
5 "io"
6 "log/slog"
7 "net/http"
8 "net/http/httptest"
9 "strings"
10 "testing"
11
12 "tangled.org/core/knotmirror/config"
13 "tangled.org/core/knotmirror/models"
14)
15
16func uploadPackAdvert(capabilities string) string {
17 return "001e# service=git-upload-pack\n" +
18 "0000" +
19 "0000000000000000000000000000000000000000 capabilities^{}\x00" + capabilities + "\n" +
20 "0000"
21}
22
23func TestCheckKnotObjectFormat(t *testing.T) {
24 const gitContentType = "application/x-git-upload-pack-advertisement"
25
26 tests := []struct {
27 name string
28 status int
29 contentType string
30 body string
31 wantFormat models.ObjectFormat
32 wantErr bool
33 wantRateLimit bool
34 }{
35 {
36 name: "sha256 repo is detected",
37 status: http.StatusOK,
38 contentType: gitContentType,
39 body: uploadPackAdvert("multi_ack thin-pack side-band-64k ofs-delta object-format=sha256 agent=git/2.45.0"),
40 wantFormat: models.ObjectFormatSHA256,
41 },
42 {
43 name: "explicit sha1 repo stays on sha1",
44 status: http.StatusOK,
45 contentType: gitContentType,
46 body: uploadPackAdvert("multi_ack thin-pack side-band-64k ofs-delta object-format=sha1 agent=git/2.45.0"),
47 wantFormat: models.ObjectFormatSHA1,
48 },
49 {
50 name: "advertisement without object-format defaults to sha1",
51 status: http.StatusOK,
52 contentType: gitContentType,
53 body: uploadPackAdvert("multi_ack thin-pack side-band-64k ofs-delta agent=git/2.34.0"),
54 wantFormat: models.ObjectFormatSHA1,
55 },
56 {
57 name: "sha256 detected with content-type parameters",
58 status: http.StatusOK,
59 contentType: gitContentType + "; charset=utf-8",
60 body: uploadPackAdvert("object-format=sha256"),
61 wantFormat: models.ObjectFormatSHA256,
62 },
63 {
64 name: "rate limited knot",
65 status: http.StatusTooManyRequests,
66 wantErr: true,
67 wantRateLimit: true,
68 },
69 {
70 name: "missing repo on knot",
71 status: http.StatusNotFound,
72 wantErr: true,
73 },
74 {
75 name: "non git content-type",
76 status: http.StatusOK,
77 contentType: "text/html",
78 body: "<html>not a git server</html>",
79 wantErr: true,
80 },
81 }
82
83 for _, tt := range tests {
84 t.Run(tt.name, func(t *testing.T) {
85 var gotPath string
86 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
87 gotPath = r.URL.Path
88 if tt.contentType != "" {
89 w.Header().Set("Content-Type", tt.contentType)
90 }
91 w.WriteHeader(tt.status)
92 io.WriteString(w, tt.body)
93 }))
94 defer srv.Close()
95
96 r := &Resyncer{
97 logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
98 cfg: &config.Config{},
99 httpClient: srv.Client(),
100 }
101 repo := &models.Repo{
102 RepoDid: "did:plc:boltless",
103 KnotDomain: srv.URL,
104 }
105
106 format, err := r.checkKnot(context.Background(), repo)
107
108 if tt.wantErr {
109 if err == nil {
110 t.Fatalf("expected error, got format %q", format)
111 }
112 if format != "" {
113 t.Errorf("expected empty format on error, got %q", format)
114 }
115 if got := isRateLimitError(err); got != tt.wantRateLimit {
116 t.Errorf("isRateLimitError = %v, want %v", got, tt.wantRateLimit)
117 }
118 return
119 }
120
121 if err != nil {
122 t.Fatalf("unexpected error: %v", err)
123 }
124 if format != tt.wantFormat {
125 t.Errorf("format = %q, want %q", format, tt.wantFormat)
126 }
127 if !strings.HasSuffix(gotPath, "/info/refs") {
128 t.Errorf("expected upstream request to /info/refs, got %q", gotPath)
129 }
130 })
131 }
132}