// Copyright (c) Mainflux // SPDX-License-Identifier: Apache-2.0 package twins_test import ( "context" "fmt" "testing" "github.com/mainflux/mainflux" authmocks "github.com/mainflux/mainflux/auth/mocks" "github.com/mainflux/mainflux/internal/testsutil" "github.com/mainflux/mainflux/pkg/errors" "github.com/mainflux/mainflux/twins" "github.com/mainflux/mainflux/twins/mocks" "github.com/mainflux/senml" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) const ( twinName = "name" wrongID = "" token = "token" email = "user@example.com" numRecs = 100 ) var ( subtopics = []string{"engine", "chassis", "wheel_2"} channels = []string{"01ec3c3e-0e66-4e69-9751-a0545b44e08f", "48061e4f-7c23-4f5c-9012-0f9b7cd9d18d", "5b2180e4-e96b-4469-9dc1-b6745078d0b6"} ) func TestAddTwin(t *testing.T) { svc, auth := mocks.NewService() twin := twins.Twin{} def := twins.Definition{} cases := []struct { desc string twin twins.Twin token string err error }{ { desc: "add new twin", twin: twin, token: token, err: nil, }, { desc: "add twin with wrong credentials", twin: twin, token: authmocks.InvalidValue, err: errors.ErrAuthentication, }, } for _, tc := range cases { repoCall := auth.On("Identify", mock.Anything, &mainflux.IdentityReq{Token: tc.token}).Return(&mainflux.IdentityRes{Id: testsutil.GenerateUUID(t)}, nil) _, err := svc.AddTwin(context.Background(), tc.token, tc.twin, def) assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) repoCall.Unset() } } func TestUpdateTwin(t *testing.T) { svc, auth := mocks.NewService() twin := twins.Twin{} other := twins.Twin{} def := twins.Definition{} other.ID = wrongID repoCall := auth.On("Identify", mock.Anything, &mainflux.IdentityReq{Token: token}).Return(&mainflux.IdentityRes{Id: testsutil.GenerateUUID(t)}, nil) saved, err := svc.AddTwin(context.Background(), token, twin, def) require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err)) repoCall.Unset() saved.Name = twinName cases := []struct { desc string twin twins.Twin token string err error }{ { desc: "update existing twin", twin: saved, token: token, err: nil, }, { desc: "update twin with wrong credentials", twin: saved, token: authmocks.InvalidValue, err: errors.ErrAuthentication, }, { desc: "update non-existing twin", twin: other, token: token, err: errors.ErrNotFound, }, } for _, tc := range cases { repoCall := auth.On("Identify", mock.Anything, &mainflux.IdentityReq{Token: tc.token}).Return(&mainflux.IdentityRes{Id: testsutil.GenerateUUID(t)}, nil) err := svc.UpdateTwin(context.Background(), tc.token, tc.twin, def) assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) repoCall.Unset() } } func TestViewTwin(t *testing.T) { svc, auth := mocks.NewService() twin := twins.Twin{} def := twins.Definition{} repoCall := auth.On("Identify", mock.Anything, &mainflux.IdentityReq{Token: token}).Return(&mainflux.IdentityRes{Id: testsutil.GenerateUUID(t)}, nil) saved, err := svc.AddTwin(context.Background(), token, twin, def) require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err)) repoCall.Unset() cases := []struct { desc string id string token string err error }{ { desc: "view existing twin", id: saved.ID, token: token, err: nil, }, { desc: "view twin with wrong credentials", id: saved.ID, token: authmocks.InvalidValue, err: errors.ErrAuthentication, }, { desc: "view non-existing twin", id: wrongID, token: token, err: errors.ErrNotFound, }, } for _, tc := range cases { repoCall := auth.On("Identify", mock.Anything, &mainflux.IdentityReq{Token: tc.token}).Return(&mainflux.IdentityRes{Id: testsutil.GenerateUUID(t)}, nil) _, err := svc.ViewTwin(context.Background(), tc.token, tc.id) assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) repoCall.Unset() } } func TestListTwins(t *testing.T) { svc, auth := mocks.NewService() twin := twins.Twin{Name: twinName, Owner: email} def := twins.Definition{} m := make(map[string]interface{}) m["serial"] = "123456" twin.Metadata = m n := uint64(10) for i := uint64(0); i < n; i++ { repoCall := auth.On("Identify", mock.Anything, &mainflux.IdentityReq{Token: token}).Return(&mainflux.IdentityRes{Id: testsutil.GenerateUUID(t)}, nil) _, err := svc.AddTwin(context.Background(), token, twin, def) require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err)) repoCall.Unset() } cases := []struct { desc string token string offset uint64 limit uint64 size uint64 metadata map[string]interface{} err error }{ { desc: "list all twins", token: token, offset: 0, limit: n, size: n, err: nil, }, { desc: "list with zero limit", token: token, limit: 0, offset: 0, size: 0, err: nil, }, { desc: "list with offset and limit", token: token, offset: 8, limit: 5, size: 2, err: nil, }, { desc: "list with wrong credentials", token: authmocks.InvalidValue, limit: 0, offset: n, err: errors.ErrAuthentication, }, } for _, tc := range cases { repoCall := auth.On("Identify", mock.Anything, &mainflux.IdentityReq{Token: tc.token}).Return(&mainflux.IdentityRes{Id: testsutil.GenerateUUID(t)}, nil) _, err := svc.ListTwins(context.Background(), tc.token, tc.offset, tc.limit, twinName, tc.metadata) assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) repoCall.Unset() } } func TestRemoveTwin(t *testing.T) { svc, auth := mocks.NewService() twin := twins.Twin{} def := twins.Definition{} repoCall := auth.On("Identify", mock.Anything, &mainflux.IdentityReq{Token: token}).Return(&mainflux.IdentityRes{Id: testsutil.GenerateUUID(t)}, nil) saved, err := svc.AddTwin(context.Background(), token, twin, def) require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err)) repoCall.Unset() cases := []struct { desc string id string token string err error }{ { desc: "remove twin with wrong credentials", id: saved.ID, token: authmocks.InvalidValue, err: errors.ErrAuthentication, }, { desc: "remove existing twin", id: saved.ID, token: token, err: nil, }, { desc: "remove removed twin", id: saved.ID, token: token, err: nil, }, { desc: "remove non-existing twin", id: wrongID, token: token, err: nil, }, } for _, tc := range cases { repoCall := auth.On("Identify", mock.Anything, &mainflux.IdentityReq{Token: tc.token}).Return(&mainflux.IdentityRes{Id: testsutil.GenerateUUID(t)}, nil) err := svc.RemoveTwin(context.Background(), tc.token, tc.id) assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) repoCall.Unset() } } func TestSaveStates(t *testing.T) { svc, auth := mocks.NewService() twin := twins.Twin{Owner: email} def := mocks.CreateDefinition(channels[0:2], subtopics[0:2]) attr := def.Attributes[0] attrSansTwin := mocks.CreateDefinition(channels[2:3], subtopics[2:3]).Attributes[0] repoCall := auth.On("Identify", mock.Anything, &mainflux.IdentityReq{Token: token}).Return(&mainflux.IdentityRes{Id: testsutil.GenerateUUID(t)}, nil) tw, err := svc.AddTwin(context.Background(), token, twin, def) require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) repoCall.Unset() defWildcard := mocks.CreateDefinition(channels[0:2], []string{twins.SubtopicWildcard, twins.SubtopicWildcard}) repoCall = auth.On("Identify", mock.Anything, &mainflux.IdentityReq{Token: token}).Return(&mainflux.IdentityRes{Id: testsutil.GenerateUUID(t)}, nil) twWildcard, err := svc.AddTwin(context.Background(), token, twin, defWildcard) require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) repoCall.Unset() recs := make([]senml.Record, numRecs) mocks.CreateSenML(recs) var ttlAdded uint64 cases := []struct { desc string recs []senml.Record attr twins.Attribute size uint64 err error }{ { desc: "add 100 states", recs: recs, attr: attr, size: numRecs, err: nil, }, { desc: "add 20 states", recs: recs[10:30], attr: attr, size: 20, err: nil, }, { desc: "add 20 states for atttribute without twin", recs: recs[30:50], size: 0, attr: attrSansTwin, err: errors.ErrNotFound, }, { desc: "use empty senml record", recs: []senml.Record{}, attr: attr, size: 0, err: nil, }, } for _, tc := range cases { repoCall := auth.On("Identify", mock.Anything, &mainflux.IdentityReq{Token: token}).Return(&mainflux.IdentityRes{Id: testsutil.GenerateUUID(t)}, nil) message, err := mocks.CreateMessage(tc.attr, tc.recs) assert.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) err = svc.SaveStates(context.Background(), message) assert.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) ttlAdded += tc.size page, err := svc.ListStates(context.TODO(), token, 0, 10, tw.ID) assert.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) assert.Equal(t, ttlAdded, page.Total, fmt.Sprintf("%s: expected %d total got %d total\n", tc.desc, ttlAdded, page.Total)) page, err = svc.ListStates(context.TODO(), token, 0, 10, twWildcard.ID) assert.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) assert.Equal(t, ttlAdded, page.Total, fmt.Sprintf("%s: expected %d total got %d total\n", tc.desc, ttlAdded, page.Total)) repoCall.Unset() } } func TestListStates(t *testing.T) { svc, auth := mocks.NewService() twin := twins.Twin{Owner: email} def := mocks.CreateDefinition(channels[0:2], subtopics[0:2]) attr := def.Attributes[0] repoCall := auth.On("Identify", mock.Anything, &mainflux.IdentityReq{Token: token}).Return(&mainflux.IdentityRes{Id: testsutil.GenerateUUID(t)}, nil) tw, err := svc.AddTwin(context.Background(), token, twin, def) require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) repoCall.Unset() repoCall = auth.On("Identify", mock.Anything, &mainflux.IdentityReq{Token: token}).Return(&mainflux.IdentityRes{Id: testsutil.GenerateUUID(t)}, nil) tw2, err := svc.AddTwin(context.Background(), token, twins.Twin{Owner: email}, mocks.CreateDefinition(channels[2:3], subtopics[2:3])) require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) repoCall.Unset() recs := make([]senml.Record, numRecs) mocks.CreateSenML(recs) message, err := mocks.CreateMessage(attr, recs) require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) repoCall = auth.On("Identify", mock.Anything, &mainflux.IdentityReq{Token: token}).Return(&mainflux.IdentityRes{Id: testsutil.GenerateUUID(t)}, nil) err = svc.SaveStates(context.Background(), message) require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) repoCall.Unset() cases := []struct { desc string id string token string offset uint64 limit uint64 size int err error }{ { desc: "get a list of first 10 states", id: tw.ID, token: token, offset: 0, limit: 10, size: 10, err: nil, }, { desc: "get a list of last 10 states", id: tw.ID, token: token, offset: numRecs - 10, limit: numRecs, size: 10, err: nil, }, { desc: "get a list of last 10 states with limit > numRecs", id: tw.ID, token: token, offset: numRecs - 10, limit: numRecs + 10, size: 10, err: nil, }, { desc: "get a list of first 10 states with offset == numRecs", id: tw.ID, token: token, offset: numRecs, limit: numRecs + 10, size: 0, err: nil, }, { desc: "get a list with wrong user token", id: tw.ID, token: authmocks.InvalidValue, offset: 0, limit: 10, size: 0, err: errors.ErrAuthentication, }, { desc: "get a list with id of non-existent twin", id: "1234567890", token: token, offset: 0, limit: 10, size: 0, err: nil, }, { desc: "get a list with id of existing twin without states ", id: tw2.ID, token: token, offset: 0, limit: 10, size: 0, err: nil, }, } for _, tc := range cases { repoCall := auth.On("Identify", mock.Anything, &mainflux.IdentityReq{Token: tc.token}).Return(&mainflux.IdentityRes{Id: testsutil.GenerateUUID(t)}, nil) page, err := svc.ListStates(context.TODO(), tc.token, tc.offset, tc.limit, tc.id) assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) assert.Equal(t, tc.size, len(page.States), fmt.Sprintf("%s: expected %d total got %d total\n", tc.desc, tc.size, len(page.States))) repoCall.Unset() } }