diff --git a/http/api/endpoint_test.go b/http/api/endpoint_test.go index 231120eb17..a4587879f7 100644 --- a/http/api/endpoint_test.go +++ b/http/api/endpoint_test.go @@ -17,11 +17,14 @@ import ( "github.com/absmach/magistrala/http/api" grpcChannelsV1 "github.com/absmach/magistrala/internal/grpc/channels/v1" grpcClientsV1 "github.com/absmach/magistrala/internal/grpc/clients/v1" + "github.com/absmach/magistrala/internal/testsutil" mglog "github.com/absmach/magistrala/logger" "github.com/absmach/magistrala/pkg/apiutil" mgauthn "github.com/absmach/magistrala/pkg/authn" authnMocks "github.com/absmach/magistrala/pkg/authn/mocks" + "github.com/absmach/magistrala/pkg/connections" pubsub "github.com/absmach/magistrala/pkg/messaging/mocks" + "github.com/absmach/magistrala/pkg/policies" "github.com/absmach/mgate" proxy "github.com/absmach/mgate/pkg/http" "github.com/absmach/mgate/pkg/session" @@ -34,6 +37,8 @@ const ( invalidValue = "invalid" ) +var clientID = testsutil.GenerateUUID(&testing.T{}) + func newService(authn mgauthn.Authentication, clients grpcClientsV1.ClientsServiceClient, channels grpcChannelsV1.ChannelsServiceClient) (session.Handler, *pubsub.PubSub) { pub := new(pubsub.PubSub) return server.NewHandler(pub, authn, clients, channels, mglog.NewMock()), pub @@ -76,7 +81,7 @@ func (tr testRequest) make() (*http.Response, error) { req.Header.Set("Authorization", apiutil.ClientPrefix+tr.token) } if tr.basicAuth && tr.token != "" { - req.SetBasicAuth("", tr.token) + req.SetBasicAuth("", apiutil.ClientPrefix+tr.token) } if tr.contentType != "" { req.Header.Set("Content-Type", tr.contentType) @@ -105,83 +110,119 @@ func TestPublish(t *testing.T) { defer ts.Close() - cases := map[string]struct { + cases := []struct { + desc string chanID string msg string contentType string key string status int basicAuth bool + authnErr error + authnRes *grpcClientsV1.AuthnRes + authzRes *grpcChannelsV1.AuthzRes + authzErr error + err error }{ - "publish message": { + { + desc: "publish message successfully", chanID: chanID, msg: msg, contentType: ctSenmlJSON, key: clientKey, status: http.StatusAccepted, + authnRes: &grpcClientsV1.AuthnRes{Id: clientID, Authenticated: true}, + authzRes: &grpcChannelsV1.AuthzRes{Authorized: true}, }, - "publish message with application/senml+cbor content-type": { + { + desc: "publish message with application/senml+cbor content-type", chanID: chanID, msg: msgCBOR, contentType: ctSenmlCBOR, key: clientKey, status: http.StatusAccepted, + authnRes: &grpcClientsV1.AuthnRes{Id: clientID, Authenticated: true}, + authzRes: &grpcChannelsV1.AuthzRes{Authorized: true}, }, - "publish message with application/json content-type": { + { + desc: "publish message with application/json content-type", chanID: chanID, msg: msgJSON, contentType: ctJSON, key: clientKey, status: http.StatusAccepted, + authnRes: &grpcClientsV1.AuthnRes{Id: clientID, Authenticated: true}, + authzRes: &grpcChannelsV1.AuthzRes{Authorized: true}, }, - "publish message with empty key": { + { + desc: "publish message with empty key", chanID: chanID, msg: msg, contentType: ctSenmlJSON, key: "", status: http.StatusBadGateway, }, - "publish message with basic auth": { + { + desc: "publish message with basic auth", chanID: chanID, msg: msg, contentType: ctSenmlJSON, key: clientKey, basicAuth: true, status: http.StatusAccepted, + authnRes: &grpcClientsV1.AuthnRes{Id: clientID, Authenticated: true}, + authzRes: &grpcChannelsV1.AuthzRes{Authorized: true}, }, - "publish message with invalid key": { + { + desc: "publish message with invalid key", chanID: chanID, msg: msg, contentType: ctSenmlJSON, key: invalidKey, status: http.StatusUnauthorized, + authnRes: &grpcClientsV1.AuthnRes{Authenticated: false}, }, - "publish message with invalid basic auth": { + { + desc: "publish message with invalid basic auth", chanID: chanID, msg: msg, contentType: ctSenmlJSON, key: invalidKey, basicAuth: true, status: http.StatusUnauthorized, + authnRes: &grpcClientsV1.AuthnRes{Authenticated: false}, }, - "publish message without content type": { + { + desc: "publish message without content type", chanID: chanID, msg: msg, contentType: "", key: clientKey, status: http.StatusUnsupportedMediaType, + authnRes: &grpcClientsV1.AuthnRes{Id: clientID, Authenticated: true}, + authzRes: &grpcChannelsV1.AuthzRes{Authorized: true}, }, - "publish message to invalid channel": { + { + desc: "publish message to invalid channel", chanID: "", msg: msg, contentType: ctSenmlJSON, key: clientKey, status: http.StatusBadRequest, + authnRes: &grpcClientsV1.AuthnRes{Id: clientID, Authenticated: true}, + authzRes: &grpcChannelsV1.AuthzRes{Authorized: false}, }, } - for desc, tc := range cases { - t.Run(desc, func(t *testing.T) { + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + clientsCall := clients.On("Authenticate", mock.Anything, &grpcClientsV1.AuthnReq{ClientSecret: tc.key}).Return(tc.authnRes, tc.authnErr) + channelsCall := channels.On("Authorize", mock.Anything, &grpcChannelsV1.AuthzReq{ + ChannelId: tc.chanID, + ClientId: clientID, + ClientType: policies.ClientType, + Type: uint32(connections.Publish), + }).Return(tc.authzRes, tc.authzErr) svcCall := pub.On("Publish", mock.Anything, tc.chanID, mock.Anything).Return(nil) req := testRequest{ client: ts.Client(), @@ -193,9 +234,11 @@ func TestPublish(t *testing.T) { basicAuth: tc.basicAuth, } res, err := req.make() - assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", desc, err)) - assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", desc, tc.status, res.StatusCode)) + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) svcCall.Unset() + clientsCall.Unset() + channelsCall.Unset() }) } } diff --git a/http/handler_test.go b/http/handler_test.go new file mode 100644 index 0000000000..8a9df323b6 --- /dev/null +++ b/http/handler_test.go @@ -0,0 +1,351 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package http_test + +import ( + "context" + "fmt" + "net/http" + "testing" + + chmocks "github.com/absmach/magistrala/channels/mocks" + clmocks "github.com/absmach/magistrala/clients/mocks" + mhttp "github.com/absmach/magistrala/http" + grpcChannelsV1 "github.com/absmach/magistrala/internal/grpc/channels/v1" + grpcClientsV1 "github.com/absmach/magistrala/internal/grpc/clients/v1" + "github.com/absmach/magistrala/internal/testsutil" + mglog "github.com/absmach/magistrala/logger" + "github.com/absmach/magistrala/pkg/apiutil" + mgauthn "github.com/absmach/magistrala/pkg/authn" + authnmocks "github.com/absmach/magistrala/pkg/authn/mocks" + "github.com/absmach/magistrala/pkg/errors" + svcerr "github.com/absmach/magistrala/pkg/errors/service" + "github.com/absmach/magistrala/pkg/messaging/mocks" + mghttp "github.com/absmach/mgate/pkg/http" + "github.com/absmach/mgate/pkg/session" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +const ( + clientID = "513d02d2-16c1-4f23-98be-9e12f8fee898" + clientID1 = "513d02d2-16c1-4f23-98be-9e12f8fee899" + clientKey = "password" + chanID = "123e4567-e89b-12d3-a456-000000000001" + invalidID = "invalidID" + invalidValue = "invalidValue" + invalidChannelIDTopic = "channels/**/messages" +) + +var ( + topicMsg = "channels/%s/messages" + subtopicMsg = "channels/%s/messages/subtopic" + topic = fmt.Sprintf(topicMsg, chanID) + subtopic = fmt.Sprintf(subtopicMsg, chanID) + invalidTopic = invalidValue + payload = []byte("[{'n':'test-name', 'v': 1.2}]") + sessionClient = session.Session{ + ID: clientID, + Password: []byte(clientKey), + } + validToken = "token" + validID = testsutil.GenerateUUID(&testing.T{}) + errClientNotInitialized = errors.New("client is not initialized") + errFailedPublish = errors.New("failed to publish") + errMissingTopicPub = errors.New("failed to publish due to missing topic") + errMalformedTopic = errors.New("malformed topic") + errFailedParseSubtopic = errors.New("failed to parse subtopic") + errMalformedSubtopic = errors.New("malformed subtopic") + errFailedPublishToMsgBroker = errors.New("failed to publish to magistrala message broker") +) + +var ( + clients = new(clmocks.ClientsServiceClient) + channels = new(chmocks.ChannelsServiceClient) + authn = new(authnmocks.Authentication) + publisher = new(mocks.PubSub) +) + +func newHandler() session.Handler { + logger := mglog.NewMock() + authn = new(authnmocks.Authentication) + clients = new(clmocks.ClientsServiceClient) + channels = new(chmocks.ChannelsServiceClient) + publisher = new(mocks.PubSub) + + return mhttp.NewHandler(publisher, authn, clients, channels, logger) +} + +func TestAuthConnect(t *testing.T) { + handler := newHandler() + + cases := []struct { + desc string + session *session.Session + status int + err error + }{ + { + desc: "connect with valid username and password", + err: nil, + session: &sessionClient, + }, + { + desc: "connect without active session", + session: nil, + status: http.StatusUnauthorized, + err: errClientNotInitialized, + }, + { + desc: "connect with empty key", + session: &session.Session{ + ID: clientID, + Password: []byte(""), + }, + status: http.StatusBadRequest, + err: errors.Wrap(apiutil.ErrValidation, apiutil.ErrBearerKey), + }, + { + desc: "connect with client key", + session: &session.Session{ + ID: clientID, + Password: []byte("Client " + clientKey), + }, + err: nil, + }, + } + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + ctx := context.TODO() + if tc.session != nil { + ctx = session.NewContext(ctx, tc.session) + } + err := handler.AuthConnect(ctx) + hpe, ok := err.(mghttp.HTTPProxyError) + if ok { + assert.Equal(t, tc.status, hpe.StatusCode()) + } + assert.True(t, errors.Contains(err, tc.err)) + }) + } +} + +func TestPublish(t *testing.T) { + handler := newHandler() + + malformedSubtopics := topic + "/" + subtopic + "%" + + clientKeySession := session.Session{ + Password: []byte("Client " + clientKey), + } + + tokenSession := session.Session{ + Password: []byte(apiutil.BearerPrefix + validToken), + } + cases := []struct { + desc string + topic *string + channelID string + payload *[]byte + password string + session *session.Session + status int + authNRes *grpcClientsV1.AuthnRes + authNRes1 mgauthn.Session + authNErr error + authZRes *grpcChannelsV1.AuthzRes + authZErr error + publishErr error + err error + }{ + { + desc: "publish with key successfully", + topic: &topic, + payload: &payload, + password: clientKey, + session: &clientKeySession, + channelID: chanID, + authNRes: &grpcClientsV1.AuthnRes{Id: clientID, Authenticated: true}, + authNErr: nil, + authZRes: &grpcChannelsV1.AuthzRes{Authorized: true}, + authZErr: nil, + err: nil, + }, + { + desc: "publish with token successfully", + topic: &topic, + payload: &payload, + password: validToken, + session: &tokenSession, + channelID: chanID, + authNRes1: mgauthn.Session{DomainUserID: validID, UserID: validID, DomainID: validID}, + authNErr: nil, + authZRes: &grpcChannelsV1.AuthzRes{Authorized: true}, + authZErr: nil, + err: nil, + }, + { + desc: "publish with key and subtopic successfully", + topic: &subtopic, + payload: &payload, + password: clientKey, + session: &clientKeySession, + channelID: chanID, + authNRes: &grpcClientsV1.AuthnRes{Id: clientID, Authenticated: true}, + authNErr: nil, + authZRes: &grpcChannelsV1.AuthzRes{Authorized: true}, + authZErr: nil, + err: nil, + }, + { + desc: "publish with empty topic", + topic: nil, + payload: &payload, + session: &clientKeySession, + channelID: chanID, + status: http.StatusBadRequest, + err: errMissingTopicPub, + }, + { + desc: "publish with invalid session", + topic: &topic, + payload: &payload, + session: nil, + channelID: chanID, + status: http.StatusUnauthorized, + err: errClientNotInitialized, + }, + { + desc: "publish with invalid topic", + topic: &invalidTopic, + status: http.StatusBadRequest, + password: clientKey, + session: &clientKeySession, + authNRes: &grpcClientsV1.AuthnRes{Id: clientID, Authenticated: true}, + authNErr: nil, + err: errors.Wrap(errFailedPublish, errMalformedTopic), + }, + { + desc: "publish with malformwd subtopic", + topic: &malformedSubtopics, + status: http.StatusBadRequest, + password: clientKey, + session: &clientKeySession, + authNRes: &grpcClientsV1.AuthnRes{Id: clientID, Authenticated: true}, + authNErr: nil, + err: errors.Wrap(errFailedParseSubtopic, errMalformedSubtopic), + }, + { + desc: "publish with empty password", + topic: &topic, + payload: &payload, + session: &session.Session{ + Password: []byte(""), + }, + channelID: chanID, + status: http.StatusUnauthorized, + err: svcerr.ErrAuthentication, + }, + { + desc: "publish with thing key and failed to authenticate", + topic: &topic, + payload: &payload, + password: clientKey, + session: &clientKeySession, + channelID: chanID, + status: http.StatusUnauthorized, + authNRes: &grpcClientsV1.AuthnRes{Id: clientID, Authenticated: false}, + authNErr: nil, + err: svcerr.ErrAuthentication, + }, + { + desc: "publish with thing key and failed to authenticate with error", + topic: &topic, + payload: &payload, + password: clientKey, + session: &clientKeySession, + channelID: chanID, + status: http.StatusUnauthorized, + authNRes: &grpcClientsV1.AuthnRes{Id: clientID, Authenticated: false}, + authNErr: svcerr.ErrAuthentication, + err: svcerr.ErrAuthentication, + }, + { + desc: "publish with token and failed to authenticate", + topic: &topic, + payload: &payload, + password: validToken, + session: &tokenSession, + channelID: chanID, + status: http.StatusUnauthorized, + authNRes1: mgauthn.Session{DomainUserID: validID, UserID: validID, DomainID: validID}, + authNErr: svcerr.ErrAuthentication, + err: svcerr.ErrAuthentication, + }, + { + desc: "publish with unauthorized client", + topic: &topic, + payload: &payload, + password: clientKey, + session: &clientKeySession, + channelID: chanID, + authNRes: &grpcClientsV1.AuthnRes{Id: clientID, Authenticated: true}, + status: http.StatusUnauthorized, + authNErr: nil, + authZRes: &grpcChannelsV1.AuthzRes{Authorized: false}, + authZErr: nil, + err: svcerr.ErrAuthorization, + }, + { + desc: "publish with authorization error", + topic: &topic, + payload: &payload, + password: clientKey, + session: &clientKeySession, + channelID: chanID, + authNRes: &grpcClientsV1.AuthnRes{Id: clientID, Authenticated: true}, + status: http.StatusBadRequest, + authNErr: nil, + authZRes: &grpcChannelsV1.AuthzRes{Authorized: false}, + authZErr: svcerr.ErrAuthorization, + err: svcerr.ErrAuthorization, + }, + { + desc: "publish with failed to publish", + topic: &topic, + payload: &payload, + password: clientKey, + session: &clientKeySession, + channelID: chanID, + authNRes: &grpcClientsV1.AuthnRes{Id: clientID, Authenticated: true}, + authNErr: nil, + authZRes: &grpcChannelsV1.AuthzRes{Authorized: true}, + authZErr: nil, + publishErr: errors.New("failed to publish"), + err: errFailedPublishToMsgBroker, + }, + } + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + ctx := context.TODO() + if tc.session != nil { + ctx = session.NewContext(ctx, tc.session) + } + clientsCall := clients.On("Authenticate", ctx, &grpcClientsV1.AuthnReq{ClientSecret: tc.password}).Return(tc.authNRes, tc.authNErr) + authCall := authn.On("Authenticate", ctx, mock.Anything).Return(tc.authNRes1, tc.authNErr) + channelsCall := channels.On("Authorize", ctx, mock.Anything).Return(tc.authZRes, tc.authZErr) + repoCall := publisher.On("Publish", ctx, tc.channelID, mock.Anything).Return(tc.publishErr) + err := handler.Publish(ctx, tc.topic, tc.payload) + hpe, ok := err.(mghttp.HTTPProxyError) + if ok { + assert.Equal(t, tc.status, hpe.StatusCode()) + } + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("expected: %v, got: %v", tc.err, err)) + authCall.Unset() + repoCall.Unset() + clientsCall.Unset() + channelsCall.Unset() + }) + } +} diff --git a/mqtt/handler_test.go b/mqtt/handler_test.go index 8446d478ab..33a14f0550 100644 --- a/mqtt/handler_test.go +++ b/mqtt/handler_test.go @@ -12,11 +12,16 @@ import ( chmocks "github.com/absmach/magistrala/channels/mocks" climocks "github.com/absmach/magistrala/clients/mocks" + grpcChannelsV1 "github.com/absmach/magistrala/internal/grpc/channels/v1" + grpcClientsV1 "github.com/absmach/magistrala/internal/grpc/clients/v1" + "github.com/absmach/magistrala/internal/testsutil" mglog "github.com/absmach/magistrala/logger" "github.com/absmach/magistrala/mqtt" "github.com/absmach/magistrala/mqtt/mocks" + "github.com/absmach/magistrala/pkg/connections" "github.com/absmach/magistrala/pkg/errors" svcerr "github.com/absmach/magistrala/pkg/errors/service" + "github.com/absmach/magistrala/pkg/policies" "github.com/absmach/mgate/pkg/session" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -59,15 +64,24 @@ var ( Username: invalidID, Password: []byte(password), } + errInvalidUserId = errors.New("invalid user id") +) + +var ( + clients = new(climocks.ClientsServiceClient) + channels = new(chmocks.ChannelsServiceClient) + eventStore = new(mocks.EventStore) ) func TestAuthConnect(t *testing.T) { - handler, _, _, eventStore := newHandler() + handler := newHandler() cases := []struct { - desc string - err error - session *session.Session + desc string + session *session.Session + authNRes *grpcClientsV1.AuthnRes + authNErr error + err error }{ { desc: "connect without active session", @@ -84,49 +98,84 @@ func TestAuthConnect(t *testing.T) { }, }, { - desc: "connect with invalid password", - err: nil, + desc: "connect with empty password", session: &session.Session{ ID: clientID, Username: clientID, Password: []byte(""), }, + authNErr: svcerr.ErrAuthentication, + err: svcerr.ErrAuthentication, + }, + { + desc: "connect with invalid password", + session: &session.Session{ + ID: clientID, + Username: clientID, + Password: []byte("invalid"), + }, + authNRes: &grpcClientsV1.AuthnRes{ + Authenticated: false, + }, + err: svcerr.ErrAuthentication, }, { desc: "connect with valid password and invalid username", - err: nil, session: &invalidClientSessionClient, + authNRes: &grpcClientsV1.AuthnRes{ + Authenticated: true, + Id: testsutil.GenerateUUID(t), + }, + err: errInvalidUserId, }, { desc: "connect with valid username and password", err: nil, session: &sessionClient, + authNRes: &grpcClientsV1.AuthnRes{ + Authenticated: true, + Id: clientID, + }, }, } for _, tc := range cases { - ctx := context.TODO() - password := "" - if tc.session != nil { - ctx = session.NewContext(ctx, tc.session) - password = string(tc.session.Password) - } - svcCall := eventStore.On("Connect", mock.Anything, password).Return(tc.err) - err := handler.AuthConnect(ctx) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - svcCall.Unset() + t.Run(tc.desc, func(t *testing.T) { + ctx := context.TODO() + password := "" + if tc.session != nil { + ctx = session.NewContext(ctx, tc.session) + password = string(tc.session.Password) + } + clientsCall := clients.On("Authenticate", mock.Anything, &grpcClientsV1.AuthnReq{ClientSecret: password}).Return(tc.authNRes, tc.authNErr) + svcCall := eventStore.On("Connect", mock.Anything, password).Return(tc.err) + err := handler.AuthConnect(ctx) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + svcCall.Unset() + clientsCall.Unset() + }) } } func TestAuthPublish(t *testing.T) { - handler, _, _, _ := newHandler() + handler := newHandler() cases := []struct { - desc string - session *session.Session - err error - topic *string - payload []byte + desc string + session *session.Session + err error + topic *string + payload []byte + authZRes *grpcChannelsV1.AuthzRes + authZErr error }{ + { + desc: "publish successfully", + session: &sessionClient, + err: nil, + topic: &topic, + payload: payload, + authZRes: &grpcChannelsV1.AuthzRes{Authorized: true}, + }, { desc: "publish with an inactive client", session: nil, @@ -149,32 +198,46 @@ func TestAuthPublish(t *testing.T) { payload: payload, }, { - desc: "publish successfully", - session: &sessionClient, - err: nil, - topic: &topic, - payload: payload, + desc: "publish with authorization error", + session: &sessionClient, + err: svcerr.ErrAuthorization, + topic: &topic, + payload: payload, + authZRes: &grpcChannelsV1.AuthzRes{Authorized: false}, + authZErr: svcerr.ErrAuthorization, }, } for _, tc := range cases { - ctx := context.TODO() - if tc.session != nil { - ctx = session.NewContext(ctx, tc.session) - } - err := handler.AuthPublish(ctx, tc.topic, &tc.payload) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + t.Run(tc.desc, func(t *testing.T) { + ctx := context.TODO() + if tc.session != nil { + ctx = session.NewContext(ctx, tc.session) + } + channelsCall := channels.On("Authorize", mock.Anything, &grpcChannelsV1.AuthzReq{ + ChannelId: chanID, + ClientId: clientID, + ClientType: policies.ClientType, + Type: uint32(connections.Publish), + }).Return(tc.authZRes, tc.authZErr) + err := handler.AuthPublish(ctx, tc.topic, &tc.payload) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + channelsCall.Unset() + }) } } func TestAuthSubscribe(t *testing.T) { - handler, _, _, _ := newHandler() + handler := newHandler() cases := []struct { - desc string - session *session.Session - err error - topic *[]string + desc string + session *session.Session + err error + topic *[]string + channelID string + authZRes *grpcChannelsV1.AuthzRes + authZErr error }{ { desc: "subscribe without active session", @@ -195,31 +258,52 @@ func TestAuthSubscribe(t *testing.T) { topic: &invalidTopics, }, { - desc: "subscribe with invalid channel ID", - session: &sessionClient, - err: svcerr.ErrAuthorization, - topic: &invalidChanIDTopics, + desc: "subscribe with invalid channel ID", + session: &sessionClientSub, + err: svcerr.ErrAuthorization, + topic: &invalidChanIDTopics, + authZRes: &grpcChannelsV1.AuthzRes{Authorized: false}, + channelID: invalidValue, }, { - desc: "subscribe successfully", - session: &sessionClientSub, - err: nil, - topic: &topics, + desc: "subscribe successfully", + session: &sessionClientSub, + err: nil, + topic: &topics, + authZRes: &grpcChannelsV1.AuthzRes{Authorized: true}, + channelID: chanID, + }, + { + desc: "subscribe with failed authorization", + session: &sessionClientSub, + err: svcerr.ErrAuthorization, + topic: &topics, + authZRes: &grpcChannelsV1.AuthzRes{Authorized: false}, + channelID: chanID, }, } for _, tc := range cases { - ctx := context.TODO() - if tc.session != nil { - ctx = session.NewContext(ctx, tc.session) - } - err := handler.AuthSubscribe(ctx, tc.topic) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + t.Run(tc.desc, func(t *testing.T) { + ctx := context.TODO() + if tc.session != nil { + ctx = session.NewContext(ctx, tc.session) + } + channelsCall := channels.On("Authorize", mock.Anything, &grpcChannelsV1.AuthzReq{ + ChannelId: tc.channelID, + ClientId: clientID1, + ClientType: policies.ClientType, + Type: uint32(connections.Subscribe), + }).Return(tc.authZRes, tc.authZErr) + err := handler.AuthSubscribe(ctx, tc.topic) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + channelsCall.Unset() + }) } } func TestConnect(t *testing.T) { - handler, _, _, _ := newHandler() + handler := newHandler() logBuffer.Reset() cases := []struct { @@ -253,7 +337,7 @@ func TestConnect(t *testing.T) { } func TestPublish(t *testing.T) { - handler, _, _, _ := newHandler() + handler := newHandler() logBuffer.Reset() malformedSubtopics := topic + "/" + subtopic + "%" @@ -332,7 +416,7 @@ func TestPublish(t *testing.T) { } func TestSubscribe(t *testing.T) { - handler, _, _, _ := newHandler() + handler := newHandler() logBuffer.Reset() cases := []struct { @@ -368,7 +452,7 @@ func TestSubscribe(t *testing.T) { } func TestUnsubscribe(t *testing.T) { - handler, _, _, _ := newHandler() + handler := newHandler() logBuffer.Reset() cases := []struct { @@ -404,7 +488,7 @@ func TestUnsubscribe(t *testing.T) { } func TestDisconnect(t *testing.T) { - handler, _, _, eventStore := newHandler() + handler := newHandler() logBuffer.Reset() cases := []struct { @@ -443,13 +527,13 @@ func TestDisconnect(t *testing.T) { } } -func newHandler() (session.Handler, *climocks.ClientsServiceClient, *chmocks.ChannelsServiceClient, *mocks.EventStore) { +func newHandler() session.Handler { logger, err := mglog.New(&logBuffer, "debug") if err != nil { log.Fatalf("failed to create logger: %s", err) } - clients := new(climocks.ClientsServiceClient) - channels := new(chmocks.ChannelsServiceClient) - eventStore := new(mocks.EventStore) - return mqtt.NewHandler(mocks.NewPublisher(), eventStore, logger, clients, channels), clients, channels, eventStore + clients = new(climocks.ClientsServiceClient) + channels = new(chmocks.ChannelsServiceClient) + eventStore = new(mocks.EventStore) + return mqtt.NewHandler(mocks.NewPublisher(), eventStore, logger, clients, channels) } diff --git a/ws/adapter_test.go b/ws/adapter_test.go index cbd8e6fcc7..3e63465df1 100644 --- a/ws/adapter_test.go +++ b/ws/adapter_test.go @@ -10,10 +10,14 @@ import ( chmocks "github.com/absmach/magistrala/channels/mocks" climocks "github.com/absmach/magistrala/clients/mocks" + grpcChannelsV1 "github.com/absmach/magistrala/internal/grpc/channels/v1" + grpcClientsV1 "github.com/absmach/magistrala/internal/grpc/clients/v1" "github.com/absmach/magistrala/internal/testsutil" + "github.com/absmach/magistrala/pkg/connections" svcerr "github.com/absmach/magistrala/pkg/errors/service" "github.com/absmach/magistrala/pkg/messaging" "github.com/absmach/magistrala/pkg/messaging/mocks" + "github.com/absmach/magistrala/pkg/policies" "github.com/absmach/magistrala/ws" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -24,18 +28,21 @@ const ( invalidID = "invalidID" invalidKey = "invalidKey" id = "1" - clientKey = "client_key" + clientKey = "client_key" subTopic = "subtopic" protocol = "ws" ) -var msg = messaging.Message{ - Channel: chanID, - Publisher: id, - Subtopic: "", - Protocol: protocol, - Payload: []byte(`[{"n":"current","t":-5,"v":1.2}]`), -} +var ( + msg = messaging.Message{ + Channel: chanID, + Publisher: id, + Subtopic: "", + Protocol: protocol, + Payload: []byte(`[{"n":"current","t":-5,"v":1.2}]`), + } + clientID = testsutil.GenerateUUID(&testing.T{}) +) func newService() (ws.Service, *mocks.PubSub, *climocks.ClientsServiceClient, *chmocks.ChannelsServiceClient) { pubsub := new(mocks.PubSub) @@ -46,78 +53,127 @@ func newService() (ws.Service, *mocks.PubSub, *climocks.ClientsServiceClient, *c } func TestSubscribe(t *testing.T) { - svc, pubsub, _, _ := newService() + svc, pubsub, clients, channels := newService() c := ws.NewClient(nil) cases := []struct { - desc string + desc string clientKey string - chanID string - subtopic string - err error + chanID string + subtopic string + authNRes *grpcClientsV1.AuthnRes + authNErr error + authZRes *grpcChannelsV1.AuthzRes + authZErr error + subErr error + err error }{ { - desc: "subscribe to channel with valid clientKey, chanID, subtopic", + desc: "subscribe to channel with valid clientKey, chanID, subtopic", clientKey: clientKey, - chanID: chanID, - subtopic: subTopic, - err: nil, + chanID: chanID, + subtopic: subTopic, + authNRes: &grpcClientsV1.AuthnRes{Id: clientID, Authenticated: true}, + authZRes: &grpcChannelsV1.AuthzRes{Authorized: true}, + err: nil, }, { - desc: "subscribe again to channel with valid clientKey, chanID, subtopic", + desc: "subscribe again to channel with valid clientKey, chanID, subtopic", clientKey: clientKey, - chanID: chanID, - subtopic: subTopic, - err: nil, + chanID: chanID, + subtopic: subTopic, + authNRes: &grpcClientsV1.AuthnRes{Id: clientID, Authenticated: true}, + authZRes: &grpcChannelsV1.AuthzRes{Authorized: true}, + err: nil, }, { - desc: "subscribe to channel with subscribe set to fail", + desc: "subscribe to channel with subscribe set to fail", clientKey: clientKey, - chanID: chanID, - subtopic: subTopic, - err: ws.ErrFailedSubscription, + chanID: chanID, + subtopic: subTopic, + subErr: ws.ErrFailedSubscription, + authNRes: &grpcClientsV1.AuthnRes{Id: clientID, Authenticated: true}, + authZRes: &grpcChannelsV1.AuthzRes{Authorized: true}, + err: ws.ErrFailedSubscription, }, { - desc: "subscribe to channel with invalid chanID and invalid clientKey", + desc: "subscribe to channel with invalid clientKey", clientKey: invalidKey, - chanID: invalidID, - subtopic: subTopic, - err: ws.ErrFailedSubscription, + chanID: invalidID, + subtopic: subTopic, + authNRes: &grpcClientsV1.AuthnRes{Authenticated: false}, + authNErr: svcerr.ErrAuthentication, + err: svcerr.ErrAuthorization, }, { - desc: "subscribe to channel with empty channel", + desc: "subscribe to channel with empty channel", clientKey: clientKey, - chanID: "", - subtopic: subTopic, - err: svcerr.ErrAuthentication, + chanID: "", + subtopic: subTopic, + err: svcerr.ErrAuthentication, }, { - desc: "subscribe to channel with empty clientKey", + desc: "subscribe to channel with empty clientKey", clientKey: "", - chanID: chanID, - subtopic: subTopic, - err: svcerr.ErrAuthentication, + chanID: chanID, + subtopic: subTopic, + err: svcerr.ErrAuthentication, }, { - desc: "subscribe to channel with empty clientKey and empty channel", + desc: "subscribe to channel with empty clientKey and empty channel", clientKey: "", - chanID: "", - subtopic: subTopic, - err: svcerr.ErrAuthentication, + chanID: "", + subtopic: subTopic, + err: svcerr.ErrAuthentication, + }, + { + desc: "subscribe to channel with invalid channel", + clientKey: clientKey, + chanID: invalidID, + subtopic: subTopic, + authNRes: &grpcClientsV1.AuthnRes{Id: clientID, Authenticated: true}, + authZRes: &grpcChannelsV1.AuthzRes{Authorized: false}, + authZErr: svcerr.ErrAuthorization, + err: svcerr.ErrAuthorization, + }, + { + desc: "subscribe to channel with failed authentication", + clientKey: clientKey, + chanID: chanID, + subtopic: subTopic, + authNRes: &grpcClientsV1.AuthnRes{Authenticated: false}, + err: svcerr.ErrAuthorization, + }, + { + desc: "subscribe to channel with failed authorization", + clientKey: clientKey, + chanID: chanID, + subtopic: subTopic, + authNRes: &grpcClientsV1.AuthnRes{Id: clientID, Authenticated: true}, + authZRes: &grpcChannelsV1.AuthzRes{Authorized: false}, + err: svcerr.ErrAuthorization, }, } for _, tc := range cases { - clientID := testsutil.GenerateUUID(t) subConfig := messaging.SubscriberConfig{ ID: clientID, Topic: "channels." + tc.chanID + "." + subTopic, Handler: c, } - repocall := pubsub.On("Subscribe", mock.Anything, subConfig).Return(tc.err) + clientsCall := clients.On("Authenticate", mock.Anything, &grpcClientsV1.AuthnReq{ClientSecret: tc.clientKey}).Return(tc.authNRes, tc.authNErr) + channelsCall := channels.On("Authorize", mock.Anything, &grpcChannelsV1.AuthzReq{ + ClientType: policies.ClientType, + ClientId: tc.authNRes.GetId(), + Type: uint32(connections.Subscribe), + ChannelId: tc.chanID, + }).Return(tc.authZRes, tc.authZErr) + repocall := pubsub.On("Subscribe", mock.Anything, subConfig).Return(tc.subErr) err := svc.Subscribe(context.Background(), tc.clientKey, tc.chanID, tc.subtopic, c) assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) repocall.Unset() + clientsCall.Unset() + channelsCall.Unset() } } diff --git a/ws/api/endpoint_test.go b/ws/api/endpoint_test.go index 4efb797986..35cb132b7c 100644 --- a/ws/api/endpoint_test.go +++ b/ws/api/endpoint_test.go @@ -17,6 +17,7 @@ import ( grpcChannelsV1 "github.com/absmach/magistrala/internal/grpc/channels/v1" grpcClientsV1 "github.com/absmach/magistrala/internal/grpc/clients/v1" mglog "github.com/absmach/magistrala/logger" + mgauthn "github.com/absmach/magistrala/pkg/authn" authnMocks "github.com/absmach/magistrala/pkg/authn/mocks" "github.com/absmach/magistrala/pkg/messaging/mocks" "github.com/absmach/magistrala/ws" @@ -105,6 +106,9 @@ func TestHandshake(t *testing.T) { defer ts.Close() pubsub.On("Subscribe", mock.Anything, mock.Anything).Return(nil) pubsub.On("Publish", mock.Anything, mock.Anything, mock.Anything).Return(nil) + clients.On("Authenticate", mock.Anything, mock.Anything).Return(&grpcClientsV1.AuthnRes{Authenticated: true}, nil) + authn.On("Authenticate", mock.Anything, mock.Anything).Return(mgauthn.Session{}, nil) + channels.On("Authorize", mock.Anything, mock.Anything, mock.Anything).Return(&grpcChannelsV1.AuthzRes{Authorized: true}, nil) cases := []struct { desc string