diff --git a/docs/internal/publish.md b/docs/internal/publish.md index dfc1d2a..4d69e90 100644 --- a/docs/internal/publish.md +++ b/docs/internal/publish.md @@ -18,7 +18,7 @@ Supported actions are publish new, replace older destination, skip same source, The current implementation publishes source files and Markdown-to-HTML sidecar outputs. Remote backend execution is not implemented. -The package uses `internal/state` for destination comparison and `internal/storage` for IO. It does not parse CLI flags or load config files. +The package uses `internal/state` for destination comparison, `internal/storage` for IO, and the shared `internal/config` publish/transform policy helper for request validation. It does not parse CLI flags or load config files. ## Safety diff --git a/internal/config/validate.go b/internal/config/validate.go index 5f60a5c..d555858 100644 --- a/internal/config/validate.go +++ b/internal/config/validate.go @@ -57,8 +57,7 @@ func Validate(cfg Config) error { } errs = validateBackend(errs, destinationContext, destination.Backend, destination.Path, destination.URI, destination.Endpoint, destination.Bucket) - errs = validatePublishPolicy(errs, destinationContext+".publish", destination.Publish) - errs = validateTransform(errs, destinationContext+".transform", destination.Publish, destination.Transform) + errs = validatePublishTransformPolicy(errs, destinationContext, destination.Publish, destination.Transform) errs = validateTransferPolicy(errs, destinationContext+".transfer", destination.Transfer) } } @@ -104,36 +103,40 @@ func validateValidationPolicy(errs ValidationErrors, context string, policy Vali return errs } -func validatePublishPolicy(errs ValidationErrors, context string, policy *PublishPolicy) ValidationErrors { +func validatePublishTransformPolicy(errs ValidationErrors, context string, policy *PublishPolicy, transform Transform) ValidationErrors { if policy == nil { - errs = append(errs, context+" is required") + errs = append(errs, context+".publish is required") return errs } - if !policy.Source && !policy.HTML { - errs = append(errs, context+" must enable source or html") + if err := ValidatePublishTransformPolicy(*policy, transform); err != nil { + errs = append(errs, context+"."+err.Error()) } return errs } -func validateTransform(errs ValidationErrors, context string, publish *PublishPolicy, transform Transform) ValidationErrors { - publishesHTML := publish != nil && publish.HTML - if transform.MarkdownToHTML == nil { - if publishesHTML { - errs = append(errs, context+".markdown_to_html is required when publish.html is true") - } - return errs +func ValidatePublishTransformPolicy(publish PublishPolicy, transform Transform) error { + if !publish.Source && !publish.HTML { + return fmt.Errorf("publish must enable source or html") } - - if publishesHTML && !transform.MarkdownToHTML.Enabled { - errs = append(errs, context+".markdown_to_html.enabled must be true when publish.html is true") + if publish.HTML && transform.MarkdownToHTML == nil { + return fmt.Errorf("transform.markdown_to_html is required when publish.html is true") + } + if transform.MarkdownToHTML == nil { + return nil + } + if publish.HTML && !transform.MarkdownToHTML.Enabled { + return fmt.Errorf("transform.markdown_to_html.enabled must be true when publish.html is true") + } + if publish.HTML && transform.MarkdownToHTML.Mode != TransformModeSidecar { + return fmt.Errorf("transform.markdown_to_html.mode must be %s", TransformModeSidecar) } if transform.MarkdownToHTML.Enabled && transform.MarkdownToHTML.Mode != TransformModeSidecar { - errs = append(errs, context+".markdown_to_html.mode must be "+TransformModeSidecar) + return fmt.Errorf("transform.markdown_to_html.mode must be %s", TransformModeSidecar) } if !transform.MarkdownToHTML.Enabled && transform.MarkdownToHTML.Mode != "" && transform.MarkdownToHTML.Mode != TransformModeSidecar { - errs = append(errs, context+".markdown_to_html.mode must be "+TransformModeSidecar) + return fmt.Errorf("transform.markdown_to_html.mode must be %s", TransformModeSidecar) } - return errs + return nil } func validateTransferPolicy(errs ValidationErrors, context string, policy TransferPolicy) ValidationErrors { diff --git a/internal/config/validate_test.go b/internal/config/validate_test.go new file mode 100644 index 0000000..db8561b --- /dev/null +++ b/internal/config/validate_test.go @@ -0,0 +1,141 @@ +package config + +import "testing" + +func TestValidatePublishTransformPolicy(t *testing.T) { + tests := publishTransformPolicyCases() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidatePublishTransformPolicy(tt.publish, tt.transform) + if tt.wantErr && err == nil { + t.Fatal("ValidatePublishTransformPolicy() error = nil, want error") + } + if !tt.wantErr && err != nil { + t.Fatalf("ValidatePublishTransformPolicy() error = %v", err) + } + }) + } +} + +func TestValidateChecksPublishTransformPolicy(t *testing.T) { + tests := publishTransformPolicyCases() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := Config{Pipelines: []Pipeline{{ + ID: "reports", + Source: Backend{ + Backend: BackendLocal, + Path: "/source", + }, + Destinations: []Destination{{ + ID: "archive", + Backend: BackendLocal, + Path: "/destination", + Publish: &tt.publish, + Transform: tt.transform, + }}, + }}} + ApplyDefaults(&cfg) + err := Validate(cfg) + if tt.wantErr && err == nil { + t.Fatal("Validate() error = nil, want error") + } + if !tt.wantErr && err != nil { + t.Fatalf("Validate() error = %v", err) + } + }) + } +} + +type publishTransformPolicyCase struct { + name string + publish PublishPolicy + transform Transform + wantErr bool +} + +func publishTransformPolicyCases() []publishTransformPolicyCase { + return []publishTransformPolicyCase{ + { + name: "source only allowed", + publish: PublishPolicy{Source: true}, + }, + { + name: "html only sidecar allowed", + publish: PublishPolicy{HTML: true}, + transform: Transform{MarkdownToHTML: &MarkdownToHTML{ + Enabled: true, + Mode: TransformModeSidecar, + }}, + }, + { + name: "source and html sidecar allowed", + publish: PublishPolicy{Source: true, HTML: true}, + transform: Transform{MarkdownToHTML: &MarkdownToHTML{ + Enabled: true, + Mode: TransformModeSidecar, + }}, + }, + { + name: "no outputs rejected", + publish: PublishPolicy{}, + wantErr: true, + }, + { + name: "html without transform rejected", + publish: PublishPolicy{HTML: true}, + wantErr: true, + }, + { + name: "html with disabled transform rejected", + publish: PublishPolicy{HTML: true}, + transform: Transform{MarkdownToHTML: &MarkdownToHTML{ + Enabled: false, + Mode: TransformModeSidecar, + }}, + wantErr: true, + }, + { + name: "html with wrong mode rejected", + publish: PublishPolicy{HTML: true}, + transform: Transform{MarkdownToHTML: &MarkdownToHTML{ + Enabled: true, + Mode: "inline", + }}, + wantErr: true, + }, + { + name: "enabled markdown wrong mode rejected", + publish: PublishPolicy{Source: true}, + transform: Transform{MarkdownToHTML: &MarkdownToHTML{ + Enabled: true, + Mode: "inline", + }}, + wantErr: true, + }, + { + name: "disabled markdown empty mode allowed", + publish: PublishPolicy{Source: true}, + transform: Transform{MarkdownToHTML: &MarkdownToHTML{ + Enabled: false, + }}, + }, + { + name: "disabled markdown sidecar mode allowed", + publish: PublishPolicy{Source: true}, + transform: Transform{MarkdownToHTML: &MarkdownToHTML{ + Enabled: false, + Mode: TransformModeSidecar, + }}, + }, + { + name: "disabled markdown wrong mode rejected", + publish: PublishPolicy{Source: true}, + transform: Transform{MarkdownToHTML: &MarkdownToHTML{ + Enabled: false, + Mode: "inline", + }}, + wantErr: true, + }, + } +} diff --git a/internal/publish/output_test.go b/internal/publish/output_test.go index f5f583a..38363f0 100644 --- a/internal/publish/output_test.go +++ b/internal/publish/output_test.go @@ -99,3 +99,112 @@ func TestBuildRejectsHTMLWithoutTransform(t *testing.T) { 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: "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: "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) + } + }) + } +} diff --git a/internal/publish/plan.go b/internal/publish/plan.go index b80904e..3f76595 100644 --- a/internal/publish/plan.go +++ b/internal/publish/plan.go @@ -102,13 +102,8 @@ func validateRequest(req Request) error { if req.DestinationBackend == nil { return fmt.Errorf("destination backend is required") } - if !req.Publish.Source && !req.Publish.HTML { - return fmt.Errorf("publish source or html must be enabled") - } - if req.Publish.HTML { - if req.Transform.MarkdownToHTML == nil || !req.Transform.MarkdownToHTML.Enabled || req.Transform.MarkdownToHTML.Mode != config.TransformModeSidecar { - return fmt.Errorf("publish html requires markdown_to_html transform enabled with sidecar mode") - } + if err := config.ValidatePublishTransformPolicy(req.Publish, req.Transform); err != nil { + return fmt.Errorf("publish/transform policy: %w", err) } return nil }