diff --git a/docs/docs.go b/docs/docs.go index 46a64e53..01a7ef74 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -29671,6 +29671,9 @@ const docTemplate = `{ "enabled": { "type": "boolean" }, + "error": { + "type": "string" + }, "metadata": { "type": "object", "additionalProperties": { diff --git a/docs/swagger.json b/docs/swagger.json index 6fa337f6..4eed8ef6 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -29665,6 +29665,9 @@ "enabled": { "type": "boolean" }, + "error": { + "type": "string" + }, "metadata": { "type": "object", "additionalProperties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index b03b756d..c740ac6d 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1708,6 +1708,8 @@ definitions: type: string enabled: type: boolean + error: + type: string metadata: additionalProperties: type: string diff --git a/internal/api/handler/notifications.go b/internal/api/handler/notifications.go index ecd2bd8e..d1bdbe49 100644 --- a/internal/api/handler/notifications.go +++ b/internal/api/handler/notifications.go @@ -51,6 +51,7 @@ type availableNotificationProviderResponse struct { DisplayName string `json:"displayName"` Description string `json:"description"` Enabled bool `json:"enabled"` + Error string `json:"error,omitempty"` Metadata map[string]string `json:"metadata,omitempty"` } @@ -359,6 +360,7 @@ func (h *NotificationsHandler) ListNotificationProviders(ctx echo.Context) error DisplayName: provider.DisplayName, Description: provider.Description, Enabled: provider.Enabled, + Error: provider.Error, Metadata: provider.Metadata, }) } diff --git a/internal/api/handler/notifications_test.go b/internal/api/handler/notifications_test.go index 0f2abe96..0389608f 100644 --- a/internal/api/handler/notifications_test.go +++ b/internal/api/handler/notifications_test.go @@ -19,6 +19,26 @@ import ( "go.uber.org/zap" ) +type stubProviderCatalog struct { + providers []notification.ProviderMetadata +} + +func (s stubProviderCatalog) Provider(providerID string) (notification.Provider, bool) { + return nil, false +} + +func (s stubProviderCatalog) ProviderIDs() []string { + providerIDs := make([]string, 0, len(s.providers)) + for _, provider := range s.providers { + providerIDs = append(providerIDs, provider.ProviderType) + } + return providerIDs +} + +func (s stubProviderCatalog) Providers() []notification.ProviderMetadata { + return append([]notification.ProviderMetadata(nil), s.providers...) +} + type stubNotificationTestEnqueuer struct { started bool jobIDs []int64 @@ -71,3 +91,37 @@ func TestSendTestNotificationReturnsJobIDs(t *testing.T) { require.Equal(t, []int64{42}, response.Data.JobIDs) require.Equal(t, notification.DeliveryChannelEmail, response.Data.ProviderType) } + +func TestListNotificationProvidersReturnsProviderErrors(t *testing.T) { + e := echo.New() + handler := &NotificationsHandler{ + sugar: zap.NewNop().Sugar(), + providers: stubProviderCatalog{providers: []notification.ProviderMetadata{ + { + ProviderType: notification.DeliveryChannelSlack, + DisplayName: "Slack", + Description: "Configured Slack workspace", + Enabled: true, + Error: "invalid_auth", + }, + }}, + } + + req := httptest.NewRequest(http.MethodGet, "/api/admin/notifications/providers", nil) + rec := httptest.NewRecorder() + + err := handler.ListNotificationProviders(e.NewContext(req, rec)) + require.NoError(t, err) + require.Equal(t, http.StatusOK, rec.Code) + + var response GenericDataListResponse[availableNotificationProviderResponse] + require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &response)) + require.Len(t, response.Data, 1) + require.Equal(t, availableNotificationProviderResponse{ + ProviderType: notification.DeliveryChannelSlack, + DisplayName: "Slack", + Description: "Configured Slack workspace", + Enabled: true, + Error: "invalid_auth", + }, response.Data[0]) +} diff --git a/internal/service/notification/providers/slack/provider.go b/internal/service/notification/providers/slack/provider.go index 55f19d27..169afe96 100644 --- a/internal/service/notification/providers/slack/provider.go +++ b/internal/service/notification/providers/slack/provider.go @@ -90,6 +90,7 @@ type Provider struct { workspaceConfigurationMu sync.Mutex workspaceConfigurationLoaded bool workspaceConfiguration slacksvc.WorkspaceConfiguration + workspaceConfigurationErr error } const ( @@ -163,7 +164,10 @@ func (p *Provider) ProviderMetadata() notification.ProviderMetadata { Enabled: p.enabled(), } - configuration := p.workspaceConfigurationDetails() + configuration, err := p.workspaceConfigurationDetails() + if err != nil { + metadata.Error = err.Error() + } if configuration.WorkspaceName != "" { metadata.Description = fmt.Sprintf("Configured Slack workspace %s", configuration.WorkspaceName) } @@ -213,16 +217,16 @@ func (p *Provider) enabled() bool { return sender != nil && sender.IsEnabled() } -func (p *Provider) workspaceConfigurationDetails() slacksvc.WorkspaceConfiguration { +func (p *Provider) workspaceConfigurationDetails() (slacksvc.WorkspaceConfiguration, error) { if p == nil || p.workspaceConfigurationResolver == nil { - return slacksvc.WorkspaceConfiguration{} + return slacksvc.WorkspaceConfiguration{}, nil } p.workspaceConfigurationMu.Lock() defer p.workspaceConfigurationMu.Unlock() if p.workspaceConfigurationLoaded { - return p.workspaceConfiguration + return p.workspaceConfiguration, p.workspaceConfigurationErr } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) @@ -230,12 +234,14 @@ func (p *Provider) workspaceConfigurationDetails() slacksvc.WorkspaceConfigurati configuration, err := p.workspaceConfigurationResolver(ctx) if err != nil { - return p.workspaceConfiguration + p.workspaceConfigurationErr = err + return p.workspaceConfiguration, err } p.workspaceConfiguration = configuration + p.workspaceConfigurationErr = nil p.workspaceConfigurationLoaded = true - return p.workspaceConfiguration + return p.workspaceConfiguration, nil } func (p *Provider) ResolveUserTarget(user notification.User) (notification.Target, bool, error) { diff --git a/internal/service/notification/providers/slack/provider_test.go b/internal/service/notification/providers/slack/provider_test.go index 17330863..cdf3f6c9 100644 --- a/internal/service/notification/providers/slack/provider_test.go +++ b/internal/service/notification/providers/slack/provider_test.go @@ -88,10 +88,12 @@ func TestProviderMetadataRetriesWorkspaceDetailsAfterResolverError(t *testing.T) firstMetadata := provider.ProviderMetadata() assert.Equal(t, "Configured Slack workspace", firstMetadata.Description) + assert.Equal(t, "temporary slack failure", firstMetadata.Error) assert.Empty(t, firstMetadata.Metadata) secondMetadata := provider.ProviderMetadata() assert.Equal(t, "Configured Slack workspace Recovered Workspace", secondMetadata.Description) + assert.Empty(t, secondMetadata.Error) assert.Equal(t, "Recovered Workspace", secondMetadata.Metadata[MetadataKeyWorkspaceName]) assert.Equal(t, "T456", secondMetadata.Metadata[MetadataKeyTeamID]) assert.Equal(t, 2, attempts) diff --git a/internal/service/notification/transport.go b/internal/service/notification/transport.go index 6d91e455..0df6a89f 100644 --- a/internal/service/notification/transport.go +++ b/internal/service/notification/transport.go @@ -28,6 +28,7 @@ type ProviderMetadata struct { DisplayName string Description string Enabled bool + Error string Metadata map[string]string }