diff --git a/.github/workflows/acctest.yml b/.github/workflows/acctest.yml index 28d62af..ec8dbc1 100644 --- a/.github/workflows/acctest.yml +++ b/.github/workflows/acctest.yml @@ -24,6 +24,11 @@ on: - "Makefile" schedule: - cron: "0 13 * * *" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + jobs: test: name: Matrix Test diff --git a/go.mod b/go.mod index 4c5a952..378416b 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/filipowm/terraform-provider-unifi go 1.23.5 -//replace github.com/filipowm/go-unifi v1.5.3 => ../go-unifi +//replace github.com/filipowm/go-unifi v1.6.0 => ../go-unifi // replace github.com/hashicorp/terraform-plugin-docs => ../../hashicorp/terraform-plugin-docs // replace github.com/hashicorp/terraform-plugin-sdk/v2 => ../../hashicorp/terraform-plugin-sdk @@ -10,7 +10,7 @@ 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.6.0 + github.com/filipowm/go-unifi v1.6.1 github.com/golangci/golangci-lint v1.64.7 github.com/hashicorp/go-version v1.7.0 github.com/hashicorp/terraform-plugin-docs v0.21.0 diff --git a/go.sum b/go.sum index 2041615..92ad8c1 100644 --- a/go.sum +++ b/go.sum @@ -284,6 +284,8 @@ github.com/filipowm/go-unifi v1.5.4 h1:kc1jWx0Rht+tiEyFsDjPAYJ6h2EV+Atq7eIoTA6fH github.com/filipowm/go-unifi v1.5.4/go.mod h1:NQgqx3ylLVDqxVgY3uJ3ZMIecSl46fvqCCXzwHRuVDc= github.com/filipowm/go-unifi v1.6.0 h1:0oLOrsLWcaU8sUsyMyjyGwaAWNC9Ee4YZ1ehtijXahg= github.com/filipowm/go-unifi v1.6.0/go.mod h1:hB5XyhjtnnU9GC6lYPYxuNmYq4J/SyjmElRVazCKT0U= +github.com/filipowm/go-unifi v1.6.1 h1:zkxLUkbUWO3d62cUGr97knIzSJd+/id9RVc32iPojqo= +github.com/filipowm/go-unifi v1.6.1/go.mod h1:hB5XyhjtnnU9GC6lYPYxuNmYq4J/SyjmElRVazCKT0U= 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= diff --git a/internal/provider/acctest/resource_setting_guest_access_test.go b/internal/provider/acctest/resource_setting_guest_access_test.go new file mode 100644 index 0000000..bd2d830 --- /dev/null +++ b/internal/provider/acctest/resource_setting_guest_access_test.go @@ -0,0 +1,1129 @@ +package acctest + +import ( + "fmt" + "sync" + "testing" + + pt "github.com/filipowm/terraform-provider-unifi/internal/provider/testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +var settingGuestAccessLock = &sync.Mutex{} + +func TestAccSettingGuestAccess_basic(t *testing.T) { + AcceptanceTest(t, AcceptanceTestCase{ + Lock: settingGuestAccessLock, + Steps: []resource.TestStep{ + { + Config: testAccSettingGuestAccessConfig_basic(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "auth", "none"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "portal_enabled", "true"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "portal_use_hostname", "true"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "portal_hostname", "guest.example.com"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "template_engine", "angular"), + + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "expire", "60"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "expire_number", "1"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "expire_unit", "60"), + + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "ec_enabled", "true"), + ), + }, + pt.ImportStepWithSite("unifi_setting_guest_access.test"), + { + Config: testAccSettingGuestAccessConfig_basicUpdated(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "auth", "hotspot"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "portal_enabled", "false"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "template_engine", "jsp"), + + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "expire", "1440"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "expire_number", "1"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "expire_unit", "1440"), + + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "ec_enabled", "false"), + ), + }, + }, + }) +} + +func TestAccSettingGuestAccess_customAuth(t *testing.T) { + AcceptanceTest(t, AcceptanceTestCase{ + Lock: settingGuestAccessLock, + Steps: []resource.TestStep{ + { + Config: testAccSettingGuestAccessConfig_customAuth("192.168.1.1"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "auth", "custom"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "custom_ip", "192.168.1.1"), + ), + }, + { + Config: testAccSettingGuestAccessConfig_customAuth("192.168.1.2"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "auth", "custom"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "custom_ip", "192.168.1.2"), + ), + }, + { + Config: testAccSettingGuestAccessConfig_auth("none"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "auth", "none"), + resource.TestCheckNoResourceAttr("unifi_setting_guest_access.test", "custom_ip"), + ), + }, + }, + }) +} + +func TestAccSettingGuestAccess_password(t *testing.T) { + AcceptanceTest(t, AcceptanceTestCase{ + Lock: settingGuestAccessLock, + Steps: []resource.TestStep{ + { + Config: testAccSettingGuestAccessConfig_password("pass1"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "auth", "hotspot"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "password", "pass1"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "password_enabled", "true"), + ), + }, + { + Config: testAccSettingGuestAccessConfig_password("pass2"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "auth", "hotspot"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "password", "pass2"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "password_enabled", "true"), + ), + }, + { + Config: testAccSettingGuestAccessConfig_auth("hotspot"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "auth", "hotspot"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "password_enabled", "false"), + ), + }, + }, + }) +} + +func TestAccSettingGuestAccess_voucher(t *testing.T) { + AcceptanceTest(t, AcceptanceTestCase{ + Lock: settingGuestAccessLock, + Steps: []resource.TestStep{ + { + Config: testAccSettingGuestAccessConfig_voucher(true), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "auth", "hotspot"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "voucher_enabled", "true"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "voucher_customized", "false"), + ), + }, + { + Config: testAccSettingGuestAccessConfig_voucherCustomized(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "auth", "hotspot"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "voucher_enabled", "true"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "voucher_customized", "true"), + ), + }, + { + Config: testAccSettingGuestAccessConfig_voucher(false), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "auth", "hotspot"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "voucher_enabled", "false"), + ), + }, + }, + }) +} + +func TestAccSettingGuestAccess_allowedSubnet(t *testing.T) { + t.Skip("api.err.InvalidPayload; api.err.InvalidKey: ") + AcceptanceTest(t, AcceptanceTestCase{ + Lock: settingGuestAccessLock, + Steps: []resource.TestStep{ + { + Config: testAccSettingGuestAccessConfig_allowedSubnet("192.168.1.0/24"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "allowed_subnet", "192.168.1.0/24"), + ), + }, + { + Config: testAccSettingGuestAccessConfig_allowedSubnet("10.0.0.0/24"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "allowed_subnet", "10.0.0.0/24"), + ), + }, + }, + }) +} + +func TestAccSettingGuestAccess_paymentPaypal(t *testing.T) { + AcceptanceTest(t, AcceptanceTestCase{ + Lock: settingGuestAccessLock, + Steps: []resource.TestStep{ + { + Config: testAccSettingGuestAccessConfig_paymentPaypal(true), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "auth", "hotspot"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "payment_enabled", "true"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "payment_gateway", "paypal"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "paypal.username", "test@example.com"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "paypal.password", "paypal-password"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "paypal.signature", "paypal-signature"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "paypal.use_sandbox", "true"), + ), + }, + pt.ImportStepWithSite("unifi_setting_guest_access.test"), + { + Config: testAccSettingGuestAccessConfig_paymentPaypal(false), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "auth", "hotspot"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "payment_enabled", "true"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "payment_gateway", "paypal"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "paypal.username", "test@example.com"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "paypal.password", "paypal-password"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "paypal.signature", "paypal-signature"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "paypal.use_sandbox", "false"), + ), + }, + { + Config: testAccSettingGuestAccessConfig_paymentPaypalUpdated(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "auth", "hotspot"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "payment_enabled", "true"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "payment_gateway", "paypal"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "paypal.username", "updated@example.com"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "paypal.password", "updated-password"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "paypal.signature", "updated-signature"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "paypal.use_sandbox", "true"), + ), + }, + }, + }) +} + +func TestAccSettingGuestAccess_paymentStripe(t *testing.T) { + AcceptanceTest(t, AcceptanceTestCase{ + Lock: settingGuestAccessLock, + Steps: []resource.TestStep{ + { + Config: testAccSettingGuestAccessConfig_paymentStripe("stripe-api-key"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "auth", "hotspot"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "payment_enabled", "true"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "payment_gateway", "stripe"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "stripe.api_key", "stripe-api-key"), + ), + }, + pt.ImportStepWithSite("unifi_setting_guest_access.test"), + { + Config: testAccSettingGuestAccessConfig_paymentStripe("updated-stripe-api-key"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "auth", "hotspot"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "payment_enabled", "true"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "payment_gateway", "stripe"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "stripe.api_key", "updated-stripe-api-key"), + ), + }, + }, + }) +} + +func TestAccSettingGuestAccess_paymentAuthorize(t *testing.T) { + AcceptanceTest(t, AcceptanceTestCase{ + Lock: settingGuestAccessLock, + Steps: []resource.TestStep{ + { + Config: testAccSettingGuestAccessConfig_paymentAuthorize(true), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "auth", "hotspot"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "payment_enabled", "true"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "payment_gateway", "authorize"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "authorize.login_id", "authorize-login"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "authorize.transaction_key", "authorize-transaction-key"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "authorize.use_sandbox", "true"), + ), + }, + pt.ImportStepWithSite("unifi_setting_guest_access.test"), + { + Config: testAccSettingGuestAccessConfig_paymentAuthorize(false), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "auth", "hotspot"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "payment_enabled", "true"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "payment_gateway", "authorize"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "authorize.login_id", "authorize-login"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "authorize.transaction_key", "authorize-transaction-key"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "authorize.use_sandbox", "false"), + ), + }, + }, + }) +} + +func TestAccSettingGuestAccess_paymentQuickpay(t *testing.T) { + AcceptanceTest(t, AcceptanceTestCase{ + Lock: settingGuestAccessLock, + Steps: []resource.TestStep{ + { + Config: testAccSettingGuestAccessConfig_paymentQuickpay(true), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "auth", "hotspot"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "payment_enabled", "true"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "payment_gateway", "quickpay"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "quickpay.agreement_id", "quickpay-agreement"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "quickpay.api_key", "quickpay-api-key"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "quickpay.merchant_id", "quickpay-merchant"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "quickpay.use_sandbox", "true"), + ), + }, + pt.ImportStepWithSite("unifi_setting_guest_access.test"), + { + Config: testAccSettingGuestAccessConfig_paymentQuickpay(false), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "auth", "hotspot"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "payment_enabled", "true"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "payment_gateway", "quickpay"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "quickpay.agreement_id", "quickpay-agreement"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "quickpay.api_key", "quickpay-api-key"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "quickpay.merchant_id", "quickpay-merchant"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "quickpay.use_sandbox", "false"), + ), + }, + }, + }) +} + +func TestAccSettingGuestAccess_paymentMerchantWarrior(t *testing.T) { + AcceptanceTest(t, AcceptanceTestCase{ + Lock: settingGuestAccessLock, + Steps: []resource.TestStep{ + { + Config: testAccSettingGuestAccessConfig_paymentMerchantWarrior(true), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "auth", "hotspot"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "payment_enabled", "true"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "payment_gateway", "merchantwarrior"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "merchant_warrior.api_key", "mw-api-key"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "merchant_warrior.api_passphrase", "mw-passphrase"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "merchant_warrior.merchant_uuid", "mw-merchant-id"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "merchant_warrior.use_sandbox", "true"), + ), + }, + pt.ImportStepWithSite("unifi_setting_guest_access.test"), + { + Config: testAccSettingGuestAccessConfig_paymentMerchantWarrior(false), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "auth", "hotspot"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "payment_enabled", "true"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "payment_gateway", "merchantwarrior"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "merchant_warrior.api_key", "mw-api-key"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "merchant_warrior.api_passphrase", "mw-passphrase"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "merchant_warrior.merchant_uuid", "mw-merchant-id"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "merchant_warrior.use_sandbox", "false"), + ), + }, + }, + }) +} + +func TestAccSettingGuestAccess_paymentIPpay(t *testing.T) { + AcceptanceTest(t, AcceptanceTestCase{ + Lock: settingGuestAccessLock, + Steps: []resource.TestStep{ + { + Config: testAccSettingGuestAccessConfig_paymentIPpay(true), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "auth", "hotspot"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "payment_enabled", "true"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "payment_gateway", "ippay"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "ippay.terminal_id", "ippay-terminal"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "ippay.use_sandbox", "true"), + ), + }, + pt.ImportStepWithSite("unifi_setting_guest_access.test"), + { + Config: testAccSettingGuestAccessConfig_paymentIPpay(false), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "auth", "hotspot"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "payment_enabled", "true"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "payment_gateway", "ippay"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "ippay.terminal_id", "ippay-terminal"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "ippay.use_sandbox", "false"), + ), + }, + }, + }) +} + +func TestAccSettingGuestAccess_paymentSwitchGateways(t *testing.T) { + AcceptanceTest(t, AcceptanceTestCase{ + Lock: settingGuestAccessLock, + Steps: []resource.TestStep{ + { + Config: testAccSettingGuestAccessConfig_paymentPaypal(true), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "payment_gateway", "paypal"), + ), + }, + { + Config: testAccSettingGuestAccessConfig_paymentStripe("stripe-api-key"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "payment_gateway", "stripe"), + resource.TestCheckNoResourceAttr("unifi_setting_guest_access.test", "paypal.username"), + ), + }, + { + Config: testAccSettingGuestAccessConfig_paymentAuthorize(true), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "payment_gateway", "authorize"), + resource.TestCheckNoResourceAttr("unifi_setting_guest_access.test", "stripe.api_key"), + ), + }, + { + Config: testAccSettingGuestAccessConfig_paymentQuickpay(true), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "payment_gateway", "quickpay"), + resource.TestCheckNoResourceAttr("unifi_setting_guest_access.test", "authorize.login_id"), + ), + }, + { + Config: testAccSettingGuestAccessConfig_paymentMerchantWarrior(true), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "payment_gateway", "merchantwarrior"), + resource.TestCheckNoResourceAttr("unifi_setting_guest_access.test", "quickpay.api_key"), + ), + }, + { + Config: testAccSettingGuestAccessConfig_paymentIPpay(true), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "payment_gateway", "ippay"), + resource.TestCheckNoResourceAttr("unifi_setting_guest_access.test", "merchant_warrior.api_key"), + ), + }, + { + Config: testAccSettingGuestAccessConfig_auth("hotspot"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "auth", "hotspot"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "payment_enabled", "false"), + resource.TestCheckNoResourceAttr("unifi_setting_guest_access.test", "payment_gateway"), + ), + }, + }, + }) +} + +func TestAccSettingGuestAccess_redirect(t *testing.T) { + AcceptanceTest(t, AcceptanceTestCase{ + Lock: settingGuestAccessLock, + Steps: []resource.TestStep{ + { + Config: testAccSettingGuestAccessConfig_redirect("https://example.com", true, true), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "redirect_enabled", "true"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "redirect.use_https", "true"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "redirect.to_https", "true"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "redirect.url", "https://example.com"), + ), + }, + pt.ImportStepWithSite("unifi_setting_guest_access.test"), + { + Config: testAccSettingGuestAccessConfig_redirect("https://updated-example.com", false, false), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "redirect_enabled", "true"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "redirect.use_https", "false"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "redirect.to_https", "false"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "redirect.url", "https://updated-example.com"), + ), + }, + { + Config: testAccSettingGuestAccessConfig_auth("none"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "redirect_enabled", "false"), + resource.TestCheckNoResourceAttr("unifi_setting_guest_access.test", "redirect"), + ), + }, + }, + }) +} + +func TestAccSettingGuestAccess_facebook(t *testing.T) { + AcceptanceTest(t, AcceptanceTestCase{ + Lock: settingGuestAccessLock, + Steps: []resource.TestStep{ + { + Config: testAccSettingGuestAccessConfig_facebook("facebook-app-id", "facebook-app-secret", true), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "auth", "hotspot"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "facebook_enabled", "true"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "facebook.app_id", "facebook-app-id"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "facebook.app_secret", "facebook-app-secret"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "facebook.scope_email", "true"), + ), + }, + pt.ImportStepWithSite("unifi_setting_guest_access.test"), + { + Config: testAccSettingGuestAccessConfig_facebook("updated-app-id", "updated-app-secret", false), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "auth", "hotspot"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "facebook_enabled", "true"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "facebook.app_id", "updated-app-id"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "facebook.app_secret", "updated-app-secret"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "facebook.scope_email", "false"), + ), + }, + { + Config: testAccSettingGuestAccessConfig_auth("none"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "auth", "none"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "facebook_enabled", "false"), + resource.TestCheckNoResourceAttr("unifi_setting_guest_access.test", "facebook"), + ), + }, + }, + }) +} + +func TestAccSettingGuestAccess_google(t *testing.T) { + AcceptanceTest(t, AcceptanceTestCase{ + Lock: settingGuestAccessLock, + Steps: []resource.TestStep{ + { + Config: testAccSettingGuestAccessConfig_google("google-client-id", "google-client-secret", "example.com", true), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "auth", "hotspot"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "google_enabled", "true"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "google.client_id", "google-client-id"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "google.client_secret", "google-client-secret"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "google.domain", "example.com"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "google.scope_email", "true"), + ), + }, + pt.ImportStepWithSite("unifi_setting_guest_access.test"), + { + Config: testAccSettingGuestAccessConfig_google("updated-client-id", "updated-client-secret", "", false), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "auth", "hotspot"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "google_enabled", "true"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "google.client_id", "updated-client-id"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "google.client_secret", "updated-client-secret"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "google.domain", ""), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "google.scope_email", "false"), + ), + }, + { + Config: testAccSettingGuestAccessConfig_auth("none"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "auth", "none"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "google_enabled", "false"), + resource.TestCheckNoResourceAttr("unifi_setting_guest_access.test", "google"), + ), + }, + }, + }) +} + +func TestAccSettingGuestAccess_radius(t *testing.T) { + AcceptanceTest(t, AcceptanceTestCase{ + Lock: settingGuestAccessLock, + Steps: []resource.TestStep{ + { + Config: testAccSettingGuestAccessConfig_radius("chap", "radius-profile-id", true, 3799), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "auth", "hotspot"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "radius_enabled", "true"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "radius.auth_type", "chap"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "radius.profile_id", "radius-profile-id"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "radius.disconnect_enabled", "true"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "radius.disconnect_port", "3799"), + ), + }, + pt.ImportStepWithSite("unifi_setting_guest_access.test"), + { + Config: testAccSettingGuestAccessConfig_radius("mschapv2", "updated-profile-id", false, 1812), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "auth", "hotspot"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "radius_enabled", "true"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "radius.auth_type", "mschapv2"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "radius.profile_id", "updated-profile-id"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "radius.disconnect_enabled", "false"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "radius.disconnect_port", "1812"), + ), + }, + { + Config: testAccSettingGuestAccessConfig_auth("none"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "auth", "none"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "radius_enabled", "false"), + resource.TestCheckNoResourceAttr("unifi_setting_guest_access.test", "radius"), + ), + }, + }, + }) +} + +func TestAccSettingGuestAccess_wechat(t *testing.T) { + AcceptanceTest(t, AcceptanceTestCase{ + Lock: settingGuestAccessLock, + Steps: []resource.TestStep{ + { + Config: testAccSettingGuestAccessConfig_wechat("wechat-app-id", "wechat-app-secret", "wechat-secret-key", "wechat-shop-id"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "auth", "hotspot"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "wechat_enabled", "true"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "wechat.app_id", "wechat-app-id"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "wechat.app_secret", "wechat-app-secret"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "wechat.secret_key", "wechat-secret-key"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "wechat.shop_id", "wechat-shop-id"), + ), + }, + pt.ImportStepWithSite("unifi_setting_guest_access.test"), + { + Config: testAccSettingGuestAccessConfig_wechat("updated-app-id", "updated-app-secret", "updated-secret-key", "updated-shop-id"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "auth", "hotspot"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "wechat_enabled", "true"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "wechat.app_id", "updated-app-id"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "wechat.app_secret", "updated-app-secret"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "wechat.secret_key", "updated-secret-key"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "wechat.shop_id", "updated-shop-id"), + ), + }, + { + Config: testAccSettingGuestAccessConfig_auth("none"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "auth", "none"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "wechat_enabled", "false"), + resource.TestCheckNoResourceAttr("unifi_setting_guest_access.test", "wechat"), + ), + }, + }, + }) +} + +func TestAccSettingGuestAccess_facebookWifi(t *testing.T) { + AcceptanceTest(t, AcceptanceTestCase{ + Lock: settingGuestAccessLock, + Steps: []resource.TestStep{ + { + Config: testAccSettingGuestAccessConfig_facebookWifi("gateway-id", "gateway-name", "gateway-secret", true), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "auth", "facebook_wifi"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "facebook_wifi.gateway_id", "gateway-id"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "facebook_wifi.gateway_name", "gateway-name"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "facebook_wifi.gateway_secret", "gateway-secret"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "facebook_wifi.block_https", "true"), + ), + }, + pt.ImportStepWithSite("unifi_setting_guest_access.test"), + { + Config: testAccSettingGuestAccessConfig_facebookWifi("updated-gateway-id", "updated-gateway-name", "updated-gateway-secret", false), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "auth", "facebook_wifi"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "facebook_wifi.gateway_id", "updated-gateway-id"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "facebook_wifi.gateway_name", "updated-gateway-name"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "facebook_wifi.gateway_secret", "updated-gateway-secret"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "facebook_wifi.block_https", "false"), + ), + }, + { + Config: testAccSettingGuestAccessConfig_auth("none"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "auth", "none"), + resource.TestCheckNoResourceAttr("unifi_setting_guest_access.test", "facebook_wifi"), + ), + }, + }, + }) +} + +func TestAccSettingGuestAccess_restrictedDNS(t *testing.T) { + AcceptanceTest(t, AcceptanceTestCase{ + Lock: settingGuestAccessLock, + Steps: []resource.TestStep{ + { + Config: testAccSettingGuestAccessConfig_restrictedDNS([]string{"8.8.8.8", "1.1.1.1"}), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "restricted_dns_enabled", "true"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "restricted_dns_servers.#", "2"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "restricted_dns_servers.0", "8.8.8.8"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "restricted_dns_servers.1", "1.1.1.1"), + ), + }, + pt.ImportStepWithSite("unifi_setting_guest_access.test"), + { + Config: testAccSettingGuestAccessConfig_restrictedDNS([]string{"8.8.4.4", "1.0.0.1", "9.9.9.9"}), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "restricted_dns_enabled", "true"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "restricted_dns_servers.#", "3"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "restricted_dns_servers.0", "8.8.4.4"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "restricted_dns_servers.1", "1.0.0.1"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "restricted_dns_servers.2", "9.9.9.9"), + ), + }, + { + Config: testAccSettingGuestAccessConfig_restrictedDNS([]string{}), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "restricted_dns_enabled", "false"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "restricted_dns_servers.#", "0"), + ), + }, + { + Config: testAccSettingGuestAccessConfig_basic(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "restricted_dns_enabled", "false"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "restricted_dns_servers.#", "0"), + ), + }, + }, + }) +} + +func TestAccSettingGuestAccess_portalCustomizationPostVersion74(t *testing.T) { + AcceptanceTest(t, AcceptanceTestCase{ + VersionConstraint: ">= 7.4", + Lock: settingGuestAccessLock, + Steps: []resource.TestStep{ + { + Config: testAccSettingGuestAccessConfig_portalCustomizationBasicPost74(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "portal_customization.customized", "true"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "portal_customization.bg_type", "color"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "portal_customization.box_radius", "12"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "portal_customization.button_text", "Login"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "portal_customization.authentication_text", "Please authenticate to access the internet"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "portal_customization.success_text", "You are now connected!"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "portal_customization.logo_position", "center"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "portal_customization.logo_size", "150"), + ), + }, + }, + }) +} + +func TestAccSettingGuestAccess_portalCustomization(t *testing.T) { + AcceptanceTest(t, AcceptanceTestCase{ + Lock: settingGuestAccessLock, + Steps: []resource.TestStep{ + { + // Initial configuration with color theme and basic settings + Config: testAccSettingGuestAccessConfig_portalCustomizationBasic(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "portal_customization.customized", "true"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "portal_customization.bg_color", "#f5f5f5"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "portal_customization.box_color", "#ffffff"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "portal_customization.box_opacity", "90"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "portal_customization.title", "Guest WiFi Portal"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "portal_customization.tos_enabled", "true"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "portal_customization.tos", "By using this WiFi service, you agree to our terms and conditions."), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "portal_customization.box_text_color", "#333333"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "portal_customization.text_color", "#222222"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "portal_customization.link_color", "#0066cc"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "portal_customization.box_link_color", "#0055aa"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "portal_customization.button_color", "#4CAF50"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "portal_customization.button_text_color", "#ffffff"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "portal_customization.languages.#", "3"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "portal_customization.languages.0", "en"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "portal_customization.languages.1", "es"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "portal_customization.languages.2", "fr"), + ), + }, + pt.ImportStepWithSite("unifi_setting_guest_access.test"), + { + // Update with gallery background and text customizations + Config: testAccSettingGuestAccessConfig_portalCustomizationGallery(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "portal_customization.customized", "true"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "portal_customization.unsplash_author_name", "John Doe"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "portal_customization.unsplash_author_username", "johndoe"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "portal_customization.welcome_text_enabled", "true"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "portal_customization.welcome_text", "Welcome to our WiFi network!"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "portal_customization.welcome_text_position", "above_boxes"), + ), + }, + { + // Disable customization + Config: testAccSettingGuestAccessConfig_portalCustomizationDisabled(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "portal_customization.customized", "false"), + ), + }, + { + // Back to basic configuration + Config: testAccSettingGuestAccessConfig_basic(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "portal_customization.customized", "false"), + resource.TestCheckResourceAttr("unifi_setting_guest_access.test", "portal_customization.%", "27"), + ), + }, + }, + }) +} + +func testAccSettingGuestAccessConfig_basic() string { + return ` +resource "unifi_setting_guest_access" "test" { + auth = "none" + portal_enabled = true + portal_use_hostname = true + portal_hostname = "guest.example.com" + template_engine = "angular" + expire = 60 + expire_number = 1 + expire_unit = 60 + ec_enabled = true +} +` +} + +func testAccSettingGuestAccessConfig_basicUpdated() string { + return ` +resource "unifi_setting_guest_access" "test" { + auth = "hotspot" + portal_enabled = false + template_engine = "jsp" + expire = 1440 + expire_number = 1 + expire_unit = 1440 + ec_enabled = false +} +` +} + +func testAccSettingGuestAccessConfig_auth(auth string) string { + return fmt.Sprintf(` +resource "unifi_setting_guest_access" "test" { + auth = "%s" +} +`, auth) +} + +func testAccSettingGuestAccessConfig_customAuth(ip string) string { + return fmt.Sprintf(` +resource "unifi_setting_guest_access" "test" { + auth = "custom" + custom_ip = %q +} +`, ip) +} + +func testAccSettingGuestAccessConfig_password(password string) string { + return fmt.Sprintf(` +resource "unifi_setting_guest_access" "test" { + auth = "hotspot" + password = %q +} +`, password) +} + +func testAccSettingGuestAccessConfig_voucher(enabled bool) string { + return fmt.Sprintf(` +resource "unifi_setting_guest_access" "test" { + auth = "hotspot" + voucher_enabled = %t +} +`, enabled) +} + +func testAccSettingGuestAccessConfig_voucherCustomized() string { + return ` +resource "unifi_setting_guest_access" "test" { + auth = "hotspot" + voucher_enabled = true + voucher_customized = true +} +` +} + +func testAccSettingGuestAccessConfig_allowedSubnet(subnet string) string { + return fmt.Sprintf(` +resource "unifi_setting_guest_access" "test" { + allowed_subnet = %q +} +`, subnet) +} + +func testAccSettingGuestAccessConfig_paymentPaypal(useSandbox bool) string { + return fmt.Sprintf(` +resource "unifi_setting_guest_access" "test" { + auth = "hotspot" + payment_gateway = "paypal" + paypal = { + username = "test@example.com" + password = "paypal-password" + signature = "paypal-signature" + use_sandbox = %t + } +} +`, useSandbox) +} + +func testAccSettingGuestAccessConfig_paymentPaypalUpdated() string { + return ` +resource "unifi_setting_guest_access" "test" { + auth = "hotspot" + payment_gateway = "paypal" + paypal = { + username = "updated@example.com" + password = "updated-password" + signature = "updated-signature" + use_sandbox = true + } +} +` +} + +func testAccSettingGuestAccessConfig_paymentStripe(apiKey string) string { + return fmt.Sprintf(` +resource "unifi_setting_guest_access" "test" { + auth = "hotspot" + payment_gateway = "stripe" + stripe = { + api_key = %q + } +} +`, apiKey) +} + +func testAccSettingGuestAccessConfig_paymentAuthorize(useSandbox bool) string { + return fmt.Sprintf(` +resource "unifi_setting_guest_access" "test" { + auth = "hotspot" + payment_gateway = "authorize" + authorize = { + login_id = "authorize-login" + transaction_key = "authorize-transaction-key" + use_sandbox = %t + } +} +`, useSandbox) +} + +func testAccSettingGuestAccessConfig_paymentQuickpay(useSandbox bool) string { + return fmt.Sprintf(` +resource "unifi_setting_guest_access" "test" { + auth = "hotspot" + payment_gateway = "quickpay" + quickpay = { + agreement_id = "quickpay-agreement" + api_key = "quickpay-api-key" + merchant_id = "quickpay-merchant" + use_sandbox = %t + } +} +`, useSandbox) +} + +func testAccSettingGuestAccessConfig_paymentMerchantWarrior(useSandbox bool) string { + return fmt.Sprintf(` +resource "unifi_setting_guest_access" "test" { + auth = "hotspot" + payment_gateway = "merchantwarrior" + merchant_warrior = { + api_key = "mw-api-key" + api_passphrase = "mw-passphrase" + merchant_uuid = "mw-merchant-id" + use_sandbox = %t + } +} +`, useSandbox) +} + +func testAccSettingGuestAccessConfig_paymentIPpay(useSandbox bool) string { + return fmt.Sprintf(` +resource "unifi_setting_guest_access" "test" { + auth = "hotspot" + payment_gateway = "ippay" + ippay = { + terminal_id = "ippay-terminal" + use_sandbox = %t + } +} +`, useSandbox) +} + +func testAccSettingGuestAccessConfig_redirect(url string, useHttps bool, toHttps bool) string { + return fmt.Sprintf(` +resource "unifi_setting_guest_access" "test" { + auth = "hotspot" + redirect = { + url = %q + use_https = %t + to_https = %t + } +} +`, url, useHttps, toHttps) +} + +func testAccSettingGuestAccessConfig_facebook(appId, appSecret string, scopeEmail bool) string { + return fmt.Sprintf(` +resource "unifi_setting_guest_access" "test" { + auth = "hotspot" + facebook = { + app_id = %q + app_secret = %q + scope_email = %t + } +} +`, appId, appSecret, scopeEmail) +} + +func testAccSettingGuestAccessConfig_google(clientId, clientSecret, domain string, scopeEmail bool) string { + domainConfig := "" + if domain != "" { + domainConfig = fmt.Sprintf(" domain = %q", domain) + } + + return fmt.Sprintf(` +resource "unifi_setting_guest_access" "test" { + auth = "hotspot" + google = { + client_id = %q + client_secret = %q +%s + scope_email = %t + } +} +`, clientId, clientSecret, domainConfig, scopeEmail) +} + +func testAccSettingGuestAccessConfig_radius(authType, profileId string, disconnectEnabled bool, disconnectPort int) string { + return fmt.Sprintf(` +resource "unifi_setting_guest_access" "test" { + auth = "hotspot" + radius = { + auth_type = %q + profile_id = %q + disconnect_enabled = %t + disconnect_port = %d + } +} +`, authType, profileId, disconnectEnabled, disconnectPort) +} + +func testAccSettingGuestAccessConfig_wechat(appId, appSecret, secretKey, shopId string) string { + shopIdConfig := "" + if shopId != "" { + shopIdConfig = fmt.Sprintf(" shop_id = %q", shopId) + } + + return fmt.Sprintf(` +resource "unifi_setting_guest_access" "test" { + auth = "hotspot" + wechat = { + app_id = %q + app_secret = %q + secret_key = %q +%s + } +} +`, appId, appSecret, secretKey, shopIdConfig) +} + +func testAccSettingGuestAccessConfig_facebookWifi(gatewayId, gatewayName, gatewaySecret string, blockHttps bool) string { + return fmt.Sprintf(` +resource "unifi_setting_guest_access" "test" { + auth = "facebook_wifi" + facebook_wifi = { + gateway_id = %q + gateway_name = %q + gateway_secret = %q + block_https = %t + } +} +`, gatewayId, gatewayName, gatewaySecret, blockHttps) +} + +func testAccSettingGuestAccessConfig_restrictedDNS(dnsServers []string) string { + serversStr := "" + for i, server := range dnsServers { + if i > 0 { + serversStr += ", " + } + serversStr += fmt.Sprintf("%q", server) + } + + return fmt.Sprintf(` +resource "unifi_setting_guest_access" "test" { + auth = "none" + restricted_dns_servers = [%s] +} +`, serversStr) +} + +func testAccSettingGuestAccessConfig_portalCustomizationBasic() string { + return ` +resource "unifi_setting_guest_access" "test" { + auth = "none" + portal_customization = { + customized = true + bg_color = "#f5f5f5" + box_color = "#ffffff" + box_opacity = 90 + title = "Guest WiFi Portal" + tos_enabled = true + tos = "By using this WiFi service, you agree to our terms and conditions." + box_text_color = "#333333" + text_color = "#222222" + link_color = "#0066cc" + box_link_color = "#0055aa" + button_color = "#4CAF50" + button_text_color = "#ffffff" + languages = ["en", "es", "fr"] + } +} +` +} + +func testAccSettingGuestAccessConfig_portalCustomizationBasicPost74() string { + return ` +resource "unifi_setting_guest_access" "test" { + auth = "none" + portal_customization = { + customized = true + bg_type = "color" + box_radius = 12 + button_text = "Login", + authentication_text = "Please authenticate to access the internet", + success_text = "You are now connected!", + logo_position = "center", + logo_size = 150 + } +} +` +} + +func testAccSettingGuestAccessConfig_portalCustomizationGallery() string { + return ` +resource "unifi_setting_guest_access" "test" { + auth = "none" + portal_customization = { + customized = true + unsplash_author_name = "John Doe" + unsplash_author_username = "johndoe" + welcome_text_enabled = true + welcome_text = "Welcome to our WiFi network!" + welcome_text_position = "above_boxes" + box_color = "#ffffff" + box_opacity = 90 + title = "Guest WiFi Portal" + } +} +` +} + +func testAccSettingGuestAccessConfig_portalCustomizationDisabled() string { + return ` +resource "unifi_setting_guest_access" "test" { + auth = "none" + portal_customization = { + customized = false + } +} +` +} diff --git a/internal/provider/base/base.go b/internal/provider/base/base.go index aa9fc73..8e8c0d8 100644 --- a/internal/provider/base/base.go +++ b/internal/provider/base/base.go @@ -3,7 +3,9 @@ package base import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -42,6 +44,20 @@ type DatasourceModel interface { Merge(context.Context, interface{}) diag.Diagnostics } +type NestedObject interface { + AttributeTypes() map[string]attr.Type +} + +func ObjectNull(obj interface{}) (basetypes.ObjectValue, diag.Diagnostics) { + diags := diag.Diagnostics{} + if nested, ok := obj.(NestedObject); ok { + obj := types.ObjectNull(nested.AttributeTypes()) + return obj, diags + } + diags.AddError("Invalid object type", fmt.Sprintf("Expected NestedObject, got: %T", obj)) + return types.ObjectNull(map[string]attr.Type{}), diags +} + type Model struct { ID types.String `tfsdk:"id"` Site types.String `tfsdk:"site"` diff --git a/internal/provider/base/client.go b/internal/provider/base/client.go index c886bfa..4100235 100644 --- a/internal/provider/base/client.go +++ b/internal/provider/base/client.go @@ -13,6 +13,7 @@ import ( "log" "net" "net/http" + "sync" "time" ) @@ -54,7 +55,7 @@ func NewClient(cfg *ClientConfig) (*Client, error) { return nil, err } c := &Client{ - Client: unifiClient, + Client: NewRetryableUnifiClient(unifiClient), Site: cfg.Site, Version: version.Must(version.NewVersion(unifiClient.Version())), } @@ -64,6 +65,41 @@ func NewClient(cfg *ClientConfig) (*Client, error) { return c, nil } +func NewRetryableUnifiClient(client unifi.Client) unifi.Client { + return &RetryableUnifiClient{ + Client: client, + loginMutex: sync.Mutex{}, + } +} + +type RetryableUnifiClient struct { + unifi.Client + loginMutex sync.Mutex +} + +func (c *RetryableUnifiClient) relogin(err error) error { + c.loginMutex.Lock() + defer c.loginMutex.Unlock() + loginErr := c.Client.Login() + if loginErr != nil { + return fmt.Errorf("Tried relogging in after %w, but failed: %w.", err, loginErr) + } else { + return nil + } +} + +func (c *RetryableUnifiClient) Do(ctx context.Context, method string, apiPath string, reqBody interface{}, respBody interface{}) error { + err := c.Client.Do(ctx, method, apiPath, reqBody, respBody) + if err != nil && IsServerErrorStatusCode(err, 401) { + err := c.relogin(err) + if err != nil { + return err + } + return c.Client.Do(ctx, method, apiPath, reqBody, respBody) + } + return err +} + type Client struct { unifi.Client Site string diff --git a/internal/provider/base/errors.go b/internal/provider/base/errors.go index b07c2e4..68c83b0 100644 --- a/internal/provider/base/errors.go +++ b/internal/provider/base/errors.go @@ -14,6 +14,17 @@ func ErrorInvalidModelMergeTarget(expectedType, actualType interface{}) diag.Dia return diag.NewErrorDiagnostic("Invalid model merge target", "Expected target type to be the same a receiver: "+e+". Was : "+a) } +func IsServerErrorStatusCode(err error, statusCode int) bool { + if err == nil { + return false + } + var se *unifi.ServerError + if errors.As(err, &se) { + return se.StatusCode == statusCode + } + return false +} + func IsServerErrorContains(err error, messageContains string) bool { if err == nil { return false diff --git a/internal/provider/base/features_test.go b/internal/provider/base/features_test.go index 26a8f66..d1fc140 100644 --- a/internal/provider/base/features_test.go +++ b/internal/provider/base/features_test.go @@ -148,6 +148,13 @@ func TestFeaturesIsUnavailable(t *testing.T) { } } +func newTestClient(mock *MockUnifiClient) *Client { + return &Client{ + Client: mock, + Site: "default", + } +} + // TestNewFeatureValidator tests the NewFeatureValidator function func TestNewFeatureValidator(t *testing.T) { mockUnifiClient := &MockUnifiClient{ @@ -156,10 +163,7 @@ func TestNewFeatureValidator(t *testing.T) { }, } - client := &Client{ - Client: mockUnifiClient, - Site: "default", - } + client := newTestClient(mockUnifiClient) validator := NewFeatureValidator(client) @@ -225,10 +229,7 @@ func TestGetFeatures(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockUnifiClient := tt.setup() - client := &Client{ - Client: mockUnifiClient, - Site: "default", - } + client := newTestClient(mockUnifiClient) validator := &featureEnabledValidator{ client: client, @@ -267,10 +268,7 @@ func TestGetFeaturesConcurrent(t *testing.T) { }, } - client := &Client{ - Client: mockUnifiClient, - Site: "default", - } + client := newTestClient(mockUnifiClient) validator := &featureEnabledValidator{ client: client, @@ -367,10 +365,7 @@ func TestRequireFeatures(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockUnifiClient := &MockUnifiClient{} - client := &Client{ - Client: mockUnifiClient, - Site: "default", - } + client := newTestClient(mockUnifiClient) validator := &featureEnabledValidator{ client: client, @@ -465,10 +460,7 @@ func TestRequireFeaturesEnabledForPath(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockUnifiClient := tt.setupClient() - client := &Client{ - Client: mockUnifiClient, - Site: "default", - } + client := newTestClient(mockUnifiClient) // Create a wrapper FeatureValidator that provides a minimal implementation // of RequireFeaturesEnabledForPath without needing a real tfsdk.Config @@ -538,10 +530,7 @@ func TestRequireFeaturesEnabled(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockUnifiClient := tt.setupClient() - client := &Client{ - Client: mockUnifiClient, - Site: "default", - } + client := newTestClient(mockUnifiClient) validator := NewFeatureValidator(client) diags := validator.RequireFeaturesEnabled(context.Background(), "site1", tt.requiredFeatures...) assert.Equal(t, tt.expectedHasErrors, diags.HasError()) @@ -605,10 +594,7 @@ func TestFeatureValidatorCache(t *testing.T) { }, } - client := &Client{ - Client: mockUnifiClient, - Site: "default", - } + client := newTestClient(mockUnifiClient) validator := NewFeatureValidator(client) diff --git a/internal/provider/provider_v2.go b/internal/provider/provider_v2.go index 5869075..d04b2bb 100644 --- a/internal/provider/provider_v2.go +++ b/internal/provider/provider_v2.go @@ -179,6 +179,7 @@ func (p *unifiProvider) Resources(_ context.Context) []func() resource.Resource settings.NewAutoSpeedtestResource, settings.NewCountryResource, settings.NewDpiResource, + settings.NewGuestAccessResource, settings.NewIpsResource, settings.NewLcmResource, settings.NewLocaleResource, diff --git a/internal/provider/settings/resource_setting_guest_access.go b/internal/provider/settings/resource_setting_guest_access.go new file mode 100644 index 0000000..13a380d --- /dev/null +++ b/internal/provider/settings/resource_setting_guest_access.go @@ -0,0 +1,1618 @@ +package settings + +import ( + "context" + "fmt" + "github.com/filipowm/terraform-provider-unifi/internal/utils" + "github.com/hashicorp/terraform-plugin-framework-validators/int32validator" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/listdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/filipowm/go-unifi/unifi" + + "github.com/filipowm/terraform-provider-unifi/internal/provider/base" + "github.com/filipowm/terraform-provider-unifi/internal/provider/validators" +) + +// TODO add support for uploading files and configuring logo and background custom images + +type guestAccessModel struct { + base.Model + AllowedSubnet types.String `tfsdk:"allowed_subnet"` + RestrictedSubnet types.String `tfsdk:"restricted_subnet"` + + Auth types.String `tfsdk:"auth"` + AuthUrl types.String `tfsdk:"auth_url"` + + Authorize types.Object `tfsdk:"authorize"` + + CustomIP types.String `tfsdk:"custom_ip"` + EcEnabled types.Bool `tfsdk:"ec_enabled"` + + Expire types.Int32 `tfsdk:"expire"` + ExpireNumber types.Int32 `tfsdk:"expire_number"` + ExpireUnit types.Int32 `tfsdk:"expire_unit"` + + FacebookEnabled types.Bool `tfsdk:"facebook_enabled"` + Facebook types.Object `tfsdk:"facebook"` + + FacebookWifi types.Object `tfsdk:"facebook_wifi"` + + GoogleEnabled types.Bool `tfsdk:"google_enabled"` + Google types.Object `tfsdk:"google"` + + IPpay types.Object `tfsdk:"ippay"` + MerchantWarrior types.Object `tfsdk:"merchant_warrior"` + + PasswordEnabled types.Bool `tfsdk:"password_enabled"` + Password types.String `tfsdk:"password"` + + PaymentEnabled types.Bool `tfsdk:"payment_enabled"` + PaymentGateway types.String `tfsdk:"payment_gateway"` + + Paypal types.Object `tfsdk:"paypal"` + + PortalCustomization types.Object `tfsdk:"portal_customization"` + + PortalEnabled types.Bool `tfsdk:"portal_enabled"` + PortalHostname types.String `tfsdk:"portal_hostname"` + PortalUseHostname types.Bool `tfsdk:"portal_use_hostname"` + + Quickpay types.Object `tfsdk:"quickpay"` + + RadiusEnabled types.Bool `tfsdk:"radius_enabled"` + Radius types.Object `tfsdk:"radius"` + + RedirectEnabled types.Bool `tfsdk:"redirect_enabled"` + Redirect types.Object `tfsdk:"redirect"` + + RestrictedDNSEnabled types.Bool `tfsdk:"restricted_dns_enabled"` + RestrictedDNSServers types.List `tfsdk:"restricted_dns_servers"` + + TemplateEngine types.String `tfsdk:"template_engine"` + + Stripe types.Object `tfsdk:"stripe"` + + VoucherCustomized types.Bool `tfsdk:"voucher_customized"` + VoucherEnabled types.Bool `tfsdk:"voucher_enabled"` + + WechatEnabled types.Bool `tfsdk:"wechat_enabled"` + Wechat types.Object `tfsdk:"wechat"` +} + +type portalCustomizationModel struct { + Customized types.Bool `tfsdk:"customized"` + AuthenticationText types.String `tfsdk:"authentication_text"` + BgColor types.String `tfsdk:"bg_color"` + BgImageTile types.Bool `tfsdk:"bg_image_tile"` + BgType types.String `tfsdk:"bg_type"` + BoxColor types.String `tfsdk:"box_color"` + BoxLinkColor types.String `tfsdk:"box_link_color"` + BoxOpacity types.Int32 `tfsdk:"box_opacity"` + BoxRadius types.Int32 `tfsdk:"box_radius"` + BoxTextColor types.String `tfsdk:"box_text_color"` + ButtonColor types.String `tfsdk:"button_color"` + ButtonText types.String `tfsdk:"button_text"` + ButtonTextColor types.String `tfsdk:"button_text_color"` + Languages types.List `tfsdk:"languages"` + LinkColor types.String `tfsdk:"link_color"` + LogoPosition types.String `tfsdk:"logo_position"` + LogoSize types.Int32 `tfsdk:"logo_size"` + SuccessText types.String `tfsdk:"success_text"` + TextColor types.String `tfsdk:"text_color"` + Title types.String `tfsdk:"title"` + Tos types.String `tfsdk:"tos"` + TosEnabled types.Bool `tfsdk:"tos_enabled"` + UnsplashAuthorName types.String `tfsdk:"unsplash_author_name"` + UnsplashAuthorUsername types.String `tfsdk:"unsplash_author_username"` + WelcomeText types.String `tfsdk:"welcome_text"` + WelcomeTextEnabled types.Bool `tfsdk:"welcome_text_enabled"` + WelcomeTextPosition types.String `tfsdk:"welcome_text_position"` +} + +func (m *portalCustomizationModel) AttributeTypes() map[string]attr.Type { + return map[string]attr.Type{ + "customized": types.BoolType, + "authentication_text": types.StringType, + "bg_color": types.StringType, + "bg_image_tile": types.BoolType, + "bg_type": types.StringType, + "box_color": types.StringType, + "box_link_color": types.StringType, + "box_opacity": types.Int32Type, + "box_radius": types.Int32Type, + "box_text_color": types.StringType, + "button_color": types.StringType, + "button_text": types.StringType, + "button_text_color": types.StringType, + "languages": types.ListType{ + ElemType: types.StringType, + }, + "link_color": types.StringType, + "logo_position": types.StringType, + "logo_size": types.Int32Type, + "success_text": types.StringType, + "text_color": types.StringType, + "title": types.StringType, + "tos": types.StringType, + "tos_enabled": types.BoolType, + "unsplash_author_name": types.StringType, + "unsplash_author_username": types.StringType, + "welcome_text": types.StringType, + "welcome_text_enabled": types.BoolType, + "welcome_text_position": types.StringType, + } +} + +type facebookModel struct { + AppID types.String `tfsdk:"app_id"` + AppSecret types.String `tfsdk:"app_secret"` + ScopeEmail types.Bool `tfsdk:"scope_email"` +} + +func (m *facebookModel) AttributeTypes() map[string]attr.Type { + return map[string]attr.Type{ + "app_id": types.StringType, + "app_secret": types.StringType, + "scope_email": types.BoolType, + } +} + +type facebookWifiModel struct { + BlockHttps types.Bool `tfsdk:"block_https"` + GwID types.String `tfsdk:"gateway_id"` + GwName types.String `tfsdk:"gateway_name"` + GwSecret types.String `tfsdk:"gateway_secret"` +} + +func (m *facebookWifiModel) AttributeTypes() map[string]attr.Type { + return map[string]attr.Type{ + "block_https": types.BoolType, + "gateway_id": types.StringType, + "gateway_name": types.StringType, + "gateway_secret": types.StringType, + } +} + +type googleModel struct { + ClientID types.String `tfsdk:"client_id"` + ClientSecret types.String `tfsdk:"client_secret"` + Domain types.String `tfsdk:"domain"` + ScopeEmail types.Bool `tfsdk:"scope_email"` +} + +func (m *googleModel) AttributeTypes() map[string]attr.Type { + return map[string]attr.Type{ + "client_id": types.StringType, + "client_secret": types.StringType, + "domain": types.StringType, + "scope_email": types.BoolType, + } +} + +type paypalModel struct { + Password types.String `tfsdk:"password"` + Username types.String `tfsdk:"username"` + UseSandbox types.Bool `tfsdk:"use_sandbox"` + Signature types.String `tfsdk:"signature"` +} + +func (m *paypalModel) AttributeTypes() map[string]attr.Type { + return map[string]attr.Type{ + "password": types.StringType, + "username": types.StringType, + "use_sandbox": types.BoolType, + "signature": types.StringType, + } +} + +type ipPayModel struct { + UseSandbox types.Bool `tfsdk:"use_sandbox"` + TerminalID types.String `tfsdk:"terminal_id"` +} + +func (m *ipPayModel) AttributeTypes() map[string]attr.Type { + return map[string]attr.Type{ + "use_sandbox": types.BoolType, + "terminal_id": types.StringType, + } +} + +type quickpayModel struct { + AgreementID types.String `tfsdk:"agreement_id"` + ApiKey types.String `tfsdk:"api_key"` + MerchantID types.String `tfsdk:"merchant_id"` + UseSandbox types.Bool `tfsdk:"use_sandbox"` +} + +func (m *quickpayModel) AttributeTypes() map[string]attr.Type { + return map[string]attr.Type{ + "agreement_id": types.StringType, + "api_key": types.StringType, + "merchant_id": types.StringType, + "use_sandbox": types.BoolType, + } +} + +type radiusModel struct { + AuthType types.String `tfsdk:"auth_type"` + DisconnectEnabled types.Bool `tfsdk:"disconnect_enabled"` + DisconnectPort types.Int32 `tfsdk:"disconnect_port"` + ProfileID types.String `tfsdk:"profile_id"` +} + +func (m *radiusModel) AttributeTypes() map[string]attr.Type { + return map[string]attr.Type{ + "auth_type": types.StringType, + "disconnect_enabled": types.BoolType, + "disconnect_port": types.Int32Type, + "profile_id": types.StringType, + } +} + +type redirectModel struct { + UseHttps types.Bool `tfsdk:"use_https"` + ToHttps types.Bool `tfsdk:"to_https"` + Url types.String `tfsdk:"url"` +} + +func (m *redirectModel) AttributeTypes() map[string]attr.Type { + return map[string]attr.Type{ + "use_https": types.BoolType, + "to_https": types.BoolType, + "url": types.StringType, + } +} + +type wechatModel struct { + AppID types.String `tfsdk:"app_id"` + AppSecret types.String `tfsdk:"app_secret"` + ShopID types.String `tfsdk:"shop_id"` + SecretKey types.String `tfsdk:"secret_key"` +} + +func (m *wechatModel) AttributeTypes() map[string]attr.Type { + return map[string]attr.Type{ + "app_id": types.StringType, + "app_secret": types.StringType, + "shop_id": types.StringType, + "secret_key": types.StringType, + } +} + +type authorizeModel struct { + LoginID types.String `tfsdk:"login_id"` + TransactionKey types.String `tfsdk:"transaction_key"` + UseSandbox types.Bool `tfsdk:"use_sandbox"` +} + +func (m *authorizeModel) AttributeTypes() map[string]attr.Type { + return map[string]attr.Type{ + "login_id": types.StringType, + "transaction_key": types.StringType, + "use_sandbox": types.BoolType, + } +} + +type merchantWarriorModel struct { + ApiKey types.String `tfsdk:"api_key"` + ApiPassphrase types.String `tfsdk:"api_passphrase"` + MerchantID types.String `tfsdk:"merchant_uuid"` + UseSandbox types.Bool `tfsdk:"use_sandbox"` +} + +func (m *merchantWarriorModel) AttributeTypes() map[string]attr.Type { + return map[string]attr.Type{ + "api_key": types.StringType, + "api_passphrase": types.StringType, + "merchant_uuid": types.StringType, + "use_sandbox": types.BoolType, + } +} + +type stripeModel struct { + ApiKey types.String `tfsdk:"api_key"` +} + +func (m *stripeModel) AttributeTypes() map[string]attr.Type { + return map[string]attr.Type{ + "api_key": types.StringType, + } +} + +func (d *guestAccessModel) AsUnifiModel(ctx context.Context) (interface{}, diag.Diagnostics) { + var diags diag.Diagnostics + + model := &unifi.SettingGuestAccess{ + AllowedSubnet: d.AllowedSubnet.ValueString(), + RestrictedSubnet: d.RestrictedSubnet.ValueString(), + Auth: d.Auth.ValueString(), + AuthUrl: d.AuthUrl.ValueString(), + CustomIP: d.CustomIP.ValueString(), + EcEnabled: d.EcEnabled.ValueBool(), + Expire: int(d.Expire.ValueInt32()), + ExpireNumber: int(d.ExpireNumber.ValueInt32()), + ExpireUnit: int(d.ExpireUnit.ValueInt32()), + + PortalEnabled: d.PortalEnabled.ValueBool(), + PortalHostname: d.PortalHostname.ValueString(), + PortalUseHostname: d.PortalUseHostname.ValueBool(), + TemplateEngine: d.TemplateEngine.ValueString(), + VoucherCustomized: d.VoucherCustomized.ValueBool(), + VoucherEnabled: d.VoucherEnabled.ValueBool(), + } + if base.IsEmptyString(d.Password) { + model.PasswordEnabled = false + } else { + model.PasswordEnabled = true + model.XPassword = d.Password.ValueString() + } + diags = d.paymentAsUnifiModel(ctx, model) + if diags.HasError() { + return nil, diags + } + if base.IsDefined(d.Redirect) { + var redirect *redirectModel + diags.Append(d.Redirect.As(ctx, &redirect, basetypes.ObjectAsOptions{})...) + if diags.HasError() { + return nil, diags + } + model.RedirectEnabled = true + model.RedirectUrl = redirect.Url.ValueString() + model.RedirectToHttps = redirect.ToHttps.ValueBool() + model.RedirectHttps = redirect.UseHttps.ValueBool() + } else { + model.RedirectEnabled = false + } + + if base.IsDefined(d.Facebook) { + var facebook *facebookModel + diags.Append(d.Facebook.As(ctx, &facebook, basetypes.ObjectAsOptions{})...) + if diags.HasError() { + return nil, diags + } + model.FacebookEnabled = true + model.FacebookAppID = facebook.AppID.ValueString() + model.XFacebookAppSecret = facebook.AppSecret.ValueString() + model.FacebookScopeEmail = facebook.ScopeEmail.ValueBool() + } else { + model.FacebookEnabled = false + } + + if base.IsDefined(d.Google) { + var google *googleModel + diags.Append(d.Google.As(ctx, &google, basetypes.ObjectAsOptions{})...) + if diags.HasError() { + return nil, diags + } + model.GoogleEnabled = true + model.GoogleClientID = google.ClientID.ValueString() + model.XGoogleClientSecret = google.ClientSecret.ValueString() + model.GoogleScopeEmail = google.ScopeEmail.ValueBool() + model.GoogleDomain = google.Domain.ValueString() + } else { + model.GoogleEnabled = false + } + + if base.IsDefined(d.Radius) { + var radius *radiusModel + diags.Append(d.Radius.As(ctx, &radius, basetypes.ObjectAsOptions{})...) + if diags.HasError() { + return nil, diags + } + model.RADIUSEnabled = true + model.RADIUSAuthType = radius.AuthType.ValueString() + model.RADIUSDisconnectEnabled = radius.DisconnectEnabled.ValueBool() + model.RADIUSDisconnectPort = int(radius.DisconnectPort.ValueInt32()) + model.RADIUSProfileID = radius.ProfileID.ValueString() + } else { + model.RADIUSEnabled = false + } + + if base.IsDefined(d.Wechat) { + var wechat *wechatModel + diags.Append(d.Wechat.As(ctx, &wechat, basetypes.ObjectAsOptions{})...) + if diags.HasError() { + return nil, diags + } + model.WechatEnabled = true + model.WechatAppID = wechat.AppID.ValueString() + model.XWechatAppSecret = wechat.AppSecret.ValueString() + model.WechatShopID = wechat.ShopID.ValueString() + model.XWechatSecretKey = wechat.SecretKey.ValueString() + } else { + model.WechatEnabled = false + } + + if base.IsDefined(d.FacebookWifi) { + var facebookWifi *facebookWifiModel + diags.Append(d.FacebookWifi.As(ctx, &facebookWifi, basetypes.ObjectAsOptions{})...) + if diags.HasError() { + return nil, diags + } + model.FacebookWifiBlockHttps = facebookWifi.BlockHttps.ValueBool() + model.FacebookWifiGwID = facebookWifi.GwID.ValueString() + model.FacebookWifiGwName = facebookWifi.GwName.ValueString() + model.XFacebookWifiGwSecret = facebookWifi.GwSecret.ValueString() + } + + if base.IsDefined(d.RestrictedDNSServers) { + var servers []string + diags := utils.ListElementsAs(d.RestrictedDNSServers, &servers) + if diags.HasError() { + return nil, diags + } + if len(servers) > 0 { + model.RestrictedDNSEnabled = true + } + model.RestrictedDNSServers = servers + } else { + model.RestrictedDNSEnabled = false + } + + if base.IsDefined(d.PortalCustomization) { + var portalCustomization *portalCustomizationModel + diags.Append(d.PortalCustomization.As(ctx, &portalCustomization, basetypes.ObjectAsOptions{})...) + if diags.HasError() { + return nil, diags + } + var languages []string + diags := utils.ListElementsAs(portalCustomization.Languages, &languages) + if diags.HasError() { + return nil, diags + } + model.PortalCustomized = portalCustomization.Customized.ValueBool() + model.PortalCustomizedAuthenticationText = portalCustomization.AuthenticationText.ValueString() + model.PortalCustomizedBgColor = portalCustomization.BgColor.ValueString() + model.PortalCustomizedBgImageTile = portalCustomization.BgImageTile.ValueBool() + model.PortalCustomizedBgType = portalCustomization.BgType.ValueString() + model.PortalCustomizedBoxColor = portalCustomization.BoxColor.ValueString() + model.PortalCustomizedBoxLinkColor = portalCustomization.BoxLinkColor.ValueString() + model.PortalCustomizedBoxOpacity = int(portalCustomization.BoxOpacity.ValueInt32()) + model.PortalCustomizedBoxRADIUS = int(portalCustomization.BoxRadius.ValueInt32()) + model.PortalCustomizedBoxTextColor = portalCustomization.BoxTextColor.ValueString() + model.PortalCustomizedButtonColor = portalCustomization.ButtonColor.ValueString() + model.PortalCustomizedButtonText = portalCustomization.ButtonText.ValueString() + model.PortalCustomizedButtonTextColor = portalCustomization.ButtonTextColor.ValueString() + model.PortalCustomizedLanguages = languages + model.PortalCustomizedLinkColor = portalCustomization.LinkColor.ValueString() + model.PortalCustomizedLogoPosition = portalCustomization.LogoPosition.ValueString() + model.PortalCustomizedLogoSize = int(portalCustomization.LogoSize.ValueInt32()) + model.PortalCustomizedSuccessText = portalCustomization.SuccessText.ValueString() + model.PortalCustomizedTextColor = portalCustomization.TextColor.ValueString() + model.PortalCustomizedTitle = portalCustomization.Title.ValueString() + model.PortalCustomizedTos = portalCustomization.Tos.ValueString() + model.PortalCustomizedTosEnabled = portalCustomization.TosEnabled.ValueBool() + model.PortalCustomizedUnsplashAuthorName = portalCustomization.UnsplashAuthorName.ValueString() + model.PortalCustomizedUnsplashAuthorUsername = portalCustomization.UnsplashAuthorUsername.ValueString() + model.PortalCustomizedWelcomeText = portalCustomization.WelcomeText.ValueString() + model.PortalCustomizedWelcomeTextEnabled = portalCustomization.WelcomeTextEnabled.ValueBool() + model.PortalCustomizedWelcomeTextPosition = portalCustomization.WelcomeTextPosition.ValueString() + } else { + model.PortalCustomized = false + } + + return model, diags +} + +func (d *guestAccessModel) paymentAsUnifiModel(ctx context.Context, model *unifi.SettingGuestAccess) diag.Diagnostics { + diags := diag.Diagnostics{} + if base.IsEmptyString(d.PaymentGateway) { + model.PaymentEnabled = false + } else { + gateway := d.PaymentGateway.ValueString() + model.PaymentEnabled = true + model.Gateway = gateway + switch gateway { + case "authorize": + var authorize *authorizeModel + diags.Append(d.Authorize.As(ctx, &authorize, basetypes.ObjectAsOptions{})...) + if diags.HasError() { + return diags + } + if base.IsDefined(authorize.UseSandbox) { + model.AuthorizeUseSandbox = authorize.UseSandbox.ValueBool() + } + model.XAuthorizeLoginid = authorize.LoginID.ValueString() + model.XAuthorizeTransactionkey = authorize.TransactionKey.ValueString() + case "ippay": + var ippay *ipPayModel + diags.Append(d.IPpay.As(ctx, &ippay, basetypes.ObjectAsOptions{})...) + if diags.HasError() { + return diags + } + if base.IsDefined(ippay.UseSandbox) { + model.IPpayUseSandbox = ippay.UseSandbox.ValueBool() + } + model.XIPpayTerminalid = ippay.TerminalID.ValueString() + case "merchantwarrior": + var merchantWarrior *merchantWarriorModel + diags.Append(d.MerchantWarrior.As(ctx, &merchantWarrior, basetypes.ObjectAsOptions{})...) + if diags.HasError() { + return diags + } + if base.IsDefined(merchantWarrior.UseSandbox) { + model.MerchantwarriorUseSandbox = merchantWarrior.UseSandbox.ValueBool() + } + model.XMerchantwarriorApikey = merchantWarrior.ApiKey.ValueString() + model.XMerchantwarriorApipassphrase = merchantWarrior.ApiPassphrase.ValueString() + model.XMerchantwarriorMerchantuuid = merchantWarrior.MerchantID.ValueString() + case "paypal": + var paypal *paypalModel + diags.Append(d.Paypal.As(ctx, &paypal, basetypes.ObjectAsOptions{})...) + if diags.HasError() { + return diags + } + if base.IsDefined(paypal.UseSandbox) { + model.PaypalUseSandbox = paypal.UseSandbox.ValueBool() + } + model.XPaypalPassword = paypal.Password.ValueString() + model.XPaypalUsername = paypal.Username.ValueString() + model.XPaypalSignature = paypal.Signature.ValueString() + case "quickpay": + var quickpay *quickpayModel + diags.Append(d.Quickpay.As(ctx, &quickpay, basetypes.ObjectAsOptions{})...) + if diags.HasError() { + return diags + } + if base.IsDefined(quickpay.UseSandbox) { + model.QuickpayTestmode = quickpay.UseSandbox.ValueBool() + } + model.XQuickpayAgreementid = quickpay.AgreementID.ValueString() + model.XQuickpayApikey = quickpay.ApiKey.ValueString() + model.XQuickpayMerchantid = quickpay.MerchantID.ValueString() + case "stripe": + var stripe *stripeModel + diags.Append(d.Stripe.As(ctx, &stripe, basetypes.ObjectAsOptions{})...) + if diags.HasError() { + return diags + } + model.XStripeApiKey = stripe.ApiKey.ValueString() + default: + diags.AddError("Invalid payment gateway", fmt.Sprintf("Payment gateway %q is not supported", gateway)) + } + } + return diags +} + +func (d *guestAccessModel) mergePaymentModel(ctx context.Context, model *unifi.SettingGuestAccess) diag.Diagnostics { + diags := diag.Diagnostics{} + switch model.Gateway { + case "authorize": + authorize := &authorizeModel{ + LoginID: types.StringValue(model.XAuthorizeLoginid), + TransactionKey: types.StringValue(model.XAuthorizeTransactionkey), + UseSandbox: types.BoolValue(model.AuthorizeUseSandbox), + } + d.Authorize, diags = types.ObjectValueFrom(ctx, authorize.AttributeTypes(), authorize) + case "ippay": + ippay := &ipPayModel{ + UseSandbox: types.BoolValue(model.IPpayUseSandbox), + TerminalID: types.StringValue(model.XIPpayTerminalid), + } + d.IPpay, diags = types.ObjectValueFrom(ctx, ippay.AttributeTypes(), ippay) + case "merchantwarrior": + merchantWarrior := &merchantWarriorModel{ + ApiKey: types.StringValue(model.XMerchantwarriorApikey), + ApiPassphrase: types.StringValue(model.XMerchantwarriorApipassphrase), + MerchantID: types.StringValue(model.XMerchantwarriorMerchantuuid), + UseSandbox: types.BoolValue(model.MerchantwarriorUseSandbox), + } + d.MerchantWarrior, diags = types.ObjectValueFrom(ctx, merchantWarrior.AttributeTypes(), merchantWarrior) + case "paypal": + paypal := &paypalModel{ + Password: types.StringValue(model.XPaypalPassword), + Username: types.StringValue(model.XPaypalUsername), + UseSandbox: types.BoolValue(model.PaypalUseSandbox), + Signature: types.StringValue(model.XPaypalSignature), + } + d.Paypal, diags = types.ObjectValueFrom(ctx, paypal.AttributeTypes(), paypal) + case "quickpay": + quickpay := &quickpayModel{ + AgreementID: types.StringValue(model.XQuickpayAgreementid), + ApiKey: types.StringValue(model.XQuickpayApikey), + MerchantID: types.StringValue(model.XQuickpayMerchantid), + UseSandbox: types.BoolValue(model.QuickpayTestmode), + } + d.Quickpay, diags = types.ObjectValueFrom(ctx, quickpay.AttributeTypes(), quickpay) + case "stripe": + stripe := &stripeModel{ + ApiKey: types.StringValue(model.XStripeApiKey), + } + d.Stripe, diags = types.ObjectValueFrom(ctx, stripe.AttributeTypes(), stripe) + default: + diags.AddError("Invalid payment gateway", fmt.Sprintf("Payment gateway returned by controller is not supported: %s", model.Gateway)) + } + return diags +} + +func (d *guestAccessModel) Merge(ctx context.Context, unifiModel interface{}) diag.Diagnostics { + diags := diag.Diagnostics{} + + model, ok := unifiModel.(*unifi.SettingGuestAccess) + if !ok { + diags.AddError("Invalid model type", "Expected *unifi.SettingGuestAccess") + return diags + } + + d.ID = types.StringValue(model.ID) + d.AllowedSubnet = types.StringValue(model.AllowedSubnet) + d.RestrictedSubnet = types.StringValue(model.RestrictedSubnet) + d.Auth = types.StringValue(model.Auth) + d.AuthUrl = types.StringValue(model.AuthUrl) + switch model.Auth { + case "custom": + d.CustomIP = types.StringValue(model.CustomIP) + default: + d.CustomIP = types.StringNull() + } + d.EcEnabled = types.BoolValue(model.EcEnabled) + d.Expire = types.Int32Value(int32(model.Expire)) + d.ExpireNumber = types.Int32Value(int32(model.ExpireNumber)) + d.ExpireUnit = types.Int32Value(int32(model.ExpireUnit)) + + d.PaymentEnabled = types.BoolValue(model.PaymentEnabled) + var od diag.Diagnostics + d.Authorize, od = base.ObjectNull(&authorizeModel{}) + diags.Append(od...) + d.Paypal, od = base.ObjectNull(&paypalModel{}) + diags.Append(od...) + d.IPpay, od = base.ObjectNull(&ipPayModel{}) + diags.Append(od...) + d.MerchantWarrior, od = base.ObjectNull(&merchantWarriorModel{}) + diags.Append(od...) + d.Quickpay, od = base.ObjectNull(&quickpayModel{}) + diags.Append(od...) + d.Stripe, od = base.ObjectNull(&stripeModel{}) + diags.Append(od...) + if diags.HasError() { + return diags + } + if model.PaymentEnabled { + d.PaymentGateway = types.StringValue(model.Gateway) + d.mergePaymentModel(ctx, model) + } else { + d.PaymentGateway = types.StringNull() + } + + d.PasswordEnabled = types.BoolValue(model.PasswordEnabled) + if model.PasswordEnabled { + d.Password = types.StringValue(model.XPassword) + } else { + d.Password = types.StringNull() + } + + d.RedirectEnabled = types.BoolValue(model.RedirectEnabled) + d.Redirect, diags = base.ObjectNull(&redirectModel{}) + if diags.HasError() { + return diags + } + if model.RedirectEnabled { + redirect := &redirectModel{ + UseHttps: types.BoolValue(model.RedirectHttps), + ToHttps: types.BoolValue(model.RedirectToHttps), + Url: types.StringValue(model.RedirectUrl), + } + d.Redirect, diags = types.ObjectValueFrom(ctx, redirect.AttributeTypes(), redirect) + if diags.HasError() { + return diags + } + } + + d.FacebookEnabled = types.BoolValue(model.FacebookEnabled) + d.Facebook, diags = base.ObjectNull(&facebookModel{}) + if diags.HasError() { + return diags + } + if model.FacebookEnabled { + facebook := &facebookModel{ + AppID: types.StringValue(model.FacebookAppID), + AppSecret: types.StringValue(model.XFacebookAppSecret), + ScopeEmail: types.BoolValue(model.FacebookScopeEmail), + } + d.Facebook, diags = types.ObjectValueFrom(ctx, facebook.AttributeTypes(), facebook) + if diags.HasError() { + return diags + } + } + + d.GoogleEnabled = types.BoolValue(model.GoogleEnabled) + d.Google, diags = base.ObjectNull(&googleModel{}) + if diags.HasError() { + return diags + } + if model.GoogleEnabled { + google := &googleModel{ + ClientID: types.StringValue(model.GoogleClientID), + ClientSecret: types.StringValue(model.XGoogleClientSecret), + Domain: types.StringValue(model.GoogleDomain), + ScopeEmail: types.BoolValue(model.GoogleScopeEmail), + } + d.Google, diags = types.ObjectValueFrom(ctx, google.AttributeTypes(), google) + if diags.HasError() { + return diags + } + } + + d.RadiusEnabled = types.BoolValue(model.RADIUSEnabled) + d.Radius, diags = base.ObjectNull(&radiusModel{}) + if diags.HasError() { + return diags + } + if model.RADIUSEnabled { + radius := &radiusModel{ + AuthType: types.StringValue(model.RADIUSAuthType), + DisconnectEnabled: types.BoolValue(model.RADIUSDisconnectEnabled), + DisconnectPort: types.Int32Value(int32(model.RADIUSDisconnectPort)), + ProfileID: types.StringValue(model.RADIUSProfileID), + } + d.Radius, diags = types.ObjectValueFrom(ctx, radius.AttributeTypes(), radius) + if diags.HasError() { + return diags + } + } + + d.WechatEnabled = types.BoolValue(model.WechatEnabled) + d.Wechat, diags = base.ObjectNull(&wechatModel{}) + if diags.HasError() { + return diags + } + if model.WechatEnabled { + wechat := &wechatModel{ + AppID: types.StringValue(model.WechatAppID), + ShopID: types.StringValue(model.WechatShopID), + AppSecret: types.StringValue(model.XWechatAppSecret), + SecretKey: types.StringValue(model.XWechatSecretKey), + } + d.Wechat, diags = types.ObjectValueFrom(ctx, wechat.AttributeTypes(), wechat) + if diags.HasError() { + return diags + } + } + + d.FacebookWifi, diags = base.ObjectNull(&facebookWifiModel{}) + if diags.HasError() { + return diags + } + if model.Auth == "facebook_wifi" { + facebookWifi := &facebookWifiModel{ + BlockHttps: types.BoolValue(model.FacebookWifiBlockHttps), + GwID: types.StringValue(model.FacebookWifiGwID), + GwName: types.StringValue(model.FacebookWifiGwName), + GwSecret: types.StringValue(model.XFacebookWifiGwSecret), + } + d.FacebookWifi, diags = types.ObjectValueFrom(ctx, facebookWifi.AttributeTypes(), facebookWifi) + if diags.HasError() { + return diags + } + } + + d.RestrictedDNSEnabled = types.BoolValue(model.RestrictedDNSEnabled) + if model.RestrictedDNSEnabled && len(model.RestrictedDNSServers) > 0 { + d.RestrictedDNSServers, diags = types.ListValueFrom(ctx, types.StringType, model.RestrictedDNSServers) + if diags.HasError() { + return diags + } + } else { + d.RestrictedDNSServers = utils.EmptyList(types.StringType) + } + + languages, diags := types.ListValueFrom(ctx, types.StringType, model.PortalCustomizedLanguages) + customizations := &portalCustomizationModel{ + Customized: types.BoolValue(model.PortalCustomized), + AuthenticationText: types.StringValue(model.PortalCustomizedAuthenticationText), + BgColor: types.StringValue(model.PortalCustomizedBgColor), + BgImageTile: types.BoolValue(model.PortalCustomizedBgImageTile), + BgType: types.StringValue(model.PortalCustomizedBgType), + BoxColor: types.StringValue(model.PortalCustomizedBoxColor), + BoxLinkColor: types.StringValue(model.PortalCustomizedBoxLinkColor), + BoxOpacity: types.Int32Value(int32(model.PortalCustomizedBoxOpacity)), + BoxRadius: types.Int32Value(int32(model.PortalCustomizedBoxRADIUS)), + BoxTextColor: types.StringValue(model.PortalCustomizedBoxTextColor), + ButtonColor: types.StringValue(model.PortalCustomizedButtonColor), + ButtonText: types.StringValue(model.PortalCustomizedButtonText), + ButtonTextColor: types.StringValue(model.PortalCustomizedButtonTextColor), + Languages: languages, + LinkColor: types.StringValue(model.PortalCustomizedLinkColor), + LogoPosition: types.StringValue(model.PortalCustomizedLogoPosition), + LogoSize: types.Int32Value(int32(model.PortalCustomizedLogoSize)), + SuccessText: types.StringValue(model.PortalCustomizedSuccessText), + TextColor: types.StringValue(model.PortalCustomizedTextColor), + Title: types.StringValue(model.PortalCustomizedTitle), + Tos: types.StringValue(model.PortalCustomizedTos), + TosEnabled: types.BoolValue(model.PortalCustomizedTosEnabled), + UnsplashAuthorName: types.StringValue(model.PortalCustomizedUnsplashAuthorName), + UnsplashAuthorUsername: types.StringValue(model.PortalCustomizedUnsplashAuthorUsername), + WelcomeText: types.StringValue(model.PortalCustomizedWelcomeText), + WelcomeTextEnabled: types.BoolValue(model.PortalCustomizedWelcomeTextEnabled), + WelcomeTextPosition: types.StringValue(model.PortalCustomizedWelcomeTextPosition), + } + d.PortalCustomization, diags = types.ObjectValueFrom(ctx, customizations.AttributeTypes(), customizations) + if diags.HasError() { + return diags + } + + d.PortalEnabled = types.BoolValue(model.PortalEnabled) + d.PortalHostname = types.StringValue(model.PortalHostname) + d.PortalUseHostname = types.BoolValue(model.PortalUseHostname) + + d.TemplateEngine = types.StringValue(model.TemplateEngine) + d.VoucherCustomized = types.BoolValue(model.VoucherCustomized) + d.VoucherEnabled = types.BoolValue(model.VoucherEnabled) + + return diags +} + +var ( + _ resource.Resource = &guestAccessResource{} + _ resource.ResourceWithConfigure = &guestAccessResource{} + _ resource.ResourceWithImportState = &guestAccessResource{} + _ resource.ResourceWithConfigValidators = &guestAccessResource{} + _ resource.ResourceWithModifyPlan = &guestAccessResource{} + _ base.Resource = &guestAccessResource{} +) + +type guestAccessResource struct { + *base.GenericResource[*guestAccessModel] +} + +func (g *guestAccessResource) ModifyPlan(_ context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + resp.Diagnostics.Append(g.RequireMinVersionForPath("7.4", path.Root("portal_customization").AtName("bg_type"), req.Config)...) + resp.Diagnostics.Append(g.RequireMinVersionForPath("7.4", path.Root("portal_customization").AtName("box_radius"), req.Config)...) + resp.Diagnostics.Append(g.RequireMinVersionForPath("7.4", path.Root("portal_customization").AtName("button_text"), req.Config)...) + resp.Diagnostics.Append(g.RequireMinVersionForPath("7.4", path.Root("portal_customization").AtName("success_text"), req.Config)...) + resp.Diagnostics.Append(g.RequireMinVersionForPath("7.4", path.Root("portal_customization").AtName("authentication_text"), req.Config)...) + resp.Diagnostics.Append(g.RequireMinVersionForPath("7.4", path.Root("portal_customization").AtName("logo_size"), req.Config)...) + resp.Diagnostics.Append(g.RequireMinVersionForPath("7.4", path.Root("portal_customization").AtName("logo_position"), req.Config)...) +} + +func requiredTogetherIfTrue(condition string, attrs ...string) validators.RequiredTogetherIfValidator { + var expressions []path.Expression + for _, attr := range attrs { + expressions = append(expressions, path.MatchRoot(attr)) + } + return validators.RequiredTogetherIf(path.MatchRoot(condition), types.BoolValue(true), expressions...) +} + +func requiredTogetherIfStringVal(condition, value string, attrs ...string) validators.RequiredTogetherIfValidator { + var expressions []path.Expression + for _, attr := range attrs { + expressions = append(expressions, path.MatchRoot(attr)) + } + return validators.RequiredTogetherIf(path.MatchRoot(condition), types.StringValue(value), expressions...) +} + +func requiredStringValueIfTrue(conditionAttr, targetAttr, targetVal string) validators.RequiredValueIfValidator { + return validators.RequiredValueIf(path.MatchRoot(conditionAttr), types.BoolValue(true), path.MatchRoot(targetAttr), types.StringValue(targetVal)) +} + +func (g *guestAccessResource) ConfigValidators(_ context.Context) []resource.ConfigValidator { + return []resource.ConfigValidator{ + // Auth validators + requiredTogetherIfStringVal("auth", "custom", "custom_ip"), + //requiredTogetherIfStringVal("auth", "facebook_wifi", "facebook_wifi.gateway_id", "facebook_wifi.gateway_name", "facebook_wifi.gateway_secret"), + + // Facebook validators + + // Google validators + requiredTogetherIfTrue("google.enabled", "google.client_id", "google.client_secret"), + requiredStringValueIfTrue("google.enabled", "auth", "hotspot"), + + // Password validators + requiredTogetherIfTrue("password_enabled", "password"), + requiredStringValueIfTrue("password_enabled", "auth", "hotspot"), + + // Payment validators + requiredTogetherIfStringVal("payment_gateway", "authorize", "authorize"), + requiredTogetherIfStringVal("payment_gateway", "ippay", "ippay"), + requiredTogetherIfStringVal("payment_gateway", "merchantwarrior", "merchant_warrior"), + requiredTogetherIfStringVal("payment_gateway", "paypal", "paypal"), + requiredTogetherIfStringVal("payment_gateway", "quickpay", "quickpay"), + requiredTogetherIfStringVal("payment_gateway", "stripe", "stripe"), + + // Portal validators + //requiredTogetherIfStringVal("portal_customized_bg_type", "color", "portal_customized_bg_color"), + //requiredTogetherIfStringVal("portal_customized_bg_type", "gallery", "portal_customized_unsplash_author_name", "portal_customized_unsplash_author_username"), + //requiredTogetherIfStringVal("portal_customized_bg_type", "image", "portal_customized_bg_image_filename"), + //requiredTogetherIfTrue("portal_customized_bg_image_enabled", "portal_customized_bg_image_filename"), + //requiredTogetherIfTrue("portal_customized_logo_enabled", "portal_customized_logo_filename"), + //requiredTogetherIfTrue("portal_customized_tos_enabled", "portal_customized_tos"), + //requiredTogetherIfTrue("portal_customized_welcome_text_enabled", "portal_customized_welcome_text"), + //requiredTogetherIfTrue("portal_use_hostname", "portal_hostname"), + + // RADIUS validators + //requiredTogetherIfTrue("radius_disconnect_enabled", "radius_disconnect_port"), + //requiredTogetherIfTrue("radius_enabled", "radius_auth_type", "radius_profile_id"), + + // Restricted DNS validators + //requiredTogetherIfTrue("restricted_dns_enabled", "restricted_dns_servers"), + + // Voucher validators + requiredStringValueIfTrue("voucher_enabled", "auth", "hotspot"), + } +} + +func (g *guestAccessResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "The `unifi_setting_guest_access` resource manages the guest access settings in the UniFi controller.\n\nThis resource allows you to configure all aspects of guest network access including authentication methods, portal customization, and payment options.", + Attributes: map[string]schema.Attribute{ + "id": base.ID(), + "site": base.SiteAttribute(), + "allowed_subnet": schema.StringAttribute{ + MarkdownDescription: "Subnet allowed for guest access.", + Optional: true, + Computed: true, + }, + "restricted_subnet": schema.StringAttribute{ + MarkdownDescription: "Subnet for restricted guest access.", + Optional: true, + Computed: true, + }, + "auth": schema.StringAttribute{ + MarkdownDescription: "Authentication method for guest access. Valid values are:\n" + + "* `none` - No authentication required\n" + + "* `hotspot` - Password authentication\n" + + "* `facebook_wifi` - Facebook auth entication\n" + + "* `custom` - Custom authentication\n\n" + + "For password authentication, set `auth` to `hotspot` and `password_enabled` to `true`.\n" + + "For voucher authentication, set `auth` to `hotspot` and `voucher_enabled` to `true`.\n" + + "For payment authentication, set `auth` to `hotspot` and `payment_enabled` to `true`.", + Optional: true, + Computed: true, + Default: stringdefault.StaticString("none"), + Validators: []validator.String{ + stringvalidator.OneOf("none", "hotspot", "facebook_wifi", "custom"), + }, + }, + "auth_url": schema.StringAttribute{ + MarkdownDescription: "URL for authentication. Must be a valid URL including the protocol.", + Optional: true, + Computed: true, + Validators: []validator.String{ + validators.URL(), + }, + }, + "authorize": schema.SingleNestedAttribute{ + MarkdownDescription: "Authorize.net payment settings.", + Optional: true, + Validators: []validator.Object{}, + Attributes: map[string]schema.Attribute{ + "use_sandbox": schema.BoolAttribute{ + MarkdownDescription: "Use sandbox mode for Authorize.net payments.", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + }, + "login_id": schema.StringAttribute{ + MarkdownDescription: "Authorize.net login ID for authentication.", + Required: true, + }, + "transaction_key": schema.StringAttribute{ + MarkdownDescription: "Authorize.net transaction key for authentication.", + Required: true, + }, + }, + }, + "custom_ip": schema.StringAttribute{ + MarkdownDescription: "Custom IP address. Must be a valid IPv4 address (e.g., `192.168.1.1`).", + Optional: true, + Validators: []validator.String{ + validators.IPv4(), + }, + }, + "ec_enabled": schema.BoolAttribute{ + MarkdownDescription: "Enable enterprise controller functionality.", + Optional: true, + Computed: true, + }, + "expire": schema.Int32Attribute{ + MarkdownDescription: "Expiration time for guest access.", + Optional: true, + Computed: true, + }, + "expire_number": schema.Int32Attribute{ + MarkdownDescription: "Number value for the expiration time.", + Optional: true, + Computed: true, + }, + "expire_unit": schema.Int32Attribute{ + MarkdownDescription: "Unit for the expiration time. Valid values are:\n" + + "* `1` - Minute\n" + + "* `60` - Hour\n" + + "* `1440` - Day\n" + + "* `10080` - Week", + Optional: true, + Computed: true, + Validators: []validator.Int32{ + int32validator.OneOf(1, 60, 1440, 10080), + }, + }, + "facebook_enabled": schema.BoolAttribute{ + MarkdownDescription: "Whether Facebook authentication for guest access is enabled.", + Computed: true, + }, + "facebook": schema.SingleNestedAttribute{ + MarkdownDescription: "Facebook authentication settings.", + Optional: true, + Attributes: map[string]schema.Attribute{ + "app_id": schema.StringAttribute{ + MarkdownDescription: "Facebook application ID for authentication.", + Required: true, + }, + "app_secret": schema.StringAttribute{ + MarkdownDescription: "Facebook application secret for authentication.", + Required: true, + Sensitive: true, + }, + "scope_email": schema.BoolAttribute{ + MarkdownDescription: "Request email scope for Facebook authentication.", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(true), + }, + }, + }, + "facebook_wifi": schema.SingleNestedAttribute{ + MarkdownDescription: "Facebook WiFi authentication settings.", + Optional: true, + Attributes: map[string]schema.Attribute{ + "block_https": schema.BoolAttribute{ + MarkdownDescription: "Mode HTTPS for Facebook WiFi.", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + }, + "gateway_id": schema.StringAttribute{ + MarkdownDescription: "Facebook WiFi gateway ID.", + Required: true, + }, + "gateway_name": schema.StringAttribute{ + MarkdownDescription: "Facebook WiFi gateway name.", + Required: true, + }, + "gateway_secret": schema.StringAttribute{ + MarkdownDescription: "Facebook WiFi gateway secret.", + Required: true, + Sensitive: true, + }, + }, + }, + "google_enabled": schema.BoolAttribute{ + MarkdownDescription: "Whether Google authentication for guest access is enabled.", + Computed: true, + }, + "google": schema.SingleNestedAttribute{ + MarkdownDescription: "Google authentication settings.", + Optional: true, + Attributes: map[string]schema.Attribute{ + "client_id": schema.StringAttribute{ + MarkdownDescription: "Google client ID for authentication.", + Required: true, + //Sensitive: true, + }, + "client_secret": schema.StringAttribute{ + MarkdownDescription: "Google client secret for authentication.", + Required: true, + //Sensitive: true, + }, + "domain": schema.StringAttribute{ + MarkdownDescription: "Restrict Google authentication to specific domain.", + Optional: true, + Computed: true, + Validators: []validator.String{ + validators.Hostname(), + }, + }, + "scope_email": schema.BoolAttribute{ + MarkdownDescription: "Request email scope for Google authentication.", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(true), + }, + }, + }, + "ippay": schema.SingleNestedAttribute{ + MarkdownDescription: "IPpay Payments settings.", + Optional: true, + Attributes: map[string]schema.Attribute{ + "terminal_id": schema.StringAttribute{ + MarkdownDescription: "Terminal ID for IP Payments.", + Required: true, + Sensitive: true, + }, + "use_sandbox": schema.BoolAttribute{ + MarkdownDescription: "Whether to use sandbox mode for IPPay payments.", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + }, + }, + }, + "merchant_warrior": schema.SingleNestedAttribute{ + MarkdownDescription: "MerchantWarrior payment settings.", + Optional: true, + Attributes: map[string]schema.Attribute{ + "api_key": schema.StringAttribute{ + MarkdownDescription: "MerchantWarrior API key.", + Required: true, + Sensitive: true, + }, + "api_passphrase": schema.StringAttribute{ + MarkdownDescription: "MerchantWarrior API passphrase.", + Required: true, + Sensitive: true, + }, + "merchant_uuid": schema.StringAttribute{ + MarkdownDescription: "MerchantWarrior merchant UUID.", + Required: true, + Sensitive: true, + }, + "use_sandbox": schema.BoolAttribute{ + MarkdownDescription: "Whether to use sandbox mode for MerchantWarrior payments.", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + }, + }, + }, + "paypal": schema.SingleNestedAttribute{ + MarkdownDescription: "PayPal payment settings.", + Optional: true, + Attributes: map[string]schema.Attribute{ + "password": schema.StringAttribute{ + MarkdownDescription: "PayPal password.", + Required: true, + Sensitive: true, + }, + "signature": schema.StringAttribute{ + MarkdownDescription: "PayPal signature.", + Required: true, + Sensitive: true, + }, + "use_sandbox": schema.BoolAttribute{ + MarkdownDescription: "Whether to use sandbox mode for PayPal payments.", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + }, + "username": schema.StringAttribute{ + MarkdownDescription: "PayPal username. Must be a valid email address.", + Required: true, + Sensitive: true, + Validators: []validator.String{ + validators.Email(), + }, + }, + }, + }, + "password_enabled": schema.BoolAttribute{ + MarkdownDescription: "Enable password authentication for guest access.", + Computed: true, + }, + "password": schema.StringAttribute{ + MarkdownDescription: "Password for guest access.", + Optional: true, + Sensitive: true, + }, + "payment_enabled": schema.BoolAttribute{ + MarkdownDescription: "Enable payment for guest access.", + Computed: true, + }, + "payment_gateway": schema.StringAttribute{ + MarkdownDescription: "Payment gateway. Valid values are:\n" + + "* `paypal` - PayPal\n" + + "* `stripe` - Stripe\n" + + "* `authorize` - Authorize.net\n" + + "* `quickpay` - QuickPay\n" + + "* `merchantwarrior` - Merchant Warrior\n" + + "* `ippay` - IP Payments", + Optional: true, + Validators: []validator.String{ + stringvalidator.OneOf("paypal", "stripe", "authorize", "quickpay", "merchantwarrior", "ippay"), + }, + }, + "portal_customization": schema.SingleNestedAttribute{ + MarkdownDescription: "Portal customization settings.", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Object{ + objectplanmodifier.UseStateForUnknown(), + }, + Attributes: map[string]schema.Attribute{ + "customized": schema.BoolAttribute{ + MarkdownDescription: "Whether the portal is customized.", + Optional: true, + Computed: true, + }, + "authentication_text": schema.StringAttribute{ + MarkdownDescription: "Custom authentication text for the portal.", + Optional: true, + Computed: true, + }, + "bg_color": schema.StringAttribute{ + MarkdownDescription: "Background color for the custom portal. Must be a valid hex color code (e.g., #FFF or #FFFFFF).", + Optional: true, + Computed: true, + Validators: []validator.String{ + validators.HexColor(), + }, + }, + "bg_image_tile": schema.BoolAttribute{ + MarkdownDescription: "Tile the background image.", + Optional: true, + Computed: true, + }, + "bg_type": schema.StringAttribute{ + MarkdownDescription: "Type of portal background. Valid values are:\n" + + "* `color` - Solid color background\n" + + "* `image` - (not yet supported!) Custom image background\n" + + "* `gallery` - Image from Unsplash gallery", + Optional: true, + Computed: true, + Validators: []validator.String{ + stringvalidator.OneOf("color", "image", "gallery"), + }, + }, + "box_color": schema.StringAttribute{ + MarkdownDescription: "Color of the login box in the portal. Must be a valid hex color code (e.g., #FFF or #FFFFFF).", + Optional: true, + Computed: true, + Validators: []validator.String{ + validators.HexColor(), + }, + }, + "box_link_color": schema.StringAttribute{ + MarkdownDescription: "Color of links in the login box. Must be a valid hex color code (e.g., #FFF or #FFFFFF).", + Optional: true, + Computed: true, + Validators: []validator.String{ + validators.HexColor(), + }, + }, + "box_opacity": schema.Int32Attribute{ + MarkdownDescription: "Opacity of the login box (0-100).", + Optional: true, + Computed: true, + Validators: []validator.Int32{ + int32validator.Between(0, 100), + }, + }, + "box_radius": schema.Int32Attribute{ + MarkdownDescription: "Border radius of the login box in pixels.", + Optional: true, + Computed: true, + Validators: []validator.Int32{ + int32validator.AtLeast(0), + }, + }, + "box_text_color": schema.StringAttribute{ + MarkdownDescription: "Text color in the login box. Must be a valid hex color code (e.g., #FFF or #FFFFFF).", + Optional: true, + Computed: true, + Validators: []validator.String{ + validators.HexColor(), + }, + }, + "button_color": schema.StringAttribute{ + MarkdownDescription: "Button color in the portal. Must be a valid hex color code (e.g., #FFF or #FFFFFF).", + Optional: true, + Computed: true, + Validators: []validator.String{ + validators.HexColor(), + }, + }, + "button_text": schema.StringAttribute{ + MarkdownDescription: "Custom text for the login button.", + Optional: true, + Computed: true, + }, + "button_text_color": schema.StringAttribute{ + MarkdownDescription: "Button text color. Must be a valid hex color code (e.g., #FFF or #FFFFFF).", + Optional: true, + Computed: true, + Validators: []validator.String{ + validators.HexColor(), + }, + }, + "languages": schema.ListAttribute{ + MarkdownDescription: "List of enabled languages for the portal.", + Optional: true, + Computed: true, + ElementType: types.StringType, + }, + "link_color": schema.StringAttribute{ + MarkdownDescription: "Color for links in the portal. Must be a valid hex color code (e.g., #FFF or #FFFFFF).", + Optional: true, + Computed: true, + Validators: []validator.String{ + validators.HexColor(), + }, + }, + "logo_position": schema.StringAttribute{ + MarkdownDescription: "Position of the logo in the portal. Valid values are: left, center, right.", + Optional: true, + Computed: true, + Validators: []validator.String{ + stringvalidator.OneOf("left", "center", "right"), + }, + }, + "logo_size": schema.Int32Attribute{ + MarkdownDescription: "Size of the logo in pixels.", + Optional: true, + Computed: true, + Validators: []validator.Int32{ + int32validator.AtLeast(0), + }, + }, + "success_text": schema.StringAttribute{ + MarkdownDescription: "Text displayed after successful authentication.", + Optional: true, + Computed: true, + }, + "text_color": schema.StringAttribute{ + MarkdownDescription: "Main text color for the portal. Must be a valid hex color code (e.g., #FFF or #FFFFFF).", + Optional: true, + Computed: true, + Validators: []validator.String{ + validators.HexColor(), + }, + }, + "title": schema.StringAttribute{ + MarkdownDescription: "Title of the portal page.", + Optional: true, + Computed: true, + }, + "tos": schema.StringAttribute{ + MarkdownDescription: "Terms of service text.", + Optional: true, + Computed: true, + }, + "tos_enabled": schema.BoolAttribute{ + MarkdownDescription: "Enable terms of service acceptance requirement.", + Optional: true, + Computed: true, + }, + "unsplash_author_name": schema.StringAttribute{ + MarkdownDescription: "Name of the Unsplash author for gallery background.", + Optional: true, + Computed: true, + }, + "unsplash_author_username": schema.StringAttribute{ + MarkdownDescription: "Username of the Unsplash author for gallery background.", + Optional: true, + Computed: true, + }, + "welcome_text": schema.StringAttribute{ + MarkdownDescription: "Welcome text displayed on the portal.", + Optional: true, + Computed: true, + }, + "welcome_text_enabled": schema.BoolAttribute{ + MarkdownDescription: "Enable welcome text display.", + Optional: true, + Computed: true, + }, + "welcome_text_position": schema.StringAttribute{ + MarkdownDescription: "Position of the welcome text. Valid values are: `under_logo`, `above_boxes`.", + Optional: true, + Computed: true, + Validators: []validator.String{ + stringvalidator.OneOf("under_logo", "above_boxes"), + }, + }, + }, + }, + "portal_enabled": schema.BoolAttribute{ + MarkdownDescription: "Enable the guest portal.", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(true), + }, + "portal_hostname": schema.StringAttribute{ + MarkdownDescription: "Hostname to use for the captive portal.", + Optional: true, + Computed: true, + Validators: []validator.String{ + validators.Hostname(), + }, + }, + "portal_use_hostname": schema.BoolAttribute{ + MarkdownDescription: "Use a custom hostname for the portal.", + Optional: true, + Computed: true, + }, + "quickpay": schema.SingleNestedAttribute{ + MarkdownDescription: "QuickPay payment settings.", + Optional: true, + Attributes: map[string]schema.Attribute{ + "agreement_id": schema.StringAttribute{ + MarkdownDescription: "QuickPay agreement ID.", + Required: true, + Sensitive: true, + }, + "api_key": schema.StringAttribute{ + MarkdownDescription: "QuickPay API key.", + Required: true, + Sensitive: true, + }, + "merchant_id": schema.StringAttribute{ + MarkdownDescription: "QuickPay merchant ID.", + Required: true, + Sensitive: true, + }, + "use_sandbox": schema.BoolAttribute{ + MarkdownDescription: "Enable sandbox mode for QuickPay payments.", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + }, + }, + }, + "radius_enabled": schema.BoolAttribute{ + MarkdownDescription: "Whether RADIUS authentication for guest access is enabled.", + Computed: true, + }, + "radius": schema.SingleNestedAttribute{ + MarkdownDescription: "RADIUS authentication settings.", + Optional: true, + Attributes: map[string]schema.Attribute{ + "auth_type": schema.StringAttribute{ + MarkdownDescription: "RADIUS authentication type. Valid values are: `chap`, `mschapv2`.", + Required: true, + Validators: []validator.String{ + stringvalidator.OneOf("chap", "mschapv2"), + }, + }, + "disconnect_enabled": schema.BoolAttribute{ + MarkdownDescription: "Enable RADIUS disconnect messages.", + Optional: true, + Computed: true, + }, + "disconnect_port": schema.Int32Attribute{ + MarkdownDescription: "Port for RADIUS disconnect messages.", + Optional: true, + Computed: true, + Validators: []validator.Int32{ + int32validator.Between(1, 65535), + }, + }, + "profile_id": schema.StringAttribute{ + MarkdownDescription: "ID of the RADIUS profile to use.", + Required: true, + }, + }, + }, + "redirect_enabled": schema.BoolAttribute{ + MarkdownDescription: "Whether redirect after authentication is enabled.", + Computed: true, + }, + "redirect": schema.SingleNestedAttribute{ + MarkdownDescription: "Redirect after authentication settings.", + Optional: true, + Attributes: map[string]schema.Attribute{ + "use_https": schema.BoolAttribute{ + MarkdownDescription: "Use HTTPS for the redirect URL.", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(true), + }, + "to_https": schema.BoolAttribute{ + MarkdownDescription: "Redirect HTTP requests to HTTPS.", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(true), + }, + "url": schema.StringAttribute{ + MarkdownDescription: "URL to redirect to after authentication. Must be a valid URL.", + Required: true, + Validators: []validator.String{ + validators.URL(), + }, + }, + }, + }, + "restricted_dns_enabled": schema.BoolAttribute{ + MarkdownDescription: "Whether restricted DNS servers for guest networks are enabled.", + Computed: true, + }, + "restricted_dns_servers": schema.ListAttribute{ + MarkdownDescription: "List of restricted DNS servers for guest networks. Each value must be a valid IPv4 address.", + Optional: true, + Computed: true, + ElementType: types.StringType, + Default: listdefault.StaticValue(utils.EmptyList(types.StringType)), + Validators: []validator.List{ + listvalidator.ValueStringsAre(validators.IPv4()), + }, + }, + "stripe": schema.SingleNestedAttribute{ + MarkdownDescription: "Stripe payment settings.", + Optional: true, + Attributes: map[string]schema.Attribute{ + "api_key": schema.StringAttribute{ + MarkdownDescription: "Stripe API key.", + Required: true, + Sensitive: true, + }, + }, + }, + "template_engine": schema.StringAttribute{ + MarkdownDescription: "Template engine for the portal. Valid values are: `jsp`, `angular`.", + Optional: true, + Computed: true, + Validators: []validator.String{ + stringvalidator.OneOf("jsp", "angular"), + }, + }, + "voucher_customized": schema.BoolAttribute{ + MarkdownDescription: "Whether vouchers are customized.", + Optional: true, + Computed: true, + }, + "voucher_enabled": schema.BoolAttribute{ + MarkdownDescription: "Enable voucher-based authentication for guest access.", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + }, + "wechat_enabled": schema.BoolAttribute{ + MarkdownDescription: "Whether WeChat authentication for guest access is enabled.", + Computed: true, + }, + "wechat": schema.SingleNestedAttribute{ + MarkdownDescription: "WeChat authentication settings.", + Optional: true, + Attributes: map[string]schema.Attribute{ + "app_id": schema.StringAttribute{ + MarkdownDescription: "WeChat App ID for social authentication.", + Required: true, + }, + "app_secret": schema.StringAttribute{ + MarkdownDescription: "WeChat App secret.", + Required: true, + Sensitive: true, + }, + "secret_key": schema.StringAttribute{ + MarkdownDescription: "WeChat secret key.", + Required: true, + Sensitive: true, + }, + "shop_id": schema.StringAttribute{ + MarkdownDescription: "WeChat Shop ID for payments.", + Optional: true, + Computed: true, + }, + }, + }, + }, + } +} + +func NewGuestAccessResource() resource.Resource { + r := &guestAccessResource{} + r.GenericResource = NewSettingResource( + "unifi_setting_guest_access", + func() *guestAccessModel { return &guestAccessModel{} }, + func(ctx context.Context, client *base.Client, site string) (interface{}, error) { + return client.GetSettingGuestAccess(ctx, site) + }, + func(ctx context.Context, client *base.Client, site string, body interface{}) (interface{}, error) { + return client.UpdateSettingGuestAccess(ctx, site, body.(*unifi.SettingGuestAccess)) + }, + ) + return r +} diff --git a/internal/provider/testing/test_environment.go b/internal/provider/testing/test_environment.go index 4d34cf5..d33dbd6 100644 --- a/internal/provider/testing/test_environment.go +++ b/internal/provider/testing/test_environment.go @@ -6,6 +6,7 @@ import ( "crypto/tls" "encoding/json" "fmt" + "github.com/filipowm/terraform-provider-unifi/internal/provider/base" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "net/http" @@ -266,12 +267,14 @@ func (te *TestEnvironment) newTestClient() (unifi.Client, error) { return nil, err } - return unifi.NewClient(&unifi.ClientConfig{ + client, err := unifi.NewClient(&unifi.ClientConfig{ URL: te.Endpoint, User: user, Password: password, VerifySSL: false, + RememberMe: true, ValidationMode: unifi.DisableValidation, Logger: unifi.NewDefaultLogger(unifi.WarnLevel), }) + return base.NewRetryableUnifiClient(client), err } diff --git a/internal/provider/validators/email.go b/internal/provider/validators/email.go new file mode 100644 index 0000000..6a8122d --- /dev/null +++ b/internal/provider/validators/email.go @@ -0,0 +1,48 @@ +package validators + +import ( + "context" + "fmt" + "regexp" + + "github.com/filipowm/terraform-provider-unifi/internal/provider/base" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +// A common regex pattern for validating email addresses +// This is a simplified version and may not catch all edge cases +var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`) + +// Email returns a validator which ensures that the string value is a valid email address. +func Email() validator.String { + return emailValidator{} +} + +type emailValidator struct{} + +func (v emailValidator) Description(_ context.Context) string { + return "must be a valid email address" +} + +func (v emailValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +func (v emailValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + value := req.ConfigValue + if !base.IsDefined(value) { + return + } + + val := value.ValueString() + if !emailRegex.MatchString(val) { + resp.Diagnostics.Append( + validatordiag.InvalidAttributeValueDiagnostic( + req.Path, + v.Description(ctx), + fmt.Sprintf("%q is not a valid email address", val), + ), + ) + } +} diff --git a/internal/provider/validators/email_test.go b/internal/provider/validators/email_test.go new file mode 100644 index 0000000..12546dc --- /dev/null +++ b/internal/provider/validators/email_test.go @@ -0,0 +1,82 @@ +package validators_test + +import ( + "context" + "github.com/filipowm/terraform-provider-unifi/internal/provider/validators" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestEmailValidator(t *testing.T) { + t.Parallel() + + type testCase struct { + val types.String + expectError bool + } + tests := map[string]testCase{ + "unknown": { + val: types.StringUnknown(), + }, + "null": { + val: types.StringNull(), + }, + "valid-simple": { + val: types.StringValue("test@example.com"), + }, + "valid-with-dots": { + val: types.StringValue("john.doe@example.com"), + }, + "valid-with-plus": { + val: types.StringValue("john+test@example.com"), + }, + "valid-with-subdomain": { + val: types.StringValue("john@sub.example.com"), + }, + "valid-with-numbers": { + val: types.StringValue("user123@example.com"), + }, + "invalid-no-at": { + val: types.StringValue("testexample.com"), + expectError: true, + }, + "invalid-no-domain": { + val: types.StringValue("test@"), + expectError: true, + }, + "invalid-no-tld": { + val: types.StringValue("test@example"), + expectError: true, + }, + "invalid-space": { + val: types.StringValue("test user@example.com"), + expectError: true, + }, + "invalid-special-chars": { + val: types.StringValue("test*user@example.com"), + expectError: true, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + request := validator.StringRequest{ + ConfigValue: test.val, + } + response := validator.StringResponse{} + validators.Email().ValidateString(context.Background(), request, &response) + + if !response.Diagnostics.HasError() && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Diagnostics.HasError() && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Diagnostics) + } + }) + } +} diff --git a/internal/provider/validators/hex_color.go b/internal/provider/validators/hex_color.go new file mode 100644 index 0000000..0f81224 --- /dev/null +++ b/internal/provider/validators/hex_color.go @@ -0,0 +1,47 @@ +package validators + +import ( + "context" + "fmt" + "regexp" + + "github.com/filipowm/terraform-provider-unifi/internal/provider/base" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +var hexColorRegex = regexp.MustCompile("^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$") + +// HexColor returns a validator which ensures that the string value is a valid hex color code. +// Valid formats are: #RGB or #RRGGBB +func HexColor() validator.String { + return hexColorValidator{} +} + +type hexColorValidator struct{} + +func (v hexColorValidator) Description(_ context.Context) string { + return "must be a valid hex color code (e.g., #FFF or #FFFFFF)" +} + +func (v hexColorValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +func (v hexColorValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + value := req.ConfigValue + if !base.IsDefined(value) { + return + } + + val := value.ValueString() + if !hexColorRegex.MatchString(val) { + resp.Diagnostics.Append( + validatordiag.InvalidAttributeValueDiagnostic( + req.Path, + v.Description(ctx), + fmt.Sprintf("%q is not a valid hex color code", val), + ), + ) + } +} diff --git a/internal/provider/validators/hex_color_test.go b/internal/provider/validators/hex_color_test.go new file mode 100644 index 0000000..53385ca --- /dev/null +++ b/internal/provider/validators/hex_color_test.go @@ -0,0 +1,75 @@ +package validators_test + +import ( + "context" + "github.com/filipowm/terraform-provider-unifi/internal/provider/validators" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestHexColorValidator(t *testing.T) { + t.Parallel() + + type testCase struct { + val types.String + expectError bool + } + tests := map[string]testCase{ + "unknown": { + val: types.StringUnknown(), + }, + "null": { + val: types.StringNull(), + }, + "valid-6-digits": { + val: types.StringValue("#123456"), + }, + "valid-3-digits": { + val: types.StringValue("#123"), + }, + "valid-uppercase": { + val: types.StringValue("#ABCDEF"), + }, + "valid-mixed-case": { + val: types.StringValue("#aBcDeF"), + }, + "invalid-missing-hash": { + val: types.StringValue("123456"), + expectError: true, + }, + "invalid-too-short": { + val: types.StringValue("#12"), + expectError: true, + }, + "invalid-too-long": { + val: types.StringValue("#1234567"), + expectError: true, + }, + "invalid-wrong-chars": { + val: types.StringValue("#12345G"), + expectError: true, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + request := validator.StringRequest{ + ConfigValue: test.val, + } + response := validator.StringResponse{} + validators.HexColor().ValidateString(context.Background(), request, &response) + + if !response.Diagnostics.HasError() && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Diagnostics.HasError() && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Diagnostics) + } + }) + } +} diff --git a/templates/index.md.tmpl b/templates/index.md.tmpl index d95fdf4..2f9793d 100644 --- a/templates/index.md.tmpl +++ b/templates/index.md.tmpl @@ -11,16 +11,17 @@ The UniFi provider enables infrastructure-as-code management of [Ubiquiti's UniF ## Supported Features -The provider supports management of: - -* Networks and VLANs -* Wireless Networks (WLANs) -* Firewall Rules and Groups -* Port Forwarding -* DNS Records -* User Management -* Device Configuration -* And more... +- Manage UniFi network resources using Infrastructure as Code +- Support for UniFi Controller version 6.x and later +- Compatible with UDM, UDM-Pro, UCG, and standard controller deployments +- Comprehensive resource management including: + - Network/WLAN configuration + - Firewall rules and groups + - Port forwarding + - DNS records + - User management + - Device management + - And more... ## Supported Platforms