diff --git a/internal/provider/aclmapping/acl_mapping_resource.go b/internal/provider/aclmapping/acl_mapping_resource.go index 250eb75..955b28a 100644 --- a/internal/provider/aclmapping/acl_mapping_resource.go +++ b/internal/provider/aclmapping/acl_mapping_resource.go @@ -197,7 +197,7 @@ func (r *ACLMappingResource) Delete(ctx context.Context, req resource.DeleteRequ func (r *ACLMappingResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { parts := strings.Split(req.ID, "/") if len(parts) != 2 { - resp.Diagnostics.AddError("Invalid import ID", "Expected ID in the format 'team_id/project_id'") + resp.Diagnostics.AddError("Invalid import ID", fmt.Sprintf("Expected ID in the format 'team_id/project_id', got [%s]", req.ID)) return } diff --git a/internal/provider/notificationruleproject/notification_rule_project_resource.go b/internal/provider/notificationruleproject/notification_rule_project_resource.go index 77c0278..d2a8a8d 100644 --- a/internal/provider/notificationruleproject/notification_rule_project_resource.go +++ b/internal/provider/notificationruleproject/notification_rule_project_resource.go @@ -165,7 +165,7 @@ func (r *NotificationRuleProjectResource) Delete(ctx context.Context, req resour func (r *NotificationRuleProjectResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { parts := strings.Split(req.ID, "/") if len(parts) != 2 { - resp.Diagnostics.AddError("Invalid import ID", "Expected ID in the format 'project_id/rule_id'") + resp.Diagnostics.AddError("Invalid import ID", fmt.Sprintf("Expected ID in the format 'project_id/rule_id', got [%s]", req.ID)) return } diff --git a/internal/provider/project/project_resource_test.go b/internal/provider/project/project_resource_test.go index a952028..4605333 100644 --- a/internal/provider/project/project_resource_test.go +++ b/internal/provider/project/project_resource_test.go @@ -1,17 +1,13 @@ package project_test import ( - "context" - "errors" "fmt" dtrack "github.com/futurice/dependency-track-client-go" "github.com/futurice/terraform-provider-dependencytrack/internal/testutils" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" + "github.com/futurice/terraform-provider-dependencytrack/internal/testutils/projecttestutils" "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/terraform" "os" "strconv" "testing" @@ -34,7 +30,7 @@ func TestAccProjectResource_basic(t *testing.T) { projectName := acctest.RandomWithPrefix("test-project") otherProjectName := acctest.RandomWithPrefix("other-test-project") - projectResourceName := createProjectResourceName("test") + projectResourceName := projecttestutils.CreateProjectResourceName("test") testProject := dtrack.Project{ Name: projectName, @@ -52,7 +48,7 @@ func TestAccProjectResource_basic(t *testing.T) { { Config: testAccProjectConfigBasic(testDependencyTrack, projectName), Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckProjectExistsAndHasExpectedData(ctx, testDependencyTrack, projectResourceName, testProject), + projecttestutils.TestAccCheckProjectExistsAndHasExpectedData(ctx, testDependencyTrack, projectResourceName, testProject), resource.TestCheckResourceAttrSet(projectResourceName, "id"), resource.TestCheckResourceAttr(projectResourceName, "name", projectName), resource.TestCheckResourceAttr(projectResourceName, "classifier", testProject.Classifier), @@ -69,12 +65,12 @@ func TestAccProjectResource_basic(t *testing.T) { { Config: testAccProjectConfigBasic(testDependencyTrack, otherProjectName), Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckProjectExistsAndHasExpectedData(ctx, testDependencyTrack, projectResourceName, testUpdatedProject), + projecttestutils.TestAccCheckProjectExistsAndHasExpectedData(ctx, testDependencyTrack, projectResourceName, testUpdatedProject), resource.TestCheckResourceAttr(projectResourceName, "name", otherProjectName), ), }, }, - CheckDestroy: testAccCheckProjectDoesNotExists(ctx, testDependencyTrack, projectResourceName), + CheckDestroy: projecttestutils.TestAccCheckProjectDoesNotExists(ctx, testDependencyTrack, projectResourceName), }) } @@ -82,7 +78,7 @@ func TestAccProjectResource_description(t *testing.T) { ctx := testutils.CreateTestContext(t) projectName := acctest.RandomWithPrefix("test-project") - projectResourceName := createProjectResourceName("test") + projectResourceName := projecttestutils.CreateProjectResourceName("test") testProject := dtrack.Project{ Name: projectName, @@ -101,14 +97,14 @@ func TestAccProjectResource_description(t *testing.T) { { Config: testAccProjectConfigDescription(testDependencyTrack, projectName, testProject.Description), Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckProjectExistsAndHasExpectedData(ctx, testDependencyTrack, projectResourceName, testProject), + projecttestutils.TestAccCheckProjectExistsAndHasExpectedData(ctx, testDependencyTrack, projectResourceName, testProject), resource.TestCheckResourceAttr(projectResourceName, "description", testProject.Description), ), }, { Config: testAccProjectConfigDescription(testDependencyTrack, projectName, testUpdatedProject.Description), Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckProjectExistsAndHasExpectedData(ctx, testDependencyTrack, projectResourceName, testUpdatedProject), + projecttestutils.TestAccCheckProjectExistsAndHasExpectedData(ctx, testDependencyTrack, projectResourceName, testUpdatedProject), resource.TestCheckResourceAttr(projectResourceName, "description", testUpdatedProject.Description), ), }, @@ -120,7 +116,7 @@ func TestAccProjectResource_inactive(t *testing.T) { ctx := testutils.CreateTestContext(t) projectName := acctest.RandomWithPrefix("test-project") - projectResourceName := createProjectResourceName("test") + projectResourceName := projecttestutils.CreateProjectResourceName("test") testProject := dtrack.Project{ Name: projectName, @@ -138,14 +134,14 @@ func TestAccProjectResource_inactive(t *testing.T) { { Config: testAccProjectConfigActivity(testDependencyTrack, projectName, testProject.Active), Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckProjectExistsAndHasExpectedData(ctx, testDependencyTrack, projectResourceName, testProject), + projecttestutils.TestAccCheckProjectExistsAndHasExpectedData(ctx, testDependencyTrack, projectResourceName, testProject), resource.TestCheckResourceAttr(projectResourceName, "active", strconv.FormatBool(testProject.Active)), ), }, { Config: testAccProjectConfigActivity(testDependencyTrack, projectName, testUpdatedProject.Active), Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckProjectExistsAndHasExpectedData(ctx, testDependencyTrack, projectResourceName, testUpdatedProject), + projecttestutils.TestAccCheckProjectExistsAndHasExpectedData(ctx, testDependencyTrack, projectResourceName, testUpdatedProject), resource.TestCheckResourceAttr(projectResourceName, "active", strconv.FormatBool(testUpdatedProject.Active)), ), }, @@ -157,7 +153,7 @@ func TestAccProjectResource_classifier(t *testing.T) { ctx := testutils.CreateTestContext(t) projectName := acctest.RandomWithPrefix("test-project") - projectResourceName := createProjectResourceName("test") + projectResourceName := projecttestutils.CreateProjectResourceName("test") testProject := dtrack.Project{ Name: projectName, @@ -175,14 +171,14 @@ func TestAccProjectResource_classifier(t *testing.T) { { Config: testAccProjectConfigClassifier(testDependencyTrack, testProject.Name, testProject.Classifier), Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckProjectExistsAndHasExpectedData(ctx, testDependencyTrack, projectResourceName, testProject), + projecttestutils.TestAccCheckProjectExistsAndHasExpectedData(ctx, testDependencyTrack, projectResourceName, testProject), resource.TestCheckResourceAttr(projectResourceName, "classifier", testProject.Classifier), ), }, { Config: testAccProjectConfigClassifier(testDependencyTrack, testProject.Name, testUpdatedProject.Classifier), Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckProjectExistsAndHasExpectedData(ctx, testDependencyTrack, projectResourceName, testUpdatedProject), + projecttestutils.TestAccCheckProjectExistsAndHasExpectedData(ctx, testDependencyTrack, projectResourceName, testUpdatedProject), resource.TestCheckResourceAttr(projectResourceName, "classifier", testUpdatedProject.Classifier), ), }, @@ -193,9 +189,9 @@ func TestAccProjectResource_classifier(t *testing.T) { func TestAccProjectResource_parent(t *testing.T) { ctx := testutils.CreateTestContext(t) - projectResourceName := createProjectResourceName("test") - parentProjectResourceName := createProjectResourceName("parent") - otherParentProjectResourceName := createProjectResourceName("other_parent") + projectResourceName := projecttestutils.CreateProjectResourceName("test") + parentProjectResourceName := projecttestutils.CreateProjectResourceName("parent") + otherParentProjectResourceName := projecttestutils.CreateProjectResourceName("other_parent") projectName := acctest.RandomWithPrefix("test-project") @@ -218,7 +214,7 @@ func TestAccProjectResource_parent(t *testing.T) { Config: testAccProjectConfigParent(testDependencyTrack, projectName), Check: resource.ComposeAggregateTestCheckFunc( testutils.TestAccCheckGetResourceID(parentProjectResourceName, &parentProjectID), - testAccCheckProjectExistsAndHasExpectedLazyData(ctx, testDependencyTrack, projectResourceName, func() dtrack.Project { return createTestProject(&parentProjectID) }), + projecttestutils.TestAccCheckProjectExistsAndHasExpectedLazyData(ctx, testDependencyTrack, projectResourceName, func() dtrack.Project { return createTestProject(&parentProjectID) }), resource.TestCheckResourceAttrPtr(projectResourceName, "parent_id", &parentProjectID), ), }, @@ -226,7 +222,7 @@ func TestAccProjectResource_parent(t *testing.T) { Config: testAccProjectConfigOtherParent(testDependencyTrack, projectName), Check: resource.ComposeAggregateTestCheckFunc( testutils.TestAccCheckGetResourceID(otherParentProjectResourceName, &otherParentProjectID), - testAccCheckProjectExistsAndHasExpectedLazyData(ctx, testDependencyTrack, projectResourceName, func() dtrack.Project { return createTestProject(&otherParentProjectID) }), + projecttestutils.TestAccCheckProjectExistsAndHasExpectedLazyData(ctx, testDependencyTrack, projectResourceName, func() dtrack.Project { return createTestProject(&otherParentProjectID) }), resource.TestCheckResourceAttrPtr(projectResourceName, "parent_id", &otherParentProjectID), ), }, @@ -349,75 +345,3 @@ resource "dependencytrack_project" "test" { ), ) } - -func testAccCheckProjectExistsAndHasExpectedData(ctx context.Context, testDependencyTrack *testutils.TestDependencyTrack, resourceName string, expectedProject dtrack.Project) resource.TestCheckFunc { - return testAccCheckProjectExistsAndHasExpectedLazyData(ctx, testDependencyTrack, resourceName, func() dtrack.Project { return expectedProject }) -} - -func testAccCheckProjectExistsAndHasExpectedLazyData(ctx context.Context, testDependencyTrack *testutils.TestDependencyTrack, resourceName string, expectedProjectCreator func() dtrack.Project) resource.TestCheckFunc { - return func(state *terraform.State) error { - expectedProject := expectedProjectCreator() - - project, err := findProjectByResourceName(ctx, testDependencyTrack, state, resourceName) - if err != nil { - return err - } - if project == nil { - return fmt.Errorf("project for resource %s does not exist in Dependency-Track", resourceName) - } - - diff := cmp.Diff(project, &expectedProject, cmpopts.IgnoreFields(dtrack.Project{}, "UUID", "Properties", "Tags", "Metrics")) - if diff != "" { - return fmt.Errorf("project for resource %s is different than expected: %s", resourceName, diff) - } - - return nil - } -} - -func testAccCheckProjectDoesNotExists(ctx context.Context, testDependencyTrack *testutils.TestDependencyTrack, resourceName string) resource.TestCheckFunc { - return func(state *terraform.State) error { - project, err := findProjectByResourceName(ctx, testDependencyTrack, state, resourceName) - if err != nil { - return err - } - if project != nil { - return fmt.Errorf("project for resource %s exists in Dependency-Track, even though it shouldn't: %v", resourceName, project) - } - - return nil - } -} - -func findProjectByResourceName(ctx context.Context, testDependencyTrack *testutils.TestDependencyTrack, state *terraform.State, resourceName string) (*dtrack.Project, error) { - projectID, err := testutils.GetResourceID(state, resourceName) - if err != nil { - return nil, err - } - - project, err := findProject(ctx, testDependencyTrack, projectID) - if err != nil { - return nil, fmt.Errorf("failed to get project for resource %s: %w", resourceName, err) - } - - return project, nil -} - -func findProject(ctx context.Context, testDependencyTrack *testutils.TestDependencyTrack, projectID uuid.UUID) (*dtrack.Project, error) { - project, err := testDependencyTrack.Client.Project.Get(ctx, projectID) - if err != nil { - var apiErr *dtrack.APIError - ok := errors.As(err, &apiErr) - if !ok || apiErr.StatusCode != 404 { - return nil, fmt.Errorf("failed to get project from Dependency-Track: %w", err) - } - - return nil, nil - } - - return &project, nil -} - -func createProjectResourceName(localName string) string { - return fmt.Sprintf("dependencytrack_project.%s", localName) -} diff --git a/internal/provider/team/team_resource_test.go b/internal/provider/team/team_resource_test.go index 4bc389e..177bc23 100644 --- a/internal/provider/team/team_resource_test.go +++ b/internal/provider/team/team_resource_test.go @@ -1,18 +1,14 @@ package team_test import ( - "context" - "errors" "fmt" + "testing" + dtrack "github.com/futurice/dependency-track-client-go" "github.com/futurice/terraform-provider-dependencytrack/internal/testutils" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" - "github.com/google/uuid" + "github.com/futurice/terraform-provider-dependencytrack/internal/testutils/teamtestutils" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/terraform" - "testing" ) func TestAccTeamResource_basic(t *testing.T) { @@ -20,7 +16,7 @@ func TestAccTeamResource_basic(t *testing.T) { teamName := acctest.RandomWithPrefix("test-team") otherTeamName := acctest.RandomWithPrefix("other-test-team") - teamResourceName := createTeamResourceName("test") + teamResourceName := teamtestutils.CreateTeamResourceName("test") testTeam := dtrack.Team{ Name: teamName, @@ -36,7 +32,7 @@ func TestAccTeamResource_basic(t *testing.T) { { Config: testAccTeamConfigBasic(testDependencyTrack, teamName), Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckTeamExistsAndHasExpectedData(ctx, testDependencyTrack, teamResourceName, testTeam), + teamtestutils.TestAccCheckTeamExistsAndHasExpectedData(ctx, testDependencyTrack, teamResourceName, testTeam), resource.TestCheckResourceAttrSet(teamResourceName, "id"), resource.TestCheckResourceAttr(teamResourceName, "name", teamName), ), @@ -49,12 +45,12 @@ func TestAccTeamResource_basic(t *testing.T) { { Config: testAccTeamConfigBasic(testDependencyTrack, otherTeamName), Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckTeamExistsAndHasExpectedData(ctx, testDependencyTrack, teamResourceName, testUpdatedTeam), + teamtestutils.TestAccCheckTeamExistsAndHasExpectedData(ctx, testDependencyTrack, teamResourceName, testUpdatedTeam), resource.TestCheckResourceAttr(teamResourceName, "name", otherTeamName), ), }, }, - CheckDestroy: testAccCheckTeamDoesNotExists(ctx, testDependencyTrack, teamResourceName), + CheckDestroy: teamtestutils.TestAccCheckTeamDoesNotExists(ctx, testDependencyTrack, teamResourceName), }) } @@ -69,80 +65,3 @@ resource "dependencytrack_team" "test" { ), ) } - -func testAccCheckTeamExistsAndHasExpectedData(ctx context.Context, testDependencyTrack *testutils.TestDependencyTrack, resourceName string, expectedTeam dtrack.Team) resource.TestCheckFunc { - return testAccCheckTeamExistsAndHasExpectedLazyData(ctx, testDependencyTrack, resourceName, func() dtrack.Team { return expectedTeam }) -} - -func testAccCheckTeamExistsAndHasExpectedLazyData(ctx context.Context, testDependencyTrack *testutils.TestDependencyTrack, resourceName string, expectedTeamCreator func() dtrack.Team) resource.TestCheckFunc { - return func(state *terraform.State) error { - expectedTeam := expectedTeamCreator() - - team, err := findTeamByResourceName(ctx, testDependencyTrack, state, resourceName) - if err != nil { - return err - } - if team == nil { - return fmt.Errorf("team for resource %s does not exist in Dependency-Track", resourceName) - } - - diff := cmp.Diff(team, &expectedTeam, cmpopts.IgnoreFields(dtrack.Team{}, "UUID")) - if diff != "" { - return fmt.Errorf("team for resource %s is different than expected: %s", resourceName, diff) - } - - return nil - } -} - -func testAccCheckTeamDoesNotExists(ctx context.Context, testDependencyTrack *testutils.TestDependencyTrack, resourceName string) resource.TestCheckFunc { - return func(state *terraform.State) error { - team, err := findTeamByResourceName(ctx, testDependencyTrack, state, resourceName) - if err != nil { - return err - } - if team != nil { - return fmt.Errorf("team for resource %s exists in Dependency-Track, even though it shouldn't: %v", resourceName, team) - } - - return nil - } -} - -func findTeamByResourceName(ctx context.Context, testDependencyTrack *testutils.TestDependencyTrack, state *terraform.State, resourceName string) (*dtrack.Team, error) { - teamID, err := testutils.GetResourceID(state, resourceName) - if err != nil { - return nil, err - } - - team, err := findTeam(ctx, testDependencyTrack, teamID) - if err != nil { - return nil, fmt.Errorf("failed to get team for resource %s: %w", resourceName, err) - } - - return team, nil -} - -func findTeam(ctx context.Context, testDependencyTrack *testutils.TestDependencyTrack, teamID uuid.UUID) (*dtrack.Team, error) { - team, err := testDependencyTrack.Client.Team.Get(ctx, teamID) - if err != nil { - var apiErr *dtrack.APIError - ok := errors.As(err, &apiErr) - if !ok || apiErr.StatusCode != 404 { - return nil, fmt.Errorf("failed to get team from Dependency-Track: %w", err) - } - - return nil, nil - } - - // normalize the returned object not to contain an empty array reference - if len(team.Permissions) == 0 { - team.Permissions = nil - } - - return &team, nil -} - -func createTeamResourceName(localName string) string { - return fmt.Sprintf("dependencytrack_team.%s", localName) -} diff --git a/internal/provider/teamapikey/team_api_key_resource.go b/internal/provider/teamapikey/team_api_key_resource.go index 9c4090a..2fff0b0 100644 --- a/internal/provider/teamapikey/team_api_key_resource.go +++ b/internal/provider/teamapikey/team_api_key_resource.go @@ -165,7 +165,7 @@ func (r *TeamAPIKeyResource) Delete(ctx context.Context, req resource.DeleteRequ func (r *TeamAPIKeyResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { parts := strings.Split(req.ID, "/") if len(parts) != 2 { - resp.Diagnostics.AddError("Invalid import ID", "Expected ID in the format 'team_id/api_key'") + resp.Diagnostics.AddError("Invalid import ID", fmt.Sprintf("Expected ID in the format 'team_id/api_key', got [%s]", req.ID)) return } diff --git a/internal/provider/teampermission/team_permission_resource.go b/internal/provider/teampermission/team_permission_resource.go index 09777f1..079c6e3 100644 --- a/internal/provider/teampermission/team_permission_resource.go +++ b/internal/provider/teampermission/team_permission_resource.go @@ -6,11 +6,11 @@ package teampermission import ( "context" "fmt" + "github.com/google/uuid" "strings" dtrack "github.com/futurice/dependency-track-client-go" - "github.com/google/uuid" - + "github.com/futurice/terraform-provider-dependencytrack/internal/utils" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -32,6 +32,7 @@ type TeamPermissionResource struct { // TeamPermissionResourceModel describes the resource data model. type TeamPermissionResourceModel struct { + ID types.String `tfsdk:"id"` TeamID types.String `tfsdk:"team_id"` Name types.String `tfsdk:"name"` } @@ -53,6 +54,10 @@ func (r *TeamPermissionResource) Schema(ctx context.Context, req resource.Schema MarkdownDescription: "Name of the permission", Required: true, }, + "id": schema.StringAttribute{ + MarkdownDescription: "Synthetic permission ID in the form of team_id/permission_name", + Computed: true, + }, }, } } @@ -77,10 +82,15 @@ func (r *TeamPermissionResource) Configure(ctx context.Context, req resource.Con } func (r *TeamPermissionResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - var plan TeamPermissionResourceModel + var plan, state TeamPermissionResourceModel resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + teamUUID, teamUUIDDiags := utils.ParseUUID(plan.TeamID.ValueString()) + resp.Diagnostics.Append(teamUUIDDiags...) if resp.Diagnostics.HasError() { return } @@ -89,7 +99,7 @@ func (r *TeamPermissionResource) Create(ctx context.Context, req resource.Create Name: plan.Name.ValueString(), } - respTeam, err := r.client.Permission.AddPermissionToTeam(ctx, permission, uuid.MustParse(plan.TeamID.ValueString())) + _, err := r.client.Permission.AddPermissionToTeam(ctx, permission, teamUUID) if err != nil { if apiErr, ok := err.(*dtrack.APIError); ok { switch apiErr.StatusCode { @@ -104,21 +114,28 @@ func (r *TeamPermissionResource) Create(ctx context.Context, req resource.Create return } - plan.TeamID = types.StringValue(respTeam.UUID.String()) + state.ID = types.StringValue(makePermissionID(teamUUID, permission.Name)) + state.TeamID = types.StringValue(teamUUID.String()) + state.Name = types.StringValue(permission.Name) - resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) } func (r *TeamPermissionResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { var state TeamPermissionResourceModel resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + teamUUID, teamUUIDDiags := utils.ParseUUID(state.TeamID.ValueString()) + resp.Diagnostics.Append(teamUUIDDiags...) if resp.Diagnostics.HasError() { return } - respTeam, err := r.client.Team.Get(ctx, uuid.MustParse(state.TeamID.ValueString())) + respTeam, err := r.client.Team.Get(ctx, teamUUID) if err != nil { if apiErr, ok := err.(*dtrack.APIError); ok && apiErr.StatusCode == 404 { resp.State.RemoveResource(ctx) @@ -150,7 +167,14 @@ func (r *TeamPermissionResource) Update(ctx context.Context, req resource.Update resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + newTeamUUID, newTeamUUIDDiags := utils.ParseUUID(plan.TeamID.ValueString()) + oldTeamUUID, oldTeamUUIDDiags := utils.ParseUUID(state.TeamID.ValueString()) + resp.Diagnostics.Append(newTeamUUIDDiags...) + resp.Diagnostics.Append(oldTeamUUIDDiags...) if resp.Diagnostics.HasError() { return } @@ -159,9 +183,9 @@ func (r *TeamPermissionResource) Update(ctx context.Context, req resource.Update Name: plan.Name.ValueString(), } - _, err := r.client.Permission.AddPermissionToTeam(ctx, newPermission, uuid.MustParse(state.TeamID.ValueString())) + _, err := r.client.Permission.AddPermissionToTeam(ctx, newPermission, newTeamUUID) if err != nil { - resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete permission, got error: %s", err)) + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create new permission, got error: %s", err)) return } @@ -169,12 +193,14 @@ func (r *TeamPermissionResource) Update(ctx context.Context, req resource.Update Name: state.Name.ValueString(), } - _, err = r.client.Permission.RemovePermissionFromTeam(ctx, oldPermission, uuid.MustParse(state.TeamID.ValueString())) + _, err = r.client.Permission.RemovePermissionFromTeam(ctx, oldPermission, oldTeamUUID) if err != nil { - resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete permission, got error: %s", err)) + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete old permission, got error: %s", err)) return } + state.ID = types.StringValue(makePermissionID(newTeamUUID, newPermission.Name)) + state.TeamID = types.StringValue(newTeamUUID.String()) state.Name = types.StringValue(newPermission.Name) resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) @@ -184,7 +210,12 @@ func (r *TeamPermissionResource) Delete(ctx context.Context, req resource.Delete var state TeamPermissionResourceModel resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + teamUUID, teamUUIDDiags := utils.ParseUUID(state.TeamID.ValueString()) + resp.Diagnostics.Append(teamUUIDDiags...) if resp.Diagnostics.HasError() { return } @@ -193,7 +224,7 @@ func (r *TeamPermissionResource) Delete(ctx context.Context, req resource.Delete Name: state.Name.ValueString(), } - _, err := r.client.Permission.RemovePermissionFromTeam(ctx, permission, uuid.MustParse(state.TeamID.ValueString())) + _, err := r.client.Permission.RemovePermissionFromTeam(ctx, permission, teamUUID) if err != nil { resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete permission, got error: %s", err)) return @@ -205,10 +236,15 @@ func (r *TeamPermissionResource) Delete(ctx context.Context, req resource.Delete func (r *TeamPermissionResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { parts := strings.Split(req.ID, "/") if len(parts) != 2 { - resp.Diagnostics.AddError("Invalid import ID", "Expected ID in the format 'team_id/permission_name'") + resp.Diagnostics.AddError("Invalid import ID", fmt.Sprintf("Expected ID in the format 'team_id/permission_name', got [%s]", req.ID)) return } + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), req.ID)...) resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("team_id"), parts[0])...) resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), parts[1])...) } + +func makePermissionID(teamUUID uuid.UUID, permission string) string { + return fmt.Sprintf("%s/%s", teamUUID.String(), permission) +} diff --git a/internal/provider/teampermission/team_permission_resource_test.go b/internal/provider/teampermission/team_permission_resource_test.go new file mode 100644 index 0000000..a96378d --- /dev/null +++ b/internal/provider/teampermission/team_permission_resource_test.go @@ -0,0 +1,146 @@ +package teampermission_test + +import ( + "fmt" + "github.com/futurice/terraform-provider-dependencytrack/internal/testutils" + "github.com/futurice/terraform-provider-dependencytrack/internal/testutils/teamtestutils" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "testing" +) + +func TestAccTeamResource_basic(t *testing.T) { + ctx := testutils.CreateTestContext(t) + + teamName := acctest.RandomWithPrefix("test-team") + permissionName := "ACCESS_MANAGEMENT" + otherPermissionName := "BOM_UPLOAD" + + teamResourceName := teamtestutils.CreateTeamResourceName("test") + otherTeamResourceName := teamtestutils.CreateTeamResourceName("test-other") + permissionResourceName := teamtestutils.CreateTeamPermissionResourceName("test") + + var teamID, otherTeamID string + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testutils.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: testutils.TestAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccTeamPermissionConfigBasic(testDependencyTrack, teamName, permissionName), + Check: resource.ComposeAggregateTestCheckFunc( + teamtestutils.TestAccCheckTeamHasExpectedPermissions(ctx, testDependencyTrack, teamResourceName, []string{permissionName}), + testutils.TestAccCheckGetResourceID(teamResourceName, &teamID), + resource.TestCheckResourceAttrSet(permissionResourceName, "id"), + resource.TestCheckResourceAttrPtr(permissionResourceName, "team_id", &teamID), + resource.TestCheckResourceAttr(permissionResourceName, "name", permissionName), + ), + }, + { + ResourceName: permissionResourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccTeamPermissionConfigBasic(testDependencyTrack, teamName, otherPermissionName), + Check: resource.ComposeAggregateTestCheckFunc( + teamtestutils.TestAccCheckTeamHasExpectedPermissions(ctx, testDependencyTrack, teamResourceName, []string{otherPermissionName}), + resource.TestCheckResourceAttrSet(permissionResourceName, "id"), + resource.TestCheckResourceAttrPtr(permissionResourceName, "team_id", &teamID), + resource.TestCheckResourceAttr(permissionResourceName, "name", otherPermissionName), + ), + }, + { + Config: testAccTeamPermissionConfigOtherTeam(testDependencyTrack, teamName, permissionName), + Check: resource.ComposeAggregateTestCheckFunc( + teamtestutils.TestAccCheckTeamHasExpectedPermissions(ctx, testDependencyTrack, teamResourceName, []string{}), + teamtestutils.TestAccCheckTeamHasExpectedPermissions(ctx, testDependencyTrack, otherTeamResourceName, []string{permissionName}), + testutils.TestAccCheckGetResourceID(otherTeamResourceName, &otherTeamID), + resource.TestCheckResourceAttrSet(permissionResourceName, "id"), + resource.TestCheckResourceAttrPtr(permissionResourceName, "team_id", &otherTeamID), + resource.TestCheckResourceAttr(permissionResourceName, "name", permissionName), + ), + }, + { + Config: testAccTeamPermissionConfigNoPermission(testDependencyTrack, teamName), + Check: resource.ComposeAggregateTestCheckFunc( + teamtestutils.TestAccCheckTeamHasExpectedPermissions(ctx, testDependencyTrack, teamResourceName, []string{}), + teamtestutils.TestAccCheckTeamHasExpectedPermissions(ctx, testDependencyTrack, otherTeamResourceName, []string{}), + ), + }, + }, + // CheckDestroy is not practical here since the team is destroyed as well, and we can no longer query its permissions + }) +} + +func testAccTeamPermissionConfigBasic(testDependencyTrack *testutils.TestDependencyTrack, teamName string, permissionName string) string { + return testDependencyTrack.AddProviderConfiguration( + testutils.ComposeConfigs( + fmt.Sprintf(` +resource "dependencytrack_team" "test" { + name = %[1]q +} +`, + teamName, + ), + fmt.Sprintf(` +resource "dependencytrack_team_permission" "test" { + team_id = dependencytrack_team.test.id + name = %[1]q +} +`, + permissionName, + ), + ), + ) +} + +func testAccTeamPermissionConfigOtherTeam(testDependencyTrack *testutils.TestDependencyTrack, teamName string, permissionName string) string { + return testDependencyTrack.AddProviderConfiguration( + testutils.ComposeConfigs( + fmt.Sprintf(` +resource "dependencytrack_team" "test" { + name = %[1]q +} +`, + teamName, + ), + fmt.Sprintf(` +resource "dependencytrack_team" "test-other" { + name = "%[1]s-other" +} +`, + teamName, + ), + fmt.Sprintf(` +resource "dependencytrack_team_permission" "test" { + team_id = dependencytrack_team.test-other.id + name = %[1]q +} +`, + permissionName, + ), + ), + ) +} + +func testAccTeamPermissionConfigNoPermission(testDependencyTrack *testutils.TestDependencyTrack, teamName string) string { + return testDependencyTrack.AddProviderConfiguration( + testutils.ComposeConfigs( + fmt.Sprintf(` +resource "dependencytrack_team" "test" { + name = %[1]q +} +`, + teamName, + ), + fmt.Sprintf(` +resource "dependencytrack_team" "test-other" { + name = "%[1]s-other" +} +`, + teamName, + ), + ), + ) +} diff --git a/internal/provider/teampermission/team_permission_test.go b/internal/provider/teampermission/team_permission_test.go new file mode 100644 index 0000000..6bc7e0e --- /dev/null +++ b/internal/provider/teampermission/team_permission_test.go @@ -0,0 +1,20 @@ +package teampermission_test + +import ( + "github.com/futurice/terraform-provider-dependencytrack/internal/testutils" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "os" + "testing" +) + +var testDependencyTrack *testutils.TestDependencyTrack + +func TestMain(m *testing.M) { + if os.Getenv(resource.EnvTfAcc) != "" { + var cleanup func() + testDependencyTrack, cleanup = testutils.InitTestDependencyTrack() + defer cleanup() + } + + m.Run() +} diff --git a/internal/testutils/projecttestutils/projecttestutils.go b/internal/testutils/projecttestutils/projecttestutils.go new file mode 100644 index 0000000..54788be --- /dev/null +++ b/internal/testutils/projecttestutils/projecttestutils.go @@ -0,0 +1,86 @@ +package projecttestutils + +import ( + "context" + "errors" + "fmt" + dtrack "github.com/futurice/dependency-track-client-go" + "github.com/futurice/terraform-provider-dependencytrack/internal/testutils" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +func TestAccCheckProjectExistsAndHasExpectedData(ctx context.Context, testDependencyTrack *testutils.TestDependencyTrack, resourceName string, expectedProject dtrack.Project) resource.TestCheckFunc { + return TestAccCheckProjectExistsAndHasExpectedLazyData(ctx, testDependencyTrack, resourceName, func() dtrack.Project { return expectedProject }) +} + +func TestAccCheckProjectExistsAndHasExpectedLazyData(ctx context.Context, testDependencyTrack *testutils.TestDependencyTrack, resourceName string, expectedProjectCreator func() dtrack.Project) resource.TestCheckFunc { + return func(state *terraform.State) error { + expectedProject := expectedProjectCreator() + + project, err := FindProjectByResourceName(ctx, testDependencyTrack, state, resourceName) + if err != nil { + return err + } + if project == nil { + return fmt.Errorf("project for resource %s does not exist in Dependency-Track", resourceName) + } + + diff := cmp.Diff(project, &expectedProject, cmpopts.IgnoreFields(dtrack.Project{}, "UUID", "Properties", "Tags", "Metrics")) + if diff != "" { + return fmt.Errorf("project for resource %s is different than expected: %s", resourceName, diff) + } + + return nil + } +} + +func TestAccCheckProjectDoesNotExists(ctx context.Context, testDependencyTrack *testutils.TestDependencyTrack, resourceName string) resource.TestCheckFunc { + return func(state *terraform.State) error { + project, err := FindProjectByResourceName(ctx, testDependencyTrack, state, resourceName) + if err != nil { + return err + } + if project != nil { + return fmt.Errorf("project for resource %s exists in Dependency-Track, even though it shouldn't: %v", resourceName, project) + } + + return nil + } +} + +func FindProjectByResourceName(ctx context.Context, testDependencyTrack *testutils.TestDependencyTrack, state *terraform.State, resourceName string) (*dtrack.Project, error) { + projectID, err := testutils.GetResourceID(state, resourceName) + if err != nil { + return nil, err + } + + project, err := FindProject(ctx, testDependencyTrack, projectID) + if err != nil { + return nil, fmt.Errorf("failed to get project for resource %s: %w", resourceName, err) + } + + return project, nil +} + +func FindProject(ctx context.Context, testDependencyTrack *testutils.TestDependencyTrack, projectID uuid.UUID) (*dtrack.Project, error) { + project, err := testDependencyTrack.Client.Project.Get(ctx, projectID) + if err != nil { + var apiErr *dtrack.APIError + ok := errors.As(err, &apiErr) + if !ok || apiErr.StatusCode != 404 { + return nil, fmt.Errorf("failed to get project from Dependency-Track: %w", err) + } + + return nil, nil + } + + return &project, nil +} + +func CreateProjectResourceName(localName string) string { + return fmt.Sprintf("dependencytrack_project.%s", localName) +} diff --git a/internal/testutils/teamtestutils/teamtestutils.go b/internal/testutils/teamtestutils/teamtestutils.go new file mode 100644 index 0000000..80fa620 --- /dev/null +++ b/internal/testutils/teamtestutils/teamtestutils.go @@ -0,0 +1,125 @@ +package teamtestutils + +import ( + "context" + "errors" + "fmt" + dtrack "github.com/futurice/dependency-track-client-go" + "github.com/futurice/terraform-provider-dependencytrack/internal/testutils" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "slices" +) + +func TestAccCheckTeamExistsAndHasExpectedData(ctx context.Context, testDependencyTrack *testutils.TestDependencyTrack, resourceName string, expectedTeam dtrack.Team) resource.TestCheckFunc { + return TestAccCheckTeamExistsAndHasExpectedLazyData(ctx, testDependencyTrack, resourceName, func() dtrack.Team { return expectedTeam }) +} + +func TestAccCheckTeamExistsAndHasExpectedLazyData(ctx context.Context, testDependencyTrack *testutils.TestDependencyTrack, resourceName string, expectedTeamCreator func() dtrack.Team) resource.TestCheckFunc { + return func(state *terraform.State) error { + expectedTeam := expectedTeamCreator() + + team, err := FindTeamByResourceName(ctx, testDependencyTrack, state, resourceName) + if err != nil { + return err + } + if team == nil { + return fmt.Errorf("team for resource %s does not exist in Dependency-Track", resourceName) + } + + diff := cmp.Diff(team, &expectedTeam, cmpopts.IgnoreFields(dtrack.Team{}, "UUID")) + if diff != "" { + return fmt.Errorf("team for resource %s is different than expected: %s", resourceName, diff) + } + + return nil + } +} + +func TestAccCheckTeamDoesNotExists(ctx context.Context, testDependencyTrack *testutils.TestDependencyTrack, resourceName string) resource.TestCheckFunc { + return func(state *terraform.State) error { + team, err := FindTeamByResourceName(ctx, testDependencyTrack, state, resourceName) + if err != nil { + return err + } + if team != nil { + return fmt.Errorf("team for resource %s exists in Dependency-Track, even though it shouldn't: %v", resourceName, team) + } + + return nil + } +} + +func TestAccCheckTeamHasExpectedPermissions(ctx context.Context, testDependencyTrack *testutils.TestDependencyTrack, resourceName string, expectedPermissions []string) resource.TestCheckFunc { + return func(state *terraform.State) error { + team, err := FindTeamByResourceName(ctx, testDependencyTrack, state, resourceName) + if err != nil { + return err + } + if team == nil { + return fmt.Errorf("team for resource %s does not exist in Dependency-Track", resourceName) + } + + if len(team.Permissions) != len(expectedPermissions) { + return fmt.Errorf("team for resource %s has %d permissions instead of the expected %d", resourceName, len(team.Permissions), len(expectedPermissions)) + } + + actualPermissions := make([]string, len(team.Permissions)) + for i, permission := range team.Permissions { + actualPermissions[i] = permission.Name + } + + for _, expectedPermission := range expectedPermissions { + if !slices.Contains(actualPermissions, expectedPermission) { + return fmt.Errorf("team for resource %s is missing expected permission %s, got [%v]", resourceName, expectedPermission, actualPermissions) + } + } + + return nil + } +} + +func FindTeamByResourceName(ctx context.Context, testDependencyTrack *testutils.TestDependencyTrack, state *terraform.State, resourceName string) (*dtrack.Team, error) { + teamID, err := testutils.GetResourceID(state, resourceName) + if err != nil { + return nil, err + } + + team, err := FindTeam(ctx, testDependencyTrack, teamID) + if err != nil { + return nil, fmt.Errorf("failed to get team for resource %s: %w", resourceName, err) + } + + return team, nil +} + +func FindTeam(ctx context.Context, testDependencyTrack *testutils.TestDependencyTrack, teamID uuid.UUID) (*dtrack.Team, error) { + team, err := testDependencyTrack.Client.Team.Get(ctx, teamID) + if err != nil { + var apiErr *dtrack.APIError + ok := errors.As(err, &apiErr) + if !ok || apiErr.StatusCode != 404 { + return nil, fmt.Errorf("failed to get team from Dependency-Track: %w", err) + } + + return nil, nil + } + + // normalize the returned object not to contain an empty array reference + if len(team.Permissions) == 0 { + team.Permissions = nil + } + + return &team, nil +} + +func CreateTeamResourceName(localName string) string { + return fmt.Sprintf("dependencytrack_team.%s", localName) +} + +func CreateTeamPermissionResourceName(localName string) string { + return fmt.Sprintf("dependencytrack_team_permission.%s", localName) +}