feat: support complete USG resource (#44)
* feat: add support for UPNP and Geo IP filtering to USG settings resource * feat: support complete USG settings resource * fix messages in required_together_if.go * improve docs of USG resource * tests: require version at least 9.0 for unbind_wan_monitors * feat: require version at least 8.5 for dns_verification * fix: use go-unifi 1.5.2 to fix NTP * require 7.0 or later for timeout preference * require 7.0 or later for geo IP filtering
This commit is contained in:
committed by
GitHub
parent
35c74bf59d
commit
fcea1e0ba4
10
go.mod
10
go.mod
@@ -10,8 +10,8 @@ require (
|
||||
github.com/apparentlymart/go-cidr v1.1.0
|
||||
github.com/biter777/countries v1.7.5
|
||||
github.com/deckarep/golang-set/v2 v2.7.0
|
||||
github.com/filipowm/go-unifi v1.5.0
|
||||
github.com/golangci/golangci-lint v1.64.5
|
||||
github.com/filipowm/go-unifi v1.5.2
|
||||
github.com/golangci/golangci-lint v1.64.6
|
||||
github.com/hashicorp/go-version v1.7.0
|
||||
github.com/hashicorp/terraform-plugin-docs v0.21.0
|
||||
github.com/hashicorp/terraform-plugin-framework v1.14.1
|
||||
@@ -27,7 +27,7 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
4d63.com/gocheckcompilerdirectives v1.2.1 // indirect
|
||||
4d63.com/gocheckcompilerdirectives v1.3.0 // indirect
|
||||
4d63.com/gochecknoglobals v0.2.2 // indirect
|
||||
dario.cat/mergo v1.0.1 // indirect
|
||||
github.com/4meepo/tagalign v1.4.2 // indirect
|
||||
@@ -219,7 +219,7 @@ require (
|
||||
github.com/karamaru-alpha/copyloopvar v1.2.1 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/kisielk/errcheck v1.9.0 // indirect
|
||||
github.com/kkHAIKE/contextcheck v1.1.5 // indirect
|
||||
github.com/kkHAIKE/contextcheck v1.1.6 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/kulti/thelper v0.6.3 // indirect
|
||||
github.com/kunwardeep/paralleltest v1.0.10 // indirect
|
||||
@@ -299,7 +299,7 @@ require (
|
||||
github.com/r3labs/sse v0.0.0-20210224172625-26fe804710bc // indirect
|
||||
github.com/raeperd/recvcheck v0.2.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/ryancurrah/gomodguard v1.3.5 // indirect
|
||||
github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect
|
||||
github.com/sagikazarmark/locafero v0.7.0 // indirect
|
||||
|
||||
8
go.sum
8
go.sum
@@ -1,5 +1,6 @@
|
||||
4d63.com/gocheckcompilerdirectives v1.2.1 h1:AHcMYuw56NPjq/2y615IGg2kYkBdTvOaojYCBcRE7MA=
|
||||
4d63.com/gocheckcompilerdirectives v1.2.1/go.mod h1:yjDJSxmDTtIHHCqX0ufRYZDL6vQtMG7tJdKVeWwsqvs=
|
||||
4d63.com/gocheckcompilerdirectives v1.3.0/go.mod h1:ofsJ4zx2QAuIP/NO/NAh1ig6R1Fb18/GI7RVMwz7kAY=
|
||||
4d63.com/gochecknoglobals v0.2.2 h1:H1vdnwnMaZdQW/N+NrkT1SZMTBmcwHe9Vq8lJcYYTtU=
|
||||
4d63.com/gochecknoglobals v0.2.2/go.mod h1:lLxwTQjL5eIesRbvnzIP3jZtG140FnTdz+AlMa+ogt0=
|
||||
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||
@@ -278,6 +279,10 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/filipowm/go-unifi v1.5.0 h1:DgnPKA23urg4jG3k2jaoigbk1Mt22QB0nNE/ItgNZTU=
|
||||
github.com/filipowm/go-unifi v1.5.0/go.mod h1:dr+YNQ1Y2EjOTttAkWAMWMU/J8iv+FueAxZKmptnVEI=
|
||||
github.com/filipowm/go-unifi v1.5.1 h1:dXdu4ei3ta+r8TAXc2CxPGEdBW01Bxhf1sxBgDreeWk=
|
||||
github.com/filipowm/go-unifi v1.5.1/go.mod h1:ldtL5szykvR9fPWB9GlGZqaJGxKd8IWLQY5M8O1PDQ8=
|
||||
github.com/filipowm/go-unifi v1.5.2 h1:S974Fi3BWt7qm0P8G/SwUVOjzgWTSjNJGV0jW5yD6K0=
|
||||
github.com/filipowm/go-unifi v1.5.2/go.mod h1:ldtL5szykvR9fPWB9GlGZqaJGxKd8IWLQY5M8O1PDQ8=
|
||||
github.com/firefart/nonamedreturns v1.0.5 h1:tM+Me2ZaXs8tfdDw3X6DOX++wMCOqzYUho6tUTYIdRA=
|
||||
github.com/firefart/nonamedreturns v1.0.5/go.mod h1:gHJjDqhGM4WyPt639SOZs+G89Ko7QKH5R5BhnO6xJhw=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
@@ -391,6 +396,7 @@ github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d h1:viFft9sS/dxoYY0a
|
||||
github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d/go.mod h1:ivJ9QDg0XucIkmwhzCDsqcnxxlDStoTl89jDMIoNxKY=
|
||||
github.com/golangci/golangci-lint v1.64.5 h1:5omC86XFBKXZgCrVdUWU+WNHKd+CWCxNx717KXnzKZY=
|
||||
github.com/golangci/golangci-lint v1.64.5/go.mod h1:WZnwq8TF0z61h3jLQ7Sk5trcP7b3kUFxLD6l1ivtdvU=
|
||||
github.com/golangci/golangci-lint v1.64.6/go.mod h1:Wz9q+6EVuqGQ94GQ96RB2mjpcZYTOGhBhbt4O7REPu4=
|
||||
github.com/golangci/misspell v0.6.0 h1:JCle2HUTNWirNlDIAUO44hUsKhOFqGPoC4LZxlaSXDs=
|
||||
github.com/golangci/misspell v0.6.0/go.mod h1:keMNyY6R9isGaSAu+4Q8NMBwMPkh15Gtc8UCVoDtAWo=
|
||||
github.com/golangci/plugin-module-register v0.1.1 h1:TCmesur25LnyJkpsVrupv1Cdzo+2f7zX0H6Jkw1Ol6c=
|
||||
@@ -565,6 +571,7 @@ github.com/kisielk/errcheck v1.9.0/go.mod h1:kQxWMMVZgIkDq7U8xtG/n2juOjbLgZtedi0
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kkHAIKE/contextcheck v1.1.5 h1:CdnJh63tcDe53vG+RebdpdXJTc9atMgGqdx8LXxiilg=
|
||||
github.com/kkHAIKE/contextcheck v1.1.5/go.mod h1:O930cpht4xb1YQpK+1+AgoM3mFsvxr7uyFptcnWTYUA=
|
||||
github.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
@@ -809,6 +816,7 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.14.0 h1:unbRd941gNa8SS77YznHXOYVBDgWcF9xhzECdm8juZc=
|
||||
github.com/rogpeppe/go-internal v1.14.0/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryancurrah/gomodguard v1.3.5 h1:cShyguSwUEeC0jS7ylOiG/idnd1TpJ1LfHGpV3oJmPU=
|
||||
github.com/ryancurrah/gomodguard v1.3.5/go.mod h1:MXlEPQRxgfPQa62O8wzK3Ozbkv9Rkqr+wKjSxTdsNJE=
|
||||
|
||||
@@ -50,7 +50,7 @@ func TestAccSettingUsg_mdns_v7(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccSettingUsg_dhcpRelay(t *testing.T) {
|
||||
func TestAccSettingUsg_dhcpRelayServers(t *testing.T) {
|
||||
AcceptanceTest(t, AcceptanceTestCase{
|
||||
Lock: &settingUsgLock,
|
||||
Steps: []resource.TestStep{
|
||||
@@ -81,6 +81,495 @@ func TestAccSettingUsg_site(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccSettingUsg_geoIpFiltering(t *testing.T) {
|
||||
AcceptanceTest(t, AcceptanceTestCase{
|
||||
VersionConstraint: ">= 7",
|
||||
Lock: &settingUsgLock,
|
||||
Steps: []resource.TestStep{
|
||||
{
|
||||
Config: testAccSettingUsgConfig_geoIpFilteringBasic(),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "geo_ip_filtering.enabled", "true"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "geo_ip_filtering.block", "block"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "geo_ip_filtering.traffic_direction", "both"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "geo_ip_filtering.countries.#", "3"),
|
||||
resource.TestCheckTypeSetElemAttr("unifi_setting_usg.test", "geo_ip_filtering.countries.*", "RU"),
|
||||
resource.TestCheckTypeSetElemAttr("unifi_setting_usg.test", "geo_ip_filtering.countries.*", "CN"),
|
||||
resource.TestCheckTypeSetElemAttr("unifi_setting_usg.test", "geo_ip_filtering.countries.*", "KP"),
|
||||
),
|
||||
},
|
||||
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||
{
|
||||
Config: testAccSettingUsgConfig_geoIpFilteringAllow(),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "geo_ip_filtering.enabled", "true"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "geo_ip_filtering.block", "allow"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "geo_ip_filtering.traffic_direction", "both"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "geo_ip_filtering.countries.#", "3"),
|
||||
resource.TestCheckTypeSetElemAttr("unifi_setting_usg.test", "geo_ip_filtering.countries.*", "US"),
|
||||
resource.TestCheckTypeSetElemAttr("unifi_setting_usg.test", "geo_ip_filtering.countries.*", "CA"),
|
||||
resource.TestCheckTypeSetElemAttr("unifi_setting_usg.test", "geo_ip_filtering.countries.*", "GB"),
|
||||
),
|
||||
},
|
||||
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||
{
|
||||
Config: testAccSettingUsgConfig_geoIpFilteringDirections(),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "geo_ip_filtering.enabled", "true"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "geo_ip_filtering.block", "block"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "geo_ip_filtering.traffic_direction", "ingress"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "geo_ip_filtering.countries.#", "2"),
|
||||
resource.TestCheckTypeSetElemAttr("unifi_setting_usg.test", "geo_ip_filtering.countries.*", "RU"),
|
||||
resource.TestCheckTypeSetElemAttr("unifi_setting_usg.test", "geo_ip_filtering.countries.*", "CN"),
|
||||
),
|
||||
},
|
||||
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||
{
|
||||
Config: testAccSettingUsgConfig_geoIpFilteringDisabled(),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "geo_ip_filtering.enabled", "false"),
|
||||
),
|
||||
},
|
||||
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||
{
|
||||
Config: testAccSettingUsgConfig_geoIpFilteringBasic(),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "geo_ip_filtering.enabled", "true"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "geo_ip_filtering.block", "block"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "geo_ip_filtering.traffic_direction", "both"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "geo_ip_filtering.countries.#", "3"),
|
||||
resource.TestCheckTypeSetElemAttr("unifi_setting_usg.test", "geo_ip_filtering.countries.*", "RU"),
|
||||
resource.TestCheckTypeSetElemAttr("unifi_setting_usg.test", "geo_ip_filtering.countries.*", "CN"),
|
||||
resource.TestCheckTypeSetElemAttr("unifi_setting_usg.test", "geo_ip_filtering.countries.*", "KP"),
|
||||
),
|
||||
},
|
||||
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccSettingUsg_upnp(t *testing.T) {
|
||||
AcceptanceTest(t, AcceptanceTestCase{
|
||||
Lock: &settingUsgLock,
|
||||
Steps: []resource.TestStep{
|
||||
{
|
||||
Config: testAccSettingUsgConfig_upnpBasic(),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "upnp.enabled", "true"),
|
||||
),
|
||||
},
|
||||
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||
{
|
||||
Config: testAccSettingUsgConfig_upnpAdvanced(),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "upnp.enabled", "true"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "upnp.nat_pmp_enabled", "true"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "upnp.secure_mode", "true"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "upnp.wan_interface", "WAN"),
|
||||
),
|
||||
},
|
||||
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||
{
|
||||
Config: testAccSettingUsgConfig_upnpDisabled(),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "upnp.enabled", "false"),
|
||||
),
|
||||
},
|
||||
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccSettingUsg_dnsVerification(t *testing.T) {
|
||||
AcceptanceTest(t, AcceptanceTestCase{
|
||||
VersionConstraint: ">= 8.5",
|
||||
Lock: &settingUsgLock,
|
||||
Steps: []resource.TestStep{
|
||||
{
|
||||
Config: testAccSettingUsgConfig_dnsVerification(),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttrSet("unifi_setting_usg.test", "dns_verification.domain"),
|
||||
resource.TestCheckResourceAttrSet("unifi_setting_usg.test", "dns_verification.primary_dns_server"),
|
||||
resource.TestCheckResourceAttrSet("unifi_setting_usg.test", "dns_verification.secondary_dns_server"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "dns_verification.setting_preference", "auto"),
|
||||
),
|
||||
},
|
||||
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||
{
|
||||
Config: testAccSettingUsgConfig_dnsVerificationUpdated(),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "dns_verification.domain", "example.com"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "dns_verification.primary_dns_server", "1.1.1.1"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "dns_verification.secondary_dns_server", "1.0.0.1"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "dns_verification.setting_preference", "manual"),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
func TestAccSettingUsg_tcpTimeouts(t *testing.T) {
|
||||
AcceptanceTest(t, AcceptanceTestCase{
|
||||
Lock: &settingUsgLock,
|
||||
Steps: []resource.TestStep{
|
||||
{
|
||||
Config: testAccSettingUsgConfig_tcpTimeouts(),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "tcp_timeouts.close_timeout", "10"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "tcp_timeouts.established_timeout", "3600"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "tcp_timeouts.close_wait_timeout", "20"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "tcp_timeouts.fin_wait_timeout", "30"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "tcp_timeouts.last_ack_timeout", "30"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "tcp_timeouts.syn_recv_timeout", "60"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "tcp_timeouts.syn_sent_timeout", "120"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "tcp_timeouts.time_wait_timeout", "120"),
|
||||
),
|
||||
},
|
||||
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||
{
|
||||
Config: testAccSettingUsgConfig_tcpTimeoutsUpdated(),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "tcp_timeouts.close_timeout", "20"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "tcp_timeouts.established_timeout", "7200"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "tcp_timeouts.close_wait_timeout", "40"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "tcp_timeouts.fin_wait_timeout", "60"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "tcp_timeouts.last_ack_timeout", "60"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "tcp_timeouts.syn_recv_timeout", "120"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "tcp_timeouts.syn_sent_timeout", "240"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "tcp_timeouts.time_wait_timeout", "240"),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccSettingUsg_arpCache(t *testing.T) {
|
||||
AcceptanceTest(t, AcceptanceTestCase{
|
||||
Lock: &settingUsgLock,
|
||||
Steps: []resource.TestStep{
|
||||
{
|
||||
Config: testAccSettingUsgConfig_arpCache(),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "arp_cache_base_reachable", "60"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "arp_cache_timeout", "custom"),
|
||||
),
|
||||
},
|
||||
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||
{
|
||||
Config: testAccSettingUsgConfig_arpCacheUpdated(),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "arp_cache_base_reachable", "120"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "arp_cache_timeout", "normal"),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccSettingUsg_dhcpConfig(t *testing.T) {
|
||||
AcceptanceTest(t, AcceptanceTestCase{
|
||||
Lock: &settingUsgLock,
|
||||
Steps: []resource.TestStep{
|
||||
{
|
||||
Config: testAccSettingUsgConfig_dhcpConfig(),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "broadcast_ping", "true"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "dhcpd_hostfile_update", "true"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "dhcpd_use_dnsmasq", "true"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "dnsmasq_all_servers", "true"),
|
||||
),
|
||||
},
|
||||
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||
{
|
||||
Config: testAccSettingUsgConfig_dhcpConfigUpdated(),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "broadcast_ping", "false"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "dhcpd_hostfile_update", "false"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "dhcpd_use_dnsmasq", "false"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "dnsmasq_all_servers", "false"),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccSettingUsg_dhcpRelayConfig(t *testing.T) {
|
||||
AcceptanceTest(t, AcceptanceTestCase{
|
||||
Lock: &settingUsgLock,
|
||||
Steps: []resource.TestStep{
|
||||
{
|
||||
Config: testAccSettingUsgConfig_dhcpRelayConfig(),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "dhcp_relay.agents_packets", "forward"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "dhcp_relay.hop_count", "5"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "dhcp_relay.max_size", "1400"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "dhcp_relay.port", "67"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "dhcp_relay_servers.#", "2"),
|
||||
resource.TestCheckTypeSetElemAttr("unifi_setting_usg.test", "dhcp_relay_servers.*", "10.1.2.3"),
|
||||
resource.TestCheckTypeSetElemAttr("unifi_setting_usg.test", "dhcp_relay_servers.*", "10.1.2.4"),
|
||||
),
|
||||
},
|
||||
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||
{
|
||||
Config: testAccSettingUsgConfig_dhcpRelayConfigUpdated(),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "dhcp_relay.agents_packets", "replace"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "dhcp_relay.hop_count", "10"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "dhcp_relay.max_size", "64"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "dhcp_relay.port", "68"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "dhcp_relay_servers.#", "3"),
|
||||
resource.TestCheckTypeSetElemAttr("unifi_setting_usg.test", "dhcp_relay_servers.*", "10.1.2.5"),
|
||||
resource.TestCheckTypeSetElemAttr("unifi_setting_usg.test", "dhcp_relay_servers.*", "10.1.2.6"),
|
||||
resource.TestCheckTypeSetElemAttr("unifi_setting_usg.test", "dhcp_relay_servers.*", "10.1.2.7"),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccSettingUsg_networkTools(t *testing.T) {
|
||||
AcceptanceTest(t, AcceptanceTestCase{
|
||||
Lock: &settingUsgLock,
|
||||
Steps: []resource.TestStep{
|
||||
{
|
||||
Config: testAccSettingUsgConfig_networkTools(),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "echo_server", "echo.example.com"),
|
||||
),
|
||||
},
|
||||
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccSettingUsg_protocolModules(t *testing.T) {
|
||||
AcceptanceTest(t, AcceptanceTestCase{
|
||||
Lock: &settingUsgLock,
|
||||
Steps: []resource.TestStep{
|
||||
{
|
||||
Config: testAccSettingUsgConfig_protocolModules(),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "ftp_module", "true"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "gre_module", "true"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "h323_module", "true"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "pptp_module", "true"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "sip_module", "true"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "tftp_module", "true"),
|
||||
),
|
||||
},
|
||||
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||
{
|
||||
Config: testAccSettingUsgConfig_protocolModulesUpdated(),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "ftp_module", "false"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "gre_module", "true"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "h323_module", "false"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "pptp_module", "true"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "sip_module", "true"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "tftp_module", "false"),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccSettingUsg_icmpAndLldp(t *testing.T) {
|
||||
AcceptanceTest(t, AcceptanceTestCase{
|
||||
Lock: &settingUsgLock,
|
||||
Steps: []resource.TestStep{
|
||||
{
|
||||
Config: testAccSettingUsgConfig_icmpAndLldp(),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "icmp_timeout", "60"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "lldp_enable_all", "true"),
|
||||
),
|
||||
},
|
||||
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||
{
|
||||
Config: testAccSettingUsgConfig_icmpAndLldpUpdated(),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "icmp_timeout", "120"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "lldp_enable_all", "false"),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccSettingUsg_mssClamp(t *testing.T) {
|
||||
AcceptanceTest(t, AcceptanceTestCase{
|
||||
Lock: &settingUsgLock,
|
||||
Steps: []resource.TestStep{
|
||||
{
|
||||
Config: testAccSettingUsgConfig_mssClamp(),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "mss_clamp", "auto"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "mss_clamp_mss", "1452"),
|
||||
),
|
||||
},
|
||||
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||
{
|
||||
Config: testAccSettingUsgConfig_mssClampUpdated(),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "mss_clamp", "custom"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "mss_clamp_mss", "1400"),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccSettingUsg_offloadSettings(t *testing.T) {
|
||||
AcceptanceTest(t, AcceptanceTestCase{
|
||||
Lock: &settingUsgLock,
|
||||
Steps: []resource.TestStep{
|
||||
{
|
||||
Config: testAccSettingUsgConfig_offloadSettings(),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "offload_accounting", "true"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "offload_l2_blocking", "true"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "offload_sch", "true"),
|
||||
),
|
||||
},
|
||||
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||
{
|
||||
Config: testAccSettingUsgConfig_offloadSettingsUpdated(),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "offload_accounting", "false"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "offload_l2_blocking", "false"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "offload_sch", "false"),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccSettingUsg_timeoutSettings(t *testing.T) {
|
||||
AcceptanceTest(t, AcceptanceTestCase{
|
||||
VersionConstraint: ">= 7",
|
||||
Lock: &settingUsgLock,
|
||||
Steps: []resource.TestStep{
|
||||
{
|
||||
Config: testAccSettingUsgConfig_timeoutSettings(),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "other_timeout", "600"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "timeout_setting_preference", "auto"),
|
||||
),
|
||||
},
|
||||
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||
{
|
||||
Config: testAccSettingUsgConfig_timeoutSettingsUpdated(),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "other_timeout", "1200"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "timeout_setting_preference", "manual"),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccSettingUsg_redirectsAndSecurity(t *testing.T) {
|
||||
AcceptanceTest(t, AcceptanceTestCase{
|
||||
Lock: &settingUsgLock,
|
||||
Steps: []resource.TestStep{
|
||||
{
|
||||
Config: testAccSettingUsgConfig_redirectsAndSecurity(),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "receive_redirects", "false"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "send_redirects", "true"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "syn_cookies", "true"),
|
||||
),
|
||||
},
|
||||
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||
{
|
||||
Config: testAccSettingUsgConfig_redirectsAndSecurityUpdated(),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "receive_redirects", "true"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "send_redirects", "false"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "syn_cookies", "false"),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccSettingUsg_udp(t *testing.T) {
|
||||
AcceptanceTest(t, AcceptanceTestCase{
|
||||
Lock: &settingUsgLock,
|
||||
Steps: []resource.TestStep{
|
||||
{
|
||||
Config: testAccSettingUsgConfig_udp(),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "udp_other_timeout", "30"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "udp_stream_timeout", "120"),
|
||||
),
|
||||
},
|
||||
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||
{
|
||||
Config: testAccSettingUsgConfig_udpUpdated(),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "udp_other_timeout", "60"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "udp_stream_timeout", "240"),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccSettingUsg_unbindWanMonitor(t *testing.T) {
|
||||
AcceptanceTest(t, AcceptanceTestCase{
|
||||
VersionConstraint: ">= 9",
|
||||
Lock: &settingUsgLock,
|
||||
Steps: []resource.TestStep{
|
||||
{
|
||||
Config: testAccSettingUsgConfig_unbindWanMonitor(true),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "unbind_wan_monitors", "true"),
|
||||
),
|
||||
},
|
||||
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||
{
|
||||
Config: testAccSettingUsgConfig_unbindWanMonitor(false),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "unbind_wan_monitors", "false"),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccSettingUsg_comprehensive(t *testing.T) {
|
||||
AcceptanceTest(t, AcceptanceTestCase{
|
||||
VersionConstraint: ">= 7",
|
||||
Lock: &settingUsgLock,
|
||||
Steps: []resource.TestStep{
|
||||
{
|
||||
Config: testAccSettingUsgConfig_comprehensive(),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
// ARP Cache
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "arp_cache_base_reachable", "60"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "arp_cache_timeout", "custom"),
|
||||
|
||||
// DHCP Config
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "broadcast_ping", "true"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "dhcpd_hostfile_update", "true"),
|
||||
|
||||
// Protocol Modules (sample)
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "ftp_module", "true"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "tftp_module", "true"),
|
||||
|
||||
// Timeouts
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "other_timeout", "600"),
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "udp_stream_timeout", "120"),
|
||||
|
||||
// Security
|
||||
resource.TestCheckResourceAttr("unifi_setting_usg.test", "syn_cookies", "true"),
|
||||
),
|
||||
},
|
||||
pt.ImportStepWithSite("unifi_setting_usg.test"),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func testAccSettingUsgConfig_mdns(mdns bool) string {
|
||||
return fmt.Sprintf(`
|
||||
resource "unifi_setting_usg" "test" {
|
||||
@@ -111,3 +600,443 @@ resource "unifi_setting_usg" "test" {
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
func testAccSettingUsgConfig_geoIpFilteringBasic() string {
|
||||
return `
|
||||
resource "unifi_setting_usg" "test" {
|
||||
geo_ip_filtering = {
|
||||
enabled = true
|
||||
countries = ["RU", "CN", "KP"]
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
func testAccSettingUsgConfig_geoIpFilteringAllow() string {
|
||||
return `
|
||||
resource "unifi_setting_usg" "test" {
|
||||
geo_ip_filtering = {
|
||||
enabled = true
|
||||
block = "allow"
|
||||
countries = ["US", "CA", "GB"]
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
func testAccSettingUsgConfig_geoIpFilteringDirections() string {
|
||||
return `
|
||||
resource "unifi_setting_usg" "test" {
|
||||
geo_ip_filtering = {
|
||||
enabled = true
|
||||
traffic_direction = "ingress"
|
||||
countries = ["RU", "CN"]
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
func testAccSettingUsgConfig_geoIpFilteringDisabled() string {
|
||||
return `
|
||||
resource "unifi_setting_usg" "test" {
|
||||
geo_ip_filtering = {
|
||||
enabled = false
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
func testAccSettingUsgConfig_upnpBasic() string {
|
||||
return `
|
||||
resource "unifi_setting_usg" "test" {
|
||||
upnp = {
|
||||
enabled = true
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
func testAccSettingUsgConfig_upnpAdvanced() string {
|
||||
return `
|
||||
resource "unifi_setting_usg" "test" {
|
||||
upnp = {
|
||||
enabled = true
|
||||
nat_pmp_enabled = true
|
||||
secure_mode = true
|
||||
wan_interface = "WAN"
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
func testAccSettingUsgConfig_upnpDisabled() string {
|
||||
return `
|
||||
resource "unifi_setting_usg" "test" {
|
||||
upnp = {
|
||||
enabled = false
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
func testAccSettingUsgConfig_dnsVerification() string {
|
||||
return `
|
||||
resource "unifi_setting_usg" "test" {
|
||||
dns_verification = {
|
||||
setting_preference = "auto"
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
func testAccSettingUsgConfig_dnsVerificationUpdated() string {
|
||||
return `
|
||||
resource "unifi_setting_usg" "test" {
|
||||
dns_verification = {
|
||||
domain = "example.com"
|
||||
primary_dns_server = "1.1.1.1"
|
||||
secondary_dns_server = "1.0.0.1"
|
||||
setting_preference = "manual"
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
func testAccSettingUsgConfig_tcpTimeouts() string {
|
||||
return `
|
||||
resource "unifi_setting_usg" "test" {
|
||||
tcp_timeouts = {
|
||||
close_timeout = 10
|
||||
established_timeout = 3600
|
||||
close_wait_timeout = 20
|
||||
fin_wait_timeout = 30
|
||||
last_ack_timeout = 30
|
||||
syn_recv_timeout = 60
|
||||
syn_sent_timeout = 120
|
||||
time_wait_timeout = 120
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
func testAccSettingUsgConfig_tcpTimeoutsUpdated() string {
|
||||
return `
|
||||
resource "unifi_setting_usg" "test" {
|
||||
tcp_timeouts = {
|
||||
close_timeout = 20
|
||||
established_timeout = 7200
|
||||
close_wait_timeout = 40
|
||||
fin_wait_timeout = 60
|
||||
last_ack_timeout = 60
|
||||
syn_recv_timeout = 120
|
||||
syn_sent_timeout = 240
|
||||
time_wait_timeout = 240
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
func testAccSettingUsgConfig_arpCache() string {
|
||||
return `
|
||||
resource "unifi_setting_usg" "test" {
|
||||
arp_cache_base_reachable = 60
|
||||
arp_cache_timeout = "custom"
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
func testAccSettingUsgConfig_dhcpConfig() string {
|
||||
return `
|
||||
resource "unifi_setting_usg" "test" {
|
||||
broadcast_ping = true
|
||||
dhcpd_hostfile_update = true
|
||||
dhcpd_use_dnsmasq = true
|
||||
dnsmasq_all_servers = true
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
func testAccSettingUsgConfig_dhcpRelayConfig() string {
|
||||
return `
|
||||
resource "unifi_setting_usg" "test" {
|
||||
dhcp_relay = {
|
||||
agents_packets = "forward"
|
||||
hop_count = 5
|
||||
max_size = 1400
|
||||
port = 67
|
||||
}
|
||||
dhcp_relay_servers = ["10.1.2.3","10.1.2.4"]
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
func testAccSettingUsgConfig_dhcpRelayConfigUpdated() string {
|
||||
return `
|
||||
resource "unifi_setting_usg" "test" {
|
||||
dhcp_relay = {
|
||||
agents_packets = "replace"
|
||||
hop_count = 10
|
||||
max_size = 64
|
||||
port = 68
|
||||
}
|
||||
dhcp_relay_servers = ["10.1.2.5","10.1.2.6","10.1.2.7"]
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
func testAccSettingUsgConfig_networkTools() string {
|
||||
return `
|
||||
resource "unifi_setting_usg" "test" {
|
||||
echo_server = "echo.example.com"
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
func testAccSettingUsgConfig_protocolModules() string {
|
||||
return `
|
||||
resource "unifi_setting_usg" "test" {
|
||||
ftp_module = true
|
||||
gre_module = true
|
||||
h323_module = true
|
||||
pptp_module = true
|
||||
sip_module = true
|
||||
tftp_module = true
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
func testAccSettingUsgConfig_icmpAndLldp() string {
|
||||
return `
|
||||
resource "unifi_setting_usg" "test" {
|
||||
icmp_timeout = 60
|
||||
lldp_enable_all = true
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
func testAccSettingUsgConfig_mssClamp() string {
|
||||
return `
|
||||
resource "unifi_setting_usg" "test" {
|
||||
mss_clamp = "auto"
|
||||
mss_clamp_mss = 1452
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
func testAccSettingUsgConfig_offloadSettings() string {
|
||||
return `
|
||||
resource "unifi_setting_usg" "test" {
|
||||
offload_accounting = true
|
||||
offload_l2_blocking = true
|
||||
offload_sch = true
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
func testAccSettingUsgConfig_timeoutSettings() string {
|
||||
return `
|
||||
resource "unifi_setting_usg" "test" {
|
||||
other_timeout = 600
|
||||
timeout_setting_preference = "auto"
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
func testAccSettingUsgConfig_redirectsAndSecurity() string {
|
||||
return `
|
||||
resource "unifi_setting_usg" "test" {
|
||||
receive_redirects = false
|
||||
send_redirects = true
|
||||
syn_cookies = true
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
func testAccSettingUsgConfig_udp() string {
|
||||
return `
|
||||
resource "unifi_setting_usg" "test" {
|
||||
udp_other_timeout = 30
|
||||
udp_stream_timeout = 120
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
func testAccSettingUsgConfig_comprehensive() string {
|
||||
return `
|
||||
resource "unifi_setting_usg" "test" {
|
||||
// ARP Cache Configuration
|
||||
arp_cache_base_reachable = 60
|
||||
arp_cache_timeout = "custom"
|
||||
|
||||
// DHCP Configuration
|
||||
broadcast_ping = true
|
||||
dhcpd_hostfile_update = true
|
||||
dhcpd_use_dnsmasq = true
|
||||
dnsmasq_all_servers = true
|
||||
|
||||
// DHCP Relay
|
||||
dhcp_relay = {
|
||||
agents_packets = "forward"
|
||||
hop_count = 5
|
||||
}
|
||||
dhcp_relay_servers = ["10.1.2.3", "10.1.2.4"]
|
||||
|
||||
// Network Tools
|
||||
echo_server = "echo.example.com"
|
||||
|
||||
// Protocol Modules
|
||||
ftp_module = true
|
||||
gre_module = true
|
||||
tftp_module = true
|
||||
|
||||
// ICMP & LLDP
|
||||
icmp_timeout = 20
|
||||
lldp_enable_all = true
|
||||
|
||||
// MSS Clamp
|
||||
mss_clamp = "auto"
|
||||
mss_clamp_mss = 1452
|
||||
|
||||
// Offload Settings
|
||||
offload_accounting = true
|
||||
offload_l2_blocking = true
|
||||
|
||||
// Timeout Settings
|
||||
other_timeout = 600
|
||||
timeout_setting_preference = "auto"
|
||||
|
||||
// TCP Settings
|
||||
tcp_timeouts = {
|
||||
close_timeout = 10
|
||||
established_timeout = 3600
|
||||
close_wait_timeout = 20
|
||||
fin_wait_timeout = 30
|
||||
last_ack_timeout = 30
|
||||
syn_recv_timeout = 60
|
||||
syn_sent_timeout = 120
|
||||
time_wait_timeout = 120
|
||||
}
|
||||
|
||||
// Redirects & Security
|
||||
receive_redirects = false
|
||||
send_redirects = true
|
||||
syn_cookies = true
|
||||
|
||||
// UDP
|
||||
udp_other_timeout = 30
|
||||
udp_stream_timeout = 120
|
||||
|
||||
// Geo IP Filtering
|
||||
geo_ip_filtering = {
|
||||
enabled = true
|
||||
block = "block"
|
||||
countries = ["RU", "CN"]
|
||||
traffic_direction = "both"
|
||||
}
|
||||
|
||||
// UPNP Settings
|
||||
upnp = {
|
||||
enabled = true
|
||||
nat_pmp_enabled = true
|
||||
secure_mode = true
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
func testAccSettingUsgConfig_arpCacheUpdated() string {
|
||||
return `
|
||||
resource "unifi_setting_usg" "test" {
|
||||
arp_cache_base_reachable = 120
|
||||
arp_cache_timeout = "normal"
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
func testAccSettingUsgConfig_dhcpConfigUpdated() string {
|
||||
return `
|
||||
resource "unifi_setting_usg" "test" {
|
||||
broadcast_ping = false
|
||||
dhcpd_hostfile_update = false
|
||||
dhcpd_use_dnsmasq = false
|
||||
dnsmasq_all_servers = false
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
func testAccSettingUsgConfig_protocolModulesUpdated() string {
|
||||
return `
|
||||
resource "unifi_setting_usg" "test" {
|
||||
ftp_module = false
|
||||
gre_module = true
|
||||
h323_module = false
|
||||
pptp_module = true
|
||||
sip_module = true
|
||||
tftp_module = false
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
func testAccSettingUsgConfig_icmpAndLldpUpdated() string {
|
||||
return `
|
||||
resource "unifi_setting_usg" "test" {
|
||||
icmp_timeout = 120
|
||||
lldp_enable_all = false
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
func testAccSettingUsgConfig_mssClampUpdated() string {
|
||||
return `
|
||||
resource "unifi_setting_usg" "test" {
|
||||
mss_clamp = "custom"
|
||||
mss_clamp_mss = 1400
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
func testAccSettingUsgConfig_offloadSettingsUpdated() string {
|
||||
return `
|
||||
resource "unifi_setting_usg" "test" {
|
||||
offload_accounting = false
|
||||
offload_l2_blocking = false
|
||||
offload_sch = false
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
func testAccSettingUsgConfig_timeoutSettingsUpdated() string {
|
||||
return `
|
||||
resource "unifi_setting_usg" "test" {
|
||||
other_timeout = 1200
|
||||
timeout_setting_preference = "manual"
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
func testAccSettingUsgConfig_redirectsAndSecurityUpdated() string {
|
||||
return `
|
||||
resource "unifi_setting_usg" "test" {
|
||||
receive_redirects = true
|
||||
send_redirects = false
|
||||
syn_cookies = false
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
func testAccSettingUsgConfig_udpUpdated() string {
|
||||
return `
|
||||
resource "unifi_setting_usg" "test" {
|
||||
udp_other_timeout = 60
|
||||
udp_stream_timeout = 240
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
func testAccSettingUsgConfig_unbindWanMonitor(enabled bool) string {
|
||||
return fmt.Sprintf(`
|
||||
resource "unifi_setting_usg" "test" {
|
||||
unbind_wan_monitors = %t
|
||||
}
|
||||
`, enabled)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||
|
||||
@@ -22,8 +23,8 @@ type ResourceModel interface {
|
||||
GetID() string
|
||||
GetRawID() types.String
|
||||
SetID(string)
|
||||
AsUnifiModel() (interface{}, diag.Diagnostics)
|
||||
Merge(interface{}) diag.Diagnostics
|
||||
AsUnifiModel(context.Context) (interface{}, diag.Diagnostics)
|
||||
Merge(context.Context, interface{}) diag.Diagnostics
|
||||
}
|
||||
|
||||
type Model struct {
|
||||
|
||||
@@ -104,8 +104,8 @@ func (p *unifiProvider) Configure(ctx context.Context, req provider.ConfigureReq
|
||||
path.Root("api_url"),
|
||||
"Unknown UniFi Controller API URL",
|
||||
"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 "+
|
||||
"the configuration, or use the UNIFI_API environment variable.",
|
||||
"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.",
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -87,13 +87,14 @@ func (b *BaseSettingResource[T]) Create(ctx context.Context, req resource.Create
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
body, diags := plan.AsUnifiModel()
|
||||
site := b.client.ResolveSite(plan)
|
||||
|
||||
body, diags := plan.AsUnifiModel(ctx)
|
||||
|
||||
if diags.HasError() {
|
||||
resp.Diagnostics.Append(diags...)
|
||||
return
|
||||
}
|
||||
site := b.client.ResolveSite(plan)
|
||||
|
||||
res, err := b.updater(ctx, b.client, site, body)
|
||||
if err != nil {
|
||||
@@ -104,7 +105,7 @@ func (b *BaseSettingResource[T]) Create(ctx context.Context, req resource.Create
|
||||
resp.Diagnostics.AddError("Error creating settings", fmt.Sprintf("No %[1]s settings returned from the UniFi controller. %[1]s might not be supported on this controller", b.typeName))
|
||||
return
|
||||
}
|
||||
plan.Merge(res)
|
||||
plan.Merge(ctx, res)
|
||||
plan.SetSite(site)
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||
}
|
||||
@@ -128,7 +129,7 @@ func (b *BaseSettingResource[T]) read(ctx context.Context, site string, state T,
|
||||
return
|
||||
}
|
||||
if res != nil {
|
||||
state.Merge(res)
|
||||
state.Merge(ctx, res)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,7 +173,7 @@ func (b *BaseSettingResource[T]) Update(ctx context.Context, req resource.Update
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
body, diags := plan.AsUnifiModel()
|
||||
body, diags := plan.AsUnifiModel(ctx)
|
||||
if diags.HasError() {
|
||||
resp.Diagnostics.Append(diags...)
|
||||
return
|
||||
@@ -184,7 +185,7 @@ func (b *BaseSettingResource[T]) Update(ctx context.Context, req resource.Update
|
||||
resp.Diagnostics.AddError("Error updating settings", err.Error())
|
||||
return
|
||||
}
|
||||
state.Merge(res)
|
||||
state.Merge(ctx, res)
|
||||
state.SetSite(site)
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ type autoSpeedtestModel struct {
|
||||
Enabled types.Bool `tfsdk:"enabled"`
|
||||
}
|
||||
|
||||
func (d *autoSpeedtestModel) AsUnifiModel() (interface{}, diag.Diagnostics) {
|
||||
func (d *autoSpeedtestModel) AsUnifiModel(_ context.Context) (interface{}, diag.Diagnostics) {
|
||||
return &unifi.SettingAutoSpeedtest{
|
||||
ID: d.ID.ValueString(),
|
||||
CronExpr: d.CronExpression.ValueString(),
|
||||
@@ -32,7 +32,7 @@ func (d *autoSpeedtestModel) AsUnifiModel() (interface{}, diag.Diagnostics) {
|
||||
}, diag.Diagnostics{}
|
||||
}
|
||||
|
||||
func (d *autoSpeedtestModel) Merge(other interface{}) diag.Diagnostics {
|
||||
func (d *autoSpeedtestModel) Merge(_ context.Context, other interface{}) diag.Diagnostics {
|
||||
if typed, ok := other.(*unifi.SettingAutoSpeedtest); ok {
|
||||
d.ID = types.StringValue(typed.ID)
|
||||
d.CronExpression = types.StringValue(typed.CronExpr)
|
||||
|
||||
@@ -26,7 +26,7 @@ type countryModel struct {
|
||||
CodeNumeric types.Int32 `tfsdk:"code_numeric"`
|
||||
}
|
||||
|
||||
func (d *countryModel) AsUnifiModel() (interface{}, diag.Diagnostics) {
|
||||
func (d *countryModel) AsUnifiModel(_ context.Context) (interface{}, diag.Diagnostics) {
|
||||
code := countries.ByName(d.Code.ValueString())
|
||||
return &unifi.SettingCountry{
|
||||
ID: d.ID.ValueString(),
|
||||
@@ -34,7 +34,7 @@ func (d *countryModel) AsUnifiModel() (interface{}, diag.Diagnostics) {
|
||||
}, diag.Diagnostics{}
|
||||
}
|
||||
|
||||
func (d *countryModel) Merge(other interface{}) diag.Diagnostics {
|
||||
func (d *countryModel) Merge(_ context.Context, other interface{}) diag.Diagnostics {
|
||||
if typed, ok := other.(*unifi.SettingCountry); ok {
|
||||
d.ID = types.StringValue(typed.ID)
|
||||
code := countries.ByNumeric(typed.Code)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/filipowm/go-unifi/unifi"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -26,7 +27,7 @@ func TestSettingCountry_ProperCountryCodeMappingFromModel(t *testing.T) {
|
||||
model := countryModel{
|
||||
Code: types.StringValue(tc.code),
|
||||
}
|
||||
unifiModel, _ := model.AsUnifiModel()
|
||||
unifiModel, _ := model.AsUnifiModel(context.Background())
|
||||
typed, ok := unifiModel.(*unifi.SettingCountry)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, tc.expectedNumericCode, typed.Code)
|
||||
@@ -54,7 +55,7 @@ func TestSettingCountry_ProperCountryCodeMappingToModel(t *testing.T) {
|
||||
Code: tc.numericCode,
|
||||
}
|
||||
model := countryModel{}
|
||||
model.Merge(unifiModel)
|
||||
model.Merge(context.Background(), unifiModel)
|
||||
assert.Equal(t, tc.expectedCode, model.Code.ValueString())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ type localeModel struct {
|
||||
Timezone types.String `tfsdk:"timezone"`
|
||||
}
|
||||
|
||||
func (d *localeModel) AsUnifiModel() (interface{}, diag.Diagnostics) {
|
||||
func (d *localeModel) AsUnifiModel(_ context.Context) (interface{}, diag.Diagnostics) {
|
||||
diags := diag.Diagnostics{}
|
||||
|
||||
model := &unifi.SettingLocale{
|
||||
@@ -29,7 +29,7 @@ func (d *localeModel) AsUnifiModel() (interface{}, diag.Diagnostics) {
|
||||
return model, diags
|
||||
}
|
||||
|
||||
func (d *localeModel) Merge(other interface{}) diag.Diagnostics {
|
||||
func (d *localeModel) Merge(_ context.Context, other interface{}) diag.Diagnostics {
|
||||
diags := diag.Diagnostics{}
|
||||
|
||||
model, ok := other.(*unifi.SettingLocale)
|
||||
|
||||
@@ -16,7 +16,7 @@ type magicSiteToSiteVpnModel struct {
|
||||
Enabled types.Bool `tfsdk:"enabled"`
|
||||
}
|
||||
|
||||
func (d *magicSiteToSiteVpnModel) AsUnifiModel() (interface{}, diag.Diagnostics) {
|
||||
func (d *magicSiteToSiteVpnModel) AsUnifiModel(_ context.Context) (interface{}, diag.Diagnostics) {
|
||||
diags := diag.Diagnostics{}
|
||||
|
||||
model := &unifi.SettingMagicSiteToSiteVpn{
|
||||
@@ -27,7 +27,7 @@ func (d *magicSiteToSiteVpnModel) AsUnifiModel() (interface{}, diag.Diagnostics)
|
||||
return model, diags
|
||||
}
|
||||
|
||||
func (d *magicSiteToSiteVpnModel) Merge(other interface{}) diag.Diagnostics {
|
||||
func (d *magicSiteToSiteVpnModel) Merge(_ context.Context, other interface{}) diag.Diagnostics {
|
||||
diags := diag.Diagnostics{}
|
||||
|
||||
model, ok := other.(*unifi.SettingMagicSiteToSiteVpn)
|
||||
|
||||
@@ -16,7 +16,7 @@ type networkOptimizationModel struct {
|
||||
Enabled types.Bool `tfsdk:"enabled"`
|
||||
}
|
||||
|
||||
func (d *networkOptimizationModel) AsUnifiModel() (interface{}, diag.Diagnostics) {
|
||||
func (d *networkOptimizationModel) AsUnifiModel(_ context.Context) (interface{}, diag.Diagnostics) {
|
||||
diags := diag.Diagnostics{}
|
||||
|
||||
model := &unifi.SettingNetworkOptimization{
|
||||
@@ -27,7 +27,7 @@ func (d *networkOptimizationModel) AsUnifiModel() (interface{}, diag.Diagnostics
|
||||
return model, diags
|
||||
}
|
||||
|
||||
func (d *networkOptimizationModel) Merge(other interface{}) diag.Diagnostics {
|
||||
func (d *networkOptimizationModel) Merge(_ context.Context, other interface{}) diag.Diagnostics {
|
||||
diags := diag.Diagnostics{}
|
||||
|
||||
model, ok := other.(*unifi.SettingNetworkOptimization)
|
||||
|
||||
@@ -26,7 +26,7 @@ type ntpModel struct {
|
||||
Mode types.String `tfsdk:"mode"`
|
||||
}
|
||||
|
||||
func (d *ntpModel) AsUnifiModel() (interface{}, diag.Diagnostics) {
|
||||
func (d *ntpModel) AsUnifiModel(_ context.Context) (interface{}, diag.Diagnostics) {
|
||||
diags := diag.Diagnostics{}
|
||||
|
||||
model := &unifi.SettingNtp{
|
||||
@@ -56,7 +56,7 @@ func (d *ntpModel) AsUnifiModel() (interface{}, diag.Diagnostics) {
|
||||
return model, diags
|
||||
}
|
||||
|
||||
func (d *ntpModel) Merge(other interface{}) diag.Diagnostics {
|
||||
func (d *ntpModel) Merge(_ context.Context, other interface{}) diag.Diagnostics {
|
||||
diags := diag.Diagnostics{}
|
||||
|
||||
model, ok := other.(*unifi.SettingNtp)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/filipowm/go-unifi/unifi"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -21,7 +22,7 @@ func TestNtpModel_AsUnifiModel_Auto(t *testing.T) {
|
||||
model.ID = types.StringValue("test-id")
|
||||
|
||||
// Convert to UnifiModel
|
||||
unifiModel, diags := model.AsUnifiModel()
|
||||
unifiModel, diags := model.AsUnifiModel(context.Background())
|
||||
|
||||
// Verify no diagnostics errors
|
||||
assert.False(t, diags.HasError())
|
||||
@@ -138,7 +139,7 @@ func TestNtpModel_AsUnifiModel_Manual(t *testing.T) {
|
||||
model.ID = types.StringValue("test-id")
|
||||
|
||||
// Convert to UnifiModel
|
||||
unifiModel, diags := model.AsUnifiModel()
|
||||
unifiModel, diags := model.AsUnifiModel(context.Background())
|
||||
|
||||
// Verify no diagnostics errors
|
||||
assert.False(t, diags.HasError())
|
||||
|
||||
@@ -18,7 +18,7 @@ type sslInspectionModel struct {
|
||||
State types.String `tfsdk:"state"`
|
||||
}
|
||||
|
||||
func (d *sslInspectionModel) AsUnifiModel() (interface{}, diag.Diagnostics) {
|
||||
func (d *sslInspectionModel) AsUnifiModel(_ context.Context) (interface{}, diag.Diagnostics) {
|
||||
diags := diag.Diagnostics{}
|
||||
|
||||
model := &unifi.SettingSslInspection{
|
||||
@@ -29,7 +29,7 @@ func (d *sslInspectionModel) AsUnifiModel() (interface{}, diag.Diagnostics) {
|
||||
return model, diags
|
||||
}
|
||||
|
||||
func (d *sslInspectionModel) Merge(other interface{}) diag.Diagnostics {
|
||||
func (d *sslInspectionModel) Merge(_ context.Context, other interface{}) diag.Diagnostics {
|
||||
diags := diag.Diagnostics{}
|
||||
|
||||
model, ok := other.(*unifi.SettingSslInspection)
|
||||
|
||||
@@ -19,7 +19,7 @@ type teleportModel struct {
|
||||
Subnet types.String `tfsdk:"subnet"`
|
||||
}
|
||||
|
||||
func (d *teleportModel) AsUnifiModel() (interface{}, diag.Diagnostics) {
|
||||
func (d *teleportModel) AsUnifiModel(_ context.Context) (interface{}, diag.Diagnostics) {
|
||||
diags := diag.Diagnostics{}
|
||||
|
||||
model := &unifi.SettingTeleport{
|
||||
@@ -31,7 +31,7 @@ func (d *teleportModel) AsUnifiModel() (interface{}, diag.Diagnostics) {
|
||||
return model, diags
|
||||
}
|
||||
|
||||
func (d *teleportModel) Merge(other interface{}) diag.Diagnostics {
|
||||
func (d *teleportModel) Merge(_ context.Context, other interface{}) diag.Diagnostics {
|
||||
diags := diag.Diagnostics{}
|
||||
|
||||
model, ok := other.(*unifi.SettingTeleport)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,6 +20,7 @@ var (
|
||||
_ datasource.ConfigValidator = &RequiredNoneIfValidator{}
|
||||
_ provider.ConfigValidator = &RequiredNoneIfValidator{}
|
||||
_ resource.ConfigValidator = &RequiredNoneIfValidator{}
|
||||
_ validator.Object = &RequiredNoneIfValidator{}
|
||||
)
|
||||
|
||||
type RequiredNoneIfValidator struct {
|
||||
@@ -27,6 +28,10 @@ type RequiredNoneIfValidator struct {
|
||||
TargetExpressions path.Expressions
|
||||
}
|
||||
|
||||
func (v RequiredNoneIfValidator) ValidateObject(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) {
|
||||
resp.Diagnostics = v.Validate(ctx, req.Config)
|
||||
}
|
||||
|
||||
func (v RequiredNoneIfValidator) Description(ctx context.Context) string {
|
||||
return v.MarkdownDescription(ctx)
|
||||
}
|
||||
|
||||
194
internal/provider/validators/required_together_if.go
Normal file
194
internal/provider/validators/required_together_if.go
Normal file
@@ -0,0 +1,194 @@
|
||||
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 = &RequiredTogetherIfValidator{}
|
||||
_ provider.ConfigValidator = &RequiredTogetherIfValidator{}
|
||||
_ resource.ConfigValidator = &RequiredTogetherIfValidator{}
|
||||
_ validator.Object = &RequiredTogetherIfValidator{}
|
||||
_ validator.String = &RequiredTogetherIfValidator{}
|
||||
_ validator.Bool = &RequiredTogetherIfValidator{}
|
||||
)
|
||||
|
||||
type RequiredTogetherIfValidator struct {
|
||||
ConditionPath path.Expression
|
||||
ConditionValue attr.Value
|
||||
TargetExpressions path.Expressions
|
||||
CheckOnlyIfSet bool // When true, only checks if the condition value is set (not null), not its actual value
|
||||
}
|
||||
|
||||
func (v RequiredTogetherIfValidator) ValidateBool(ctx context.Context, req validator.BoolRequest, resp *validator.BoolResponse) {
|
||||
resp.Diagnostics = v.Validate(ctx, req.Config)
|
||||
}
|
||||
|
||||
func (v RequiredTogetherIfValidator) Description(ctx context.Context) string {
|
||||
return v.MarkdownDescription(ctx)
|
||||
}
|
||||
|
||||
func (v RequiredTogetherIfValidator) MarkdownDescription(_ context.Context) string {
|
||||
if v.CheckOnlyIfSet {
|
||||
return fmt.Sprintf("If %s is set, these attributes must be configured together: %s", v.ConditionPath, v.TargetExpressions)
|
||||
}
|
||||
return fmt.Sprintf("If %s equals %s, these attributes must be configured together: %s", v.ConditionPath, v.ConditionValue, v.TargetExpressions)
|
||||
}
|
||||
|
||||
func (v RequiredTogetherIfValidator) ValidateDataSource(ctx context.Context, req datasource.ValidateConfigRequest, resp *datasource.ValidateConfigResponse) {
|
||||
resp.Diagnostics = v.Validate(ctx, req.Config)
|
||||
}
|
||||
|
||||
func (v RequiredTogetherIfValidator) ValidateProvider(ctx context.Context, req provider.ValidateConfigRequest, resp *provider.ValidateConfigResponse) {
|
||||
resp.Diagnostics = v.Validate(ctx, req.Config)
|
||||
}
|
||||
|
||||
func (v RequiredTogetherIfValidator) ValidateResource(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
|
||||
resp.Diagnostics = v.Validate(ctx, req.Config)
|
||||
}
|
||||
|
||||
func (v RequiredTogetherIfValidator) ValidateEphemeralResource(ctx context.Context, req ephemeral.ValidateConfigRequest, resp *ephemeral.ValidateConfigResponse) {
|
||||
resp.Diagnostics = v.Validate(ctx, req.Config)
|
||||
}
|
||||
|
||||
func (v RequiredTogetherIfValidator) ValidateObject(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) {
|
||||
resp.Diagnostics.Append(v.Validate(ctx, req.Config)...)
|
||||
}
|
||||
|
||||
func (v RequiredTogetherIfValidator) 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)
|
||||
}
|
||||
|
||||
func (v RequiredTogetherIfValidator) Validate(ctx context.Context, config tfsdk.Config) diag.Diagnostics {
|
||||
diags := diag.Diagnostics{}
|
||||
if !v.shouldValidate(ctx, config) {
|
||||
return diags
|
||||
}
|
||||
|
||||
// Condition matched, now apply the RequiredTogether 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)
|
||||
}
|
||||
}
|
||||
|
||||
// Return early if all paths were null
|
||||
//if len(configuredPaths) == 0 {
|
||||
// return diags
|
||||
//}
|
||||
|
||||
// If there are unknown values, we cannot know if the validator should
|
||||
// succeed or not
|
||||
if len(unknownPaths) > 0 {
|
||||
return diags
|
||||
}
|
||||
|
||||
// If configured paths does not equal all matched paths, then something
|
||||
// was missing
|
||||
if len(configuredPaths) != len(foundPaths) {
|
||||
diags.Append(validatordiag.InvalidAttributeCombinationDiagnostic(
|
||||
foundPaths[0],
|
||||
v.Description(ctx),
|
||||
))
|
||||
}
|
||||
|
||||
return diags
|
||||
}
|
||||
|
||||
// ValidateString method to implement the validator.String interface
|
||||
func (v RequiredTogetherIfValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) {
|
||||
resp.Diagnostics.Append(v.Validate(ctx, req.Config)...)
|
||||
}
|
||||
|
||||
// RequiredTogetherIf creates a validator for string type attributes that ensures
|
||||
// a set of target attributes are configured together if a condition attribute equals a specific value.
|
||||
func RequiredTogetherIf(conditionPath path.Expression, conditionValue attr.Value, targetExpressions ...path.Expression) RequiredTogetherIfValidator {
|
||||
return RequiredTogetherIfValidator{
|
||||
ConditionPath: conditionPath,
|
||||
ConditionValue: conditionValue,
|
||||
TargetExpressions: targetExpressions,
|
||||
CheckOnlyIfSet: false,
|
||||
}
|
||||
}
|
||||
|
||||
// RequiredTogetherIfSet creates a validator that ensures a set of target attributes
|
||||
// are configured together if a condition attribute is set (not null), regardless of its value.
|
||||
func RequiredTogetherIfSet(conditionPath path.Expression, targetExpressions ...path.Expression) RequiredTogetherIfValidator {
|
||||
return RequiredTogetherIfValidator{
|
||||
ConditionPath: conditionPath,
|
||||
ConditionValue: nil, // Not used for this validator
|
||||
TargetExpressions: targetExpressions,
|
||||
CheckOnlyIfSet: true,
|
||||
}
|
||||
}
|
||||
875
internal/provider/validators/required_together_if_test.go
Normal file
875
internal/provider/validators/required_together_if_test.go
Normal file
@@ -0,0 +1,875 @@
|
||||
package validators_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/provider"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||
"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"
|
||||
)
|
||||
|
||||
// Helper function to convert types.Int32 to tftypes.Value
|
||||
func numberToTfValue(value types.Int32) tftypes.Value {
|
||||
if value.IsNull() {
|
||||
return tftypes.NewValue(tftypes.Number, nil)
|
||||
} else if value.IsUnknown() {
|
||||
return tftypes.NewValue(tftypes.Number, tftypes.UnknownValue)
|
||||
}
|
||||
|
||||
return tftypes.NewValue(tftypes.Number, float64(value.ValueInt32()))
|
||||
}
|
||||
|
||||
// Common test case structure for string conditions
|
||||
type requiredTogetherIfStringConditionTestCase struct {
|
||||
condition types.String
|
||||
field1 types.String
|
||||
field2 types.String
|
||||
expectError bool
|
||||
expectErrorText string
|
||||
}
|
||||
|
||||
// Common test case structure for bool conditions
|
||||
type requiredTogetherIfBoolConditionTestCase struct {
|
||||
condition types.Bool
|
||||
field1 types.String
|
||||
field2 types.String
|
||||
expectError bool
|
||||
expectErrorText string
|
||||
}
|
||||
|
||||
// Common test case structure for int32 conditions
|
||||
type int32ConditionTestCase struct {
|
||||
condition types.Int32
|
||||
field1 types.String
|
||||
field2 types.String
|
||||
expectError bool
|
||||
expectErrorText string
|
||||
}
|
||||
|
||||
// Function to create a schema object with string condition
|
||||
func createRequiredIfStringConditionSchema() 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 createRequiredIfBoolConditionSchema() 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 schema object with int32 condition
|
||||
func createInt32ConditionSchema() schema.Schema {
|
||||
return schema.Schema{
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"condition": schema.Int32Attribute{
|
||||
Optional: true,
|
||||
},
|
||||
"field1": schema.StringAttribute{
|
||||
Optional: true,
|
||||
},
|
||||
"field2": schema.StringAttribute{
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Function to create a config with string condition
|
||||
func createRequiredIfStringConditionConfig(schema schema.Schema, testCase requiredTogetherIfStringConditionTestCase) 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 createRequiredIfBoolConditionConfig(schema schema.Schema, testCase requiredTogetherIfBoolConditionTestCase) 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),
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// Function to create a config with int32 condition
|
||||
func createInt32ConditionConfig(schema schema.Schema, testCase int32ConditionTestCase) tfsdk.Config {
|
||||
return tfsdk.Config{
|
||||
Schema: schema,
|
||||
Raw: tftypes.NewValue(
|
||||
tftypes.Object{
|
||||
AttributeTypes: map[string]tftypes.Type{
|
||||
"condition": tftypes.Number,
|
||||
"field1": tftypes.String,
|
||||
"field2": tftypes.String,
|
||||
},
|
||||
},
|
||||
map[string]tftypes.Value{
|
||||
"condition": numberToTfValue(testCase.condition),
|
||||
"field1": stringToTfValue(testCase.field1),
|
||||
"field2": stringToTfValue(testCase.field2),
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// Function to verify validation results
|
||||
func verifyValidationResults(t *testing.T, response interface{}, testCase interface{}) {
|
||||
t.Helper()
|
||||
|
||||
var hasDiagnosticError bool
|
||||
var diagnosticErrors []interface{}
|
||||
|
||||
// Extract diagnostic errors based on the type
|
||||
switch d := response.(type) {
|
||||
case resource.ValidateConfigResponse:
|
||||
hasDiagnosticError = d.Diagnostics.HasError()
|
||||
if hasDiagnosticError {
|
||||
diagnosticErrors = []interface{}{d.Diagnostics.Errors()[0]}
|
||||
}
|
||||
case *resource.ValidateConfigResponse:
|
||||
hasDiagnosticError = d.Diagnostics.HasError()
|
||||
if hasDiagnosticError {
|
||||
diagnosticErrors = []interface{}{d.Diagnostics.Errors()[0]}
|
||||
}
|
||||
case datasource.ValidateConfigResponse:
|
||||
hasDiagnosticError = d.Diagnostics.HasError()
|
||||
if hasDiagnosticError {
|
||||
diagnosticErrors = []interface{}{d.Diagnostics.Errors()[0]}
|
||||
}
|
||||
case *datasource.ValidateConfigResponse:
|
||||
hasDiagnosticError = d.Diagnostics.HasError()
|
||||
if hasDiagnosticError {
|
||||
diagnosticErrors = []interface{}{d.Diagnostics.Errors()[0]}
|
||||
}
|
||||
case provider.ValidateConfigResponse:
|
||||
hasDiagnosticError = d.Diagnostics.HasError()
|
||||
if hasDiagnosticError {
|
||||
diagnosticErrors = []interface{}{d.Diagnostics.Errors()[0]}
|
||||
}
|
||||
case *provider.ValidateConfigResponse:
|
||||
hasDiagnosticError = d.Diagnostics.HasError()
|
||||
if hasDiagnosticError {
|
||||
diagnosticErrors = []interface{}{d.Diagnostics.Errors()[0]}
|
||||
}
|
||||
case validator.StringResponse:
|
||||
hasDiagnosticError = d.Diagnostics.HasError()
|
||||
if hasDiagnosticError {
|
||||
diagnosticErrors = []interface{}{d.Diagnostics.Errors()[0]}
|
||||
}
|
||||
case *validator.StringResponse:
|
||||
hasDiagnosticError = d.Diagnostics.HasError()
|
||||
if hasDiagnosticError {
|
||||
diagnosticErrors = []interface{}{d.Diagnostics.Errors()[0]}
|
||||
}
|
||||
default:
|
||||
t.Fatalf("Unsupported response type: %T", response)
|
||||
}
|
||||
|
||||
// Verify results based on test case type
|
||||
switch tc := testCase.(type) {
|
||||
case requiredTogetherIfStringConditionTestCase:
|
||||
if tc.expectError {
|
||||
assert.True(t, hasDiagnosticError)
|
||||
if hasDiagnosticError {
|
||||
assert.Contains(t, fmt.Sprintf("%v", diagnosticErrors[0]), tc.expectErrorText)
|
||||
}
|
||||
} else {
|
||||
assert.False(t, hasDiagnosticError)
|
||||
}
|
||||
case requiredTogetherIfBoolConditionTestCase:
|
||||
if tc.expectError {
|
||||
assert.True(t, hasDiagnosticError)
|
||||
if hasDiagnosticError {
|
||||
assert.Contains(t, fmt.Sprintf("%v", diagnosticErrors[0]), tc.expectErrorText)
|
||||
}
|
||||
} else {
|
||||
assert.False(t, hasDiagnosticError)
|
||||
}
|
||||
case int32ConditionTestCase:
|
||||
if tc.expectError {
|
||||
require.True(t, hasDiagnosticError)
|
||||
if hasDiagnosticError {
|
||||
assert.Contains(t, fmt.Sprintf("%v", diagnosticErrors[0]), tc.expectErrorText)
|
||||
}
|
||||
} else {
|
||||
assert.False(t, hasDiagnosticError)
|
||||
}
|
||||
default:
|
||||
t.Fatalf("Unsupported test case type: %T", testCase)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequiredTogetherIf(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]requiredTogetherIfStringConditionTestCase{
|
||||
"condition-matches-all-fields-set": {
|
||||
condition: types.StringValue("expected"),
|
||||
field1: types.StringValue("value1"),
|
||||
field2: types.StringValue("value2"),
|
||||
expectError: false,
|
||||
},
|
||||
"condition-matches-field1-missing": {
|
||||
condition: types.StringValue("expected"),
|
||||
field1: types.StringNull(),
|
||||
field2: types.StringValue("value2"),
|
||||
expectError: true,
|
||||
expectErrorText: "If condition equals \"expected\", these attributes must be configured together",
|
||||
},
|
||||
"condition-matches-field2-missing": {
|
||||
condition: types.StringValue("expected"),
|
||||
field1: types.StringValue("value1"),
|
||||
field2: types.StringNull(),
|
||||
expectError: true,
|
||||
expectErrorText: "If condition equals \"expected\", these attributes must be configured together",
|
||||
},
|
||||
"condition-matches-both-fields-missing": {
|
||||
condition: types.StringValue("expected"),
|
||||
field1: types.StringNull(),
|
||||
field2: types.StringNull(),
|
||||
expectError: true,
|
||||
},
|
||||
"condition-does-not-match": {
|
||||
condition: types.StringValue("different"),
|
||||
field1: types.StringNull(),
|
||||
field2: types.StringValue("value2"),
|
||||
expectError: false,
|
||||
},
|
||||
"condition-is-null": {
|
||||
condition: types.StringNull(),
|
||||
field1: types.StringNull(),
|
||||
field2: types.StringValue("value2"),
|
||||
expectError: false,
|
||||
},
|
||||
"condition-is-unknown": {
|
||||
condition: types.StringUnknown(),
|
||||
field1: types.StringNull(),
|
||||
field2: types.StringValue("value2"),
|
||||
expectError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
name, testCase := name, testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
schemaObject := createRequiredIfStringConditionSchema()
|
||||
val := validators.RequiredTogetherIf(
|
||||
path.MatchRoot("condition"),
|
||||
types.StringValue("expected"),
|
||||
path.MatchRoot("field1"),
|
||||
path.MatchRoot("field2"),
|
||||
)
|
||||
|
||||
config := createRequiredIfStringConditionConfig(schemaObject, testCase)
|
||||
request := validator.StringRequest{
|
||||
ConfigValue: testCase.condition,
|
||||
Config: config,
|
||||
Path: path.Root("condition"),
|
||||
}
|
||||
|
||||
response := validator.StringResponse{}
|
||||
val.ValidateString(ctx, request, &response)
|
||||
|
||||
verifyValidationResults(t, response, testCase)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequiredTogetherIfWithBoolCondition(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]requiredTogetherIfBoolConditionTestCase{
|
||||
"condition-true-all-fields-set": {
|
||||
condition: types.BoolValue(true),
|
||||
field1: types.StringValue("value1"),
|
||||
field2: types.StringValue("value2"),
|
||||
expectError: false,
|
||||
},
|
||||
"condition-true-field1-missing": {
|
||||
condition: types.BoolValue(true),
|
||||
field1: types.StringNull(),
|
||||
field2: types.StringValue("value2"),
|
||||
expectError: true,
|
||||
expectErrorText: "If condition equals true, these attributes must be configured together",
|
||||
},
|
||||
"condition-false-fields-missing": {
|
||||
condition: types.BoolValue(false),
|
||||
field1: types.StringNull(),
|
||||
field2: types.StringValue("value2"),
|
||||
expectError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
name, testCase := name, testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
schemaObject := createRequiredIfBoolConditionSchema()
|
||||
|
||||
// Create a validator with Bool condition
|
||||
val := validators.RequiredTogetherIf(
|
||||
path.MatchRoot("condition"),
|
||||
types.BoolValue(true),
|
||||
path.MatchRoot("field1"),
|
||||
path.MatchRoot("field2"),
|
||||
)
|
||||
|
||||
config := createRequiredIfBoolConditionConfig(schemaObject, testCase)
|
||||
request := resource.ValidateConfigRequest{
|
||||
Config: config,
|
||||
}
|
||||
|
||||
response := resource.ValidateConfigResponse{}
|
||||
val.ValidateResource(context.Background(), request, &response)
|
||||
|
||||
verifyValidationResults(t, response, testCase)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequiredTogetherIfWithNumberCondition(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]int32ConditionTestCase{
|
||||
"condition-matches-all-fields-set": {
|
||||
condition: types.Int32Value(42),
|
||||
field1: types.StringValue("value1"),
|
||||
field2: types.StringValue("value2"),
|
||||
expectError: false,
|
||||
},
|
||||
"condition-matches-field-missing": {
|
||||
condition: types.Int32Value(42),
|
||||
field1: types.StringNull(),
|
||||
field2: types.StringValue("value2"),
|
||||
expectError: true,
|
||||
expectErrorText: "If condition equals 42, these attributes must be configured together",
|
||||
},
|
||||
"condition-does-not-match": {
|
||||
condition: types.Int32Value(24),
|
||||
field1: types.StringNull(),
|
||||
field2: types.StringValue("value2"),
|
||||
expectError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
name, testCase := name, testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
schemaObject := createInt32ConditionSchema()
|
||||
val := validators.RequiredTogetherIf(
|
||||
path.MatchRoot("condition"),
|
||||
types.Int32Value(42),
|
||||
path.MatchRoot("field1"),
|
||||
path.MatchRoot("field2"),
|
||||
)
|
||||
|
||||
config := createInt32ConditionConfig(schemaObject, testCase)
|
||||
request := resource.ValidateConfigRequest{
|
||||
Config: config,
|
||||
}
|
||||
|
||||
response := resource.ValidateConfigResponse{}
|
||||
val.ValidateResource(ctx, request, &response)
|
||||
|
||||
verifyValidationResults(t, response, testCase)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceRequiredTogetherIf(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]requiredTogetherIfStringConditionTestCase{
|
||||
"condition-matches-all-fields-set": {
|
||||
condition: types.StringValue("expected"),
|
||||
field1: types.StringValue("value1"),
|
||||
field2: types.StringValue("value2"),
|
||||
expectError: false,
|
||||
},
|
||||
"condition-matches-field-missing": {
|
||||
condition: types.StringValue("expected"),
|
||||
field1: types.StringNull(),
|
||||
field2: types.StringValue("value2"),
|
||||
expectError: true,
|
||||
expectErrorText: "If condition equals \"expected\", these attributes must be configured together",
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
name, testCase := name, testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
schemaObject := createRequiredIfStringConditionSchema()
|
||||
|
||||
// Test the RequiredTogetherIf function
|
||||
val := validators.RequiredTogetherIf(
|
||||
path.MatchRoot("condition"),
|
||||
types.StringValue("expected"),
|
||||
path.MatchRoot("field1"),
|
||||
path.MatchRoot("field2"),
|
||||
)
|
||||
|
||||
config := createRequiredIfStringConditionConfig(schemaObject, testCase)
|
||||
request := resource.ValidateConfigRequest{
|
||||
Config: config,
|
||||
}
|
||||
|
||||
response := resource.ValidateConfigResponse{}
|
||||
val.ValidateResource(ctx, request, &response)
|
||||
|
||||
verifyValidationResults(t, response, testCase)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDataSourceRequiredTogetherIf(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]requiredTogetherIfStringConditionTestCase{
|
||||
"condition-matches-all-fields-set": {
|
||||
condition: types.StringValue("expected"),
|
||||
field1: types.StringValue("value1"),
|
||||
field2: types.StringValue("value2"),
|
||||
expectError: false,
|
||||
},
|
||||
"condition-matches-field-missing": {
|
||||
condition: types.StringValue("expected"),
|
||||
field1: types.StringNull(),
|
||||
field2: types.StringValue("value2"),
|
||||
expectError: true,
|
||||
expectErrorText: "If condition equals \"expected\", these attributes must be configured together",
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
name, testCase := name, testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
schemaObject := createRequiredIfStringConditionSchema()
|
||||
|
||||
// Test the RequiredTogetherIf function with a datasource validator
|
||||
val := validators.RequiredTogetherIf(
|
||||
path.MatchRoot("condition"),
|
||||
types.StringValue("expected"),
|
||||
path.MatchRoot("field1"),
|
||||
path.MatchRoot("field2"),
|
||||
)
|
||||
|
||||
config := createRequiredIfStringConditionConfig(schemaObject, testCase)
|
||||
request := datasource.ValidateConfigRequest{
|
||||
Config: config,
|
||||
}
|
||||
|
||||
response := datasource.ValidateConfigResponse{}
|
||||
val.ValidateDataSource(ctx, request, &response)
|
||||
|
||||
verifyValidationResults(t, response, testCase)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderRequiredTogetherIf(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]requiredTogetherIfStringConditionTestCase{
|
||||
"condition-matches-all-fields-set": {
|
||||
condition: types.StringValue("expected"),
|
||||
field1: types.StringValue("value1"),
|
||||
field2: types.StringValue("value2"),
|
||||
expectError: false,
|
||||
},
|
||||
"condition-matches-field-missing": {
|
||||
condition: types.StringValue("expected"),
|
||||
field1: types.StringNull(),
|
||||
field2: types.StringValue("value2"),
|
||||
expectError: true,
|
||||
expectErrorText: "If condition equals \"expected\", these attributes must be configured together",
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
name, testCase := name, testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
schemaObject := createRequiredIfStringConditionSchema()
|
||||
|
||||
// Test the RequiredTogetherIf function with a provider validator
|
||||
val := validators.RequiredTogetherIf(
|
||||
path.MatchRoot("condition"),
|
||||
types.StringValue("expected"),
|
||||
path.MatchRoot("field1"),
|
||||
path.MatchRoot("field2"),
|
||||
)
|
||||
|
||||
config := createRequiredIfStringConditionConfig(schemaObject, testCase)
|
||||
request := provider.ValidateConfigRequest{
|
||||
Config: config,
|
||||
}
|
||||
|
||||
response := provider.ValidateConfigResponse{}
|
||||
val.ValidateProvider(ctx, request, &response)
|
||||
|
||||
verifyValidationResults(t, response, testCase)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequiredTogetherIfWithNonBooleanCondition(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type customType struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
testStruct := customType{Name: "test"}
|
||||
|
||||
testCases := map[string]struct {
|
||||
condValue types.String
|
||||
expectedValue any
|
||||
field1 types.String
|
||||
field2 types.String
|
||||
expectError bool
|
||||
matcherChanged bool
|
||||
}{
|
||||
"custom-object-not-equal": {
|
||||
condValue: types.StringValue("test"),
|
||||
expectedValue: testStruct,
|
||||
field1: types.StringNull(),
|
||||
field2: types.StringValue("value2"),
|
||||
expectError: true, // Changed to true - we expect validation to fail when the condition matches
|
||||
matcherChanged: true,
|
||||
},
|
||||
"same-string-different-value": {
|
||||
condValue: types.StringValue("test"),
|
||||
expectedValue: "different",
|
||||
field1: types.StringNull(),
|
||||
field2: types.StringValue("value2"),
|
||||
expectError: false,
|
||||
matcherChanged: false,
|
||||
},
|
||||
"different-types": {
|
||||
condValue: types.StringValue("123"),
|
||||
expectedValue: 123,
|
||||
field1: types.StringNull(),
|
||||
field2: types.StringValue("value2"),
|
||||
expectError: true, // Changed to true - condition equals 123 and field1 is null, should cause validation to fail
|
||||
matcherChanged: false,
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
name, testCase := name, testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
schemaObject := schema.Schema{
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"condition": schema.StringAttribute{
|
||||
Optional: true,
|
||||
},
|
||||
"field1": schema.StringAttribute{
|
||||
Optional: true,
|
||||
},
|
||||
"field2": schema.StringAttribute{
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expectedValue := testCase.expectedValue
|
||||
var v validators.RequiredTogetherIfValidator
|
||||
|
||||
if testCase.matcherChanged {
|
||||
// Using the string "test" as the expected condition value
|
||||
v = validators.RequiredTogetherIf(
|
||||
path.MatchRoot("condition"),
|
||||
types.StringValue("test"),
|
||||
path.MatchRoot("field1"),
|
||||
path.MatchRoot("field2"),
|
||||
)
|
||||
} else {
|
||||
// Normal string comparison using the string representation of expectedValue
|
||||
v = validators.RequiredTogetherIf(
|
||||
path.MatchRoot("condition"),
|
||||
types.StringValue(fmt.Sprintf("%v", expectedValue)),
|
||||
path.MatchRoot("field1"),
|
||||
path.MatchRoot("field2"),
|
||||
)
|
||||
}
|
||||
|
||||
config := tfsdk.Config{
|
||||
Schema: schemaObject,
|
||||
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.condValue),
|
||||
"field1": stringToTfValue(testCase.field1),
|
||||
"field2": stringToTfValue(testCase.field2),
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
request := resource.ValidateConfigRequest{
|
||||
Config: config,
|
||||
}
|
||||
|
||||
response := resource.ValidateConfigResponse{}
|
||||
v.ValidateResource(ctx, request, &response)
|
||||
|
||||
if testCase.expectError {
|
||||
assert.True(t, response.Diagnostics.HasError())
|
||||
} else {
|
||||
assert.False(t, response.Diagnostics.HasError())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequiredTogetherIfWithUnknownTargetPaths(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
condition types.String
|
||||
field1 types.String
|
||||
field2 types.String
|
||||
fieldPath string // Path that will be used in the validator but doesn't exist in schema
|
||||
expectError bool
|
||||
expectErrorText string
|
||||
}{
|
||||
"unknown-path-condition-matches": {
|
||||
condition: types.StringValue("expected"),
|
||||
field1: types.StringValue("value1"),
|
||||
field2: types.StringValue("value2"),
|
||||
fieldPath: "unknown_field",
|
||||
expectError: true, // Changed to true since the validator errors when a path doesn't exist
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
name, testCase := name, testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
schemaObject := schema.Schema{
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"condition": schema.StringAttribute{
|
||||
Optional: true,
|
||||
},
|
||||
"field1": schema.StringAttribute{
|
||||
Optional: true,
|
||||
},
|
||||
"field2": schema.StringAttribute{
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
val := validators.RequiredTogetherIf(
|
||||
path.MatchRoot("condition"),
|
||||
types.StringValue("expected"),
|
||||
path.MatchRoot("field1"),
|
||||
path.MatchRoot("field2"),
|
||||
path.MatchRoot(testCase.fieldPath),
|
||||
)
|
||||
|
||||
config := tfsdk.Config{
|
||||
Schema: schemaObject,
|
||||
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),
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
request := resource.ValidateConfigRequest{
|
||||
Config: config,
|
||||
}
|
||||
|
||||
response := resource.ValidateConfigResponse{}
|
||||
val.ValidateResource(ctx, request, &response)
|
||||
|
||||
if testCase.expectError {
|
||||
assert.True(t, response.Diagnostics.HasError())
|
||||
assert.Contains(t, response.Diagnostics.Errors()[0].Detail(), testCase.expectErrorText)
|
||||
} else {
|
||||
assert.False(t, response.Diagnostics.HasError())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequiredTogetherIfSet(t *testing.T) {
|
||||
schema := createRequiredIfStringConditionSchema()
|
||||
testCases := map[string]requiredTogetherIfStringConditionTestCase{
|
||||
"No condition value specified": {
|
||||
condition: types.StringNull(),
|
||||
field1: types.StringValue("value1"),
|
||||
field2: types.StringNull(),
|
||||
expectError: false,
|
||||
},
|
||||
"Unknown condition value": {
|
||||
condition: types.StringUnknown(),
|
||||
field1: types.StringValue("value1"),
|
||||
field2: types.StringNull(),
|
||||
expectError: false,
|
||||
},
|
||||
"Condition set, both fields set": {
|
||||
condition: types.StringValue("any_value"),
|
||||
field1: types.StringValue("value1"),
|
||||
field2: types.StringValue("value2"),
|
||||
expectError: false,
|
||||
},
|
||||
"Condition set, neither field set": {
|
||||
condition: types.StringValue("any_value"),
|
||||
field1: types.StringNull(),
|
||||
field2: types.StringNull(),
|
||||
expectError: true,
|
||||
},
|
||||
"Condition set, only field1 set": {
|
||||
condition: types.StringValue("any_value"),
|
||||
field1: types.StringValue("value1"),
|
||||
field2: types.StringNull(),
|
||||
expectError: true,
|
||||
expectErrorText: "If condition is set, these attributes must be configured together: [field1,field2]",
|
||||
},
|
||||
"Condition set, only field2 set": {
|
||||
condition: types.StringValue("any_value"),
|
||||
field1: types.StringNull(),
|
||||
field2: types.StringValue("value2"),
|
||||
expectError: true,
|
||||
expectErrorText: "If condition is set, these attributes must be configured together: [field1,field2]",
|
||||
},
|
||||
"Condition set, field1 set, field2 unknown": {
|
||||
condition: types.StringValue("any_value"),
|
||||
field1: types.StringValue("value1"),
|
||||
field2: types.StringUnknown(),
|
||||
expectError: false,
|
||||
},
|
||||
"Condition set, field1 unknown, field2 set": {
|
||||
condition: types.StringValue("any_value"),
|
||||
field1: types.StringUnknown(),
|
||||
field2: types.StringValue("value2"),
|
||||
expectError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
config := createRequiredIfStringConditionConfig(schema, testCase)
|
||||
v := validators.RequiredTogetherIfSet(
|
||||
path.MatchRoot("condition"),
|
||||
path.MatchRoot("field1"),
|
||||
path.MatchRoot("field2"),
|
||||
)
|
||||
|
||||
request := resource.ValidateConfigRequest{
|
||||
Config: config,
|
||||
}
|
||||
response := &resource.ValidateConfigResponse{}
|
||||
|
||||
v.ValidateResource(context.Background(), request, response)
|
||||
verifyValidationResults(t, response, testCase)
|
||||
})
|
||||
}
|
||||
}
|
||||
126
internal/provider/validators/required_value_if.go
Normal file
126
internal/provider/validators/required_value_if.go
Normal file
@@ -0,0 +1,126 @@
|
||||
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/tfsdk"
|
||||
)
|
||||
|
||||
var (
|
||||
_ datasource.ConfigValidator = &RequiredValueIfValidator{}
|
||||
_ provider.ConfigValidator = &RequiredValueIfValidator{}
|
||||
_ resource.ConfigValidator = &RequiredValueIfValidator{}
|
||||
)
|
||||
|
||||
// RequiredValueIfValidator validates that if a condition attribute is set to a specific value,
|
||||
// then a target attribute must be set to a specific value.
|
||||
type RequiredValueIfValidator struct {
|
||||
ConditionPath path.Expression
|
||||
ConditionValue attr.Value
|
||||
TargetPath path.Expression
|
||||
TargetValue attr.Value
|
||||
}
|
||||
|
||||
func (v RequiredValueIfValidator) Description(ctx context.Context) string {
|
||||
return v.MarkdownDescription(ctx)
|
||||
}
|
||||
|
||||
func (v RequiredValueIfValidator) MarkdownDescription(_ context.Context) string {
|
||||
return fmt.Sprintf("If %s equals %v, then %s must equal %v", v.ConditionPath, v.ConditionValue, v.TargetPath, v.TargetValue)
|
||||
}
|
||||
|
||||
func (v RequiredValueIfValidator) ValidateDataSource(ctx context.Context, req datasource.ValidateConfigRequest, resp *datasource.ValidateConfigResponse) {
|
||||
resp.Diagnostics = v.Validate(ctx, req.Config)
|
||||
}
|
||||
|
||||
func (v RequiredValueIfValidator) ValidateProvider(ctx context.Context, req provider.ValidateConfigRequest, resp *provider.ValidateConfigResponse) {
|
||||
resp.Diagnostics = v.Validate(ctx, req.Config)
|
||||
}
|
||||
|
||||
func (v RequiredValueIfValidator) ValidateResource(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
|
||||
resp.Diagnostics = v.Validate(ctx, req.Config)
|
||||
}
|
||||
|
||||
func (v RequiredValueIfValidator) ValidateEphemeralResource(ctx context.Context, req ephemeral.ValidateConfigRequest, resp *ephemeral.ValidateConfigResponse) {
|
||||
resp.Diagnostics = v.Validate(ctx, req.Config)
|
||||
}
|
||||
|
||||
func (v RequiredValueIfValidator) 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
|
||||
return conditionValueMatches(ctx, conditionValue, v.ConditionValue)
|
||||
}
|
||||
|
||||
func (v RequiredValueIfValidator) Validate(ctx context.Context, config tfsdk.Config) diag.Diagnostics {
|
||||
diags := diag.Diagnostics{}
|
||||
if !v.shouldValidate(ctx, config) {
|
||||
return diags
|
||||
}
|
||||
|
||||
// Condition matched, now validate that the target attribute has the required value
|
||||
matchedPaths, matchedPathsDiags := config.PathMatches(ctx, v.TargetPath)
|
||||
diags.Append(matchedPathsDiags...)
|
||||
if diags.HasError() || len(matchedPaths) == 0 {
|
||||
return diags
|
||||
}
|
||||
|
||||
// Get the value of the target attribute
|
||||
var targetValue attr.Value
|
||||
getTargetDiags := config.GetAttribute(ctx, matchedPaths[0], &targetValue)
|
||||
diags.Append(getTargetDiags...)
|
||||
if diags.HasError() {
|
||||
return diags
|
||||
}
|
||||
|
||||
// Skip validation if the target value is unknown
|
||||
if targetValue.IsUnknown() {
|
||||
return diags
|
||||
}
|
||||
|
||||
// If the target value is null or doesn't match the required value, add a diagnostic
|
||||
if targetValue.IsNull() || !conditionValueMatches(ctx, targetValue, v.TargetValue) {
|
||||
diags.Append(validatordiag.InvalidAttributeCombinationDiagnostic(
|
||||
matchedPaths[0],
|
||||
fmt.Sprintf("When %s is set to %v, %s must be set to %v", v.ConditionPath, v.ConditionValue, v.TargetPath, v.TargetValue),
|
||||
))
|
||||
}
|
||||
|
||||
return diags
|
||||
}
|
||||
|
||||
// RequiredValueIf creates a validator that ensures if a condition attribute equals a specific value,
|
||||
// then a target attribute must equal a specific value.
|
||||
func RequiredValueIf(conditionPath path.Expression, conditionValue attr.Value, targetPath path.Expression, targetValue attr.Value) RequiredValueIfValidator {
|
||||
return RequiredValueIfValidator{
|
||||
ConditionPath: conditionPath,
|
||||
ConditionValue: conditionValue,
|
||||
TargetPath: targetPath,
|
||||
TargetValue: targetValue,
|
||||
}
|
||||
}
|
||||
201
internal/provider/validators/required_value_if_test.go
Normal file
201
internal/provider/validators/required_value_if_test.go
Normal file
@@ -0,0 +1,201 @@
|
||||
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 requiredValueIfTestCase struct {
|
||||
conditionValue types.String
|
||||
targetValue types.String
|
||||
expectError bool
|
||||
}
|
||||
|
||||
// Function to create a schema object for RequiredValueIf tests
|
||||
func createRequiredValueIfSchema() schema.Schema {
|
||||
return schema.Schema{
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"condition_attr": schema.StringAttribute{
|
||||
Optional: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
},
|
||||
},
|
||||
"target_attr": schema.StringAttribute{
|
||||
Optional: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Function to create a config for RequiredValueIf tests
|
||||
func createRequiredValueIfConfig(schema schema.Schema, testCase requiredValueIfTestCase) tfsdk.Config {
|
||||
var conditionValue, targetValue tftypes.Value
|
||||
|
||||
if testCase.conditionValue.IsNull() {
|
||||
conditionValue = tftypes.NewValue(tftypes.String, nil)
|
||||
} else if testCase.conditionValue.IsUnknown() {
|
||||
conditionValue = tftypes.NewValue(tftypes.String, tftypes.UnknownValue)
|
||||
} else {
|
||||
conditionValue = tftypes.NewValue(tftypes.String, testCase.conditionValue.ValueString())
|
||||
}
|
||||
|
||||
if testCase.targetValue.IsNull() {
|
||||
targetValue = tftypes.NewValue(tftypes.String, nil)
|
||||
} else if testCase.targetValue.IsUnknown() {
|
||||
targetValue = tftypes.NewValue(tftypes.String, tftypes.UnknownValue)
|
||||
} else {
|
||||
targetValue = tftypes.NewValue(tftypes.String, testCase.targetValue.ValueString())
|
||||
}
|
||||
|
||||
return tfsdk.Config{
|
||||
Schema: schema,
|
||||
Raw: tftypes.NewValue(tftypes.Object{
|
||||
AttributeTypes: map[string]tftypes.Type{
|
||||
"condition_attr": tftypes.String,
|
||||
"target_attr": tftypes.String,
|
||||
},
|
||||
}, map[string]tftypes.Value{
|
||||
"condition_attr": conditionValue,
|
||||
"target_attr": targetValue,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequiredValueIf(t *testing.T) {
|
||||
schema := createRequiredValueIfSchema()
|
||||
testCases := map[string]requiredValueIfTestCase{
|
||||
"condition match target match": {
|
||||
conditionValue: types.StringValue("active"),
|
||||
targetValue: types.StringValue("enabled"),
|
||||
expectError: false,
|
||||
},
|
||||
"condition match target mismatch": {
|
||||
conditionValue: types.StringValue("active"),
|
||||
targetValue: types.StringValue("disabled"),
|
||||
expectError: true,
|
||||
},
|
||||
"condition match target null": {
|
||||
conditionValue: types.StringValue("active"),
|
||||
targetValue: types.StringNull(),
|
||||
expectError: true,
|
||||
},
|
||||
"condition match target unknown": {
|
||||
conditionValue: types.StringValue("active"),
|
||||
targetValue: types.StringUnknown(),
|
||||
expectError: false,
|
||||
},
|
||||
"condition mismatch": {
|
||||
conditionValue: types.StringValue("inactive"),
|
||||
targetValue: types.StringValue("disabled"),
|
||||
expectError: false,
|
||||
},
|
||||
"condition null": {
|
||||
conditionValue: types.StringNull(),
|
||||
targetValue: types.StringNull(),
|
||||
expectError: false,
|
||||
},
|
||||
"condition unknown": {
|
||||
conditionValue: types.StringUnknown(),
|
||||
targetValue: types.StringNull(),
|
||||
expectError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
config := createRequiredValueIfConfig(schema, testCase)
|
||||
validator := validators.RequiredValueIf(
|
||||
path.MatchRoot("condition_attr"),
|
||||
types.StringValue("active"),
|
||||
path.MatchRoot("target_attr"),
|
||||
types.StringValue("enabled"),
|
||||
)
|
||||
diags := validator.Validate(context.Background(), config)
|
||||
|
||||
if testCase.expectError {
|
||||
assert.True(t, diags.HasError(), "expected error, but got none")
|
||||
} else {
|
||||
assert.False(t, diags.HasError(), "expected no error, but got: %v", diags)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceRequiredValueIf(t *testing.T) {
|
||||
schema := createRequiredValueIfSchema()
|
||||
testCase := requiredValueIfTestCase{
|
||||
conditionValue: types.StringValue("active"),
|
||||
targetValue: types.StringValue("disabled"),
|
||||
expectError: true,
|
||||
}
|
||||
config := createRequiredValueIfConfig(schema, testCase)
|
||||
validator := validators.RequiredValueIf(
|
||||
path.MatchRoot("condition_attr"),
|
||||
types.StringValue("active"),
|
||||
path.MatchRoot("target_attr"),
|
||||
types.StringValue("enabled"),
|
||||
)
|
||||
|
||||
resp := &resource.ValidateConfigResponse{}
|
||||
validator.ValidateResource(context.Background(), resource.ValidateConfigRequest{Config: config}, resp)
|
||||
assert.True(t, resp.Diagnostics.HasError(), "expected error, but got none")
|
||||
}
|
||||
|
||||
func TestDataSourceRequiredValueIf(t *testing.T) {
|
||||
schema := createRequiredValueIfSchema()
|
||||
testCase := requiredValueIfTestCase{
|
||||
conditionValue: types.StringValue("active"),
|
||||
targetValue: types.StringValue("disabled"),
|
||||
expectError: true,
|
||||
}
|
||||
config := createRequiredValueIfConfig(schema, testCase)
|
||||
validator := validators.RequiredValueIf(
|
||||
path.MatchRoot("condition_attr"),
|
||||
types.StringValue("active"),
|
||||
path.MatchRoot("target_attr"),
|
||||
types.StringValue("enabled"),
|
||||
)
|
||||
|
||||
resp := &datasource.ValidateConfigResponse{}
|
||||
validator.ValidateDataSource(context.Background(), datasource.ValidateConfigRequest{Config: config}, resp)
|
||||
assert.True(t, resp.Diagnostics.HasError(), "expected error, but got none")
|
||||
}
|
||||
|
||||
func TestProviderRequiredValueIf(t *testing.T) {
|
||||
schema := createRequiredValueIfSchema()
|
||||
testCase := requiredValueIfTestCase{
|
||||
conditionValue: types.StringValue("active"),
|
||||
targetValue: types.StringValue("disabled"),
|
||||
expectError: true,
|
||||
}
|
||||
config := createRequiredValueIfConfig(schema, testCase)
|
||||
validator := validators.RequiredValueIf(
|
||||
path.MatchRoot("condition_attr"),
|
||||
types.StringValue("active"),
|
||||
path.MatchRoot("target_attr"),
|
||||
types.StringValue("enabled"),
|
||||
)
|
||||
|
||||
resp := &provider.ValidateConfigResponse{}
|
||||
validator.ValidateProvider(context.Background(), provider.ValidateConfigRequest{Config: config}, resp)
|
||||
assert.True(t, resp.Diagnostics.HasError(), "expected error, but got none")
|
||||
}
|
||||
@@ -3,10 +3,21 @@ package utils
|
||||
import (
|
||||
"context"
|
||||
"github.com/filipowm/terraform-provider-unifi/internal/provider/base"
|
||||
"github.com/hashicorp/terraform-plugin-framework/attr"
|
||||
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/listdefault"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
)
|
||||
|
||||
func DefaultEmptyList(elementType attr.Type) defaults.List {
|
||||
return listdefault.StaticValue(EmptyList(elementType))
|
||||
}
|
||||
|
||||
func EmptyList(elementType attr.Type) types.List {
|
||||
return types.ListValueMust(elementType, []attr.Value{})
|
||||
}
|
||||
|
||||
func ListElementsAs(list types.List, target interface{}) diag.Diagnostics {
|
||||
diags := diag.Diagnostics{}
|
||||
if !base.IsDefined(list) {
|
||||
@@ -17,3 +28,32 @@ func ListElementsAs(list types.List, target interface{}) diag.Diagnostics {
|
||||
}
|
||||
return diags
|
||||
}
|
||||
|
||||
func ListElementsToString(ctx context.Context, list types.List) (string, diag.Diagnostics) {
|
||||
diags := diag.Diagnostics{}
|
||||
if !base.IsDefined(list) {
|
||||
return "", diags
|
||||
}
|
||||
if list.ElementType(ctx) == types.StringType {
|
||||
var target []string
|
||||
diags.Append(ListElementsAs(list, &target)...)
|
||||
if diags.HasError() {
|
||||
return "", diags
|
||||
}
|
||||
return JoinNonEmpty(target, ","), diags
|
||||
}
|
||||
diags.AddError("List is not a list of types.StringType", "List is not a list of strings")
|
||||
return "", diags
|
||||
}
|
||||
|
||||
func StringToListElements(ctx context.Context, value string) (types.List, diag.Diagnostics) {
|
||||
countries := SplitAndTrim(value, ",")
|
||||
if len(countries) == 0 {
|
||||
return types.ListNull(types.StringType), diag.Diagnostics{}
|
||||
}
|
||||
list, diags := types.ListValueFrom(ctx, types.StringType, countries)
|
||||
if diags.HasError() {
|
||||
return types.ListNull(types.StringType), diags
|
||||
}
|
||||
return list, diags
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package utils
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
|
||||
)
|
||||
@@ -38,3 +39,35 @@ func StringSliceToSet(src []string) *schema.Set {
|
||||
func IsStringValueNotEmpty(s basetypes.StringValue) bool {
|
||||
return !s.IsUnknown() && !s.IsNull() && s.ValueString() != ""
|
||||
}
|
||||
|
||||
// JoinNonEmpty joins non-empty strings from a slice with the specified separator.
|
||||
// Empty strings in the slice are filtered out.
|
||||
func JoinNonEmpty(elements []string, separator string) string {
|
||||
var nonEmpty []string
|
||||
for _, elem := range elements {
|
||||
if elem != "" {
|
||||
nonEmpty = append(nonEmpty, elem)
|
||||
}
|
||||
}
|
||||
return strings.Join(nonEmpty, separator)
|
||||
}
|
||||
|
||||
// SplitAndTrim splits a string by the specified separator and trims whitespace from each element.
|
||||
// Empty strings after trimming are filtered out.
|
||||
func SplitAndTrim(s string, separator string) []string {
|
||||
if s == "" {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
parts := strings.Split(s, separator)
|
||||
var result []string
|
||||
|
||||
for _, part := range parts {
|
||||
trimmed := strings.TrimSpace(part)
|
||||
if trimmed != "" {
|
||||
result = append(result, trimmed)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user