Files
distributor/internal/bundle/manifest_test.go

192 lines
7.2 KiB
Go

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": "../report.md"`,
`"path": "/report.md"`,
`"path": "nested/../report.md"`,
`"path": "manifest.json"`,
`"path": "` + 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
},
"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)
}
}