diff --git a/monitor_alert_channels.go b/monitor_alert_channels.go index 7de93d86e..7c53148d4 100644 --- a/monitor_alert_channels.go +++ b/monitor_alert_channels.go @@ -77,8 +77,60 @@ func (a *AlertChannel) UnmarshalJSON(b []byte) error { return nil } +// AlertChannelCreateOptions represents options for creating an alert notification channel. +type AlertChannelCreateOptions struct { + ChannelType AlertNotificationType `json:"channel_type"` + Details AlertChannelDetailsOptions `json:"details"` + Label *string `json:"label,omitzero"` +} + +// AlertChannelDetailsOptions represents the details configuration for an alert channel. +type AlertChannelDetailsOptions struct { + Email *EmailChannelCreateOptions `json:"email,omitzero"` +} + +// EmailChannelCreateOptions represents email-specific configuration for an alert channel. +type EmailChannelCreateOptions struct { + Usernames []string `json:"usernames"` + RecipientType *string `json:"recipient_type,omitzero"` +} + +// AlertChannelUpdateOptions represents options for updating an alert notification channel. +type AlertChannelUpdateOptions struct { + Details *AlertChannelUpdateDetailsOptions `json:"details,omitzero"` + Label *string `json:"label,omitzero"` +} + +// AlertChannelUpdateDetailsOptions represents update details for an alert channel. +type AlertChannelUpdateDetailsOptions struct { + Email *EmailChannelUpdateOptions `json:"email,omitzero"` +} + +// EmailChannelUpdateOptions represents email-specific update configuration for an alert channel. +type EmailChannelUpdateOptions struct { + Usernames []string `json:"usernames,omitzero"` +} + // ListAlertChannels gets a paginated list of Alert Channels. func (c *Client) ListAlertChannels(ctx context.Context, opts *ListOptions) ([]AlertChannel, error) { endpoint := formatAPIPath("monitor/alert-channels") return getPaginatedResults[AlertChannel](ctx, c, endpoint, opts) } + +// CreateAlertChannel creates a new alert notification channel. +func (c *Client) CreateAlertChannel(ctx context.Context, opts AlertChannelCreateOptions) (*AlertChannel, error) { + endpoint := formatAPIPath("monitor/alert-channels") + return doPOSTRequest[AlertChannel](ctx, c, endpoint, opts) +} + +// UpdateAlertChannel updates an alert notification channel. +func (c *Client) UpdateAlertChannel(ctx context.Context, channelID int, opts AlertChannelUpdateOptions) (*AlertChannel, error) { + endpoint := formatAPIPath("monitor/alert-channels/%d", channelID) + return doPUTRequest[AlertChannel](ctx, c, endpoint, opts) +} + +// DeleteAlertChannel deletes an alert notification channel. +func (c *Client) DeleteAlertChannel(ctx context.Context, channelID int) error { + endpoint := formatAPIPath("monitor/alert-channels/%d", channelID) + return doDELETERequest(ctx, c, endpoint) +} diff --git a/test/integration/fixtures/TestMonitorAlertChannel_Create.yaml b/test/integration/fixtures/TestMonitorAlertChannel_Create.yaml new file mode 100644 index 000000000..7d627db54 --- /dev/null +++ b/test/integration/fixtures/TestMonitorAlertChannel_Create.yaml @@ -0,0 +1,276 @@ +--- +version: 1 +interactions: +- request: + body: "" + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + User-Agent: + - linodego/dev https://github.com/linode/linodego + url: https://api.linode.com/v4beta/account/users?page=1 + method: GET + response: + body: '{"data": [{"username": "test_user1", "email": "test_user1@example.com", + "restricted": false, "ssh_keys": [], "tfa_enabled": true, "verified_phone_number": + "+48695154186", "password_created": "2018-01-02T03:04:05", "last_login": {"login_datetime": + "2018-01-02T03:04:05", "status": "successful"}, "user_type": "default"}, {"username": + "test_user2", "email": "test_user2@example.com", "restricted": false, + "ssh_keys": ["bfenski", "fenio", "fenio", "fenio", "fenio"], "tfa_enabled": + true, "verified_phone_number": "+48513467293", "password_created": "2018-01-02T03:04:05", + "last_login": {"login_datetime": "2018-01-02T03:04:05", "status": "successful"}, + "user_type": "default"}], "page": 1, "pages": 1, "results": 2}' + headers: + Access-Control-Allow-Credentials: + - "true" + Access-Control-Allow-Headers: + - Authorization, Origin, X-Requested-With, Content-Type, Accept, X-Filter + Access-Control-Allow-Methods: + - HEAD, GET, OPTIONS, POST, PUT, DELETE + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Status + Akamai-Internal-Account: + - '*' + Cache-Control: + - max-age=0, no-cache, no-store + Connection: + - keep-alive + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json + Expires: + - Mon, 06 Jul 2026 18:46:20 GMT + Pragma: + - no-cache + Strict-Transport-Security: + - max-age=31536000 + Vary: + - Authorization, X-Filter + - Authorization, X-Filter + - Accept-Encoding + X-Accepted-Oauth-Scopes: + - account:read_only + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + - DENY + X-Oauth-Scopes: + - account:read_only databases:read_only domains:read_only events:read_only firewall:read_only + images:read_only ips:read_only linodes:read_only lke:read_only longview:read_only + monitor:read_write nodebalancers:read_only object_storage:read_only stackscripts:read_only + volumes:read_only + X-Ratelimit-Limit: + - "1840" + X-Xss-Protection: + - 1; mode=block + status: 200 OK + code: 200 + duration: "" +- request: + body: '{"channel_type":"email","details":{"email":{"usernames":["test_user1","test_user2"],"recipient_type":"user"}},"label":"linodego-sdk-test-alert-channel"}' + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + User-Agent: + - linodego/dev https://github.com/linode/linodego + url: https://api.linode.com/v4beta/monitor/alert-channels + method: POST + response: + body: '{"id": 10284, "label": "linodego-sdk-test-alert-channel", "channel_type": + "email", "type": "user", "details": {"email": {"usernames": ["test_user1", + "test_user2"], "recipient_type": "user"}}, "alerts": {"url": "/monitor/alert-channels/10284/alerts", + "type": "alerts-definitions", "alert_count": 0}, "created": "2018-01-02T03:04:05", + "updated": "2018-01-02T03:04:05", "created_by": "tester", "updated_by": + "tester"}' + headers: + Access-Control-Allow-Credentials: + - "true" + Access-Control-Allow-Headers: + - Authorization, Origin, X-Requested-With, Content-Type, Accept, X-Filter + Access-Control-Allow-Methods: + - HEAD, GET, OPTIONS, POST, PUT, DELETE + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Status + Akamai-Internal-Account: + - '*' + Cache-Control: + - max-age=0, no-cache, no-store + Connection: + - keep-alive + Content-Length: + - "470" + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json + Expires: + - Mon, 06 Jul 2026 18:46:21 GMT + Pragma: + - no-cache + Strict-Transport-Security: + - max-age=31536000 + Vary: + - Authorization, X-Filter + X-Accepted-Oauth-Scopes: + - monitor:read_write + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + - DENY + X-Oauth-Scopes: + - account:read_only databases:read_only domains:read_only events:read_only firewall:read_only + images:read_only ips:read_only linodes:read_only lke:read_only longview:read_only + monitor:read_write nodebalancers:read_only object_storage:read_only stackscripts:read_only + volumes:read_only + X-Ratelimit-Limit: + - "1840" + X-Xss-Protection: + - 1; mode=block + status: 200 OK + code: 200 + duration: "" +- request: + body: '{"details":{"email":{"usernames":["test_user1","test_user2"]}},"label":"linodego-sdk-test-alert-channel-updated"}' + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + User-Agent: + - linodego/dev https://github.com/linode/linodego + url: https://api.linode.com/v4beta/monitor/alert-channels/10284 + method: PUT + response: + body: '{"id": 10284, "label": "linodego-sdk-test-alert-channel-updated", "channel_type": + "email", "type": "user", "details": {"email": {"usernames": ["test_user1", + "test_user2"], "recipient_type": "user"}}, "alerts": {"url": "/monitor/alert-channels/10284/alerts", + "type": "alerts-definitions", "alert_count": 0}, "created": "2018-01-02T03:04:05", + "created_by": "tester", "updated_by": "tester"}' + headers: + Access-Control-Allow-Credentials: + - "true" + Access-Control-Allow-Headers: + - Authorization, Origin, X-Requested-With, Content-Type, Accept, X-Filter + Access-Control-Allow-Methods: + - HEAD, GET, OPTIONS, POST, PUT, DELETE + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Status + Akamai-Internal-Account: + - '*' + Cache-Control: + - max-age=0, no-cache, no-store + Connection: + - keep-alive + Content-Length: + - "478" + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json + Expires: + - Mon, 06 Jul 2026 18:46:22 GMT + Pragma: + - no-cache + Strict-Transport-Security: + - max-age=31536000 + Vary: + - Authorization, X-Filter + X-Accepted-Oauth-Scopes: + - monitor:read_write + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + - DENY + X-Oauth-Scopes: + - account:read_only databases:read_only domains:read_only events:read_only firewall:read_only + images:read_only ips:read_only linodes:read_only lke:read_only longview:read_only + monitor:read_write nodebalancers:read_only object_storage:read_only stackscripts:read_only + volumes:read_only + X-Ratelimit-Limit: + - "1840" + X-Xss-Protection: + - 1; mode=block + status: 200 OK + code: 200 + duration: "" +- request: + body: "" + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + User-Agent: + - linodego/dev https://github.com/linode/linodego + url: https://api.linode.com/v4beta/monitor/alert-channels/10284 + method: DELETE + response: + body: '{}' + headers: + Access-Control-Allow-Credentials: + - "true" + Access-Control-Allow-Headers: + - Authorization, Origin, X-Requested-With, Content-Type, Accept, X-Filter + Access-Control-Allow-Methods: + - HEAD, GET, OPTIONS, POST, PUT, DELETE + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Status + Akamai-Internal-Account: + - '*' + Cache-Control: + - max-age=0, no-cache, no-store + Connection: + - keep-alive + Content-Length: + - "2" + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json + Expires: + - Mon, 06 Jul 2026 18:46:25 GMT + Pragma: + - no-cache + Strict-Transport-Security: + - max-age=31536000 + Vary: + - Authorization, X-Filter + X-Accepted-Oauth-Scopes: + - monitor:read_write + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + - DENY + X-Oauth-Scopes: + - account:read_only databases:read_only domains:read_only events:read_only firewall:read_only + images:read_only ips:read_only linodes:read_only lke:read_only longview:read_only + monitor:read_write nodebalancers:read_only object_storage:read_only stackscripts:read_only + volumes:read_only + X-Ratelimit-Limit: + - "1840" + X-Xss-Protection: + - 1; mode=block + status: 200 OK + code: 200 + duration: "" diff --git a/test/integration/monitor_alert_definitions_test.go b/test/integration/monitor_alerts_test.go similarity index 81% rename from test/integration/monitor_alert_definitions_test.go rename to test/integration/monitor_alerts_test.go index 42050d3d9..5dd601f92 100644 --- a/test/integration/monitor_alert_definitions_test.go +++ b/test/integration/monitor_alerts_test.go @@ -8,6 +8,7 @@ import ( "github.com/linode/linodego/v2" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) const ( @@ -331,3 +332,79 @@ func TestMonitorAlertDefinitionEntities_List(t *testing.T) { assert.NotEmpty(t, entity.URL) } } + +func TestMonitorAlertChannel_Create_smoke(t *testing.T) { + client, teardown := createTestClient(t, "fixtures/TestMonitorAlertChannel_Create") + defer teardown() + + // Get valid users to use for the email alert channel + users, err := client.ListUsers(context.Background(), nil) + if err != nil { + t.Fatalf("Error listing users: %v", err) + } + + label := "linodego-sdk-test-alert-channel" + recipientType := "user" + + createOpts := linodego.AlertChannelCreateOptions{ + ChannelType: linodego.EmailAlertNotification, + Label: &label, + Details: linodego.AlertChannelDetailsOptions{ + Email: &linodego.EmailChannelCreateOptions{ + Usernames: []string{users[0].Username, users[1].Username}, + RecipientType: &recipientType, + }, + }, + } + + // Create the alert channel + channel, err := client.CreateAlertChannel(context.Background(), createOpts) + require.NoError(t, err) + require.NotNil(t, channel) + + // Delete the created alert channel after the test completes + defer func() { + if channel != nil { + time.Sleep(2 * time.Second) + if err := client.DeleteAlertChannel(context.Background(), channel.ID); err != nil { + t.Logf("DeleteAlertChannel returned error: %#v", err) + } + } + }() + + assert.NotZero(t, channel.ID) + assert.Equal(t, label, channel.Label) + assert.Equal(t, createOpts.ChannelType, channel.ChannelType) + assert.Equal(t, linodego.UserAlertChannel, channel.Type) + + require.NotNil(t, channel.Details.Email) + assert.Equal(t, createOpts.Details.Email.Usernames, channel.Details.Email.Usernames) + assert.Equal(t, recipientType, channel.Details.Email.RecipientType) + + assert.NotEmpty(t, channel.Alerts.URL) + assert.NotEmpty(t, channel.Alerts.Type) + assert.GreaterOrEqual(t, channel.Alerts.AlertCount, 0) + + assertDateSet(t, channel.Created) + assertDateSet(t, channel.Updated) + + // Update the created alert channel + updatedLabel := label + "-updated" + updateOpts := linodego.AlertChannelUpdateOptions{ + Label: &updatedLabel, + Details: &linodego.AlertChannelUpdateDetailsOptions{ + Email: &linodego.EmailChannelUpdateOptions{ + Usernames: []string{users[0].Username, users[1].Username}, + }, + }, + } + updatedChannel, err := client.UpdateAlertChannel(context.Background(), channel.ID, updateOpts) + require.NoError(t, err) + require.NotNil(t, updatedChannel) + + assert.Equal(t, channel.ID, updatedChannel.ID) + assert.Equal(t, updatedLabel, updatedChannel.Label) + assert.Equal(t, createOpts.ChannelType, updatedChannel.ChannelType) + require.NotNil(t, updatedChannel.Details.Email) + assert.Equal(t, createOpts.Details.Email.Usernames, updatedChannel.Details.Email.Usernames) +} diff --git a/test/unit/monitor_alert_channels_test.go b/test/unit/monitor_alert_channels_test.go index 6b4bac32f..2cbffec6f 100644 --- a/test/unit/monitor_alert_channels_test.go +++ b/test/unit/monitor_alert_channels_test.go @@ -10,35 +10,81 @@ import ( "github.com/stretchr/testify/require" ) -const monitorAlertChannelListResponse = `{ - "data": [{ - "id": 123, - "label": "alert notification channel", +const ( + monitorAlertChannelListResponse = `{ + "data": [{ + "id": 123, + "label": "alert notification channel", + "channel_type": "email", + "type": "user", + "details": { + "email": { + "usernames": [ + "admin-user1", + "admin-user2" + ], + "recipient_type": "user" + } + }, + "alerts": { + "url": "/monitor/alert-channels/123/alerts", + "type": "alerts-definitions", + "alert_count": 0 + }, + "created": "2024-01-01T00:00:00", + "updated": "2024-01-01T00:00:00", + "created_by": "tester", + "updated_by": "tester" + }], + "page": 1, + "pages": 1, + "results": 1 + }` + + monitorAlertChannelCreateResponse = `{ + "id": 10000, + "label": "Email Alert Channel", "channel_type": "email", "type": "user", + "created": "2026-06-23T09:43:00", + "created_by": "tester", + "updated": "2026-06-23T09:43:00", + "updated_by": "tester", "details": { "email": { - "usernames": [ - "admin-user1", - "admin-user2" - ], - "recipient_type": "user" + "recipient_type": "user", + "usernames": ["tester1", "tester2"] } }, "alerts": { - "url": "/monitor/alert-channels/123/alerts", - "type": "alerts-definitions", - "alert_count": 0 - }, - "created": "2024-01-01T00:00:00", - "updated": "2024-01-01T00:00:00", + "alert_count": 0, + "type": "alert-definitions", + "url": "/monitor/alert-channels/10000/alerts" + } + }` + + monitorAlertChannelUpdateResponse = `{ + "id": 10000, + "label": "Email Alert Channel Updated", + "channel_type": "email", + "type": "user", + "created": "2026-06-23T09:43:00", "created_by": "tester", - "updated_by": "tester" - }], - "page": 1, - "pages": 1, - "results": 1 -}` + "updated": "2026-06-24T09:43:00", + "updated_by": "tester", + "details": { + "email": { + "recipient_type": "user", + "usernames": ["tester"] + } + }, + "alerts": { + "alert_count": 0, + "type": "alert-definitions", + "url": "/monitor/alert-channels/10000/alerts" + } + }` +) func TestListAlertChannels(t *testing.T) { var base ClientBaseCase @@ -62,3 +108,73 @@ func TestListAlertChannels(t *testing.T) { assert.Equal(t, 0, channel.Alerts.AlertCount) assert.Equal(t, "/monitor/alert-channels/123/alerts", channel.Alerts.URL) } + +func TestCreateAlertChannel(t *testing.T) { + var base ClientBaseCase + base.SetUp(t) + defer base.TearDown(t) + + base.MockPost("monitor/alert-channels", json.RawMessage(monitorAlertChannelCreateResponse)) + + opts := linodego.AlertChannelCreateOptions{ + ChannelType: linodego.EmailAlertNotification, + Label: linodego.Pointer("Email Alert Channel"), + Details: linodego.AlertChannelDetailsOptions{ + Email: &linodego.EmailChannelCreateOptions{ + Usernames: []string{"tester1", "tester2"}, + }, + }, + } + + channel, err := base.Client.CreateAlertChannel(context.Background(), opts) + require.NoError(t, err) + require.NotNil(t, channel) + + assert.Equal(t, 10000, channel.ID) + assert.Equal(t, "Email Alert Channel", channel.Label) + assert.Equal(t, linodego.EmailAlertNotification, channel.ChannelType) + assert.Equal(t, linodego.UserAlertChannel, channel.Type) + require.NotNil(t, channel.Details.Email) + assert.Equal(t, []string{"tester1", "tester2"}, channel.Details.Email.Usernames) + assert.Equal(t, "user", channel.Details.Email.RecipientType) + assert.Equal(t, 0, channel.Alerts.AlertCount) + assert.Equal(t, "/monitor/alert-channels/10000/alerts", channel.Alerts.URL) +} + +func TestDeleteAlertChannel(t *testing.T) { + var base ClientBaseCase + base.SetUp(t) + defer base.TearDown(t) + + base.MockDelete("monitor/alert-channels/10000", nil) + + err := base.Client.DeleteAlertChannel(context.Background(), 10000) + assert.NoError(t, err) +} + +func TestUpdateAlertChannel(t *testing.T) { + var base ClientBaseCase + base.SetUp(t) + defer base.TearDown(t) + + base.MockPut("monitor/alert-channels/10000", json.RawMessage(monitorAlertChannelUpdateResponse)) + + label := "Email Alert Channel Updated" + opts := linodego.AlertChannelUpdateOptions{ + Label: &label, + } + + channel, err := base.Client.UpdateAlertChannel(context.Background(), 10000, opts) + require.NoError(t, err) + require.NotNil(t, channel) + + assert.Equal(t, 10000, channel.ID) + assert.Equal(t, label, channel.Label) + assert.Equal(t, linodego.EmailAlertNotification, channel.ChannelType) + assert.Equal(t, linodego.UserAlertChannel, channel.Type) + require.NotNil(t, channel.Details.Email) + assert.Equal(t, []string{"tester"}, channel.Details.Email.Usernames) + assert.Equal(t, "user", channel.Details.Email.RecipientType) + assert.Equal(t, 0, channel.Alerts.AlertCount) + assert.Equal(t, "/monitor/alert-channels/10000/alerts", channel.Alerts.URL) +}