diff --git a/Makefile b/Makefile index 6c745207..0b0cd02f 100644 --- a/Makefile +++ b/Makefile @@ -67,7 +67,9 @@ juju-unit-test: .PHONY: envtestlxd envtestlxd: ## envtestlxd: Under development - Include env var and run unit tests against lxd - JUJU_CONTROLLER_ADDRESSES=${CONTROLLER_ADDRESSES} JUJU_USERNAME=${USERNAME} JUJU_PASSWORD=${PASSWORD} JUJU_CA_CERT=${CA_CERT} TF_ACC=1 TEST_CLOUD=lxd go test ./... -v $(TESTARGS) -timeout 120m + JUJU_CONTROLLER_ADDRESSES=${CONTROLLER_ADDRESSES} \ + JUJU_USERNAME=${USERNAME} JUJU_PASSWORD=${PASSWORD} \ + JUJU_CA_CERT=${CA_CERT} TF_ACC=1 TEST_CLOUD=lxd go test ./... -v $(TESTARGS) -timeout 120m .PHONY: testlxd testlxd: diff --git a/internal/provider/resource_application.go b/internal/provider/resource_application.go index c4151d6c..89703d6c 100644 --- a/internal/provider/resource_application.go +++ b/internal/provider/resource_application.go @@ -240,7 +240,7 @@ func (r *applicationResource) Schema(_ context.Context, _ resource.SchemaRequest stringplanmodifier.UseStateForUnknown(), }, Validators: []validator.String{ - stringIsChannelValidator{}, + StringIsChannelValidator{}, }, }, "revision": schema.Int64Attribute{ diff --git a/internal/provider/validator_channel.go b/internal/provider/validator_channel.go index 53eb063e..b747b9a8 100644 --- a/internal/provider/validator_channel.go +++ b/internal/provider/validator_channel.go @@ -10,26 +10,33 @@ import ( "github.com/juju/charm/v11" ) -type stringIsChannelValidator struct{} +type StringIsChannelValidator struct{} // Description returns a plain text description of the validator's behavior, suitable for a practitioner to understand its impact. -func (v stringIsChannelValidator) Description(context.Context) string { +func (v StringIsChannelValidator) Description(context.Context) string { return "string must conform to track/risk or track/risk/branch e.g. latest/stable" } // MarkdownDescription returns a markdown formatted description of the validator's behavior, suitable for a practitioner to understand its impact. -func (v stringIsChannelValidator) MarkdownDescription(context.Context) string { +func (v StringIsChannelValidator) MarkdownDescription(context.Context) string { return "string must conform to track/risk or track/risk/branch e.g. latest/stable" } // Validate runs the main validation logic of the validator, reading configuration data out of `req` and updating `resp` with diagnostics. -func (v stringIsChannelValidator) ValidateString(_ context.Context, req validator.StringRequest, resp *validator.StringResponse) { +func (v StringIsChannelValidator) ValidateString(_ context.Context, req validator.StringRequest, resp *validator.StringResponse) { // If the value is unknown or null, there is nothing to validate. if req.ConfigValue.IsUnknown() || req.ConfigValue.IsNull() { return } - if channel, err := charm.ParseChannel(req.ConfigValue.ValueString()); err != nil || channel.Track == "" || channel.Risk == "" { + if channel, err := charm.ParseChannel(req.ConfigValue.ValueString()); err != nil { + resp.Diagnostics.AddAttributeError( + req.Path, + "Invalid Channel", + err.Error(), + ) + return + } else if channel.Track == "" || channel.Risk == "" { resp.Diagnostics.AddAttributeError( req.Path, "Invalid Channel", diff --git a/internal/provider/validator_channel_test.go b/internal/provider/validator_channel_test.go new file mode 100644 index 00000000..980bc75e --- /dev/null +++ b/internal/provider/validator_channel_test.go @@ -0,0 +1,73 @@ +// Copyright 2024 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package provider_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/juju/terraform-provider-juju/internal/provider" +) + +func TestChannelValidatorValid(t *testing.T) { + validChannels := []types.String{ + types.StringValue("track/stable"), + types.StringValue("track/edge/branch"), + types.StringNull(), + types.StringUnknown(), + } + + channelValidator := provider.StringIsChannelValidator{} + for _, channel := range validChannels { + req := validator.StringRequest{ + ConfigValue: channel, + } + var resp validator.StringResponse + channelValidator.ValidateString(context.Background(), req, &resp) + + if resp.Diagnostics.HasError() { + t.Errorf("errors %v", resp.Diagnostics.Errors()) + } + } +} + +func TestChannelValidatorInvalid(t *testing.T) { + invalidChannels := []struct { + str types.String + err string + }{{ + str: types.StringValue("track"), + err: "String must conform to track/risk or track/risk/branch, e.g. latest/stable", + }, { + str: types.StringValue("edge"), + err: "String must conform to track/risk or track/risk/branch, e.g. latest/stable", + }, { + str: types.StringValue(`track\risk`), + err: "String must conform to track/risk or track/risk/branch, e.g. latest/stable", + }, { + str: types.StringValue(`track/invalidrisk`), + err: `risk in channel "track/invalidrisk" not valid`, + }, { + str: types.StringValue(`track/invalidrisk/branch`), + err: `risk in channel "track/invalidrisk/branch" not valid`, + }} + + channelValidator := provider.StringIsChannelValidator{} + for _, test := range invalidChannels { + req := validator.StringRequest{ + ConfigValue: test.str, + } + var resp validator.StringResponse + channelValidator.ValidateString(context.Background(), req, &resp) + + if c := resp.Diagnostics.ErrorsCount(); c != 1 { + t.Errorf("expected one error, got %d", c) + } + if deets := resp.Diagnostics.Errors()[0].Detail(); deets != test.err { + t.Errorf("expected error %q, got %q", test.err, deets) + } + } +}