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:
parent
d7670e7adb
commit
b4c80132e6
|
@ -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},
|
||||
}
|
||||
}
|
|
@ -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"`
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue