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