mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat(coderd): add inbox notifications endpoints (#16889)
This PR is part of the inbox notifications topic, and rely on previous PRs merged - it adds : - Endpoints to : - WS : watch new inbox notifications - REST : list inbox notifications - REST : update the read status of a notification Also, this PR acts as a follow-up PR from previous work and : - fix DB query issues - fix DBMem logic to match DB
This commit is contained in:
+1
-1
@@ -934,7 +934,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
|
|||||||
// The notification manager is responsible for:
|
// The notification manager is responsible for:
|
||||||
// - creating notifiers and managing their lifecycles (notifiers are responsible for dequeueing/sending notifications)
|
// - creating notifiers and managing their lifecycles (notifiers are responsible for dequeueing/sending notifications)
|
||||||
// - keeping the store updated with status updates
|
// - keeping the store updated with status updates
|
||||||
notificationsManager, err = notifications.NewManager(notificationsCfg, options.Database, helpers, metrics, logger.Named("notifications.manager"))
|
notificationsManager, err = notifications.NewManager(notificationsCfg, options.Database, options.Pubsub, helpers, metrics, logger.Named("notifications.manager"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("failed to instantiate notification manager: %w", err)
|
return xerrors.Errorf("failed to instantiate notification manager: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+206
@@ -1660,6 +1660,130 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/notifications/inbox": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"CoderSessionToken": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Notifications"
|
||||||
|
],
|
||||||
|
"summary": "List inbox notifications",
|
||||||
|
"operationId": "list-inbox-notifications",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Comma-separated list of target IDs to filter notifications",
|
||||||
|
"name": "targets",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Comma-separated list of template IDs to filter notifications",
|
||||||
|
"name": "templates",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Filter notifications by read status. Possible values: read, unread, all",
|
||||||
|
"name": "read_status",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/codersdk.ListInboxNotificationsResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/notifications/inbox/watch": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"CoderSessionToken": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Notifications"
|
||||||
|
],
|
||||||
|
"summary": "Watch for new inbox notifications",
|
||||||
|
"operationId": "watch-for-new-inbox-notifications",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Comma-separated list of target IDs to filter notifications",
|
||||||
|
"name": "targets",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Comma-separated list of template IDs to filter notifications",
|
||||||
|
"name": "templates",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Filter notifications by read status. Possible values: read, unread, all",
|
||||||
|
"name": "read_status",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/codersdk.GetInboxNotificationResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/notifications/inbox/{id}/read-status": {
|
||||||
|
"put": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"CoderSessionToken": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Notifications"
|
||||||
|
],
|
||||||
|
"summary": "Update read status of a notification",
|
||||||
|
"operationId": "update-read-status-of-a-notification",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "id of the notification",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/codersdk.Response"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/notifications/settings": {
|
"/notifications/settings": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -11890,6 +12014,17 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"codersdk.GetInboxNotificationResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"notification": {
|
||||||
|
"$ref": "#/definitions/codersdk.InboxNotification"
|
||||||
|
},
|
||||||
|
"unread_count": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"codersdk.GetUserStatusCountsResponse": {
|
"codersdk.GetUserStatusCountsResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -12071,6 +12206,63 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"codersdk.InboxNotification": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"actions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/codersdk.InboxNotificationAction"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"icon": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
"read_at": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"targets": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"template_id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"codersdk.InboxNotificationAction": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"label": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"codersdk.InsightsReportInterval": {
|
"codersdk.InsightsReportInterval": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
@@ -12181,6 +12373,20 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"codersdk.ListInboxNotificationsResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"notifications": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/codersdk.InboxNotification"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"unread_count": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"codersdk.LogLevel": {
|
"codersdk.LogLevel": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
|
|||||||
Generated
+194
@@ -1445,6 +1445,118 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/notifications/inbox": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"CoderSessionToken": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"produces": ["application/json"],
|
||||||
|
"tags": ["Notifications"],
|
||||||
|
"summary": "List inbox notifications",
|
||||||
|
"operationId": "list-inbox-notifications",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Comma-separated list of target IDs to filter notifications",
|
||||||
|
"name": "targets",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Comma-separated list of template IDs to filter notifications",
|
||||||
|
"name": "templates",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Filter notifications by read status. Possible values: read, unread, all",
|
||||||
|
"name": "read_status",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/codersdk.ListInboxNotificationsResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/notifications/inbox/watch": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"CoderSessionToken": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"produces": ["application/json"],
|
||||||
|
"tags": ["Notifications"],
|
||||||
|
"summary": "Watch for new inbox notifications",
|
||||||
|
"operationId": "watch-for-new-inbox-notifications",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Comma-separated list of target IDs to filter notifications",
|
||||||
|
"name": "targets",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Comma-separated list of template IDs to filter notifications",
|
||||||
|
"name": "templates",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Filter notifications by read status. Possible values: read, unread, all",
|
||||||
|
"name": "read_status",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/codersdk.GetInboxNotificationResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/notifications/inbox/{id}/read-status": {
|
||||||
|
"put": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"CoderSessionToken": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"produces": ["application/json"],
|
||||||
|
"tags": ["Notifications"],
|
||||||
|
"summary": "Update read status of a notification",
|
||||||
|
"operationId": "update-read-status-of-a-notification",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "id of the notification",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/codersdk.Response"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/notifications/settings": {
|
"/notifications/settings": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -10667,6 +10779,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"codersdk.GetInboxNotificationResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"notification": {
|
||||||
|
"$ref": "#/definitions/codersdk.InboxNotification"
|
||||||
|
},
|
||||||
|
"unread_count": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"codersdk.GetUserStatusCountsResponse": {
|
"codersdk.GetUserStatusCountsResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -10842,6 +10965,63 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"codersdk.InboxNotification": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"actions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/codersdk.InboxNotificationAction"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"icon": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
"read_at": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"targets": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"template_id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"codersdk.InboxNotificationAction": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"label": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"codersdk.InsightsReportInterval": {
|
"codersdk.InsightsReportInterval": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["day", "week"],
|
"enum": ["day", "week"],
|
||||||
@@ -10938,6 +11118,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"codersdk.ListInboxNotificationsResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"notifications": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/codersdk.InboxNotification"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"unread_count": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"codersdk.LogLevel": {
|
"codersdk.LogLevel": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["trace", "debug", "info", "warn", "error"],
|
"enum": ["trace", "debug", "info", "warn", "error"],
|
||||||
|
|||||||
@@ -1387,6 +1387,11 @@ func New(options *Options) *API {
|
|||||||
})
|
})
|
||||||
r.Route("/notifications", func(r chi.Router) {
|
r.Route("/notifications", func(r chi.Router) {
|
||||||
r.Use(apiKeyMiddleware)
|
r.Use(apiKeyMiddleware)
|
||||||
|
r.Route("/inbox", func(r chi.Router) {
|
||||||
|
r.Get("/", api.listInboxNotifications)
|
||||||
|
r.Get("/watch", api.watchInboxNotifications)
|
||||||
|
r.Put("/{id}/read-status", api.updateInboxNotificationReadStatus)
|
||||||
|
})
|
||||||
r.Get("/settings", api.notificationsSettings)
|
r.Get("/settings", api.notificationsSettings)
|
||||||
r.Put("/settings", api.putNotificationsSettings)
|
r.Put("/settings", api.putNotificationsSettings)
|
||||||
r.Route("/templates", func(r chi.Router) {
|
r.Route("/templates", func(r chi.Router) {
|
||||||
|
|||||||
@@ -3296,34 +3296,52 @@ func (q *FakeQuerier) GetFilteredInboxNotificationsByUserID(_ context.Context, a
|
|||||||
defer q.mutex.RUnlock()
|
defer q.mutex.RUnlock()
|
||||||
|
|
||||||
notifications := make([]database.InboxNotification, 0)
|
notifications := make([]database.InboxNotification, 0)
|
||||||
for _, notification := range q.inboxNotifications {
|
// TODO : after using go version >= 1.23 , we can change this one to https://pkg.go.dev/slices#Backward
|
||||||
|
for idx := len(q.inboxNotifications) - 1; idx >= 0; idx-- {
|
||||||
|
notification := q.inboxNotifications[idx]
|
||||||
|
|
||||||
if notification.UserID == arg.UserID {
|
if notification.UserID == arg.UserID {
|
||||||
|
if !arg.CreatedAtOpt.IsZero() && !notification.CreatedAt.Before(arg.CreatedAtOpt) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
templateFound := false
|
||||||
for _, template := range arg.Templates {
|
for _, template := range arg.Templates {
|
||||||
templateFound := false
|
|
||||||
if notification.TemplateID == template {
|
if notification.TemplateID == template {
|
||||||
templateFound = true
|
templateFound = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if !templateFound {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(arg.Templates) > 0 && !templateFound {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
targetsFound := true
|
||||||
for _, target := range arg.Targets {
|
for _, target := range arg.Targets {
|
||||||
isFound := false
|
targetFound := false
|
||||||
for _, insertedTarget := range notification.Targets {
|
for _, insertedTarget := range notification.Targets {
|
||||||
if insertedTarget == target {
|
if insertedTarget == target {
|
||||||
isFound = true
|
targetFound = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isFound {
|
if !targetFound {
|
||||||
continue
|
targetsFound = false
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
notifications = append(notifications, notification)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !targetsFound {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg.LimitOpt == 0 && len(notifications) == 25) ||
|
||||||
|
(arg.LimitOpt != 0 && len(notifications) == int(arg.LimitOpt)) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
notifications = append(notifications, notification)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8223,7 +8241,7 @@ func (q *FakeQuerier) InsertInboxNotification(_ context.Context, arg database.In
|
|||||||
Content: arg.Content,
|
Content: arg.Content,
|
||||||
Icon: arg.Icon,
|
Icon: arg.Icon,
|
||||||
Actions: arg.Actions,
|
Actions: arg.Actions,
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: arg.CreatedAt,
|
||||||
}
|
}
|
||||||
|
|
||||||
q.inboxNotifications = append(q.inboxNotifications, notification)
|
q.inboxNotifications = append(q.inboxNotifications, notification)
|
||||||
|
|||||||
@@ -4310,8 +4310,8 @@ func (q *sqlQuerier) CountUnreadInboxNotificationsByUserID(ctx context.Context,
|
|||||||
const getFilteredInboxNotificationsByUserID = `-- name: GetFilteredInboxNotificationsByUserID :many
|
const getFilteredInboxNotificationsByUserID = `-- name: GetFilteredInboxNotificationsByUserID :many
|
||||||
SELECT id, user_id, template_id, targets, title, content, icon, actions, read_at, created_at FROM inbox_notifications WHERE
|
SELECT id, user_id, template_id, targets, title, content, icon, actions, read_at, created_at FROM inbox_notifications WHERE
|
||||||
user_id = $1 AND
|
user_id = $1 AND
|
||||||
template_id = ANY($2::UUID[]) AND
|
($2::UUID[] IS NULL OR template_id = ANY($2::UUID[])) AND
|
||||||
targets @> COALESCE($3, ARRAY[]::UUID[]) AND
|
($3::UUID[] IS NULL OR targets @> $3::UUID[]) AND
|
||||||
($4::inbox_notification_read_status = 'all' OR ($4::inbox_notification_read_status = 'unread' AND read_at IS NULL) OR ($4::inbox_notification_read_status = 'read' AND read_at IS NOT NULL)) AND
|
($4::inbox_notification_read_status = 'all' OR ($4::inbox_notification_read_status = 'unread' AND read_at IS NULL) OR ($4::inbox_notification_read_status = 'read' AND read_at IS NOT NULL)) AND
|
||||||
($5::TIMESTAMPTZ = '0001-01-01 00:00:00Z' OR created_at < $5::TIMESTAMPTZ)
|
($5::TIMESTAMPTZ = '0001-01-01 00:00:00Z' OR created_at < $5::TIMESTAMPTZ)
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ SELECT * FROM inbox_notifications WHERE
|
|||||||
-- param limit_opt: The limit of notifications to fetch. If the limit is not specified, it defaults to 25
|
-- param limit_opt: The limit of notifications to fetch. If the limit is not specified, it defaults to 25
|
||||||
SELECT * FROM inbox_notifications WHERE
|
SELECT * FROM inbox_notifications WHERE
|
||||||
user_id = @user_id AND
|
user_id = @user_id AND
|
||||||
template_id = ANY(@templates::UUID[]) AND
|
(@templates::UUID[] IS NULL OR template_id = ANY(@templates::UUID[])) AND
|
||||||
targets @> COALESCE(@targets, ARRAY[]::UUID[]) AND
|
(@targets::UUID[] IS NULL OR targets @> @targets::UUID[]) AND
|
||||||
(@read_status::inbox_notification_read_status = 'all' OR (@read_status::inbox_notification_read_status = 'unread' AND read_at IS NULL) OR (@read_status::inbox_notification_read_status = 'read' AND read_at IS NOT NULL)) AND
|
(@read_status::inbox_notification_read_status = 'all' OR (@read_status::inbox_notification_read_status = 'unread' AND read_at IS NULL) OR (@read_status::inbox_notification_read_status = 'read' AND read_at IS NOT NULL)) AND
|
||||||
(@created_at_opt::TIMESTAMPTZ = '0001-01-01 00:00:00Z' OR created_at < @created_at_opt::TIMESTAMPTZ)
|
(@created_at_opt::TIMESTAMPTZ = '0001-01-01 00:00:00Z' OR created_at < @created_at_opt::TIMESTAMPTZ)
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
|
|||||||
@@ -0,0 +1,347 @@
|
|||||||
|
package coderd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"slices"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
|
||||||
|
"cdr.dev/slog"
|
||||||
|
|
||||||
|
"github.com/coder/coder/v2/coderd/database"
|
||||||
|
"github.com/coder/coder/v2/coderd/database/dbtime"
|
||||||
|
"github.com/coder/coder/v2/coderd/httpapi"
|
||||||
|
"github.com/coder/coder/v2/coderd/httpmw"
|
||||||
|
"github.com/coder/coder/v2/coderd/pubsub"
|
||||||
|
"github.com/coder/coder/v2/codersdk"
|
||||||
|
"github.com/coder/coder/v2/codersdk/wsjson"
|
||||||
|
"github.com/coder/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
// convertInboxNotificationResponse works as a util function to transform a database.InboxNotification to codersdk.InboxNotification
|
||||||
|
func convertInboxNotificationResponse(ctx context.Context, logger slog.Logger, notif database.InboxNotification) codersdk.InboxNotification {
|
||||||
|
return codersdk.InboxNotification{
|
||||||
|
ID: notif.ID,
|
||||||
|
UserID: notif.UserID,
|
||||||
|
TemplateID: notif.TemplateID,
|
||||||
|
Targets: notif.Targets,
|
||||||
|
Title: notif.Title,
|
||||||
|
Content: notif.Content,
|
||||||
|
Icon: notif.Icon,
|
||||||
|
Actions: func() []codersdk.InboxNotificationAction {
|
||||||
|
var actionsList []codersdk.InboxNotificationAction
|
||||||
|
err := json.Unmarshal([]byte(notif.Actions), &actionsList)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(ctx, "unmarshal inbox notification actions", slog.Error(err))
|
||||||
|
}
|
||||||
|
return actionsList
|
||||||
|
}(),
|
||||||
|
ReadAt: func() *time.Time {
|
||||||
|
if !notif.ReadAt.Valid {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return ¬if.ReadAt.Time
|
||||||
|
}(),
|
||||||
|
CreatedAt: notif.CreatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// watchInboxNotifications watches for new inbox notifications and sends them to the client.
|
||||||
|
// The client can specify a list of target IDs to filter the notifications.
|
||||||
|
// @Summary Watch for new inbox notifications
|
||||||
|
// @ID watch-for-new-inbox-notifications
|
||||||
|
// @Security CoderSessionToken
|
||||||
|
// @Produce json
|
||||||
|
// @Tags Notifications
|
||||||
|
// @Param targets query string false "Comma-separated list of target IDs to filter notifications"
|
||||||
|
// @Param templates query string false "Comma-separated list of template IDs to filter notifications"
|
||||||
|
// @Param read_status query string false "Filter notifications by read status. Possible values: read, unread, all"
|
||||||
|
// @Success 200 {object} codersdk.GetInboxNotificationResponse
|
||||||
|
// @Router /notifications/inbox/watch [get]
|
||||||
|
func (api *API) watchInboxNotifications(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
p := httpapi.NewQueryParamParser()
|
||||||
|
vals := r.URL.Query()
|
||||||
|
|
||||||
|
var (
|
||||||
|
ctx = r.Context()
|
||||||
|
apikey = httpmw.APIKey(r)
|
||||||
|
|
||||||
|
targets = p.UUIDs(vals, []uuid.UUID{}, "targets")
|
||||||
|
templates = p.UUIDs(vals, []uuid.UUID{}, "templates")
|
||||||
|
readStatus = p.String(vals, "all", "read_status")
|
||||||
|
)
|
||||||
|
p.ErrorExcessParams(vals)
|
||||||
|
if len(p.Errors) > 0 {
|
||||||
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||||
|
Message: "Query parameters have invalid values.",
|
||||||
|
Validations: p.Errors,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !slices.Contains([]string{
|
||||||
|
string(database.InboxNotificationReadStatusAll),
|
||||||
|
string(database.InboxNotificationReadStatusRead),
|
||||||
|
string(database.InboxNotificationReadStatusUnread),
|
||||||
|
}, readStatus) {
|
||||||
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||||
|
Message: "starting_before query parameter should be any of 'all', 'read', 'unread'.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := websocket.Accept(rw, r, nil)
|
||||||
|
if err != nil {
|
||||||
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||||
|
Message: "Failed to upgrade connection to websocket.",
|
||||||
|
Detail: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go httpapi.Heartbeat(ctx, conn)
|
||||||
|
defer conn.Close(websocket.StatusNormalClosure, "connection closed")
|
||||||
|
|
||||||
|
notificationCh := make(chan codersdk.InboxNotification, 10)
|
||||||
|
|
||||||
|
closeInboxNotificationsSubscriber, err := api.Pubsub.SubscribeWithErr(pubsub.InboxNotificationForOwnerEventChannel(apikey.UserID),
|
||||||
|
pubsub.HandleInboxNotificationEvent(
|
||||||
|
func(ctx context.Context, payload pubsub.InboxNotificationEvent, err error) {
|
||||||
|
if err != nil {
|
||||||
|
api.Logger.Error(ctx, "inbox notification event", slog.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleInboxNotificationEvent cb receives all the inbox notifications - without any filters excepted the user_id.
|
||||||
|
// Based on query parameters defined above and filters defined by the client - we then filter out the
|
||||||
|
// notifications we do not want to forward and discard it.
|
||||||
|
|
||||||
|
// filter out notifications that don't match the targets
|
||||||
|
if len(targets) > 0 {
|
||||||
|
for _, target := range targets {
|
||||||
|
if isFound := slices.Contains(payload.InboxNotification.Targets, target); !isFound {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter out notifications that don't match the templates
|
||||||
|
if len(templates) > 0 {
|
||||||
|
if isFound := slices.Contains(templates, payload.InboxNotification.TemplateID); !isFound {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter out notifications that don't match the read status
|
||||||
|
if readStatus != "" {
|
||||||
|
if readStatus == string(database.InboxNotificationReadStatusRead) {
|
||||||
|
if payload.InboxNotification.ReadAt == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if readStatus == string(database.InboxNotificationReadStatusUnread) {
|
||||||
|
if payload.InboxNotification.ReadAt != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// keep a safe guard in case of latency to push notifications through websocket
|
||||||
|
select {
|
||||||
|
case notificationCh <- payload.InboxNotification:
|
||||||
|
default:
|
||||||
|
api.Logger.Error(ctx, "failed to push consumed notification into websocket handler, check latency")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
))
|
||||||
|
if err != nil {
|
||||||
|
api.Logger.Error(ctx, "subscribe to inbox notification event", slog.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer closeInboxNotificationsSubscriber()
|
||||||
|
|
||||||
|
encoder := wsjson.NewEncoder[codersdk.GetInboxNotificationResponse](conn, websocket.MessageText)
|
||||||
|
defer encoder.Close(websocket.StatusNormalClosure)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case notif := <-notificationCh:
|
||||||
|
unreadCount, err := api.Database.CountUnreadInboxNotificationsByUserID(ctx, apikey.UserID)
|
||||||
|
if err != nil {
|
||||||
|
api.Logger.Error(ctx, "failed to count unread inbox notifications", slog.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := encoder.Encode(codersdk.GetInboxNotificationResponse{
|
||||||
|
Notification: notif,
|
||||||
|
UnreadCount: int(unreadCount),
|
||||||
|
}); err != nil {
|
||||||
|
api.Logger.Error(ctx, "encode notification", slog.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// listInboxNotifications lists the notifications for the user.
|
||||||
|
// @Summary List inbox notifications
|
||||||
|
// @ID list-inbox-notifications
|
||||||
|
// @Security CoderSessionToken
|
||||||
|
// @Produce json
|
||||||
|
// @Tags Notifications
|
||||||
|
// @Param targets query string false "Comma-separated list of target IDs to filter notifications"
|
||||||
|
// @Param templates query string false "Comma-separated list of template IDs to filter notifications"
|
||||||
|
// @Param read_status query string false "Filter notifications by read status. Possible values: read, unread, all"
|
||||||
|
// @Success 200 {object} codersdk.ListInboxNotificationsResponse
|
||||||
|
// @Router /notifications/inbox [get]
|
||||||
|
func (api *API) listInboxNotifications(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
p := httpapi.NewQueryParamParser()
|
||||||
|
vals := r.URL.Query()
|
||||||
|
|
||||||
|
var (
|
||||||
|
ctx = r.Context()
|
||||||
|
apikey = httpmw.APIKey(r)
|
||||||
|
|
||||||
|
targets = p.UUIDs(vals, nil, "targets")
|
||||||
|
templates = p.UUIDs(vals, nil, "templates")
|
||||||
|
readStatus = p.String(vals, "all", "read_status")
|
||||||
|
startingBefore = p.UUID(vals, uuid.Nil, "starting_before")
|
||||||
|
)
|
||||||
|
p.ErrorExcessParams(vals)
|
||||||
|
if len(p.Errors) > 0 {
|
||||||
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||||
|
Message: "Query parameters have invalid values.",
|
||||||
|
Validations: p.Errors,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !slices.Contains([]string{
|
||||||
|
string(database.InboxNotificationReadStatusAll),
|
||||||
|
string(database.InboxNotificationReadStatusRead),
|
||||||
|
string(database.InboxNotificationReadStatusUnread),
|
||||||
|
}, readStatus) {
|
||||||
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||||
|
Message: "starting_before query parameter should be any of 'all', 'read', 'unread'.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
createdBefore := dbtime.Now()
|
||||||
|
if startingBefore != uuid.Nil {
|
||||||
|
lastNotif, err := api.Database.GetInboxNotificationByID(ctx, startingBefore)
|
||||||
|
if err == nil {
|
||||||
|
createdBefore = lastNotif.CreatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notifs, err := api.Database.GetFilteredInboxNotificationsByUserID(ctx, database.GetFilteredInboxNotificationsByUserIDParams{
|
||||||
|
UserID: apikey.UserID,
|
||||||
|
Templates: templates,
|
||||||
|
Targets: targets,
|
||||||
|
ReadStatus: database.InboxNotificationReadStatus(readStatus),
|
||||||
|
CreatedAtOpt: createdBefore,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
api.Logger.Error(ctx, "failed to get filtered inbox notifications", slog.Error(err))
|
||||||
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||||
|
Message: "Failed to get filtered inbox notifications.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
unreadCount, err := api.Database.CountUnreadInboxNotificationsByUserID(ctx, apikey.UserID)
|
||||||
|
if err != nil {
|
||||||
|
api.Logger.Error(ctx, "failed to count unread inbox notifications", slog.Error(err))
|
||||||
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||||
|
Message: "Failed to count unread inbox notifications.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
httpapi.Write(ctx, rw, http.StatusOK, codersdk.ListInboxNotificationsResponse{
|
||||||
|
Notifications: func() []codersdk.InboxNotification {
|
||||||
|
notificationsList := make([]codersdk.InboxNotification, 0, len(notifs))
|
||||||
|
for _, notification := range notifs {
|
||||||
|
notificationsList = append(notificationsList, convertInboxNotificationResponse(ctx, api.Logger, notification))
|
||||||
|
}
|
||||||
|
return notificationsList
|
||||||
|
}(),
|
||||||
|
UnreadCount: int(unreadCount),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateInboxNotificationReadStatus changes the read status of a notification.
|
||||||
|
// @Summary Update read status of a notification
|
||||||
|
// @ID update-read-status-of-a-notification
|
||||||
|
// @Security CoderSessionToken
|
||||||
|
// @Produce json
|
||||||
|
// @Tags Notifications
|
||||||
|
// @Param id path string true "id of the notification"
|
||||||
|
// @Success 200 {object} codersdk.Response
|
||||||
|
// @Router /notifications/inbox/{id}/read-status [put]
|
||||||
|
func (api *API) updateInboxNotificationReadStatus(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
ctx = r.Context()
|
||||||
|
apikey = httpmw.APIKey(r)
|
||||||
|
)
|
||||||
|
|
||||||
|
notificationID, ok := httpmw.ParseUUIDParam(rw, r, "id")
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var body codersdk.UpdateInboxNotificationReadStatusRequest
|
||||||
|
if !httpapi.Read(ctx, rw, r, &body) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := api.Database.UpdateInboxNotificationReadStatus(ctx, database.UpdateInboxNotificationReadStatusParams{
|
||||||
|
ID: notificationID,
|
||||||
|
ReadAt: func() sql.NullTime {
|
||||||
|
if body.IsRead {
|
||||||
|
return sql.NullTime{
|
||||||
|
Time: dbtime.Now(),
|
||||||
|
Valid: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sql.NullTime{}
|
||||||
|
}(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
api.Logger.Error(ctx, "failed to update inbox notification read status", slog.Error(err))
|
||||||
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||||
|
Message: "Failed to update inbox notification read status.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
unreadCount, err := api.Database.CountUnreadInboxNotificationsByUserID(ctx, apikey.UserID)
|
||||||
|
if err != nil {
|
||||||
|
api.Logger.Error(ctx, "failed to call count unread inbox notifications", slog.Error(err))
|
||||||
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||||
|
Message: "Failed to call count unread inbox notifications.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedNotification, err := api.Database.GetInboxNotificationByID(ctx, notificationID)
|
||||||
|
if err != nil {
|
||||||
|
api.Logger.Error(ctx, "failed to get notification by id", slog.Error(err))
|
||||||
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||||
|
Message: "Failed to get notification by id.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
httpapi.Write(ctx, rw, http.StatusOK, codersdk.UpdateInboxNotificationReadStatusResponse{
|
||||||
|
Notification: convertInboxNotificationResponse(ctx, api.Logger, updatedNotification),
|
||||||
|
UnreadCount: int(unreadCount),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,725 @@
|
|||||||
|
package coderd_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||||
|
"github.com/coder/coder/v2/coderd/database"
|
||||||
|
"github.com/coder/coder/v2/coderd/database/dbgen"
|
||||||
|
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
||||||
|
"github.com/coder/coder/v2/coderd/database/dbtime"
|
||||||
|
"github.com/coder/coder/v2/coderd/notifications"
|
||||||
|
"github.com/coder/coder/v2/coderd/notifications/dispatch"
|
||||||
|
"github.com/coder/coder/v2/coderd/notifications/types"
|
||||||
|
"github.com/coder/coder/v2/coderd/rbac"
|
||||||
|
"github.com/coder/coder/v2/codersdk"
|
||||||
|
"github.com/coder/coder/v2/testutil"
|
||||||
|
"github.com/coder/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
inboxNotificationsPageSize = 25
|
||||||
|
)
|
||||||
|
|
||||||
|
var failingPaginationUUID = uuid.MustParse("fba6966a-9061-4111-8e1a-f6a9fbea4b16")
|
||||||
|
|
||||||
|
func TestInboxNotification_Watch(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// I skip these tests specifically on windows as for now they are flaky - only on Windows.
|
||||||
|
// For now the idea is that the runner takes too long to insert the entries, could be worth
|
||||||
|
// investigating a manual Tx.
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("our runners are randomly taking too long to insert entries")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("Failure Modes", func(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
expectedError string
|
||||||
|
listTemplate string
|
||||||
|
listTarget string
|
||||||
|
listReadStatus string
|
||||||
|
listStartingBefore string
|
||||||
|
}{
|
||||||
|
{"nok - wrong targets", `Query param "targets" has invalid values`, "", "wrong_target", "", ""},
|
||||||
|
{"nok - wrong templates", `Query param "templates" has invalid values`, "wrong_template", "", "", ""},
|
||||||
|
{"nok - wrong read status", "starting_before query parameter should be any of 'all', 'read', 'unread'", "", "", "erroneous", ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
client, _, _ := coderdtest.NewWithAPI(t, &coderdtest.Options{})
|
||||||
|
firstUser := coderdtest.CreateFirstUser(t, client)
|
||||||
|
client, _ = coderdtest.CreateAnotherUser(t, client, firstUser.OrganizationID)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
resp, err := client.Request(ctx, http.MethodGet, "/api/v2/notifications/inbox/watch", nil,
|
||||||
|
codersdk.ListInboxNotificationsRequestToQueryParams(codersdk.ListInboxNotificationsRequest{
|
||||||
|
Targets: tt.listTarget,
|
||||||
|
Templates: tt.listTemplate,
|
||||||
|
ReadStatus: tt.listReadStatus,
|
||||||
|
StartingBefore: tt.listStartingBefore,
|
||||||
|
})...)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
err = codersdk.ReadBodyAsError(resp)
|
||||||
|
require.ErrorContains(t, err, tt.expectedError)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("OK", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
|
logger := testutil.Logger(t)
|
||||||
|
|
||||||
|
db, ps := dbtestutil.NewDB(t)
|
||||||
|
|
||||||
|
firstClient, _, _ := coderdtest.NewWithAPI(t, &coderdtest.Options{
|
||||||
|
Pubsub: ps,
|
||||||
|
Database: db,
|
||||||
|
})
|
||||||
|
firstUser := coderdtest.CreateFirstUser(t, firstClient)
|
||||||
|
member, memberClient := coderdtest.CreateAnotherUser(t, firstClient, firstUser.OrganizationID, rbac.RoleTemplateAdmin())
|
||||||
|
|
||||||
|
u, err := member.URL.Parse("/api/v2/notifications/inbox/watch")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// nolint:bodyclose
|
||||||
|
wsConn, resp, err := websocket.Dial(ctx, u.String(), &websocket.DialOptions{
|
||||||
|
HTTPHeader: http.Header{
|
||||||
|
"Coder-Session-Token": []string{member.SessionToken()},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if resp.StatusCode != http.StatusSwitchingProtocols {
|
||||||
|
err = codersdk.ReadBodyAsError(resp)
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
defer wsConn.Close(websocket.StatusNormalClosure, "done")
|
||||||
|
|
||||||
|
inboxHandler := dispatch.NewInboxHandler(logger, db, ps)
|
||||||
|
dispatchFunc, err := inboxHandler.Dispatcher(types.MessagePayload{
|
||||||
|
UserID: memberClient.ID.String(),
|
||||||
|
NotificationTemplateID: notifications.TemplateWorkspaceOutOfMemory.String(),
|
||||||
|
}, "notification title", "notification content", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
dispatchFunc(ctx, uuid.New())
|
||||||
|
|
||||||
|
_, message, err := wsConn.Read(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var notif codersdk.GetInboxNotificationResponse
|
||||||
|
err = json.Unmarshal(message, ¬if)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, 1, notif.UnreadCount)
|
||||||
|
require.Equal(t, memberClient.ID, notif.Notification.UserID)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("OK - filters on templates", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
|
logger := testutil.Logger(t)
|
||||||
|
|
||||||
|
db, ps := dbtestutil.NewDB(t)
|
||||||
|
|
||||||
|
firstClient, _, _ := coderdtest.NewWithAPI(t, &coderdtest.Options{
|
||||||
|
Pubsub: ps,
|
||||||
|
Database: db,
|
||||||
|
})
|
||||||
|
firstUser := coderdtest.CreateFirstUser(t, firstClient)
|
||||||
|
member, memberClient := coderdtest.CreateAnotherUser(t, firstClient, firstUser.OrganizationID, rbac.RoleTemplateAdmin())
|
||||||
|
|
||||||
|
u, err := member.URL.Parse(fmt.Sprintf("/api/v2/notifications/inbox/watch?templates=%v", notifications.TemplateWorkspaceOutOfMemory))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// nolint:bodyclose
|
||||||
|
wsConn, resp, err := websocket.Dial(ctx, u.String(), &websocket.DialOptions{
|
||||||
|
HTTPHeader: http.Header{
|
||||||
|
"Coder-Session-Token": []string{member.SessionToken()},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if resp.StatusCode != http.StatusSwitchingProtocols {
|
||||||
|
err = codersdk.ReadBodyAsError(resp)
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
defer wsConn.Close(websocket.StatusNormalClosure, "done")
|
||||||
|
|
||||||
|
inboxHandler := dispatch.NewInboxHandler(logger, db, ps)
|
||||||
|
dispatchFunc, err := inboxHandler.Dispatcher(types.MessagePayload{
|
||||||
|
UserID: memberClient.ID.String(),
|
||||||
|
NotificationTemplateID: notifications.TemplateWorkspaceOutOfMemory.String(),
|
||||||
|
}, "memory related title", "memory related content", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
dispatchFunc(ctx, uuid.New())
|
||||||
|
|
||||||
|
_, message, err := wsConn.Read(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var notif codersdk.GetInboxNotificationResponse
|
||||||
|
err = json.Unmarshal(message, ¬if)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, 1, notif.UnreadCount)
|
||||||
|
require.Equal(t, memberClient.ID, notif.Notification.UserID)
|
||||||
|
require.Equal(t, "memory related title", notif.Notification.Title)
|
||||||
|
|
||||||
|
dispatchFunc, err = inboxHandler.Dispatcher(types.MessagePayload{
|
||||||
|
UserID: memberClient.ID.String(),
|
||||||
|
NotificationTemplateID: notifications.TemplateWorkspaceOutOfDisk.String(),
|
||||||
|
}, "disk related title", "disk related title", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
dispatchFunc(ctx, uuid.New())
|
||||||
|
|
||||||
|
dispatchFunc, err = inboxHandler.Dispatcher(types.MessagePayload{
|
||||||
|
UserID: memberClient.ID.String(),
|
||||||
|
NotificationTemplateID: notifications.TemplateWorkspaceOutOfMemory.String(),
|
||||||
|
}, "second memory related title", "second memory related title", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
dispatchFunc(ctx, uuid.New())
|
||||||
|
|
||||||
|
_, message, err = wsConn.Read(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = json.Unmarshal(message, ¬if)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, 3, notif.UnreadCount)
|
||||||
|
require.Equal(t, memberClient.ID, notif.Notification.UserID)
|
||||||
|
require.Equal(t, "second memory related title", notif.Notification.Title)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("OK - filters on targets", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
|
logger := testutil.Logger(t)
|
||||||
|
|
||||||
|
db, ps := dbtestutil.NewDB(t)
|
||||||
|
|
||||||
|
firstClient, _, _ := coderdtest.NewWithAPI(t, &coderdtest.Options{
|
||||||
|
Pubsub: ps,
|
||||||
|
Database: db,
|
||||||
|
})
|
||||||
|
firstUser := coderdtest.CreateFirstUser(t, firstClient)
|
||||||
|
member, memberClient := coderdtest.CreateAnotherUser(t, firstClient, firstUser.OrganizationID, rbac.RoleTemplateAdmin())
|
||||||
|
|
||||||
|
correctTarget := uuid.New()
|
||||||
|
|
||||||
|
u, err := member.URL.Parse(fmt.Sprintf("/api/v2/notifications/inbox/watch?targets=%v", correctTarget.String()))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// nolint:bodyclose
|
||||||
|
wsConn, resp, err := websocket.Dial(ctx, u.String(), &websocket.DialOptions{
|
||||||
|
HTTPHeader: http.Header{
|
||||||
|
"Coder-Session-Token": []string{member.SessionToken()},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if resp.StatusCode != http.StatusSwitchingProtocols {
|
||||||
|
err = codersdk.ReadBodyAsError(resp)
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
defer wsConn.Close(websocket.StatusNormalClosure, "done")
|
||||||
|
|
||||||
|
inboxHandler := dispatch.NewInboxHandler(logger, db, ps)
|
||||||
|
dispatchFunc, err := inboxHandler.Dispatcher(types.MessagePayload{
|
||||||
|
UserID: memberClient.ID.String(),
|
||||||
|
NotificationTemplateID: notifications.TemplateWorkspaceOutOfMemory.String(),
|
||||||
|
Targets: []uuid.UUID{correctTarget},
|
||||||
|
}, "memory related title", "memory related content", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
dispatchFunc(ctx, uuid.New())
|
||||||
|
|
||||||
|
_, message, err := wsConn.Read(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var notif codersdk.GetInboxNotificationResponse
|
||||||
|
err = json.Unmarshal(message, ¬if)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, 1, notif.UnreadCount)
|
||||||
|
require.Equal(t, memberClient.ID, notif.Notification.UserID)
|
||||||
|
require.Equal(t, "memory related title", notif.Notification.Title)
|
||||||
|
|
||||||
|
dispatchFunc, err = inboxHandler.Dispatcher(types.MessagePayload{
|
||||||
|
UserID: memberClient.ID.String(),
|
||||||
|
NotificationTemplateID: notifications.TemplateWorkspaceOutOfMemory.String(),
|
||||||
|
Targets: []uuid.UUID{uuid.New()},
|
||||||
|
}, "second memory related title", "second memory related title", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
dispatchFunc(ctx, uuid.New())
|
||||||
|
|
||||||
|
dispatchFunc, err = inboxHandler.Dispatcher(types.MessagePayload{
|
||||||
|
UserID: memberClient.ID.String(),
|
||||||
|
NotificationTemplateID: notifications.TemplateWorkspaceOutOfMemory.String(),
|
||||||
|
Targets: []uuid.UUID{correctTarget},
|
||||||
|
}, "another memory related title", "another memory related title", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
dispatchFunc(ctx, uuid.New())
|
||||||
|
|
||||||
|
_, message, err = wsConn.Read(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = json.Unmarshal(message, ¬if)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, 3, notif.UnreadCount)
|
||||||
|
require.Equal(t, memberClient.ID, notif.Notification.UserID)
|
||||||
|
require.Equal(t, "another memory related title", notif.Notification.Title)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInboxNotifications_List(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// I skip these tests specifically on windows as for now they are flaky - only on Windows.
|
||||||
|
// For now the idea is that the runner takes too long to insert the entries, could be worth
|
||||||
|
// investigating a manual Tx.
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("our runners are randomly taking too long to insert entries")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("Failure Modes", func(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
expectedError string
|
||||||
|
listTemplate string
|
||||||
|
listTarget string
|
||||||
|
listReadStatus string
|
||||||
|
listStartingBefore string
|
||||||
|
}{
|
||||||
|
{"nok - wrong targets", `Query param "targets" has invalid values`, "", "wrong_target", "", ""},
|
||||||
|
{"nok - wrong templates", `Query param "templates" has invalid values`, "wrong_template", "", "", ""},
|
||||||
|
{"nok - wrong read status", "starting_before query parameter should be any of 'all', 'read', 'unread'", "", "", "erroneous", ""},
|
||||||
|
{"nok - wrong starting before", `Query param "starting_before" must be a valid uuid`, "", "", "", "xxx-xxx-xxx"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
client, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{})
|
||||||
|
firstUser := coderdtest.CreateFirstUser(t, client)
|
||||||
|
client, member := coderdtest.CreateAnotherUser(t, client, firstUser.OrganizationID)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
notifs, err := client.ListInboxNotifications(ctx, codersdk.ListInboxNotificationsRequest{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, notifs)
|
||||||
|
require.Equal(t, 0, notifs.UnreadCount)
|
||||||
|
require.Empty(t, notifs.Notifications)
|
||||||
|
|
||||||
|
// create a new notifications to fill the database with data
|
||||||
|
for i := range 20 {
|
||||||
|
dbgen.NotificationInbox(t, api.Database, database.InsertInboxNotificationParams{
|
||||||
|
ID: uuid.New(),
|
||||||
|
UserID: member.ID,
|
||||||
|
TemplateID: notifications.TemplateWorkspaceOutOfMemory,
|
||||||
|
Title: fmt.Sprintf("Notification %d", i),
|
||||||
|
Actions: json.RawMessage("[]"),
|
||||||
|
Content: fmt.Sprintf("Content of the notif %d", i),
|
||||||
|
CreatedAt: dbtime.Now(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
notifs, err = client.ListInboxNotifications(ctx, codersdk.ListInboxNotificationsRequest{
|
||||||
|
Templates: tt.listTemplate,
|
||||||
|
Targets: tt.listTarget,
|
||||||
|
ReadStatus: tt.listReadStatus,
|
||||||
|
StartingBefore: tt.listStartingBefore,
|
||||||
|
})
|
||||||
|
require.ErrorContains(t, err, tt.expectedError)
|
||||||
|
require.Empty(t, notifs.Notifications)
|
||||||
|
require.Zero(t, notifs.UnreadCount)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("OK empty", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
client, _, _ := coderdtest.NewWithAPI(t, &coderdtest.Options{})
|
||||||
|
firstUser := coderdtest.CreateFirstUser(t, client)
|
||||||
|
client, _ = coderdtest.CreateAnotherUser(t, client, firstUser.OrganizationID)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
notifs, err := client.ListInboxNotifications(ctx, codersdk.ListInboxNotificationsRequest{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, notifs)
|
||||||
|
|
||||||
|
require.Equal(t, 0, notifs.UnreadCount)
|
||||||
|
require.Empty(t, notifs.Notifications)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("OK with pagination", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
client, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{})
|
||||||
|
firstUser := coderdtest.CreateFirstUser(t, client)
|
||||||
|
client, member := coderdtest.CreateAnotherUser(t, client, firstUser.OrganizationID)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
notifs, err := client.ListInboxNotifications(ctx, codersdk.ListInboxNotificationsRequest{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, notifs)
|
||||||
|
require.Equal(t, 0, notifs.UnreadCount)
|
||||||
|
require.Empty(t, notifs.Notifications)
|
||||||
|
|
||||||
|
for i := range 40 {
|
||||||
|
dbgen.NotificationInbox(t, api.Database, database.InsertInboxNotificationParams{
|
||||||
|
ID: uuid.New(),
|
||||||
|
UserID: member.ID,
|
||||||
|
TemplateID: notifications.TemplateWorkspaceOutOfMemory,
|
||||||
|
Title: fmt.Sprintf("Notification %d", i),
|
||||||
|
Actions: json.RawMessage("[]"),
|
||||||
|
Content: fmt.Sprintf("Content of the notif %d", i),
|
||||||
|
CreatedAt: dbtime.Now(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
notifs, err = client.ListInboxNotifications(ctx, codersdk.ListInboxNotificationsRequest{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, notifs)
|
||||||
|
require.Equal(t, 40, notifs.UnreadCount)
|
||||||
|
require.Len(t, notifs.Notifications, inboxNotificationsPageSize)
|
||||||
|
|
||||||
|
require.Equal(t, "Notification 39", notifs.Notifications[0].Title)
|
||||||
|
|
||||||
|
notifs, err = client.ListInboxNotifications(ctx, codersdk.ListInboxNotificationsRequest{
|
||||||
|
StartingBefore: notifs.Notifications[inboxNotificationsPageSize-1].ID.String(),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, notifs)
|
||||||
|
require.Equal(t, 40, notifs.UnreadCount)
|
||||||
|
require.Len(t, notifs.Notifications, 15)
|
||||||
|
|
||||||
|
require.Equal(t, "Notification 14", notifs.Notifications[0].Title)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("OK with template filter", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
client, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{})
|
||||||
|
firstUser := coderdtest.CreateFirstUser(t, client)
|
||||||
|
client, member := coderdtest.CreateAnotherUser(t, client, firstUser.OrganizationID)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
notifs, err := client.ListInboxNotifications(ctx, codersdk.ListInboxNotificationsRequest{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, notifs)
|
||||||
|
require.Equal(t, 0, notifs.UnreadCount)
|
||||||
|
require.Empty(t, notifs.Notifications)
|
||||||
|
|
||||||
|
for i := range 10 {
|
||||||
|
dbgen.NotificationInbox(t, api.Database, database.InsertInboxNotificationParams{
|
||||||
|
ID: uuid.New(),
|
||||||
|
UserID: member.ID,
|
||||||
|
TemplateID: func() uuid.UUID {
|
||||||
|
if i%2 == 0 {
|
||||||
|
return notifications.TemplateWorkspaceOutOfMemory
|
||||||
|
}
|
||||||
|
|
||||||
|
return notifications.TemplateWorkspaceOutOfDisk
|
||||||
|
}(),
|
||||||
|
Title: fmt.Sprintf("Notification %d", i),
|
||||||
|
Actions: json.RawMessage("[]"),
|
||||||
|
Content: fmt.Sprintf("Content of the notif %d", i),
|
||||||
|
CreatedAt: dbtime.Now(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
notifs, err = client.ListInboxNotifications(ctx, codersdk.ListInboxNotificationsRequest{
|
||||||
|
Templates: notifications.TemplateWorkspaceOutOfMemory.String(),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, notifs)
|
||||||
|
require.Equal(t, 10, notifs.UnreadCount)
|
||||||
|
require.Len(t, notifs.Notifications, 5)
|
||||||
|
|
||||||
|
require.Equal(t, "Notification 8", notifs.Notifications[0].Title)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("OK with target filter", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
client, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{})
|
||||||
|
firstUser := coderdtest.CreateFirstUser(t, client)
|
||||||
|
client, member := coderdtest.CreateAnotherUser(t, client, firstUser.OrganizationID)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
notifs, err := client.ListInboxNotifications(ctx, codersdk.ListInboxNotificationsRequest{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, notifs)
|
||||||
|
require.Equal(t, 0, notifs.UnreadCount)
|
||||||
|
require.Empty(t, notifs.Notifications)
|
||||||
|
|
||||||
|
filteredTarget := uuid.New()
|
||||||
|
|
||||||
|
for i := range 10 {
|
||||||
|
dbgen.NotificationInbox(t, api.Database, database.InsertInboxNotificationParams{
|
||||||
|
ID: uuid.New(),
|
||||||
|
UserID: member.ID,
|
||||||
|
TemplateID: notifications.TemplateWorkspaceOutOfMemory,
|
||||||
|
Targets: func() []uuid.UUID {
|
||||||
|
if i%2 == 0 {
|
||||||
|
return []uuid.UUID{filteredTarget}
|
||||||
|
}
|
||||||
|
|
||||||
|
return []uuid.UUID{}
|
||||||
|
}(),
|
||||||
|
Title: fmt.Sprintf("Notification %d", i),
|
||||||
|
Actions: json.RawMessage("[]"),
|
||||||
|
Content: fmt.Sprintf("Content of the notif %d", i),
|
||||||
|
CreatedAt: dbtime.Now(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
notifs, err = client.ListInboxNotifications(ctx, codersdk.ListInboxNotificationsRequest{
|
||||||
|
Targets: filteredTarget.String(),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, notifs)
|
||||||
|
require.Equal(t, 10, notifs.UnreadCount)
|
||||||
|
require.Len(t, notifs.Notifications, 5)
|
||||||
|
|
||||||
|
require.Equal(t, "Notification 8", notifs.Notifications[0].Title)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("OK with multiple filters", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
client, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{})
|
||||||
|
firstUser := coderdtest.CreateFirstUser(t, client)
|
||||||
|
client, member := coderdtest.CreateAnotherUser(t, client, firstUser.OrganizationID)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
notifs, err := client.ListInboxNotifications(ctx, codersdk.ListInboxNotificationsRequest{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, notifs)
|
||||||
|
require.Equal(t, 0, notifs.UnreadCount)
|
||||||
|
require.Empty(t, notifs.Notifications)
|
||||||
|
|
||||||
|
filteredTarget := uuid.New()
|
||||||
|
|
||||||
|
for i := range 10 {
|
||||||
|
dbgen.NotificationInbox(t, api.Database, database.InsertInboxNotificationParams{
|
||||||
|
ID: uuid.New(),
|
||||||
|
UserID: member.ID,
|
||||||
|
TemplateID: func() uuid.UUID {
|
||||||
|
if i < 5 {
|
||||||
|
return notifications.TemplateWorkspaceOutOfMemory
|
||||||
|
}
|
||||||
|
|
||||||
|
return notifications.TemplateWorkspaceOutOfDisk
|
||||||
|
}(),
|
||||||
|
Targets: func() []uuid.UUID {
|
||||||
|
if i%2 == 0 {
|
||||||
|
return []uuid.UUID{filteredTarget}
|
||||||
|
}
|
||||||
|
|
||||||
|
return []uuid.UUID{}
|
||||||
|
}(),
|
||||||
|
Title: fmt.Sprintf("Notification %d", i),
|
||||||
|
Actions: json.RawMessage("[]"),
|
||||||
|
Content: fmt.Sprintf("Content of the notif %d", i),
|
||||||
|
CreatedAt: dbtime.Now(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
notifs, err = client.ListInboxNotifications(ctx, codersdk.ListInboxNotificationsRequest{
|
||||||
|
Targets: filteredTarget.String(),
|
||||||
|
Templates: notifications.TemplateWorkspaceOutOfDisk.String(),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, notifs)
|
||||||
|
require.Equal(t, 10, notifs.UnreadCount)
|
||||||
|
require.Len(t, notifs.Notifications, 2)
|
||||||
|
|
||||||
|
require.Equal(t, "Notification 8", notifs.Notifications[0].Title)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInboxNotifications_ReadStatus(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// I skip these tests specifically on windows as for now they are flaky - only on Windows.
|
||||||
|
// For now the idea is that the runner takes too long to insert the entries, could be worth
|
||||||
|
// investigating a manual Tx.
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("our runners are randomly taking too long to insert entries")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("ok", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
client, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{})
|
||||||
|
firstUser := coderdtest.CreateFirstUser(t, client)
|
||||||
|
client, member := coderdtest.CreateAnotherUser(t, client, firstUser.OrganizationID)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
notifs, err := client.ListInboxNotifications(ctx, codersdk.ListInboxNotificationsRequest{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, notifs)
|
||||||
|
require.Equal(t, 0, notifs.UnreadCount)
|
||||||
|
require.Empty(t, notifs.Notifications)
|
||||||
|
|
||||||
|
for i := range 20 {
|
||||||
|
dbgen.NotificationInbox(t, api.Database, database.InsertInboxNotificationParams{
|
||||||
|
ID: uuid.New(),
|
||||||
|
UserID: member.ID,
|
||||||
|
TemplateID: notifications.TemplateWorkspaceOutOfMemory,
|
||||||
|
Title: fmt.Sprintf("Notification %d", i),
|
||||||
|
Actions: json.RawMessage("[]"),
|
||||||
|
Content: fmt.Sprintf("Content of the notif %d", i),
|
||||||
|
CreatedAt: dbtime.Now(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
notifs, err = client.ListInboxNotifications(ctx, codersdk.ListInboxNotificationsRequest{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, notifs)
|
||||||
|
require.Equal(t, 20, notifs.UnreadCount)
|
||||||
|
require.Len(t, notifs.Notifications, 20)
|
||||||
|
|
||||||
|
updatedNotif, err := client.UpdateInboxNotificationReadStatus(ctx, notifs.Notifications[19].ID.String(), codersdk.UpdateInboxNotificationReadStatusRequest{
|
||||||
|
IsRead: true,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, updatedNotif)
|
||||||
|
require.NotZero(t, updatedNotif.Notification.ReadAt)
|
||||||
|
require.Equal(t, 19, updatedNotif.UnreadCount)
|
||||||
|
|
||||||
|
updatedNotif, err = client.UpdateInboxNotificationReadStatus(ctx, notifs.Notifications[19].ID.String(), codersdk.UpdateInboxNotificationReadStatusRequest{
|
||||||
|
IsRead: false,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, updatedNotif)
|
||||||
|
require.Nil(t, updatedNotif.Notification.ReadAt)
|
||||||
|
require.Equal(t, 20, updatedNotif.UnreadCount)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("NOK - wrong id", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
client, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{})
|
||||||
|
firstUser := coderdtest.CreateFirstUser(t, client)
|
||||||
|
client, member := coderdtest.CreateAnotherUser(t, client, firstUser.OrganizationID)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
notifs, err := client.ListInboxNotifications(ctx, codersdk.ListInboxNotificationsRequest{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, notifs)
|
||||||
|
require.Equal(t, 0, notifs.UnreadCount)
|
||||||
|
require.Empty(t, notifs.Notifications)
|
||||||
|
|
||||||
|
for i := range 20 {
|
||||||
|
dbgen.NotificationInbox(t, api.Database, database.InsertInboxNotificationParams{
|
||||||
|
ID: uuid.New(),
|
||||||
|
UserID: member.ID,
|
||||||
|
TemplateID: notifications.TemplateWorkspaceOutOfMemory,
|
||||||
|
Title: fmt.Sprintf("Notification %d", i),
|
||||||
|
Actions: json.RawMessage("[]"),
|
||||||
|
Content: fmt.Sprintf("Content of the notif %d", i),
|
||||||
|
CreatedAt: dbtime.Now(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
notifs, err = client.ListInboxNotifications(ctx, codersdk.ListInboxNotificationsRequest{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, notifs)
|
||||||
|
require.Equal(t, 20, notifs.UnreadCount)
|
||||||
|
require.Len(t, notifs.Notifications, 20)
|
||||||
|
|
||||||
|
updatedNotif, err := client.UpdateInboxNotificationReadStatus(ctx, "xxx-xxx-xxx", codersdk.UpdateInboxNotificationReadStatusRequest{
|
||||||
|
IsRead: true,
|
||||||
|
})
|
||||||
|
require.ErrorContains(t, err, `Invalid UUID "xxx-xxx-xxx"`)
|
||||||
|
require.Equal(t, 0, updatedNotif.UnreadCount)
|
||||||
|
require.Empty(t, updatedNotif.Notification)
|
||||||
|
})
|
||||||
|
t.Run("NOK - unknown id", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
client, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{})
|
||||||
|
firstUser := coderdtest.CreateFirstUser(t, client)
|
||||||
|
client, member := coderdtest.CreateAnotherUser(t, client, firstUser.OrganizationID)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
notifs, err := client.ListInboxNotifications(ctx, codersdk.ListInboxNotificationsRequest{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, notifs)
|
||||||
|
require.Equal(t, 0, notifs.UnreadCount)
|
||||||
|
require.Empty(t, notifs.Notifications)
|
||||||
|
|
||||||
|
for i := range 20 {
|
||||||
|
dbgen.NotificationInbox(t, api.Database, database.InsertInboxNotificationParams{
|
||||||
|
ID: uuid.New(),
|
||||||
|
UserID: member.ID,
|
||||||
|
TemplateID: notifications.TemplateWorkspaceOutOfMemory,
|
||||||
|
Title: fmt.Sprintf("Notification %d", i),
|
||||||
|
Actions: json.RawMessage("[]"),
|
||||||
|
Content: fmt.Sprintf("Content of the notif %d", i),
|
||||||
|
CreatedAt: dbtime.Now(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
notifs, err = client.ListInboxNotifications(ctx, codersdk.ListInboxNotificationsRequest{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, notifs)
|
||||||
|
require.Equal(t, 20, notifs.UnreadCount)
|
||||||
|
require.Len(t, notifs.Notifications, 20)
|
||||||
|
|
||||||
|
updatedNotif, err := client.UpdateInboxNotificationReadStatus(ctx, failingPaginationUUID.String(), codersdk.UpdateInboxNotificationReadStatusRequest{
|
||||||
|
IsRead: true,
|
||||||
|
})
|
||||||
|
require.ErrorContains(t, err, `Failed to update inbox notification read status`)
|
||||||
|
require.Equal(t, 0, updatedNotif.UnreadCount)
|
||||||
|
require.Empty(t, updatedNotif.Notification)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -13,8 +13,11 @@ import (
|
|||||||
|
|
||||||
"github.com/coder/coder/v2/coderd/database"
|
"github.com/coder/coder/v2/coderd/database"
|
||||||
"github.com/coder/coder/v2/coderd/database/dbtime"
|
"github.com/coder/coder/v2/coderd/database/dbtime"
|
||||||
|
"github.com/coder/coder/v2/coderd/database/pubsub"
|
||||||
"github.com/coder/coder/v2/coderd/notifications/types"
|
"github.com/coder/coder/v2/coderd/notifications/types"
|
||||||
|
coderdpubsub "github.com/coder/coder/v2/coderd/pubsub"
|
||||||
markdown "github.com/coder/coder/v2/coderd/render"
|
markdown "github.com/coder/coder/v2/coderd/render"
|
||||||
|
"github.com/coder/coder/v2/codersdk"
|
||||||
)
|
)
|
||||||
|
|
||||||
type InboxStore interface {
|
type InboxStore interface {
|
||||||
@@ -23,12 +26,13 @@ type InboxStore interface {
|
|||||||
|
|
||||||
// InboxHandler is responsible for dispatching notification messages to the Coder Inbox.
|
// InboxHandler is responsible for dispatching notification messages to the Coder Inbox.
|
||||||
type InboxHandler struct {
|
type InboxHandler struct {
|
||||||
log slog.Logger
|
log slog.Logger
|
||||||
store InboxStore
|
store InboxStore
|
||||||
|
pubsub pubsub.Pubsub
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewInboxHandler(log slog.Logger, store InboxStore) *InboxHandler {
|
func NewInboxHandler(log slog.Logger, store InboxStore, ps pubsub.Pubsub) *InboxHandler {
|
||||||
return &InboxHandler{log: log, store: store}
|
return &InboxHandler{log: log, store: store, pubsub: ps}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *InboxHandler) Dispatcher(payload types.MessagePayload, titleTmpl, bodyTmpl string, _ template.FuncMap) (DeliveryFunc, error) {
|
func (s *InboxHandler) Dispatcher(payload types.MessagePayload, titleTmpl, bodyTmpl string, _ template.FuncMap) (DeliveryFunc, error) {
|
||||||
@@ -62,7 +66,7 @@ func (s *InboxHandler) dispatch(payload types.MessagePayload, title, body string
|
|||||||
}
|
}
|
||||||
|
|
||||||
// nolint:exhaustruct
|
// nolint:exhaustruct
|
||||||
_, err = s.store.InsertInboxNotification(ctx, database.InsertInboxNotificationParams{
|
insertedNotif, err := s.store.InsertInboxNotification(ctx, database.InsertInboxNotificationParams{
|
||||||
ID: msgID,
|
ID: msgID,
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
TemplateID: templateID,
|
TemplateID: templateID,
|
||||||
@@ -76,6 +80,38 @@ func (s *InboxHandler) dispatch(payload types.MessagePayload, title, body string
|
|||||||
return false, xerrors.Errorf("insert inbox notification: %w", err)
|
return false, xerrors.Errorf("insert inbox notification: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
event := coderdpubsub.InboxNotificationEvent{
|
||||||
|
Kind: coderdpubsub.InboxNotificationEventKindNew,
|
||||||
|
InboxNotification: codersdk.InboxNotification{
|
||||||
|
ID: msgID,
|
||||||
|
UserID: userID,
|
||||||
|
TemplateID: templateID,
|
||||||
|
Targets: payload.Targets,
|
||||||
|
Title: title,
|
||||||
|
Content: body,
|
||||||
|
Actions: func() []codersdk.InboxNotificationAction {
|
||||||
|
var actions []codersdk.InboxNotificationAction
|
||||||
|
err := json.Unmarshal(insertedNotif.Actions, &actions)
|
||||||
|
if err != nil {
|
||||||
|
return actions
|
||||||
|
}
|
||||||
|
return actions
|
||||||
|
}(),
|
||||||
|
ReadAt: nil, // notification just has been inserted
|
||||||
|
CreatedAt: insertedNotif.CreatedAt,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := json.Marshal(event)
|
||||||
|
if err != nil {
|
||||||
|
return false, xerrors.Errorf("marshal event: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.pubsub.Publish(coderdpubsub.InboxNotificationForOwnerEventChannel(userID), payload)
|
||||||
|
if err != nil {
|
||||||
|
return false, xerrors.Errorf("publish event: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ func TestInbox(t *testing.T) {
|
|||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
db, _ := dbtestutil.NewDB(t)
|
db, pubsub := dbtestutil.NewDB(t)
|
||||||
|
|
||||||
if tc.payload.UserID == "valid" {
|
if tc.payload.UserID == "valid" {
|
||||||
user := dbgen.User(t, db, database.User{})
|
user := dbgen.User(t, db, database.User{})
|
||||||
@@ -82,7 +82,7 @@ func TestInbox(t *testing.T) {
|
|||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
handler := dispatch.NewInboxHandler(logger.Named("smtp"), db)
|
handler := dispatch.NewInboxHandler(logger.Named("smtp"), db, pubsub)
|
||||||
dispatcherFunc, err := handler.Dispatcher(tc.payload, "", "", nil)
|
dispatcherFunc, err := handler.Dispatcher(tc.payload, "", "", nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/coder/quartz"
|
"github.com/coder/quartz"
|
||||||
|
|
||||||
"github.com/coder/coder/v2/coderd/database"
|
"github.com/coder/coder/v2/coderd/database"
|
||||||
|
"github.com/coder/coder/v2/coderd/database/pubsub"
|
||||||
"github.com/coder/coder/v2/coderd/notifications/dispatch"
|
"github.com/coder/coder/v2/coderd/notifications/dispatch"
|
||||||
"github.com/coder/coder/v2/codersdk"
|
"github.com/coder/coder/v2/codersdk"
|
||||||
)
|
)
|
||||||
@@ -75,8 +76,7 @@ func WithTestClock(clock quartz.Clock) ManagerOption {
|
|||||||
//
|
//
|
||||||
// helpers is a map of template helpers which are used to customize notification messages to use global settings like
|
// helpers is a map of template helpers which are used to customize notification messages to use global settings like
|
||||||
// access URL etc.
|
// access URL etc.
|
||||||
func NewManager(cfg codersdk.NotificationsConfig, store Store, helpers template.FuncMap, metrics *Metrics, log slog.Logger, opts ...ManagerOption) (*Manager, error) {
|
func NewManager(cfg codersdk.NotificationsConfig, store Store, ps pubsub.Pubsub, helpers template.FuncMap, metrics *Metrics, log slog.Logger, opts ...ManagerOption) (*Manager, error) {
|
||||||
// TODO(dannyk): add the ability to use multiple notification methods.
|
|
||||||
var method database.NotificationMethod
|
var method database.NotificationMethod
|
||||||
if err := method.Scan(cfg.Method.String()); err != nil {
|
if err := method.Scan(cfg.Method.String()); err != nil {
|
||||||
return nil, xerrors.Errorf("notification method %q is invalid", cfg.Method)
|
return nil, xerrors.Errorf("notification method %q is invalid", cfg.Method)
|
||||||
@@ -109,7 +109,7 @@ func NewManager(cfg codersdk.NotificationsConfig, store Store, helpers template.
|
|||||||
stop: make(chan any),
|
stop: make(chan any),
|
||||||
done: make(chan any),
|
done: make(chan any),
|
||||||
|
|
||||||
handlers: defaultHandlers(cfg, log, store),
|
handlers: defaultHandlers(cfg, log, store, ps),
|
||||||
helpers: helpers,
|
helpers: helpers,
|
||||||
|
|
||||||
clock: quartz.NewReal(),
|
clock: quartz.NewReal(),
|
||||||
@@ -121,11 +121,11 @@ func NewManager(cfg codersdk.NotificationsConfig, store Store, helpers template.
|
|||||||
}
|
}
|
||||||
|
|
||||||
// defaultHandlers builds a set of known handlers; panics if any error occurs as these handlers should be valid at compile time.
|
// defaultHandlers builds a set of known handlers; panics if any error occurs as these handlers should be valid at compile time.
|
||||||
func defaultHandlers(cfg codersdk.NotificationsConfig, log slog.Logger, store Store) map[database.NotificationMethod]Handler {
|
func defaultHandlers(cfg codersdk.NotificationsConfig, log slog.Logger, store Store, ps pubsub.Pubsub) map[database.NotificationMethod]Handler {
|
||||||
return map[database.NotificationMethod]Handler{
|
return map[database.NotificationMethod]Handler{
|
||||||
database.NotificationMethodSmtp: dispatch.NewSMTPHandler(cfg.SMTP, log.Named("dispatcher.smtp")),
|
database.NotificationMethodSmtp: dispatch.NewSMTPHandler(cfg.SMTP, log.Named("dispatcher.smtp")),
|
||||||
database.NotificationMethodWebhook: dispatch.NewWebhookHandler(cfg.Webhook, log.Named("dispatcher.webhook")),
|
database.NotificationMethodWebhook: dispatch.NewWebhookHandler(cfg.Webhook, log.Named("dispatcher.webhook")),
|
||||||
database.NotificationMethodInbox: dispatch.NewInboxHandler(log.Named("dispatcher.inbox"), store),
|
database.NotificationMethodInbox: dispatch.NewInboxHandler(log.Named("dispatcher.inbox"), store, ps),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ func TestBufferedUpdates(t *testing.T) {
|
|||||||
|
|
||||||
// nolint:gocritic // Unit test.
|
// nolint:gocritic // Unit test.
|
||||||
ctx := dbauthz.AsSystemRestricted(testutil.Context(t, testutil.WaitSuperLong))
|
ctx := dbauthz.AsSystemRestricted(testutil.Context(t, testutil.WaitSuperLong))
|
||||||
store, _ := dbtestutil.NewDB(t)
|
store, ps := dbtestutil.NewDB(t)
|
||||||
logger := testutil.Logger(t)
|
logger := testutil.Logger(t)
|
||||||
|
|
||||||
interceptor := &syncInterceptor{Store: store}
|
interceptor := &syncInterceptor{Store: store}
|
||||||
@@ -44,7 +44,7 @@ func TestBufferedUpdates(t *testing.T) {
|
|||||||
cfg.StoreSyncInterval = serpent.Duration(time.Hour) // Ensure we don't sync the store automatically.
|
cfg.StoreSyncInterval = serpent.Duration(time.Hour) // Ensure we don't sync the store automatically.
|
||||||
|
|
||||||
// GIVEN: a manager which will pass or fail notifications based on their "nice" labels
|
// GIVEN: a manager which will pass or fail notifications based on their "nice" labels
|
||||||
mgr, err := notifications.NewManager(cfg, interceptor, defaultHelpers(), createMetrics(), logger.Named("notifications-manager"))
|
mgr, err := notifications.NewManager(cfg, interceptor, ps, defaultHelpers(), createMetrics(), logger.Named("notifications-manager"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
handlers := map[database.NotificationMethod]notifications.Handler{
|
handlers := map[database.NotificationMethod]notifications.Handler{
|
||||||
@@ -168,11 +168,11 @@ func TestStopBeforeRun(t *testing.T) {
|
|||||||
|
|
||||||
// nolint:gocritic // Unit test.
|
// nolint:gocritic // Unit test.
|
||||||
ctx := dbauthz.AsSystemRestricted(testutil.Context(t, testutil.WaitSuperLong))
|
ctx := dbauthz.AsSystemRestricted(testutil.Context(t, testutil.WaitSuperLong))
|
||||||
store, _ := dbtestutil.NewDB(t)
|
store, ps := dbtestutil.NewDB(t)
|
||||||
logger := testutil.Logger(t)
|
logger := testutil.Logger(t)
|
||||||
|
|
||||||
// GIVEN: a standard manager
|
// GIVEN: a standard manager
|
||||||
mgr, err := notifications.NewManager(defaultNotificationsConfig(database.NotificationMethodSmtp), store, defaultHelpers(), createMetrics(), logger.Named("notifications-manager"))
|
mgr, err := notifications.NewManager(defaultNotificationsConfig(database.NotificationMethodSmtp), store, ps, defaultHelpers(), createMetrics(), logger.Named("notifications-manager"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// THEN: validate that the manager can be stopped safely without Run() having been called yet
|
// THEN: validate that the manager can be stopped safely without Run() having been called yet
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ func TestMetrics(t *testing.T) {
|
|||||||
|
|
||||||
// nolint:gocritic // Unit test.
|
// nolint:gocritic // Unit test.
|
||||||
ctx := dbauthz.AsSystemRestricted(testutil.Context(t, testutil.WaitSuperLong))
|
ctx := dbauthz.AsSystemRestricted(testutil.Context(t, testutil.WaitSuperLong))
|
||||||
store, _ := dbtestutil.NewDB(t)
|
store, pubsub := dbtestutil.NewDB(t)
|
||||||
logger := testutil.Logger(t)
|
logger := testutil.Logger(t)
|
||||||
|
|
||||||
reg := prometheus.NewRegistry()
|
reg := prometheus.NewRegistry()
|
||||||
@@ -60,7 +60,7 @@ func TestMetrics(t *testing.T) {
|
|||||||
cfg.RetryInterval = serpent.Duration(time.Millisecond * 50)
|
cfg.RetryInterval = serpent.Duration(time.Millisecond * 50)
|
||||||
cfg.StoreSyncInterval = serpent.Duration(time.Millisecond * 100) // Twice as long as fetch interval to ensure we catch pending updates.
|
cfg.StoreSyncInterval = serpent.Duration(time.Millisecond * 100) // Twice as long as fetch interval to ensure we catch pending updates.
|
||||||
|
|
||||||
mgr, err := notifications.NewManager(cfg, store, defaultHelpers(), metrics, logger.Named("manager"))
|
mgr, err := notifications.NewManager(cfg, store, pubsub, defaultHelpers(), metrics, logger.Named("manager"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
assert.NoError(t, mgr.Stop(ctx))
|
assert.NoError(t, mgr.Stop(ctx))
|
||||||
@@ -228,7 +228,7 @@ func TestPendingUpdatesMetric(t *testing.T) {
|
|||||||
// SETUP
|
// SETUP
|
||||||
// nolint:gocritic // Unit test.
|
// nolint:gocritic // Unit test.
|
||||||
ctx := dbauthz.AsSystemRestricted(testutil.Context(t, testutil.WaitSuperLong))
|
ctx := dbauthz.AsSystemRestricted(testutil.Context(t, testutil.WaitSuperLong))
|
||||||
store, _ := dbtestutil.NewDB(t)
|
store, pubsub := dbtestutil.NewDB(t)
|
||||||
logger := testutil.Logger(t)
|
logger := testutil.Logger(t)
|
||||||
|
|
||||||
reg := prometheus.NewRegistry()
|
reg := prometheus.NewRegistry()
|
||||||
@@ -250,7 +250,7 @@ func TestPendingUpdatesMetric(t *testing.T) {
|
|||||||
defer trap.Close()
|
defer trap.Close()
|
||||||
fetchTrap := mClock.Trap().TickerFunc("notifier", "fetchInterval")
|
fetchTrap := mClock.Trap().TickerFunc("notifier", "fetchInterval")
|
||||||
defer fetchTrap.Close()
|
defer fetchTrap.Close()
|
||||||
mgr, err := notifications.NewManager(cfg, interceptor, defaultHelpers(), metrics, logger.Named("manager"),
|
mgr, err := notifications.NewManager(cfg, interceptor, pubsub, defaultHelpers(), metrics, logger.Named("manager"),
|
||||||
notifications.WithTestClock(mClock))
|
notifications.WithTestClock(mClock))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
@@ -322,7 +322,7 @@ func TestInflightDispatchesMetric(t *testing.T) {
|
|||||||
// SETUP
|
// SETUP
|
||||||
// nolint:gocritic // Unit test.
|
// nolint:gocritic // Unit test.
|
||||||
ctx := dbauthz.AsSystemRestricted(testutil.Context(t, testutil.WaitSuperLong))
|
ctx := dbauthz.AsSystemRestricted(testutil.Context(t, testutil.WaitSuperLong))
|
||||||
store, _ := dbtestutil.NewDB(t)
|
store, pubsub := dbtestutil.NewDB(t)
|
||||||
logger := testutil.Logger(t)
|
logger := testutil.Logger(t)
|
||||||
|
|
||||||
reg := prometheus.NewRegistry()
|
reg := prometheus.NewRegistry()
|
||||||
@@ -338,7 +338,7 @@ func TestInflightDispatchesMetric(t *testing.T) {
|
|||||||
cfg.RetryInterval = serpent.Duration(time.Hour) // Delay retries so they don't interfere.
|
cfg.RetryInterval = serpent.Duration(time.Hour) // Delay retries so they don't interfere.
|
||||||
cfg.StoreSyncInterval = serpent.Duration(time.Millisecond * 100)
|
cfg.StoreSyncInterval = serpent.Duration(time.Millisecond * 100)
|
||||||
|
|
||||||
mgr, err := notifications.NewManager(cfg, store, defaultHelpers(), metrics, logger.Named("manager"))
|
mgr, err := notifications.NewManager(cfg, store, pubsub, defaultHelpers(), metrics, logger.Named("manager"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
assert.NoError(t, mgr.Stop(ctx))
|
assert.NoError(t, mgr.Stop(ctx))
|
||||||
@@ -402,7 +402,7 @@ func TestCustomMethodMetricCollection(t *testing.T) {
|
|||||||
|
|
||||||
// nolint:gocritic // Unit test.
|
// nolint:gocritic // Unit test.
|
||||||
ctx := dbauthz.AsSystemRestricted(testutil.Context(t, testutil.WaitSuperLong))
|
ctx := dbauthz.AsSystemRestricted(testutil.Context(t, testutil.WaitSuperLong))
|
||||||
store, _ := dbtestutil.NewDB(t)
|
store, pubsub := dbtestutil.NewDB(t)
|
||||||
logger := testutil.Logger(t)
|
logger := testutil.Logger(t)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -427,7 +427,7 @@ func TestCustomMethodMetricCollection(t *testing.T) {
|
|||||||
|
|
||||||
// WHEN: two notifications (each with different templates) are enqueued.
|
// WHEN: two notifications (each with different templates) are enqueued.
|
||||||
cfg := defaultNotificationsConfig(defaultMethod)
|
cfg := defaultNotificationsConfig(defaultMethod)
|
||||||
mgr, err := notifications.NewManager(cfg, store, defaultHelpers(), metrics, logger.Named("manager"))
|
mgr, err := notifications.NewManager(cfg, store, pubsub, defaultHelpers(), metrics, logger.Named("manager"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
assert.NoError(t, mgr.Stop(ctx))
|
assert.NoError(t, mgr.Stop(ctx))
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ func TestBasicNotificationRoundtrip(t *testing.T) {
|
|||||||
|
|
||||||
// nolint:gocritic // Unit test.
|
// nolint:gocritic // Unit test.
|
||||||
ctx := dbauthz.AsNotifier(testutil.Context(t, testutil.WaitSuperLong))
|
ctx := dbauthz.AsNotifier(testutil.Context(t, testutil.WaitSuperLong))
|
||||||
store, _ := dbtestutil.NewDB(t)
|
store, pubsub := dbtestutil.NewDB(t)
|
||||||
logger := testutil.Logger(t)
|
logger := testutil.Logger(t)
|
||||||
method := database.NotificationMethodSmtp
|
method := database.NotificationMethodSmtp
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ func TestBasicNotificationRoundtrip(t *testing.T) {
|
|||||||
interceptor := &syncInterceptor{Store: store}
|
interceptor := &syncInterceptor{Store: store}
|
||||||
cfg := defaultNotificationsConfig(method)
|
cfg := defaultNotificationsConfig(method)
|
||||||
cfg.RetryInterval = serpent.Duration(time.Hour) // Ensure retries don't interfere with the test
|
cfg.RetryInterval = serpent.Duration(time.Hour) // Ensure retries don't interfere with the test
|
||||||
mgr, err := notifications.NewManager(cfg, interceptor, defaultHelpers(), createMetrics(), logger.Named("manager"))
|
mgr, err := notifications.NewManager(cfg, interceptor, pubsub, defaultHelpers(), createMetrics(), logger.Named("manager"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
mgr.WithHandlers(map[database.NotificationMethod]notifications.Handler{
|
mgr.WithHandlers(map[database.NotificationMethod]notifications.Handler{
|
||||||
method: handler,
|
method: handler,
|
||||||
@@ -138,7 +138,7 @@ func TestSMTPDispatch(t *testing.T) {
|
|||||||
|
|
||||||
// nolint:gocritic // Unit test.
|
// nolint:gocritic // Unit test.
|
||||||
ctx := dbauthz.AsNotifier(testutil.Context(t, testutil.WaitSuperLong))
|
ctx := dbauthz.AsNotifier(testutil.Context(t, testutil.WaitSuperLong))
|
||||||
store, _ := dbtestutil.NewDB(t)
|
store, pubsub := dbtestutil.NewDB(t)
|
||||||
logger := testutil.Logger(t)
|
logger := testutil.Logger(t)
|
||||||
|
|
||||||
// start mock SMTP server
|
// start mock SMTP server
|
||||||
@@ -161,7 +161,7 @@ func TestSMTPDispatch(t *testing.T) {
|
|||||||
Hello: "localhost",
|
Hello: "localhost",
|
||||||
}
|
}
|
||||||
handler := newDispatchInterceptor(dispatch.NewSMTPHandler(cfg.SMTP, logger.Named("smtp")))
|
handler := newDispatchInterceptor(dispatch.NewSMTPHandler(cfg.SMTP, logger.Named("smtp")))
|
||||||
mgr, err := notifications.NewManager(cfg, store, defaultHelpers(), createMetrics(), logger.Named("manager"))
|
mgr, err := notifications.NewManager(cfg, store, pubsub, defaultHelpers(), createMetrics(), logger.Named("manager"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
mgr.WithHandlers(map[database.NotificationMethod]notifications.Handler{
|
mgr.WithHandlers(map[database.NotificationMethod]notifications.Handler{
|
||||||
method: handler,
|
method: handler,
|
||||||
@@ -204,7 +204,7 @@ func TestWebhookDispatch(t *testing.T) {
|
|||||||
|
|
||||||
// nolint:gocritic // Unit test.
|
// nolint:gocritic // Unit test.
|
||||||
ctx := dbauthz.AsNotifier(testutil.Context(t, testutil.WaitSuperLong))
|
ctx := dbauthz.AsNotifier(testutil.Context(t, testutil.WaitSuperLong))
|
||||||
store, _ := dbtestutil.NewDB(t)
|
store, pubsub := dbtestutil.NewDB(t)
|
||||||
logger := testutil.Logger(t)
|
logger := testutil.Logger(t)
|
||||||
|
|
||||||
sent := make(chan dispatch.WebhookPayload, 1)
|
sent := make(chan dispatch.WebhookPayload, 1)
|
||||||
@@ -230,7 +230,7 @@ func TestWebhookDispatch(t *testing.T) {
|
|||||||
cfg.Webhook = codersdk.NotificationsWebhookConfig{
|
cfg.Webhook = codersdk.NotificationsWebhookConfig{
|
||||||
Endpoint: *serpent.URLOf(endpoint),
|
Endpoint: *serpent.URLOf(endpoint),
|
||||||
}
|
}
|
||||||
mgr, err := notifications.NewManager(cfg, store, defaultHelpers(), createMetrics(), logger.Named("manager"))
|
mgr, err := notifications.NewManager(cfg, store, pubsub, defaultHelpers(), createMetrics(), logger.Named("manager"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
assert.NoError(t, mgr.Stop(ctx))
|
assert.NoError(t, mgr.Stop(ctx))
|
||||||
@@ -284,7 +284,7 @@ func TestBackpressure(t *testing.T) {
|
|||||||
t.Skip("This test requires postgres; it relies on business-logic only implemented in the database")
|
t.Skip("This test requires postgres; it relies on business-logic only implemented in the database")
|
||||||
}
|
}
|
||||||
|
|
||||||
store, _ := dbtestutil.NewDB(t)
|
store, pubsub := dbtestutil.NewDB(t)
|
||||||
logger := testutil.Logger(t)
|
logger := testutil.Logger(t)
|
||||||
// nolint:gocritic // Unit test.
|
// nolint:gocritic // Unit test.
|
||||||
ctx := dbauthz.AsNotifier(testutil.Context(t, testutil.WaitShort))
|
ctx := dbauthz.AsNotifier(testutil.Context(t, testutil.WaitShort))
|
||||||
@@ -319,7 +319,7 @@ func TestBackpressure(t *testing.T) {
|
|||||||
defer fetchTrap.Close()
|
defer fetchTrap.Close()
|
||||||
|
|
||||||
// GIVEN: a notification manager whose updates will be intercepted
|
// GIVEN: a notification manager whose updates will be intercepted
|
||||||
mgr, err := notifications.NewManager(cfg, storeInterceptor, defaultHelpers(), createMetrics(),
|
mgr, err := notifications.NewManager(cfg, storeInterceptor, pubsub, defaultHelpers(), createMetrics(),
|
||||||
logger.Named("manager"), notifications.WithTestClock(mClock))
|
logger.Named("manager"), notifications.WithTestClock(mClock))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
mgr.WithHandlers(map[database.NotificationMethod]notifications.Handler{
|
mgr.WithHandlers(map[database.NotificationMethod]notifications.Handler{
|
||||||
@@ -417,7 +417,7 @@ func TestRetries(t *testing.T) {
|
|||||||
const maxAttempts = 3
|
const maxAttempts = 3
|
||||||
// nolint:gocritic // Unit test.
|
// nolint:gocritic // Unit test.
|
||||||
ctx := dbauthz.AsNotifier(testutil.Context(t, testutil.WaitSuperLong))
|
ctx := dbauthz.AsNotifier(testutil.Context(t, testutil.WaitSuperLong))
|
||||||
store, _ := dbtestutil.NewDB(t)
|
store, pubsub := dbtestutil.NewDB(t)
|
||||||
logger := testutil.Logger(t)
|
logger := testutil.Logger(t)
|
||||||
|
|
||||||
// GIVEN: a mock HTTP server which will receive webhooksand a map to track the dispatch attempts
|
// GIVEN: a mock HTTP server which will receive webhooksand a map to track the dispatch attempts
|
||||||
@@ -468,7 +468,7 @@ func TestRetries(t *testing.T) {
|
|||||||
// Intercept calls to submit the buffered updates to the store.
|
// Intercept calls to submit the buffered updates to the store.
|
||||||
storeInterceptor := &syncInterceptor{Store: store}
|
storeInterceptor := &syncInterceptor{Store: store}
|
||||||
|
|
||||||
mgr, err := notifications.NewManager(cfg, storeInterceptor, defaultHelpers(), createMetrics(), logger.Named("manager"))
|
mgr, err := notifications.NewManager(cfg, storeInterceptor, pubsub, defaultHelpers(), createMetrics(), logger.Named("manager"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
assert.NoError(t, mgr.Stop(ctx))
|
assert.NoError(t, mgr.Stop(ctx))
|
||||||
@@ -517,7 +517,7 @@ func TestExpiredLeaseIsRequeued(t *testing.T) {
|
|||||||
|
|
||||||
// nolint:gocritic // Unit test.
|
// nolint:gocritic // Unit test.
|
||||||
ctx := dbauthz.AsNotifier(testutil.Context(t, testutil.WaitSuperLong))
|
ctx := dbauthz.AsNotifier(testutil.Context(t, testutil.WaitSuperLong))
|
||||||
store, _ := dbtestutil.NewDB(t)
|
store, pubsub := dbtestutil.NewDB(t)
|
||||||
logger := testutil.Logger(t)
|
logger := testutil.Logger(t)
|
||||||
|
|
||||||
// GIVEN: a manager which has its updates intercepted and paused until measurements can be taken
|
// GIVEN: a manager which has its updates intercepted and paused until measurements can be taken
|
||||||
@@ -539,7 +539,7 @@ func TestExpiredLeaseIsRequeued(t *testing.T) {
|
|||||||
mgrCtx, cancelManagerCtx := context.WithCancel(dbauthz.AsNotifier(context.Background()))
|
mgrCtx, cancelManagerCtx := context.WithCancel(dbauthz.AsNotifier(context.Background()))
|
||||||
t.Cleanup(cancelManagerCtx)
|
t.Cleanup(cancelManagerCtx)
|
||||||
|
|
||||||
mgr, err := notifications.NewManager(cfg, noopInterceptor, defaultHelpers(), createMetrics(), logger.Named("manager"))
|
mgr, err := notifications.NewManager(cfg, noopInterceptor, pubsub, defaultHelpers(), createMetrics(), logger.Named("manager"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
enq, err := notifications.NewStoreEnqueuer(cfg, store, defaultHelpers(), logger.Named("enqueuer"), quartz.NewReal())
|
enq, err := notifications.NewStoreEnqueuer(cfg, store, defaultHelpers(), logger.Named("enqueuer"), quartz.NewReal())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -588,7 +588,7 @@ func TestExpiredLeaseIsRequeued(t *testing.T) {
|
|||||||
// Intercept calls to submit the buffered updates to the store.
|
// Intercept calls to submit the buffered updates to the store.
|
||||||
storeInterceptor := &syncInterceptor{Store: store}
|
storeInterceptor := &syncInterceptor{Store: store}
|
||||||
handler := newDispatchInterceptor(&fakeHandler{})
|
handler := newDispatchInterceptor(&fakeHandler{})
|
||||||
mgr, err = notifications.NewManager(cfg, storeInterceptor, defaultHelpers(), createMetrics(), logger.Named("manager"))
|
mgr, err = notifications.NewManager(cfg, storeInterceptor, pubsub, defaultHelpers(), createMetrics(), logger.Named("manager"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
mgr.WithHandlers(map[database.NotificationMethod]notifications.Handler{
|
mgr.WithHandlers(map[database.NotificationMethod]notifications.Handler{
|
||||||
method: handler,
|
method: handler,
|
||||||
@@ -620,7 +620,7 @@ func TestExpiredLeaseIsRequeued(t *testing.T) {
|
|||||||
func TestInvalidConfig(t *testing.T) {
|
func TestInvalidConfig(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
store, _ := dbtestutil.NewDB(t)
|
store, pubsub := dbtestutil.NewDB(t)
|
||||||
logger := testutil.Logger(t)
|
logger := testutil.Logger(t)
|
||||||
|
|
||||||
// GIVEN: invalid config with dispatch period <= lease period
|
// GIVEN: invalid config with dispatch period <= lease period
|
||||||
@@ -633,7 +633,7 @@ func TestInvalidConfig(t *testing.T) {
|
|||||||
cfg.DispatchTimeout = serpent.Duration(leasePeriod)
|
cfg.DispatchTimeout = serpent.Duration(leasePeriod)
|
||||||
|
|
||||||
// WHEN: the manager is created with invalid config
|
// WHEN: the manager is created with invalid config
|
||||||
_, err := notifications.NewManager(cfg, store, defaultHelpers(), createMetrics(), logger.Named("manager"))
|
_, err := notifications.NewManager(cfg, store, pubsub, defaultHelpers(), createMetrics(), logger.Named("manager"))
|
||||||
|
|
||||||
// THEN: the manager will fail to be created, citing invalid config as error
|
// THEN: the manager will fail to be created, citing invalid config as error
|
||||||
require.ErrorIs(t, err, notifications.ErrInvalidDispatchTimeout)
|
require.ErrorIs(t, err, notifications.ErrInvalidDispatchTimeout)
|
||||||
@@ -646,7 +646,7 @@ func TestNotifierPaused(t *testing.T) {
|
|||||||
|
|
||||||
// nolint:gocritic // Unit test.
|
// nolint:gocritic // Unit test.
|
||||||
ctx := dbauthz.AsNotifier(testutil.Context(t, testutil.WaitSuperLong))
|
ctx := dbauthz.AsNotifier(testutil.Context(t, testutil.WaitSuperLong))
|
||||||
store, _ := dbtestutil.NewDB(t)
|
store, pubsub := dbtestutil.NewDB(t)
|
||||||
logger := testutil.Logger(t)
|
logger := testutil.Logger(t)
|
||||||
|
|
||||||
// Prepare the test.
|
// Prepare the test.
|
||||||
@@ -657,7 +657,7 @@ func TestNotifierPaused(t *testing.T) {
|
|||||||
const fetchInterval = time.Millisecond * 100
|
const fetchInterval = time.Millisecond * 100
|
||||||
cfg := defaultNotificationsConfig(method)
|
cfg := defaultNotificationsConfig(method)
|
||||||
cfg.FetchInterval = serpent.Duration(fetchInterval)
|
cfg.FetchInterval = serpent.Duration(fetchInterval)
|
||||||
mgr, err := notifications.NewManager(cfg, store, defaultHelpers(), createMetrics(), logger.Named("manager"))
|
mgr, err := notifications.NewManager(cfg, store, pubsub, defaultHelpers(), createMetrics(), logger.Named("manager"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
mgr.WithHandlers(map[database.NotificationMethod]notifications.Handler{
|
mgr.WithHandlers(map[database.NotificationMethod]notifications.Handler{
|
||||||
method: handler,
|
method: handler,
|
||||||
@@ -1229,6 +1229,8 @@ func TestNotificationTemplates_Golden(t *testing.T) {
|
|||||||
// nolint:gocritic // Unit test.
|
// nolint:gocritic // Unit test.
|
||||||
ctx := dbauthz.AsNotifier(testutil.Context(t, testutil.WaitSuperLong))
|
ctx := dbauthz.AsNotifier(testutil.Context(t, testutil.WaitSuperLong))
|
||||||
|
|
||||||
|
_, pubsub := dbtestutil.NewDB(t)
|
||||||
|
|
||||||
// smtp config shared between client and server
|
// smtp config shared between client and server
|
||||||
smtpConfig := codersdk.NotificationsEmailConfig{
|
smtpConfig := codersdk.NotificationsEmailConfig{
|
||||||
Hello: hello,
|
Hello: hello,
|
||||||
@@ -1296,6 +1298,7 @@ func TestNotificationTemplates_Golden(t *testing.T) {
|
|||||||
smtpManager, err := notifications.NewManager(
|
smtpManager, err := notifications.NewManager(
|
||||||
smtpCfg,
|
smtpCfg,
|
||||||
*db,
|
*db,
|
||||||
|
pubsub,
|
||||||
defaultHelpers(),
|
defaultHelpers(),
|
||||||
createMetrics(),
|
createMetrics(),
|
||||||
logger.Named("manager"),
|
logger.Named("manager"),
|
||||||
@@ -1410,6 +1413,7 @@ func TestNotificationTemplates_Golden(t *testing.T) {
|
|||||||
return &db, &api.Logger, &user
|
return &db, &api.Logger, &user
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
_, pubsub := dbtestutil.NewDB(t)
|
||||||
// nolint:gocritic // Unit test.
|
// nolint:gocritic // Unit test.
|
||||||
ctx := dbauthz.AsNotifier(testutil.Context(t, testutil.WaitSuperLong))
|
ctx := dbauthz.AsNotifier(testutil.Context(t, testutil.WaitSuperLong))
|
||||||
|
|
||||||
@@ -1437,6 +1441,7 @@ func TestNotificationTemplates_Golden(t *testing.T) {
|
|||||||
webhookManager, err := notifications.NewManager(
|
webhookManager, err := notifications.NewManager(
|
||||||
webhookCfg,
|
webhookCfg,
|
||||||
*db,
|
*db,
|
||||||
|
pubsub,
|
||||||
defaultHelpers(),
|
defaultHelpers(),
|
||||||
createMetrics(),
|
createMetrics(),
|
||||||
logger.Named("manager"),
|
logger.Named("manager"),
|
||||||
@@ -1613,13 +1618,13 @@ func TestDisabledAfterEnqueue(t *testing.T) {
|
|||||||
|
|
||||||
// nolint:gocritic // Unit test.
|
// nolint:gocritic // Unit test.
|
||||||
ctx := dbauthz.AsNotifier(testutil.Context(t, testutil.WaitSuperLong))
|
ctx := dbauthz.AsNotifier(testutil.Context(t, testutil.WaitSuperLong))
|
||||||
store, _ := dbtestutil.NewDB(t)
|
store, pubsub := dbtestutil.NewDB(t)
|
||||||
logger := testutil.Logger(t)
|
logger := testutil.Logger(t)
|
||||||
|
|
||||||
method := database.NotificationMethodSmtp
|
method := database.NotificationMethodSmtp
|
||||||
cfg := defaultNotificationsConfig(method)
|
cfg := defaultNotificationsConfig(method)
|
||||||
|
|
||||||
mgr, err := notifications.NewManager(cfg, store, defaultHelpers(), createMetrics(), logger.Named("manager"))
|
mgr, err := notifications.NewManager(cfg, store, pubsub, defaultHelpers(), createMetrics(), logger.Named("manager"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
assert.NoError(t, mgr.Stop(ctx))
|
assert.NoError(t, mgr.Stop(ctx))
|
||||||
@@ -1670,7 +1675,7 @@ func TestCustomNotificationMethod(t *testing.T) {
|
|||||||
|
|
||||||
// nolint:gocritic // Unit test.
|
// nolint:gocritic // Unit test.
|
||||||
ctx := dbauthz.AsNotifier(testutil.Context(t, testutil.WaitSuperLong))
|
ctx := dbauthz.AsNotifier(testutil.Context(t, testutil.WaitSuperLong))
|
||||||
store, _ := dbtestutil.NewDB(t)
|
store, pubsub := dbtestutil.NewDB(t)
|
||||||
logger := testutil.Logger(t)
|
logger := testutil.Logger(t)
|
||||||
|
|
||||||
received := make(chan uuid.UUID, 1)
|
received := make(chan uuid.UUID, 1)
|
||||||
@@ -1728,7 +1733,7 @@ func TestCustomNotificationMethod(t *testing.T) {
|
|||||||
Endpoint: *serpent.URLOf(endpoint),
|
Endpoint: *serpent.URLOf(endpoint),
|
||||||
}
|
}
|
||||||
|
|
||||||
mgr, err := notifications.NewManager(cfg, store, defaultHelpers(), createMetrics(), logger.Named("manager"))
|
mgr, err := notifications.NewManager(cfg, store, pubsub, defaultHelpers(), createMetrics(), logger.Named("manager"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
_ = mgr.Stop(ctx)
|
_ = mgr.Stop(ctx)
|
||||||
@@ -1811,13 +1816,13 @@ func TestNotificationDuplicates(t *testing.T) {
|
|||||||
|
|
||||||
// nolint:gocritic // Unit test.
|
// nolint:gocritic // Unit test.
|
||||||
ctx := dbauthz.AsNotifier(testutil.Context(t, testutil.WaitSuperLong))
|
ctx := dbauthz.AsNotifier(testutil.Context(t, testutil.WaitSuperLong))
|
||||||
store, _ := dbtestutil.NewDB(t)
|
store, pubsub := dbtestutil.NewDB(t)
|
||||||
logger := testutil.Logger(t)
|
logger := testutil.Logger(t)
|
||||||
|
|
||||||
method := database.NotificationMethodSmtp
|
method := database.NotificationMethodSmtp
|
||||||
cfg := defaultNotificationsConfig(method)
|
cfg := defaultNotificationsConfig(method)
|
||||||
|
|
||||||
mgr, err := notifications.NewManager(cfg, store, defaultHelpers(), createMetrics(), logger.Named("manager"))
|
mgr, err := notifications.NewManager(cfg, store, pubsub, defaultHelpers(), createMetrics(), logger.Named("manager"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
assert.NoError(t, mgr.Stop(ctx))
|
assert.NoError(t, mgr.Stop(ctx))
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package pubsub
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"golang.org/x/xerrors"
|
||||||
|
|
||||||
|
"github.com/coder/coder/v2/codersdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
func InboxNotificationForOwnerEventChannel(ownerID uuid.UUID) string {
|
||||||
|
return fmt.Sprintf("inbox_notification:owner:%s", ownerID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleInboxNotificationEvent(cb func(ctx context.Context, payload InboxNotificationEvent, err error)) func(ctx context.Context, message []byte, err error) {
|
||||||
|
return func(ctx context.Context, message []byte, err error) {
|
||||||
|
if err != nil {
|
||||||
|
cb(ctx, InboxNotificationEvent{}, xerrors.Errorf("inbox notification event pubsub: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var payload InboxNotificationEvent
|
||||||
|
if err := json.Unmarshal(message, &payload); err != nil {
|
||||||
|
cb(ctx, InboxNotificationEvent{}, xerrors.Errorf("unmarshal inbox notification event"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cb(ctx, payload, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type InboxNotificationEvent struct {
|
||||||
|
Kind InboxNotificationEventKind `json:"kind"`
|
||||||
|
InboxNotification codersdk.InboxNotification `json:"inbox_notification"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type InboxNotificationEventKind string
|
||||||
|
|
||||||
|
const (
|
||||||
|
InboxNotificationEventKindNew InboxNotificationEventKind = "new"
|
||||||
|
)
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package codersdk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type InboxNotification struct {
|
||||||
|
ID uuid.UUID `json:"id" format:"uuid"`
|
||||||
|
UserID uuid.UUID `json:"user_id" format:"uuid"`
|
||||||
|
TemplateID uuid.UUID `json:"template_id" format:"uuid"`
|
||||||
|
Targets []uuid.UUID `json:"targets" format:"uuid"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
Icon string `json:"icon"`
|
||||||
|
Actions []InboxNotificationAction `json:"actions"`
|
||||||
|
ReadAt *time.Time `json:"read_at"`
|
||||||
|
CreatedAt time.Time `json:"created_at" format:"date-time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type InboxNotificationAction struct {
|
||||||
|
Label string `json:"label"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetInboxNotificationResponse struct {
|
||||||
|
Notification InboxNotification `json:"notification"`
|
||||||
|
UnreadCount int `json:"unread_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListInboxNotificationsRequest struct {
|
||||||
|
Targets string `json:"targets,omitempty"`
|
||||||
|
Templates string `json:"templates,omitempty"`
|
||||||
|
ReadStatus string `json:"read_status,omitempty"`
|
||||||
|
StartingBefore string `json:"starting_before,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListInboxNotificationsResponse struct {
|
||||||
|
Notifications []InboxNotification `json:"notifications"`
|
||||||
|
UnreadCount int `json:"unread_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListInboxNotificationsRequestToQueryParams(req ListInboxNotificationsRequest) []RequestOption {
|
||||||
|
var opts []RequestOption
|
||||||
|
if req.Targets != "" {
|
||||||
|
opts = append(opts, WithQueryParam("targets", req.Targets))
|
||||||
|
}
|
||||||
|
if req.Templates != "" {
|
||||||
|
opts = append(opts, WithQueryParam("templates", req.Templates))
|
||||||
|
}
|
||||||
|
if req.ReadStatus != "" {
|
||||||
|
opts = append(opts, WithQueryParam("read_status", req.ReadStatus))
|
||||||
|
}
|
||||||
|
if req.StartingBefore != "" {
|
||||||
|
opts = append(opts, WithQueryParam("starting_before", req.StartingBefore))
|
||||||
|
}
|
||||||
|
|
||||||
|
return opts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) ListInboxNotifications(ctx context.Context, req ListInboxNotificationsRequest) (ListInboxNotificationsResponse, error) {
|
||||||
|
res, err := c.Request(
|
||||||
|
ctx, http.MethodGet,
|
||||||
|
"/api/v2/notifications/inbox",
|
||||||
|
nil, ListInboxNotificationsRequestToQueryParams(req)...,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return ListInboxNotificationsResponse{}, err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
return ListInboxNotificationsResponse{}, ReadBodyAsError(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
var listInboxNotificationsResponse ListInboxNotificationsResponse
|
||||||
|
return listInboxNotificationsResponse, json.NewDecoder(res.Body).Decode(&listInboxNotificationsResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateInboxNotificationReadStatusRequest struct {
|
||||||
|
IsRead bool `json:"is_read"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateInboxNotificationReadStatusResponse struct {
|
||||||
|
Notification InboxNotification `json:"notification"`
|
||||||
|
UnreadCount int `json:"unread_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) UpdateInboxNotificationReadStatus(ctx context.Context, notifID string, req UpdateInboxNotificationReadStatusRequest) (UpdateInboxNotificationReadStatusResponse, error) {
|
||||||
|
res, err := c.Request(
|
||||||
|
ctx, http.MethodPut,
|
||||||
|
fmt.Sprintf("/api/v2/notifications/inbox/%v/read-status", notifID),
|
||||||
|
req,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return UpdateInboxNotificationReadStatusResponse{}, err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
return UpdateInboxNotificationReadStatusResponse{}, ReadBodyAsError(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp UpdateInboxNotificationReadStatusResponse
|
||||||
|
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
||||||
|
}
|
||||||
Generated
+162
@@ -46,6 +46,168 @@ Status Code **200**
|
|||||||
|
|
||||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||||
|
|
||||||
|
## List inbox notifications
|
||||||
|
|
||||||
|
### Code samples
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# Example request using curl
|
||||||
|
curl -X GET http://coder-server:8080/api/v2/notifications/inbox \
|
||||||
|
-H 'Accept: application/json' \
|
||||||
|
-H 'Coder-Session-Token: API_KEY'
|
||||||
|
```
|
||||||
|
|
||||||
|
`GET /notifications/inbox`
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | In | Type | Required | Description |
|
||||||
|
|---------------|-------|--------|----------|-------------------------------------------------------------------------|
|
||||||
|
| `targets` | query | string | false | Comma-separated list of target IDs to filter notifications |
|
||||||
|
| `templates` | query | string | false | Comma-separated list of template IDs to filter notifications |
|
||||||
|
| `read_status` | query | string | false | Filter notifications by read status. Possible values: read, unread, all |
|
||||||
|
|
||||||
|
### Example responses
|
||||||
|
|
||||||
|
> 200 Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"notifications": [
|
||||||
|
{
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"label": "string",
|
||||||
|
"url": "string"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"content": "string",
|
||||||
|
"created_at": "2019-08-24T14:15:22Z",
|
||||||
|
"icon": "string",
|
||||||
|
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||||
|
"read_at": "string",
|
||||||
|
"targets": [
|
||||||
|
"497f6eca-6276-4993-bfeb-53cbbbba6f08"
|
||||||
|
],
|
||||||
|
"template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc",
|
||||||
|
"title": "string",
|
||||||
|
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unread_count": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Responses
|
||||||
|
|
||||||
|
| Status | Meaning | Description | Schema |
|
||||||
|
|--------|---------------------------------------------------------|-------------|----------------------------------------------------------------------------------------------|
|
||||||
|
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.ListInboxNotificationsResponse](schemas.md#codersdklistinboxnotificationsresponse) |
|
||||||
|
|
||||||
|
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||||
|
|
||||||
|
## Watch for new inbox notifications
|
||||||
|
|
||||||
|
### Code samples
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# Example request using curl
|
||||||
|
curl -X GET http://coder-server:8080/api/v2/notifications/inbox/watch \
|
||||||
|
-H 'Accept: application/json' \
|
||||||
|
-H 'Coder-Session-Token: API_KEY'
|
||||||
|
```
|
||||||
|
|
||||||
|
`GET /notifications/inbox/watch`
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | In | Type | Required | Description |
|
||||||
|
|---------------|-------|--------|----------|-------------------------------------------------------------------------|
|
||||||
|
| `targets` | query | string | false | Comma-separated list of target IDs to filter notifications |
|
||||||
|
| `templates` | query | string | false | Comma-separated list of template IDs to filter notifications |
|
||||||
|
| `read_status` | query | string | false | Filter notifications by read status. Possible values: read, unread, all |
|
||||||
|
|
||||||
|
### Example responses
|
||||||
|
|
||||||
|
> 200 Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"notification": {
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"label": "string",
|
||||||
|
"url": "string"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"content": "string",
|
||||||
|
"created_at": "2019-08-24T14:15:22Z",
|
||||||
|
"icon": "string",
|
||||||
|
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||||
|
"read_at": "string",
|
||||||
|
"targets": [
|
||||||
|
"497f6eca-6276-4993-bfeb-53cbbbba6f08"
|
||||||
|
],
|
||||||
|
"template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc",
|
||||||
|
"title": "string",
|
||||||
|
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5"
|
||||||
|
},
|
||||||
|
"unread_count": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Responses
|
||||||
|
|
||||||
|
| Status | Meaning | Description | Schema |
|
||||||
|
|--------|---------------------------------------------------------|-------------|------------------------------------------------------------------------------------------|
|
||||||
|
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.GetInboxNotificationResponse](schemas.md#codersdkgetinboxnotificationresponse) |
|
||||||
|
|
||||||
|
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||||
|
|
||||||
|
## Update read status of a notification
|
||||||
|
|
||||||
|
### Code samples
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# Example request using curl
|
||||||
|
curl -X PUT http://coder-server:8080/api/v2/notifications/inbox/{id}/read-status \
|
||||||
|
-H 'Accept: application/json' \
|
||||||
|
-H 'Coder-Session-Token: API_KEY'
|
||||||
|
```
|
||||||
|
|
||||||
|
`PUT /notifications/inbox/{id}/read-status`
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | In | Type | Required | Description |
|
||||||
|
|------|------|--------|----------|------------------------|
|
||||||
|
| `id` | path | string | true | id of the notification |
|
||||||
|
|
||||||
|
### Example responses
|
||||||
|
|
||||||
|
> 200 Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"detail": "string",
|
||||||
|
"message": "string",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"detail": "string",
|
||||||
|
"field": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Responses
|
||||||
|
|
||||||
|
| Status | Meaning | Description | Schema |
|
||||||
|
|--------|---------------------------------------------------------|-------------|--------------------------------------------------|
|
||||||
|
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.Response](schemas.md#codersdkresponse) |
|
||||||
|
|
||||||
|
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||||
|
|
||||||
## Get notifications settings
|
## Get notifications settings
|
||||||
|
|
||||||
### Code samples
|
### Code samples
|
||||||
|
|||||||
Generated
+125
@@ -3016,6 +3016,40 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith
|
|||||||
|-------|--------|----------|--------------|-------------|
|
|-------|--------|----------|--------------|-------------|
|
||||||
| `key` | string | false | | |
|
| `key` | string | false | | |
|
||||||
|
|
||||||
|
## codersdk.GetInboxNotificationResponse
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"notification": {
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"label": "string",
|
||||||
|
"url": "string"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"content": "string",
|
||||||
|
"created_at": "2019-08-24T14:15:22Z",
|
||||||
|
"icon": "string",
|
||||||
|
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||||
|
"read_at": "string",
|
||||||
|
"targets": [
|
||||||
|
"497f6eca-6276-4993-bfeb-53cbbbba6f08"
|
||||||
|
],
|
||||||
|
"template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc",
|
||||||
|
"title": "string",
|
||||||
|
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5"
|
||||||
|
},
|
||||||
|
"unread_count": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Properties
|
||||||
|
|
||||||
|
| Name | Type | Required | Restrictions | Description |
|
||||||
|
|----------------|----------------------------------------------------------|----------|--------------|-------------|
|
||||||
|
| `notification` | [codersdk.InboxNotification](#codersdkinboxnotification) | false | | |
|
||||||
|
| `unread_count` | integer | false | | |
|
||||||
|
|
||||||
## codersdk.GetUserStatusCountsResponse
|
## codersdk.GetUserStatusCountsResponse
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -3251,6 +3285,61 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith
|
|||||||
| `refresh` | integer | false | | |
|
| `refresh` | integer | false | | |
|
||||||
| `threshold_database` | integer | false | | |
|
| `threshold_database` | integer | false | | |
|
||||||
|
|
||||||
|
## codersdk.InboxNotification
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"label": "string",
|
||||||
|
"url": "string"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"content": "string",
|
||||||
|
"created_at": "2019-08-24T14:15:22Z",
|
||||||
|
"icon": "string",
|
||||||
|
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||||
|
"read_at": "string",
|
||||||
|
"targets": [
|
||||||
|
"497f6eca-6276-4993-bfeb-53cbbbba6f08"
|
||||||
|
],
|
||||||
|
"template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc",
|
||||||
|
"title": "string",
|
||||||
|
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Properties
|
||||||
|
|
||||||
|
| Name | Type | Required | Restrictions | Description |
|
||||||
|
|---------------|-------------------------------------------------------------------------------|----------|--------------|-------------|
|
||||||
|
| `actions` | array of [codersdk.InboxNotificationAction](#codersdkinboxnotificationaction) | false | | |
|
||||||
|
| `content` | string | false | | |
|
||||||
|
| `created_at` | string | false | | |
|
||||||
|
| `icon` | string | false | | |
|
||||||
|
| `id` | string | false | | |
|
||||||
|
| `read_at` | string | false | | |
|
||||||
|
| `targets` | array of string | false | | |
|
||||||
|
| `template_id` | string | false | | |
|
||||||
|
| `title` | string | false | | |
|
||||||
|
| `user_id` | string | false | | |
|
||||||
|
|
||||||
|
## codersdk.InboxNotificationAction
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"label": "string",
|
||||||
|
"url": "string"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Properties
|
||||||
|
|
||||||
|
| Name | Type | Required | Restrictions | Description |
|
||||||
|
|---------|--------|----------|--------------|-------------|
|
||||||
|
| `label` | string | false | | |
|
||||||
|
| `url` | string | false | | |
|
||||||
|
|
||||||
## codersdk.InsightsReportInterval
|
## codersdk.InsightsReportInterval
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -3380,6 +3469,42 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith
|
|||||||
| `icon` | `chat` |
|
| `icon` | `chat` |
|
||||||
| `icon` | `docs` |
|
| `icon` | `docs` |
|
||||||
|
|
||||||
|
## codersdk.ListInboxNotificationsResponse
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"notifications": [
|
||||||
|
{
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"label": "string",
|
||||||
|
"url": "string"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"content": "string",
|
||||||
|
"created_at": "2019-08-24T14:15:22Z",
|
||||||
|
"icon": "string",
|
||||||
|
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||||
|
"read_at": "string",
|
||||||
|
"targets": [
|
||||||
|
"497f6eca-6276-4993-bfeb-53cbbbba6f08"
|
||||||
|
],
|
||||||
|
"template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc",
|
||||||
|
"title": "string",
|
||||||
|
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unread_count": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Properties
|
||||||
|
|
||||||
|
| Name | Type | Required | Restrictions | Description |
|
||||||
|
|-----------------|-------------------------------------------------------------------|----------|--------------|-------------|
|
||||||
|
| `notifications` | array of [codersdk.InboxNotification](#codersdkinboxnotification) | false | | |
|
||||||
|
| `unread_count` | integer | false | | |
|
||||||
|
|
||||||
## codersdk.LogLevel
|
## codersdk.LogLevel
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
Generated
+51
@@ -892,6 +892,12 @@ export interface GenerateAPIKeyResponse {
|
|||||||
readonly key: string;
|
readonly key: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// From codersdk/inboxnotification.go
|
||||||
|
export interface GetInboxNotificationResponse {
|
||||||
|
readonly notification: InboxNotification;
|
||||||
|
readonly unread_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
// From codersdk/insights.go
|
// From codersdk/insights.go
|
||||||
export interface GetUserStatusCountsRequest {
|
export interface GetUserStatusCountsRequest {
|
||||||
readonly offset: string;
|
readonly offset: string;
|
||||||
@@ -1076,6 +1082,26 @@ export interface IDPSyncMapping<ResourceIdType extends string | string> {
|
|||||||
readonly Gets: ResourceIdType;
|
readonly Gets: ResourceIdType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// From codersdk/inboxnotification.go
|
||||||
|
export interface InboxNotification {
|
||||||
|
readonly id: string;
|
||||||
|
readonly user_id: string;
|
||||||
|
readonly template_id: string;
|
||||||
|
readonly targets: readonly string[];
|
||||||
|
readonly title: string;
|
||||||
|
readonly content: string;
|
||||||
|
readonly icon: string;
|
||||||
|
readonly actions: readonly InboxNotificationAction[];
|
||||||
|
readonly read_at: string | null;
|
||||||
|
readonly created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// From codersdk/inboxnotification.go
|
||||||
|
export interface InboxNotificationAction {
|
||||||
|
readonly label: string;
|
||||||
|
readonly url: string;
|
||||||
|
}
|
||||||
|
|
||||||
// From codersdk/insights.go
|
// From codersdk/insights.go
|
||||||
export type InsightsReportInterval = "day" | "week";
|
export type InsightsReportInterval = "day" | "week";
|
||||||
|
|
||||||
@@ -1133,6 +1159,20 @@ export interface LinkConfig {
|
|||||||
readonly icon: string;
|
readonly icon: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// From codersdk/inboxnotification.go
|
||||||
|
export interface ListInboxNotificationsRequest {
|
||||||
|
readonly targets?: string;
|
||||||
|
readonly templates?: string;
|
||||||
|
readonly read_status?: string;
|
||||||
|
readonly starting_before?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// From codersdk/inboxnotification.go
|
||||||
|
export interface ListInboxNotificationsResponse {
|
||||||
|
readonly notifications: readonly InboxNotification[];
|
||||||
|
readonly unread_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
// From codersdk/externalauth.go
|
// From codersdk/externalauth.go
|
||||||
export interface ListUserExternalAuthResponse {
|
export interface ListUserExternalAuthResponse {
|
||||||
readonly providers: readonly ExternalAuthLinkProvider[];
|
readonly providers: readonly ExternalAuthLinkProvider[];
|
||||||
@@ -2653,6 +2693,17 @@ export interface UpdateHealthSettings {
|
|||||||
readonly dismissed_healthchecks: readonly HealthSection[];
|
readonly dismissed_healthchecks: readonly HealthSection[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// From codersdk/inboxnotification.go
|
||||||
|
export interface UpdateInboxNotificationReadStatusRequest {
|
||||||
|
readonly is_read: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// From codersdk/inboxnotification.go
|
||||||
|
export interface UpdateInboxNotificationReadStatusResponse {
|
||||||
|
readonly notification: InboxNotification;
|
||||||
|
readonly unread_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
// From codersdk/notifications.go
|
// From codersdk/notifications.go
|
||||||
export interface UpdateNotificationTemplateMethod {
|
export interface UpdateNotificationTemplateMethod {
|
||||||
readonly method?: string;
|
readonly method?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user