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)
|
||||
}
|
||||
@@ -178,6 +178,7 @@ func (p *unifiProvider) Resources(_ context.Context) []func() resource.Resource
|
||||
settings.NewCountryResource,
|
||||
settings.NewLocaleResource,
|
||||
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 (
|
||||
"context"
|
||||
"github.com/filipowm/terraform-provider-unifi/internal/provider/validators"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
@@ -25,7 +26,7 @@ func TestCountryCodeValidation(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
v := countryCodeAlpha2Validator{}
|
||||
v := validators.CountryCodeAlpha2()
|
||||
req, resp := newStringValidatorRequestResponse(tc.code)
|
||||
v.ValidateString(context.Background(), req, resp)
|
||||
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 (
|
||||
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
"github.com/hashicorp/terraform-plugin-go/tftypes"
|
||||
)
|
||||
|
||||
func newStringValidatorRequestResponse(value string) (validator.StringRequest, *validator.StringResponse) {
|
||||
@@ -17,3 +18,23 @@ func newStringValidatorRequestResponse(value string) (validator.StringRequest, *
|
||||
}
|
||||
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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/filipowm/terraform-provider-unifi/internal/provider/validators"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
@@ -26,7 +27,7 @@ func TestStringLengthExactlyValidation(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("%s-expected-length-%d", tc.value, tc.length), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
v := StringLengthExactly(tc.length)
|
||||
v := validators.StringLengthExactly(tc.length)
|
||||
req, resp := newStringValidatorRequestResponse(tc.value)
|
||||
v.ValidateString(context.Background(), req, resp)
|
||||
assert.Equal(t, tc.validationFailed, resp.Diagnostics.HasError())
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
package validators
|
||||
package validators_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/filipowm/terraform-provider-unifi/internal/provider/validators"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
||||
@@ -71,7 +72,7 @@ func TestTimezoneValidator(t *testing.T) {
|
||||
ConfigValue: test.val,
|
||||
}
|
||||
response := validator.StringResponse{}
|
||||
Timezone().ValidateString(context.Background(), request, &response)
|
||||
validators.Timezone().ValidateString(context.Background(), request, &response)
|
||||
|
||||
if !response.Diagnostics.HasError() && test.expectError {
|
||||
t.Fatal("expected error, got no error")
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
package validators
|
||||
package validators_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/filipowm/terraform-provider-unifi/internal/provider/validators"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
||||
@@ -59,7 +60,7 @@ func TestURLValidator(t *testing.T) {
|
||||
ConfigValue: test.val,
|
||||
}
|
||||
response := validator.StringResponse{}
|
||||
URL().ValidateString(context.Background(), request, &response)
|
||||
validators.URL().ValidateString(context.Background(), request, &response)
|
||||
|
||||
if !response.Diagnostics.HasError() && test.expectError {
|
||||
t.Fatal("expected error, got no error")
|
||||
@@ -114,7 +115,7 @@ func TestHTTPSURLValidator(t *testing.T) {
|
||||
ConfigValue: test.val,
|
||||
}
|
||||
response := validator.StringResponse{}
|
||||
HTTPSUrl().ValidateString(context.Background(), request, &response)
|
||||
validators.HTTPSUrl().ValidateString(context.Background(), request, &response)
|
||||
|
||||
if !response.Diagnostics.HasError() && test.expectError {
|
||||
t.Fatal("expected error, got no error")
|
||||
|
||||
Reference in New Issue
Block a user