1
0
mirror of https://github.com/mainflux/mainflux.git synced 2025-04-24 13:48:49 +08:00

NOISSUE - Unify group and clients implementations on things and users (#1793)

* unify groups repo implementation

Signed-off-by: SammyOina <sammyoina@gmail.com>

* unify clients implementation

Signed-off-by: SammyOina <sammyoina@gmail.com>

* closer client integration

Signed-off-by: SammyOina <sammyoina@gmail.com>

* further unification of groups

Signed-off-by: SammyOina <sammyoina@gmail.com>

* enable on update secret & owner

Signed-off-by: SammyOina <sammyoina@gmail.com>

* unify retrieve all

Signed-off-by: SammyOina <sammyoina@gmail.com>

* fully unify groups repository

Signed-off-by: SammyOina <sammyoina@gmail.com>

* add secret to retrieve all

Signed-off-by: SammyOina <sammyoina@gmail.com>

* save updated at

Signed-off-by: SammyOina <sammyoina@gmail.com>

* fix test

Signed-off-by: SammyOina <sammyoina@gmail.com>

* fix retrieve all tests

Signed-off-by: SammyOina <sammyoina@gmail.com>

* restore files

Signed-off-by: SammyOina <sammyoina@gmail.com>

* fix build

Signed-off-by: SammyOina <sammyoina@gmail.com>

* remove unused files

Signed-off-by: SammyOina <sammyoina@gmail.com>

* fix retrieve all tests

Signed-off-by: SammyOina <sammyoina@gmail.com>

* fix linting

Signed-off-by: SammyOina <sammyoina@gmail.com>

* fix linting

Signed-off-by: SammyOina <sammyoina@gmail.com>

* restore broken changes

Signed-off-by: SammyOina <sammyoina@gmail.com>

* restore setup tests

Signed-off-by: SammyOina <sammyoina@gmail.com>

* update where condition

Signed-off-by: SammyOina <sammyoina@gmail.com>

* remove extra db object

Signed-off-by: SammyOina <sammyoina@gmail.com>

* unify groups test

Signed-off-by: SammyOina <sammyoina@gmail.com>

* unify clients test

Signed-off-by: SammyOina <sammyoina@gmail.com>

* remove unused variables

Signed-off-by: SammyOina <sammyoina@gmail.com>

* update changes

Signed-off-by: SammyOina <sammyoina@gmail.com>

* sync with master current updates

Signed-off-by: SammyOina <sammyoina@gmail.com>

* update test

Signed-off-by: SammyOina <sammyoina@gmail.com>

* fix tests

Signed-off-by: SammyOina <sammyoina@gmail.com>

* fix test

Signed-off-by: SammyOina <sammyoina@gmail.com>

* fix test

Signed-off-by: SammyOina <sammyoina@gmail.com>

* fix tests

Signed-off-by: SammyOina <sammyoina@gmail.com>

* match changes in #1877

Signed-off-by: SammyOina <sammyoina@gmail.com>

* separate things and users repos

Signed-off-by: SammyOina <sammyoina@gmail.com>

* remove comments
 implement retrieveBysecret in things only

Signed-off-by: SammyOina <sammyoina@gmail.com>

* remove exec

Signed-off-by: SammyOina <sammyoina@gmail.com>

* remove duplicate imports

Signed-off-by: SammyOina <sammyoina@gmail.com>

* wrap errors

Signed-off-by: SammyOina <sammyoina@gmail.com>

---------

Signed-off-by: SammyOina <sammyoina@gmail.com>
This commit is contained in:
Sammy Kerata Oina 2023-08-08 13:19:54 +03:00 committed by GitHub
parent 0f0d761a1b
commit 06800c1038
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1669 additions and 3829 deletions

View File

@ -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()

View File

@ -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

View File

@ -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.

View File

@ -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
}

View File

@ -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))
}
}
}

View File

@ -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

View File

@ -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)
}

View File

@ -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
}

View File

@ -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),

View File

@ -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)
}

View File

@ -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))
}
}
}

View File

@ -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)

View File

@ -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"`
}

View File

@ -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))
}
}
}

View File

@ -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),

View File

@ -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) {

View File

@ -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
}

View File

@ -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))
}
}
}

View File

@ -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) {

View File

@ -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

View File

@ -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,