NOISSUE - Add searchable Channels name (#754)

* NOISSUE - Add searchable Things name

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix reviews

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix typo

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add postgres schema validation and tests

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add namme tests in requests_test

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* NOISSUE - Add searchable Channels name

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix test description

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix bootstrap mocks

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix reviews

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
This commit is contained in:
Manuel Imperiale 2019-06-11 10:37:25 +02:00 committed by GitHub
parent 44cc20b9ca
commit 63de955a7c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 151 additions and 39 deletions

View File

@ -186,7 +186,7 @@ func (svc *mainfluxThings) UpdateChannel(string, things.Channel) error {
panic("not implemented")
}
func (svc *mainfluxThings) ListChannels(string, uint64, uint64) (things.ChannelsPage, error) {
func (svc *mainfluxThings) ListChannels(string, uint64, uint64, string) (things.ChannelsPage, error) {
panic("not implemented")
}

View File

@ -51,7 +51,7 @@ var cmdChannels = []cobra.Command{
}
if args[0] == "all" {
l, err := sdk.Channels(args[1], uint64(Offset), uint64(Limit))
l, err := sdk.Channels(args[1], uint64(Offset), uint64(Limit), Name)
if err != nil {
logError(err)
return

View File

@ -50,8 +50,8 @@ func (sdk mfSDK) CreateChannel(channel Channel, token string) (string, error) {
return id, nil
}
func (sdk mfSDK) Channels(token string, offset, limit uint64) (ChannelsPage, error) {
endpoint := fmt.Sprintf("%s?offset=%d&limit=%d", channelsEndpoint, offset, limit)
func (sdk mfSDK) Channels(token string, offset, limit uint64, name string) (ChannelsPage, error) {
endpoint := fmt.Sprintf("%s?offset=%d&limit=%d&name=%s", channelsEndpoint, offset, limit, name)
url := createURL(sdk.baseURL, sdk.thingsPrefix, endpoint)
req, err := http.NewRequest(http.MethodGet, url, nil)

View File

@ -162,6 +162,7 @@ func TestChannels(t *testing.T) {
token string
offset uint64
limit uint64
name string
err error
response []sdk.Channel
}{
@ -223,7 +224,7 @@ func TestChannels(t *testing.T) {
},
}
for _, tc := range cases {
page, err := mainfluxSDK.Channels(tc.token, tc.offset, tc.limit)
page, err := mainfluxSDK.Channels(tc.token, tc.offset, tc.limit, tc.name)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected error %s, got %s", tc.desc, tc.err, err))
assert.Equal(t, tc.response, page.Channels, fmt.Sprintf("%s: expected response channel %s, got %s", tc.desc, tc.response, page.Channels))
}

View File

@ -157,7 +157,7 @@ type SDK interface {
CreateChannel(channel Channel, token string) (string, error)
// Channels returns page of channels.
Channels(token string, offset, limit uint64) (ChannelsPage, error)
Channels(token string, offset, limit uint64, name string) (ChannelsPage, error)
// ChannelsByThing returns page of channels that are connected to specified
// thing.

View File

@ -276,7 +276,7 @@ func listChannelsEndpoint(svc things.Service) endpoint.Endpoint {
return nil, err
}
page, err := svc.ListChannels(req.token, req.offset, req.limit)
page, err := svc.ListChannels(req.token, req.offset, req.limit, req.name)
if err != nil {
return nil, err
}

View File

@ -876,6 +876,10 @@ func TestCreateChannel(t *testing.T) {
data := toJSON(channel)
th := channel
th.Name = invalidName
invalidData := toJSON(th)
cases := []struct {
desc string
req string
@ -940,6 +944,14 @@ func TestCreateChannel(t *testing.T) {
status: http.StatusUnsupportedMediaType,
location: "",
},
{
desc: "create new channel with invalid name",
req: invalidData,
contentType: contentType,
auth: token,
status: http.StatusBadRequest,
location: "",
},
}
for _, tc := range cases {
@ -965,9 +977,15 @@ func TestUpdateChannel(t *testing.T) {
ts := newServer(svc)
defer ts.Close()
updateData := toJSON(map[string]string{"name": "updated_channel"})
sch, _ := svc.CreateChannel(token, channel)
ch := channel
ch.Name = "updated_channel"
updateData := toJSON(ch)
ch.Name = invalidName
invalidData := toJSON(ch)
cases := []struct {
desc string
req string
@ -1048,6 +1066,13 @@ func TestUpdateChannel(t *testing.T) {
auth: token,
status: http.StatusUnsupportedMediaType,
},
{
desc: "update channel with invalid name",
req: invalidData,
contentType: contentType,
auth: token,
status: http.StatusBadRequest,
},
}
for _, tc := range cases {
@ -1270,6 +1295,13 @@ func TestListChannels(t *testing.T) {
url: fmt.Sprintf("%s%s", channelURL, "?offset=5&limit=e"),
res: nil,
},
{
desc: "get a list of channels with invalid name",
auth: token,
status: http.StatusBadRequest,
url: fmt.Sprintf("%s?offset=%d&limit=%d&name=%s", channelURL, 0, 10, invalidName),
res: nil,
},
}
for _, tc := range cases {

View File

@ -162,9 +162,13 @@ func (lm *loggingMiddleware) ViewChannel(token, id string) (channel things.Chann
return lm.svc.ViewChannel(token, id)
}
func (lm *loggingMiddleware) ListChannels(token string, offset, limit uint64) (_ things.ChannelsPage, err error) {
func (lm *loggingMiddleware) ListChannels(token string, offset, limit uint64, name string) (_ things.ChannelsPage, err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method list_channels for token %s took %s to complete", token, time.Since(begin))
nlog := ""
if name != "" {
nlog = fmt.Sprintf("with name %s ", name)
}
message := fmt.Sprintf("Method list_channels %sfor token %s took %s to complete", nlog, token, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
@ -172,7 +176,7 @@ func (lm *loggingMiddleware) ListChannels(token string, offset, limit uint64) (_
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.ListChannels(token, offset, limit)
return lm.svc.ListChannels(token, offset, limit, name)
}
func (lm *loggingMiddleware) ListChannelsByThing(token, id string, offset, limit uint64) (_ things.ChannelsPage, err error) {

View File

@ -124,13 +124,13 @@ func (ms *metricsMiddleware) ViewChannel(token, id string) (things.Channel, erro
return ms.svc.ViewChannel(token, id)
}
func (ms *metricsMiddleware) ListChannels(token string, offset, limit uint64) (things.ChannelsPage, error) {
func (ms *metricsMiddleware) ListChannels(token string, offset, limit uint64, name string) (things.ChannelsPage, error) {
defer func(begin time.Time) {
ms.counter.With("method", "list_channels").Add(1)
ms.latency.With("method", "list_channels").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.ListChannels(token, offset, limit)
return ms.svc.ListChannels(token, offset, limit, name)
}
func (ms *metricsMiddleware) ListChannelsByThing(token, id string, offset, limit uint64) (things.ChannelsPage, error) {

View File

@ -39,7 +39,7 @@ type ChannelRepository interface {
RetrieveByID(string, string) (Channel, error)
// RetrieveAll retrieves the subset of channels owned by the specified user.
RetrieveAll(string, uint64, uint64) (ChannelsPage, error)
RetrieveAll(string, uint64, uint64, string) (ChannelsPage, error)
// RetrieveByThing retrieves the subset of channels owned by the specified
// user and have specified thing connected to them.

View File

@ -79,7 +79,7 @@ func (crm *channelRepositoryMock) RetrieveByID(owner, id string) (things.Channel
return things.Channel{}, things.ErrNotFound
}
func (crm *channelRepositoryMock) RetrieveAll(owner string, offset, limit uint64) (things.ChannelsPage, error) {
func (crm *channelRepositoryMock) RetrieveAll(owner string, offset, limit uint64, name string) (things.ChannelsPage, error) {
channels := make([]things.Channel, 0)
if offset < 0 || limit <= 0 {

View File

@ -10,6 +10,7 @@ package postgres
import (
"database/sql"
"encoding/json"
"fmt"
"github.com/gofrs/uuid"
"github.com/jmoiron/sqlx"
@ -42,8 +43,11 @@ func (cr channelRepository) Save(channel things.Channel) (string, error) {
if _, err := cr.db.NamedExec(q, dbch); err != nil {
pqErr, ok := err.(*pq.Error)
if ok && errInvalid == pqErr.Code.Name() {
return "", things.ErrMalformedEntity
if ok {
switch pqErr.Code.Name() {
case errInvalid, errTruncation:
return "", things.ErrMalformedEntity
}
}
return "", err
@ -63,8 +67,11 @@ func (cr channelRepository) Update(channel things.Channel) error {
res, err := cr.db.NamedExec(q, dbch)
if err != nil {
pqErr, ok := err.(*pq.Error)
if ok && errInvalid == pqErr.Code.Name() {
return things.ErrMalformedEntity
if ok {
switch pqErr.Code.Name() {
case errInvalid, errTruncation:
return things.ErrMalformedEntity
}
}
return err
@ -100,13 +107,21 @@ func (cr channelRepository) RetrieveByID(owner, id string) (things.Channel, erro
return toChannel(dbch)
}
func (cr channelRepository) RetrieveAll(owner string, offset, limit uint64) (things.ChannelsPage, error) {
q := `SELECT id, name, metadata FROM channels WHERE owner = :owner ORDER BY id LIMIT :limit OFFSET :offset;`
func (cr channelRepository) RetrieveAll(owner string, offset, limit uint64, name string) (things.ChannelsPage, error) {
nq := ""
if name != "" {
name = fmt.Sprintf(`%%%s%%`, name)
nq = `AND name LIKE :name`
}
q := fmt.Sprintf(`SELECT id, name, metadata FROM channels
WHERE owner = :owner %s ORDER BY id LIMIT :limit OFFSET :offset;`, nq)
params := map[string]interface{}{
"owner": owner,
"limit": limit,
"offset": offset,
"name": name,
}
rows, err := cr.db.NamedQuery(q, params)
if err != nil {
@ -128,11 +143,23 @@ func (cr channelRepository) RetrieveAll(owner string, offset, limit uint64) (thi
items = append(items, ch)
}
q = `SELECT COUNT(*) FROM channels WHERE owner = $1;`
cq := ""
if name != "" {
cq = `AND name = $2`
}
var total uint64
if err := cr.db.Get(&total, q, owner); err != nil {
return things.ChannelsPage{}, err
q = fmt.Sprintf(`SELECT COUNT(*) FROM channels WHERE owner = $1 %s;`, cq)
total := uint64(0)
switch name {
case "":
if err := cr.db.Get(&total, q, owner); err != nil {
return things.ChannelsPage{}, err
}
default:
if err := cr.db.Get(&total, q, owner, name); err != nil {
return things.ChannelsPage{}, err
}
}
page := things.ChannelsPage{

View File

@ -25,6 +25,7 @@ func TestChannelSave(t *testing.T) {
id, err := uuid.New().ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
channel := things.Channel{
ID: id,
Owner: email,
@ -41,13 +42,22 @@ func TestChannelSave(t *testing.T) {
err: nil,
},
{
desc: "create invalid channel",
desc: "create channel with invalid ID",
channel: things.Channel{
ID: "invalid",
Owner: email,
},
err: things.ErrMalformedEntity,
},
{
desc: "create channel with invalid name",
channel: things.Channel{
ID: id,
Owner: email,
Name: invalidName,
},
err: things.ErrMalformedEntity,
},
}
for _, tc := range cases {
@ -182,9 +192,9 @@ func TestSingleChannelRetrieval(t *testing.T) {
func TestMultiChannelRetrieval(t *testing.T) {
email := "channel-multi-retrieval@example.com"
chanRepo := postgres.NewChannelRepository(db)
channelName := "channel_name"
n := uint64(10)
for i := uint64(0); i < n; i++ {
chid, err := uuid.New().ID()
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
@ -193,6 +203,11 @@ func TestMultiChannelRetrieval(t *testing.T) {
ID: chid,
Owner: email,
}
if i == 0 {
c.Name = channelName
}
chanRepo.Save(c)
}
@ -200,6 +215,7 @@ func TestMultiChannelRetrieval(t *testing.T) {
owner string
offset uint64
limit uint64
name string
size uint64
}{
"retrieve all channels with existing owner": {
@ -220,10 +236,24 @@ func TestMultiChannelRetrieval(t *testing.T) {
limit: n,
size: 0,
},
"retrieve all channels with existing name": {
owner: email,
offset: 0,
limit: n,
name: channelName,
size: 1,
},
"retrieve all channels with non-existing name": {
owner: email,
offset: 0,
limit: n,
name: "wrong",
size: 0,
},
}
for desc, tc := range cases {
page, err := chanRepo.RetrieveAll(tc.owner, tc.offset, tc.limit)
page, err := chanRepo.RetrieveAll(tc.owner, tc.offset, tc.limit, tc.name)
size := uint64(len(page.Channels))
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.size, size))
assert.Nil(t, err, fmt.Sprintf("%s: expected no error got %d\n", desc, err))

View File

@ -63,7 +63,7 @@ func migrateDB(db *sqlx.DB) error {
`CREATE TABLE IF NOT EXISTS channels (
id UUID,
owner VARCHAR(254),
name TEXT,
name VARCHAR(1024),
metadata JSON,
PRIMARY KEY (id, owner)
)`,

View File

@ -412,14 +412,14 @@ func TestMultiThingRetrieval(t *testing.T) {
limit: n,
size: 0,
},
"retrieve things with non-existing name": {
"retrieve things with existing name": {
owner: email,
offset: 0,
limit: n,
name: name,
size: 1,
},
"retrieve things with existing name": {
"retrieve things with non-existing name": {
owner: email,
offset: 0,
limit: n,

View File

@ -158,8 +158,8 @@ func (es eventStore) ViewChannel(token, id string) (things.Channel, error) {
return es.svc.ViewChannel(token, id)
}
func (es eventStore) ListChannels(token string, offset, limit uint64) (things.ChannelsPage, error) {
return es.svc.ListChannels(token, offset, limit)
func (es eventStore) ListChannels(token string, offset, limit uint64, name string) (things.ChannelsPage, error) {
return es.svc.ListChannels(token, offset, limit, name)
}
func (es eventStore) ListChannelsByThing(token, id string, offset, limit uint64) (things.ChannelsPage, error) {

View File

@ -417,8 +417,8 @@ func TestListChannels(t *testing.T) {
require.Nil(t, err, fmt.Sprintf("unexpected error %s", err))
essvc := redis.NewEventStoreMiddleware(svc, redisClient)
eschs, eserr := essvc.ListChannels(token, 0, 10)
chs, err := svc.ListChannels(token, 0, 10)
eschs, eserr := essvc.ListChannels(token, 0, 10, "")
chs, err := svc.ListChannels(token, 0, 10, "")
assert.Equal(t, chs, eschs, fmt.Sprintf("event sourcing changed service behaviour: expected %v got %v", chs, eschs))
assert.Equal(t, err, eserr, fmt.Sprintf("event sourcing changed service behaviour: expected %v got %v", err, eserr))
}

View File

@ -75,7 +75,7 @@ type Service interface {
// ListChannels retrieves data about subset of channels that belongs to the
// user identified by the provided key.
ListChannels(string, uint64, uint64) (ChannelsPage, error)
ListChannels(string, uint64, uint64, string) (ChannelsPage, error)
// ListChannelsByThing retrieves data about subset of channels that have
// specified thing connected to them and belong to the user identified by
@ -106,6 +106,7 @@ type PageMetadata struct {
Total uint64
Offset uint64
Limit uint64
Name string
}
var _ Service = (*thingsService)(nil)
@ -299,7 +300,7 @@ func (ts *thingsService) ViewChannel(token, id string) (Channel, error) {
return ts.channels.RetrieveByID(res.GetValue(), id)
}
func (ts *thingsService) ListChannels(token string, offset, limit uint64) (ChannelsPage, error) {
func (ts *thingsService) ListChannels(token string, offset, limit uint64, name string) (ChannelsPage, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
@ -308,7 +309,7 @@ func (ts *thingsService) ListChannels(token string, offset, limit uint64) (Chann
return ChannelsPage{}, ErrUnauthorizedAccess
}
return ts.channels.RetrieveAll(res.GetValue(), offset, limit)
return ts.channels.RetrieveAll(res.GetValue(), offset, limit, name)
}
func (ts *thingsService) ListChannelsByThing(token, thing string, offset, limit uint64) (ChannelsPage, error) {

View File

@ -491,6 +491,7 @@ func TestListChannels(t *testing.T) {
offset uint64
limit uint64
size uint64
name string
err error
}{
"list all channels": {
@ -535,10 +536,26 @@ func TestListChannels(t *testing.T) {
size: 0,
err: things.ErrUnauthorizedAccess,
},
"list with existing name": {
token: token,
offset: 0,
limit: n,
size: n,
name: "chanel_name",
err: nil,
},
"list with non-existent name": {
token: token,
offset: 0,
limit: n,
size: n,
name: "wrong",
err: nil,
},
}
for desc, tc := range cases {
page, err := svc.ListChannels(tc.token, tc.offset, tc.limit)
page, err := svc.ListChannels(tc.token, tc.offset, tc.limit, tc.name)
size := uint64(len(page.Channels))
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.size, size))
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))