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) } }