210 lines
7.8 KiB
Go
210 lines
7.8 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": ""`,
|
|
`"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)
|
|
}
|
|
}
|