From 61f249fd1de0020d3f323a045b38f8d7b0676c80 Mon Sep 17 00:00:00 2001 From: Michal Dobaczewski Date: Mon, 29 Jul 2024 10:58:41 +0300 Subject: [PATCH 1/3] Add notification rule project test --- ...notification_rule_project_resource_test.go | 238 ++++++++++++++++++ .../notification_rule_test_utils.go | 35 +++ .../teamtestutils/team_test_utils.go | 2 +- 3 files changed, 274 insertions(+), 1 deletion(-) create mode 100644 internal/provider/notificationruleproject/notification_rule_project_resource_test.go diff --git a/internal/provider/notificationruleproject/notification_rule_project_resource_test.go b/internal/provider/notificationruleproject/notification_rule_project_resource_test.go new file mode 100644 index 0000000..963da73 --- /dev/null +++ b/internal/provider/notificationruleproject/notification_rule_project_resource_test.go @@ -0,0 +1,238 @@ +package notificationruleproject_test + +import ( + "fmt" + notificationruletestutils "github.com/futurice/terraform-provider-dependencytrack/internal/testutils/notificationrule" + "os" + "testing" + + "github.com/futurice/terraform-provider-dependencytrack/internal/testutils" + "github.com/futurice/terraform-provider-dependencytrack/internal/testutils/projecttestutils" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +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() +} + +func TestAccNotificationRuleProjectResource_basic(t *testing.T) { + ctx := testutils.CreateTestContext(t) + + publisherName := acctest.RandomWithPrefix("test-notification-publisher") + ruleName := acctest.RandomWithPrefix("test-notification-rule") + projectName := acctest.RandomWithPrefix("test-project") + + ruleResourceName := notificationruletestutils.CreateNotificationRuleResourceName("test") + otherRuleResourceName := notificationruletestutils.CreateNotificationRuleResourceName("test-other") + + projectResourceName := projecttestutils.CreateProjectResourceName("test") + otherProjectResourceName := projecttestutils.CreateProjectResourceName("test-other") + + notificationRuleProjectResourceName := notificationruletestutils.CreateNotificationRuleProjectResourceName("test") + + var ruleID, projectID, otherRuleID, otherProjectID string + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testutils.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: testutils.TestAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccNotificationRuleProjectConfigBasic(testDependencyTrack, publisherName, ruleName, projectName), + Check: resource.ComposeAggregateTestCheckFunc( + testutils.TestAccCheckGetResourceID(ruleResourceName, &ruleID), + testutils.TestAccCheckGetResourceID(projectResourceName, &projectID), + notificationruletestutils.TestAccCheckNotificationRuleHasExpectedProjects(ctx, testDependencyTrack, ruleResourceName, []*string{&projectID}), + resource.TestCheckResourceAttrPtr(notificationRuleProjectResourceName, "rule_id", &ruleID), + resource.TestCheckResourceAttrPtr(notificationRuleProjectResourceName, "project_id", &projectID), + ), + }, + // TODO + //{ + // ResourceName: notificationRuleProjectResourceName, + // ImportState: true, + // ImportStateVerify: true, + //}, + { + Config: testAccNotificationRuleProjectConfigOtherRuleAndProject(testDependencyTrack, publisherName, ruleName, projectName), + Check: resource.ComposeAggregateTestCheckFunc( + testutils.TestAccCheckGetResourceID(otherRuleResourceName, &otherRuleID), + testutils.TestAccCheckGetResourceID(otherProjectResourceName, &otherProjectID), + notificationruletestutils.TestAccCheckNotificationRuleHasExpectedProjects(ctx, testDependencyTrack, ruleResourceName, []*string{}), + notificationruletestutils.TestAccCheckNotificationRuleHasExpectedProjects(ctx, testDependencyTrack, otherRuleResourceName, []*string{&otherProjectID}), + resource.TestCheckResourceAttrPtr(notificationRuleProjectResourceName, "rule_id", &otherRuleID), + resource.TestCheckResourceAttrPtr(notificationRuleProjectResourceName, "project_id", &otherProjectID), + ), + }, + { + Config: testAccNotificationRuleProjectConfigNoProject(testDependencyTrack, publisherName, ruleName, projectName), + Check: resource.ComposeAggregateTestCheckFunc( + notificationruletestutils.TestAccCheckNotificationRuleHasExpectedProjects(ctx, testDependencyTrack, ruleResourceName, []*string{}), + notificationruletestutils.TestAccCheckNotificationRuleHasExpectedProjects(ctx, testDependencyTrack, otherRuleResourceName, []*string{}), + ), + }, + }, + // CheckDestroy is not practical here since the notification rule is destroyed as well, and we can no longer query its projects + }) +} + +func testAccNotificationRuleProjectConfigBasic(testDependencyTrack *testutils.TestDependencyTrack, providerName, ruleName, projectName string) string { + return testDependencyTrack.AddProviderConfiguration( + testutils.ComposeConfigs( + fmt.Sprintf(` +resource "dependencytrack_notification_publisher" "test" { + name = %[1]q + publisher_class = "org.dependencytrack.notification.publisher.SlackPublisher" + template_mime_type = "application/json" + template = "{}" +} +`, + providerName, + ), + fmt.Sprintf(` +resource "dependencytrack_notification_rule" "test" { + name = %[1]q + publisher_id = dependencytrack_notification_publisher.test.id + scope = "PORTFOLIO" + notification_level = "INFORMATIONAL" +} +`, + ruleName, + ), + fmt.Sprintf(` +resource "dependencytrack_project" "test" { + name = %[1]q + classifier = "APPLICATION" +} +`, + projectName, + ), + ` +resource "dependencytrack_notification_rule_project" "test" { + rule_id = dependencytrack_notification_rule.test.id + project_id = dependencytrack_project.test.id +} +`, + ), + ) +} + +func testAccNotificationRuleProjectConfigOtherRuleAndProject(testDependencyTrack *testutils.TestDependencyTrack, providerName, ruleName, projectName string) string { + return testDependencyTrack.AddProviderConfiguration( + testutils.ComposeConfigs( + fmt.Sprintf(` +resource "dependencytrack_notification_publisher" "test" { + name = %[1]q + publisher_class = "org.dependencytrack.notification.publisher.SlackPublisher" + template_mime_type = "application/json" + template = "{}" +} +`, + providerName, + ), + fmt.Sprintf(` +resource "dependencytrack_notification_rule" "test" { + name = %[1]q + publisher_id = dependencytrack_notification_publisher.test.id + scope = "PORTFOLIO" + notification_level = "INFORMATIONAL" +} +`, + ruleName, + ), + fmt.Sprintf(` +resource "dependencytrack_notification_rule" "test-other" { + name = "%[1]s-other" + publisher_id = dependencytrack_notification_publisher.test.id + scope = "PORTFOLIO" + notification_level = "INFORMATIONAL" +} +`, + ruleName, + ), + fmt.Sprintf(` +resource "dependencytrack_project" "test" { + name = %[1]q + classifier = "APPLICATION" +} +`, + projectName, + ), + fmt.Sprintf(` +resource "dependencytrack_project" "test-other" { + name = "%[1]s-other" + classifier = "APPLICATION" +} +`, + projectName, + ), + ` +resource "dependencytrack_notification_rule_project" "test" { + rule_id = dependencytrack_notification_rule.test-other.id + project_id = dependencytrack_project.test-other.id +} +`, + ), + ) +} + +func testAccNotificationRuleProjectConfigNoProject(testDependencyTrack *testutils.TestDependencyTrack, providerName, ruleName, projectName string) string { + return testDependencyTrack.AddProviderConfiguration( + testutils.ComposeConfigs( + fmt.Sprintf(` +resource "dependencytrack_notification_publisher" "test" { + name = %[1]q + publisher_class = "org.dependencytrack.notification.publisher.SlackPublisher" + template_mime_type = "application/json" + template = "{}" +} +`, + providerName, + ), + fmt.Sprintf(` +resource "dependencytrack_notification_rule" "test" { + name = %[1]q + publisher_id = dependencytrack_notification_publisher.test.id + scope = "PORTFOLIO" + notification_level = "INFORMATIONAL" +} +`, + ruleName, + ), + fmt.Sprintf(` +resource "dependencytrack_notification_rule" "test-other" { + name = "%[1]s-other" + publisher_id = dependencytrack_notification_publisher.test.id + scope = "PORTFOLIO" + notification_level = "INFORMATIONAL" +} +`, + ruleName, + ), + fmt.Sprintf(` +resource "dependencytrack_project" "test" { + name = %[1]q + classifier = "APPLICATION" +} +`, + projectName, + ), + fmt.Sprintf(` +resource "dependencytrack_project" "test-other" { + name = "%[1]s-other" + classifier = "APPLICATION" +} +`, + projectName, + ), + ), + ) +} diff --git a/internal/testutils/notificationrule/notification_rule_test_utils.go b/internal/testutils/notificationrule/notification_rule_test_utils.go index b9cf0b7..dc0fae7 100644 --- a/internal/testutils/notificationrule/notification_rule_test_utils.go +++ b/internal/testutils/notificationrule/notification_rule_test_utils.go @@ -3,6 +3,8 @@ package notificationruletestutils import ( "context" "fmt" + "slices" + dtrack "github.com/futurice/dependency-track-client-go" "github.com/futurice/terraform-provider-dependencytrack/internal/testutils" "github.com/google/go-cmp/cmp" @@ -77,6 +79,39 @@ func FindNotificationRule(ctx context.Context, testDependencyTrack *testutils.Te return nil, nil } +func TestAccCheckNotificationRuleHasExpectedProjects(ctx context.Context, testDependencyTrack *testutils.TestDependencyTrack, resourceName string, expectedProjectIDs []*string) resource.TestCheckFunc { + return func(state *terraform.State) error { + rule, err := FindNotificationRuleByResourceName(ctx, testDependencyTrack, state, resourceName) + if err != nil { + return err + } + if rule == nil { + return fmt.Errorf("notification rule for resource %s does not exist in Dependency-Track", resourceName) + } + + if len(rule.Projects) != len(expectedProjectIDs) { + return fmt.Errorf("notification rule for resource %s has %d projects instead of the expected %d", resourceName, len(rule.Projects), len(expectedProjectIDs)) + } + + actualProjectIDs := make([]string, len(rule.Projects)) + for i, project := range rule.Projects { + actualProjectIDs[i] = project.UUID.String() + } + + for _, expectedProjectID := range expectedProjectIDs { + if !slices.Contains(actualProjectIDs, *expectedProjectID) { + return fmt.Errorf("notification rule for resource %s is missing expected project %s, got [%v]", resourceName, *expectedProjectID, actualProjectIDs) + } + } + + return nil + } +} + func CreateNotificationRuleResourceName(localName string) string { return fmt.Sprintf("dependencytrack_notification_rule.%s", localName) } + +func CreateNotificationRuleProjectResourceName(localName string) string { + return fmt.Sprintf("dependencytrack_notification_rule_project.%s", localName) +} diff --git a/internal/testutils/teamtestutils/team_test_utils.go b/internal/testutils/teamtestutils/team_test_utils.go index d1b50c3..3d0ac40 100644 --- a/internal/testutils/teamtestutils/team_test_utils.go +++ b/internal/testutils/teamtestutils/team_test_utils.go @@ -107,7 +107,7 @@ func TestAccCheckTeamHasExpectedACLMappings(ctx context.Context, testDependencyT for _, expectedACLMappingProjectID := range expectedACLMappingProjectIDs { if !slices.Contains(actualACLMappingProjectIDs, *expectedACLMappingProjectID) { - return fmt.Errorf("team for resource %s is missing expected permission %s, got [%v]", resourceName, *expectedACLMappingProjectID, actualACLMappingProjectIDs) + return fmt.Errorf("team for resource %s is missing expected ACL mapping project %s, got [%v]", resourceName, *expectedACLMappingProjectID, actualACLMappingProjectIDs) } } From eb9425c169a287009f5753bead043ff926e84f7d Mon Sep 17 00:00:00 2001 From: Michal Dobaczewski Date: Mon, 29 Jul 2024 12:37:52 +0300 Subject: [PATCH 2/3] Add notification rule project import test --- docs/resources/notification_rule_project.md | 4 ++ .../notification_rule_project_resource.go | 45 ++++++++++++++++--- ...notification_rule_project_resource_test.go | 11 +++-- 3 files changed, 47 insertions(+), 13 deletions(-) diff --git a/docs/resources/notification_rule_project.md b/docs/resources/notification_rule_project.md index d026758..92e1db0 100644 --- a/docs/resources/notification_rule_project.md +++ b/docs/resources/notification_rule_project.md @@ -19,3 +19,7 @@ Notification rule project - `project_id` (String) ID of the project - `rule_id` (String) ID of the notification rule + +### Read-Only + +- `id` (String) Synthetic notification rule project ID in the form of project_id/rule_id diff --git a/internal/provider/notificationruleproject/notification_rule_project_resource.go b/internal/provider/notificationruleproject/notification_rule_project_resource.go index d2a8a8d..d2371d3 100644 --- a/internal/provider/notificationruleproject/notification_rule_project_resource.go +++ b/internal/provider/notificationruleproject/notification_rule_project_resource.go @@ -9,8 +9,8 @@ import ( "strings" dtrack "github.com/futurice/dependency-track-client-go" + "github.com/futurice/terraform-provider-dependencytrack/internal/utils" "github.com/google/uuid" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -34,6 +34,7 @@ type NotificationRuleProjectResource struct { // NotificationRuleProjectResourceModel describes the resource data model. type NotificationRuleProjectResourceModel struct { + ID types.String `tfsdk:"id"` ProjectID types.String `tfsdk:"project_id"` RuleID types.String `tfsdk:"rule_id"` } @@ -61,6 +62,10 @@ func (r *NotificationRuleProjectResource) Schema(ctx context.Context, req resour stringplanmodifier.RequiresReplace(), }, }, + "id": schema.StringAttribute{ + MarkdownDescription: "Synthetic notification rule project ID in the form of project_id/rule_id", + Computed: true, + }, }, } } @@ -85,28 +90,40 @@ func (r *NotificationRuleProjectResource) Configure(ctx context.Context, req res } func (r *NotificationRuleProjectResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - var plan NotificationRuleProjectResourceModel + var plan, state NotificationRuleProjectResourceModel resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + ruleID, ruleIDDiags := utils.ParseAttributeUUID(plan.RuleID.ValueString(), "rule_id") + resp.Diagnostics.Append(ruleIDDiags...) + + projectID, projectIDDiags := utils.ParseAttributeUUID(plan.ProjectID.ValueString(), "project_id") + resp.Diagnostics.Append(projectIDDiags...) if resp.Diagnostics.HasError() { return } - _, err := r.client.Notification.AddProjectToRule(ctx, uuid.MustParse(plan.RuleID.String()), uuid.MustParse(plan.ProjectID.String())) + _, err := r.client.Notification.AddProjectToRule(ctx, ruleID, projectID) if err != nil { - resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create API key, got error: %s", err)) + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create notification rule project, got error: %s", err)) return } - resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + state.ID = types.StringValue(makeNotificationRuleProjectID(ruleID, projectID)) + state.RuleID = types.StringValue(ruleID.String()) + state.ProjectID = types.StringValue(projectID.String()) + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) } func (r *NotificationRuleProjectResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { var state NotificationRuleProjectResourceModel resp.Diagnostics.Append(req.State.Get(ctx, &state)...) - if resp.Diagnostics.HasError() { return } @@ -148,12 +165,21 @@ func (r *NotificationRuleProjectResource) Delete(ctx context.Context, req resour var state NotificationRuleProjectResourceModel resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + ruleID, ruleIDDiags := utils.ParseAttributeUUID(state.RuleID.ValueString(), "rule_id") + resp.Diagnostics.Append(ruleIDDiags...) + + projectID, projectIDDiags := utils.ParseAttributeUUID(state.ProjectID.ValueString(), "project_id") + resp.Diagnostics.Append(projectIDDiags...) if resp.Diagnostics.HasError() { return } - _, err := r.client.Notification.DeleteProjectFromRule(ctx, uuid.MustParse(state.RuleID.ValueString()), uuid.MustParse(state.ProjectID.ValueString())) + _, err := r.client.Notification.DeleteProjectFromRule(ctx, ruleID, projectID) if err != nil { resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete notification rule project relation, got error: %s", err)) return @@ -169,6 +195,11 @@ func (r *NotificationRuleProjectResource) ImportState(ctx context.Context, req r return } + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), req.ID)...) resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), parts[0])...) resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("rule_id"), parts[1])...) } + +func makeNotificationRuleProjectID(ruleID uuid.UUID, projectID uuid.UUID) string { + return fmt.Sprintf("%s/%s", projectID.String(), ruleID.String()) +} diff --git a/internal/provider/notificationruleproject/notification_rule_project_resource_test.go b/internal/provider/notificationruleproject/notification_rule_project_resource_test.go index 963da73..5ea62ed 100644 --- a/internal/provider/notificationruleproject/notification_rule_project_resource_test.go +++ b/internal/provider/notificationruleproject/notification_rule_project_resource_test.go @@ -55,12 +55,11 @@ func TestAccNotificationRuleProjectResource_basic(t *testing.T) { resource.TestCheckResourceAttrPtr(notificationRuleProjectResourceName, "project_id", &projectID), ), }, - // TODO - //{ - // ResourceName: notificationRuleProjectResourceName, - // ImportState: true, - // ImportStateVerify: true, - //}, + { + ResourceName: notificationRuleProjectResourceName, + ImportState: true, + ImportStateVerify: true, + }, { Config: testAccNotificationRuleProjectConfigOtherRuleAndProject(testDependencyTrack, publisherName, ruleName, projectName), Check: resource.ComposeAggregateTestCheckFunc( From 1e556abb972748857cacb05ea31bce57678f6e11 Mon Sep 17 00:00:00 2001 From: Michal Dobaczewski Date: Mon, 29 Jul 2024 12:44:36 +0300 Subject: [PATCH 3/3] Standardise uuid handling in notification rule resource --- .../notification_rule_resource.go | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/internal/provider/notificationrule/notification_rule_resource.go b/internal/provider/notificationrule/notification_rule_resource.go index 2fa2b6d..9834e07 100644 --- a/internal/provider/notificationrule/notification_rule_resource.go +++ b/internal/provider/notificationrule/notification_rule_resource.go @@ -8,8 +8,8 @@ import ( "fmt" dtrack "github.com/futurice/dependency-track-client-go" + "github.com/futurice/terraform-provider-dependencytrack/internal/utils" "github.com/google/uuid" - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" @@ -237,12 +237,17 @@ func (r *NotificationRuleResource) Delete(ctx context.Context, req resource.Dele var state NotificationRuleResourceModel resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + ruleID, ruleIDDiags := utils.ParseAttributeUUID(state.ID.ValueString(), "id") + resp.Diagnostics.Append(ruleIDDiags...) if resp.Diagnostics.HasError() { return } - err := r.client.Notification.DeleteRule(ctx, uuid.MustParse(state.ID.ValueString())) + err := r.client.Notification.DeleteRule(ctx, ruleID) if err != nil { resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete notification rule, got error: %s", err)) return @@ -282,9 +287,14 @@ func DTRuleToTFRule(ctx context.Context, dtRule dtrack.NotificationRule) (Notifi } func TFRuleToDTRule(ctx context.Context, tfRule NotificationRuleResourceModel) (dtrack.NotificationRule, diag.Diagnostics) { + var diags diag.Diagnostics + + publisherID, publisherIDDiags := utils.ParseAttributeUUID(tfRule.PublisherID.ValueString(), "publisher_id") + diags.Append(publisherIDDiags...) + rule := dtrack.NotificationRule{ Name: tfRule.Name.ValueString(), - Publisher: dtrack.NotificationPublisher{UUID: uuid.MustParse(tfRule.PublisherID.ValueString())}, + Publisher: dtrack.NotificationPublisher{UUID: publisherID}, Scope: tfRule.Scope.ValueString(), NotificationLevel: tfRule.NotificationLevel.ValueString(), Enabled: tfRule.Enabled.ValueBool(), @@ -294,16 +304,22 @@ func TFRuleToDTRule(ctx context.Context, tfRule NotificationRuleResourceModel) ( } elements := make([]types.String, 0, len(tfRule.NotifyOn.Elements())) - diags := tfRule.NotifyOn.ElementsAs(ctx, &elements, false) - rule.NotifyOn = make([]string, len(elements)) - for i := range elements { - rule.NotifyOn[i] = elements[i].ValueString() + notifyOnDiags := tfRule.NotifyOn.ElementsAs(ctx, &elements, false) + diags.Append(notifyOnDiags...) + if !notifyOnDiags.HasError() { + rule.NotifyOn = make([]string, len(elements)) + for i := range elements { + rule.NotifyOn[i] = elements[i].ValueString() + } } if tfRule.ID.IsUnknown() { rule.UUID = uuid.Nil } else { - rule.UUID = uuid.MustParse(tfRule.ID.ValueString()) + ruleID, ruleIDDiags := utils.ParseAttributeUUID(tfRule.ID.ValueString(), "id") + diags.Append(ruleIDDiags...) + + rule.UUID = ruleID } return rule, diags