feat: add NTP setting resource support with unifi_setting_ntp resource (#36)
* feat: add NTP setting resource support with `unifi_setting_ntp` resource * linting * fix missing method * add missing validators
This commit is contained in:
committed by
GitHub
parent
a78667e669
commit
8b5ed14d8d
158
internal/provider/acctest/resource_setting_ntp_test.go
Normal file
158
internal/provider/acctest/resource_setting_ntp_test.go
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
package acctest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
pt "github.com/filipowm/terraform-provider-unifi/internal/provider/testing"
|
||||||
|
"github.com/hashicorp/terraform-plugin-testing/plancheck"
|
||||||
|
"regexp"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
|
||||||
|
)
|
||||||
|
|
||||||
|
var settingNtpLock = &sync.Mutex{}
|
||||||
|
|
||||||
|
func TestAccSettingNtp(t *testing.T) {
|
||||||
|
AcceptanceTest(t, AcceptanceTestCase{
|
||||||
|
VersionConstraint: ">= 7.3",
|
||||||
|
Lock: settingNtpLock,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
{
|
||||||
|
Config: testAccSettingNtpModeOnly("auto"),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttrSet("unifi_setting_ntp.test", "id"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ntp.test", "site", "default"),
|
||||||
|
resource.TestCheckResourceAttrSet("unifi_setting_ntp.test", "ntp_server_1"),
|
||||||
|
resource.TestCheckResourceAttrSet("unifi_setting_ntp.test", "ntp_server_2"),
|
||||||
|
resource.TestCheckResourceAttrSet("unifi_setting_ntp.test", "ntp_server_3"),
|
||||||
|
resource.TestCheckResourceAttrSet("unifi_setting_ntp.test", "ntp_server_4"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ntp.test", "mode", "auto"),
|
||||||
|
),
|
||||||
|
ConfigPlanChecks: pt.CheckResourceActions("unifi_setting_ntp.test", plancheck.ResourceActionCreate),
|
||||||
|
},
|
||||||
|
pt.ImportStepWithSite("unifi_setting_ntp.test"),
|
||||||
|
{
|
||||||
|
Config: testAccSettingNtpConfig2Servers("time.google.com", "pool.ntp.org", "manual"),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttrSet("unifi_setting_ntp.test", "id"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ntp.test", "site", "default"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ntp.test", "ntp_server_1", "time.google.com"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ntp.test", "ntp_server_2", "pool.ntp.org"),
|
||||||
|
resource.TestCheckResourceAttrSet("unifi_setting_ntp.test", "ntp_server_3"),
|
||||||
|
resource.TestCheckResourceAttrSet("unifi_setting_ntp.test", "ntp_server_4"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ntp.test", "mode", "manual"),
|
||||||
|
),
|
||||||
|
ConfigPlanChecks: pt.CheckResourceActions("unifi_setting_ntp.test", plancheck.ResourceActionUpdate),
|
||||||
|
},
|
||||||
|
pt.ImportStepWithSite("unifi_setting_ntp.test"),
|
||||||
|
{
|
||||||
|
Config: testAccSettingNtpConfig2Servers("0.pool.ntp.org", "1.pool.ntp.org", "manual"),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttrSet("unifi_setting_ntp.test", "id"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ntp.test", "site", "default"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ntp.test", "ntp_server_1", "0.pool.ntp.org"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ntp.test", "ntp_server_2", "1.pool.ntp.org"),
|
||||||
|
resource.TestCheckResourceAttrSet("unifi_setting_ntp.test", "ntp_server_3"),
|
||||||
|
resource.TestCheckResourceAttrSet("unifi_setting_ntp.test", "ntp_server_4"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ntp.test", "mode", "manual"),
|
||||||
|
),
|
||||||
|
ConfigPlanChecks: pt.CheckResourceActions("unifi_setting_ntp.test", plancheck.ResourceActionUpdate),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Config: testAccSettingNtpConfig2Servers("192.168.1.10", "10.0.0.1", "manual"),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttrSet("unifi_setting_ntp.test", "id"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ntp.test", "site", "default"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ntp.test", "ntp_server_1", "192.168.1.10"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ntp.test", "ntp_server_2", "10.0.0.1"),
|
||||||
|
resource.TestCheckResourceAttrSet("unifi_setting_ntp.test", "ntp_server_3"),
|
||||||
|
resource.TestCheckResourceAttrSet("unifi_setting_ntp.test", "ntp_server_4"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ntp.test", "mode", "manual"),
|
||||||
|
),
|
||||||
|
ConfigPlanChecks: pt.CheckResourceActions("unifi_setting_ntp.test", plancheck.ResourceActionUpdate),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Config: testAccSettingNtpConfig4Servers("time.cloudflare.com", "8.8.8.8", "1.1.1.1", "2.2.2.2", "manual"),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttrSet("unifi_setting_ntp.test", "id"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ntp.test", "site", "default"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ntp.test", "ntp_server_1", "time.cloudflare.com"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ntp.test", "ntp_server_2", "8.8.8.8"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ntp.test", "ntp_server_3", "1.1.1.1"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ntp.test", "ntp_server_4", "2.2.2.2"),
|
||||||
|
resource.TestCheckResourceAttr("unifi_setting_ntp.test", "mode", "manual"),
|
||||||
|
),
|
||||||
|
ConfigPlanChecks: pt.CheckResourceActions("unifi_setting_ntp.test", plancheck.ResourceActionUpdate),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccSettingNtpInvalid(t *testing.T) {
|
||||||
|
AcceptanceTest(t, AcceptanceTestCase{
|
||||||
|
VersionConstraint: ">= 7.3",
|
||||||
|
Lock: settingNtpLock,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
{
|
||||||
|
Config: testAccSettingNtpSimpleConfig("http://invalid-server.com", "auto"),
|
||||||
|
ExpectError: regexp.MustCompile("is not a valid"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Config: testAccSettingNtpSimpleConfig("192.168.1", "auto"),
|
||||||
|
ExpectError: regexp.MustCompile("is not a valid"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Config: testAccSettingNtpSimpleConfig("time.google.com", "invalid"),
|
||||||
|
ExpectError: regexp.MustCompile(`must be one of`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Config: testAccSettingNtpSimpleConfig("time.google.com", "auto"),
|
||||||
|
ExpectError: regexp.MustCompile(`must not be configured`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Config: testAccSettingNtpModeOnly("manual"),
|
||||||
|
ExpectError: regexp.MustCompile(`At least one of`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingNtpModeOnly(mode string) string {
|
||||||
|
return fmt.Sprintf(`
|
||||||
|
resource "unifi_setting_ntp" "test" {
|
||||||
|
mode = %q
|
||||||
|
}
|
||||||
|
`, mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingNtpConfig2Servers(server1, server2, mode string) string {
|
||||||
|
return fmt.Sprintf(`
|
||||||
|
resource "unifi_setting_ntp" "test" {
|
||||||
|
ntp_server_1 = %q
|
||||||
|
ntp_server_2 = %q
|
||||||
|
mode = %q
|
||||||
|
}
|
||||||
|
`, server1, server2, mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingNtpConfig4Servers(server1, server2, server3, server4, mode string) string {
|
||||||
|
return fmt.Sprintf(`
|
||||||
|
resource "unifi_setting_ntp" "test" {
|
||||||
|
ntp_server_1 = %q
|
||||||
|
ntp_server_2 = %q
|
||||||
|
ntp_server_3 = %q
|
||||||
|
ntp_server_4 = %q
|
||||||
|
mode = %q
|
||||||
|
}
|
||||||
|
`, server1, server2, server3, server4, mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccSettingNtpSimpleConfig(server string, mode string) string {
|
||||||
|
return fmt.Sprintf(`
|
||||||
|
resource "unifi_setting_ntp" "test" {
|
||||||
|
ntp_server_1 = %q
|
||||||
|
mode = %q
|
||||||
|
}
|
||||||
|
`, server, mode)
|
||||||
|
}
|
||||||
@@ -104,8 +104,8 @@ func (p *unifiProvider) Configure(ctx context.Context, req provider.ConfigureReq
|
|||||||
path.Root("api_url"),
|
path.Root("api_url"),
|
||||||
"Unknown UniFi Controller API URL",
|
"Unknown UniFi Controller API URL",
|
||||||
"The provider cannot create the UniFi Controller API client as there is an unknown configuration value "+
|
"The provider cannot create the UniFi Controller API client as there is an unknown configuration value "+
|
||||||
"for the API endpoint. Either target apply the source of the value first, set the value statically in "+
|
"for the API endpoint. Either target apply the source of the value first, set the value statically in "+
|
||||||
"the configuration, or use the UNIFI_API environment variable.",
|
"the configuration, or use the UNIFI_API environment variable.",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,6 +178,7 @@ func (p *unifiProvider) Resources(_ context.Context) []func() resource.Resource
|
|||||||
settings.NewCountryResource,
|
settings.NewCountryResource,
|
||||||
settings.NewLocaleResource,
|
settings.NewLocaleResource,
|
||||||
settings.NewMagicSiteToSiteVpnResource,
|
settings.NewMagicSiteToSiteVpnResource,
|
||||||
|
settings.NewNtpResource,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
178
internal/provider/settings/resource_setting_ntp.go
Normal file
178
internal/provider/settings/resource_setting_ntp.go
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
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-validators/resourcevalidator"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
|
||||||
|
"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/resource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ntpModel represents the data model for NTP (Network Time Protocol) settings.
|
||||||
|
// It defines how NTP servers are configured for a UniFi site.
|
||||||
|
type ntpModel struct {
|
||||||
|
base.Model
|
||||||
|
NtpServer1 types.String `tfsdk:"ntp_server_1"`
|
||||||
|
NtpServer2 types.String `tfsdk:"ntp_server_2"`
|
||||||
|
NtpServer3 types.String `tfsdk:"ntp_server_3"`
|
||||||
|
NtpServer4 types.String `tfsdk:"ntp_server_4"`
|
||||||
|
Mode types.String `tfsdk:"mode"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ntpModel) AsUnifiModel() (interface{}, diag.Diagnostics) {
|
||||||
|
diags := diag.Diagnostics{}
|
||||||
|
|
||||||
|
model := &unifi.SettingNtp{
|
||||||
|
ID: d.ID.ValueString(),
|
||||||
|
SettingPreference: d.Mode.ValueString(),
|
||||||
|
}
|
||||||
|
if d.Mode.ValueString() == "auto" {
|
||||||
|
model.NtpServer1 = ""
|
||||||
|
model.NtpServer2 = ""
|
||||||
|
model.NtpServer3 = ""
|
||||||
|
model.NtpServer4 = ""
|
||||||
|
} else {
|
||||||
|
if !base.IsEmptyString(d.NtpServer1) {
|
||||||
|
model.NtpServer1 = d.NtpServer1.ValueString()
|
||||||
|
}
|
||||||
|
if !base.IsEmptyString(d.NtpServer2) {
|
||||||
|
model.NtpServer2 = d.NtpServer2.ValueString()
|
||||||
|
}
|
||||||
|
if !base.IsEmptyString(d.NtpServer3) {
|
||||||
|
model.NtpServer3 = d.NtpServer3.ValueString()
|
||||||
|
}
|
||||||
|
if !base.IsEmptyString(d.NtpServer4) {
|
||||||
|
model.NtpServer4 = d.NtpServer4.ValueString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return model, diags
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ntpModel) Merge(other interface{}) diag.Diagnostics {
|
||||||
|
diags := diag.Diagnostics{}
|
||||||
|
|
||||||
|
model, ok := other.(*unifi.SettingNtp)
|
||||||
|
if !ok {
|
||||||
|
diags.AddError("Cannot merge", "Cannot merge type that is not *unifi.SettingNtp")
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
|
d.ID = types.StringValue(model.ID)
|
||||||
|
d.Mode = types.StringValue(model.SettingPreference)
|
||||||
|
|
||||||
|
if model.NtpServer1 != "" {
|
||||||
|
d.NtpServer1 = types.StringValue(model.NtpServer1)
|
||||||
|
}
|
||||||
|
if model.NtpServer2 != "" {
|
||||||
|
d.NtpServer2 = types.StringValue(model.NtpServer2)
|
||||||
|
}
|
||||||
|
if model.NtpServer3 != "" {
|
||||||
|
d.NtpServer3 = types.StringValue(model.NtpServer3)
|
||||||
|
}
|
||||||
|
if model.NtpServer4 != "" {
|
||||||
|
d.NtpServer4 = types.StringValue(model.NtpServer4)
|
||||||
|
}
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ base.ResourceModel = &ntpModel{}
|
||||||
|
_ resource.Resource = &ntpResource{}
|
||||||
|
_ resource.ResourceWithConfigure = &ntpResource{}
|
||||||
|
_ resource.ResourceWithImportState = &ntpResource{}
|
||||||
|
_ resource.ResourceWithConfigValidators = &ntpResource{}
|
||||||
|
)
|
||||||
|
|
||||||
|
type ntpResource struct {
|
||||||
|
*BaseSettingResource[*ntpModel]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ntpResource) ConfigValidators(_ context.Context) []resource.ConfigValidator {
|
||||||
|
return []resource.ConfigValidator{
|
||||||
|
validators.RequiredNoneIf(path.MatchRoot("mode"), types.StringValue("auto"), path.MatchRoot("ntp_server_1"), path.MatchRoot("ntp_server_2"), path.MatchRoot("ntp_server_3"), path.MatchRoot("ntp_server_4")),
|
||||||
|
validators.ResourceIf(path.MatchRoot("mode"),
|
||||||
|
types.StringValue("manual"),
|
||||||
|
resourcevalidator.AtLeastOneOf(path.MatchRoot("ntp_server_1"), path.MatchRoot("ntp_server_2"), path.MatchRoot("ntp_server_3"), path.MatchRoot("ntp_server_4")),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ntpResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||||
|
serverValidators := func() []validator.String {
|
||||||
|
return []validator.String{
|
||||||
|
stringvalidator.Any(validators.Hostname(), validators.IPv4()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "The `unifi_setting_ntp` resource allows you to configure Network Time Protocol (NTP) server settings for your UniFi network.\n\n" +
|
||||||
|
"NTP servers provide time synchronization for your network devices. This resource supports both automatic and manual NTP configuration modes.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": base.ID(),
|
||||||
|
"site": base.SiteAttribute(),
|
||||||
|
"ntp_server_1": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "Primary NTP server hostname or IP address. Must be a valid hostname (e.g., `pool.ntp.org`) or IPv4 address. " +
|
||||||
|
"Only applicable when `mode` is set to `manual`.",
|
||||||
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
|
Validators: serverValidators(),
|
||||||
|
},
|
||||||
|
"ntp_server_2": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "Secondary NTP server hostname or IP address. Must be a valid hostname (e.g., `time.google.com`) or IPv4 address. " +
|
||||||
|
"Only applicable when `mode` is set to `manual`.",
|
||||||
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
|
Validators: serverValidators(),
|
||||||
|
},
|
||||||
|
"ntp_server_3": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "Tertiary NTP server hostname or IP address. Must be a valid hostname or IPv4 address. " +
|
||||||
|
"Only applicable when `mode` is set to `manual`.",
|
||||||
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
|
Validators: serverValidators(),
|
||||||
|
},
|
||||||
|
"ntp_server_4": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "Quaternary NTP server hostname or IP address. Must be a valid hostname or IPv4 address. " +
|
||||||
|
"Only applicable when `mode` is set to `manual`.",
|
||||||
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
|
Validators: serverValidators(),
|
||||||
|
},
|
||||||
|
"mode": schema.StringAttribute{
|
||||||
|
MarkdownDescription: "NTP server configuration mode. Valid values are:\n" +
|
||||||
|
"* `auto` - Use NTP servers configured on the controller\n" +
|
||||||
|
"* `manual` - Use custom NTP servers specified in this resource\n\n" +
|
||||||
|
"When set to `auto`, all NTP server fields will be cleared. " +
|
||||||
|
"When set to `manual`, at least one NTP server must be specified.",
|
||||||
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
|
Validators: []validator.String{
|
||||||
|
stringvalidator.OneOf("auto", "manual"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNtpResource creates a new instance of the NTP resource.
|
||||||
|
func NewNtpResource() resource.Resource {
|
||||||
|
r := &ntpResource{}
|
||||||
|
r.BaseSettingResource = NewBaseSettingResource(
|
||||||
|
"unifi_setting_ntp",
|
||||||
|
func() *ntpModel { return &ntpModel{} },
|
||||||
|
func(ctx context.Context, client *base.Client, site string) (interface{}, error) {
|
||||||
|
return client.GetSettingNtp(ctx, site)
|
||||||
|
},
|
||||||
|
func(ctx context.Context, client *base.Client, site string, body interface{}) (interface{}, error) {
|
||||||
|
return client.UpdateSettingNtp(ctx, site, body.(*unifi.SettingNtp))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return r
|
||||||
|
}
|
||||||
161
internal/provider/settings/resource_setting_ntp_test.go
Normal file
161
internal/provider/settings/resource_setting_ntp_test.go
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/filipowm/go-unifi/unifi"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNtpModel_AsUnifiModel_Auto(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// Test case for "auto" mode
|
||||||
|
model := ntpModel{
|
||||||
|
Mode: types.StringValue("auto"),
|
||||||
|
NtpServer1: types.StringValue("time.google.com"),
|
||||||
|
NtpServer2: types.StringValue("pool.ntp.org"),
|
||||||
|
NtpServer3: types.StringValue("0.pool.ntp.org"),
|
||||||
|
NtpServer4: types.StringValue("1.pool.ntp.org"),
|
||||||
|
}
|
||||||
|
model.ID = types.StringValue("test-id")
|
||||||
|
|
||||||
|
// Convert to UnifiModel
|
||||||
|
unifiModel, diags := model.AsUnifiModel()
|
||||||
|
|
||||||
|
// Verify no diagnostics errors
|
||||||
|
assert.False(t, diags.HasError())
|
||||||
|
|
||||||
|
// Verify correct type conversion
|
||||||
|
typed, ok := unifiModel.(*unifi.SettingNtp)
|
||||||
|
assert.True(t, ok, "Expected model to be *unifi.SettingNtp")
|
||||||
|
|
||||||
|
// Verify ID and mode are set correctly
|
||||||
|
assert.Equal(t, "test-id", typed.ID)
|
||||||
|
assert.Equal(t, "auto", typed.SettingPreference)
|
||||||
|
|
||||||
|
// In auto mode, all server fields should be empty regardless of input values
|
||||||
|
assert.Equal(t, "", typed.NtpServer1)
|
||||||
|
assert.Equal(t, "", typed.NtpServer2)
|
||||||
|
assert.Equal(t, "", typed.NtpServer3)
|
||||||
|
assert.Equal(t, "", typed.NtpServer4)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNtpModel_AsUnifiModel_Manual(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
server1 types.String
|
||||||
|
server2 types.String
|
||||||
|
server3 types.String
|
||||||
|
server4 types.String
|
||||||
|
expected1 string
|
||||||
|
expected2 string
|
||||||
|
expected3 string
|
||||||
|
expected4 string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "All servers set",
|
||||||
|
server1: types.StringValue("time.google.com"),
|
||||||
|
server2: types.StringValue("pool.ntp.org"),
|
||||||
|
server3: types.StringValue("0.pool.ntp.org"),
|
||||||
|
server4: types.StringValue("1.pool.ntp.org"),
|
||||||
|
expected1: "time.google.com",
|
||||||
|
expected2: "pool.ntp.org",
|
||||||
|
expected3: "0.pool.ntp.org",
|
||||||
|
expected4: "1.pool.ntp.org",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Only one server set",
|
||||||
|
server1: types.StringValue("time.google.com"),
|
||||||
|
server2: types.StringNull(),
|
||||||
|
server3: types.StringNull(),
|
||||||
|
server4: types.StringNull(),
|
||||||
|
expected1: "time.google.com",
|
||||||
|
expected2: "",
|
||||||
|
expected3: "",
|
||||||
|
expected4: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Mixed null, unknown and empty values",
|
||||||
|
server1: types.StringValue("time.google.com"),
|
||||||
|
server2: types.StringNull(),
|
||||||
|
server3: types.StringUnknown(),
|
||||||
|
server4: types.StringValue(""),
|
||||||
|
expected1: "time.google.com",
|
||||||
|
expected2: "",
|
||||||
|
expected3: "",
|
||||||
|
expected4: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "All null values",
|
||||||
|
server1: types.StringNull(),
|
||||||
|
server2: types.StringNull(),
|
||||||
|
server3: types.StringNull(),
|
||||||
|
server4: types.StringNull(),
|
||||||
|
expected1: "",
|
||||||
|
expected2: "",
|
||||||
|
expected3: "",
|
||||||
|
expected4: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "All unknown values",
|
||||||
|
server1: types.StringUnknown(),
|
||||||
|
server2: types.StringUnknown(),
|
||||||
|
server3: types.StringUnknown(),
|
||||||
|
server4: types.StringUnknown(),
|
||||||
|
expected1: "",
|
||||||
|
expected2: "",
|
||||||
|
expected3: "",
|
||||||
|
expected4: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "All empty string values",
|
||||||
|
server1: types.StringValue(""),
|
||||||
|
server2: types.StringValue(""),
|
||||||
|
server3: types.StringValue(""),
|
||||||
|
server4: types.StringValue(""),
|
||||||
|
expected1: "",
|
||||||
|
expected2: "",
|
||||||
|
expected3: "",
|
||||||
|
expected4: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// Create model with manual mode and test case values
|
||||||
|
model := ntpModel{
|
||||||
|
Mode: types.StringValue("manual"),
|
||||||
|
NtpServer1: tc.server1,
|
||||||
|
NtpServer2: tc.server2,
|
||||||
|
NtpServer3: tc.server3,
|
||||||
|
NtpServer4: tc.server4,
|
||||||
|
}
|
||||||
|
model.ID = types.StringValue("test-id")
|
||||||
|
|
||||||
|
// Convert to UnifiModel
|
||||||
|
unifiModel, diags := model.AsUnifiModel()
|
||||||
|
|
||||||
|
// Verify no diagnostics errors
|
||||||
|
assert.False(t, diags.HasError())
|
||||||
|
|
||||||
|
// Verify correct type conversion
|
||||||
|
typed, ok := unifiModel.(*unifi.SettingNtp)
|
||||||
|
assert.True(t, ok, "Expected model to be *unifi.SettingNtp")
|
||||||
|
|
||||||
|
// Verify ID and mode are set correctly
|
||||||
|
assert.Equal(t, "test-id", typed.ID)
|
||||||
|
assert.Equal(t, "manual", typed.SettingPreference)
|
||||||
|
|
||||||
|
// Verify server values based on test case expectations
|
||||||
|
assert.Equal(t, tc.expected1, typed.NtpServer1)
|
||||||
|
assert.Equal(t, tc.expected2, typed.NtpServer2)
|
||||||
|
assert.Equal(t, tc.expected3, typed.NtpServer3)
|
||||||
|
assert.Equal(t, tc.expected4, typed.NtpServer4)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
package validators
|
package validators_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"github.com/filipowm/terraform-provider-unifi/internal/provider/validators"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -25,7 +26,7 @@ func TestCountryCodeValidation(t *testing.T) {
|
|||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
v := countryCodeAlpha2Validator{}
|
v := validators.CountryCodeAlpha2()
|
||||||
req, resp := newStringValidatorRequestResponse(tc.code)
|
req, resp := newStringValidatorRequestResponse(tc.code)
|
||||||
v.ValidateString(context.Background(), req, resp)
|
v.ValidateString(context.Background(), req, resp)
|
||||||
assert.Equal(t, tc.validationFailed, resp.Diagnostics.HasError())
|
assert.Equal(t, tc.validationFailed, resp.Diagnostics.HasError())
|
||||||
|
|||||||
18
internal/provider/validators/helpers.go
Normal file
18
internal/provider/validators/helpers.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package validators
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/attr"
|
||||||
|
)
|
||||||
|
|
||||||
|
// conditionValueMatches checks if the condition value matches the expected value
|
||||||
|
func conditionValueMatches(ctx context.Context, condition, expected attr.Value) bool {
|
||||||
|
// If types don't match, can't be equal
|
||||||
|
if condition.Type(ctx) != expected.Type(ctx) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if condition.IsNull() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return condition.Equal(expected)
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
package validators
|
package validators_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/hashicorp/terraform-plugin-framework/diag"
|
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||||
"github.com/hashicorp/terraform-plugin-framework/path"
|
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||||
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
||||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
"github.com/hashicorp/terraform-plugin-go/tftypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newStringValidatorRequestResponse(value string) (validator.StringRequest, *validator.StringResponse) {
|
func newStringValidatorRequestResponse(value string) (validator.StringRequest, *validator.StringResponse) {
|
||||||
@@ -17,3 +18,23 @@ func newStringValidatorRequestResponse(value string) (validator.StringRequest, *
|
|||||||
}
|
}
|
||||||
return req, &resp
|
return req, &resp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to convert types.String to tftypes.Value
|
||||||
|
func stringToTfValue(value types.String) tftypes.Value {
|
||||||
|
if value.IsNull() {
|
||||||
|
return tftypes.NewValue(tftypes.String, nil)
|
||||||
|
} else if value.IsUnknown() {
|
||||||
|
return tftypes.NewValue(tftypes.String, tftypes.UnknownValue)
|
||||||
|
}
|
||||||
|
return tftypes.NewValue(tftypes.String, value.ValueString())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to convert types.Bool to tftypes.Value
|
||||||
|
func boolToTfValue(value types.Bool) tftypes.Value {
|
||||||
|
if value.IsNull() {
|
||||||
|
return tftypes.NewValue(tftypes.Bool, nil)
|
||||||
|
} else if value.IsUnknown() {
|
||||||
|
return tftypes.NewValue(tftypes.Bool, tftypes.UnknownValue)
|
||||||
|
}
|
||||||
|
return tftypes.NewValue(tftypes.Bool, value.ValueBool())
|
||||||
|
}
|
||||||
|
|||||||
65
internal/provider/validators/hostname.go
Normal file
65
internal/provider/validators/hostname.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package validators
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/filipowm/terraform-provider-unifi/internal/provider/base"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A regex pattern for validating hostnames without protocol schemes
|
||||||
|
// This matches hostnames according to RFC 1035 with some limitations
|
||||||
|
var hostnameRegex = regexp.MustCompile(`^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$`)
|
||||||
|
|
||||||
|
// Hostname returns a validator which ensures that the string value is a valid hostname without protocol schemes.
|
||||||
|
func Hostname() validator.String {
|
||||||
|
return hostnameValidator{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type hostnameValidator struct{}
|
||||||
|
|
||||||
|
func (v hostnameValidator) Description(_ context.Context) string {
|
||||||
|
return "must be a valid hostname (without protocol scheme)"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v hostnameValidator) MarkdownDescription(ctx context.Context) string {
|
||||||
|
return v.Description(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v hostnameValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) {
|
||||||
|
value := req.ConfigValue
|
||||||
|
if !base.IsDefined(value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val := value.ValueString()
|
||||||
|
|
||||||
|
// Check if the hostname has a scheme (which it shouldn't)
|
||||||
|
if strings.Contains(val, "://") {
|
||||||
|
resp.Diagnostics.Append(
|
||||||
|
validatordiag.InvalidAttributeValueDiagnostic(
|
||||||
|
req.Path,
|
||||||
|
v.Description(ctx),
|
||||||
|
fmt.Sprintf("%q should not include a protocol scheme (e.g., http:// or https://)", val),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to lowercase for validation
|
||||||
|
hostname := strings.ToLower(val)
|
||||||
|
|
||||||
|
if !hostnameRegex.MatchString(hostname) {
|
||||||
|
resp.Diagnostics.Append(
|
||||||
|
validatordiag.InvalidAttributeValueDiagnostic(
|
||||||
|
req.Path,
|
||||||
|
v.Description(ctx),
|
||||||
|
fmt.Sprintf("%q is not a valid hostname", val),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
105
internal/provider/validators/hostname_test.go
Normal file
105
internal/provider/validators/hostname_test.go
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
package validators_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/filipowm/terraform-provider-unifi/internal/provider/validators"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHostnameValidator(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
type testCase struct {
|
||||||
|
val types.String
|
||||||
|
expectError bool
|
||||||
|
}
|
||||||
|
tests := map[string]testCase{
|
||||||
|
"unknown": {
|
||||||
|
val: types.StringUnknown(),
|
||||||
|
},
|
||||||
|
"null": {
|
||||||
|
val: types.StringNull(),
|
||||||
|
},
|
||||||
|
"valid-simple": {
|
||||||
|
val: types.StringValue("example.com"),
|
||||||
|
},
|
||||||
|
"valid-subdomain": {
|
||||||
|
val: types.StringValue("sub.example.com"),
|
||||||
|
},
|
||||||
|
"valid-multiple-subdomains": {
|
||||||
|
val: types.StringValue("a.b.c.example.com"),
|
||||||
|
},
|
||||||
|
"valid-with-hyphen": {
|
||||||
|
val: types.StringValue("my-hostname.example.com"),
|
||||||
|
},
|
||||||
|
"valid-with-numbers": {
|
||||||
|
val: types.StringValue("example123.com"),
|
||||||
|
},
|
||||||
|
"valid-tld-with-numbers": {
|
||||||
|
val: types.StringValue("example.co2"),
|
||||||
|
},
|
||||||
|
"invalid-with-scheme": {
|
||||||
|
val: types.StringValue("http://example.com"),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
"invalid-with-https-scheme": {
|
||||||
|
val: types.StringValue("https://example.com"),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
"invalid-with-path": {
|
||||||
|
val: types.StringValue("example.com/path"),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
"invalid-with-port": {
|
||||||
|
val: types.StringValue("example.com:8080"),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
"invalid-with-underscore": {
|
||||||
|
val: types.StringValue("invalid_hostname.com"),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
"invalid-single-label": {
|
||||||
|
val: types.StringValue("localhost"),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
"invalid-ends-with-hyphen": {
|
||||||
|
val: types.StringValue("hostname-.com"),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
"invalid-begins-with-hyphen": {
|
||||||
|
val: types.StringValue("-hostname.com"),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
"invalid-empty-string": {
|
||||||
|
val: types.StringValue(""),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
"invalid-special-chars": {
|
||||||
|
val: types.StringValue("hostname!.com"),
|
||||||
|
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.Hostname().ValidateString(context.Background(), request, &response)
|
||||||
|
|
||||||
|
if !response.Diagnostics.HasError() && test.expectError {
|
||||||
|
t.Fatal("expected error, got no error")
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.Diagnostics.HasError() && !test.expectError {
|
||||||
|
t.Fatalf("got unexpected error: %s", response.Diagnostics)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
140
internal/provider/validators/if.go
Normal file
140
internal/provider/validators/if.go
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
package validators
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/attr"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/provider"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ datasource.ConfigValidator = &IfValidator{}
|
||||||
|
_ provider.ConfigValidator = &IfValidator{}
|
||||||
|
_ resource.ConfigValidator = &IfValidator{}
|
||||||
|
)
|
||||||
|
|
||||||
|
type ifValidatorBase struct {
|
||||||
|
ConditionPath path.Expression
|
||||||
|
ConditionValue attr.Value
|
||||||
|
CheckOnlyIfSet bool // When true, only checks if the condition value is set (not null), not its actual value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v ifValidatorBase) shouldValidate(ctx context.Context, config tfsdk.Config) bool {
|
||||||
|
// First check the condition attribute's value
|
||||||
|
matchedPaths, matchedPathsDiags := config.PathMatches(ctx, v.ConditionPath)
|
||||||
|
if matchedPathsDiags.HasError() || len(matchedPaths) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the value of the condition attribute
|
||||||
|
var conditionValue attr.Value
|
||||||
|
getConditionDiags := config.GetAttribute(ctx, matchedPaths[0], &conditionValue)
|
||||||
|
if getConditionDiags.HasError() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the condition attribute is null or unknown, skip validation
|
||||||
|
if conditionValue.IsNull() || conditionValue.IsUnknown() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the condition matches
|
||||||
|
if v.CheckOnlyIfSet {
|
||||||
|
return !conditionValue.IsNull()
|
||||||
|
}
|
||||||
|
return conditionValueMatches(ctx, conditionValue, v.ConditionValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
type IfValidator struct {
|
||||||
|
ifValidatorBase
|
||||||
|
resourceValidators []resource.ConfigValidator
|
||||||
|
providerValidators []provider.ConfigValidator
|
||||||
|
datasourceValidators []datasource.ConfigValidator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v IfValidator) Description(ctx context.Context) string {
|
||||||
|
return v.MarkdownDescription(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v IfValidator) MarkdownDescription(_ context.Context) string {
|
||||||
|
if v.CheckOnlyIfSet {
|
||||||
|
return fmt.Sprintf("If %q is set, then check validators", v.ConditionPath)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("If %q equals %s, then check validators", v.ConditionPath, v.ConditionValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v IfValidator) ValidateDataSource(ctx context.Context, req datasource.ValidateConfigRequest, resp *datasource.ValidateConfigResponse) {
|
||||||
|
if !v.shouldValidate(ctx, req.Config) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, v := range v.datasourceValidators {
|
||||||
|
v.ValidateDataSource(ctx, req, resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v IfValidator) ValidateProvider(ctx context.Context, req provider.ValidateConfigRequest, resp *provider.ValidateConfigResponse) {
|
||||||
|
if !v.shouldValidate(ctx, req.Config) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, v := range v.providerValidators {
|
||||||
|
v.ValidateProvider(ctx, req, resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v IfValidator) ValidateResource(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
|
||||||
|
if !v.shouldValidate(ctx, req.Config) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range v.resourceValidators {
|
||||||
|
v.ValidateResource(ctx, req, resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResourceIf(conditionPath path.Expression, conditionValue attr.Value, validators ...resource.ConfigValidator) IfValidator {
|
||||||
|
return IfValidator{
|
||||||
|
ifValidatorBase: ifValidatorBase{
|
||||||
|
ConditionPath: conditionPath,
|
||||||
|
ConditionValue: conditionValue,
|
||||||
|
CheckOnlyIfSet: false,
|
||||||
|
},
|
||||||
|
resourceValidators: validators,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResourceIfSet(conditionPath path.Expression, validators ...resource.ConfigValidator) IfValidator {
|
||||||
|
return IfValidator{
|
||||||
|
ifValidatorBase: ifValidatorBase{
|
||||||
|
ConditionPath: conditionPath,
|
||||||
|
ConditionValue: nil,
|
||||||
|
CheckOnlyIfSet: true,
|
||||||
|
},
|
||||||
|
resourceValidators: validators,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ProviderIfSet(conditionPath path.Expression, validators ...provider.ConfigValidator) IfValidator {
|
||||||
|
return IfValidator{
|
||||||
|
ifValidatorBase: ifValidatorBase{
|
||||||
|
ConditionPath: conditionPath,
|
||||||
|
ConditionValue: nil,
|
||||||
|
CheckOnlyIfSet: true,
|
||||||
|
},
|
||||||
|
providerValidators: validators,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func DatasourceIfSet(conditionPath path.Expression, validators ...datasource.ConfigValidator) IfValidator {
|
||||||
|
return IfValidator{
|
||||||
|
ifValidatorBase: ifValidatorBase{
|
||||||
|
ConditionPath: conditionPath,
|
||||||
|
ConditionValue: nil,
|
||||||
|
CheckOnlyIfSet: true,
|
||||||
|
},
|
||||||
|
datasourceValidators: validators,
|
||||||
|
}
|
||||||
|
}
|
||||||
580
internal/provider/validators/if_test.go
Normal file
580
internal/provider/validators/if_test.go
Normal file
@@ -0,0 +1,580 @@
|
|||||||
|
package validators_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"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/provider"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
"github.com/hashicorp/terraform-plugin-go/tftypes"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/filipowm/terraform-provider-unifi/internal/provider/validators"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Common test case structure for string conditions
|
||||||
|
type stringConditionTestCase struct {
|
||||||
|
condition types.String
|
||||||
|
field1 types.String
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common test case structure for bool conditions
|
||||||
|
type boolConditionTestCase struct {
|
||||||
|
condition types.Bool
|
||||||
|
field1 types.String
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to create a schema object with string condition
|
||||||
|
func createStringConditionSchema() schema.Schema {
|
||||||
|
return schema.Schema{
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"condition": schema.StringAttribute{
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"field1": schema.StringAttribute{
|
||||||
|
Optional: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to create a schema object with bool condition
|
||||||
|
func createBoolConditionSchema() schema.Schema {
|
||||||
|
return schema.Schema{
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"condition": schema.BoolAttribute{
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"field1": schema.StringAttribute{
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to create a config with string condition
|
||||||
|
func createStringConditionConfig(schema schema.Schema, testCase stringConditionTestCase) tfsdk.Config {
|
||||||
|
return tfsdk.Config{
|
||||||
|
Schema: schema,
|
||||||
|
Raw: tftypes.NewValue(
|
||||||
|
tftypes.Object{
|
||||||
|
AttributeTypes: map[string]tftypes.Type{
|
||||||
|
"condition": tftypes.String,
|
||||||
|
"field1": tftypes.String,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
map[string]tftypes.Value{
|
||||||
|
"condition": stringToTfValue(testCase.condition),
|
||||||
|
"field1": stringToTfValue(testCase.field1),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to create a config with bool condition
|
||||||
|
func createBoolConditionConfig(schema schema.Schema, testCase boolConditionTestCase) tfsdk.Config {
|
||||||
|
return tfsdk.Config{
|
||||||
|
Schema: schema,
|
||||||
|
Raw: tftypes.NewValue(
|
||||||
|
tftypes.Object{
|
||||||
|
AttributeTypes: map[string]tftypes.Type{
|
||||||
|
"condition": tftypes.Bool,
|
||||||
|
"field1": tftypes.String,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
map[string]tftypes.Value{
|
||||||
|
"condition": boolToTfValue(testCase.condition),
|
||||||
|
"field1": stringToTfValue(testCase.field1),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock validators
|
||||||
|
type mockResourceValidator struct {
|
||||||
|
called bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *mockResourceValidator) Description(ctx context.Context) string {
|
||||||
|
return "Mock Resource Validator"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *mockResourceValidator) MarkdownDescription(ctx context.Context) string {
|
||||||
|
return "Mock Resource Validator"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *mockResourceValidator) ValidateResource(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
|
||||||
|
v.called = true
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockProviderValidator struct {
|
||||||
|
called bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *mockProviderValidator) Description(ctx context.Context) string {
|
||||||
|
return "Mock Provider Validator"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *mockProviderValidator) MarkdownDescription(ctx context.Context) string {
|
||||||
|
return "Mock Provider Validator"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *mockProviderValidator) ValidateProvider(ctx context.Context, req provider.ValidateConfigRequest, resp *provider.ValidateConfigResponse) {
|
||||||
|
v.called = true
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockDatasourceValidator struct {
|
||||||
|
called bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *mockDatasourceValidator) Description(ctx context.Context) string {
|
||||||
|
return "Mock Datasource Validator"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *mockDatasourceValidator) MarkdownDescription(ctx context.Context) string {
|
||||||
|
return "Mock Datasource Validator"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *mockDatasourceValidator) ValidateDataSource(ctx context.Context, req datasource.ValidateConfigRequest, resp *datasource.ValidateConfigResponse) {
|
||||||
|
v.called = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test ResourceIf with string condition
|
||||||
|
func TestResourceIf(t *testing.T) {
|
||||||
|
testCases := map[string]struct {
|
||||||
|
createValidator func(mock *mockResourceValidator) validators.IfValidator
|
||||||
|
conditionValue string
|
||||||
|
testCase stringConditionTestCase
|
||||||
|
expectedCalled bool
|
||||||
|
}{
|
||||||
|
"matching_condition": {
|
||||||
|
createValidator: func(mock *mockResourceValidator) validators.IfValidator {
|
||||||
|
return validators.ResourceIf(
|
||||||
|
path.MatchRoot("condition"),
|
||||||
|
types.StringValue("test"),
|
||||||
|
mock,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
conditionValue: "test",
|
||||||
|
testCase: stringConditionTestCase{
|
||||||
|
condition: types.StringValue("test"),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
},
|
||||||
|
expectedCalled: true,
|
||||||
|
},
|
||||||
|
"non_matching_condition": {
|
||||||
|
createValidator: func(mock *mockResourceValidator) validators.IfValidator {
|
||||||
|
return validators.ResourceIf(
|
||||||
|
path.MatchRoot("condition"),
|
||||||
|
types.StringValue("test"),
|
||||||
|
mock,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
conditionValue: "test",
|
||||||
|
testCase: stringConditionTestCase{
|
||||||
|
condition: types.StringValue("not-test"),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
},
|
||||||
|
expectedCalled: false,
|
||||||
|
},
|
||||||
|
"null_condition": {
|
||||||
|
createValidator: func(mock *mockResourceValidator) validators.IfValidator {
|
||||||
|
return validators.ResourceIf(
|
||||||
|
path.MatchRoot("condition"),
|
||||||
|
types.StringValue("test"),
|
||||||
|
mock,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
conditionValue: "test",
|
||||||
|
testCase: stringConditionTestCase{
|
||||||
|
condition: types.StringNull(),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
},
|
||||||
|
expectedCalled: false,
|
||||||
|
},
|
||||||
|
"unknown_condition": {
|
||||||
|
createValidator: func(mock *mockResourceValidator) validators.IfValidator {
|
||||||
|
return validators.ResourceIf(
|
||||||
|
path.MatchRoot("condition"),
|
||||||
|
types.StringValue("test"),
|
||||||
|
mock,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
conditionValue: "test",
|
||||||
|
testCase: stringConditionTestCase{
|
||||||
|
condition: types.StringUnknown(),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
},
|
||||||
|
expectedCalled: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
mock := &mockResourceValidator{}
|
||||||
|
validator := testCase.createValidator(mock)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
schema := createStringConditionSchema()
|
||||||
|
config := createStringConditionConfig(schema, testCase.testCase)
|
||||||
|
|
||||||
|
request := resource.ValidateConfigRequest{
|
||||||
|
Config: config,
|
||||||
|
}
|
||||||
|
|
||||||
|
response := &resource.ValidateConfigResponse{
|
||||||
|
Diagnostics: diag.Diagnostics{},
|
||||||
|
}
|
||||||
|
|
||||||
|
validator.ValidateResource(ctx, request, response)
|
||||||
|
|
||||||
|
assert.Equal(t, testCase.expectedCalled, mock.called)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test ResourceIfSet with string condition
|
||||||
|
func TestResourceIfSet(t *testing.T) {
|
||||||
|
testCases := map[string]struct {
|
||||||
|
testCase stringConditionTestCase
|
||||||
|
expectedCalled bool
|
||||||
|
}{
|
||||||
|
"condition_set": {
|
||||||
|
testCase: stringConditionTestCase{
|
||||||
|
condition: types.StringValue("any-value"),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
},
|
||||||
|
expectedCalled: true,
|
||||||
|
},
|
||||||
|
"condition_null": {
|
||||||
|
testCase: stringConditionTestCase{
|
||||||
|
condition: types.StringNull(),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
},
|
||||||
|
expectedCalled: false,
|
||||||
|
},
|
||||||
|
"condition_unknown": {
|
||||||
|
testCase: stringConditionTestCase{
|
||||||
|
condition: types.StringUnknown(),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
},
|
||||||
|
expectedCalled: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
mock := &mockResourceValidator{}
|
||||||
|
validator := validators.ResourceIfSet(
|
||||||
|
path.MatchRoot("condition"),
|
||||||
|
mock,
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
schema := createStringConditionSchema()
|
||||||
|
config := createStringConditionConfig(schema, testCase.testCase)
|
||||||
|
|
||||||
|
request := resource.ValidateConfigRequest{
|
||||||
|
Config: config,
|
||||||
|
}
|
||||||
|
|
||||||
|
response := &resource.ValidateConfigResponse{
|
||||||
|
Diagnostics: diag.Diagnostics{},
|
||||||
|
}
|
||||||
|
|
||||||
|
validator.ValidateResource(ctx, request, response)
|
||||||
|
|
||||||
|
assert.Equal(t, testCase.expectedCalled, mock.called)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test ProviderIfSet with string condition
|
||||||
|
func TestProviderIfSet(t *testing.T) {
|
||||||
|
testCases := map[string]struct {
|
||||||
|
testCase stringConditionTestCase
|
||||||
|
expectedCalled bool
|
||||||
|
}{
|
||||||
|
"condition_set": {
|
||||||
|
testCase: stringConditionTestCase{
|
||||||
|
condition: types.StringValue("any-value"),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
},
|
||||||
|
expectedCalled: true,
|
||||||
|
},
|
||||||
|
"condition_null": {
|
||||||
|
testCase: stringConditionTestCase{
|
||||||
|
condition: types.StringNull(),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
},
|
||||||
|
expectedCalled: false,
|
||||||
|
},
|
||||||
|
"condition_unknown": {
|
||||||
|
testCase: stringConditionTestCase{
|
||||||
|
condition: types.StringUnknown(),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
},
|
||||||
|
expectedCalled: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
mock := &mockProviderValidator{}
|
||||||
|
validator := validators.ProviderIfSet(
|
||||||
|
path.MatchRoot("condition"),
|
||||||
|
mock,
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
schema := createStringConditionSchema()
|
||||||
|
config := createStringConditionConfig(schema, testCase.testCase)
|
||||||
|
|
||||||
|
request := provider.ValidateConfigRequest{
|
||||||
|
Config: config,
|
||||||
|
}
|
||||||
|
|
||||||
|
response := &provider.ValidateConfigResponse{
|
||||||
|
Diagnostics: diag.Diagnostics{},
|
||||||
|
}
|
||||||
|
|
||||||
|
validator.ValidateProvider(ctx, request, response)
|
||||||
|
|
||||||
|
assert.Equal(t, testCase.expectedCalled, mock.called)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test DatasourceIfSet with string condition
|
||||||
|
func TestDatasourceIfSet(t *testing.T) {
|
||||||
|
testCases := map[string]struct {
|
||||||
|
testCase stringConditionTestCase
|
||||||
|
expectedCalled bool
|
||||||
|
}{
|
||||||
|
"condition_set": {
|
||||||
|
testCase: stringConditionTestCase{
|
||||||
|
condition: types.StringValue("any-value"),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
},
|
||||||
|
expectedCalled: true,
|
||||||
|
},
|
||||||
|
"condition_null": {
|
||||||
|
testCase: stringConditionTestCase{
|
||||||
|
condition: types.StringNull(),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
},
|
||||||
|
expectedCalled: false,
|
||||||
|
},
|
||||||
|
"condition_unknown": {
|
||||||
|
testCase: stringConditionTestCase{
|
||||||
|
condition: types.StringUnknown(),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
},
|
||||||
|
expectedCalled: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
mock := &mockDatasourceValidator{}
|
||||||
|
validator := validators.DatasourceIfSet(
|
||||||
|
path.MatchRoot("condition"),
|
||||||
|
mock,
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
schema := createStringConditionSchema()
|
||||||
|
config := createStringConditionConfig(schema, testCase.testCase)
|
||||||
|
|
||||||
|
request := datasource.ValidateConfigRequest{
|
||||||
|
Config: config,
|
||||||
|
}
|
||||||
|
|
||||||
|
response := &datasource.ValidateConfigResponse{
|
||||||
|
Diagnostics: diag.Diagnostics{},
|
||||||
|
}
|
||||||
|
|
||||||
|
validator.ValidateDataSource(ctx, request, response)
|
||||||
|
|
||||||
|
assert.Equal(t, testCase.expectedCalled, mock.called)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test the Description and MarkdownDescription methods for both variants
|
||||||
|
func TestIfValidatorDescription(t *testing.T) {
|
||||||
|
t.Run("ResourceIf description", func(t *testing.T) {
|
||||||
|
mock := &mockResourceValidator{}
|
||||||
|
validator := validators.ResourceIf(
|
||||||
|
path.MatchRoot("condition"),
|
||||||
|
types.StringValue("test"),
|
||||||
|
mock,
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
desc := validator.Description(ctx)
|
||||||
|
mdDesc := validator.MarkdownDescription(ctx)
|
||||||
|
|
||||||
|
expectedDesc := `If "condition" equals "test", then check validators`
|
||||||
|
assert.Equal(t, expectedDesc, desc)
|
||||||
|
assert.Equal(t, expectedDesc, mdDesc)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ResourceIfSet description", func(t *testing.T) {
|
||||||
|
mock := &mockResourceValidator{}
|
||||||
|
validator := validators.ResourceIfSet(
|
||||||
|
path.MatchRoot("condition"),
|
||||||
|
mock,
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
desc := validator.Description(ctx)
|
||||||
|
mdDesc := validator.MarkdownDescription(ctx)
|
||||||
|
|
||||||
|
expectedDesc := `If "condition" is set, then check validators`
|
||||||
|
assert.Equal(t, expectedDesc, desc)
|
||||||
|
assert.Equal(t, expectedDesc, mdDesc)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with bool condition
|
||||||
|
func TestResourceIfWithBoolCondition(t *testing.T) {
|
||||||
|
testCases := map[string]struct {
|
||||||
|
testCase boolConditionTestCase
|
||||||
|
expectedCalled bool
|
||||||
|
}{
|
||||||
|
"matching_true_condition": {
|
||||||
|
testCase: boolConditionTestCase{
|
||||||
|
condition: types.BoolValue(true),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
},
|
||||||
|
expectedCalled: true,
|
||||||
|
},
|
||||||
|
"non_matching_false_condition": {
|
||||||
|
testCase: boolConditionTestCase{
|
||||||
|
condition: types.BoolValue(false),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
},
|
||||||
|
expectedCalled: false,
|
||||||
|
},
|
||||||
|
"null_condition": {
|
||||||
|
testCase: boolConditionTestCase{
|
||||||
|
condition: types.BoolNull(),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
},
|
||||||
|
expectedCalled: false,
|
||||||
|
},
|
||||||
|
"unknown_condition": {
|
||||||
|
testCase: boolConditionTestCase{
|
||||||
|
condition: types.BoolUnknown(),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
},
|
||||||
|
expectedCalled: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
mock := &mockResourceValidator{}
|
||||||
|
validator := validators.ResourceIf(
|
||||||
|
path.MatchRoot("condition"),
|
||||||
|
types.BoolValue(true),
|
||||||
|
mock,
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
schema := createBoolConditionSchema()
|
||||||
|
config := createBoolConditionConfig(schema, testCase.testCase)
|
||||||
|
|
||||||
|
request := resource.ValidateConfigRequest{
|
||||||
|
Config: config,
|
||||||
|
}
|
||||||
|
|
||||||
|
response := &resource.ValidateConfigResponse{
|
||||||
|
Diagnostics: diag.Diagnostics{},
|
||||||
|
}
|
||||||
|
|
||||||
|
validator.ValidateResource(ctx, request, response)
|
||||||
|
|
||||||
|
assert.Equal(t, testCase.expectedCalled, mock.called)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with missing path
|
||||||
|
func TestIfValidatorWithMissingPath(t *testing.T) {
|
||||||
|
mock := &mockResourceValidator{}
|
||||||
|
validator := validators.ResourceIf(
|
||||||
|
path.MatchRoot("non_existent"),
|
||||||
|
types.StringValue("test"),
|
||||||
|
mock,
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
schema := createStringConditionSchema()
|
||||||
|
testCase := stringConditionTestCase{
|
||||||
|
condition: types.StringValue("test"),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
}
|
||||||
|
config := createStringConditionConfig(schema, testCase)
|
||||||
|
|
||||||
|
request := resource.ValidateConfigRequest{
|
||||||
|
Config: config,
|
||||||
|
}
|
||||||
|
|
||||||
|
response := &resource.ValidateConfigResponse{
|
||||||
|
Diagnostics: diag.Diagnostics{},
|
||||||
|
}
|
||||||
|
|
||||||
|
validator.ValidateResource(ctx, request, response)
|
||||||
|
|
||||||
|
// The validator should not be called because the path doesn't exist
|
||||||
|
assert.False(t, mock.called)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with multiple validators
|
||||||
|
func TestIfValidatorWithMultipleValidators(t *testing.T) {
|
||||||
|
mock1 := &mockResourceValidator{}
|
||||||
|
mock2 := &mockResourceValidator{}
|
||||||
|
mock3 := &mockResourceValidator{}
|
||||||
|
|
||||||
|
validator := validators.ResourceIf(
|
||||||
|
path.MatchRoot("condition"),
|
||||||
|
types.StringValue("test"),
|
||||||
|
mock1, mock2, mock3,
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
schema := createStringConditionSchema()
|
||||||
|
testCase := stringConditionTestCase{
|
||||||
|
condition: types.StringValue("test"),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
}
|
||||||
|
config := createStringConditionConfig(schema, testCase)
|
||||||
|
|
||||||
|
request := resource.ValidateConfigRequest{
|
||||||
|
Config: config,
|
||||||
|
}
|
||||||
|
|
||||||
|
response := &resource.ValidateConfigResponse{
|
||||||
|
Diagnostics: diag.Diagnostics{},
|
||||||
|
}
|
||||||
|
|
||||||
|
validator.ValidateResource(ctx, request, response)
|
||||||
|
|
||||||
|
// All validators should be called
|
||||||
|
assert.True(t, mock1.called)
|
||||||
|
assert.True(t, mock2.called)
|
||||||
|
assert.True(t, mock3.called)
|
||||||
|
}
|
||||||
47
internal/provider/validators/ipv4.go
Normal file
47
internal/provider/validators/ipv4.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package validators
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IPv4 returns a validator which ensures that a string value is a valid IPv4 address.
|
||||||
|
func IPv4() validator.String {
|
||||||
|
return ipv4Validator{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ validator.String = ipv4Validator{}
|
||||||
|
|
||||||
|
type ipv4Validator struct{}
|
||||||
|
|
||||||
|
func (v ipv4Validator) Description(ctx context.Context) string {
|
||||||
|
return "value must be a valid IPv4 address"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v ipv4Validator) MarkdownDescription(ctx context.Context) string {
|
||||||
|
return "value must be a valid IPv4 address"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v ipv4Validator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) {
|
||||||
|
if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
value := req.ConfigValue.ValueString()
|
||||||
|
if value == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ip := net.ParseIP(value)
|
||||||
|
if ip == nil || ip.To4() == nil {
|
||||||
|
resp.Diagnostics.AddAttributeError(
|
||||||
|
req.Path,
|
||||||
|
"Invalid IPv4 Address",
|
||||||
|
fmt.Sprintf("Value %q is not a valid IPv4 address", value),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
73
internal/provider/validators/ipv4_test.go
Normal file
73
internal/provider/validators/ipv4_test.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIPv4Validator(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
type testCase struct {
|
||||||
|
val types.String
|
||||||
|
expectError bool
|
||||||
|
}
|
||||||
|
tests := map[string]testCase{
|
||||||
|
"unknown": {
|
||||||
|
val: types.StringUnknown(),
|
||||||
|
},
|
||||||
|
"null": {
|
||||||
|
val: types.StringNull(),
|
||||||
|
},
|
||||||
|
"empty": {
|
||||||
|
val: types.StringValue(""),
|
||||||
|
},
|
||||||
|
"valid ipv4": {
|
||||||
|
val: types.StringValue("192.168.1.1"),
|
||||||
|
},
|
||||||
|
"valid ipv4 with leading zeros": {
|
||||||
|
val: types.StringValue("192.168.001.001"),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
"invalid ipv4 - out of range": {
|
||||||
|
val: types.StringValue("192.168.1.256"),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
"invalid ipv4 - incomplete": {
|
||||||
|
val: types.StringValue("192.168.1"),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
"invalid ipv4 - ipv6": {
|
||||||
|
val: types.StringValue("::1"),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
"invalid ipv4 - characters": {
|
||||||
|
val: types.StringValue("not-an-ip"),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, test := range tests {
|
||||||
|
name, test := name, test
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
req := validator.StringRequest{
|
||||||
|
ConfigValue: test.val,
|
||||||
|
}
|
||||||
|
resp := validator.StringResponse{}
|
||||||
|
validators.IPv4().ValidateString(context.Background(), req, &resp)
|
||||||
|
|
||||||
|
if !test.expectError && resp.Diagnostics.HasError() {
|
||||||
|
t.Fatalf("got unexpected error: %s", resp.Diagnostics.Errors()[0].Detail())
|
||||||
|
}
|
||||||
|
|
||||||
|
if test.expectError && !resp.Diagnostics.HasError() {
|
||||||
|
t.Fatalf("expected error but got none")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
151
internal/provider/validators/required_none_if.go
Normal file
151
internal/provider/validators/required_none_if.go
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
package validators
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/attr"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/ephemeral"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/provider"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ datasource.ConfigValidator = &RequiredNoneIfValidator{}
|
||||||
|
_ provider.ConfigValidator = &RequiredNoneIfValidator{}
|
||||||
|
_ resource.ConfigValidator = &RequiredNoneIfValidator{}
|
||||||
|
)
|
||||||
|
|
||||||
|
type RequiredNoneIfValidator struct {
|
||||||
|
ifValidatorBase
|
||||||
|
TargetExpressions path.Expressions
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v RequiredNoneIfValidator) Description(ctx context.Context) string {
|
||||||
|
return v.MarkdownDescription(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v RequiredNoneIfValidator) MarkdownDescription(_ context.Context) string {
|
||||||
|
if v.CheckOnlyIfSet {
|
||||||
|
return fmt.Sprintf("If %q is set, any of those attributes must not be configured: %s", v.ConditionPath, v.TargetExpressions)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("If %q equals %s, any of those attributes must not be configured: %s", v.ConditionPath, v.ConditionValue, v.TargetExpressions)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v RequiredNoneIfValidator) ValidateDataSource(ctx context.Context, req datasource.ValidateConfigRequest, resp *datasource.ValidateConfigResponse) {
|
||||||
|
resp.Diagnostics = v.Validate(ctx, req.Config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v RequiredNoneIfValidator) ValidateProvider(ctx context.Context, req provider.ValidateConfigRequest, resp *provider.ValidateConfigResponse) {
|
||||||
|
resp.Diagnostics = v.Validate(ctx, req.Config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v RequiredNoneIfValidator) ValidateResource(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
|
||||||
|
resp.Diagnostics = v.Validate(ctx, req.Config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v RequiredNoneIfValidator) ValidateEphemeralResource(ctx context.Context, req ephemeral.ValidateConfigRequest, resp *ephemeral.ValidateConfigResponse) {
|
||||||
|
resp.Diagnostics = v.Validate(ctx, req.Config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v RequiredNoneIfValidator) Validate(ctx context.Context, config tfsdk.Config) diag.Diagnostics {
|
||||||
|
diags := diag.Diagnostics{}
|
||||||
|
if !v.shouldValidate(ctx, config) {
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
|
// Condition matched, now apply the RequiredNone validation
|
||||||
|
configuredPaths := path.Paths{}
|
||||||
|
foundPaths := path.Paths{}
|
||||||
|
unknownPaths := path.Paths{}
|
||||||
|
|
||||||
|
// Check that all target attributes are present
|
||||||
|
for _, expression := range v.TargetExpressions {
|
||||||
|
matchedPaths, matchedPathsDiags := config.PathMatches(ctx, expression)
|
||||||
|
diags.Append(matchedPathsDiags...)
|
||||||
|
|
||||||
|
// Collect all errors
|
||||||
|
if matchedPathsDiags.HasError() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture all matched paths
|
||||||
|
foundPaths.Append(matchedPaths...)
|
||||||
|
|
||||||
|
for _, matchedPath := range matchedPaths {
|
||||||
|
var value attr.Value
|
||||||
|
getAttributeDiags := config.GetAttribute(ctx, matchedPath, &value)
|
||||||
|
diags.Append(getAttributeDiags...)
|
||||||
|
|
||||||
|
// Collect all errors
|
||||||
|
if getAttributeDiags.HasError() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// If value is unknown, collect the path to skip validation later
|
||||||
|
if value.IsUnknown() {
|
||||||
|
unknownPaths.Append(matchedPath)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// If value is null, move onto the next one
|
||||||
|
if value.IsNull() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value is known and not null, it is configured
|
||||||
|
configuredPaths.Append(matchedPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(unknownPaths) > 0 {
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
|
// If configured paths does not equal all matched paths, then something
|
||||||
|
// was missing
|
||||||
|
if len(configuredPaths) > 0 {
|
||||||
|
diags.Append(validatordiag.InvalidAttributeCombinationDiagnostic(
|
||||||
|
foundPaths[0],
|
||||||
|
v.Description(ctx),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateString method to implement the validator.String interface
|
||||||
|
func (v RequiredNoneIfValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) {
|
||||||
|
resp.Diagnostics.Append(v.Validate(ctx, req.Config)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequiredNoneIf creates a validator for attributes that ensures
|
||||||
|
// a set of target attributes are not configured together if a condition attribute equals a specific value.
|
||||||
|
func RequiredNoneIf(conditionPath path.Expression, conditionValue attr.Value, targetExpressions ...path.Expression) RequiredNoneIfValidator {
|
||||||
|
return RequiredNoneIfValidator{
|
||||||
|
ifValidatorBase: ifValidatorBase{
|
||||||
|
ConditionPath: conditionPath,
|
||||||
|
ConditionValue: conditionValue,
|
||||||
|
CheckOnlyIfSet: false,
|
||||||
|
},
|
||||||
|
TargetExpressions: targetExpressions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequiredNoneIfSet creates a validator that ensures a set of target attributes
|
||||||
|
// are configured not together if a condition attribute is set (not null), regardless of its value.
|
||||||
|
func RequiredNoneIfSet(conditionPath path.Expression, targetExpressions ...path.Expression) RequiredNoneIfValidator {
|
||||||
|
return RequiredNoneIfValidator{
|
||||||
|
ifValidatorBase: ifValidatorBase{
|
||||||
|
ConditionPath: conditionPath,
|
||||||
|
CheckOnlyIfSet: true,
|
||||||
|
},
|
||||||
|
TargetExpressions: targetExpressions,
|
||||||
|
}
|
||||||
|
}
|
||||||
532
internal/provider/validators/required_none_if_test.go
Normal file
532
internal/provider/validators/required_none_if_test.go
Normal file
@@ -0,0 +1,532 @@
|
|||||||
|
package validators_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/provider"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
"github.com/hashicorp/terraform-plugin-go/tftypes"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/filipowm/terraform-provider-unifi/internal/provider/validators"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Common test case structure for string conditions
|
||||||
|
type requiredNoneIfTestCase struct {
|
||||||
|
condition types.String
|
||||||
|
field1 types.String
|
||||||
|
field2 types.String
|
||||||
|
expectError bool
|
||||||
|
expectErrorText string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common test case structure for bool conditions
|
||||||
|
type requiredNoneIfBoolTestCase struct {
|
||||||
|
condition types.Bool
|
||||||
|
field1 types.String
|
||||||
|
field2 types.String
|
||||||
|
expectError bool
|
||||||
|
expectErrorText string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to create a schema object with string condition
|
||||||
|
func createRequiredNoneIfSchema() schema.Schema {
|
||||||
|
return schema.Schema{
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"condition": schema.StringAttribute{
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"field1": schema.StringAttribute{
|
||||||
|
Optional: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"field2": schema.StringAttribute{
|
||||||
|
Optional: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to create a schema object with bool condition
|
||||||
|
func createRequiredNoneIfBoolSchema() schema.Schema {
|
||||||
|
return schema.Schema{
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"condition": schema.BoolAttribute{
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"field1": schema.StringAttribute{
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"field2": schema.StringAttribute{
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to create a config with string condition
|
||||||
|
func createRequiredNoneIfConfig(schema schema.Schema, testCase requiredNoneIfTestCase) tfsdk.Config {
|
||||||
|
return tfsdk.Config{
|
||||||
|
Schema: schema,
|
||||||
|
Raw: tftypes.NewValue(
|
||||||
|
tftypes.Object{
|
||||||
|
AttributeTypes: map[string]tftypes.Type{
|
||||||
|
"condition": tftypes.String,
|
||||||
|
"field1": tftypes.String,
|
||||||
|
"field2": tftypes.String,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
map[string]tftypes.Value{
|
||||||
|
"condition": stringToTfValue(testCase.condition),
|
||||||
|
"field1": stringToTfValue(testCase.field1),
|
||||||
|
"field2": stringToTfValue(testCase.field2),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to create a config with bool condition
|
||||||
|
func createRequiredNoneIfBoolConfig(schema schema.Schema, testCase requiredNoneIfBoolTestCase) tfsdk.Config {
|
||||||
|
return tfsdk.Config{
|
||||||
|
Schema: schema,
|
||||||
|
Raw: tftypes.NewValue(
|
||||||
|
tftypes.Object{
|
||||||
|
AttributeTypes: map[string]tftypes.Type{
|
||||||
|
"condition": tftypes.Bool,
|
||||||
|
"field1": tftypes.String,
|
||||||
|
"field2": tftypes.String,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
map[string]tftypes.Value{
|
||||||
|
"condition": boolToTfValue(testCase.condition),
|
||||||
|
"field1": stringToTfValue(testCase.field1),
|
||||||
|
"field2": stringToTfValue(testCase.field2),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test RequiredNoneIf with string condition
|
||||||
|
func TestRequiredNoneIf(t *testing.T) {
|
||||||
|
testCases := map[string]requiredNoneIfTestCase{
|
||||||
|
"matching_condition_all_configured": {
|
||||||
|
condition: types.StringValue("test"),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
field2: types.StringValue("value2"),
|
||||||
|
expectError: true,
|
||||||
|
expectErrorText: "If \"condition\" equals \"test\", any of those attributes must not be configured: [field1,field2]",
|
||||||
|
},
|
||||||
|
"matching_condition_one_configured": {
|
||||||
|
condition: types.StringValue("test"),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
field2: types.StringNull(),
|
||||||
|
expectError: true,
|
||||||
|
expectErrorText: "If \"condition\" equals \"test\", any of those attributes must not be configured: [field1,field2]",
|
||||||
|
},
|
||||||
|
"matching_condition_none_configured": {
|
||||||
|
condition: types.StringValue("test"),
|
||||||
|
field1: types.StringNull(),
|
||||||
|
field2: types.StringNull(),
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
"non_matching_condition_all_configured": {
|
||||||
|
condition: types.StringValue("non-test"),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
field2: types.StringValue("value2"),
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
"matching_condition_unknown_values": {
|
||||||
|
condition: types.StringValue("test"),
|
||||||
|
field1: types.StringUnknown(),
|
||||||
|
field2: types.StringValue("value2"),
|
||||||
|
expectError: false, // Unknown values should skip validation
|
||||||
|
},
|
||||||
|
"null_condition_all_configured": {
|
||||||
|
condition: types.StringNull(),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
field2: types.StringValue("value2"),
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
"unknown_condition_all_configured": {
|
||||||
|
condition: types.StringUnknown(),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
field2: types.StringValue("value2"),
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
schema := createRequiredNoneIfSchema()
|
||||||
|
config := createRequiredNoneIfConfig(schema, testCase)
|
||||||
|
|
||||||
|
validator := validators.RequiredNoneIf(
|
||||||
|
path.MatchRoot("condition"),
|
||||||
|
types.StringValue("test"),
|
||||||
|
path.MatchRoot("field1"),
|
||||||
|
path.MatchRoot("field2"),
|
||||||
|
)
|
||||||
|
|
||||||
|
diagnostics := validator.Validate(ctx, config)
|
||||||
|
|
||||||
|
if testCase.expectError {
|
||||||
|
assert.True(t, diagnostics.HasError())
|
||||||
|
if testCase.expectErrorText != "" {
|
||||||
|
found := false
|
||||||
|
for _, diag := range diagnostics {
|
||||||
|
if diag.Detail() != "" && diag.Detail() == testCase.expectErrorText {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(t, found, "Expected error text not found")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assert.False(t, diagnostics.HasError())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test RequiredNoneIfSet with string condition
|
||||||
|
func TestRequiredNoneIfSet(t *testing.T) {
|
||||||
|
testCases := map[string]requiredNoneIfTestCase{
|
||||||
|
"condition_set_all_configured": {
|
||||||
|
condition: types.StringValue("any-value"),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
field2: types.StringValue("value2"),
|
||||||
|
expectError: true,
|
||||||
|
expectErrorText: "If \"condition\" is set, any of those attributes must not be configured: [field1,field2]",
|
||||||
|
},
|
||||||
|
"condition_set_one_configured": {
|
||||||
|
condition: types.StringValue("any-value"),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
field2: types.StringNull(),
|
||||||
|
expectError: true,
|
||||||
|
expectErrorText: "If \"condition\" is set, any of those attributes must not be configured: [field1,field2]",
|
||||||
|
},
|
||||||
|
"condition_set_none_configured": {
|
||||||
|
condition: types.StringValue("any-value"),
|
||||||
|
field1: types.StringNull(),
|
||||||
|
field2: types.StringNull(),
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
"condition_null_all_configured": {
|
||||||
|
condition: types.StringNull(),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
field2: types.StringValue("value2"),
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
"condition_unknown_all_configured": {
|
||||||
|
condition: types.StringUnknown(),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
field2: types.StringValue("value2"),
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
schema := createRequiredNoneIfSchema()
|
||||||
|
config := createRequiredNoneIfConfig(schema, testCase)
|
||||||
|
|
||||||
|
validator := validators.RequiredNoneIfSet(
|
||||||
|
path.MatchRoot("condition"),
|
||||||
|
path.MatchRoot("field1"),
|
||||||
|
path.MatchRoot("field2"),
|
||||||
|
)
|
||||||
|
|
||||||
|
diagnostics := validator.Validate(ctx, config)
|
||||||
|
|
||||||
|
if testCase.expectError {
|
||||||
|
assert.True(t, diagnostics.HasError())
|
||||||
|
if testCase.expectErrorText != "" {
|
||||||
|
found := false
|
||||||
|
for _, diag := range diagnostics {
|
||||||
|
if diag.Detail() != "" && diag.Detail() == testCase.expectErrorText {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(t, found, "Expected error text not found")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assert.False(t, diagnostics.HasError())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test RequiredNoneIf with boolean condition
|
||||||
|
func TestRequiredNoneIfWithBoolCondition(t *testing.T) {
|
||||||
|
testCases := map[string]requiredNoneIfBoolTestCase{
|
||||||
|
"matching_true_condition_all_configured": {
|
||||||
|
condition: types.BoolValue(true),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
field2: types.StringValue("value2"),
|
||||||
|
expectError: true,
|
||||||
|
expectErrorText: "If \"condition\" equals true, any of those attributes must not be configured: [field1,field2]",
|
||||||
|
},
|
||||||
|
"matching_true_condition_none_configured": {
|
||||||
|
condition: types.BoolValue(true),
|
||||||
|
field1: types.StringNull(),
|
||||||
|
field2: types.StringNull(),
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
"non_matching_false_condition_all_configured": {
|
||||||
|
condition: types.BoolValue(false),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
field2: types.StringValue("value2"),
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
schema := createRequiredNoneIfBoolSchema()
|
||||||
|
config := createRequiredNoneIfBoolConfig(schema, testCase)
|
||||||
|
|
||||||
|
validator := validators.RequiredNoneIf(
|
||||||
|
path.MatchRoot("condition"),
|
||||||
|
types.BoolValue(true),
|
||||||
|
path.MatchRoot("field1"),
|
||||||
|
path.MatchRoot("field2"),
|
||||||
|
)
|
||||||
|
|
||||||
|
diagnostics := validator.Validate(ctx, config)
|
||||||
|
|
||||||
|
if testCase.expectError {
|
||||||
|
assert.True(t, diagnostics.HasError())
|
||||||
|
if testCase.expectErrorText != "" {
|
||||||
|
found := false
|
||||||
|
for _, diag := range diagnostics {
|
||||||
|
if diag.Detail() != "" && diag.Detail() == testCase.expectErrorText {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(t, found, "Expected error text not found")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assert.False(t, diagnostics.HasError())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test ValidateDataSource method
|
||||||
|
func TestRequiredNoneIfValidateDataSource(t *testing.T) {
|
||||||
|
testCases := map[string]requiredNoneIfTestCase{
|
||||||
|
"matching_condition_all_configured": {
|
||||||
|
condition: types.StringValue("test"),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
field2: types.StringValue("value2"),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
"matching_condition_none_configured": {
|
||||||
|
condition: types.StringValue("test"),
|
||||||
|
field1: types.StringNull(),
|
||||||
|
field2: types.StringNull(),
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
schema := createRequiredNoneIfSchema()
|
||||||
|
config := createRequiredNoneIfConfig(schema, testCase)
|
||||||
|
|
||||||
|
validator := validators.RequiredNoneIf(
|
||||||
|
path.MatchRoot("condition"),
|
||||||
|
types.StringValue("test"),
|
||||||
|
path.MatchRoot("field1"),
|
||||||
|
path.MatchRoot("field2"),
|
||||||
|
)
|
||||||
|
|
||||||
|
request := datasource.ValidateConfigRequest{
|
||||||
|
Config: config,
|
||||||
|
}
|
||||||
|
|
||||||
|
response := &datasource.ValidateConfigResponse{}
|
||||||
|
|
||||||
|
validator.ValidateDataSource(ctx, request, response)
|
||||||
|
|
||||||
|
if testCase.expectError {
|
||||||
|
assert.True(t, response.Diagnostics.HasError())
|
||||||
|
} else {
|
||||||
|
assert.False(t, response.Diagnostics.HasError())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test ValidateProvider method
|
||||||
|
func TestRequiredNoneIfValidateProvider(t *testing.T) {
|
||||||
|
testCases := map[string]requiredNoneIfTestCase{
|
||||||
|
"matching_condition_all_configured": {
|
||||||
|
condition: types.StringValue("test"),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
field2: types.StringValue("value2"),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
"matching_condition_none_configured": {
|
||||||
|
condition: types.StringValue("test"),
|
||||||
|
field1: types.StringNull(),
|
||||||
|
field2: types.StringNull(),
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
schema := createRequiredNoneIfSchema()
|
||||||
|
config := createRequiredNoneIfConfig(schema, testCase)
|
||||||
|
|
||||||
|
validator := validators.RequiredNoneIf(
|
||||||
|
path.MatchRoot("condition"),
|
||||||
|
types.StringValue("test"),
|
||||||
|
path.MatchRoot("field1"),
|
||||||
|
path.MatchRoot("field2"),
|
||||||
|
)
|
||||||
|
|
||||||
|
request := provider.ValidateConfigRequest{
|
||||||
|
Config: config,
|
||||||
|
}
|
||||||
|
|
||||||
|
response := &provider.ValidateConfigResponse{}
|
||||||
|
|
||||||
|
validator.ValidateProvider(ctx, request, response)
|
||||||
|
|
||||||
|
if testCase.expectError {
|
||||||
|
assert.True(t, response.Diagnostics.HasError())
|
||||||
|
} else {
|
||||||
|
assert.False(t, response.Diagnostics.HasError())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test ValidateResource method
|
||||||
|
func TestRequiredNoneIfValidateResource(t *testing.T) {
|
||||||
|
testCases := map[string]requiredNoneIfTestCase{
|
||||||
|
"matching_condition_all_configured": {
|
||||||
|
condition: types.StringValue("test"),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
field2: types.StringValue("value2"),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
"matching_condition_none_configured": {
|
||||||
|
condition: types.StringValue("test"),
|
||||||
|
field1: types.StringNull(),
|
||||||
|
field2: types.StringNull(),
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, testCase := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
schema := createRequiredNoneIfSchema()
|
||||||
|
config := createRequiredNoneIfConfig(schema, testCase)
|
||||||
|
|
||||||
|
validator := validators.RequiredNoneIf(
|
||||||
|
path.MatchRoot("condition"),
|
||||||
|
types.StringValue("test"),
|
||||||
|
path.MatchRoot("field1"),
|
||||||
|
path.MatchRoot("field2"),
|
||||||
|
)
|
||||||
|
|
||||||
|
request := resource.ValidateConfigRequest{
|
||||||
|
Config: config,
|
||||||
|
}
|
||||||
|
|
||||||
|
response := &resource.ValidateConfigResponse{}
|
||||||
|
|
||||||
|
validator.ValidateResource(ctx, request, response)
|
||||||
|
|
||||||
|
if testCase.expectError {
|
||||||
|
assert.True(t, response.Diagnostics.HasError())
|
||||||
|
} else {
|
||||||
|
assert.False(t, response.Diagnostics.HasError())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test the Description and MarkdownDescription methods for both variants
|
||||||
|
func TestRequiredNoneIfDescription(t *testing.T) {
|
||||||
|
t.Run("RequiredNoneIf description", func(t *testing.T) {
|
||||||
|
validator := validators.RequiredNoneIf(
|
||||||
|
path.MatchRoot("condition"),
|
||||||
|
types.StringValue("test"),
|
||||||
|
path.MatchRoot("field1"),
|
||||||
|
path.MatchRoot("field2"),
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
desc := validator.Description(ctx)
|
||||||
|
mdDesc := validator.MarkdownDescription(ctx)
|
||||||
|
|
||||||
|
expectedDesc := `If "condition" equals "test", any of those attributes must not be configured: [field1,field2]`
|
||||||
|
assert.Equal(t, expectedDesc, desc)
|
||||||
|
assert.Equal(t, expectedDesc, mdDesc)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("RequiredNoneIfSet description", func(t *testing.T) {
|
||||||
|
validator := validators.RequiredNoneIfSet(
|
||||||
|
path.MatchRoot("condition"),
|
||||||
|
path.MatchRoot("field1"),
|
||||||
|
path.MatchRoot("field2"),
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
desc := validator.Description(ctx)
|
||||||
|
mdDesc := validator.MarkdownDescription(ctx)
|
||||||
|
|
||||||
|
expectedDesc := `If "condition" is set, any of those attributes must not be configured: [field1,field2]`
|
||||||
|
assert.Equal(t, expectedDesc, desc)
|
||||||
|
assert.Equal(t, expectedDesc, mdDesc)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with missing path
|
||||||
|
func TestRequiredNoneIfWithMissingPath(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
schema := createRequiredNoneIfSchema()
|
||||||
|
testCase := requiredNoneIfTestCase{
|
||||||
|
condition: types.StringValue("test"),
|
||||||
|
field1: types.StringValue("value1"),
|
||||||
|
field2: types.StringValue("value2"),
|
||||||
|
}
|
||||||
|
config := createRequiredNoneIfConfig(schema, testCase)
|
||||||
|
|
||||||
|
validator := validators.RequiredNoneIf(
|
||||||
|
path.MatchRoot("non_existent"),
|
||||||
|
types.StringValue("test"),
|
||||||
|
path.MatchRoot("field1"),
|
||||||
|
path.MatchRoot("field2"),
|
||||||
|
)
|
||||||
|
|
||||||
|
diagnostics := validator.Validate(ctx, config)
|
||||||
|
|
||||||
|
// Should not get an error because the condition path doesn't match
|
||||||
|
assert.False(t, diagnostics.HasError())
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
package validators
|
package validators_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/filipowm/terraform-provider-unifi/internal/provider/validators"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -26,7 +27,7 @@ func TestStringLengthExactlyValidation(t *testing.T) {
|
|||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(fmt.Sprintf("%s-expected-length-%d", tc.value, tc.length), func(t *testing.T) {
|
t.Run(fmt.Sprintf("%s-expected-length-%d", tc.value, tc.length), func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
v := StringLengthExactly(tc.length)
|
v := validators.StringLengthExactly(tc.length)
|
||||||
req, resp := newStringValidatorRequestResponse(tc.value)
|
req, resp := newStringValidatorRequestResponse(tc.value)
|
||||||
v.ValidateString(context.Background(), req, resp)
|
v.ValidateString(context.Background(), req, resp)
|
||||||
assert.Equal(t, tc.validationFailed, resp.Diagnostics.HasError())
|
assert.Equal(t, tc.validationFailed, resp.Diagnostics.HasError())
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
package validators
|
package validators_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"github.com/filipowm/terraform-provider-unifi/internal/provider/validators"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
||||||
@@ -71,7 +72,7 @@ func TestTimezoneValidator(t *testing.T) {
|
|||||||
ConfigValue: test.val,
|
ConfigValue: test.val,
|
||||||
}
|
}
|
||||||
response := validator.StringResponse{}
|
response := validator.StringResponse{}
|
||||||
Timezone().ValidateString(context.Background(), request, &response)
|
validators.Timezone().ValidateString(context.Background(), request, &response)
|
||||||
|
|
||||||
if !response.Diagnostics.HasError() && test.expectError {
|
if !response.Diagnostics.HasError() && test.expectError {
|
||||||
t.Fatal("expected error, got no error")
|
t.Fatal("expected error, got no error")
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
package validators
|
package validators_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"github.com/filipowm/terraform-provider-unifi/internal/provider/validators"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
||||||
@@ -59,7 +60,7 @@ func TestURLValidator(t *testing.T) {
|
|||||||
ConfigValue: test.val,
|
ConfigValue: test.val,
|
||||||
}
|
}
|
||||||
response := validator.StringResponse{}
|
response := validator.StringResponse{}
|
||||||
URL().ValidateString(context.Background(), request, &response)
|
validators.URL().ValidateString(context.Background(), request, &response)
|
||||||
|
|
||||||
if !response.Diagnostics.HasError() && test.expectError {
|
if !response.Diagnostics.HasError() && test.expectError {
|
||||||
t.Fatal("expected error, got no error")
|
t.Fatal("expected error, got no error")
|
||||||
@@ -114,7 +115,7 @@ func TestHTTPSURLValidator(t *testing.T) {
|
|||||||
ConfigValue: test.val,
|
ConfigValue: test.val,
|
||||||
}
|
}
|
||||||
response := validator.StringResponse{}
|
response := validator.StringResponse{}
|
||||||
HTTPSUrl().ValidateString(context.Background(), request, &response)
|
validators.HTTPSUrl().ValidateString(context.Background(), request, &response)
|
||||||
|
|
||||||
if !response.Diagnostics.HasError() && test.expectError {
|
if !response.Diagnostics.HasError() && test.expectError {
|
||||||
t.Fatal("expected error, got no error")
|
t.Fatal("expected error, got no error")
|
||||||
|
|||||||
Reference in New Issue
Block a user