315 lines
10 KiB
Go
315 lines
10 KiB
Go
package state
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.maximumdirect.net/eric/distributor/internal/bundle"
|
|
)
|
|
|
|
func TestParseValidState(t *testing.T) {
|
|
state, err := Parse([]byte(validStateJSON(t)))
|
|
if err != nil {
|
|
t.Fatalf("Parse() error = %v", err)
|
|
}
|
|
if state.SchemaVersion != SchemaVersion {
|
|
t.Fatalf("schema version = %d, want %d", state.SchemaVersion, SchemaVersion)
|
|
}
|
|
if state.PipelineID != "reports" || state.DestinationID != "archive" {
|
|
t.Fatalf("identity = %q/%q", state.PipelineID, state.DestinationID)
|
|
}
|
|
if got, want := state.PublishedAtString(), "2026-05-30T11:12:00Z"; got != want {
|
|
t.Fatalf("PublishedAtString() = %q, want %q", got, want)
|
|
}
|
|
if got, want := len(state.Outputs), 1; got != want {
|
|
t.Fatalf("output count = %d, want %d", got, want)
|
|
}
|
|
}
|
|
|
|
func TestParseValidStateWithLinks(t *testing.T) {
|
|
body := strings.Replace(validStateJSON(t), `"outputs": [`, `"links": {"primary_url": "https://reports.example.com/archive/report.md"},`+"\n "+`"outputs": [`, 1)
|
|
body = strings.Replace(body, `"source_path": "report.md",`, `"source_path": "report.md",`+"\n "+`"url": "https://reports.example.com/archive/report.md",`, 1)
|
|
|
|
state, err := Parse([]byte(body))
|
|
if err != nil {
|
|
t.Fatalf("Parse() error = %v", err)
|
|
}
|
|
if state.Links == nil || state.Links.PrimaryURL != "https://reports.example.com/archive/report.md" {
|
|
t.Fatalf("links = %#v, want primary URL", state.Links)
|
|
}
|
|
if state.Outputs[0].URL != "https://reports.example.com/archive/report.md" {
|
|
t.Fatalf("output URL = %q", state.Outputs[0].URL)
|
|
}
|
|
}
|
|
|
|
func TestParseNormalizesPublishedAtOffset(t *testing.T) {
|
|
body := strings.Replace(validStateJSON(t), `"published_at": "2026-05-30T11:12:00Z"`, `"published_at": "2026-05-30T13:12:00+02:00"`, 1)
|
|
state, err := Parse([]byte(body))
|
|
if err != nil {
|
|
t.Fatalf("Parse() error = %v", err)
|
|
}
|
|
if got, want := state.PublishedAtString(), "2026-05-30T11:12:00Z"; got != want {
|
|
t.Fatalf("PublishedAtString() = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestParseRejectsMissingFields(t *testing.T) {
|
|
tests := map[string]string{
|
|
"schema_version": `"schema_version"`,
|
|
"pipeline_id": `"pipeline_id"`,
|
|
"destination_id": `"destination_id"`,
|
|
"published_at": `"published_at"`,
|
|
"source": `"source"`,
|
|
"outputs": `"outputs"`,
|
|
}
|
|
for name, field := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
body := strings.Replace(validStateJSON(t), field, `"missing_`+name+`"`, 1)
|
|
_, err := Parse([]byte(body))
|
|
assertStateErrorContains(t, err, "required")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseRejectsInvalidSchemaVersion(t *testing.T) {
|
|
body := strings.Replace(validStateJSON(t), `"schema_version": 1`, `"schema_version": 2`, 1)
|
|
_, err := Parse([]byte(body))
|
|
assertStateErrorContains(t, err, "schema_version must be 1")
|
|
}
|
|
|
|
func TestParseRejectsInvalidEmbeddedManifest(t *testing.T) {
|
|
body := validStateWithManifestJSON(t, strings.Replace(manifestJSON(t), `"schema_version": 1`, `"schema_version": 2`, 1))
|
|
_, err := Parse([]byte(body))
|
|
assertStateErrorContains(t, err, "source.manifest")
|
|
}
|
|
|
|
func TestValidateRejectsInvalidEmbeddedManifest(t *testing.T) {
|
|
tests := map[string]func(bundle.Manifest) bundle.Manifest{
|
|
"schema version": func(manifest bundle.Manifest) bundle.Manifest {
|
|
manifest.SchemaVersion = 2
|
|
return manifest
|
|
},
|
|
"empty id": func(manifest bundle.Manifest) bundle.Manifest {
|
|
manifest.ID = ""
|
|
return manifest
|
|
},
|
|
"bad digest": func(manifest bundle.Manifest) bundle.Manifest {
|
|
manifest.Digest = "SHA256:099b205780d2b050024868399961b05731729a548d5d6329c7b06a6740dd75fe"
|
|
return manifest
|
|
},
|
|
"zero created": func(manifest bundle.Manifest) bundle.Manifest {
|
|
manifest.Created = time.Time{}
|
|
return manifest
|
|
},
|
|
"empty files": func(manifest bundle.Manifest) bundle.Manifest {
|
|
manifest.Files = nil
|
|
manifest.Digest = bundle.BundleDigest(manifest.Files)
|
|
return manifest
|
|
},
|
|
"unsafe path": func(manifest bundle.Manifest) bundle.Manifest {
|
|
manifest.Files[0].Path = "../report.md"
|
|
manifest.Digest = bundle.BundleDigest(manifest.Files)
|
|
return manifest
|
|
},
|
|
"duplicate path": func(manifest bundle.Manifest) bundle.Manifest {
|
|
manifest.Files[1].Path = manifest.Files[0].Path
|
|
manifest.Digest = bundle.BundleDigest(manifest.Files)
|
|
return manifest
|
|
},
|
|
"negative size": func(manifest bundle.Manifest) bundle.Manifest {
|
|
manifest.Files[0].Size = -1
|
|
manifest.Digest = bundle.BundleDigest(manifest.Files)
|
|
return manifest
|
|
},
|
|
"digest mismatch": func(manifest bundle.Manifest) bundle.Manifest {
|
|
manifest.Digest = "sha256:0000000000000000000000000000000000000000000000000000000000000000"
|
|
return manifest
|
|
},
|
|
}
|
|
for name, mutate := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
source := mutate(validManifest(t))
|
|
state := *withState(t, validManifest(t), func(*DistributorState) {})
|
|
state.Source.Manifest = source
|
|
err := Validate(state)
|
|
assertStateErrorContains(t, err, "source.manifest")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseRejectsInvalidOutputMetadata(t *testing.T) {
|
|
source := validManifest(t)
|
|
tests := map[string]func(*DistributorState){
|
|
"unsafe path": func(s *DistributorState) {
|
|
s.Outputs[0].Path = "../report.md"
|
|
},
|
|
"invalid kind": func(s *DistributorState) {
|
|
s.Outputs[0].Kind = "other"
|
|
},
|
|
"invalid source": func(s *DistributorState) {
|
|
s.Outputs[0].SourcePath = "../report.md"
|
|
},
|
|
"generated missing": func(s *DistributorState) {
|
|
s.Outputs[0].Kind = OutputKindGenerated
|
|
},
|
|
"invalid digest": func(s *DistributorState) {
|
|
s.Outputs[0].SHA256 = "SHA256:3640fd37140ee4d2e0e93e78834f232ea67a50e7bc6279203690cc7de1975fa6"
|
|
},
|
|
"negative size": func(s *DistributorState) {
|
|
s.Outputs[0].Size = -1
|
|
},
|
|
"invalid output url": func(s *DistributorState) {
|
|
s.Outputs[0].URL = "file:///tmp/report.md"
|
|
},
|
|
"invalid primary url": func(s *DistributorState) {
|
|
s.Links = &LinkState{PrimaryURL: "file:///tmp/report.md"}
|
|
},
|
|
}
|
|
for name, mutate := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
state := *withState(t, source, mutate)
|
|
err := Validate(state)
|
|
if err == nil {
|
|
t.Fatal("Validate() error = nil, want error")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateReportsURLFieldContext(t *testing.T) {
|
|
source := validManifest(t)
|
|
state := *withState(t, source, func(s *DistributorState) {
|
|
s.Links = &LinkState{PrimaryURL: "https://reports.example.com/archive#top"}
|
|
})
|
|
err := Validate(state)
|
|
assertStateErrorContains(t, err, "state links.primary_url")
|
|
assertStateErrorContains(t, err, "must not include a fragment")
|
|
|
|
state = *withState(t, source, func(s *DistributorState) {
|
|
s.Outputs[0].URL = "https://reports.example.com/archive?preview=1"
|
|
})
|
|
err = Validate(state)
|
|
assertStateErrorContains(t, err, "state outputs[0].url")
|
|
assertStateErrorContains(t, err, "must not include a query string")
|
|
}
|
|
|
|
func TestParseRejectsMalformedPublishedTimestamp(t *testing.T) {
|
|
body := strings.Replace(validStateJSON(t), `"published_at": "2026-05-30T11:12:00Z"`, `"published_at": "May 30"`, 1)
|
|
_, err := Parse([]byte(body))
|
|
assertStateErrorContains(t, err, "published_at must be RFC3339")
|
|
}
|
|
|
|
func TestMarshalNormalizesPublishedAtUTC(t *testing.T) {
|
|
source := validManifest(t)
|
|
state := DistributorState{
|
|
SchemaVersion: SchemaVersion,
|
|
PipelineID: "reports",
|
|
DestinationID: "archive",
|
|
PublishedAt: time.Date(2026, 5, 30, 13, 12, 0, 0, time.FixedZone("offset", 2*60*60)),
|
|
Source: SourceState{Manifest: source},
|
|
Outputs: []OutputFile{{
|
|
Path: "report.md",
|
|
Kind: OutputKindSource,
|
|
SourcePath: "report.md",
|
|
SHA256: source.Files[0].SHA256,
|
|
Size: source.Files[0].Size,
|
|
}},
|
|
}
|
|
data, err := json.Marshal(state)
|
|
if err != nil {
|
|
t.Fatalf("Marshal() error = %v", err)
|
|
}
|
|
if !strings.Contains(string(data), `"published_at":"2026-05-30T11:12:00Z"`) {
|
|
t.Fatalf("json = %s, want UTC RFC3339 published_at", data)
|
|
}
|
|
}
|
|
|
|
func TestMarshalIncludesLinksWhenPresent(t *testing.T) {
|
|
source := validManifest(t)
|
|
state := DistributorState{
|
|
SchemaVersion: SchemaVersion,
|
|
PipelineID: "reports",
|
|
DestinationID: "archive",
|
|
PublishedAt: time.Date(2026, 5, 30, 11, 12, 0, 0, time.UTC),
|
|
Source: SourceState{Manifest: source},
|
|
Links: &LinkState{PrimaryURL: "https://reports.example.com/archive/report.md"},
|
|
Outputs: []OutputFile{{
|
|
Path: "report.md",
|
|
Kind: OutputKindSource,
|
|
SourcePath: "report.md",
|
|
URL: "https://reports.example.com/archive/report.md",
|
|
SHA256: source.Files[0].SHA256,
|
|
Size: source.Files[0].Size,
|
|
}},
|
|
}
|
|
data, err := json.Marshal(state)
|
|
if err != nil {
|
|
t.Fatalf("Marshal() error = %v", err)
|
|
}
|
|
if !strings.Contains(string(data), `"links":{"primary_url":"https://reports.example.com/archive/report.md"}`) {
|
|
t.Fatalf("json = %s, want links primary URL", data)
|
|
}
|
|
if !strings.Contains(string(data), `"url":"https://reports.example.com/archive/report.md"`) {
|
|
t.Fatalf("json = %s, want output URL", data)
|
|
}
|
|
}
|
|
|
|
func validStateJSON(t *testing.T) string {
|
|
t.Helper()
|
|
return validStateWithManifestJSON(t, manifestJSON(t))
|
|
}
|
|
|
|
func validStateWithManifestJSON(t *testing.T, manifest string) string {
|
|
t.Helper()
|
|
return `{
|
|
"schema_version": 1,
|
|
"distributor_version": "dev",
|
|
"pipeline_id": "reports",
|
|
"destination_id": "archive",
|
|
"published_at": "2026-05-30T11:12:00Z",
|
|
"source": {
|
|
"manifest": ` + manifest + `
|
|
},
|
|
"outputs": [
|
|
{
|
|
"path": "report.md",
|
|
"kind": "source",
|
|
"source_path": "report.md",
|
|
"sha256": "sha256:3640fd37140ee4d2e0e93e78834f232ea67a50e7bc6279203690cc7de1975fa6",
|
|
"size": 16
|
|
}
|
|
]
|
|
}`
|
|
}
|
|
|
|
func manifestJSON(t *testing.T) string {
|
|
t.Helper()
|
|
data, err := os.ReadFile("../bundle/testdata/valid_bundle/manifest.json")
|
|
if err != nil {
|
|
t.Fatalf("read manifest fixture: %v", err)
|
|
}
|
|
return string(data)
|
|
}
|
|
|
|
func validManifest(t *testing.T) bundle.Manifest {
|
|
t.Helper()
|
|
manifest, err := bundle.ParseManifest([]byte(manifestJSON(t)))
|
|
if err != nil {
|
|
t.Fatalf("ParseManifest() error = %v", err)
|
|
}
|
|
return manifest
|
|
}
|
|
|
|
func assertStateErrorContains(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)
|
|
}
|
|
}
|