364 lines
11 KiB
Go
364 lines
11 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 TestBuildManifestRejectsUnsafePath(t *testing.T) {
|
|
root := t.TempDir()
|
|
writeFile(t, root, "report.txt", "report")
|
|
|
|
_, err := BuildManifest(BuildOptions{
|
|
Root: root,
|
|
ID: "reports.unsafe",
|
|
Files: []string{"../report.txt"},
|
|
})
|
|
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", "nested/../report.md", `nested\report.md`, ManifestName, 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)
|
|
}
|
|
}
|