diff --git a/internal/provider/acctest/resource_setting_teleport_test.go b/internal/provider/acctest/resource_setting_teleport_test.go new file mode 100644 index 0000000..2b5ba66 --- /dev/null +++ b/internal/provider/acctest/resource_setting_teleport_test.go @@ -0,0 +1,65 @@ +package acctest + +import ( + "fmt" + pt "github.com/filipowm/terraform-provider-unifi/internal/provider/testing" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "sync" + "testing" +) + +var settingTeleportLock = &sync.Mutex{} + +func TestAccSettingTeleport(t *testing.T) { + AcceptanceTest(t, AcceptanceTestCase{ + VersionConstraint: ">= 7.1", + Lock: settingTeleportLock, + Steps: []resource.TestStep{ + { + Config: testAccSettingTeleportConfig(true, ""), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("unifi_setting_teleport.test", "id"), + resource.TestCheckResourceAttr("unifi_setting_teleport.test", "site", "default"), + resource.TestCheckResourceAttr("unifi_setting_teleport.test", "enabled", "true"), + resource.TestCheckResourceAttr("unifi_setting_teleport.test", "subnet", ""), + ), + ConfigPlanChecks: pt.CheckResourceActions("unifi_setting_teleport.test", plancheck.ResourceActionCreate), + }, + pt.ImportStepWithSite("unifi_setting_teleport.test"), + { + Config: testAccSettingTeleportConfig(true, "192.168.100.0/24"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_teleport.test", "enabled", "true"), + resource.TestCheckResourceAttr("unifi_setting_teleport.test", "subnet", "192.168.100.0/24"), + ), + ConfigPlanChecks: pt.CheckResourceActions("unifi_setting_teleport.test", plancheck.ResourceActionUpdate), + }, + { + Config: testAccSettingTeleportConfigWithoutSubnet(false), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_teleport.test", "enabled", "false"), + resource.TestCheckResourceAttr("unifi_setting_teleport.test", "subnet", ""), + ), + ConfigPlanChecks: pt.CheckResourceActions("unifi_setting_teleport.test", plancheck.ResourceActionUpdate), + }, + }, + }) +} + +func testAccSettingTeleportConfig(enabled bool, subnetCidr string) string { + return fmt.Sprintf(` +resource "unifi_setting_teleport" "test" { + enabled = %t + subnet = %q +} +`, enabled, subnetCidr) +} + +func testAccSettingTeleportConfigWithoutSubnet(enabled bool) string { + return fmt.Sprintf(` +resource "unifi_setting_teleport" "test" { + enabled = %t +} +`, enabled) +} diff --git a/internal/provider/base/controller_versions.go b/internal/provider/base/controller_versions.go index 1c9a5c0..97899f9 100644 --- a/internal/provider/base/controller_versions.go +++ b/internal/provider/base/controller_versions.go @@ -9,6 +9,12 @@ func asVersion(versionString string) *version.Version { return version.Must(version.NewVersion(versionString)) } +// AsVersion converts a string version to a *version.Version +// This is a utility function for consumers of this package +func AsVersion(versionString string) *version.Version { + return asVersion(versionString) +} + var ( ControllerV6 = asVersion("6.0.0") ControllerV7 = asVersion("7.0.0") diff --git a/internal/provider/provider_v2.go b/internal/provider/provider_v2.go index ed6d266..0c53d06 100644 --- a/internal/provider/provider_v2.go +++ b/internal/provider/provider_v2.go @@ -181,6 +181,7 @@ func (p *unifiProvider) Resources(_ context.Context) []func() resource.Resource settings.NewNetworkOptimizationResource, settings.NewNtpResource, settings.NewSslInspectionResource, + settings.NewTeleportResource, } } diff --git a/internal/provider/settings/resource_setting_teleport.go b/internal/provider/settings/resource_setting_teleport.go new file mode 100644 index 0000000..deb7cc6 --- /dev/null +++ b/internal/provider/settings/resource_setting_teleport.go @@ -0,0 +1,103 @@ +package settings + +import ( + "context" + + "github.com/filipowm/go-unifi/unifi" + "github.com/filipowm/terraform-provider-unifi/internal/provider/base" + "github.com/filipowm/terraform-provider-unifi/internal/provider/validators" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type teleportModel struct { + base.Model + Enabled types.Bool `tfsdk:"enabled"` + Subnet types.String `tfsdk:"subnet"` +} + +func (d *teleportModel) AsUnifiModel() (interface{}, diag.Diagnostics) { + diags := diag.Diagnostics{} + + model := &unifi.SettingTeleport{ + ID: d.ID.ValueString(), + Enabled: d.Enabled.ValueBool(), + SubnetCidr: d.Subnet.ValueString(), + } + + return model, diags +} + +func (d *teleportModel) Merge(other interface{}) diag.Diagnostics { + diags := diag.Diagnostics{} + + model, ok := other.(*unifi.SettingTeleport) + if !ok { + diags.AddError("Cannot merge", "Cannot merge type that is not *unifi.SettingTeleport") + return diags + } + + d.ID = types.StringValue(model.ID) + d.Enabled = types.BoolValue(model.Enabled) + d.Subnet = types.StringValue(model.SubnetCidr) + + return diags +} + +var ( + _ base.ResourceModel = &teleportModel{} + _ resource.Resource = &teleportResource{} + _ resource.ResourceWithConfigure = &teleportResource{} + _ resource.ResourceWithImportState = &teleportResource{} + _ resource.ResourceWithConfigValidators = &teleportResource{} +) + +type teleportResource struct { + *BaseSettingResource[*teleportModel] +} + +func (r *teleportResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "Manages Teleport settings for a UniFi site. Teleport is a secure remote access technology that allows authorized users to connect to UniFi devices from anywhere.", + Attributes: map[string]schema.Attribute{ + "id": base.ID(), + "site": base.SiteAttribute(), + "enabled": schema.BoolAttribute{ + MarkdownDescription: "Whether Teleport is enabled.", + Required: true, + }, + "subnet": schema.StringAttribute{ + MarkdownDescription: "The subnet CIDR for Teleport (e.g., `192.168.1.0/24`). Can be empty but must be set explicitly.", + Optional: true, + Computed: true, + Validators: []validator.String{ + validators.CIDROrEmpty(), + }, + }, + }, + } +} + +func (r *teleportResource) ConfigValidators(_ context.Context) []resource.ConfigValidator { + return []resource.ConfigValidator{ + validators.ResourceRequireMinVersion(r.GetClient(), "7.1", "Teleport requires UniFi controller version 7.1 or higher"), + } +} + +func NewTeleportResource() resource.Resource { + r := &teleportResource{} + r.BaseSettingResource = NewBaseSettingResource( + "unifi_setting_teleport", + func() *teleportModel { return &teleportModel{} }, + func(ctx context.Context, client *base.Client, site string) (interface{}, error) { + return client.GetSettingTeleport(ctx, site) + }, + func(ctx context.Context, client *base.Client, site string, body interface{}) (interface{}, error) { + return client.UpdateSettingTeleport(ctx, site, body.(*unifi.SettingTeleport)) + }, + ) + return r +} diff --git a/internal/provider/validators/cidr.go b/internal/provider/validators/cidr.go new file mode 100644 index 0000000..4433832 --- /dev/null +++ b/internal/provider/validators/cidr.go @@ -0,0 +1,67 @@ +package validators + +import ( + "context" + "fmt" + "net" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +// CIDR returns a validator which ensures that a string value is a valid CIDR notation. +func CIDR() validator.String { + return cidrValidator{ + allowEmpty: false, + } +} + +// CIDROrEmpty returns a validator which ensures that a string value is either empty or a valid CIDR notation. +func CIDROrEmpty() validator.String { + return cidrValidator{ + allowEmpty: true, + } +} + +var ( + _ validator.String = cidrValidator{} +) + +type cidrValidator struct { + allowEmpty bool +} + +func (v cidrValidator) Description(ctx context.Context) string { + return "value must be a valid CIDR notation (e.g., '192.168.1.0/24')" +} + +func (v cidrValidator) MarkdownDescription(ctx context.Context) string { + return "value must be a valid CIDR notation (e.g., `192.168.1.0/24`)" +} + +func (v cidrValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + value := req.ConfigValue.ValueString() + if value == "" { + if !v.allowEmpty { + resp.Diagnostics.AddAttributeError( + req.Path, + "Invalid CIDR Notation", + "CIDR notation cannot be empty", + ) + } + return + } + + _, _, err := net.ParseCIDR(value) + if err != nil { + resp.Diagnostics.AddAttributeError( + req.Path, + "Invalid CIDR Notation", + fmt.Sprintf("Value %q is not a valid CIDR notation: %v", value, err), + ) + return + } +} diff --git a/internal/provider/validators/cidr_test.go b/internal/provider/validators/cidr_test.go new file mode 100644 index 0000000..160cd26 --- /dev/null +++ b/internal/provider/validators/cidr_test.go @@ -0,0 +1,127 @@ +package validators_test + +import ( + "context" + "testing" + + "github.com/filipowm/terraform-provider-unifi/internal/provider/validators" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stretchr/testify/require" +) + +func TestCIDR(t *testing.T) { + t.Parallel() + + type testCase struct { + val types.String + expectError bool + } + tests := map[string]testCase{ + "unknown": { + val: types.StringUnknown(), + expectError: false, + }, + "null": { + val: types.StringNull(), + expectError: false, + }, + "empty": { + val: types.StringValue(""), + expectError: true, + }, + "valid-ipv4": { + val: types.StringValue("192.168.1.0/24"), + expectError: false, + }, + "invalid-ipv4": { + val: types.StringValue("192.168.1.0"), + expectError: true, + }, + "valid-ipv6": { + val: types.StringValue("2001:db8::/32"), + expectError: false, + }, + "invalid-ipv6": { + val: types.StringValue("2001:db8::"), + expectError: true, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + + request := validator.StringRequest{ + ConfigValue: test.val, + } + response := validator.StringResponse{} + validators.CIDR().ValidateString(context.Background(), request, &response) + + if test.expectError { + require.NotEmpty(t, response.Diagnostics) + return + } + require.Empty(t, response.Diagnostics) + }) + } +} + +func TestCIDROrEmpty(t *testing.T) { + t.Parallel() + + type testCase struct { + val types.String + expectError bool + } + tests := map[string]testCase{ + "unknown": { + val: types.StringUnknown(), + expectError: false, + }, + "null": { + val: types.StringNull(), + expectError: false, + }, + "empty": { + val: types.StringValue(""), + expectError: false, + }, + "valid-ipv4": { + val: types.StringValue("192.168.1.0/24"), + expectError: false, + }, + "invalid-ipv4": { + val: types.StringValue("192.168.1.0"), + expectError: true, + }, + "valid-ipv6": { + val: types.StringValue("2001:db8::/32"), + expectError: false, + }, + "invalid-ipv6": { + val: types.StringValue("2001:db8::"), + expectError: true, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + + request := validator.StringRequest{ + ConfigValue: test.val, + } + response := validator.StringResponse{} + validators.CIDROrEmpty().ValidateString(context.Background(), request, &response) + + if test.expectError { + require.NotEmpty(t, response.Diagnostics) + return + } + require.Empty(t, response.Diagnostics) + }) + } +} diff --git a/internal/provider/validators/controller_version.go b/internal/provider/validators/controller_version.go new file mode 100644 index 0000000..bbb817c --- /dev/null +++ b/internal/provider/validators/controller_version.go @@ -0,0 +1,437 @@ +package validators + +import ( + "context" + "fmt" + + "github.com/filipowm/terraform-provider-unifi/internal/provider/base" + "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +var ( + _ resource.ConfigValidator = &ControllerVersionValidator{} + _ datasource.ConfigValidator = &ControllerVersionValidator{} + _ validator.String = &ControllerVersionValidator{} + _ validator.Bool = &ControllerVersionValidator{} + _ validator.Int64 = &ControllerVersionValidator{} + _ validator.Float64 = &ControllerVersionValidator{} + _ validator.List = &ControllerVersionValidator{} + _ validator.Map = &ControllerVersionValidator{} + _ validator.Object = &ControllerVersionValidator{} + _ validator.Set = &ControllerVersionValidator{} +) + +// ControllerVersionValidator is a validator that checks if the UniFi controller version +// matches the specified constraints. +type ControllerVersionValidator struct { + client *base.Client + minVersion *version.Version + maxVersion *version.Version + exactVersion *version.Version + conditionMessage string +} + +// Description returns a description of the validator. +func (v ControllerVersionValidator) Description(ctx context.Context) string { + return v.MarkdownDescription(ctx) +} + +// MarkdownDescription returns a markdown description of the validator. +func (v ControllerVersionValidator) MarkdownDescription(_ context.Context) string { + if v.exactVersion != nil { + return fmt.Sprintf("Validates that the controller version is exactly %s", v.exactVersion) + } + if v.minVersion != nil && v.maxVersion != nil { + return fmt.Sprintf("Validates that the controller version is between %s and %s", v.minVersion, v.maxVersion) + } + if v.minVersion != nil { + return fmt.Sprintf("Validates that the controller version is at least %s", v.minVersion) + } + if v.maxVersion != nil { + return fmt.Sprintf("Validates that the controller version is at most %s", v.maxVersion) + } + return "Validates the controller version" +} + +// ValidateResource validates the resource configuration. +func (v ControllerVersionValidator) ValidateResource(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { + if v.client == nil || v.client.Version == nil { + resp.Diagnostics.AddWarning("Controller version not available", "Provider was not initialized properly. UniFi client or controller version is not available") + return + } + + v.validateVersion(ctx, &resp.Diagnostics) +} + +// ValidateDataSource validates the datasource configuration. +func (v ControllerVersionValidator) ValidateDataSource(ctx context.Context, req datasource.ValidateConfigRequest, resp *datasource.ValidateConfigResponse) { + if v.client == nil || v.client.Version == nil { + resp.Diagnostics.AddWarning("Controller version not available", "Provider was not initialized properly. UniFi client or controller version is not available") + return + } + + v.validateVersion(ctx, &resp.Diagnostics) +} + +// validateVersion checks if the controller version meets the constraints +func (v ControllerVersionValidator) validateVersion(_ context.Context, diags *diag.Diagnostics) { + controllerVersion := v.client.Version + + message := v.conditionMessage + if message == "" { + message = "Controller version does not meet requirements" + } + + if v.exactVersion != nil && !controllerVersion.Equal(v.exactVersion) { + diags.AddError( + message, + fmt.Sprintf("Controller version %s does not match required version %s", controllerVersion, v.exactVersion), + ) + return + } + + if v.minVersion != nil && controllerVersion.LessThan(v.minVersion) { + diags.AddError( + message, + fmt.Sprintf("Controller version %s is less than minimum required version %s", controllerVersion, v.minVersion), + ) + return + } + + if v.maxVersion != nil && controllerVersion.GreaterThan(v.maxVersion) { + diags.AddError( + message, + fmt.Sprintf("Controller version %s is greater than maximum allowed version %s", controllerVersion, v.maxVersion), + ) + return + } +} + +// validateAttributeVersion is a helper function for attribute validators +func (v ControllerVersionValidator) validateAttributeVersion(ctx context.Context, req path.Path) diag.Diagnostics { + diags := diag.Diagnostics{} + + if v.client == nil || v.client.Version == nil { + diags.AddWarning("Controller version not available", "Provider was not initialized properly. UniFi client or controller version is not available") + return diags + } + + controllerVersion := v.client.Version + + message := v.conditionMessage + if message == "" { + message = "Controller version does not meet requirements" + } + + if v.exactVersion != nil && !controllerVersion.Equal(v.exactVersion) { + diags.Append(validatordiag.InvalidAttributeValueDiagnostic( + req, + message, + fmt.Sprintf("Controller version %s does not match required version %s to use given attribute", controllerVersion, v.exactVersion), + )) + return diags + } + + if v.minVersion != nil && controllerVersion.LessThan(v.minVersion) { + diags.Append(validatordiag.InvalidAttributeValueDiagnostic( + req, + message, + fmt.Sprintf("Controller version %s is less than minimum required version %s to use given attribute", controllerVersion, v.minVersion), + )) + return diags + } + + if v.maxVersion != nil && controllerVersion.GreaterThan(v.maxVersion) { + diags.Append(validatordiag.InvalidAttributeValueDiagnostic( + req, + message, + fmt.Sprintf("Controller version %s is greater than maximum allowed version %s to use given attribute", controllerVersion, v.maxVersion), + )) + return diags + } + + return diags +} + +// ValidateString implements validator.String +func (v ControllerVersionValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + resp.Diagnostics.Append(v.validateAttributeVersion(ctx, req.Path)...) +} + +// ValidateBool implements validator.Bool +func (v ControllerVersionValidator) ValidateBool(ctx context.Context, req validator.BoolRequest, resp *validator.BoolResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + resp.Diagnostics.Append(v.validateAttributeVersion(ctx, req.Path)...) +} + +// ValidateInt64 implements validator.Int64 +func (v ControllerVersionValidator) ValidateInt64(ctx context.Context, req validator.Int64Request, resp *validator.Int64Response) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + resp.Diagnostics.Append(v.validateAttributeVersion(ctx, req.Path)...) +} + +// ValidateFloat64 implements validator.Float64 +func (v ControllerVersionValidator) ValidateFloat64(ctx context.Context, req validator.Float64Request, resp *validator.Float64Response) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + resp.Diagnostics.Append(v.validateAttributeVersion(ctx, req.Path)...) +} + +// ValidateList implements validator.List +func (v ControllerVersionValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + resp.Diagnostics.Append(v.validateAttributeVersion(ctx, req.Path)...) +} + +// ValidateMap implements validator.Map +func (v ControllerVersionValidator) ValidateMap(ctx context.Context, req validator.MapRequest, resp *validator.MapResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + resp.Diagnostics.Append(v.validateAttributeVersion(ctx, req.Path)...) +} + +// ValidateObject implements validator.Object +func (v ControllerVersionValidator) ValidateObject(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + resp.Diagnostics.Append(v.validateAttributeVersion(ctx, req.Path)...) +} + +// ValidateSet implements validator.Set +func (v ControllerVersionValidator) ValidateSet(ctx context.Context, req validator.SetRequest, resp *validator.SetResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + resp.Diagnostics.Append(v.validateAttributeVersion(ctx, req.Path)...) +} + +// ResourceRequireMinVersion returns a resource validator that checks if the controller version +// is at least the specified version. +func ResourceRequireMinVersion(client *base.Client, minVersion string, conditionMessage string) resource.ConfigValidator { + return ControllerVersionValidator{ + client: client, + minVersion: base.AsVersion(minVersion), + conditionMessage: conditionMessage, + } +} + +// ResourceRequireMaxVersion returns a resource validator that checks if the controller version +// is at most the specified version. +func ResourceRequireMaxVersion(client *base.Client, maxVersion string, conditionMessage string) resource.ConfigValidator { + return ControllerVersionValidator{ + client: client, + maxVersion: base.AsVersion(maxVersion), + conditionMessage: conditionMessage, + } +} + +// ResourceRequireVersionRange returns a resource validator that checks if the controller version +// is within the specified range (inclusive). +func ResourceRequireVersionRange(client *base.Client, minVersion, maxVersion string, conditionMessage string) resource.ConfigValidator { + return ControllerVersionValidator{ + client: client, + minVersion: base.AsVersion(minVersion), + maxVersion: base.AsVersion(maxVersion), + conditionMessage: conditionMessage, + } +} + +// ResourceRequireExactVersion returns a resource validator that checks if the controller version +// matches the specified version exactly. +func ResourceRequireExactVersion(client *base.Client, exactVersion string, conditionMessage string) resource.ConfigValidator { + return ControllerVersionValidator{ + client: client, + exactVersion: base.AsVersion(exactVersion), + conditionMessage: conditionMessage, + } +} + +// DatasourceRequireMinVersion returns a datasource validator that checks if the controller version +// is at least the specified version. +func DatasourceRequireMinVersion(client *base.Client, minVersion string, conditionMessage string) datasource.ConfigValidator { + return ControllerVersionValidator{ + client: client, + minVersion: base.AsVersion(minVersion), + conditionMessage: conditionMessage, + } +} + +// DatasourceRequireMaxVersion returns a datasource validator that checks if the controller version +// is at most the specified version. +func DatasourceRequireMaxVersion(client *base.Client, maxVersion string, conditionMessage string) datasource.ConfigValidator { + return ControllerVersionValidator{ + client: client, + maxVersion: base.AsVersion(maxVersion), + conditionMessage: conditionMessage, + } +} + +// DatasourceRequireVersionRange returns a datasource validator that checks if the controller version +// is within the specified range (inclusive). +func DatasourceRequireVersionRange(client *base.Client, minVersion, maxVersion string, conditionMessage string) datasource.ConfigValidator { + return ControllerVersionValidator{ + client: client, + minVersion: base.AsVersion(minVersion), + maxVersion: base.AsVersion(maxVersion), + conditionMessage: conditionMessage, + } +} + +// DatasourceRequireExactVersion returns a datasource validator that checks if the controller version +// matches the specified version exactly. +func DatasourceRequireExactVersion(client *base.Client, exactVersion string, conditionMessage string) datasource.ConfigValidator { + return ControllerVersionValidator{ + client: client, + exactVersion: base.AsVersion(exactVersion), + conditionMessage: conditionMessage, + } +} + +// StringRequireMinVersion returns a string validator that checks if the controller version +// is at least the specified version. +func StringRequireMinVersion(client *base.Client, minVersion string, conditionMessage string) validator.String { + return ControllerVersionValidator{ + client: client, + minVersion: base.AsVersion(minVersion), + conditionMessage: conditionMessage, + } +} + +// StringRequireMaxVersion returns a string validator that checks if the controller version +// is at most the specified version. +func StringRequireMaxVersion(client *base.Client, maxVersion string, conditionMessage string) validator.String { + return ControllerVersionValidator{ + client: client, + maxVersion: base.AsVersion(maxVersion), + conditionMessage: conditionMessage, + } +} + +// StringRequireVersionRange returns a string validator that checks if the controller version +// is within the specified range (inclusive). +func StringRequireVersionRange(client *base.Client, minVersion, maxVersion string, conditionMessage string) validator.String { + return ControllerVersionValidator{ + client: client, + minVersion: base.AsVersion(minVersion), + maxVersion: base.AsVersion(maxVersion), + conditionMessage: conditionMessage, + } +} + +// StringRequireExactVersion returns a string validator that checks if the controller version +// matches the specified version exactly. +func StringRequireExactVersion(client *base.Client, exactVersion string, conditionMessage string) validator.String { + return ControllerVersionValidator{ + client: client, + exactVersion: base.AsVersion(exactVersion), + conditionMessage: conditionMessage, + } +} + +// BoolRequireMinVersion returns a bool validator that checks if the controller version +// is at least the specified version. +func BoolRequireMinVersion(client *base.Client, minVersion string, conditionMessage string) validator.Bool { + return ControllerVersionValidator{ + client: client, + minVersion: base.AsVersion(minVersion), + conditionMessage: conditionMessage, + } +} + +// BoolRequireMaxVersion returns a bool validator that checks if the controller version +// is at most the specified version. +func BoolRequireMaxVersion(client *base.Client, maxVersion string, conditionMessage string) validator.Bool { + return ControllerVersionValidator{ + client: client, + maxVersion: base.AsVersion(maxVersion), + conditionMessage: conditionMessage, + } +} + +// BoolRequireVersionRange returns a bool validator that checks if the controller version +// is within the specified range (inclusive). +func BoolRequireVersionRange(client *base.Client, minVersion, maxVersion string, conditionMessage string) validator.Bool { + return ControllerVersionValidator{ + client: client, + minVersion: base.AsVersion(minVersion), + maxVersion: base.AsVersion(maxVersion), + conditionMessage: conditionMessage, + } +} + +// BoolRequireExactVersion returns a bool validator that checks if the controller version +// matches the specified version exactly. +func BoolRequireExactVersion(client *base.Client, exactVersion string, conditionMessage string) validator.Bool { + return ControllerVersionValidator{ + client: client, + exactVersion: base.AsVersion(exactVersion), + conditionMessage: conditionMessage, + } +} + +// Int64RequireMinVersion returns an int64 validator that checks if the controller version +// is at least the specified version. +func Int64RequireMinVersion(client *base.Client, minVersion string, conditionMessage string) validator.Int64 { + return ControllerVersionValidator{ + client: client, + minVersion: base.AsVersion(minVersion), + conditionMessage: conditionMessage, + } +} + +// Int64RequireMaxVersion returns an int64 validator that checks if the controller version +// is at most the specified version. +func Int64RequireMaxVersion(client *base.Client, maxVersion string, conditionMessage string) validator.Int64 { + return ControllerVersionValidator{ + client: client, + maxVersion: base.AsVersion(maxVersion), + conditionMessage: conditionMessage, + } +} + +// Int64RequireVersionRange returns an int64 validator that checks if the controller version +// is within the specified range (inclusive). +func Int64RequireVersionRange(client *base.Client, minVersion, maxVersion string, conditionMessage string) validator.Int64 { + return ControllerVersionValidator{ + client: client, + minVersion: base.AsVersion(minVersion), + maxVersion: base.AsVersion(maxVersion), + conditionMessage: conditionMessage, + } +} + +// Int64RequireExactVersion returns an int64 validator that checks if the controller version +// matches the specified version exactly. +func Int64RequireExactVersion(client *base.Client, exactVersion string, conditionMessage string) validator.Int64 { + return ControllerVersionValidator{ + client: client, + exactVersion: base.AsVersion(exactVersion), + conditionMessage: conditionMessage, + } +} diff --git a/internal/provider/validators/controller_version_test.go b/internal/provider/validators/controller_version_test.go new file mode 100644 index 0000000..73dd0c0 --- /dev/null +++ b/internal/provider/validators/controller_version_test.go @@ -0,0 +1,375 @@ +package validators + +import ( + "context" + "testing" + + "github.com/filipowm/terraform-provider-unifi/internal/provider/base" + "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/stretchr/testify/assert" +) + +func TestControllerVersionValidator_Description(t *testing.T) { + tests := []struct { + name string + validator ControllerVersionValidator + expected string + description string + }{ + { + name: "exact version", + validator: ControllerVersionValidator{ + exactVersion: base.AsVersion("7.0.0"), + }, + expected: "Validates that the controller version is exactly 7.0.0", + description: "Should describe exact version check", + }, + { + name: "min version", + validator: ControllerVersionValidator{ + minVersion: base.AsVersion("7.0.0"), + }, + expected: "Validates that the controller version is at least 7.0.0", + description: "Should describe minimum version check", + }, + { + name: "max version", + validator: ControllerVersionValidator{ + maxVersion: base.AsVersion("7.0.0"), + }, + expected: "Validates that the controller version is at most 7.0.0", + description: "Should describe maximum version check", + }, + { + name: "version range", + validator: ControllerVersionValidator{ + minVersion: base.AsVersion("7.0.0"), + maxVersion: base.AsVersion("8.0.0"), + }, + expected: "Validates that the controller version is between 7.0.0 and 8.0.0", + description: "Should describe version range check", + }, + { + name: "no constraint", + validator: ControllerVersionValidator{}, + expected: "Validates the controller version", + description: "Should provide generic description when no constraints", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := context.Background() + actual := test.validator.Description(ctx) + assert.Equal(t, test.expected, actual, test.description) + }) + } +} + +func TestControllerVersionValidator_ValidateResource(t *testing.T) { + tests := []struct { + name string + controllerVersion string + validator ControllerVersionValidator + expectError bool + description string + }{ + { + name: "exact version match", + controllerVersion: "7.0.0", + validator: ControllerVersionValidator{ + exactVersion: base.AsVersion("7.0.0"), + }, + expectError: false, + description: "Should pass when exact version matches", + }, + { + name: "exact version mismatch", + controllerVersion: "7.0.0", + validator: ControllerVersionValidator{ + exactVersion: base.AsVersion("7.1.0"), + }, + expectError: true, + description: "Should fail when exact version doesn't match", + }, + { + name: "min version satisfied", + controllerVersion: "7.5.0", + validator: ControllerVersionValidator{ + minVersion: base.AsVersion("7.0.0"), + }, + expectError: false, + description: "Should pass when version meets minimum", + }, + { + name: "min version not satisfied", + controllerVersion: "6.5.0", + validator: ControllerVersionValidator{ + minVersion: base.AsVersion("7.0.0"), + }, + expectError: true, + description: "Should fail when version doesn't meet minimum", + }, + { + name: "max version satisfied", + controllerVersion: "6.5.0", + validator: ControllerVersionValidator{ + maxVersion: base.AsVersion("7.0.0"), + }, + expectError: false, + description: "Should pass when version is below maximum", + }, + { + name: "max version not satisfied", + controllerVersion: "7.5.0", + validator: ControllerVersionValidator{ + maxVersion: base.AsVersion("7.0.0"), + }, + expectError: true, + description: "Should fail when version exceeds maximum", + }, + { + name: "version in range", + controllerVersion: "7.5.0", + validator: ControllerVersionValidator{ + minVersion: base.AsVersion("7.0.0"), + maxVersion: base.AsVersion("8.0.0"), + }, + expectError: false, + description: "Should pass when version is in range", + }, + { + name: "version below range", + controllerVersion: "6.5.0", + validator: ControllerVersionValidator{ + minVersion: base.AsVersion("7.0.0"), + maxVersion: base.AsVersion("8.0.0"), + }, + expectError: true, + description: "Should fail when version is below range", + }, + { + name: "version above range", + controllerVersion: "8.5.0", + validator: ControllerVersionValidator{ + minVersion: base.AsVersion("7.0.0"), + maxVersion: base.AsVersion("8.0.0"), + }, + expectError: true, + description: "Should fail when version is above range", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := context.Background() + + // Create a mock client with the specified version + mockClient := &base.Client{ + Version: version.Must(version.NewVersion(test.controllerVersion)), + } + + // Update the validator with the mock client + test.validator.client = mockClient + + // Create request and response objects + req := resource.ValidateConfigRequest{} + resp := resource.ValidateConfigResponse{ + Diagnostics: diag.Diagnostics{}, + } + + // Call the validator + test.validator.ValidateResource(ctx, req, &resp) + + // Check if the result matches expectations + if test.expectError { + assert.True(t, resp.Diagnostics.HasError(), test.description) + } else { + assert.False(t, resp.Diagnostics.HasError(), test.description) + } + }) + } +} + +func TestResourceHelperFunctions(t *testing.T) { + mockClient := &base.Client{ + Version: base.AsVersion("7.5.0"), + } + + tests := []struct { + name string + validator resource.ConfigValidator + expectError bool + description string + }{ + { + name: "ResourceRequireMinVersion passing", + validator: ResourceRequireMinVersion(mockClient, "7.0.0", ""), + expectError: false, + description: "ResourceRequireMinVersion should pass with sufficient version", + }, + { + name: "ResourceRequireMinVersion failing", + validator: ResourceRequireMinVersion(mockClient, "8.0.0", ""), + expectError: true, + description: "ResourceRequireMinVersion should fail with insufficient version", + }, + { + name: "ResourceRequireMaxVersion passing", + validator: ResourceRequireMaxVersion(mockClient, "8.0.0", ""), + expectError: false, + description: "ResourceRequireMaxVersion should pass with acceptable version", + }, + { + name: "ResourceRequireMaxVersion failing", + validator: ResourceRequireMaxVersion(mockClient, "7.0.0", ""), + expectError: true, + description: "ResourceRequireMaxVersion should fail with too high version", + }, + { + name: "ResourceRequireVersionRange passing", + validator: ResourceRequireVersionRange(mockClient, "7.0.0", "8.0.0", ""), + expectError: false, + description: "ResourceRequireVersionRange should pass with version in range", + }, + { + name: "ResourceRequireVersionRange failing (below)", + validator: ResourceRequireVersionRange(mockClient, "7.6.0", "8.0.0", ""), + expectError: true, + description: "ResourceRequireVersionRange should fail with version below range", + }, + { + name: "ResourceRequireVersionRange failing (above)", + validator: ResourceRequireVersionRange(mockClient, "6.0.0", "7.0.0", ""), + expectError: true, + description: "ResourceRequireVersionRange should fail with version above range", + }, + { + name: "ResourceRequireExactVersion passing", + validator: ResourceRequireExactVersion(mockClient, "7.5.0", ""), + expectError: false, + description: "ResourceRequireExactVersion should pass with exact version match", + }, + { + name: "ResourceRequireExactVersion failing", + validator: ResourceRequireExactVersion(mockClient, "7.5.1", ""), + expectError: true, + description: "ResourceRequireExactVersion should fail with version mismatch", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := context.Background() + + // Create request and response objects + req := resource.ValidateConfigRequest{} + resp := resource.ValidateConfigResponse{ + Diagnostics: diag.Diagnostics{}, + } + + // Call the validator + test.validator.(ControllerVersionValidator).ValidateResource(ctx, req, &resp) + + // Check if the result matches expectations + if test.expectError { + assert.True(t, resp.Diagnostics.HasError(), test.description) + } else { + assert.False(t, resp.Diagnostics.HasError(), test.description) + } + }) + } +} + +func TestDatasourceHelperFunctions(t *testing.T) { + mockClient := &base.Client{ + Version: base.AsVersion("7.5.0"), + } + + tests := []struct { + name string + validator datasource.ConfigValidator + expectError bool + description string + }{ + { + name: "DatasourceRequireMinVersion passing", + validator: DatasourceRequireMinVersion(mockClient, "7.0.0", ""), + expectError: false, + description: "DatasourceRequireMinVersion should pass with sufficient version", + }, + { + name: "DatasourceRequireMinVersion failing", + validator: DatasourceRequireMinVersion(mockClient, "8.0.0", ""), + expectError: true, + description: "DatasourceRequireMinVersion should fail with insufficient version", + }, + { + name: "DatasourceRequireMaxVersion passing", + validator: DatasourceRequireMaxVersion(mockClient, "8.0.0", ""), + expectError: false, + description: "DatasourceRequireMaxVersion should pass with acceptable version", + }, + { + name: "DatasourceRequireMaxVersion failing", + validator: DatasourceRequireMaxVersion(mockClient, "7.0.0", ""), + expectError: true, + description: "DatasourceRequireMaxVersion should fail with too high version", + }, + { + name: "DatasourceRequireVersionRange passing", + validator: DatasourceRequireVersionRange(mockClient, "7.0.0", "8.0.0", ""), + expectError: false, + description: "DatasourceRequireVersionRange should pass with version in range", + }, + { + name: "DatasourceRequireVersionRange failing (below)", + validator: DatasourceRequireVersionRange(mockClient, "7.6.0", "8.0.0", ""), + expectError: true, + description: "DatasourceRequireVersionRange should fail with version below range", + }, + { + name: "DatasourceRequireVersionRange failing (above)", + validator: DatasourceRequireVersionRange(mockClient, "6.0.0", "7.0.0", ""), + expectError: true, + description: "DatasourceRequireVersionRange should fail with version above range", + }, + { + name: "DatasourceRequireExactVersion passing", + validator: DatasourceRequireExactVersion(mockClient, "7.5.0", ""), + expectError: false, + description: "DatasourceRequireExactVersion should pass with exact version match", + }, + { + name: "DatasourceRequireExactVersion failing", + validator: DatasourceRequireExactVersion(mockClient, "7.5.1", ""), + expectError: true, + description: "DatasourceRequireExactVersion should fail with version mismatch", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := context.Background() + + // Create request and response objects + req := datasource.ValidateConfigRequest{} + resp := datasource.ValidateConfigResponse{ + Diagnostics: diag.Diagnostics{}, + } + + // Call the validator + test.validator.(ControllerVersionValidator).ValidateDataSource(ctx, req, &resp) + + // Check if the result matches expectations + if test.expectError { + assert.True(t, resp.Diagnostics.HasError(), test.description) + } else { + assert.False(t, resp.Diagnostics.HasError(), test.description) + } + }) + } +}