package main import ( "context" "encoding/base64" "encoding/json" "fmt" "net/http" "sort" "strings" "testing" ) // sourceRepo is another repository of the fake forge, answering the contents, branch, repository and commit // reads in the shapes the module already parses elsewhere (forge.go, reads.go, commit.go); a file's content // is the forge's base64 "content" with its "encoding". type sourceRepo struct { branches map[string]string trees map[string]map[string]string // commit -> path -> content symlinks map[string]bool // paths that are symlinks, in every tree moveTo map[string]string // branch -> commit it moves to right after it is first read tags map[string]string reads []string } func (f *fakeForge) addSource(name string, s *sourceRepo) { if f.sources == nil { f.sources = map[string]*sourceRepo{} } f.sources[name] = s } func (f *fakeForge) handleSource(w http.ResponseWriter, r *http.Request, path string) { rest := strings.TrimPrefix(path, "/api/v1/repos/acme/") name, rest, _ := strings.Cut(rest, "/") s := f.sources[name] if s == nil { http.NotFound(w, r) return } reply := func(v any) { _ = json.NewEncoder(w).Encode(v) } s.reads = append(s.reads, r.Method+" "+path+"?"+r.URL.RawQuery) switch { case rest == "": reply(map[string]any{"full_name": "acme/" + name, "default_branch": "main"}) case strings.HasPrefix(rest, "branches/"): b := strings.TrimPrefix(rest, "branches/") c, ok := s.branches[b] if !ok { http.NotFound(w, r) return } reply(map[string]any{"name": b, "commit": map[string]any{"id": c}}) if to, ok := s.moveTo[b]; ok { delete(s.moveTo, b) s.branches[b] = to } case rest == "commits": if c, ok := s.tags[r.URL.Query().Get("sha")]; ok { reply([]any{map[string]any{"sha": c}}) return } reply([]any{}) case strings.HasPrefix(rest, "contents/"): p := strings.TrimPrefix(rest, "contents/") tree, ok := s.trees[r.URL.Query().Get("ref")] if !ok { http.NotFound(w, r) return } if content, ok := tree[p]; ok { typ := "file" if s.symlinks[p] { typ = "symlink" } reply(map[string]any{"type": typ, "path": p, "name": p[strings.LastIndex(p, "/")+1:], "sha": blob(content), "size": len(content), "encoding": "base64", "content": base64.StdEncoding.EncodeToString([]byte(content))}) return } // A directory: its entries, each with its full path. entries := map[string]map[string]any{} for q, content := range tree { if !strings.HasPrefix(q, p+"/") { continue } sub := strings.TrimPrefix(q, p+"/") first, more, nested := strings.Cut(sub, "/") _ = more if nested { entries[first] = map[string]any{"name": first, "path": p + "/" + first, "type": "dir"} continue } typ := "file" if s.symlinks[q] { typ = "symlink" } entries[first] = map[string]any{"name": first, "path": q, "type": typ, "size": len(content), "sha": blob(content)} } if len(entries) == 0 { http.NotFound(w, r) return } var names []string for n := range entries { names = append(names, n) } sort.Strings(names) list := []any{} for _, n := range names { list = append(list, entries[n]) } reply(list) default: http.NotFound(w, r) } } func newSource() *sourceRepo { return &sourceRepo{ branches: map[string]string{"main": "s0"}, tags: map[string]string{"v1": "s0"}, trees: map[string]map[string]string{ "s0": {"lib/a.go": "package a\n", "lib/deep/b.go": "package b\n", "lib/deep/er/c.txt": "c\n", "top.md": "top\n", "other/x": "x\n"}, }, } } func copyAsk(t *testing.T, files ...map[string]any) CommitAsk { return ask(t, map[string]any{"branch": "fix/1365-x", "files": filesArg(files...)}) } func cp(path, repo, ref, src string) map[string]any { return map[string]any{"path": path, "copy_from": map[string]any{"owner": "acme", "repo": repo, "ref": ref, "path": src}} } // Seen failing first: with the expansion switched off the commit carried no copy and the answer named no source. func TestACopyFromAFileCommitsTheSourcesContentAndNamesItsCommit(t *testing.T) { f := newFake(t) f.addSource("src", newSource()) got, err := commit(context.Background(), f.serve(), copyAsk(t, cp("vendor/top.md", "src", "main", "top.md"))) if err != nil { t.Fatal(err) } if f.trees[got.Commit]["vendor/top.md"] != "top\n" { t.Fatalf("tree %v", f.trees[got.Commit]) } if len(got.Copies) != 1 || got.Copies[0].Source != "s0" || got.Copies[0].Path != "vendor/top.md" || got.Copies[0].Files != 1 { t.Fatalf("copies %+v", got.Copies) } } // Seen failing first, as above. func TestACopyFromADirectoryCopiesEveryFileUnderItToTheSameRelativePaths(t *testing.T) { f := newFake(t) f.addSource("src", newSource()) got, err := commit(context.Background(), f.serve(), copyAsk(t, cp("pkg/lib", "src", "v1", "lib"), map[string]any{"path": "own.md", "content": "own\n"})) if err != nil { t.Fatal(err) } tree := f.trees[got.Commit] want := map[string]string{"pkg/lib/a.go": "package a\n", "pkg/lib/deep/b.go": "package b\n", "pkg/lib/deep/er/c.txt": "c\n", "own.md": "own\n"} for p, c := range want { if tree[p] != c { t.Errorf("%s is %q, want %q", p, tree[p], c) } } if tree["pkg/lib/top.md"] != "" || tree["pkg/other/x"] != "" { t.Errorf("files outside the directory were copied: %v", tree) } if len(got.Copies) != 1 || got.Copies[0].Source != "s0" || got.Copies[0].Files != 3 || got.Copies[0].Bytes != len("package a\npackage b\nc\n") { t.Fatalf("copies %+v", got.Copies) } if len(got.Files) != 4 { t.Errorf("files %+v", got.Files) } } // Seen failing first, as above: the files were read at the branch's new head. func TestEveryCopiedFileIsReadAtTheOneShaTheRefResolvedTo(t *testing.T) { f := newFake(t) s := newSource() s.trees["s1"] = map[string]string{"lib/a.go": "package MOVED\n", "lib/deep/b.go": "package MOVED\n", "lib/deep/er/c.txt": "MOVED\n"} s.moveTo = map[string]string{"main": "s1"} f.addSource("src", s) got, err := commit(context.Background(), f.serve(), copyAsk(t, cp("pkg", "src", "main", "lib"))) if err != nil { t.Fatal(err) } if f.trees[got.Commit]["pkg/a.go"] != "package a\n" || got.Copies[0].Source != "s0" { t.Fatalf("tree %v, copies %+v", f.trees[got.Commit], got.Copies) } for _, r := range s.reads { if strings.Contains(r, "/contents/") && !strings.Contains(r, "ref=s0") { t.Errorf("a file was read at another ref: %s", r) } } } // Seen failing first: each case was taken or refused for another reason (or sent). func TestEveryRefusalOfACopySendsNothingAndSaysWhy(t *testing.T) { big := strings.Repeat("z", maxFileBytes+1) many := map[string]string{} for i := 0; i < maxFiles+1; i++ { many[fmt.Sprintf("many/f%03d", i)] = "m" } for name, c := range map[string]struct { file map[string]any want string }{ "unknown repository": {cp("t", "nope", "main", "top.md"), "no repository acme/nope"}, "unknown ref": {cp("t", "src", "nothing", "top.md"), `no branch, tag or commit "nothing"`}, "unknown path": {cp("t", "src", "main", "missing/dir"), `no "missing/dir"`}, "symlink in source": {cp("t", "src", "main", "link"), "link is a symlink"}, "symlink under dir": {cp("t", "src", "main", "withlink"), "withlink/l is a symlink"}, "file over 4 MiB": {cp("t", "src", "main", "big"), "more than the 4.0 MiB"}, "over 200 files": {cp("t", "src", "main", "many"), "201 files"}, "with content": {map[string]any{"path": "t", "content": "x", "copy_from": cp("t", "src", "main", "top.md")["copy_from"]}, "copy_from together with content"}, "with base64": {map[string]any{"path": "t", "content_base64": "eA==", "copy_from": cp("t", "src", "main", "top.md")["copy_from"]}, "copy_from together with content"}, "with delete": {map[string]any{"path": "t", "delete": true, "copy_from": cp("t", "src", "main", "top.md")["copy_from"]}, "copy_from together with content"}, "meets a given file": {cp("pkg", "src", "main", "lib"), "pkg/a.go is named twice"}, } { f := newFake(t) s := newSource() s.trees["s0"]["link"] = "top.md" s.symlinks = map[string]bool{"link": true, "withlink/l": true} s.trees["s0"]["withlink/l"] = "top.md" s.trees["s0"]["withlink/ok"] = "ok" s.trees["s0"]["big/f"] = big for p, v := range many { s.trees["s0"][p] = v } f.addSource("src", s) files := []map[string]any{c.file} if name == "meets a given file" { files = append(files, map[string]any{"path": "pkg/a.go", "content": "mine"}) } a, err := readCommitAsk(map[string]any{"owner": "acme", "repo": "app", "message": "m", "branch": "fix/1365-x", "files": filesArg(files...)}) if err == nil { _, err = commit(context.Background(), f.serve(), a) } if err == nil || !strings.Contains(err.Error(), c.want) || !strings.Contains(err.Error(), "Nothing was sent") { t.Errorf("%s: %v (want %q)", name, err, c.want) } if len(f.writes) != 0 || len(f.patches) != 0 || f.branches["fix/1365-x"] != "" { t.Errorf("%s: something was sent: %v %v", name, f.writes, f.branches) } } } // Seen failing first: the copy was not counted, so the commit was sent. func TestACopyOverTheTotalNamesItsCountAndSizeSoItCanBeSplit(t *testing.T) { f := newFake(t) s := newSource() chunk := strings.Repeat("q", maxFileBytes) for i := 0; i < 5; i++ { s.trees["s0"][fmt.Sprintf("heavy/f%d", i)] = chunk } f.addSource("src", s) _, err := commit(context.Background(), f.serve(), copyAsk(t, cp("h", "src", "main", "heavy"))) if err == nil || !strings.Contains(err.Error(), "5 files, 20.0 MiB together") || !strings.Contains(err.Error(), "by subdirectory") { t.Fatalf("%v", err) } if len(f.writes) != 0 { t.Fatal("sent") } } func TestCopyFromIsInTheToolsSchemaAndTheReadme(t *testing.T) { var schema map[string]any for _, tl := range Tools(func() (*Forge, error) { return nil, fmt.Errorf("unset") }) { if tl.Name == "gitea_commit" { schema = tl.Input } } item := schema["properties"].(map[string]any)["files"].(map[string]any)["items"].(map[string]any)["properties"].(map[string]any) cf, ok := item["copy_from"].(map[string]any) if !ok { t.Fatalf("copy_from is not in the schema: %v", item) } props := cf["properties"].(map[string]any) for _, k := range []string{"owner", "repo", "ref", "path"} { if _, ok := props[k]; !ok { t.Errorf("copy_from has no %s", k) } } } func TestACopyFromThatIsNotAnObjectIsRefused(t *testing.T) { _, err := readCommitAsk(map[string]any{"owner": "acme", "repo": "app", "message": "m", "branch": "b", "files": filesArg(map[string]any{"path": "x", "copy_from": "acme/src:top.md"})}) if err == nil || !strings.Contains(err.Error(), "copy_from is an object") { t.Fatalf("%v", err) } }