Files
distributor/pkg/bundle/bundle_test.go

394 lines
12 KiB
Go

package bundle
import (
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"time"
)
func TestBuildManifestExplicitFilesPreservesOrder(t *testing.T) {
root := t.TempDir()
writeFile(t, root, "b.txt", "bravo")
writeFile(t, root, "a.txt", "alpha")
before := time.Now().UTC()
manifest, err := BuildManifest(BuildOptions{
Root: root,
ID: "reports.example",
Files: []string{"b.txt", "a.txt"},
})
if err != nil {
t.Fatalf("BuildManifest() error = %v", err)
}
after := time.Now().UTC()
paths := manifestPaths(manifest)
if want := []string{"b.txt", "a.txt"}; !reflect.DeepEqual(paths, want) {
t.Fatalf("paths = %v, want %v", paths, want)
}
if manifest.SchemaVersion != SchemaVersion {
t.Fatalf("schema version = %d, want %d", manifest.SchemaVersion, SchemaVersion)
}
if manifest.ID != "reports.example" {
t.Fatalf("id = %q", manifest.ID)
}
if manifest.Created.Before(before) || manifest.Created.After(after) {
t.Fatalf("created = %s, want between %s and %s", manifest.Created, before, after)
}
if err := ValidateManifest(manifest); err != nil {
t.Fatalf("ValidateManifest() error = %v", err)
}
if err := ValidateBundle(root, manifest); err != nil {
t.Fatalf("ValidateBundle() error = %v", err)
}
}
func TestBuildManifestScanSortsAndFiltersMetadata(t *testing.T) {
root := t.TempDir()
writeFile(t, root, "z.txt", "zulu")
writeFile(t, root, "nested/.hidden", "hidden")
writeFile(t, root, ManifestName, "old manifest")
writeFile(t, root, distributorStateName, "state")
writeFile(t, root, "nested/manifest.json", "nested manifest")
writeFile(t, root, "nested/.distributor.json", "nested state")
created := time.Date(2026, 5, 30, 11, 10, 0, 0, time.UTC)
manifest, err := BuildManifest(BuildOptions{
Root: root,
ID: "reports.scan",
Created: created,
Scan: true,
})
if err != nil {
t.Fatalf("BuildManifest() error = %v", err)
}
if !manifest.Created.Equal(created) {
t.Fatalf("created = %s, want %s", manifest.Created, created)
}
paths := manifestPaths(manifest)
if want := []string{"nested/.hidden", "z.txt"}; !reflect.DeepEqual(paths, want) {
t.Fatalf("paths = %v, want %v", paths, want)
}
}
func TestBuildManifestRequiresOneFileMode(t *testing.T) {
root := t.TempDir()
writeFile(t, root, "report.txt", "report")
tests := []BuildOptions{
{Root: root, ID: "reports.none"},
{Root: root, ID: "reports.both", Files: []string{"report.txt"}, Scan: true},
}
for _, opts := range tests {
if _, err := BuildManifest(opts); err == nil {
t.Fatalf("BuildManifest(%+v) error = nil, want error", opts)
}
}
}
func TestBuildManifestRejectsUnsafeExplicitPaths(t *testing.T) {
root := t.TempDir()
writeFile(t, root, "report.txt", "report")
tests := []string{
"",
"../report.txt",
"/report.txt",
"nested/../report.txt",
"nested/./report.txt",
`nested\report.txt`,
ManifestName,
distributorStateName,
"nested/" + ManifestName,
"nested/" + distributorStateName,
}
for _, sourcePath := range tests {
t.Run(sourcePath, func(t *testing.T) {
_, err := BuildManifest(BuildOptions{
Root: root,
ID: "reports.unsafe",
Files: []string{sourcePath},
})
if err == nil {
t.Fatal("BuildManifest() error = nil, want unsafe path error")
}
})
}
}
func TestBuildManifestRejectsSymlink(t *testing.T) {
root := t.TempDir()
writeFile(t, root, "target.txt", "target")
if err := os.Symlink("target.txt", filepath.Join(root, "link.txt")); err != nil {
t.Skipf("symlink unavailable: %v", err)
}
_, err := BuildManifest(BuildOptions{
Root: root,
ID: "reports.symlink",
Files: []string{"link.txt"},
})
if err == nil || !strings.Contains(err.Error(), "regular file") {
t.Fatalf("BuildManifest() error = %v, want regular file error", err)
}
}
func TestScanRejectsSymlink(t *testing.T) {
root := t.TempDir()
writeFile(t, root, "target.txt", "target")
if err := os.Symlink("target.txt", filepath.Join(root, "link.txt")); err != nil {
t.Skipf("symlink unavailable: %v", err)
}
_, err := BuildManifest(BuildOptions{
Root: root,
ID: "reports.scan.symlink",
Scan: true,
})
if err == nil || !strings.Contains(err.Error(), "regular file") {
t.Fatalf("BuildManifest() error = %v, want regular file error", err)
}
}
func TestDigestFunctionsUseCanonicalFilePayload(t *testing.T) {
files := []ManifestFile{
{Path: "report.md", SHA256: FileDigest([]byte("report")), Size: 6},
{Path: "summary.txt", SHA256: FileDigest([]byte("summary")), Size: 7},
}
payload := CanonicalFilePayload(files)
if want := `[{"path":"report.md","sha256":"sha256:845e91831319e89c4d656bdb80c278ac09a7230d61e5dfd2e1b1fbb436ac8917","size":6},{"path":"summary.txt","sha256":"sha256:761b7ad8ad439b2855fcbb611331c646ef0870b0631247bba3f3025cb6df5a53","size":7}]`; payload != want {
t.Fatalf("payload = %q, want %q", payload, want)
}
if digest := BundleDigest(files); !strings.HasPrefix(digest, "sha256:") || len(digest) != len("sha256:")+64 {
t.Fatalf("BundleDigest() = %q, want sha256 digest", digest)
}
}
func TestParseMarshalLoadAndWriteManifest(t *testing.T) {
root := t.TempDir()
writeFile(t, root, "report.txt", "report")
manifest, err := BuildManifest(BuildOptions{
Root: root,
ID: "reports.json",
Created: time.Date(2026, 5, 30, 11, 10, 0, 0, time.UTC),
Files: []string{"report.txt"},
})
if err != nil {
t.Fatalf("BuildManifest() error = %v", err)
}
data, err := MarshalManifest(manifest)
if err != nil {
t.Fatalf("MarshalManifest() error = %v", err)
}
if !strings.HasSuffix(string(data), "\n") {
t.Fatalf("manifest JSON = %q, want trailing newline", string(data))
}
if !strings.Contains(string(data), `"schema_version": 1`) || !strings.Contains(string(data), `"sha256": "`) {
t.Fatalf("manifest JSON = %q, want fixed manifest fields", string(data))
}
parsed, err := ParseManifest(data)
if err != nil {
t.Fatalf("ParseManifest() error = %v", err)
}
if !reflect.DeepEqual(parsed, manifest) {
t.Fatalf("parsed manifest = %#v, want %#v", parsed, manifest)
}
if err := WriteManifest(root, manifest, WriteManifestOptions{}); err != nil {
t.Fatalf("WriteManifest() error = %v", err)
}
loaded, err := LoadManifest(root)
if err != nil {
t.Fatalf("LoadManifest() error = %v", err)
}
if !reflect.DeepEqual(loaded, manifest) {
t.Fatalf("loaded manifest = %#v, want %#v", loaded, manifest)
}
if err := WriteManifest(root, manifest, WriteManifestOptions{}); err == nil {
t.Fatal("WriteManifest() error = nil, want exists error")
}
manifest.ID = "reports.updated"
manifest.Digest = BundleDigest(manifest.Files)
if err := WriteManifest(root, manifest, WriteManifestOptions{Overwrite: true}); err != nil {
t.Fatalf("WriteManifest(overwrite) error = %v", err)
}
loaded, err = LoadManifest(root)
if err != nil {
t.Fatalf("LoadManifest() after overwrite error = %v", err)
}
if loaded.ID != "reports.updated" {
t.Fatalf("loaded id = %q, want updated", loaded.ID)
}
}
func TestValidateManifestRejectsInvalidManifest(t *testing.T) {
created := time.Date(2026, 5, 30, 11, 10, 0, 0, time.UTC)
fileDigest := FileDigest([]byte("report"))
valid := Manifest{
SchemaVersion: SchemaVersion,
ID: "reports.valid",
Created: created,
Files: []ManifestFile{{Path: "report.txt", SHA256: fileDigest, Size: 6}},
}
valid.Digest = BundleDigest(valid.Files)
tests := map[string]func(Manifest) Manifest{
"schema version": func(manifest Manifest) Manifest {
manifest.SchemaVersion = 2
return manifest
},
"id": func(manifest Manifest) Manifest {
manifest.ID = ""
return manifest
},
"created": func(manifest Manifest) Manifest {
manifest.Created = time.Time{}
return manifest
},
"files": func(manifest Manifest) Manifest {
manifest.Files = nil
manifest.Digest = BundleDigest(manifest.Files)
return manifest
},
"path": func(manifest Manifest) Manifest {
manifest.Files[0].Path = "manifest.json"
manifest.Digest = BundleDigest(manifest.Files)
return manifest
},
"duplicate": func(manifest Manifest) Manifest {
manifest.Files = append(manifest.Files, manifest.Files[0])
manifest.Digest = BundleDigest(manifest.Files)
return manifest
},
"size": func(manifest Manifest) Manifest {
manifest.Files[0].Size = -1
manifest.Digest = BundleDigest(manifest.Files)
return manifest
},
"digest": func(manifest Manifest) Manifest {
manifest.Digest = "sha256:0000000000000000000000000000000000000000000000000000000000000000"
return manifest
},
}
for name, mutate := range tests {
t.Run(name, func(t *testing.T) {
if err := ValidateManifest(mutate(valid)); err == nil {
t.Fatal("ValidateManifest() error = nil, want error")
}
})
}
}
func TestValidateBundleRejectsLocalFileProblems(t *testing.T) {
root := t.TempDir()
writeFile(t, root, "report.txt", "report")
manifest, err := BuildManifest(BuildOptions{
Root: root,
ID: "reports.bundle",
Files: []string{"report.txt"},
})
if err != nil {
t.Fatalf("BuildManifest() error = %v", err)
}
if err := ValidateBundle(root, manifest); err != nil {
t.Fatalf("ValidateBundle() error = %v", err)
}
missing := cloneManifest(manifest)
missing.Files[0].Path = "missing.txt"
missing.Digest = BundleDigest(missing.Files)
if err := ValidateBundle(root, missing); err == nil {
t.Fatal("ValidateBundle() missing error = nil, want error")
}
sizeMismatch := cloneManifest(manifest)
sizeMismatch.Files[0].Size++
sizeMismatch.Digest = BundleDigest(sizeMismatch.Files)
if err := ValidateBundle(root, sizeMismatch); err == nil || !strings.Contains(err.Error(), "size mismatch") {
t.Fatalf("ValidateBundle() size error = %v, want size mismatch", err)
}
digestMismatch := cloneManifest(manifest)
writeFile(t, root, "report.txt", "change")
if err := ValidateBundle(root, digestMismatch); err == nil || !strings.Contains(err.Error(), "sha256 mismatch") {
t.Fatalf("ValidateBundle() digest error = %v, want digest mismatch", err)
}
}
func TestValidateBundleRejectsSymlink(t *testing.T) {
root := t.TempDir()
writeFile(t, root, "target.txt", "target")
if err := os.Symlink("target.txt", filepath.Join(root, "link.txt")); err != nil {
t.Skipf("symlink unavailable: %v", err)
}
file := ManifestFile{Path: "link.txt", SHA256: FileDigest([]byte("target")), Size: 6}
manifest := Manifest{
SchemaVersion: SchemaVersion,
ID: "reports.link",
Created: time.Date(2026, 5, 30, 11, 10, 0, 0, time.UTC),
Files: []ManifestFile{file},
}
manifest.Digest = BundleDigest(manifest.Files)
err := ValidateBundle(root, manifest)
if err == nil || !strings.Contains(err.Error(), "regular file") {
t.Fatalf("ValidateBundle() error = %v, want regular file error", err)
}
}
func TestValidateSourcePath(t *testing.T) {
valid := []string{"report.md", "nested/report.md", ".well-known/report.txt"}
for _, path := range valid {
if err := ValidateSourcePath(path); err != nil {
t.Fatalf("ValidateSourcePath(%q) error = %v", path, err)
}
}
invalid := []string{
"",
".",
"./report.md",
"../report.md",
"/report.md",
"nested/../report.md",
"nested/./report.md",
"nested//report.md",
`nested\report.md`,
ManifestName,
distributorStateName,
"nested/" + ManifestName,
"nested/" + distributorStateName,
}
for _, path := range invalid {
if err := ValidateSourcePath(path); err == nil {
t.Fatalf("ValidateSourcePath(%q) error = nil, want error", path)
}
}
}
func manifestPaths(manifest Manifest) []string {
paths := make([]string, 0, len(manifest.Files))
for _, file := range manifest.Files {
paths = append(paths, file.Path)
}
return paths
}
func cloneManifest(manifest Manifest) Manifest {
manifest.Files = append([]ManifestFile(nil), manifest.Files...)
return manifest
}
func writeFile(t *testing.T, root, relative, body string) {
t.Helper()
path := filepath.Join(root, filepath.FromSlash(relative))
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatalf("mkdir %s: %v", filepath.Dir(path), err)
}
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
t.Fatalf("write %s: %v", path, err)
}
}