Add source bundle validation and inspection
This commit is contained in:
119
internal/bundle/manifest_test.go
Normal file
119
internal/bundle/manifest_test.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package bundle
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
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": "../report.md"`,
|
||||
`"path": "/report.md"`,
|
||||
`"path": "nested/../report.md"`,
|
||||
`"path": "manifest.json"`,
|
||||
`"path": ".distributor.json"`,
|
||||
}
|
||||
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 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user