package tfmap import ( "encoding/json" "testing" "github.com/hashicorp/terraform-plugin-framework/types" ) type sample struct { ID types.String `tfsdk:"id" dokploy:"thingId,id"` Name types.String `tfsdk:"name" dokploy:"name"` Desc types.String `tfsdk:"description" dokploy:"description,nullable"` Networks types.List `tfsdk:"network_ids" dokploy:"networkIds"` Detach types.Bool `tfsdk:"detach" dokploy:"detach"` Replicas types.Int64 `tfsdk:"replicas" dokploy:"replicas"` Secret types.String `tfsdk:"secret" dokploy:"secret,noread"` ServerID types.String `tfsdk:"server_id" dokploy:"serverId,create"` Status types.String `tfsdk:"status" dokploy:"status,ro"` } // A JSON null in the response must produce a null attribute, never leave the // attribute unknown: Terraform rejects unknown values after apply. func TestFromAPIConvertsJSONNullToNullAttribute(t *testing.T) { model := &sample{ Networks: types.ListUnknown(types.StringType), Detach: types.BoolUnknown(), Replicas: types.Int64Unknown(), } raw := json.RawMessage(`{"networkIds":null,"detach":null,"replicas":null}`) if err := FromAPI(raw, model); err != nil { t.Fatalf("FromAPI returned an error: %v", err) } if model.Networks.IsUnknown() { t.Error("networkIds stayed unknown; want null") } if !model.Networks.IsNull() { t.Errorf("networkIds = %v; want null", model.Networks) } if model.Detach.IsUnknown() || !model.Detach.IsNull() { t.Errorf("detach = %v; want null", model.Detach) } if model.Replicas.IsUnknown() || !model.Replicas.IsNull() { t.Errorf("replicas = %v; want null", model.Replicas) } } // Keys absent from the response must not clobber existing values, but they // must also not survive as unknown once NullifyUnknown runs. func TestFromAPILeavesAbsentKeysUntouched(t *testing.T) { model := &sample{ Name: types.StringValue("keep-me"), Networks: types.ListUnknown(types.StringType), } if err := FromAPI(json.RawMessage(`{"description":"hi"}`), model); err != nil { t.Fatalf("FromAPI returned an error: %v", err) } if got := model.Name.ValueString(); got != "keep-me" { t.Errorf("name = %q; want %q", got, "keep-me") } if got := model.Desc.ValueString(); got != "hi" { t.Errorf("description = %q; want %q", got, "hi") } if !model.Networks.IsUnknown() { t.Error("networkIds should still be unknown before NullifyUnknown runs") } if err := NullifyUnknown(model); err != nil { t.Fatalf("NullifyUnknown returned an error: %v", err) } if model.Networks.IsUnknown() { t.Error("networkIds stayed unknown after NullifyUnknown") } } // noread fields must never be overwritten by the API response. func TestFromAPISkipsNoReadFields(t *testing.T) { model := &sample{Secret: types.StringValue("plaintext")} if err := FromAPI(json.RawMessage(`{"secret":"$2b$10$hashed"}`), model); err != nil { t.Fatalf("FromAPI returned an error: %v", err) } if got := model.Secret.ValueString(); got != "plaintext" { t.Errorf("secret = %q; want the configured value to survive", got) } } func TestToAPIPhaseSelection(t *testing.T) { model := &sample{ ID: types.StringValue("abc123"), Name: types.StringValue("thing"), Desc: types.StringNull(), ServerID: types.StringValue("srv1"), Status: types.StringValue("running"), Networks: types.ListUnknown(types.StringType), } create, err := ToAPI(model, PhaseCreate) if err != nil { t.Fatalf("ToAPI(create) returned an error: %v", err) } if _, ok := create["thingId"]; ok { t.Error("create body must not carry the server-generated ID") } if create["serverId"] != "srv1" { t.Errorf("create body serverId = %v; want srv1", create["serverId"]) } if _, ok := create["status"]; ok { t.Error("read-only fields must never be sent") } if _, ok := create["networkIds"]; ok { t.Error("unknown values must never be sent") } // description is null but tagged nullable, so it is sent explicitly. value, ok := create["description"] if !ok || value != nil { t.Errorf("description = %v (present %v); want an explicit null", value, ok) } update, err := ToAPI(model, PhaseUpdate) if err != nil { t.Fatalf("ToAPI(update) returned an error: %v", err) } if update["thingId"] != "abc123" { t.Errorf("update body thingId = %v; want abc123", update["thingId"]) } if _, ok := update["serverId"]; ok { t.Error("create-only fields must not be sent on update") } }