This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / gitutil / archive_test.go
9.4 kB 187 lines
1package gitutil 2 3import ( 4 "archive/zip" 5 "bytes" 6 "context" 7 "mime" 8 "net/http" 9 "net/url" 10 "os" 11 "os/exec" 12 "path/filepath" 13 "strings" 14 "testing" 15 "unicode/utf8" 16 17 "github.com/go-git/go-git/v5/plumbing" 18 "github.com/samber/lo" 19 "github.com/stretchr/testify/assert" 20 "github.com/stretchr/testify/require" 21) 22 23const testArchiveEndpoint = "https://knot.nel.pet/xrpc/sh.tangled.repo.archive" 24 25func TestParseArchiveParams(t *testing.T) { 26 cases := []struct { 27 name string 28 query url.Values 29 repo RepoName 30 want ArchiveParams 31 wantStem ArchivePrefix 32 wantFilename string 33 wantType string 34 wantErr string 35 }{ 36 {"all empty", url.Values{}, "", ArchiveParams{Format: ArchiveTarGz}, "", "", "", ""}, 37 {"tar.gz", url.Values{"format": {"tar.gz"}}, "", ArchiveParams{Format: ArchiveTarGz}, "", "", "", ""}, 38 {"zip", url.Values{"format": {"zip"}}, "", ArchiveParams{Format: ArchiveZip}, "", "", "", ""}, 39 { 40 "every param", 41 url.Values{"ref": {"refs/tags/v1.0.0"}, "format": {"zip"}, "prefix": {"/kelp/"}}, 42 "", ArchiveParams{Rev: "refs/tags/v1.0.0", Format: ArchiveZip, Prefix: "kelp"}, 43 "kelp", "squid-v1.0.0.zip", "application/zip", "", 44 }, 45 46 {"branch", url.Values{"ref": {"main"}}, "", ArchiveParams{Rev: "main", Format: ArchiveTarGz}, "squid-main", "squid-main.tar.gz", "application/gzip", ""}, 47 {"full ref", url.Values{"ref": {"refs/heads/feat/uni"}}, "", ArchiveParams{Rev: "refs/heads/feat/uni", Format: ArchiveTarGz}, "squid-feat-uni", "squid-feat-uni.tar.gz", "application/gzip", ""}, 48 {"head", url.Values{"ref": {"HEAD"}}, "", ArchiveParams{Rev: "HEAD", Format: ArchiveTarGz}, "squid-HEAD", "", "", ""}, 49 {"trailing slash", url.Values{"ref": {"refs/heads/main/"}}, "", ArchiveParams{Rev: "refs/heads/main/", Format: ArchiveTarGz}, "squid-main-", "", "", ""}, 50 {"traversal in a ref", url.Values{"ref": {"../../etc"}}, "", ArchiveParams{Rev: "../../etc", Format: ArchiveTarGz}, "squid-..-..-etc", "", "", ""}, 51 {"windows separator in a ref", url.Values{"ref": {`feat\uni`}}, "", ArchiveParams{Rev: `feat\uni`, Format: ArchiveTarGz}, "squid-feat-uni", "", "", ""}, 52 {"slash in a repo name", url.Values{"ref": {"main"}}, "kelp/limpet", ArchiveParams{Rev: "main", Format: ArchiveTarGz}, "kelp-limpet-main", "kelp-limpet-main.tar.gz", "application/gzip", ""}, 53 {"quote in a repo name", url.Values{"ref": {"main"}}, `squid-a"b`, ArchiveParams{Rev: "main", Format: ArchiveTarGz}, `squid-a"b-main`, `squid-a"b-main.tar.gz`, "application/gzip", ""}, 54 {"non-ascii repo name", url.Values{"ref": {"main"}, "format": {"zip"}}, "squid-über", ArchiveParams{Rev: "main", Format: ArchiveZip}, "squid-über-main", "squid-über-main.zip", "application/zip", ""}, 55 56 {"bare slash prefix", url.Values{"prefix": {"/"}}, "", ArchiveParams{Format: ArchiveTarGz}, "", "", "", ""}, 57 {"did prefix", url.Values{"prefix": {"did:plc:boltless"}}, "", ArchiveParams{Format: ArchiveTarGz, Prefix: "did:plc:boltless"}, "", "", "", ""}, 58 {"nested prefix", url.Values{"prefix": {"squid/main"}}, "", ArchiveParams{Format: ArchiveTarGz, Prefix: "squid/main"}, "", "", "", ""}, 59 {"prefix wrapped in slashes", url.Values{"prefix": {"/squid/main/"}}, "", ArchiveParams{Format: ArchiveTarGz, Prefix: "squid/main"}, "", "", "", ""}, 60 {"redundant prefix segments", url.Values{"prefix": {"squid/../limpet"}}, "", ArchiveParams{Format: ArchiveTarGz, Prefix: "limpet"}, "", "", "", ""}, 61 {"space in a prefix", url.Values{"prefix": {"squid main"}}, "", ArchiveParams{Format: ArchiveTarGz, Prefix: "squid main"}, "", "", "", ""}, 62 63 {"unsupported format", url.Values{"format": {"tar"}}, "", ArchiveParams{}, "", "", "", "only tar.gz and zip formats are supported"}, 64 {"space in a ref", url.Values{"ref": {"refs/tags/a b"}}, "", ArchiveParams{}, "", "", "", "ref contains whitespace"}, 65 {"control character in a ref", url.Values{"ref": {"refs/tags/a\nb"}}, "", ArchiveParams{}, "", "", "", "ref contains whitespace"}, 66 {"ref that git would read as an option", url.Values{"ref": {"--output=/tmp/evil"}}, "", ArchiveParams{}, "", "", "", "ref starts with a dash"}, 67 {"prefix escaping the root", url.Values{"prefix": {"../../evil"}}, "", ArchiveParams{}, "", "", "", "prefix escapes the archive root"}, 68 {"prefix escaping after cleaning", url.Values{"prefix": {"squid/../../evil"}}, "", ArchiveParams{}, "", "", "", "prefix escapes the archive root"}, 69 {"bare dot prefix", url.Values{"prefix": {"."}}, "", ArchiveParams{}, "", "", "", "prefix escapes the archive root"}, 70 {"control character in a prefix", url.Values{"prefix": {"squid\nmain"}}, "", ArchiveParams{}, "", "", "", "prefix contains a control character"}, 71 {"windows separator in a prefix", url.Values{"prefix": {`..\..\evil`}}, "", ArchiveParams{}, "", "", "", "prefix contains a backslash"}, 72 {"prefix over the length limit", url.Values{"prefix": {strings.Repeat("a", MaxArchivePrefixLen+1)}}, "", ArchiveParams{}, "", "", "", "over the 255 byte limit"}, 73 } 74 75 for _, tc := range cases { 76 t.Run(tc.name, func(t *testing.T) { 77 got, err := ParseArchiveParams(tc.query) 78 rejected := err != nil 79 if got != tc.want || rejected != (tc.wantErr != "") || (rejected && !strings.Contains(err.Error(), tc.wantErr)) { 80 t.Fatalf("params = %+v with err %v, want %+v and an error mentioning %q", got, err, tc.want, tc.wantErr) 81 } 82 if rejected { 83 return 84 } 85 86 repo := RepoName("squid") 87 if tc.repo != "" { 88 repo = tc.repo 89 } 90 if stem := got.Prefix.OrDefault(repo, got.Rev); tc.wantStem != "" && stem != tc.wantStem { 91 t.Errorf("default prefix = %q, want %q", stem, tc.wantStem) 92 } 93 if tc.wantFilename != "" { 94 header := http.Header{} 95 got.SetHeaders(header, repo) 96 mediatype, fields, err := mime.ParseMediaType(header.Get("Content-Disposition")) 97 if err != nil || mediatype != "attachment" || fields["filename"] != tc.wantFilename { 98 t.Errorf("Content-Disposition = %q (err %v), want an attachment with filename %q", header.Get("Content-Disposition"), err, tc.wantFilename) 99 } 100 if ct, sniff := header.Get("Content-Type"), header.Get("X-Content-Type-Options"); ct != tc.wantType || sniff != "nosniff" { 101 t.Errorf("Content-Type = %q with X-Content-Type-Options %q, want %q and nosniff", ct, sniff, tc.wantType) 102 } 103 } 104 105 query := got.Query("did:plc:limpet") 106 if back, err := ParseArchiveParams(query); err != nil || back != got { 107 t.Errorf("query round trip = %+v (err %v), want %+v", back, err, got) 108 } 109 back, err := ParseImmutableLink(ImmutableLink(testArchiveEndpoint + "?" + query.Encode())) 110 if got.Rev != "" && (err != nil || back != got.Rev) { 111 t.Errorf("Link round trip = %q (err %v), want %q", back, err, got.Rev) 112 } 113 }) 114 } 115} 116 117func TestArchiveFallbacks(t *testing.T) { 118 hash := plumbing.NewHash("6f1d3a2b4c5d6e7f8091a2b3c4d5e6f708192a3b") 119 if kept, filled := Rev("refs/heads/main").OrHash(hash), Rev("").OrHash(hash); kept != "refs/heads/main" || filled != RevFromHash(hash) { 120 t.Errorf("OrHash kept %q and filled %q, want refs/heads/main and the hash %q", kept, filled, hash) 121 } 122 if got := Rev("").Or(RevHead); got != RevHead { 123 t.Errorf("empty rev = %q, want HEAD", got) 124 } 125 if _, err := ParseImmutableLink(""); err == nil { 126 t.Error("ParseImmutableLink must reject an empty header") 127 } 128 129 params := ArchiveParams{Rev: "main", Format: ArchiveZip, Prefix: "kelp"} 130 if got := params.WithRev("6f1d3a2"); got != (ArchiveParams{Rev: "6f1d3a2", Format: ArchiveZip, Prefix: "kelp"}) || params.Rev != "main" { 131 t.Errorf("WithRev = %+v leaving the receiver at %q, want only the rev replaced", got, params.Rev) 132 } 133 134 long := ArchivePrefix("").OrDefault("squid", Rev("refs/heads/"+strings.Repeat("ü", 400))) 135 if _, err := ParseArchivePrefix(long.String()); len(long) > MaxArchivePrefixLen || !utf8.ValidString(long.String()) || err != nil { 136 t.Errorf("default prefix is %d bytes %q (err %v), want at most %d bytes ending on a rune boundary", len(long), long, err, MaxArchivePrefixLen) 137 } 138} 139 140func TestWriteArchive(t *testing.T) { 141 repoPath := t.TempDir() 142 require.NoError(t, os.WriteFile(filepath.Join(repoPath, "README.md"), []byte("# squid\n"), 0644)) 143 for _, args := range [][]string{ 144 {"init", "-q", "-b", "main"}, 145 {"add", "README.md"}, 146 {"-c", "user.name=nel", "-c", "user.email=nel@nel.pet", "commit", "-qm", "Initial commit"}, 147 } { 148 cmd := exec.Command("git", args...) 149 cmd.Dir = repoPath 150 require.NoError(t, cmd.Run(), "git %v", args) 151 } 152 153 canceled, cancel := context.WithCancel(context.Background()) 154 cancel() 155 156 cases := []struct { 157 name string 158 ctx context.Context 159 prefix ArchivePrefix 160 want []string 161 }{ 162 { 163 "prefix on every entry", 164 context.Background(), 165 ArchivePrefix("").OrDefault("squid", "refs/heads/feat/uni"), 166 []string{"squid-feat-uni/", "squid-feat-uni/README.md"}, 167 }, 168 {"empty prefix", context.Background(), "", []string{"README.md"}}, 169 {"canceled context", canceled, "squid-main", nil}, 170 } 171 172 for _, tc := range cases { 173 t.Run(tc.name, func(t *testing.T) { 174 var body bytes.Buffer 175 err := WriteArchive(tc.ctx, &body, repoPath, RevHead, ArchiveZip, tc.prefix) 176 if tc.want == nil { 177 assert.Error(t, err) 178 return 179 } 180 require.NoError(t, err) 181 182 entries, err := zip.NewReader(bytes.NewReader(body.Bytes()), int64(body.Len())) 183 require.NoError(t, err) 184 assert.Equal(t, tc.want, lo.Map(entries.File, func(f *zip.File, _ int) string { return f.Name })) 185 }) 186 } 187}