feat: support Guest Access settings with resource_setting_guest_access (#61)
* feat: support Guest Access settings with `resource_setting_guest_access` * feat: add support for redirect after authentication in guest access settings * feat: add support for Facebook authentication in guest access settings * feat: add support for Google authentication in guest access settings * feat: add support for RADIUS authentication in guest access settings * feat: add support for Wechat authentication in guest access settings * feat: add support for Facebook Wifi authentication in guest access settings * feat: add support for restricted DNS servers * feat: add support for guest portal UI customization * feat: add support for restricted subnet in guest portal * feat: retry client action on HTTP 401, but first attempt relogging in * require controllr version 7.4 for several portal customization attributes * enable acceptance tests workflow concurrency
This commit is contained in:
committed by
GitHub
parent
28d28f17f6
commit
ca21f79083
5
.github/workflows/acctest.yml
vendored
5
.github/workflows/acctest.yml
vendored
@@ -24,6 +24,11 @@ on:
|
|||||||
- "Makefile"
|
- "Makefile"
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "0 13 * * *"
|
- cron: "0 13 * * *"
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
name: Matrix Test
|
name: Matrix Test
|
||||||
|
|||||||
4
go.mod
4
go.mod
@@ -2,7 +2,7 @@ module github.com/filipowm/terraform-provider-unifi
|
|||||||
|
|
||||||
go 1.23.5
|
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-docs => ../../hashicorp/terraform-plugin-docs
|
||||||
// replace github.com/hashicorp/terraform-plugin-sdk/v2 => ../../hashicorp/terraform-plugin-sdk
|
// 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/apparentlymart/go-cidr v1.1.0
|
||||||
github.com/biter777/countries v1.7.5
|
github.com/biter777/countries v1.7.5
|
||||||
github.com/deckarep/golang-set/v2 v2.7.0
|
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/golangci/golangci-lint v1.64.7
|
||||||
github.com/hashicorp/go-version v1.7.0
|
github.com/hashicorp/go-version v1.7.0
|
||||||
github.com/hashicorp/terraform-plugin-docs v0.21.0
|
github.com/hashicorp/terraform-plugin-docs v0.21.0
|
||||||
|
|||||||
2
go.sum
2
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.5.4/go.mod h1:NQgqx3ylLVDqxVgY3uJ3ZMIecSl46fvqCCXzwHRuVDc=
|
||||||
github.com/filipowm/go-unifi v1.6.0 h1:0oLOrsLWcaU8sUsyMyjyGwaAWNC9Ee4YZ1ehtijXahg=
|
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.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 h1:tM+Me2ZaXs8tfdDw3X6DOX++wMCOqzYUho6tUTYIdRA=
|
||||||
github.com/firefart/nonamedreturns v1.0.5/go.mod h1:gHJjDqhGM4WyPt639SOZs+G89Ko7QKH5R5BhnO6xJhw=
|
github.com/firefart/nonamedreturns v1.0.5/go.mod h1:gHJjDqhGM4WyPt639SOZs+G89Ko7QKH5R5BhnO6xJhw=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
|
|||||||
1129
internal/provider/acctest/resource_setting_guest_access_test.go
Normal file
1129
internal/provider/acctest/resource_setting_guest_access_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,9 @@ package base
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/attr"
|
||||||
"github.com/hashicorp/terraform-plugin-framework/diag"
|
"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/datasource"
|
||||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||||
@@ -42,6 +44,20 @@ type DatasourceModel interface {
|
|||||||
Merge(context.Context, interface{}) diag.Diagnostics
|
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 {
|
type Model struct {
|
||||||
ID types.String `tfsdk:"id"`
|
ID types.String `tfsdk:"id"`
|
||||||
Site types.String `tfsdk:"site"`
|
Site types.String `tfsdk:"site"`
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -54,7 +55,7 @@ func NewClient(cfg *ClientConfig) (*Client, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
c := &Client{
|
c := &Client{
|
||||||
Client: unifiClient,
|
Client: NewRetryableUnifiClient(unifiClient),
|
||||||
Site: cfg.Site,
|
Site: cfg.Site,
|
||||||
Version: version.Must(version.NewVersion(unifiClient.Version())),
|
Version: version.Must(version.NewVersion(unifiClient.Version())),
|
||||||
}
|
}
|
||||||
@@ -64,6 +65,41 @@ func NewClient(cfg *ClientConfig) (*Client, error) {
|
|||||||
return c, nil
|
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 {
|
type Client struct {
|
||||||
unifi.Client
|
unifi.Client
|
||||||
Site string
|
Site string
|
||||||
|
|||||||
@@ -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)
|
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 {
|
func IsServerErrorContains(err error, messageContains string) bool {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -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
|
// TestNewFeatureValidator tests the NewFeatureValidator function
|
||||||
func TestNewFeatureValidator(t *testing.T) {
|
func TestNewFeatureValidator(t *testing.T) {
|
||||||
mockUnifiClient := &MockUnifiClient{
|
mockUnifiClient := &MockUnifiClient{
|
||||||
@@ -156,10 +163,7 @@ func TestNewFeatureValidator(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
client := &Client{
|
client := newTestClient(mockUnifiClient)
|
||||||
Client: mockUnifiClient,
|
|
||||||
Site: "default",
|
|
||||||
}
|
|
||||||
|
|
||||||
validator := NewFeatureValidator(client)
|
validator := NewFeatureValidator(client)
|
||||||
|
|
||||||
@@ -225,10 +229,7 @@ func TestGetFeatures(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
mockUnifiClient := tt.setup()
|
mockUnifiClient := tt.setup()
|
||||||
client := &Client{
|
client := newTestClient(mockUnifiClient)
|
||||||
Client: mockUnifiClient,
|
|
||||||
Site: "default",
|
|
||||||
}
|
|
||||||
|
|
||||||
validator := &featureEnabledValidator{
|
validator := &featureEnabledValidator{
|
||||||
client: client,
|
client: client,
|
||||||
@@ -267,10 +268,7 @@ func TestGetFeaturesConcurrent(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
client := &Client{
|
client := newTestClient(mockUnifiClient)
|
||||||
Client: mockUnifiClient,
|
|
||||||
Site: "default",
|
|
||||||
}
|
|
||||||
|
|
||||||
validator := &featureEnabledValidator{
|
validator := &featureEnabledValidator{
|
||||||
client: client,
|
client: client,
|
||||||
@@ -367,10 +365,7 @@ func TestRequireFeatures(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
mockUnifiClient := &MockUnifiClient{}
|
mockUnifiClient := &MockUnifiClient{}
|
||||||
client := &Client{
|
client := newTestClient(mockUnifiClient)
|
||||||
Client: mockUnifiClient,
|
|
||||||
Site: "default",
|
|
||||||
}
|
|
||||||
|
|
||||||
validator := &featureEnabledValidator{
|
validator := &featureEnabledValidator{
|
||||||
client: client,
|
client: client,
|
||||||
@@ -465,10 +460,7 @@ func TestRequireFeaturesEnabledForPath(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
mockUnifiClient := tt.setupClient()
|
mockUnifiClient := tt.setupClient()
|
||||||
client := &Client{
|
client := newTestClient(mockUnifiClient)
|
||||||
Client: mockUnifiClient,
|
|
||||||
Site: "default",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a wrapper FeatureValidator that provides a minimal implementation
|
// Create a wrapper FeatureValidator that provides a minimal implementation
|
||||||
// of RequireFeaturesEnabledForPath without needing a real tfsdk.Config
|
// of RequireFeaturesEnabledForPath without needing a real tfsdk.Config
|
||||||
@@ -538,10 +530,7 @@ func TestRequireFeaturesEnabled(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
mockUnifiClient := tt.setupClient()
|
mockUnifiClient := tt.setupClient()
|
||||||
client := &Client{
|
client := newTestClient(mockUnifiClient)
|
||||||
Client: mockUnifiClient,
|
|
||||||
Site: "default",
|
|
||||||
}
|
|
||||||
validator := NewFeatureValidator(client)
|
validator := NewFeatureValidator(client)
|
||||||
diags := validator.RequireFeaturesEnabled(context.Background(), "site1", tt.requiredFeatures...)
|
diags := validator.RequireFeaturesEnabled(context.Background(), "site1", tt.requiredFeatures...)
|
||||||
assert.Equal(t, tt.expectedHasErrors, diags.HasError())
|
assert.Equal(t, tt.expectedHasErrors, diags.HasError())
|
||||||
@@ -605,10 +594,7 @@ func TestFeatureValidatorCache(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
client := &Client{
|
client := newTestClient(mockUnifiClient)
|
||||||
Client: mockUnifiClient,
|
|
||||||
Site: "default",
|
|
||||||
}
|
|
||||||
|
|
||||||
validator := NewFeatureValidator(client)
|
validator := NewFeatureValidator(client)
|
||||||
|
|
||||||
|
|||||||
@@ -179,6 +179,7 @@ func (p *unifiProvider) Resources(_ context.Context) []func() resource.Resource
|
|||||||
settings.NewAutoSpeedtestResource,
|
settings.NewAutoSpeedtestResource,
|
||||||
settings.NewCountryResource,
|
settings.NewCountryResource,
|
||||||
settings.NewDpiResource,
|
settings.NewDpiResource,
|
||||||
|
settings.NewGuestAccessResource,
|
||||||
settings.NewIpsResource,
|
settings.NewIpsResource,
|
||||||
settings.NewLcmResource,
|
settings.NewLcmResource,
|
||||||
settings.NewLocaleResource,
|
settings.NewLocaleResource,
|
||||||
|
|||||||
1618
internal/provider/settings/resource_setting_guest_access.go
Normal file
1618
internal/provider/settings/resource_setting_guest_access.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/filipowm/terraform-provider-unifi/internal/provider/base"
|
||||||
"github.com/hashicorp/terraform-plugin-log/tflog"
|
"github.com/hashicorp/terraform-plugin-log/tflog"
|
||||||
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
|
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -266,12 +267,14 @@ func (te *TestEnvironment) newTestClient() (unifi.Client, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return unifi.NewClient(&unifi.ClientConfig{
|
client, err := unifi.NewClient(&unifi.ClientConfig{
|
||||||
URL: te.Endpoint,
|
URL: te.Endpoint,
|
||||||
User: user,
|
User: user,
|
||||||
Password: password,
|
Password: password,
|
||||||
VerifySSL: false,
|
VerifySSL: false,
|
||||||
|
RememberMe: true,
|
||||||
ValidationMode: unifi.DisableValidation,
|
ValidationMode: unifi.DisableValidation,
|
||||||
Logger: unifi.NewDefaultLogger(unifi.WarnLevel),
|
Logger: unifi.NewDefaultLogger(unifi.WarnLevel),
|
||||||
})
|
})
|
||||||
|
return base.NewRetryableUnifiClient(client), err
|
||||||
}
|
}
|
||||||
|
|||||||
48
internal/provider/validators/email.go
Normal file
48
internal/provider/validators/email.go
Normal file
@@ -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),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
82
internal/provider/validators/email_test.go
Normal file
82
internal/provider/validators/email_test.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
47
internal/provider/validators/hex_color.go
Normal file
47
internal/provider/validators/hex_color.go
Normal file
@@ -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),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
75
internal/provider/validators/hex_color_test.go
Normal file
75
internal/provider/validators/hex_color_test.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,16 +11,17 @@ The UniFi provider enables infrastructure-as-code management of [Ubiquiti's UniF
|
|||||||
|
|
||||||
## Supported Features
|
## Supported Features
|
||||||
|
|
||||||
The provider supports management of:
|
- Manage UniFi network resources using Infrastructure as Code
|
||||||
|
- Support for UniFi Controller version 6.x and later
|
||||||
* Networks and VLANs
|
- Compatible with UDM, UDM-Pro, UCG, and standard controller deployments
|
||||||
* Wireless Networks (WLANs)
|
- Comprehensive resource management including:
|
||||||
* Firewall Rules and Groups
|
- Network/WLAN configuration
|
||||||
* Port Forwarding
|
- Firewall rules and groups
|
||||||
* DNS Records
|
- Port forwarding
|
||||||
* User Management
|
- DNS records
|
||||||
* Device Configuration
|
- User management
|
||||||
* And more...
|
- Device management
|
||||||
|
- And more...
|
||||||
|
|
||||||
## Supported Platforms
|
## Supported Platforms
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user