diff --git a/bootstrap/mocks/things.go b/bootstrap/mocks/things.go index aadcf138..44918d53 100644 --- a/bootstrap/mocks/things.go +++ b/bootstrap/mocks/things.go @@ -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") } diff --git a/cli/channels.go b/cli/channels.go index 4167dc55..11408cd0 100644 --- a/cli/channels.go +++ b/cli/channels.go @@ -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 diff --git a/sdk/go/channels.go b/sdk/go/channels.go index 8ba2a244..ae4a4056 100644 --- a/sdk/go/channels.go +++ b/sdk/go/channels.go @@ -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) diff --git a/sdk/go/channels_test.go b/sdk/go/channels_test.go index 0bacf83a..9c78d153 100644 --- a/sdk/go/channels_test.go +++ b/sdk/go/channels_test.go @@ -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)) } diff --git a/sdk/go/sdk.go b/sdk/go/sdk.go index 55186878..1785ac1d 100644 --- a/sdk/go/sdk.go +++ b/sdk/go/sdk.go @@ -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. diff --git a/things/api/http/endpoint.go b/things/api/http/endpoint.go index f86a95b1..1e0eb496 100644 --- a/things/api/http/endpoint.go +++ b/things/api/http/endpoint.go @@ -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 } diff --git a/things/api/http/endpoint_test.go b/things/api/http/endpoint_test.go index 15d3259d..50f9143f 100644 --- a/things/api/http/endpoint_test.go +++ b/things/api/http/endpoint_test.go @@ -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 { diff --git a/things/api/logging.go b/things/api/logging.go index f587f8ff..6ae69c30 100644 --- a/things/api/logging.go +++ b/things/api/logging.go @@ -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) { diff --git a/things/api/metrics.go b/things/api/metrics.go index 7f170271..a1bc013a 100644 --- a/things/api/metrics.go +++ b/things/api/metrics.go @@ -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) { diff --git a/things/channels.go b/things/channels.go index 3cb51209..bab02ff0 100644 --- a/things/channels.go +++ b/things/channels.go @@ -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. diff --git a/things/mocks/channels.go b/things/mocks/channels.go index ce2e38d2..7f2d741f 100644 --- a/things/mocks/channels.go +++ b/things/mocks/channels.go @@ -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 { diff --git a/things/postgres/channels.go b/things/postgres/channels.go index 61f12583..8fba80a9 100644 --- a/things/postgres/channels.go +++ b/things/postgres/channels.go @@ -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{ diff --git a/things/postgres/channels_test.go b/things/postgres/channels_test.go index abd65478..494c3129 100644 --- a/things/postgres/channels_test.go +++ b/things/postgres/channels_test.go @@ -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)) diff --git a/things/postgres/init.go b/things/postgres/init.go index 2b6e7673..ca8aef74 100644 --- a/things/postgres/init.go +++ b/things/postgres/init.go @@ -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) )`, diff --git a/things/postgres/things_test.go b/things/postgres/things_test.go index 323e2ad2..381ff936 100644 --- a/things/postgres/things_test.go +++ b/things/postgres/things_test.go @@ -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, diff --git a/things/redis/streams.go b/things/redis/streams.go index 15411284..b72c024a 100644 --- a/things/redis/streams.go +++ b/things/redis/streams.go @@ -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) { diff --git a/things/redis/streams_test.go b/things/redis/streams_test.go index 18f70aeb..bf116a10 100644 --- a/things/redis/streams_test.go +++ b/things/redis/streams_test.go @@ -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)) } diff --git a/things/service.go b/things/service.go index d71b580f..6784c3a6 100644 --- a/things/service.go +++ b/things/service.go @@ -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) { diff --git a/things/service_test.go b/things/service_test.go index 90e47eb3..22226480 100644 --- a/things/service_test.go +++ b/things/service_test.go @@ -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))