From 06800c1038ee829547181e24b43767fef9ac60d8 Mon Sep 17 00:00:00 2001 From: Sammy Kerata Oina <44265300+SammyOina@users.noreply.github.com> Date: Tue, 8 Aug 2023 13:19:54 +0300 Subject: [PATCH] NOISSUE - Unify group and clients implementations on things and users (#1793) * unify groups repo implementation Signed-off-by: SammyOina * unify clients implementation Signed-off-by: SammyOina * closer client integration Signed-off-by: SammyOina * further unification of groups Signed-off-by: SammyOina * enable on update secret & owner Signed-off-by: SammyOina * unify retrieve all Signed-off-by: SammyOina * fully unify groups repository Signed-off-by: SammyOina * add secret to retrieve all Signed-off-by: SammyOina * save updated at Signed-off-by: SammyOina * fix test Signed-off-by: SammyOina * fix retrieve all tests Signed-off-by: SammyOina * restore files Signed-off-by: SammyOina * fix build Signed-off-by: SammyOina * remove unused files Signed-off-by: SammyOina * fix retrieve all tests Signed-off-by: SammyOina * fix linting Signed-off-by: SammyOina * fix linting Signed-off-by: SammyOina * restore broken changes Signed-off-by: SammyOina * restore setup tests Signed-off-by: SammyOina * update where condition Signed-off-by: SammyOina * remove extra db object Signed-off-by: SammyOina * unify groups test Signed-off-by: SammyOina * unify clients test Signed-off-by: SammyOina * remove unused variables Signed-off-by: SammyOina * update changes Signed-off-by: SammyOina * sync with master current updates Signed-off-by: SammyOina * update test Signed-off-by: SammyOina * fix tests Signed-off-by: SammyOina * fix test Signed-off-by: SammyOina * fix test Signed-off-by: SammyOina * fix tests Signed-off-by: SammyOina * match changes in #1877 Signed-off-by: SammyOina * separate things and users repos Signed-off-by: SammyOina * remove comments implement retrieveBysecret in things only Signed-off-by: SammyOina * remove exec Signed-off-by: SammyOina * remove duplicate imports Signed-off-by: SammyOina * wrap errors Signed-off-by: SammyOina --------- Signed-off-by: SammyOina --- cmd/things/main.go | 4 +- cmd/users/main.go | 10 +- pkg/clients/clients.go | 5 - pkg/clients/postgres/clients.go | 456 ++++++++ pkg/clients/postgres/clients_test.go | 997 ++++++++++++++++++ pkg/clients/postgres/doc.go | 5 + .../clients}/postgres/setup_test.go | 5 +- {things => pkg}/groups/postgres/doc.go | 0 {users => pkg}/groups/postgres/groups.go | 182 ++-- {users => pkg}/groups/postgres/groups_test.go | 14 +- {users => pkg}/groups/postgres/setup_test.go | 0 things/clients/postgres/clients.go | 441 +------- things/clients/postgres/clients_test.go | 834 --------------- things/clients/service.go | 7 +- things/groups/postgres/groups.go | 443 -------- things/groups/postgres/groups_test.go | 600 ----------- things/policies/postgres/policies_test.go | 14 +- users/clients/mocks/clients.go | 12 +- users/clients/postgres/clients.go | 478 +-------- users/clients/postgres/clients_test.go | 951 +---------------- users/clients/service.go | 7 +- users/groups/postgres/doc.go | 5 - users/policies/postgres/policies_test.go | 28 +- 23 files changed, 1669 insertions(+), 3829 deletions(-) create mode 100644 pkg/clients/postgres/clients.go create mode 100644 pkg/clients/postgres/clients_test.go create mode 100644 pkg/clients/postgres/doc.go rename {things/groups => pkg/clients}/postgres/setup_test.go (94%) rename {things => pkg}/groups/postgres/doc.go (100%) rename {users => pkg}/groups/postgres/groups.go (96%) rename {users => pkg}/groups/postgres/groups_test.go (98%) rename {users => pkg}/groups/postgres/setup_test.go (100%) delete mode 100644 things/groups/postgres/groups.go delete mode 100644 things/groups/postgres/groups_test.go delete mode 100644 users/groups/postgres/doc.go diff --git a/cmd/things/main.go b/cmd/things/main.go index de0db41d..d4341b03 100644 --- a/cmd/things/main.go +++ b/cmd/things/main.go @@ -27,6 +27,7 @@ import ( grpcserver "github.com/mainflux/mainflux/internal/server/grpc" httpserver "github.com/mainflux/mainflux/internal/server/http" mflog "github.com/mainflux/mainflux/logger" + gpostgres "github.com/mainflux/mainflux/pkg/groups/postgres" "github.com/mainflux/mainflux/pkg/uuid" "github.com/mainflux/mainflux/things/clients" capi "github.com/mainflux/mainflux/things/clients/api" @@ -36,7 +37,6 @@ import ( ctracing "github.com/mainflux/mainflux/things/clients/tracing" "github.com/mainflux/mainflux/things/groups" gapi "github.com/mainflux/mainflux/things/groups/api" - gpostgres "github.com/mainflux/mainflux/things/groups/postgres" chcache "github.com/mainflux/mainflux/things/groups/redis" gtracing "github.com/mainflux/mainflux/things/groups/tracing" tpolicies "github.com/mainflux/mainflux/things/policies" @@ -214,7 +214,7 @@ func main() { func newService(ctx context.Context, db *sqlx.DB, dbConfig pgClient.Config, auth upolicies.AuthServiceClient, cacheClient *redis.Client, esClient *redis.Client, keyDuration string, tracer trace.Tracer, logger mflog.Logger) (clients.Service, groups.Service, tpolicies.Service) { database := postgres.NewDatabase(db, dbConfig, tracer) cRepo := cpostgres.NewRepository(database) - gRepo := gpostgres.NewRepository(database) + gRepo := gpostgres.New(database) pRepo := ppostgres.NewRepository(database) idp := uuid.New() diff --git a/cmd/users/main.go b/cmd/users/main.go index 2a6e4f5a..34e17ca7 100644 --- a/cmd/users/main.go +++ b/cmd/users/main.go @@ -29,16 +29,16 @@ import ( httpserver "github.com/mainflux/mainflux/internal/server/http" mflog "github.com/mainflux/mainflux/logger" mfclients "github.com/mainflux/mainflux/pkg/clients" + gpostgres "github.com/mainflux/mainflux/pkg/groups/postgres" "github.com/mainflux/mainflux/pkg/uuid" "github.com/mainflux/mainflux/users/clients" capi "github.com/mainflux/mainflux/users/clients/api" "github.com/mainflux/mainflux/users/clients/emailer" - cpostgres "github.com/mainflux/mainflux/users/clients/postgres" + uclients "github.com/mainflux/mainflux/users/clients/postgres" ucache "github.com/mainflux/mainflux/users/clients/redis" ctracing "github.com/mainflux/mainflux/users/clients/tracing" "github.com/mainflux/mainflux/users/groups" gapi "github.com/mainflux/mainflux/users/groups/api" - gpostgres "github.com/mainflux/mainflux/users/groups/postgres" gcache "github.com/mainflux/mainflux/users/groups/redis" gtracing "github.com/mainflux/mainflux/users/groups/tracing" "github.com/mainflux/mainflux/users/hasher" @@ -200,8 +200,8 @@ func main() { func newService(ctx context.Context, db *sqlx.DB, dbConfig pgClient.Config, esClient *redis.Client, tracer trace.Tracer, c config, ec email.Config, logger mflog.Logger) (clients.Service, groups.Service, policies.Service) { database := postgres.NewDatabase(db, dbConfig, tracer) - cRepo := cpostgres.NewRepository(database) - gRepo := gpostgres.NewRepository(database) + cRepo := uclients.NewRepository(database) + gRepo := gpostgres.New(database) pRepo := ppostgres.NewRepository(database) idp := uuid.New() @@ -250,7 +250,7 @@ func newService(ctx context.Context, db *sqlx.DB, dbConfig pgClient.Config, esCl return csvc, gsvc, psvc } -func createAdmin(ctx context.Context, c config, crepo mfclients.Repository, hsr clients.Hasher, svc clients.Service) error { +func createAdmin(ctx context.Context, c config, crepo uclients.Repository, hsr clients.Hasher, svc clients.Service) error { id, err := uuid.New().ID() if err != nil { return err diff --git a/pkg/clients/clients.go b/pkg/clients/clients.go index c64c8532..d25bbcc5 100644 --- a/pkg/clients/clients.go +++ b/pkg/clients/clients.go @@ -68,9 +68,6 @@ type MembersPage struct { // Repository specifies an account persistence API. type Repository interface { - // Save persists the client account. A non-nil error is returned to indicate - // operation failure. - Save(ctx context.Context, client ...Client) ([]Client, error) // RetrieveByID retrieves client by its unique ID. RetrieveByID(ctx context.Context, id string) (Client, error) @@ -101,8 +98,6 @@ type Repository interface { // ChangeStatus changes client status to enabled or disabled ChangeStatus(ctx context.Context, client Client) (Client, error) - - RetrieveBySecret(ctx context.Context, key string) (Client, error) } // Validate returns an error if client representation is invalid. diff --git a/pkg/clients/postgres/clients.go b/pkg/clients/postgres/clients.go new file mode 100644 index 00000000..fdacdd57 --- /dev/null +++ b/pkg/clients/postgres/clients.go @@ -0,0 +1,456 @@ +package postgres + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/jackc/pgtype" + "github.com/mainflux/mainflux/internal/postgres" + "github.com/mainflux/mainflux/pkg/clients" + "github.com/mainflux/mainflux/pkg/errors" + "github.com/mainflux/mainflux/pkg/groups" +) + +type ClientRepository struct { + DB postgres.Database +} + +func (repo ClientRepository) Update(ctx context.Context, client clients.Client) (clients.Client, error) { + var query []string + var upq string + if client.Name != "" { + query = append(query, "name = :name,") + } + if client.Metadata != nil { + query = append(query, "metadata = :metadata,") + } + if len(query) > 0 { + upq = strings.Join(query, " ") + } + client.Status = clients.EnabledStatus + q := fmt.Sprintf(`UPDATE clients SET %s updated_at = :updated_at, updated_by = :updated_by + WHERE id = :id AND status = :status + RETURNING id, name, tags, identity, secret, metadata, COALESCE(owner_id, '') AS owner_id, status, created_at, updated_at, updated_by`, + upq) + + return repo.update(ctx, client, q) +} + +func (repo ClientRepository) UpdateTags(ctx context.Context, client clients.Client) (clients.Client, error) { + client.Status = clients.EnabledStatus + q := `UPDATE clients SET tags = :tags, updated_at = :updated_at, updated_by = :updated_by + WHERE id = :id AND status = :status + RETURNING id, name, tags, identity, metadata, COALESCE(owner_id, '') AS owner_id, status, created_at, updated_at, updated_by` + + return repo.update(ctx, client, q) +} + +func (repo ClientRepository) UpdateIdentity(ctx context.Context, client clients.Client) (clients.Client, error) { + q := `UPDATE clients SET identity = :identity, updated_at = :updated_at, updated_by = :updated_by + WHERE id = :id AND status = :status + RETURNING id, name, tags, identity, metadata, COALESCE(owner_id, '') AS owner_id, status, created_at, updated_at, updated_by` + + return repo.update(ctx, client, q) +} + +func (repo ClientRepository) UpdateSecret(ctx context.Context, client clients.Client) (clients.Client, error) { + q := `UPDATE clients SET secret = :secret, updated_at = :updated_at, updated_by = :updated_by + WHERE id = :id AND status = :status + RETURNING id, name, tags, identity, metadata, COALESCE(owner_id, '') AS owner_id, status, created_at, updated_at, updated_by` + + return repo.update(ctx, client, q) +} + +func (repo ClientRepository) UpdateOwner(ctx context.Context, client clients.Client) (clients.Client, error) { + q := `UPDATE clients SET owner_id = :owner_id, updated_at = :updated_at, updated_by = :updated_by + WHERE id = :id AND status = :status + RETURNING id, name, tags, identity, metadata, COALESCE(owner_id, '') AS owner_id, status, created_at, updated_at, updated_by` + + return repo.update(ctx, client, q) +} + +func (repo ClientRepository) ChangeStatus(ctx context.Context, client clients.Client) (clients.Client, error) { + q := `UPDATE clients SET status = :status WHERE id = :id + RETURNING id, name, tags, identity, metadata, COALESCE(owner_id, '') AS owner_id, status, created_at, updated_at, updated_by` + + return repo.update(ctx, client, q) +} + +func (repo ClientRepository) RetrieveByID(ctx context.Context, id string) (clients.Client, error) { + q := `SELECT id, name, tags, COALESCE(owner_id, '') AS owner_id, identity, secret, metadata, created_at, updated_at, updated_by, status + FROM clients WHERE id = :id` + + dbc := DBClient{ + ID: id, + } + + row, err := repo.DB.NamedQueryContext(ctx, q, dbc) + if err != nil { + if err == sql.ErrNoRows { + return clients.Client{}, errors.Wrap(errors.ErrNotFound, err) + } + return clients.Client{}, errors.Wrap(errors.ErrViewEntity, err) + } + + defer row.Close() + row.Next() + dbc = DBClient{} + if err := row.StructScan(&dbc); err != nil { + return clients.Client{}, errors.Wrap(errors.ErrNotFound, err) + } + + return ToClient(dbc) +} + +func (repo ClientRepository) RetrieveByIdentity(ctx context.Context, identity string) (clients.Client, error) { + q := `SELECT id, name, tags, COALESCE(owner_id, '') AS owner_id, identity, secret, metadata, created_at, updated_at, updated_by, status + FROM clients WHERE identity = :identity AND status = :status` + + dbc := DBClient{ + Identity: identity, + Status: clients.EnabledStatus, + } + + row, err := repo.DB.NamedQueryContext(ctx, q, dbc) + if err != nil { + if err == sql.ErrNoRows { + return clients.Client{}, errors.Wrap(errors.ErrNotFound, err) + } + return clients.Client{}, errors.Wrap(errors.ErrViewEntity, err) + } + + defer row.Close() + row.Next() + dbc = DBClient{} + if err := row.StructScan(&dbc); err != nil { + return clients.Client{}, errors.Wrap(errors.ErrNotFound, err) + } + + return ToClient(dbc) +} + +func (repo ClientRepository) RetrieveAll(ctx context.Context, pm clients.Page) (clients.ClientsPage, error) { + query, err := pageQuery(pm) + if err != nil { + return clients.ClientsPage{}, errors.Wrap(errors.ErrViewEntity, err) + } + + q := fmt.Sprintf(`SELECT c.id, c.name, c.tags, c.identity, c.secret, c.metadata, COALESCE(c.owner_id, '') AS owner_id, c.status, + c.created_at, c.updated_at, COALESCE(c.updated_by, '') AS updated_by FROM clients c %s ORDER BY c.created_at LIMIT :limit OFFSET :offset;`, query) + + dbPage, err := toDBClientsPage(pm) + if err != nil { + return clients.ClientsPage{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) + } + rows, err := repo.DB.NamedQueryContext(ctx, q, dbPage) + if err != nil { + return clients.ClientsPage{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) + } + defer rows.Close() + + var items []clients.Client + for rows.Next() { + dbc := DBClient{} + if err := rows.StructScan(&dbc); err != nil { + return clients.ClientsPage{}, errors.Wrap(errors.ErrViewEntity, err) + } + + c, err := ToClient(dbc) + if err != nil { + return clients.ClientsPage{}, err + } + + items = append(items, c) + } + cq := fmt.Sprintf(`SELECT COUNT(*) FROM clients c %s;`, query) + + total, err := postgres.Total(ctx, repo.DB, cq, dbPage) + if err != nil { + return clients.ClientsPage{}, errors.Wrap(errors.ErrViewEntity, err) + } + + page := clients.ClientsPage{ + Clients: items, + Page: clients.Page{ + Total: total, + Offset: pm.Offset, + Limit: pm.Limit, + }, + } + + return page, nil +} + +func (repo ClientRepository) Members(ctx context.Context, groupID string, pm clients.Page) (clients.MembersPage, error) { + emq, err := pageQuery(pm) + if err != nil { + return clients.MembersPage{}, err + } + + aq := "" + // If not admin, the client needs to have a g_list action on the group or they are the owner. + if pm.Subject != "" { + aq = `AND (EXISTS (SELECT 1 FROM policies p WHERE p.subject = :subject AND :action=ANY(actions)) + OR EXISTS (SELECT 1 FROM groups g WHERE g.owner_id = :subject AND g.id = :group_id)) + AND c.id != :subject` + } + q := fmt.Sprintf(`SELECT c.id, c.name, c.tags, c.metadata, c.identity, c.status, + c.created_at, c.updated_at FROM clients c + INNER JOIN policies ON c.id=policies.subject %s AND policies.object = :group_id %s + ORDER BY c.created_at LIMIT :limit OFFSET :offset;`, emq, aq) + dbPage, err := toDBClientsPage(pm) + if err != nil { + return clients.MembersPage{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) + } + dbPage.GroupID = groupID + rows, err := repo.DB.NamedQueryContext(ctx, q, dbPage) + if err != nil { + return clients.MembersPage{}, errors.Wrap(postgres.ErrFailedToRetrieveMembers, err) + } + defer rows.Close() + + var items []clients.Client + for rows.Next() { + dbc := DBClient{} + if err := rows.StructScan(&dbc); err != nil { + return clients.MembersPage{}, errors.Wrap(postgres.ErrFailedToRetrieveMembers, err) + } + + c, err := ToClient(dbc) + if err != nil { + return clients.MembersPage{}, err + } + + items = append(items, c) + } + cq := fmt.Sprintf(`SELECT COUNT(*) FROM clients c INNER JOIN policies ON c.id=policies.subject %s AND policies.object = :group_id`, emq) + if pm.Subject != "" { + cq = fmt.Sprintf("%s AND c.id != :subject", cq) + } + + total, err := postgres.Total(ctx, repo.DB, cq, dbPage) + if err != nil { + return clients.MembersPage{}, errors.Wrap(postgres.ErrFailedToRetrieveMembers, err) + } + + page := clients.MembersPage{ + Members: items, + Page: clients.Page{ + Total: total, + Offset: pm.Offset, + Limit: pm.Limit, + }, + } + return page, nil +} + +// generic update function. +func (repo ClientRepository) update(ctx context.Context, client clients.Client, query string) (clients.Client, error) { + dbc, err := ToDBClient(client) + if err != nil { + return clients.Client{}, errors.Wrap(errors.ErrUpdateEntity, err) + } + + row, err := repo.DB.NamedQueryContext(ctx, query, dbc) + if err != nil { + return clients.Client{}, postgres.HandleError(err, errors.ErrUpdateEntity) + } + + defer row.Close() + if ok := row.Next(); !ok { + return clients.Client{}, errors.Wrap(errors.ErrNotFound, row.Err()) + } + dbc = DBClient{} + if err := row.StructScan(&dbc); err != nil { + return clients.Client{}, err + } + + return ToClient(dbc) +} + +type DBClient struct { + ID string `db:"id"` + Name string `db:"name,omitempty"` + Tags pgtype.TextArray `db:"tags,omitempty"` + Identity string `db:"identity"` + Owner *string `db:"owner_id,omitempty"` // nullable + Secret string `db:"secret"` + Metadata []byte `db:"metadata,omitempty"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt sql.NullTime `db:"updated_at,omitempty"` + UpdatedBy *string `db:"updated_by,omitempty"` + Groups []groups.Group `db:"groups,omitempty"` + Status clients.Status `db:"status"` + Role clients.Role `db:"role,omitempty"` +} + +func ToDBClient(c clients.Client) (DBClient, error) { + data := []byte("{}") + if len(c.Metadata) > 0 { + b, err := json.Marshal(c.Metadata) + if err != nil { + return DBClient{}, errors.Wrap(errors.ErrMalformedEntity, err) + } + data = b + } + var tags pgtype.TextArray + if err := tags.Set(c.Tags); err != nil { + return DBClient{}, err + } + var owner *string + if c.Owner != "" { + owner = &c.Owner + } + var updatedBy *string + if c.UpdatedBy != "" { + updatedBy = &c.UpdatedBy + } + var updatedAt sql.NullTime + if c.UpdatedAt != (time.Time{}) { + updatedAt = sql.NullTime{Time: c.UpdatedAt, Valid: true} + } + + return DBClient{ + ID: c.ID, + Name: c.Name, + Tags: tags, + Owner: owner, + Identity: c.Credentials.Identity, + Secret: c.Credentials.Secret, + Metadata: data, + CreatedAt: c.CreatedAt, + UpdatedAt: updatedAt, + UpdatedBy: updatedBy, + Status: c.Status, + Role: c.Role, + }, nil +} + +func ToClient(c DBClient) (clients.Client, error) { + var metadata clients.Metadata + if c.Metadata != nil { + if err := json.Unmarshal([]byte(c.Metadata), &metadata); err != nil { + return clients.Client{}, errors.Wrap(errors.ErrMalformedEntity, err) + } + } + var tags []string + for _, e := range c.Tags.Elements { + tags = append(tags, e.String) + } + var owner string + if c.Owner != nil { + owner = *c.Owner + } + var updatedBy string + if c.UpdatedBy != nil { + updatedBy = *c.UpdatedBy + } + var updatedAt time.Time + if c.UpdatedAt.Valid { + updatedAt = c.UpdatedAt.Time + } + + return clients.Client{ + ID: c.ID, + Name: c.Name, + Tags: tags, + Owner: owner, + Credentials: clients.Credentials{ + Identity: c.Identity, + Secret: c.Secret, + }, + Metadata: metadata, + CreatedAt: c.CreatedAt, + UpdatedAt: updatedAt, + UpdatedBy: updatedBy, + Status: c.Status, + }, nil +} + +func toDBClientsPage(pm clients.Page) (dbClientsPage, error) { + _, data, err := postgres.CreateMetadataQuery("", pm.Metadata) + if err != nil { + return dbClientsPage{}, errors.Wrap(errors.ErrViewEntity, err) + } + return dbClientsPage{ + Name: pm.Name, + Identity: pm.Identity, + Metadata: data, + Owner: pm.Owner, + Total: pm.Total, + Offset: pm.Offset, + Limit: pm.Limit, + Status: pm.Status, + Tag: pm.Tag, + Subject: pm.Subject, + Action: pm.Action, + SharedBy: pm.SharedBy, + }, nil +} + +type dbClientsPage struct { + Total uint64 `db:"total"` + Limit uint64 `db:"limit"` + Offset uint64 `db:"offset"` + Name string `db:"name"` + Owner string `db:"owner_id"` + Identity string `db:"identity"` + Metadata []byte `db:"metadata"` + Tag string `db:"tag"` + Status clients.Status `db:"status"` + GroupID string `db:"group_id"` + SharedBy string `db:"shared_by"` + Subject string `db:"subject"` + Action string `db:"action"` +} + +func pageQuery(pm clients.Page) (string, error) { + mq, _, err := postgres.CreateMetadataQuery("", pm.Metadata) + if err != nil { + return "", errors.Wrap(errors.ErrViewEntity, err) + } + var query []string + var emq string + if mq != "" { + query = append(query, mq) + } + if len(pm.IDs) != 0 { + query = append(query, fmt.Sprintf("id IN ('%s')", strings.Join(pm.IDs, "','"))) + } + if pm.Identity != "" { + query = append(query, "c.identity = :identity") + } + if pm.Name != "" { + query = append(query, "c.name = :name") + } + if pm.Tag != "" { + query = append(query, ":tag = ANY(c.tags)") + } + if pm.Status != clients.AllStatus { + query = append(query, "c.status = :status") + } + // For listing clients that the specified client owns but not sharedby + if pm.Owner != "" && pm.SharedBy == "" { + query = append(query, "c.owner_id = :owner_id") + } + + // For listing clients that the specified client owns and that are shared with the specified client + if pm.Owner != "" && pm.SharedBy != "" { + query = append(query, "(c.owner_id = :owner_id OR (policies.object IN (SELECT object FROM policies WHERE subject = :shared_by AND :action=ANY(actions)))) AND c.id != :shared_by") + } + // For listing clients that the specified client is shared with + if pm.SharedBy != "" && pm.Owner == "" { + query = append(query, "c.owner_id != :shared_by AND (policies.object IN (SELECT object FROM policies WHERE subject = :shared_by AND :action=ANY(actions)))") + } + if len(query) > 0 { + emq = fmt.Sprintf("WHERE %s", strings.Join(query, " AND ")) + if strings.Contains(emq, "policies") { + emq = fmt.Sprintf("LEFT JOIN policies ON policies.subject = c.id %s", emq) + } + } + return emq, nil +} diff --git a/pkg/clients/postgres/clients_test.go b/pkg/clients/postgres/clients_test.go new file mode 100644 index 00000000..9a6165d3 --- /dev/null +++ b/pkg/clients/postgres/clients_test.go @@ -0,0 +1,997 @@ +// Copyright (c) Mainflux +// SPDX-License-Identifier: Apache-2.0 + +package postgres_test + +import ( + "context" + "fmt" + "testing" + + "github.com/mainflux/mainflux/internal/testsutil" + mfclients "github.com/mainflux/mainflux/pkg/clients" + "github.com/mainflux/mainflux/pkg/errors" + mfgroups "github.com/mainflux/mainflux/pkg/groups" + gpostgres "github.com/mainflux/mainflux/pkg/groups/postgres" + "github.com/mainflux/mainflux/pkg/uuid" + cpostgres "github.com/mainflux/mainflux/users/clients/postgres" + "github.com/mainflux/mainflux/users/policies" + ppostgres "github.com/mainflux/mainflux/users/policies/postgres" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + idProvider = uuid.New() + password = "$tr0ngPassw0rd" + clientIdentity = "client-identity@example.com" + clientName = "client name" + wrongName = "wrong-name" + wrongID = "wrong-id" +) + +func TestClientsRetrieveByID(t *testing.T) { + t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) + repo := cpostgres.NewRepository(database) + + client := mfclients.Client{ + ID: testsutil.GenerateUUID(t, idProvider), + Name: clientName, + Credentials: mfclients.Credentials{ + Identity: clientIdentity, + Secret: password, + }, + Status: mfclients.EnabledStatus, + } + + clients, err := repo.Save(context.Background(), client) + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + client = clients + + cases := map[string]struct { + ID string + err error + }{ + "retrieve existing client": {client.ID, nil}, + "retrieve non-existing client": {wrongID, errors.ErrNotFound}, + } + + for desc, tc := range cases { + cli, err := repo.RetrieveByID(context.Background(), tc.ID) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err)) + if err == nil { + assert.Equal(t, client.ID, cli.ID, fmt.Sprintf("retrieve client by ID : client ID : expected %s got %s\n", client.ID, cli.ID)) + assert.Equal(t, client.Name, cli.Name, fmt.Sprintf("retrieve client by ID : client Name : expected %s got %s\n", client.Name, cli.Name)) + assert.Equal(t, client.Credentials.Identity, cli.Credentials.Identity, fmt.Sprintf("retrieve client by ID : client Identity : expected %s got %s\n", client.Credentials.Identity, cli.Credentials.Identity)) + assert.Equal(t, client.Status, cli.Status, fmt.Sprintf("retrieve client by ID : client Status : expected %d got %d\n", client.Status, cli.Status)) + } + } +} + +func TestClientsRetrieveByIdentity(t *testing.T) { + t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) + repo := cpostgres.NewRepository(database) + + client := mfclients.Client{ + ID: testsutil.GenerateUUID(t, idProvider), + Name: clientName, + Credentials: mfclients.Credentials{ + Identity: clientIdentity, + Secret: password, + }, + Status: mfclients.EnabledStatus, + } + + _, err := repo.Save(context.Background(), client) + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + cases := map[string]struct { + identity string + err error + }{ + "retrieve existing client": {clientIdentity, nil}, + "retrieve non-existing client": {wrongID, errors.ErrNotFound}, + } + + for desc, tc := range cases { + _, err := repo.RetrieveByIdentity(context.Background(), tc.identity) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err)) + } +} + +func TestClientsRetrieveAll(t *testing.T) { + t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) + repo := cpostgres.NewRepository(database) + grepo := gpostgres.New(database) + prepo := ppostgres.NewRepository(database) + + var nClients = uint64(200) + var ownerID = testsutil.GenerateUUID(t, idProvider) + + meta := mfclients.Metadata{ + "admin": "true", + } + wrongMeta := mfclients.Metadata{ + "admin": "false", + } + var expectedClients = []mfclients.Client{} + + var sharedGroup = mfgroups.Group{ + ID: testsutil.GenerateUUID(t, idProvider), + Name: "shared-group", + } + _, err := grepo.Save(context.Background(), sharedGroup) + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + + for i := uint64(0); i < nClients; i++ { + identity := fmt.Sprintf("TestRetrieveAll%d@example.com", i) + client := mfclients.Client{ + ID: testsutil.GenerateUUID(t, idProvider), + Name: identity, + Credentials: mfclients.Credentials{ + Identity: identity, + Secret: password, + }, + Metadata: mfclients.Metadata{}, + Status: mfclients.EnabledStatus, + } + if i%10 == 0 { + client.Owner = ownerID + client.Metadata = meta + client.Tags = []string{"Test"} + } + if i%50 == 0 { + client.Status = mfclients.DisabledStatus + } + _, err := repo.Save(context.Background(), client) + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + expectedClients = append(expectedClients, client) + var policy = policies.Policy{ + Subject: client.ID, + Object: sharedGroup.ID, + Actions: []string{"c_list"}, + } + err = prepo.Save(context.Background(), policy) + require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) + } + + cases := map[string]struct { + size uint64 + pm mfclients.Page + response []mfclients.Client + }{ + "retrieve all clients empty page": { + pm: mfclients.Page{}, + response: []mfclients.Client{}, + size: 0, + }, + "retrieve all clients": { + pm: mfclients.Page{ + Offset: 0, + Limit: nClients, + Status: mfclients.AllStatus, + }, + response: expectedClients, + size: 200, + }, + "retrieve all clients with limit": { + pm: mfclients.Page{ + Offset: 0, + Limit: 50, + Status: mfclients.AllStatus, + }, + response: expectedClients[0:50], + size: 50, + }, + "retrieve all clients with offset": { + pm: mfclients.Page{ + Offset: 50, + Limit: nClients, + Status: mfclients.AllStatus, + }, + response: expectedClients[50:200], + size: 150, + }, + "retrieve all clients with limit and offset": { + pm: mfclients.Page{ + Offset: 50, + Limit: 50, + Status: mfclients.AllStatus, + }, + response: expectedClients[50:100], + size: 50, + }, + "retrieve all clients with limit and offset not full": { + pm: mfclients.Page{ + Offset: 170, + Limit: 50, + Status: mfclients.AllStatus, + }, + response: expectedClients[170:200], + size: 30, + }, + "retrieve all clients by metadata": { + pm: mfclients.Page{ + Offset: 0, + Limit: nClients, + Total: nClients, + Metadata: meta, + Status: mfclients.AllStatus, + }, + response: []mfclients.Client{expectedClients[0], expectedClients[10], expectedClients[20], expectedClients[30], expectedClients[40], expectedClients[50], expectedClients[60], + expectedClients[70], expectedClients[80], expectedClients[90], expectedClients[100], expectedClients[110], expectedClients[120], expectedClients[130], + expectedClients[140], expectedClients[150], expectedClients[160], expectedClients[170], expectedClients[180], expectedClients[190], + }, + size: 20, + }, + "retrieve clients by wrong metadata": { + pm: mfclients.Page{ + Offset: 0, + Limit: nClients, + Total: nClients, + Metadata: wrongMeta, + Status: mfclients.AllStatus, + }, + response: []mfclients.Client{}, + size: 0, + }, + "retrieve all clients by name": { + pm: mfclients.Page{ + Offset: 0, + Limit: nClients, + Total: nClients, + Name: "TestRetrieveAll3@example.com", + Status: mfclients.AllStatus, + }, + response: []mfclients.Client{expectedClients[3]}, + size: 1, + }, + "retrieve clients by wrong name": { + pm: mfclients.Page{ + Offset: 0, + Limit: nClients, + Total: nClients, + Name: wrongName, + Status: mfclients.AllStatus, + }, + response: []mfclients.Client{}, + size: 0, + }, + "retrieve all clients by owner": { + pm: mfclients.Page{ + Offset: 0, + Limit: nClients, + Total: nClients, + Owner: ownerID, + Status: mfclients.AllStatus, + }, + response: []mfclients.Client{expectedClients[0], expectedClients[10], expectedClients[20], expectedClients[30], expectedClients[40], expectedClients[50], expectedClients[60], + expectedClients[70], expectedClients[80], expectedClients[90], expectedClients[100], expectedClients[110], expectedClients[120], expectedClients[130], + expectedClients[140], expectedClients[150], expectedClients[160], expectedClients[170], expectedClients[180], expectedClients[190], + }, + size: 20, + }, + "retrieve clients by wrong owner": { + pm: mfclients.Page{ + Offset: 0, + Limit: nClients, + Total: nClients, + Owner: wrongID, + Status: mfclients.AllStatus, + }, + response: []mfclients.Client{}, + size: 0, + }, + "retrieve all clients shared by": { + pm: mfclients.Page{ + Offset: 0, + Limit: nClients, + Total: nClients, + SharedBy: expectedClients[0].ID, + Action: "c_list", + Status: mfclients.AllStatus, + }, + response: []mfclients.Client{expectedClients[0], expectedClients[10], expectedClients[20], expectedClients[30], expectedClients[40], expectedClients[50], expectedClients[60], + expectedClients[70], expectedClients[80], expectedClients[90], expectedClients[100], expectedClients[110], expectedClients[120], expectedClients[130], + expectedClients[140], expectedClients[150], expectedClients[160], expectedClients[170], expectedClients[180], expectedClients[190], + }, + size: 20, + }, + "retrieve all clients shared by and owned by": { + pm: mfclients.Page{ + Offset: 0, + Limit: nClients, + Total: nClients, + SharedBy: ownerID, + Owner: ownerID, + Status: mfclients.AllStatus, + }, + response: []mfclients.Client{expectedClients[0], expectedClients[10], expectedClients[20], expectedClients[30], expectedClients[40], expectedClients[50], expectedClients[60], + expectedClients[70], expectedClients[80], expectedClients[90], expectedClients[100], expectedClients[110], expectedClients[120], expectedClients[130], + expectedClients[140], expectedClients[150], expectedClients[160], expectedClients[170], expectedClients[180], expectedClients[190], + }, + size: 20, + }, + "retrieve all clients by disabled status": { + pm: mfclients.Page{ + Offset: 0, + Limit: nClients, + Total: nClients, + Status: mfclients.DisabledStatus, + }, + response: []mfclients.Client{expectedClients[0], expectedClients[50], expectedClients[100], expectedClients[150]}, + size: 4, + }, + "retrieve all clients by combined status": { + pm: mfclients.Page{ + Offset: 0, + Limit: nClients, + Total: nClients, + Status: mfclients.AllStatus, + }, + response: expectedClients, + size: 200, + }, + "retrieve clients by the wrong status": { + pm: mfclients.Page{ + Offset: 0, + Limit: nClients, + Total: nClients, + Status: 10, + }, + response: []mfclients.Client{}, + size: 0, + }, + "retrieve all clients by tags": { + pm: mfclients.Page{ + Offset: 0, + Limit: nClients, + Total: nClients, + Tag: "Test", + Status: mfclients.AllStatus, + }, + response: []mfclients.Client{expectedClients[0], expectedClients[10], expectedClients[20], expectedClients[30], expectedClients[40], expectedClients[50], expectedClients[60], + expectedClients[70], expectedClients[80], expectedClients[90], expectedClients[100], expectedClients[110], expectedClients[120], expectedClients[130], + expectedClients[140], expectedClients[150], expectedClients[160], expectedClients[170], expectedClients[180], expectedClients[190], + }, + size: 20, + }, + "retrieve clients by wrong tags": { + pm: mfclients.Page{ + Offset: 0, + Limit: nClients, + Total: nClients, + Tag: "wrongTags", + Status: mfclients.AllStatus, + }, + response: []mfclients.Client{}, + size: 0, + }, + "retrieve all clients by sharedby": { + pm: mfclients.Page{ + Offset: 0, + Limit: nClients, + Total: nClients, + SharedBy: expectedClients[0].ID, + Status: mfclients.AllStatus, + Action: "c_list", + }, + response: []mfclients.Client{expectedClients[0], expectedClients[10], expectedClients[20], expectedClients[30], expectedClients[40], expectedClients[50], expectedClients[60], + expectedClients[70], expectedClients[80], expectedClients[90], expectedClients[100], expectedClients[110], expectedClients[120], expectedClients[130], + expectedClients[140], expectedClients[150], expectedClients[160], expectedClients[170], expectedClients[180], expectedClients[190], + }, + size: 20, + }, + } + for desc, tc := range cases { + page, err := repo.RetrieveAll(context.Background(), tc.pm) + size := uint64(len(page.Clients)) + assert.ElementsMatch(t, page.Clients, tc.response, fmt.Sprintf("%s: expected %v got %v\n", desc, tc.response, page.Clients)) + assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected size %d got %d\n", desc, tc.size, size)) + assert.Nil(t, err, fmt.Sprintf("%s: expected no error got %d\n", desc, err)) + } +} + +func TestGroupsMembers(t *testing.T) { + t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) + crepo := cpostgres.NewRepository(database) + grepo := gpostgres.New(database) + prepo := ppostgres.NewRepository(database) + + clientA := mfclients.Client{ + ID: testsutil.GenerateUUID(t, idProvider), + Name: "client-memberships", + Credentials: mfclients.Credentials{ + Identity: "client-memberships1@example.com", + Secret: password, + }, + Metadata: mfclients.Metadata{}, + Status: mfclients.EnabledStatus, + } + clientB := mfclients.Client{ + ID: testsutil.GenerateUUID(t, idProvider), + Name: "client-memberships", + Credentials: mfclients.Credentials{ + Identity: "client-memberships2@example.com", + Secret: password, + }, + Metadata: mfclients.Metadata{}, + Status: mfclients.EnabledStatus, + } + group := mfgroups.Group{ + ID: testsutil.GenerateUUID(t, idProvider), + Name: "group-membership", + Metadata: mfclients.Metadata{}, + Status: mfclients.EnabledStatus, + } + + policyA := policies.Policy{ + Subject: clientA.ID, + Object: group.ID, + Actions: []string{"g_list"}, + } + policyB := policies.Policy{ + Subject: clientB.ID, + Object: group.ID, + Actions: []string{"g_list"}, + } + + _, err := crepo.Save(context.Background(), clientA) + assert.True(t, errors.Contains(err, nil), fmt.Sprintf("save client: expected %v got %s\n", nil, err)) + clientA.Credentials.Secret = "" + _, err = crepo.Save(context.Background(), clientB) + assert.True(t, errors.Contains(err, nil), fmt.Sprintf("save client: expected %v got %s\n", nil, err)) + clientB.Credentials.Secret = "" + _, err = grepo.Save(context.Background(), group) + assert.True(t, errors.Contains(err, nil), fmt.Sprintf("save group: expected %v got %s\n", nil, err)) + err = prepo.Save(context.Background(), policyA) + assert.True(t, errors.Contains(err, nil), fmt.Sprintf("save policy: expected %v got %s\n", nil, err)) + err = prepo.Save(context.Background(), policyB) + assert.True(t, errors.Contains(err, nil), fmt.Sprintf("save policy: expected %v got %s\n", nil, err)) + + cases := map[string]struct { + ID string + err error + }{ + "retrieve members for existing group": {group.ID, nil}, + "retrieve members for non-existing group": {wrongID, nil}, + } + + for desc, tc := range cases { + mp, err := crepo.Members(context.Background(), tc.ID, mfclients.Page{Total: 10, Offset: 0, Limit: 10, Status: mfclients.AllStatus, Subject: clientB.ID, Action: "g_list"}) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err)) + if tc.ID == group.ID { + assert.ElementsMatch(t, mp.Members, []mfclients.Client{clientA}, fmt.Sprintf("%s: expected %v got %v\n", desc, []mfclients.Client{clientA, clientB}, mp.Members)) + } + } +} + +func TestClientsUpdateMetadata(t *testing.T) { + t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) + repo := cpostgres.NewRepository(database) + + client1 := mfclients.Client{ + ID: testsutil.GenerateUUID(t, idProvider), + Name: "enabled-client", + Credentials: mfclients.Credentials{ + Identity: "client1-update@example.com", + Secret: password, + }, + Metadata: mfclients.Metadata{ + "name": "enabled-client", + }, + Tags: []string{"enabled", "tag1"}, + Status: mfclients.EnabledStatus, + } + + client2 := mfclients.Client{ + ID: testsutil.GenerateUUID(t, idProvider), + Name: "disabled-client", + Credentials: mfclients.Credentials{ + Identity: "client2-update@example.com", + Secret: password, + }, + Metadata: mfclients.Metadata{ + "name": "disabled-client", + }, + Tags: []string{"disabled", "tag1"}, + Status: mfclients.DisabledStatus, + } + + clients1, err := repo.Save(context.Background(), client1) + assert.True(t, errors.Contains(err, nil), fmt.Sprintf("add new client with metadata: expected %v got %s\n", nil, err)) + clients2, err := repo.Save(context.Background(), client2) + assert.True(t, errors.Contains(err, nil), fmt.Sprintf("add new disabled client: expected %v got %s\n", nil, err)) + client1 = clients1 + client2 = clients2 + + ucases := []struct { + desc string + update string + client mfclients.Client + err error + }{ + { + desc: "update metadata for enabled client", + update: "metadata", + client: mfclients.Client{ + ID: client1.ID, + Metadata: mfclients.Metadata{ + "update": "metadata", + }, + }, + err: nil, + }, + { + desc: "update metadata for disabled client", + update: "metadata", + client: mfclients.Client{ + ID: client2.ID, + Metadata: mfclients.Metadata{ + "update": "metadata", + }, + }, + err: errors.ErrNotFound, + }, + { + desc: "update name for enabled client", + update: "name", + client: mfclients.Client{ + ID: client1.ID, + Name: "updated name", + }, + err: nil, + }, + { + desc: "update name for disabled client", + update: "name", + client: mfclients.Client{ + ID: client2.ID, + Name: "updated name", + }, + err: errors.ErrNotFound, + }, + { + desc: "update name and metadata for enabled client", + update: "both", + client: mfclients.Client{ + ID: client1.ID, + Name: "updated name and metadata", + Metadata: mfclients.Metadata{ + "update": "name and metadata", + }, + }, + err: nil, + }, + { + desc: "update name and metadata for a disabled client", + update: "both", + client: mfclients.Client{ + ID: client2.ID, + Name: "updated name and metadata", + Metadata: mfclients.Metadata{ + "update": "name and metadata", + }, + }, + err: errors.ErrNotFound, + }, + { + desc: "update metadata for invalid client", + update: "metadata", + client: mfclients.Client{ + ID: wrongID, + Metadata: mfclients.Metadata{ + "update": "metadata", + }, + }, + err: errors.ErrNotFound, + }, + { + desc: "update name for invalid client", + update: "name", + client: mfclients.Client{ + ID: wrongID, + Name: "updated name", + }, + err: errors.ErrNotFound, + }, + { + desc: "update name and metadata for invalid client", + update: "both", + client: mfclients.Client{ + ID: client2.ID, + Name: "updated name and metadata", + Metadata: mfclients.Metadata{ + "update": "name and metadata", + }, + }, + err: errors.ErrNotFound, + }, + } + for _, tc := range ucases { + expected, err := repo.Update(context.Background(), tc.client) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + if err == nil { + if tc.client.Name != "" { + assert.Equal(t, expected.Name, tc.client.Name, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, expected.Name, tc.client.Name)) + } + if tc.client.Metadata != nil { + assert.Equal(t, expected.Metadata, tc.client.Metadata, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, expected.Metadata, tc.client.Metadata)) + } + + } + } +} + +func TestClientsUpdateTags(t *testing.T) { + t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) + repo := cpostgres.NewRepository(database) + + client1 := mfclients.Client{ + ID: testsutil.GenerateUUID(t, idProvider), + Name: "enabled-client-with-tags", + Credentials: mfclients.Credentials{ + Identity: "client1-update-tags@example.com", + Secret: password, + }, + Tags: []string{"test", "enabled"}, + Status: mfclients.EnabledStatus, + } + client2 := mfclients.Client{ + ID: testsutil.GenerateUUID(t, idProvider), + Name: "disabled-client-with-tags", + Credentials: mfclients.Credentials{ + Identity: "client2-update-tags@example.com", + Secret: password, + }, + Tags: []string{"test", "disabled"}, + Status: mfclients.DisabledStatus, + } + + clients1, err := repo.Save(context.Background(), client1) + assert.True(t, errors.Contains(err, nil), fmt.Sprintf("add new client with tags: expected %v got %s\n", nil, err)) + if err == nil { + assert.Equal(t, client1.ID, client1.ID, fmt.Sprintf("add new client with tags: expected %v got %s\n", nil, err)) + } + client1 = clients1 + clients2, err := repo.Save(context.Background(), client2) + assert.True(t, errors.Contains(err, nil), fmt.Sprintf("add new disabled client with tags: expected %v got %s\n", nil, err)) + if err == nil { + assert.Equal(t, client2.ID, client2.ID, fmt.Sprintf("add new disabled client with tags: expected %v got %s\n", nil, err)) + } + client2 = clients2 + ucases := []struct { + desc string + client mfclients.Client + err error + }{ + { + desc: "update tags for enabled client", + client: mfclients.Client{ + ID: client1.ID, + Tags: []string{"updated"}, + }, + err: nil, + }, + { + desc: "update tags for disabled client", + client: mfclients.Client{ + ID: client2.ID, + Tags: []string{"updated"}, + }, + err: errors.ErrNotFound, + }, + { + desc: "update tags for invalid client", + client: mfclients.Client{ + ID: wrongID, + Tags: []string{"updated"}, + }, + err: errors.ErrNotFound, + }, + } + for _, tc := range ucases { + expected, err := repo.UpdateTags(context.Background(), tc.client) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + if err == nil { + assert.Equal(t, tc.client.Tags, expected.Tags, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.client.Tags, expected.Tags)) + } + } +} + +func TestClientsUpdateSecret(t *testing.T) { + t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) + repo := cpostgres.NewRepository(database) + + client1 := mfclients.Client{ + ID: testsutil.GenerateUUID(t, idProvider), + Name: "enabled-client", + Credentials: mfclients.Credentials{ + Identity: "client1-update@example.com", + Secret: password, + }, + Status: mfclients.EnabledStatus, + } + client2 := mfclients.Client{ + ID: testsutil.GenerateUUID(t, idProvider), + Name: "disabled-client", + Credentials: mfclients.Credentials{ + Identity: "client2-update@example.com", + Secret: password, + }, + Status: mfclients.DisabledStatus, + } + + rClients1, err := repo.Save(context.Background(), client1) + assert.True(t, errors.Contains(err, nil), fmt.Sprintf("add new client: expected %v got %s\n", nil, err)) + if err == nil { + assert.Equal(t, client1.ID, rClients1.ID, fmt.Sprintf("add new client: expected %v got %s\n", nil, err)) + } + rClients2, err := repo.Save(context.Background(), client2) + assert.True(t, errors.Contains(err, nil), fmt.Sprintf("add new disabled client: expected %v got %s\n", nil, err)) + if err == nil { + assert.Equal(t, client2.ID, rClients2.ID, fmt.Sprintf("add new disabled client: expected %v got %s\n", nil, err)) + } + + ucases := []struct { + desc string + client mfclients.Client + err error + }{ + { + desc: "update secret for enabled client", + client: mfclients.Client{ + ID: client1.ID, + Credentials: mfclients.Credentials{ + Identity: "client1-update@example.com", + Secret: "newpassword", + }, + }, + err: nil, + }, + { + desc: "update secret for disabled client", + client: mfclients.Client{ + ID: client2.ID, + Credentials: mfclients.Credentials{ + Identity: "client2-update@example.com", + Secret: "newpassword", + }, + }, + err: errors.ErrNotFound, + }, + { + desc: "update secret for invalid client", + client: mfclients.Client{ + ID: wrongID, + Credentials: mfclients.Credentials{ + Identity: "client3-update@example.com", + Secret: "newpassword", + }, + }, + err: errors.ErrNotFound, + }, + } + for _, tc := range ucases { + _, err := repo.UpdateSecret(context.Background(), tc.client) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + if err == nil { + c, err := repo.RetrieveByIdentity(context.Background(), tc.client.Credentials.Identity) + require.Nil(t, err, fmt.Sprintf("retrieve client by id during update of secret unexpected error: %s", err)) + assert.Equal(t, tc.client.Credentials.Secret, c.Credentials.Secret, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.client.Credentials.Secret, c.Credentials.Secret)) + } + } +} + +func TestClientsUpdateIdentity(t *testing.T) { + t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) + repo := cpostgres.NewRepository(database) + + client1 := mfclients.Client{ + ID: testsutil.GenerateUUID(t, idProvider), + Name: "enabled-client", + Credentials: mfclients.Credentials{ + Identity: "client1-update@example.com", + Secret: password, + }, + Status: mfclients.EnabledStatus, + } + client2 := mfclients.Client{ + ID: testsutil.GenerateUUID(t, idProvider), + Name: "disabled-client", + Credentials: mfclients.Credentials{ + Identity: "client2-update@example.com", + Secret: password, + }, + Status: mfclients.DisabledStatus, + } + + rClients1, err := repo.Save(context.Background(), client1) + assert.True(t, errors.Contains(err, nil), fmt.Sprintf("add new client: expected %v got %s\n", nil, err)) + if err == nil { + assert.Equal(t, client1.ID, rClients1.ID, fmt.Sprintf("add new client: expected %v got %s\n", nil, err)) + } + rClients2, err := repo.Save(context.Background(), client2) + assert.True(t, errors.Contains(err, nil), fmt.Sprintf("add new disabled client: expected %v got %s\n", nil, err)) + if err == nil { + assert.Equal(t, client2.ID, rClients2.ID, fmt.Sprintf("add new disabled client: expected %v got %s\n", nil, err)) + } + + ucases := []struct { + desc string + client mfclients.Client + err error + }{ + { + desc: "update identity for enabled client", + client: mfclients.Client{ + ID: client1.ID, + Credentials: mfclients.Credentials{ + Identity: "client1-updated@example.com", + }, + }, + err: nil, + }, + { + desc: "update identity for disabled client", + client: mfclients.Client{ + ID: client2.ID, + Credentials: mfclients.Credentials{ + Identity: "client2-updated@example.com", + }, + }, + err: errors.ErrNotFound, + }, + { + desc: "update identity for invalid client", + client: mfclients.Client{ + ID: wrongID, + Credentials: mfclients.Credentials{ + Identity: "client3-updated@example.com", + }, + }, + err: errors.ErrNotFound, + }, + } + for _, tc := range ucases { + expected, err := repo.UpdateIdentity(context.Background(), tc.client) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + if err == nil { + assert.Equal(t, tc.client.Credentials.Identity, expected.Credentials.Identity, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.client.Credentials.Identity, expected.Credentials.Identity)) + } + } +} + +func TestClientsUpdateOwner(t *testing.T) { + t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) + repo := cpostgres.NewRepository(database) + + client1 := mfclients.Client{ + ID: testsutil.GenerateUUID(t, idProvider), + Name: "enabled-client-with-owner", + Credentials: mfclients.Credentials{ + Identity: "client1-update-owner@example.com", + Secret: password, + }, + Owner: testsutil.GenerateUUID(t, idProvider), + Status: mfclients.EnabledStatus, + } + client2 := mfclients.Client{ + ID: testsutil.GenerateUUID(t, idProvider), + Name: "disabled-client-with-owner", + Credentials: mfclients.Credentials{ + Identity: "client2-update-owner@example.com", + Secret: password, + }, + Owner: testsutil.GenerateUUID(t, idProvider), + Status: mfclients.DisabledStatus, + } + + clients1, err := repo.Save(context.Background(), client1) + client1 = clients1 + assert.True(t, errors.Contains(err, nil), fmt.Sprintf("add new client with owner: expected %v got %s\n", nil, err)) + if err == nil { + assert.Equal(t, client1.ID, client1.ID, fmt.Sprintf("add new client with owner: expected %v got %s\n", nil, err)) + } + clients2, err := repo.Save(context.Background(), client2) + client2 = clients2 + assert.True(t, errors.Contains(err, nil), fmt.Sprintf("add new disabled client with owner: expected %v got %s\n", nil, err)) + if err == nil { + assert.Equal(t, client2.ID, client2.ID, fmt.Sprintf("add new disabled client with owner: expected %v got %s\n", nil, err)) + } + ucases := []struct { + desc string + client mfclients.Client + err error + }{ + { + desc: "update owner for enabled client", + client: mfclients.Client{ + ID: client1.ID, + Owner: testsutil.GenerateUUID(t, idProvider), + }, + err: nil, + }, + { + desc: "update owner for disabled client", + client: mfclients.Client{ + ID: client2.ID, + Owner: testsutil.GenerateUUID(t, idProvider), + }, + err: errors.ErrNotFound, + }, + { + desc: "update owner for invalid client", + client: mfclients.Client{ + ID: wrongID, + Owner: testsutil.GenerateUUID(t, idProvider), + }, + err: errors.ErrNotFound, + }, + } + for _, tc := range ucases { + expected, err := repo.UpdateOwner(context.Background(), tc.client) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + if err == nil { + assert.Equal(t, tc.client.Owner, expected.Owner, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.client.Owner, expected.Owner)) + } + } +} + +func TestClientsChangeStatus(t *testing.T) { + t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) + repo := cpostgres.NewRepository(database) + + client1 := mfclients.Client{ + ID: testsutil.GenerateUUID(t, idProvider), + Name: "enabled-client", + Credentials: mfclients.Credentials{ + Identity: "client1-update@example.com", + Secret: password, + }, + Status: mfclients.EnabledStatus, + } + + clients1, err := repo.Save(context.Background(), client1) + assert.True(t, errors.Contains(err, nil), fmt.Sprintf("add new client: expected %v got %s\n", nil, err)) + client1 = clients1 + + ucases := []struct { + desc string + client mfclients.Client + err error + }{ + { + desc: "change client status for an enabled client", + client: mfclients.Client{ + ID: client1.ID, + Status: 0, + }, + err: nil, + }, + { + desc: "change client status for a disabled client", + client: mfclients.Client{ + ID: client1.ID, + Status: 1, + }, + err: nil, + }, + { + desc: "change client status for non-existing client", + client: mfclients.Client{ + ID: "invalid", + Status: 2, + }, + err: errors.ErrNotFound, + }, + } + + for _, tc := range ucases { + expected, err := repo.ChangeStatus(context.Background(), tc.client) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + if err == nil { + assert.Equal(t, tc.client.Status, expected.Status, fmt.Sprintf("%s: expected %d got %d\n", tc.desc, tc.client.Status, expected.Status)) + } + } +} diff --git a/pkg/clients/postgres/doc.go b/pkg/clients/postgres/doc.go new file mode 100644 index 00000000..f88b679a --- /dev/null +++ b/pkg/clients/postgres/doc.go @@ -0,0 +1,5 @@ +// Copyright (c) Mainflux +// SPDX-License-Identifier: Apache-2.0 + +// Package postgres contains the database implementation of clients repository layer. +package postgres diff --git a/things/groups/postgres/setup_test.go b/pkg/clients/postgres/setup_test.go similarity index 94% rename from things/groups/postgres/setup_test.go rename to pkg/clients/postgres/setup_test.go index d2f91425..108ff6b3 100644 --- a/things/groups/postgres/setup_test.go +++ b/pkg/clients/postgres/setup_test.go @@ -14,7 +14,7 @@ import ( "github.com/jmoiron/sqlx" pgClient "github.com/mainflux/mainflux/internal/clients/postgres" "github.com/mainflux/mainflux/internal/postgres" - gpostgres "github.com/mainflux/mainflux/things/postgres" + upostgres "github.com/mainflux/mainflux/users/postgres" dockertest "github.com/ory/dockertest/v3" "github.com/ory/dockertest/v3/docker" "go.opentelemetry.io/otel" @@ -75,7 +75,8 @@ func TestMain(m *testing.M) { SSLKey: "", SSLRootCert: "", } - if db, err = pgClient.SetupDB(dbConfig, *gpostgres.Migration()); err != nil { + + if db, err = pgClient.SetupDB(dbConfig, *upostgres.Migration()); err != nil { log.Fatalf("Could not setup test DB connection: %s", err) } diff --git a/things/groups/postgres/doc.go b/pkg/groups/postgres/doc.go similarity index 100% rename from things/groups/postgres/doc.go rename to pkg/groups/postgres/doc.go diff --git a/users/groups/postgres/groups.go b/pkg/groups/postgres/groups.go similarity index 96% rename from users/groups/postgres/groups.go rename to pkg/groups/postgres/groups.go index 3dcf1451..22bcb317 100644 --- a/users/groups/postgres/groups.go +++ b/pkg/groups/postgres/groups.go @@ -1,6 +1,3 @@ -// Copyright (c) Mainflux -// SPDX-License-Identifier: Apache-2.0 - package postgres import ( @@ -11,6 +8,7 @@ import ( "strings" "time" + "github.com/jmoiron/sqlx" "github.com/mainflux/mainflux/internal/postgres" mfclients "github.com/mainflux/mainflux/pkg/clients" "github.com/mainflux/mainflux/pkg/errors" @@ -23,9 +21,9 @@ type groupRepository struct { db postgres.Database } -// NewRepository instantiates a PostgreSQL implementation of group +// New instantiates a PostgreSQL implementation of group // repository. -func NewRepository(db postgres.Database) mfgroups.Repository { +func New(db postgres.Database) mfgroups.Repository { return &groupRepository{ db: db, } @@ -36,7 +34,6 @@ func (repo groupRepository) Save(ctx context.Context, g mfgroups.Group) (mfgroup q := `INSERT INTO groups (name, description, id, owner_id, parent_id, metadata, created_at, status) VALUES (:name, :description, :id, :owner_id, :parent_id, :metadata, :created_at, :status) RETURNING id, name, description, owner_id, COALESCE(parent_id, '') AS parent_id, metadata, created_at, status;` - dbg, err := toDBGroup(g) if err != nil { return mfgroups.Group{}, err @@ -56,89 +53,6 @@ func (repo groupRepository) Save(ctx context.Context, g mfgroups.Group) (mfgroup return toGroup(dbg) } -func (repo groupRepository) RetrieveByID(ctx context.Context, id string) (mfgroups.Group, error) { - q := `SELECT id, name, owner_id, COALESCE(parent_id, '') AS parent_id, description, metadata, created_at, updated_at, updated_by, status FROM groups - WHERE id = :id` - - dbg := dbGroup{ - ID: id, - } - - row, err := repo.db.NamedQueryContext(ctx, q, dbg) - if err != nil { - if err == sql.ErrNoRows { - return mfgroups.Group{}, errors.Wrap(errors.ErrNotFound, err) - } - return mfgroups.Group{}, errors.Wrap(errors.ErrViewEntity, err) - } - - defer row.Close() - row.Next() - dbg = dbGroup{} - if err := row.StructScan(&dbg); err != nil { - return mfgroups.Group{}, errors.Wrap(errors.ErrNotFound, err) - } - - return toGroup(dbg) - -} - -func (repo groupRepository) RetrieveAll(ctx context.Context, gm mfgroups.GroupsPage) (mfgroups.GroupsPage, error) { - var q string - query, err := buildQuery(gm) - if err != nil { - return mfgroups.GroupsPage{}, err - } - - if gm.ID != "" { - q = buildHierachy(gm) - } - if gm.ID == "" { - q = `SELECT g.id, g.owner_id, COALESCE(g.parent_id, '') AS parent_id, g.name, g.description, - g.metadata, g.created_at, g.updated_at, g.updated_by, g.status FROM groups g` - } - q = fmt.Sprintf("%s %s ORDER BY g.updated_at LIMIT :limit OFFSET :offset;", q, query) - - dbPage, err := toDBGroupPage(gm) - if err != nil { - return mfgroups.GroupsPage{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) - } - rows, err := repo.db.NamedQueryContext(ctx, q, dbPage) - if err != nil { - return mfgroups.GroupsPage{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) - } - defer rows.Close() - - var items []mfgroups.Group - for rows.Next() { - dbg := dbGroup{} - if err := rows.StructScan(&dbg); err != nil { - return mfgroups.GroupsPage{}, err - } - group, err := toGroup(dbg) - if err != nil { - return mfgroups.GroupsPage{}, err - } - items = append(items, group) - } - - cq := "SELECT COUNT(*) FROM groups g" - if query != "" { - cq = fmt.Sprintf(" %s %s", cq, query) - } - - total, err := postgres.Total(ctx, repo.db, cq, dbPage) - if err != nil { - return mfgroups.GroupsPage{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) - } - - page := gm - page.Groups = items - page.Total = total - - return page, nil -} - func (repo groupRepository) Memberships(ctx context.Context, clientID string, gm mfgroups.GroupsPage) (mfgroups.MembershipsPage, error) { var q string query, err := buildQuery(gm) @@ -267,6 +181,80 @@ func (repo groupRepository) ChangeStatus(ctx context.Context, group mfgroups.Gro return toGroup(dbg) } +func (repo groupRepository) RetrieveByID(ctx context.Context, id string) (mfgroups.Group, error) { + q := `SELECT id, name, owner_id, COALESCE(parent_id, '') AS parent_id, description, metadata, created_at, updated_at, updated_by, status FROM groups + WHERE id = :id` + + dbg := dbGroup{ + ID: id, + } + + row, err := repo.db.NamedQueryContext(ctx, q, dbg) + if err != nil { + if err == sql.ErrNoRows { + return mfgroups.Group{}, errors.Wrap(errors.ErrNotFound, err) + } + return mfgroups.Group{}, errors.Wrap(errors.ErrViewEntity, err) + } + + defer row.Close() + row.Next() + dbg = dbGroup{} + if err := row.StructScan(&dbg); err != nil { + return mfgroups.Group{}, errors.Wrap(errors.ErrNotFound, err) + } + + return toGroup(dbg) +} + +func (repo groupRepository) RetrieveAll(ctx context.Context, gm mfgroups.GroupsPage) (mfgroups.GroupsPage, error) { + var q string + query, err := buildQuery(gm) + if err != nil { + return mfgroups.GroupsPage{}, err + } + + if gm.ID != "" { + q = buildHierachy(gm) + } + if gm.ID == "" { + q = `SELECT DISTINCT g.id, g.owner_id, COALESCE(g.parent_id, '') AS parent_id, g.name, g.description, + g.metadata, g.created_at, g.updated_at, g.updated_by, g.status FROM groups g` + } + q = fmt.Sprintf("%s %s ORDER BY g.updated_at LIMIT :limit OFFSET :offset;", q, query) + + dbPage, err := toDBGroupPage(gm) + if err != nil { + return mfgroups.GroupsPage{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) + } + rows, err := repo.db.NamedQueryContext(ctx, q, dbPage) + if err != nil { + return mfgroups.GroupsPage{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) + } + defer rows.Close() + + items, err := repo.processRows(rows) + if err != nil { + return mfgroups.GroupsPage{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) + } + + cq := "SELECT COUNT(*) FROM groups g" + if query != "" { + cq = fmt.Sprintf(" %s %s", cq, query) + } + + total, err := postgres.Total(ctx, repo.db, cq, dbPage) + if err != nil { + return mfgroups.GroupsPage{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) + } + + page := gm + page.Groups = items + page.Total = total + + return page, nil +} + func buildHierachy(gm mfgroups.GroupsPage) string { query := "" switch { @@ -446,3 +434,19 @@ type dbGroupPage struct { Action string `db:"action"` Status mfclients.Status `db:"status"` } + +func (gr groupRepository) processRows(rows *sqlx.Rows) ([]mfgroups.Group, error) { + var items []mfgroups.Group + for rows.Next() { + dbg := dbGroup{} + if err := rows.StructScan(&dbg); err != nil { + return items, err + } + group, err := toGroup(dbg) + if err != nil { + return items, err + } + items = append(items, group) + } + return items, nil +} diff --git a/users/groups/postgres/groups_test.go b/pkg/groups/postgres/groups_test.go similarity index 98% rename from users/groups/postgres/groups_test.go rename to pkg/groups/postgres/groups_test.go index 27234dc1..bcd7bb5e 100644 --- a/users/groups/postgres/groups_test.go +++ b/pkg/groups/postgres/groups_test.go @@ -14,9 +14,9 @@ import ( mfclients "github.com/mainflux/mainflux/pkg/clients" "github.com/mainflux/mainflux/pkg/errors" mfgroups "github.com/mainflux/mainflux/pkg/groups" + gpostgres "github.com/mainflux/mainflux/pkg/groups/postgres" "github.com/mainflux/mainflux/pkg/uuid" cpostgres "github.com/mainflux/mainflux/users/clients/postgres" - gpostgres "github.com/mainflux/mainflux/users/groups/postgres" "github.com/mainflux/mainflux/users/policies" ppostgres "github.com/mainflux/mainflux/users/policies/postgres" "github.com/stretchr/testify/assert" @@ -45,7 +45,7 @@ var ( func TestGroupSave(t *testing.T) { t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) - groupRepo := gpostgres.NewRepository(database) + groupRepo := gpostgres.New(database) usrID := testsutil.GenerateUUID(t, idProvider) grpID := testsutil.GenerateUUID(t, idProvider) @@ -173,7 +173,7 @@ func TestGroupSave(t *testing.T) { func TestGroupRetrieveByID(t *testing.T) { t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) - groupRepo := gpostgres.NewRepository(database) + groupRepo := gpostgres.New(database) uid := testsutil.GenerateUUID(t, idProvider) group1 := mfgroups.Group{ @@ -221,7 +221,7 @@ func TestGroupRetrieveByID(t *testing.T) { func TestGroupRetrieveAll(t *testing.T) { t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) - groupRepo := gpostgres.NewRepository(database) + groupRepo := gpostgres.New(database) var nGroups = uint64(200) var ownerID = testsutil.GenerateUUID(t, idProvider) @@ -339,7 +339,7 @@ func TestGroupRetrieveAll(t *testing.T) { func TestGroupUpdate(t *testing.T) { t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) - groupRepo := gpostgres.NewRepository(database) + groupRepo := gpostgres.New(database) uid := testsutil.GenerateUUID(t, idProvider) @@ -464,7 +464,7 @@ func TestGroupUpdate(t *testing.T) { func TestClientsMemberships(t *testing.T) { t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) crepo := cpostgres.NewRepository(database) - grepo := gpostgres.NewRepository(database) + grepo := gpostgres.New(database) prepo := ppostgres.NewRepository(database) clientA := mfclients.Client{ @@ -535,7 +535,7 @@ func TestClientsMemberships(t *testing.T) { func TestGroupChangeStatus(t *testing.T) { t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) - repo := gpostgres.NewRepository(database) + repo := gpostgres.New(database) group1 := mfgroups.Group{ ID: testsutil.GenerateUUID(t, idProvider), diff --git a/users/groups/postgres/setup_test.go b/pkg/groups/postgres/setup_test.go similarity index 100% rename from users/groups/postgres/setup_test.go rename to pkg/groups/postgres/setup_test.go diff --git a/things/clients/postgres/clients.go b/things/clients/postgres/clients.go index 3c7cec0e..1d920b13 100644 --- a/things/clients/postgres/clients.go +++ b/things/clients/postgres/clients.go @@ -6,29 +6,38 @@ package postgres import ( "context" "database/sql" - "encoding/json" "fmt" - "strings" - "time" - "github.com/jackc/pgtype" // required for SQL access + // required for SQL access. + "github.com/mainflux/mainflux/internal/postgres" mfclients "github.com/mainflux/mainflux/pkg/clients" + pgclients "github.com/mainflux/mainflux/pkg/clients/postgres" "github.com/mainflux/mainflux/pkg/errors" - "github.com/mainflux/mainflux/pkg/groups" ) var _ mfclients.Repository = (*clientRepo)(nil) type clientRepo struct { - db postgres.Database + pgclients.ClientRepository +} + +type Repository interface { + mfclients.Repository + + // Save persists the client account. A non-nil error is returned to indicate + // operation failure. + Save(ctx context.Context, client ...mfclients.Client) ([]mfclients.Client, error) + + // RetrieveBySecret retrieves a client based on the secret (key). + RetrieveBySecret(ctx context.Context, key string) (mfclients.Client, error) } // NewRepository instantiates a PostgreSQL // implementation of Clients repository. -func NewRepository(db postgres.Database) mfclients.Repository { +func NewRepository(db postgres.Database) Repository { return &clientRepo{ - db: db, + ClientRepository: pgclients.ClientRepository{DB: db}, } } @@ -38,52 +47,48 @@ func (clientRepo) RetrieveByIdentity(ctx context.Context, identity string) (mfcl } func (repo clientRepo) Save(ctx context.Context, cs ...mfclients.Client) ([]mfclients.Client, error) { - tx, err := repo.db.BeginTxx(ctx, nil) + tx, err := repo.ClientRepository.DB.BeginTxx(ctx, nil) if err != nil { return []mfclients.Client{}, errors.Wrap(errors.ErrCreateEntity, err) } + var clients []mfclients.Client for _, cli := range cs { q := `INSERT INTO clients (id, name, tags, owner_id, identity, secret, metadata, created_at, updated_at, updated_by, status) VALUES (:id, :name, :tags, :owner_id, :identity, :secret, :metadata, :created_at, :updated_at, :updated_by, :status) RETURNING id, name, tags, identity, secret, metadata, COALESCE(owner_id, '') AS owner_id, status, created_at, updated_at, updated_by` - dbcli, err := toDBClient(cli) + dbcli, err := pgclients.ToDBClient(cli) if err != nil { return []mfclients.Client{}, errors.Wrap(errors.ErrCreateEntity, err) } - if _, err := tx.NamedExecContext(ctx, q, dbcli); err != nil { + row, err := repo.ClientRepository.DB.NamedQueryContext(ctx, q, dbcli) + if err != nil { if err := tx.Rollback(); err != nil { return []mfclients.Client{}, postgres.HandleError(err, errors.ErrCreateEntity) } + return []mfclients.Client{}, errors.Wrap(errors.ErrCreateEntity, err) } + + defer row.Close() + row.Next() + dbcli = pgclients.DBClient{} + if err := row.StructScan(&dbcli); err != nil { + return []mfclients.Client{}, err + } + + client, err := pgclients.ToClient(dbcli) + if err != nil { + return []mfclients.Client{}, err + } + clients = append(clients, client) } if err = tx.Commit(); err != nil { return []mfclients.Client{}, errors.Wrap(errors.ErrCreateEntity, err) } - return cs, nil -} - -func (repo clientRepo) RetrieveByID(ctx context.Context, id string) (mfclients.Client, error) { - q := `SELECT id, name, tags, COALESCE(owner_id, '') AS owner_id, identity, secret, metadata, created_at, updated_at, updated_by, status - FROM clients - WHERE id = $1` - - dbc := dbClient{ - ID: id, - } - - if err := repo.db.QueryRowxContext(ctx, q, id).StructScan(&dbc); err != nil { - if err == sql.ErrNoRows { - return mfclients.Client{}, errors.Wrap(errors.ErrNotFound, err) - - } - return mfclients.Client{}, errors.Wrap(errors.ErrViewEntity, err) - } - - return toClient(dbc) + return clients, nil } func (repo clientRepo) RetrieveBySecret(ctx context.Context, key string) (mfclients.Client, error) { @@ -91,11 +96,11 @@ func (repo clientRepo) RetrieveBySecret(ctx context.Context, key string) (mfclie FROM clients WHERE secret = $1 AND status = %d`, mfclients.EnabledStatus) - dbc := dbClient{ + dbc := pgclients.DBClient{ Secret: key, } - if err := repo.db.QueryRowxContext(ctx, q, key).StructScan(&dbc); err != nil { + if err := repo.DB.QueryRowxContext(ctx, q, key).StructScan(&dbc); err != nil { if err == sql.ErrNoRows { return mfclients.Client{}, errors.Wrap(errors.ErrNotFound, err) @@ -103,371 +108,5 @@ func (repo clientRepo) RetrieveBySecret(ctx context.Context, key string) (mfclie return mfclients.Client{}, errors.Wrap(errors.ErrViewEntity, err) } - return toClient(dbc) -} - -func (repo clientRepo) RetrieveAll(ctx context.Context, pm mfclients.Page) (mfclients.ClientsPage, error) { - query, err := pageQuery(pm) - if err != nil { - return mfclients.ClientsPage{}, errors.Wrap(errors.ErrViewEntity, err) - } - - q := fmt.Sprintf(`SELECT c.id, c.name, c.tags, c.identity, c.secret, c.metadata, COALESCE(c.owner_id, '') AS owner_id, c.status, c.created_at - FROM clients c %s ORDER BY c.created_at LIMIT :limit OFFSET :offset;`, query) - - dbPage, err := toDBClientsPage(pm) - if err != nil { - return mfclients.ClientsPage{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) - } - rows, err := repo.db.NamedQueryContext(ctx, q, dbPage) - if err != nil { - return mfclients.ClientsPage{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) - } - defer rows.Close() - - var items []mfclients.Client - for rows.Next() { - dbc := dbClient{} - if err := rows.StructScan(&dbc); err != nil { - return mfclients.ClientsPage{}, errors.Wrap(errors.ErrViewEntity, err) - } - - c, err := toClient(dbc) - if err != nil { - return mfclients.ClientsPage{}, err - } - - items = append(items, c) - } - cq := fmt.Sprintf(`SELECT COUNT(*) FROM clients c %s;`, query) - - total, err := postgres.Total(ctx, repo.db, cq, dbPage) - if err != nil { - return mfclients.ClientsPage{}, errors.Wrap(errors.ErrViewEntity, err) - } - - page := mfclients.ClientsPage{ - Clients: items, - Page: mfclients.Page{ - Total: total, - Offset: pm.Offset, - Limit: pm.Limit, - }, - } - - return page, nil -} - -func (repo clientRepo) Members(ctx context.Context, groupID string, pm mfclients.Page) (mfclients.MembersPage, error) { - emq, err := pageQuery(pm) - if err != nil { - return mfclients.MembersPage{}, err - } - - aq := "" - // If not admin, the client needs to have a g_list action on the group or they are the owner. - if pm.Subject != "" { - aq = `AND (EXISTS (SELECT 1 FROM policies p WHERE p.subject = :subject AND :action=ANY(actions)) - OR EXISTS (SELECT 1 FROM groups g WHERE g.owner_id = :subject AND g.id = :group_id))` - } - - q := fmt.Sprintf(`SELECT c.id, c.name, c.tags, c.metadata, c.identity, c.secret, c.status, c.created_at FROM clients c - INNER JOIN policies ON c.id=policies.subject %s AND policies.object = :group_id %s ORDER BY c.created_at LIMIT :limit OFFSET :offset;`, emq, aq) - dbPage, err := toDBClientsPage(pm) - if err != nil { - return mfclients.MembersPage{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) - } - dbPage.GroupID = groupID - rows, err := repo.db.NamedQueryContext(ctx, q, dbPage) - if err != nil { - return mfclients.MembersPage{}, errors.Wrap(postgres.ErrFailedToRetrieveMembers, err) - } - defer rows.Close() - - var items []mfclients.Client - for rows.Next() { - dbc := dbClient{} - if err := rows.StructScan(&dbc); err != nil { - return mfclients.MembersPage{}, errors.Wrap(postgres.ErrFailedToRetrieveMembers, err) - } - - c, err := toClient(dbc) - if err != nil { - return mfclients.MembersPage{}, err - } - - items = append(items, c) - } - cq := fmt.Sprintf(`SELECT COUNT(*) FROM clients c INNER JOIN policies ON c.id=policies.subject %s AND policies.object = :group_id;`, emq) - - total, err := postgres.Total(ctx, repo.db, cq, dbPage) - if err != nil { - return mfclients.MembersPage{}, errors.Wrap(postgres.ErrFailedToRetrieveMembers, err) - } - - page := mfclients.MembersPage{ - Members: items, - Page: mfclients.Page{ - Total: total, - Offset: pm.Offset, - Limit: pm.Limit, - }, - } - return page, nil -} - -func (repo clientRepo) Update(ctx context.Context, client mfclients.Client) (mfclients.Client, error) { - var query []string - var upq string - if client.Name != "" { - query = append(query, "name = :name,") - } - if client.Metadata != nil { - query = append(query, "metadata = :metadata,") - } - if len(query) > 0 { - upq = strings.Join(query, " ") - } - client.Status = mfclients.EnabledStatus - q := fmt.Sprintf(`UPDATE clients SET %s updated_at = :updated_at, updated_by = :updated_by - WHERE id = :id AND status = :status - RETURNING id, name, tags, identity, secret, metadata, COALESCE(owner_id, '') AS owner_id, status, created_at, updated_at, updated_by`, - upq) - - return repo.update(ctx, client, q) -} - -func (repo clientRepo) UpdateTags(ctx context.Context, client mfclients.Client) (mfclients.Client, error) { - client.Status = mfclients.EnabledStatus - q := `UPDATE clients SET tags = :tags, updated_at = :updated_at, updated_by = :updated_by - WHERE id = :id AND status = :status - RETURNING id, name, tags, identity, secret, metadata, COALESCE(owner_id, '') AS owner_id, status, created_at, updated_at, updated_by` - - return repo.update(ctx, client, q) -} - -func (repo clientRepo) UpdateIdentity(ctx context.Context, client mfclients.Client) (mfclients.Client, error) { - client.Status = mfclients.EnabledStatus - q := `UPDATE clients SET identity = :identity, updated_at = :updated_at, updated_by = :updated_by - WHERE id = :id AND status = :status - RETURNING id, name, tags, identity, secret, metadata, COALESCE(owner_id, '') AS owner_id, status, created_at, updated_at, updated_by` - - return repo.update(ctx, client, q) -} - -func (repo clientRepo) UpdateSecret(ctx context.Context, client mfclients.Client) (mfclients.Client, error) { - client.Status = mfclients.EnabledStatus - q := `UPDATE clients SET secret = :secret, updated_at = :updated_at, updated_by = :updated_by - WHERE id = :id AND status = :status - RETURNING id, name, tags, identity, secret, metadata, COALESCE(owner_id, '') AS owner_id, status, created_at, updated_at, updated_by` - - return repo.update(ctx, client, q) -} - -func (repo clientRepo) UpdateOwner(ctx context.Context, client mfclients.Client) (mfclients.Client, error) { - client.Status = mfclients.EnabledStatus - q := `UPDATE clients SET owner_id = :owner_id, updated_at = :updated_at, updated_by = :updated_by - WHERE id = :id AND status = :status - RETURNING id, name, tags, identity, secret, metadata, COALESCE(owner_id, '') AS owner_id, status, created_at, updated_at, updated_by` - - return repo.update(ctx, client, q) -} - -func (repo clientRepo) ChangeStatus(ctx context.Context, client mfclients.Client) (mfclients.Client, error) { - q := `UPDATE clients SET status = :status WHERE id = :id - RETURNING id, name, tags, identity, secret, metadata, COALESCE(owner_id, '') AS owner_id, status, created_at, updated_at, updated_by` - - return repo.update(ctx, client, q) -} - -// generic update function. -func (repo clientRepo) update(ctx context.Context, client mfclients.Client, query string) (mfclients.Client, error) { - dbc, err := toDBClient(client) - if err != nil { - return mfclients.Client{}, errors.Wrap(errors.ErrUpdateEntity, err) - } - - row, err := repo.db.NamedQueryContext(ctx, query, dbc) - if err != nil { - return mfclients.Client{}, postgres.HandleError(err, errors.ErrUpdateEntity) - } - - defer row.Close() - if ok := row.Next(); !ok { - return mfclients.Client{}, errors.Wrap(errors.ErrNotFound, row.Err()) - } - dbc = dbClient{} - if err := row.StructScan(&dbc); err != nil { - return mfclients.Client{}, err - } - - return toClient(dbc) -} - -type dbClient struct { - ID string `db:"id"` - Name string `db:"name,omitempty"` - Tags pgtype.TextArray `db:"tags,omitempty"` - Identity string `db:"identity"` - Owner string `db:"owner_id,omitempty"` // nullable - Secret string `db:"secret"` - Metadata []byte `db:"metadata,omitempty"` - CreatedAt time.Time `db:"created_at"` - UpdatedAt sql.NullTime `db:"updated_at,omitempty"` - UpdatedBy *string `db:"updated_by,omitempty"` - Groups []groups.Group `db:"groups"` - Status mfclients.Status `db:"status"` -} - -func toDBClient(c mfclients.Client) (dbClient, error) { - data := []byte("{}") - if len(c.Metadata) > 0 { - b, err := json.Marshal(c.Metadata) - if err != nil { - return dbClient{}, errors.Wrap(errors.ErrMalformedEntity, err) - } - data = b - } - var tags pgtype.TextArray - if err := tags.Set(c.Tags); err != nil { - return dbClient{}, err - } - var updatedBy *string - if c.UpdatedBy != "" { - updatedBy = &c.UpdatedBy - } - var updatedAt sql.NullTime - if !c.UpdatedAt.IsZero() { - updatedAt = sql.NullTime{Time: c.UpdatedAt, Valid: true} - } - - return dbClient{ - ID: c.ID, - Name: c.Name, - Tags: tags, - Owner: c.Owner, - Identity: c.Credentials.Identity, - Secret: c.Credentials.Secret, - Metadata: data, - CreatedAt: c.CreatedAt, - UpdatedAt: updatedAt, - UpdatedBy: updatedBy, - Status: c.Status, - }, nil -} - -func toClient(c dbClient) (mfclients.Client, error) { - var metadata mfclients.Metadata - if c.Metadata != nil { - if err := json.Unmarshal([]byte(c.Metadata), &metadata); err != nil { - return mfclients.Client{}, errors.Wrap(errors.ErrMalformedEntity, err) - } - } - var tags []string - for _, e := range c.Tags.Elements { - tags = append(tags, e.String) - } - var updatedBy string - if c.UpdatedBy != nil { - updatedBy = *c.UpdatedBy - } - var updatedAt time.Time - if c.UpdatedAt.Valid { - updatedAt = c.UpdatedAt.Time - } - - return mfclients.Client{ - ID: c.ID, - Name: c.Name, - Tags: tags, - Owner: c.Owner, - Credentials: mfclients.Credentials{ - Identity: c.Identity, - Secret: c.Secret, - }, - Metadata: metadata, - CreatedAt: c.CreatedAt, - UpdatedAt: updatedAt, - UpdatedBy: updatedBy, - Status: c.Status, - }, nil -} - -func pageQuery(pm mfclients.Page) (string, error) { - mq, _, err := postgres.CreateMetadataQuery("", pm.Metadata) - if err != nil { - return "", errors.Wrap(errors.ErrViewEntity, err) - } - var query []string - var emq string - if mq != "" { - query = append(query, mq) - } - if len(pm.IDs) != 0 { - query = append(query, fmt.Sprintf("id IN ('%s')", strings.Join(pm.IDs, "','"))) - } - if pm.Name != "" { - query = append(query, "c.name = :name") - } - if pm.Tag != "" { - query = append(query, ":tag = ANY(c.tags)") - } - if pm.Status != mfclients.AllStatus { - query = append(query, "c.status = :status") - } - // For listing clients that the specified client owns but not sharedby - if pm.Owner != "" && pm.SharedBy == "" { - query = append(query, "c.owner_id = :owner_id") - } - - // For listing clients that the specified client owns and that are shared with the specified client - if pm.Owner != "" && pm.SharedBy != "" { - query = append(query, "(c.owner_id = :owner_id OR c.id IN (SELECT subject FROM policies WHERE object IN (SELECT object FROM policies WHERE subject = :shared_by AND :action=ANY(actions))))") - } - // For listing clients that the specified client is shared with - if pm.SharedBy != "" && pm.Owner == "" { - query = append(query, "c.owner_id != :shared_by AND (c.id IN (SELECT subject FROM policies WHERE object IN (SELECT object FROM policies WHERE subject = :shared_by AND :action=ANY(actions))))") - } - if len(query) > 0 { - emq = fmt.Sprintf("WHERE %s", strings.Join(query, " AND ")) - } - return emq, nil - -} - -func toDBClientsPage(pm mfclients.Page) (dbClientsPage, error) { - _, data, err := postgres.CreateMetadataQuery("", pm.Metadata) - if err != nil { - return dbClientsPage{}, errors.Wrap(errors.ErrViewEntity, err) - } - return dbClientsPage{ - Name: pm.Name, - Metadata: data, - Owner: pm.Owner, - Total: pm.Total, - Offset: pm.Offset, - Limit: pm.Limit, - Status: pm.Status, - Tag: pm.Tag, - Identity: pm.Identity, - SharedBy: pm.SharedBy, - Subject: pm.Subject, - Action: pm.Action, - }, nil -} - -type dbClientsPage struct { - GroupID string `db:"group_id"` - Name string `db:"name"` - Owner string `db:"owner_id"` - Identity string `db:"identity"` - Metadata []byte `db:"metadata"` - Tag string `db:"tag"` - Status mfclients.Status `db:"status"` - Total uint64 `db:"total"` - Limit uint64 `db:"limit"` - Offset uint64 `db:"offset"` - SharedBy string `db:"shared_by"` - Subject string `db:"subject"` - Action string `db:"action"` + return pgclients.ToClient(dbc) } diff --git a/things/clients/postgres/clients_test.go b/things/clients/postgres/clients_test.go index c4e0d7ff..dc22a7c6 100644 --- a/things/clients/postgres/clients_test.go +++ b/things/clients/postgres/clients_test.go @@ -12,14 +12,9 @@ import ( "github.com/mainflux/mainflux/internal/testsutil" mfclients "github.com/mainflux/mainflux/pkg/clients" "github.com/mainflux/mainflux/pkg/errors" - mfgroups "github.com/mainflux/mainflux/pkg/groups" "github.com/mainflux/mainflux/pkg/uuid" cpostgres "github.com/mainflux/mainflux/things/clients/postgres" - gpostgres "github.com/mainflux/mainflux/things/groups/postgres" - "github.com/mainflux/mainflux/things/policies" - ppostgres "github.com/mainflux/mainflux/things/policies/postgres" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) const maxNameSize = 1024 @@ -29,8 +24,6 @@ var ( invalidName = strings.Repeat("m", maxNameSize+10) clientIdentity = "client-identity@example.com" clientName = "client name" - wrongName = "wrong-name" - wrongID = "wrong-id" ) func TestClientsSave(t *testing.T) { @@ -163,830 +156,3 @@ func TestClientsSave(t *testing.T) { } } } - -func TestClientsRetrieveByID(t *testing.T) { - t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) - repo := cpostgres.NewRepository(database) - - client := mfclients.Client{ - ID: testsutil.GenerateUUID(t, idProvider), - Name: clientName, - Credentials: mfclients.Credentials{ - Identity: clientIdentity, - Secret: testsutil.GenerateUUID(t, idProvider), - }, - Status: mfclients.EnabledStatus, - } - - _, err := repo.Save(context.Background(), client) - require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - - cases := map[string]struct { - ID string - err error - }{ - "retrieve existing client": {client.ID, nil}, - "retrieve non-existing client": {wrongID, errors.ErrNotFound}, - } - - for desc, tc := range cases { - cli, err := repo.RetrieveByID(context.Background(), tc.ID) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err)) - if err == nil { - assert.Equal(t, client.ID, cli.ID, fmt.Sprintf("retrieve client by ID : client ID : expected %s got %s\n", client.ID, cli.ID)) - assert.Equal(t, client.Name, cli.Name, fmt.Sprintf("retrieve client by ID : client Name : expected %s got %s\n", client.Name, cli.Name)) - assert.Equal(t, client.Credentials.Identity, cli.Credentials.Identity, fmt.Sprintf("retrieve client by ID : client Identity : expected %s got %s\n", client.Credentials.Identity, cli.Credentials.Identity)) - assert.Equal(t, client.Status, cli.Status, fmt.Sprintf("retrieve client by ID : client Status : expected %d got %d\n", client.Status, cli.Status)) - } - } -} - -func TestClientsRetrieveAll(t *testing.T) { - t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) - repo := cpostgres.NewRepository(database) - grepo := gpostgres.NewRepository(database) - prepo := ppostgres.NewRepository(database) - - var nClients = uint64(200) - var ownerID = testsutil.GenerateUUID(t, idProvider) - - meta := mfclients.Metadata{ - "admin": "true", - } - wrongMeta := mfclients.Metadata{ - "admin": "false", - } - var expectedClients = []mfclients.Client{} - - var sharedGroup = mfgroups.Group{ - ID: testsutil.GenerateUUID(t, idProvider), - Name: "shared-group", - } - _, err := grepo.Save(context.Background(), sharedGroup) - require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - - for i := uint64(0); i < nClients; i++ { - identity := fmt.Sprintf("TestRetrieveAll%d@example.com", i) - client := mfclients.Client{ - ID: testsutil.GenerateUUID(t, idProvider), - Name: identity, - Credentials: mfclients.Credentials{ - Identity: identity, - Secret: testsutil.GenerateUUID(t, idProvider), - }, - Metadata: mfclients.Metadata{}, - Status: mfclients.EnabledStatus, - } - if i%10 == 0 { - client.Owner = ownerID - client.Metadata = meta - client.Tags = []string{"Test"} - } - if i%50 == 0 { - client.Status = mfclients.DisabledStatus - } - _, err := repo.Save(context.Background(), client) - require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - expectedClients = append(expectedClients, client) - var policy = policies.Policy{ - Subject: client.ID, - Object: sharedGroup.ID, - Actions: []string{"c_list"}, - } - _, err = prepo.Save(context.Background(), policy) - require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - } - - cases := map[string]struct { - size uint64 - pm mfclients.Page - response []mfclients.Client - }{ - "retrieve all clients empty page": { - pm: mfclients.Page{}, - response: []mfclients.Client{}, - size: 0, - }, - "retrieve all clients": { - pm: mfclients.Page{ - Offset: 0, - Limit: nClients, - Status: mfclients.AllStatus, - }, - response: expectedClients, - size: 200, - }, - "retrieve all clients with limit": { - pm: mfclients.Page{ - Offset: 0, - Limit: 50, - Status: mfclients.AllStatus, - }, - response: expectedClients[0:50], - size: 50, - }, - "retrieve all clients with offset": { - pm: mfclients.Page{ - Offset: 50, - Limit: nClients, - Status: mfclients.AllStatus, - }, - response: expectedClients[50:200], - size: 150, - }, - "retrieve all clients with limit and offset": { - pm: mfclients.Page{ - Offset: 50, - Limit: 50, - Status: mfclients.AllStatus, - }, - response: expectedClients[50:100], - size: 50, - }, - "retrieve all clients with limit and offset not full": { - pm: mfclients.Page{ - Offset: 170, - Limit: 50, - Status: mfclients.AllStatus, - }, - response: expectedClients[170:200], - size: 30, - }, - "retrieve all clients by metadata": { - pm: mfclients.Page{ - Offset: 0, - Limit: nClients, - Total: nClients, - Metadata: meta, - Status: mfclients.AllStatus, - }, - response: []mfclients.Client{expectedClients[0], expectedClients[10], expectedClients[20], expectedClients[30], expectedClients[40], expectedClients[50], expectedClients[60], - expectedClients[70], expectedClients[80], expectedClients[90], expectedClients[100], expectedClients[110], expectedClients[120], expectedClients[130], - expectedClients[140], expectedClients[150], expectedClients[160], expectedClients[170], expectedClients[180], expectedClients[190], - }, - size: 20, - }, - "retrieve clients by wrong metadata": { - pm: mfclients.Page{ - Offset: 0, - Limit: nClients, - Total: nClients, - Metadata: wrongMeta, - Status: mfclients.AllStatus, - }, - response: []mfclients.Client{}, - size: 0, - }, - "retrieve all clients by name": { - pm: mfclients.Page{ - Offset: 0, - Limit: nClients, - Total: nClients, - Name: "TestRetrieveAll3@example.com", - Status: mfclients.AllStatus, - }, - response: []mfclients.Client{expectedClients[3]}, - size: 1, - }, - "retrieve clients by wrong name": { - pm: mfclients.Page{ - Offset: 0, - Limit: nClients, - Total: nClients, - Name: wrongName, - Status: mfclients.AllStatus, - }, - response: []mfclients.Client{}, - size: 0, - }, - "retrieve all clients by owner": { - pm: mfclients.Page{ - Offset: 0, - Limit: nClients, - Total: nClients, - Owner: ownerID, - Status: mfclients.AllStatus, - }, - response: []mfclients.Client{expectedClients[0], expectedClients[10], expectedClients[20], expectedClients[30], expectedClients[40], expectedClients[50], expectedClients[60], - expectedClients[70], expectedClients[80], expectedClients[90], expectedClients[100], expectedClients[110], expectedClients[120], expectedClients[130], - expectedClients[140], expectedClients[150], expectedClients[160], expectedClients[170], expectedClients[180], expectedClients[190], - }, - size: 20, - }, - "retrieve clients by wrong owner": { - pm: mfclients.Page{ - Offset: 0, - Limit: nClients, - Total: nClients, - Owner: wrongID, - Status: mfclients.AllStatus, - }, - response: []mfclients.Client{}, - size: 0, - }, - "retrieve all clients shared by": { - pm: mfclients.Page{ - Offset: 0, - Limit: nClients, - Total: nClients, - SharedBy: expectedClients[0].ID, - Action: "c_list", - Status: mfclients.AllStatus, - }, - response: expectedClients, - size: nClients, - }, - "retrieve all clients shared by and owned by": { - pm: mfclients.Page{ - Offset: 0, - Limit: nClients, - Total: nClients, - SharedBy: ownerID, - Owner: ownerID, - Status: mfclients.AllStatus, - }, - response: []mfclients.Client{expectedClients[0], expectedClients[10], expectedClients[20], expectedClients[30], expectedClients[40], expectedClients[50], expectedClients[60], - expectedClients[70], expectedClients[80], expectedClients[90], expectedClients[100], expectedClients[110], expectedClients[120], expectedClients[130], - expectedClients[140], expectedClients[150], expectedClients[160], expectedClients[170], expectedClients[180], expectedClients[190], - }, - size: 20, - }, - "retrieve all clients by disabled status": { - pm: mfclients.Page{ - Offset: 0, - Limit: nClients, - Total: nClients, - Status: mfclients.DisabledStatus, - }, - response: []mfclients.Client{expectedClients[0], expectedClients[50], expectedClients[100], expectedClients[150]}, - size: 4, - }, - "retrieve all clients by combined status": { - pm: mfclients.Page{ - Offset: 0, - Limit: nClients, - Total: nClients, - Status: mfclients.AllStatus, - }, - response: expectedClients, - size: 200, - }, - "retrieve clients by the wrong status": { - pm: mfclients.Page{ - Offset: 0, - Limit: nClients, - Total: nClients, - Status: 10, - }, - response: []mfclients.Client{}, - size: 0, - }, - "retrieve all clients by tags": { - pm: mfclients.Page{ - Offset: 0, - Limit: nClients, - Total: nClients, - Tag: "Test", - Status: mfclients.AllStatus, - }, - response: []mfclients.Client{expectedClients[0], expectedClients[10], expectedClients[20], expectedClients[30], expectedClients[40], expectedClients[50], expectedClients[60], - expectedClients[70], expectedClients[80], expectedClients[90], expectedClients[100], expectedClients[110], expectedClients[120], expectedClients[130], - expectedClients[140], expectedClients[150], expectedClients[160], expectedClients[170], expectedClients[180], expectedClients[190], - }, - size: 20, - }, - "retrieve clients by wrong tags": { - pm: mfclients.Page{ - Offset: 0, - Limit: nClients, - Total: nClients, - Tag: "wrongTags", - Status: mfclients.AllStatus, - }, - response: []mfclients.Client{}, - size: 0, - }, - } - for desc, tc := range cases { - page, err := repo.RetrieveAll(context.Background(), tc.pm) - size := uint64(len(page.Clients)) - assert.ElementsMatch(t, page.Clients, tc.response, fmt.Sprintf("%s: expected %v got %v\n", desc, tc.response, page.Clients)) - assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected size %d got %d\n", desc, tc.size, size)) - assert.Nil(t, err, fmt.Sprintf("%s: expected no error got %d\n", desc, err)) - } -} - -func TestGroupsMembers(t *testing.T) { - t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) - crepo := cpostgres.NewRepository(database) - grepo := gpostgres.NewRepository(database) - prepo := ppostgres.NewRepository(database) - - clientA := mfclients.Client{ - ID: testsutil.GenerateUUID(t, idProvider), - Name: "client-memberships", - Credentials: mfclients.Credentials{ - Secret: testsutil.GenerateUUID(t, idProvider), - }, - Metadata: mfclients.Metadata{}, - Status: mfclients.EnabledStatus, - } - clientB := mfclients.Client{ - ID: testsutil.GenerateUUID(t, idProvider), - Name: "client-memberships", - Credentials: mfclients.Credentials{ - Identity: "client-memberships2@example.com", - Secret: testsutil.GenerateUUID(t, idProvider), - }, - Metadata: mfclients.Metadata{}, - Status: mfclients.EnabledStatus, - } - group := mfgroups.Group{ - ID: testsutil.GenerateUUID(t, idProvider), - Name: "group-membership", - Metadata: mfclients.Metadata{}, - Status: mfclients.EnabledStatus, - } - - policyA := policies.Policy{ - Subject: clientA.ID, - Object: group.ID, - Actions: []string{"g_list"}, - } - policyB := policies.Policy{ - Subject: clientB.ID, - Object: group.ID, - Actions: []string{"g_list"}, - } - - _, err := crepo.Save(context.Background(), clientA) - assert.True(t, errors.Contains(err, nil), fmt.Sprintf("save client: expected %v got %s\n", nil, err)) - _, err = crepo.Save(context.Background(), clientB) - assert.True(t, errors.Contains(err, nil), fmt.Sprintf("save client: expected %v got %s\n", nil, err)) - _, err = grepo.Save(context.Background(), group) - assert.True(t, errors.Contains(err, nil), fmt.Sprintf("save group: expected %v got %s\n", nil, err)) - _, err = prepo.Save(context.Background(), policyA) - assert.True(t, errors.Contains(err, nil), fmt.Sprintf("save policy: expected %v got %s\n", nil, err)) - _, err = prepo.Save(context.Background(), policyB) - assert.True(t, errors.Contains(err, nil), fmt.Sprintf("save policy: expected %v got %s\n", nil, err)) - - cases := map[string]struct { - ID string - err error - }{ - "retrieve members for existing group": {group.ID, nil}, - "retrieve members for non-existing group": {wrongID, nil}, - } - - for desc, tc := range cases { - mp, err := crepo.Members(context.Background(), tc.ID, mfclients.Page{Total: 10, Offset: 0, Limit: 10, Status: mfclients.AllStatus, Subject: clientB.ID, Action: "g_list"}) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err)) - if tc.ID == group.ID { - assert.ElementsMatch(t, mp.Members, []mfclients.Client{clientA, clientB}, fmt.Sprintf("%s: expected %v got %v\n", desc, []mfclients.Client{clientA, clientB}, mp.Members)) - } - } -} - -func TestClientsUpdateMetadata(t *testing.T) { - t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) - repo := cpostgres.NewRepository(database) - - client1 := mfclients.Client{ - ID: testsutil.GenerateUUID(t, idProvider), - Name: "enabled-client", - Credentials: mfclients.Credentials{ - Identity: "client1-update@example.com", - Secret: testsutil.GenerateUUID(t, idProvider), - }, - Metadata: mfclients.Metadata{ - "name": "enabled-client", - }, - Tags: []string{"enabled", "tag1"}, - Status: mfclients.EnabledStatus, - } - - client2 := mfclients.Client{ - ID: testsutil.GenerateUUID(t, idProvider), - Name: "disabled-client", - Credentials: mfclients.Credentials{ - Identity: "client2-update@example.com", - Secret: testsutil.GenerateUUID(t, idProvider), - }, - Metadata: mfclients.Metadata{ - "name": "disabled-client", - }, - Tags: []string{"disabled", "tag1"}, - Status: mfclients.DisabledStatus, - } - - _, err := repo.Save(context.Background(), client1) - assert.True(t, errors.Contains(err, nil), fmt.Sprintf("add new client with metadata: expected %v got %s\n", nil, err)) - _, err = repo.Save(context.Background(), client2) - assert.True(t, errors.Contains(err, nil), fmt.Sprintf("add new disabled client: expected %v got %s\n", nil, err)) - - ucases := []struct { - desc string - update string - client mfclients.Client - err error - }{ - { - desc: "update metadata for enabled client", - update: "metadata", - client: mfclients.Client{ - ID: client1.ID, - Metadata: mfclients.Metadata{ - "update": "metadata", - }, - }, - err: nil, - }, - { - desc: "update metadata for disabled client", - update: "metadata", - client: mfclients.Client{ - ID: client2.ID, - Metadata: mfclients.Metadata{ - "update": "metadata", - }, - }, - err: errors.ErrNotFound, - }, - { - desc: "update name for enabled client", - update: "name", - client: mfclients.Client{ - ID: client1.ID, - Name: "updated name", - }, - err: nil, - }, - { - desc: "update name for disabled client", - update: "name", - client: mfclients.Client{ - ID: client2.ID, - Name: "updated name", - }, - err: errors.ErrNotFound, - }, - { - desc: "update name and metadata for enabled client", - update: "both", - client: mfclients.Client{ - ID: client1.ID, - Name: "updated name and metadata", - Metadata: mfclients.Metadata{ - "update": "name and metadata", - }, - }, - err: nil, - }, - { - desc: "update name and metadata for a disabled client", - update: "both", - client: mfclients.Client{ - ID: client2.ID, - Name: "updated name and metadata", - Metadata: mfclients.Metadata{ - "update": "name and metadata", - }, - }, - err: errors.ErrNotFound, - }, - { - desc: "update metadata for invalid client", - update: "metadata", - client: mfclients.Client{ - ID: wrongID, - Metadata: mfclients.Metadata{ - "update": "metadata", - }, - }, - err: errors.ErrNotFound, - }, - { - desc: "update name for invalid client", - update: "name", - client: mfclients.Client{ - ID: wrongID, - Name: "updated name", - }, - err: errors.ErrNotFound, - }, - { - desc: "update name and metadata for invalid client", - update: "both", - client: mfclients.Client{ - ID: client2.ID, - Name: "updated name and metadata", - Metadata: mfclients.Metadata{ - "update": "name and metadata", - }, - }, - err: errors.ErrNotFound, - }, - } - for _, tc := range ucases { - expected, err := repo.Update(context.Background(), tc.client) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - if err == nil { - if tc.client.Name != "" { - assert.Equal(t, expected.Name, tc.client.Name, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, expected.Name, tc.client.Name)) - } - if tc.client.Metadata != nil { - assert.Equal(t, expected.Metadata, tc.client.Metadata, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, expected.Metadata, tc.client.Metadata)) - } - - } - } -} - -func TestClientsUpdateTags(t *testing.T) { - t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) - repo := cpostgres.NewRepository(database) - - client1 := mfclients.Client{ - ID: testsutil.GenerateUUID(t, idProvider), - Name: "enabled-client-with-tags", - Credentials: mfclients.Credentials{ - Identity: "client1-update-tags@example.com", - Secret: testsutil.GenerateUUID(t, idProvider), - }, - Tags: []string{"test", "enabled"}, - Status: mfclients.EnabledStatus, - } - client2 := mfclients.Client{ - ID: testsutil.GenerateUUID(t, idProvider), - Name: "disabled-client-with-tags", - Credentials: mfclients.Credentials{ - Identity: "client2-update-tags@example.com", - Secret: testsutil.GenerateUUID(t, idProvider), - }, - Tags: []string{"test", "disabled"}, - Status: mfclients.DisabledStatus, - } - - _, err := repo.Save(context.Background(), client1) - assert.True(t, errors.Contains(err, nil), fmt.Sprintf("add new client with tags: expected %v got %s\n", nil, err)) - if err == nil { - assert.Equal(t, client1.ID, client1.ID, fmt.Sprintf("add new client with tags: expected %v got %s\n", nil, err)) - } - _, err = repo.Save(context.Background(), client2) - assert.True(t, errors.Contains(err, nil), fmt.Sprintf("add new disabled client with tags: expected %v got %s\n", nil, err)) - if err == nil { - assert.Equal(t, client2.ID, client2.ID, fmt.Sprintf("add new disabled client with tags: expected %v got %s\n", nil, err)) - } - ucases := []struct { - desc string - client mfclients.Client - err error - }{ - { - desc: "update tags for enabled client", - client: mfclients.Client{ - ID: client1.ID, - Tags: []string{"updated"}, - }, - err: nil, - }, - { - desc: "update tags for disabled client", - client: mfclients.Client{ - ID: client2.ID, - Tags: []string{"updated"}, - }, - err: errors.ErrNotFound, - }, - { - desc: "update tags for invalid client", - client: mfclients.Client{ - ID: wrongID, - Tags: []string{"updated"}, - }, - err: errors.ErrNotFound, - }, - } - for _, tc := range ucases { - expected, err := repo.UpdateTags(context.Background(), tc.client) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - if err == nil { - assert.Equal(t, tc.client.Tags, expected.Tags, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.client.Tags, expected.Tags)) - } - } -} - -func TestClientsUpdateSecret(t *testing.T) { - t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) - repo := cpostgres.NewRepository(database) - - client1 := mfclients.Client{ - ID: testsutil.GenerateUUID(t, idProvider), - Name: "enabled-client", - Credentials: mfclients.Credentials{ - Identity: "client1-update@example.com", - Secret: testsutil.GenerateUUID(t, idProvider), - }, - Status: mfclients.EnabledStatus, - } - client2 := mfclients.Client{ - ID: testsutil.GenerateUUID(t, idProvider), - Name: "disabled-client", - Credentials: mfclients.Credentials{ - Identity: "client2-update@example.com", - Secret: testsutil.GenerateUUID(t, idProvider), - }, - Status: mfclients.DisabledStatus, - } - - rClient1, err := repo.Save(context.Background(), client1) - assert.True(t, errors.Contains(err, nil), fmt.Sprintf("add new client: expected %v got %s\n", nil, err)) - if err == nil { - assert.Equal(t, client1.ID, rClient1[0].ID, fmt.Sprintf("add new client: expected %v got %s\n", nil, err)) - } - rClient2, err := repo.Save(context.Background(), client2) - assert.True(t, errors.Contains(err, nil), fmt.Sprintf("add new disabled client: expected %v got %s\n", nil, err)) - if err == nil { - assert.Equal(t, client2.ID, rClient2[0].ID, fmt.Sprintf("add new disabled client: expected %v got %s\n", nil, err)) - } - - ucases := []struct { - desc string - client mfclients.Client - err error - }{ - { - desc: "update secret for enabled client", - client: mfclients.Client{ - ID: client1.ID, - Credentials: mfclients.Credentials{ - Identity: "client1-update@example.com", - Secret: "newpassword", - }, - }, - err: nil, - }, - { - desc: "update secret for disabled client", - client: mfclients.Client{ - ID: client2.ID, - Credentials: mfclients.Credentials{ - Identity: "client2-update@example.com", - Secret: "newpassword", - }, - }, - err: errors.ErrNotFound, - }, - { - desc: "update secret for invalid client", - client: mfclients.Client{ - ID: wrongID, - Credentials: mfclients.Credentials{ - Identity: "client3-update@example.com", - Secret: "newpassword", - }, - }, - err: errors.ErrNotFound, - }, - } - for _, tc := range ucases { - _, err := repo.UpdateSecret(context.Background(), tc.client) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - if err == nil { - c, err := repo.RetrieveByID(context.Background(), tc.client.ID) - require.Nil(t, err, fmt.Sprintf("retrieve client by id during update of secret unexpected error: %s", err)) - assert.Equal(t, tc.client.Credentials.Secret, c.Credentials.Secret, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.client.Credentials.Secret, c.Credentials.Secret)) - } - } -} - -func TestClientsUpdateOwner(t *testing.T) { - t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) - repo := cpostgres.NewRepository(database) - - client1 := mfclients.Client{ - ID: testsutil.GenerateUUID(t, idProvider), - Name: "enabled-client-with-owner", - Credentials: mfclients.Credentials{ - Identity: "client1-update-owner@example.com", - Secret: testsutil.GenerateUUID(t, idProvider), - }, - Owner: testsutil.GenerateUUID(t, idProvider), - Status: mfclients.EnabledStatus, - } - client2 := mfclients.Client{ - ID: testsutil.GenerateUUID(t, idProvider), - Name: "disabled-client-with-owner", - Credentials: mfclients.Credentials{ - Identity: "client2-update-owner@example.com", - Secret: testsutil.GenerateUUID(t, idProvider), - }, - Owner: testsutil.GenerateUUID(t, idProvider), - Status: mfclients.DisabledStatus, - } - - _, err := repo.Save(context.Background(), client1) - assert.True(t, errors.Contains(err, nil), fmt.Sprintf("add new client with owner: expected %v got %s\n", nil, err)) - if err == nil { - assert.Equal(t, client1.ID, client1.ID, fmt.Sprintf("add new client with owner: expected %v got %s\n", nil, err)) - } - _, err = repo.Save(context.Background(), client2) - assert.True(t, errors.Contains(err, nil), fmt.Sprintf("add new disabled client with owner: expected %v got %s\n", nil, err)) - if err == nil { - assert.Equal(t, client2.ID, client2.ID, fmt.Sprintf("add new disabled client with owner: expected %v got %s\n", nil, err)) - } - ucases := []struct { - desc string - client mfclients.Client - err error - }{ - { - desc: "update owner for enabled client", - client: mfclients.Client{ - ID: client1.ID, - Owner: testsutil.GenerateUUID(t, idProvider), - }, - err: nil, - }, - { - desc: "update owner for disabled client", - client: mfclients.Client{ - ID: client2.ID, - Owner: testsutil.GenerateUUID(t, idProvider), - }, - err: errors.ErrNotFound, - }, - { - desc: "update owner for invalid client", - client: mfclients.Client{ - ID: wrongID, - Owner: testsutil.GenerateUUID(t, idProvider), - }, - err: errors.ErrNotFound, - }, - } - for _, tc := range ucases { - expected, err := repo.UpdateOwner(context.Background(), tc.client) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - if err == nil { - assert.Equal(t, tc.client.Owner, expected.Owner, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.client.Owner, expected.Owner)) - } - } -} - -func TestClientsChangeStatus(t *testing.T) { - t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) - repo := cpostgres.NewRepository(database) - - client1 := mfclients.Client{ - ID: testsutil.GenerateUUID(t, idProvider), - Name: "enabled-client", - Credentials: mfclients.Credentials{ - Identity: "client1-update@example.com", - Secret: testsutil.GenerateUUID(t, idProvider), - }, - Status: mfclients.EnabledStatus, - } - - _, err := repo.Save(context.Background(), client1) - assert.True(t, errors.Contains(err, nil), fmt.Sprintf("add new client: expected %v got %s\n", nil, err)) - - ucases := []struct { - desc string - client mfclients.Client - err error - }{ - { - desc: "change client status for an enabled client", - client: mfclients.Client{ - ID: client1.ID, - Status: 0, - }, - err: nil, - }, - { - desc: "change client status for a disabled client", - client: mfclients.Client{ - ID: client1.ID, - Status: 1, - }, - err: nil, - }, - { - desc: "change client status for non-existing client", - client: mfclients.Client{ - ID: "invalid", - Status: 2, - }, - err: errors.ErrNotFound, - }, - } - - for _, tc := range ucases { - expected, err := repo.ChangeStatus(context.Background(), tc.client) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - if err == nil { - assert.Equal(t, tc.client.Status, expected.Status, fmt.Sprintf("%s: expected %d got %d\n", tc.desc, tc.client.Status, expected.Status)) - } - } -} diff --git a/things/clients/service.go b/things/clients/service.go index 31b314f5..11c73fe5 100644 --- a/things/clients/service.go +++ b/things/clients/service.go @@ -11,6 +11,7 @@ import ( mfclients "github.com/mainflux/mainflux/pkg/clients" "github.com/mainflux/mainflux/pkg/errors" mfgroups "github.com/mainflux/mainflux/pkg/groups" + "github.com/mainflux/mainflux/things/clients/postgres" tpolicies "github.com/mainflux/mainflux/things/policies" upolicies "github.com/mainflux/mainflux/users/policies" ) @@ -31,14 +32,14 @@ const ( type service struct { uauth upolicies.AuthServiceClient policies tpolicies.Service - clients mfclients.Repository + clients postgres.Repository clientCache Cache idProvider mainflux.IDProvider grepo mfgroups.Repository } // NewService returns a new Clients service implementation. -func NewService(uauth upolicies.AuthServiceClient, policies tpolicies.Service, c mfclients.Repository, grepo mfgroups.Repository, tcache Cache, idp mainflux.IDProvider) Service { +func NewService(uauth upolicies.AuthServiceClient, policies tpolicies.Service, c postgres.Repository, grepo mfgroups.Repository, tcache Cache, idp mainflux.IDProvider) Service { return service{ uauth: uauth, policies: policies, @@ -198,6 +199,7 @@ func (svc service) UpdateClientSecret(ctx context.Context, token, id, key string }, UpdatedAt: time.Now(), UpdatedBy: userID, + Status: mfclients.EnabledStatus, } return svc.clients.UpdateSecret(ctx, client) @@ -217,6 +219,7 @@ func (svc service) UpdateClientOwner(ctx context.Context, token string, cli mfcl Owner: cli.Owner, UpdatedAt: time.Now(), UpdatedBy: userID, + Status: mfclients.EnabledStatus, } return svc.clients.UpdateOwner(ctx, client) diff --git a/things/groups/postgres/groups.go b/things/groups/postgres/groups.go deleted file mode 100644 index ab31d77d..00000000 --- a/things/groups/postgres/groups.go +++ /dev/null @@ -1,443 +0,0 @@ -// Copyright (c) Mainflux -// SPDX-License-Identifier: Apache-2.0 - -package postgres - -import ( - "context" - "database/sql" - "encoding/json" - "fmt" - "strings" - "time" - - "github.com/jmoiron/sqlx" - "github.com/mainflux/mainflux/internal/postgres" - mfclients "github.com/mainflux/mainflux/pkg/clients" - "github.com/mainflux/mainflux/pkg/errors" - mfgroups "github.com/mainflux/mainflux/pkg/groups" -) - -var _ mfgroups.Repository = (*grepo)(nil) - -type grepo struct { - db postgres.Database -} - -// NewRepository instantiates a PostgreSQL implementation of group -// repository. -func NewRepository(db postgres.Database) mfgroups.Repository { - return &grepo{ - db: db, - } -} - -// TODO - check parent group write access. -func (repo grepo) Save(ctx context.Context, g mfgroups.Group) (mfgroups.Group, error) { - q := `INSERT INTO groups (name, description, id, owner_id, metadata, created_at, updated_at, updated_by, status) - VALUES (:name, :description, :id, :owner_id, :metadata, :created_at, :updated_at, :updated_by, :status) - RETURNING id, name, description, owner_id, COALESCE(parent_id, '') AS parent_id, metadata, created_at, updated_at, updated_by, status;` - if g.Parent != "" { - q = `INSERT INTO groups (name, description, id, owner_id, parent_id, metadata, created_at, updated_at, updated_by, status) - VALUES (:name, :description, :id, :owner_id, :parent_id, :metadata, :created_at, :updated_at, :updated_by, :status) - RETURNING id, name, description, owner_id, COALESCE(parent_id, '') AS parent_id, metadata, created_at, updated_at, updated_by, status;` - } - dbg, err := toDBGroup(g) - if err != nil { - return mfgroups.Group{}, err - } - row, err := repo.db.NamedQueryContext(ctx, q, dbg) - if err != nil { - return mfgroups.Group{}, postgres.HandleError(err, errors.ErrCreateEntity) - } - - defer row.Close() - row.Next() - dbg = dbGroup{} - if err := row.StructScan(&dbg); err != nil { - return mfgroups.Group{}, err - } - - return toGroup(dbg) -} - -func (repo grepo) RetrieveByID(ctx context.Context, id string) (mfgroups.Group, error) { - dbu := dbGroup{ - ID: id, - } - q := `SELECT id, name, owner_id, COALESCE(parent_id, '') AS parent_id, description, metadata, created_at, updated_at, updated_by, status FROM groups - WHERE id = $1` - if err := repo.db.QueryRowxContext(ctx, q, dbu.ID).StructScan(&dbu); err != nil { - if err == sql.ErrNoRows { - return mfgroups.Group{}, errors.Wrap(errors.ErrNotFound, err) - - } - return mfgroups.Group{}, errors.Wrap(errors.ErrViewEntity, err) - } - return toGroup(dbu) -} - -func (repo grepo) RetrieveAll(ctx context.Context, gm mfgroups.GroupsPage) (mfgroups.GroupsPage, error) { - var q string - query, err := buildQuery(gm) - if err != nil { - return mfgroups.GroupsPage{}, err - } - - if gm.ID != "" { - q = buildHierachy(gm) - } - if gm.ID == "" { - q = `SELECT DISTINCT g.id, g.owner_id, COALESCE(g.parent_id, '') AS parent_id, g.name, g.description, - g.metadata, g.created_at, g.updated_at, g.updated_by, g.status FROM groups g` - } - q = fmt.Sprintf("%s %s ORDER BY g.updated_at LIMIT :limit OFFSET :offset;", q, query) - - dbPage, err := toDBGroupPage(gm) - if err != nil { - return mfgroups.GroupsPage{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) - } - rows, err := repo.db.NamedQueryContext(ctx, q, dbPage) - if err != nil { - return mfgroups.GroupsPage{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) - } - defer rows.Close() - - items, err := repo.processRows(rows) - if err != nil { - return mfgroups.GroupsPage{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) - } - - cq := "SELECT COUNT(*) FROM groups g" - if query != "" { - cq = fmt.Sprintf(" %s %s", cq, query) - } - - total, err := postgres.Total(ctx, repo.db, cq, dbPage) - if err != nil { - return mfgroups.GroupsPage{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) - } - - page := gm - page.Groups = items - page.Total = total - - return page, nil -} - -func (repo grepo) Memberships(ctx context.Context, clientID string, gm mfgroups.GroupsPage) (mfgroups.MembershipsPage, error) { - var q string - query, err := buildQuery(gm) - if err != nil { - return mfgroups.MembershipsPage{}, err - } - if gm.ID != "" { - q = buildHierachy(gm) - } - if gm.ID == "" { - q = `SELECT g.id, g.owner_id, COALESCE(g.parent_id, '') AS parent_id, g.name, g.description, - g.metadata, g.created_at, g.updated_at, g.updated_by, g.status FROM groups g` - } - aq := "" - // If not admin, the client needs to have a g_list action on the group or they are the owner. - if gm.Subject != "" { - aq = `AND policies.object IN (SELECT object FROM policies WHERE subject = :subject AND :action=ANY(actions)) OR g.owner_id = :subject` - } - q = fmt.Sprintf(`%s INNER JOIN policies ON g.id=policies.object %s AND policies.subject = :client_id %s - ORDER BY g.updated_at LIMIT :limit OFFSET :offset;`, q, query, aq) - - dbPage, err := toDBGroupPage(gm) - if err != nil { - return mfgroups.MembershipsPage{}, errors.Wrap(postgres.ErrFailedToRetrieveMembership, err) - } - dbPage.ClientID = clientID - rows, err := repo.db.NamedQueryContext(ctx, q, dbPage) - if err != nil { - return mfgroups.MembershipsPage{}, errors.Wrap(postgres.ErrFailedToRetrieveMembership, err) - } - defer rows.Close() - - var items []mfgroups.Group - for rows.Next() { - dbg := dbGroup{} - if err := rows.StructScan(&dbg); err != nil { - return mfgroups.MembershipsPage{}, errors.Wrap(postgres.ErrFailedToRetrieveMembership, err) - } - group, err := toGroup(dbg) - if err != nil { - return mfgroups.MembershipsPage{}, errors.Wrap(postgres.ErrFailedToRetrieveMembership, err) - } - items = append(items, group) - } - - cq := fmt.Sprintf(`SELECT COUNT(*) FROM groups g INNER JOIN policies - ON g.id=policies.object %s AND policies.subject = :client_id`, query) - - total, err := postgres.Total(ctx, repo.db, cq, dbPage) - if err != nil { - return mfgroups.MembershipsPage{}, errors.Wrap(postgres.ErrFailedToRetrieveMembership, err) - } - page := mfgroups.MembershipsPage{ - Memberships: items, - Page: mfgroups.Page{ - Total: total, - }, - } - - return page, nil -} - -func (repo grepo) Update(ctx context.Context, g mfgroups.Group) (mfgroups.Group, error) { - var query []string - var upq string - if g.Name != "" { - query = append(query, "name = :name,") - } - if g.Description != "" { - query = append(query, "description = :description,") - } - if g.Metadata != nil { - query = append(query, "metadata = :metadata,") - } - if len(query) > 0 { - upq = strings.Join(query, " ") - } - g.Status = mfclients.EnabledStatus - q := fmt.Sprintf(`UPDATE groups SET %s updated_at = :updated_at, updated_by = :updated_by - WHERE owner_id = :owner_id AND id = :id AND status = :status - RETURNING id, name, description, owner_id, COALESCE(parent_id, '') AS parent_id, metadata, created_at, updated_at, updated_by, status`, upq) - - dbu, err := toDBGroup(g) - if err != nil { - return mfgroups.Group{}, errors.Wrap(errors.ErrUpdateEntity, err) - } - - row, err := repo.db.NamedQueryContext(ctx, q, dbu) - if err != nil { - return mfgroups.Group{}, postgres.HandleError(err, errors.ErrUpdateEntity) - } - - defer row.Close() - if ok := row.Next(); !ok { - return mfgroups.Group{}, errors.Wrap(errors.ErrNotFound, row.Err()) - } - dbu = dbGroup{} - if err := row.StructScan(&dbu); err != nil { - return mfgroups.Group{}, errors.Wrap(err, errors.ErrUpdateEntity) - } - return toGroup(dbu) -} - -func (repo grepo) ChangeStatus(ctx context.Context, group mfgroups.Group) (mfgroups.Group, error) { - qc := `UPDATE groups SET status = :status WHERE id = :id RETURNING id, name, description, owner_id, COALESCE(parent_id, '') AS parent_id, metadata, created_at, updated_at, updated_by, status` - - dbg, err := toDBGroup(group) - if err != nil { - return mfgroups.Group{}, errors.Wrap(errors.ErrUpdateEntity, err) - } - - row, err := repo.db.NamedQueryContext(ctx, qc, dbg) - if err != nil { - return mfgroups.Group{}, postgres.HandleError(err, errors.ErrUpdateEntity) - } - - defer row.Close() - if ok := row.Next(); !ok { - return mfgroups.Group{}, errors.Wrap(errors.ErrNotFound, row.Err()) - } - dbg = dbGroup{} - if err := row.StructScan(&dbg); err != nil { - return mfgroups.Group{}, errors.Wrap(err, errors.ErrUpdateEntity) - } - - return toGroup(dbg) - -} - -func buildHierachy(gm mfgroups.GroupsPage) string { - query := "" - switch { - case gm.Direction >= 0: // ancestors - query = `WITH RECURSIVE groups_cte as ( - SELECT id, COALESCE(parent_id, '') AS parent_id, owner_id, name, description, metadata, created_at, updated_at, updated_by, status, 0 as level from groups WHERE id = :id - UNION SELECT x.id, COALESCE(x.parent_id, '') AS parent_id, x.owner_id, x.name, x.description, x.metadata, x.created_at, x.updated_at, x.updated_by, x.status, level - 1 from groups x - INNER JOIN groups_cte a ON a.parent_id = x.id - ) SELECT * FROM groups_cte g` - - case gm.Direction < 0: // descendants - query = `WITH RECURSIVE groups_cte as ( - SELECT id, COALESCE(parent_id, '') AS parent_id, owner_id, name, description, metadata, created_at, updated_at, updated_by, status, 0 as level, CONCAT('', '', id) as path from groups WHERE id = :id - UNION SELECT x.id, COALESCE(x.parent_id, '') AS parent_id, x.owner_id, x.name, x.description, x.metadata, x.created_at, x.updated_at, x.updated_by, x.status, level + 1, CONCAT(path, '.', x.id) as path from groups x - INNER JOIN groups_cte d ON d.id = x.parent_id - ) SELECT * FROM groups_cte g` - } - return query -} -func buildQuery(gm mfgroups.GroupsPage) (string, error) { - queries := []string{} - - if gm.Name != "" { - queries = append(queries, "g.name = :name") - } - if gm.Status != mfclients.AllStatus { - queries = append(queries, "g.status = :status") - } - if gm.OwnerID != "" { - queries = append(queries, "g.owner_id = :owner_id") - } - if gm.Tag != "" { - queries = append(queries, ":tag = ANY(c.tags)") - } - if gm.Subject != "" { - queries = append(queries, "(g.owner_id = :owner_id OR id IN (SELECT object as id FROM policies WHERE subject = :subject AND :action=ANY(actions)))") - } - if len(gm.Metadata) > 0 { - queries = append(queries, "'g.metadata @> :metadata'") - } - if len(queries) > 0 { - return fmt.Sprintf("WHERE %s", strings.Join(queries, " AND ")), nil - } - return "", nil -} - -type dbGroup struct { - ID string `db:"id"` - Parent string `db:"parent_id"` - Owner string `db:"owner_id"` - Name string `db:"name"` - Description string `db:"description"` - Level int `db:"level"` - Path string `db:"path,omitempty"` - Metadata []byte `db:"metadata"` - CreatedAt time.Time `db:"created_at"` - UpdatedAt sql.NullTime `db:"updated_at,omitempty"` - UpdatedBy *string `db:"updated_by,omitempty"` - Status mfclients.Status `db:"status"` -} - -func toDBGroup(g mfgroups.Group) (dbGroup, error) { - data := []byte("{}") - if len(g.Metadata) > 0 { - b, err := json.Marshal(g.Metadata) - if err != nil { - return dbGroup{}, errors.Wrap(errors.ErrMalformedEntity, err) - } - data = b - } - var updatedAt sql.NullTime - if !g.UpdatedAt.IsZero() { - updatedAt = sql.NullTime{Time: g.UpdatedAt, Valid: true} - } - var updatedBy *string - if g.UpdatedBy != "" { - updatedBy = &g.UpdatedBy - } - return dbGroup{ - ID: g.ID, - Name: g.Name, - Parent: g.Parent, - Owner: g.Owner, - Description: g.Description, - Metadata: data, - Path: g.Path, - CreatedAt: g.CreatedAt, - UpdatedAt: updatedAt, - UpdatedBy: updatedBy, - Status: g.Status, - }, nil -} - -func toGroup(g dbGroup) (mfgroups.Group, error) { - var metadata mfclients.Metadata - if g.Metadata != nil { - if err := json.Unmarshal([]byte(g.Metadata), &metadata); err != nil { - return mfgroups.Group{}, errors.Wrap(errors.ErrMalformedEntity, err) - } - } - var updatedAt time.Time - if g.UpdatedAt.Valid { - updatedAt = g.UpdatedAt.Time - } - var updatedBy string - if g.UpdatedBy != nil { - updatedBy = *g.UpdatedBy - } - - return mfgroups.Group{ - ID: g.ID, - Name: g.Name, - Parent: g.Parent, - Owner: g.Owner, - Description: g.Description, - Metadata: metadata, - Level: g.Level, - Path: g.Path, - UpdatedAt: updatedAt, - CreatedAt: g.CreatedAt, - UpdatedBy: updatedBy, - Status: g.Status, - }, nil -} - -func (gr grepo) processRows(rows *sqlx.Rows) ([]mfgroups.Group, error) { - var items []mfgroups.Group - for rows.Next() { - dbg := dbGroup{} - if err := rows.StructScan(&dbg); err != nil { - return items, err - } - group, err := toGroup(dbg) - if err != nil { - return items, err - } - items = append(items, group) - } - return items, nil -} - -func toDBGroupPage(pm mfgroups.GroupsPage) (dbGroupPage, error) { - level := mfgroups.MaxLevel - if pm.Level < mfgroups.MaxLevel { - level = pm.Level - } - data := []byte("{}") - if len(pm.Metadata) > 0 { - b, err := json.Marshal(pm.Metadata) - if err != nil { - return dbGroupPage{}, errors.Wrap(errors.ErrMalformedEntity, err) - } - data = b - } - return dbGroupPage{ - ID: pm.ID, - Name: pm.Name, - Metadata: data, - Path: pm.Path, - Level: level, - Total: pm.Total, - Offset: pm.Offset, - Limit: pm.Limit, - ParentID: pm.ID, - Owner: pm.OwnerID, - Subject: pm.Subject, - Action: pm.Action, - Status: pm.Status, - }, nil -} - -type dbGroupPage struct { - ClientID string `db:"client_id"` - ID string `db:"id"` - Name string `db:"name"` - ParentID string `db:"parent_id"` - Owner string `db:"owner_id"` - Metadata []byte `db:"metadata"` - Path string `db:"path"` - Level uint64 `db:"level"` - Total uint64 `db:"total"` - Limit uint64 `db:"limit"` - Offset uint64 `db:"offset"` - Subject string `db:"subject"` - Action string `db:"action"` - Status mfclients.Status `db:"status"` -} diff --git a/things/groups/postgres/groups_test.go b/things/groups/postgres/groups_test.go deleted file mode 100644 index b9df710b..00000000 --- a/things/groups/postgres/groups_test.go +++ /dev/null @@ -1,600 +0,0 @@ -// Copyright (c) Mainflux -// SPDX-License-Identifier: Apache-2.0 - -package postgres_test - -import ( - "context" - "fmt" - "strings" - "testing" - "time" - - "github.com/mainflux/mainflux/internal/testsutil" - mfclients "github.com/mainflux/mainflux/pkg/clients" - "github.com/mainflux/mainflux/pkg/errors" - mfgroups "github.com/mainflux/mainflux/pkg/groups" - "github.com/mainflux/mainflux/pkg/uuid" - cpostgres "github.com/mainflux/mainflux/things/clients/postgres" - gpostgres "github.com/mainflux/mainflux/things/groups/postgres" - "github.com/mainflux/mainflux/things/policies" - ppostgres "github.com/mainflux/mainflux/things/policies/postgres" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const ( - maxNameSize = 1024 - maxDescSize = 1024 - maxLevel = uint64(5) - groupName = "group" - description = "description" -) - -var ( - wrongID = "wrong-id" - invalidName = strings.Repeat("m", maxNameSize+10) - validDesc = strings.Repeat("m", 100) - invalidDesc = strings.Repeat("m", maxDescSize+1) - metadata = mfclients.Metadata{ - "admin": "true", - } - idProvider = uuid.New() -) - -func TestGroupSave(t *testing.T) { - t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) - groupRepo := gpostgres.NewRepository(database) - - usrID := testsutil.GenerateUUID(t, idProvider) - grpID := testsutil.GenerateUUID(t, idProvider) - - cases := []struct { - desc string - group mfgroups.Group - err error - }{ - { - desc: "create new group successfully", - group: mfgroups.Group{ - ID: grpID, - Name: groupName, - Status: mfclients.EnabledStatus, - }, - err: nil, - }, - { - desc: "create a new group with an existing name", - group: mfgroups.Group{ - ID: grpID, - Name: groupName, - Status: mfclients.EnabledStatus, - }, - err: errors.ErrConflict, - }, - { - desc: "create group with an invalid name", - group: mfgroups.Group{ - ID: testsutil.GenerateUUID(t, idProvider), - Name: invalidName, - Status: mfclients.EnabledStatus, - }, - err: errors.ErrMalformedEntity, - }, - { - desc: "create a group with invalid ID", - group: mfgroups.Group{ - ID: usrID, - Name: "withInvalidDescription", - Description: invalidDesc, - Status: mfclients.EnabledStatus, - }, - err: errors.ErrMalformedEntity, - }, - { - desc: "create group with description", - group: mfgroups.Group{ - ID: testsutil.GenerateUUID(t, idProvider), - Name: "withDescription", - Description: validDesc, - Status: mfclients.EnabledStatus, - }, - err: nil, - }, - { - desc: "create group with invalid description", - group: mfgroups.Group{ - ID: testsutil.GenerateUUID(t, idProvider), - Name: "withInvalidDescription", - Description: invalidDesc, - Status: mfclients.EnabledStatus, - }, - err: errors.ErrMalformedEntity, - }, - { - desc: "create group with parent", - group: mfgroups.Group{ - ID: testsutil.GenerateUUID(t, idProvider), - Parent: grpID, - Name: "withParent", - Status: mfclients.EnabledStatus, - }, - err: nil, - }, - { - desc: "create a group with an invalid parent", - group: mfgroups.Group{ - ID: testsutil.GenerateUUID(t, idProvider), - Parent: invalidName, - Name: "withInvalidParent", - Status: mfclients.EnabledStatus, - }, - err: errors.ErrMalformedEntity, - }, - { - desc: "create a group with an owner", - group: mfgroups.Group{ - ID: testsutil.GenerateUUID(t, idProvider), - Owner: usrID, - Name: "withOwner", - Status: mfclients.EnabledStatus, - }, - err: nil, - }, - { - desc: "create a group with an invalid owner", - group: mfgroups.Group{ - ID: testsutil.GenerateUUID(t, idProvider), - Owner: invalidName, - Name: "withInvalidOwner", - Status: mfclients.EnabledStatus, - }, - err: errors.ErrMalformedEntity, - }, - { - desc: "create a group with metadata", - group: mfgroups.Group{ - ID: testsutil.GenerateUUID(t, idProvider), - Name: "withMetadata", - Metadata: metadata, - Status: mfclients.EnabledStatus, - }, - err: nil, - }, - } - - for _, tc := range cases { - _, err := groupRepo.Save(context.Background(), tc.group) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - } - -} - -func TestGroupRetrieveByID(t *testing.T) { - t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) - groupRepo := gpostgres.NewRepository(database) - - uid := testsutil.GenerateUUID(t, idProvider) - group1 := mfgroups.Group{ - ID: testsutil.GenerateUUID(t, idProvider), - Name: groupName + "TestGroupRetrieveByID1", - Owner: uid, - Status: mfclients.EnabledStatus, - } - - _, err := groupRepo.Save(context.Background(), group1) - require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) - - retrieved, err := groupRepo.RetrieveByID(context.Background(), group1.ID) - require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) - assert.True(t, retrieved.ID == group1.ID, fmt.Sprintf("Save group, ID: expected %s got %s\n", group1.ID, retrieved.ID)) - - // Round to milliseconds as otherwise saving and retrieving from DB - // adds rounding error. - creationTime := time.Now().UTC().Round(time.Millisecond) - group2 := mfgroups.Group{ - ID: testsutil.GenerateUUID(t, idProvider), - Name: groupName + "TestGroupRetrieveByID", - Owner: uid, - Parent: group1.ID, - CreatedAt: creationTime, - UpdatedAt: creationTime, - Description: description, - Metadata: metadata, - Status: mfclients.EnabledStatus, - } - - _, err = groupRepo.Save(context.Background(), group2) - require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) - - retrieved, err = groupRepo.RetrieveByID(context.Background(), group2.ID) - require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err)) - assert.True(t, retrieved.ID == group2.ID, fmt.Sprintf("Save group, ID: expected %s got %s\n", group2.ID, retrieved.ID)) - assert.True(t, retrieved.CreatedAt.Equal(creationTime), fmt.Sprintf("Save group, CreatedAt: expected %s got %s\n", creationTime, retrieved.CreatedAt)) - assert.True(t, retrieved.UpdatedAt.Equal(creationTime), fmt.Sprintf("Save group, UpdatedAt: expected %s got %s\n", creationTime, retrieved.UpdatedAt)) - assert.True(t, retrieved.Parent == group1.ID, fmt.Sprintf("Save group, Level: expected %s got %s\n", group1.ID, retrieved.Parent)) - assert.True(t, retrieved.Description == description, fmt.Sprintf("Save group, Description: expected %v got %v\n", retrieved.Description, description)) - - retrieved, err = groupRepo.RetrieveByID(context.Background(), testsutil.GenerateUUID(t, idProvider)) - assert.True(t, errors.Contains(err, errors.ErrNotFound), fmt.Sprintf("Retrieve group: expected %s got %s\n", errors.ErrNotFound, err)) -} - -func TestGroupRetrieveAll(t *testing.T) { - t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) - groupRepo := gpostgres.NewRepository(database) - - var nGroups = uint64(200) - var ownerID = testsutil.GenerateUUID(t, idProvider) - var parentID string - for i := uint64(0); i < nGroups; i++ { - creationTime := time.Now().UTC() - group := mfgroups.Group{ - ID: testsutil.GenerateUUID(t, idProvider), - Name: fmt.Sprintf("%s-%d", groupName, i), - Description: fmt.Sprintf("%s-description-%d", groupName, i), - CreatedAt: creationTime, - UpdatedAt: creationTime, - Status: mfclients.EnabledStatus, - } - if i == 1 { - parentID = group.ID - } - if i%10 == 0 { - group.Owner = ownerID - group.Parent = parentID - } - if i%50 == 0 { - group.Status = mfclients.DisabledStatus - } - _, err := groupRepo.Save(context.Background(), group) - require.Nil(t, err, fmt.Sprintf("unexpected error: %s\n", err)) - parentID = group.ID - } - - cases := map[string]struct { - Size uint64 - Metadata mfgroups.GroupsPage - }{ - "retrieve all groups": { - Metadata: mfgroups.GroupsPage{ - Page: mfgroups.Page{ - Total: nGroups, - Limit: nGroups, - Status: mfclients.AllStatus, - }, - Level: maxLevel, - }, - Size: nGroups, - }, - "retrieve all groups with offset": { - Metadata: mfgroups.GroupsPage{ - Page: mfgroups.Page{ - Total: nGroups, - Offset: 50, - Limit: nGroups, - Status: mfclients.AllStatus, - }, - Level: maxLevel, - }, - Size: nGroups - 50, - }, - "retrieve all groups with limit": { - Metadata: mfgroups.GroupsPage{ - Page: mfgroups.Page{ - Total: nGroups, - Offset: 0, - Limit: 50, - Status: mfclients.AllStatus, - }, - Level: maxLevel, - }, - Size: 50, - }, - "retrieve all groups with offset and limit": { - Metadata: mfgroups.GroupsPage{ - Page: mfgroups.Page{ - Total: nGroups, - Offset: 50, - Limit: 50, - Status: mfclients.AllStatus, - }, - Level: maxLevel, - }, - Size: 50, - }, - "retrieve all groups with offset greater than limit": { - Metadata: mfgroups.GroupsPage{ - Page: mfgroups.Page{ - Total: nGroups, - Offset: 250, - Limit: nGroups, - Status: mfclients.AllStatus, - }, - Level: maxLevel, - }, - Size: 0, - }, - "retrieve all groups with owner id": { - Metadata: mfgroups.GroupsPage{ - Page: mfgroups.Page{ - Total: nGroups, - Limit: nGroups, - Subject: ownerID, - OwnerID: ownerID, - Status: mfclients.AllStatus, - }, - Level: maxLevel, - }, - Size: 20, - }, - } - - for desc, tc := range cases { - page, err := groupRepo.RetrieveAll(context.Background(), tc.Metadata) - size := len(page.Groups) - assert.Equal(t, tc.Size, uint64(size), fmt.Sprintf("%s: expected size %d got %d\n", desc, tc.Size, size)) - assert.Nil(t, err, fmt.Sprintf("%s: expected no error got %d\n", desc, err)) - } -} - -func TestGroupUpdate(t *testing.T) { - t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) - groupRepo := gpostgres.NewRepository(database) - - uid := testsutil.GenerateUUID(t, idProvider) - - creationTime := time.Now().UTC() - updateTime := time.Now().UTC() - groupID := testsutil.GenerateUUID(t, idProvider) - - group := mfgroups.Group{ - ID: groupID, - Name: groupName + "TestGroupUpdate", - Owner: uid, - CreatedAt: creationTime, - UpdatedAt: creationTime, - Description: description, - Metadata: metadata, - Status: mfclients.EnabledStatus, - } - updatedName := groupName + "Updated" - updatedMetadata := mfclients.Metadata{"admin": "false"} - updatedDescription := description + "updated" - _, err := groupRepo.Save(context.Background(), group) - require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err)) - - retrieved, err := groupRepo.RetrieveByID(context.Background(), group.ID) - require.Nil(t, err, fmt.Sprintf("group save got unexpected error: %s", err)) - - cases := []struct { - desc string - groupUpdate mfgroups.Group - groupExpected mfgroups.Group - err error - }{ - { - desc: "update group name for existing id", - groupUpdate: mfgroups.Group{ - ID: group.ID, - Name: updatedName, - UpdatedAt: updateTime, - Owner: uid, - }, - groupExpected: mfgroups.Group{ - Name: updatedName, - Metadata: retrieved.Metadata, - Description: retrieved.Description, - }, - err: nil, - }, - { - desc: "update group metadata for existing id", - groupUpdate: mfgroups.Group{ - ID: group.ID, - UpdatedAt: updateTime, - Metadata: updatedMetadata, - Owner: uid, - }, - groupExpected: mfgroups.Group{ - Name: updatedName, - UpdatedAt: updateTime, - Metadata: updatedMetadata, - Description: retrieved.Description, - }, - err: nil, - }, - { - desc: "update group description for existing id", - groupUpdate: mfgroups.Group{ - ID: group.ID, - UpdatedAt: updateTime, - Description: updatedDescription, - Owner: uid, - }, - groupExpected: mfgroups.Group{ - Name: updatedName, - Description: updatedDescription, - UpdatedAt: updateTime, - Metadata: updatedMetadata, - }, - err: nil, - }, - { - desc: "update group name and metadata for existing id", - groupUpdate: mfgroups.Group{ - ID: group.ID, - Name: updatedName, - UpdatedAt: updateTime, - Metadata: updatedMetadata, - Owner: uid, - }, - groupExpected: mfgroups.Group{ - Name: updatedName, - UpdatedAt: updateTime, - Metadata: updatedMetadata, - Description: updatedDescription, - }, - err: nil, - }, - { - desc: "update group for invalid name", - groupUpdate: mfgroups.Group{ - ID: group.ID, - Owner: uid, - Name: invalidName, - }, - err: errors.ErrMalformedEntity, - }, - { - desc: "update group for invalid description", - groupUpdate: mfgroups.Group{ - ID: group.ID, - Owner: uid, - Description: invalidDesc, - }, - err: errors.ErrMalformedEntity, - }, - } - - for _, tc := range cases { - updated, err := groupRepo.Update(context.Background(), tc.groupUpdate) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - if err == nil { - assert.True(t, updated.Name == tc.groupExpected.Name, fmt.Sprintf("%s:Name: expected %s got %s\n", tc.desc, tc.groupExpected.Name, updated.Name)) - assert.True(t, updated.Description == tc.groupExpected.Description, fmt.Sprintf("%s:Description: expected %s got %s\n", tc.desc, tc.groupExpected.Description, updated.Description)) - assert.True(t, updated.Metadata["admin"] == tc.groupExpected.Metadata["admin"], fmt.Sprintf("%s:Metadata: expected %d got %d\n", tc.desc, tc.groupExpected.Metadata["admin"], updated.Metadata["admin"])) - } - } -} - -func TestClientsMemberships(t *testing.T) { - t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) - crepo := cpostgres.NewRepository(database) - grepo := gpostgres.NewRepository(database) - prepo := ppostgres.NewRepository(database) - - clientA := mfclients.Client{ - ID: testsutil.GenerateUUID(t, idProvider), - Name: "client-memberships", - Credentials: mfclients.Credentials{ - Identity: "client-memberships1@example.com", - Secret: testsutil.GenerateUUID(t, idProvider), - }, - Metadata: mfclients.Metadata{}, - Status: mfclients.EnabledStatus, - } - clientB := mfclients.Client{ - ID: testsutil.GenerateUUID(t, idProvider), - Name: "client-memberships", - Credentials: mfclients.Credentials{ - Identity: "client-memberships2@example.com", - Secret: testsutil.GenerateUUID(t, idProvider), - }, - Metadata: mfclients.Metadata{}, - Status: mfclients.EnabledStatus, - } - group := mfgroups.Group{ - ID: testsutil.GenerateUUID(t, idProvider), - Name: "group-membership", - Metadata: mfclients.Metadata{}, - Status: mfclients.EnabledStatus, - } - - policyA := policies.Policy{ - Subject: clientA.ID, - Object: group.ID, - Actions: []string{"g_list"}, - } - policyB := policies.Policy{ - Subject: clientB.ID, - Object: group.ID, - Actions: []string{"g_list"}, - } - - _, err := crepo.Save(context.Background(), clientA) - assert.True(t, errors.Contains(err, nil), fmt.Sprintf("save client: expected %v got %s\n", nil, err)) - _, err = crepo.Save(context.Background(), clientB) - assert.True(t, errors.Contains(err, nil), fmt.Sprintf("save client: expected %v got %s\n", nil, err)) - _, err = grepo.Save(context.Background(), group) - assert.True(t, errors.Contains(err, nil), fmt.Sprintf("save group: expected %v got %s\n", nil, err)) - _, err = prepo.Save(context.Background(), policyA) - assert.True(t, errors.Contains(err, nil), fmt.Sprintf("save policy: expected %v got %s\n", nil, err)) - _, err = prepo.Save(context.Background(), policyB) - assert.True(t, errors.Contains(err, nil), fmt.Sprintf("save policy: expected %v got %s\n", nil, err)) - - cases := map[string]struct { - ID string - err error - }{ - "retrieve membership for existing client": {clientA.ID, nil}, - "retrieve membership for non-existing client": {wrongID, nil}, - } - - for desc, tc := range cases { - mp, err := grepo.Memberships(context.Background(), tc.ID, mfgroups.GroupsPage{Page: mfgroups.Page{Total: 10, Offset: 0, Limit: 10, Status: mfclients.AllStatus, Subject: clientB.ID, Action: "g_list"}}) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err)) - if tc.ID == clientA.ID { - assert.ElementsMatch(t, mp.Memberships, []mfgroups.Group{group}, fmt.Sprintf("%s: expected %v got %v\n", desc, []mfgroups.Group{group}, mp.Memberships)) - } - } -} - -func TestGroupChangeStatus(t *testing.T) { - t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) - repo := gpostgres.NewRepository(database) - - group1 := mfgroups.Group{ - ID: testsutil.GenerateUUID(t, idProvider), - Name: "active-group", - Status: mfclients.EnabledStatus, - } - group2 := mfgroups.Group{ - ID: testsutil.GenerateUUID(t, idProvider), - Name: "inactive-group", - Status: mfclients.DisabledStatus, - } - - group1, err := repo.Save(context.Background(), group1) - assert.True(t, errors.Contains(err, nil), fmt.Sprintf("add new group: expected %v got %s\n", nil, err)) - group2, err = repo.Save(context.Background(), group2) - assert.True(t, errors.Contains(err, nil), fmt.Sprintf("add new disabled group: expected %v got %s\n", nil, err)) - - cases := []struct { - desc string - group mfgroups.Group - err error - }{ - { - desc: "change group status for an active group", - group: mfgroups.Group{ - ID: group1.ID, - Status: mfclients.EnabledStatus, - }, - err: nil, - }, - { - desc: "change group status for a inactive group", - group: mfgroups.Group{ - ID: group2.ID, - Status: mfclients.EnabledStatus, - }, - err: nil, - }, - { - desc: "change group status for an invalid group", - group: mfgroups.Group{ - ID: "invalid", - Status: mfclients.DisabledStatus, - }, - err: errors.ErrNotFound, - }, - } - - for _, tc := range cases { - expected, err := repo.ChangeStatus(context.Background(), tc.group) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - if err == nil { - assert.Equal(t, tc.group.Status, expected.Status, fmt.Sprintf("%s: expected %d got %d\n", tc.desc, tc.group.Status, expected.Status)) - } - } -} diff --git a/things/policies/postgres/policies_test.go b/things/policies/postgres/policies_test.go index 96ea9a89..2b5ee446 100644 --- a/things/policies/postgres/policies_test.go +++ b/things/policies/postgres/policies_test.go @@ -12,9 +12,9 @@ import ( mfclients "github.com/mainflux/mainflux/pkg/clients" "github.com/mainflux/mainflux/pkg/errors" mfgroups "github.com/mainflux/mainflux/pkg/groups" + gpostgres "github.com/mainflux/mainflux/pkg/groups/postgres" "github.com/mainflux/mainflux/pkg/uuid" cpostgres "github.com/mainflux/mainflux/things/clients/postgres" - gpostgres "github.com/mainflux/mainflux/things/groups/postgres" "github.com/mainflux/mainflux/things/policies" ppostgres "github.com/mainflux/mainflux/things/policies/postgres" "github.com/stretchr/testify/assert" @@ -28,7 +28,7 @@ var ( func TestPoliciesSave(t *testing.T) { t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) repo := ppostgres.NewRepository(database) - grepo := gpostgres.NewRepository(database) + grepo := gpostgres.New(database) uid := testsutil.GenerateUUID(t, idProvider) @@ -80,7 +80,7 @@ func TestPoliciesEvaluate(t *testing.T) { t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) repo := ppostgres.NewRepository(database) crepo := cpostgres.NewRepository(database) - grepo := gpostgres.NewRepository(database) + grepo := gpostgres.New(database) client1 := mfclients.Client{ ID: testsutil.GenerateUUID(t, idProvider), @@ -167,7 +167,7 @@ func TestPoliciesRetrieve(t *testing.T) { t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) repo := ppostgres.NewRepository(database) crepo := cpostgres.NewRepository(database) - grepo := gpostgres.NewRepository(database) + grepo := gpostgres.New(database) uid := testsutil.GenerateUUID(t, idProvider) @@ -225,7 +225,7 @@ func TestPoliciesUpdate(t *testing.T) { t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) repo := ppostgres.NewRepository(database) crepo := cpostgres.NewRepository(database) - grepo := gpostgres.NewRepository(database) + grepo := gpostgres.New(database) client := mfclients.Client{ ID: testsutil.GenerateUUID(t, idProvider), @@ -347,7 +347,7 @@ func TestPoliciesRetrievalAll(t *testing.T) { t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) repo := ppostgres.NewRepository(database) crepo := cpostgres.NewRepository(database) - grepo := gpostgres.NewRepository(database) + grepo := gpostgres.New(database) var nPolicies = uint64(10) @@ -605,7 +605,7 @@ func TestPoliciesDelete(t *testing.T) { t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) repo := ppostgres.NewRepository(database) crepo := cpostgres.NewRepository(database) - grepo := gpostgres.NewRepository(database) + grepo := gpostgres.New(database) client := mfclients.Client{ ID: testsutil.GenerateUUID(t, idProvider), diff --git a/users/clients/mocks/clients.go b/users/clients/mocks/clients.go index fc065b24..1760939e 100644 --- a/users/clients/mocks/clients.go +++ b/users/clients/mocks/clients.go @@ -8,12 +8,13 @@ import ( mfclients "github.com/mainflux/mainflux/pkg/clients" "github.com/mainflux/mainflux/pkg/errors" + "github.com/mainflux/mainflux/users/clients/postgres" "github.com/stretchr/testify/mock" ) const WrongID = "wrongID" -var _ mfclients.Repository = (*Repository)(nil) +var _ postgres.Repository = (*Repository)(nil) type Repository struct { mock.Mock @@ -68,17 +69,16 @@ func (m *Repository) RetrieveByIdentity(ctx context.Context, identity string) (m return ret.Get(0).(mfclients.Client), ret.Error(1) } -func (m *Repository) Save(ctx context.Context, clients ...mfclients.Client) ([]mfclients.Client, error) { - client := clients[0] +func (m *Repository) Save(ctx context.Context, client mfclients.Client) (mfclients.Client, error) { ret := m.Called(ctx, client) if client.Owner == WrongID { - return []mfclients.Client{}, errors.ErrMalformedEntity + return mfclients.Client{}, errors.ErrMalformedEntity } if client.Credentials.Secret == "" { - return []mfclients.Client{}, errors.ErrMalformedEntity + return mfclients.Client{}, errors.ErrMalformedEntity } - return clients, ret.Error(1) + return client, ret.Error(1) } func (m *Repository) Update(ctx context.Context, client mfclients.Client) (mfclients.Client, error) { diff --git a/users/clients/postgres/clients.go b/users/clients/postgres/clients.go index 4ef48588..4f0d9b08 100644 --- a/users/clients/postgres/clients.go +++ b/users/clients/postgres/clients.go @@ -5,494 +5,62 @@ package postgres import ( "context" - "database/sql" - "encoding/json" - "fmt" - "strings" - "time" - "github.com/jackc/pgtype" // required for SQL access + // required for SQL access. + "github.com/mainflux/mainflux/internal/postgres" mfclients "github.com/mainflux/mainflux/pkg/clients" + pgclients "github.com/mainflux/mainflux/pkg/clients/postgres" "github.com/mainflux/mainflux/pkg/errors" - "github.com/mainflux/mainflux/pkg/groups" ) var _ mfclients.Repository = (*clientRepo)(nil) type clientRepo struct { - db postgres.Database + pgclients.ClientRepository +} + +type Repository interface { + mfclients.Repository + + // Save persists the client account. A non-nil error is returned to indicate + // operation failure. + Save(ctx context.Context, client mfclients.Client) (mfclients.Client, error) } // NewRepository instantiates a PostgreSQL // implementation of Clients repository. -func NewRepository(db postgres.Database) mfclients.Repository { +func NewRepository(db postgres.Database) Repository { return &clientRepo{ - db: db, + ClientRepository: pgclients.ClientRepository{DB: db}, } } -func (clientRepo) RetrieveBySecret(ctx context.Context, key string) (mfclients.Client, error) { - return mfclients.Client{}, nil -} - -func (repo clientRepo) Save(ctx context.Context, c ...mfclients.Client) ([]mfclients.Client, error) { +func (repo clientRepo) Save(ctx context.Context, c mfclients.Client) (mfclients.Client, error) { q := `INSERT INTO clients (id, name, tags, owner_id, identity, secret, metadata, created_at, status, role) VALUES (:id, :name, :tags, :owner_id, :identity, :secret, :metadata, :created_at, :status, :role) RETURNING id, name, tags, identity, metadata, COALESCE(owner_id, '') AS owner_id, status, created_at` - dbc, err := toDBClient(c[0]) + dbc, err := pgclients.ToDBClient(c) if err != nil { - return []mfclients.Client{}, errors.Wrap(errors.ErrCreateEntity, err) + return mfclients.Client{}, errors.Wrap(errors.ErrCreateEntity, err) } - row, err := repo.db.NamedQueryContext(ctx, q, dbc) + row, err := repo.ClientRepository.DB.NamedQueryContext(ctx, q, dbc) if err != nil { - return []mfclients.Client{}, postgres.HandleError(err, errors.ErrCreateEntity) + return mfclients.Client{}, postgres.HandleError(err, errors.ErrCreateEntity) } defer row.Close() row.Next() - dbc = dbClient{} - if err := row.StructScan(&dbc); err != nil { - return []mfclients.Client{}, err - } - - client, err := toClient(dbc) - if err != nil { - return []mfclients.Client{}, err - } - - return []mfclients.Client{client}, nil -} - -func (repo clientRepo) RetrieveByID(ctx context.Context, id string) (mfclients.Client, error) { - q := `SELECT id, name, tags, COALESCE(owner_id, '') AS owner_id, identity, secret, metadata, created_at, updated_at, updated_by, status - FROM clients WHERE id = :id` - - dbc := dbClient{ - ID: id, - } - - row, err := repo.db.NamedQueryContext(ctx, q, dbc) - if err != nil { - if err == sql.ErrNoRows { - return mfclients.Client{}, errors.Wrap(errors.ErrNotFound, err) - } - return mfclients.Client{}, errors.Wrap(errors.ErrViewEntity, err) - } - - defer row.Close() - row.Next() - dbc = dbClient{} - if err := row.StructScan(&dbc); err != nil { - return mfclients.Client{}, errors.Wrap(errors.ErrNotFound, err) - } - - return toClient(dbc) -} - -func (repo clientRepo) RetrieveByIdentity(ctx context.Context, identity string) (mfclients.Client, error) { - q := `SELECT id, name, tags, COALESCE(owner_id, '') AS owner_id, identity, secret, metadata, created_at, updated_at, updated_by, status - FROM clients WHERE identity = :identity AND status = :status` - - dbc := dbClient{ - Identity: identity, - Status: mfclients.EnabledStatus, - } - - row, err := repo.db.NamedQueryContext(ctx, q, dbc) - if err != nil { - if err == sql.ErrNoRows { - return mfclients.Client{}, errors.Wrap(errors.ErrNotFound, err) - } - return mfclients.Client{}, errors.Wrap(errors.ErrViewEntity, err) - } - - defer row.Close() - row.Next() - dbc = dbClient{} - if err := row.StructScan(&dbc); err != nil { - return mfclients.Client{}, errors.Wrap(errors.ErrNotFound, err) - } - - return toClient(dbc) -} - -func (repo clientRepo) RetrieveAll(ctx context.Context, pm mfclients.Page) (mfclients.ClientsPage, error) { - query, err := pageQuery(pm) - if err != nil { - return mfclients.ClientsPage{}, errors.Wrap(errors.ErrViewEntity, err) - } - - q := fmt.Sprintf(`SELECT c.id, c.name, c.tags, c.identity, c.metadata, COALESCE(c.owner_id, '') AS owner_id, c.status, - c.created_at, c.updated_at, COALESCE(c.updated_by, '') AS updated_by FROM clients c %s ORDER BY c.created_at LIMIT :limit OFFSET :offset;`, query) - - dbPage, err := toDBClientsPage(pm) - if err != nil { - return mfclients.ClientsPage{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) - } - rows, err := repo.db.NamedQueryContext(ctx, q, dbPage) - if err != nil { - return mfclients.ClientsPage{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) - } - defer rows.Close() - - var items []mfclients.Client - for rows.Next() { - dbc := dbClient{} - if err := rows.StructScan(&dbc); err != nil { - return mfclients.ClientsPage{}, errors.Wrap(errors.ErrViewEntity, err) - } - - c, err := toClient(dbc) - if err != nil { - return mfclients.ClientsPage{}, err - } - - items = append(items, c) - } - cq := fmt.Sprintf(`SELECT COUNT(*) FROM clients c %s;`, query) - - total, err := postgres.Total(ctx, repo.db, cq, dbPage) - if err != nil { - return mfclients.ClientsPage{}, errors.Wrap(errors.ErrViewEntity, err) - } - - page := mfclients.ClientsPage{ - Clients: items, - Page: mfclients.Page{ - Total: total, - Offset: pm.Offset, - Limit: pm.Limit, - }, - } - - return page, nil -} - -func (repo clientRepo) Members(ctx context.Context, groupID string, pm mfclients.Page) (mfclients.MembersPage, error) { - emq, err := pageQuery(pm) - if err != nil { - return mfclients.MembersPage{}, err - } - - aq := "" - // If not admin, the client needs to have a g_list action on the group or they are the owner. - if pm.Subject != "" { - aq = `AND (EXISTS (SELECT 1 FROM policies p WHERE p.subject = :subject AND :action=ANY(actions)) - OR EXISTS (SELECT 1 FROM groups g WHERE g.owner_id = :subject AND g.id = :group_id)) - AND c.id != :subject` - } - q := fmt.Sprintf(`SELECT c.id, c.name, c.tags, c.metadata, c.identity, c.status, - c.created_at, c.updated_at FROM clients c - INNER JOIN policies ON c.id=policies.subject %s AND policies.object = :group_id %s - ORDER BY c.created_at LIMIT :limit OFFSET :offset;`, emq, aq) - dbPage, err := toDBClientsPage(pm) - if err != nil { - return mfclients.MembersPage{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) - } - dbPage.GroupID = groupID - rows, err := repo.db.NamedQueryContext(ctx, q, dbPage) - if err != nil { - return mfclients.MembersPage{}, errors.Wrap(postgres.ErrFailedToRetrieveMembers, err) - } - defer rows.Close() - - var items []mfclients.Client - for rows.Next() { - dbc := dbClient{} - if err := rows.StructScan(&dbc); err != nil { - return mfclients.MembersPage{}, errors.Wrap(postgres.ErrFailedToRetrieveMembers, err) - } - - c, err := toClient(dbc) - if err != nil { - return mfclients.MembersPage{}, err - } - - items = append(items, c) - } - cq := fmt.Sprintf(`SELECT COUNT(*) FROM clients c INNER JOIN policies ON c.id=policies.subject %s AND policies.object = :group_id`, emq) - if pm.Subject != "" { - cq = fmt.Sprintf("%s AND c.id != :subject", cq) - } - - total, err := postgres.Total(ctx, repo.db, cq, dbPage) - if err != nil { - return mfclients.MembersPage{}, errors.Wrap(postgres.ErrFailedToRetrieveMembers, err) - } - - page := mfclients.MembersPage{ - Members: items, - Page: mfclients.Page{ - Total: total, - Offset: pm.Offset, - Limit: pm.Limit, - }, - } - return page, nil -} - -func (repo clientRepo) Update(ctx context.Context, client mfclients.Client) (mfclients.Client, error) { - var query []string - var upq string - if client.Name != "" { - query = append(query, "name = :name,") - } - if client.Metadata != nil { - query = append(query, "metadata = :metadata,") - } - if len(query) > 0 { - upq = strings.Join(query, " ") - } - client.Status = mfclients.EnabledStatus - q := fmt.Sprintf(`UPDATE clients SET %s updated_at = :updated_at, updated_by = :updated_by - WHERE id = :id AND status = :status - RETURNING id, name, tags, identity, metadata, COALESCE(owner_id, '') AS owner_id, status, created_at, updated_at, updated_by`, - upq) - - return repo.update(ctx, client, q) -} - -func (repo clientRepo) UpdateTags(ctx context.Context, client mfclients.Client) (mfclients.Client, error) { - client.Status = mfclients.EnabledStatus - q := `UPDATE clients SET tags = :tags, updated_at = :updated_at, updated_by = :updated_by - WHERE id = :id AND status = :status - RETURNING id, name, tags, identity, metadata, COALESCE(owner_id, '') AS owner_id, status, created_at, updated_at, updated_by` - - return repo.update(ctx, client, q) -} - -func (repo clientRepo) UpdateIdentity(ctx context.Context, client mfclients.Client) (mfclients.Client, error) { - q := `UPDATE clients SET identity = :identity, updated_at = :updated_at, updated_by = :updated_by - WHERE id = :id AND status = :status - RETURNING id, name, tags, identity, metadata, COALESCE(owner_id, '') AS owner_id, status, created_at, updated_at, updated_by` - - return repo.update(ctx, client, q) -} - -func (repo clientRepo) UpdateSecret(ctx context.Context, client mfclients.Client) (mfclients.Client, error) { - q := `UPDATE clients SET secret = :secret, updated_at = :updated_at, updated_by = :updated_by - WHERE identity = :identity AND status = :status - RETURNING id, name, tags, identity, metadata, COALESCE(owner_id, '') AS owner_id, status, created_at, updated_at, updated_by` - - return repo.update(ctx, client, q) -} - -func (repo clientRepo) UpdateOwner(ctx context.Context, client mfclients.Client) (mfclients.Client, error) { - q := `UPDATE clients SET owner_id = :owner_id, updated_at = :updated_at, updated_by = :updated_by - WHERE id = :id AND status = :status - RETURNING id, name, tags, identity, metadata, COALESCE(owner_id, '') AS owner_id, status, created_at, updated_at, updated_by` - - return repo.update(ctx, client, q) -} - -func (repo clientRepo) ChangeStatus(ctx context.Context, client mfclients.Client) (mfclients.Client, error) { - q := `UPDATE clients SET status = :status WHERE id = :id - RETURNING id, name, tags, identity, metadata, COALESCE(owner_id, '') AS owner_id, status, created_at, updated_at, updated_by` - - return repo.update(ctx, client, q) -} - -func (repo clientRepo) update(ctx context.Context, client mfclients.Client, query string) (mfclients.Client, error) { - dbc, err := toDBClient(client) - if err != nil { - return mfclients.Client{}, errors.Wrap(errors.ErrUpdateEntity, err) - } - row, err := repo.db.NamedQueryContext(ctx, query, dbc) - if err != nil { - return mfclients.Client{}, postgres.HandleError(err, errors.ErrUpdateEntity) - } - - defer row.Close() - if ok := row.Next(); !ok { - return mfclients.Client{}, errors.Wrap(errors.ErrNotFound, row.Err()) - } - dbc = dbClient{} + dbc = pgclients.DBClient{} if err := row.StructScan(&dbc); err != nil { return mfclients.Client{}, err } - return toClient(dbc) -} - -type dbClient struct { - ID string `db:"id"` - Name string `db:"name,omitempty"` - Tags pgtype.TextArray `db:"tags,omitempty"` - Identity string `db:"identity"` - Owner *string `db:"owner_id,omitempty"` // nullable - Secret string `db:"secret"` - Metadata []byte `db:"metadata,omitempty"` - CreatedAt time.Time `db:"created_at"` - UpdatedAt sql.NullTime `db:"updated_at,omitempty"` - UpdatedBy *string `db:"updated_by,omitempty"` - Groups []groups.Group `db:"groups,omitempty"` - Status mfclients.Status `db:"status"` - Role mfclients.Role `db:"role"` -} - -func toDBClient(c mfclients.Client) (dbClient, error) { - data := []byte("{}") - if len(c.Metadata) > 0 { - b, err := json.Marshal(c.Metadata) - if err != nil { - return dbClient{}, errors.Wrap(errors.ErrMalformedEntity, err) - } - data = b - } - var tags pgtype.TextArray - if err := tags.Set(c.Tags); err != nil { - return dbClient{}, err - } - var owner *string - if c.Owner != "" { - owner = &c.Owner - } - var updatedBy *string - if c.UpdatedBy != "" { - updatedBy = &c.UpdatedBy - } - var updatedAt sql.NullTime - if !c.UpdatedAt.IsZero() { - updatedAt = sql.NullTime{Time: c.UpdatedAt, Valid: true} - } - - return dbClient{ - ID: c.ID, - Name: c.Name, - Tags: tags, - Owner: owner, - Identity: c.Credentials.Identity, - Secret: c.Credentials.Secret, - Metadata: data, - CreatedAt: c.CreatedAt, - UpdatedAt: updatedAt, - UpdatedBy: updatedBy, - Status: c.Status, - Role: c.Role, - }, nil -} - -func toClient(c dbClient) (mfclients.Client, error) { - var metadata mfclients.Metadata - if c.Metadata != nil { - if err := json.Unmarshal([]byte(c.Metadata), &metadata); err != nil { - return mfclients.Client{}, errors.Wrap(errors.ErrMalformedEntity, err) - } - } - var tags []string - for _, e := range c.Tags.Elements { - tags = append(tags, e.String) - } - var owner string - if c.Owner != nil { - owner = *c.Owner - } - var updatedBy string - if c.UpdatedBy != nil { - updatedBy = *c.UpdatedBy - } - var updatedAt time.Time - if c.UpdatedAt.Valid { - updatedAt = c.UpdatedAt.Time - } - - return mfclients.Client{ - ID: c.ID, - Name: c.Name, - Tags: tags, - Owner: owner, - Credentials: mfclients.Credentials{ - Identity: c.Identity, - Secret: c.Secret, - }, - Metadata: metadata, - CreatedAt: c.CreatedAt, - UpdatedAt: updatedAt, - UpdatedBy: updatedBy, - Status: c.Status, - }, nil -} - -func pageQuery(pm mfclients.Page) (string, error) { - mq, _, err := postgres.CreateMetadataQuery("", pm.Metadata) + client, err := pgclients.ToClient(dbc) if err != nil { - return "", errors.Wrap(errors.ErrViewEntity, err) - } - var query []string - var emq string - if mq != "" { - query = append(query, mq) - } - if pm.Identity != "" { - query = append(query, "c.identity = :identity") - } - if pm.Name != "" { - query = append(query, "c.name = :name") - } - if pm.Tag != "" { - query = append(query, ":tag = ANY(c.tags)") - } - if pm.Status != mfclients.AllStatus { - query = append(query, "c.status = :status") - } - // For listing clients that the specified client owns but not sharedby - if pm.Owner != "" && pm.SharedBy == "" { - query = append(query, "c.owner_id = :owner_id") + return mfclients.Client{}, err } - // For listing clients that the specified client owns and that are shared with the specified client - if pm.Owner != "" && pm.SharedBy != "" { - query = append(query, "(c.owner_id = :owner_id OR (policies.object IN (SELECT object FROM policies WHERE subject = :shared_by AND :action=ANY(actions)))) AND c.id != :shared_by") - } - // For listing clients that the specified client is shared with - if pm.SharedBy != "" && pm.Owner == "" { - query = append(query, "c.owner_id != :shared_by AND (policies.object IN (SELECT object FROM policies WHERE subject = :shared_by AND :action=ANY(actions))) AND c.id != :shared_by") - } - if len(query) > 0 { - emq = fmt.Sprintf("WHERE %s", strings.Join(query, " AND ")) - if strings.Contains(emq, "policies") { - emq = fmt.Sprintf("LEFT JOIN policies ON policies.subject = c.id %s", emq) - } - } - return emq, nil - -} - -func toDBClientsPage(pm mfclients.Page) (dbClientsPage, error) { - _, data, err := postgres.CreateMetadataQuery("", pm.Metadata) - if err != nil { - return dbClientsPage{}, errors.Wrap(errors.ErrViewEntity, err) - } - return dbClientsPage{ - Name: pm.Name, - Identity: pm.Identity, - Metadata: data, - Owner: pm.Owner, - Total: pm.Total, - Offset: pm.Offset, - Limit: pm.Limit, - Status: pm.Status, - Tag: pm.Tag, - Subject: pm.Subject, - Action: pm.Action, - SharedBy: pm.SharedBy, - }, nil -} - -type dbClientsPage struct { - Total uint64 `db:"total"` - Limit uint64 `db:"limit"` - Offset uint64 `db:"offset"` - Name string `db:"name"` - Owner string `db:"owner_id"` - Identity string `db:"identity"` - Metadata []byte `db:"metadata"` - Tag string `db:"tag"` - Status mfclients.Status `db:"status"` - GroupID string `db:"group_id"` - SharedBy string `db:"shared_by"` - Subject string `db:"subject"` - Action string `db:"action"` + return client, nil } diff --git a/users/clients/postgres/clients_test.go b/users/clients/postgres/clients_test.go index 0c81a244..9da5bfe5 100644 --- a/users/clients/postgres/clients_test.go +++ b/users/clients/postgres/clients_test.go @@ -12,14 +12,9 @@ import ( "github.com/mainflux/mainflux/internal/testsutil" mfclients "github.com/mainflux/mainflux/pkg/clients" "github.com/mainflux/mainflux/pkg/errors" - mfgroups "github.com/mainflux/mainflux/pkg/groups" "github.com/mainflux/mainflux/pkg/uuid" cpostgres "github.com/mainflux/mainflux/users/clients/postgres" - gpostgres "github.com/mainflux/mainflux/users/groups/postgres" - "github.com/mainflux/mainflux/users/policies" - ppostgres "github.com/mainflux/mainflux/users/policies/postgres" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) const ( @@ -32,8 +27,6 @@ var ( password = "$tr0ngPassw0rd" clientIdentity = "client-identity@example.com" clientName = "client name" - wrongName = "wrong-name" - wrongID = "wrong-id" ) func TestClientsSave(t *testing.T) { @@ -175,948 +168,8 @@ func TestClientsSave(t *testing.T) { rClient, err := repo.Save(context.Background(), tc.client) assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) if err == nil { - rClient[0].Credentials.Secret = tc.client.Credentials.Secret - assert.Equal(t, tc.client, rClient[0], fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.client, rClient)) - } - } -} - -func TestClientsRetrieveByID(t *testing.T) { - t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) - repo := cpostgres.NewRepository(database) - - client := mfclients.Client{ - ID: testsutil.GenerateUUID(t, idProvider), - Name: clientName, - Credentials: mfclients.Credentials{ - Identity: clientIdentity, - Secret: password, - }, - Status: mfclients.EnabledStatus, - } - - clients, err := repo.Save(context.Background(), client) - require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - client = clients[0] - - cases := map[string]struct { - ID string - err error - }{ - "retrieve existing client": {client.ID, nil}, - "retrieve non-existing client": {wrongID, errors.ErrNotFound}, - } - - for desc, tc := range cases { - cli, err := repo.RetrieveByID(context.Background(), tc.ID) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err)) - if err == nil { - assert.Equal(t, client.ID, cli.ID, fmt.Sprintf("retrieve client by ID : client ID : expected %s got %s\n", client.ID, cli.ID)) - assert.Equal(t, client.Name, cli.Name, fmt.Sprintf("retrieve client by ID : client Name : expected %s got %s\n", client.Name, cli.Name)) - assert.Equal(t, client.Credentials.Identity, cli.Credentials.Identity, fmt.Sprintf("retrieve client by ID : client Identity : expected %s got %s\n", client.Credentials.Identity, cli.Credentials.Identity)) - assert.Equal(t, client.Status, cli.Status, fmt.Sprintf("retrieve client by ID : client Status : expected %d got %d\n", client.Status, cli.Status)) - } - } -} - -func TestClientsRetrieveByIdentity(t *testing.T) { - t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) - repo := cpostgres.NewRepository(database) - - client := mfclients.Client{ - ID: testsutil.GenerateUUID(t, idProvider), - Name: clientName, - Credentials: mfclients.Credentials{ - Identity: clientIdentity, - Secret: password, - }, - Status: mfclients.EnabledStatus, - } - - _, err := repo.Save(context.Background(), client) - require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - - cases := map[string]struct { - identity string - err error - }{ - "retrieve existing client": {clientIdentity, nil}, - "retrieve non-existing client": {wrongID, errors.ErrNotFound}, - } - - for desc, tc := range cases { - _, err := repo.RetrieveByIdentity(context.Background(), tc.identity) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err)) - } -} - -func TestClientsRetrieveAll(t *testing.T) { - t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) - repo := cpostgres.NewRepository(database) - grepo := gpostgres.NewRepository(database) - prepo := ppostgres.NewRepository(database) - - var nClients = uint64(200) - var ownerID string - - meta := mfclients.Metadata{ - "admin": "true", - } - wrongMeta := mfclients.Metadata{ - "admin": "false", - } - var expectedClients = []mfclients.Client{} - - var sharedGroup = mfgroups.Group{ - ID: testsutil.GenerateUUID(t, idProvider), - Name: "shared-group", - } - _, err := grepo.Save(context.Background(), sharedGroup) - require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - - for i := uint64(0); i < nClients; i++ { - identity := fmt.Sprintf("TestRetrieveAll%d@example.com", i) - client := mfclients.Client{ - ID: testsutil.GenerateUUID(t, idProvider), - Name: identity, - Credentials: mfclients.Credentials{ - Identity: identity, - Secret: password, - }, - Metadata: mfclients.Metadata{}, - Status: mfclients.EnabledStatus, - } - if i == 1 { - ownerID = client.ID - } - if i%10 == 0 { - client.Owner = ownerID - client.Metadata = meta - client.Tags = []string{"Test"} - } - if i%50 == 0 { - client.Status = mfclients.DisabledStatus - } - _, err := repo.Save(context.Background(), client) - require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - client.Credentials.Secret = "" - expectedClients = append(expectedClients, client) - var policy = policies.Policy{ - Subject: client.ID, - Object: sharedGroup.ID, - Actions: []string{"c_list"}, - } - err = prepo.Save(context.Background(), policy) - require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - } - - cases := map[string]struct { - size uint64 - pm mfclients.Page - response []mfclients.Client - }{ - "retrieve all clients empty page": { - pm: mfclients.Page{}, - response: []mfclients.Client{}, - size: 0, - }, - "retrieve all clients": { - pm: mfclients.Page{ - Offset: 0, - Limit: nClients, - Status: mfclients.AllStatus, - }, - response: expectedClients, - size: 200, - }, - "retrieve all clients with limit": { - pm: mfclients.Page{ - Offset: 0, - Limit: 50, - Status: mfclients.AllStatus, - }, - response: expectedClients[0:50], - size: 50, - }, - "retrieve all clients with offset": { - pm: mfclients.Page{ - Offset: 50, - Limit: nClients, - Status: mfclients.AllStatus, - }, - response: expectedClients[50:200], - size: 150, - }, - "retrieve all clients with limit and offset": { - pm: mfclients.Page{ - Offset: 50, - Limit: 50, - Status: mfclients.AllStatus, - }, - response: expectedClients[50:100], - size: 50, - }, - "retrieve all clients with limit and offset not full": { - pm: mfclients.Page{ - Offset: 170, - Limit: 50, - Status: mfclients.AllStatus, - }, - response: expectedClients[170:200], - size: 30, - }, - "retrieve all clients by metadata": { - pm: mfclients.Page{ - Offset: 0, - Limit: nClients, - Total: nClients, - Metadata: meta, - Status: mfclients.AllStatus, - }, - response: []mfclients.Client{expectedClients[0], expectedClients[10], expectedClients[20], expectedClients[30], expectedClients[40], expectedClients[50], expectedClients[60], - expectedClients[70], expectedClients[80], expectedClients[90], expectedClients[100], expectedClients[110], expectedClients[120], expectedClients[130], - expectedClients[140], expectedClients[150], expectedClients[160], expectedClients[170], expectedClients[180], expectedClients[190], - }, - size: 20, - }, - "retrieve clients by wrong metadata": { - pm: mfclients.Page{ - Offset: 0, - Limit: nClients, - Total: nClients, - Metadata: wrongMeta, - Status: mfclients.AllStatus, - }, - response: []mfclients.Client{}, - size: 0, - }, - "retrieve all clients by name": { - pm: mfclients.Page{ - Offset: 0, - Limit: nClients, - Total: nClients, - Name: "TestRetrieveAll3@example.com", - Status: mfclients.AllStatus, - }, - response: []mfclients.Client{expectedClients[3]}, - size: 1, - }, - "retrieve clients by wrong name": { - pm: mfclients.Page{ - Offset: 0, - Limit: nClients, - Total: nClients, - Name: wrongName, - Status: mfclients.AllStatus, - }, - response: []mfclients.Client{}, - size: 0, - }, - "retrieve all clients by owner": { - pm: mfclients.Page{ - Offset: 0, - Limit: nClients, - Total: nClients, - Owner: ownerID, - Status: mfclients.AllStatus, - }, - response: []mfclients.Client{expectedClients[10], expectedClients[20], expectedClients[30], expectedClients[40], expectedClients[50], expectedClients[60], - expectedClients[70], expectedClients[80], expectedClients[90], expectedClients[100], expectedClients[110], expectedClients[120], expectedClients[130], - expectedClients[140], expectedClients[150], expectedClients[160], expectedClients[170], expectedClients[180], expectedClients[190], - }, - size: 19, - }, - "retrieve clients by wrong owner": { - pm: mfclients.Page{ - Offset: 0, - Limit: nClients, - Total: nClients, - Owner: wrongID, - Status: mfclients.AllStatus, - }, - response: []mfclients.Client{}, - size: 0, - }, - "retrieve all clients by disabled status": { - pm: mfclients.Page{ - Offset: 0, - Limit: nClients, - Total: nClients, - Status: mfclients.DisabledStatus, - }, - response: []mfclients.Client{expectedClients[0], expectedClients[50], expectedClients[100], expectedClients[150]}, - size: 4, - }, - "retrieve all clients by combined status": { - pm: mfclients.Page{ - Offset: 0, - Limit: nClients, - Total: nClients, - Status: mfclients.AllStatus, - }, - response: expectedClients, - size: 200, - }, - "retrieve clients by the wrong status": { - pm: mfclients.Page{ - Offset: 0, - Limit: nClients, - Total: nClients, - Status: 10, - }, - response: []mfclients.Client{}, - size: 0, - }, - "retrieve all clients by tags": { - pm: mfclients.Page{ - Offset: 0, - Limit: nClients, - Total: nClients, - Tag: "Test", - Status: mfclients.AllStatus, - }, - response: []mfclients.Client{expectedClients[0], expectedClients[10], expectedClients[20], expectedClients[30], expectedClients[40], expectedClients[50], expectedClients[60], - expectedClients[70], expectedClients[80], expectedClients[90], expectedClients[100], expectedClients[110], expectedClients[120], expectedClients[130], - expectedClients[140], expectedClients[150], expectedClients[160], expectedClients[170], expectedClients[180], expectedClients[190], - }, - size: 20, - }, - "retrieve clients by wrong tags": { - pm: mfclients.Page{ - Offset: 0, - Limit: nClients, - Total: nClients, - Tag: "wrongTags", - Status: mfclients.AllStatus, - }, - response: []mfclients.Client{}, - size: 0, - }, - "retrieve all clients by sharedby": { - pm: mfclients.Page{ - Offset: 0, - Limit: nClients, - Total: nClients, - SharedBy: expectedClients[0].ID, - Status: mfclients.AllStatus, - Action: "c_list", - }, - response: []mfclients.Client{expectedClients[10], expectedClients[20], expectedClients[30], expectedClients[40], expectedClients[50], expectedClients[60], - expectedClients[70], expectedClients[80], expectedClients[90], expectedClients[100], expectedClients[110], expectedClients[120], expectedClients[130], - expectedClients[140], expectedClients[150], expectedClients[160], expectedClients[170], expectedClients[180], expectedClients[190], - }, - size: 19, - }, - } - for desc, tc := range cases { - page, err := repo.RetrieveAll(context.Background(), tc.pm) - size := uint64(len(page.Clients)) - assert.ElementsMatch(t, page.Clients, tc.response, fmt.Sprintf("%s: expected %v got %v\n", desc, tc.response, page.Clients)) - assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected size %d got %d\n", desc, tc.size, size)) - assert.Nil(t, err, fmt.Sprintf("%s: expected no error got %d\n", desc, err)) - } -} - -func TestGroupsMembers(t *testing.T) { - t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) - crepo := cpostgres.NewRepository(database) - grepo := gpostgres.NewRepository(database) - prepo := ppostgres.NewRepository(database) - - clientA := mfclients.Client{ - ID: testsutil.GenerateUUID(t, idProvider), - Name: "client-memberships", - Credentials: mfclients.Credentials{ - Identity: "client-memberships1@example.com", - Secret: password, - }, - Metadata: mfclients.Metadata{}, - Status: mfclients.EnabledStatus, - } - clientB := mfclients.Client{ - ID: testsutil.GenerateUUID(t, idProvider), - Name: "client-memberships", - Credentials: mfclients.Credentials{ - Identity: "client-memberships2@example.com", - Secret: password, - }, - Metadata: mfclients.Metadata{}, - Status: mfclients.EnabledStatus, - } - group := mfgroups.Group{ - ID: testsutil.GenerateUUID(t, idProvider), - Name: "group-membership", - Metadata: mfclients.Metadata{}, - Status: mfclients.EnabledStatus, - } - - policyA := policies.Policy{ - Subject: clientA.ID, - Object: group.ID, - Actions: []string{"g_list"}, - } - policyB := policies.Policy{ - Subject: clientB.ID, - Object: group.ID, - Actions: []string{"g_list"}, - } - - _, err := crepo.Save(context.Background(), clientA) - assert.True(t, errors.Contains(err, nil), fmt.Sprintf("save client: expected %v got %s\n", nil, err)) - clientA.Credentials.Secret = "" - _, err = crepo.Save(context.Background(), clientB) - assert.True(t, errors.Contains(err, nil), fmt.Sprintf("save client: expected %v got %s\n", nil, err)) - clientB.Credentials.Secret = "" - _, err = grepo.Save(context.Background(), group) - assert.True(t, errors.Contains(err, nil), fmt.Sprintf("save group: expected %v got %s\n", nil, err)) - err = prepo.Save(context.Background(), policyA) - assert.True(t, errors.Contains(err, nil), fmt.Sprintf("save policy: expected %v got %s\n", nil, err)) - err = prepo.Save(context.Background(), policyB) - assert.True(t, errors.Contains(err, nil), fmt.Sprintf("save policy: expected %v got %s\n", nil, err)) - - cases := map[string]struct { - ID string - err error - }{ - "retrieve members for existing group": {group.ID, nil}, - "retrieve members for non-existing group": {wrongID, nil}, - } - - for desc, tc := range cases { - mp, err := crepo.Members(context.Background(), tc.ID, mfclients.Page{Total: 10, Offset: 0, Limit: 10, Status: mfclients.AllStatus, Subject: clientB.ID, Action: "g_list"}) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err)) - if tc.ID == group.ID { - assert.ElementsMatch(t, mp.Members, []mfclients.Client{clientA}, fmt.Sprintf("%s: expected %v got %v\n", desc, []mfclients.Client{clientA, clientB}, mp.Members)) - } - } -} - -func TestClientsUpdateMetadata(t *testing.T) { - t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) - repo := cpostgres.NewRepository(database) - - client1 := mfclients.Client{ - ID: testsutil.GenerateUUID(t, idProvider), - Name: "enabled-client", - Credentials: mfclients.Credentials{ - Identity: "client1-update@example.com", - Secret: password, - }, - Metadata: mfclients.Metadata{ - "name": "enabled-client", - }, - Tags: []string{"enabled", "tag1"}, - Status: mfclients.EnabledStatus, - } - - client2 := mfclients.Client{ - ID: testsutil.GenerateUUID(t, idProvider), - Name: "disabled-client", - Credentials: mfclients.Credentials{ - Identity: "client2-update@example.com", - Secret: password, - }, - Metadata: mfclients.Metadata{ - "name": "disabled-client", - }, - Tags: []string{"disabled", "tag1"}, - Status: mfclients.DisabledStatus, - } - - clients1, err := repo.Save(context.Background(), client1) - assert.True(t, errors.Contains(err, nil), fmt.Sprintf("add new client with metadata: expected %v got %s\n", nil, err)) - clients2, err := repo.Save(context.Background(), client2) - assert.True(t, errors.Contains(err, nil), fmt.Sprintf("add new disabled client: expected %v got %s\n", nil, err)) - client1 = clients1[0] - client2 = clients2[0] - - ucases := []struct { - desc string - update string - client mfclients.Client - err error - }{ - { - desc: "update metadata for enabled client", - update: "metadata", - client: mfclients.Client{ - ID: client1.ID, - Metadata: mfclients.Metadata{ - "update": "metadata", - }, - }, - err: nil, - }, - { - desc: "update metadata for disabled client", - update: "metadata", - client: mfclients.Client{ - ID: client2.ID, - Metadata: mfclients.Metadata{ - "update": "metadata", - }, - }, - err: errors.ErrNotFound, - }, - { - desc: "update name for enabled client", - update: "name", - client: mfclients.Client{ - ID: client1.ID, - Name: "updated name", - }, - err: nil, - }, - { - desc: "update name for disabled client", - update: "name", - client: mfclients.Client{ - ID: client2.ID, - Name: "updated name", - }, - err: errors.ErrNotFound, - }, - { - desc: "update name and metadata for enabled client", - update: "both", - client: mfclients.Client{ - ID: client1.ID, - Name: "updated name and metadata", - Metadata: mfclients.Metadata{ - "update": "name and metadata", - }, - }, - err: nil, - }, - { - desc: "update name and metadata for a disabled client", - update: "both", - client: mfclients.Client{ - ID: client2.ID, - Name: "updated name and metadata", - Metadata: mfclients.Metadata{ - "update": "name and metadata", - }, - }, - err: errors.ErrNotFound, - }, - { - desc: "update metadata for invalid client", - update: "metadata", - client: mfclients.Client{ - ID: wrongID, - Metadata: mfclients.Metadata{ - "update": "metadata", - }, - }, - err: errors.ErrNotFound, - }, - { - desc: "update name for invalid client", - update: "name", - client: mfclients.Client{ - ID: wrongID, - Name: "updated name", - }, - err: errors.ErrNotFound, - }, - { - desc: "update name and metadata for invalid client", - update: "both", - client: mfclients.Client{ - ID: client2.ID, - Name: "updated name and metadata", - Metadata: mfclients.Metadata{ - "update": "name and metadata", - }, - }, - err: errors.ErrNotFound, - }, - } - for _, tc := range ucases { - expected, err := repo.Update(context.Background(), tc.client) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - if err == nil { - if tc.client.Name != "" { - assert.Equal(t, expected.Name, tc.client.Name, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, expected.Name, tc.client.Name)) - } - if tc.client.Metadata != nil { - assert.Equal(t, expected.Metadata, tc.client.Metadata, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, expected.Metadata, tc.client.Metadata)) - } - - } - } -} - -func TestClientsUpdateTags(t *testing.T) { - t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) - repo := cpostgres.NewRepository(database) - - client1 := mfclients.Client{ - ID: testsutil.GenerateUUID(t, idProvider), - Name: "enabled-client-with-tags", - Credentials: mfclients.Credentials{ - Identity: "client1-update-tags@example.com", - Secret: password, - }, - Tags: []string{"test", "enabled"}, - Status: mfclients.EnabledStatus, - } - client2 := mfclients.Client{ - ID: testsutil.GenerateUUID(t, idProvider), - Name: "disabled-client-with-tags", - Credentials: mfclients.Credentials{ - Identity: "client2-update-tags@example.com", - Secret: password, - }, - Tags: []string{"test", "disabled"}, - Status: mfclients.DisabledStatus, - } - - clients1, err := repo.Save(context.Background(), client1) - assert.True(t, errors.Contains(err, nil), fmt.Sprintf("add new client with tags: expected %v got %s\n", nil, err)) - if err == nil { - assert.Equal(t, client1.ID, client1.ID, fmt.Sprintf("add new client with tags: expected %v got %s\n", nil, err)) - } - client1 = clients1[0] - clients2, err := repo.Save(context.Background(), client2) - assert.True(t, errors.Contains(err, nil), fmt.Sprintf("add new disabled client with tags: expected %v got %s\n", nil, err)) - if err == nil { - assert.Equal(t, client2.ID, client2.ID, fmt.Sprintf("add new disabled client with tags: expected %v got %s\n", nil, err)) - } - client2 = clients2[0] - ucases := []struct { - desc string - client mfclients.Client - err error - }{ - { - desc: "update tags for enabled client", - client: mfclients.Client{ - ID: client1.ID, - Tags: []string{"updated"}, - }, - err: nil, - }, - { - desc: "update tags for disabled client", - client: mfclients.Client{ - ID: client2.ID, - Tags: []string{"updated"}, - }, - err: errors.ErrNotFound, - }, - { - desc: "update tags for invalid client", - client: mfclients.Client{ - ID: wrongID, - Tags: []string{"updated"}, - }, - err: errors.ErrNotFound, - }, - } - for _, tc := range ucases { - expected, err := repo.UpdateTags(context.Background(), tc.client) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - if err == nil { - assert.Equal(t, tc.client.Tags, expected.Tags, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.client.Tags, expected.Tags)) - } - } -} - -func TestClientsUpdateSecret(t *testing.T) { - t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) - repo := cpostgres.NewRepository(database) - - client1 := mfclients.Client{ - ID: testsutil.GenerateUUID(t, idProvider), - Name: "enabled-client", - Credentials: mfclients.Credentials{ - Identity: "client1-update@example.com", - Secret: password, - }, - Status: mfclients.EnabledStatus, - } - client2 := mfclients.Client{ - ID: testsutil.GenerateUUID(t, idProvider), - Name: "disabled-client", - Credentials: mfclients.Credentials{ - Identity: "client2-update@example.com", - Secret: password, - }, - Status: mfclients.DisabledStatus, - } - - rClients1, err := repo.Save(context.Background(), client1) - assert.True(t, errors.Contains(err, nil), fmt.Sprintf("add new client: expected %v got %s\n", nil, err)) - if err == nil { - assert.Equal(t, client1.ID, rClients1[0].ID, fmt.Sprintf("add new client: expected %v got %s\n", nil, err)) - } - rClients2, err := repo.Save(context.Background(), client2) - assert.True(t, errors.Contains(err, nil), fmt.Sprintf("add new disabled client: expected %v got %s\n", nil, err)) - if err == nil { - assert.Equal(t, client2.ID, rClients2[0].ID, fmt.Sprintf("add new disabled client: expected %v got %s\n", nil, err)) - } - - ucases := []struct { - desc string - client mfclients.Client - err error - }{ - { - desc: "update secret for enabled client", - client: mfclients.Client{ - ID: client1.ID, - Credentials: mfclients.Credentials{ - Identity: "client1-update@example.com", - Secret: "newpassword", - }, - }, - err: nil, - }, - { - desc: "update secret for disabled client", - client: mfclients.Client{ - ID: client2.ID, - Credentials: mfclients.Credentials{ - Identity: "client2-update@example.com", - Secret: "newpassword", - }, - }, - err: errors.ErrNotFound, - }, - { - desc: "update secret for invalid client", - client: mfclients.Client{ - ID: wrongID, - Credentials: mfclients.Credentials{ - Identity: "client3-update@example.com", - Secret: "newpassword", - }, - }, - err: errors.ErrNotFound, - }, - } - for _, tc := range ucases { - _, err := repo.UpdateSecret(context.Background(), tc.client) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - if err == nil { - c, err := repo.RetrieveByIdentity(context.Background(), tc.client.Credentials.Identity) - require.Nil(t, err, fmt.Sprintf("retrieve client by id during update of secret unexpected error: %s", err)) - assert.Equal(t, tc.client.Credentials.Secret, c.Credentials.Secret, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.client.Credentials.Secret, c.Credentials.Secret)) - } - } -} - -func TestClientsUpdateIdentity(t *testing.T) { - t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) - repo := cpostgres.NewRepository(database) - - client1 := mfclients.Client{ - ID: testsutil.GenerateUUID(t, idProvider), - Name: "enabled-client", - Credentials: mfclients.Credentials{ - Identity: "client1-update@example.com", - Secret: password, - }, - Status: mfclients.EnabledStatus, - } - client2 := mfclients.Client{ - ID: testsutil.GenerateUUID(t, idProvider), - Name: "disabled-client", - Credentials: mfclients.Credentials{ - Identity: "client2-update@example.com", - Secret: password, - }, - Status: mfclients.DisabledStatus, - } - - rClients1, err := repo.Save(context.Background(), client1) - assert.True(t, errors.Contains(err, nil), fmt.Sprintf("add new client: expected %v got %s\n", nil, err)) - if err == nil { - assert.Equal(t, client1.ID, rClients1[0].ID, fmt.Sprintf("add new client: expected %v got %s\n", nil, err)) - } - rClients2, err := repo.Save(context.Background(), client2) - assert.True(t, errors.Contains(err, nil), fmt.Sprintf("add new disabled client: expected %v got %s\n", nil, err)) - if err == nil { - assert.Equal(t, client2.ID, rClients2[0].ID, fmt.Sprintf("add new disabled client: expected %v got %s\n", nil, err)) - } - - ucases := []struct { - desc string - client mfclients.Client - err error - }{ - { - desc: "update identity for enabled client", - client: mfclients.Client{ - ID: client1.ID, - Credentials: mfclients.Credentials{ - Identity: "client1-updated@example.com", - }, - }, - err: nil, - }, - { - desc: "update identity for disabled client", - client: mfclients.Client{ - ID: client2.ID, - Credentials: mfclients.Credentials{ - Identity: "client2-updated@example.com", - }, - }, - err: errors.ErrNotFound, - }, - { - desc: "update identity for invalid client", - client: mfclients.Client{ - ID: wrongID, - Credentials: mfclients.Credentials{ - Identity: "client3-updated@example.com", - }, - }, - err: errors.ErrNotFound, - }, - } - for _, tc := range ucases { - expected, err := repo.UpdateIdentity(context.Background(), tc.client) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - if err == nil { - assert.Equal(t, tc.client.Credentials.Identity, expected.Credentials.Identity, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.client.Credentials.Identity, expected.Credentials.Identity)) - } - } -} - -func TestClientsUpdateOwner(t *testing.T) { - t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) - repo := cpostgres.NewRepository(database) - - client1 := mfclients.Client{ - ID: testsutil.GenerateUUID(t, idProvider), - Name: "enabled-client-with-owner", - Credentials: mfclients.Credentials{ - Identity: "client1-update-owner@example.com", - Secret: password, - }, - Owner: testsutil.GenerateUUID(t, idProvider), - Status: mfclients.EnabledStatus, - } - client2 := mfclients.Client{ - ID: testsutil.GenerateUUID(t, idProvider), - Name: "disabled-client-with-owner", - Credentials: mfclients.Credentials{ - Identity: "client2-update-owner@example.com", - Secret: password, - }, - Owner: testsutil.GenerateUUID(t, idProvider), - Status: mfclients.DisabledStatus, - } - - clients1, err := repo.Save(context.Background(), client1) - client1 = clients1[0] - assert.True(t, errors.Contains(err, nil), fmt.Sprintf("add new client with owner: expected %v got %s\n", nil, err)) - if err == nil { - assert.Equal(t, client1.ID, client1.ID, fmt.Sprintf("add new client with owner: expected %v got %s\n", nil, err)) - } - clients2, err := repo.Save(context.Background(), client2) - client2 = clients2[0] - assert.True(t, errors.Contains(err, nil), fmt.Sprintf("add new disabled client with owner: expected %v got %s\n", nil, err)) - if err == nil { - assert.Equal(t, client2.ID, client2.ID, fmt.Sprintf("add new disabled client with owner: expected %v got %s\n", nil, err)) - } - ucases := []struct { - desc string - client mfclients.Client - err error - }{ - { - desc: "update owner for enabled client", - client: mfclients.Client{ - ID: client1.ID, - Owner: testsutil.GenerateUUID(t, idProvider), - }, - err: nil, - }, - { - desc: "update owner for disabled client", - client: mfclients.Client{ - ID: client2.ID, - Owner: testsutil.GenerateUUID(t, idProvider), - }, - err: errors.ErrNotFound, - }, - { - desc: "update owner for invalid client", - client: mfclients.Client{ - ID: wrongID, - Owner: testsutil.GenerateUUID(t, idProvider), - }, - err: errors.ErrNotFound, - }, - } - for _, tc := range ucases { - expected, err := repo.UpdateOwner(context.Background(), tc.client) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - if err == nil { - assert.Equal(t, tc.client.Owner, expected.Owner, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.client.Owner, expected.Owner)) - } - } -} - -func TestClientsChangeStatus(t *testing.T) { - t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) - repo := cpostgres.NewRepository(database) - - client1 := mfclients.Client{ - ID: testsutil.GenerateUUID(t, idProvider), - Name: "enabled-client", - Credentials: mfclients.Credentials{ - Identity: "client1-update@example.com", - Secret: password, - }, - Status: mfclients.EnabledStatus, - } - - clients1, err := repo.Save(context.Background(), client1) - assert.True(t, errors.Contains(err, nil), fmt.Sprintf("add new client: expected %v got %s\n", nil, err)) - client1 = clients1[0] - - ucases := []struct { - desc string - client mfclients.Client - err error - }{ - { - desc: "change client status for an enabled client", - client: mfclients.Client{ - ID: client1.ID, - Status: 0, - }, - err: nil, - }, - { - desc: "change client status for a disabled client", - client: mfclients.Client{ - ID: client1.ID, - Status: 1, - }, - err: nil, - }, - { - desc: "change client status for non-existing client", - client: mfclients.Client{ - ID: "invalid", - Status: 2, - }, - err: errors.ErrNotFound, - }, - } - - for _, tc := range ucases { - expected, err := repo.ChangeStatus(context.Background(), tc.client) - assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - if err == nil { - assert.Equal(t, tc.client.Status, expected.Status, fmt.Sprintf("%s: expected %d got %d\n", tc.desc, tc.client.Status, expected.Status)) + rClient.Credentials.Secret = tc.client.Credentials.Secret + assert.Equal(t, tc.client, rClient, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.client, rClient)) } } } diff --git a/users/clients/service.go b/users/clients/service.go index 5d2759d8..dcb0ecc7 100644 --- a/users/clients/service.go +++ b/users/clients/service.go @@ -12,6 +12,7 @@ import ( "github.com/mainflux/mainflux/internal/apiutil" mfclients "github.com/mainflux/mainflux/pkg/clients" "github.com/mainflux/mainflux/pkg/errors" + "github.com/mainflux/mainflux/users/clients/postgres" "github.com/mainflux/mainflux/users/jwt" "github.com/mainflux/mainflux/users/policies" ) @@ -50,7 +51,7 @@ type Service interface { } type service struct { - clients mfclients.Repository + clients postgres.Repository policies policies.Repository idProvider mainflux.IDProvider hasher Hasher @@ -60,7 +61,7 @@ type service struct { } // NewService returns a new Clients service implementation. -func NewService(c mfclients.Repository, p policies.Repository, t jwt.Repository, e Emailer, h Hasher, idp mainflux.IDProvider, pr *regexp.Regexp) Service { +func NewService(c postgres.Repository, p policies.Repository, t jwt.Repository, e Emailer, h Hasher, idp mainflux.IDProvider, pr *regexp.Regexp) Service { return service{ clients: c, policies: p, @@ -105,7 +106,7 @@ func (svc service) RegisterClient(ctx context.Context, token string, cli mfclien return mfclients.Client{}, err } - return client[0], nil + return client, nil } func (svc service) IssueToken(ctx context.Context, identity, secret string) (jwt.Token, error) { diff --git a/users/groups/postgres/doc.go b/users/groups/postgres/doc.go deleted file mode 100644 index 46e51f96..00000000 --- a/users/groups/postgres/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright (c) Mainflux -// SPDX-License-Identifier: Apache-2.0 - -// Package postgres contains the database implementation of groups repository layer. -package postgres diff --git a/users/policies/postgres/policies_test.go b/users/policies/postgres/policies_test.go index 6957847b..056a1752 100644 --- a/users/policies/postgres/policies_test.go +++ b/users/policies/postgres/policies_test.go @@ -12,9 +12,9 @@ import ( mfclients "github.com/mainflux/mainflux/pkg/clients" "github.com/mainflux/mainflux/pkg/errors" mfgroups "github.com/mainflux/mainflux/pkg/groups" + gpostgres "github.com/mainflux/mainflux/pkg/groups/postgres" "github.com/mainflux/mainflux/pkg/uuid" cpostgres "github.com/mainflux/mainflux/users/clients/postgres" - gpostgres "github.com/mainflux/mainflux/users/groups/postgres" "github.com/mainflux/mainflux/users/policies" ppostgres "github.com/mainflux/mainflux/users/policies/postgres" "github.com/stretchr/testify/assert" @@ -29,7 +29,7 @@ func TestPoliciesSave(t *testing.T) { t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) repo := ppostgres.NewRepository(database) crepo := cpostgres.NewRepository(database) - grepo := gpostgres.NewRepository(database) + grepo := gpostgres.New(database) group := mfgroups.Group{ ID: testsutil.GenerateUUID(t, idProvider), @@ -50,7 +50,7 @@ func TestPoliciesSave(t *testing.T) { clients, err := crepo.Save(context.Background(), client) require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - client = clients[0] + client = clients cases := []struct { desc string @@ -89,7 +89,7 @@ func TestPoliciesEvaluate(t *testing.T) { t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) repo := ppostgres.NewRepository(database) crepo := cpostgres.NewRepository(database) - grepo := gpostgres.NewRepository(database) + grepo := gpostgres.New(database) client1 := mfclients.Client{ ID: testsutil.GenerateUUID(t, idProvider), @@ -116,10 +116,10 @@ func TestPoliciesEvaluate(t *testing.T) { clients1, err := crepo.Save(context.Background(), client1) require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - client1 = clients1[0] + client1 = clients1 clients2, err := crepo.Save(context.Background(), client2) require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - client2 = clients2[0] + client2 = clients2 group, err = grepo.Save(context.Background(), group) require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) @@ -178,7 +178,7 @@ func TestPoliciesRetrieve(t *testing.T) { t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) repo := ppostgres.NewRepository(database) crepo := cpostgres.NewRepository(database) - grepo := gpostgres.NewRepository(database) + grepo := gpostgres.New(database) group := mfgroups.Group{ ID: testsutil.GenerateUUID(t, idProvider), @@ -199,7 +199,7 @@ func TestPoliciesRetrieve(t *testing.T) { clients, err := crepo.Save(context.Background(), client) require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - client = clients[0] + client = clients policy := policies.Policy{ OwnerID: client.ID, @@ -234,7 +234,7 @@ func TestPoliciesUpdate(t *testing.T) { t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) repo := ppostgres.NewRepository(database) crepo := cpostgres.NewRepository(database) - grepo := gpostgres.NewRepository(database) + grepo := gpostgres.New(database) group := mfgroups.Group{ ID: testsutil.GenerateUUID(t, idProvider), @@ -352,7 +352,7 @@ func TestPoliciesRetrievalAll(t *testing.T) { t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) repo := ppostgres.NewRepository(database) crepo := cpostgres.NewRepository(database) - grepo := gpostgres.NewRepository(database) + grepo := gpostgres.New(database) var nPolicies = uint64(10) @@ -377,10 +377,10 @@ func TestPoliciesRetrievalAll(t *testing.T) { clientsA, err := crepo.Save(context.Background(), clientA) require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - clientA = clientsA[0] + clientA = clientsA clientsB, err := crepo.Save(context.Background(), clientB) require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - clientB = clientsB[0] + clientB = clientsB grps := []string{} for i := uint64(0); i < nPolicies; i++ { @@ -628,7 +628,7 @@ func TestPoliciesDelete(t *testing.T) { t.Cleanup(func() { testsutil.CleanUpDB(t, db) }) repo := ppostgres.NewRepository(database) crepo := cpostgres.NewRepository(database) - grepo := gpostgres.NewRepository(database) + grepo := gpostgres.New(database) group := mfgroups.Group{ ID: testsutil.GenerateUUID(t, idProvider), @@ -649,7 +649,7 @@ func TestPoliciesDelete(t *testing.T) { clients, err := crepo.Save(context.Background(), client) require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err)) - client = clients[0] + client = clients policy := policies.Policy{ OwnerID: client.ID,