MF-995 - Add Twins tests for endpoint list twins and list states (#1174)

* Add ListTwins test

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Remove monotonic time from twins, definitions and attributes creation and update

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Separate twins and states endpoint tests in two files

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add state generation helper funcs to state endpoint tests

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add createStateResponse() to states test

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add states test cases

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Simplify RetrieveAll twins and states methods

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add service.go to mocks

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Rename mocks.NewService to mocks.New

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add error checking to endpoint state tests

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Fix method comment

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add json response decode success check

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Remove created and updated fields from twin and state res

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Remove definition fields from twin req and res

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add Create funcs to mocks package

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add service save state tests

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add service list states test

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>
This commit is contained in:
Darko Draskovic 2020-05-18 18:46:50 +02:00 committed by GitHub
parent d7670e7adb
commit b4c80132e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 725 additions and 100 deletions

View File

@ -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},
}
}

View File

@ -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"`
}

View File

@ -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.

View File

@ -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,
}

71
twins/mocks/service.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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"
attrName1 = "temperature"
attrSubtopic1 = "engine"
attrName2 = "humidity"
attrSubtopic2 = "chassis"
attrName3 = "speed"
attrSubtopic3 = "wheel_2"
numRecs = 100
)
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 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)))
}
}

View File

@ -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