feat: add locale setting resource support with unifi_setting_locale resource (#34)

* feat: add locale setting resource support with `unifi_setting_locale` resource

* lint
This commit is contained in:
Mateusz Filipowicz
2025-03-01 18:03:58 +01:00
committed by GitHub
parent 273d0daddd
commit f815ffef79
7 changed files with 563 additions and 0 deletions

View File

@@ -0,0 +1,93 @@
package validators
import (
"context"
"fmt"
"strings"
"time"
"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"
)
// Timezone returns a validator which ensures that the string value is a valid IANA timezone identifier
// according to the time.LoadLocation function.
func Timezone() validator.String {
return timezoneValidator{}
}
type timezoneValidator struct{}
func (v timezoneValidator) Description(_ context.Context) string {
return "must be a valid IANA timezone identifier (e.g., 'America/New_York')"
}
func (v timezoneValidator) MarkdownDescription(ctx context.Context) string {
return v.Description(ctx)
}
func (v timezoneValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) {
value := req.ConfigValue
if !base.IsDefined(value) {
return
}
val := value.ValueString()
// Check for empty string
if val == "" {
resp.Diagnostics.Append(
validatordiag.InvalidAttributeValueDiagnostic(
req.Path,
v.Description(ctx),
"Timezone cannot be empty. Use a valid IANA timezone identifier like 'America/New_York'",
),
)
return
}
// Check for proper case (IANA timezone identifiers are case-sensitive)
// Regions should start with uppercase
if val[0] >= 'a' && val[0] <= 'z' {
resp.Diagnostics.Append(
validatordiag.InvalidAttributeValueDiagnostic(
req.Path,
v.Description(ctx),
fmt.Sprintf("%q has incorrect case. IANA timezone regions should start with uppercase (e.g., 'America/New_York')", val),
),
)
return
}
// Try to load the timezone location
_, err := time.LoadLocation(val)
if err != nil {
// For better error messages, check common mistakes
if strings.Contains(val, "UTC") && val != "UTC" {
resp.Diagnostics.Append(
validatordiag.InvalidAttributeValueDiagnostic(
req.Path,
v.Description(ctx),
fmt.Sprintf("%q is not a valid timezone. For UTC offset use the standard 'UTC' timezone instead.", val),
),
)
} else if strings.Contains(val, " ") {
resp.Diagnostics.Append(
validatordiag.InvalidAttributeValueDiagnostic(
req.Path,
v.Description(ctx),
fmt.Sprintf("%q is not a valid timezone. Timezones should not contain spaces.", val),
),
)
} else {
resp.Diagnostics.Append(
validatordiag.InvalidAttributeValueDiagnostic(
req.Path,
v.Description(ctx),
fmt.Sprintf("%q is not a valid IANA timezone identifier. Use a value like 'America/New_York'", val),
),
)
}
}
}

View File

@@ -0,0 +1,85 @@
package validators
import (
"context"
"testing"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
)
func TestTimezoneValidator(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-america": {
val: types.StringValue("America/Los_Angeles"),
},
"valid-europe": {
val: types.StringValue("Europe/London"),
},
"valid-asia": {
val: types.StringValue("Asia/Tokyo"),
},
"valid-australia": {
val: types.StringValue("Australia/Sydney"),
},
"valid-utc": {
val: types.StringValue("UTC"),
},
"invalid-with-space": {
val: types.StringValue("America/New York"),
expectError: true,
},
"invalid-nonexistent": {
val: types.StringValue("NonExistent/Timezone"),
expectError: true,
},
"invalid-empty-string": {
val: types.StringValue(""),
expectError: true,
},
"invalid-just-region": {
val: types.StringValue("America"),
expectError: true,
},
"invalid-lowercase": {
val: types.StringValue("america/los_angeles"),
expectError: true,
},
"invalid-utc-offset": {
val: types.StringValue("UTC+01:00"),
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{}
Timezone().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)
}
})
}
}

View File

@@ -0,0 +1,92 @@
package validators
import (
"context"
"fmt"
"net/url"
"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"
)
// URL returns a validator which ensures that the string value is a valid URL.
func URL() validator.String {
return urlValidator{requireHTTPS: false}
}
// HTTPSUrl returns a validator which ensures that the string value is a valid HTTPS URL.
func HTTPSUrl() validator.String {
return urlValidator{requireHTTPS: true}
}
type urlValidator struct {
requireHTTPS bool
}
func (v urlValidator) Description(_ context.Context) string {
if v.requireHTTPS {
return "must be a valid HTTPS URL"
}
return "must be a valid URL"
}
func (v urlValidator) MarkdownDescription(ctx context.Context) string {
return v.Description(ctx)
}
func (v urlValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) {
value := req.ConfigValue
if !base.IsDefined(value) {
return
}
val := value.ValueString()
parsedURL, err := url.Parse(val)
if err != nil {
resp.Diagnostics.Append(
validatordiag.InvalidAttributeValueDiagnostic(
req.Path,
v.Description(ctx),
fmt.Sprintf("%q is not a valid URL: %s", val, err),
),
)
return
}
// Check if URL has a scheme
if parsedURL.Scheme == "" {
resp.Diagnostics.Append(
validatordiag.InvalidAttributeValueDiagnostic(
req.Path,
v.Description(ctx),
fmt.Sprintf("%q is missing a scheme (e.g., http:// or https://)", val),
),
)
return
}
// Check if HTTPS is required
if v.requireHTTPS && parsedURL.Scheme != "https" {
resp.Diagnostics.Append(
validatordiag.InvalidAttributeValueDiagnostic(
req.Path,
v.Description(ctx),
fmt.Sprintf("%q must use HTTPS scheme", val),
),
)
return
}
// Check if URL has a host
if parsedURL.Host == "" {
resp.Diagnostics.Append(
validatordiag.InvalidAttributeValueDiagnostic(
req.Path,
v.Description(ctx),
fmt.Sprintf("%q is missing a host", val),
),
)
}
}

View File

@@ -0,0 +1,128 @@
package validators
import (
"context"
"testing"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
)
func TestURLValidator(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-http": {
val: types.StringValue("http://example.com"),
},
"valid-https": {
val: types.StringValue("https://example.com"),
},
"valid-with-path": {
val: types.StringValue("https://example.com/path"),
},
"valid-with-query": {
val: types.StringValue("https://example.com/path?query=value"),
},
"valid-with-port": {
val: types.StringValue("https://example.com:8443"),
},
"invalid-no-scheme": {
val: types.StringValue("example.com"),
expectError: true,
},
"invalid-no-host": {
val: types.StringValue("https://"),
expectError: true,
},
"invalid-malformed": {
val: types.StringValue("htt ps://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{}
URL().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)
}
})
}
}
func TestHTTPSURLValidator(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-https": {
val: types.StringValue("https://example.com"),
},
"valid-with-path": {
val: types.StringValue("https://example.com/path"),
},
"invalid-http": {
val: types.StringValue("http://example.com"),
expectError: true,
},
"invalid-no-scheme": {
val: types.StringValue("example.com"),
expectError: true,
},
"invalid-no-host": {
val: types.StringValue("https://"),
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{}
HTTPSUrl().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)
}
})
}
}