diff --git a/helper_test.go b/helper_test.go index 7d8b508ec..6d7062426 100644 --- a/helper_test.go +++ b/helper_test.go @@ -615,6 +615,55 @@ func createNotificationConfiguration(t *testing.T, client *Client, w *Workspace, } } +func createTeamNotificationConfiguration(t *testing.T, client *Client, team *Team, options *NotificationConfigurationCreateOptions) (*NotificationConfiguration, func()) { + var tCleanup func() + + if team == nil { + team, tCleanup = createTeam(t, client, nil) + } + + // Team notification configurations do not actually require a run task, but we'll + // reuse this as a URL that returns a 200. + runTaskURL := os.Getenv("TFC_RUN_TASK_URL") + if runTaskURL == "" { + t.Error("You must set TFC_RUN_TASK_URL for run task related tests.") + } + + if options == nil { + options = &NotificationConfigurationCreateOptions{ + DestinationType: NotificationDestination(NotificationDestinationTypeGeneric), + Enabled: Bool(false), + Name: String(randomString(t)), + Token: String(randomString(t)), + URL: String(runTaskURL), + Triggers: []NotificationTriggerType{NotificationTriggerChangeRequestCreated}, + SubscribableChoice: &NotificationConfigurationSubscribableChoice{Team: team}, + } + } + + ctx := context.Background() + nc, err := client.NotificationConfigurations.Create( + ctx, + team.ID, + *options, + ) + if err != nil { + t.Fatal(err) + } + + return nc, func() { + if err := client.NotificationConfigurations.Delete(ctx, nc.ID); err != nil { + t.Errorf("Error destroying team notification configuration! WARNING: Dangling\n"+ + "resources may exist! The full error is shown below.\n\n"+ + "NotificationConfiguration: %s\nError: %s", nc.ID, err) + } + + if tCleanup != nil { + tCleanup() + } + } +} + func createPolicySetParameter(t *testing.T, client *Client, ps *PolicySet) (*PolicySetParameter, func()) { var psCleanup func() diff --git a/mocks/notification_configuration_mocks.go b/mocks/notification_configuration_mocks.go index c34212e83..5fb999a40 100644 --- a/mocks/notification_configuration_mocks.go +++ b/mocks/notification_configuration_mocks.go @@ -41,18 +41,18 @@ func (m *MockNotificationConfigurations) EXPECT() *MockNotificationConfiguration } // Create mocks base method. -func (m *MockNotificationConfigurations) Create(ctx context.Context, workspaceID string, options tfe.NotificationConfigurationCreateOptions) (*tfe.NotificationConfiguration, error) { +func (m *MockNotificationConfigurations) Create(ctx context.Context, subscribableID string, options tfe.NotificationConfigurationCreateOptions) (*tfe.NotificationConfiguration, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Create", ctx, workspaceID, options) + ret := m.ctrl.Call(m, "Create", ctx, subscribableID, options) ret0, _ := ret[0].(*tfe.NotificationConfiguration) ret1, _ := ret[1].(error) return ret0, ret1 } // Create indicates an expected call of Create. -func (mr *MockNotificationConfigurationsMockRecorder) Create(ctx, workspaceID, options any) *gomock.Call { +func (mr *MockNotificationConfigurationsMockRecorder) Create(ctx, subscribableID, options any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockNotificationConfigurations)(nil).Create), ctx, workspaceID, options) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockNotificationConfigurations)(nil).Create), ctx, subscribableID, options) } // Delete mocks base method. @@ -70,18 +70,18 @@ func (mr *MockNotificationConfigurationsMockRecorder) Delete(ctx, notificationCo } // List mocks base method. -func (m *MockNotificationConfigurations) List(ctx context.Context, workspaceID string, options *tfe.NotificationConfigurationListOptions) (*tfe.NotificationConfigurationList, error) { +func (m *MockNotificationConfigurations) List(ctx context.Context, subscribableID string, options *tfe.NotificationConfigurationListOptions) (*tfe.NotificationConfigurationList, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "List", ctx, workspaceID, options) + ret := m.ctrl.Call(m, "List", ctx, subscribableID, options) ret0, _ := ret[0].(*tfe.NotificationConfigurationList) ret1, _ := ret[1].(error) return ret0, ret1 } // List indicates an expected call of List. -func (mr *MockNotificationConfigurationsMockRecorder) List(ctx, workspaceID, options any) *gomock.Call { +func (mr *MockNotificationConfigurationsMockRecorder) List(ctx, subscribableID, options any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockNotificationConfigurations)(nil).List), ctx, workspaceID, options) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockNotificationConfigurations)(nil).List), ctx, subscribableID, options) } // Read mocks base method. diff --git a/notification_configuration.go b/notification_configuration.go index aeac2f04e..ad6c8dfae 100644 --- a/notification_configuration.go +++ b/notification_configuration.go @@ -20,10 +20,10 @@ var _ NotificationConfigurations = (*notificationConfigurations)(nil) // https://developer.hashicorp.com/terraform/cloud-docs/api-docs/notification-configurations type NotificationConfigurations interface { // List all the notification configurations within a workspace. - List(ctx context.Context, workspaceID string, options *NotificationConfigurationListOptions) (*NotificationConfigurationList, error) + List(ctx context.Context, subscribableID string, options *NotificationConfigurationListOptions) (*NotificationConfigurationList, error) // Create a new notification configuration with the given options. - Create(ctx context.Context, workspaceID string, options NotificationConfigurationCreateOptions) (*NotificationConfiguration, error) + Create(ctx context.Context, subscribableID string, options NotificationConfigurationCreateOptions) (*NotificationConfiguration, error) // Read a notification configuration by its ID. Read(ctx context.Context, notificationConfigurationID string) (*NotificationConfiguration, error) @@ -59,6 +59,7 @@ const ( NotificationTriggerAssessmentCheckFailed NotificationTriggerType = "assessment:check_failure" NotificationTriggerWorkspaceAutoDestroyReminder NotificationTriggerType = "workspace:auto_destroy_reminder" NotificationTriggerWorkspaceAutoDestroyRunResults NotificationTriggerType = "workspace:auto_destroy_run_results" + NotificationTriggerChangeRequestCreated NotificationTriggerType = "change_request:created" ) // NotificationDestinationType represents the destination type of the @@ -80,6 +81,14 @@ type NotificationConfigurationList struct { Items []*NotificationConfiguration } +// NotificationConfigurationSubscribableChoice is a choice type struct that represents the possible values +// within a polymorphic relation. If a value is available, exactly one field +// will be non-nil. +type NotificationConfigurationSubscribableChoice struct { + Team *Team + Workspace *Workspace +} + // NotificationConfiguration represents a Notification Configuration. type NotificationConfiguration struct { ID string `jsonapi:"primary,notification-configurations"` @@ -97,8 +106,11 @@ type NotificationConfiguration struct { EmailAddresses []string `jsonapi:"attr,email-addresses"` // Relations - Subscribable *Workspace `jsonapi:"relation,subscribable"` - EmailUsers []*User `jsonapi:"relation,users"` + // DEPRECATED. The subscribable field is polymorphic. Use NotificationConfigurationSubscribableChoice instead. + Subscribable *Workspace `jsonapi:"relation,subscribable,omitempty"` + SubscribableChoice *NotificationConfigurationSubscribableChoice `jsonapi:"polyrelation,subscribable"` + + EmailUsers []*User `jsonapi:"relation,users"` } // DeliveryResponse represents a notification configuration delivery response. @@ -115,6 +127,8 @@ type DeliveryResponse struct { // notification configurations. type NotificationConfigurationListOptions struct { ListOptions + + SubscribableChoice *NotificationConfigurationSubscribableChoice } // NotificationConfigurationCreateOptions represents the options for @@ -150,6 +164,9 @@ type NotificationConfigurationCreateOptions struct { // Optional: The list of users belonging to the organization that will receive notification emails. EmailUsers []*User `jsonapi:"relation,users,omitempty"` + + // Required: The workspace or team that the notification configuration is associated with. + SubscribableChoice *NotificationConfigurationSubscribableChoice `jsonapi:"polyrelation,subscribable,omitempty"` } // NotificationConfigurationUpdateOptions represents the options for @@ -185,12 +202,32 @@ type NotificationConfigurationUpdateOptions struct { } // List all the notification configurations associated with a workspace. -func (s *notificationConfigurations) List(ctx context.Context, workspaceID string, options *NotificationConfigurationListOptions) (*NotificationConfigurationList, error) { - if !validStringID(&workspaceID) { - return nil, ErrInvalidWorkspaceID +func (s *notificationConfigurations) List(ctx context.Context, subscribableID string, options *NotificationConfigurationListOptions) (*NotificationConfigurationList, error) { + var u string + if options == nil { + options = &NotificationConfigurationListOptions{ + SubscribableChoice: &NotificationConfigurationSubscribableChoice{ + Workspace: &Workspace{ID: subscribableID}, + }, + } + } else if options.SubscribableChoice == nil { + options.SubscribableChoice = &NotificationConfigurationSubscribableChoice{ + Workspace: &Workspace{ID: subscribableID}, + } + } + + if options.SubscribableChoice.Team != nil { + if !validStringID(&subscribableID) { + return nil, ErrInvalidTeamID + } + u = fmt.Sprintf("teams/%s/notification-configurations", url.PathEscape(subscribableID)) + } else { + if !validStringID(&subscribableID) { + return nil, ErrInvalidWorkspaceID + } + u = fmt.Sprintf("workspaces/%s/notification-configurations", url.PathEscape(subscribableID)) } - u := fmt.Sprintf("workspaces/%s/notification-configurations", url.PathEscape(workspaceID)) req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err @@ -202,30 +239,43 @@ func (s *notificationConfigurations) List(ctx context.Context, workspaceID strin return nil, err } + for i := range ncl.Items { + backfillDeprecatedSubscribable(ncl.Items[i]) + } + return ncl, nil } // Create a notification configuration with the given options. -func (s *notificationConfigurations) Create(ctx context.Context, workspaceID string, options NotificationConfigurationCreateOptions) (*NotificationConfiguration, error) { - if !validStringID(&workspaceID) { - return nil, ErrInvalidWorkspaceID +func (s *notificationConfigurations) Create(ctx context.Context, subscribableID string, options NotificationConfigurationCreateOptions) (*NotificationConfiguration, error) { + var u string + var subscribableChoice *NotificationConfigurationSubscribableChoice + if options.SubscribableChoice == nil || options.SubscribableChoice.Team == nil { + u = fmt.Sprintf("workspaces/%s/notification-configurations", url.PathEscape(subscribableID)) + options.SubscribableChoice = &NotificationConfigurationSubscribableChoice{Workspace: &Workspace{ID: subscribableID}} + } else { + u = fmt.Sprintf("teams/%s/notification-configurations", url.PathEscape(subscribableID)) + options.SubscribableChoice = &NotificationConfigurationSubscribableChoice{Team: &Team{ID: subscribableID}} } + if err := options.valid(); err != nil { return nil, err } - u := fmt.Sprintf("workspaces/%s/notification-configurations", url.PathEscape(workspaceID)) req, err := s.client.NewRequest("POST", u, &options) if err != nil { return nil, err } - nc := &NotificationConfiguration{} + nc := &NotificationConfiguration{SubscribableChoice: subscribableChoice} err = req.Do(ctx, nc) + if err != nil { return nil, err } + backfillDeprecatedSubscribable(nc) + return nc, nil } @@ -247,6 +297,8 @@ func (s *notificationConfigurations) Read(ctx context.Context, notificationConfi return nil, err } + backfillDeprecatedSubscribable(nc) + return nc, nil } @@ -272,6 +324,8 @@ func (s *notificationConfigurations) Update(ctx context.Context, notificationCon return nil, err } + backfillDeprecatedSubscribable(nc) + return nc, nil } @@ -314,6 +368,16 @@ func (s *notificationConfigurations) Verify(ctx context.Context, notificationCon } func (o NotificationConfigurationCreateOptions) valid() error { + if o.SubscribableChoice == nil || o.SubscribableChoice.Workspace != nil { + if !validStringID(&o.SubscribableChoice.Workspace.ID) { + return ErrInvalidWorkspaceID + } + } else { + if !validStringID(&o.SubscribableChoice.Team.ID) { + return ErrInvalidTeamID + } + } + if o.DestinationType == nil { return ErrRequiredDestinationType } @@ -350,6 +414,16 @@ func (o NotificationConfigurationUpdateOptions) valid() error { return nil } +func backfillDeprecatedSubscribable(notification *NotificationConfiguration) { + if notification.Subscribable != nil || notification.SubscribableChoice == nil { + return + } + + if notification.SubscribableChoice.Workspace != nil { + notification.Subscribable = notification.SubscribableChoice.Workspace + } +} + func validNotificationTriggerType(triggers []NotificationTriggerType) bool { for _, t := range triggers { switch t { @@ -363,6 +437,7 @@ func validNotificationTriggerType(triggers []NotificationTriggerType) bool { NotificationTriggerAssessmentFailed, NotificationTriggerWorkspaceAutoDestroyReminder, NotificationTriggerWorkspaceAutoDestroyRunResults, + NotificationTriggerChangeRequestCreated, NotificationTriggerAssessmentCheckFailed: continue default: diff --git a/notification_configuration_integration_test.go b/notification_configuration_integration_test.go index 927c08212..d81ede21d 100644 --- a/notification_configuration_integration_test.go +++ b/notification_configuration_integration_test.go @@ -34,13 +34,16 @@ func TestNotificationConfigurationList(t *testing.T) { assert.Contains(t, ncl.Items, ncTest1) assert.Contains(t, ncl.Items, ncTest2) - t.Skip("paging not supported yet in API") - assert.Equal(t, 1, ncl.CurrentPage) - assert.Equal(t, 2, ncl.TotalCount) + assert.Equal(t, 0, ncl.CurrentPage) + assert.Equal(t, 0, ncl.TotalCount) + + assert.NotNil(t, ncl.Items[0].Subscribable) + assert.NotEmpty(t, ncl.Items[0].Subscribable) + assert.NotNil(t, ncl.Items[0].SubscribableChoice.Workspace) + assert.NotEmpty(t, ncl.Items[0].SubscribableChoice.Workspace) }) t.Run("with list options", func(t *testing.T) { - t.Skip("paging not supported yet in API") // Request a page number which is out of range. The result should // be successful, but return no results if the paging options are // properly passed along. @@ -71,6 +74,55 @@ func TestNotificationConfigurationList(t *testing.T) { }) } +func TestNotificationConfigurationList_forTeams(t *testing.T) { + skipUnlessBeta(t) + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + t.Cleanup(orgTestCleanup) + + newSubscriptionUpdater(orgTest).WithPlusEntitlementPlan().Update(t) + + tmTest, tmTestCleanup := createTeam(t, client, orgTest) + t.Cleanup(tmTestCleanup) + require.NotNil(t, tmTest) + + ncTest1, ncTestCleanup1 := createTeamNotificationConfiguration(t, client, tmTest, nil) + t.Cleanup(ncTestCleanup1) + ncTest2, ncTestCleanup2 := createTeamNotificationConfiguration(t, client, tmTest, nil) + t.Cleanup(ncTestCleanup2) + + t.Run("with a valid team", func(t *testing.T) { + ncl, err := client.NotificationConfigurations.List( + ctx, + tmTest.ID, + &NotificationConfigurationListOptions{ + SubscribableChoice: &NotificationConfigurationSubscribableChoice{ + Team: tmTest, + }, + }, + ) + require.NoError(t, err) + assert.Contains(t, ncl.Items, ncTest1) + assert.Contains(t, ncl.Items, ncTest2) + }) + + t.Run("without a valid team", func(t *testing.T) { + ncl, err := client.NotificationConfigurations.List( + ctx, + badIdentifier, + &NotificationConfigurationListOptions{ + SubscribableChoice: &NotificationConfigurationSubscribableChoice{ + Team: tmTest, + }, + }, + ) + assert.Nil(t, ncl) + assert.EqualError(t, err, ErrInvalidTeamID.Error()) + }) +} + func TestNotificationConfigurationCreate(t *testing.T) { client := testClient(t) ctx := context.Background() @@ -255,6 +307,156 @@ func TestNotificationConfigurationsCreate_byType(t *testing.T) { } } +func TestNotificationConfigurationCreate_forTeams(t *testing.T) { + skipUnlessBeta(t) + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + t.Cleanup(orgTestCleanup) + + newSubscriptionUpdater(orgTest).WithPlusEntitlementPlan().Update(t) + + tmTest, tmTestCleanup := createTeam(t, client, orgTest) + t.Cleanup(tmTestCleanup) + + // Create user to use when testing email destination type + orgMemberTest, orgMemberTestCleanup := createOrganizationMembership(t, client, orgTest) + t.Cleanup(orgMemberTestCleanup) + + // Add user to team + options := TeamMemberAddOptions{ + OrganizationMembershipIDs: []string{orgMemberTest.ID}, + } + err := client.TeamMembers.Add(ctx, tmTest.ID, options) + require.NoError(t, err) + + t.Run("with all required values", func(t *testing.T) { + options := NotificationConfigurationCreateOptions{ + DestinationType: NotificationDestination(NotificationDestinationTypeGeneric), + Enabled: Bool(false), + Name: String(randomString(t)), + Token: String(randomString(t)), + URL: String("http://example.com"), + Triggers: []NotificationTriggerType{NotificationTriggerChangeRequestCreated}, + SubscribableChoice: &NotificationConfigurationSubscribableChoice{Team: tmTest}, + } + nc, err := client.NotificationConfigurations.Create(ctx, tmTest.ID, options) + + require.NoError(t, err) + require.NotNil(t, nc) + }) + + t.Run("without a required value", func(t *testing.T) { + options := NotificationConfigurationCreateOptions{ + DestinationType: NotificationDestination(NotificationDestinationTypeGeneric), + Enabled: Bool(false), + Token: String(randomString(t)), + URL: String("http://example.com"), + Triggers: []NotificationTriggerType{NotificationTriggerChangeRequestCreated}, + SubscribableChoice: &NotificationConfigurationSubscribableChoice{Team: tmTest}, + } + nc, err := client.NotificationConfigurations.Create(ctx, tmTest.ID, options) + + assert.Nil(t, nc) + assert.EqualError(t, err, ErrRequiredName.Error()) + }) + + t.Run("without a required value URL when destination type is generic", func(t *testing.T) { + options := NotificationConfigurationCreateOptions{ + DestinationType: NotificationDestination(NotificationDestinationTypeGeneric), + Enabled: Bool(false), + Name: String(randomString(t)), + Token: String(randomString(t)), + Triggers: []NotificationTriggerType{NotificationTriggerChangeRequestCreated}, + SubscribableChoice: &NotificationConfigurationSubscribableChoice{Team: tmTest}, + } + + nc, err := client.NotificationConfigurations.Create(ctx, tmTest.ID, options) + assert.Nil(t, nc) + assert.Equal(t, err, ErrRequiredURL) + }) + + t.Run("without a required value URL when destination type is slack", func(t *testing.T) { + options := NotificationConfigurationCreateOptions{ + DestinationType: NotificationDestination(NotificationDestinationTypeSlack), + Enabled: Bool(false), + Name: String(randomString(t)), + Triggers: []NotificationTriggerType{NotificationTriggerChangeRequestCreated}, + SubscribableChoice: &NotificationConfigurationSubscribableChoice{Team: tmTest}, + } + + nc, err := client.NotificationConfigurations.Create(ctx, tmTest.ID, options) + assert.Nil(t, nc) + assert.Equal(t, err, ErrRequiredURL) + }) + + t.Run("without a required value URL when destination type is MS Teams", func(t *testing.T) { + options := NotificationConfigurationCreateOptions{ + DestinationType: NotificationDestination(NotificationDestinationTypeMicrosoftTeams), + Enabled: Bool(false), + Name: String(randomString(t)), + Triggers: []NotificationTriggerType{NotificationTriggerChangeRequestCreated}, + SubscribableChoice: &NotificationConfigurationSubscribableChoice{Team: tmTest}, + } + + nc, err := client.NotificationConfigurations.Create(ctx, tmTest.ID, options) + assert.Nil(t, nc) + assert.Equal(t, err, ErrRequiredURL) + }) + + t.Run("without a valid team", func(t *testing.T) { + nc, err := client.NotificationConfigurations.Create(ctx, badIdentifier, NotificationConfigurationCreateOptions{ + SubscribableChoice: &NotificationConfigurationSubscribableChoice{ + Team: tmTest, + }, + }) + assert.Nil(t, nc) + assert.EqualError(t, err, ErrInvalidTeamID.Error()) + }) + + t.Run("with an invalid notification trigger", func(t *testing.T) { + options := NotificationConfigurationCreateOptions{ + DestinationType: NotificationDestination(NotificationDestinationTypeGeneric), + Enabled: Bool(false), + Name: String(randomString(t)), + Token: String(randomString(t)), + URL: String("http://example.com"), + Triggers: []NotificationTriggerType{"the beacons of gondor are lit"}, + SubscribableChoice: &NotificationConfigurationSubscribableChoice{Team: tmTest}, + } + + nc, err := client.NotificationConfigurations.Create(ctx, tmTest.ID, options) + assert.Nil(t, nc) + assert.EqualError(t, err, ErrInvalidNotificationTrigger.Error()) + }) + + t.Run("with email users when destination type is email", func(t *testing.T) { + options := NotificationConfigurationCreateOptions{ + DestinationType: NotificationDestination(NotificationDestinationTypeEmail), + Enabled: Bool(false), + Name: String(randomString(t)), + EmailUsers: []*User{orgMemberTest.User}, + SubscribableChoice: &NotificationConfigurationSubscribableChoice{Team: tmTest}, + } + + _, err := client.NotificationConfigurations.Create(ctx, tmTest.ID, options) + require.NoError(t, err) + }) + + t.Run("without email users when destination type is email", func(t *testing.T) { + options := NotificationConfigurationCreateOptions{ + DestinationType: NotificationDestination(NotificationDestinationTypeEmail), + Enabled: Bool(false), + Name: String(randomString(t)), + SubscribableChoice: &NotificationConfigurationSubscribableChoice{Team: tmTest}, + } + + _, err := client.NotificationConfigurations.Create(ctx, tmTest.ID, options) + require.NoError(t, err) + }) +} + func TestNotificationConfigurationRead(t *testing.T) { client := testClient(t) ctx := context.Background() @@ -279,6 +481,149 @@ func TestNotificationConfigurationRead(t *testing.T) { }) } +func TestNotificationConfigurationRead_forTeams(t *testing.T) { + skipUnlessBeta(t) + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + t.Cleanup(orgTestCleanup) + + newSubscriptionUpdater(orgTest).WithPlusEntitlementPlan().Update(t) + + tmTest, tmTestCleanup := createTeam(t, client, orgTest) + t.Cleanup(tmTestCleanup) + + ncTest, ncTestCleanup := createTeamNotificationConfiguration(t, client, tmTest, nil) + t.Cleanup(ncTestCleanup) + + t.Run("with a valid ID", func(t *testing.T) { + nc, err := client.NotificationConfigurations.Read(ctx, ncTest.ID) + require.NoError(t, err) + assert.Equal(t, ncTest.ID, nc.ID) + }) + + t.Run("when the notification configuration does not exist", func(t *testing.T) { + _, err := client.NotificationConfigurations.Read(ctx, "nonexisting") + assert.Equal(t, err, ErrResourceNotFound) + }) + + t.Run("when the notification configuration ID is invalid", func(t *testing.T) { + _, err := client.NotificationConfigurations.Read(ctx, badIdentifier) + assert.Equal(t, err, ErrInvalidNotificationConfigID) + }) +} + +func TestNotificationConfigurationUpdate_forTeams(t *testing.T) { + skipUnlessBeta(t) + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + t.Cleanup(orgTestCleanup) + + newSubscriptionUpdater(orgTest).WithPlusEntitlementPlan().Update(t) + + tmTest, tmTestCleanup := createTeam(t, client, orgTest) + t.Cleanup(tmTestCleanup) + + ncTest, ncTestCleanup := createTeamNotificationConfiguration(t, client, tmTest, nil) + t.Cleanup(ncTestCleanup) + + // Create users to use when testing email destination type + orgMemberTest1, orgMemberTest1Cleanup := createOrganizationMembership(t, client, orgTest) + defer orgMemberTest1Cleanup() + orgMemberTest2, orgMemberTest2Cleanup := createOrganizationMembership(t, client, orgTest) + defer orgMemberTest2Cleanup() + + orgMemberTest1.User = &User{ID: orgMemberTest1.User.ID} + orgMemberTest2.User = &User{ID: orgMemberTest2.User.ID} + + // Add users to team + for _, orgMember := range []*OrganizationMembership{orgMemberTest1, orgMemberTest2} { + options := TeamMemberAddOptions{ + OrganizationMembershipIDs: []string{orgMember.ID}, + } + err := client.TeamMembers.Add(ctx, tmTest.ID, options) + require.NoError(t, err) + } + + options := &NotificationConfigurationCreateOptions{ + DestinationType: NotificationDestination(NotificationDestinationTypeEmail), + Enabled: Bool(false), + Name: String(randomString(t)), + EmailUsers: []*User{orgMemberTest1.User}, + SubscribableChoice: &NotificationConfigurationSubscribableChoice{Team: tmTest}, + } + ncEmailTest, ncEmailTestCleanup := createTeamNotificationConfiguration(t, client, tmTest, options) + t.Cleanup(ncEmailTestCleanup) + + t.Run("with options", func(t *testing.T) { + options := NotificationConfigurationUpdateOptions{ + Enabled: Bool(true), + Name: String("newName"), + } + + nc, err := client.NotificationConfigurations.Update(ctx, ncTest.ID, options) + require.NoError(t, err) + assert.Equal(t, nc.Enabled, true) + assert.Equal(t, nc.Name, "newName") + }) + + t.Run("with invalid notification trigger", func(t *testing.T) { + options := NotificationConfigurationUpdateOptions{ + Triggers: []NotificationTriggerType{"fly you fools!"}, + } + + nc, err := client.NotificationConfigurations.Update(ctx, ncTest.ID, options) + assert.Nil(t, nc) + assert.EqualError(t, err, ErrInvalidNotificationTrigger.Error()) + }) + + t.Run("with email users when destination type is email", func(t *testing.T) { + options := NotificationConfigurationUpdateOptions{ + Enabled: Bool(true), + Name: String("newName"), + EmailUsers: []*User{orgMemberTest1.User, orgMemberTest2.User}, + } + + nc, err := client.NotificationConfigurations.Update(ctx, ncEmailTest.ID, options) + require.NoError(t, err) + assert.Equal(t, nc.Enabled, true) + assert.Equal(t, nc.Name, "newName") + assert.Contains(t, nc.EmailUsers, orgMemberTest1.User) + assert.Contains(t, nc.EmailUsers, orgMemberTest2.User) + }) + + t.Run("without email users when destination type is email", func(t *testing.T) { + options := NotificationConfigurationUpdateOptions{ + Enabled: Bool(true), + Name: String("newName"), + } + + nc, err := client.NotificationConfigurations.Update(ctx, ncEmailTest.ID, options) + require.NoError(t, err) + assert.Equal(t, nc.Enabled, true) + assert.Equal(t, nc.Name, "newName") + assert.Empty(t, nc.EmailUsers) + }) + + t.Run("without options", func(t *testing.T) { + _, err := client.NotificationConfigurations.Update(ctx, ncTest.ID, NotificationConfigurationUpdateOptions{}) + require.NoError(t, err) + }) + + t.Run("when the notification configuration does not exist", func(t *testing.T) { + _, err := client.NotificationConfigurations.Update(ctx, "nonexisting", NotificationConfigurationUpdateOptions{}) + assert.Equal(t, err, ErrResourceNotFound) + }) + + t.Run("when the notification configuration ID is invalid", func(t *testing.T) { + _, err := client.NotificationConfigurations.Update(ctx, badIdentifier, NotificationConfigurationUpdateOptions{}) + assert.Equal(t, err, ErrInvalidNotificationConfigID) + }) +} + func TestNotificationConfigurationUpdate(t *testing.T) { client := testClient(t) ctx := context.Background() @@ -404,6 +749,40 @@ func TestNotificationConfigurationDelete(t *testing.T) { }) } +func TestNotificationConfigurationDelete_forTeams(t *testing.T) { + skipUnlessBeta(t) + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + t.Cleanup(orgTestCleanup) + + newSubscriptionUpdater(orgTest).WithPlusEntitlementPlan().Update(t) + + tmTest, tmTestCleanup := createTeam(t, client, orgTest) + t.Cleanup(tmTestCleanup) + + ncTest, _ := createTeamNotificationConfiguration(t, client, tmTest, nil) + + t.Run("with a valid ID", func(t *testing.T) { + err := client.NotificationConfigurations.Delete(ctx, ncTest.ID) + require.NoError(t, err) + + _, err = client.NotificationConfigurations.Read(ctx, ncTest.ID) + assert.Equal(t, err, ErrResourceNotFound) + }) + + t.Run("when the notification configuration does not exist", func(t *testing.T) { + err := client.NotificationConfigurations.Delete(ctx, "nonexisting") + assert.Equal(t, err, ErrResourceNotFound) + }) + + t.Run("when the notification configuration ID is invalid", func(t *testing.T) { + err := client.NotificationConfigurations.Delete(ctx, badIdentifier) + assert.Equal(t, err, ErrInvalidNotificationConfigID) + }) +} + func TestNotificationConfigurationVerify(t *testing.T) { client := testClient(t) ctx := context.Background() @@ -426,3 +805,35 @@ func TestNotificationConfigurationVerify(t *testing.T) { assert.Equal(t, err, ErrInvalidNotificationConfigID) }) } + +func TestNotificationConfigurationVerify_forTeams(t *testing.T) { + skipUnlessBeta(t) + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + t.Cleanup(orgTestCleanup) + + newSubscriptionUpdater(orgTest).WithPlusEntitlementPlan().Update(t) + + tmTest, tmTestCleanup := createTeam(t, client, orgTest) + t.Cleanup(tmTestCleanup) + + ncTest, ncTestCleanup := createTeamNotificationConfiguration(t, client, tmTest, nil) + t.Cleanup(ncTestCleanup) + + t.Run("with a valid ID", func(t *testing.T) { + _, err := client.NotificationConfigurations.Verify(ctx, ncTest.ID) + require.NoError(t, err) + }) + + t.Run("when the notification configuration does not exists", func(t *testing.T) { + _, err := client.NotificationConfigurations.Verify(ctx, "nonexisting") + assert.Equal(t, err, ErrResourceNotFound) + }) + + t.Run("when the notification configuration ID is invalid", func(t *testing.T) { + _, err := client.NotificationConfigurations.Verify(ctx, badIdentifier) + assert.Equal(t, err, ErrInvalidNotificationConfigID) + }) +}