Repository: mainflux/mainflux (go)

Issue:
Metadata in URL query not being properly parsed

**BUG REPORT**

1. What were you trying to achieve?

Search things/users by metadata

2. What are the expected results?

List of things that match the metadata query

3. What are the received results?
```
curl -i  -s -X GET http://localhost:8182/things\?name\=test\&metadata\=\{\"role\":\"manager\"\} -H "Authorization: $TOK" -H 'Content-Type: application/json'

HTTP/1.1 400 Bad Request
Content-Type: application/json
Date: Fri, 05 Feb 2021 20:04:15 GMT
Content-Length: 33

{"error":"invalid query params"}
```



Pull request:
MF-1357 - Add new endpoint for searching things
Signed-off-by: Ivan Milosevic <iva@blokovi.com>

### What does this do?
Add /things/search POST endpoint with filter parameters for name and metadata in json body

### Which issue(s) does this PR fix/relate to?
Resolves #1357 

### List any changes that modify/break current functionality

### Have you included tests for your changes?
Yes

### Did you document any new/modified functionality?

### Notes


Patch:
diff --git a/things/api/things/http/requests.go b/things/api/things/http/requests.go
index dd63d562..c4a9bb86 100644
--- a/things/api/things/http/requests.go
+++ b/things/api/things/http/requests.go
@@ -185,7 +185,11 @@ func (req *listResourcesReq) validate() error {
 		return things.ErrUnauthorizedAccess
 	}
 
-	if req.pageMetadata.Limit == 0 || req.pageMetadata.Limit > maxLimitSize {
+	if req.pageMetadata.Limit == 0 {
+		req.pageMetadata.Limit = defLimit
+	}
+
+	if req.pageMetadata.Limit > maxLimitSize {
 		return things.ErrMalformedEntity
 	}
 
diff --git a/things/api/things/http/transport.go b/things/api/things/http/transport.go
index f8f4092a..b559a4d1 100644
--- a/things/api/things/http/transport.go
+++ b/things/api/things/http/transport.go
@@ -105,6 +105,13 @@ func MakeHandler(tracer opentracing.Tracer, svc things.Service) http.Handler {
 		opts...,
 	))
 
