diff --git a/Dockerfile b/Dockerfile index 28f3fb8..ffcb456 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,7 +20,6 @@ RUN make install-code-generation-tools # copy api related source files and generate api docs COPY pkg/web pkg/web COPY pkg/models pkg/models -COPY pkg/request pkg/request RUN make api-docs # copy rest of the source files diff --git a/api/notificationservice/notificationservice_docs.go b/api/notificationservice/notificationservice_docs.go index c435b65..d29cede 100644 --- a/api/notificationservice/notificationservice_docs.go +++ b/api/notificationservice/notificationservice_docs.go @@ -18,6 +18,49 @@ const docTemplatenotificationservice = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { + "/notification-channel/Teams/check": { + "post": { + "security": [ + { + "KeycloakAuth": [] + } + ], + "description": "Check if a Teams server is able to send a test message", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Teams-channel" + ], + "summary": "Check Teams server", + "parameters": [ + { + "type": "string", + "description": "Teams channel ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "Teams server test message sent successfully" + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, "/notification-channel/mail": { "get": { "security": [ @@ -47,7 +90,7 @@ const docTemplatenotificationservice = `{ "schema": { "type": "array", "items": { - "$ref": "#/definitions/request.MailNotificationChannelRequest" + "$ref": "#/definitions/maildto.MailNotificationChannelRequest" } } }, @@ -86,7 +129,7 @@ const docTemplatenotificationservice = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/request.MailNotificationChannelRequest" + "$ref": "#/definitions/maildto.MailNotificationChannelRequest" } } ], @@ -94,7 +137,7 @@ const docTemplatenotificationservice = `{ "201": { "description": "Created", "schema": { - "$ref": "#/definitions/request.MailNotificationChannelRequest" + "$ref": "#/definitions/maildto.MailNotificationChannelRequest" } }, "400": { @@ -150,7 +193,7 @@ const docTemplatenotificationservice = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/request.MailNotificationChannelRequest" + "$ref": "#/definitions/maildto.MailNotificationChannelRequest" } } ], @@ -158,7 +201,7 @@ const docTemplatenotificationservice = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/request.MailNotificationChannelRequest" + "$ref": "#/definitions/maildto.MailNotificationChannelRequest" } }, "400": { @@ -246,7 +289,7 @@ const docTemplatenotificationservice = `{ "schema": { "type": "array", "items": { - "$ref": "#/definitions/request.MattermostNotificationChannelRequest" + "$ref": "#/definitions/mattermostdto.MattermostNotificationChannelRequest" } } }, @@ -285,7 +328,7 @@ const docTemplatenotificationservice = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/request.MattermostNotificationChannelRequest" + "$ref": "#/definitions/mattermostdto.MattermostNotificationChannelRequest" } } ], @@ -293,7 +336,7 @@ const docTemplatenotificationservice = `{ "201": { "description": "Created", "schema": { - "$ref": "#/definitions/request.MattermostNotificationChannelRequest" + "$ref": "#/definitions/mattermostdto.MattermostNotificationChannelRequest" } }, "400": { @@ -317,6 +360,40 @@ const docTemplatenotificationservice = `{ } } }, + "/notification-channel/mattermost/check": { + "post": { + "security": [ + { + "KeycloakAuth": [] + } + ], + "description": "Check if a mattermost server is able to send a test message", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "mattermost-channel" + ], + "summary": "Check mattermost server", + "responses": { + "204": { + "description": "Mattermost server test message sent successfully" + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, "/notification-channel/mattermost/{id}": { "put": { "security": [ @@ -349,7 +426,7 @@ const docTemplatenotificationservice = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/request.MattermostNotificationChannelRequest" + "$ref": "#/definitions/mattermostdto.MattermostNotificationChannelRequest" } } ], @@ -357,7 +434,7 @@ const docTemplatenotificationservice = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/request.MattermostNotificationChannelRequest" + "$ref": "#/definitions/mattermostdto.MattermostNotificationChannelRequest" } }, "400": { @@ -434,6 +511,223 @@ const docTemplatenotificationservice = `{ } } }, + "/notification-channel/teams": { + "get": { + "security": [ + { + "KeycloakAuth": [] + } + ], + "description": "List teams notification channels by type", + "produces": [ + "application/json" + ], + "tags": [ + "teams-channel" + ], + "summary": "List Teams Channels", + "parameters": [ + { + "type": "string", + "description": "Channel type", + "name": "type", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/teamsdto.TeamsNotificationChannelRequest" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "post": { + "security": [ + { + "KeycloakAuth": [] + } + ], + "description": "Create a new teams notification channel", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "teams-channel" + ], + "summary": "Create Teams Channel", + "parameters": [ + { + "description": "Teams channel to add", + "name": "TeamsChannel", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/teamsdto.TeamsNotificationChannelRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/teamsdto.TeamsNotificationChannelRequest" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/notification-channel/teams/{id}": { + "put": { + "security": [ + { + "KeycloakAuth": [] + } + ], + "description": "Update an existing teams notification channel", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "teams-channel" + ], + "summary": "Update Teams Channel", + "parameters": [ + { + "type": "string", + "description": "Teams channel ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Teams channel to update", + "name": "TeamsChannel", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/teamsdto.TeamsNotificationChannelRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/teamsdto.TeamsNotificationChannelRequest" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "delete": { + "security": [ + { + "KeycloakAuth": [] + } + ], + "description": "Delete a teams notification channel", + "tags": [ + "teams-channel" + ], + "summary": "Delete Teams Channel", + "parameters": [ + { + "type": "string", + "description": "Teams channel ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "Deleted successfully" + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, "/notifications": { "put": { "security": [ @@ -547,7 +841,7 @@ const docTemplatenotificationservice = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/dtos.CheckMailServerRequest" + "$ref": "#/definitions/maildto.CheckMailServerRequest" } } ], @@ -595,7 +889,7 @@ const docTemplatenotificationservice = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/dtos.CheckMailServerEntityRequest" + "$ref": "#/definitions/maildto.CheckMailServerEntityRequest" } } ], @@ -651,56 +945,6 @@ const docTemplatenotificationservice = `{ } }, "definitions": { - "dtos.CheckMailServerEntityRequest": { - "type": "object", - "properties": { - "domain": { - "type": "string" - }, - "isAuthenticationRequired": { - "type": "boolean", - "default": false - }, - "isTlsEnforced": { - "type": "boolean", - "default": false - }, - "password": { - "type": "string" - }, - "port": { - "type": "integer" - }, - "username": { - "type": "string" - } - } - }, - "dtos.CheckMailServerRequest": { - "type": "object", - "properties": { - "domain": { - "type": "string" - }, - "isAuthenticationRequired": { - "type": "boolean", - "default": false - }, - "isTlsEnforced": { - "type": "boolean", - "default": false - }, - "password": { - "type": "string" - }, - "port": { - "type": "integer" - }, - "username": { - "type": "string" - } - } - }, "filter.CompareOperator": { "type": "string", "enum": [ @@ -874,6 +1118,110 @@ const docTemplatenotificationservice = `{ } } }, + "maildto.CheckMailServerEntityRequest": { + "type": "object", + "properties": { + "domain": { + "type": "string" + }, + "isAuthenticationRequired": { + "type": "boolean", + "default": false + }, + "isTlsEnforced": { + "type": "boolean", + "default": false + }, + "password": { + "type": "string" + }, + "port": { + "type": "integer" + }, + "username": { + "type": "string" + } + } + }, + "maildto.CheckMailServerRequest": { + "type": "object", + "properties": { + "domain": { + "type": "string" + }, + "isAuthenticationRequired": { + "type": "boolean", + "default": false + }, + "isTlsEnforced": { + "type": "boolean", + "default": false + }, + "password": { + "type": "string" + }, + "port": { + "type": "integer" + }, + "username": { + "type": "string" + } + } + }, + "maildto.MailNotificationChannelRequest": { + "type": "object", + "properties": { + "channelName": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "id": { + "type": "string" + }, + "isAuthenticationRequired": { + "type": "boolean", + "default": false + }, + "isTlsEnforced": { + "type": "boolean", + "default": false + }, + "maxEmailAttachmentSizeMb": { + "type": "integer" + }, + "maxEmailIncludeSizeMb": { + "type": "integer" + }, + "password": { + "type": "string" + }, + "port": { + "type": "integer" + }, + "senderEmailAddress": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "mattermostdto.MattermostNotificationChannelRequest": { + "type": "object", + "properties": { + "channelName": { + "type": "string" + }, + "description": { + "type": "string" + }, + "webhookUrl": { + "type": "string" + } + } + }, "models.Notification": { "type": "object", "required": [ @@ -1079,60 +1427,6 @@ const docTemplatenotificationservice = `{ } } }, - "request.MailNotificationChannelRequest": { - "type": "object", - "properties": { - "channelName": { - "type": "string" - }, - "domain": { - "type": "string" - }, - "id": { - "type": "string" - }, - "isAuthenticationRequired": { - "type": "boolean", - "default": false - }, - "isTlsEnforced": { - "type": "boolean", - "default": false - }, - "maxEmailAttachmentSizeMb": { - "type": "integer" - }, - "maxEmailIncludeSizeMb": { - "type": "integer" - }, - "password": { - "type": "string" - }, - "port": { - "type": "integer" - }, - "senderEmailAddress": { - "type": "string" - }, - "username": { - "type": "string" - } - } - }, - "request.MattermostNotificationChannelRequest": { - "type": "object", - "properties": { - "channelName": { - "type": "string" - }, - "description": { - "type": "string" - }, - "webhookUrl": { - "type": "string" - } - } - }, "sorting.Request": { "type": "object", "properties": { @@ -1156,6 +1450,20 @@ const docTemplatenotificationservice = `{ "DirectionAscending", "NoDirection" ] + }, + "teamsdto.TeamsNotificationChannelRequest": { + "type": "object", + "properties": { + "channelName": { + "type": "string" + }, + "description": { + "type": "string" + }, + "webhookUrl": { + "type": "string" + } + } } }, "securityDefinitions": { diff --git a/api/notificationservice/notificationservice_swagger.yaml b/api/notificationservice/notificationservice_swagger.yaml index b3ceb6f..4f2b9eb 100644 --- a/api/notificationservice/notificationservice_swagger.yaml +++ b/api/notificationservice/notificationservice_swagger.yaml @@ -1,39 +1,5 @@ basePath: /api/notification-service definitions: - dtos.CheckMailServerEntityRequest: - properties: - domain: - type: string - isAuthenticationRequired: - default: false - type: boolean - isTlsEnforced: - default: false - type: boolean - password: - type: string - port: - type: integer - username: - type: string - type: object - dtos.CheckMailServerRequest: - properties: - domain: - type: string - isAuthenticationRequired: - default: false - type: boolean - isTlsEnforced: - default: false - type: boolean - password: - type: string - port: - type: integer - username: - type: string - type: object filter.CompareOperator: enum: - beginsWith @@ -173,6 +139,76 @@ definitions: - enum - bool type: object + maildto.CheckMailServerEntityRequest: + properties: + domain: + type: string + isAuthenticationRequired: + default: false + type: boolean + isTlsEnforced: + default: false + type: boolean + password: + type: string + port: + type: integer + username: + type: string + type: object + maildto.CheckMailServerRequest: + properties: + domain: + type: string + isAuthenticationRequired: + default: false + type: boolean + isTlsEnforced: + default: false + type: boolean + password: + type: string + port: + type: integer + username: + type: string + type: object + maildto.MailNotificationChannelRequest: + properties: + channelName: + type: string + domain: + type: string + id: + type: string + isAuthenticationRequired: + default: false + type: boolean + isTlsEnforced: + default: false + type: boolean + maxEmailAttachmentSizeMb: + type: integer + maxEmailIncludeSizeMb: + type: integer + password: + type: string + port: + type: integer + senderEmailAddress: + type: string + username: + type: string + type: object + mattermostdto.MattermostNotificationChannelRequest: + properties: + channelName: + type: string + description: + type: string + webhookUrl: + type: string + type: object models.Notification: properties: customFields: @@ -312,42 +348,6 @@ definitions: sorting: $ref: '#/definitions/sorting.Request' type: object - request.MailNotificationChannelRequest: - properties: - channelName: - type: string - domain: - type: string - id: - type: string - isAuthenticationRequired: - default: false - type: boolean - isTlsEnforced: - default: false - type: boolean - maxEmailAttachmentSizeMb: - type: integer - maxEmailIncludeSizeMb: - type: integer - password: - type: string - port: - type: integer - senderEmailAddress: - type: string - username: - type: string - type: object - request.MattermostNotificationChannelRequest: - properties: - channelName: - type: string - description: - type: string - webhookUrl: - type: string - type: object sorting.Request: properties: column: @@ -365,6 +365,15 @@ definitions: - DirectionDescending - DirectionAscending - NoDirection + teamsdto.TeamsNotificationChannelRequest: + properties: + channelName: + type: string + description: + type: string + webhookUrl: + type: string + type: object externalDocs: description: OpenAPI url: https://swagger.io/resources/open-api/ @@ -376,6 +385,33 @@ info: title: Notification Service API version: "1.0" paths: + /notification-channel/Teams/check: + post: + consumes: + - application/json + description: Check if a Teams server is able to send a test message + parameters: + - description: Teams channel ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "204": + description: Teams server test message sent successfully + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + security: + - KeycloakAuth: [] + summary: Check Teams server + tags: + - Teams-channel /notification-channel/mail: get: description: List mail notification channels by type @@ -391,7 +427,7 @@ paths: description: OK schema: items: - $ref: '#/definitions/request.MailNotificationChannelRequest' + $ref: '#/definitions/maildto.MailNotificationChannelRequest' type: array "500": description: Internal Server Error @@ -414,14 +450,14 @@ paths: name: MailChannel required: true schema: - $ref: '#/definitions/request.MailNotificationChannelRequest' + $ref: '#/definitions/maildto.MailNotificationChannelRequest' produces: - application/json responses: "201": description: Created schema: - $ref: '#/definitions/request.MailNotificationChannelRequest' + $ref: '#/definitions/maildto.MailNotificationChannelRequest' "400": description: Bad Request schema: @@ -477,14 +513,14 @@ paths: name: MailChannel required: true schema: - $ref: '#/definitions/request.MailNotificationChannelRequest' + $ref: '#/definitions/maildto.MailNotificationChannelRequest' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/request.MailNotificationChannelRequest' + $ref: '#/definitions/maildto.MailNotificationChannelRequest' "400": description: Bad Request schema: @@ -517,7 +553,7 @@ paths: description: OK schema: items: - $ref: '#/definitions/request.MattermostNotificationChannelRequest' + $ref: '#/definitions/mattermostdto.MattermostNotificationChannelRequest' type: array "500": description: Internal Server Error @@ -540,14 +576,14 @@ paths: name: MattermostChannel required: true schema: - $ref: '#/definitions/request.MattermostNotificationChannelRequest' + $ref: '#/definitions/mattermostdto.MattermostNotificationChannelRequest' produces: - application/json responses: "201": description: Created schema: - $ref: '#/definitions/request.MattermostNotificationChannelRequest' + $ref: '#/definitions/mattermostdto.MattermostNotificationChannelRequest' "400": description: Bad Request schema: @@ -609,14 +645,14 @@ paths: name: MattermostChannel required: true schema: - $ref: '#/definitions/request.MattermostNotificationChannelRequest' + $ref: '#/definitions/mattermostdto.MattermostNotificationChannelRequest' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/request.MattermostNotificationChannelRequest' + $ref: '#/definitions/mattermostdto.MattermostNotificationChannelRequest' "400": description: Bad Request schema: @@ -640,6 +676,165 @@ paths: summary: Update Mattermost Channel tags: - mattermost-channel + /notification-channel/mattermost/check: + post: + consumes: + - application/json + description: Check if a mattermost server is able to send a test message + produces: + - application/json + responses: + "204": + description: Mattermost server test message sent successfully + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + security: + - KeycloakAuth: [] + summary: Check mattermost server + tags: + - mattermost-channel + /notification-channel/teams: + get: + description: List teams notification channels by type + parameters: + - description: Channel type + in: query + name: type + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/teamsdto.TeamsNotificationChannelRequest' + type: array + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + security: + - KeycloakAuth: [] + summary: List Teams Channels + tags: + - teams-channel + post: + consumes: + - application/json + description: Create a new teams notification channel + parameters: + - description: Teams channel to add + in: body + name: TeamsChannel + required: true + schema: + $ref: '#/definitions/teamsdto.TeamsNotificationChannelRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/teamsdto.TeamsNotificationChannelRequest' + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + security: + - KeycloakAuth: [] + summary: Create Teams Channel + tags: + - teams-channel + /notification-channel/teams/{id}: + delete: + description: Delete a teams notification channel + parameters: + - description: Teams channel ID + in: path + name: id + required: true + type: string + responses: + "204": + description: Deleted successfully + "404": + description: Not Found + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + security: + - KeycloakAuth: [] + summary: Delete Teams Channel + tags: + - teams-channel + put: + consumes: + - application/json + description: Update an existing teams notification channel + parameters: + - description: Teams channel ID + in: path + name: id + required: true + type: string + - description: Teams channel to update + in: body + name: TeamsChannel + required: true + schema: + $ref: '#/definitions/teamsdto.TeamsNotificationChannelRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/teamsdto.TeamsNotificationChannelRequest' + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "404": + description: Not Found + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + security: + - KeycloakAuth: [] + summary: Update Teams Channel + tags: + - teams-channel /notifications: post: consumes: @@ -706,7 +901,7 @@ paths: name: MailServerConfig required: true schema: - $ref: '#/definitions/dtos.CheckMailServerEntityRequest' + $ref: '#/definitions/maildto.CheckMailServerEntityRequest' produces: - application/json responses: @@ -736,7 +931,7 @@ paths: name: MailServerConfig required: true schema: - $ref: '#/definitions/dtos.CheckMailServerRequest' + $ref: '#/definitions/maildto.CheckMailServerRequest' produces: - application/json responses: diff --git a/cmd/notification-service/main.go b/cmd/notification-service/main.go index 845f619..270cf47 100644 --- a/cmd/notification-service/main.go +++ b/cmd/notification-service/main.go @@ -13,16 +13,16 @@ import ( "os/signal" "time" - "github.com/gin-gonic/gin" "github.com/go-co-op/gocron/v2" "github.com/greenbone/keycloak-client-golang/auth" "github.com/greenbone/opensight-notification-service/pkg/security" "github.com/greenbone/opensight-notification-service/pkg/services/notificationchannelservice" + "github.com/greenbone/opensight-notification-service/pkg/web/errmap" "github.com/greenbone/opensight-notification-service/pkg/web/mailcontroller" + "github.com/greenbone/opensight-notification-service/pkg/web/teamsController" "github.com/go-playground/validator" "github.com/greenbone/opensight-notification-service/pkg/jobs/checkmailconnectivity" - "github.com/greenbone/opensight-notification-service/pkg/web/helper" "github.com/greenbone/opensight-notification-service/pkg/web/mattermostcontroller" "github.com/kelseyhightower/envconfig" "github.com/rs/zerolog/log" @@ -106,8 +106,9 @@ func run(config config.Config) error { notificationService := notificationservice.NewNotificationService(notificationRepository) notificationChannelService := notificationchannelservice.NewNotificationChannelService(notificationChannelRepository) - mailChannelService := notificationchannelservice.NewMailChannelService(notificationChannelService, config.ChannelLimit.EMailLimit) + mailChannelService := notificationchannelservice.NewMailChannelService(notificationChannelService, notificationChannelRepository, config.ChannelLimit.EMailLimit) mattermostChannelService := notificationchannelservice.NewMattermostChannelService(notificationChannelService, config.ChannelLimit.MattermostLimit) + teamsChannelService := notificationchannelservice.NewTeamsChannelService(notificationChannelService, config.ChannelLimit.TeamsLimit) healthService := healthservice.NewHealthService(pgClient) // scheduler @@ -117,15 +118,16 @@ func run(config config.Config) error { } _, err = scheduler.NewJob( gocron.DurationJob(1*time.Hour), - gocron.NewTask(checkmailconnectivity.NewJob(notificationService, notificationChannelService)), + gocron.NewTask(checkmailconnectivity.NewJob(notificationService, notificationChannelService, mailChannelService)), ) if err != nil { return fmt.Errorf("error creating mail connectivity check job: %w", err) } scheduler.Start() - router := web.NewWebEngine(config.Http) - router.Use(helper.ValidationErrorHandler(gin.ErrorTypePrivate)) + registry := errmap.NewRegistry() + + router := web.NewWebEngine(config.Http, registry) rootRouter := router.Group("/") notificationServiceRouter := router.Group("/api/notification-service") docsRouter := router.Group("/docs/notification-service") @@ -136,9 +138,10 @@ func run(config config.Config) error { //instantiate controllers notificationcontroller.AddNotificationController(notificationServiceRouter, notificationService, authMiddleware) - mailcontroller.NewMailController(notificationServiceRouter, notificationChannelService, mailChannelService, authMiddleware) - mailcontroller.AddCheckMailServerController(notificationServiceRouter, notificationChannelService, authMiddleware) - mattermostcontroller.NewMattermostController(notificationServiceRouter, notificationChannelService, mattermostChannelService, authMiddleware) + mailcontroller.NewMailController(notificationServiceRouter, notificationChannelService, mailChannelService, authMiddleware, registry) + mailcontroller.AddCheckMailServerController(notificationServiceRouter, mailChannelService, authMiddleware, registry) + mattermostcontroller.NewMattermostController(notificationServiceRouter, notificationChannelService, mattermostChannelService, authMiddleware, registry) + teamsController.AddTeamsController(router, notificationChannelRepository, teamsChannelService, authMiddleware, registry) healthcontroller.NewHealthController(rootRouter, healthService) // for health probes (not a data source) srv := &http.Server{ diff --git a/pkg/config/config.go b/pkg/config/config.go index ee8df49..c294f1a 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -23,6 +23,7 @@ type Config struct { type ChannelLimits struct { EMailLimit int `envconfig:"EMAIL_LIMIT" default:"1"` MattermostLimit int `envconfig:"MATTERMOST_LIMIT" default:"20"` + TeamsLimit int `envconfig:"TEAMS_LIMIT" default:"20"` } type Http struct { diff --git a/pkg/jobs/checkmailconnectivity/checkMailConnectivity.go b/pkg/jobs/checkmailconnectivity/checkMailConnectivity.go index da351e1..404a27a 100644 --- a/pkg/jobs/checkmailconnectivity/checkMailConnectivity.go +++ b/pkg/jobs/checkmailconnectivity/checkMailConnectivity.go @@ -5,8 +5,9 @@ import ( "fmt" "time" + "github.com/greenbone/opensight-notification-service/pkg/helper" "github.com/greenbone/opensight-notification-service/pkg/models" - "github.com/greenbone/opensight-notification-service/pkg/port" + "github.com/greenbone/opensight-notification-service/pkg/services/notificationchannelservice" "github.com/greenbone/opensight-notification-service/pkg/services/notificationservice" ) @@ -16,20 +17,21 @@ const ( ) func NewJob( - notificationService *notificationservice.NotificationService, - service port.NotificationChannelService, + notificationService notificationservice.NotificationService, + notificationChannelService notificationchannelservice.NotificationChannelService, + mailChannelService notificationchannelservice.MailChannelService, ) func() error { return func() error { ctx, cancel := context.WithTimeout(context.Background(), channelListTimeout) defer cancel() - mailChannels, err := service.ListNotificationChannelsByType(ctx, models.ChannelTypeMail) + mailChannels, err := notificationChannelService.ListNotificationChannelsByType(ctx, models.ChannelTypeMail) if err != nil { return err } for _, channel := range mailChannels { - if err := checkChannelConnectivity(service, channel); err != nil { + if err := checkChannelConnectivity(mailChannelService, channel); err != nil { _, err := notificationService.CreateNotification(context.Background(), models.Notification{ Origin: "Communication service", Timestamp: time.Now().UTC().Format(time.RFC3339), @@ -37,9 +39,9 @@ func NewJob( Detail: fmt.Sprintf("Mailserver:%s not reachable: %s", *channel.Domain, err), Level: "info", CustomFields: map[string]any{ - "Domain": Value(channel.Domain), - "Port": Value(channel.Port), - "Username": Value(channel.Username), + "Domain": helper.SafeDereference(channel.Domain), + "Port": helper.SafeDereference(channel.Port), + "Username": helper.SafeDereference(channel.Username), }, }) if err != nil { @@ -51,18 +53,14 @@ func NewJob( } } -func Value[T any](value *T) any { - if value == nil { - return nil - } - return *value -} - -func checkChannelConnectivity(service port.NotificationChannelService, channel models.NotificationChannel) error { +func checkChannelConnectivity( + mailChannelService notificationchannelservice.MailChannelService, + channel models.NotificationChannel, +) error { ctx, cancel := context.WithTimeout(context.Background(), channelCheckTimeout) defer cancel() - if err := service.CheckNotificationChannelConnectivity(ctx, channel); err != nil { + if err := mailChannelService.CheckNotificationChannelConnectivity(ctx, channel); err != nil { return err } return nil diff --git a/pkg/mapper/notification_channel_mapper.go b/pkg/mapper/notification_channel_mapper.go deleted file mode 100644 index db0d58b..0000000 --- a/pkg/mapper/notification_channel_mapper.go +++ /dev/null @@ -1,78 +0,0 @@ -package mapper - -import ( - "github.com/greenbone/opensight-notification-service/pkg/models" - "github.com/greenbone/opensight-notification-service/pkg/request" - "github.com/greenbone/opensight-notification-service/pkg/response" -) - -// MapNotificationChannelToMail maps NotificationChannel to MailNotificationChannelRequest. -func MapNotificationChannelToMail(channel models.NotificationChannel) request.MailNotificationChannelRequest { - return request.MailNotificationChannelRequest{ - Id: channel.Id, - ChannelName: *channel.ChannelName, - Domain: *channel.Domain, - Port: *channel.Port, - IsAuthenticationRequired: *channel.IsAuthenticationRequired, - IsTlsEnforced: *channel.IsTlsEnforced, - Username: channel.Username, - Password: channel.Password, - MaxEmailAttachmentSizeMb: channel.MaxEmailAttachmentSizeMb, - MaxEmailIncludeSizeMb: channel.MaxEmailIncludeSizeMb, - SenderEmailAddress: *channel.SenderEmailAddress, - } -} - -func MapMailToNotificationChannel(mail request.MailNotificationChannelRequest) models.NotificationChannel { - return models.NotificationChannel{ - ChannelType: string(models.ChannelTypeMail), - Id: mail.Id, - ChannelName: &mail.ChannelName, - Domain: &mail.Domain, - Port: &mail.Port, - IsAuthenticationRequired: &mail.IsAuthenticationRequired, - IsTlsEnforced: &mail.IsTlsEnforced, - Username: mail.Username, - Password: mail.Password, - MaxEmailAttachmentSizeMb: mail.MaxEmailAttachmentSizeMb, - MaxEmailIncludeSizeMb: mail.MaxEmailIncludeSizeMb, - SenderEmailAddress: &mail.SenderEmailAddress, - } -} - -// MapNotificationChannelsToMailWithEmptyPassword maps a slice of NotificationChannel to MailNotificationChannelRequest. -func MapNotificationChannelsToMailWithEmptyPassword(channels []models.NotificationChannel) []request.MailNotificationChannelRequest { - mailChannels := make([]request.MailNotificationChannelRequest, 0, len(channels)) - for _, ch := range channels { - mailChannels = append(mailChannels, MapNotificationChannelToMail(ch).WithEmptyPassword()) - } - return mailChannels -} - -// MapNotificationChannelToMattermost maps NotificationChannel to MattermostNotificationChannelRequest. -func MapNotificationChannelToMattermost(channel models.NotificationChannel) response.MattermostNotificationChannelResponse { - return response.MattermostNotificationChannelResponse{ - Id: channel.Id, - ChannelName: *channel.ChannelName, - WebhookUrl: *channel.WebhookUrl, - Description: *channel.Description, - } -} - -func MapMattermostToNotificationChannel(mail request.MattermostNotificationChannelRequest) models.NotificationChannel { - return models.NotificationChannel{ - ChannelType: string(models.ChannelTypeMattermost), - ChannelName: &mail.ChannelName, - WebhookUrl: &mail.WebhookUrl, - Description: &mail.Description, - } -} - -// MapNotificationChannelsToMattermost maps a slice of NotificationChannel to MattermostNotificationChannelRequest. -func MapNotificationChannelsToMattermost(channels []models.NotificationChannel) []response.MattermostNotificationChannelResponse { - mattermostChannels := make([]response.MattermostNotificationChannelResponse, 0, len(channels)) - for _, ch := range channels { - mattermostChannels = append(mattermostChannels, MapNotificationChannelToMattermost(ch)) - } - return mattermostChannels -} diff --git a/pkg/mapper/notification_channel_mapper_test.go b/pkg/mapper/notification_channel_mapper_test.go deleted file mode 100644 index dc962d7..0000000 --- a/pkg/mapper/notification_channel_mapper_test.go +++ /dev/null @@ -1,73 +0,0 @@ -package mapper - -import ( - "testing" - - "github.com/greenbone/opensight-notification-service/pkg/helper" - "github.com/greenbone/opensight-notification-service/pkg/models" - "github.com/greenbone/opensight-notification-service/pkg/request" - "github.com/stretchr/testify/assert" -) - -func TestMapNotificationChannelToMail(t *testing.T) { - channel := models.NotificationChannel{ - Id: helper.ToPtr("id1"), - ChannelName: helper.ToPtr("TestChannel"), - Domain: helper.ToPtr("example.com"), - Port: helper.ToPtr(587), - IsAuthenticationRequired: helper.ToPtr(true), - IsTlsEnforced: helper.ToPtr(true), - Username: helper.ToPtr("user"), - MaxEmailAttachmentSizeMb: helper.ToPtr(10), - MaxEmailIncludeSizeMb: helper.ToPtr(5), - SenderEmailAddress: helper.ToPtr("sender@example.com"), - } - - mail := MapNotificationChannelToMail(channel) - - t.Run("assert all fields", func(t *testing.T) { - assert.Equal(t, channel.Id, mail.Id) - assert.Equal(t, channel.ChannelName, &mail.ChannelName) - assert.Equal(t, channel.Domain, &mail.Domain) - assert.Equal(t, channel.Port, &mail.Port) - assert.Equal(t, channel.IsAuthenticationRequired, &mail.IsAuthenticationRequired) - assert.Equal(t, channel.IsTlsEnforced, &mail.IsTlsEnforced) - assert.Equal(t, channel.Username, mail.Username) - assert.Equal(t, channel.MaxEmailAttachmentSizeMb, mail.MaxEmailAttachmentSizeMb) - assert.Equal(t, channel.MaxEmailIncludeSizeMb, mail.MaxEmailIncludeSizeMb) - assert.Equal(t, channel.SenderEmailAddress, &mail.SenderEmailAddress) - }) -} - -func TestMapMailToNotificationChannel(t *testing.T) { - mail := request.MailNotificationChannelRequest{ - Id: helper.ToPtr("id2"), - ChannelName: "MailChannel", - Domain: "mail.com", - Port: 465, - IsAuthenticationRequired: false, - IsTlsEnforced: false, - Username: helper.ToPtr("mailuser"), - Password: helper.ToPtr("secret"), - MaxEmailAttachmentSizeMb: helper.ToPtr(20), - MaxEmailIncludeSizeMb: helper.ToPtr(15), - SenderEmailAddress: "mail@domain.com", - } - - channel := MapMailToNotificationChannel(mail) - - t.Run("assert all fields", func(t *testing.T) { - assert.Equal(t, "mail", channel.ChannelType) - assert.Equal(t, mail.Id, channel.Id) - assert.Equal(t, mail.ChannelName, *channel.ChannelName) - assert.Equal(t, mail.Domain, *channel.Domain) - assert.Equal(t, mail.Port, *channel.Port) - assert.Equal(t, mail.IsAuthenticationRequired, *channel.IsAuthenticationRequired) - assert.Equal(t, mail.IsTlsEnforced, *channel.IsTlsEnforced) - assert.Equal(t, mail.Username, channel.Username) - assert.Equal(t, mail.Password, channel.Password) - assert.Equal(t, mail.MaxEmailAttachmentSizeMb, channel.MaxEmailAttachmentSizeMb) - assert.Equal(t, mail.MaxEmailIncludeSizeMb, channel.MaxEmailIncludeSizeMb) - assert.Equal(t, mail.SenderEmailAddress, *channel.SenderEmailAddress) - }) -} diff --git a/pkg/models/validation.go b/pkg/models/validation.go new file mode 100644 index 0000000..429b59a --- /dev/null +++ b/pkg/models/validation.go @@ -0,0 +1,7 @@ +package models + +type ValidationErrors map[string]string + +func (v ValidationErrors) Error() string { + return "validation error" +} diff --git a/pkg/policy/notificationchannel.go b/pkg/policy/notificationchannel.go new file mode 100644 index 0000000..60a8a49 --- /dev/null +++ b/pkg/policy/notificationchannel.go @@ -0,0 +1,39 @@ +package policy + +import ( + "errors" + "fmt" + "net/url" + "regexp" +) + +func TeamsWebhookUrlPolicy(webhook string) (*url.URL, error) { + if webhook == "" { + return nil, errors.New("webhook URL is required") + } + + u, err := url.Parse(webhook) + if err != nil { + return nil, fmt.Errorf("invalid URL: %w", err) + } + + var re = regexp.MustCompile(`^https://[\w.-]+/webhook/[a-zA-Z0-9]+$`) + if !re.MatchString(webhook) { + return nil, errors.New("invalid Teams webhook URL") + } + + return u, nil +} + +func MattermostWebhookUrlPolicy(webhook string) (*url.URL, error) { + if webhook == "" { + return nil, errors.New("webhook URL is required") + } + + u, err := url.Parse(webhook) + if err != nil { + return nil, fmt.Errorf("invalid URL: %w", err) + } + + return u, nil +} diff --git a/pkg/port/mocks/MailChannelService.go b/pkg/port/mocks/MailChannelService.go deleted file mode 100644 index e4175d9..0000000 --- a/pkg/port/mocks/MailChannelService.go +++ /dev/null @@ -1,105 +0,0 @@ -// Code generated by mockery; DO NOT EDIT. -// github.com/vektra/mockery -// template: testify - -package mocks - -import ( - "context" - - "github.com/greenbone/opensight-notification-service/pkg/request" - mock "github.com/stretchr/testify/mock" -) - -// NewMailChannelService creates a new instance of MailChannelService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewMailChannelService(t interface { - mock.TestingT - Cleanup(func()) -}) *MailChannelService { - mock := &MailChannelService{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} - -// MailChannelService is an autogenerated mock type for the MailChannelService type -type MailChannelService struct { - mock.Mock -} - -type MailChannelService_Expecter struct { - mock *mock.Mock -} - -func (_m *MailChannelService) EXPECT() *MailChannelService_Expecter { - return &MailChannelService_Expecter{mock: &_m.Mock} -} - -// CreateMailChannel provides a mock function for the type MailChannelService -func (_mock *MailChannelService) CreateMailChannel(ctx context.Context, channel request.MailNotificationChannelRequest) (request.MailNotificationChannelRequest, error) { - ret := _mock.Called(ctx, channel) - - if len(ret) == 0 { - panic("no return value specified for CreateMailChannel") - } - - var r0 request.MailNotificationChannelRequest - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, request.MailNotificationChannelRequest) (request.MailNotificationChannelRequest, error)); ok { - return returnFunc(ctx, channel) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, request.MailNotificationChannelRequest) request.MailNotificationChannelRequest); ok { - r0 = returnFunc(ctx, channel) - } else { - r0 = ret.Get(0).(request.MailNotificationChannelRequest) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, request.MailNotificationChannelRequest) error); ok { - r1 = returnFunc(ctx, channel) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// MailChannelService_CreateMailChannel_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateMailChannel' -type MailChannelService_CreateMailChannel_Call struct { - *mock.Call -} - -// CreateMailChannel is a helper method to define mock.On call -// - ctx context.Context -// - channel request.MailNotificationChannelRequest -func (_e *MailChannelService_Expecter) CreateMailChannel(ctx interface{}, channel interface{}) *MailChannelService_CreateMailChannel_Call { - return &MailChannelService_CreateMailChannel_Call{Call: _e.mock.On("CreateMailChannel", ctx, channel)} -} - -func (_c *MailChannelService_CreateMailChannel_Call) Run(run func(ctx context.Context, channel request.MailNotificationChannelRequest)) *MailChannelService_CreateMailChannel_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 request.MailNotificationChannelRequest - if args[1] != nil { - arg1 = args[1].(request.MailNotificationChannelRequest) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *MailChannelService_CreateMailChannel_Call) Return(mailNotificationChannelRequest request.MailNotificationChannelRequest, err error) *MailChannelService_CreateMailChannel_Call { - _c.Call.Return(mailNotificationChannelRequest, err) - return _c -} - -func (_c *MailChannelService_CreateMailChannel_Call) RunAndReturn(run func(ctx context.Context, channel request.MailNotificationChannelRequest) (request.MailNotificationChannelRequest, error)) *MailChannelService_CreateMailChannel_Call { - _c.Call.Return(run) - return _c -} diff --git a/pkg/port/mocks/MattermostChannelService.go b/pkg/port/mocks/MattermostChannelService.go deleted file mode 100644 index a99ad96..0000000 --- a/pkg/port/mocks/MattermostChannelService.go +++ /dev/null @@ -1,106 +0,0 @@ -// Code generated by mockery; DO NOT EDIT. -// github.com/vektra/mockery -// template: testify - -package mocks - -import ( - "context" - - "github.com/greenbone/opensight-notification-service/pkg/request" - "github.com/greenbone/opensight-notification-service/pkg/response" - mock "github.com/stretchr/testify/mock" -) - -// NewMattermostChannelService creates a new instance of MattermostChannelService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewMattermostChannelService(t interface { - mock.TestingT - Cleanup(func()) -}) *MattermostChannelService { - mock := &MattermostChannelService{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} - -// MattermostChannelService is an autogenerated mock type for the MattermostChannelService type -type MattermostChannelService struct { - mock.Mock -} - -type MattermostChannelService_Expecter struct { - mock *mock.Mock -} - -func (_m *MattermostChannelService) EXPECT() *MattermostChannelService_Expecter { - return &MattermostChannelService_Expecter{mock: &_m.Mock} -} - -// CreateMattermostChannel provides a mock function for the type MattermostChannelService -func (_mock *MattermostChannelService) CreateMattermostChannel(ctx context.Context, channel request.MattermostNotificationChannelRequest) (response.MattermostNotificationChannelResponse, error) { - ret := _mock.Called(ctx, channel) - - if len(ret) == 0 { - panic("no return value specified for CreateMattermostChannel") - } - - var r0 response.MattermostNotificationChannelResponse - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, request.MattermostNotificationChannelRequest) (response.MattermostNotificationChannelResponse, error)); ok { - return returnFunc(ctx, channel) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, request.MattermostNotificationChannelRequest) response.MattermostNotificationChannelResponse); ok { - r0 = returnFunc(ctx, channel) - } else { - r0 = ret.Get(0).(response.MattermostNotificationChannelResponse) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, request.MattermostNotificationChannelRequest) error); ok { - r1 = returnFunc(ctx, channel) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// MattermostChannelService_CreateMattermostChannel_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateMattermostChannel' -type MattermostChannelService_CreateMattermostChannel_Call struct { - *mock.Call -} - -// CreateMattermostChannel is a helper method to define mock.On call -// - ctx context.Context -// - channel request.MattermostNotificationChannelRequest -func (_e *MattermostChannelService_Expecter) CreateMattermostChannel(ctx interface{}, channel interface{}) *MattermostChannelService_CreateMattermostChannel_Call { - return &MattermostChannelService_CreateMattermostChannel_Call{Call: _e.mock.On("CreateMattermostChannel", ctx, channel)} -} - -func (_c *MattermostChannelService_CreateMattermostChannel_Call) Run(run func(ctx context.Context, channel request.MattermostNotificationChannelRequest)) *MattermostChannelService_CreateMattermostChannel_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 request.MattermostNotificationChannelRequest - if args[1] != nil { - arg1 = args[1].(request.MattermostNotificationChannelRequest) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *MattermostChannelService_CreateMattermostChannel_Call) Return(mattermostNotificationChannelResponse response.MattermostNotificationChannelResponse, err error) *MattermostChannelService_CreateMattermostChannel_Call { - _c.Call.Return(mattermostNotificationChannelResponse, err) - return _c -} - -func (_c *MattermostChannelService_CreateMattermostChannel_Call) RunAndReturn(run func(ctx context.Context, channel request.MattermostNotificationChannelRequest) (response.MattermostNotificationChannelResponse, error)) *MattermostChannelService_CreateMattermostChannel_Call { - _c.Call.Return(run) - return _c -} diff --git a/pkg/port/repository.go b/pkg/port/repository.go deleted file mode 100644 index 27b63fd..0000000 --- a/pkg/port/repository.go +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Greenbone AG -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -package port - -import ( - "context" - - "github.com/greenbone/opensight-golang-libraries/pkg/query" - "github.com/greenbone/opensight-notification-service/pkg/models" -) - -type NotificationRepository interface { - ListNotifications( - ctx context.Context, - resultSelector query.ResultSelector, - ) (notifications []models.Notification, totalResult uint64, err error) - CreateNotification( - ctx context.Context, - notificationIn models.Notification, - ) (notification models.Notification, err error) -} - -type NotificationChannelRepository interface { - CreateNotificationChannel( - ctx context.Context, - channelIn models.NotificationChannel, - ) (models.NotificationChannel, error) - GetNotificationChannelByIdAndType( - ctx context.Context, - id string, - channelType models.ChannelType, - ) (models.NotificationChannel, error) - ListNotificationChannelsByType( - ctx context.Context, - channelType models.ChannelType, - ) ([]models.NotificationChannel, error) - DeleteNotificationChannel(ctx context.Context, id string) error - UpdateNotificationChannel( - ctx context.Context, - id string, - in models.NotificationChannel, - ) (models.NotificationChannel, error) -} diff --git a/pkg/port/security.go b/pkg/port/security.go deleted file mode 100644 index 00e14c7..0000000 --- a/pkg/port/security.go +++ /dev/null @@ -1,6 +0,0 @@ -package port - -type EncryptManager interface { - Encrypt(plaintext string) ([]byte, error) - Decrypt(data []byte) (string, error) -} diff --git a/pkg/port/service.go b/pkg/port/service.go deleted file mode 100644 index 337cd9c..0000000 --- a/pkg/port/service.go +++ /dev/null @@ -1,71 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Greenbone AG -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -package port - -import ( - "context" - - "github.com/greenbone/opensight-golang-libraries/pkg/query" - "github.com/greenbone/opensight-notification-service/pkg/models" - "github.com/greenbone/opensight-notification-service/pkg/request" - "github.com/greenbone/opensight-notification-service/pkg/response" -) - -type NotificationService interface { - ListNotifications( - ctx context.Context, - resultSelector query.ResultSelector, - ) (notifications []models.Notification, totalResult uint64, err error) - CreateNotification( - ctx context.Context, - notificationIn models.Notification, - ) (notification models.Notification, err error) -} - -type HealthService interface { - Ready(ctx context.Context) bool -} - -type NotificationChannelService interface { - CreateNotificationChannel( - ctx context.Context, - channelIn models.NotificationChannel, - ) (models.NotificationChannel, error) - GetNotificationChannelByIdAndType( - ctx context.Context, - id string, - channelType models.ChannelType, - ) (models.NotificationChannel, error) - ListNotificationChannelsByType( - ctx context.Context, - channelType models.ChannelType, - ) ([]models.NotificationChannel, error) - UpdateNotificationChannel( - ctx context.Context, - id string, - channelIn models.NotificationChannel, - ) (models.NotificationChannel, error) - DeleteNotificationChannel(ctx context.Context, id string) error - CheckNotificationChannelConnectivity(ctx context.Context, channel models.NotificationChannel) error - CheckNotificationChannelEntityConnectivity( - ctx context.Context, - id string, - channel models.NotificationChannel, - ) error -} - -type MailChannelService interface { - CreateMailChannel( - ctx context.Context, - channel request.MailNotificationChannelRequest, - ) (request.MailNotificationChannelRequest, error) -} - -type MattermostChannelService interface { - CreateMattermostChannel( - ctx context.Context, - channel request.MattermostNotificationChannelRequest, - ) (response.MattermostNotificationChannelResponse, error) -} diff --git a/pkg/port/mocks/NotificationChannelRepository.go b/pkg/repository/notificationrepository/mocks/NotificationChannelRepository.go similarity index 100% rename from pkg/port/mocks/NotificationChannelRepository.go rename to pkg/repository/notificationrepository/mocks/NotificationChannelRepository.go diff --git a/pkg/port/mocks/NotificationRepository.go b/pkg/repository/notificationrepository/mocks/NotificationRepository.go similarity index 97% rename from pkg/port/mocks/NotificationRepository.go rename to pkg/repository/notificationrepository/mocks/NotificationRepository.go index 5ba4257..d02d31b 100644 --- a/pkg/port/mocks/NotificationRepository.go +++ b/pkg/repository/notificationrepository/mocks/NotificationRepository.go @@ -169,8 +169,8 @@ func (_c *NotificationRepository_ListNotifications_Call) Run(run func(ctx contex return _c } -func (_c *NotificationRepository_ListNotifications_Call) Return(notifications []models.Notification, totalResult uint64, err error) *NotificationRepository_ListNotifications_Call { - _c.Call.Return(notifications, totalResult, err) +func (_c *NotificationRepository_ListNotifications_Call) Return(notifications []models.Notification, totalResults uint64, err error) *NotificationRepository_ListNotifications_Call { + _c.Call.Return(notifications, totalResults, err) return _c } diff --git a/pkg/repository/notificationrepository/notificationChannelRepository.go b/pkg/repository/notificationrepository/notificationChannelRepository.go index 91f8d37..46402cd 100644 --- a/pkg/repository/notificationrepository/notificationChannelRepository.go +++ b/pkg/repository/notificationrepository/notificationChannelRepository.go @@ -7,24 +7,46 @@ import ( "strings" "github.com/greenbone/opensight-notification-service/pkg/models" - "github.com/greenbone/opensight-notification-service/pkg/port" + "github.com/greenbone/opensight-notification-service/pkg/security" "github.com/jmoiron/sqlx" "github.com/rs/zerolog/log" ) -type NotificationChannelRepository struct { +type NotificationChannelRepository interface { + CreateNotificationChannel( + ctx context.Context, + channelIn models.NotificationChannel, + ) (models.NotificationChannel, error) + GetNotificationChannelByIdAndType( + ctx context.Context, + id string, + channelType models.ChannelType, + ) (models.NotificationChannel, error) + ListNotificationChannelsByType( + ctx context.Context, + channelType models.ChannelType, + ) ([]models.NotificationChannel, error) + UpdateNotificationChannel( + ctx context.Context, + id string, + in models.NotificationChannel, + ) (models.NotificationChannel, error) + DeleteNotificationChannel(ctx context.Context, id string) error +} + +type notificationChannelRepository struct { client *sqlx.DB - encryptManager port.EncryptManager + encryptManager security.EncryptManager } func NewNotificationChannelRepository( db *sqlx.DB, - encryptService port.EncryptManager, -) (port.NotificationChannelRepository, error) { + encryptService security.EncryptManager, +) (NotificationChannelRepository, error) { if db == nil { return nil, errors.New("nil db reference") } - client := &NotificationChannelRepository{ + client := ¬ificationChannelRepository{ client: db, encryptManager: encryptService, } @@ -72,7 +94,7 @@ func buildUpdateNotificationChannelQuery(in models.NotificationChannel) string { } // CreateNotificationChannel is now transactional and supports commit/rollback. -func (r *NotificationChannelRepository) CreateNotificationChannel( +func (r *notificationChannelRepository) CreateNotificationChannel( ctx context.Context, channelIn models.NotificationChannel, ) (models.NotificationChannel, error) { @@ -111,7 +133,7 @@ func (r *NotificationChannelRepository) CreateNotificationChannel( return r.decrypt(row).ToModel(), nil } -func (r *NotificationChannelRepository) GetNotificationChannelByIdAndType( +func (r *notificationChannelRepository) GetNotificationChannelByIdAndType( ctx context.Context, id string, channelType models.ChannelType, @@ -126,7 +148,7 @@ func (r *NotificationChannelRepository) GetNotificationChannelByIdAndType( return r.decrypt(row).ToModel(), nil } -func (r *NotificationChannelRepository) ListNotificationChannelsByType( +func (r *notificationChannelRepository) ListNotificationChannelsByType( ctx context.Context, channelType models.ChannelType, ) ([]models.NotificationChannel, error) { @@ -146,7 +168,7 @@ func (r *NotificationChannelRepository) ListNotificationChannelsByType( } // UpdateNotificationChannel is now transactional. -func (r *NotificationChannelRepository) UpdateNotificationChannel( +func (r *notificationChannelRepository) UpdateNotificationChannel( ctx context.Context, id string, in models.NotificationChannel, @@ -187,7 +209,7 @@ func (r *NotificationChannelRepository) UpdateNotificationChannel( return r.decrypt(row).ToModel(), nil } -func (r *NotificationChannelRepository) encrypt(row notificationChannelRow) (notificationChannelRow, error) { +func (r *notificationChannelRepository) encrypt(row notificationChannelRow) (notificationChannelRow, error) { if row.Password != nil && strings.TrimSpace(*row.Password) != "" { encryptedPasswd, err := r.encryptManager.Encrypt(*row.Password) if err != nil { @@ -211,7 +233,7 @@ func (r *NotificationChannelRepository) encrypt(row notificationChannelRow) (not return row, nil } -func (r *NotificationChannelRepository) decrypt(row notificationChannelRow) notificationChannelRow { +func (r *notificationChannelRepository) decrypt(row notificationChannelRow) notificationChannelRow { if row.Password != nil && strings.TrimSpace(*row.Password) != "" { dPasswd := *row.Password dcPassword, err := r.encryptManager.Decrypt([]byte(dPasswd)) @@ -236,7 +258,7 @@ func (r *NotificationChannelRepository) decrypt(row notificationChannelRow) noti } // DeleteNotificationChannel is now transactional. -func (r *NotificationChannelRepository) DeleteNotificationChannel(ctx context.Context, id string) error { +func (r *notificationChannelRepository) DeleteNotificationChannel(ctx context.Context, id string) error { tx, err := r.client.BeginTxx(ctx, nil) if err != nil { return fmt.Errorf("could not begin transaction: %w", err) diff --git a/pkg/repository/notificationrepository/notificationChannelRepository_test.go b/pkg/repository/notificationrepository/notificationChannelRepository_test.go index bcad85d..43f41fa 100644 --- a/pkg/repository/notificationrepository/notificationChannelRepository_test.go +++ b/pkg/repository/notificationrepository/notificationChannelRepository_test.go @@ -12,13 +12,12 @@ import ( "github.com/greenbone/opensight-notification-service/pkg/helper" "github.com/greenbone/opensight-notification-service/pkg/models" "github.com/greenbone/opensight-notification-service/pkg/pgtesting" - "github.com/greenbone/opensight-notification-service/pkg/port" "github.com/greenbone/opensight-notification-service/pkg/security" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func setupTestRepo(t *testing.T) (context.Context, port.NotificationChannelRepository) { +func setupTestRepo(t *testing.T) (context.Context, NotificationChannelRepository) { encryptMgr := security.NewEncryptManager() encryptMgr.UpdateKeys(config.DatabaseEncryptionKey{ Password: "password", diff --git a/pkg/repository/notificationrepository/notificationRepository.go b/pkg/repository/notificationrepository/notificationRepository.go index 5c326d4..06ec1ca 100644 --- a/pkg/repository/notificationrepository/notificationRepository.go +++ b/pkg/repository/notificationrepository/notificationRepository.go @@ -12,26 +12,39 @@ import ( pgquery "github.com/greenbone/opensight-golang-libraries/pkg/postgres/query" "github.com/greenbone/opensight-golang-libraries/pkg/query" "github.com/greenbone/opensight-notification-service/pkg/models" - "github.com/greenbone/opensight-notification-service/pkg/port" "github.com/greenbone/opensight-notification-service/pkg/repository" "github.com/jmoiron/sqlx" ) -type NotificationRepository struct { +type NotificationRepository interface { + ListNotifications( + ctx context.Context, + resultSelector query.ResultSelector, + ) (notifications []models.Notification, totalResults uint64, err error) + CreateNotification( + ctx context.Context, + notificationIn models.Notification, + ) (notification models.Notification, err error) +} + +type notificationRepository struct { client *sqlx.DB } -func NewNotificationRepository(db *sqlx.DB) (port.NotificationRepository, error) { +func NewNotificationRepository(db *sqlx.DB) (NotificationRepository, error) { if db == nil { return nil, errors.New("nil db reference") } - client := &NotificationRepository{ + client := ¬ificationRepository{ client: db, } return client, nil } -func (r *NotificationRepository) ListNotifications(ctx context.Context, resultSelector query.ResultSelector) (notifications []models.Notification, totalResults uint64, err error) { +func (r *notificationRepository) ListNotifications( + ctx context.Context, + resultSelector query.ResultSelector, +) (notifications []models.Notification, totalResults uint64, err error) { querySettings := pgquery.Settings{ FilterFieldMapping: notificationFieldMapping(), SortingTieBreakerColumn: "id", @@ -70,7 +83,10 @@ func (r *NotificationRepository) ListNotifications(ctx context.Context, resultSe return } -func (r *NotificationRepository) CreateNotification(ctx context.Context, notificationIn models.Notification) (notification models.Notification, err error) { +func (r *notificationRepository) CreateNotification( + ctx context.Context, + notificationIn models.Notification, +) (notification models.Notification, err error) { insertRow, err := toNotificationRow(notificationIn) if err != nil { return notification, fmt.Errorf("invalid argument for inserting notification into database: %w", err) diff --git a/pkg/repository/notificationrepository/notification_db_models.go b/pkg/repository/notificationrepository/notification_db_models.go index d6c18c6..378049b 100644 --- a/pkg/repository/notificationrepository/notification_db_models.go +++ b/pkg/repository/notificationrepository/notification_db_models.go @@ -44,7 +44,7 @@ func toNotificationRow(n models.Notification) (notificationRow, error) { customFieldsSerialized, err := json.Marshal(n.CustomFields) if err != nil { - return empty, err // TODO: return validation error ? + return empty, err } notificationRow := notificationRow{ diff --git a/pkg/repository/repository.go b/pkg/repository/repository.go index 20a80f8..50eff5c 100644 --- a/pkg/repository/repository.go +++ b/pkg/repository/repository.go @@ -38,7 +38,7 @@ func NewClient(postgres config.Database) (*sqlx.DB, error) { } if automigrateErr := autoMigrate(connectionString); automigrateErr != nil { - if errors.Is(automigrateErr, migrate.ErrNoChange) { // TODO: handle errNoChange when migration file is unchanged on restart of the app on the same environment + if errors.Is(automigrateErr, migrate.ErrNoChange) { log.Debug().Msg("nothing to migrate") return db, nil } diff --git a/pkg/response/MattermostNotificationChannelResponse.go b/pkg/response/MattermostNotificationChannelResponse.go deleted file mode 100644 index 55fea0f..0000000 --- a/pkg/response/MattermostNotificationChannelResponse.go +++ /dev/null @@ -1,8 +0,0 @@ -package response - -type MattermostNotificationChannelResponse struct { - Id *string `json:"id,omitempty"` - ChannelName string `json:"channelName"` - WebhookUrl string `json:"webhookUrl"` - Description string `json:"description"` -} diff --git a/pkg/restErrorHandler/rest_error_handler.go b/pkg/restErrorHandler/rest_error_handler.go index 47c8a07..9c1a83b 100644 --- a/pkg/restErrorHandler/rest_error_handler.go +++ b/pkg/restErrorHandler/rest_error_handler.go @@ -8,14 +8,10 @@ import ( "errors" "net/http" + "github.com/gin-gonic/gin" + "github.com/greenbone/opensight-golang-libraries/pkg/errorResponses" "github.com/greenbone/opensight-golang-libraries/pkg/logs" - "github.com/greenbone/opensight-notification-service/pkg/services/notificationchannelservice" - "github.com/greenbone/opensight-notification-service/pkg/errs" - - "github.com/greenbone/opensight-golang-libraries/pkg/errorResponses" - - "github.com/gin-gonic/gin" ) // ErrorHandler determines the appropriate error response and code from the error type. It relies on the types defined in [errs]. @@ -28,7 +24,7 @@ func ErrorHandler(gc *gin.Context, internalErrorLogMessage string, err error) { case errors.Is(err, errs.ErrItemNotFound): gc.JSON(http.StatusNotFound, errorResponses.NewErrorGenericResponse(err.Error())) case errors.As(err, &errConflict): - gc.JSON(http.StatusConflict, ErrConflictToResponse(*errConflict)) + gc.JSON(http.StatusUnprocessableEntity, ErrConflictToResponse(*errConflict)) case errors.As(err, &errValidation): gc.JSON(http.StatusBadRequest, ErrValidationToResponse(*errValidation)) default: @@ -48,28 +44,3 @@ func ErrConflictToResponse(err errs.ErrConflict) errorResponses.ErrorResponse { Errors: err.Errors, } } - -func NotificationChannelErrorHandler(gc *gin.Context, title string, errs map[string]string, err error) { - if len(errs) > 0 && title != "" { - gc.JSON(http.StatusBadRequest, errorResponses.NewErrorValidationResponse(title, "", errs)) - return - } - - switch { - case errors.Is(err, notificationchannelservice.ErrMailChannelBadRequest) || - errors.Is(err, notificationchannelservice.ErrMattermostChannelBadRequest): - gc.JSON(http.StatusBadRequest, - errorResponses.NewErrorValidationResponse("Invalid mail channel data.", "", nil)) - case errors.Is(err, notificationchannelservice.ErrListMailChannels) || - errors.Is(err, notificationchannelservice.ErrListMattermostChannels): - gc.JSON(http.StatusInternalServerError, errorResponses.ErrorInternalResponse) - case errors.Is(err, notificationchannelservice.ErrMailChannelLimitReached): - gc.JSON(http.StatusConflict, errorResponses.NewErrorValidationResponse("Mail channel limit reached.", "", - map[string]string{"channelName": "Mail channel already exists."})) - case errors.Is(err, notificationchannelservice.ErrMattermostChannelLimitReached): - gc.JSON(http.StatusUnprocessableEntity, errorResponses.NewErrorValidationResponse("Mattermost channel limit reached.", "", - map[string]string{"channelName": "Mattermost channel creation limit reached."})) - default: - gc.JSON(http.StatusInternalServerError, errorResponses.ErrorInternalResponse) - } -} diff --git a/pkg/restErrorHandler/rest_error_handler_test.go b/pkg/restErrorHandler/rest_error_handler_test.go index 94ed0ee..b0bf0f6 100644 --- a/pkg/restErrorHandler/rest_error_handler_test.go +++ b/pkg/restErrorHandler/rest_error_handler_test.go @@ -13,12 +13,10 @@ import ( "net/http/httptest" "testing" + "github.com/gin-gonic/gin" + "github.com/greenbone/opensight-golang-libraries/pkg/errorResponses" "github.com/greenbone/opensight-notification-service/pkg/errs" "github.com/greenbone/opensight-notification-service/pkg/helper" - - "github.com/greenbone/opensight-golang-libraries/pkg/errorResponses" - - "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" ) @@ -47,9 +45,9 @@ func TestErrorHandler(t *testing.T) { wantStatusCode: http.StatusNotFound, }, { - name: "conflict error", + name: "UnprocessableEntity error", err: fmt.Errorf("wrapped: %w", &someConflictError), - wantStatusCode: http.StatusConflict, + wantStatusCode: http.StatusUnprocessableEntity, wantErrorResponse: helper.ToPtr(ErrConflictToResponse(someConflictError)), }, { @@ -63,15 +61,13 @@ func TestErrorHandler(t *testing.T) { t.Run(tt.name, func(t *testing.T) { gotResponse := httptest.NewRecorder() gc, _ := gin.CreateTestContext(gotResponse) - // pretend a request has happened gc.Request = httptest.NewRequest(http.MethodGet, "/some/path", nil) ErrorHandler(gc, "some specific log message", tt.err) - // compare status code assert.Equal(t, tt.wantStatusCode, gotResponse.Code) - if tt.wantErrorResponse != nil { // compare responses + if tt.wantErrorResponse != nil { gotErrorResponse, err := io.ReadAll(gotResponse.Body) if err != nil { t.Error("could not read response body: %w", err) diff --git a/pkg/security/encryptManager.go b/pkg/security/encryptManager.go index c192410..1e46a09 100644 --- a/pkg/security/encryptManager.go +++ b/pkg/security/encryptManager.go @@ -15,15 +15,21 @@ type Key struct { PasswordSalt string } -type EncryptManager struct { +type EncryptManager interface { + UpdateKeys(keyringConfig config.DatabaseEncryptionKey) + Encrypt(plaintext string) ([]byte, error) + Decrypt(data []byte) (string, error) +} + +type encryptManager struct { activeKey Key } -func NewEncryptManager() *EncryptManager { - return &EncryptManager{} +func NewEncryptManager() EncryptManager { + return &encryptManager{} } -func (sm *EncryptManager) UpdateKeys(keyringConfig config.DatabaseEncryptionKey) { +func (sm *encryptManager) UpdateKeys(keyringConfig config.DatabaseEncryptionKey) { if strings.TrimSpace(keyringConfig.Password) == "" { log.Error().Msg("Empty password for keyring") } @@ -40,7 +46,7 @@ func (sm *EncryptManager) UpdateKeys(keyringConfig config.DatabaseEncryptionKey) log.Info().Msgf("Keyring successfully refreshed in memory") } -func (sm *EncryptManager) Encrypt(plaintext string) ([]byte, error) { +func (sm *encryptManager) Encrypt(plaintext string) ([]byte, error) { if len(strings.TrimSpace(plaintext)) == 0 { return nil, errors.New("plaintext must be a value") } @@ -61,7 +67,7 @@ func (sm *EncryptManager) Encrypt(plaintext string) ([]byte, error) { return encryptedBytes, nil } -func (sm *EncryptManager) Decrypt(data []byte) (string, error) { +func (sm *encryptManager) Decrypt(data []byte) (string, error) { if len(data) == 0 { return "", errors.New("data must be a value") } diff --git a/pkg/security/encryptManager_test.go b/pkg/security/encryptManager_test.go index 9c182df..479b572 100644 --- a/pkg/security/encryptManager_test.go +++ b/pkg/security/encryptManager_test.go @@ -18,8 +18,13 @@ func TestSaltManager_UpdateKeys(t *testing.T) { mgr := NewEncryptManager() mgr.UpdateKeys(cfg) - assert.Equal(t, mgr.activeKey.Password, cfg.Password) - assert.Equal(t, mgr.activeKey.PasswordSalt, cfg.PasswordSalt) + em, ok := mgr.(*encryptManager) + if !ok { + t.Fatalf("mgr is not of type *encryptManager") + } + + assert.Equal(t, em.activeKey.Password, cfg.Password) + assert.Equal(t, em.activeKey.PasswordSalt, cfg.PasswordSalt) } func TestSaltManager_Encrypt_Decrypt(t *testing.T) { diff --git a/pkg/port/mocks/EncryptManager.go b/pkg/security/mocks/EncryptManager.go similarity index 75% rename from pkg/port/mocks/EncryptManager.go rename to pkg/security/mocks/EncryptManager.go index 8fc726c..8417a16 100644 --- a/pkg/port/mocks/EncryptManager.go +++ b/pkg/security/mocks/EncryptManager.go @@ -5,6 +5,7 @@ package mocks import ( + "github.com/greenbone/opensight-notification-service/pkg/config" mock "github.com/stretchr/testify/mock" ) @@ -156,3 +157,43 @@ func (_c *EncryptManager_Encrypt_Call) RunAndReturn(run func(plaintext string) ( _c.Call.Return(run) return _c } + +// UpdateKeys provides a mock function for the type EncryptManager +func (_mock *EncryptManager) UpdateKeys(keyringConfig config.DatabaseEncryptionKey) { + _mock.Called(keyringConfig) + return +} + +// EncryptManager_UpdateKeys_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateKeys' +type EncryptManager_UpdateKeys_Call struct { + *mock.Call +} + +// UpdateKeys is a helper method to define mock.On call +// - keyringConfig config.DatabaseEncryptionKey +func (_e *EncryptManager_Expecter) UpdateKeys(keyringConfig interface{}) *EncryptManager_UpdateKeys_Call { + return &EncryptManager_UpdateKeys_Call{Call: _e.mock.On("UpdateKeys", keyringConfig)} +} + +func (_c *EncryptManager_UpdateKeys_Call) Run(run func(keyringConfig config.DatabaseEncryptionKey)) *EncryptManager_UpdateKeys_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 config.DatabaseEncryptionKey + if args[0] != nil { + arg0 = args[0].(config.DatabaseEncryptionKey) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *EncryptManager_UpdateKeys_Call) Return() *EncryptManager_UpdateKeys_Call { + _c.Call.Return() + return _c +} + +func (_c *EncryptManager_UpdateKeys_Call) RunAndReturn(run func(keyringConfig config.DatabaseEncryptionKey)) *EncryptManager_UpdateKeys_Call { + _c.Run(run) + return _c +} diff --git a/pkg/services/healthservice/healthService.go b/pkg/services/healthservice/healthService.go index ad797a2..16d37d1 100644 --- a/pkg/services/healthservice/healthService.go +++ b/pkg/services/healthservice/healthService.go @@ -11,19 +11,23 @@ import ( "github.com/rs/zerolog/log" ) -type HealthService struct { +type HealthService interface { + Ready(ctx context.Context) (ready bool) +} + +type healthService struct { pgClient *sqlx.DB } -func NewHealthService(pgClient *sqlx.DB) *HealthService { - return &HealthService{ +func NewHealthService(pgClient *sqlx.DB) HealthService { + return &healthService{ pgClient: pgClient, } } // Ready indicates if the service is ready to serve traffic. // Check that databases are up and ready to serve data -func (s *HealthService) Ready(ctx context.Context) (ready bool) { +func (s *healthService) Ready(ctx context.Context) (ready bool) { // check postgres health err := s.pgClient.Ping() if err != nil { diff --git a/pkg/port/mocks/HealthService.go b/pkg/services/healthservice/mocks/HealthService.go similarity index 95% rename from pkg/port/mocks/HealthService.go rename to pkg/services/healthservice/mocks/HealthService.go index ee604e4..1bc4fd4 100644 --- a/pkg/port/mocks/HealthService.go +++ b/pkg/services/healthservice/mocks/HealthService.go @@ -78,8 +78,8 @@ func (_c *HealthService_Ready_Call) Run(run func(ctx context.Context)) *HealthSe return _c } -func (_c *HealthService_Ready_Call) Return(b bool) *HealthService_Ready_Call { - _c.Call.Return(b) +func (_c *HealthService_Ready_Call) Return(ready bool) *HealthService_Ready_Call { + _c.Call.Return(ready) return _c } diff --git a/pkg/services/notificationchannelservice/connectionCheck.go b/pkg/services/notificationchannelservice/connectionCheck.go index e3e0904..9a208e4 100644 --- a/pkg/services/notificationchannelservice/connectionCheck.go +++ b/pkg/services/notificationchannelservice/connectionCheck.go @@ -2,13 +2,18 @@ package notificationchannelservice import ( "context" - "fmt" + "errors" "time" "github.com/greenbone/opensight-notification-service/pkg/models" "github.com/wneessen/go-mail" ) +var ( + ErrCreateMailFailed = errors.New("failed to create mail client") + ErrMailServerUnreachable = errors.New("mail server is unreachable") +) + func ConnectionCheckMail(ctx context.Context, mailServer models.NotificationChannel) error { options := []mail.Option{ mail.WithPort(*mailServer.Port), @@ -32,14 +37,12 @@ func ConnectionCheckMail(ctx context.Context, mailServer models.NotificationChan }() if err != nil { - return fmt.Errorf("failed to create mail client: %w", err) + return errors.Join(err, ErrCreateMailFailed) } if err = client.DialWithContext(ctx); err != nil { - return fmt.Errorf("failed to reach mail server: %w", err) + return errors.Join(err, ErrMailServerUnreachable) } - // TODO: 21.01.2026 stolksdorf - username and password are not validated - return nil } diff --git a/pkg/services/notificationchannelservice/mailChannelService.go b/pkg/services/notificationchannelservice/mailChannelService.go index aa7e3c4..86ead9e 100644 --- a/pkg/services/notificationchannelservice/mailChannelService.go +++ b/pkg/services/notificationchannelservice/mailChannelService.go @@ -4,77 +4,83 @@ import ( "context" "errors" - "github.com/greenbone/opensight-notification-service/pkg/mapper" "github.com/greenbone/opensight-notification-service/pkg/models" - "github.com/greenbone/opensight-notification-service/pkg/port" - "github.com/greenbone/opensight-notification-service/pkg/request" + "github.com/greenbone/opensight-notification-service/pkg/repository/notificationrepository" + "github.com/greenbone/opensight-notification-service/pkg/web/mailcontroller/maildto" ) var ( ErrMailChannelLimitReached = errors.New("mail channel limit reached") ErrListMailChannels = errors.New("failed to list mail channels") - ErrMailChannelBadRequest = errors.New("bad request for mail channel") + ErrGetMailChannel = errors.New("unable to get notification channel id and type") ) -type MailChannelService struct { - notificationChannelService port.NotificationChannelService +type MailChannelService interface { + CreateMailChannel( + c context.Context, + channel maildto.MailNotificationChannelRequest, + ) (maildto.MailNotificationChannelResponse, error) + CheckNotificationChannelConnectivity( + ctx context.Context, + mailServer models.NotificationChannel, + ) error + CheckNotificationChannelEntityConnectivity( + ctx context.Context, + id string, + mailServer models.NotificationChannel, + ) error +} + +type mailChannelService struct { + notificationChannelService NotificationChannelService + store notificationrepository.NotificationChannelRepository emailLimit int } func NewMailChannelService( - notificationChannelService port.NotificationChannelService, + notificationChannelService NotificationChannelService, + store notificationrepository.NotificationChannelRepository, emailLimit int, -) *MailChannelService { - return &MailChannelService{ +) MailChannelService { + return &mailChannelService{ notificationChannelService: notificationChannelService, + store: store, emailLimit: emailLimit, } } -func (m *MailChannelService) mailChannelAlreadyExists(c context.Context) error { - channels, err := m.notificationChannelService.ListNotificationChannelsByType(c, models.ChannelTypeMail) - if err != nil { - return errors.Join(ErrListMailChannels, err) - } - - if len(channels) >= m.emailLimit { - return ErrMailChannelLimitReached - } - return nil -} - -func (m *MailChannelService) CreateMailChannel( +func (m *mailChannelService) CreateMailChannel( c context.Context, - channel request.MailNotificationChannelRequest, -) (request.MailNotificationChannelRequest, error) { + channel maildto.MailNotificationChannelRequest, +) (maildto.MailNotificationChannelResponse, error) { if errResp := m.mailChannelAlreadyExists(c); errResp != nil { - return request.MailNotificationChannelRequest{}, errResp + return maildto.MailNotificationChannelResponse{}, errResp } - notificationChannel := mapper.MapMailToNotificationChannel(channel) + notificationChannel := maildto.MapMailToNotificationChannel(channel) created, err := m.notificationChannelService.CreateNotificationChannel(c, notificationChannel) if err != nil { - return request.MailNotificationChannelRequest{}, err + return maildto.MailNotificationChannelResponse{}, err } - return mapper.MapNotificationChannelToMail(created), nil + return maildto.MapNotificationChannelToMail(created), nil } -func (s *NotificationChannelService) CheckNotificationChannelConnectivity( +func (m *mailChannelService) CheckNotificationChannelConnectivity( ctx context.Context, mailServer models.NotificationChannel, ) error { return ConnectionCheckMail(ctx, mailServer) } -func (s *NotificationChannelService) CheckNotificationChannelEntityConnectivity( +func (m *mailChannelService) CheckNotificationChannelEntityConnectivity( ctx context.Context, id string, mailServer models.NotificationChannel, ) error { - channel, err := s.GetNotificationChannelByIdAndType(ctx, id, models.ChannelTypeMail) + channel, err := m.store.GetNotificationChannelByIdAndType(ctx, id, models.ChannelTypeMail) if err != nil { - return err + return errors.Join(ErrGetMailChannel, err) } if *mailServer.Password == "" && *mailServer.Username != "" { @@ -85,3 +91,15 @@ func (s *NotificationChannelService) CheckNotificationChannelEntityConnectivity( return ConnectionCheckMail(ctx, mailServer) } + +func (m *mailChannelService) mailChannelAlreadyExists(c context.Context) error { + channels, err := m.notificationChannelService.ListNotificationChannelsByType(c, models.ChannelTypeMail) + if err != nil { + return errors.Join(ErrListMailChannels, err) + } + + if len(channels) >= m.emailLimit { + return ErrMailChannelLimitReached + } + return nil +} diff --git a/pkg/services/notificationchannelservice/mattermostChannelService.go b/pkg/services/notificationchannelservice/mattermostChannelService.go index 26fcb24..fd62eac 100644 --- a/pkg/services/notificationchannelservice/mattermostChannelService.go +++ b/pkg/services/notificationchannelservice/mattermostChannelService.go @@ -1,62 +1,101 @@ package notificationchannelservice import ( + "bytes" "context" + "encoding/json" "errors" + "fmt" + "net/http" - "github.com/greenbone/opensight-notification-service/pkg/mapper" "github.com/greenbone/opensight-notification-service/pkg/models" - "github.com/greenbone/opensight-notification-service/pkg/port" - "github.com/greenbone/opensight-notification-service/pkg/request" - "github.com/greenbone/opensight-notification-service/pkg/response" + "github.com/greenbone/opensight-notification-service/pkg/web/mattermostcontroller/mattermostdto" ) var ( ErrMattermostChannelLimitReached = errors.New("mattermost channel limit reached") ErrListMattermostChannels = errors.New("failed to list mattermost channels") - ErrMattermostChannelBadRequest = errors.New("bad request for mattermost channel") + ErrMattermostChannelNameExists = errors.New("mattermost channel name already exists") ) -type MattermostChannelService struct { - notificationChannelService port.NotificationChannelService +type MattermostChannelService interface { + SendMattermostTestMessage(webhookUrl string) error + CreateMattermostChannel( + c context.Context, + channel mattermostdto.MattermostNotificationChannelRequest, + ) (mattermostdto.MattermostNotificationChannelResponse, error) +} + +type mattermostChannelService struct { + notificationChannelService NotificationChannelService mattermostChannelLimit int } func NewMattermostChannelService( - notificationChannelService port.NotificationChannelService, + notificationChannelService NotificationChannelService, mattermostChannelLimit int, -) *MattermostChannelService { - return &MattermostChannelService{ +) MattermostChannelService { + return &mattermostChannelService{ notificationChannelService: notificationChannelService, mattermostChannelLimit: mattermostChannelLimit, } } -func (m *MattermostChannelService) mattermostChannelLimitReached(c context.Context) error { - channels, err := m.notificationChannelService.ListNotificationChannelsByType(c, models.ChannelTypeMattermost) +func (m *mattermostChannelService) SendMattermostTestMessage(webhookUrl string) error { + body, err := json.Marshal(map[string]string{ + "text": "Hello This is a test message", + }) if err != nil { - return errors.Join(ErrListMattermostChannels, err) + return err } - if len(channels) >= m.mattermostChannelLimit { - return ErrMattermostChannelLimitReached + resp, err := http.Post(webhookUrl, "application/json", bytes.NewBuffer(body)) + if err != nil { + return err + } + defer func() { + _ = resp.Body.Close() + }() + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("failed to send test message to Mattermost webhook: %s", resp.Status) } + return nil } -func (m *MattermostChannelService) CreateMattermostChannel( +func (m *mattermostChannelService) CreateMattermostChannel( c context.Context, - channel request.MattermostNotificationChannelRequest, -) (response.MattermostNotificationChannelResponse, error) { - if errResp := m.mattermostChannelLimitReached(c); errResp != nil { - return response.MattermostNotificationChannelResponse{}, errResp + channel mattermostdto.MattermostNotificationChannelRequest, +) (mattermostdto.MattermostNotificationChannelResponse, error) { + if errResp := m.mattermostChannelValidations(c, channel.ChannelName); errResp != nil { + return mattermostdto.MattermostNotificationChannelResponse{}, errResp } - notificationChannel := mapper.MapMattermostToNotificationChannel(channel) + notificationChannel := mattermostdto.MapMattermostToNotificationChannel(channel) created, err := m.notificationChannelService.CreateNotificationChannel(c, notificationChannel) if err != nil { - return response.MattermostNotificationChannelResponse{}, err + return mattermostdto.MattermostNotificationChannelResponse{}, err + } + + return mattermostdto.MapNotificationChannelToMattermost(created), nil +} + +func (m *mattermostChannelService) mattermostChannelValidations(c context.Context, channelName string) error { + channels, err := m.notificationChannelService.ListNotificationChannelsByType(c, models.ChannelTypeMattermost) + if err != nil { + return errors.Join(ErrListMattermostChannels, err) + } + + if len(channels) >= m.mattermostChannelLimit { + return ErrMattermostChannelLimitReached } - return mapper.MapNotificationChannelToMattermost(created), nil + for _, ch := range channels { + if ch.ChannelName != nil && *ch.ChannelName == channelName { + return ErrMattermostChannelNameExists + } + } + + return nil } diff --git a/pkg/services/notificationchannelservice/mocks/MailChannelService.go b/pkg/services/notificationchannelservice/mocks/MailChannelService.go new file mode 100644 index 0000000..6319ae6 --- /dev/null +++ b/pkg/services/notificationchannelservice/mocks/MailChannelService.go @@ -0,0 +1,226 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package mocks + +import ( + "context" + + "github.com/greenbone/opensight-notification-service/pkg/models" + "github.com/greenbone/opensight-notification-service/pkg/web/mailcontroller/maildto" + mock "github.com/stretchr/testify/mock" +) + +// NewMailChannelService creates a new instance of MailChannelService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMailChannelService(t interface { + mock.TestingT + Cleanup(func()) +}) *MailChannelService { + mock := &MailChannelService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MailChannelService is an autogenerated mock type for the MailChannelService type +type MailChannelService struct { + mock.Mock +} + +type MailChannelService_Expecter struct { + mock *mock.Mock +} + +func (_m *MailChannelService) EXPECT() *MailChannelService_Expecter { + return &MailChannelService_Expecter{mock: &_m.Mock} +} + +// CheckNotificationChannelConnectivity provides a mock function for the type MailChannelService +func (_mock *MailChannelService) CheckNotificationChannelConnectivity(ctx context.Context, mailServer models.NotificationChannel) error { + ret := _mock.Called(ctx, mailServer) + + if len(ret) == 0 { + panic("no return value specified for CheckNotificationChannelConnectivity") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, models.NotificationChannel) error); ok { + r0 = returnFunc(ctx, mailServer) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MailChannelService_CheckNotificationChannelConnectivity_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CheckNotificationChannelConnectivity' +type MailChannelService_CheckNotificationChannelConnectivity_Call struct { + *mock.Call +} + +// CheckNotificationChannelConnectivity is a helper method to define mock.On call +// - ctx context.Context +// - mailServer models.NotificationChannel +func (_e *MailChannelService_Expecter) CheckNotificationChannelConnectivity(ctx interface{}, mailServer interface{}) *MailChannelService_CheckNotificationChannelConnectivity_Call { + return &MailChannelService_CheckNotificationChannelConnectivity_Call{Call: _e.mock.On("CheckNotificationChannelConnectivity", ctx, mailServer)} +} + +func (_c *MailChannelService_CheckNotificationChannelConnectivity_Call) Run(run func(ctx context.Context, mailServer models.NotificationChannel)) *MailChannelService_CheckNotificationChannelConnectivity_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 models.NotificationChannel + if args[1] != nil { + arg1 = args[1].(models.NotificationChannel) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MailChannelService_CheckNotificationChannelConnectivity_Call) Return(err error) *MailChannelService_CheckNotificationChannelConnectivity_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MailChannelService_CheckNotificationChannelConnectivity_Call) RunAndReturn(run func(ctx context.Context, mailServer models.NotificationChannel) error) *MailChannelService_CheckNotificationChannelConnectivity_Call { + _c.Call.Return(run) + return _c +} + +// CheckNotificationChannelEntityConnectivity provides a mock function for the type MailChannelService +func (_mock *MailChannelService) CheckNotificationChannelEntityConnectivity(ctx context.Context, id string, mailServer models.NotificationChannel) error { + ret := _mock.Called(ctx, id, mailServer) + + if len(ret) == 0 { + panic("no return value specified for CheckNotificationChannelEntityConnectivity") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string, models.NotificationChannel) error); ok { + r0 = returnFunc(ctx, id, mailServer) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MailChannelService_CheckNotificationChannelEntityConnectivity_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CheckNotificationChannelEntityConnectivity' +type MailChannelService_CheckNotificationChannelEntityConnectivity_Call struct { + *mock.Call +} + +// CheckNotificationChannelEntityConnectivity is a helper method to define mock.On call +// - ctx context.Context +// - id string +// - mailServer models.NotificationChannel +func (_e *MailChannelService_Expecter) CheckNotificationChannelEntityConnectivity(ctx interface{}, id interface{}, mailServer interface{}) *MailChannelService_CheckNotificationChannelEntityConnectivity_Call { + return &MailChannelService_CheckNotificationChannelEntityConnectivity_Call{Call: _e.mock.On("CheckNotificationChannelEntityConnectivity", ctx, id, mailServer)} +} + +func (_c *MailChannelService_CheckNotificationChannelEntityConnectivity_Call) Run(run func(ctx context.Context, id string, mailServer models.NotificationChannel)) *MailChannelService_CheckNotificationChannelEntityConnectivity_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 models.NotificationChannel + if args[2] != nil { + arg2 = args[2].(models.NotificationChannel) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *MailChannelService_CheckNotificationChannelEntityConnectivity_Call) Return(err error) *MailChannelService_CheckNotificationChannelEntityConnectivity_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MailChannelService_CheckNotificationChannelEntityConnectivity_Call) RunAndReturn(run func(ctx context.Context, id string, mailServer models.NotificationChannel) error) *MailChannelService_CheckNotificationChannelEntityConnectivity_Call { + _c.Call.Return(run) + return _c +} + +// CreateMailChannel provides a mock function for the type MailChannelService +func (_mock *MailChannelService) CreateMailChannel(c context.Context, channel maildto.MailNotificationChannelRequest) (maildto.MailNotificationChannelResponse, error) { + ret := _mock.Called(c, channel) + + if len(ret) == 0 { + panic("no return value specified for CreateMailChannel") + } + + var r0 maildto.MailNotificationChannelResponse + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, maildto.MailNotificationChannelRequest) (maildto.MailNotificationChannelResponse, error)); ok { + return returnFunc(c, channel) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, maildto.MailNotificationChannelRequest) maildto.MailNotificationChannelResponse); ok { + r0 = returnFunc(c, channel) + } else { + r0 = ret.Get(0).(maildto.MailNotificationChannelResponse) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, maildto.MailNotificationChannelRequest) error); ok { + r1 = returnFunc(c, channel) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MailChannelService_CreateMailChannel_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateMailChannel' +type MailChannelService_CreateMailChannel_Call struct { + *mock.Call +} + +// CreateMailChannel is a helper method to define mock.On call +// - c context.Context +// - channel maildto.MailNotificationChannelRequest +func (_e *MailChannelService_Expecter) CreateMailChannel(c interface{}, channel interface{}) *MailChannelService_CreateMailChannel_Call { + return &MailChannelService_CreateMailChannel_Call{Call: _e.mock.On("CreateMailChannel", c, channel)} +} + +func (_c *MailChannelService_CreateMailChannel_Call) Run(run func(c context.Context, channel maildto.MailNotificationChannelRequest)) *MailChannelService_CreateMailChannel_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 maildto.MailNotificationChannelRequest + if args[1] != nil { + arg1 = args[1].(maildto.MailNotificationChannelRequest) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MailChannelService_CreateMailChannel_Call) Return(mailNotificationChannelResponse maildto.MailNotificationChannelResponse, err error) *MailChannelService_CreateMailChannel_Call { + _c.Call.Return(mailNotificationChannelResponse, err) + return _c +} + +func (_c *MailChannelService_CreateMailChannel_Call) RunAndReturn(run func(c context.Context, channel maildto.MailNotificationChannelRequest) (maildto.MailNotificationChannelResponse, error)) *MailChannelService_CreateMailChannel_Call { + _c.Call.Return(run) + return _c +} diff --git a/pkg/services/notificationchannelservice/mocks/MattermostChannelService.go b/pkg/services/notificationchannelservice/mocks/MattermostChannelService.go new file mode 100644 index 0000000..765a1b8 --- /dev/null +++ b/pkg/services/notificationchannelservice/mocks/MattermostChannelService.go @@ -0,0 +1,156 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package mocks + +import ( + "context" + + "github.com/greenbone/opensight-notification-service/pkg/web/mattermostcontroller/mattermostdto" + mock "github.com/stretchr/testify/mock" +) + +// NewMattermostChannelService creates a new instance of MattermostChannelService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMattermostChannelService(t interface { + mock.TestingT + Cleanup(func()) +}) *MattermostChannelService { + mock := &MattermostChannelService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MattermostChannelService is an autogenerated mock type for the MattermostChannelService type +type MattermostChannelService struct { + mock.Mock +} + +type MattermostChannelService_Expecter struct { + mock *mock.Mock +} + +func (_m *MattermostChannelService) EXPECT() *MattermostChannelService_Expecter { + return &MattermostChannelService_Expecter{mock: &_m.Mock} +} + +// CreateMattermostChannel provides a mock function for the type MattermostChannelService +func (_mock *MattermostChannelService) CreateMattermostChannel(c context.Context, channel mattermostdto.MattermostNotificationChannelRequest) (mattermostdto.MattermostNotificationChannelResponse, error) { + ret := _mock.Called(c, channel) + + if len(ret) == 0 { + panic("no return value specified for CreateMattermostChannel") + } + + var r0 mattermostdto.MattermostNotificationChannelResponse + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, mattermostdto.MattermostNotificationChannelRequest) (mattermostdto.MattermostNotificationChannelResponse, error)); ok { + return returnFunc(c, channel) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, mattermostdto.MattermostNotificationChannelRequest) mattermostdto.MattermostNotificationChannelResponse); ok { + r0 = returnFunc(c, channel) + } else { + r0 = ret.Get(0).(mattermostdto.MattermostNotificationChannelResponse) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, mattermostdto.MattermostNotificationChannelRequest) error); ok { + r1 = returnFunc(c, channel) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MattermostChannelService_CreateMattermostChannel_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateMattermostChannel' +type MattermostChannelService_CreateMattermostChannel_Call struct { + *mock.Call +} + +// CreateMattermostChannel is a helper method to define mock.On call +// - c context.Context +// - channel mattermostdto.MattermostNotificationChannelRequest +func (_e *MattermostChannelService_Expecter) CreateMattermostChannel(c interface{}, channel interface{}) *MattermostChannelService_CreateMattermostChannel_Call { + return &MattermostChannelService_CreateMattermostChannel_Call{Call: _e.mock.On("CreateMattermostChannel", c, channel)} +} + +func (_c *MattermostChannelService_CreateMattermostChannel_Call) Run(run func(c context.Context, channel mattermostdto.MattermostNotificationChannelRequest)) *MattermostChannelService_CreateMattermostChannel_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 mattermostdto.MattermostNotificationChannelRequest + if args[1] != nil { + arg1 = args[1].(mattermostdto.MattermostNotificationChannelRequest) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MattermostChannelService_CreateMattermostChannel_Call) Return(mattermostNotificationChannelResponse mattermostdto.MattermostNotificationChannelResponse, err error) *MattermostChannelService_CreateMattermostChannel_Call { + _c.Call.Return(mattermostNotificationChannelResponse, err) + return _c +} + +func (_c *MattermostChannelService_CreateMattermostChannel_Call) RunAndReturn(run func(c context.Context, channel mattermostdto.MattermostNotificationChannelRequest) (mattermostdto.MattermostNotificationChannelResponse, error)) *MattermostChannelService_CreateMattermostChannel_Call { + _c.Call.Return(run) + return _c +} + +// SendMattermostTestMessage provides a mock function for the type MattermostChannelService +func (_mock *MattermostChannelService) SendMattermostTestMessage(webhookUrl string) error { + ret := _mock.Called(webhookUrl) + + if len(ret) == 0 { + panic("no return value specified for SendMattermostTestMessage") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(string) error); ok { + r0 = returnFunc(webhookUrl) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MattermostChannelService_SendMattermostTestMessage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendMattermostTestMessage' +type MattermostChannelService_SendMattermostTestMessage_Call struct { + *mock.Call +} + +// SendMattermostTestMessage is a helper method to define mock.On call +// - webhookUrl string +func (_e *MattermostChannelService_Expecter) SendMattermostTestMessage(webhookUrl interface{}) *MattermostChannelService_SendMattermostTestMessage_Call { + return &MattermostChannelService_SendMattermostTestMessage_Call{Call: _e.mock.On("SendMattermostTestMessage", webhookUrl)} +} + +func (_c *MattermostChannelService_SendMattermostTestMessage_Call) Run(run func(webhookUrl string)) *MattermostChannelService_SendMattermostTestMessage_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *MattermostChannelService_SendMattermostTestMessage_Call) Return(err error) *MattermostChannelService_SendMattermostTestMessage_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MattermostChannelService_SendMattermostTestMessage_Call) RunAndReturn(run func(webhookUrl string) error) *MattermostChannelService_SendMattermostTestMessage_Call { + _c.Call.Return(run) + return _c +} diff --git a/pkg/port/mocks/NotificationChannelService.go b/pkg/services/notificationchannelservice/mocks/NotificationChannelService.go similarity index 74% rename from pkg/port/mocks/NotificationChannelService.go rename to pkg/services/notificationchannelservice/mocks/NotificationChannelService.go index ae2bb0e..b482993 100644 --- a/pkg/port/mocks/NotificationChannelService.go +++ b/pkg/services/notificationchannelservice/mocks/NotificationChannelService.go @@ -38,126 +38,6 @@ func (_m *NotificationChannelService) EXPECT() *NotificationChannelService_Expec return &NotificationChannelService_Expecter{mock: &_m.Mock} } -// CheckNotificationChannelConnectivity provides a mock function for the type NotificationChannelService -func (_mock *NotificationChannelService) CheckNotificationChannelConnectivity(ctx context.Context, channel models.NotificationChannel) error { - ret := _mock.Called(ctx, channel) - - if len(ret) == 0 { - panic("no return value specified for CheckNotificationChannelConnectivity") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context, models.NotificationChannel) error); ok { - r0 = returnFunc(ctx, channel) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// NotificationChannelService_CheckNotificationChannelConnectivity_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CheckNotificationChannelConnectivity' -type NotificationChannelService_CheckNotificationChannelConnectivity_Call struct { - *mock.Call -} - -// CheckNotificationChannelConnectivity is a helper method to define mock.On call -// - ctx context.Context -// - channel models.NotificationChannel -func (_e *NotificationChannelService_Expecter) CheckNotificationChannelConnectivity(ctx interface{}, channel interface{}) *NotificationChannelService_CheckNotificationChannelConnectivity_Call { - return &NotificationChannelService_CheckNotificationChannelConnectivity_Call{Call: _e.mock.On("CheckNotificationChannelConnectivity", ctx, channel)} -} - -func (_c *NotificationChannelService_CheckNotificationChannelConnectivity_Call) Run(run func(ctx context.Context, channel models.NotificationChannel)) *NotificationChannelService_CheckNotificationChannelConnectivity_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 models.NotificationChannel - if args[1] != nil { - arg1 = args[1].(models.NotificationChannel) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *NotificationChannelService_CheckNotificationChannelConnectivity_Call) Return(err error) *NotificationChannelService_CheckNotificationChannelConnectivity_Call { - _c.Call.Return(err) - return _c -} - -func (_c *NotificationChannelService_CheckNotificationChannelConnectivity_Call) RunAndReturn(run func(ctx context.Context, channel models.NotificationChannel) error) *NotificationChannelService_CheckNotificationChannelConnectivity_Call { - _c.Call.Return(run) - return _c -} - -// CheckNotificationChannelEntityConnectivity provides a mock function for the type NotificationChannelService -func (_mock *NotificationChannelService) CheckNotificationChannelEntityConnectivity(ctx context.Context, id string, channel models.NotificationChannel) error { - ret := _mock.Called(ctx, id, channel) - - if len(ret) == 0 { - panic("no return value specified for CheckNotificationChannelEntityConnectivity") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context, string, models.NotificationChannel) error); ok { - r0 = returnFunc(ctx, id, channel) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// NotificationChannelService_CheckNotificationChannelEntityConnectivity_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CheckNotificationChannelEntityConnectivity' -type NotificationChannelService_CheckNotificationChannelEntityConnectivity_Call struct { - *mock.Call -} - -// CheckNotificationChannelEntityConnectivity is a helper method to define mock.On call -// - ctx context.Context -// - id string -// - channel models.NotificationChannel -func (_e *NotificationChannelService_Expecter) CheckNotificationChannelEntityConnectivity(ctx interface{}, id interface{}, channel interface{}) *NotificationChannelService_CheckNotificationChannelEntityConnectivity_Call { - return &NotificationChannelService_CheckNotificationChannelEntityConnectivity_Call{Call: _e.mock.On("CheckNotificationChannelEntityConnectivity", ctx, id, channel)} -} - -func (_c *NotificationChannelService_CheckNotificationChannelEntityConnectivity_Call) Run(run func(ctx context.Context, id string, channel models.NotificationChannel)) *NotificationChannelService_CheckNotificationChannelEntityConnectivity_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 string - if args[1] != nil { - arg1 = args[1].(string) - } - var arg2 models.NotificationChannel - if args[2] != nil { - arg2 = args[2].(models.NotificationChannel) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *NotificationChannelService_CheckNotificationChannelEntityConnectivity_Call) Return(err error) *NotificationChannelService_CheckNotificationChannelEntityConnectivity_Call { - _c.Call.Return(err) - return _c -} - -func (_c *NotificationChannelService_CheckNotificationChannelEntityConnectivity_Call) RunAndReturn(run func(ctx context.Context, id string, channel models.NotificationChannel) error) *NotificationChannelService_CheckNotificationChannelEntityConnectivity_Call { - _c.Call.Return(run) - return _c -} - // CreateNotificationChannel provides a mock function for the type NotificationChannelService func (_mock *NotificationChannelService) CreateNotificationChannel(ctx context.Context, channelIn models.NotificationChannel) (models.NotificationChannel, error) { ret := _mock.Called(ctx, channelIn) diff --git a/pkg/services/notificationchannelservice/mocks/NotificationChannelServicer.go b/pkg/services/notificationchannelservice/mocks/NotificationChannelServicer.go deleted file mode 100644 index 9f913c0..0000000 --- a/pkg/services/notificationchannelservice/mocks/NotificationChannelServicer.go +++ /dev/null @@ -1,494 +0,0 @@ -// Code generated by mockery; DO NOT EDIT. -// github.com/vektra/mockery -// template: testify - -package mocks - -import ( - "context" - - "github.com/greenbone/opensight-notification-service/pkg/models" - mock "github.com/stretchr/testify/mock" -) - -// NewNotificationChannelServicer creates a new instance of NotificationChannelServicer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewNotificationChannelServicer(t interface { - mock.TestingT - Cleanup(func()) -}) *NotificationChannelServicer { - mock := &NotificationChannelServicer{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} - -// NotificationChannelServicer is an autogenerated mock type for the NotificationChannelServicer type -type NotificationChannelServicer struct { - mock.Mock -} - -type NotificationChannelServicer_Expecter struct { - mock *mock.Mock -} - -func (_m *NotificationChannelServicer) EXPECT() *NotificationChannelServicer_Expecter { - return &NotificationChannelServicer_Expecter{mock: &_m.Mock} -} - -// CheckNotificationChannelConnectivity provides a mock function for the type NotificationChannelServicer -func (_mock *NotificationChannelServicer) CheckNotificationChannelConnectivity(ctx context.Context, channel models.NotificationChannel) error { - ret := _mock.Called(ctx, channel) - - if len(ret) == 0 { - panic("no return value specified for CheckNotificationChannelConnectivity") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context, models.NotificationChannel) error); ok { - r0 = returnFunc(ctx, channel) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// NotificationChannelServicer_CheckNotificationChannelConnectivity_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CheckNotificationChannelConnectivity' -type NotificationChannelServicer_CheckNotificationChannelConnectivity_Call struct { - *mock.Call -} - -// CheckNotificationChannelConnectivity is a helper method to define mock.On call -// - ctx context.Context -// - channel models.NotificationChannel -func (_e *NotificationChannelServicer_Expecter) CheckNotificationChannelConnectivity(ctx interface{}, channel interface{}) *NotificationChannelServicer_CheckNotificationChannelConnectivity_Call { - return &NotificationChannelServicer_CheckNotificationChannelConnectivity_Call{Call: _e.mock.On("CheckNotificationChannelConnectivity", ctx, channel)} -} - -func (_c *NotificationChannelServicer_CheckNotificationChannelConnectivity_Call) Run(run func(ctx context.Context, channel models.NotificationChannel)) *NotificationChannelServicer_CheckNotificationChannelConnectivity_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 models.NotificationChannel - if args[1] != nil { - arg1 = args[1].(models.NotificationChannel) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *NotificationChannelServicer_CheckNotificationChannelConnectivity_Call) Return(err error) *NotificationChannelServicer_CheckNotificationChannelConnectivity_Call { - _c.Call.Return(err) - return _c -} - -func (_c *NotificationChannelServicer_CheckNotificationChannelConnectivity_Call) RunAndReturn(run func(ctx context.Context, channel models.NotificationChannel) error) *NotificationChannelServicer_CheckNotificationChannelConnectivity_Call { - _c.Call.Return(run) - return _c -} - -// CheckNotificationChannelEntityConnectivity provides a mock function for the type NotificationChannelServicer -func (_mock *NotificationChannelServicer) CheckNotificationChannelEntityConnectivity(ctx context.Context, id string, channel models.NotificationChannel) error { - ret := _mock.Called(ctx, id, channel) - - if len(ret) == 0 { - panic("no return value specified for CheckNotificationChannelEntityConnectivity") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context, string, models.NotificationChannel) error); ok { - r0 = returnFunc(ctx, id, channel) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// NotificationChannelServicer_CheckNotificationChannelEntityConnectivity_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CheckNotificationChannelEntityConnectivity' -type NotificationChannelServicer_CheckNotificationChannelEntityConnectivity_Call struct { - *mock.Call -} - -// CheckNotificationChannelEntityConnectivity is a helper method to define mock.On call -// - ctx context.Context -// - id string -// - channel models.NotificationChannel -func (_e *NotificationChannelServicer_Expecter) CheckNotificationChannelEntityConnectivity(ctx interface{}, id interface{}, channel interface{}) *NotificationChannelServicer_CheckNotificationChannelEntityConnectivity_Call { - return &NotificationChannelServicer_CheckNotificationChannelEntityConnectivity_Call{Call: _e.mock.On("CheckNotificationChannelEntityConnectivity", ctx, id, channel)} -} - -func (_c *NotificationChannelServicer_CheckNotificationChannelEntityConnectivity_Call) Run(run func(ctx context.Context, id string, channel models.NotificationChannel)) *NotificationChannelServicer_CheckNotificationChannelEntityConnectivity_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 string - if args[1] != nil { - arg1 = args[1].(string) - } - var arg2 models.NotificationChannel - if args[2] != nil { - arg2 = args[2].(models.NotificationChannel) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *NotificationChannelServicer_CheckNotificationChannelEntityConnectivity_Call) Return(err error) *NotificationChannelServicer_CheckNotificationChannelEntityConnectivity_Call { - _c.Call.Return(err) - return _c -} - -func (_c *NotificationChannelServicer_CheckNotificationChannelEntityConnectivity_Call) RunAndReturn(run func(ctx context.Context, id string, channel models.NotificationChannel) error) *NotificationChannelServicer_CheckNotificationChannelEntityConnectivity_Call { - _c.Call.Return(run) - return _c -} - -// CreateNotificationChannel provides a mock function for the type NotificationChannelServicer -func (_mock *NotificationChannelServicer) CreateNotificationChannel(ctx context.Context, req models.NotificationChannel) (models.NotificationChannel, error) { - ret := _mock.Called(ctx, req) - - if len(ret) == 0 { - panic("no return value specified for CreateNotificationChannel") - } - - var r0 models.NotificationChannel - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, models.NotificationChannel) (models.NotificationChannel, error)); ok { - return returnFunc(ctx, req) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, models.NotificationChannel) models.NotificationChannel); ok { - r0 = returnFunc(ctx, req) - } else { - r0 = ret.Get(0).(models.NotificationChannel) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, models.NotificationChannel) error); ok { - r1 = returnFunc(ctx, req) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// NotificationChannelServicer_CreateNotificationChannel_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateNotificationChannel' -type NotificationChannelServicer_CreateNotificationChannel_Call struct { - *mock.Call -} - -// CreateNotificationChannel is a helper method to define mock.On call -// - ctx context.Context -// - req models.NotificationChannel -func (_e *NotificationChannelServicer_Expecter) CreateNotificationChannel(ctx interface{}, req interface{}) *NotificationChannelServicer_CreateNotificationChannel_Call { - return &NotificationChannelServicer_CreateNotificationChannel_Call{Call: _e.mock.On("CreateNotificationChannel", ctx, req)} -} - -func (_c *NotificationChannelServicer_CreateNotificationChannel_Call) Run(run func(ctx context.Context, req models.NotificationChannel)) *NotificationChannelServicer_CreateNotificationChannel_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 models.NotificationChannel - if args[1] != nil { - arg1 = args[1].(models.NotificationChannel) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *NotificationChannelServicer_CreateNotificationChannel_Call) Return(notificationChannel models.NotificationChannel, err error) *NotificationChannelServicer_CreateNotificationChannel_Call { - _c.Call.Return(notificationChannel, err) - return _c -} - -func (_c *NotificationChannelServicer_CreateNotificationChannel_Call) RunAndReturn(run func(ctx context.Context, req models.NotificationChannel) (models.NotificationChannel, error)) *NotificationChannelServicer_CreateNotificationChannel_Call { - _c.Call.Return(run) - return _c -} - -// DeleteNotificationChannel provides a mock function for the type NotificationChannelServicer -func (_mock *NotificationChannelServicer) DeleteNotificationChannel(ctx context.Context, id string) error { - ret := _mock.Called(ctx, id) - - if len(ret) == 0 { - panic("no return value specified for DeleteNotificationChannel") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok { - r0 = returnFunc(ctx, id) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// NotificationChannelServicer_DeleteNotificationChannel_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteNotificationChannel' -type NotificationChannelServicer_DeleteNotificationChannel_Call struct { - *mock.Call -} - -// DeleteNotificationChannel is a helper method to define mock.On call -// - ctx context.Context -// - id string -func (_e *NotificationChannelServicer_Expecter) DeleteNotificationChannel(ctx interface{}, id interface{}) *NotificationChannelServicer_DeleteNotificationChannel_Call { - return &NotificationChannelServicer_DeleteNotificationChannel_Call{Call: _e.mock.On("DeleteNotificationChannel", ctx, id)} -} - -func (_c *NotificationChannelServicer_DeleteNotificationChannel_Call) Run(run func(ctx context.Context, id string)) *NotificationChannelServicer_DeleteNotificationChannel_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 string - if args[1] != nil { - arg1 = args[1].(string) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *NotificationChannelServicer_DeleteNotificationChannel_Call) Return(err error) *NotificationChannelServicer_DeleteNotificationChannel_Call { - _c.Call.Return(err) - return _c -} - -func (_c *NotificationChannelServicer_DeleteNotificationChannel_Call) RunAndReturn(run func(ctx context.Context, id string) error) *NotificationChannelServicer_DeleteNotificationChannel_Call { - _c.Call.Return(run) - return _c -} - -// GetNotificationChannelByIdAndType provides a mock function for the type NotificationChannelServicer -func (_mock *NotificationChannelServicer) GetNotificationChannelByIdAndType(ctx context.Context, id string, channelType models.ChannelType) (models.NotificationChannel, error) { - ret := _mock.Called(ctx, id, channelType) - - if len(ret) == 0 { - panic("no return value specified for GetNotificationChannelByIdAndType") - } - - var r0 models.NotificationChannel - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, string, models.ChannelType) (models.NotificationChannel, error)); ok { - return returnFunc(ctx, id, channelType) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, string, models.ChannelType) models.NotificationChannel); ok { - r0 = returnFunc(ctx, id, channelType) - } else { - r0 = ret.Get(0).(models.NotificationChannel) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, string, models.ChannelType) error); ok { - r1 = returnFunc(ctx, id, channelType) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// NotificationChannelServicer_GetNotificationChannelByIdAndType_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetNotificationChannelByIdAndType' -type NotificationChannelServicer_GetNotificationChannelByIdAndType_Call struct { - *mock.Call -} - -// GetNotificationChannelByIdAndType is a helper method to define mock.On call -// - ctx context.Context -// - id string -// - channelType models.ChannelType -func (_e *NotificationChannelServicer_Expecter) GetNotificationChannelByIdAndType(ctx interface{}, id interface{}, channelType interface{}) *NotificationChannelServicer_GetNotificationChannelByIdAndType_Call { - return &NotificationChannelServicer_GetNotificationChannelByIdAndType_Call{Call: _e.mock.On("GetNotificationChannelByIdAndType", ctx, id, channelType)} -} - -func (_c *NotificationChannelServicer_GetNotificationChannelByIdAndType_Call) Run(run func(ctx context.Context, id string, channelType models.ChannelType)) *NotificationChannelServicer_GetNotificationChannelByIdAndType_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 string - if args[1] != nil { - arg1 = args[1].(string) - } - var arg2 models.ChannelType - if args[2] != nil { - arg2 = args[2].(models.ChannelType) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *NotificationChannelServicer_GetNotificationChannelByIdAndType_Call) Return(notificationChannel models.NotificationChannel, err error) *NotificationChannelServicer_GetNotificationChannelByIdAndType_Call { - _c.Call.Return(notificationChannel, err) - return _c -} - -func (_c *NotificationChannelServicer_GetNotificationChannelByIdAndType_Call) RunAndReturn(run func(ctx context.Context, id string, channelType models.ChannelType) (models.NotificationChannel, error)) *NotificationChannelServicer_GetNotificationChannelByIdAndType_Call { - _c.Call.Return(run) - return _c -} - -// ListNotificationChannelsByType provides a mock function for the type NotificationChannelServicer -func (_mock *NotificationChannelServicer) ListNotificationChannelsByType(ctx context.Context, channelType models.ChannelType) ([]models.NotificationChannel, error) { - ret := _mock.Called(ctx, channelType) - - if len(ret) == 0 { - panic("no return value specified for ListNotificationChannelsByType") - } - - var r0 []models.NotificationChannel - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, models.ChannelType) ([]models.NotificationChannel, error)); ok { - return returnFunc(ctx, channelType) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, models.ChannelType) []models.NotificationChannel); ok { - r0 = returnFunc(ctx, channelType) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]models.NotificationChannel) - } - } - if returnFunc, ok := ret.Get(1).(func(context.Context, models.ChannelType) error); ok { - r1 = returnFunc(ctx, channelType) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// NotificationChannelServicer_ListNotificationChannelsByType_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListNotificationChannelsByType' -type NotificationChannelServicer_ListNotificationChannelsByType_Call struct { - *mock.Call -} - -// ListNotificationChannelsByType is a helper method to define mock.On call -// - ctx context.Context -// - channelType models.ChannelType -func (_e *NotificationChannelServicer_Expecter) ListNotificationChannelsByType(ctx interface{}, channelType interface{}) *NotificationChannelServicer_ListNotificationChannelsByType_Call { - return &NotificationChannelServicer_ListNotificationChannelsByType_Call{Call: _e.mock.On("ListNotificationChannelsByType", ctx, channelType)} -} - -func (_c *NotificationChannelServicer_ListNotificationChannelsByType_Call) Run(run func(ctx context.Context, channelType models.ChannelType)) *NotificationChannelServicer_ListNotificationChannelsByType_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 models.ChannelType - if args[1] != nil { - arg1 = args[1].(models.ChannelType) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *NotificationChannelServicer_ListNotificationChannelsByType_Call) Return(notificationChannels []models.NotificationChannel, err error) *NotificationChannelServicer_ListNotificationChannelsByType_Call { - _c.Call.Return(notificationChannels, err) - return _c -} - -func (_c *NotificationChannelServicer_ListNotificationChannelsByType_Call) RunAndReturn(run func(ctx context.Context, channelType models.ChannelType) ([]models.NotificationChannel, error)) *NotificationChannelServicer_ListNotificationChannelsByType_Call { - _c.Call.Return(run) - return _c -} - -// UpdateNotificationChannel provides a mock function for the type NotificationChannelServicer -func (_mock *NotificationChannelServicer) UpdateNotificationChannel(ctx context.Context, id string, req models.NotificationChannel) (models.NotificationChannel, error) { - ret := _mock.Called(ctx, id, req) - - if len(ret) == 0 { - panic("no return value specified for UpdateNotificationChannel") - } - - var r0 models.NotificationChannel - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, string, models.NotificationChannel) (models.NotificationChannel, error)); ok { - return returnFunc(ctx, id, req) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, string, models.NotificationChannel) models.NotificationChannel); ok { - r0 = returnFunc(ctx, id, req) - } else { - r0 = ret.Get(0).(models.NotificationChannel) - } - if returnFunc, ok := ret.Get(1).(func(context.Context, string, models.NotificationChannel) error); ok { - r1 = returnFunc(ctx, id, req) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// NotificationChannelServicer_UpdateNotificationChannel_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateNotificationChannel' -type NotificationChannelServicer_UpdateNotificationChannel_Call struct { - *mock.Call -} - -// UpdateNotificationChannel is a helper method to define mock.On call -// - ctx context.Context -// - id string -// - req models.NotificationChannel -func (_e *NotificationChannelServicer_Expecter) UpdateNotificationChannel(ctx interface{}, id interface{}, req interface{}) *NotificationChannelServicer_UpdateNotificationChannel_Call { - return &NotificationChannelServicer_UpdateNotificationChannel_Call{Call: _e.mock.On("UpdateNotificationChannel", ctx, id, req)} -} - -func (_c *NotificationChannelServicer_UpdateNotificationChannel_Call) Run(run func(ctx context.Context, id string, req models.NotificationChannel)) *NotificationChannelServicer_UpdateNotificationChannel_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 string - if args[1] != nil { - arg1 = args[1].(string) - } - var arg2 models.NotificationChannel - if args[2] != nil { - arg2 = args[2].(models.NotificationChannel) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *NotificationChannelServicer_UpdateNotificationChannel_Call) Return(notificationChannel models.NotificationChannel, err error) *NotificationChannelServicer_UpdateNotificationChannel_Call { - _c.Call.Return(notificationChannel, err) - return _c -} - -func (_c *NotificationChannelServicer_UpdateNotificationChannel_Call) RunAndReturn(run func(ctx context.Context, id string, req models.NotificationChannel) (models.NotificationChannel, error)) *NotificationChannelServicer_UpdateNotificationChannel_Call { - _c.Call.Return(run) - return _c -} diff --git a/pkg/services/notificationchannelservice/mocks/TeamsChannelService.go b/pkg/services/notificationchannelservice/mocks/TeamsChannelService.go new file mode 100644 index 0000000..88feb12 --- /dev/null +++ b/pkg/services/notificationchannelservice/mocks/TeamsChannelService.go @@ -0,0 +1,156 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package mocks + +import ( + "context" + + "github.com/greenbone/opensight-notification-service/pkg/web/teamsController/teamsdto" + mock "github.com/stretchr/testify/mock" +) + +// NewTeamsChannelService creates a new instance of TeamsChannelService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewTeamsChannelService(t interface { + mock.TestingT + Cleanup(func()) +}) *TeamsChannelService { + mock := &TeamsChannelService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// TeamsChannelService is an autogenerated mock type for the TeamsChannelService type +type TeamsChannelService struct { + mock.Mock +} + +type TeamsChannelService_Expecter struct { + mock *mock.Mock +} + +func (_m *TeamsChannelService) EXPECT() *TeamsChannelService_Expecter { + return &TeamsChannelService_Expecter{mock: &_m.Mock} +} + +// CreateTeamsChannel provides a mock function for the type TeamsChannelService +func (_mock *TeamsChannelService) CreateTeamsChannel(c context.Context, channel teamsdto.TeamsNotificationChannelRequest) (teamsdto.TeamsNotificationChannelResponse, error) { + ret := _mock.Called(c, channel) + + if len(ret) == 0 { + panic("no return value specified for CreateTeamsChannel") + } + + var r0 teamsdto.TeamsNotificationChannelResponse + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, teamsdto.TeamsNotificationChannelRequest) (teamsdto.TeamsNotificationChannelResponse, error)); ok { + return returnFunc(c, channel) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, teamsdto.TeamsNotificationChannelRequest) teamsdto.TeamsNotificationChannelResponse); ok { + r0 = returnFunc(c, channel) + } else { + r0 = ret.Get(0).(teamsdto.TeamsNotificationChannelResponse) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, teamsdto.TeamsNotificationChannelRequest) error); ok { + r1 = returnFunc(c, channel) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// TeamsChannelService_CreateTeamsChannel_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateTeamsChannel' +type TeamsChannelService_CreateTeamsChannel_Call struct { + *mock.Call +} + +// CreateTeamsChannel is a helper method to define mock.On call +// - c context.Context +// - channel teamsdto.TeamsNotificationChannelRequest +func (_e *TeamsChannelService_Expecter) CreateTeamsChannel(c interface{}, channel interface{}) *TeamsChannelService_CreateTeamsChannel_Call { + return &TeamsChannelService_CreateTeamsChannel_Call{Call: _e.mock.On("CreateTeamsChannel", c, channel)} +} + +func (_c *TeamsChannelService_CreateTeamsChannel_Call) Run(run func(c context.Context, channel teamsdto.TeamsNotificationChannelRequest)) *TeamsChannelService_CreateTeamsChannel_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 teamsdto.TeamsNotificationChannelRequest + if args[1] != nil { + arg1 = args[1].(teamsdto.TeamsNotificationChannelRequest) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *TeamsChannelService_CreateTeamsChannel_Call) Return(teamsNotificationChannelResponse teamsdto.TeamsNotificationChannelResponse, err error) *TeamsChannelService_CreateTeamsChannel_Call { + _c.Call.Return(teamsNotificationChannelResponse, err) + return _c +} + +func (_c *TeamsChannelService_CreateTeamsChannel_Call) RunAndReturn(run func(c context.Context, channel teamsdto.TeamsNotificationChannelRequest) (teamsdto.TeamsNotificationChannelResponse, error)) *TeamsChannelService_CreateTeamsChannel_Call { + _c.Call.Return(run) + return _c +} + +// SendTeamsTestMessage provides a mock function for the type TeamsChannelService +func (_mock *TeamsChannelService) SendTeamsTestMessage(webhookUrl string) error { + ret := _mock.Called(webhookUrl) + + if len(ret) == 0 { + panic("no return value specified for SendTeamsTestMessage") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(string) error); ok { + r0 = returnFunc(webhookUrl) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// TeamsChannelService_SendTeamsTestMessage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendTeamsTestMessage' +type TeamsChannelService_SendTeamsTestMessage_Call struct { + *mock.Call +} + +// SendTeamsTestMessage is a helper method to define mock.On call +// - webhookUrl string +func (_e *TeamsChannelService_Expecter) SendTeamsTestMessage(webhookUrl interface{}) *TeamsChannelService_SendTeamsTestMessage_Call { + return &TeamsChannelService_SendTeamsTestMessage_Call{Call: _e.mock.On("SendTeamsTestMessage", webhookUrl)} +} + +func (_c *TeamsChannelService_SendTeamsTestMessage_Call) Run(run func(webhookUrl string)) *TeamsChannelService_SendTeamsTestMessage_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *TeamsChannelService_SendTeamsTestMessage_Call) Return(err error) *TeamsChannelService_SendTeamsTestMessage_Call { + _c.Call.Return(err) + return _c +} + +func (_c *TeamsChannelService_SendTeamsTestMessage_Call) RunAndReturn(run func(webhookUrl string) error) *TeamsChannelService_SendTeamsTestMessage_Call { + _c.Call.Return(run) + return _c +} diff --git a/pkg/services/notificationchannelservice/notificationChannelService.go b/pkg/services/notificationchannelservice/notificationChannelService.go index e7e9bef..573ac2f 100644 --- a/pkg/services/notificationchannelservice/notificationChannelService.go +++ b/pkg/services/notificationchannelservice/notificationChannelService.go @@ -4,46 +4,45 @@ import ( "context" "github.com/greenbone/opensight-notification-service/pkg/models" - "github.com/greenbone/opensight-notification-service/pkg/port" + "github.com/greenbone/opensight-notification-service/pkg/repository/notificationrepository" ) -type NotificationChannelServicer interface { - CreateNotificationChannel(ctx context.Context, req models.NotificationChannel) (models.NotificationChannel, error) +type NotificationChannelService interface { + CreateNotificationChannel( + ctx context.Context, + channelIn models.NotificationChannel, + ) (models.NotificationChannel, error) GetNotificationChannelByIdAndType( ctx context.Context, id string, channelType models.ChannelType, ) (models.NotificationChannel, error) - ListNotificationChannelsByType( - ctx context.Context, - channelType models.ChannelType, - ) ([]models.NotificationChannel, error) UpdateNotificationChannel( ctx context.Context, id string, - req models.NotificationChannel, + channelIn models.NotificationChannel, ) (models.NotificationChannel, error) DeleteNotificationChannel(ctx context.Context, id string) error - CheckNotificationChannelConnectivity(ctx context.Context, channel models.NotificationChannel) error - CheckNotificationChannelEntityConnectivity( + ListNotificationChannelsByType( ctx context.Context, - id string, - channel models.NotificationChannel, - ) error + channelType models.ChannelType, + ) ([]models.NotificationChannel, error) } - -type NotificationChannelService struct { - store port.NotificationChannelRepository +type notificationChannelService struct { + store notificationrepository.NotificationChannelRepository } -func NewNotificationChannelService(store port.NotificationChannelRepository) *NotificationChannelService { - return &NotificationChannelService{store: store} +func NewNotificationChannelService(store notificationrepository.NotificationChannelRepository) NotificationChannelService { + return ¬ificationChannelService{ + store: store, + } } -func (s *NotificationChannelService) CreateNotificationChannel( +func (s *notificationChannelService) CreateNotificationChannel( ctx context.Context, channelIn models.NotificationChannel, ) (models.NotificationChannel, error) { + notificationChannel, err := s.store.CreateNotificationChannel(ctx, channelIn) if err != nil { return models.NotificationChannel{}, err @@ -52,21 +51,21 @@ func (s *NotificationChannelService) CreateNotificationChannel( return notificationChannel, nil } -func (s *NotificationChannelService) GetNotificationChannelByIdAndType( +func (s *notificationChannelService) GetNotificationChannelByIdAndType( ctx context.Context, id string, channelType models.ChannelType, ) (models.NotificationChannel, error) { return s.store.GetNotificationChannelByIdAndType(ctx, id, channelType) } -func (s *NotificationChannelService) ListNotificationChannelsByType( +func (s *notificationChannelService) ListNotificationChannelsByType( ctx context.Context, channelType models.ChannelType, ) ([]models.NotificationChannel, error) { return s.store.ListNotificationChannelsByType(ctx, channelType) } -func (s *NotificationChannelService) UpdateNotificationChannel( +func (s *notificationChannelService) UpdateNotificationChannel( ctx context.Context, id string, channelIn models.NotificationChannel, @@ -79,6 +78,6 @@ func (s *NotificationChannelService) UpdateNotificationChannel( return notificationChannel, nil } -func (s *NotificationChannelService) DeleteNotificationChannel(ctx context.Context, id string) error { +func (s *notificationChannelService) DeleteNotificationChannel(ctx context.Context, id string) error { return s.store.DeleteNotificationChannel(ctx, id) } diff --git a/pkg/services/notificationchannelservice/teamsChannelService.go b/pkg/services/notificationchannelservice/teamsChannelService.go new file mode 100644 index 0000000..8103062 --- /dev/null +++ b/pkg/services/notificationchannelservice/teamsChannelService.go @@ -0,0 +1,101 @@ +package notificationchannelservice + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + + "github.com/greenbone/opensight-notification-service/pkg/models" + "github.com/greenbone/opensight-notification-service/pkg/web/teamsController/teamsdto" +) + +var ( + ErrTeamsChannelLimitReached = errors.New("teams channel limit reached") + ErrListTeamsChannels = errors.New("failed to list teams channels") + ErrTeamsChannelNameExists = errors.New("teams channel name already exists") +) + +type TeamsChannelService interface { + SendTeamsTestMessage(webhookUrl string) error + CreateTeamsChannel( + c context.Context, + channel teamsdto.TeamsNotificationChannelRequest, + ) (teamsdto.TeamsNotificationChannelResponse, error) +} + +type teamsChannelService struct { + notificationChannelService NotificationChannelService + teamsChannelLimit int +} + +func NewTeamsChannelService( + notificationChannelService NotificationChannelService, + teamsChannelLimit int, +) TeamsChannelService { + return &teamsChannelService{ + notificationChannelService: notificationChannelService, + teamsChannelLimit: teamsChannelLimit, + } +} + +func (t *teamsChannelService) SendTeamsTestMessage(webhookUrl string) error { + body, err := json.Marshal(map[string]string{ + "text": "Hello This is a test message", + }) + if err != nil { + return err + } + + resp, err := http.Post(webhookUrl, "application/json", bytes.NewBuffer(body)) + if err != nil { + return err + } + defer func() { + _ = resp.Body.Close() + }() + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("failed to send test message to Teams webhook: %s", resp.Status) + } + + return nil +} + +func (t *teamsChannelService) CreateTeamsChannel( + c context.Context, + channel teamsdto.TeamsNotificationChannelRequest, +) (teamsdto.TeamsNotificationChannelResponse, error) { + if errResp := t.teamsChannelValidations(c, channel.ChannelName); errResp != nil { + return teamsdto.TeamsNotificationChannelResponse{}, errResp + } + + notificationChannel := teamsdto.MapTeamsToNotificationChannel(channel) + created, err := t.notificationChannelService.CreateNotificationChannel(c, notificationChannel) + if err != nil { + return teamsdto.TeamsNotificationChannelResponse{}, err + } + + return teamsdto.MapNotificationChannelToTeams(created), nil +} + +func (t *teamsChannelService) teamsChannelValidations(c context.Context, channelName string) error { + channels, err := t.notificationChannelService.ListNotificationChannelsByType(c, models.ChannelTypeTeams) + if err != nil { + return errors.Join(ErrListTeamsChannels, err) + } + + if len(channels) >= t.teamsChannelLimit { + return ErrTeamsChannelLimitReached + } + + for _, ch := range channels { + if ch.ChannelName != nil && *ch.ChannelName == channelName { + return ErrTeamsChannelNameExists + } + } + + return nil +} diff --git a/pkg/port/mocks/NotificationService.go b/pkg/services/notificationservice/mocks/NotificationService.go similarity index 100% rename from pkg/port/mocks/NotificationService.go rename to pkg/services/notificationservice/mocks/NotificationService.go diff --git a/pkg/services/notificationservice/notificationService.go b/pkg/services/notificationservice/notificationService.go index cae9402..fc2d74f 100644 --- a/pkg/services/notificationservice/notificationService.go +++ b/pkg/services/notificationservice/notificationService.go @@ -9,25 +9,36 @@ import ( "github.com/greenbone/opensight-golang-libraries/pkg/query" "github.com/greenbone/opensight-notification-service/pkg/models" - "github.com/greenbone/opensight-notification-service/pkg/port" + "github.com/greenbone/opensight-notification-service/pkg/repository/notificationrepository" ) -type NotificationService struct { - store port.NotificationRepository +type NotificationService interface { + ListNotifications( + ctx context.Context, + resultSelector query.ResultSelector, + ) (notifications []models.Notification, totalResult uint64, err error) + CreateNotification( + ctx context.Context, + notificationIn models.Notification, + ) (notification models.Notification, err error) } -func NewNotificationService(store port.NotificationRepository) *NotificationService { - return &NotificationService{store: store} +type notificationService struct { + store notificationrepository.NotificationRepository } -func (s *NotificationService) ListNotifications( +func NewNotificationService(store notificationrepository.NotificationRepository) NotificationService { + return ¬ificationService{store: store} +} + +func (s *notificationService) ListNotifications( ctx context.Context, resultSelector query.ResultSelector, ) (notifications []models.Notification, totalResult uint64, err error) { return s.store.ListNotifications(ctx, resultSelector) } -func (s *NotificationService) CreateNotification( +func (s notificationService) CreateNotification( ctx context.Context, notificationIn models.Notification, ) (notification models.Notification, err error) { diff --git a/pkg/web/errmap/mocks/ErrorRegistry.go b/pkg/web/errmap/mocks/ErrorRegistry.go new file mode 100644 index 0000000..f7d7cf3 --- /dev/null +++ b/pkg/web/errmap/mocks/ErrorRegistry.go @@ -0,0 +1,150 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package mocks + +import ( + "github.com/greenbone/opensight-golang-libraries/pkg/errorResponses" + "github.com/greenbone/opensight-notification-service/pkg/web/errmap" + mock "github.com/stretchr/testify/mock" +) + +// NewErrorRegistry creates a new instance of ErrorRegistry. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewErrorRegistry(t interface { + mock.TestingT + Cleanup(func()) +}) *ErrorRegistry { + mock := &ErrorRegistry{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// ErrorRegistry is an autogenerated mock type for the ErrorRegistry type +type ErrorRegistry struct { + mock.Mock +} + +type ErrorRegistry_Expecter struct { + mock *mock.Mock +} + +func (_m *ErrorRegistry) EXPECT() *ErrorRegistry_Expecter { + return &ErrorRegistry_Expecter{mock: &_m.Mock} +} + +// Lookup provides a mock function for the type ErrorRegistry +func (_mock *ErrorRegistry) Lookup(err error) (errmap.Result, bool) { + ret := _mock.Called(err) + + if len(ret) == 0 { + panic("no return value specified for Lookup") + } + + var r0 errmap.Result + var r1 bool + if returnFunc, ok := ret.Get(0).(func(error) (errmap.Result, bool)); ok { + return returnFunc(err) + } + if returnFunc, ok := ret.Get(0).(func(error) errmap.Result); ok { + r0 = returnFunc(err) + } else { + r0 = ret.Get(0).(errmap.Result) + } + if returnFunc, ok := ret.Get(1).(func(error) bool); ok { + r1 = returnFunc(err) + } else { + r1 = ret.Get(1).(bool) + } + return r0, r1 +} + +// ErrorRegistry_Lookup_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Lookup' +type ErrorRegistry_Lookup_Call struct { + *mock.Call +} + +// Lookup is a helper method to define mock.On call +// - err error +func (_e *ErrorRegistry_Expecter) Lookup(err interface{}) *ErrorRegistry_Lookup_Call { + return &ErrorRegistry_Lookup_Call{Call: _e.mock.On("Lookup", err)} +} + +func (_c *ErrorRegistry_Lookup_Call) Run(run func(err error)) *ErrorRegistry_Lookup_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 error + if args[0] != nil { + arg0 = args[0].(error) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *ErrorRegistry_Lookup_Call) Return(result errmap.Result, b bool) *ErrorRegistry_Lookup_Call { + _c.Call.Return(result, b) + return _c +} + +func (_c *ErrorRegistry_Lookup_Call) RunAndReturn(run func(err error) (errmap.Result, bool)) *ErrorRegistry_Lookup_Call { + _c.Call.Return(run) + return _c +} + +// Register provides a mock function for the type ErrorRegistry +func (_mock *ErrorRegistry) Register(err error, status int, response errorResponses.ErrorResponse) { + _mock.Called(err, status, response) + return +} + +// ErrorRegistry_Register_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Register' +type ErrorRegistry_Register_Call struct { + *mock.Call +} + +// Register is a helper method to define mock.On call +// - err error +// - status int +// - response errorResponses.ErrorResponse +func (_e *ErrorRegistry_Expecter) Register(err interface{}, status interface{}, response interface{}) *ErrorRegistry_Register_Call { + return &ErrorRegistry_Register_Call{Call: _e.mock.On("Register", err, status, response)} +} + +func (_c *ErrorRegistry_Register_Call) Run(run func(err error, status int, response errorResponses.ErrorResponse)) *ErrorRegistry_Register_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 error + if args[0] != nil { + arg0 = args[0].(error) + } + var arg1 int + if args[1] != nil { + arg1 = args[1].(int) + } + var arg2 errorResponses.ErrorResponse + if args[2] != nil { + arg2 = args[2].(errorResponses.ErrorResponse) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *ErrorRegistry_Register_Call) Return() *ErrorRegistry_Register_Call { + _c.Call.Return() + return _c +} + +func (_c *ErrorRegistry_Register_Call) RunAndReturn(run func(err error, status int, response errorResponses.ErrorResponse)) *ErrorRegistry_Register_Call { + _c.Run(run) + return _c +} diff --git a/pkg/web/errmap/registry.go b/pkg/web/errmap/registry.go new file mode 100644 index 0000000..50933f5 --- /dev/null +++ b/pkg/web/errmap/registry.go @@ -0,0 +1,43 @@ +package errmap + +import ( + "errors" + + "github.com/greenbone/opensight-golang-libraries/pkg/errorResponses" +) + +type ErrorRegistry interface { + Lookup(err error) (Result, bool) + Register(err error, status int, response errorResponses.ErrorResponse) +} +type Result struct { + Status int + Response errorResponses.ErrorResponse +} + +type Registry struct { + mappings map[error]Result +} + +func NewRegistry() *Registry { + return &Registry{ + mappings: make(map[error]Result), + } +} + +func (m *Registry) Register(err error, status int, response errorResponses.ErrorResponse) { + m.mappings[err] = Result{ + Status: status, + Response: response, + } +} + +func (m *Registry) Lookup(err error) (Result, bool) { + // Traversing is intentional not accidental. Wrapped errors may come. + for e, mapping := range m.mappings { + if errors.Is(err, e) { + return mapping, true + } + } + return Result{}, false +} diff --git a/pkg/web/errmap/registry_test.go b/pkg/web/errmap/registry_test.go new file mode 100644 index 0000000..cefcd7c --- /dev/null +++ b/pkg/web/errmap/registry_test.go @@ -0,0 +1,62 @@ +package errmap + +import ( + "errors" + "fmt" + "net/http" + "testing" + + "github.com/greenbone/opensight-golang-libraries/pkg/errorResponses" + "github.com/stretchr/testify/assert" +) + +func TestRegistry_Lookup(t *testing.T) { + reg := NewRegistry() + + errSentinel := errors.New("sentinel error") + errNotRegistered := errors.New("not registered") + + mockResponse := errorResponses.ErrorInternalResponse + reg.Register(errSentinel, http.StatusBadRequest, mockResponse) + + tests := []struct { + name string + inputErr error + wantFound bool + wantStatus int + }{ + { + name: "Direct match returns correctly", + inputErr: errSentinel, + wantFound: true, + wantStatus: http.StatusBadRequest, + }, + { + name: "Wrapped error match (using %w) returns correctly", + inputErr: fmt.Errorf("context: %w", errSentinel), + wantFound: true, + wantStatus: http.StatusBadRequest, + }, + { + name: "Unregistered error returns false", + inputErr: errNotRegistered, + wantFound: false, + }, + { + name: "Nil error returns false", + inputErr: nil, + wantFound: false, + }, + } + + // 3. Execution + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, found := reg.Lookup(tt.inputErr) + assert.Equal(t, tt.wantFound, found) + if tt.wantFound { + assert.Equal(t, tt.wantStatus, result.Status) + } + }) + } +} diff --git a/pkg/web/ginEx/binding.go b/pkg/web/ginEx/binding.go new file mode 100644 index 0000000..6a1b23a --- /dev/null +++ b/pkg/web/ginEx/binding.go @@ -0,0 +1,84 @@ +package ginEx + +import ( + "encoding/json" + "errors" + "io" + + "github.com/gin-gonic/gin" + "github.com/greenbone/opensight-notification-service/pkg/models" +) + +type BindingError struct { + message string +} + +func newBindingError(message string) BindingError { + return BindingError{message: message} +} + +func (e BindingError) Error() string { + return e.message +} + +// Validate can be implemented by a dto that is used in BindAndValidateBody for custom validation +type Validate interface { + Validate() models.ValidationErrors +} + +// Cleaner can be implemented by a dto that is used in BindAndValidateBody for clean up values before validate +type Cleaner interface { + Cleanup() +} + +func BindAndValidateBody(c *gin.Context, bodyDto any) bool { + err := c.ShouldBindJSON(bodyDto) + if err != nil { + if errors.Is(err, io.EOF) { + _ = c.Error(newBindingError("body can not be empty")) + return false + } else if errors.Is(err, io.ErrUnexpectedEOF) { + _ = c.Error(newBindingError("error parsing body")) + return false + } + + if isUnmarshallError(err) { + _ = c.Error(newBindingError("error unmarshalling body")) + return false + } + + _ = c.Error(err) + return false + } + + if value, ok := bodyDto.(Cleaner); ok { + value.Cleanup() + } + + if value, ok := bodyDto.(Validate); ok { + err := value.Validate() + if len(err) > 0 { + _ = c.Error(err) + return false + } + } + + return !c.IsAborted() +} + +func AddError(c *gin.Context, err error) (errorIsNotNil bool) { + if err == nil { + return false + } + + _ = c.Error(err) + return true +} + +func isUnmarshallError(err error) bool { + var syntaxErr *json.SyntaxError + var unmarshalErr *json.UnmarshalTypeError + var invalidUnmarshalErr *json.InvalidUnmarshalError + + return errors.As(err, &syntaxErr) || errors.As(err, &unmarshalErr) || errors.As(err, &invalidUnmarshalErr) +} diff --git a/pkg/web/ginEx/binding_test.go b/pkg/web/ginEx/binding_test.go new file mode 100644 index 0000000..4114524 --- /dev/null +++ b/pkg/web/ginEx/binding_test.go @@ -0,0 +1,89 @@ +package ginEx + +import ( + "bytes" + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/greenbone/opensight-notification-service/pkg/models" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBindBody_BindingErrors(t *testing.T) { + gin.SetMode(gin.TestMode) + + tests := []struct { + name string + requestBody string + expectedMsg string + checkErrorType bool + }{ + { + name: "Empty Body Returns BindingError", + requestBody: ``, + expectedMsg: "body can not be empty", + checkErrorType: true, + }, + { + name: "Unexpected EOF Returns BindingError", + requestBody: `{"name": "incomplete json`, + expectedMsg: "error parsing body", + checkErrorType: true, + }, + { + name: "Successful Bind without self-validation check", + requestBody: `{"name": "expert"}`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest(http.MethodPost, "/", bytes.NewBufferString(tt.requestBody)) + + var dto map[string]string + result := BindAndValidateBody(c, &dto) + if result && !tt.checkErrorType { + return + } + + lastErr := c.Errors.Last().Err + assert.Equal(t, tt.expectedMsg, lastErr.Error()) + + var bindingError BindingError + assert.True(t, errors.As(lastErr, &bindingError), "Error should be of type BindingError") + }) + } +} + +type sample struct { +} + +func (s sample) Validate() models.ValidationErrors { + return models.ValidationErrors{ + "a": "b", + } +} + +func TestBindBody_Validate(t *testing.T) { + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + c.Request = httptest.NewRequest( + http.MethodPost, + "/", + bytes.NewBufferString(`{}`), + ) + + var s sample + result := BindAndValidateBody(c, &s) + require.False(t, result) + + lastErr := c.Errors.Last().Err + assert.Contains(t, lastErr.Error(), "validation error") +} diff --git a/pkg/web/ginEx/mocks/Cleaner.go b/pkg/web/ginEx/mocks/Cleaner.go new file mode 100644 index 0000000..79566b7 --- /dev/null +++ b/pkg/web/ginEx/mocks/Cleaner.go @@ -0,0 +1,69 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package mocks + +import ( + mock "github.com/stretchr/testify/mock" +) + +// NewCleaner creates a new instance of Cleaner. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewCleaner(t interface { + mock.TestingT + Cleanup(func()) +}) *Cleaner { + mock := &Cleaner{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// Cleaner is an autogenerated mock type for the Cleaner type +type Cleaner struct { + mock.Mock +} + +type Cleaner_Expecter struct { + mock *mock.Mock +} + +func (_m *Cleaner) EXPECT() *Cleaner_Expecter { + return &Cleaner_Expecter{mock: &_m.Mock} +} + +// Cleanup provides a mock function for the type Cleaner +func (_mock *Cleaner) Cleanup() { + _mock.Called() + return +} + +// Cleaner_Cleanup_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Cleanup' +type Cleaner_Cleanup_Call struct { + *mock.Call +} + +// Cleanup is a helper method to define mock.On call +func (_e *Cleaner_Expecter) Cleanup() *Cleaner_Cleanup_Call { + return &Cleaner_Cleanup_Call{Call: _e.mock.On("Cleanup")} +} + +func (_c *Cleaner_Cleanup_Call) Run(run func()) *Cleaner_Cleanup_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Cleaner_Cleanup_Call) Return() *Cleaner_Cleanup_Call { + _c.Call.Return() + return _c +} + +func (_c *Cleaner_Cleanup_Call) RunAndReturn(run func()) *Cleaner_Cleanup_Call { + _c.Run(run) + return _c +} diff --git a/pkg/web/ginEx/mocks/Validate.go b/pkg/web/ginEx/mocks/Validate.go new file mode 100644 index 0000000..1ef37b6 --- /dev/null +++ b/pkg/web/ginEx/mocks/Validate.go @@ -0,0 +1,83 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package mocks + +import ( + "github.com/greenbone/opensight-notification-service/pkg/models" + mock "github.com/stretchr/testify/mock" +) + +// NewValidate creates a new instance of Validate. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewValidate(t interface { + mock.TestingT + Cleanup(func()) +}) *Validate { + mock := &Validate{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// Validate is an autogenerated mock type for the Validate type +type Validate struct { + mock.Mock +} + +type Validate_Expecter struct { + mock *mock.Mock +} + +func (_m *Validate) EXPECT() *Validate_Expecter { + return &Validate_Expecter{mock: &_m.Mock} +} + +// Validate provides a mock function for the type Validate +func (_mock *Validate) Validate() models.ValidationErrors { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for Validate") + } + + var r0 models.ValidationErrors + if returnFunc, ok := ret.Get(0).(func() models.ValidationErrors); ok { + r0 = returnFunc() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(models.ValidationErrors) + } + } + return r0 +} + +// Validate_Validate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Validate' +type Validate_Validate_Call struct { + *mock.Call +} + +// Validate is a helper method to define mock.On call +func (_e *Validate_Expecter) Validate() *Validate_Validate_Call { + return &Validate_Validate_Call{Call: _e.mock.On("Validate")} +} + +func (_c *Validate_Validate_Call) Run(run func()) *Validate_Validate_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Validate_Validate_Call) Return(validationErrors models.ValidationErrors) *Validate_Validate_Call { + _c.Call.Return(validationErrors) + return _c +} + +func (_c *Validate_Validate_Call) RunAndReturn(run func() models.ValidationErrors) *Validate_Validate_Call { + _c.Call.Return(run) + return _c +} diff --git a/pkg/web/healthcontroller/healthController.go b/pkg/web/healthcontroller/healthController.go index d1ad7f0..42cca98 100644 --- a/pkg/web/healthcontroller/healthController.go +++ b/pkg/web/healthcontroller/healthController.go @@ -5,20 +5,21 @@ package healthcontroller import ( - "github.com/greenbone/opensight-notification-service/pkg/web/healthcontroller/dtos" "net/http" + "github.com/greenbone/opensight-notification-service/pkg/services/healthservice" + "github.com/greenbone/opensight-notification-service/pkg/web/healthcontroller/dtos" + "github.com/gin-gonic/gin" - "github.com/greenbone/opensight-notification-service/pkg/port" app "github.com/greenbone/opensight-notification-service" ) type HealthController struct { - healthService port.HealthService + healthService healthservice.HealthService } -func NewHealthController(router gin.IRouter, healthService port.HealthService) *HealthController { +func NewHealthController(router gin.IRouter, healthService healthservice.HealthService) *HealthController { ctrl := HealthController{ healthService: healthService, } @@ -35,30 +36,30 @@ func (c *HealthController) registerRoutes(router gin.IRouter) { router.GET("/api/notification-service/version", c.readVersion) } -// @Summary Service health status Started -// @Description Endpoint for 'started' health probes -// @Tags health -// @Success 200 "Started" -// @Router /health/started [get] +// @Summary Service health status Started +// @Description Endpoint for 'started' health probes +// @Tags health +// @Success 200 "Started" +// @Router /health/started [get] func (c *HealthController) Started(gc *gin.Context) { gc.Status(http.StatusOK) } -// @Summary Service health status Alive -// @Description Endpoint for 'alive' health probes -// @Tags health -// @Success 200 "Alive" -// @Router /health/alive [get] +// @Summary Service health status Alive +// @Description Endpoint for 'alive' health probes +// @Tags health +// @Success 200 "Alive" +// @Router /health/alive [get] func (c *HealthController) Alive(gc *gin.Context) { gc.Status(http.StatusOK) } -// @Summary Service health status Ready -// @Description Indicates if the service is ready to serve traffic -// @Tags health -// @Success 200 "Ready" -// @Failure 404 "Not ready" -// @Router /health/ready [get] +// @Summary Service health status Ready +// @Description Indicates if the service is ready to serve traffic +// @Tags health +// @Success 200 "Ready" +// @Failure 404 "Not ready" +// @Router /health/ready [get] func (c *HealthController) Ready(gc *gin.Context) { if c.healthService.Ready(gc.Request.Context()) { gc.Status(http.StatusOK) diff --git a/pkg/web/helper/validationErrorHandler.go b/pkg/web/helper/validationErrorHandler.go deleted file mode 100644 index 1e79337..0000000 --- a/pkg/web/helper/validationErrorHandler.go +++ /dev/null @@ -1,28 +0,0 @@ -package helper - -import ( - "errors" - "net/http" - - "github.com/gin-gonic/gin" - "github.com/greenbone/opensight-golang-libraries/pkg/errorResponses" -) - -type ValidateErrors map[string]string - -func (v ValidateErrors) Error() string { - return "validation error" -} - -func ValidationErrorHandler(errorType gin.ErrorType) gin.HandlerFunc { - return func(c *gin.Context) { - c.Next() - for _, errorValue := range c.Errors.ByType(errorType) { - validateErrors := ValidateErrors{} - if errors.As(errorValue, &validateErrors) { - c.AbortWithStatusJSON(http.StatusBadRequest, errorResponses.NewErrorValidationResponse("", "", validateErrors)) - return - } - } - } -} diff --git a/pkg/web/mailcontroller/checkMailServer.go b/pkg/web/mailcontroller/checkMailServer.go index 67497e4..c57e818 100644 --- a/pkg/web/mailcontroller/checkMailServer.go +++ b/pkg/web/mailcontroller/checkMailServer.go @@ -5,20 +5,23 @@ import ( "net/http" "github.com/gin-gonic/gin" - "github.com/greenbone/opensight-notification-service/pkg/restErrorHandler" + "github.com/greenbone/opensight-golang-libraries/pkg/errorResponses" "github.com/greenbone/opensight-notification-service/pkg/services/notificationchannelservice" - "github.com/greenbone/opensight-notification-service/pkg/web/mailcontroller/dtos" + "github.com/greenbone/opensight-notification-service/pkg/web/errmap" + "github.com/greenbone/opensight-notification-service/pkg/web/ginEx" + "github.com/greenbone/opensight-notification-service/pkg/web/mailcontroller/maildto" "github.com/greenbone/opensight-notification-service/pkg/web/middleware" ) type CheckMailServerController struct { - notificationChannelServicer notificationchannelservice.NotificationChannelServicer + notificationChannelServicer notificationchannelservice.MailChannelService } func AddCheckMailServerController( router gin.IRouter, - notificationChannelServicer notificationchannelservice.NotificationChannelServicer, + notificationChannelServicer notificationchannelservice.MailChannelService, auth gin.HandlerFunc, + registry errmap.ErrorRegistry, ) *CheckMailServerController { ctrl := &CheckMailServerController{ notificationChannelServicer: notificationChannelServicer, @@ -29,9 +32,30 @@ func AddCheckMailServerController( group.POST("/check", ctrl.CheckMailServer) + ctrl.configureMappings(registry) + return ctrl } +func (mc *CheckMailServerController) configureMappings(r errmap.ErrorRegistry) { + r.Register( + notificationchannelservice.ErrGetMailChannel, + http.StatusInternalServerError, + errorResponses.ErrorInternalResponse, + ) + r.Register( + notificationchannelservice.ErrCreateMailFailed, + http.StatusUnprocessableEntity, + errorResponses.NewErrorGenericResponse("Unable to create mail client"), + ) + + r.Register( + notificationchannelservice.ErrMailServerUnreachable, + http.StatusUnprocessableEntity, + errorResponses.NewErrorGenericResponse("Server is unreachable"), + ) +} + // CheckMailServer // // @Summary Check mail server @@ -40,28 +64,20 @@ func AddCheckMailServerController( // @Accept json // @Produce json // @Security KeycloakAuth -// @Param MailServerConfig body dtos.CheckMailServerRequest true "Mail server to check" +// @Param MailServerConfig body maildto.CheckMailServerRequest true "Mail server to check" // @Success 204 "Mail server reachable" // @Failure 400 {object} map[string]string // @Failure 422 "Mail server error" // @Router /notifications/mail/check [post] func (mc *CheckMailServerController) CheckMailServer(c *gin.Context) { - var mailServer dtos.CheckMailServerRequest - if err := c.ShouldBindJSON(&mailServer); err != nil { - restErrorHandler.NotificationChannelErrorHandler(c, "", nil, notificationchannelservice.ErrMailChannelBadRequest) - return - } - if err := mailServer.Validate(); err != nil { - _ = c.Error(err) + var mailServer maildto.CheckMailServerRequest + if !ginEx.BindAndValidateBody(c, &mailServer) { return } err := mc.notificationChannelServicer.CheckNotificationChannelConnectivity(context.Background(), mailServer.ToModel()) if err != nil { - c.JSON(http.StatusUnprocessableEntity, gin.H{ - "type": "greenbone/generic-error", - "title": err.Error()}, - ) + ginEx.AddError(c, err) return } diff --git a/pkg/web/mailcontroller/checkMailServer_test.go b/pkg/web/mailcontroller/checkMailServer_test.go index 0cba651..bb26af0 100644 --- a/pkg/web/mailcontroller/checkMailServer_test.go +++ b/pkg/web/mailcontroller/checkMailServer_test.go @@ -6,18 +6,20 @@ import ( "github.com/gin-gonic/gin" "github.com/greenbone/opensight-golang-libraries/pkg/httpassert" - "github.com/greenbone/opensight-notification-service/pkg/port/mocks" + "github.com/greenbone/opensight-notification-service/pkg/services/notificationchannelservice" + "github.com/greenbone/opensight-notification-service/pkg/services/notificationchannelservice/mocks" + "github.com/greenbone/opensight-notification-service/pkg/web/errmap" "github.com/greenbone/opensight-notification-service/pkg/web/testhelper" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) -func setup(t *testing.T) (*gin.Engine, *mocks.NotificationChannelService) { - engine := testhelper.NewTestWebEngine() +func setup(t *testing.T) (*gin.Engine, *mocks.MailChannelService) { + registry := errmap.NewRegistry() + engine := testhelper.NewTestWebEngine(registry) - notificationChannelServicer := mocks.NewNotificationChannelService(t) + notificationChannelServicer := mocks.NewMailChannelService(t) - AddCheckMailServerController(engine, notificationChannelServicer, testhelper.MockAuthMiddlewareWithAdmin) + AddCheckMailServerController(engine, notificationChannelServicer, testhelper.MockAuthMiddlewareWithAdmin, registry) return engine, notificationChannelServicer } @@ -43,6 +45,21 @@ func TestCheckMailServer(t *testing.T) { NoContent() }) + t.Run("none request body", func(t *testing.T) { + engine, _ := setup(t) + + httpassert.New(t, engine). + Post("/notification-channel/mail/check"). + Content(`-`). + Expect(). + StatusCode(http.StatusBadRequest). + Json(`{ + "type": "greenbone/validation-error", + "title": "unable to parse the request", + "details":"error parsing body" + }`) + }) + t.Run("minimal required fields", func(t *testing.T) { engine, _ := setup(t) @@ -55,8 +72,8 @@ func TestCheckMailServer(t *testing.T) { "type": "greenbone/validation-error", "title": "", "errors": { - "domain": "required", - "port": "required" + "domain": "A Mailhub is required.", + "port": "A port is required." } }`) }) @@ -80,8 +97,8 @@ func TestCheckMailServer(t *testing.T) { "type": "greenbone/validation-error", "title": "", "errors": { - "password": "required", - "username": "required" + "username": "An Username is required.", + "password": "A Password is required." } }`) }) @@ -90,7 +107,7 @@ func TestCheckMailServer(t *testing.T) { engine, notificationChannelServicer := setup(t) notificationChannelServicer.EXPECT().CheckNotificationChannelConnectivity(mock.Anything, mock.Anything). - Return(assert.AnError) + Return(notificationchannelservice.ErrMailServerUnreachable) httpassert.New(t, engine). Post("/notification-channel/mail/check"). @@ -104,7 +121,7 @@ func TestCheckMailServer(t *testing.T) { StatusCode(http.StatusUnprocessableEntity). Json(`{ "type": "greenbone/generic-error", - "title": "assert.AnError general error for testing" + "title": "Server is unreachable" }`) }) } diff --git a/pkg/web/mailcontroller/dtos/CheckMailServerRequest.go b/pkg/web/mailcontroller/dtos/CheckMailServerRequest.go deleted file mode 100644 index 0221ace..0000000 --- a/pkg/web/mailcontroller/dtos/CheckMailServerRequest.go +++ /dev/null @@ -1,95 +0,0 @@ -package dtos - -import ( - "github.com/greenbone/opensight-notification-service/pkg/models" - "github.com/greenbone/opensight-notification-service/pkg/web/helper" -) - -type CheckMailServerRequest struct { - Domain string `json:"domain"` - Port int `json:"port"` - IsAuthenticationRequired bool `json:"isAuthenticationRequired" default:"false"` - IsTlsEnforced bool `json:"isTlsEnforced" default:"false"` - Username string `json:"username"` - Password string `json:"password"` -} - -func (v CheckMailServerRequest) ToModel() models.NotificationChannel { - return models.NotificationChannel{ - Domain: &v.Domain, - Port: &v.Port, - IsAuthenticationRequired: &v.IsAuthenticationRequired, - IsTlsEnforced: &v.IsTlsEnforced, - Username: &v.Username, - Password: &v.Password, - } -} - -func (v CheckMailServerRequest) Validate() helper.ValidateErrors { - errors := make(helper.ValidateErrors) - - if v.Domain == "" { - errors["domain"] = "required" - } - if v.Port == 0 { - errors["port"] = "required" - } - - if v.IsAuthenticationRequired { - if v.Username == "" { - errors["username"] = "required" - } - if v.Password == "" { - errors["password"] = "required" - } - } - - if len(errors) > 0 { - return errors - } - - return nil -} - -type CheckMailServerEntityRequest struct { - Domain string `json:"domain"` - Port int `json:"port"` - IsAuthenticationRequired bool `json:"isAuthenticationRequired" default:"false"` - IsTlsEnforced bool `json:"isTlsEnforced" default:"false"` - Username string `json:"username"` - Password string `json:"password"` -} - -func (v CheckMailServerEntityRequest) ToModel() models.NotificationChannel { - return models.NotificationChannel{ - Domain: &v.Domain, - Port: &v.Port, - IsAuthenticationRequired: &v.IsAuthenticationRequired, - IsTlsEnforced: &v.IsTlsEnforced, - Username: &v.Username, - Password: &v.Password, - } -} - -func (v CheckMailServerEntityRequest) Validate() helper.ValidateErrors { - errors := make(helper.ValidateErrors) - - if v.Domain == "" { - errors["domain"] = "required" - } - if v.Port == 0 { - errors["port"] = "required" - } - - if v.IsAuthenticationRequired { - if v.Username == "" { - errors["username"] = "required" - } - } - - if len(errors) > 0 { - return errors - } - - return nil -} diff --git a/pkg/web/mailcontroller/mailController.go b/pkg/web/mailcontroller/mailController.go index 717c9ae..71cd1d8 100644 --- a/pkg/web/mailcontroller/mailController.go +++ b/pkg/web/mailcontroller/mailController.go @@ -3,34 +3,52 @@ package mailcontroller import ( "context" "net/http" - "regexp" "github.com/gin-gonic/gin" - "github.com/greenbone/opensight-notification-service/pkg/mapper" + "github.com/greenbone/opensight-golang-libraries/pkg/errorResponses" "github.com/greenbone/opensight-notification-service/pkg/models" - "github.com/greenbone/opensight-notification-service/pkg/port" - "github.com/greenbone/opensight-notification-service/pkg/request" - "github.com/greenbone/opensight-notification-service/pkg/restErrorHandler" "github.com/greenbone/opensight-notification-service/pkg/services/notificationchannelservice" - "github.com/greenbone/opensight-notification-service/pkg/web/mailcontroller/dtos" + "github.com/greenbone/opensight-notification-service/pkg/web/errmap" + "github.com/greenbone/opensight-notification-service/pkg/web/ginEx" + "github.com/greenbone/opensight-notification-service/pkg/web/mailcontroller/maildto" "github.com/greenbone/opensight-notification-service/pkg/web/middleware" ) type MailController struct { - Service port.NotificationChannelService - MailChannelService port.MailChannelService + Service notificationchannelservice.NotificationChannelService + MailChannelService notificationchannelservice.MailChannelService } func NewMailController( router gin.IRouter, - service port.NotificationChannelService, mailChannelService port.MailChannelService, + service notificationchannelservice.NotificationChannelService, + mailChannelService notificationchannelservice.MailChannelService, auth gin.HandlerFunc, + registry errmap.ErrorRegistry, ) *MailController { - ctrl := &MailController{Service: service, MailChannelService: mailChannelService} + ctrl := &MailController{ + Service: service, + MailChannelService: mailChannelService, + } ctrl.registerRoutes(router, auth) + ctrl.configureMappings(registry) return ctrl } +func (mc *MailController) configureMappings(r errmap.ErrorRegistry) { + r.Register( + notificationchannelservice.ErrMailChannelLimitReached, + http.StatusUnprocessableEntity, + errorResponses.NewErrorGenericResponse("Mail channel limit reached."), + ) + + r.Register( + notificationchannelservice.ErrListMailChannels, + http.StatusInternalServerError, + errorResponses.ErrorInternalResponse, + ) +} + func (mc *MailController) registerRoutes(router gin.IRouter, auth gin.HandlerFunc) { group := router.Group("/notification-channel/mail"). Use(middleware.AuthorizeRoles(auth, "admin")...) @@ -49,31 +67,24 @@ func (mc *MailController) registerRoutes(router gin.IRouter, auth gin.HandlerFun // @Accept json // @Produce json // @Security KeycloakAuth -// @Param MailChannel body request.MailNotificationChannelRequest true "Mail channel to add" -// @Success 201 {object} request.MailNotificationChannelRequest +// @Param MailChannel body maildto.MailNotificationChannelRequest true "Mail channel to add" +// @Success 201 {object} maildto.MailNotificationChannelRequest // @Failure 400 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /notification-channel/mail [post] func (mc *MailController) CreateMailChannel(c *gin.Context) { - var channel request.MailNotificationChannelRequest - if err := c.ShouldBindJSON(&channel); err != nil { - restErrorHandler.NotificationChannelErrorHandler(c, "", nil, notificationchannelservice.ErrMailChannelBadRequest) - return - } - - if err := mc.validateFields(channel); err != nil { - restErrorHandler.NotificationChannelErrorHandler(c, "Mandatory fields of mail configuration cannot be empty", - err, nil) + var channel maildto.MailNotificationChannelRequest + if !ginEx.BindAndValidateBody(c, &channel) { return } mailChannel, err := mc.MailChannelService.CreateMailChannel(c, channel) if err != nil { - restErrorHandler.NotificationChannelErrorHandler(c, "", nil, err) + ginEx.AddError(c, err) return } - c.JSON(http.StatusCreated, mailChannel.WithEmptyPassword()) + c.JSON(http.StatusCreated, mailChannel) } // ListMailChannelsByType @@ -84,17 +95,17 @@ func (mc *MailController) CreateMailChannel(c *gin.Context) { // @Produce json // @Security KeycloakAuth // @Param type query string false "Channel type" -// @Success 200 {array} request.MailNotificationChannelRequest +// @Success 200 {array} maildto.MailNotificationChannelRequest // @Failure 500 {object} map[string]string // @Router /notification-channel/mail [get] func (mc *MailController) ListMailChannelsByType(c *gin.Context) { channels, err := mc.Service.ListNotificationChannelsByType(c, models.ChannelTypeMail) - if err != nil { - restErrorHandler.NotificationChannelErrorHandler(c, "", nil, err) + ginEx.AddError(c, err) return } - c.JSON(http.StatusOK, mapper.MapNotificationChannelsToMailWithEmptyPassword(channels)) + + c.JSON(http.StatusOK, maildto.MapNotificationChannelsToMail(channels)) } // UpdateMailChannel @@ -106,33 +117,27 @@ func (mc *MailController) ListMailChannelsByType(c *gin.Context) { // @Produce json // @Security KeycloakAuth // @Param id path string true "Mail channel ID" -// @Param MailChannel body request.MailNotificationChannelRequest true "Mail channel to update" -// @Success 200 {object} request.MailNotificationChannelRequest +// @Param MailChannel body maildto.MailNotificationChannelRequest true "Mail channel to update" +// @Success 200 {object} maildto.MailNotificationChannelRequest // @Failure 400 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /notification-channel/mail/{id} [put] func (mc *MailController) UpdateMailChannel(c *gin.Context) { id := c.Param("id") - var channel request.MailNotificationChannelRequest - if err := c.ShouldBindJSON(&channel); err != nil { - restErrorHandler.NotificationChannelErrorHandler(c, "", nil, notificationchannelservice.ErrMailChannelBadRequest) - return - } - - if err := mc.validateFields(channel); err != nil { - restErrorHandler.NotificationChannelErrorHandler(c, "Mandatory fields of mail configuration cannot be empty", - err, nil) + var channel maildto.MailNotificationChannelRequest + if !ginEx.BindAndValidateBody(c, &channel) { return } - notificationChannel := mapper.MapMailToNotificationChannel(channel) + notificationChannel := maildto.MapMailToNotificationChannel(channel) updated, err := mc.Service.UpdateNotificationChannel(c, id, notificationChannel) if err != nil { - restErrorHandler.NotificationChannelErrorHandler(c, "", nil, err) + ginEx.AddError(c, err) return } - mailChannel := mapper.MapNotificationChannelToMail(updated) - c.JSON(http.StatusOK, mailChannel.WithEmptyPassword()) + + mailChannel := maildto.MapNotificationChannelToMail(updated) + c.JSON(http.StatusOK, mailChannel) } // DeleteMailChannel @@ -147,8 +152,9 @@ func (mc *MailController) UpdateMailChannel(c *gin.Context) { // @Router /notification-channel/mail/{id} [delete] func (mc *MailController) DeleteMailChannel(c *gin.Context) { id := c.Param("id") + if err := mc.Service.DeleteNotificationChannel(c, id); err != nil { - restErrorHandler.NotificationChannelErrorHandler(c, "", nil, err) + ginEx.AddError(c, err) return } @@ -163,7 +169,7 @@ func (mc *MailController) DeleteMailChannel(c *gin.Context) { // @Accept json // @Produce json // @Security KeycloakAuth -// @Param MailServerConfig body dtos.CheckMailServerEntityRequest true "Mail server to check" +// @Param MailServerConfig body maildto.CheckMailServerEntityRequest true "Mail server to check" // @Success 204 "Mail server reachable" // @Failure 400 {object} map[string]string // @Failure 422 "Mail server error" @@ -171,55 +177,16 @@ func (mc *MailController) DeleteMailChannel(c *gin.Context) { func (mc *MailController) CheckMailServer(c *gin.Context) { id := c.Param("id") - var mailServer dtos.CheckMailServerEntityRequest - if err := c.ShouldBindJSON(&mailServer); err != nil { - restErrorHandler.NotificationChannelErrorHandler(c, "", nil, notificationchannelservice.ErrMailChannelBadRequest) - return - } - if err := mailServer.Validate(); err != nil { - _ = c.Error(err) + var mailServer maildto.CheckMailServerEntityRequest + if !ginEx.BindAndValidateBody(c, &mailServer) { return } - err := mc.Service.CheckNotificationChannelEntityConnectivity(context.Background(), id, mailServer.ToModel()) + err := mc.MailChannelService.CheckNotificationChannelEntityConnectivity(context.Background(), id, mailServer.ToModel()) if err != nil { - c.JSON(http.StatusUnprocessableEntity, gin.H{ - "type": "greenbone/generic-error", - "title": err.Error()}, - ) + ginEx.AddError(c, err) return } c.Status(http.StatusNoContent) } - -func (v *MailController) validateFields(channel request.MailNotificationChannelRequest) map[string]string { - errors := make(map[string]string) - if channel.Domain == "" { - errors["domain"] = "A Mailhub is required." - } - if channel.Port == 0 { - errors["port"] = "A port is required." - } - if channel.SenderEmailAddress == "" { - errors["senderEmailAddress"] = "A sender is required." - } else { - v.validateEmailAddress(channel.SenderEmailAddress, errors) - } - if channel.ChannelName == "" { - errors["channelName"] = "A Channel Name is required." - } - - if len(errors) > 0 { - return errors - } - return nil -} - -func (v *MailController) validateEmailAddress(channel string, errors map[string]string) { - emailRegex := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$` - matched, _ := regexp.MatchString(emailRegex, channel) - if !matched { - errors["senderEmailAddress"] = "A sender is required." - } -} diff --git a/pkg/web/mailcontroller/mailController_integration_test.go b/pkg/web/mailcontroller/mailController_integration_test.go index d1af152..e450206 100644 --- a/pkg/web/mailcontroller/mailController_integration_test.go +++ b/pkg/web/mailcontroller/mailController_integration_test.go @@ -11,6 +11,7 @@ import ( "github.com/gin-gonic/gin" "github.com/greenbone/opensight-golang-libraries/pkg/httpassert" "github.com/greenbone/opensight-notification-service/pkg/services/notificationchannelservice" + "github.com/greenbone/opensight-notification-service/pkg/web/errmap" "github.com/greenbone/opensight-notification-service/pkg/web/testhelper" "github.com/jmoiron/sqlx" _ "github.com/lib/pq" @@ -159,11 +160,12 @@ func TestIntegration_MailController_CRUD(t *testing.T) { func setupTestRouter(t *testing.T) (*gin.Engine, *sqlx.DB) { repo, db := testhelper.SetupNotificationChannelTestEnv(t) svc := notificationchannelservice.NewNotificationChannelService(repo) - mailSvc := notificationchannelservice.NewMailChannelService(svc, 1) + mailSvc := notificationchannelservice.NewMailChannelService(svc, repo, 1) - router := testhelper.NewTestWebEngine() + registry := errmap.NewRegistry() + router := testhelper.NewTestWebEngine(registry) - NewMailController(router, svc, mailSvc, testhelper.MockAuthMiddlewareWithAdmin) + NewMailController(router, svc, mailSvc, testhelper.MockAuthMiddlewareWithAdmin, registry) return router, db } diff --git a/pkg/web/mailcontroller/mailController_test.go b/pkg/web/mailcontroller/mailController_test.go index 97dbd5d..ec75b75 100644 --- a/pkg/web/mailcontroller/mailController_test.go +++ b/pkg/web/mailcontroller/mailController_test.go @@ -7,16 +7,16 @@ import ( "github.com/gin-gonic/gin" "github.com/greenbone/opensight-golang-libraries/pkg/httpassert" - "github.com/greenbone/opensight-notification-service/pkg/mapper" "github.com/greenbone/opensight-notification-service/pkg/models" - "github.com/greenbone/opensight-notification-service/pkg/port/mocks" - mailmocks "github.com/greenbone/opensight-notification-service/pkg/port/mocks" - "github.com/greenbone/opensight-notification-service/pkg/request" + "github.com/greenbone/opensight-notification-service/pkg/services/notificationchannelservice/mocks" + "github.com/greenbone/opensight-notification-service/pkg/web/errmap" + "github.com/greenbone/opensight-notification-service/pkg/web/mailcontroller/maildto" "github.com/greenbone/opensight-notification-service/pkg/web/testhelper" "github.com/stretchr/testify/mock" ) func getValidNotificationChannel() models.NotificationChannel { + id := "mail-id-1" name := "mail1" domain := "example.com" port := 25 @@ -28,6 +28,7 @@ func getValidNotificationChannel() models.NotificationChannel { maxInclude := 5 sender := "sender@example.com" return models.NotificationChannel{ + Id: &id, ChannelName: &name, Domain: &domain, Port: &port, @@ -41,22 +42,23 @@ func getValidNotificationChannel() models.NotificationChannel { } } -func setupRouter(service *mocks.NotificationChannelService, mailService *mailmocks.MailChannelService) *gin.Engine { - engine := testhelper.NewTestWebEngine() +func setupRouter(service *mocks.NotificationChannelService, mailService *mocks.MailChannelService) *gin.Engine { + registry := errmap.NewRegistry() + engine := testhelper.NewTestWebEngine(registry) - NewMailController(engine, service, mailService, testhelper.MockAuthMiddlewareWithAdmin) + NewMailController(engine, service, mailService, testhelper.MockAuthMiddlewareWithAdmin, registry) return engine } func TestMailController_CreateMailChannel(t *testing.T) { valid := getValidNotificationChannel() - mailValid := mapper.MapNotificationChannelToMail(valid) + mailValid := maildto.MapNotificationChannelToMail(valid) created := mailValid // Simulate returned object tests := []struct { name string input any - mockReturn request.MailNotificationChannelRequest + mockReturn maildto.MailNotificationChannelResponse mockErr error wantStatusCode int }{ @@ -84,7 +86,7 @@ func TestMailController_CreateMailChannel(t *testing.T) { }, { name: "invalid sender email", - input: func() request.MailNotificationChannelRequest { + input: func() maildto.MailNotificationChannelResponse { invalid := mailValid invalid.SenderEmailAddress = "not-an-email" return invalid @@ -96,7 +98,7 @@ func TestMailController_CreateMailChannel(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockService := mocks.NewNotificationChannelService(t) - mockMailService := mailmocks.NewMailChannelService(t) + mockMailService := mocks.NewMailChannelService(t) router := setupRouter(mockService, mockMailService) if tt.wantStatusCode == http.StatusCreated || tt.wantStatusCode == http.StatusInternalServerError { @@ -244,7 +246,7 @@ func TestMailController_UpdateMailChannel(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockService := mocks.NewNotificationChannelService(t) - mockMailService := mailmocks.NewMailChannelService(t) + mockMailService := mocks.NewMailChannelService(t) router := setupRouter(mockService, mockMailService) if tt.wantStatusCode == http.StatusOK || tt.wantStatusCode == http.StatusInternalServerError { diff --git a/pkg/web/mailcontroller/maildto/mapper.go b/pkg/web/mailcontroller/maildto/mapper.go new file mode 100644 index 0000000..c1af52f --- /dev/null +++ b/pkg/web/mailcontroller/maildto/mapper.go @@ -0,0 +1,45 @@ +package maildto + +import "github.com/greenbone/opensight-notification-service/pkg/models" + +// MapNotificationChannelToMail maps NotificationChannel to MailNotificationChannelResponse. +func MapNotificationChannelToMail(channel models.NotificationChannel) MailNotificationChannelResponse { + return MailNotificationChannelResponse{ + Id: *channel.Id, + ChannelName: *channel.ChannelName, + Domain: *channel.Domain, + Port: *channel.Port, + IsAuthenticationRequired: *channel.IsAuthenticationRequired, + IsTlsEnforced: *channel.IsTlsEnforced, + Username: channel.Username, + MaxEmailAttachmentSizeMb: channel.MaxEmailAttachmentSizeMb, + MaxEmailIncludeSizeMb: channel.MaxEmailIncludeSizeMb, + SenderEmailAddress: *channel.SenderEmailAddress, + } +} + +func MapMailToNotificationChannel(mail MailNotificationChannelRequest) models.NotificationChannel { + return models.NotificationChannel{ + ChannelType: string(models.ChannelTypeMail), + Id: mail.Id, + ChannelName: &mail.ChannelName, + Domain: &mail.Domain, + Port: &mail.Port, + IsAuthenticationRequired: &mail.IsAuthenticationRequired, + IsTlsEnforced: &mail.IsTlsEnforced, + Username: mail.Username, + Password: mail.Password, + MaxEmailAttachmentSizeMb: mail.MaxEmailAttachmentSizeMb, + MaxEmailIncludeSizeMb: mail.MaxEmailIncludeSizeMb, + SenderEmailAddress: &mail.SenderEmailAddress, + } +} + +// MapNotificationChannelsToMail maps a slice of NotificationChannel to MailNotificationChannelRequest. +func MapNotificationChannelsToMail(channels []models.NotificationChannel) []MailNotificationChannelResponse { + mailChannels := make([]MailNotificationChannelResponse, 0, len(channels)) + for _, ch := range channels { + mailChannels = append(mailChannels, MapNotificationChannelToMail(ch)) + } + return mailChannels +} diff --git a/pkg/web/mailcontroller/maildto/request.go b/pkg/web/mailcontroller/maildto/request.go new file mode 100644 index 0000000..8b2024b --- /dev/null +++ b/pkg/web/mailcontroller/maildto/request.go @@ -0,0 +1,149 @@ +package maildto + +import ( + "net/mail" + "strings" + + "github.com/greenbone/opensight-notification-service/pkg/models" + "github.com/rs/zerolog/log" +) + +// CheckMailServerRequest check mail server request +type CheckMailServerRequest struct { + Domain string `json:"domain"` + Port int `json:"port"` + IsAuthenticationRequired bool `json:"isAuthenticationRequired" default:"false"` + IsTlsEnforced bool `json:"isTlsEnforced" default:"false"` + Username string `json:"username"` + Password string `json:"password"` +} + +func (v CheckMailServerRequest) ToModel() models.NotificationChannel { + return models.NotificationChannel{ + Domain: &v.Domain, + Port: &v.Port, + IsAuthenticationRequired: &v.IsAuthenticationRequired, + IsTlsEnforced: &v.IsTlsEnforced, + Username: &v.Username, + Password: &v.Password, + } +} + +func (r *CheckMailServerRequest) Cleanup() { + r.Domain = strings.TrimSpace(r.Domain) +} + +func (v CheckMailServerRequest) Validate() models.ValidationErrors { + errors := make(models.ValidationErrors) + + if v.Domain == "" { + errors["domain"] = "A Mailhub is required." + } + if v.Port == 0 { + errors["port"] = "A port is required." + } + + if v.IsAuthenticationRequired { + if v.Username == "" { + errors["username"] = "An Username is required." + } + if v.Password == "" { + errors["password"] = "A Password is required." + } + } + + return errors +} + +// CheckMailServerEntityRequest check mail server entity request +type CheckMailServerEntityRequest struct { + Domain string `json:"domain"` + Port int `json:"port"` + IsAuthenticationRequired bool `json:"isAuthenticationRequired" default:"false"` + IsTlsEnforced bool `json:"isTlsEnforced" default:"false"` + Username string `json:"username"` + Password string `json:"password"` +} + +func (v CheckMailServerEntityRequest) ToModel() models.NotificationChannel { + return models.NotificationChannel{ + Domain: &v.Domain, + Port: &v.Port, + IsAuthenticationRequired: &v.IsAuthenticationRequired, + IsTlsEnforced: &v.IsTlsEnforced, + Username: &v.Username, + Password: &v.Password, + } +} + +func (r *CheckMailServerEntityRequest) Cleanup() { + r.Domain = strings.TrimSpace(r.Domain) +} + +func (v CheckMailServerEntityRequest) Validate() models.ValidationErrors { + errs := make(models.ValidationErrors) + + if v.Domain == "" { + errs["domain"] = "required" + } + if v.Port == 0 { + errs["port"] = "required" + } + + if v.IsAuthenticationRequired { + if v.Username == "" { + errs["username"] = "required" + } + } + + return errs +} + +// MailNotificationChannelRequest mail notification channel request +type MailNotificationChannelRequest struct { + Id *string `json:"id,omitempty"` + ChannelName string `json:"channelName"` + Domain string `json:"domain"` + Port int `json:"port"` + IsAuthenticationRequired bool `json:"isAuthenticationRequired" default:"false"` + IsTlsEnforced bool `json:"isTlsEnforced" default:"false"` + Username *string `json:"username,omitempty"` + Password *string `json:"password,omitempty"` + MaxEmailAttachmentSizeMb *int `json:"maxEmailAttachmentSizeMb,omitempty"` + MaxEmailIncludeSizeMb *int `json:"maxEmailIncludeSizeMb,omitempty"` + SenderEmailAddress string `json:"senderEmailAddress"` +} + +func (r *MailNotificationChannelRequest) Cleanup() { + r.Domain = strings.TrimSpace(r.Domain) + r.SenderEmailAddress = strings.TrimSpace(r.SenderEmailAddress) + r.ChannelName = strings.TrimSpace(r.ChannelName) +} + +func (r MailNotificationChannelRequest) Validate() models.ValidationErrors { + errMap := make(models.ValidationErrors) + + if r.Domain == "" { + errMap["domain"] = "required" + } + + if r.Port < 1 || r.Port > 65535 { + errMap["port"] = "required" + } + + if r.SenderEmailAddress == "" { + errMap["senderEmailAddress"] = "required" + } + + _, err := mail.ParseAddress(r.SenderEmailAddress) + if err != nil && !strings.Contains(err.Error(), "mail: no address") { + log.Info().Msgf("unable to parse email address %s", err.Error()) + errMap["senderEmailAddress"] = "invalid" + } + + if r.ChannelName == "" { + errMap["channelName"] = "required" + } + + return errMap +} diff --git a/pkg/request/MailNotificationChannelRequest.go b/pkg/web/mailcontroller/maildto/response.go similarity index 66% rename from pkg/request/MailNotificationChannelRequest.go rename to pkg/web/mailcontroller/maildto/response.go index 87b4065..ae07509 100644 --- a/pkg/request/MailNotificationChannelRequest.go +++ b/pkg/web/mailcontroller/maildto/response.go @@ -1,20 +1,14 @@ -package request +package maildto -type MailNotificationChannelRequest struct { - Id *string `json:"id,omitempty"` +type MailNotificationChannelResponse struct { + Id string `json:"id,omitempty"` ChannelName string `json:"channelName"` Domain string `json:"domain"` Port int `json:"port"` IsAuthenticationRequired bool `json:"isAuthenticationRequired" default:"false"` IsTlsEnforced bool `json:"isTlsEnforced" default:"false"` Username *string `json:"username,omitempty"` - Password *string `json:"password,omitempty"` MaxEmailAttachmentSizeMb *int `json:"maxEmailAttachmentSizeMb,omitempty"` MaxEmailIncludeSizeMb *int `json:"maxEmailIncludeSizeMb,omitempty"` SenderEmailAddress string `json:"senderEmailAddress"` } - -func (r MailNotificationChannelRequest) WithEmptyPassword() MailNotificationChannelRequest { - r.Password = nil - return r -} diff --git a/pkg/web/mailcontroller/negative_usecases_test.go b/pkg/web/mailcontroller/negative_usecases_test.go index 4881026..e1f2165 100644 --- a/pkg/web/mailcontroller/negative_usecases_test.go +++ b/pkg/web/mailcontroller/negative_usecases_test.go @@ -117,6 +117,6 @@ func TestIntegration_MailController_Negative_Cases(t *testing.T) { request.Post("/notification-channel/mail"). JsonContentObject(valid). Expect(). - StatusCode(http.StatusConflict) + StatusCode(http.StatusUnprocessableEntity) }) } diff --git a/pkg/web/mattermostcontroller/mattermostController.go b/pkg/web/mattermostcontroller/mattermostController.go index ca12ea2..94284d4 100644 --- a/pkg/web/mattermostcontroller/mattermostController.go +++ b/pkg/web/mattermostcontroller/mattermostController.go @@ -4,30 +4,54 @@ import ( "net/http" "github.com/gin-gonic/gin" - "github.com/greenbone/opensight-notification-service/pkg/mapper" + "github.com/greenbone/opensight-golang-libraries/pkg/errorResponses" "github.com/greenbone/opensight-notification-service/pkg/models" - "github.com/greenbone/opensight-notification-service/pkg/port" - "github.com/greenbone/opensight-notification-service/pkg/request" - "github.com/greenbone/opensight-notification-service/pkg/restErrorHandler" "github.com/greenbone/opensight-notification-service/pkg/services/notificationchannelservice" + "github.com/greenbone/opensight-notification-service/pkg/web/errmap" + "github.com/greenbone/opensight-notification-service/pkg/web/ginEx" + "github.com/greenbone/opensight-notification-service/pkg/web/mattermostcontroller/mattermostdto" "github.com/greenbone/opensight-notification-service/pkg/web/middleware" ) type MattermostController struct { - service port.NotificationChannelService - mattermostChannelService port.MattermostChannelService + notificationChannelServicer notificationchannelservice.NotificationChannelService + mattermostChannelService notificationchannelservice.MattermostChannelService } func NewMattermostController( router gin.IRouter, - service port.NotificationChannelService, mattermostChannelService port.MattermostChannelService, + notificationChannelServicer notificationchannelservice.NotificationChannelService, + mattermostChannelService notificationchannelservice.MattermostChannelService, auth gin.HandlerFunc, + registry *errmap.Registry, ) *MattermostController { - ctrl := &MattermostController{service: service, mattermostChannelService: mattermostChannelService} + ctrl := &MattermostController{ + notificationChannelServicer: notificationChannelServicer, + mattermostChannelService: mattermostChannelService, + } ctrl.registerRoutes(router, auth) + ctrl.configureMappings(registry) return ctrl } +func (mc *MattermostController) configureMappings(r *errmap.Registry) { + r.Register( + notificationchannelservice.ErrMattermostChannelLimitReached, + http.StatusUnprocessableEntity, + errorResponses.NewErrorGenericResponse("Mattermost channel limit reached."), + ) + r.Register( + notificationchannelservice.ErrListMattermostChannels, + http.StatusInternalServerError, + errorResponses.ErrorInternalResponse, + ) + r.Register( + notificationchannelservice.ErrMattermostChannelNameExists, + http.StatusBadRequest, + errorResponses.NewErrorGenericResponse(notificationchannelservice.ErrMattermostChannelNameExists.Error()), + ) +} + func (mc *MattermostController) registerRoutes(router gin.IRouter, auth gin.HandlerFunc) { group := router.Group("/notification-channel/mattermost"). Use(middleware.AuthorizeRoles(auth, "admin")...) @@ -35,6 +59,7 @@ func (mc *MattermostController) registerRoutes(router gin.IRouter, auth gin.Hand group.GET("", mc.ListMattermostChannelsByType) group.PUT("/:id", mc.UpdateMattermostChannel) group.DELETE("/:id", mc.DeleteMattermostChannel) + group.POST("/check", mc.SendMattermostTestMessage) } // CreateMattermostChannel @@ -45,27 +70,20 @@ func (mc *MattermostController) registerRoutes(router gin.IRouter, auth gin.Hand // @Accept json // @Produce json // @Security KeycloakAuth -// @Param MattermostChannel body request.MattermostNotificationChannelRequest true "Mattermost channel to add" -// @Success 201 {object} request.MattermostNotificationChannelRequest +// @Param MattermostChannel body mattermostdto.MattermostNotificationChannelRequest true "Mattermost channel to add" +// @Success 201 {object} mattermostdto.MattermostNotificationChannelRequest // @Failure 400 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /notification-channel/mattermost [post] func (mc *MattermostController) CreateMattermostChannel(c *gin.Context) { - var channel request.MattermostNotificationChannelRequest - if err := c.ShouldBindJSON(&channel); err != nil { - restErrorHandler.NotificationChannelErrorHandler(c, "", nil, notificationchannelservice.ErrMattermostChannelBadRequest) - return - } - - if err := mc.validateFields(channel); err != nil { - restErrorHandler.NotificationChannelErrorHandler(c, "Mandatory fields of mattermost configuration cannot be empty", - err, nil) + var channel mattermostdto.MattermostNotificationChannelRequest + if !ginEx.BindAndValidateBody(c, &channel) { return } mattermostChannel, err := mc.mattermostChannelService.CreateMattermostChannel(c, channel) if err != nil { - restErrorHandler.NotificationChannelErrorHandler(c, "", nil, err) + ginEx.AddError(c, err) return } @@ -80,17 +98,17 @@ func (mc *MattermostController) CreateMattermostChannel(c *gin.Context) { // @Produce json // @Security KeycloakAuth // @Param type query string false "Channel type" -// @Success 200 {array} request.MattermostNotificationChannelRequest +// @Success 200 {array} mattermostdto.MattermostNotificationChannelRequest // @Failure 500 {object} map[string]string // @Router /notification-channel/mattermost [get] func (mc *MattermostController) ListMattermostChannelsByType(c *gin.Context) { - channels, err := mc.service.ListNotificationChannelsByType(c, models.ChannelTypeMattermost) - + channels, err := mc.notificationChannelServicer.ListNotificationChannelsByType(c, models.ChannelTypeMattermost) if err != nil { - restErrorHandler.NotificationChannelErrorHandler(c, "", nil, err) + ginEx.AddError(c, err) return } - c.JSON(http.StatusOK, mapper.MapNotificationChannelsToMattermost(channels)) + + c.JSON(http.StatusOK, mattermostdto.MapNotificationChannelsToMattermost(channels)) } // UpdateMattermostChannel @@ -102,33 +120,27 @@ func (mc *MattermostController) ListMattermostChannelsByType(c *gin.Context) { // @Produce json // @Security KeycloakAuth // @Param id path string true "Mattermost channel ID" -// @Param MattermostChannel body request.MattermostNotificationChannelRequest true "Mattermost channel to update" -// @Success 200 {object} request.MattermostNotificationChannelRequest +// @Param MattermostChannel body mattermostdto.MattermostNotificationChannelRequest true "Mattermost channel to update" +// @Success 200 {object} mattermostdto.MattermostNotificationChannelRequest // @Failure 400 {object} map[string]string // @Failure 404 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /notification-channel/mattermost/{id} [put] func (mc *MattermostController) UpdateMattermostChannel(c *gin.Context) { id := c.Param("id") - var channel request.MattermostNotificationChannelRequest - if err := c.ShouldBindJSON(&channel); err != nil { - restErrorHandler.NotificationChannelErrorHandler(c, "", nil, notificationchannelservice.ErrMattermostChannelBadRequest) - return - } - if err := mc.validateFields(channel); err != nil { - restErrorHandler.NotificationChannelErrorHandler(c, "Mandatory fields of mattermost configuration cannot be empty", - err, nil) + var channel mattermostdto.MattermostNotificationChannelRequest + if !ginEx.BindAndValidateBody(c, &channel) { return } - notificationChannel := mapper.MapMattermostToNotificationChannel(channel) - updated, err := mc.service.UpdateNotificationChannel(c, id, notificationChannel) + notificationChannel := mattermostdto.MapMattermostToNotificationChannel(channel) + updated, err := mc.notificationChannelServicer.UpdateNotificationChannel(c, id, notificationChannel) if err != nil { - restErrorHandler.NotificationChannelErrorHandler(c, "", nil, err) + ginEx.AddError(c, err) return } - response := mapper.MapNotificationChannelToMattermost(updated) + response := mattermostdto.MapNotificationChannelToMattermost(updated) c.JSON(http.StatusOK, response) } @@ -145,22 +157,35 @@ func (mc *MattermostController) UpdateMattermostChannel(c *gin.Context) { // @Router /notification-channel/mattermost/{id} [delete] func (mc *MattermostController) DeleteMattermostChannel(c *gin.Context) { id := c.Param("id") - if err := mc.service.DeleteNotificationChannel(c, id); err != nil { - restErrorHandler.NotificationChannelErrorHandler(c, "", nil, err) + if err := mc.notificationChannelServicer.DeleteNotificationChannel(c, id); err != nil { + ginEx.AddError(c, err) return } c.Status(http.StatusNoContent) } -func (v *MattermostController) validateFields(channel request.MattermostNotificationChannelRequest) map[string]string { - errors := make(map[string]string) - if channel.WebhookUrl == "" { - errors["webhookUrl"] = "A WebhookUrl is required." +// SendMattermostTestMessage +// +// @Summary Check mattermost server +// @Description Check if a mattermost server is able to send a test message +// @Tags mattermost-channel +// @Accept json +// @Produce json +// @Security KeycloakAuth +// @Success 204 "Mattermost server test message sent successfully" +// @Failure 400 {object} map[string]string +// @Router /notification-channel/mattermost/check [post] +func (mc *MattermostController) SendMattermostTestMessage(c *gin.Context) { + var channel mattermostdto.MattermostNotificationChannelCheckRequest + if !ginEx.BindAndValidateBody(c, &channel) { + return } - if len(errors) > 0 { - return errors + if err := mc.mattermostChannelService.SendMattermostTestMessage(channel.WebhookUrl); err != nil { + ginEx.AddError(c, err) + return } - return nil + + c.Status(http.StatusNoContent) } diff --git a/pkg/web/mattermostcontroller/mattermostController_integration_test.go b/pkg/web/mattermostcontroller/mattermostController_integration_test.go index 273e90d..11c5429 100644 --- a/pkg/web/mattermostcontroller/mattermostController_integration_test.go +++ b/pkg/web/mattermostcontroller/mattermostController_integration_test.go @@ -9,14 +9,26 @@ import ( "github.com/gin-gonic/gin" "github.com/greenbone/opensight-golang-libraries/pkg/httpassert" - "github.com/greenbone/opensight-notification-service/pkg/request" "github.com/greenbone/opensight-notification-service/pkg/services/notificationchannelservice" + "github.com/greenbone/opensight-notification-service/pkg/web/errmap" + "github.com/greenbone/opensight-notification-service/pkg/web/mattermostcontroller/mattermostdto" "github.com/greenbone/opensight-notification-service/pkg/web/testhelper" "github.com/jmoiron/sqlx" _ "github.com/lib/pq" "github.com/stretchr/testify/require" ) +func setupTestRouter(t *testing.T) (*gin.Engine, *sqlx.DB) { + repo, db := testhelper.SetupNotificationChannelTestEnv(t) + svc := notificationchannelservice.NewNotificationChannelService(repo) + mattermostSvc := notificationchannelservice.NewMattermostChannelService(svc, 20) + registry := errmap.NewRegistry() + router := testhelper.NewTestWebEngine(registry) + NewMattermostController(router, svc, mattermostSvc, testhelper.MockAuthMiddlewareWithAdmin, registry) + + return router, db +} + func TestIntegration_MattermostController_CRUD(t *testing.T) { t.Parallel() @@ -35,7 +47,7 @@ func TestIntegration_MattermostController_CRUD(t *testing.T) { JsonPath("$", httpassert.HasSize(4)). JsonPath("$.id", httpassert.ExtractTo(&mattermostId)). JsonPath("$.channelName", "mattermost1"). - JsonPath("$.webhookUrl", "http://webhookurl.com/id1"). + JsonPath("$.webhookUrl", "https://webhookurl.com/hooks/id1"). JsonPath("$.description", "This is a test mattermost channel") require.NotEmpty(t, mattermostId) }) @@ -54,11 +66,11 @@ func TestIntegration_MattermostController_CRUD(t *testing.T) { JsonPath("$[0]", httpassert.HasSize(4)). JsonPath("$[0].id", httpassert.ExtractTo(&mattermostId)). JsonPath("$[0].channelName", "mattermost1"). - JsonPath("$[0].webhookUrl", "http://webhookurl.com/id1"). + JsonPath("$[0].webhookUrl", "https://webhookurl.com/hooks/id1"). JsonPath("$[0].description", "This is a test mattermost channel") }) - t.Run("Perform the Update operations", func(t *testing.T) { + t.Run("Perform the Update operation", func(t *testing.T) { router, db := setupTestRouter(t) defer db.Close() request := httpassert.New(t, router) @@ -75,11 +87,11 @@ func TestIntegration_MattermostController_CRUD(t *testing.T) { JsonPath("$", httpassert.HasSize(4)). JsonPath("$.id", mattermostId). JsonPath("$.channelName", newName). - JsonPath("$.webhookUrl", "http://webhookurl.com/id1"). + JsonPath("$.webhookUrl", "https://webhookurl.com/hooks/id1"). JsonPath("$.description", "This is a test mattermost channel") }) - t.Run("Perform the Delete operations", func(t *testing.T) { + t.Run("Perform the Delete operation", func(t *testing.T) { router, db := setupTestRouter(t) defer db.Close() request := httpassert.New(t, router) @@ -97,12 +109,13 @@ func TestIntegration_MattermostController_CRUD(t *testing.T) { }) t.Run("Verify Limit check on mattermost limit", func(t *testing.T) { + registry := errmap.NewRegistry() + router := testhelper.NewTestWebEngine(registry) + repo, db := testhelper.SetupNotificationChannelTestEnv(t) svc := notificationchannelservice.NewNotificationChannelService(repo) mattermostSvc := notificationchannelservice.NewMattermostChannelService(svc, 1) - gin.SetMode(gin.TestMode) - router := gin.New() - NewMattermostController(router, svc, mattermostSvc, testhelper.MockAuthMiddlewareWithAdmin) + NewMattermostController(router, svc, mattermostSvc, testhelper.MockAuthMiddlewareWithAdmin, registry) defer db.Close() request := httpassert.New(t, router) @@ -115,9 +128,29 @@ func TestIntegration_MattermostController_CRUD(t *testing.T) { StatusCode(http.StatusUnprocessableEntity). JsonPath("$.title", "Mattermost channel limit reached.") }) + + t.Run("Create two mattermost channels with the same name", func(t *testing.T) { + router, db := setupTestRouter(t) + defer db.Close() + + request := httpassert.New(t, router) + + createMattermostNotification(t, request, "mattermost1", valid) + + request.Post("/notification-channel/mattermost"). + JsonContentObject(valid). + Expect(). + StatusCode(http.StatusBadRequest). + JsonPath("$.title", "mattermost channel name already exists") + }) } -func createMattermostNotification(t *testing.T, request httpassert.Request, channelName string, valid request.MattermostNotificationChannelRequest) string { +func createMattermostNotification( + t *testing.T, + request httpassert.Request, + channelName string, + valid mattermostdto.MattermostNotificationChannelRequest, +) string { var mattermostId string valid.ChannelName = channelName @@ -128,20 +161,9 @@ func createMattermostNotification(t *testing.T, request httpassert.Request, chan JsonPath("$", httpassert.HasSize(4)). JsonPath("$.id", httpassert.ExtractTo(&mattermostId)). JsonPath("$.channelName", channelName). - JsonPath("$.webhookUrl", "http://webhookurl.com/id1"). + JsonPath("$.webhookUrl", "https://webhookurl.com/hooks/id1"). JsonPath("$.description", "This is a test mattermost channel") require.NotEmpty(t, mattermostId) return mattermostId } - -func setupTestRouter(t *testing.T) (*gin.Engine, *sqlx.DB) { - repo, db := testhelper.SetupNotificationChannelTestEnv(t) - svc := notificationchannelservice.NewNotificationChannelService(repo) - mattermostSvc := notificationchannelservice.NewMattermostChannelService(svc, 20) - gin.SetMode(gin.TestMode) - router := gin.New() - NewMattermostController(router, svc, mattermostSvc, testhelper.MockAuthMiddlewareWithAdmin) - - return router, db -} diff --git a/pkg/web/mattermostcontroller/mattermost_test.go b/pkg/web/mattermostcontroller/mattermostController_test.go similarity index 80% rename from pkg/web/mattermostcontroller/mattermost_test.go rename to pkg/web/mattermostcontroller/mattermostController_test.go index 612dc6d..4feb281 100644 --- a/pkg/web/mattermostcontroller/mattermost_test.go +++ b/pkg/web/mattermostcontroller/mattermostController_test.go @@ -7,44 +7,39 @@ import ( "github.com/gin-gonic/gin" "github.com/greenbone/opensight-notification-service/pkg/helper" - "github.com/greenbone/opensight-notification-service/pkg/response" + "github.com/greenbone/opensight-notification-service/pkg/services/notificationchannelservice/mocks" + "github.com/greenbone/opensight-notification-service/pkg/web/errmap" + "github.com/greenbone/opensight-notification-service/pkg/web/mattermostcontroller/mattermostdto" + "github.com/greenbone/opensight-notification-service/pkg/web/testhelper" "github.com/stretchr/testify/mock" "github.com/greenbone/opensight-golang-libraries/pkg/httpassert" "github.com/greenbone/opensight-notification-service/pkg/models" - "github.com/greenbone/opensight-notification-service/pkg/port/mocks" - "github.com/greenbone/opensight-notification-service/pkg/request" ) func setupTestController() (*gin.Engine, *mocks.NotificationChannelService, *mocks.MattermostChannelService) { - gin.SetMode(gin.TestMode) - r := gin.Default() + registry := errmap.NewRegistry() + r := testhelper.NewTestWebEngine(registry) + notificationService := &mocks.NotificationChannelService{} mattermostService := &mocks.MattermostChannelService{} - ctrl := &MattermostController{ - service: notificationService, - mattermostChannelService: mattermostService, - } - group := r.Group("/notification-channel/mattermost") - group.POST("", ctrl.CreateMattermostChannel) - group.GET("", ctrl.ListMattermostChannelsByType) - group.PUT(":id", ctrl.UpdateMattermostChannel) - group.DELETE(":id", ctrl.DeleteMattermostChannel) + NewMattermostController(r, notificationService, mattermostService, testhelper.MockAuthMiddlewareWithAdmin, registry) + return r, notificationService, mattermostService } func TestCreateMattermostChannel_Success(t *testing.T) { r, _, mattermostService := setupTestController() - input := request.MattermostNotificationChannelRequest{ + input := mattermostdto.MattermostNotificationChannelRequest{ ChannelName: "test-channel", - WebhookUrl: "http://webhook", + WebhookUrl: "https://webhookurl.com/hooks/id1", Description: "desc", } - output := response.MattermostNotificationChannelResponse{ + output := mattermostdto.MattermostNotificationChannelResponse{ ChannelName: "test-channel", - WebhookUrl: "http://webhook", + WebhookUrl: "https://webhookurl.com/hooks/id1", Description: "desc", - Id: helper.ToPtr("1"), + Id: "1", } mattermostService.On("CreateMattermostChannel", mock.Anything, input).Return(output, nil) @@ -106,14 +101,14 @@ func TestListMattermostChannelsByType_Error(t *testing.T) { func TestUpdateMattermostChannel_Success(t *testing.T) { r, notificationService, _ := setupTestController() id := "1" - input := request.MattermostNotificationChannelRequest{ + input := mattermostdto.MattermostNotificationChannelRequest{ ChannelName: "test", - WebhookUrl: "url", + WebhookUrl: "https://webhookurl.com/hooks/id1", Description: "desc"} updated := models.NotificationChannel{ Id: helper.ToPtr(id), ChannelName: helper.ToPtr("test"), - WebhookUrl: helper.ToPtr("url"), + WebhookUrl: helper.ToPtr("https://webhookurl.com/hooks/id1"), Description: helper.ToPtr("desc")} notificationService. @@ -126,7 +121,7 @@ func TestUpdateMattermostChannel_Success(t *testing.T) { Expect(). StatusCode(http.StatusOK). JsonPath("$.channelName", "test"). - JsonPath("$.webhookUrl", "url"). + JsonPath("$.webhookUrl", "https://webhookurl.com/hooks/id1"). JsonPath("$.description", "desc"). JsonPath("$.id", id) } diff --git a/pkg/web/mattermostcontroller/mattermostdto/mapper.go b/pkg/web/mattermostcontroller/mattermostdto/mapper.go new file mode 100644 index 0000000..ce277d8 --- /dev/null +++ b/pkg/web/mattermostcontroller/mattermostdto/mapper.go @@ -0,0 +1,34 @@ +package mattermostdto + +import ( + "github.com/greenbone/opensight-notification-service/pkg/helper" + "github.com/greenbone/opensight-notification-service/pkg/models" +) + +// MapNotificationChannelToMattermost maps NotificationChannel to MattermostNotificationChannelRequest. +func MapNotificationChannelToMattermost(channel models.NotificationChannel) MattermostNotificationChannelResponse { + return MattermostNotificationChannelResponse{ + Id: *channel.Id, + ChannelName: helper.SafeDereference(channel.ChannelName), + WebhookUrl: helper.SafeDereference(channel.WebhookUrl), + Description: helper.SafeDereference(channel.Description), + } +} + +func MapMattermostToNotificationChannel(mail MattermostNotificationChannelRequest) models.NotificationChannel { + return models.NotificationChannel{ + ChannelType: string(models.ChannelTypeMattermost), + ChannelName: &mail.ChannelName, + WebhookUrl: &mail.WebhookUrl, + Description: &mail.Description, + } +} + +// MapNotificationChannelsToMattermost maps a slice of NotificationChannel to MattermostNotificationChannelRequest. +func MapNotificationChannelsToMattermost(channels []models.NotificationChannel) []MattermostNotificationChannelResponse { + mattermostChannels := make([]MattermostNotificationChannelResponse, 0, len(channels)) + for _, ch := range channels { + mattermostChannels = append(mattermostChannels, MapNotificationChannelToMattermost(ch)) + } + return mattermostChannels +} diff --git a/pkg/web/mattermostcontroller/mattermostdto/reponse.go b/pkg/web/mattermostcontroller/mattermostdto/reponse.go new file mode 100644 index 0000000..0cbcb68 --- /dev/null +++ b/pkg/web/mattermostcontroller/mattermostdto/reponse.go @@ -0,0 +1,8 @@ +package mattermostdto + +type MattermostNotificationChannelResponse struct { + Id string `json:"id,omitempty"` + ChannelName string `json:"channelName"` + WebhookUrl string `json:"webhookUrl"` + Description string `json:"description"` +} diff --git a/pkg/web/mattermostcontroller/mattermostdto/request.go b/pkg/web/mattermostcontroller/mattermostdto/request.go new file mode 100644 index 0000000..47c5a58 --- /dev/null +++ b/pkg/web/mattermostcontroller/mattermostdto/request.go @@ -0,0 +1,62 @@ +package mattermostdto + +import ( + "strings" + + "github.com/greenbone/opensight-notification-service/pkg/models" + "github.com/greenbone/opensight-notification-service/pkg/policy" +) + +// MattermostNotificationChannelRequest mattermost notification channel request +type MattermostNotificationChannelRequest struct { + ChannelName string `json:"channelName"` + WebhookUrl string `json:"webhookUrl"` + Description string `json:"description"` +} + +func (m *MattermostNotificationChannelRequest) Cleanup() { + m.ChannelName = strings.TrimSpace(m.ChannelName) + m.WebhookUrl = strings.TrimSpace(m.WebhookUrl) + m.Description = strings.TrimSpace(m.Description) +} + +func (m MattermostNotificationChannelRequest) Validate() models.ValidationErrors { + errs := make(models.ValidationErrors) + + if m.ChannelName == "" { + errs["channelName"] = "A channel name is required." + } + + if m.WebhookUrl == "" { + errs["webhookUrl"] = "A Webhook URL is required." + } else { + if _, err := policy.MattermostWebhookUrlPolicy(m.WebhookUrl); err != nil { + errs["webhookUrl"] = "Invalid mattermost webhook URL format." + } + } + + return errs +} + +// MattermostNotificationChannelCheckRequest mattermost notification channel check request +type MattermostNotificationChannelCheckRequest struct { + WebhookUrl string `json:"webhookUrl"` +} + +func (r *MattermostNotificationChannelCheckRequest) Cleanup() { + r.WebhookUrl = strings.TrimSpace(r.WebhookUrl) +} + +func (r *MattermostNotificationChannelCheckRequest) Validate() models.ValidationErrors { + errs := make(models.ValidationErrors) + + if r.WebhookUrl == "" { + errs["webhookUrl"] = "A Webhook URL is required." + } else { + if _, err := policy.MattermostWebhookUrlPolicy(r.WebhookUrl); err != nil { + errs["webhookUrl"] = "Invalid mattermost webhook URL format." + } + } + + return errs +} diff --git a/pkg/web/middleware/error_interpreter.go b/pkg/web/middleware/error_interpreter.go new file mode 100644 index 0000000..be13ead --- /dev/null +++ b/pkg/web/middleware/error_interpreter.go @@ -0,0 +1,43 @@ +package middleware + +import ( + "errors" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/greenbone/opensight-golang-libraries/pkg/errorResponses" + "github.com/greenbone/opensight-notification-service/pkg/models" + "github.com/greenbone/opensight-notification-service/pkg/web/errmap" + "github.com/greenbone/opensight-notification-service/pkg/web/ginEx" +) + +func InterpretErrors(errorType gin.ErrorType, r errmap.ErrorRegistry) gin.HandlerFunc { + return func(c *gin.Context) { + c.Next() + for _, errorValue := range c.Errors.ByType(errorType) { + + actual := errorValue.Unwrap() + + bindingError := ginEx.BindingError{} + if errors.As(actual, &bindingError) { + c.AbortWithStatusJSON(http.StatusBadRequest, errorResponses.NewErrorValidationResponse("unable to parse the request", bindingError.Error(), nil)) + return + } + + validationErrors := models.ValidationErrors{} + if errors.As(actual, &validationErrors) { + c.AbortWithStatusJSON(http.StatusBadRequest, errorResponses.NewErrorValidationResponse("", "", validationErrors)) + return + } + + err, ok := r.Lookup(actual) + if ok { + c.AbortWithStatusJSON(err.Status, err.Response) + return + } + + // Unknown error + c.AbortWithStatusJSON(http.StatusInternalServerError, errorResponses.ErrorInternalResponse) + } + } +} diff --git a/pkg/web/notificationcontroller/notificationController.go b/pkg/web/notificationcontroller/notificationController.go index f046709..d599938 100644 --- a/pkg/web/notificationcontroller/notificationController.go +++ b/pkg/web/notificationcontroller/notificationController.go @@ -11,25 +11,28 @@ import ( "net/http" "time" + "github.com/greenbone/opensight-notification-service/pkg/services/notificationservice" "github.com/greenbone/opensight-notification-service/pkg/web/middleware" - "github.com/greenbone/opensight-notification-service/pkg/web/notificationcontroller/dtos" "github.com/samber/lo" "github.com/gin-gonic/gin" "github.com/greenbone/opensight-golang-libraries/pkg/query" "github.com/greenbone/opensight-notification-service/pkg/errs" "github.com/greenbone/opensight-notification-service/pkg/models" - "github.com/greenbone/opensight-notification-service/pkg/port" "github.com/greenbone/opensight-notification-service/pkg/restErrorHandler" "github.com/greenbone/opensight-notification-service/pkg/web" "github.com/greenbone/opensight-notification-service/pkg/web/helper" ) type NotificationController struct { - notificationService port.NotificationService + notificationService notificationservice.NotificationService } -func AddNotificationController(router gin.IRouter, notificationService port.NotificationService, auth gin.HandlerFunc) { +func AddNotificationController( + router gin.IRouter, + notificationService notificationservice.NotificationService, + auth gin.HandlerFunc, +) { ctrl := &NotificationController{ notificationService: notificationService, } @@ -85,7 +88,7 @@ func (c *NotificationController) CreateNotification(gc *gin.Context) { func (c *NotificationController) ListNotifications(gc *gin.Context) { gc.Header(web.APIVersionKey, web.APIVersion) - resultSelector, err := helper.PrepareResultSelector(gc, dtos.NotificationsRequestOptions, dtos.AllowedNotificationsSortFields, helper.ResultSelectorDefaults(dtos.DefaultSortingRequest)) + resultSelector, err := helper.PrepareResultSelector(gc, NotificationsRequestOptions, AllowedNotificationsSortFields, helper.ResultSelectorDefaults(DefaultSortingRequest)) if err != nil { restErrorHandler.ErrorHandler(gc, "could not prepare result selector", err) return @@ -116,7 +119,7 @@ func (c *NotificationController) ListNotifications(gc *gin.Context) { func (c *NotificationController) GetOptions(gc *gin.Context) { gc.Header(web.APIVersionKey, web.APIVersion) - requestOptions := lo.Map(dtos.NotificationsRequestOptions, web.ToFilterOption) + requestOptions := lo.Map(NotificationsRequestOptions, web.ToFilterOption) response := query.ResponseWithMetadata[[]query.FilterOption]{ Data: requestOptions, } diff --git a/pkg/web/notificationcontroller/notificationController_test.go b/pkg/web/notificationcontroller/notificationController_test.go index f6288a3..742d908 100644 --- a/pkg/web/notificationcontroller/notificationController_test.go +++ b/pkg/web/notificationcontroller/notificationController_test.go @@ -13,13 +13,14 @@ import ( "github.com/greenbone/opensight-golang-libraries/pkg/query/sorting" "github.com/greenbone/opensight-notification-service/pkg/services/notificationservice/dtos" + "github.com/greenbone/opensight-notification-service/pkg/services/notificationservice/mocks" + "github.com/greenbone/opensight-notification-service/pkg/web/errmap" "github.com/greenbone/opensight-golang-libraries/pkg/query" "github.com/greenbone/opensight-golang-libraries/pkg/query/filter" "github.com/greenbone/opensight-golang-libraries/pkg/query/paging" "github.com/greenbone/opensight-notification-service/pkg/helper" "github.com/greenbone/opensight-notification-service/pkg/models" - "github.com/greenbone/opensight-notification-service/pkg/port/mocks" "github.com/greenbone/opensight-notification-service/pkg/web/testhelper" "github.com/stretchr/testify/mock" ) @@ -107,7 +108,8 @@ func TestListNotifications(t *testing.T) { t.Run(tt.name, func(t *testing.T) { mockNotificationService := mocks.NewNotificationService(t) - engine := testhelper.NewTestWebEngine() + registry := errmap.NewRegistry() + engine := testhelper.NewTestWebEngine(registry) AddNotificationController(engine, mockNotificationService, testhelper.MockAuthMiddleware) if tt.want.serviceCall { @@ -188,7 +190,8 @@ func TestCreateNotification(t *testing.T) { t.Run(tt.name, func(t *testing.T) { mockNotificationService := mocks.NewNotificationService(t) - engine := testhelper.NewTestWebEngine() + registry := errmap.NewRegistry() + engine := testhelper.NewTestWebEngine(registry) AddNotificationController(engine, mockNotificationService, testhelper.MockAuthMiddleware) if tt.want.notificationServiceArg != nil { diff --git a/pkg/web/notificationcontroller/dtos/request_options.go b/pkg/web/notificationcontroller/request_options.go similarity index 98% rename from pkg/web/notificationcontroller/dtos/request_options.go rename to pkg/web/notificationcontroller/request_options.go index 40eb388..2e15862 100644 --- a/pkg/web/notificationcontroller/dtos/request_options.go +++ b/pkg/web/notificationcontroller/request_options.go @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: AGPL-3.0-or-later -package dtos +package notificationcontroller import ( "github.com/greenbone/opensight-golang-libraries/pkg/query/filter" diff --git a/pkg/web/teamsController/teamsController.go b/pkg/web/teamsController/teamsController.go new file mode 100644 index 0000000..fff6d8f --- /dev/null +++ b/pkg/web/teamsController/teamsController.go @@ -0,0 +1,194 @@ +package teamsController + +import ( + "errors" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/greenbone/opensight-golang-libraries/pkg/errorResponses" + "github.com/greenbone/opensight-notification-service/pkg/models" + "github.com/greenbone/opensight-notification-service/pkg/services/notificationchannelservice" + "github.com/greenbone/opensight-notification-service/pkg/web/errmap" + "github.com/greenbone/opensight-notification-service/pkg/web/ginEx" + "github.com/greenbone/opensight-notification-service/pkg/web/middleware" + "github.com/greenbone/opensight-notification-service/pkg/web/teamsController/teamsdto" +) + +var ErrTeamsChannelBadRequest = errors.New("bad request for teams channel") + +type TeamsController struct { + notificationChannelServicer notificationchannelservice.NotificationChannelService + teamsChannelService notificationchannelservice.TeamsChannelService +} + +func AddTeamsController( + router gin.IRouter, + notificationChannelServicer notificationchannelservice.NotificationChannelService, + teamsChannelService notificationchannelservice.TeamsChannelService, + auth gin.HandlerFunc, + registry *errmap.Registry, + +) *TeamsController { + ctrl := &TeamsController{ + notificationChannelServicer: notificationChannelServicer, + teamsChannelService: teamsChannelService, + } + + group := router.Group("/notification-channel/teams"). + Use(middleware.AuthorizeRoles(auth, "admin")...) + + group.POST("", ctrl.CreateTeamsChannel) + group.GET("", ctrl.ListTeamsChannels) + group.PUT("/:id", ctrl.UpdateTeamsChannel) + group.DELETE("/:id", ctrl.DeleteTeamsChannel) + group.POST("/check", ctrl.SendTeamsTestMessage) + + ctrl.configureMappings(registry) + return ctrl +} + +func (tc *TeamsController) configureMappings(r *errmap.Registry) { + r.Register( + notificationchannelservice.ErrTeamsChannelLimitReached, + http.StatusUnprocessableEntity, + errorResponses.NewErrorGenericResponse("Teams channel limit reached."), + ) + r.Register( + notificationchannelservice.ErrListTeamsChannels, + http.StatusInternalServerError, + errorResponses.ErrorInternalResponse, + ) + r.Register( + notificationchannelservice.ErrTeamsChannelNameExists, + http.StatusBadRequest, + errorResponses.NewErrorGenericResponse("Teams channel name already exists."), + ) +} + +// CreateTeamsChannel +// +// @Summary Create Teams Channel +// @Description Create a new teams notification channel +// @Tags teams-channel +// @Accept json +// @Produce json +// @Security KeycloakAuth +// @Param TeamsChannel body teamsdto.TeamsNotificationChannelRequest true "Teams channel to add" +// @Success 201 {object} teamsdto.TeamsNotificationChannelRequest +// @Failure 400 {object} map[string]string +// @Failure 500 {object} map[string]string +// @Router /notification-channel/teams [post] +func (tc *TeamsController) CreateTeamsChannel(c *gin.Context) { + var channel teamsdto.TeamsNotificationChannelRequest + if !ginEx.BindAndValidateBody(c, &channel) { + return + } + + teamsChannel, err := tc.teamsChannelService.CreateTeamsChannel(c, channel) + if err != nil { + ginEx.AddError(c, err) + return + } + + c.JSON(http.StatusCreated, teamsChannel) +} + +// ListTeamsChannels +// +// @Summary List Teams Channels +// @Description List teams notification channels by type +// @Tags teams-channel +// @Produce json +// @Security KeycloakAuth +// @Param type query string false "Channel type" +// @Success 200 {array} teamsdto.TeamsNotificationChannelRequest +// @Failure 500 {object} map[string]string +// @Router /notification-channel/teams [get] +func (tc *TeamsController) ListTeamsChannels(c *gin.Context) { + channels, err := tc.notificationChannelServicer.ListNotificationChannelsByType(c, models.ChannelTypeTeams) + if err != nil { + ginEx.AddError(c, err) + return + } + + c.JSON(http.StatusOK, teamsdto.MapNotificationChannelsToTeams(channels)) +} + +// UpdateTeamsChannel +// +// @Summary Update Teams Channel +// @Description Update an existing teams notification channel +// @Tags teams-channel +// @Accept json +// @Produce json +// @Security KeycloakAuth +// @Param id path string true "Teams channel ID" +// @Param TeamsChannel body teamsdto.TeamsNotificationChannelRequest true "Teams channel to update" +// @Success 200 {object} teamsdto.TeamsNotificationChannelRequest +// @Failure 400 {object} map[string]string +// @Failure 404 {object} map[string]string +// @Failure 500 {object} map[string]string +// @Router /notification-channel/teams/{id} [put] +func (tc *TeamsController) UpdateTeamsChannel(c *gin.Context) { + id := c.Param("id") + var channel teamsdto.TeamsNotificationChannelRequest + if !ginEx.BindAndValidateBody(c, &channel) { + return + } + + notificationChannel := teamsdto.MapTeamsToNotificationChannel(channel) + updated, err := tc.notificationChannelServicer.UpdateNotificationChannel(c, id, notificationChannel) + if err != nil { + ginEx.AddError(c, err) + return + } + response := teamsdto.MapNotificationChannelToTeams(updated) + c.JSON(http.StatusOK, response) +} + +// DeleteTeamsChannel +// +// @Summary Delete Teams Channel +// @Description Delete a teams notification channel +// @Tags teams-channel +// @Security KeycloakAuth +// @Param id path string true "Teams channel ID" +// @Success 204 "Deleted successfully" +// @Failure 500 {object} map[string]string +// @Failure 404 {object} map[string]string +// @Router /notification-channel/teams/{id} [delete] +func (tc *TeamsController) DeleteTeamsChannel(c *gin.Context) { + id := c.Param("id") + if err := tc.notificationChannelServicer.DeleteNotificationChannel(c, id); err != nil { + ginEx.AddError(c, err) + return + } + + c.Status(http.StatusNoContent) +} + +// SendTeamsTestMessage +// +// @Summary Check Teams server +// @Description Check if a Teams server is able to send a test message +// @Tags Teams-channel +// @Accept json +// @Produce json +// @Security KeycloakAuth +// @Param id path string true "Teams channel ID" +// @Success 204 "Teams server test message sent successfully" +// @Failure 400 {object} map[string]string +// @Router /notification-channel/Teams/check [post] +func (tc *TeamsController) SendTeamsTestMessage(c *gin.Context) { + var channel teamsdto.TeamsNotificationChannelCheckRequest + if !ginEx.BindAndValidateBody(c, &channel) { + return + } + + if err := tc.teamsChannelService.SendTeamsTestMessage(channel.WebhookUrl); err != nil { + ginEx.AddError(c, err) + return + } + + c.Status(http.StatusNoContent) +} diff --git a/pkg/web/teamsController/teamsController_integration_test.go b/pkg/web/teamsController/teamsController_integration_test.go new file mode 100644 index 0000000..359a7aa --- /dev/null +++ b/pkg/web/teamsController/teamsController_integration_test.go @@ -0,0 +1,172 @@ +//go:build integration +// +build integration + +package teamsController + +import ( + "net/http" + "testing" + + "github.com/gin-gonic/gin" + "github.com/greenbone/opensight-golang-libraries/pkg/httpassert" + "github.com/greenbone/opensight-notification-service/pkg/services/notificationchannelservice" + "github.com/greenbone/opensight-notification-service/pkg/web/errmap" + "github.com/greenbone/opensight-notification-service/pkg/web/teamsController/teamsdto" + "github.com/greenbone/opensight-notification-service/pkg/web/testhelper" + "github.com/jmoiron/sqlx" + _ "github.com/lib/pq" + "github.com/stretchr/testify/require" +) + +func TestIntegration_TeamsController_CRUD(t *testing.T) { + t.Parallel() + + valid := testhelper.GetValidTeamsNotificationChannel() + + t.Run("Perform the Create operation", func(t *testing.T) { + router, db := setupTestRouter(t) + defer db.Close() + request := httpassert.New(t, router) + + var teamsId string + request.Post("/notification-channel/teams"). + Content(`{ + "channelName": "teams1", + "webhookUrl": "https://webhookurl.com/webhook/id1", + "description": "This is a test teams channel" + }`). + Expect(). + StatusCode(http.StatusCreated). + JsonPath("$", httpassert.HasSize(4)). + JsonPath("$.id", httpassert.ExtractTo(&teamsId)). + JsonPath("$.channelName", "teams1"). + JsonPath("$.webhookUrl", "https://webhookurl.com/webhook/id1"). + JsonPath("$.description", "This is a test teams channel") + require.NotEmpty(t, teamsId) + }) + + t.Run("Perform the GET operations", func(t *testing.T) { + router, db := setupTestRouter(t) + defer db.Close() + request := httpassert.New(t, router) + + teamsId := createTeamsNotification(t, request, "teams1", valid) + + request.Get("/notification-channel/teams"). + Expect(). + StatusCode(http.StatusOK). + JsonPath("$", httpassert.HasSize(1)). + JsonPath("$[0]", httpassert.HasSize(4)). + JsonPath("$[0].id", httpassert.ExtractTo(&teamsId)). + JsonPath("$[0].channelName", "teams1"). + JsonPath("$[0].webhookUrl", "https://webhookurl.com/webhook/id1"). + JsonPath("$[0].description", "This is a test teams channel") + }) + + t.Run("Perform the Update operations", func(t *testing.T) { + router, db := setupTestRouter(t) + defer db.Close() + request := httpassert.New(t, router) + + teamsId := createTeamsNotification(t, request, "teams1", valid) + + updated := valid + newName := "updated teams" + updated.ChannelName = newName + request.Putf("/notification-channel/teams/%s", teamsId). + JsonContentObject(updated). + Expect(). + StatusCode(http.StatusOK). + JsonPath("$", httpassert.HasSize(4)). + JsonPath("$.id", teamsId). + JsonPath("$.channelName", newName). + JsonPath("$.webhookUrl", "https://webhookurl.com/webhook/id1"). + JsonPath("$.description", "This is a test teams channel") + }) + + t.Run("Perform the Delete operations", func(t *testing.T) { + router, db := setupTestRouter(t) + defer db.Close() + request := httpassert.New(t, router) + + teamsId := createTeamsNotification(t, request, "teams1", valid) + + request.Delete("/notification-channel/teams/" + teamsId). + Expect(). + StatusCode(http.StatusNoContent) + + request.Get("/notification-channel/teams"). + Expect(). + StatusCode(http.StatusOK). + JsonPath("$", httpassert.HasSize(0)) + }) + + t.Run("Verify Limit check on teams limit", func(t *testing.T) { + repo, db := testhelper.SetupNotificationChannelTestEnv(t) + svc := notificationchannelservice.NewNotificationChannelService(repo) + teamsSvc := notificationchannelservice.NewTeamsChannelService(svc, 1) + + registry := errmap.NewRegistry() + router := testhelper.NewTestWebEngine(registry) + AddTeamsController(router, svc, teamsSvc, testhelper.MockAuthMiddlewareWithAdmin, registry) + defer db.Close() + + request := httpassert.New(t, router) + + createTeamsNotification(t, request, "teams1", valid) + + request.Post("/notification-channel/teams"). + JsonContentObject(valid). + Expect(). + StatusCode(http.StatusUnprocessableEntity). + JsonPath("$.title", "Teams channel limit reached.") + }) + + t.Run("Create two teams channels with the same name", func(t *testing.T) { + router, db := setupTestRouter(t) + defer db.Close() + request := httpassert.New(t, router) + + createTeamsNotification(t, request, "teams1", valid) + + request.Post("/notification-channel/teams"). + JsonContentObject(valid). + Expect(). + StatusCode(http.StatusBadRequest). + JsonPath("$.title", "Teams channel name already exists.") + }) +} + +func createTeamsNotification( + t *testing.T, + request httpassert.Request, + channelName string, + valid teamsdto.TeamsNotificationChannelRequest, +) string { + var teamsId string + valid.ChannelName = channelName + + request.Post("/notification-channel/teams"). + JsonContentObject(valid). + Expect(). + StatusCode(http.StatusCreated). + JsonPath("$", httpassert.HasSize(4)). + JsonPath("$.id", httpassert.ExtractTo(&teamsId)). + JsonPath("$.channelName", channelName). + JsonPath("$.webhookUrl", "https://webhookurl.com/webhook/id1"). + JsonPath("$.description", "This is a test teams channel") + require.NotEmpty(t, teamsId) + + return teamsId +} + +func setupTestRouter(t *testing.T) (*gin.Engine, *sqlx.DB) { + repo, db := testhelper.SetupNotificationChannelTestEnv(t) + svc := notificationchannelservice.NewNotificationChannelService(repo) + teamsSvc := notificationchannelservice.NewTeamsChannelService(svc, 20) + registry := errmap.NewRegistry() + router := testhelper.NewTestWebEngine(registry) + AddTeamsController(router, svc, teamsSvc, testhelper.MockAuthMiddlewareWithAdmin, registry) + + return router, db +} diff --git a/pkg/web/teamsController/teamsdto/mapper.go b/pkg/web/teamsController/teamsdto/mapper.go new file mode 100644 index 0000000..3587890 --- /dev/null +++ b/pkg/web/teamsController/teamsdto/mapper.go @@ -0,0 +1,34 @@ +package teamsdto + +import ( + "github.com/greenbone/opensight-notification-service/pkg/helper" + "github.com/greenbone/opensight-notification-service/pkg/models" +) + +// MapNotificationChannelToTeams maps NotificationChannel to TeamsNotificationChannelRequest. +func MapNotificationChannelToTeams(channel models.NotificationChannel) TeamsNotificationChannelResponse { + return TeamsNotificationChannelResponse{ + Id: *channel.Id, + ChannelName: helper.SafeDereference(channel.ChannelName), + WebhookUrl: helper.SafeDereference(channel.WebhookUrl), + Description: helper.SafeDereference(channel.Description), + } +} + +func MapTeamsToNotificationChannel(mail TeamsNotificationChannelRequest) models.NotificationChannel { + return models.NotificationChannel{ + ChannelType: string(models.ChannelTypeTeams), + ChannelName: &mail.ChannelName, + WebhookUrl: &mail.WebhookUrl, + Description: &mail.Description, + } +} + +// MapNotificationChannelsToTeams maps a slice of NotificationChannel to TeamsNotificationChannelRequest. +func MapNotificationChannelsToTeams(channels []models.NotificationChannel) []TeamsNotificationChannelResponse { + teamsChannels := make([]TeamsNotificationChannelResponse, 0, len(channels)) + for _, ch := range channels { + teamsChannels = append(teamsChannels, MapNotificationChannelToTeams(ch)) + } + return teamsChannels +} diff --git a/pkg/request/MattermostNotificationChannelRequest.go b/pkg/web/teamsController/teamsdto/reponse.go similarity index 54% rename from pkg/request/MattermostNotificationChannelRequest.go rename to pkg/web/teamsController/teamsdto/reponse.go index 8b52357..32bf9ac 100644 --- a/pkg/request/MattermostNotificationChannelRequest.go +++ b/pkg/web/teamsController/teamsdto/reponse.go @@ -1,6 +1,7 @@ -package request +package teamsdto -type MattermostNotificationChannelRequest struct { +type TeamsNotificationChannelResponse struct { + Id string `json:"id,omitempty"` ChannelName string `json:"channelName"` WebhookUrl string `json:"webhookUrl"` Description string `json:"description"` diff --git a/pkg/web/teamsController/teamsdto/requrest.go b/pkg/web/teamsController/teamsdto/requrest.go new file mode 100644 index 0000000..4bd5e99 --- /dev/null +++ b/pkg/web/teamsController/teamsdto/requrest.go @@ -0,0 +1,49 @@ +package teamsdto + +import ( + "github.com/greenbone/opensight-notification-service/pkg/models" + "github.com/greenbone/opensight-notification-service/pkg/policy" +) + +// TeamsNotificationChannelRequest teams notification channel request +type TeamsNotificationChannelRequest struct { + ChannelName string `json:"channelName"` + WebhookUrl string `json:"webhookUrl"` + Description string `json:"description"` +} + +func (m *TeamsNotificationChannelRequest) Validate() models.ValidationErrors { + errs := make(map[string]string) + if m.ChannelName == "" { + errs["channelName"] = "A channel name is required." + } + + if m.WebhookUrl == "" { + errs["webhookUrl"] = "A Webhook URL is required." + } else { + if _, err := policy.TeamsWebhookUrlPolicy(m.WebhookUrl); err != nil { + errs["webhookUrl"] = "Invalid teams webhook URL format." + } + } + + return errs +} + +// TeamsNotificationChannelCheckRequest teams notification channel check request +type TeamsNotificationChannelCheckRequest struct { + WebhookUrl string `json:"webhookUrl"` +} + +func (r *TeamsNotificationChannelCheckRequest) Validate() models.ValidationErrors { + errs := make(models.ValidationErrors) + + if r.WebhookUrl == "" { + errs["webhookUrl"] = "A Webhook URL is required." + } else { + if _, err := policy.TeamsWebhookUrlPolicy(r.WebhookUrl); err != nil { + errs["webhookUrl"] = "Invalid teams webhook URL format." + } + } + + return errs +} diff --git a/pkg/web/testhelper/helper.go b/pkg/web/testhelper/helper.go index fb1b560..738c0d6 100644 --- a/pkg/web/testhelper/helper.go +++ b/pkg/web/testhelper/helper.go @@ -14,10 +14,11 @@ import ( "github.com/greenbone/opensight-notification-service/pkg/config" "github.com/greenbone/opensight-notification-service/pkg/helper" "github.com/greenbone/opensight-notification-service/pkg/pgtesting" - "github.com/greenbone/opensight-notification-service/pkg/port" "github.com/greenbone/opensight-notification-service/pkg/repository/notificationrepository" - "github.com/greenbone/opensight-notification-service/pkg/request" "github.com/greenbone/opensight-notification-service/pkg/security" + "github.com/greenbone/opensight-notification-service/pkg/web/mailcontroller/maildto" + "github.com/greenbone/opensight-notification-service/pkg/web/mattermostcontroller/mattermostdto" + "github.com/greenbone/opensight-notification-service/pkg/web/teamsController/teamsdto" "github.com/jmoiron/sqlx" "github.com/stretchr/testify/assert" ) @@ -96,7 +97,7 @@ func MockAuthMiddlewareWithAdmin(ctx *gin.Context) { ctx.Next() } -func SetupNotificationChannelTestEnv(t *testing.T) (port.NotificationChannelRepository, *sqlx.DB) { +func SetupNotificationChannelTestEnv(t *testing.T) (notificationrepository.NotificationChannelRepository, *sqlx.DB) { encryptMgr := security.NewEncryptManager() encryptMgr.UpdateKeys(config.DatabaseEncryptionKey{ Password: "password", @@ -112,8 +113,8 @@ func SetupNotificationChannelTestEnv(t *testing.T) (port.NotificationChannelRepo return repo, db } -func GetValidMailNotificationChannel() request.MailNotificationChannelRequest { - return request.MailNotificationChannelRequest{ +func GetValidMailNotificationChannel() maildto.MailNotificationChannelRequest { + return maildto.MailNotificationChannelRequest{ ChannelName: "mail1", Domain: "example.com", Port: 25, @@ -127,10 +128,18 @@ func GetValidMailNotificationChannel() request.MailNotificationChannelRequest { } } -func GetValidMattermostNotificationChannel() request.MattermostNotificationChannelRequest { - return request.MattermostNotificationChannelRequest{ +func GetValidMattermostNotificationChannel() mattermostdto.MattermostNotificationChannelRequest { + return mattermostdto.MattermostNotificationChannelRequest{ ChannelName: "mattermost1", - WebhookUrl: "http://webhookurl.com/id1", + WebhookUrl: "https://webhookurl.com/hooks/id1", Description: "This is a test mattermost channel", } } + +func GetValidTeamsNotificationChannel() teamsdto.TeamsNotificationChannelRequest { + return teamsdto.TeamsNotificationChannelRequest{ + ChannelName: "teams1", + WebhookUrl: "https://webhookurl.com/webhook/id1", + Description: "This is a test teams channel", + } +} diff --git a/pkg/web/testhelper/testWebEngine.go b/pkg/web/testhelper/testWebEngine.go index f0802e6..2f3eb7a 100644 --- a/pkg/web/testhelper/testWebEngine.go +++ b/pkg/web/testhelper/testWebEngine.go @@ -2,14 +2,24 @@ package testhelper import ( "github.com/gin-gonic/gin" - "github.com/greenbone/opensight-notification-service/pkg/web/helper" + logsMiddleware "github.com/greenbone/opensight-golang-libraries/pkg/logs/ginMiddleware" + "github.com/greenbone/opensight-notification-service/pkg/web/errmap" + "github.com/greenbone/opensight-notification-service/pkg/web/middleware" ) -func NewTestWebEngine() *gin.Engine { +func NewTestWebEngine(registry *errmap.Registry) *gin.Engine { gin.SetMode(gin.TestMode) - router := gin.New() - router.Use(helper.ValidationErrorHandler(gin.ErrorTypePrivate)) + ginWebEngine := gin.New() + ginWebEngine.Use( + logsMiddleware.Logging(), + gin.Recovery(), + middleware.CORS([]string{ + "http://example.com", + }), + middleware.ErrorHandler(gin.ErrorTypeAny), + ) + ginWebEngine.Use(middleware.InterpretErrors(gin.ErrorTypePrivate, registry)) - return router + return ginWebEngine } diff --git a/pkg/web/web.go b/pkg/web/web.go index 4a74943..389e3ba 100644 --- a/pkg/web/web.go +++ b/pkg/web/web.go @@ -8,10 +8,11 @@ import ( "github.com/gin-gonic/gin" logsMiddleware "github.com/greenbone/opensight-golang-libraries/pkg/logs/ginMiddleware" "github.com/greenbone/opensight-notification-service/pkg/config" + "github.com/greenbone/opensight-notification-service/pkg/web/errmap" "github.com/greenbone/opensight-notification-service/pkg/web/middleware" ) -func NewWebEngine(httpConfig config.Http) *gin.Engine { +func NewWebEngine(httpConfig config.Http, registry *errmap.Registry) *gin.Engine { ginWebEngine := gin.New() ginWebEngine.Use( logsMiddleware.Logging(), @@ -19,5 +20,6 @@ func NewWebEngine(httpConfig config.Http) *gin.Engine { middleware.CORS(httpConfig.AllowedOrigins), middleware.ErrorHandler(gin.ErrorTypeAny), ) + ginWebEngine.Use(middleware.InterpretErrors(gin.ErrorTypePrivate, registry)) return ginWebEngine }