-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: send slack notifications to authorized workspaces (#37)
* feat: setup api to send slack notification Co-authored-by: Praveen Yadav <pyadav9678@gmail.com> * feat: send notifications to users and joined public/private channels Co-authored-by: Praveen Yadav <pyadav9678@gmail.com> * test: fix failing tests Co-authored-by: Praveen Yadav <pyadav9678@gmail.com> * test: refactor and test slack notifier client Co-authored-by: Praveen Yadav <pyadav9678@gmail.com> * test: refactor and test slack notifier service Co-authored-by: Praveen Yadav <pyadav9678@gmail.com> * test: slack notifier handler Co-authored-by: Praveen Yadav <pyadav9678@gmail.com> * test: fetching access token for a workspace * docs: add swagger schema * feat: add request payload validation in notifications api handler * refactor: move validation into domain Co-authored-by: Abhishek <abhi.sah.97@gmail.com> * refactor: custom validation using oneof tag Co-authored-by: Abhishek <abhi.sah.97@gmail.com> * refactor: use custom error Co-authored-by: Abhishek <abhi.sah.97@gmail.com> Co-authored-by: Praveen Yadav <pyadav9678@gmail.com>
- Loading branch information
Showing
26 changed files
with
1,407 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
package handlers | ||
|
||
import ( | ||
"encoding/json" | ||
"github.com/odpf/siren/domain" | ||
"github.com/odpf/siren/pkg/slacknotifier" | ||
"github.com/pkg/errors" | ||
"go.uber.org/zap" | ||
"gopkg.in/go-playground/validator.v9" | ||
"net/http" | ||
) | ||
|
||
// Notify handler | ||
func Notify(notifierServices domain.NotifierServices, logger *zap.Logger) http.HandlerFunc { | ||
return func(w http.ResponseWriter, r *http.Request) { | ||
provider := r.URL.Query().Get("provider") | ||
switch provider { | ||
case "slack": | ||
var payload domain.SlackMessage | ||
err := json.NewDecoder(r.Body).Decode(&payload) | ||
if err != nil { | ||
badRequest(w, err, logger) | ||
return | ||
} | ||
|
||
err = payload.Validate() | ||
if err != nil { | ||
var e *validator.InvalidValidationError | ||
if errors.As(err, &e) { | ||
logger.Error("invalid validation error") | ||
internalServerError(w, err, logger) | ||
return | ||
} | ||
badRequest(w, err, logger) | ||
return | ||
} | ||
result, err := notifierServices.Slack.Notify(&payload) | ||
if err != nil { | ||
if isBadRequest(err) { | ||
badRequest(w, err, logger) | ||
return | ||
} | ||
internalServerError(w, err, logger) | ||
return | ||
} | ||
returnJSON(w, result) | ||
return | ||
case "": | ||
badRequest(w, errors.New("provider not given in query params"), logger) | ||
return | ||
default: | ||
badRequest(w, errors.New("unrecognized provider"), logger) | ||
return | ||
} | ||
} | ||
} | ||
|
||
func isBadRequest(err error) bool { | ||
|
||
var noChannelFoundError *slacknotifier.NoChannelFoundErr | ||
var userLookupByEmailErr *slacknotifier.UserLookupByEmailErr | ||
|
||
return errors.As(err, &noChannelFoundError) || errors.As(err, &userLookupByEmailErr) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
package handlers_test | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"errors" | ||
"github.com/odpf/siren/api/handlers" | ||
"github.com/odpf/siren/domain" | ||
"github.com/odpf/siren/mocks" | ||
"github.com/odpf/siren/pkg/slacknotifier" | ||
"github.com/stretchr/testify/assert" | ||
"net/http" | ||
"net/http/httptest" | ||
"testing" | ||
) | ||
|
||
func TestNotifier_Notify(t *testing.T) { | ||
t.Run("should return 200 OK on success", func(t *testing.T) { | ||
mockedSlackNotifierService := &mocks.SlackNotifierService{} | ||
notifierServices := domain.NotifierServices{Slack: mockedSlackNotifierService} | ||
dummyMessage := domain.SlackMessage{ReceiverName: "foo", | ||
ReceiverType: "user", | ||
Message: "some text", | ||
Entity: "odpf"} | ||
|
||
payload := []byte(`{"receiver_name": "foo","receiver_type": "user","entity": "odpf","message": "some text"}`) | ||
expectedResponse := domain.SlackMessageSendResponse{ | ||
OK: true, | ||
} | ||
mockedSlackNotifierService.On("Notify", &dummyMessage).Return(&expectedResponse, nil).Once() | ||
r, err := http.NewRequest(http.MethodPost, "/notifications?provider=slack", bytes.NewBuffer(payload)) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
w := httptest.NewRecorder() | ||
handler := handlers.Notify(notifierServices, getPanicLogger()) | ||
expectedStatusCode := http.StatusOK | ||
response, _ := json.Marshal(expectedResponse) | ||
expectedStringBody := string(response) + "\n" | ||
|
||
handler.ServeHTTP(w, r) | ||
|
||
assert.Equal(t, expectedStatusCode, w.Code) | ||
assert.Equal(t, expectedStringBody, w.Body.String()) | ||
}) | ||
|
||
t.Run("should return 500 Internal server error on failure", func(t *testing.T) { | ||
mockedSlackNotifierService := &mocks.SlackNotifierService{} | ||
notifierServices := domain.NotifierServices{Slack: mockedSlackNotifierService} | ||
dummyMessage := domain.SlackMessage{ReceiverName: "foo", | ||
ReceiverType: "user", | ||
Message: "some text", | ||
Entity: "odpf"} | ||
|
||
payload := []byte(`{"receiver_name": "foo","receiver_type": "user","entity": "odpf","message": "some text"}`) | ||
mockedSlackNotifierService.On("Notify", &dummyMessage).Return(nil, errors.New("random error")).Once() | ||
r, err := http.NewRequest(http.MethodPost, "/notifications?provider=slack", bytes.NewBuffer(payload)) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
w := httptest.NewRecorder() | ||
handler := handlers.Notify(notifierServices, getPanicLogger()) | ||
expectedStatusCode := http.StatusInternalServerError | ||
expectedStringBody := `{"code":500,"message":"Internal server error","data":null}` | ||
handler.ServeHTTP(w, r) | ||
assert.Equal(t, expectedStatusCode, w.Code) | ||
assert.Equal(t, expectedStringBody, w.Body.String()) | ||
}) | ||
|
||
t.Run("should return 400 Bad request if app not part of channel", func(t *testing.T) { | ||
mockedSlackNotifierService := &mocks.SlackNotifierService{} | ||
notifierServices := domain.NotifierServices{Slack: mockedSlackNotifierService} | ||
dummyMessage := domain.SlackMessage{ReceiverName: "test", | ||
ReceiverType: "channel", | ||
Message: "some text", | ||
Entity: "odpf"} | ||
expectedError := &slacknotifier.NoChannelFoundErr{ | ||
Err: errors.New("app is not part of test"), | ||
} | ||
payload := []byte(`{"receiver_name": "test","receiver_type": "channel","entity": "odpf","message": "some text"}`) | ||
mockedSlackNotifierService.On("Notify", &dummyMessage).Return(nil, expectedError).Once() | ||
r, err := http.NewRequest(http.MethodPost, "/notifications?provider=slack", bytes.NewBuffer(payload)) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
w := httptest.NewRecorder() | ||
handler := handlers.Notify(notifierServices, getPanicLogger()) | ||
expectedStatusCode := http.StatusBadRequest | ||
expectedStringBody := `{"code":400,"message":"app is not part of test","data":null}` | ||
handler.ServeHTTP(w, r) | ||
assert.Equal(t, expectedStatusCode, w.Code) | ||
assert.Equal(t, expectedStringBody, w.Body.String()) | ||
}) | ||
|
||
t.Run("should return 400 Bad request if user not found", func(t *testing.T) { | ||
mockedSlackNotifierService := &mocks.SlackNotifierService{} | ||
notifierServices := domain.NotifierServices{Slack: mockedSlackNotifierService} | ||
dummyMessage := domain.SlackMessage{ReceiverName: "foo", | ||
ReceiverType: "user", | ||
Message: "some text", | ||
Entity: "odpf"} | ||
expectedError := &slacknotifier.UserLookupByEmailErr{ | ||
Err: errors.New("failed to get id for foo"), | ||
} | ||
payload := []byte(`{"receiver_name": "foo","receiver_type": "user","entity": "odpf","message": "some text"}`) | ||
mockedSlackNotifierService.On("Notify", &dummyMessage).Return(nil, expectedError).Once() | ||
r, err := http.NewRequest(http.MethodPost, "/notifications?provider=slack", bytes.NewBuffer(payload)) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
w := httptest.NewRecorder() | ||
handler := handlers.Notify(notifierServices, getPanicLogger()) | ||
expectedStatusCode := http.StatusBadRequest | ||
expectedStringBody := `{"code":400,"message":"failed to get id for foo","data":null}` | ||
handler.ServeHTTP(w, r) | ||
assert.Equal(t, expectedStatusCode, w.Code) | ||
assert.Equal(t, expectedStringBody, w.Body.String()) | ||
}) | ||
|
||
t.Run("should return 400 Bad request if no provider specified", func(t *testing.T) { | ||
mockedSlackNotifierService := &mocks.SlackNotifierService{} | ||
notifierServices := domain.NotifierServices{Slack: mockedSlackNotifierService} | ||
payload := []byte(`{"receiver_name": "foo","receiver_type": "user","entity": "odpf","message": "some text"}`) | ||
r, err := http.NewRequest(http.MethodPost, "/notifications", bytes.NewBuffer(payload)) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
w := httptest.NewRecorder() | ||
handler := handlers.Notify(notifierServices, getPanicLogger()) | ||
expectedStatusCode := http.StatusBadRequest | ||
expectedStringBody := `{"code":400,"message":"provider not given in query params","data":null}` | ||
handler.ServeHTTP(w, r) | ||
assert.Equal(t, expectedStatusCode, w.Code) | ||
assert.Equal(t, expectedStringBody, w.Body.String()) | ||
}) | ||
|
||
t.Run("should return 400 Bad request if unknown provider specified", func(t *testing.T) { | ||
mockedSlackNotifierService := &mocks.SlackNotifierService{} | ||
notifierServices := domain.NotifierServices{Slack: mockedSlackNotifierService} | ||
payload := []byte(`{"receiver_name": "foo","receiver_type": "user","entity": "odpf","message": "some text"}`) | ||
r, err := http.NewRequest(http.MethodPost, "/notifications?provider=email", bytes.NewBuffer(payload)) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
w := httptest.NewRecorder() | ||
handler := handlers.Notify(notifierServices, getPanicLogger()) | ||
expectedStatusCode := http.StatusBadRequest | ||
expectedStringBody := `{"code":400,"message":"unrecognized provider","data":null}` | ||
handler.ServeHTTP(w, r) | ||
assert.Equal(t, expectedStatusCode, w.Code) | ||
assert.Equal(t, expectedStringBody, w.Body.String()) | ||
}) | ||
|
||
t.Run("should return 400 Bad request for invalid payload", func(t *testing.T) { | ||
mockedSlackNotifierService := &mocks.SlackNotifierService{} | ||
notifierServices := domain.NotifierServices{Slack: mockedSlackNotifierService} | ||
payload := []byte(`abcd`) | ||
r, err := http.NewRequest(http.MethodPost, "/notifications?provider=slack", bytes.NewBuffer(payload)) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
w := httptest.NewRecorder() | ||
handler := handlers.Notify(notifierServices, getPanicLogger()) | ||
expectedStatusCode := http.StatusBadRequest | ||
expectedStringBody := `{"code":400,"message":"invalid character 'a' looking for beginning of value","data":null}` | ||
handler.ServeHTTP(w, r) | ||
assert.Equal(t, expectedStatusCode, w.Code) | ||
assert.Equal(t, expectedStringBody, w.Body.String()) | ||
}) | ||
|
||
t.Run("should return 400 Bad request if payload validation", func(t *testing.T) { | ||
mockedSlackNotifierService := &mocks.SlackNotifierService{} | ||
notifierServices := domain.NotifierServices{Slack: mockedSlackNotifierService} | ||
payload := []byte(`{"receiver_name": "","receiver_type": "","entity": "","message": ""}`) | ||
r, err := http.NewRequest(http.MethodPost, "/notifications?provider=slack", bytes.NewBuffer(payload)) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
w := httptest.NewRecorder() | ||
handler := handlers.Notify(notifierServices, getPanicLogger()) | ||
expectedStatusCode := http.StatusBadRequest | ||
handler.ServeHTTP(w, r) | ||
assert.Equal(t, expectedStatusCode, w.Code) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package docs | ||
|
||
import "github.com/odpf/siren/domain" | ||
|
||
//------------------------- | ||
//------------------------- | ||
// swagger:route POST /notifications notifications postNotificationsRequest | ||
// POST Notifications API | ||
// This API sends notifications to configured channel | ||
// responses: | ||
// 200: postResponse | ||
|
||
// swagger:parameters postNotificationsRequest | ||
type postNotificationsRequest struct { | ||
// in:query | ||
Provider string `json:"provider"` | ||
// in:body | ||
Body domain.SlackMessage | ||
} | ||
|
||
// POST notifications response | ||
// swagger:response postNotificationsResponse | ||
type postNotificationsResponse struct { | ||
// in:body | ||
Body domain.SlackMessageSendResponse | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
package domain | ||
|
||
import ( | ||
"gopkg.in/go-playground/validator.v9" | ||
) | ||
|
||
type NotifierServices struct { | ||
Slack SlackNotifierService | ||
} | ||
|
||
type SlackMessageSendResponse struct { | ||
OK bool `json:"ok"` | ||
} | ||
|
||
type SlackNotifierService interface { | ||
Notify(*SlackMessage) (*SlackMessageSendResponse, error) | ||
} | ||
|
||
type SlackMessage struct { | ||
ReceiverName string `json:"receiver_name" validate:"required"` | ||
ReceiverType string `json:"receiver_type" validate:"required,oneof=user channel"` | ||
Entity string `json:"entity" validate:"required"` | ||
Message string `json:"message" validate:"required"` | ||
} | ||
|
||
func (sm *SlackMessage) Validate() error { | ||
v := validator.New() | ||
return v.Struct(sm) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.