package gitx
import (
"context"
"errors"
"fmt"
"strings"
"testing"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/filemode"
"github.com/go-git/go-git/v5/plumbing/object"
"sourcecraft.dev/bigbes/sr-ht-spec/core"
)
func TestWalkDocumentsYieldsOnlyMarkdown(t *testing.T) {
repo, _ := newSpace(t)
ctx := context.Background()
pushApproved(t, repo, ownerMeta("seed", 1),
Write{Path: "specs/0007-storage.md", Content: doc("SPEC-0007", "Storage", "alpha")},
Write{Path: "notes/daily.md", Content: doc("NOTE-0001", "Daily", "beta")},
Write{Path: core.PolicyFile, Content: []byte("review:\n auto_merge: [notes/**]\n")},
Write{Path: "specs/diagram.png", Content: []byte{0x89, 'P', 'N', 'G'}},
)
docs, err := repo.ListDocuments(ctx, DefaultApprovedBranch)
if err != nil {
t.Fatalf("ListDocuments: %v", err)
}
var paths []string
for _, d := range docs {
paths = append(paths, d.Path)
if d.Blob.IsZero() {
t.Fatalf("%q has no blob sha; it is the render cache key", d.Path)
}
if len(d.Data) == 0 {
t.Fatalf("%q has no data", d.Path)
}
}
want := []string{"notes/daily.md", "specs/0007-storage.md"}
if strings.Join(paths, ",") != strings.Join(want, ",") {
t.Fatalf("documents = %v, want %v (attachments and %s are not documents)", paths, want, core.PolicyFile)
}
// .spec.yml is reachable as a blob even though it is not a document.
data, _, err := repo.ReadBlob(ctx, DefaultApprovedBranch, core.PolicyFile)
if err != nil {
t.Fatalf("ReadBlob(%s): %v", core.PolicyFile, err)
}
if _, err := core.ParsePolicy(data); err != nil {
t.Fatalf("ParsePolicy: %v", err)
}
}
func TestReadIsTheSamePathForEveryRevision(t *testing.T) {
repo, _ := newSpace(t)
ctx := context.Background()
first := pushApproved(t, repo, ownerMeta("v1", 1),
Write{Path: "specs/0007.md", Content: doc("SPEC-0007", "Storage", "first")})
pushApproved(t, repo, ownerMeta("v2", 2),
Write{Path: "specs/0007.md", Content: doc("SPEC-0007", "Storage", "second")})
// A pinned ?rev= of a superseded revision renders that revision, not the head.
if got := mustRead(t, repo, first.String(), "specs/0007.md"); !strings.Contains(got, "first") {
t.Fatalf("pinned read = %q, want the superseded revision", got)
}
if got := mustRead(t, repo, DefaultApprovedBranch, "specs/0007.md"); !strings.Contains(got, "second") {
t.Fatalf("branch read = %q, want the head revision", got)
}
// A proposal branch reads through the very same call.
base, err := repo.ApprovedHead(ctx)
if err != nil {
t.Fatal(err)
}
openProposal(t, repo, "proposals/1", base.String(), meta("propose", 3),
Write{Path: "specs/0007.md", Content: doc("SPEC-0007", "Storage", "draft")})
if got := mustRead(t, repo, "proposals/1", "specs/0007.md"); !strings.Contains(got, "draft") {
t.Fatalf("proposal read = %q, want the draft", got)
}
}
func TestReadMissingAndMalformedPaths(t *testing.T) {
repo, _ := newSpace(t)
ctx := context.Background()
pushApproved(t, repo, ownerMeta("seed", 1),
Write{Path: "specs/0007.md", Content: doc("SPEC-0007", "Storage", "body")})
if _, err := repo.ReadDocument(ctx, DefaultApprovedBranch, "specs/nope.md"); !errors.Is(err, ErrNotFound) {
t.Fatalf("missing document = %v, want ErrNotFound", err)
}
// A directory is refused as a distinct class from "absent".
if _, _, err := repo.ReadBlob(ctx, DefaultApprovedBranch, "specs"); !errors.Is(err, ErrUnsupportedEntry) {
t.Fatalf("reading a directory = %v, want ErrUnsupportedEntry", err)
}
// core owns path validation; gitx does not re-derive it.
for _, p := range []string{"../escape.md", "/abs.md", "specs/.git/x.md", "specs/0007.txt"} {
if _, err := repo.ReadDocument(ctx, DefaultApprovedBranch, p); !errors.Is(err, core.ErrInvalidPath) {
t.Fatalf("ReadDocument(%q) = %v, want core.ErrInvalidPath", p, err)
}
}
}
func TestOversizedBlobIsRefusedNotTruncated(t *testing.T) {
repo, _ := newSpace(t)
ctx := context.Background()
body := strings.Repeat("x", 4096)
pushApproved(t, repo, ownerMeta("big", 1),
Write{Path: "specs/0007.md", Content: doc("SPEC-0007", "Storage", body)},
Write{Path: "specs/small.md", Content: doc("SPEC-0008", "Small", "tiny")},
)
// Squeeze the per-blob cap below the big document. A truncated markdown
// document would be indexed and served as though it were whole, so the read
// has to fail instead of returning a prefix.
repo.docLimit = 512
_, err := repo.ReadDocument(ctx, DefaultApprovedBranch, "specs/0007.md")
if !errors.Is(err, ErrTooLarge) {
t.Fatalf("oversized ReadDocument = %v, want ErrTooLarge", err)
}
if _, _, err := repo.ReadBlob(ctx, DefaultApprovedBranch, "specs/0007.md"); !errors.Is(err, ErrTooLarge) {
t.Fatalf("oversized ReadBlob = %v, want ErrTooLarge", err)
}
if _, err := repo.ListDocuments(ctx, DefaultApprovedBranch); !errors.Is(err, ErrTooLarge) {
t.Fatalf("walk over an oversized document = %v, want ErrTooLarge", err)
}
// The small document is unaffected.
if got := mustRead(t, repo, DefaultApprovedBranch, "specs/small.md"); !strings.Contains(got, "tiny") {
t.Fatalf("small document = %q", got)
}
// The write path refuses the same content rather than storing something the
// read path could never return.
base, err := repo.ApprovedHead(ctx)
if err != nil {
t.Fatal(err)
}
if _, err := repo.CreateProposalBranch(ctx, "proposals/1", base.String()); err != nil {
t.Fatal(err)
}
_, err = repo.CommitProposal(ctx, "proposals/1",
[]Write{{Path: "specs/0009.md", Content: doc("SPEC-0009", "Huge", body)}}, meta("too big", 2))
if !errors.Is(err, ErrTooLarge) {
t.Fatalf("oversized CommitProposal = %v, want ErrTooLarge", err)
}
}
func TestWalkBudgetsBoundTotalBytesAndEntries(t *testing.T) {
repo, _ := newSpace(t)
ctx := context.Background()
var writes []Write
for i := 1; i <= 6; i++ {
writes = append(writes, Write{
Path: fmt.Sprintf("notes/%d.md", i),
Content: doc(fmt.Sprintf("NOTE-%04d", i), "Note", strings.Repeat("y", 200)),
})
}
pushApproved(t, repo, ownerMeta("many", 1), writes...)
repo.walkLimit = 300
if _, err := repo.ListDocuments(ctx, DefaultApprovedBranch); !errors.Is(err, ErrTooLarge) {
t.Fatalf("walk over the byte budget = %v, want ErrTooLarge", err)
}
repo.walkLimit = 0
repo.entryLimit = 3
if _, err := repo.ListDocuments(ctx, DefaultApprovedBranch); !errors.Is(err, ErrTooLarge) {
t.Fatalf("walk over the entry budget = %v, want ErrTooLarge", err)
}
}
func TestWalkRefusesADocumentThatIsNotABlob(t *testing.T) {
repo, _ := newSpace(t)
ctx := context.Background()
// Build a tree carrying a symlink at a document path. The update hook is
// what keeps these out, but --push-option=skip-validation means one can
// exist, and quietly skipping it would drop a document out of the index
// with nothing recording that it was ever there.
target, err := repo.writeBlob("link", []byte("specs/0007.md"))
if err != nil {
t.Fatal(err)
}
head, err := repo.ApprovedHead(ctx)
if err != nil {
t.Fatal(err)
}
tree, err := repo.treeOf(head)
if err != nil {
t.Fatal(err)
}
node, err := repo.loadTree(tree, 0)
if err != nil {
t.Fatal(err)
}
node.files["alias.md"] = object.TreeEntry{Name: "alias.md", Mode: filemode.Symlink, Hash: target}
treeHash, err := node.write(repo.repo.Storer)
if err != nil {
t.Fatal(err)
}
commit, err := repo.writeCommit(ownerMeta("symlink", 1), treeHash, []plumbing.Hash{head})
if err != nil {
t.Fatal(err)
}
ref := plumbing.NewBranchReferenceName(repo.ApprovedBranch())
if err := repo.repo.Storer.SetReference(plumbing.NewHashReference(ref, commit)); err != nil {
t.Fatal(err)
}
if _, err := repo.ListDocuments(ctx, DefaultApprovedBranch); !errors.Is(err, ErrUnsupportedEntry) {
t.Fatalf("walk over a symlinked document = %v, want ErrUnsupportedEntry", err)
}
}
func TestReadHonoursContextCancellation(t *testing.T) {
repo, _ := newSpace(t)
pushApproved(t, repo, ownerMeta("seed", 1),
Write{Path: "specs/0007.md", Content: doc("SPEC-0007", "Storage", "body")})
ctx, cancel := context.WithCancel(context.Background())
cancel()
if _, err := repo.ListDocuments(ctx, DefaultApprovedBranch); !errors.Is(err, context.Canceled) {
t.Fatalf("walk with a cancelled context = %v, want context.Canceled", err)
}
}