From 87e4befbfeb01f83624c9ee9e689591299b1aabe Mon Sep 17 00:00:00 2001 From: Josh Freda Date: Thu, 5 Dec 2024 11:34:42 -0600 Subject: [PATCH 01/10] Add support for team notification configurations --- helper_test.go | 48 ++ notification_configuration.go | 1 + team_notification_configuration.go | 327 +++++++++++++ ...fication_configuration_integration_test.go | 457 ++++++++++++++++++ tfe.go | 130 ++--- 5 files changed, 899 insertions(+), 64 deletions(-) create mode 100644 team_notification_configuration.go create mode 100644 team_notification_configuration_integration_test.go diff --git a/helper_test.go b/helper_test.go index 7d8b508ec..9e82d54d4 100644 --- a/helper_test.go +++ b/helper_test.go @@ -615,6 +615,54 @@ func createNotificationConfiguration(t *testing.T, client *Client, w *Workspace, } } +func createTeamNotificationConfiguration(t *testing.T, client *Client, team *Team, options *TeamNotificationConfigurationCreateOptions) (*TeamNotificationConfiguration, 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 = &TeamNotificationConfigurationCreateOptions{ + DestinationType: NotificationDestination(NotificationDestinationTypeGeneric), + Enabled: Bool(false), + Name: String(randomString(t)), + Token: String(randomString(t)), + URL: String(runTaskURL), + Triggers: []NotificationTriggerType{NotificationTriggerChangeRequestCreated}, + } + } + + ctx := context.Background() + nc, err := client.TeamNotificationConfigurations.Create( + ctx, + team.ID, + *options, + ) + if err != nil { + t.Fatal(err) + } + + return nc, func() { + if err := client.TeamNotificationConfigurations.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"+ + "TeamNotificationConfiguration: %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/notification_configuration.go b/notification_configuration.go index aeac2f04e..ece867477 100644 --- a/notification_configuration.go +++ b/notification_configuration.go @@ -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 diff --git a/team_notification_configuration.go b/team_notification_configuration.go new file mode 100644 index 000000000..68b86319f --- /dev/null +++ b/team_notification_configuration.go @@ -0,0 +1,327 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfe + +import ( + "context" + "fmt" + "net/url" + "time" +) + +// Compile-time proof of interface implementation. +var _ TeamNotificationConfigurations = (*teamNotificationConfigurations)(nil) + +// TeamNotificationConfigurations describes all the Team Notification Configuration +// related methods that the Terraform Enterprise API supports. +// +// TFE API docs: +// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/notification-configurations#team-notification-configuration +type TeamNotificationConfigurations interface { + // List all the notification configurations within a team. + List(ctx context.Context, teamID string, options *TeamNotificationConfigurationListOptions) (*TeamNotificationConfigurationList, error) + + // Create a new team notification configuration with the given options. + Create(ctx context.Context, teamID string, options TeamNotificationConfigurationCreateOptions) (*TeamNotificationConfiguration, error) + + // Read a notification configuration by its ID. + Read(ctx context.Context, teamNotificationConfigurationID string) (*TeamNotificationConfiguration, error) + + // Update an existing team notification configuration. + Update(ctx context.Context, teamNotificationConfigurationID string, options TeamNotificationConfigurationUpdateOptions) (*TeamNotificationConfiguration, error) + + // Delete a team notification configuration by its ID. + Delete(ctx context.Context, teamNotificationConfigurationID string) error + + // Verify a team notification configuration by its ID. + Verify(ctx context.Context, teamNotificationConfigurationID string) (*TeamNotificationConfiguration, error) +} + +// teamNotificationConfigurations implements TeamNotificationConfigurations. +type teamNotificationConfigurations struct { + client *Client +} + +// TeamNotificationConfigurationList represents a list of team notification +// configurations. +type TeamNotificationConfigurationList struct { + *Pagination + Items []*TeamNotificationConfiguration +} + +// TeamNotificationConfiguration represents a team notification configuration. +type TeamNotificationConfiguration struct { + ID string `jsonapi:"primary,notification-configurations"` + CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"` + DeliveryResponses []*DeliveryResponse `jsonapi:"attr,delivery-responses"` + DestinationType NotificationDestinationType `jsonapi:"attr,destination-type"` + Enabled bool `jsonapi:"attr,enabled"` + Name string `jsonapi:"attr,name"` + Token string `jsonapi:"attr,token"` + Triggers []string `jsonapi:"attr,triggers"` + UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"` + URL string `jsonapi:"attr,url"` + + // EmailAddresses is only available for TFE users. It is not available in HCP Terraform. + EmailAddresses []string `jsonapi:"attr,email-addresses"` + + // Relations + Subscribable *Team `jsonapi:"relation,subscribable"` + EmailUsers []*User `jsonapi:"relation,users"` +} + +// TeamNotificationConfigurationListOptions represents the options for listing +// notification configurations. +type TeamNotificationConfigurationListOptions struct { + ListOptions +} + +// TeamNotificationConfigurationCreateOptions represents the options for +// creating a new team notification configuration. +type TeamNotificationConfigurationCreateOptions struct { + // Type is a public field utilized by JSON:API to + // set the resource type via the field tag. + // It is not a user-defined value and does not need to be set. + // https://jsonapi.org/format/#crud-creating + Type string `jsonapi:"primary,notification-configurations"` + + // Required: The destination type of the team notification configuration + DestinationType *NotificationDestinationType `jsonapi:"attr,destination-type"` + + // Required: Whether the team notification configuration should be enabled or not + Enabled *bool `jsonapi:"attr,enabled"` + + // Required: The name of the team notification configuration + Name *string `jsonapi:"attr,name"` + + // Optional: The token of the team notification configuration + Token *string `jsonapi:"attr,token,omitempty"` + + // Optional: The list of events that will trigger team notifications + Triggers []NotificationTriggerType `jsonapi:"attr,triggers,omitempty"` + + // Optional: The URL of the team notification configuration + URL *string `jsonapi:"attr,url,omitempty"` + + // Optional: The list of email addresses that will receive team notification emails. + // EmailAddresses is only available for TFE users. It is not available in HCP Terraform. + EmailAddresses []string `jsonapi:"attr,email-addresses,omitempty"` + + // Optional: The list of users belonging to the organization that will receive + // team notification emails. + EmailUsers []*User `jsonapi:"relation,users,omitempty"` +} + +// TeamNotificationConfigurationUpdateOptions represents the options for +// updating a existing team notification configuration. +type TeamNotificationConfigurationUpdateOptions struct { + // Type is a public field utilized by JSON:API to + // set the resource type via the field tag. + // It is not a user-defined value and does not need to be set. + // https://jsonapi.org/format/#crud-creating + Type string `jsonapi:"primary,notification-configurations"` + + // Optional: Whether the team notification configuration should be enabled or not + Enabled *bool `jsonapi:"attr,enabled,omitempty"` + + // Optional: The name of the team notification configuration + Name *string `jsonapi:"attr,name,omitempty"` + + // Optional: The token of the team notification configuration + Token *string `jsonapi:"attr,token,omitempty"` + + // Optional: The list of events that will trigger team notifications + Triggers []NotificationTriggerType `jsonapi:"attr,triggers,omitempty"` + + // Optional: The URL of the team notification configuration + URL *string `jsonapi:"attr,url,omitempty"` + + // Optional: The list of email addresses that will receive team notification emails. + // EmailAddresses is only available for TFE users. It is not available in HCP Terraform. + EmailAddresses []string `jsonapi:"attr,email-addresses,omitempty"` + + // Optional: The list of users belonging to the organization that will receive + // team notification emails. + EmailUsers []*User `jsonapi:"relation,users,omitempty"` +} + +// List all the notification configurations associated with a team. +func (s *teamNotificationConfigurations) List(ctx context.Context, teamID string, options *TeamNotificationConfigurationListOptions) (*TeamNotificationConfigurationList, error) { + if !validStringID(&teamID) { + return nil, ErrInvalidTeamID + } + + u := fmt.Sprintf("teams/%s/notification-configurations", url.PathEscape(teamID)) + req, err := s.client.NewRequest("GET", u, options) + if err != nil { + return nil, err + } + + ncl := &TeamNotificationConfigurationList{} + err = req.Do(ctx, ncl) + if err != nil { + return nil, err + } + + return ncl, nil +} + +// Create a team notification configuration with the given options. +func (s *teamNotificationConfigurations) Create(ctx context.Context, teamID string, options TeamNotificationConfigurationCreateOptions) (*TeamNotificationConfiguration, error) { + if !validStringID(&teamID) { + return nil, ErrInvalidTeamID + } + if err := options.valid(); err != nil { + return nil, err + } + + u := fmt.Sprintf("teams/%s/notification-configurations", url.PathEscape(teamID)) + req, err := s.client.NewRequest("POST", u, &options) + if err != nil { + return nil, err + } + + nc := &TeamNotificationConfiguration{} + err = req.Do(ctx, nc) + if err != nil { + return nil, err + } + + return nc, nil +} + +// Read a team notification configuration by its ID. +func (s *teamNotificationConfigurations) Read(ctx context.Context, teamNotificationConfigurationID string) (*TeamNotificationConfiguration, error) { + if !validStringID(&teamNotificationConfigurationID) { + return nil, ErrInvalidNotificationConfigID + } + + u := fmt.Sprintf("notification-configurations/%s", url.PathEscape(teamNotificationConfigurationID)) + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, err + } + + nc := &TeamNotificationConfiguration{} + err = req.Do(ctx, nc) + if err != nil { + return nil, err + } + + return nc, nil +} + +// Updates a team notification configuration with the given options. +func (s *teamNotificationConfigurations) Update(ctx context.Context, teamNotificationConfigurationID string, options TeamNotificationConfigurationUpdateOptions) (*TeamNotificationConfiguration, error) { + if !validStringID(&teamNotificationConfigurationID) { + return nil, ErrInvalidNotificationConfigID + } + + if err := options.valid(); err != nil { + return nil, err + } + + u := fmt.Sprintf("notification-configurations/%s", url.PathEscape(teamNotificationConfigurationID)) + req, err := s.client.NewRequest("PATCH", u, &options) + if err != nil { + return nil, err + } + + nc := &TeamNotificationConfiguration{} + err = req.Do(ctx, nc) + if err != nil { + return nil, err + } + + return nc, nil +} + +// Delete a team notification configuration by its ID. +func (s *teamNotificationConfigurations) Delete(ctx context.Context, teamNotificationConfigurationID string) error { + if !validStringID(&teamNotificationConfigurationID) { + return ErrInvalidNotificationConfigID + } + + u := fmt.Sprintf("notification-configurations/%s", url.PathEscape(teamNotificationConfigurationID)) + req, err := s.client.NewRequest("DELETE", u, nil) + if err != nil { + return err + } + + return req.Do(ctx, nil) +} + +// Verify a team notification configuration by delivering a verification payload +// to the configured URL. +func (s *teamNotificationConfigurations) Verify(ctx context.Context, teamNotificationConfigurationID string) (*TeamNotificationConfiguration, error) { + if !validStringID(&teamNotificationConfigurationID) { + return nil, ErrInvalidNotificationConfigID + } + + u := fmt.Sprintf( + "notification-configurations/%s/actions/verify", url.PathEscape(teamNotificationConfigurationID)) + req, err := s.client.NewRequest("POST", u, nil) + if err != nil { + return nil, err + } + + nc := &TeamNotificationConfiguration{} + err = req.Do(ctx, nc) + if err != nil { + return nil, err + } + + return nc, nil +} + +func (o TeamNotificationConfigurationCreateOptions) valid() error { + if o.DestinationType == nil { + return ErrRequiredDestinationType + } + if o.Enabled == nil { + return ErrRequiredEnabled + } + if !validString(o.Name) { + return ErrRequiredName + } + + if !validTeamNotificationTriggerType(o.Triggers) { + return ErrInvalidNotificationTrigger + } + + if *o.DestinationType == NotificationDestinationTypeGeneric || + *o.DestinationType == NotificationDestinationTypeSlack || + *o.DestinationType == NotificationDestinationTypeMicrosoftTeams { + if o.URL == nil { + return ErrRequiredURL + } + } + return nil +} + +func (o TeamNotificationConfigurationUpdateOptions) valid() error { + if o.Name != nil && !validString(o.Name) { + return ErrRequiredName + } + + if !validTeamNotificationTriggerType(o.Triggers) { + return ErrInvalidNotificationTrigger + } + + return nil +} + +func validTeamNotificationTriggerType(triggers []NotificationTriggerType) bool { + for _, t := range triggers { + switch t { + case + NotificationTriggerChangeRequestCreated: + continue + default: + return false + } + } + + return true +} diff --git a/team_notification_configuration_integration_test.go b/team_notification_configuration_integration_test.go new file mode 100644 index 000000000..d78e0fe67 --- /dev/null +++ b/team_notification_configuration_integration_test.go @@ -0,0 +1,457 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfe + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTeamNotificationConfigurationList(t *testing.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.TeamNotificationConfigurations.List( + ctx, + tmTest.ID, + nil, + ) + require.NoError(t, err) + 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) + }) + + 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. + ncl, err := client.TeamNotificationConfigurations.List( + ctx, + tmTest.ID, + &TeamNotificationConfigurationListOptions{ + ListOptions: ListOptions{ + PageNumber: 999, + PageSize: 100, + }, + }, + ) + require.NoError(t, err) + assert.Empty(t, ncl.Items) + assert.Equal(t, 999, ncl.CurrentPage) + assert.Equal(t, 2, ncl.TotalCount) + }) + + t.Run("without a valid team", func(t *testing.T) { + ncl, err := client.TeamNotificationConfigurations.List( + ctx, + badIdentifier, + nil, + ) + assert.Nil(t, ncl) + assert.EqualError(t, err, ErrInvalidTeamID.Error()) + }) +} + +func TestTeamNotificationConfigurationCreate(t *testing.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 := TeamNotificationConfigurationCreateOptions{ + DestinationType: NotificationDestination(NotificationDestinationTypeGeneric), + Enabled: Bool(false), + Name: String(randomString(t)), + Token: String(randomString(t)), + URL: String("http://example.com"), + Triggers: []NotificationTriggerType{NotificationTriggerChangeRequestCreated}, + } + + _, err := client.TeamNotificationConfigurations.Create(ctx, tmTest.ID, options) + require.NoError(t, err) + }) + + t.Run("without a required value", func(t *testing.T) { + options := TeamNotificationConfigurationCreateOptions{ + DestinationType: NotificationDestination(NotificationDestinationTypeGeneric), + Enabled: Bool(false), + Token: String(randomString(t)), + URL: String("http://example.com"), + Triggers: []NotificationTriggerType{NotificationTriggerChangeRequestCreated}, + } + + nc, err := client.TeamNotificationConfigurations.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 := TeamNotificationConfigurationCreateOptions{ + DestinationType: NotificationDestination(NotificationDestinationTypeGeneric), + Enabled: Bool(false), + Name: String(randomString(t)), + Token: String(randomString(t)), + Triggers: []NotificationTriggerType{NotificationTriggerChangeRequestCreated}, + } + + nc, err := client.TeamNotificationConfigurations.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 := TeamNotificationConfigurationCreateOptions{ + DestinationType: NotificationDestination(NotificationDestinationTypeSlack), + Enabled: Bool(false), + Name: String(randomString(t)), + Triggers: []NotificationTriggerType{NotificationTriggerChangeRequestCreated}, + } + + nc, err := client.TeamNotificationConfigurations.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 := TeamNotificationConfigurationCreateOptions{ + DestinationType: NotificationDestination(NotificationDestinationTypeMicrosoftTeams), + Enabled: Bool(false), + Name: String(randomString(t)), + Triggers: []NotificationTriggerType{NotificationTriggerChangeRequestCreated}, + } + + nc, err := client.TeamNotificationConfigurations.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.TeamNotificationConfigurations.Create(ctx, badIdentifier, TeamNotificationConfigurationCreateOptions{}) + assert.Nil(t, nc) + assert.EqualError(t, err, ErrInvalidTeamID.Error()) + }) + + t.Run("with an invalid notification trigger", func(t *testing.T) { + options := TeamNotificationConfigurationCreateOptions{ + 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"}, + } + + nc, err := client.TeamNotificationConfigurations.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 := TeamNotificationConfigurationCreateOptions{ + DestinationType: NotificationDestination(NotificationDestinationTypeEmail), + Enabled: Bool(false), + Name: String(randomString(t)), + EmailUsers: []*User{orgMemberTest.User}, + } + + _, err := client.TeamNotificationConfigurations.Create(ctx, tmTest.ID, options) + require.NoError(t, err) + }) + + t.Run("without email users when destination type is email", func(t *testing.T) { + options := TeamNotificationConfigurationCreateOptions{ + DestinationType: NotificationDestination(NotificationDestinationTypeEmail), + Enabled: Bool(false), + Name: String(randomString(t)), + } + + _, err := client.TeamNotificationConfigurations.Create(ctx, tmTest.ID, options) + require.NoError(t, err) + }) +} + +func TestTeamNotificationConfigurationsCreate_byType(t *testing.T) { + t.Parallel() + + 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) + + testCases := []NotificationTriggerType{ + NotificationTriggerChangeRequestCreated, + } + + for _, trigger := range testCases { + trigger := trigger + message := fmt.Sprintf("with trigger %s and all required values", trigger) + + t.Run(message, func(t *testing.T) { + t.Parallel() + options := TeamNotificationConfigurationCreateOptions{ + DestinationType: NotificationDestination(NotificationDestinationTypeGeneric), + Enabled: Bool(false), + Name: String(randomString(t)), + Token: String(randomString(t)), + URL: String("http://example.com"), + Triggers: []NotificationTriggerType{trigger}, + } + + _, err := client.TeamNotificationConfigurations.Create(ctx, tmTest.ID, options) + require.NoError(t, err) + }) + } +} + +func TestTeamNotificationConfigurationRead(t *testing.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.TeamNotificationConfigurations.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.TeamNotificationConfigurations.Read(ctx, "nonexisting") + assert.Equal(t, err, ErrResourceNotFound) + }) + + t.Run("when the notification configuration ID is invalid", func(t *testing.T) { + _, err := client.TeamNotificationConfigurations.Read(ctx, badIdentifier) + assert.Equal(t, err, ErrInvalidNotificationConfigID) + }) +} + +func TestTeamNotificationConfigurationUpdate(t *testing.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 := &TeamNotificationConfigurationCreateOptions{ + DestinationType: NotificationDestination(NotificationDestinationTypeEmail), + Enabled: Bool(false), + Name: String(randomString(t)), + EmailUsers: []*User{orgMemberTest1.User}, + } + ncEmailTest, ncEmailTestCleanup := createTeamNotificationConfiguration(t, client, tmTest, options) + t.Cleanup(ncEmailTestCleanup) + + t.Run("with options", func(t *testing.T) { + options := TeamNotificationConfigurationUpdateOptions{ + Enabled: Bool(true), + Name: String("newName"), + } + + nc, err := client.TeamNotificationConfigurations.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 := TeamNotificationConfigurationUpdateOptions{ + Triggers: []NotificationTriggerType{"fly you fools!"}, + } + + nc, err := client.TeamNotificationConfigurations.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 := TeamNotificationConfigurationUpdateOptions{ + Enabled: Bool(true), + Name: String("newName"), + EmailUsers: []*User{orgMemberTest1.User, orgMemberTest2.User}, + } + + nc, err := client.TeamNotificationConfigurations.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 := TeamNotificationConfigurationUpdateOptions{ + Enabled: Bool(true), + Name: String("newName"), + } + + nc, err := client.TeamNotificationConfigurations.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.TeamNotificationConfigurations.Update(ctx, ncTest.ID, TeamNotificationConfigurationUpdateOptions{}) + require.NoError(t, err) + }) + + t.Run("when the notification configuration does not exist", func(t *testing.T) { + _, err := client.TeamNotificationConfigurations.Update(ctx, "nonexisting", TeamNotificationConfigurationUpdateOptions{}) + assert.Equal(t, err, ErrResourceNotFound) + }) + + t.Run("when the notification configuration ID is invalid", func(t *testing.T) { + _, err := client.TeamNotificationConfigurations.Update(ctx, badIdentifier, TeamNotificationConfigurationUpdateOptions{}) + assert.Equal(t, err, ErrInvalidNotificationConfigID) + }) +} + +func TestTeamNotificationConfigurationDelete(t *testing.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.TeamNotificationConfigurations.Delete(ctx, ncTest.ID) + require.NoError(t, err) + + _, err = client.TeamNotificationConfigurations.Read(ctx, ncTest.ID) + assert.Equal(t, err, ErrResourceNotFound) + }) + + t.Run("when the notification configuration does not exist", func(t *testing.T) { + err := client.TeamNotificationConfigurations.Delete(ctx, "nonexisting") + assert.Equal(t, err, ErrResourceNotFound) + }) + + t.Run("when the notification configuration ID is invalid", func(t *testing.T) { + err := client.TeamNotificationConfigurations.Delete(ctx, badIdentifier) + assert.Equal(t, err, ErrInvalidNotificationConfigID) + }) +} + +func TestTeamNotificationConfigurationVerify(t *testing.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.TeamNotificationConfigurations.Verify(ctx, ncTest.ID) + require.NoError(t, err) + }) + + t.Run("when the notification configuration does not exists", func(t *testing.T) { + _, err := client.TeamNotificationConfigurations.Verify(ctx, "nonexisting") + assert.Equal(t, err, ErrResourceNotFound) + }) + + t.Run("when the notification configuration ID is invalid", func(t *testing.T) { + _, err := client.TeamNotificationConfigurations.Verify(ctx, badIdentifier) + assert.Equal(t, err, ErrInvalidNotificationConfigID) + }) +} diff --git a/tfe.go b/tfe.go index a4f40be3e..c229c26fe 100644 --- a/tfe.go +++ b/tfe.go @@ -123,70 +123,71 @@ type Client struct { remoteTFEVersion string appName string - Admin Admin - Agents Agents - AgentPools AgentPools - AgentTokens AgentTokens - Applies Applies - AuditTrails AuditTrails - Comments Comments - ConfigurationVersions ConfigurationVersions - CostEstimates CostEstimates - GHAInstallations GHAInstallations - GPGKeys GPGKeys - NotificationConfigurations NotificationConfigurations - OAuthClients OAuthClients - OAuthTokens OAuthTokens - Organizations Organizations - OrganizationMemberships OrganizationMemberships - OrganizationTags OrganizationTags - OrganizationTokens OrganizationTokens - Plans Plans - PlanExports PlanExports - Policies Policies - PolicyChecks PolicyChecks - PolicyEvaluations PolicyEvaluations - PolicySetOutcomes PolicySetOutcomes - PolicySetParameters PolicySetParameters - PolicySetVersions PolicySetVersions - PolicySets PolicySets - RegistryModules RegistryModules - RegistryNoCodeModules RegistryNoCodeModules - RegistryProviders RegistryProviders - RegistryProviderPlatforms RegistryProviderPlatforms - RegistryProviderVersions RegistryProviderVersions - Runs Runs - RunEvents RunEvents - RunTasks RunTasks - RunTasksIntegration RunTasksIntegration - RunTriggers RunTriggers - SSHKeys SSHKeys - Stacks Stacks - StackConfigurations StackConfigurations - StackDeployments StackDeployments - StackPlans StackPlans - StackPlanOperations StackPlanOperations - StackSources StackSources - StateVersionOutputs StateVersionOutputs - StateVersions StateVersions - TaskResults TaskResults - TaskStages TaskStages - Teams Teams - TeamAccess TeamAccesses - TeamMembers TeamMembers - TeamProjectAccess TeamProjectAccesses - TeamTokens TeamTokens - TestRuns TestRuns - TestVariables TestVariables - Users Users - UserTokens UserTokens - Variables Variables - VariableSets VariableSets - VariableSetVariables VariableSetVariables - Workspaces Workspaces - WorkspaceResources WorkspaceResources - WorkspaceRunTasks WorkspaceRunTasks - Projects Projects + Admin Admin + Agents Agents + AgentPools AgentPools + AgentTokens AgentTokens + Applies Applies + AuditTrails AuditTrails + Comments Comments + ConfigurationVersions ConfigurationVersions + CostEstimates CostEstimates + GHAInstallations GHAInstallations + GPGKeys GPGKeys + NotificationConfigurations NotificationConfigurations + OAuthClients OAuthClients + OAuthTokens OAuthTokens + Organizations Organizations + OrganizationMemberships OrganizationMemberships + OrganizationTags OrganizationTags + OrganizationTokens OrganizationTokens + Plans Plans + PlanExports PlanExports + Policies Policies + PolicyChecks PolicyChecks + PolicyEvaluations PolicyEvaluations + PolicySetOutcomes PolicySetOutcomes + PolicySetParameters PolicySetParameters + PolicySetVersions PolicySetVersions + PolicySets PolicySets + RegistryModules RegistryModules + RegistryNoCodeModules RegistryNoCodeModules + RegistryProviders RegistryProviders + RegistryProviderPlatforms RegistryProviderPlatforms + RegistryProviderVersions RegistryProviderVersions + Runs Runs + RunEvents RunEvents + RunTasks RunTasks + RunTasksIntegration RunTasksIntegration + RunTriggers RunTriggers + SSHKeys SSHKeys + Stacks Stacks + StackConfigurations StackConfigurations + StackDeployments StackDeployments + StackPlans StackPlans + StackPlanOperations StackPlanOperations + StackSources StackSources + StateVersionOutputs StateVersionOutputs + StateVersions StateVersions + TaskResults TaskResults + TaskStages TaskStages + Teams Teams + TeamAccess TeamAccesses + TeamMembers TeamMembers + TeamNotificationConfigurations TeamNotificationConfigurations + TeamProjectAccess TeamProjectAccesses + TeamTokens TeamTokens + TestRuns TestRuns + TestVariables TestVariables + Users Users + UserTokens UserTokens + Variables Variables + VariableSets VariableSets + VariableSetVariables VariableSetVariables + Workspaces Workspaces + WorkspaceResources WorkspaceResources + WorkspaceRunTasks WorkspaceRunTasks + Projects Projects Meta Meta } @@ -501,6 +502,7 @@ func NewClient(cfg *Config) (*Client, error) { client.TaskStages = &taskStages{client: client} client.TeamAccess = &teamAccesses{client: client} client.TeamMembers = &teamMembers{client: client} + client.TeamNotificationConfigurations = &teamNotificationConfigurations{client: client} client.TeamProjectAccess = &teamProjectAccesses{client: client} client.Teams = &teams{client: client} client.TeamTokens = &teamTokens{client: client} From 07477b9b757f6e13ec344b00a559197edef3b36d Mon Sep 17 00:00:00 2001 From: Josh Freda Date: Tue, 10 Dec 2024 18:42:56 -0600 Subject: [PATCH 02/10] Add skipUnlessBeta to team notification configuration tests until feature is GA --- team_notification_configuration_integration_test.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/team_notification_configuration_integration_test.go b/team_notification_configuration_integration_test.go index d78e0fe67..328c6cbb6 100644 --- a/team_notification_configuration_integration_test.go +++ b/team_notification_configuration_integration_test.go @@ -13,6 +13,7 @@ import ( ) func TestTeamNotificationConfigurationList(t *testing.T) { + skipUnlessBeta(t) client := testClient(t) ctx := context.Background() @@ -78,6 +79,7 @@ func TestTeamNotificationConfigurationList(t *testing.T) { } func TestTeamNotificationConfigurationCreate(t *testing.T) { + skipUnlessBeta(t) client := testClient(t) ctx := context.Background() @@ -214,6 +216,7 @@ func TestTeamNotificationConfigurationCreate(t *testing.T) { } func TestTeamNotificationConfigurationsCreate_byType(t *testing.T) { + skipUnlessBeta(t) t.Parallel() client := testClient(t) @@ -253,6 +256,7 @@ func TestTeamNotificationConfigurationsCreate_byType(t *testing.T) { } func TestTeamNotificationConfigurationRead(t *testing.T) { + skipUnlessBeta(t) client := testClient(t) ctx := context.Background() @@ -285,6 +289,7 @@ func TestTeamNotificationConfigurationRead(t *testing.T) { } func TestTeamNotificationConfigurationUpdate(t *testing.T) { + skipUnlessBeta(t) client := testClient(t) ctx := context.Background() @@ -393,6 +398,7 @@ func TestTeamNotificationConfigurationUpdate(t *testing.T) { } func TestTeamNotificationConfigurationDelete(t *testing.T) { + skipUnlessBeta(t) client := testClient(t) ctx := context.Background() @@ -426,6 +432,7 @@ func TestTeamNotificationConfigurationDelete(t *testing.T) { } func TestTeamNotificationConfigurationVerify(t *testing.T) { + skipUnlessBeta(t) client := testClient(t) ctx := context.Background() From b5ede4b1f43e41d1036ba4f6a7b5363d2e2cca90 Mon Sep 17 00:00:00 2001 From: Taylor Chaparro Date: Mon, 16 Dec 2024 11:47:32 -0800 Subject: [PATCH 03/10] use plus feature set --- subscription_updater_test.go | 13 +++++++++++++ ..._notification_configuration_integration_test.go | 14 +++++++------- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/subscription_updater_test.go b/subscription_updater_test.go index 25554fc1f..8b677ee17 100644 --- a/subscription_updater_test.go +++ b/subscription_updater_test.go @@ -72,6 +72,19 @@ func (b *organizationSubscriptionUpdater) WithTrialPlan() *organizationSubscript return b } +func (b *organizationSubscriptionUpdater) WithPlusPlan() *organizationSubscriptionUpdater { + b.planName = "Plus" + + start := time.Now() + ceiling := 1 + managedResourcesLimit := 1000 + + b.updateOpts.ContractStartAt = &start + b.updateOpts.RunsCeiling = &ceiling + b.updateOpts.ContractManagedResourcesLimit = &managedResourcesLimit + return b +} + func (b *organizationSubscriptionUpdater) WithPlusEntitlementPlan() *organizationSubscriptionUpdater { b.planName = "Plus (entitlement)" diff --git a/team_notification_configuration_integration_test.go b/team_notification_configuration_integration_test.go index 328c6cbb6..289da16e6 100644 --- a/team_notification_configuration_integration_test.go +++ b/team_notification_configuration_integration_test.go @@ -20,7 +20,7 @@ func TestTeamNotificationConfigurationList(t *testing.T) { orgTest, orgTestCleanup := createOrganization(t, client) t.Cleanup(orgTestCleanup) - newSubscriptionUpdater(orgTest).WithPlusEntitlementPlan().Update(t) + newSubscriptionUpdater(orgTest).WithPlusPlan().Update(t) tmTest, tmTestCleanup := createTeam(t, client, orgTest) t.Cleanup(tmTestCleanup) @@ -86,7 +86,7 @@ func TestTeamNotificationConfigurationCreate(t *testing.T) { orgTest, orgTestCleanup := createOrganization(t, client) t.Cleanup(orgTestCleanup) - newSubscriptionUpdater(orgTest).WithPlusEntitlementPlan().Update(t) + newSubscriptionUpdater(orgTest).WithPlusPlan().Update(t) tmTest, tmTestCleanup := createTeam(t, client, orgTest) t.Cleanup(tmTestCleanup) @@ -225,7 +225,7 @@ func TestTeamNotificationConfigurationsCreate_byType(t *testing.T) { orgTest, orgTestCleanup := createOrganization(t, client) t.Cleanup(orgTestCleanup) - newSubscriptionUpdater(orgTest).WithPlusEntitlementPlan().Update(t) + newSubscriptionUpdater(orgTest).WithPlusPlan().Update(t) tmTest, tmTestCleanup := createTeam(t, client, orgTest) t.Cleanup(tmTestCleanup) @@ -263,7 +263,7 @@ func TestTeamNotificationConfigurationRead(t *testing.T) { orgTest, orgTestCleanup := createOrganization(t, client) t.Cleanup(orgTestCleanup) - newSubscriptionUpdater(orgTest).WithPlusEntitlementPlan().Update(t) + newSubscriptionUpdater(orgTest).WithPlusPlan().Update(t) tmTest, tmTestCleanup := createTeam(t, client, orgTest) t.Cleanup(tmTestCleanup) @@ -296,7 +296,7 @@ func TestTeamNotificationConfigurationUpdate(t *testing.T) { orgTest, orgTestCleanup := createOrganization(t, client) t.Cleanup(orgTestCleanup) - newSubscriptionUpdater(orgTest).WithPlusEntitlementPlan().Update(t) + newSubscriptionUpdater(orgTest).WithPlusPlan().Update(t) tmTest, tmTestCleanup := createTeam(t, client, orgTest) t.Cleanup(tmTestCleanup) @@ -405,7 +405,7 @@ func TestTeamNotificationConfigurationDelete(t *testing.T) { orgTest, orgTestCleanup := createOrganization(t, client) t.Cleanup(orgTestCleanup) - newSubscriptionUpdater(orgTest).WithPlusEntitlementPlan().Update(t) + newSubscriptionUpdater(orgTest).WithPlusPlan().Update(t) tmTest, tmTestCleanup := createTeam(t, client, orgTest) t.Cleanup(tmTestCleanup) @@ -439,7 +439,7 @@ func TestTeamNotificationConfigurationVerify(t *testing.T) { orgTest, orgTestCleanup := createOrganization(t, client) t.Cleanup(orgTestCleanup) - newSubscriptionUpdater(orgTest).WithPlusEntitlementPlan().Update(t) + newSubscriptionUpdater(orgTest).WithPlusPlan().Update(t) tmTest, tmTestCleanup := createTeam(t, client, orgTest) t.Cleanup(tmTestCleanup) From 57017b9a619b8bb7ee4010d6a4d678d72696ed87 Mon Sep 17 00:00:00 2001 From: Taylor Chaparro Date: Mon, 23 Dec 2024 15:27:07 -0800 Subject: [PATCH 04/10] move team notifications to notification configuration resources, use patched jsonapi --- errors.go | 2 + go.mod | 2 + go.sum | 4 +- helper_test.go | 23 +- notification_configuration.go | 68 ++- ...fication_configuration_integration_test.go | 419 +++++++++++++++- subscription_updater_test.go | 13 - team_notification_configuration.go | 327 ------------ ...fication_configuration_integration_test.go | 464 ------------------ tfe.go | 130 +++-- 10 files changed, 553 insertions(+), 899 deletions(-) delete mode 100644 team_notification_configuration.go delete mode 100644 team_notification_configuration_integration_test.go diff --git a/errors.go b/errors.go index 935f5b361..d1f027fca 100644 --- a/errors.go +++ b/errors.go @@ -145,6 +145,8 @@ var ( ErrInvalidNotificationConfigID = errors.New("invalid value for notification configuration ID") + ErrInvalidNotificationConfigSubscribableChoice = errors.New("invalid value for notification configuration subscribable choice") + ErrInvalidMembership = errors.New("invalid value for membership") ErrInvalidMembershipIDs = errors.New("invalid value for organization membership ids") diff --git a/go.mod b/go.mod index 22ad6cda3..116fc9581 100644 --- a/go.mod +++ b/go.mod @@ -22,3 +22,5 @@ require ( golang.org/x/sys v0.25.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +replace github.com/hashicorp/jsonapi => github.com/notchairmk/jsonapi v0.0.0-20241223221631-b0c6a5b7edd8 diff --git a/go.sum b/go.sum index 1293b8565..89e2b6bcc 100644 --- a/go.sum +++ b/go.sum @@ -16,10 +16,10 @@ github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/C github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/jsonapi v1.3.1 h1:GtPvnmcWgYwCuDGvYT5VZBHcUyFdq9lSyCzDjn1DdPo= -github.com/hashicorp/jsonapi v1.3.1/go.mod h1:kWfdn49yCjQvbpnvY1dxxAuAFzISwrrMDQOcu6NsFoM= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/notchairmk/jsonapi v0.0.0-20241223221631-b0c6a5b7edd8 h1:Nll3UptyKamtMP60oCHnRKI3l/kgadZHKQ6/uLYPyVM= +github.com/notchairmk/jsonapi v0.0.0-20241223221631-b0c6a5b7edd8/go.mod h1:kWfdn49yCjQvbpnvY1dxxAuAFzISwrrMDQOcu6NsFoM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= diff --git a/helper_test.go b/helper_test.go index 9e82d54d4..6d7062426 100644 --- a/helper_test.go +++ b/helper_test.go @@ -615,7 +615,7 @@ func createNotificationConfiguration(t *testing.T, client *Client, w *Workspace, } } -func createTeamNotificationConfiguration(t *testing.T, client *Client, team *Team, options *TeamNotificationConfigurationCreateOptions) (*TeamNotificationConfiguration, func()) { +func createTeamNotificationConfiguration(t *testing.T, client *Client, team *Team, options *NotificationConfigurationCreateOptions) (*NotificationConfiguration, func()) { var tCleanup func() if team == nil { @@ -630,18 +630,19 @@ func createTeamNotificationConfiguration(t *testing.T, client *Client, team *Tea } if options == nil { - options = &TeamNotificationConfigurationCreateOptions{ - DestinationType: NotificationDestination(NotificationDestinationTypeGeneric), - Enabled: Bool(false), - Name: String(randomString(t)), - Token: String(randomString(t)), - URL: String(runTaskURL), - Triggers: []NotificationTriggerType{NotificationTriggerChangeRequestCreated}, + 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.TeamNotificationConfigurations.Create( + nc, err := client.NotificationConfigurations.Create( ctx, team.ID, *options, @@ -651,10 +652,10 @@ func createTeamNotificationConfiguration(t *testing.T, client *Client, team *Tea } return nc, func() { - if err := client.TeamNotificationConfigurations.Delete(ctx, nc.ID); err != nil { + 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"+ - "TeamNotificationConfiguration: %s\nError: %s", nc.ID, err) + "NotificationConfiguration: %s\nError: %s", nc.ID, err) } if tCleanup != nil { diff --git a/notification_configuration.go b/notification_configuration.go index ece867477..bd74a2d0b 100644 --- a/notification_configuration.go +++ b/notification_configuration.go @@ -81,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"` @@ -98,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"` + SubscribableChoice *NotificationConfigurationSubscribableChoice `jsonapi:"polyrelation,subscribable"` + + EmailUsers []*User `jsonapi:"relation,users"` } // DeliveryResponse represents a notification configuration delivery response. @@ -116,6 +127,8 @@ type DeliveryResponse struct { // notification configurations. type NotificationConfigurationListOptions struct { ListOptions + + SubscribableChoice *NotificationConfigurationSubscribableChoice `jsonapi:"polyrelation,subscribable"` } // NotificationConfigurationCreateOptions represents the options for @@ -151,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"` } // NotificationConfigurationUpdateOptions represents the options for @@ -186,12 +202,22 @@ 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.SubscribableChoice == nil || options.SubscribableChoice.Workspace != nil { + if !validStringID(&subscribableID) { + return nil, ErrInvalidWorkspaceID + } + u = fmt.Sprintf("workspaces/%s/notification-configurations", url.PathEscape(subscribableID)) + } else if options.SubscribableChoice.Team != nil { + if !validStringID(&subscribableID) { + return nil, ErrInvalidTeamID + } + u = fmt.Sprintf("teams/%s/notification-configurations", url.PathEscape(subscribableID)) + } else { + return nil, ErrInvalidNotificationConfigSubscribableChoice } - u := fmt.Sprintf("workspaces/%s/notification-configurations", url.PathEscape(workspaceID)) req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err @@ -207,22 +233,39 @@ func (s *notificationConfigurations) List(ctx context.Context, workspaceID strin } // 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) { if err := options.valid(); err != nil { return nil, err } - u := fmt.Sprintf("workspaces/%s/notification-configurations", url.PathEscape(workspaceID)) + var u string + var subscribableChoice *NotificationConfigurationSubscribableChoice + if options.SubscribableChoice == nil || options.SubscribableChoice.Workspace != nil { + if !validStringID(&subscribableID) { + return nil, ErrInvalidWorkspaceID + } + + u = fmt.Sprintf("workspaces/%s/notification-configurations", url.PathEscape(subscribableID)) + subscribableChoice = &NotificationConfigurationSubscribableChoice{Workspace: &Workspace{ID: subscribableID}} + } else if options.SubscribableChoice != nil && options.SubscribableChoice.Team != nil { + if !validStringID(&subscribableID) { + return nil, ErrInvalidTeamID + } + + u = fmt.Sprintf("teams/%s/notification-configurations", url.PathEscape(subscribableID)) + subscribableChoice = &NotificationConfigurationSubscribableChoice{Team: &Team{ID: subscribableID}} + } else { + return nil, ErrInvalidNotificationConfigSubscribableChoice + } + 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 } @@ -364,6 +407,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) + }) +} diff --git a/subscription_updater_test.go b/subscription_updater_test.go index 8b677ee17..25554fc1f 100644 --- a/subscription_updater_test.go +++ b/subscription_updater_test.go @@ -72,19 +72,6 @@ func (b *organizationSubscriptionUpdater) WithTrialPlan() *organizationSubscript return b } -func (b *organizationSubscriptionUpdater) WithPlusPlan() *organizationSubscriptionUpdater { - b.planName = "Plus" - - start := time.Now() - ceiling := 1 - managedResourcesLimit := 1000 - - b.updateOpts.ContractStartAt = &start - b.updateOpts.RunsCeiling = &ceiling - b.updateOpts.ContractManagedResourcesLimit = &managedResourcesLimit - return b -} - func (b *organizationSubscriptionUpdater) WithPlusEntitlementPlan() *organizationSubscriptionUpdater { b.planName = "Plus (entitlement)" diff --git a/team_notification_configuration.go b/team_notification_configuration.go deleted file mode 100644 index 68b86319f..000000000 --- a/team_notification_configuration.go +++ /dev/null @@ -1,327 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package tfe - -import ( - "context" - "fmt" - "net/url" - "time" -) - -// Compile-time proof of interface implementation. -var _ TeamNotificationConfigurations = (*teamNotificationConfigurations)(nil) - -// TeamNotificationConfigurations describes all the Team Notification Configuration -// related methods that the Terraform Enterprise API supports. -// -// TFE API docs: -// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/notification-configurations#team-notification-configuration -type TeamNotificationConfigurations interface { - // List all the notification configurations within a team. - List(ctx context.Context, teamID string, options *TeamNotificationConfigurationListOptions) (*TeamNotificationConfigurationList, error) - - // Create a new team notification configuration with the given options. - Create(ctx context.Context, teamID string, options TeamNotificationConfigurationCreateOptions) (*TeamNotificationConfiguration, error) - - // Read a notification configuration by its ID. - Read(ctx context.Context, teamNotificationConfigurationID string) (*TeamNotificationConfiguration, error) - - // Update an existing team notification configuration. - Update(ctx context.Context, teamNotificationConfigurationID string, options TeamNotificationConfigurationUpdateOptions) (*TeamNotificationConfiguration, error) - - // Delete a team notification configuration by its ID. - Delete(ctx context.Context, teamNotificationConfigurationID string) error - - // Verify a team notification configuration by its ID. - Verify(ctx context.Context, teamNotificationConfigurationID string) (*TeamNotificationConfiguration, error) -} - -// teamNotificationConfigurations implements TeamNotificationConfigurations. -type teamNotificationConfigurations struct { - client *Client -} - -// TeamNotificationConfigurationList represents a list of team notification -// configurations. -type TeamNotificationConfigurationList struct { - *Pagination - Items []*TeamNotificationConfiguration -} - -// TeamNotificationConfiguration represents a team notification configuration. -type TeamNotificationConfiguration struct { - ID string `jsonapi:"primary,notification-configurations"` - CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"` - DeliveryResponses []*DeliveryResponse `jsonapi:"attr,delivery-responses"` - DestinationType NotificationDestinationType `jsonapi:"attr,destination-type"` - Enabled bool `jsonapi:"attr,enabled"` - Name string `jsonapi:"attr,name"` - Token string `jsonapi:"attr,token"` - Triggers []string `jsonapi:"attr,triggers"` - UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"` - URL string `jsonapi:"attr,url"` - - // EmailAddresses is only available for TFE users. It is not available in HCP Terraform. - EmailAddresses []string `jsonapi:"attr,email-addresses"` - - // Relations - Subscribable *Team `jsonapi:"relation,subscribable"` - EmailUsers []*User `jsonapi:"relation,users"` -} - -// TeamNotificationConfigurationListOptions represents the options for listing -// notification configurations. -type TeamNotificationConfigurationListOptions struct { - ListOptions -} - -// TeamNotificationConfigurationCreateOptions represents the options for -// creating a new team notification configuration. -type TeamNotificationConfigurationCreateOptions struct { - // Type is a public field utilized by JSON:API to - // set the resource type via the field tag. - // It is not a user-defined value and does not need to be set. - // https://jsonapi.org/format/#crud-creating - Type string `jsonapi:"primary,notification-configurations"` - - // Required: The destination type of the team notification configuration - DestinationType *NotificationDestinationType `jsonapi:"attr,destination-type"` - - // Required: Whether the team notification configuration should be enabled or not - Enabled *bool `jsonapi:"attr,enabled"` - - // Required: The name of the team notification configuration - Name *string `jsonapi:"attr,name"` - - // Optional: The token of the team notification configuration - Token *string `jsonapi:"attr,token,omitempty"` - - // Optional: The list of events that will trigger team notifications - Triggers []NotificationTriggerType `jsonapi:"attr,triggers,omitempty"` - - // Optional: The URL of the team notification configuration - URL *string `jsonapi:"attr,url,omitempty"` - - // Optional: The list of email addresses that will receive team notification emails. - // EmailAddresses is only available for TFE users. It is not available in HCP Terraform. - EmailAddresses []string `jsonapi:"attr,email-addresses,omitempty"` - - // Optional: The list of users belonging to the organization that will receive - // team notification emails. - EmailUsers []*User `jsonapi:"relation,users,omitempty"` -} - -// TeamNotificationConfigurationUpdateOptions represents the options for -// updating a existing team notification configuration. -type TeamNotificationConfigurationUpdateOptions struct { - // Type is a public field utilized by JSON:API to - // set the resource type via the field tag. - // It is not a user-defined value and does not need to be set. - // https://jsonapi.org/format/#crud-creating - Type string `jsonapi:"primary,notification-configurations"` - - // Optional: Whether the team notification configuration should be enabled or not - Enabled *bool `jsonapi:"attr,enabled,omitempty"` - - // Optional: The name of the team notification configuration - Name *string `jsonapi:"attr,name,omitempty"` - - // Optional: The token of the team notification configuration - Token *string `jsonapi:"attr,token,omitempty"` - - // Optional: The list of events that will trigger team notifications - Triggers []NotificationTriggerType `jsonapi:"attr,triggers,omitempty"` - - // Optional: The URL of the team notification configuration - URL *string `jsonapi:"attr,url,omitempty"` - - // Optional: The list of email addresses that will receive team notification emails. - // EmailAddresses is only available for TFE users. It is not available in HCP Terraform. - EmailAddresses []string `jsonapi:"attr,email-addresses,omitempty"` - - // Optional: The list of users belonging to the organization that will receive - // team notification emails. - EmailUsers []*User `jsonapi:"relation,users,omitempty"` -} - -// List all the notification configurations associated with a team. -func (s *teamNotificationConfigurations) List(ctx context.Context, teamID string, options *TeamNotificationConfigurationListOptions) (*TeamNotificationConfigurationList, error) { - if !validStringID(&teamID) { - return nil, ErrInvalidTeamID - } - - u := fmt.Sprintf("teams/%s/notification-configurations", url.PathEscape(teamID)) - req, err := s.client.NewRequest("GET", u, options) - if err != nil { - return nil, err - } - - ncl := &TeamNotificationConfigurationList{} - err = req.Do(ctx, ncl) - if err != nil { - return nil, err - } - - return ncl, nil -} - -// Create a team notification configuration with the given options. -func (s *teamNotificationConfigurations) Create(ctx context.Context, teamID string, options TeamNotificationConfigurationCreateOptions) (*TeamNotificationConfiguration, error) { - if !validStringID(&teamID) { - return nil, ErrInvalidTeamID - } - if err := options.valid(); err != nil { - return nil, err - } - - u := fmt.Sprintf("teams/%s/notification-configurations", url.PathEscape(teamID)) - req, err := s.client.NewRequest("POST", u, &options) - if err != nil { - return nil, err - } - - nc := &TeamNotificationConfiguration{} - err = req.Do(ctx, nc) - if err != nil { - return nil, err - } - - return nc, nil -} - -// Read a team notification configuration by its ID. -func (s *teamNotificationConfigurations) Read(ctx context.Context, teamNotificationConfigurationID string) (*TeamNotificationConfiguration, error) { - if !validStringID(&teamNotificationConfigurationID) { - return nil, ErrInvalidNotificationConfigID - } - - u := fmt.Sprintf("notification-configurations/%s", url.PathEscape(teamNotificationConfigurationID)) - req, err := s.client.NewRequest("GET", u, nil) - if err != nil { - return nil, err - } - - nc := &TeamNotificationConfiguration{} - err = req.Do(ctx, nc) - if err != nil { - return nil, err - } - - return nc, nil -} - -// Updates a team notification configuration with the given options. -func (s *teamNotificationConfigurations) Update(ctx context.Context, teamNotificationConfigurationID string, options TeamNotificationConfigurationUpdateOptions) (*TeamNotificationConfiguration, error) { - if !validStringID(&teamNotificationConfigurationID) { - return nil, ErrInvalidNotificationConfigID - } - - if err := options.valid(); err != nil { - return nil, err - } - - u := fmt.Sprintf("notification-configurations/%s", url.PathEscape(teamNotificationConfigurationID)) - req, err := s.client.NewRequest("PATCH", u, &options) - if err != nil { - return nil, err - } - - nc := &TeamNotificationConfiguration{} - err = req.Do(ctx, nc) - if err != nil { - return nil, err - } - - return nc, nil -} - -// Delete a team notification configuration by its ID. -func (s *teamNotificationConfigurations) Delete(ctx context.Context, teamNotificationConfigurationID string) error { - if !validStringID(&teamNotificationConfigurationID) { - return ErrInvalidNotificationConfigID - } - - u := fmt.Sprintf("notification-configurations/%s", url.PathEscape(teamNotificationConfigurationID)) - req, err := s.client.NewRequest("DELETE", u, nil) - if err != nil { - return err - } - - return req.Do(ctx, nil) -} - -// Verify a team notification configuration by delivering a verification payload -// to the configured URL. -func (s *teamNotificationConfigurations) Verify(ctx context.Context, teamNotificationConfigurationID string) (*TeamNotificationConfiguration, error) { - if !validStringID(&teamNotificationConfigurationID) { - return nil, ErrInvalidNotificationConfigID - } - - u := fmt.Sprintf( - "notification-configurations/%s/actions/verify", url.PathEscape(teamNotificationConfigurationID)) - req, err := s.client.NewRequest("POST", u, nil) - if err != nil { - return nil, err - } - - nc := &TeamNotificationConfiguration{} - err = req.Do(ctx, nc) - if err != nil { - return nil, err - } - - return nc, nil -} - -func (o TeamNotificationConfigurationCreateOptions) valid() error { - if o.DestinationType == nil { - return ErrRequiredDestinationType - } - if o.Enabled == nil { - return ErrRequiredEnabled - } - if !validString(o.Name) { - return ErrRequiredName - } - - if !validTeamNotificationTriggerType(o.Triggers) { - return ErrInvalidNotificationTrigger - } - - if *o.DestinationType == NotificationDestinationTypeGeneric || - *o.DestinationType == NotificationDestinationTypeSlack || - *o.DestinationType == NotificationDestinationTypeMicrosoftTeams { - if o.URL == nil { - return ErrRequiredURL - } - } - return nil -} - -func (o TeamNotificationConfigurationUpdateOptions) valid() error { - if o.Name != nil && !validString(o.Name) { - return ErrRequiredName - } - - if !validTeamNotificationTriggerType(o.Triggers) { - return ErrInvalidNotificationTrigger - } - - return nil -} - -func validTeamNotificationTriggerType(triggers []NotificationTriggerType) bool { - for _, t := range triggers { - switch t { - case - NotificationTriggerChangeRequestCreated: - continue - default: - return false - } - } - - return true -} diff --git a/team_notification_configuration_integration_test.go b/team_notification_configuration_integration_test.go deleted file mode 100644 index 289da16e6..000000000 --- a/team_notification_configuration_integration_test.go +++ /dev/null @@ -1,464 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package tfe - -import ( - "context" - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestTeamNotificationConfigurationList(t *testing.T) { - skipUnlessBeta(t) - client := testClient(t) - ctx := context.Background() - - orgTest, orgTestCleanup := createOrganization(t, client) - t.Cleanup(orgTestCleanup) - - newSubscriptionUpdater(orgTest).WithPlusPlan().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.TeamNotificationConfigurations.List( - ctx, - tmTest.ID, - nil, - ) - require.NoError(t, err) - 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) - }) - - 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. - ncl, err := client.TeamNotificationConfigurations.List( - ctx, - tmTest.ID, - &TeamNotificationConfigurationListOptions{ - ListOptions: ListOptions{ - PageNumber: 999, - PageSize: 100, - }, - }, - ) - require.NoError(t, err) - assert.Empty(t, ncl.Items) - assert.Equal(t, 999, ncl.CurrentPage) - assert.Equal(t, 2, ncl.TotalCount) - }) - - t.Run("without a valid team", func(t *testing.T) { - ncl, err := client.TeamNotificationConfigurations.List( - ctx, - badIdentifier, - nil, - ) - assert.Nil(t, ncl) - assert.EqualError(t, err, ErrInvalidTeamID.Error()) - }) -} - -func TestTeamNotificationConfigurationCreate(t *testing.T) { - skipUnlessBeta(t) - client := testClient(t) - ctx := context.Background() - - orgTest, orgTestCleanup := createOrganization(t, client) - t.Cleanup(orgTestCleanup) - - newSubscriptionUpdater(orgTest).WithPlusPlan().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 := TeamNotificationConfigurationCreateOptions{ - DestinationType: NotificationDestination(NotificationDestinationTypeGeneric), - Enabled: Bool(false), - Name: String(randomString(t)), - Token: String(randomString(t)), - URL: String("http://example.com"), - Triggers: []NotificationTriggerType{NotificationTriggerChangeRequestCreated}, - } - - _, err := client.TeamNotificationConfigurations.Create(ctx, tmTest.ID, options) - require.NoError(t, err) - }) - - t.Run("without a required value", func(t *testing.T) { - options := TeamNotificationConfigurationCreateOptions{ - DestinationType: NotificationDestination(NotificationDestinationTypeGeneric), - Enabled: Bool(false), - Token: String(randomString(t)), - URL: String("http://example.com"), - Triggers: []NotificationTriggerType{NotificationTriggerChangeRequestCreated}, - } - - nc, err := client.TeamNotificationConfigurations.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 := TeamNotificationConfigurationCreateOptions{ - DestinationType: NotificationDestination(NotificationDestinationTypeGeneric), - Enabled: Bool(false), - Name: String(randomString(t)), - Token: String(randomString(t)), - Triggers: []NotificationTriggerType{NotificationTriggerChangeRequestCreated}, - } - - nc, err := client.TeamNotificationConfigurations.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 := TeamNotificationConfigurationCreateOptions{ - DestinationType: NotificationDestination(NotificationDestinationTypeSlack), - Enabled: Bool(false), - Name: String(randomString(t)), - Triggers: []NotificationTriggerType{NotificationTriggerChangeRequestCreated}, - } - - nc, err := client.TeamNotificationConfigurations.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 := TeamNotificationConfigurationCreateOptions{ - DestinationType: NotificationDestination(NotificationDestinationTypeMicrosoftTeams), - Enabled: Bool(false), - Name: String(randomString(t)), - Triggers: []NotificationTriggerType{NotificationTriggerChangeRequestCreated}, - } - - nc, err := client.TeamNotificationConfigurations.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.TeamNotificationConfigurations.Create(ctx, badIdentifier, TeamNotificationConfigurationCreateOptions{}) - assert.Nil(t, nc) - assert.EqualError(t, err, ErrInvalidTeamID.Error()) - }) - - t.Run("with an invalid notification trigger", func(t *testing.T) { - options := TeamNotificationConfigurationCreateOptions{ - 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"}, - } - - nc, err := client.TeamNotificationConfigurations.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 := TeamNotificationConfigurationCreateOptions{ - DestinationType: NotificationDestination(NotificationDestinationTypeEmail), - Enabled: Bool(false), - Name: String(randomString(t)), - EmailUsers: []*User{orgMemberTest.User}, - } - - _, err := client.TeamNotificationConfigurations.Create(ctx, tmTest.ID, options) - require.NoError(t, err) - }) - - t.Run("without email users when destination type is email", func(t *testing.T) { - options := TeamNotificationConfigurationCreateOptions{ - DestinationType: NotificationDestination(NotificationDestinationTypeEmail), - Enabled: Bool(false), - Name: String(randomString(t)), - } - - _, err := client.TeamNotificationConfigurations.Create(ctx, tmTest.ID, options) - require.NoError(t, err) - }) -} - -func TestTeamNotificationConfigurationsCreate_byType(t *testing.T) { - skipUnlessBeta(t) - t.Parallel() - - client := testClient(t) - ctx := context.Background() - - orgTest, orgTestCleanup := createOrganization(t, client) - t.Cleanup(orgTestCleanup) - - newSubscriptionUpdater(orgTest).WithPlusPlan().Update(t) - - tmTest, tmTestCleanup := createTeam(t, client, orgTest) - t.Cleanup(tmTestCleanup) - - testCases := []NotificationTriggerType{ - NotificationTriggerChangeRequestCreated, - } - - for _, trigger := range testCases { - trigger := trigger - message := fmt.Sprintf("with trigger %s and all required values", trigger) - - t.Run(message, func(t *testing.T) { - t.Parallel() - options := TeamNotificationConfigurationCreateOptions{ - DestinationType: NotificationDestination(NotificationDestinationTypeGeneric), - Enabled: Bool(false), - Name: String(randomString(t)), - Token: String(randomString(t)), - URL: String("http://example.com"), - Triggers: []NotificationTriggerType{trigger}, - } - - _, err := client.TeamNotificationConfigurations.Create(ctx, tmTest.ID, options) - require.NoError(t, err) - }) - } -} - -func TestTeamNotificationConfigurationRead(t *testing.T) { - skipUnlessBeta(t) - client := testClient(t) - ctx := context.Background() - - orgTest, orgTestCleanup := createOrganization(t, client) - t.Cleanup(orgTestCleanup) - - newSubscriptionUpdater(orgTest).WithPlusPlan().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.TeamNotificationConfigurations.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.TeamNotificationConfigurations.Read(ctx, "nonexisting") - assert.Equal(t, err, ErrResourceNotFound) - }) - - t.Run("when the notification configuration ID is invalid", func(t *testing.T) { - _, err := client.TeamNotificationConfigurations.Read(ctx, badIdentifier) - assert.Equal(t, err, ErrInvalidNotificationConfigID) - }) -} - -func TestTeamNotificationConfigurationUpdate(t *testing.T) { - skipUnlessBeta(t) - client := testClient(t) - ctx := context.Background() - - orgTest, orgTestCleanup := createOrganization(t, client) - t.Cleanup(orgTestCleanup) - - newSubscriptionUpdater(orgTest).WithPlusPlan().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 := &TeamNotificationConfigurationCreateOptions{ - DestinationType: NotificationDestination(NotificationDestinationTypeEmail), - Enabled: Bool(false), - Name: String(randomString(t)), - EmailUsers: []*User{orgMemberTest1.User}, - } - ncEmailTest, ncEmailTestCleanup := createTeamNotificationConfiguration(t, client, tmTest, options) - t.Cleanup(ncEmailTestCleanup) - - t.Run("with options", func(t *testing.T) { - options := TeamNotificationConfigurationUpdateOptions{ - Enabled: Bool(true), - Name: String("newName"), - } - - nc, err := client.TeamNotificationConfigurations.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 := TeamNotificationConfigurationUpdateOptions{ - Triggers: []NotificationTriggerType{"fly you fools!"}, - } - - nc, err := client.TeamNotificationConfigurations.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 := TeamNotificationConfigurationUpdateOptions{ - Enabled: Bool(true), - Name: String("newName"), - EmailUsers: []*User{orgMemberTest1.User, orgMemberTest2.User}, - } - - nc, err := client.TeamNotificationConfigurations.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 := TeamNotificationConfigurationUpdateOptions{ - Enabled: Bool(true), - Name: String("newName"), - } - - nc, err := client.TeamNotificationConfigurations.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.TeamNotificationConfigurations.Update(ctx, ncTest.ID, TeamNotificationConfigurationUpdateOptions{}) - require.NoError(t, err) - }) - - t.Run("when the notification configuration does not exist", func(t *testing.T) { - _, err := client.TeamNotificationConfigurations.Update(ctx, "nonexisting", TeamNotificationConfigurationUpdateOptions{}) - assert.Equal(t, err, ErrResourceNotFound) - }) - - t.Run("when the notification configuration ID is invalid", func(t *testing.T) { - _, err := client.TeamNotificationConfigurations.Update(ctx, badIdentifier, TeamNotificationConfigurationUpdateOptions{}) - assert.Equal(t, err, ErrInvalidNotificationConfigID) - }) -} - -func TestTeamNotificationConfigurationDelete(t *testing.T) { - skipUnlessBeta(t) - client := testClient(t) - ctx := context.Background() - - orgTest, orgTestCleanup := createOrganization(t, client) - t.Cleanup(orgTestCleanup) - - newSubscriptionUpdater(orgTest).WithPlusPlan().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.TeamNotificationConfigurations.Delete(ctx, ncTest.ID) - require.NoError(t, err) - - _, err = client.TeamNotificationConfigurations.Read(ctx, ncTest.ID) - assert.Equal(t, err, ErrResourceNotFound) - }) - - t.Run("when the notification configuration does not exist", func(t *testing.T) { - err := client.TeamNotificationConfigurations.Delete(ctx, "nonexisting") - assert.Equal(t, err, ErrResourceNotFound) - }) - - t.Run("when the notification configuration ID is invalid", func(t *testing.T) { - err := client.TeamNotificationConfigurations.Delete(ctx, badIdentifier) - assert.Equal(t, err, ErrInvalidNotificationConfigID) - }) -} - -func TestTeamNotificationConfigurationVerify(t *testing.T) { - skipUnlessBeta(t) - client := testClient(t) - ctx := context.Background() - - orgTest, orgTestCleanup := createOrganization(t, client) - t.Cleanup(orgTestCleanup) - - newSubscriptionUpdater(orgTest).WithPlusPlan().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.TeamNotificationConfigurations.Verify(ctx, ncTest.ID) - require.NoError(t, err) - }) - - t.Run("when the notification configuration does not exists", func(t *testing.T) { - _, err := client.TeamNotificationConfigurations.Verify(ctx, "nonexisting") - assert.Equal(t, err, ErrResourceNotFound) - }) - - t.Run("when the notification configuration ID is invalid", func(t *testing.T) { - _, err := client.TeamNotificationConfigurations.Verify(ctx, badIdentifier) - assert.Equal(t, err, ErrInvalidNotificationConfigID) - }) -} diff --git a/tfe.go b/tfe.go index c229c26fe..a4f40be3e 100644 --- a/tfe.go +++ b/tfe.go @@ -123,71 +123,70 @@ type Client struct { remoteTFEVersion string appName string - Admin Admin - Agents Agents - AgentPools AgentPools - AgentTokens AgentTokens - Applies Applies - AuditTrails AuditTrails - Comments Comments - ConfigurationVersions ConfigurationVersions - CostEstimates CostEstimates - GHAInstallations GHAInstallations - GPGKeys GPGKeys - NotificationConfigurations NotificationConfigurations - OAuthClients OAuthClients - OAuthTokens OAuthTokens - Organizations Organizations - OrganizationMemberships OrganizationMemberships - OrganizationTags OrganizationTags - OrganizationTokens OrganizationTokens - Plans Plans - PlanExports PlanExports - Policies Policies - PolicyChecks PolicyChecks - PolicyEvaluations PolicyEvaluations - PolicySetOutcomes PolicySetOutcomes - PolicySetParameters PolicySetParameters - PolicySetVersions PolicySetVersions - PolicySets PolicySets - RegistryModules RegistryModules - RegistryNoCodeModules RegistryNoCodeModules - RegistryProviders RegistryProviders - RegistryProviderPlatforms RegistryProviderPlatforms - RegistryProviderVersions RegistryProviderVersions - Runs Runs - RunEvents RunEvents - RunTasks RunTasks - RunTasksIntegration RunTasksIntegration - RunTriggers RunTriggers - SSHKeys SSHKeys - Stacks Stacks - StackConfigurations StackConfigurations - StackDeployments StackDeployments - StackPlans StackPlans - StackPlanOperations StackPlanOperations - StackSources StackSources - StateVersionOutputs StateVersionOutputs - StateVersions StateVersions - TaskResults TaskResults - TaskStages TaskStages - Teams Teams - TeamAccess TeamAccesses - TeamMembers TeamMembers - TeamNotificationConfigurations TeamNotificationConfigurations - TeamProjectAccess TeamProjectAccesses - TeamTokens TeamTokens - TestRuns TestRuns - TestVariables TestVariables - Users Users - UserTokens UserTokens - Variables Variables - VariableSets VariableSets - VariableSetVariables VariableSetVariables - Workspaces Workspaces - WorkspaceResources WorkspaceResources - WorkspaceRunTasks WorkspaceRunTasks - Projects Projects + Admin Admin + Agents Agents + AgentPools AgentPools + AgentTokens AgentTokens + Applies Applies + AuditTrails AuditTrails + Comments Comments + ConfigurationVersions ConfigurationVersions + CostEstimates CostEstimates + GHAInstallations GHAInstallations + GPGKeys GPGKeys + NotificationConfigurations NotificationConfigurations + OAuthClients OAuthClients + OAuthTokens OAuthTokens + Organizations Organizations + OrganizationMemberships OrganizationMemberships + OrganizationTags OrganizationTags + OrganizationTokens OrganizationTokens + Plans Plans + PlanExports PlanExports + Policies Policies + PolicyChecks PolicyChecks + PolicyEvaluations PolicyEvaluations + PolicySetOutcomes PolicySetOutcomes + PolicySetParameters PolicySetParameters + PolicySetVersions PolicySetVersions + PolicySets PolicySets + RegistryModules RegistryModules + RegistryNoCodeModules RegistryNoCodeModules + RegistryProviders RegistryProviders + RegistryProviderPlatforms RegistryProviderPlatforms + RegistryProviderVersions RegistryProviderVersions + Runs Runs + RunEvents RunEvents + RunTasks RunTasks + RunTasksIntegration RunTasksIntegration + RunTriggers RunTriggers + SSHKeys SSHKeys + Stacks Stacks + StackConfigurations StackConfigurations + StackDeployments StackDeployments + StackPlans StackPlans + StackPlanOperations StackPlanOperations + StackSources StackSources + StateVersionOutputs StateVersionOutputs + StateVersions StateVersions + TaskResults TaskResults + TaskStages TaskStages + Teams Teams + TeamAccess TeamAccesses + TeamMembers TeamMembers + TeamProjectAccess TeamProjectAccesses + TeamTokens TeamTokens + TestRuns TestRuns + TestVariables TestVariables + Users Users + UserTokens UserTokens + Variables Variables + VariableSets VariableSets + VariableSetVariables VariableSetVariables + Workspaces Workspaces + WorkspaceResources WorkspaceResources + WorkspaceRunTasks WorkspaceRunTasks + Projects Projects Meta Meta } @@ -502,7 +501,6 @@ func NewClient(cfg *Config) (*Client, error) { client.TaskStages = &taskStages{client: client} client.TeamAccess = &teamAccesses{client: client} client.TeamMembers = &teamMembers{client: client} - client.TeamNotificationConfigurations = &teamNotificationConfigurations{client: client} client.TeamProjectAccess = &teamProjectAccesses{client: client} client.Teams = &teams{client: client} client.TeamTokens = &teamTokens{client: client} From bd9a54b976d01e13737bd336d5cc08d14a03e141 Mon Sep 17 00:00:00 2001 From: Taylor Chaparro Date: Thu, 2 Jan 2025 12:15:20 -0800 Subject: [PATCH 05/10] update NC interface signatures, omit deprecated subscribable, backfill subscribable --- notification_configuration.go | 46 +++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/notification_configuration.go b/notification_configuration.go index bd74a2d0b..64e5b5b3b 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) @@ -107,7 +107,7 @@ type NotificationConfiguration struct { // Relations // DEPRECATED. The subscribable field is polymorphic. Use NotificationConfigurationSubscribableChoice instead. - Subscribable *Workspace `jsonapi:"relation,subscribable"` + Subscribable *Workspace `jsonapi:"relation,subscribable,omitempty"` SubscribableChoice *NotificationConfigurationSubscribableChoice `jsonapi:"polyrelation,subscribable"` EmailUsers []*User `jsonapi:"relation,users"` @@ -128,7 +128,7 @@ type DeliveryResponse struct { type NotificationConfigurationListOptions struct { ListOptions - SubscribableChoice *NotificationConfigurationSubscribableChoice `jsonapi:"polyrelation,subscribable"` + SubscribableChoice *NotificationConfigurationSubscribableChoice } // NotificationConfigurationCreateOptions represents the options for @@ -204,18 +204,28 @@ type NotificationConfigurationUpdateOptions struct { // List all the notification configurations associated with a workspace. func (s *notificationConfigurations) List(ctx context.Context, subscribableID string, options *NotificationConfigurationListOptions) (*NotificationConfigurationList, error) { var u string - if options == nil || options.SubscribableChoice == nil || options.SubscribableChoice.Workspace != nil { - if !validStringID(&subscribableID) { - return nil, ErrInvalidWorkspaceID + if options == nil { + options = &NotificationConfigurationListOptions{ + SubscribableChoice: &NotificationConfigurationSubscribableChoice{ + Workspace: &Workspace{ID: subscribableID}, + }, } - u = fmt.Sprintf("workspaces/%s/notification-configurations", url.PathEscape(subscribableID)) - } else if options.SubscribableChoice.Team != nil { + } 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 { - return nil, ErrInvalidNotificationConfigSubscribableChoice + if !validStringID(&subscribableID) { + return nil, ErrInvalidWorkspaceID + } + u = fmt.Sprintf("workspaces/%s/notification-configurations", url.PathEscape(subscribableID)) } req, err := s.client.NewRequest("GET", u, options) @@ -229,6 +239,10 @@ func (s *notificationConfigurations) List(ctx context.Context, subscribableID st return nil, err } + for i := range ncl.Items { + backfillDeprecatedSubscribable(ncl.Items[i]) + } + return ncl, nil } @@ -270,6 +284,8 @@ func (s *notificationConfigurations) Create(ctx context.Context, subscribableID return nil, err } + backfillDeprecatedSubscribable(nc) + return nc, nil } @@ -394,6 +410,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 { From 22a04151b45d1c336de26f9a61105c1e3cc070c5 Mon Sep 17 00:00:00 2001 From: Taylor Chaparro Date: Thu, 2 Jan 2025 12:22:08 -0800 Subject: [PATCH 06/10] update notification configuration mock signatures --- mocks/notification_configuration_mocks.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) 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. From e4e84f526739b7ee042afc1504a83418a96f2ef8 Mon Sep 17 00:00:00 2001 From: Taylor Chaparro Date: Thu, 2 Jan 2025 13:18:06 -0800 Subject: [PATCH 07/10] fix tests, update run triggers to handle deprecated in the same way --- notification_configuration.go | 4 ++++ notification_configuration_integration_test.go | 2 +- run_trigger.go | 18 +++++++++++++++++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/notification_configuration.go b/notification_configuration.go index 64e5b5b3b..d0a5706de 100644 --- a/notification_configuration.go +++ b/notification_configuration.go @@ -307,6 +307,8 @@ func (s *notificationConfigurations) Read(ctx context.Context, notificationConfi return nil, err } + backfillDeprecatedSubscribable(nc) + return nc, nil } @@ -332,6 +334,8 @@ func (s *notificationConfigurations) Update(ctx context.Context, notificationCon return nil, err } + backfillDeprecatedSubscribable(nc) + return nc, nil } diff --git a/notification_configuration_integration_test.go b/notification_configuration_integration_test.go index d81ede21d..88f30fa64 100644 --- a/notification_configuration_integration_test.go +++ b/notification_configuration_integration_test.go @@ -70,7 +70,7 @@ func TestNotificationConfigurationList(t *testing.T) { nil, ) assert.Nil(t, ncl) - assert.EqualError(t, err, ErrInvalidWorkspaceID.Error()) + assert.EqualError(t, err, ErrRequiredDestinationType.Error()) }) } diff --git a/run_trigger.go b/run_trigger.go index e002271fa..f59671935 100644 --- a/run_trigger.go +++ b/run_trigger.go @@ -57,7 +57,7 @@ type RunTrigger struct { SourceableName string `jsonapi:"attr,sourceable-name"` WorkspaceName string `jsonapi:"attr,workspace-name"` // DEPRECATED. The sourceable field is polymorphic. Use SourceableChoice instead. - Sourceable *Workspace `jsonapi:"relation,sourceable"` + Sourceable *Workspace `jsonapi:"relation,sourceable,omitempty"` SourceableChoice *SourceableChoice `jsonapi:"polyrelation,sourceable"` Workspace *Workspace `jsonapi:"relation,workspace"` } @@ -121,6 +121,10 @@ func (s *runTriggers) List(ctx context.Context, workspaceID string, options *Run return nil, err } + for i := range rtl.Items { + backfillDeprecatedSourceable(rtl.Items[i]) + } + return rtl, nil } @@ -145,6 +149,8 @@ func (s *runTriggers) Create(ctx context.Context, workspaceID string, options Ru return nil, err } + backfillDeprecatedSourceable(rt) + return rt, nil } @@ -166,6 +172,8 @@ func (s *runTriggers) Read(ctx context.Context, runTriggerID string) (*RunTrigge return nil, err } + backfillDeprecatedSourceable(rt) + return rt, nil } @@ -203,6 +211,14 @@ func (o *RunTriggerListOptions) valid() error { return nil } +func backfillDeprecatedSourceable(runTrigger *RunTrigger) { + if runTrigger.Sourceable != nil || runTrigger.SourceableChoice == nil { + return + } + + runTrigger.Sourceable = runTrigger.SourceableChoice.Workspace +} + func validateRunTriggerFilterParam(filterParam RunTriggerFilterOp, includeParams []RunTriggerIncludeOpt) error { switch filterParam { case RunTriggerOutbound, RunTriggerInbound: From fc1e263d45ac960b8ffe0628c4633823fcb7f728 Mon Sep 17 00:00:00 2001 From: Taylor Chaparro Date: Thu, 2 Jan 2025 15:00:26 -0800 Subject: [PATCH 08/10] move create validation to valid receiver --- errors.go | 2 -- notification_configuration.go | 36 +++++++++---------- ...fication_configuration_integration_test.go | 2 +- 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/errors.go b/errors.go index d1f027fca..935f5b361 100644 --- a/errors.go +++ b/errors.go @@ -145,8 +145,6 @@ var ( ErrInvalidNotificationConfigID = errors.New("invalid value for notification configuration ID") - ErrInvalidNotificationConfigSubscribableChoice = errors.New("invalid value for notification configuration subscribable choice") - ErrInvalidMembership = errors.New("invalid value for membership") ErrInvalidMembershipIDs = errors.New("invalid value for organization membership ids") diff --git a/notification_configuration.go b/notification_configuration.go index d0a5706de..87a7c60b1 100644 --- a/notification_configuration.go +++ b/notification_configuration.go @@ -248,28 +248,18 @@ func (s *notificationConfigurations) List(ctx context.Context, subscribableID st // Create a notification configuration with the given options. func (s *notificationConfigurations) Create(ctx context.Context, subscribableID string, options NotificationConfigurationCreateOptions) (*NotificationConfiguration, error) { - if err := options.valid(); err != nil { - return nil, err - } - var u string var subscribableChoice *NotificationConfigurationSubscribableChoice - if options.SubscribableChoice == nil || options.SubscribableChoice.Workspace != nil { - if !validStringID(&subscribableID) { - return nil, ErrInvalidWorkspaceID - } - + if options.SubscribableChoice == nil || options.SubscribableChoice.Team == nil { u = fmt.Sprintf("workspaces/%s/notification-configurations", url.PathEscape(subscribableID)) - subscribableChoice = &NotificationConfigurationSubscribableChoice{Workspace: &Workspace{ID: subscribableID}} - } else if options.SubscribableChoice != nil && options.SubscribableChoice.Team != nil { - if !validStringID(&subscribableID) { - return nil, ErrInvalidTeamID - } - - u = fmt.Sprintf("teams/%s/notification-configurations", url.PathEscape(subscribableID)) - subscribableChoice = &NotificationConfigurationSubscribableChoice{Team: &Team{ID: subscribableID}} + options.SubscribableChoice = &NotificationConfigurationSubscribableChoice{Workspace: &Workspace{ID: subscribableID}} } else { - return nil, ErrInvalidNotificationConfigSubscribableChoice + 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 } req, err := s.client.NewRequest("POST", u, &options) @@ -378,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 } diff --git a/notification_configuration_integration_test.go b/notification_configuration_integration_test.go index 88f30fa64..d81ede21d 100644 --- a/notification_configuration_integration_test.go +++ b/notification_configuration_integration_test.go @@ -70,7 +70,7 @@ func TestNotificationConfigurationList(t *testing.T) { nil, ) assert.Nil(t, ncl) - assert.EqualError(t, err, ErrRequiredDestinationType.Error()) + assert.EqualError(t, err, ErrInvalidWorkspaceID.Error()) }) } From 518ceb022e0360754e0763e288652709a1739ba0 Mon Sep 17 00:00:00 2001 From: Taylor Chaparro Date: Tue, 7 Jan 2025 13:26:14 -0800 Subject: [PATCH 09/10] run go mod tidy --- go.sum | 2 -- 1 file changed, 2 deletions(-) diff --git a/go.sum b/go.sum index 6391cb009..da2eab7f2 100644 --- a/go.sum +++ b/go.sum @@ -20,8 +20,6 @@ github.com/hashicorp/jsonapi v1.3.2 h1:gP3fX2ZT7qXi+PbwieptzkspIohO2kCSiBUvUTBAb github.com/hashicorp/jsonapi v1.3.2/go.mod h1:kWfdn49yCjQvbpnvY1dxxAuAFzISwrrMDQOcu6NsFoM= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/notchairmk/jsonapi v0.0.0-20241223221631-b0c6a5b7edd8 h1:Nll3UptyKamtMP60oCHnRKI3l/kgadZHKQ6/uLYPyVM= -github.com/notchairmk/jsonapi v0.0.0-20241223221631-b0c6a5b7edd8/go.mod h1:kWfdn49yCjQvbpnvY1dxxAuAFzISwrrMDQOcu6NsFoM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= From bb7b35f77f9af0822f7be10ac752c6967e37b50a Mon Sep 17 00:00:00 2001 From: Taylor Chaparro Date: Wed, 8 Jan 2025 10:24:02 -0800 Subject: [PATCH 10/10] omitempty when creating notification subscribable choice --- notification_configuration.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notification_configuration.go b/notification_configuration.go index 87a7c60b1..ad6c8dfae 100644 --- a/notification_configuration.go +++ b/notification_configuration.go @@ -166,7 +166,7 @@ type NotificationConfigurationCreateOptions struct { EmailUsers []*User `jsonapi:"relation,users,omitempty"` // Required: The workspace or team that the notification configuration is associated with. - SubscribableChoice *NotificationConfigurationSubscribableChoice `jsonapi:"polyrelation,subscribable"` + SubscribableChoice *NotificationConfigurationSubscribableChoice `jsonapi:"polyrelation,subscribable,omitempty"` } // NotificationConfigurationUpdateOptions represents the options for