From 879c7c1876274e04dda86be0fcfc923bb86d100c Mon Sep 17 00:00:00 2001 From: Pasha Sviderski Date: Tue, 12 Aug 2025 14:55:27 +1000 Subject: [PATCH] test: x-caddy extension parsing --- pkg/client/compose/caddy.go | 26 +++--- pkg/client/compose/caddy_test.go | 123 +++++++++++++++++++++++++++++ pkg/client/compose/project.go | 1 + pkg/client/compose/service_test.go | 1 + 4 files changed, 142 insertions(+), 9 deletions(-) create mode 100644 pkg/client/compose/caddy_test.go diff --git a/pkg/client/compose/caddy.go b/pkg/client/compose/caddy.go index 5390d719..5f82e527 100644 --- a/pkg/client/compose/caddy.go +++ b/pkg/client/compose/caddy.go @@ -1,6 +1,10 @@ package compose -import "fmt" +import ( + "fmt" + + "github.com/mitchellh/mapstructure" +) const CaddyExtensionKey = "x-caddy" @@ -17,16 +21,20 @@ func (c *Caddy) DecodeMapstructure(value any) error { *c = *v return nil case string: - // Handle x-caddy: "caddyfile config" + // Handle x-caddy: "Caddyfile config" *c = Caddy{Config: v} case map[string]any: - // Handle the long syntax with a config key. - if config, ok := v["config"]; ok { - configStr, ok := config.(string) - if !ok { - return fmt.Errorf("x-caddy.config must be a string, got %T", config) - } - *c = Caddy{Config: configStr} + // Use mapstructure to decode the map directly to the struct. + decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ + Result: c, + ErrorUnused: true, // Error if there are extra keys not in the struct. + WeaklyTypedInput: false, // Enforce strict type matching. + }) + if err != nil { + return fmt.Errorf("create decoder for x-caddy extension: %w", err) + } + if err := decoder.Decode(v); err != nil { + return fmt.Errorf("decode x-caddy extension: %w", err) } default: return fmt.Errorf("invalid type %T for x-caddy extension: expected string or object", value) diff --git a/pkg/client/compose/caddy_test.go b/pkg/client/compose/caddy_test.go new file mode 100644 index 00000000..46225d55 --- /dev/null +++ b/pkg/client/compose/caddy_test.go @@ -0,0 +1,123 @@ +package compose + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCaddyExtension(t *testing.T) { + tests := []struct { + name string + composeYAML string + expectedConfig string + wantErr bool + }{ + { + name: "x-caddy as string", + composeYAML: ` +services: + web: + image: nginx + x-caddy: | + example.com { + reverse_proxy web:80 + } +`, + expectedConfig: `example.com { + reverse_proxy web:80 +} +`, + }, + { + name: "x-caddy as object with config field", + composeYAML: ` +services: + web: + image: nginx + x-caddy: + config: | + example.com { + reverse_proxy web:80 + } +`, + expectedConfig: `example.com { + reverse_proxy web:80 +} +`, + }, + + { + name: "x-caddy with empty object", + composeYAML: ` +services: + web: + image: nginx + x-caddy: {} +`, + expectedConfig: "", + }, + { + name: "x-caddy with empty string", + composeYAML: ` +services: + web: + image: nginx + x-caddy: "" +`, + expectedConfig: "", + }, + { + name: "x-caddy with extra unknown field should fail", + composeYAML: ` +services: + web: + image: nginx + x-caddy: + config: | + example.com { + reverse_proxy web:80 + } + unknown_field: "should cause error" +`, + wantErr: true, + }, + { + name: "x-caddy with non-string config field should fail", + composeYAML: ` +services: + web: + image: nginx + x-caddy: + config: 123 +`, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + project, err := loadProjectFromContent(t, tt.composeYAML) + + if tt.wantErr { + require.Error(t, err, "expected error for test case with invalid extension") + return + } + + require.NoError(t, err) + + service, err := project.GetService("web") + require.NoError(t, err) + + // Verify the x-caddy extension was parsed correctly. + caddyExt, ok := service.Extensions[CaddyExtensionKey] + require.True(t, ok, "x-caddy extension not found") + + caddy, ok := caddyExt.(Caddy) + require.True(t, ok, "x-caddy extension is not Caddy type") + + assert.Equal(t, tt.expectedConfig, caddy.Config) + }) + } +} diff --git a/pkg/client/compose/project.go b/pkg/client/compose/project.go index 12e04876..5db06cb9 100644 --- a/pkg/client/compose/project.go +++ b/pkg/client/compose/project.go @@ -24,6 +24,7 @@ func LoadProject(ctx context.Context, paths []string, opts ...composecli.Project composecli.WithConfigFileEnv, // If none was selected, get default Compose file names from current or parent folders. composecli.WithDefaultConfigPath, + composecli.WithExtension(CaddyExtensionKey, Caddy{}), composecli.WithExtension(PortsExtensionKey, PortsSource{}), composecli.WithExtension(MachinesExtensionKey, MachinesSource{}), } diff --git a/pkg/client/compose/service_test.go b/pkg/client/compose/service_test.go index 60fb7e53..689f6999 100644 --- a/pkg/client/compose/service_test.go +++ b/pkg/client/compose/service_test.go @@ -38,6 +38,7 @@ func loadProjectFromContent(t *testing.T, content string) (*types.Project, error if o.KnownExtensions == nil { o.KnownExtensions = map[string]any{} } + o.KnownExtensions[CaddyExtensionKey] = Caddy{} o.KnownExtensions[PortsExtensionKey] = PortsSource{} o.KnownExtensions[MachinesExtensionKey] = MachinesSource{} })