package publish import ( "context" "testing" "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/fake" "gitea.maximumdirect.net/eric/distributor/internal/testutil" "gitea.maximumdirect.net/eric/distributor/internal/transform" ) func TestOutputStateProjection(t *testing.T) { sourceOutput := Output{ SourcePath: "report.md", DestinationPath: "report.md", Kind: state.OutputKindSource, URL: "https://reports.example.com/report.md", SHA256: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Size: 123, } sourceState := sourceOutput.StateOutputFile() if sourceState.Path != "report.md" || sourceState.Kind != state.OutputKindSource || sourceState.SourcePath != "report.md" || sourceState.URL != sourceOutput.URL || sourceState.SHA256 != sourceOutput.SHA256 || sourceState.Size != sourceOutput.Size { t.Fatalf("source state output = %#v", sourceState) } generatedOutput := Output{ SourcePath: "report.md", DestinationPath: "report.html", Kind: state.OutputKindGenerated, Transform: transform.MarkdownToHTML, URL: "https://reports.example.com/report.html", SHA256: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", Size: 456, } generatedState := generatedOutput.StateOutputFile() if generatedState.Path != "report.html" || generatedState.Kind != state.OutputKindGenerated || generatedState.SourcePath != "report.md" || generatedState.Transform != transform.MarkdownToHTML || generatedState.URL != generatedOutput.URL || generatedState.SHA256 != generatedOutput.SHA256 || generatedState.Size != generatedOutput.Size { t.Fatalf("generated state output = %#v", generatedState) } } func TestOutputSliceProjections(t *testing.T) { outputs := []Output{ {SourcePath: "report.md", DestinationPath: "report.md", Kind: state.OutputKindSource, SHA256: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Size: 1}, {SourcePath: "report.md", DestinationPath: "report.html", Kind: state.OutputKindGenerated, Transform: transform.MarkdownToHTML, URL: "https://reports.example.com/report.html", SHA256: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", Size: 2}, } stateOutputs := StateOutputFiles(outputs) if len(stateOutputs) != 2 || stateOutputs[1].Path != "report.html" || stateOutputs[1].Transform != transform.MarkdownToHTML || stateOutputs[1].URL != outputs[1].URL { t.Fatalf("state outputs = %#v", stateOutputs) } paths := ManagedOutputPaths(outputs) if len(paths) != 2 || paths[0] != "report.md" || paths[1] != "report.html" { t.Fatalf("managed paths = %#v", paths) } } func TestPlanOutputsRejectsCollision(t *testing.T) { sourceBackend := fake.New() sourceBundle := testutil.WriteFakeSourceBundle(t, sourceBackend, "", testutil.BundleOptions{ Files: []testutil.SourceFile{ {Path: "report.md", Data: "# Report\n"}, {Path: "report.html", Data: "

source html

\n"}, }, }) _, err := PlanOutputs(context.Background(), Request{ SourceBackend: sourceBackend, SourceBundle: sourceBundle, Publish: config.PublishPolicy{Source: true, HTML: true}, Transform: config.Transform{MarkdownToHTML: &config.MarkdownToHTML{Enabled: true, Mode: config.TransformModeSidecar}}, Transformers: testResolver{transform.MarkdownToHTML: testTransformer{outputs: []transform.Output{{ Path: "report.html", SourcePath: "report.md", Transform: transform.MarkdownToHTML, Data: []byte("

Report

\n"), SHA256: bundle.FileDigest([]byte("

Report

\n")), Size: int64(len("

Report

\n")), }}}}, }) if err == nil { t.Fatal("PlanSourceOutputs() error = nil, want collision") } } func TestPlanOutputsRejectsHTMLWithoutMarkdown(t *testing.T) { sourceBackend := fake.New() sourceBundle := testutil.WriteFakeSourceBundle(t, sourceBackend, "", testutil.BundleOptions{ Files: []testutil.SourceFile{{Path: "summary.txt", Data: "Summary\n"}}, }) _, err := PlanOutputs(context.Background(), Request{ SourceBackend: sourceBackend, SourceBundle: sourceBundle, Publish: config.PublishPolicy{HTML: true}, Transform: config.Transform{MarkdownToHTML: &config.MarkdownToHTML{Enabled: true, Mode: config.TransformModeSidecar}}, Transformers: testResolver{ transform.MarkdownToHTML: testTransformer{}, }, }) if err == nil { t.Fatal("PlanOutputs() error = nil, want no markdown failure") } } func TestPlanOutputsRejectsHTMLWithoutTransformerResolver(t *testing.T) { _, err := PlanOutputs(context.Background(), Request{ Publish: config.PublishPolicy{HTML: true}, Transform: config.Transform{MarkdownToHTML: &config.MarkdownToHTML{Enabled: true, Mode: config.TransformModeSidecar}}, }) if err == nil { t.Fatal("PlanOutputs() error = nil, want resolver failure") } } func TestPlanOutputsRejectsMissingMarkdownTransformer(t *testing.T) { _, err := PlanOutputs(context.Background(), Request{ Publish: config.PublishPolicy{HTML: true}, Transform: config.Transform{MarkdownToHTML: &config.MarkdownToHTML{Enabled: true, Mode: config.TransformModeSidecar}}, Transformers: testResolver{}, }) if err == nil { t.Fatal("PlanOutputs() error = nil, want missing transformer failure") } } func TestPlanOutputsUsesRegisteredTransformer(t *testing.T) { data := []byte("

Generated

\n") outputs, err := PlanOutputs(context.Background(), Request{ Publish: config.PublishPolicy{HTML: true}, Transform: config.Transform{MarkdownToHTML: &config.MarkdownToHTML{Enabled: true, Mode: config.TransformModeSidecar}}, 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)), }}}}, }) if err != nil { t.Fatalf("PlanOutputs() error = %v", err) } if got, want := len(outputs), 1; got != want { t.Fatalf("output count = %d, want %d", got, want) } if outputs[0].DestinationPath != "report.html" || string(outputs[0].Data) != string(data) { t.Fatalf("output = %#v", outputs[0]) } } func TestPlanOutputsPassesMarkdownOptions(t *testing.T) { data := []byte("

Generated

\n") transformer := &recordingTransformer{outputs: []transform.Output{{ Path: "index.html", SourcePath: "report.md", Transform: transform.MarkdownToHTML, Data: data, SHA256: bundle.FileDigest(data), Size: int64(len(data)), }}} _, err := PlanOutputs(context.Background(), Request{ Publish: config.PublishPolicy{HTML: true}, Transform: config.Transform{MarkdownToHTML: &config.MarkdownToHTML{ Enabled: true, Mode: config.TransformModeIndex, Input: "report.md", }}, Transformers: testResolver{transform.MarkdownToHTML: transformer}, }) if err != nil { t.Fatalf("PlanOutputs() error = %v", err) } if transformer.request.Markdown.Mode != config.TransformModeIndex || transformer.request.Markdown.Input != "report.md" { t.Fatalf("markdown options = %#v, want index/report.md", transformer.request.Markdown) } } func TestBuildRejectsHTMLWithoutTransform(t *testing.T) { sourceBackend := fake.New() destinationBackend := fake.New() sourceBundle := testutil.WriteFakeSourceBundle(t, sourceBackend, "", testutil.BundleOptions{ Files: []testutil.SourceFile{{Path: "report.md", Data: "# Report\n"}}, }) _, err := Build(context.Background(), Request{ PipelineID: "reports", DestinationID: "archive", SourceBackend: sourceBackend, DestinationBackend: destinationBackend, DestinationBundlePath: "", SourceBundle: sourceBundle, Publish: config.PublishPolicy{HTML: true}, Transfer: config.TransferPolicy{ OnDestinationSame: config.TransferActionSkip, OnDestinationOlder: config.TransferActionReplace, OnDestinationNewer: config.TransferActionSkip, OnConflict: config.TransferActionFail, }, }) if err == nil { t.Fatal("Build() error = nil, want missing transform error") } } func TestValidateRequestChecksPublishTransformPolicy(t *testing.T) { tests := []struct { name string publish config.PublishPolicy transform config.Transform wantErr bool }{ { name: "source only allowed", publish: config.PublishPolicy{Source: true}, }, { name: "html only sidecar allowed", publish: config.PublishPolicy{HTML: true}, transform: config.Transform{MarkdownToHTML: &config.MarkdownToHTML{ Enabled: true, Mode: config.TransformModeSidecar, }}, }, { name: "html only index allowed", publish: config.PublishPolicy{HTML: true}, transform: config.Transform{MarkdownToHTML: &config.MarkdownToHTML{ Enabled: true, Mode: config.TransformModeIndex, }}, }, { name: "source and html sidecar allowed", publish: config.PublishPolicy{Source: true, HTML: true}, transform: config.Transform{MarkdownToHTML: &config.MarkdownToHTML{ Enabled: true, Mode: config.TransformModeSidecar, }}, }, { name: "no outputs rejected", publish: config.PublishPolicy{}, wantErr: true, }, { name: "html without transform rejected", publish: config.PublishPolicy{HTML: true}, wantErr: true, }, { name: "html with disabled transform rejected", publish: config.PublishPolicy{HTML: true}, transform: config.Transform{MarkdownToHTML: &config.MarkdownToHTML{ Enabled: false, Mode: config.TransformModeSidecar, }}, wantErr: true, }, { name: "html with wrong mode rejected", publish: config.PublishPolicy{HTML: true}, transform: config.Transform{MarkdownToHTML: &config.MarkdownToHTML{ Enabled: true, Mode: "inline", }}, wantErr: true, }, { name: "source only enabled transform rejected", publish: config.PublishPolicy{Source: true}, transform: config.Transform{MarkdownToHTML: &config.MarkdownToHTML{ Enabled: true, Mode: config.TransformModeSidecar, }}, wantErr: true, }, { name: "enabled markdown wrong mode rejected", publish: config.PublishPolicy{Source: true}, transform: config.Transform{MarkdownToHTML: &config.MarkdownToHTML{ Enabled: true, Mode: "inline", }}, wantErr: true, }, { name: "disabled markdown empty mode allowed", publish: config.PublishPolicy{Source: true}, transform: config.Transform{MarkdownToHTML: &config.MarkdownToHTML{ Enabled: false, }}, }, { name: "disabled markdown sidecar mode allowed", publish: config.PublishPolicy{Source: true}, transform: config.Transform{MarkdownToHTML: &config.MarkdownToHTML{ Enabled: false, Mode: config.TransformModeSidecar, }}, }, { name: "disabled markdown wrong mode rejected", publish: config.PublishPolicy{Source: true}, transform: config.Transform{MarkdownToHTML: &config.MarkdownToHTML{ Enabled: false, Mode: "inline", }}, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := validateRequest(Request{ PipelineID: "reports", DestinationID: "archive", SourceBackend: fake.New(), DestinationBackend: fake.New(), Publish: tt.publish, Transform: tt.transform, }) if tt.wantErr && err == nil { t.Fatal("validateRequest() error = nil, want error") } if !tt.wantErr && err != nil { t.Fatalf("validateRequest() error = %v", err) } }) } } type testResolver map[string]transform.Transformer func (r testResolver) Get(name string) (transform.Transformer, bool) { transformer, ok := r[name] return transformer, ok } type testTransformer struct { outputs []transform.Output err error } func (t testTransformer) Generate(context.Context, transform.Request) ([]transform.Output, error) { return t.outputs, t.err } type recordingTransformer struct { outputs []transform.Output request transform.Request err error } func (t *recordingTransformer) Generate(_ context.Context, req transform.Request) ([]transform.Output, error) { t.request = req return t.outputs, t.err }