diff --git a/twins/api/http/endpoint_states_test.go b/twins/api/http/endpoint_states_test.go new file mode 100644 index 0000000000..33a9561cf6 --- /dev/null +++ b/twins/api/http/endpoint_states_test.go @@ -0,0 +1,213 @@ +// Copyright (c) Mainflux +// SPDX-License-Identifier: Apache-2.0 + +package http_test + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "testing" + + "github.com/mainflux/mainflux/twins" + "github.com/mainflux/senml" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/mainflux/mainflux/twins/mocks" +) + +const ( + nanosec = 1e9 + + attrName1 = "temperature" + attrSubtopic1 = "engine" + attrName2 = "humidity" + attrSubtopic2 = "chassis" + + publisher = "twins" +) + +type stateRes struct { + TwinID string `json:"twin_id"` + ID int64 `json:"id"` + Definition int `json:"definition"` + Payload map[string]interface{} `json:"payload"` +} + +type statesPageRes struct { + pageRes + States []stateRes `json:"states"` +} + +func TestListStates(t *testing.T) { + svc := mocks.NewService(map[string]string{token: email}) + ts := newServer(svc) + defer ts.Close() + + twin := twins.Twin{ + Owner: email, + } + def := mocks.CreateDefinition([]string{attrName1, attrName2}, []string{attrSubtopic1, attrSubtopic2}) + tw, err := svc.AddTwin(context.Background(), token, twin, def) + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + attr := def.Attributes[0] + + recs := mocks.CreateSenML(100, attrName1) + message, err := mocks.CreateMessage(attr, recs) + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + err = svc.SaveStates(message) + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + var data []stateRes + for i := 0; i < len(recs); i++ { + res := createStateResponse(i, tw, recs[i]) + data = append(data, res) + } + + baseURL := fmt.Sprintf("%s/states/%s", ts.URL, tw.ID) + queryFmt := "%s?offset=%d&limit=%d" + cases := []struct { + desc string + auth string + status int + url string + res []stateRes + }{ + { + desc: "get a list of states", + auth: token, + status: http.StatusOK, + url: baseURL, + res: data[0:10], + }, + { + desc: "get a list of states with valid offset and limit", + auth: token, + status: http.StatusOK, + url: fmt.Sprintf(queryFmt, baseURL, 20, 15), + res: data[20:35], + }, + { + desc: "get a list of states with invalid token", + auth: wrongValue, + status: http.StatusForbidden, + url: fmt.Sprintf(queryFmt, baseURL, 0, 5), + res: nil, + }, + { + desc: "get a list of states with empty token", + auth: "", + status: http.StatusForbidden, + url: fmt.Sprintf(queryFmt, baseURL, 0, 5), + res: nil, + }, + { + desc: "get a list of states with + limit > total", + auth: token, + status: http.StatusOK, + url: fmt.Sprintf(queryFmt, baseURL, 91, 20), + res: data[91:], + }, + { + desc: "get a list of states with negative offset", + auth: token, + status: http.StatusBadRequest, + url: fmt.Sprintf(queryFmt, baseURL, -1, 5), + res: nil, + }, + { + desc: "get a list of states with negative limit", + auth: token, + status: http.StatusBadRequest, + url: fmt.Sprintf(queryFmt, baseURL, 0, -5), + res: nil, + }, + { + desc: "get a list of states with zero limit", + auth: token, + status: http.StatusBadRequest, + url: fmt.Sprintf(queryFmt, baseURL, 0, 0), + res: nil, + }, + { + desc: "get a list of states with limit greater than max", + auth: token, + status: http.StatusBadRequest, + url: fmt.Sprintf(queryFmt, baseURL, 0, 110), + res: nil, + }, + { + desc: "get a list of states with invalid offset", + auth: token, + status: http.StatusBadRequest, + url: fmt.Sprintf("%s?offset=invalid&limit=%d", baseURL, 15), + res: nil, + }, + { + desc: "get a list of states with invalid limit", + auth: token, + status: http.StatusBadRequest, + url: fmt.Sprintf("%s?offset=%d&limit=invalid", baseURL, 0), + res: nil, + }, + { + desc: "get a list of states without offset", + auth: token, + status: http.StatusOK, + url: fmt.Sprintf("%s?limit=%d", baseURL, 15), + res: data[0:15], + }, + { + desc: "get a list of states without limit", + auth: token, + status: http.StatusOK, + url: fmt.Sprintf("%s?offset=%d", baseURL, 14), + res: data[14:24], + }, + { + desc: "get a list of states with invalid number of parameters", + auth: token, + status: http.StatusBadRequest, + url: fmt.Sprintf("%s%s", baseURL, "?offset=4&limit=4&limit=5&offset=5"), + res: nil, + }, + { + desc: "get a list of states with redundant query parameters", + auth: token, + status: http.StatusOK, + url: fmt.Sprintf("%s?offset=%d&limit=%d&value=something", baseURL, 0, 5), + res: data[0:5], + }, + } + + for _, tc := range cases { + req := testRequest{ + client: ts.Client(), + method: http.MethodGet, + url: tc.url, + token: tc.auth, + } + res, err := req.make() + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + + var resData statesPageRes + if tc.res != nil { + err = json.NewDecoder(res.Body).Decode(&resData) + 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)) + assert.ElementsMatch(t, tc.res, resData.States, fmt.Sprintf("%s: expected body %v got %v", tc.desc, tc.res, resData.States)) + } +} + +func createStateResponse(id int, tw twins.Twin, rec senml.Record) stateRes { + return stateRes{ + TwinID: tw.ID, + ID: int64(id), + Definition: tw.Definitions[len(tw.Definitions)-1].ID, + Payload: map[string]interface{}{rec.BaseName: nil}, + } +} diff --git a/twins/api/http/endpoint_test.go b/twins/api/http/endpoint_twins_test.go similarity index 60% rename from twins/api/http/endpoint_test.go rename to twins/api/http/endpoint_twins_test.go index f853b5ed47..537e529e91 100644 --- a/twins/api/http/endpoint_test.go +++ b/twins/api/http/endpoint_twins_test.go @@ -8,13 +8,11 @@ import ( "encoding/json" "fmt" "io" - "io/ioutil" "net/http" "net/http/httptest" "strconv" "strings" "testing" - "time" "github.com/mainflux/mainflux/twins" httpapi "github.com/mainflux/mainflux/twins/api/http" @@ -38,6 +36,31 @@ const ( var invalidName = strings.Repeat("m", maxNameSize+1) +type twinReq struct { + token string + Name string `json:"name,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` +} + +type twinRes struct { + Owner string `json:"owner"` + ID string `json:"id"` + Name string `json:"name,omitempty"` + Revision int `json:"revision"` + Metadata map[string]interface{} `json:"metadata,omitempty"` +} + +type pageRes struct { + Total uint64 `json:"total"` + Offset uint64 `json:"offset"` + Limit uint64 `json:"limit"` +} + +type twinsPageRes struct { + pageRes + Twins []twinRes `json:"twins"` +} + type testRequest struct { client *http.Client method string @@ -61,16 +84,6 @@ func (tr testRequest) make() (*http.Response, error) { return tr.client.Do(req) } -func newService(tokens map[string]string) twins.Service { - auth := mocks.NewAuthNServiceClient(tokens) - twinsRepo := mocks.NewTwinRepository() - statesRepo := mocks.NewStateRepository() - idp := mocks.NewIdentityProvider() - subs := map[string]string{"chanID": "chanID"} - broker := mocks.New(subs) - return twins.New(broker, auth, twinsRepo, statesRepo, idp, "chanID", nil) -} - func newServer(svc twins.Service) *httptest.Server { mux := httpapi.MakeHandler(mocktracer.New(), svc) return httptest.NewServer(mux) @@ -82,7 +95,7 @@ func toJSON(data interface{}) string { } func TestAddTwin(t *testing.T) { - svc := newService(map[string]string{token: email}) + svc := mocks.NewService(map[string]string{token: email}) ts := newServer(svc) defer ts.Close() @@ -185,13 +198,14 @@ func TestAddTwin(t *testing.T) { } func TestUpdateTwin(t *testing.T) { - svc := newService(map[string]string{token: email}) + svc := mocks.NewService(map[string]string{token: email}) ts := newServer(svc) defer ts.Close() twin := twins.Twin{} def := twins.Definition{} - stw, _ := svc.AddTwin(context.Background(), token, twin, def) + stw, err := svc.AddTwin(context.Background(), token, twin, def) + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) twin.Name = twinName data := toJSON(twin) @@ -297,7 +311,7 @@ func TestUpdateTwin(t *testing.T) { } func TestViewTwin(t *testing.T) { - svc := newService(map[string]string{token: email}) + svc := mocks.NewService(map[string]string{token: email}) ts := newServer(svc) defer ts.Close() @@ -307,58 +321,54 @@ func TestViewTwin(t *testing.T) { require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) twres := twinRes{ - Owner: stw.Owner, - Name: stw.Name, - ID: stw.ID, - Revision: stw.Revision, - Created: stw.Created, - Updated: stw.Updated, - Definitions: stw.Definitions, - Metadata: stw.Metadata, + Owner: stw.Owner, + Name: stw.Name, + ID: stw.ID, + Revision: stw.Revision, + Metadata: stw.Metadata, } - data := toJSON(twres) cases := []struct { desc string id string auth string status int - res string + res twinRes }{ { desc: "view existing twin", id: stw.ID, auth: token, status: http.StatusOK, - res: data, + res: twres, }, { desc: "view non-existent twin", id: strconv.FormatUint(wrongID, 10), auth: token, status: http.StatusNotFound, - res: "", + res: twinRes{}, }, { desc: "view twin by passing invalid token", id: stw.ID, auth: wrongValue, status: http.StatusForbidden, - res: "", + res: twinRes{}, }, { desc: "view twin by passing empty id", id: "", auth: token, status: http.StatusBadRequest, - res: "", + res: twinRes{}, }, { desc: "view twin by passing empty token", id: stw.ID, auth: "", status: http.StatusForbidden, - res: "", + res: twinRes{}, }, } @@ -372,20 +382,197 @@ func TestViewTwin(t *testing.T) { res, err := req.make() 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)) - body, err := ioutil.ReadAll(res.Body) - data := strings.Trim(string(body), "\n") - assert.Equal(t, tc.res, data, fmt.Sprintf("%s: expected body %s got %s", tc.desc, tc.res, data)) + + var resData twinRes + err = json.NewDecoder(res.Body).Decode(&resData) + assert.Equal(t, tc.res, resData, fmt.Sprintf("%s: expected body %v got %v", tc.desc, tc.res, resData)) + } +} + +func TestListTwins(t *testing.T) { + svc := mocks.NewService(map[string]string{token: email}) + ts := newServer(svc) + defer ts.Close() + + var data []twinRes + for i := 0; i < 100; i++ { + name := fmt.Sprintf("%s-%d", twinName, i) + twin := twins.Twin{ + Owner: email, + Name: name, + } + tw, err := svc.AddTwin(context.Background(), token, twin, twins.Definition{}) + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + twres := twinRes{ + Owner: tw.Owner, + ID: tw.ID, + Name: tw.Name, + Revision: tw.Revision, + Metadata: tw.Metadata, + } + data = append(data, twres) + } + + baseURL := fmt.Sprintf("%s/twins", ts.URL) + queryFmt := "%s?offset=%d&limit=%d" + cases := []struct { + desc string + auth string + status int + url string + res []twinRes + }{ + { + desc: "get a list of twins", + auth: token, + status: http.StatusOK, + url: baseURL, + res: data[0:10], + }, + { + desc: "get a list of twins with invalid token", + auth: wrongValue, + status: http.StatusForbidden, + url: fmt.Sprintf(queryFmt, baseURL, 0, 1), + res: nil, + }, + { + desc: "get a list of twins with empty token", + auth: "", + status: http.StatusForbidden, + url: fmt.Sprintf(queryFmt, baseURL, 0, 1), + res: nil, + }, + { + desc: "get a list of twins with valid offset and limit", + auth: token, + status: http.StatusOK, + url: fmt.Sprintf(queryFmt, baseURL, 25, 40), + res: data[25:65], + }, + { + desc: "get a list of twins with offset + limit > total", + auth: token, + status: http.StatusOK, + url: fmt.Sprintf(queryFmt, baseURL, 91, 20), + res: data[91:], + }, + { + desc: "get a list of twins with negative offset", + auth: token, + status: http.StatusBadRequest, + url: fmt.Sprintf(queryFmt, baseURL, -1, 5), + res: nil, + }, + { + desc: "get a list of twins with negative limit", + auth: token, + status: http.StatusBadRequest, + url: fmt.Sprintf(queryFmt, baseURL, 1, -5), + res: nil, + }, + { + desc: "get a list of twins with zero limit", + auth: token, + status: http.StatusBadRequest, + url: fmt.Sprintf(queryFmt, baseURL, 1, 0), + res: nil, + }, + { + desc: "get a list of twins with limit greater than max", + auth: token, + status: http.StatusBadRequest, + url: fmt.Sprintf("%s?offset=%d&limit=%d", baseURL, 0, 110), + res: nil, + }, + { + desc: "get a list of twins with invalid offset", + auth: token, + status: http.StatusBadRequest, + url: fmt.Sprintf("%s%s", baseURL, "?offset=e&limit=5"), + res: nil, + }, + { + desc: "get a list of twins with invalid limit", + auth: token, + status: http.StatusBadRequest, + url: fmt.Sprintf("%s%s", baseURL, "?offset=5&limit=e"), + res: nil, + }, + { + desc: "get a list of twins without offset", + auth: token, + status: http.StatusOK, + url: fmt.Sprintf("%s?limit=%d", baseURL, 5), + res: data[0:5], + }, + { + desc: "get a list of twins without limit", + auth: token, + status: http.StatusOK, + url: fmt.Sprintf("%s?offset=%d", baseURL, 1), + res: data[1:11], + }, + { + desc: "get a list of twins with invalid number of parameters", + auth: token, + status: http.StatusBadRequest, + url: fmt.Sprintf("%s%s", baseURL, "?offset=4&limit=4&limit=5&offset=5"), + res: nil, + }, + { + desc: "get a list of twins with redundant query parameters", + auth: token, + status: http.StatusOK, + url: fmt.Sprintf("%s?offset=%d&limit=%d&value=something", baseURL, 0, 5), + res: data[0:5], + }, + { + desc: "get a list of twins filtering with invalid name", + auth: token, + status: http.StatusBadRequest, + url: fmt.Sprintf("%s?offset=%d&limit=%d&name=%s", baseURL, 0, 5, invalidName), + res: nil, + }, + { + desc: "get a list of twins filtering with valid name", + auth: token, + status: http.StatusOK, + url: fmt.Sprintf("%s?offset=%d&limit=%d&name=%s", baseURL, 2, 1, twinName+"-2"), + res: data[2:3], + }, + } + + for _, tc := range cases { + req := testRequest{ + client: ts.Client(), + method: http.MethodGet, + url: tc.url, + token: tc.auth, + } + res, err := req.make() + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + + var resData twinsPageRes + if tc.res != nil { + err = json.NewDecoder(res.Body).Decode(&resData) + 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)) + assert.ElementsMatch(t, tc.res, resData.Twins, fmt.Sprintf("%s: expected body %v got %v", tc.desc, tc.res, resData.Twins)) } } func TestRemoveTwin(t *testing.T) { - svc := newService(map[string]string{token: email}) + svc := mocks.NewService(map[string]string{token: email}) ts := newServer(svc) defer ts.Close() def := twins.Definition{} twin := twins.Twin{} - stw, _ := svc.AddTwin(context.Background(), token, twin, def) + stw, err := svc.AddTwin(context.Background(), token, twin, def) + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) cases := []struct { desc string @@ -437,21 +624,3 @@ func TestRemoveTwin(t *testing.T) { assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) } } - -type twinReq struct { - token string - Name string `json:"name,omitempty"` - Definition twins.Definition `json:"definition,omitempty"` - Metadata map[string]interface{} `json:"metadata,omitempty"` -} - -type twinRes struct { - Owner string `json:"owner"` - Name string `json:"name,omitempty"` - ID string `json:"id"` - Revision int `json:"revision"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` - Definitions []twins.Definition `json:"definitions"` - Metadata map[string]interface{} `json:"metadata,omitempty"` -} diff --git a/twins/doc.go b/twins/doc.go index f850aaa508..4816cd0d9c 100644 --- a/twins/doc.go +++ b/twins/doc.go @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // Package twins contains the domain concept definitions needed to support -// Mainflux twins service functionality. Twin is a semantic representation of a +// Mainflux twins service functionality. Twin is a digital representation of a // real world data system consisting of data producers and consumers. It stores // the sequence of attribute based definitions of a data system and refers to a // time series of definition based states that store the system historical data. diff --git a/twins/mocks/messages.go b/twins/mocks/messages.go index 43611b33c7..6eba9e02e8 100644 --- a/twins/mocks/messages.go +++ b/twins/mocks/messages.go @@ -14,8 +14,8 @@ type mockBroker struct { subscriptions map[string]string } -// New returns mock message publisher. -func New(sub map[string]string) messaging.Publisher { +// NewBroker returns mock message publisher. +func NewBroker(sub map[string]string) messaging.Publisher { return &mockBroker{ subscriptions: sub, } diff --git a/twins/mocks/service.go b/twins/mocks/service.go new file mode 100644 index 0000000000..d52e464394 --- /dev/null +++ b/twins/mocks/service.go @@ -0,0 +1,71 @@ +package mocks + +import ( + "encoding/json" + "time" + + "github.com/mainflux/mainflux/messaging" + "github.com/mainflux/mainflux/twins" + "github.com/mainflux/mainflux/twins/uuid" + "github.com/mainflux/senml" +) + +const ( + publisher = "twins" +) + +// NewService use mock dependencies to create real twins service +func NewService(tokens map[string]string) twins.Service { + auth := NewAuthNServiceClient(tokens) + twinsRepo := NewTwinRepository() + statesRepo := NewStateRepository() + idp := NewIdentityProvider() + subs := map[string]string{"chanID": "chanID"} + broker := NewBroker(subs) + return twins.New(broker, auth, twinsRepo, statesRepo, idp, "chanID", nil) +} + +// CreateDefinition creates twin definition +func CreateDefinition(names []string, subtopics []string) twins.Definition { + var def twins.Definition + for i, v := range names { + id, _ := uuid.New().ID() + attr := twins.Attribute{ + Name: v, + Channel: id, + Subtopic: subtopics[i], + PersistState: true, + } + def.Attributes = append(def.Attributes, attr) + } + return def +} + +// CreateSenML creates SenML record array +func CreateSenML(n int, bn string) []senml.Record { + var recs []senml.Record + for i := 0; i < n; i++ { + rec := senml.Record{ + BaseName: bn, + BaseTime: float64(time.Now().Unix()), + Time: float64(i), + Value: nil, + } + recs = append(recs, rec) + } + return recs +} + +// CreateMessage creates Mainflux message using SenML record array +func CreateMessage(attr twins.Attribute, recs []senml.Record) (*messaging.Message, error) { + mRecs, err := json.Marshal(recs) + if err != nil { + return nil, err + } + return &messaging.Message{ + Channel: attr.Channel, + Subtopic: attr.Subtopic, + Payload: mRecs, + Publisher: publisher, + }, nil +} diff --git a/twins/mocks/states.go b/twins/mocks/states.go index f59f39f12b..da747c2a8f 100644 --- a/twins/mocks/states.go +++ b/twins/mocks/states.go @@ -5,7 +5,6 @@ package mocks import ( "context" - "fmt" "sort" "strings" "sync" @@ -16,9 +15,8 @@ import ( var _ twins.StateRepository = (*stateRepositoryMock)(nil) type stateRepositoryMock struct { - mu sync.Mutex - counter uint64 - states map[string]twins.State + mu sync.Mutex + states map[string]twins.State } // NewStateRepository creates in-memory twin repository. @@ -53,7 +51,7 @@ func (srm *stateRepositoryMock) Count(ctx context.Context, tw twins.Twin) (int64 return int64(len(srm.states)), nil } -func (srm *stateRepositoryMock) RetrieveAll(ctx context.Context, offset uint64, limit uint64, id string) (twins.StatesPage, error) { +func (srm *stateRepositoryMock) RetrieveAll(ctx context.Context, offset uint64, limit uint64, twinID string) (twins.StatesPage, error) { srm.mu.Lock() defer srm.mu.Unlock() @@ -63,29 +61,28 @@ func (srm *stateRepositoryMock) RetrieveAll(ctx context.Context, offset uint64, return twins.StatesPage{}, nil } - // This obscure way to examine map keys is enforced by the key structure in mocks/commons.go - prefix := fmt.Sprintf("%s-", id) for k, v := range srm.states { - if !strings.HasPrefix(k, prefix) { + if (uint64)(len(items)) >= limit { + break + } + if !strings.HasPrefix(k, twinID) { continue } id := uint64(v.ID) - if id > offset && id < limit { + if id >= offset && id < offset+limit { items = append(items, v) } - if (uint64)(len(items)) >= limit { - break - } } sort.SliceStable(items, func(i, j int) bool { return items[i].ID < items[j].ID }) + total := uint64(len(srm.states)) page := twins.StatesPage{ States: items, PageMetadata: twins.PageMetadata{ - Total: srm.counter, + Total: total, Offset: offset, Limit: limit, }, @@ -101,11 +98,16 @@ func (srm *stateRepositoryMock) RetrieveLast(ctx context.Context, id string) (tw items := make([]twins.State, 0) for _, v := range srm.states { - items = append(items, v) + if v.TwinID == id { + items = append(items, v) + } } sort.SliceStable(items, func(i, j int) bool { return items[i].ID < items[j].ID }) - return items[len(items)-1], nil + if len(items) > 0 { + return items[len(items)-1], nil + } + return twins.State{}, nil } diff --git a/twins/mocks/twins.go b/twins/mocks/twins.go index 69e4f6179f..585b721100 100644 --- a/twins/mocks/twins.go +++ b/twins/mocks/twins.go @@ -5,7 +5,6 @@ package mocks import ( "context" - "fmt" "sort" "strconv" "strings" @@ -17,9 +16,8 @@ import ( var _ twins.TwinRepository = (*twinRepositoryMock)(nil) type twinRepositoryMock struct { - mu sync.Mutex - counter uint64 - twins map[string]twins.Twin + mu sync.Mutex + twins map[string]twins.Twin } // NewTwinRepository creates in-memory twin repository. @@ -83,7 +81,10 @@ func (trm *twinRepositoryMock) RetrieveByAttribute(ctx context.Context, channel, } } - return ids, nil + if len(ids) > 0 { + return ids, nil + } + return ids, twins.ErrNotFound } func (trm *twinRepositoryMock) RetrieveAll(_ context.Context, owner string, offset uint64, limit uint64, name string, metadata twins.Metadata) (twins.Page, error) { @@ -96,18 +97,19 @@ func (trm *twinRepositoryMock) RetrieveAll(_ context.Context, owner string, offs return twins.Page{}, nil } - // This obscure way to examine map keys is enforced by the key structure in mocks/commons.go - prefix := fmt.Sprintf("%s-", owner) for k, v := range trm.twins { if (uint64)(len(items)) >= limit { break } - if !strings.HasPrefix(k, prefix) { + if len(name) > 0 && v.Name != name { + continue + } + if !strings.HasPrefix(k, owner) { continue } suffix := string(v.ID[len(u4Pref):]) id, _ := strconv.ParseUint(suffix, 10, 64) - if id > offset && id <= uint64(offset+limit) { + if id > offset && id <= offset+limit { items = append(items, v) } } @@ -116,10 +118,11 @@ func (trm *twinRepositoryMock) RetrieveAll(_ context.Context, owner string, offs return items[i].ID < items[j].ID }) + total := uint64(len(trm.twins)) page := twins.Page{ Twins: items, PageMetadata: twins.PageMetadata{ - Total: trm.counter, + Total: total, Offset: offset, Limit: limit, }, @@ -135,6 +138,7 @@ func (trm *twinRepositoryMock) Remove(ctx context.Context, id string) error { for k, v := range trm.twins { if id == v.ID { delete(trm.twins, k) + return nil } } diff --git a/twins/service.go b/twins/service.go index 0bff4ea955..a52b06ce4b 100644 --- a/twins/service.go +++ b/twins/service.go @@ -131,8 +131,9 @@ func (ts *twinsService) AddTwin(ctx context.Context, token string, twin Twin, de twin.Owner = res.GetValue() - twin.Created = time.Now() - twin.Updated = time.Now() + t := time.Now() + twin.Created = t + twin.Updated = t if def.Attributes == nil { def.Attributes = []Attribute{} @@ -324,6 +325,7 @@ func prepareState(st *State, tw *Twin, rec senml.Record, msg *messaging.Message) if st.Payload == nil { st.Payload = make(map[string]interface{}) + st.ID = -1 // state is incremented on save -> zero-based index } else { for k := range st.Payload { idx := findAttribute(k, def.Attributes) @@ -356,6 +358,7 @@ func prepareState(st *State, tw *Twin, rec senml.Record, msg *messaging.Message) } val := findValue(rec) st.Payload[attr.Name] = val + break } } diff --git a/twins/service_test.go b/twins/service_test.go index 73adee6dd1..ad262ac090 100644 --- a/twins/service_test.go +++ b/twins/service_test.go @@ -10,6 +10,7 @@ import ( "github.com/mainflux/mainflux/twins" "github.com/mainflux/mainflux/twins/mocks" + "github.com/mainflux/senml" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -21,20 +22,18 @@ const ( wrongToken = "wrong-token" email = "user@example.com" natsURL = "nats://localhost:4222" -) -func newService(tokens map[string]string) twins.Service { - auth := mocks.NewAuthNServiceClient(tokens) - twinsRepo := mocks.NewTwinRepository() - statesRepo := mocks.NewStateRepository() - idp := mocks.NewIdentityProvider() - subs := map[string]string{"chanID": "chanID"} - broker := mocks.New(subs) - return twins.New(broker, auth, twinsRepo, statesRepo, idp, "chanID", nil) -} + attrName1 = "temperature" + attrSubtopic1 = "engine" + attrName2 = "humidity" + attrSubtopic2 = "chassis" + attrName3 = "speed" + attrSubtopic3 = "wheel_2" + numRecs = 100 +) func TestAddTwin(t *testing.T) { - svc := newService(map[string]string{token: email}) + svc := mocks.NewService(map[string]string{token: email}) twin := twins.Twin{} def := twins.Definition{} @@ -65,7 +64,7 @@ func TestAddTwin(t *testing.T) { } func TestUpdateTwin(t *testing.T) { - svc := newService(map[string]string{token: email}) + svc := mocks.NewService(map[string]string{token: email}) twin := twins.Twin{} other := twins.Twin{} def := twins.Definition{} @@ -109,7 +108,7 @@ func TestUpdateTwin(t *testing.T) { } func TestViewTwin(t *testing.T) { - svc := newService(map[string]string{token: email}) + svc := mocks.NewService(map[string]string{token: email}) twin := twins.Twin{} def := twins.Definition{} saved, err := svc.AddTwin(context.Background(), token, twin, def) @@ -144,7 +143,7 @@ func TestViewTwin(t *testing.T) { } func TestListTwins(t *testing.T) { - svc := newService(map[string]string{token: email}) + svc := mocks.NewService(map[string]string{token: email}) twin := twins.Twin{Name: twinName, Owner: email} def := twins.Definition{} m := make(map[string]interface{}) @@ -202,7 +201,7 @@ func TestListTwins(t *testing.T) { } func TestRemoveTwin(t *testing.T) { - svc := newService(map[string]string{token: email}) + svc := mocks.NewService(map[string]string{token: email}) twin := twins.Twin{} def := twins.Definition{} saved, err := svc.AddTwin(context.Background(), token, twin, def) @@ -245,3 +244,168 @@ func TestRemoveTwin(t *testing.T) { assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) } } + +func TestSaveStates(t *testing.T) { + svc := mocks.NewService(map[string]string{token: email}) + + twin := twins.Twin{Owner: email} + def := mocks.CreateDefinition([]string{attrName1, attrName2}, []string{attrSubtopic1, attrSubtopic2}) + attr := def.Attributes[0] + attrSansTwin := mocks.CreateDefinition([]string{attrName3}, []string{attrSubtopic3}).Attributes[0] + tw, err := svc.AddTwin(context.Background(), token, twin, def) + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + recs := mocks.CreateSenML(numRecs, attrName1) + 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: twins.ErrNotFound, + }, + { + desc: "use empty senml record", + recs: []senml.Record{}, + attr: attr, + size: 0, + err: nil, + }, + } + + for _, tc := range cases { + message, err := mocks.CreateMessage(tc.attr, tc.recs) + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + err = svc.SaveStates(message) + assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + + ttlAdded += tc.size + page, err := svc.ListStates(context.TODO(), token, 0, 10, tw.ID) + require.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)) + } +} + +func TestListStates(t *testing.T) { + svc := mocks.NewService(map[string]string{token: email}) + + twin := twins.Twin{Owner: email} + def := mocks.CreateDefinition([]string{attrName1, attrName2}, []string{attrSubtopic1, attrSubtopic2}) + attr := def.Attributes[0] + tw, err := svc.AddTwin(context.Background(), token, twin, def) + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + tw2, err := svc.AddTwin(context.Background(), token, + twins.Twin{Owner: email}, + mocks.CreateDefinition([]string{attrName3}, []string{attrSubtopic3})) + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + recs := mocks.CreateSenML(numRecs, attrName1) + message, err := mocks.CreateMessage(attr, recs) + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + err = svc.SaveStates(message) + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + 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: wrongToken, + offset: 0, + limit: 10, + size: 0, + err: twins.ErrUnauthorizedAccess, + }, + { + 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 { + 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))) + } +} diff --git a/twins/twins.go b/twins/twins.go index 4fedeaa7e2..1689b03279 100644 --- a/twins/twins.go +++ b/twins/twins.go @@ -45,7 +45,6 @@ type PageMetadata struct { Total uint64 Offset uint64 Limit uint64 - Name string } // Page contains page related metadata as well as a list of twins that