Files
mesh-catalog/modules/gitea/cmd/forge/copy_test.go
T
mesh-tools dc3a177410
mesh/merge-gate head dc3a177410fa merged into trunk c0880c4dc271 — pass: builds gitea → novox; no bus step; every machine composes with the change as it di…
mesh/repo-check head dc3a177410fa merged into trunk c0880c4dc271 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
Let a commit copy files from another forge repository, so a large move is not typed through the caller
2026-10-20 01:07:28 +00:00

296 lines
10 KiB
Go

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)
}
}