+	r.Post("/things/search", kithttp.NewServer(
+		kitot.TraceServer(tracer, "search_things")(listThingsEndpoint(svc)),
+		decodeListByMetadata,
+		encodeResponse,
+		opts...,
+	))
+
 	r.Post("/channels", kithttp.NewServer(
 		kitot.TraceServer(tracer, "create_channel")(createChannelEndpoint(svc)),
 		decodeChannelCreation,
@@ -344,6 +351,15 @@ func decodeList(_ context.Context, r *http.Request) (interface{}, error) {
 	return req, nil
 }
 
+func decodeListByMetadata(_ context.Context, r *http.Request) (interface{}, error) {
+	req := listResourcesReq{token: r.Header.Get("Authorization")}
+	if err := json.NewDecoder(r.Body).Decode(&req.pageMetadata); err != nil {
+		return nil, errors.Wrap(things.ErrMalformedEntity, err)
+	}
+
+	return req, nil
+}
+
 func decodeListByConnection(_ context.Context, r *http.Request) (interface{}, error) {
 	o, err := readUintQuery(r, offsetKey, defOffset)
 	if err != nil {
diff --git a/things/mocks/channels.go b/things/mocks/channels.go
index f74b884a..d859d197 100644
--- a/things/mocks/channels.go
+++ b/things/mocks/channels.go
@@ -48,7 +48,7 @@ func (crm *channelRepositoryMock) Save(_ context.Context, channels ...things.Cha
 
 	for i := range channels {
 		crm.counter++
-		channels[i].ID = strconv.FormatUint(crm.counter, 10)
+		channels[i].ID = fmt.Sprintf("%03d", crm.counter)
 		crm.channels[key(channels[i].Owner, channels[i].ID)] = channels[i]
 	}
 
@@ -78,12 +78,15 @@ func (crm *channelRepositoryMock) RetrieveByID(_ context.Context, owner, id stri
 }
 
 func (crm *channelRepositoryMock) RetrieveAll(_ context.Context, owner string, pm things.PageMetadata) (things.ChannelsPage, error) {
-	if pm.Limit <= 0 {
+	if pm.Limit < 0 {
 		return things.ChannelsPage{}, nil
 	}
+	if pm.Limit == 0 {
+		pm.Limit = 10
+	}
 
-	first := uint64(pm.Offset) + 1
-	last := first + uint64(pm.Limit)
+	first := int(pm.Offset)
+	last := first + int(pm.Limit)
 
 	var chs []things.Channel
 
@@ -91,8 +94,7 @@ func (crm *channelRepositoryMock) RetrieveAll(_ context.Context, owner string, p
 	// itself (see mocks/commons.go).
 	prefix := fmt.Sprintf("%s-", owner)
 	for k, v := range crm.channels {
-		id, _ := strconv.ParseUint(v.ID, 10, 64)
-		if strings.HasPrefix(k, prefix) && id >= first && id < last {
+		if strings.HasPrefix(k, prefix) {
 			chs = append(chs, v)
 		}
 	}
@@ -100,8 +102,16 @@ func (crm *channelRepositoryMock) RetrieveAll(_ context.Context, owner string, p
 	// Sort Channels list
 	chs = sortChannels(pm, chs)
 
+	if last > len(chs) {
+		last = len(chs)
+	}
+
+	if first > last {
+		return things.ChannelsPage{}, nil
+	}
+
 	page := things.ChannelsPage{
-		Channels: chs,
+		Channels: chs[first:last],
 		PageMetadata: things.PageMetadata{
 			Total:  crm.counter,
 			Offset: pm.Offset,
diff --git a/things/mocks/commons.go b/things/mocks/commons.go
index 9976a6b4..909c6ce6 100644
--- a/things/mocks/commons.go
+++ b/things/mocks/commons.go
@@ -31,6 +31,17 @@ func sortThings(pm things.PageMetadata, ths []things.Thing) []things.Thing {
 				return ths[i].Name > ths[j].Name
 			})
 		}
+	case "id":
+		if pm.Dir == "asc" {
+			sort.SliceStable(ths, func(i, j int) bool {
+				return ths[i].ID < ths[j].ID
+			})
+		}
+		if pm.Dir == "desc" {
+			sort.SliceStable(ths, func(i, j int) bool {
+				return ths[i].ID > ths[j].ID
+			})
+		}
 	default:
 		sort.SliceStable(ths, func(i, j int) bool {
 			return ths[i].ID < ths[j].ID
@@ -53,6 +64,17 @@ func sortChannels(pm things.PageMetadata, chs []things.Channel) []things.Channel
 				return chs[i].Name > chs[j].Name
 			})
 		}
+	case "id":
+		if pm.Dir == "asc" {
+			sort.SliceStable(chs, func(i, j int) bool {
+				return chs[i].ID < chs[j].ID
+			})
+		}
+		if pm.Dir == "desc" {
+			sort.SliceStable(chs, func(i, j int) bool {
+				return chs[i].ID > chs[j].ID
+			})
+		}
 	default:
 		sort.SliceStable(chs, func(i, j int) bool {
 			return chs[i].ID < chs[j].ID
diff --git a/things/mocks/things.go b/things/mocks/things.go
index 9424ab2a..3c07ccad 100644
--- a/things/mocks/things.go
+++ b/things/mocks/things.go
@@ -55,7 +55,7 @@ func (trm *thingRepositoryMock) Save(_ context.Context, ths ...things.Thing) ([]
 		}
 
 		trm.counter++
-		ths[i].ID = strconv.FormatUint(trm.counter, 10)
+		ths[i].ID = fmt.Sprintf("%03d", trm.counter)
 		trm.things[key(ths[i].Owner, ths[i].ID)] = ths[i]
 	}
 
@@ -115,7 +115,7 @@ func (trm *thingRepositoryMock) RetrieveAll(_ context.Context, owner string, pm
 	trm.mu.Lock()
 	defer trm.mu.Unlock()
 
-	if pm.Limit <= 0 {
+	if pm.Limit < 0 {
 		return things.Page{}, nil
 	}
 
diff --git a/things/openapi.yml b/things/openapi.yml
index d4184bb7..41296c0d 100644
--- a/things/openapi.yml
+++ b/things/openapi.yml
@@ -62,6 +62,34 @@ paths:
           description: Database can't process request.
         '500':
           $ref: "#/components/responses/ServiceError"
+  /things/search:
+    post:
+      summary: Search and retrieves things
+      description: |
+        Retrieves a list of things with name and metadata filtering.
+        Due to performance concerns, data is retrieved in subsets.
+        The API things must ensure that the entire
+        dataset is consumed either by making subsequent requests, or by
+        increasing the subset size of the initial request.
+      tags:
+        - things
+      parameters:
+        - $ref: "#/components/parameters/Authorization"
+      requestBody:
+        $ref: "#/components/requestBodies/ThingsSearchReq"
+      responses:
+        '200':
+          $ref: "#/components/responses/ThingsPageRes"
+        '400':
+          description: Failed due to malformed query parameters.
+        '401':
+          description: Missing or invalid access token provided.
+        '404':
+          description: A non-existent entity request.
+        '422':
+          description: Unprocessable Entity
+        '500':
+          $ref: "#/components/responses/ServiceError"
   /things/bulk:
     post:
       summary: Bulk provisions new things
@@ -569,6 +597,43 @@ components:
         metadata:
           type: object
           description: Arbitrary, object-encoded thing's data.
+    ThingsReqSchema:
+      type: object
+      properties:
+        name:
+          type: string
+          description: Name filter. Filtering is performed as a case-insensitive partial match.
+        metadata:
+          type: object
+          description: Metadata filter. Filtering is performed matching the parameter with metadata on top level. Parameter is json.
+        total:
+          type: integer
+          description: Total number of items.
+        offset:
+          type: integer
+          description: Number of items to skip during retrieval.
+          default: 0
+          minimum: 0
+        limit:
+          type: integer
+          description: Size of the subset to retrieve.
+          default: 10
+          maximum: 100
+          minimum: 1
+        order:
+          type: string
+          description: Order type.
+          default: id
+          enum:
+            - name
+            - id
+        dir:
+          type: string
+          description: Order direction.
+          default: desc
+          enum:
+            - asc
+            - desc
     ThingResSchema:
       type: object
       properties:
@@ -800,6 +865,13 @@ components:
                 description: Free-form thing name.
               metadata:
                 type: object
+    ThingsSearchReq:
+      description: JSON-formatted document describing search parameters.
+      required: true
+      content:
+        application/json:
+          schema:
+           $ref: "#/components/schemas/ThingsReqSchema"
     KeyUpdateReq:
       required: true
       description: JSON containing thing.
diff --git a/things/service.go b/things/service.go
index a17b7bc8..4b24fa65 100644
--- a/things/service.go
+++ b/things/service.go
@@ -126,13 +126,13 @@ type Service interface {
 // PageMetadata contains page metadata that helps navigation.
 type PageMetadata struct {
 	Total     uint64
-	Offset    uint64
-	Limit     uint64
-	Name      string
-	Order     string
-	Dir       string
-	Metadata  map[string]interface{}
-	Connected bool // Used for connected or disconnected lists
+	Offset    uint64                 `json:"offset,omitempty"`
+	Limit     uint64                 `json:"limit,omitempty"`
+	Name      string                 `json:"name,omitempty"`
+	Order     string                 `json:"order,omitempty"`
+	Dir       string                 `json:"dir,omitempty"`
+	Metadata  map[string]interface{} `json:"metadata,omitempty"`
+	Connected bool                   // Used for connected or disconnected lists
 }
 
 var _ Service = (*thingsService)(nil)


Test patch:
diff --git a/pkg/sdk/go/channels_test.go b/pkg/sdk/go/channels_test.go
index a70dcb55..b51c5da2 100644
--- a/pkg/sdk/go/channels_test.go
+++ b/pkg/sdk/go/channels_test.go
@@ -6,7 +6,6 @@ package sdk_test
 import (
 	"fmt"
 	"net/http"
-	"strconv"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -16,7 +15,7 @@ import (
 )
 
 var (
-	channel      = sdk.Channel{ID: "1", Name: "test"}
+	channel      = sdk.Channel{ID: "001", Name: "test"}
 	emptyChannel = sdk.Channel{}
 )
 
@@ -99,8 +98,8 @@ func TestCreateChannels(t *testing.T) {
 	mainfluxSDK := sdk.NewSDK(sdkConf)
 
 	channels := []sdk.Channel{
-		sdk.Channel{ID: "1", Name: "1"},
-		sdk.Channel{ID: "2", Name: "2"},
+		sdk.Channel{ID: "001", Name: "1"},
+		sdk.Channel{ID: "002", Name: "2"},
 	}
 
 	cases := []struct {
@@ -221,7 +220,7 @@ func TestChannels(t *testing.T) {
 	var channels []sdk.Channel
 	mainfluxSDK := sdk.NewSDK(sdkConf)
 	for i := 1; i < 101; i++ {
-		ch := sdk.Channel{ID: strconv.Itoa(i), Name: "test"}
+		ch := sdk.Channel{ID: fmt.Sprintf("%03d", i), Name: "test"}
 		mainfluxSDK.CreateChannel(ch, token)
 		channels = append(channels, ch)
 	}
@@ -260,12 +259,12 @@ func TestChannels(t *testing.T) {
 			response: nil,
 		},
 		{
-			desc:     "get a list of channels with zero limit",
+			desc:     "get a list of channels without limit, default 10",
 			token:    token,
 			offset:   0,
 			limit:    0,
-			err:      createError(sdk.ErrFailedFetch, http.StatusBadRequest),
-			response: nil,
+			err:      nil,
+			response: channels[0:10],
 		},
 		{
 			desc:     "get a list of channels with limit greater than max",
@@ -283,14 +282,6 @@ func TestChannels(t *testing.T) {
 			err:      nil,
 			response: []sdk.Channel{},
 		},
-		{
-			desc:     "get a list of channels with invalid args (zero limit) and invalid token",
-			token:    wrongValue,
-			offset:   0,
-			limit:    0,
-			err:      createError(sdk.ErrFailedFetch, http.StatusBadRequest),
-			response: nil,
-		},
 	}
 	for _, tc := range cases {
 		page, err := mainfluxSDK.Channels(tc.token, tc.offset, tc.limit, tc.name)
@@ -323,7 +314,7 @@ func TestChannelsByThing(t *testing.T) {
 	var channels []sdk.Channel
 	for i := 1; i < n+1; i++ {
 		ch := sdk.Channel{
-			ID:   strconv.Itoa(i),
+			ID:   fmt.Sprintf("%03d", i),
 			Name: "test",
 		}
 		cid, err := mainfluxSDK.CreateChannel(ch, token)
diff --git a/pkg/sdk/go/things_test.go b/pkg/sdk/go/things_test.go
index 2ee2f111..b775855c 100644
--- a/pkg/sdk/go/things_test.go
+++ b/pkg/sdk/go/things_test.go
@@ -7,7 +7,6 @@ import (
 	"fmt"
 	"net/http"
 	"net/http/httptest"
-	"strconv"
 	"testing"
 
 	sdk "github.com/mainflux/mainflux/pkg/sdk/go"
@@ -36,7 +35,7 @@ const (
 var (
 	metadata   = map[string]interface{}{"meta": "data"}
 	metadata2  = map[string]interface{}{"meta": "data2"}
-	thing      = sdk.Thing{ID: "1", Name: "test_device", Metadata: metadata}
+	thing      = sdk.Thing{ID: "001", Name: "test_device", Metadata: metadata}
 	emptyThing = sdk.Thing{}
 )
 
@@ -86,14 +85,14 @@ func TestCreateThing(t *testing.T) {
 			thing:    thing,
 			token:    token,
 			err:      nil,
-			location: "1",
+			location: "001",
 		},
 		{
 			desc:     "create new empty thing",
 			thing:    emptyThing,
 			token:    token,
 			err:      nil,
-			location: "2",
+			location: "002",
 		},
 		{
 			desc:     "create new thing with empty token",
@@ -136,8 +135,8 @@ func TestCreateThings(t *testing.T) {
 	mainfluxSDK := sdk.NewSDK(sdkConf)
 
 	things := []sdk.Thing{
-		sdk.Thing{ID: "1", Name: "1", Key: "1"},
-		sdk.Thing{ID: "2", Name: "2", Key: "2"},
+		sdk.Thing{ID: "001", Name: "1", Key: "1"},
+		sdk.Thing{ID: "002", Name: "2", Key: "2"},
 	}
 
 	cases := []struct {
@@ -262,7 +261,7 @@ func TestThings(t *testing.T) {
 	mainfluxSDK := sdk.NewSDK(sdkConf)
 	for i := 1; i < 101; i++ {
 
-		th := sdk.Thing{ID: strconv.Itoa(i), Name: "test_device", Metadata: metadata}
+		th := sdk.Thing{ID: fmt.Sprintf("%03d", i), Name: "test_device", Metadata: metadata}
 		mainfluxSDK.CreateThing(th, token)
 		th.Key = fmt.Sprintf("%s%012d", keyPrefix, 2*i)
 		things = append(things, th)
@@ -306,8 +305,8 @@ func TestThings(t *testing.T) {
 			token:    token,
 			offset:   0,
 			limit:    0,
-			err:      createError(sdk.ErrFailedFetch, http.StatusBadRequest),
-			response: nil,
+			err:      nil,
+			response: things[0:10],
 		},
 		{
 			desc:     "get a list of things with limit greater than max",
@@ -325,14 +324,6 @@ func TestThings(t *testing.T) {
 			err:      nil,
 			response: []sdk.Thing{},
 		},
-		{
-			desc:     "get a list of things with invalid args (zero limit) and invalid token",
-			token:    wrongValue,
-			offset:   0,
-			limit:    0,
-			err:      createError(sdk.ErrFailedFetch, http.StatusBadRequest),
-			response: nil,
-		},
 	}
 	for _, tc := range cases {
 		page, err := mainfluxSDK.Things(tc.token, tc.offset, tc.limit, tc.name)
@@ -366,7 +357,7 @@ func TestThingsByChannel(t *testing.T) {
 	var things []sdk.Thing
 	for i := 1; i < n+1; i++ {
 		th := sdk.Thing{
-			ID:       strconv.Itoa(i),
+			ID:       fmt.Sprintf("%03d", i),
 			Name:     "test_device",
 			Metadata: metadata,
 			Key:      fmt.Sprintf("%s%012d", keyPrefix, 2*i+1),
diff --git a/things/api/things/http/endpoint_test.go b/things/api/things/http/endpoint_test.go
index 6aa4f383..4b01a78b 100644
--- a/things/api/things/http/endpoint_test.go
+++ b/things/api/things/http/endpoint_test.go
@@ -46,9 +46,13 @@ var (
 		Name:     "test",
 		Metadata: map[string]interface{}{"test": "data"},
 	}
-	invalidName = strings.Repeat("m", maxNameSize+1)
-	notFoundRes = toJSON(errorRes{things.ErrNotFound.Error()})
-	unauthRes   = toJSON(errorRes{things.ErrUnauthorizedAccess.Error()})
+	invalidName    = strings.Repeat("m", maxNameSize+1)
+	notFoundRes    = toJSON(errorRes{things.ErrNotFound.Error()})
+	unauthRes      = toJSON(errorRes{things.ErrUnauthorizedAccess.Error()})
+	searchThingReq = things.PageMetadata{
+		Limit:  5,
+		Offset: 0,
+	}
 )
 
 type testRequest struct {
@@ -122,7 +126,7 @@ func TestCreateThing(t *testing.T) {
 			contentType: contentType,
 			auth:        token,
 			status:      http.StatusCreated,
-			location:    "/things/1",
+			location:    "/things/001",
 		},
 		{
 			desc:        "add thing with existing key",
@@ -138,7 +142,7 @@ func TestCreateThing(t *testing.T) {
 			contentType: contentType,
 			auth:        token,
 			status:      http.StatusCreated,
-			location:    "/things/2",
+			location:    "/things/002",
 		},
 		{
 			desc:        "add thing with invalid auth token",
@@ -723,11 +727,11 @@ func TestListThings(t *testing.T) {
 			res:    nil,
 		},
 		{
-			desc:   "get a list of things with zero limit",
+			desc:   "get a list of things with zero limit and offset 1",
 			auth:   token,
-			status: http.StatusBadRequest,
+			status: http.StatusOK,
 			url:    fmt.Sprintf("%s?offset=%d&limit=%d", thingURL, 1, 0),
-			res:    nil,
+			res:    data[1:11],
 		},
 		{
 			desc:   "get a list of things without offset",
@@ -838,6 +842,201 @@ func TestListThings(t *testing.T) {
 	}
 }
 
+func TestSearchThings(t *testing.T) {
+	svc := newService(map[string]string{token: email})
+	ts := newServer(svc)
+	defer ts.Close()
+
+	th := searchThingReq
+	validData := toJSON(th)
+
+	th.Dir = "desc"
+	th.Order = "name"
+	descData := toJSON(th)
+
+	th.Dir = "asc"
+	ascData := toJSON(th)
+
+	th.Order = "wrong"
+	invalidOrderData := toJSON(th)
+
+	th = searchThingReq
+	th.Dir = "wrong"
+	invalidDirData := toJSON(th)
+
+	th = searchThingReq
+	th.Limit = 110
+	limitMaxData := toJSON(th)
+
+	th.Limit = 0
+	zeroLimitData := toJSON(th)
+
+	th = searchThingReq
+	th.Name = invalidName
+	invalidNameData := toJSON(th)
+
+	th.Name = invalidName
+	invalidData := toJSON(th)
+
+	data := []thingRes{}
+	for i := 0; i < 100; i++ {
+		name := "name_" + fmt.Sprintf("%03d", i+1)
+		ths, err := svc.CreateThings(context.Background(), token, things.Thing{Name: name, Metadata: map[string]interface{}{"test": name}})
+		require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
+		th := ths[0]
+		data = append(data, thingRes{
+			ID:       th.ID,
+			Name:     th.Name,
+			Key:      th.Key,
+			Metadata: th.Metadata,
+		})
+	}
+
+	cases := []struct {
+		desc   string
+		auth   string
+		status int
+		req    string
+		res    []thingRes
+	}{
+		{
+			desc:   "search things",
+			auth:   token,
+			status: http.StatusOK,
+			req:    validData,
+			res:    data[0:5],
+		},
+		{
+			desc:   "search things ordered by name descendent",
+			auth:   token,
+			status: http.StatusOK,
+			req:    descData,
+			res:    data[0:5],
+		},
+		{
+			desc:   "search things ordered by name ascendent",
+			auth:   token,
+			status: http.StatusOK,
+			req:    ascData,
+			res:    data[0:5],
+		},
+		{
+			desc:   "search things with invalid order",
+			auth:   token,
+			status: http.StatusBadRequest,
+			req:    invalidOrderData,
+			res:    nil,
+		},
+		{
+			desc:   "search things with invalid dir",
+			auth:   token,
+			status: http.StatusBadRequest,
+			req:    invalidDirData,
+			res:    nil,
+		},
+		{
+			desc:   "search things with invalid token",
+			auth:   wrongValue,
+			status: http.StatusUnauthorized,
+			req:    validData,
+			res:    nil,
+		},
+		{
+			desc:   "search things with invalid data",
+			auth:   token,
+			status: http.StatusBadRequest,
+			req:    invalidData,
+			res:    nil,
+		},
+		{
+			desc:   "search things with empty token",
+			auth:   "",
+			status: http.StatusUnauthorized,
+			req:    validData,
+			res:    nil,
+		},
+		{
+			desc:   "search things with zero limit",
+			auth:   token,
+			status: http.StatusOK,
+			req:    zeroLimitData,
+			res:    data[0:10],
+		},
+		{
+			desc:   "search things without offset",
+			auth:   token,
+			status: http.StatusOK,
+			req:    validData,
+			res:    data[0:5],
+		},
+		{
+			desc:   "search things with limit greater than max",
+			auth:   token,
+			status: http.StatusBadRequest,
+			req:    limitMaxData,
+			res:    nil,
+		},
+		{
+			desc:   "search things with default URL",
+			auth:   token,
+			status: http.StatusOK,
+			req:    validData,
+			res:    data[0:5],
+		},
+		{
+			desc:   "search things filtering with invalid name",
+			auth:   token,
+			status: http.StatusBadRequest,
+			req:    invalidNameData,
+			res:    nil,
+		},
+		{
+			desc:   "search things sorted by name ascendent",
+			auth:   token,
+			status: http.StatusOK,
+			req:    validData,
+			res:    data[0:5],
+		},
+		{
+			desc:   "search things sorted by name descendent",
+			auth:   token,
+			status: http.StatusOK,
+			req:    validData,
+			res:    data[0:5],
+		},
+		{
+			desc:   "search things sorted with invalid order",
+			auth:   token,
+			status: http.StatusBadRequest,
+			req:    invalidOrderData,
+			res:    nil,
+		},
+		{
+			desc:   "search things sorted by name with invalid direction",
+			auth:   token,
+			status: http.StatusBadRequest,
+			req:    invalidDirData,
+			res:    nil,
+		},
+	}
+
+	for _, tc := range cases {
+		req := testRequest{
+			client: ts.Client(),
+			method: http.MethodPost,
+			url:    fmt.Sprintf("%s/things/search", ts.URL),
+			token:  tc.auth,
+			body:   strings.NewReader(tc.req),
+		}
+		res, err := req.make()
+		assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
+		var data thingsPageRes
+		json.NewDecoder(res.Body).Decode(&data)
+		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, data.Things, fmt.Sprintf("%s: expected body %v got %v", tc.desc, tc.res, data.Things))
+	}
+}
+
 func TestListThingsByChannel(t *testing.T) {
 	svc := newService(map[string]string{token: email})
 	ts := newServer(svc)
@@ -1097,7 +1296,7 @@ func TestCreateChannel(t *testing.T) {
 			contentType: contentType,
 			auth:        token,
 			status:      http.StatusCreated,
-			location:    "/channels/1",
+			location:    "/channels/001",
 		},
 		{
 			desc:        "create new channel with invalid token",
@@ -1129,7 +1328,7 @@ func TestCreateChannel(t *testing.T) {
 			contentType: contentType,
 			auth:        token,
 			status:      http.StatusCreated,
-			location:    "/channels/2",
+			location:    "/channels/002",
 		},
 		{
 			desc:        "create new channel with empty request",
@@ -1482,7 +1681,12 @@ func TestListChannels(t *testing.T) {
 
 	channels := []channelRes{}
 	for i := 0; i < 101; i++ {
-		chs, err := svc.CreateChannels(context.Background(), token, channel)
+		name := "name_" + fmt.Sprintf("%03d", i+1)
+		chs, err := svc.CreateChannels(context.Background(), token,
+			things.Channel{
+				Name:     name,
+				Metadata: map[string]interface{}{"test": "data"},
+			})
 		require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
 		ch := chs[0]
 		ths, err := svc.CreateThings(context.Background(), token, thing)
@@ -1513,17 +1717,17 @@ func TestListChannels(t *testing.T) {
 			res:    channels[0:6],
 		},
 		{
-			desc:   "get a list of channels ordered by name descendent",
+			desc:   "get a list of channels ordered by id descendent",
 			auth:   token,
 			status: http.StatusOK,
-			url:    fmt.Sprintf("%s?offset=%d&limit=%d&order=name&dir=desc", channelURL, 0, 6),
-			res:    channels[0:6],
+			url:    fmt.Sprintf("%s?offset=%d&limit=%d&order=id&dir=desc", channelURL, 0, 6),
+			res:    channels[len(channels)-6:],
 		},
 		{
-			desc:   "get a list of channels ordered by name ascendent",
+			desc:   "get a list of channels ordered by id ascendent",
 			auth:   token,
 			status: http.StatusOK,
-			url:    fmt.Sprintf("%s?offset=%d&limit=%d&order=name&dir=asc", channelURL, 0, 6),
+			url:    fmt.Sprintf("%s?offset=%d&limit=%d&order=id&dir=asc", channelURL, 0, 6),
 			res:    channels[0:6],
 		},
 		{
@@ -1569,11 +1773,11 @@ func TestListChannels(t *testing.T) {
 			res:    nil,
 		},
 		{
-			desc:   "get a list of channels with zero limit",
+			desc:   "get a list of channels with zero limit and offset 1",
 			auth:   token,
-			status: http.StatusBadRequest,
+			status: http.StatusOK,
 			url:    fmt.Sprintf("%s?offset=%d&limit=%d", channelURL, 1, 0),
-			res:    nil,
+			res:    channels[1:11],
 		},
 		{
 			desc:   "get a list of channels with no offset provided",
@@ -1650,7 +1854,7 @@ func TestListChannels(t *testing.T) {
 			auth:   token,
 			status: http.StatusOK,
 			url:    fmt.Sprintf("%s?offset=%d&limit=%d&order=%s&dir=%s", channelURL, 0, 6, nameKey, descKey),
-			res:    channels[0:6],
+			res:    channels[len(channels)-6:],
 		},
 		{
 			desc:   "get a list of channels sorted with invalid order",
diff --git a/things/redis/streams_test.go b/things/redis/streams_test.go
index 77d8bc64..0dfa5cb1 100644
--- a/things/redis/streams_test.go
+++ b/things/redis/streams_test.go
@@ -72,7 +72,7 @@ func TestCreateThings(t *testing.T) {
 			key: token,
 			err: nil,
 			event: map[string]interface{}{
-				"id":        "1",
+				"id":        "001",
 				"name":      "a",
 				"owner":     email,
 				"metadata":  "{\"test\":\"test\"}",
@@ -298,7 +298,7 @@ func TestCreateChannels(t *testing.T) {
 			key:  token,
 			err:  nil,
 			event: map[string]interface{}{
-				"id":        "1",
+				"id":        "001",
 				"name":      "a",
 				"metadata":  "{\"test\":\"test\"}",
 				"owner":     email,
diff --git a/things/service_test.go b/things/service_test.go
index 9e3bcc2a..1e106b94 100644
--- a/things/service_test.go
+++ b/things/service_test.go
@@ -711,13 +711,13 @@ func TestListChannels(t *testing.T) {
 			size: 0,
 			err:  nil,
 		},
-		"list with zero limit": {
+		"list with zero limit and offset 1": {
 			token: token,
 			pageMetadata: things.PageMetadata{
 				Offset: 1,
 				Limit:  0,
 			},
-			size: 0,
+			size: n - 1,
 			err:  nil,
 		},
 		"list with wrong credentials": {