Files
distributor/internal/publish/plan_test.go

625 lines
23 KiB
Go

package publish
import (
"context"
"encoding/json"
"strings"
"testing"
"time"
"gitea.maximumdirect.net/eric/distributor/internal/bundle"
"gitea.maximumdirect.net/eric/distributor/internal/config"
"gitea.maximumdirect.net/eric/distributor/internal/state"
"gitea.maximumdirect.net/eric/distributor/internal/storage"
"gitea.maximumdirect.net/eric/distributor/internal/storage/fake"
"gitea.maximumdirect.net/eric/distributor/internal/testutil"
"gitea.maximumdirect.net/eric/distributor/internal/transform"
)
var (
planCreatedAt = time.Date(2026, 5, 30, 11, 12, 0, 0, time.UTC)
planUpdatedAt = time.Date(2026, 6, 1, 9, 30, 0, 0, time.UTC)
)
func TestBuildAdditivePublishesCatalogOutputs(t *testing.T) {
_, destinationBackend, req := catalogPlanRequest(t, config.WorkflowAdditive)
plan, err := Build(context.Background(), req)
if err != nil {
t.Fatalf("Build() error = %v", err)
}
if plan.Action != ActionPublishNew || plan.Workflow != config.WorkflowAdditive {
t.Fatalf("plan action=%s workflow=%s, want publish_new additive", plan.Action, plan.Workflow)
}
if len(plan.CatalogOutputsToWrite) != 2 || len(plan.CatalogOutputsToRetain) != 0 || len(plan.CatalogOutputsToDelete) != 0 {
t.Fatalf("catalog write=%d retain=%d delete=%d", len(plan.CatalogOutputsToWrite), len(plan.CatalogOutputsToRetain), len(plan.CatalogOutputsToDelete))
}
for _, output := range plan.CatalogOutputsToWrite {
if output.PipelineID != "reports" || output.DestinationID != "archive" {
t.Fatalf("catalog output owner = %s/%s", output.PipelineID, output.DestinationID)
}
if output.Source.ID != req.SourceBundle.Manifest.ID || output.Source.Digest != req.SourceBundle.Manifest.Digest || !output.Source.Created.Equal(req.SourceBundle.Manifest.Created) {
t.Fatalf("catalog output source = %#v", output.Source)
}
if output.Kind == state.OutputKindSource && output.SourcePath != "" {
t.Fatalf("source catalog output source_path = %q, want empty", output.SourcePath)
}
if !output.CreatedAt.Equal(planUpdatedAt) || !output.UpdatedAt.Equal(planUpdatedAt) {
t.Fatalf("catalog output times = %s/%s", output.CreatedAt, output.UpdatedAt)
}
}
if _, err := destinationBackend.Stat(context.Background(), storage.StateFileName); !storage.IsNotFound(err) {
t.Fatalf("destination state stat error = %v, want missing", err)
}
}
func TestBuildAdditiveOverwritesManagedAndRetainsOthers(t *testing.T) {
_, destinationBackend, req := catalogPlanRequest(t, config.WorkflowAdditive)
existing := baseCatalog(req)
existing.Outputs = []state.CatalogOutputFile{
catalogOutput(req, "reports", "archive", "report.md", state.OutputKindSource, planCreatedAt),
catalogOutput(req, "reports", "web", "old.txt", state.OutputKindSource, planCreatedAt),
}
writeCatalogState(t, destinationBackend, "", existing)
testutil.WriteFakeFile(t, destinationBackend, "report.md", "old")
plan, err := Build(context.Background(), req)
if err != nil {
t.Fatalf("Build() error = %v", err)
}
if plan.Action != ActionUpsertAdditive {
t.Fatalf("plan action = %s, want %s", plan.Action, ActionUpsertAdditive)
}
if len(plan.CatalogOutputsToDelete) != 0 {
t.Fatalf("delete outputs = %#v, want none", plan.CatalogOutputsToDelete)
}
if len(plan.CatalogOutputsToRetain) != 1 || plan.CatalogOutputsToRetain[0].Path != "old.txt" {
t.Fatalf("retained outputs = %#v, want old.txt", plan.CatalogOutputsToRetain)
}
written, ok := state.FindCatalogOutputByPath(plan.CatalogOutputsToWrite, "report.md")
if !ok {
t.Fatalf("written outputs = %#v, want report.md", plan.CatalogOutputsToWrite)
}
if !written.CreatedAt.Equal(planCreatedAt) || !written.UpdatedAt.Equal(planUpdatedAt) {
t.Fatalf("report.md times = %s/%s, want created preserved and updated now", written.CreatedAt, written.UpdatedAt)
}
}
func TestBuildReplacementDeletesCurrentOwnerAndRetainsOtherOwners(t *testing.T) {
_, destinationBackend, req := catalogPlanRequest(t, config.WorkflowReplacement)
existing := baseCatalog(req)
existing.Outputs = []state.CatalogOutputFile{
catalogOutput(req, "reports", "archive", "report.md", state.OutputKindSource, planCreatedAt),
catalogOutput(req, "reports", "archive", "stale.txt", state.OutputKindSource, planCreatedAt),
catalogOutput(req, "reports", "web", "shared.txt", state.OutputKindSource, planCreatedAt),
}
writeCatalogState(t, destinationBackend, "", existing)
plan, err := Build(context.Background(), req)
if err != nil {
t.Fatalf("Build() error = %v", err)
}
if plan.Action != ActionReplaceCatalog {
t.Fatalf("plan action = %s, want %s", plan.Action, ActionReplaceCatalog)
}
if len(plan.CatalogOutputsToDelete) != 1 || plan.CatalogOutputsToDelete[0].Path != "stale.txt" {
t.Fatalf("delete outputs = %#v, want stale.txt", plan.CatalogOutputsToDelete)
}
if len(plan.CatalogOutputsToRetain) != 1 || plan.CatalogOutputsToRetain[0].Path != "shared.txt" {
t.Fatalf("retained outputs = %#v, want shared.txt", plan.CatalogOutputsToRetain)
}
}
func TestBuildAdditiveSkipsMatchingCatalogOutputs(t *testing.T) {
_, destinationBackend, req := catalogPlanRequest(t, config.WorkflowAdditive)
existing := matchingCatalogForRequest(t, req)
existing.Outputs = append(existing.Outputs, catalogOutput(req, "reports", "web", "old.txt", state.OutputKindSource, planCreatedAt))
writeCatalogState(t, destinationBackend, "", existing)
plan, err := Build(context.Background(), req)
if err != nil {
t.Fatalf("Build() error = %v", err)
}
if plan.Action != ActionSkipSame {
t.Fatalf("plan action = %s, want %s", plan.Action, ActionSkipSame)
}
if len(plan.CatalogOutputsToWrite) != 0 || len(plan.CatalogOutputsToDelete) != 0 {
t.Fatalf("catalog write=%d delete=%d, want no writes or deletes", len(plan.CatalogOutputsToWrite), len(plan.CatalogOutputsToDelete))
}
if len(plan.CatalogOutputsToRetain) != 1 || plan.CatalogOutputsToRetain[0].Path != "old.txt" {
t.Fatalf("retained outputs = %#v, want old.txt", plan.CatalogOutputsToRetain)
}
}
func TestBuildReplacementSkipsMatchingCatalogOutputs(t *testing.T) {
_, destinationBackend, req := catalogPlanRequest(t, config.WorkflowReplacement)
existing := matchingCatalogForRequest(t, req)
existing.Outputs = append(existing.Outputs, catalogOutput(req, "reports", "web", "shared.txt", state.OutputKindSource, planCreatedAt))
writeCatalogState(t, destinationBackend, "", existing)
plan, err := Build(context.Background(), req)
if err != nil {
t.Fatalf("Build() error = %v", err)
}
if plan.Action != ActionSkipSame {
t.Fatalf("plan action = %s, want %s", plan.Action, ActionSkipSame)
}
if len(plan.CatalogOutputsToWrite) != 0 || len(plan.CatalogOutputsToDelete) != 0 {
t.Fatalf("catalog write=%d delete=%d, want no writes or deletes", len(plan.CatalogOutputsToWrite), len(plan.CatalogOutputsToDelete))
}
if len(plan.CatalogOutputsToRetain) != 1 || plan.CatalogOutputsToRetain[0].Path != "shared.txt" {
t.Fatalf("retained outputs = %#v, want shared.txt", plan.CatalogOutputsToRetain)
}
}
func TestBuildReplacementDoesNotSkipWhenCurrentOwnerOutputWouldBeDeleted(t *testing.T) {
_, destinationBackend, req := catalogPlanRequest(t, config.WorkflowReplacement)
existing := matchingCatalogForRequest(t, req)
existing.Outputs = append(existing.Outputs, catalogOutput(req, "reports", "archive", "stale.txt", state.OutputKindSource, planCreatedAt))
writeCatalogState(t, destinationBackend, "", existing)
plan, err := Build(context.Background(), req)
if err != nil {
t.Fatalf("Build() error = %v", err)
}
if plan.Action != ActionReplaceCatalog {
t.Fatalf("plan action = %s, want %s", plan.Action, ActionReplaceCatalog)
}
if len(plan.CatalogOutputsToDelete) != 1 || plan.CatalogOutputsToDelete[0].Path != "stale.txt" {
t.Fatalf("delete outputs = %#v, want stale.txt", plan.CatalogOutputsToDelete)
}
}
func TestBuildDoesNotSkipWhenCatalogMetadataDiffers(t *testing.T) {
tests := []struct {
name string
configure func(*Request)
mutate func(*state.CatalogState)
}{
{
name: "pipeline id",
mutate: func(catalog *state.CatalogState) {
catalog.Outputs[0].PipelineID = "other"
},
},
{
name: "destination id",
mutate: func(catalog *state.CatalogState) {
catalog.Outputs[0].DestinationID = "web"
},
},
{
name: "source id",
mutate: func(catalog *state.CatalogState) {
catalog.Outputs[0].Source.ID = "reports.other"
},
},
{
name: "source digest",
mutate: func(catalog *state.CatalogState) {
catalog.Outputs[0].Source.Digest = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
},
{
name: "source created",
mutate: func(catalog *state.CatalogState) {
catalog.Outputs[0].Source.Created = catalog.Outputs[0].Source.Created.Add(time.Second)
},
},
{
name: "output kind",
mutate: func(catalog *state.CatalogState) {
catalog.Outputs[0].Kind = state.OutputKindGenerated
catalog.Outputs[0].SourcePath = "report.md"
catalog.Outputs[0].Transform = transform.MarkdownToHTML
},
},
{
name: "output digest",
mutate: func(catalog *state.CatalogState) {
catalog.Outputs[0].SHA256 = "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
},
},
{
name: "output size",
mutate: func(catalog *state.CatalogState) {
catalog.Outputs[0].Size++
},
},
{
name: "url",
configure: func(req *Request) {
req.Links = &config.Links{BaseURL: "https://reports.example.com/archive", Primary: config.LinkPrimaryAuto}
},
mutate: func(catalog *state.CatalogState) {
catalog.Outputs[0].URL = "https://reports.example.com/archive/old-report.md"
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, destinationBackend, req := catalogPlanRequest(t, config.WorkflowAdditive)
if tt.configure != nil {
tt.configure(&req)
}
existing := matchingCatalogForRequest(t, req)
tt.mutate(&existing)
writeCatalogState(t, destinationBackend, "", existing)
plan, err := Build(context.Background(), req)
if err != nil {
t.Fatalf("Build() error = %v", err)
}
if plan.Action == ActionSkipSame {
t.Fatalf("plan action = %s, want write action after metadata change", plan.Action)
}
})
}
}
func TestBuildDoesNotSkipWhenGeneratedCatalogMetadataDiffers(t *testing.T) {
tests := []struct {
name string
mutate func(*state.CatalogState)
}{
{
name: "source path",
mutate: func(catalog *state.CatalogState) {
catalog.Outputs[0].SourcePath = "summary.md"
},
},
{
name: "transform",
mutate: func(catalog *state.CatalogState) {
catalog.Outputs[0].Transform = "markdown_to_html_index"
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, destinationBackend, req := catalogGeneratedPlanRequest(t, config.WorkflowAdditive)
existing := matchingCatalogForRequest(t, req)
tt.mutate(&existing)
writeCatalogState(t, destinationBackend, "", existing)
plan, err := Build(context.Background(), req)
if err != nil {
t.Fatalf("Build() error = %v", err)
}
if plan.Action == ActionSkipSame {
t.Fatalf("plan action = %s, want write action after generated metadata change", plan.Action)
}
})
}
}
func TestBuildTransfersManagedPathOwnership(t *testing.T) {
_, destinationBackend, req := catalogPlanRequest(t, config.WorkflowReplacement)
existing := baseCatalog(req)
existing.Outputs = []state.CatalogOutputFile{
catalogOutput(req, "reports", "web", "report.md", state.OutputKindSource, planCreatedAt),
}
writeCatalogState(t, destinationBackend, "", existing)
plan, err := Build(context.Background(), req)
if err != nil {
t.Fatalf("Build() error = %v", err)
}
written, ok := state.FindCatalogOutputByPath(plan.CatalogOutputsToWrite, "report.md")
if !ok {
t.Fatalf("written outputs = %#v, want report.md", plan.CatalogOutputsToWrite)
}
if written.PipelineID != "reports" || written.DestinationID != "archive" {
t.Fatalf("written owner = %s/%s, want reports/archive", written.PipelineID, written.DestinationID)
}
if !written.CreatedAt.Equal(planCreatedAt) || !written.UpdatedAt.Equal(planUpdatedAt) {
t.Fatalf("written times = %s/%s", written.CreatedAt, written.UpdatedAt)
}
if len(plan.CatalogOutputsToRetain) != 0 || len(plan.CatalogOutputsToDelete) != 0 {
t.Fatalf("retain=%#v delete=%#v, want no old record for overwritten path", plan.CatalogOutputsToRetain, plan.CatalogOutputsToDelete)
}
}
func TestBuildRejectsUnmanagedPlannedPathCollision(t *testing.T) {
_, destinationBackend, req := catalogPlanRequest(t, config.WorkflowAdditive)
writeCatalogState(t, destinationBackend, "", baseCatalog(req))
testutil.WriteFakeFile(t, destinationBackend, "report.md", "unmanaged")
plan, err := Build(context.Background(), req)
if err == nil {
t.Fatal("Build() error = nil, want unmanaged collision")
}
if plan.Action != ActionFailUnmanaged || !strings.Contains(err.Error(), "not managed by catalog state") {
t.Fatalf("plan action=%s error=%v, want unmanaged catalog collision", plan.Action, err)
}
}
func TestBuildForceReplacesUnmanagedPlannedPathCollision(t *testing.T) {
_, destinationBackend, req := catalogPlanRequest(t, config.WorkflowAdditive)
req.Force = true
existing := baseCatalog(req)
writeCatalogState(t, destinationBackend, "", existing)
testutil.WriteFakeFile(t, destinationBackend, "report.md", "unmanaged")
plan, err := Build(context.Background(), req)
if err != nil {
t.Fatalf("Build() error = %v", err)
}
if plan.Action != ActionForceReplace || !plan.Force || !plan.ClearDestinationRoot {
t.Fatalf("plan action=%s force=%t clear=%t, want forced clear", plan.Action, plan.Force, plan.ClearDestinationRoot)
}
if len(plan.CatalogOutputsToWrite) != 2 || len(plan.CatalogOutputsToRetain) != 0 || len(plan.CatalogOutputsToDelete) != 0 {
t.Fatalf("catalog write=%d retain=%d delete=%d", len(plan.CatalogOutputsToWrite), len(plan.CatalogOutputsToRetain), len(plan.CatalogOutputsToDelete))
}
}
func TestBuildRejectsNoStateNonEmptyDestinationWithoutForce(t *testing.T) {
_, destinationBackend, req := catalogPlanRequest(t, config.WorkflowAdditive)
testutil.WriteFakeFile(t, destinationBackend, "unplanned.txt", "unmanaged")
plan, err := Build(context.Background(), req)
if err == nil {
t.Fatal("Build() error = nil, want unmanaged destination")
}
if plan.Action != ActionFailUnmanaged || !strings.Contains(err.Error(), "destination has content but no distributor state") {
t.Fatalf("plan action=%s error=%v, want unmanaged destination", plan.Action, err)
}
}
func TestBuildForceReplacesNoStateNonEmptyDestination(t *testing.T) {
_, destinationBackend, req := catalogPlanRequest(t, config.WorkflowAdditive)
req.Force = true
testutil.WriteFakeFile(t, destinationBackend, "unplanned.txt", "unmanaged")
plan, err := Build(context.Background(), req)
if err != nil {
t.Fatalf("Build() error = %v", err)
}
if plan.Action != ActionForceReplace || !plan.Force || !plan.ClearDestinationRoot {
t.Fatalf("plan action=%s force=%t clear=%t, want forced clear", plan.Action, plan.Force, plan.ClearDestinationRoot)
}
if len(plan.CatalogOutputsToWrite) != 2 || len(plan.CatalogOutputsToRetain) != 0 || len(plan.CatalogOutputsToDelete) != 0 {
t.Fatalf("catalog write=%d retain=%d delete=%d", len(plan.CatalogOutputsToWrite), len(plan.CatalogOutputsToRetain), len(plan.CatalogOutputsToDelete))
}
}
func TestBuildPlansSupersededLegacyAdditive(t *testing.T) {
_, destinationBackend, req := catalogPlanRequest(t, config.WorkflowAdditive)
legacyState := legacyStateDocument(2)
writeJSONState(t, destinationBackend, "", legacyState)
testutil.WriteFakeFile(t, destinationBackend, "report.md", "legacy")
plan, err := Build(context.Background(), req)
if err != nil {
t.Fatalf("Build() error = %v", err)
}
if plan.SupersededLegacy == nil || plan.SupersededLegacy.SchemaVersion != 2 {
t.Fatalf("superseded legacy = %#v", plan.SupersededLegacy)
}
if plan.Action != ActionUpsertAdditive || plan.ClearDestinationRoot {
t.Fatalf("plan action=%s clear=%t, want additive overwrite without clear", plan.Action, plan.ClearDestinationRoot)
}
}
func TestBuildPlansSupersededLegacyReplacementClear(t *testing.T) {
_, destinationBackend, req := catalogPlanRequest(t, config.WorkflowReplacement)
legacyState := legacyStateDocument(2)
writeJSONState(t, destinationBackend, "", legacyState)
plan, err := Build(context.Background(), req)
if err != nil {
t.Fatalf("Build() error = %v", err)
}
if plan.Action != ActionReplaceCatalog || !plan.ClearDestinationRoot {
t.Fatalf("plan action=%s clear=%t, want replacement clear", plan.Action, plan.ClearDestinationRoot)
}
}
func TestBuildRejectsInvalidOrFutureState(t *testing.T) {
tests := []struct {
name string
data string
}{
{name: "invalid json", data: `{"schema_version":`},
{name: "future schema", data: `{"schema_version":99}`},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, destinationBackend, req := catalogPlanRequest(t, config.WorkflowAdditive)
testutil.WriteFakeFile(t, destinationBackend, storage.StateFileName, tt.data)
plan, err := Build(context.Background(), req)
if err == nil {
t.Fatal("Build() error = nil, want conflict")
}
if plan.Action != ActionFailConflict {
t.Fatalf("plan action = %s, want %s", plan.Action, ActionFailConflict)
}
})
}
}
func TestBuildForceReplacesInvalidOrFutureState(t *testing.T) {
tests := []struct {
name string
data string
}{
{name: "invalid json", data: `{"schema_version":`},
{name: "future schema", data: `{"schema_version":99}`},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, destinationBackend, req := catalogPlanRequest(t, config.WorkflowAdditive)
req.Force = true
testutil.WriteFakeFile(t, destinationBackend, storage.StateFileName, tt.data)
plan, err := Build(context.Background(), req)
if err != nil {
t.Fatalf("Build() error = %v", err)
}
if plan.Action != ActionForceReplace || !plan.Force || !plan.ClearDestinationRoot {
t.Fatalf("plan action=%s force=%t clear=%t, want forced clear", plan.Action, plan.Force, plan.ClearDestinationRoot)
}
if len(plan.CatalogOutputsToWrite) != 2 || len(plan.CatalogOutputsToRetain) != 0 || len(plan.CatalogOutputsToDelete) != 0 {
t.Fatalf("catalog write=%d retain=%d delete=%d", len(plan.CatalogOutputsToWrite), len(plan.CatalogOutputsToRetain), len(plan.CatalogOutputsToDelete))
}
})
}
}
func TestBuildForceDoesNotChangeValidCatalogActions(t *testing.T) {
tests := []struct {
name string
workflow string
want Action
}{
{name: "additive", workflow: config.WorkflowAdditive, want: ActionUpsertAdditive},
{name: "replacement", workflow: config.WorkflowReplacement, want: ActionReplaceCatalog},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, destinationBackend, req := catalogPlanRequest(t, tt.workflow)
req.Force = true
writeCatalogState(t, destinationBackend, "", baseCatalog(req))
plan, err := Build(context.Background(), req)
if err != nil {
t.Fatalf("Build() error = %v", err)
}
if plan.Action != tt.want || plan.Force || plan.ClearDestinationRoot {
t.Fatalf("plan action=%s force=%t clear=%t, want %s without forced clear", plan.Action, plan.Force, plan.ClearDestinationRoot, tt.want)
}
})
}
}
func TestValidateRequestRejectsInvalidWorkflow(t *testing.T) {
sourceBackend, destinationBackend, req := catalogPlanRequest(t, "append")
req.SourceBackend = sourceBackend
req.DestinationBackend = destinationBackend
err := validateRequest(req)
if err == nil {
t.Fatal("validateRequest() error = nil, want invalid workflow")
}
if !strings.Contains(err.Error(), "destination.workflow") {
t.Fatalf("validateRequest() error = %v, want workflow context", err)
}
}
func catalogPlanRequest(t *testing.T, workflow string) (*fake.Backend, *fake.Backend, Request) {
t.Helper()
sourceBackend := fake.New()
destinationBackend := fake.New()
sourceBundle := testutil.WriteFakeSourceBundle(t, sourceBackend, "", testutil.BundleOptions{})
return sourceBackend, destinationBackend, Request{
PipelineID: "reports",
DestinationID: "archive",
SourceBundle: sourceBundle,
SourceBackend: sourceBackend,
DestinationBackend: destinationBackend,
DestinationBundlePath: "",
PathMapping: config.PathMappingPreserveRelative,
Publish: config.PublishPolicy{Source: true},
Workflow: workflow,
DistributorVersion: "test",
Now: planUpdatedAt,
}
}
func catalogGeneratedPlanRequest(t *testing.T, workflow string) (*fake.Backend, *fake.Backend, Request) {
t.Helper()
sourceBackend, destinationBackend, req := catalogPlanRequest(t, workflow)
data := []byte("<p>Generated</p>\n")
req.Publish = config.PublishPolicy{HTML: true}
req.Transform = config.Transform{MarkdownToHTML: &config.MarkdownToHTML{
Enabled: true,
Mode: config.TransformModeSidecar,
}}
req.Transformers = testResolver{transform.MarkdownToHTML: testTransformer{outputs: []transform.Output{{
Path: "report.html",
SourcePath: "report.md",
Transform: transform.MarkdownToHTML,
Data: data,
SHA256: bundle.FileDigest(data),
Size: int64(len(data)),
}}}}
return sourceBackend, destinationBackend, req
}
func matchingCatalogForRequest(t *testing.T, req Request) state.CatalogState {
t.Helper()
outputs, err := PlanOutputs(context.Background(), req)
if err != nil {
t.Fatalf("PlanOutputs() error = %v", err)
}
outputs, _, err = PlanLinks(req, outputs)
if err != nil {
t.Fatalf("PlanLinks() error = %v", err)
}
catalog := baseCatalog(req)
catalog.Outputs = catalogOutputsForPlan(req, outputs, nil, state.CurrentOwnerScope(req.PipelineID, req.DestinationID), planCreatedAt)
return catalog
}
func baseCatalog(req Request) state.CatalogState {
return state.CatalogState{
SchemaVersion: state.CatalogSchemaVersion,
DistributorVersion: "previous",
CreatedAt: planCreatedAt,
UpdatedAt: planCreatedAt,
State: state.StatePolicy{Mode: state.StateModeCatalog},
Outputs: []state.CatalogOutputFile{},
}
}
func catalogOutput(req Request, pipelineID, destinationID, path, kind string, createdAt time.Time) state.CatalogOutputFile {
sourceSHA := req.SourceBundle.Manifest.Files[0].SHA256
sourceSize := req.SourceBundle.Manifest.Files[0].Size
for _, file := range req.SourceBundle.Manifest.Files {
if file.Path == path {
sourceSHA = file.SHA256
sourceSize = file.Size
break
}
}
output := state.CatalogOutputFile{
Path: path,
PipelineID: pipelineID,
DestinationID: destinationID,
Source: state.CatalogSourceIdentity{
ID: req.SourceBundle.Manifest.ID,
Digest: req.SourceBundle.Manifest.Digest,
Created: req.SourceBundle.Manifest.Created,
},
Kind: kind,
SHA256: sourceSHA,
Size: sourceSize,
CreatedAt: createdAt,
UpdatedAt: createdAt,
}
return output
}
func writeCatalogState(t *testing.T, backend *fake.Backend, relative string, catalog state.CatalogState) {
t.Helper()
writeJSONState(t, backend, relative, catalog)
}
func legacyStateDocument(schemaVersion int) map[string]int {
return map[string]int{"schema_version": schemaVersion}
}
func writeJSONState(t *testing.T, backend *fake.Backend, relative string, value any) {
t.Helper()
data, err := json.MarshalIndent(value, "", " ")
if err != nil {
t.Fatalf("marshal state: %v", err)
}
statePath, err := storage.StatePath(relative)
if err != nil {
t.Fatalf("state path: %v", err)
}
testutil.WriteFakeFile(t, backend, statePath, string(append(data, '\n')))
}