Model apps as a set instead of an ordered list #2

Merged
benvin merged 1 commits from benvin/apps-set into main 2026-08-23 00:08:58 +10:00
2 changed files with 32 additions and 14 deletions
@@ -25,7 +25,7 @@ type secretBackendRoleResource struct {
type secretBackendRoleModel struct { type secretBackendRoleModel struct {
Backend types.String `tfsdk:"backend"` Backend types.String `tfsdk:"backend"`
Name types.String `tfsdk:"name"` Name types.String `tfsdk:"name"`
Apps types.List `tfsdk:"apps"` Apps types.Set `tfsdk:"apps"`
TTL types.Int64 `tfsdk:"ttl"` TTL types.Int64 `tfsdk:"ttl"`
MaxTTL types.Int64 `tfsdk:"max_ttl"` MaxTTL types.Int64 `tfsdk:"max_ttl"`
} }
@@ -56,7 +56,7 @@ func (r *secretBackendRoleResource) Schema(_ context.Context, _ resource.SchemaR
stringplanmodifier.RequiresReplace(), stringplanmodifier.RequiresReplace(),
}, },
}, },
"apps": schema.ListAttribute{ "apps": schema.SetAttribute{
Description: "arr apps a generated key may access (subset of sonarr, radarr, prowlarr).", Description: "arr apps a generated key may access (subset of sonarr, radarr, prowlarr).",
ElementType: types.StringType, ElementType: types.StringType,
Required: true, Required: true,
@@ -212,9 +212,9 @@ func applyRoleData(m *secretBackendRoleModel, role map[string]interface{}) diag.
var diags diag.Diagnostics var diags diag.Diagnostics
apps := toStringSlice(role["apps"]) apps := toStringSlice(role["apps"])
appList, appDiags := types.ListValueFrom(context.Background(), types.StringType, apps) appSet, appDiags := types.SetValueFrom(context.Background(), types.StringType, apps)
diags.Append(appDiags...) diags.Append(appDiags...)
m.Apps = appList m.Apps = appSet
if n, ok := toInt64(role["ttl"]); ok && n != 0 { if n, ok := toInt64(role["ttl"]); ok && n != 0 {
m.TTL = types.Int64Value(n) m.TTL = types.Int64Value(n)
@@ -8,18 +8,18 @@ import (
"github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types"
) )
func listOf(t *testing.T, vals ...string) types.List { func setOf(t *testing.T, vals ...string) types.Set {
t.Helper() t.Helper()
l, diags := types.ListValueFrom(context.Background(), types.StringType, vals) s, diags := types.SetValueFrom(context.Background(), types.StringType, vals)
if diags.HasError() { if diags.HasError() {
t.Fatalf("building list: %v", diags) t.Fatalf("building set: %v", diags)
} }
return l return s
} }
func TestRoleDataOmitsUnsetTTLs(t *testing.T) { func TestRoleDataOmitsUnsetTTLs(t *testing.T) {
m := secretBackendRoleModel{ m := secretBackendRoleModel{
Apps: listOf(t, "sonarr", "radarr", "prowlarr"), Apps: setOf(t, "sonarr", "radarr", "prowlarr"),
TTL: types.Int64Null(), TTL: types.Int64Null(),
MaxTTL: types.Int64Null(), MaxTTL: types.Int64Null(),
} }
@@ -41,7 +41,7 @@ func TestRoleDataOmitsUnsetTTLs(t *testing.T) {
func TestRoleDataIncludesTTLs(t *testing.T) { func TestRoleDataIncludesTTLs(t *testing.T) {
m := secretBackendRoleModel{ m := secretBackendRoleModel{
Apps: listOf(t, "prowlarr"), Apps: setOf(t, "prowlarr"),
TTL: types.Int64Value(60), TTL: types.Int64Value(60),
MaxTTL: types.Int64Value(86400), MaxTTL: types.Int64Value(86400),
} }
@@ -70,16 +70,34 @@ func TestApplyRoleDataMapsEngineResponse(t *testing.T) {
if diags := applyRoleData(&m, role); diags.HasError() { if diags := applyRoleData(&m, role); diags.HasError() {
t.Fatalf("applyRoleData: %v", diags) t.Fatalf("applyRoleData: %v", diags)
} }
var apps []string if !m.Apps.Equal(setOf(t, "sonarr", "radarr", "prowlarr")) {
m.Apps.ElementsAs(context.Background(), &apps, false) t.Errorf("apps = %v, want {sonarr radarr prowlarr}", m.Apps)
if len(apps) != 3 || apps[2] != "prowlarr" {
t.Errorf("apps = %v, want [sonarr radarr prowlarr]", apps)
} }
if m.TTL.ValueInt64() != 60 || m.MaxTTL.ValueInt64() != 86400 { if m.TTL.ValueInt64() != 60 || m.MaxTTL.ValueInt64() != 86400 {
t.Errorf("ttl/max_ttl = %d/%d, want 60/86400", m.TTL.ValueInt64(), m.MaxTTL.ValueInt64()) t.Errorf("ttl/max_ttl = %d/%d, want 60/86400", m.TTL.ValueInt64(), m.MaxTTL.ValueInt64())
} }
} }
func TestApplyRoleDataOrderInsensitive(t *testing.T) {
// The engine returns apps alphabetically sorted regardless of the order
// they were written in; the read-back must still equal the config value.
configApps := setOf(t, "sonarr", "prowlarr", "radarr")
role := map[string]interface{}{
"apps": []interface{}{"prowlarr", "radarr", "sonarr"},
}
m := secretBackendRoleModel{
Apps: configApps,
TTL: types.Int64Null(),
MaxTTL: types.Int64Null(),
}
if diags := applyRoleData(&m, role); diags.HasError() {
t.Fatalf("applyRoleData: %v", diags)
}
if !m.Apps.Equal(configApps) {
t.Errorf("apps after read-back = %v, not equal to config value %v", m.Apps, configApps)
}
}
func TestApplyRoleDataZeroTTLLeavesNull(t *testing.T) { func TestApplyRoleDataZeroTTLLeavesNull(t *testing.T) {
// The engine returns 0 for an unset TTL, which must not clobber the null // The engine returns 0 for an unset TTL, which must not clobber the null
// model value into a spurious 0. // model value into a spurious 0.