diff --git a/internal/provider/group_resource.go b/internal/provider/group_resource.go index f9a0490..0e6239e 100644 --- a/internal/provider/group_resource.go +++ b/internal/provider/group_resource.go @@ -5,8 +5,6 @@ import ( "context" "encoding/json" "fmt" - "io" - "net/http" "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -18,6 +16,31 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" ) +// Group AAP API model +type GroupAPIModel struct { + InventoryId int64 `json:"inventory"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + URL string `json:"url,omitempty"` + Variables string `json:"variables,omitempty"` + Id int64 `json:"id,omitempty"` +} + +// GroupResourceModel maps the group resource schema to a Go struct +type GroupResourceModel struct { + InventoryId types.Int64 `tfsdk:"inventory_id"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + URL types.String `tfsdk:"url"` + Variables jsontypes.Normalized `tfsdk:"variables"` + Id types.Int64 `tfsdk:"id"` +} + +// GroupResource is the resource implementation. +type GroupResource struct { + client ProviderHTTPClient +} + // Ensure the implementation satisfies the expected interfaces. var ( _ resource.Resource = &GroupResource{} @@ -29,27 +52,34 @@ func NewGroupResource() resource.Resource { return &GroupResource{} } -type GroupResourceModelInterface interface { - ParseHttpResponse(body []byte) error - CreateRequestBody() ([]byte, diag.Diagnostics) - GetURL() string -} - -// GroupResource is the resource implementation. -type GroupResource struct { - client ProviderHTTPClient -} - // Metadata returns the resource type name. func (r *GroupResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = req.ProviderTypeName + "_group" } +// Configure adds the provider configured client to the resource +func (r *GroupResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*AAPClient) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *AAPClient, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + r.client = client +} + // Schema defines the schema for the group resource. func (r *GroupResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ Attributes: map[string]schema.Attribute{ - "inventory_id": schema.Int64Attribute{ Required: true, }, @@ -59,7 +89,7 @@ func (r *GroupResource) Schema(_ context.Context, _ resource.SchemaRequest, resp "description": schema.StringAttribute{ Optional: true, }, - "group_url": schema.StringAttribute{ + "url": schema.StringAttribute{ Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), @@ -79,272 +109,181 @@ func (r *GroupResource) Schema(_ context.Context, _ resource.SchemaRequest, resp } } -// GroupResourceModel maps the resource schema data. -type GroupResourceModel struct { - InventoryId types.Int64 `tfsdk:"inventory_id"` - Name types.String `tfsdk:"name"` - Description types.String `tfsdk:"description"` - URL types.String `tfsdk:"group_url"` - Variables jsontypes.Normalized `tfsdk:"variables"` - Id types.Int64 `tfsdk:"id"` -} - -func (d *GroupResourceModel) GetURL() string { - if IsValueProvided(d.URL) { - return d.URL.ValueString() - } - return "" -} - -func (d *GroupResourceModel) CreateRequestBody() ([]byte, diag.Diagnostics) { - body := make(map[string]interface{}) +// Create creates the group resource and sets the Terraform state on success. +func (r *GroupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data GroupResourceModel var diags diag.Diagnostics - // Inventory id - body["inventory"] = d.InventoryId.ValueInt64() - - // Name - body["name"] = d.Name.ValueString() - - // Variables - if IsValueProvided(d.Variables) { - body["variables"] = d.Variables.ValueString() - } - - // Description - if IsValueProvided(d.Description) { - body["description"] = d.Description.ValueString() - } - - json_raw, err := json.Marshal(body) - if err != nil { - diags.Append(diag.NewErrorDiagnostic("Body JSON Marshal Error", err.Error())) - return nil, diags + // Read Terraform plan data into group resource model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return } - return json_raw, diags -} - -func (d *GroupResourceModel) ParseHttpResponse(body []byte) error { - /* Unmarshal the json string */ - result := make(map[string]interface{}) - err := json.Unmarshal(body, &result) - if err != nil { - return err + // Create request body from group data + createRequestBody, diags := data.CreateRequestBody() + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return } - d.Name = types.StringValue(result["name"].(string)) - d.Id = types.Int64Value(int64(result["id"].(float64))) - d.InventoryId = types.Int64Value(int64(result["inventory"].(float64))) - d.URL = types.StringValue(result["url"].(string)) - - if result["description"] != "" { - d.Description = types.StringValue(result["description"].(string)) - } else { - d.Description = types.StringNull() - } + requestData := bytes.NewReader(createRequestBody) - if result["variables"] != "" { - d.Variables = jsontypes.NewNormalizedValue(result["variables"].(string)) - } else { - d.Variables = jsontypes.NewNormalizedNull() + // Create new group in AAP + createResponseBody, diags := r.client.Create("/api/v2/groups/", requestData) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return } - return nil -} -// Configure adds the provider configured client to the resource. -func (d *GroupResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - if req.ProviderData == nil { + // Save new group data into group resource model + diags = data.ParseHttpResponse(createResponseBody) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } - client, ok := req.ProviderData.(*AAPClient) - if !ok { - resp.Diagnostics.AddError( - "Unexpected Resource Configure Type", - fmt.Sprintf("Expected *AAPClient, got: %T. Please report this issue to the provider developers.", req.ProviderData), - ) - + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + if resp.Diagnostics.HasError() { return } - - d.client = client } -func (r GroupResource) CreateGroup(data GroupResourceModelInterface) diag.Diagnostics { +// Read refreshes the Terraform state with the latest group data. +func (r *GroupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data GroupResourceModel var diags diag.Diagnostics - var req_data io.Reader = nil - req_body, diagCreateReq := data.CreateRequestBody() - diags.Append(diagCreateReq...) - if diags.HasError() { - return diags - } - if req_body != nil { - req_data = bytes.NewReader(req_body) - } - resp, body, err := r.client.doRequest(http.MethodPost, "/api/v2/groups/", req_data) - if err != nil { - diags.AddError("Body JSON Marshal Error", err.Error()) - return diags - } - if resp == nil { - diags.AddError("Http response Error", "no http response from server") - return diags - } - if resp.StatusCode != http.StatusCreated { - diags.AddError("Unexpected Http Status code", - fmt.Sprintf("expected (%d) got (%s)", http.StatusCreated, resp.Status)) - return diags - } - err = data.ParseHttpResponse(body) - if err != nil { - diags.AddError("error while parsing the json response: ", err.Error()) - return diags + // Read current Terraform state data into group resource model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return } - return diags -} - -func (r GroupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - var data GroupResourceModel - - // Read Terraform plan data into the model - resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + // Get latest group data from AAP + readResponseBody, diags := r.client.Get(data.URL.ValueString()) + resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - resp.Diagnostics.Append(r.CreateGroup(&data)...) + // Save latest group data into group resource model + diags = data.ParseHttpResponse(readResponseBody) + resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } // Save updated data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } } -func (r GroupResource) DeleteGroup(data GroupResourceModelInterface) diag.Diagnostics { +// Update updates the group resource and sets the updated Terraform state on success. +func (r *GroupResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data GroupResourceModel var diags diag.Diagnostics - resp, _, err := r.client.doRequest(http.MethodDelete, data.GetURL(), nil) - if err != nil { - diags.AddError("Body JSON Marshal Error", err.Error()) - return diags - } - if resp == nil { - diags.AddError("Http response Error", "no http response from server") - return diags - } - if resp.StatusCode != http.StatusNoContent { - diags.AddError("Unexpected Http Status code", - fmt.Sprintf("expected (%d) got (%s)", http.StatusNoContent, resp.Status)) - return diags + // Read Terraform plan data into group resource model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return } - return diags -} -func (r GroupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { - var data GroupResourceModel + // Create request body from group data + updateRequestBody, diags := data.CreateRequestBody() + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + requestData := bytes.NewReader(updateRequestBody) - // Read Terraform plan data into the model - resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + // Update group in AAP + updateResponseBody, diags := r.client.Update(data.URL.ValueString(), requestData) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } - resp.Diagnostics.Append(r.DeleteGroup(&data)...) + // Save updated group data into group resource model + diags = data.ParseHttpResponse(updateResponseBody) + resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } // Save updated data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) -} - -func (r GroupResource) UpdateGroup(data GroupResourceModelInterface) diag.Diagnostics { - var diags diag.Diagnostics - var req_data io.Reader = nil - req_body, diagCreateReq := data.CreateRequestBody() - diags.Append(diagCreateReq...) - if diags.HasError() { - return diags - } - if req_body != nil { - req_data = bytes.NewReader(req_body) - } - resp, body, err := r.client.doRequest(http.MethodPut, data.GetURL(), req_data) - if err != nil { - diags.AddError("Body JSON Marshal Error", err.Error()) - return diags - } - if resp == nil { - diags.AddError("Http response Error", "no http response from server") - return diags - } - if resp.StatusCode != http.StatusOK { - diags.AddError("Unexpected Http Status code", - fmt.Sprintf("expected (%d) got (%s)", http.StatusOK, resp.Status)) - return diags - } - err = data.ParseHttpResponse(body) - if err != nil { - diags.AddError("error while parsing the json response: ", err.Error()) - return diags + if resp.Diagnostics.HasError() { + return } - return diags } -func (r GroupResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { +// Delete deletes the group resource. +func (r *GroupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { var data GroupResourceModel + var diags diag.Diagnostics - resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) - resp.Diagnostics.Append(r.UpdateGroup(&data)...) + // Read current Terraform state data into group resource model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) if resp.Diagnostics.HasError() { return } - // Save updated data into Terraform state - resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + // Delete group from AAP + _, diags = r.client.Delete(data.URL.ValueString()) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } } -func (r GroupResource) ReadGroup(data GroupResourceModelInterface) diag.Diagnostics { - var diags diag.Diagnostics - // Read existing Group - group_url := data.GetURL() - resp, body, err := r.client.doRequest(http.MethodGet, group_url, nil) - if err != nil { - diags.AddError("Get Error", err.Error()) - return diags - } - if resp == nil { - diags.AddError("Http response Error", "no http response from server") - return diags - } - if resp.StatusCode != http.StatusOK { - diags.AddError("Unexpected Http Status code", - fmt.Sprintf("expected (%d) got (%s)", http.StatusOK, resp.Status)) +// CreateRequestBody creates a JSON encoded request body from the group resource data +func (r *GroupResourceModel) CreateRequestBody() ([]byte, diag.Diagnostics) { + // Convert group resource data to API data model + group := GroupAPIModel{ + InventoryId: r.InventoryId.ValueInt64(), + Name: r.Name.ValueString(), + Description: r.Description.ValueString(), + Variables: r.Variables.ValueString(), } - err = data.ParseHttpResponse(body) + // Create JSON encoded request body + jsonBody, err := json.Marshal(group) if err != nil { - diags.AddError("error while parsing the json response: ", err.Error()) - return diags + var diags diag.Diagnostics + diags.AddError( + "Error marshaling request body", + fmt.Sprintf("Could not create request body for group resource, unexpected error: %s", err.Error()), + ) + return nil, diags } - return diags -} -func (r GroupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - var data GroupResourceModel + return jsonBody, nil +} - // Read Terraform prior state data into the model - resp.Diagnostics.Append(req.State.Get(ctx, &data)...) +// ParseHttpResponse updates the group resource data from an AAP API response +func (r *GroupResourceModel) ParseHttpResponse(body []byte) diag.Diagnostics { + var diags diag.Diagnostics - if resp.Diagnostics.HasError() { - return + // Unmarshal the JSON response + var resultApiGroup GroupAPIModel + err := json.Unmarshal(body, &resultApiGroup) + if err != nil { + diags.AddError("Error parsing JSON response from AAP", err.Error()) + return diags } - resp.Diagnostics.Append(r.ReadGroup(&data)...) - if resp.Diagnostics.HasError() { - return - } - // Save updated data into Terraform state - resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + // Map response to the group resource schema and update attribute values + r.InventoryId = types.Int64Value(resultApiGroup.InventoryId) + r.URL = types.StringValue(resultApiGroup.URL) + r.Id = types.Int64Value(resultApiGroup.Id) + r.Name = types.StringValue(resultApiGroup.Name) + r.Description = ParseStringValue(resultApiGroup.Description) + r.Variables = ParseNormalizedValue(resultApiGroup.Variables) + + return diags } diff --git a/internal/provider/group_resource_test.go b/internal/provider/group_resource_test.go index faa5772..1b4476c 100644 --- a/internal/provider/group_resource_test.go +++ b/internal/provider/group_resource_test.go @@ -2,328 +2,178 @@ package provider import ( "bytes" + "context" "encoding/json" "fmt" - "net/http" "os" + "reflect" "regexp" + "strconv" + "strings" "testing" "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes" "github.com/hashicorp/terraform-plugin-framework/diag" + fwresource "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" - "github.com/stretchr/testify/assert" ) -func TestGroupParseHttpResponse(t *testing.T) { - t.Run("Basic Test", func(t *testing.T) { - expected := GroupResourceModel{ - InventoryId: types.Int64Value(1), - Name: types.StringValue("group1"), - Description: types.StringNull(), - Variables: jsontypes.NewNormalizedNull(), - URL: types.StringValue("/api/v2/groups/24/"), - Id: types.Int64Value(1), - } - g := GroupResourceModel{} - body := []byte(`{"inventory": 1, "name": "group1", "url": "/api/v2/groups/24/", "id": 1, "description": "", "variables": ""}`) - err := g.ParseHttpResponse(body) - assert.NoError(t, err) - if expected != g { - t.Errorf("Expected (%s) not equal to actual (%s)", expected, g) - } - }) - t.Run("Test with variables", func(t *testing.T) { - expected := GroupResourceModel{ - InventoryId: types.Int64Value(1), - Name: types.StringValue("group1"), - URL: types.StringValue("/api/v2/groups/24/"), - Description: types.StringNull(), - Variables: jsontypes.NewNormalizedValue("{\"ansible_network_os\":\"ios\"}"), - Id: types.Int64Value(1), - } - g := GroupResourceModel{} - body := []byte(`{"inventory": 1, "name": "group1", "url": "/api/v2/groups/24/", "variables": "{\"ansible_network_os\":\"ios\"}", "id": 1, "description": ""}`) - err := g.ParseHttpResponse(body) - assert.NoError(t, err) - if expected != g { - t.Errorf("Expected (%s) not equal to actual (%s)", expected, g) - } - }) - t.Run("JSON error", func(t *testing.T) { - g := GroupResourceModel{} - body := []byte("Not valid JSON") - err := g.ParseHttpResponse(body) - assert.Error(t, err) - }) -} +func TestGroupResourceSchema(t *testing.T) { + t.Parallel() -func TestGroupCreateRequestBody(t *testing.T) { - t.Run("Basic Test", func(t *testing.T) { - g := GroupResourceModel{ - InventoryId: types.Int64Value(1), - Name: types.StringValue("group1"), - URL: types.StringValue("/api/v2/groups/24/"), - } - body := []byte(`{"inventory": 1, "name": "group1"}`) - result, diags := g.CreateRequestBody() - if diags.HasError() { - t.Fatal(diags.Errors()) - } - assert.JSONEq(t, string(body), string(result)) - }) - t.Run("Unknown Values", func(t *testing.T) { - g := GroupResourceModel{ - InventoryId: basetypes.NewInt64Unknown(), - } - result, diags := g.CreateRequestBody() - - if diags.HasError() { - t.Fatal(diags.Errors()) - } - - bytes.Equal(result, []byte(nil)) - }) - t.Run("All Values", func(t *testing.T) { - g := GroupResourceModel{ - InventoryId: basetypes.NewInt64Value(5), - Name: types.StringValue("group1"), - URL: types.StringValue("/api/v2/groups/24/"), - Variables: jsontypes.NewNormalizedValue("{\"ansible_network_os\":\"ios\"}"), - Description: types.StringValue("New Group"), - } - body := []byte(`{"name": "group1", "inventory": 5, - "description": "New Group", - "variables": "{\"ansible_network_os\":\"ios\"}"}`) - - result, diags := g.CreateRequestBody() - if diags.HasError() { - t.Fatal(diags.Errors()) - } - assert.JSONEq(t, string(body), string(result)) - }) - t.Run("Multiple values for Variables", func(t *testing.T) { - g := GroupResourceModel{ - InventoryId: basetypes.NewInt64Value(5), - Name: types.StringValue("group1"), - URL: types.StringValue("/api/v2/groups/24/"), - Variables: jsontypes.NewNormalizedValue( - "{\"ansible_network_os\":\"ios\",\"ansible_connection\":\"network_cli\",\"ansible_ssh_user\":\"ansible\",\"ansible_ssh_pass\":\"ansi\"}", - ), - Description: types.StringValue("New Group"), - } - body := []byte(`{ - "name": "group1", - "inventory": 5, - "description": "New Group", - "variables": "{\"ansible_network_os\":\"ios\",\"ansible_connection\":\"network_cli\",\"ansible_ssh_user\":\"ansible\",\"ansible_ssh_pass\":\"ansi\"}" - }`) - - result, diags := g.CreateRequestBody() - if diags.HasError() { - t.Fatal(diags.Errors()) - } - assert.JSONEq(t, string(body), string(result)) - }) -} + ctx := context.Background() + schemaRequest := fwresource.SchemaRequest{} + schemaResponse := &fwresource.SchemaResponse{} -type MockGroupResource struct { - InventoryId string - Name string - Description string - URL string - Variables string - Response map[string]string -} + // Instantiate the GroupResource and call its Schema method + NewGroupResource().Schema(ctx, schemaRequest, schemaResponse) -func NewMockGroupResource(inventory, name, description, url, variables string) *MockGroupResource { - return &MockGroupResource{ - InventoryId: inventory, - URL: url, - Name: name, - Description: description, - Variables: variables, - Response: map[string]string{}, + if schemaResponse.Diagnostics.HasError() { + t.Fatalf("Schema method diagnostics: %+v", schemaResponse.Diagnostics) } -} -func (d *MockGroupResource) GetURL() string { - return d.URL -} + // Validate the schema + diagnostics := schemaResponse.Schema.ValidateImplementation(ctx) -func (d *MockGroupResource) ParseHttpResponse(body []byte) error { - err := json.Unmarshal(body, &d.Response) - if err != nil { - return err + if diagnostics.HasError() { + t.Fatalf("Schema validation diagnostics: %+v", diagnostics) } - return nil } -func (d *MockGroupResource) CreateRequestBody() ([]byte, diag.Diagnostics) { - var diags diag.Diagnostics - - m := make(map[string]interface{}) - m["Inventory"] = d.InventoryId - m["Name"] = d.Name - jsonRaw, err := json.Marshal(m) - if err != nil { - diags.AddError("Json Marshall Error", err.Error()) - return nil, diags +func TestGroupResourceCreateRequestBody(t *testing.T) { + var testTable = []struct { + name string + input GroupResourceModel + expected []byte + }{ + { + name: "test with unknown values", + input: GroupResourceModel{ + Name: types.StringValue("test group"), + Description: types.StringUnknown(), + URL: types.StringUnknown(), + Variables: jsontypes.NewNormalizedUnknown(), + InventoryId: types.Int64Value(0), + }, + expected: []byte(`{"inventory":0,"name":"test group"}`), + }, + { + name: "test with null values", + input: GroupResourceModel{ + Name: types.StringValue("test group"), + Description: types.StringNull(), + URL: types.StringNull(), + Variables: jsontypes.NewNormalizedNull(), + InventoryId: types.Int64Value(0), + }, + expected: []byte(`{"inventory":0,"name":"test group"}`), + }, + { + name: "test with some values", + input: GroupResourceModel{ + InventoryId: types.Int64Value(1), + Name: types.StringValue("group1"), + Description: types.StringNull(), + URL: types.StringValue("/api/v2/groups/1/"), + Variables: jsontypes.NewNormalizedValue("{\"foo\":\"bar\"}"), + }, + expected: []byte( + `{"inventory":1,"name":"group1","variables":"{\"foo\":\"bar\"}"}`, + ), + }, + { + name: "test with all values", + input: GroupResourceModel{ + InventoryId: types.Int64Value(1), + Name: types.StringValue("group1"), + Description: types.StringValue("A test group"), + URL: types.StringValue("/api/v2/groups/1/"), + Variables: jsontypes.NewNormalizedValue("{\"foo\":\"bar\"}"), + }, + expected: []byte( + `{"inventory":1,"name":"group1","description":"A test group","variables":"{\"foo\":\"bar\"}"}`, + ), + }, } - return jsonRaw, diags -} - -func TestCreateGroup(t *testing.T) { - t.Run("Create Group", func(t *testing.T) { - g := NewMockGroupResource("1", "Group1", "", "", "") - group := GroupResource{ - client: NewMockHTTPClient([]string{"POST", "post"}, http.StatusCreated), - } - diags := group.CreateGroup(g) - if diags.HasError() { - t.Errorf("Create Group failed") - for _, d := range diags { - t.Errorf("Summary = '%s' - details = '%s'", d.Summary(), d.Detail()) + for _, test := range testTable { + t.Run(test.name, func(t *testing.T) { + actual, diags := test.input.CreateRequestBody() + if diags.HasError() { + t.Fatal(diags.Errors()) } - } - }) -} -func TestUpdateGroup(t *testing.T) { - t.Run("Update Group", func(t *testing.T) { - g := NewMockGroupResource("1", "Group1", "Updated Group", "/api/v2/groups/1/", "") - group := GroupResource{ - client: NewMockHTTPClient([]string{"PUT", "put"}, http.StatusOK), - } - - diags := group.UpdateGroup(g) - if diags.HasError() { - t.Errorf("Update Group failed") - for _, d := range diags { - t.Errorf("Summary = '%s' - details = '%s'", d.Summary(), d.Detail()) - } - } - }) - t.Run("Update Group with variables", func(t *testing.T) { - g := NewMockGroupResource("2", "Group1", "Updated Group", "/api/v2/groups/2/", "{\"ansible_network_os\": \"ios\"}") - group := GroupResource{ - client: NewMockHTTPClient([]string{"PUT", "put"}, http.StatusOK), - } - - diags := group.UpdateGroup(g) - if diags.HasError() { - t.Errorf("Update Group with variables failed") - for _, d := range diags { - t.Errorf("Summary = '%s' - details = '%s'", d.Summary(), d.Detail()) - } - } - }) -} -func TestReadGroup(t *testing.T) { - t.Run("Read Group", func(t *testing.T) { - g := NewMockGroupResource("1", "Group1", "", "/api/v2/groups/2/", "") - group := GroupResource{ - client: NewMockHTTPClient([]string{"GET", "get"}, http.StatusOK), - } - - diags := group.ReadGroup(g) - if diags.HasError() { - t.Errorf("Read Group failed") - for _, d := range diags { - t.Errorf("Summary = '%s' - details = '%s'", d.Summary(), d.Detail()) + if !bytes.Equal(test.expected, actual) { + t.Errorf("Expected (%s) not equal to actual (%s)", test.expected, actual) } - } - }) - t.Run("Read Group with no URL", func(t *testing.T) { - g := NewMockGroupResource("1", "Group1", "", "", "") - group := GroupResource{ - client: NewMockHTTPClient([]string{"GET", "get"}, http.StatusOK), - } - - err := group.ReadGroup(g) - if err == nil { - t.Errorf("Failure expected but the ReadJob did not fail!!") - } - }) -} - -// Acceptance tests - -func getGroupResourceFromStateFile(s *terraform.State) (map[string]interface{}, error) { - for _, rs := range s.RootModule().Resources { - if rs.Type != "aap_group" { - continue - } - groupURL := rs.Primary.Attributes["group_url"] - body, err := testGetResource(groupURL) - if err != nil { - return nil, err - } - - var result map[string]interface{} - err = json.Unmarshal(body, &result) - return result, err + }) } - return nil, fmt.Errorf("Group resource not found from state file") } -func testAccCheckGroupExists(s *terraform.State) error { - _, err := getGroupResourceFromStateFile(s) - return err -} +func TestGroupResourceParseHttpResponse(t *testing.T) { + jsonError := diag.Diagnostics{} + jsonError.AddError("Error parsing JSON response from AAP", "invalid character 'N' looking for beginning of value") + + var testTable = []struct { + name string + input []byte + expected GroupResourceModel + errors diag.Diagnostics + }{ + { + name: "test with JSON error", + input: []byte("Not valid JSON"), + expected: GroupResourceModel{}, + errors: jsonError, + }, + { + name: "test with missing values", + input: []byte(`{"inventory":1, "id": 0, "name": "group1", "url": "/api/v2/groups/1/", "description": ""}`), + expected: GroupResourceModel{ + InventoryId: types.Int64Value(1), + Id: types.Int64Value(0), + Name: types.StringValue("group1"), + URL: types.StringValue("/api/v2/groups/1/"), + Description: types.StringNull(), + }, + errors: diag.Diagnostics{}, + }, + { + name: "test with all values", + input: []byte(`{"inventory":1,"description":"A basic test group","name":"group1","url":"/api/v2/groups/1/",` + + `"variables":"{\"foo\":\"bar\",\"nested\":{\"foobar\":\"baz\"}}"}`), + expected: GroupResourceModel{ + InventoryId: types.Int64Value(1), + Id: types.Int64Value(0), + Name: types.StringValue("group1"), + URL: types.StringValue("/api/v2/groups/1/"), + Description: types.StringValue("A basic test group"), + Variables: jsontypes.NewNormalizedValue("{\"foo\":\"bar\",\"nested\":{\"foobar\":\"baz\"}}"), + }, + errors: diag.Diagnostics{}, + }, + } -func testAccCheckGroupValues(urlBefore *string, groupInventoryId string, groupDescription string, - groupName string, groupVariables string, shouldDiffer bool) func(s *terraform.State) error { - return func(s *terraform.State) error { - var groupURL, description, inventoryId, name, variables string - var differ = false - for _, rs := range s.RootModule().Resources { - if rs.Type != "aap_group" { - continue + for _, test := range testTable { + t.Run(test.name, func(t *testing.T) { + resource := GroupResourceModel{} + diags := resource.ParseHttpResponse(test.input) + if !test.errors.Equal(diags) { + t.Errorf("Expected error diagnostics (%s), actual was (%s)", test.errors, diags) } - groupURL = rs.Primary.Attributes["group_url"] - description = rs.Primary.Attributes["description"] - inventoryId = rs.Primary.Attributes["inventory_id"] - name = rs.Primary.Attributes["name"] - variables = rs.Primary.Attributes["variables"] - } - if len(groupURL) == 0 { - return fmt.Errorf("Group resource not found from state file") - } - if len(*urlBefore) == 0 { - *urlBefore = groupURL - return nil - } - - if description != groupDescription || inventoryId != groupInventoryId || name != groupName || - variables != groupVariables || groupURL != *urlBefore { - differ = true - } - - if shouldDiffer && differ { - return fmt.Errorf("Group resources are equal while expecting them to differ. "+ - "Before [URL: %s, Description: %s, Inventory ID: %s, Name: %s, Variables: %s] "+ - "After [URL: %s, Description: %s, Inventory ID: %s, Name: %s, Variables: %s]", - *urlBefore, description, inventoryId, name, variables, - groupURL, groupDescription, groupInventoryId, groupName, groupVariables) - } else if !shouldDiffer && differ { - return fmt.Errorf("Group resources are equal while expecting them to not differ. "+ - "Before [URL: %s, Description: %s, Inventory ID: %s, Name: %s, Variables: %s] "+ - "After [URL: %s, Description: %s, Inventory ID: %s, Name: %s, Variables: %s]", - *urlBefore, description, inventoryId, name, variables, - groupURL, groupDescription, groupInventoryId, groupName, groupVariables) - } - - return nil + if !reflect.DeepEqual(test.expected, resource) { + t.Errorf("Expected (%s) not equal to actual (%s)", test.expected, resource) + } + }) } } +// Acceptance tests + func testAccGroupResourcePreCheck(t *testing.T) { - // ensure provider requirements + // Ensure provider requirements testAccPreCheck(t) requiredAAPGroupEnvVars := []string{ @@ -337,58 +187,53 @@ func testAccGroupResourcePreCheck(t *testing.T) { } } -func TestAccAAPGroup_basic(t *testing.T) { - var groupURLBefore string +func TestAccGroupResource(t *testing.T) { + var groupApiModel GroupAPIModel var description = "A test group" var variables = "{\"foo\": \"bar\"}" - groupInventoryId := os.Getenv("AAP_TEST_INVENTORY_ID") randomName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) updatedName := "updated" + randomName + inventoryId := os.Getenv("AAP_TEST_INVENTORY_ID") + resource.Test(t, resource.TestCase{ PreCheck: func() { testAccGroupResourcePreCheck(t) }, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, Steps: []resource.TestStep{ - // Create and Read testing + // Invalid variables testing { - Config: testAccBasicGroup(randomName, groupInventoryId), - Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckGroupExists, - testAccCheckGroupValues(&groupURLBefore, groupInventoryId, "", randomName, "", false), - resource.TestCheckResourceAttr("aap_group.test", "name", randomName), - resource.TestCheckResourceAttr("aap_group.test", "inventory_id", groupInventoryId), - resource.TestMatchResourceAttr("aap_group.test", "group_url", regexp.MustCompile("^/api/v2/groups/[0-9]*/$")), - ), + Config: testAccGroupResourceBadVariables(updatedName, inventoryId), + ExpectError: regexp.MustCompile("A string value was provided that is not valid JSON string format"), }, - // Create and Read testing with same parameters + // Create and Read testing { - Config: testAccBasicGroup(randomName, groupInventoryId), + Config: testAccGroupResourceMinimal(randomName, inventoryId), Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckGroupExists, - testAccCheckGroupValues(&groupURLBefore, groupInventoryId, "", randomName, "", false), + testAccCheckGroupResourceExists("aap_group.test", &groupApiModel), + testAccCheckGroupResourceValues(&groupApiModel, randomName, "", "", inventoryId), resource.TestCheckResourceAttr("aap_group.test", "name", randomName), - resource.TestCheckResourceAttr("aap_group.test", "inventory_id", groupInventoryId), + resource.TestCheckResourceAttr("aap_group.test", "inventory_id", inventoryId), resource.TestMatchResourceAttr("aap_group.test", "group_url", regexp.MustCompile("^/api/v2/groups/[0-9]*/$")), ), }, - // Update and Read testing { - Config: testAccUpdateGroupComplete(updatedName, groupInventoryId), + Config: testAccGroupResourceComplete(updatedName, inventoryId), Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckGroupExists, - testAccCheckGroupValues(&groupURLBefore, groupInventoryId, description, updatedName, variables, false), + testAccCheckGroupResourceExists("aap_group.test", &groupApiModel), + testAccCheckGroupResourceValues(&groupApiModel, updatedName, description, variables, inventoryId), resource.TestCheckResourceAttr("aap_group.test", "name", updatedName), - resource.TestCheckResourceAttr("aap_group.test", "inventory_id", groupInventoryId), + resource.TestCheckResourceAttr("aap_group.test", "inventory_id", inventoryId), resource.TestCheckResourceAttr("aap_group.test", "description", description), resource.TestCheckResourceAttr("aap_group.test", "variables", variables), resource.TestMatchResourceAttr("aap_group.test", "group_url", regexp.MustCompile("^/api/v2/groups/[0-9]*/$")), ), }, }, + CheckDestroy: testAccCheckGroupResourceDestroy, }) } -func testAccBasicGroup(name, groupInventoryId string) string { +func testAccGroupResourceMinimal(name, groupInventoryId string) string { return fmt.Sprintf(` resource "aap_group" "test" { name = "%s" @@ -396,7 +241,7 @@ resource "aap_group" "test" { }`, name, groupInventoryId) } -func testAccUpdateGroupComplete(name, groupInventoryId string) string { +func testAccGroupResourceComplete(name, groupInventoryId string) string { return fmt.Sprintf(` resource "aap_group" "test" { name = "%s" @@ -405,3 +250,86 @@ resource "aap_group" "test" { variables = "{\"foo\": \"bar\"}" }`, name, groupInventoryId) } + +// testAccGroupResourceBadVariables returns a configuration for an AAP group with the provided name and invalid variables. +func testAccGroupResourceBadVariables(name, groupInventoryId string) string { + return fmt.Sprintf(` +resource "aap_group" "test" { + name = "%s" + inventory_id = %s + variables = "Not valid JSON" +}`, name, groupInventoryId) +} + +// testAccCheckGroupResourceExists queries the AAP API and retrieves the matching group. +func testAccCheckGroupResourceExists(name string, groupApiModel *GroupAPIModel) resource.TestCheckFunc { + return func(s *terraform.State) error { + groupResource, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("group (%s) not found in state", name) + } + + groupResponseBody, err := testGetResource(groupResource.Primary.Attributes["group_url"]) + if err != nil { + return err + } + + err = json.Unmarshal(groupResponseBody, &groupApiModel) + if err != nil { + return err + } + + if groupApiModel.Id == 0 { + return fmt.Errorf("group (%s) not found in AAP", groupResource.Primary.ID) + } + + return nil + } +} + +func testAccCheckGroupResourceValues(groupApiModel *GroupAPIModel, name string, description string, variables string, + inventoryId string) resource.TestCheckFunc { + return func(s *terraform.State) error { + inv, err := strconv.ParseInt(inventoryId, 10, 64) + if err != nil { + return fmt.Errorf("could not convert \"%s\", to int64", inventoryId) + } + if groupApiModel.InventoryId != inv { + return fmt.Errorf("bad roup inventory id in AAP, expected %d, got: %d", inv, groupApiModel.InventoryId) + } + if groupApiModel.URL == "" { + return fmt.Errorf("bad group URL in AAP, expected a URL path, got: %s", groupApiModel.URL) + } + if groupApiModel.Name != name { + return fmt.Errorf("bad group name in AAP, expected \"%s\", got: %s", name, groupApiModel.Name) + } + if groupApiModel.Description != description { + return fmt.Errorf("bad group description in AAP, expected \"%s\", got: %s", description, groupApiModel.Description) + } + if groupApiModel.Variables != variables { + return fmt.Errorf("bad group variables in AAP, expected \"%s\", got: %s", variables, groupApiModel.Variables) + } + + return nil + } +} + +// testAccCheckGroupResourceDestroy verifies the group has been destroyed. +func testAccCheckGroupResourceDestroy(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "group" { + continue + } + + _, err := testGetResource(rs.Primary.Attributes["url"]) + if err == nil { + return fmt.Errorf("group (%s) still exists.", rs.Primary.Attributes["id"]) + } + + if !strings.Contains(err.Error(), "404") { + return err + } + } + + return nil +} diff --git a/internal/provider/host_resource.go b/internal/provider/host_resource.go index b5d044c..321828d 100644 --- a/internal/provider/host_resource.go +++ b/internal/provider/host_resource.go @@ -24,6 +24,34 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types/basetypes" ) +// Host AAP API model +type HostAPIModel struct { + InventoryId int64 `json:"inventory"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + URL string `json:"url,omitempty"` + Variables string `json:"variables,omitempty"` + Enabled bool `json:"enabled"` + Id int64 `json:"id,omitempty"` +} + +// HostResourceModel maps the host resource schema to a Go struct +type HostResourceModel struct { + InventoryId types.Int64 `tfsdk:"inventory_id"` + Name types.String `tfsdk:"name"` + URL types.String `tfsdk:"url"` + Description types.String `tfsdk:"description"` + Variables jsontypes.Normalized `tfsdk:"variables"` + Groups types.Set `tfsdk:"groups"` + Enabled types.Bool `tfsdk:"enabled"` + Id types.Int64 `tfsdk:"id"` +} + +// HostResource is the resource implementation. +type HostResource struct { + client ProviderHTTPClient +} + // Ensure the implementation satisfies the expected interfaces. var ( _ resource.Resource = &HostResource{} @@ -35,11 +63,6 @@ func NewHostResource() resource.Resource { return &HostResource{} } -// HostResource is the resource implementation. -type HostResource struct { - client ProviderHTTPClient -} - // Metadata returns the resource type name. func (r *HostResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = req.ProviderTypeName + "_host" @@ -63,14 +86,14 @@ func (r *HostResource) Configure(_ context.Context, req resource.ConfigureReques r.client = client } -// Schema defines the schema for the resource. +// Schema defines the schema for the host resource. func (r *HostResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ Attributes: map[string]schema.Attribute{ "inventory_id": schema.Int64Attribute{ Required: true, }, - "host_url": schema.StringAttribute{ + "url": schema.StringAttribute{ Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), @@ -110,29 +133,6 @@ func (r *HostResource) Schema(_ context.Context, _ resource.SchemaRequest, resp } } -// Host AAP API model -type HostAPIModel struct { - InventoryId int64 `json:"inventory"` - Name string `json:"name"` - Description string `json:"description,omitempty"` - URL string `json:"url,omitempty"` - Variables string `json:"variables,omitempty"` - Enabled bool `json:"enabled"` - Id int64 `json:"id,omitempty"` -} - -// HostResourceModel maps the host resource schema to a Go struct -type HostResourceModel struct { - InventoryId types.Int64 `tfsdk:"inventory_id"` - Name types.String `tfsdk:"name"` - URL types.String `tfsdk:"host_url"` - Description types.String `tfsdk:"description"` - Variables jsontypes.Normalized `tfsdk:"variables"` - Groups types.Set `tfsdk:"groups"` - Enabled types.Bool `tfsdk:"enabled"` - Id types.Int64 `tfsdk:"id"` -} - // Create creates the host resource and sets the Terraform state on success. func (r *HostResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { var data HostResourceModel @@ -144,12 +144,13 @@ func (r *HostResource) Create(ctx context.Context, req resource.CreateRequest, r return } - // create request body from host data + // Create request body from host data createRequestBody, diags := data.CreateRequestBody() resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } + requestData := bytes.NewReader(createRequestBody) // Create new host in AAP @@ -257,7 +258,7 @@ func (r *HostResource) Update(ctx context.Context, req resource.UpdateRequest, r return } - // create request body from host data + // Create request body from host data updateRequestBody, diags := data.CreateRequestBody() resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -301,19 +302,6 @@ func (r *HostResource) Update(ctx context.Context, req resource.UpdateRequest, r } } -func (d *HostResourceModel) UpdateStateWithGroups(ctx context.Context, groups []int64) diag.Diagnostics { - var diags diag.Diagnostics - - convertedGroups, diagConvertToInt64 := types.SetValueFrom(ctx, types.Int64Type, groups) - diags.Append(diagConvertToInt64...) - if diags.HasError() { - return diags - } - d.Groups = convertedGroups - - return diags -} - // Delete deletes the host resource. func (r *HostResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { var data HostResourceModel @@ -325,10 +313,6 @@ func (r *HostResource) Delete(ctx context.Context, req resource.DeleteRequest, r return } - if resp.Diagnostics.HasError() { - return - } - // Delete host from AAP _, diags = r.client.Delete(data.URL.ValueString()) resp.Diagnostics.Append(diags...) @@ -337,6 +321,55 @@ func (r *HostResource) Delete(ctx context.Context, req resource.DeleteRequest, r } } +// CreateRequestBody creates a JSON encoded request body from the host resource data +func (r *HostResourceModel) CreateRequestBody() ([]byte, diag.Diagnostics) { + // Convert host resource data to API data model + host := HostAPIModel{ + InventoryId: r.InventoryId.ValueInt64(), + Name: r.Name.ValueString(), + Description: r.Description.ValueString(), + Variables: r.Variables.ValueString(), + Enabled: r.Enabled.ValueBool(), + } + + // Create JSON encoded request body + jsonBody, err := json.Marshal(host) + if err != nil { + var diags diag.Diagnostics + diags.AddError( + "Error marshaling request body", + fmt.Sprintf("Could not create request body for host resource, unexpected error: %s", err.Error()), + ) + return nil, diags + } + + return jsonBody, nil +} + +// ParseHttpResponse updates the host resource data from an AAP API response +func (r *HostResourceModel) ParseHttpResponse(body []byte) diag.Diagnostics { + var diags diag.Diagnostics + + // Unmarshal the JSON response + var resultApiHost HostAPIModel + err := json.Unmarshal(body, &resultApiHost) + if err != nil { + diags.AddError("Error parsing JSON response from AAP", err.Error()) + return diags + } + + // Map response to the host resource schema and update attribute values + r.InventoryId = types.Int64Value(resultApiHost.InventoryId) + r.URL = types.StringValue(resultApiHost.URL) + r.Id = types.Int64Value(resultApiHost.Id) + r.Name = types.StringValue(resultApiHost.Name) + r.Enabled = basetypes.NewBoolValue(resultApiHost.Enabled) + r.Description = ParseStringValue(resultApiHost.Description) + r.Variables = ParseNormalizedValue(resultApiHost.Variables) + + return diags +} + func extractIDs(data map[string]interface{}) []int64 { var ids []int64 @@ -430,6 +463,19 @@ func (r *HostResource) ReadAssociatedGroups(data HostResourceModel) ([]int64, di return extractIDs(result), diags } +func (r *HostResourceModel) UpdateStateWithGroups(ctx context.Context, groups []int64) diag.Diagnostics { + var diags diag.Diagnostics + + convertedGroups, diagConvertToInt64 := types.SetValueFrom(ctx, types.Int64Type, groups) + diags.Append(diagConvertToInt64...) + if diags.HasError() { + return diags + } + r.Groups = convertedGroups + + return diags +} + func (r *HostResource) AssociateGroups(ctx context.Context, data []int64, url string, args ...bool) diag.Diagnostics { var diags diag.Diagnostics var wg sync.WaitGroup @@ -488,61 +534,3 @@ func (r *HostResource) AssociateGroups(ctx context.Context, data []int64, url st return diags } - -// CreateRequestBody creates a JSON encoded request body from the host resource data -func (r *HostResourceModel) CreateRequestBody() ([]byte, diag.Diagnostics) { - // Convert host resource data to API data model - - host := HostAPIModel{ - InventoryId: r.InventoryId.ValueInt64(), - Name: r.Name.ValueString(), - Description: r.Description.ValueString(), - Variables: r.Variables.ValueString(), - Enabled: r.Enabled.ValueBool(), - } - - // create JSON encoded request body - jsonBody, err := json.Marshal(host) - if err != nil { - var diags diag.Diagnostics - diags.AddError( - "Error marshaling request body", - fmt.Sprintf("Could not create request body for host resource, unexpected error: %s", err.Error()), - ) - return nil, diags - } - - return jsonBody, nil -} - -// ParseHttpResponse updates the host resource data from an AAP API response -func (r *HostResourceModel) ParseHttpResponse(body []byte) diag.Diagnostics { - var diags diag.Diagnostics - - // Unmarshal the JSON response - var resultApiHost HostAPIModel - err := json.Unmarshal(body, &resultApiHost) - if err != nil { - diags.AddError("Error parsing JSON response from AAP", err.Error()) - return diags - } - - // Map response to the host resource schema and update attribute values - r.InventoryId = types.Int64Value(resultApiHost.InventoryId) - r.URL = types.StringValue(resultApiHost.URL) - r.Id = types.Int64Value(resultApiHost.Id) - r.Name = types.StringValue(resultApiHost.Name) - r.Enabled = basetypes.NewBoolValue(resultApiHost.Enabled) - if resultApiHost.Description != "" { - r.Description = types.StringValue(resultApiHost.Description) - } else { - r.Description = types.StringNull() - } - if resultApiHost.Variables != "" { - r.Variables = jsontypes.NewNormalizedValue(resultApiHost.Variables) - } else { - r.Variables = jsontypes.NewNormalizedNull() - } - - return diags -} diff --git a/internal/provider/host_resource_test.go b/internal/provider/host_resource_test.go index 38c3bfd..3c265fa 100644 --- a/internal/provider/host_resource_test.go +++ b/internal/provider/host_resource_test.go @@ -253,8 +253,10 @@ func TestHostResourceParseHttpResponse(t *testing.T) { } } +// Acceptance tests + func testAccHostResourcePreCheck(t *testing.T) { - // ensure provider requirements + // Ensure provider requirements testAccPreCheck(t) requiredAAPHostEnvVars := []string{ diff --git a/internal/provider/inventory_resource.go b/internal/provider/inventory_resource.go index 2c84711..6d0514f 100644 --- a/internal/provider/inventory_resource.go +++ b/internal/provider/inventory_resource.go @@ -292,18 +292,8 @@ func (r *inventoryResourceModel) parseHTTPResponse(body []byte) diag.Diagnostics r.Organization = types.Int64Value(apiInventory.Organization) r.Url = types.StringValue(apiInventory.Url) r.Name = types.StringValue(apiInventory.Name) - - if apiInventory.Description != "" { - r.Description = types.StringValue(apiInventory.Description) - } else { - r.Description = types.StringNull() - } - - if apiInventory.Variables != "" { - r.Variables = jsontypes.NewNormalizedValue(apiInventory.Variables) - } else { - r.Variables = jsontypes.NewNormalizedNull() - } + r.Description = ParseStringValue(apiInventory.Description) + r.Variables = ParseNormalizedValue(apiInventory.Variables) return parseResponseDiags } diff --git a/internal/provider/utils.go b/internal/provider/utils.go index a59d6e2..08d90cf 100644 --- a/internal/provider/utils.go +++ b/internal/provider/utils.go @@ -8,8 +8,10 @@ import ( "path" "slices" + "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" ) func IsValueProvided(value attr.Value) bool { @@ -55,3 +57,19 @@ func getURL(base string, paths ...string) (string, diag.Diagnostics) { return u.String(), diags } + +func ParseStringValue(description string) types.String { + if description != "" { + return types.StringValue(description) + } else { + return types.StringNull() + } +} + +func ParseNormalizedValue(variables string) jsontypes.Normalized { + if variables != "" { + return jsontypes.NewNormalizedValue(variables) + } else { + return jsontypes.NewNormalizedNull() + } +} diff --git a/internal/provider/utils_test.go b/internal/provider/utils_test.go index a5a1c6f..052e8b2 100644 --- a/internal/provider/utils_test.go +++ b/internal/provider/utils_test.go @@ -2,6 +2,9 @@ package provider import ( "testing" + + "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes" + "github.com/hashicorp/terraform-plugin-framework/types" ) func TestGetURL(t *testing.T) { @@ -37,3 +40,43 @@ func TestGetURL(t *testing.T) { }) } } + +func TestParseStringValue(t *testing.T) { + tests := []struct { + input string + expected types.String + description string + }{ + {"non-empty", types.StringValue("non-empty"), "Test non-empty string"}, + {"", types.StringNull(), "Test empty string"}, + } + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + result := ParseStringValue(test.input) + if result != test.expected { + t.Errorf("Expected %v, but got %v", test.expected, result) + } + }) + } +} + +func TestParseNormalizedValue(t *testing.T) { + tests := []struct { + input string + expected jsontypes.Normalized + description string + }{ + {"{\"foo\":\"bar\"}", jsontypes.NewNormalizedValue("{\"foo\":\"bar\"}"), "Test non-empty string"}, + {"", jsontypes.NewNormalizedNull(), "Test empty string"}, + } + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + result := ParseNormalizedValue(test.input) + if result != test.expected { + t.Errorf("Expected %v, but got %v", test.expected, result) + } + }) + } +}