Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions internal/datasources/helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package datasources

import (
"encoding/json"
"strings"
"time"

"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/types"
)

type IDNameSelector struct {
HasID bool
HasName bool
}

func ResolveIDNameSelector(kind, typeName string, id, name types.String) (IDNameSelector, diag.Diagnostics) {
var diags diag.Diagnostics

if id.IsUnknown() || name.IsUnknown() {
diags.AddError(
"Unknown "+kind+" Selector",
kind+" id and name must be known before reading the data source.",
)
return IDNameSelector{}, diags
}
if !id.IsNull() && id.ValueString() == "" {
diags.AddError(
"Empty "+kind+" ID",
kind+" id must be omitted or a non-empty string.",
)
}
if !name.IsNull() && name.ValueString() == "" {
diags.AddError(
"Empty "+kind+" Name",
kind+" name must be omitted or a non-empty string.",
)
}
if diags.HasError() {
return IDNameSelector{}, diags
}

selector := IDNameSelector{
HasID: !id.IsNull(),
HasName: !name.IsNull(),
}
if selector.HasID && selector.HasName {
diags.AddError(
"Conflicting "+kind+" Selectors",
"Configure only one of id or name for "+typeName+".",
)
return IDNameSelector{}, diags
}

return selector, diags
}

func FieldPresent(raw string) bool {
return raw != "" && strings.TrimSpace(raw) != "null"
}

func ValidResponseString(raw string, valid bool, value string) bool {
if !FieldPresent(raw) || !valid || value == "" {
return false
}

var decoded string
if err := json.Unmarshal([]byte(raw), &decoded); err != nil {
return false
}
return decoded == value
}

func ValidResponseTime(raw string, valid bool, value time.Time) bool {
if !FieldPresent(raw) || !valid || value.IsZero() {
return false
}

var decoded time.Time
if err := json.Unmarshal([]byte(raw), &decoded); err != nil {
return false
}
return decoded.Equal(value)
}

func AddInvalidResponseField(diags *diag.Diagnostics, kind, field string) {
diags.AddError(
"Invalid Kernel "+kind+" Response",
"Kernel returned a "+strings.ToLower(kind)+" with missing or invalid field "+field+".",
)
}
177 changes: 177 additions & 0 deletions internal/datasources/helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package datasources

import (
"testing"
"time"

"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/types"
)

func hasDiagnosticSummary(diags diag.Diagnostics, want string) bool {
for _, diagnostic := range diags {
if diagnostic.Summary() == want {
return true
}
}
return false
}

func TestResolveIDNameSelector(t *testing.T) {
t.Parallel()

tests := map[string]struct {
id types.String
name types.String
want IDNameSelector
wantErr string
}{
"neither set falls through to the provider default": {
id: types.StringNull(),
name: types.StringNull(),
want: IDNameSelector{},
},
"id only": {
id: types.StringValue("proj-1"),
name: types.StringNull(),
want: IDNameSelector{HasID: true},
},
"name only": {
id: types.StringNull(),
name: types.StringValue("Production"),
want: IDNameSelector{HasName: true},
},
"both set conflict": {
id: types.StringValue("proj-1"),
name: types.StringValue("Production"),
wantErr: "Conflicting Project Selectors",
},
"unknown id": {
id: types.StringUnknown(),
name: types.StringNull(),
wantErr: "Unknown Project Selector",
},
"unknown name": {
id: types.StringNull(),
name: types.StringUnknown(),
wantErr: "Unknown Project Selector",
},
"empty id": {
id: types.StringValue(""),
name: types.StringNull(),
wantErr: "Empty Project ID",
},
"empty name": {
id: types.StringNull(),
name: types.StringValue(""),
wantErr: "Empty Project Name",
},
}

for name, test := range tests {
t.Run(name, func(t *testing.T) {
t.Parallel()

selector, diags := ResolveIDNameSelector("Project", "kernel_project", test.id, test.name)
if test.wantErr != "" {
if !diags.HasError() {
t.Fatalf("expected diagnostics containing %q", test.wantErr)
}
if !hasDiagnosticSummary(diags.Errors(), test.wantErr) {
t.Fatalf("diagnostics = %v, want summary %q", diags, test.wantErr)
}
return
}
if diags.HasError() {
t.Fatalf("unexpected diagnostics: %v", diags)
}
if selector != test.want {
t.Fatalf("selector = %+v, want %+v", selector, test.want)
}
})
}
}

func TestFieldPresent(t *testing.T) {
t.Parallel()

tests := map[string]struct {
raw string
want bool
}{
"absent": {raw: "", want: false},
"json null": {raw: "null", want: false},
"json null with padding": {raw: " null ", want: false},
"string value": {raw: `"x"`, want: true},
"non-null non-string raw": {raw: "123", want: true},
}

for name, test := range tests {
t.Run(name, func(t *testing.T) {
t.Parallel()

if got := FieldPresent(test.raw); got != test.want {
t.Fatalf("FieldPresent(%q) = %v, want %v", test.raw, got, test.want)
}
})
}
}

func TestValidResponseString(t *testing.T) {
t.Parallel()

tests := map[string]struct {
raw string
valid bool
value string
want bool
}{
"raw matches decoded value": {raw: `"proj-1"`, valid: true, value: "proj-1", want: true},
"absent raw": {raw: "", valid: true, value: "proj-1", want: false},
"json null raw": {raw: "null", valid: true, value: "proj-1", want: false},
"invalid field flag": {raw: `"proj-1"`, valid: false, value: "proj-1", want: false},
"empty decoded value": {raw: `""`, valid: true, value: "", want: false},
"wrong json type": {raw: "123", valid: true, value: "123", want: false},
"raw disagrees with decoded": {raw: `"proj-1"`, valid: true, value: "proj-2", want: false},
}

for name, test := range tests {
t.Run(name, func(t *testing.T) {
t.Parallel()

if got := ValidResponseString(test.raw, test.valid, test.value); got != test.want {
t.Fatalf("ValidResponseString(%q, %v, %q) = %v, want %v", test.raw, test.valid, test.value, got, test.want)
}
})
}
}

func TestValidResponseTime(t *testing.T) {
t.Parallel()

stamp := time.Date(2026, time.June, 5, 12, 0, 0, 0, time.UTC)
tests := map[string]struct {
raw string
valid bool
value time.Time
want bool
}{
"raw matches decoded value": {raw: `"2026-06-05T12:00:00Z"`, valid: true, value: stamp, want: true},
"absent raw": {raw: "", valid: true, value: stamp, want: false},
"json null raw": {raw: "null", valid: true, value: stamp, want: false},
"invalid field flag": {raw: `"2026-06-05T12:00:00Z"`, valid: false, value: stamp, want: false},
"zero decoded value": {raw: `"2026-06-05T12:00:00Z"`, valid: true, value: time.Time{}, want: false},
"non-timestamp raw": {raw: "123", valid: true, value: stamp, want: false},
"raw disagrees with decoded": {raw: `"2026-06-05T12:00:00Z"`, valid: true, value: stamp.Add(time.Second), want: false},
}

for name, test := range tests {
t.Run(name, func(t *testing.T) {
t.Parallel()

if got := ValidResponseTime(test.raw, test.valid, test.value); got != test.want {
t.Fatalf("ValidResponseTime(%q, %v, %v) = %v, want %v", test.raw, test.valid, test.value, got, test.want)
}
})
}
}
Loading