package bundle import ( "os" "strings" "testing" "time" "gitea.maximumdirect.net/eric/distributor/internal/storage" ) func TestParseManifestValid(t *testing.T) { data := readFixture(t, "testdata/valid_bundle/manifest.json") manifest, err := ParseManifest(data) if err != nil { t.Fatalf("ParseManifest() error = %v", err) } if manifest.SchemaVersion != 1 { t.Fatalf("schema version = %d, want 1", manifest.SchemaVersion) } if manifest.ID != "weather.daily.brentwood.2026-05-30" { t.Fatalf("id = %q", manifest.ID) } if got, want := len(manifest.Files), 2; got != want { t.Fatalf("file count = %d, want %d", got, want) } } func TestParseManifestRejectsInvalidJSON(t *testing.T) { _, err := ParseManifest([]byte(`{"schema_version":`)) assertErrorContains(t, err, "parse manifest") } func TestParseManifestRejectsMissingRequiredFields(t *testing.T) { tests := map[string]string{ "schema_version": `{"id":"id","digest":"sha256:0000000000000000000000000000000000000000000000000000000000000000","created":"2026-05-30T11:10:00Z","files":[{"path":"report.md","sha256":"sha256:0000000000000000000000000000000000000000000000000000000000000000","size":0}]}`, "id": `{"schema_version":1,"digest":"sha256:0000000000000000000000000000000000000000000000000000000000000000","created":"2026-05-30T11:10:00Z","files":[{"path":"report.md","sha256":"sha256:0000000000000000000000000000000000000000000000000000000000000000","size":0}]}`, "digest": `{"schema_version":1,"id":"id","created":"2026-05-30T11:10:00Z","files":[{"path":"report.md","sha256":"sha256:0000000000000000000000000000000000000000000000000000000000000000","size":0}]}`, "created": `{"schema_version":1,"id":"id","digest":"sha256:0000000000000000000000000000000000000000000000000000000000000000","files":[{"path":"report.md","sha256":"sha256:0000000000000000000000000000000000000000000000000000000000000000","size":0}]}`, "files": `{"schema_version":1,"id":"id","digest":"sha256:0000000000000000000000000000000000000000000000000000000000000000","created":"2026-05-30T11:10:00Z"}`, "file path": `{"schema_version":1,"id":"id","digest":"sha256:0000000000000000000000000000000000000000000000000000000000000000","created":"2026-05-30T11:10:00Z","files":[{"sha256":"sha256:0000000000000000000000000000000000000000000000000000000000000000","size":0}]}`, "file digest": `{"schema_version":1,"id":"id","digest":"sha256:0000000000000000000000000000000000000000000000000000000000000000","created":"2026-05-30T11:10:00Z","files":[{"path":"report.md","size":0}]}`, "file size": `{"schema_version":1,"id":"id","digest":"sha256:0000000000000000000000000000000000000000000000000000000000000000","created":"2026-05-30T11:10:00Z","files":[{"path":"report.md","sha256":"sha256:0000000000000000000000000000000000000000000000000000000000000000"}]}`, } for name, body := range tests { t.Run(name, func(t *testing.T) { _, err := ParseManifest([]byte(body)) assertErrorContains(t, err, "required") }) } } func TestParseManifestRejectsInvalidSchemaVersion(t *testing.T) { data := replaceFixture(t, `"schema_version": 1`, `"schema_version": 2`) _, err := ParseManifest(data) assertErrorContains(t, err, "schema_version must be 1") } func TestParseManifestRejectsInvalidTimestamp(t *testing.T) { data := replaceFixture(t, `"created": "2026-05-30T11:10:00Z"`, `"created": "May 30"`) _, err := ParseManifest(data) assertErrorContains(t, err, "RFC3339") } func TestParseManifestRejectsInvalidDigestFormat(t *testing.T) { data := replaceFixture(t, `"digest": "sha256:099b205780d2b050024868399961b05731729a548d5d6329c7b06a6740dd75fe"`, `"digest": "SHA256:099b205780d2b050024868399961b05731729a548d5d6329c7b06a6740dd75fe"`) _, err := ParseManifest(data) assertErrorContains(t, err, "lowercase") } func TestParseManifestRejectsUnsafeFilePaths(t *testing.T) { tests := []string{ `"path": ""`, `"path": "."`, `"path": "./report.md"`, `"path": "../report.md"`, `"path": "/report.md"`, `"path": "nested/../report.md"`, `"path": "nested/./report.md"`, `"path": "nested//report.md"`, `"path": "nested\\report.md"`, `"path": "manifest.json"`, `"path": "nested/manifest.json"`, `"path": "` + storage.StateFileName + `"`, `"path": "nested/` + storage.StateFileName + `"`, } for _, replacement := range tests { t.Run(replacement, func(t *testing.T) { data := replaceFixture(t, `"path": "report.md"`, replacement) _, err := ParseManifest(data) if err == nil { t.Fatal("ParseManifest() error = nil, want error") } }) } } func TestParseManifestRejectsDuplicatePaths(t *testing.T) { data := replaceFixture(t, `"path": "summary.txt"`, `"path": "report.md"`) _, err := ParseManifest(data) assertErrorContains(t, err, "duplicates") } func TestValidateManifestAcceptsValidFixture(t *testing.T) { manifest := validFixtureManifest(t) if err := ValidateManifest(manifest); err != nil { t.Fatalf("ValidateManifest() error = %v", err) } } func TestValidateManifestRejectsInvalidManifest(t *testing.T) { tests := map[string]func(Manifest) Manifest{ "schema version": func(manifest Manifest) Manifest { manifest.SchemaVersion = 2 return manifest }, "empty id": func(manifest Manifest) Manifest { manifest.ID = "" return manifest }, "bad digest": func(manifest Manifest) Manifest { manifest.Digest = "SHA256:099b205780d2b050024868399961b05731729a548d5d6329c7b06a6740dd75fe" return manifest }, "zero created": func(manifest Manifest) Manifest { manifest.Created = time.Time{} return manifest }, "empty files": func(manifest Manifest) Manifest { manifest.Files = nil manifest.Digest = BundleDigest(manifest.Files) return manifest }, "unsafe path": func(manifest Manifest) Manifest { manifest.Files[0].Path = "../report.md" manifest.Digest = BundleDigest(manifest.Files) return manifest }, "nested manifest path": func(manifest Manifest) Manifest { manifest.Files[0].Path = "nested/manifest.json" manifest.Digest = BundleDigest(manifest.Files) return manifest }, "nested state path": func(manifest Manifest) Manifest { manifest.Files[0].Path = "nested/" + storage.StateFileName manifest.Digest = BundleDigest(manifest.Files) return manifest }, "duplicate path": func(manifest Manifest) Manifest { manifest.Files[1].Path = manifest.Files[0].Path manifest.Digest = BundleDigest(manifest.Files) return manifest }, "negative size": func(manifest Manifest) Manifest { manifest.Files[0].Size = -1 manifest.Digest = BundleDigest(manifest.Files) return manifest }, "digest mismatch": func(manifest Manifest) Manifest { manifest.Digest = "sha256:0000000000000000000000000000000000000000000000000000000000000000" return manifest }, } for name, mutate := range tests { t.Run(name, func(t *testing.T) { err := ValidateManifest(mutate(validFixtureManifest(t))) if err == nil { t.Fatal("ValidateManifest() error = nil, want error") } }) } } func validFixtureManifest(t *testing.T) Manifest { t.Helper() manifest, err := ParseManifest(readFixture(t, "testdata/valid_bundle/manifest.json")) if err != nil { t.Fatalf("ParseManifest() error = %v", err) } return manifest } func readFixture(t *testing.T, path string) []byte { t.Helper() data, err := os.ReadFile(path) if err != nil { t.Fatalf("read fixture %s: %v", path, err) } return data } func replaceFixture(t *testing.T, old, replacement string) []byte { t.Helper() body := string(readFixture(t, "testdata/valid_bundle/manifest.json")) if !strings.Contains(body, old) { t.Fatalf("fixture does not contain %q", old) } return []byte(strings.Replace(body, old, replacement, 1)) } func assertErrorContains(t *testing.T, err error, want string) { t.Helper() if err == nil { t.Fatalf("error = nil, want substring %q", want) } if !strings.Contains(err.Error(), want) { t.Fatalf("error = %q, want substring %q", err.Error(), want) } }