Files
supermq/bootstrap/service_test.go
T
b1ackd0t b3e2f41194 NOISSUE - Add Alarms (#106)
* WIP: alarms service

* fix(alarms): remove rule entity since it is not stored here

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* test(alarms): add tests cases for invalid alarms

* feat(alarms): add authorization

* feat(alarms): add docker deployment files

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* fix: update go mod file

* feat(alarms): support filtering by resolved_by, updated_by and severity

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* style: fix linter errors

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* fix(alarms): provide correct otel naming for create alarm

Fixes https://github.com/absmach/magistrala/pull/106#discussion_r2030151971

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* fix(alarms): group routes appropriately

Resolves https://github.com/absmach/magistrala/pull/106#discussion_r2030160891

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* fix(alarms): extract alarm id from url path rather than query params

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* fix(alarms): add all status to help in decoding

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* style(alarms): maintain consistent import as naming for supermq api package

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* refactor(alarms): update supermq dependecy to the latest

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* fix(alarms): Add domains gRPC service config to alarms service

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* test(alarms): all CRUD operations from the service

Return empty results instead of nil

This standardizes error responses across alarm endpoints to return empty
result structs rather than nil. Also renames entityReq to alarmReq and
adds HTTP status codes for created/deleted alarms.

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* test(alarms): fix failing tests due to introduction of context on sdk

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* fix(alarms): remove channel id

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* fix(alarms): standardize error handling across CRUD operations

Updated error responses to use specific repository errors for consistency

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* feat(alarms): add assignment fields to Alarm model and database

Introduced AssignedAt and AssignedBy fields to the Alarm struct and updated the database schema accordingly. Enhanced the UpdateAlarm function to handle these new fields, ensuring proper assignment tracking in the alarms system.

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* feat(alarms): enhance Alarm model with measurement attributes

Updated the Alarm struct to include Measurement, Value, Unit, and Cause fields. Modified the validation logic to ensure these fields are present. Adjusted logging and tracing middleware to reflect the new attributes. Updated database schema and related functions to accommodate these changes, ensuring comprehensive alarm data management.

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* feat(alarms): consume events from pubsub for creation of alarms

Removed session dependencies from CreateAlarm method and enhanced alarm validation to ensure all required fields are present

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* style(alarms): add newline at the end of docker compose

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* fix(alarms): Add assignee id and metadata fields when consuming messages

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* feat(alarms): add acknowledged field

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* feat(alarms): Add threshold value for the specific measurement

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* feat(alarms): Add channel, thing, and subtopic fields to Alarm model

This change adds required fields for tracking alarm sources and reorganizes
alarm-related fields for better grouping. Alarms now track the channel,
thing, and subtopic that triggered them, along with domain and rule info.

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* test(alarms): add service layer tests

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* fix(alarms): consume created at from message rather than creating it

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* feat(alarms): ready alarm as a gob encoded object

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* fix(alarms): read alarms from alarms queue and remove transformer

g

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* feat(alarms): update version of supermq

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* feat(alarms): add gob transformer

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* fix(alarms): rename thing id to client id

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* fix(alarms): create alarms stream

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* fix(alarms): check on logic to create new alarm

create new alarm if severity, status, subtopic changes
enhance logging with additional details for alarms management

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* remove conusmer and use pubsub

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* fix(alarms): use build tags for rabbitmq and nats

* fix(alarms): add health and metrics endpoint

* fix(magistrala): use supermq as build flags to see version and commit

* fix(alarms): use js config

* fix(alarms): remove validation when updating an alarm

fix authorization too

---------

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>
2025-04-15 19:32:09 +02:00

1114 lines
32 KiB
Go

// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package bootstrap_test
import (
"context"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/hex"
"fmt"
"io"
"sort"
"testing"
"github.com/absmach/magistrala/bootstrap"
mocks "github.com/absmach/magistrala/bootstrap/mocks"
"github.com/absmach/magistrala/internal/testsutil"
smqauthn "github.com/absmach/supermq/pkg/authn"
"github.com/absmach/supermq/pkg/errors"
svcerr "github.com/absmach/supermq/pkg/errors/service"
policysvc "github.com/absmach/supermq/pkg/policies"
policymocks "github.com/absmach/supermq/pkg/policies/mocks"
mgsdk "github.com/absmach/supermq/pkg/sdk"
sdkmocks "github.com/absmach/supermq/pkg/sdk/mocks"
"github.com/absmach/supermq/pkg/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
const (
validToken = "validToken"
invalidToken = "invalid"
invalidDomainID = "invalid"
email = "test@example.com"
unknown = "unknown"
channelsNum = 3
instanceID = "5de9b29a-feb9-11ed-be56-0242ac120002"
validID = "d4ebb847-5d0e-4e46-bdd9-b6aceaaa3a22"
)
var (
encKey = []byte("1234567891011121")
domainID = testsutil.GenerateUUID(&testing.T{})
channel = bootstrap.Channel{
ID: testsutil.GenerateUUID(&testing.T{}),
Name: "name",
Metadata: map[string]interface{}{"name": "value"},
}
config = bootstrap.Config{
ClientID: testsutil.GenerateUUID(&testing.T{}),
ClientSecret: testsutil.GenerateUUID(&testing.T{}),
ExternalID: testsutil.GenerateUUID(&testing.T{}),
ExternalKey: testsutil.GenerateUUID(&testing.T{}),
Channels: []bootstrap.Channel{channel},
Content: "config",
}
)
var (
boot *mocks.ConfigRepository
policies *policymocks.Service
sdk *sdkmocks.SDK
)
func newService() bootstrap.Service {
boot = new(mocks.ConfigRepository)
policies = new(policymocks.Service)
sdk = new(sdkmocks.SDK)
idp := uuid.NewMock()
return bootstrap.New(policies, boot, sdk, encKey, idp)
}
func enc(in []byte) ([]byte, error) {
block, err := aes.NewCipher(encKey)
if err != nil {
return nil, err
}
ciphertext := make([]byte, aes.BlockSize+len(in))
iv := ciphertext[:aes.BlockSize]
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return nil, err
}
stream := cipher.NewCFBEncrypter(block, iv)
stream.XORKeyStream(ciphertext[aes.BlockSize:], in)
return ciphertext, nil
}
func TestAdd(t *testing.T) {
svc := newService()
neID := config
neID.ClientID = "non-existent"
wrongChannels := config
ch := channel
ch.ID = "invalid"
wrongChannels.Channels = append(wrongChannels.Channels, ch)
cases := []struct {
desc string
config bootstrap.Config
token string
session smqauthn.Session
userID string
domainID string
clientErr error
createClientErr error
channelErr error
listExistingErr error
saveErr error
deleteClientErr error
err error
}{
{
desc: "add a new config",
config: config,
token: validToken,
userID: validID,
domainID: domainID,
err: nil,
},
{
desc: "add a config with an invalid ID",
config: neID,
token: validToken,
userID: validID,
domainID: domainID,
clientErr: errors.NewSDKError(svcerr.ErrNotFound),
err: svcerr.ErrNotFound,
},
{
desc: "add a config with invalid list of channels",
config: wrongChannels,
token: validToken,
userID: validID,
domainID: domainID,
listExistingErr: svcerr.ErrMalformedEntity,
err: svcerr.ErrMalformedEntity,
},
{
desc: "add empty config",
config: bootstrap.Config{},
token: validToken,
userID: validID,
domainID: domainID,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
tc.session = smqauthn.Session{UserID: tc.userID, DomainID: tc.domainID, DomainUserID: validID}
repoCall := sdk.On("Client", mock.Anything, tc.config.ClientID, mock.Anything, tc.token).Return(mgsdk.Client{ID: tc.config.ClientID, Credentials: mgsdk.ClientCredentials{Secret: tc.config.ClientSecret}}, tc.clientErr)
repoCall1 := sdk.On("CreateClient", mock.Anything, mock.Anything, tc.domainID, tc.token).Return(mgsdk.Client{}, tc.createClientErr)
repoCall2 := sdk.On("DeleteClient", mock.Anything, tc.config.ClientID, tc.domainID, tc.token).Return(tc.deleteClientErr)
repoCall3 := boot.On("ListExisting", context.Background(), tc.domainID, mock.Anything).Return(tc.config.Channels, tc.listExistingErr)
repoCall4 := boot.On("Save", context.Background(), mock.Anything, mock.Anything).Return(mock.Anything, tc.saveErr)
_, err := svc.Add(context.Background(), tc.session, tc.token, tc.config)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
repoCall.Unset()
repoCall1.Unset()
repoCall2.Unset()
repoCall3.Unset()
repoCall4.Unset()
})
}
}
func TestView(t *testing.T) {
svc := newService()
cases := []struct {
desc string
configID string
userID string
domain string
clientDomain string
token string
session smqauthn.Session
retrieveErr error
clientErr error
channelErr error
err error
}{
{
desc: "view an existing config",
configID: config.ClientID,
userID: validID,
clientDomain: domainID,
domain: domainID,
token: validToken,
err: nil,
},
{
desc: "view a non-existing config",
configID: unknown,
userID: validID,
clientDomain: domainID,
domain: domainID,
token: validToken,
retrieveErr: svcerr.ErrNotFound,
err: svcerr.ErrNotFound,
},
{
desc: "view a config with invalid domain",
configID: config.ClientID,
userID: validID,
clientDomain: invalidDomainID,
domain: invalidDomainID,
token: validToken,
retrieveErr: svcerr.ErrNotFound,
err: svcerr.ErrNotFound,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
tc.session = smqauthn.Session{UserID: tc.userID, DomainID: tc.domain, DomainUserID: validID}
repoCall := boot.On("RetrieveByID", context.Background(), tc.clientDomain, tc.configID).Return(config, tc.retrieveErr)
_, err := svc.View(context.Background(), tc.session, tc.configID)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
repoCall.Unset()
})
}
}
func TestUpdate(t *testing.T) {
svc := newService()
c := config
ch := channel
ch.ID = "2"
c.Channels = append(c.Channels, ch)
modifiedCreated := c
modifiedCreated.Content = "new-config"
modifiedCreated.Name = "new name"
nonExisting := c
nonExisting.ClientID = unknown
cases := []struct {
desc string
config bootstrap.Config
token string
session smqauthn.Session
userID string
domainID string
updateErr error
err error
}{
{
desc: "update a config with state Created",
config: modifiedCreated,
token: validToken,
userID: validID,
domainID: domainID,
err: nil,
},
{
desc: "update a non-existing config",
config: nonExisting,
token: validToken,
userID: validID,
domainID: domainID,
updateErr: svcerr.ErrNotFound,
err: svcerr.ErrNotFound,
},
{
desc: "update a config with update error",
config: c,
token: validToken,
userID: validID,
domainID: domainID,
updateErr: svcerr.ErrUpdateEntity,
err: svcerr.ErrUpdateEntity,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
tc.session = smqauthn.Session{UserID: tc.userID, DomainID: tc.domainID, DomainUserID: validID}
repoCall := boot.On("Update", context.Background(), mock.Anything).Return(tc.updateErr)
err := svc.Update(context.Background(), tc.session, tc.config)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
repoCall.Unset()
})
}
}
func TestUpdateCert(t *testing.T) {
svc := newService()
c := config
ch := channel
ch.ID = "2"
c.Channels = append(c.Channels, ch)
cases := []struct {
desc string
token string
session smqauthn.Session
userID string
domainID string
clientID string
clientCert string
clientKey string
caCert string
expectedConfig bootstrap.Config
authorizeErr error
authenticateErr error
updateErr error
err error
}{
{
desc: "update certs for the valid config",
userID: validID,
domainID: domainID,
clientID: c.ClientID,
clientCert: "newCert",
clientKey: "newKey",
caCert: "newCert",
token: validToken,
expectedConfig: bootstrap.Config{
Name: c.Name,
ClientSecret: c.ClientSecret,
Channels: c.Channels,
ExternalID: c.ExternalID,
ExternalKey: c.ExternalKey,
Content: c.Content,
State: c.State,
DomainID: c.DomainID,
ClientID: c.ClientID,
ClientCert: "newCert",
CACert: "newCert",
ClientKey: "newKey",
},
err: nil,
},
{
desc: "update cert for a non-existing config",
userID: validID,
domainID: domainID,
clientID: "empty",
clientCert: "newCert",
clientKey: "newKey",
caCert: "newCert",
token: validToken,
expectedConfig: bootstrap.Config{},
updateErr: svcerr.ErrNotFound,
err: svcerr.ErrNotFound,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
tc.session = smqauthn.Session{UserID: tc.userID, DomainID: tc.domainID, DomainUserID: validID}
repoCall := boot.On("UpdateCert", context.Background(), mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.expectedConfig, tc.updateErr)
cfg, err := svc.UpdateCert(context.Background(), tc.session, tc.clientID, tc.clientCert, tc.clientKey, tc.caCert)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
sort.Slice(cfg.Channels, func(i, j int) bool {
return cfg.Channels[i].ID < cfg.Channels[j].ID
})
sort.Slice(tc.expectedConfig.Channels, func(i, j int) bool {
return tc.expectedConfig.Channels[i].ID < tc.expectedConfig.Channels[j].ID
})
assert.Equal(t, tc.expectedConfig, cfg, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.expectedConfig, cfg))
repoCall.Unset()
})
}
}
func TestUpdateConnections(t *testing.T) {
svc := newService()
c := config
c.State = bootstrap.Inactive
activeConf := config
activeConf.State = bootstrap.Active
ch := channel
cases := []struct {
desc string
token string
session smqauthn.Session
id string
state bootstrap.State
userID string
domainID string
connections []string
updateErr error
clientErr error
channelErr error
retrieveErr error
listErr error
err error
}{
{
desc: "update connections for config with state Inactive",
token: validToken,
userID: validID,
domainID: domainID,
id: c.ClientID,
state: c.State,
connections: []string{ch.ID},
err: nil,
},
{
desc: "update connections for config with state Active",
token: validToken,
userID: validID,
domainID: domainID,
id: activeConf.ClientID,
state: activeConf.State,
connections: []string{ch.ID},
err: nil,
},
{
desc: "update connections with invalid channels",
token: validToken,
userID: validID,
domainID: domainID,
id: c.ClientID,
connections: []string{"wrong"},
channelErr: errors.NewSDKError(svcerr.ErrNotFound),
err: svcerr.ErrNotFound,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
tc.session = smqauthn.Session{UserID: tc.userID, DomainID: tc.domainID, DomainUserID: validID}
sdkCall := sdk.On("Channel", mock.Anything, mock.Anything, tc.domainID, tc.token).Return(mgsdk.Channel{}, tc.channelErr)
repoCall := boot.On("RetrieveByID", context.Background(), tc.domainID, tc.id).Return(c, tc.retrieveErr)
repoCall1 := boot.On("ListExisting", context.Background(), mock.Anything, mock.Anything, mock.Anything).Return(c.Channels, tc.listErr)
repoCall2 := boot.On("UpdateConnections", context.Background(), mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.updateErr)
err := svc.UpdateConnections(context.Background(), tc.session, tc.token, tc.id, tc.connections)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
sdkCall.Unset()
repoCall.Unset()
repoCall1.Unset()
repoCall2.Unset()
})
}
}
func TestList(t *testing.T) {
svc := newService()
numClients := 101
var saved []bootstrap.Config
for i := 0; i < numClients; i++ {
c := config
c.ExternalID = testsutil.GenerateUUID(t)
c.ExternalKey = testsutil.GenerateUUID(t)
c.Name = fmt.Sprintf("%s-%d", config.Name, i)
if i == 41 {
c.State = bootstrap.Active
}
saved = append(saved, c)
}
cases := []struct {
desc string
config bootstrap.ConfigsPage
filter bootstrap.Filter
offset uint64
limit uint64
token string
session smqauthn.Session
userID string
domainID string
listObjectsResponse policysvc.PolicyPage
listObjectsErr error
retrieveErr error
err error
}{
{
desc: "list configs successfully as super admin",
config: bootstrap.ConfigsPage{
Total: uint64(len(saved)),
Offset: 0,
Limit: 10,
Configs: saved[0:10],
},
filter: bootstrap.Filter{},
token: validToken,
session: smqauthn.Session{UserID: validID, DomainID: domainID, DomainUserID: validID, SuperAdmin: true},
userID: validID,
domainID: domainID,
offset: 0,
limit: 10,
err: nil,
},
{
desc: "list configs with failed super admin check",
config: bootstrap.ConfigsPage{},
filter: bootstrap.Filter{},
token: validID,
session: smqauthn.Session{UserID: validID, DomainID: domainID, DomainUserID: validID},
userID: validID,
domainID: domainID,
listObjectsResponse: policysvc.PolicyPage{},
offset: 0,
limit: 10,
err: nil,
},
{
desc: "list configs successfully as domain admin",
config: bootstrap.ConfigsPage{
Total: uint64(len(saved)),
Offset: 0,
Limit: 10,
Configs: saved[0:10],
},
filter: bootstrap.Filter{},
token: validToken,
userID: validID,
domainID: domainID,
session: smqauthn.Session{UserID: validID, DomainID: domainID, DomainUserID: validID, SuperAdmin: true},
listObjectsResponse: policysvc.PolicyPage{},
offset: 0,
limit: 10,
err: nil,
},
{
desc: "list configs successfully as non admin",
config: bootstrap.ConfigsPage{
Total: uint64(len(saved)),
Offset: 0,
Limit: 10,
Configs: saved[0:10],
},
filter: bootstrap.Filter{},
token: validToken,
userID: validID,
domainID: domainID,
session: smqauthn.Session{UserID: validID, DomainID: domainID, DomainUserID: validID},
listObjectsResponse: policysvc.PolicyPage{Policies: []string{"test", "test"}},
offset: 0,
limit: 10,
err: nil,
},
{
desc: "list configs with specified name as super admin",
config: bootstrap.ConfigsPage{
Total: 1,
Offset: 0,
Limit: 100,
Configs: saved[95:96],
},
filter: bootstrap.Filter{PartialMatch: map[string]string{"name": "95"}},
token: validToken,
session: smqauthn.Session{UserID: validID, DomainID: domainID, DomainUserID: validID, SuperAdmin: true},
userID: validID,
domainID: domainID,
offset: 0,
limit: 100,
err: nil,
},
{
desc: "list configs with specified name as domain admin",
config: bootstrap.ConfigsPage{
Total: 1,
Offset: 0,
Limit: 100,
Configs: saved[95:96],
},
filter: bootstrap.Filter{PartialMatch: map[string]string{"name": "95"}},
token: validToken,
userID: validID,
domainID: domainID,
session: smqauthn.Session{UserID: validID, DomainID: domainID, DomainUserID: validID, SuperAdmin: true},
offset: 0,
limit: 100,
err: nil,
},
{
desc: "list configs with specified name as non admin",
config: bootstrap.ConfigsPage{
Total: 1,
Offset: 0,
Limit: 100,
Configs: saved[95:96],
},
filter: bootstrap.Filter{PartialMatch: map[string]string{"name": "95"}},
token: validToken,
userID: validID,
domainID: domainID,
session: smqauthn.Session{UserID: validID, DomainID: domainID, DomainUserID: validID},
listObjectsResponse: policysvc.PolicyPage{Policies: []string{"test", "test"}},
offset: 0,
limit: 100,
err: nil,
},
{
desc: "list last page as super admin",
config: bootstrap.ConfigsPage{
Total: uint64(len(saved)),
Offset: 95,
Limit: 10,
Configs: saved[95:],
},
filter: bootstrap.Filter{},
token: validToken,
userID: validID,
domainID: domainID,
session: smqauthn.Session{UserID: validID, DomainID: domainID, DomainUserID: validID, SuperAdmin: true},
offset: 95,
limit: 10,
err: nil,
},
{
desc: "list last page as domain admin",
config: bootstrap.ConfigsPage{
Total: uint64(len(saved)),
Offset: 95,
Limit: 10,
Configs: saved[95:],
},
filter: bootstrap.Filter{},
token: validToken,
userID: validID,
domainID: domainID,
session: smqauthn.Session{UserID: validID, DomainID: domainID, DomainUserID: validID, SuperAdmin: true},
offset: 95,
limit: 10,
err: nil,
},
{
desc: "list last page as non admin",
config: bootstrap.ConfigsPage{
Total: uint64(len(saved)),
Offset: 95,
Limit: 10,
Configs: saved[95:],
},
filter: bootstrap.Filter{},
token: validToken,
userID: validID,
domainID: domainID,
session: smqauthn.Session{UserID: validID, DomainID: domainID, DomainUserID: validID},
listObjectsResponse: policysvc.PolicyPage{Policies: []string{"test", "test"}},
offset: 95,
limit: 10,
err: nil,
},
{
desc: "list configs with Active state as super admin",
config: bootstrap.ConfigsPage{
Total: 1,
Offset: 35,
Limit: 20,
Configs: []bootstrap.Config{saved[41]},
},
filter: bootstrap.Filter{FullMatch: map[string]string{"state": bootstrap.Active.String()}},
token: validToken,
userID: validID,
domainID: domainID,
session: smqauthn.Session{UserID: validID, DomainID: domainID, DomainUserID: validID, SuperAdmin: true},
offset: 35,
limit: 20,
err: nil,
},
{
desc: "list configs with Active state as domain admin",
config: bootstrap.ConfigsPage{
Total: 1,
Offset: 35,
Limit: 20,
Configs: []bootstrap.Config{saved[41]},
},
filter: bootstrap.Filter{FullMatch: map[string]string{"state": bootstrap.Active.String()}},
token: validToken,
userID: validID,
domainID: domainID,
session: smqauthn.Session{UserID: validID, DomainID: domainID, DomainUserID: validID, SuperAdmin: true},
offset: 35,
limit: 20,
err: nil,
},
{
desc: "list configs with Active state as non admin",
config: bootstrap.ConfigsPage{
Total: 1,
Offset: 35,
Limit: 20,
Configs: []bootstrap.Config{saved[41]},
},
filter: bootstrap.Filter{FullMatch: map[string]string{"state": bootstrap.Active.String()}},
token: validToken,
userID: validID,
domainID: domainID,
session: smqauthn.Session{UserID: validID, DomainID: domainID, DomainUserID: validID},
listObjectsResponse: policysvc.PolicyPage{Policies: []string{"test", "test"}},
offset: 35,
limit: 20,
err: nil,
},
{
desc: "list configs with failed to list objects",
config: bootstrap.ConfigsPage{},
filter: bootstrap.Filter{},
offset: 0,
limit: 10,
token: validToken,
userID: validID,
domainID: domainID,
session: smqauthn.Session{UserID: validID, DomainID: domainID, DomainUserID: validID},
listObjectsResponse: policysvc.PolicyPage{},
listObjectsErr: svcerr.ErrNotFound,
err: svcerr.ErrNotFound,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
policyCall := policies.On("ListAllObjects", mock.Anything, policysvc.Policy{
SubjectType: policysvc.UserType,
Subject: tc.userID,
Permission: policysvc.ViewPermission,
ObjectType: policysvc.ClientType,
}).Return(tc.listObjectsResponse, tc.listObjectsErr)
repoCall := boot.On("RetrieveAll", context.Background(), mock.Anything, mock.Anything, tc.filter, tc.offset, tc.limit).Return(tc.config, tc.retrieveErr)
result, err := svc.List(context.Background(), tc.session, tc.filter, tc.offset, tc.limit)
assert.ElementsMatch(t, tc.config.Configs, result.Configs, fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.config.Configs, result.Configs))
assert.Equal(t, tc.config.Total, result.Total, fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.config.Total, result.Total))
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
policyCall.Unset()
repoCall.Unset()
})
}
}
func TestRemove(t *testing.T) {
svc := newService()
c := config
cases := []struct {
desc string
id string
token string
session smqauthn.Session
userID string
domainID string
removeErr error
err error
}{
{
desc: "remove an existing config",
id: c.ClientID,
token: validToken,
userID: validID,
domainID: domainID,
err: nil,
},
{
desc: "remove removed config",
id: c.ClientID,
token: validToken,
userID: validID,
domainID: domainID,
err: nil,
},
{
desc: "remove a config with failed remove",
id: c.ClientID,
token: validToken,
userID: validID,
domainID: domainID,
removeErr: svcerr.ErrRemoveEntity,
err: svcerr.ErrRemoveEntity,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
tc.session = smqauthn.Session{UserID: tc.userID, DomainID: tc.domainID, DomainUserID: validID}
repoCall := boot.On("Remove", context.Background(), mock.Anything, mock.Anything).Return(tc.removeErr)
err := svc.Remove(context.Background(), tc.session, tc.id)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
repoCall.Unset()
})
}
}
func TestBootstrap(t *testing.T) {
svc := newService()
c := config
e, err := enc([]byte(c.ExternalKey))
assert.Nil(t, err, fmt.Sprintf("Encrypting external key expected to succeed: %s.\n", err))
cases := []struct {
desc string
config bootstrap.Config
externalKey string
externalID string
userID string
domainID string
err error
encrypted bool
}{
{
desc: "bootstrap using invalid external id",
config: bootstrap.Config{},
externalID: "invalid",
externalKey: c.ExternalKey,
userID: validID,
domainID: invalidDomainID,
err: svcerr.ErrNotFound,
encrypted: false,
},
{
desc: "bootstrap using invalid external key",
config: bootstrap.Config{},
externalID: c.ExternalID,
externalKey: "invalid",
userID: validID,
domainID: domainID,
err: bootstrap.ErrExternalKey,
encrypted: false,
},
{
desc: "bootstrap an existing config",
config: c,
externalID: c.ExternalID,
externalKey: c.ExternalKey,
userID: validID,
domainID: domainID,
err: nil,
encrypted: false,
},
{
desc: "bootstrap encrypted",
config: c,
externalID: c.ExternalID,
externalKey: hex.EncodeToString(e),
userID: validID,
domainID: domainID,
err: nil,
encrypted: true,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
repoCall := boot.On("RetrieveByExternalID", context.Background(), mock.Anything).Return(tc.config, tc.err)
config, err := svc.Bootstrap(context.Background(), tc.externalKey, tc.externalID, tc.encrypted)
assert.Equal(t, tc.config, config, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.config, config))
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
repoCall.Unset()
})
}
}
func TestChangeState(t *testing.T) {
svc := newService()
c := config
cases := []struct {
desc string
state bootstrap.State
id string
token string
session smqauthn.Session
userID string
domainID string
retrieveErr error
connectErr errors.SDKError
disconenctErr error
stateErr error
err error
}{
{
desc: "change state of non-existing config",
state: bootstrap.Active,
id: unknown,
token: validToken,
userID: validID,
domainID: domainID,
retrieveErr: svcerr.ErrNotFound,
err: svcerr.ErrNotFound,
},
{
desc: "change state to Active",
state: bootstrap.Active,
id: c.ClientID,
token: validToken,
userID: validID,
domainID: domainID,
err: nil,
},
{
desc: "change state to current state",
state: bootstrap.Active,
id: c.ClientID,
token: validToken,
userID: validID,
domainID: domainID,
err: nil,
},
{
desc: "change state to Inactive",
state: bootstrap.Inactive,
id: c.ClientID,
token: validToken,
userID: validID,
domainID: domainID,
err: nil,
},
{
desc: "change state with failed Connect",
state: bootstrap.Active,
id: c.ClientID,
token: validToken,
userID: validID,
domainID: domainID,
connectErr: errors.NewSDKError(bootstrap.ErrClients),
err: bootstrap.ErrClients,
},
{
desc: "change state with invalid state",
state: bootstrap.State(2),
id: c.ClientID,
token: validToken,
userID: validID,
domainID: domainID,
stateErr: svcerr.ErrMalformedEntity,
err: svcerr.ErrMalformedEntity,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
tc.session = smqauthn.Session{UserID: tc.userID, DomainID: tc.domainID, DomainUserID: validID}
repoCall := boot.On("RetrieveByID", context.Background(), tc.domainID, tc.id).Return(c, tc.retrieveErr)
sdkCall := sdk.On("ConnectClients", mock.Anything, mock.Anything, mock.Anything, []string{"Publish", "Subscribe"}, mock.Anything, tc.token).Return(tc.connectErr)
repoCall1 := boot.On("ChangeState", context.Background(), mock.Anything, mock.Anything, mock.Anything).Return(tc.stateErr)
err := svc.ChangeState(context.Background(), tc.session, tc.token, tc.id, tc.state)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
sdkCall.Unset()
repoCall.Unset()
repoCall1.Unset()
})
}
}
func TestUpdateChannelHandler(t *testing.T) {
svc := newService()
ch := bootstrap.Channel{
ID: channel.ID,
Name: "new name",
Metadata: map[string]interface{}{"meta": "new"},
}
cases := []struct {
desc string
channel bootstrap.Channel
err error
}{
{
desc: "update an existing channel",
channel: ch,
err: nil,
},
{
desc: "update a non-existing channel",
channel: bootstrap.Channel{ID: ""},
err: nil,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
repoCall := boot.On("UpdateChannel", context.Background(), mock.Anything).Return(tc.err)
err := svc.UpdateChannelHandler(context.Background(), tc.channel)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
repoCall.Unset()
})
}
}
func TestRemoveChannelHandler(t *testing.T) {
svc := newService()
cases := []struct {
desc string
id string
err error
}{
{
desc: "remove an existing channel",
id: config.Channels[0].ID,
err: nil,
},
{
desc: "remove a non-existing channel",
id: "unknown",
err: nil,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
repoCall := boot.On("RemoveChannel", context.Background(), mock.Anything).Return(tc.err)
err := svc.RemoveChannelHandler(context.Background(), tc.id)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
repoCall.Unset()
})
}
}
func TestRemoveConfigHandler(t *testing.T) {
svc := newService()
cases := []struct {
desc string
id string
err error
}{
{
desc: "remove an existing config",
id: config.ClientID,
err: nil,
},
{
desc: "remove a non-existing channel",
id: "unknown",
err: nil,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
repoCall := boot.On("RemoveClient", context.Background(), mock.Anything).Return(tc.err)
err := svc.RemoveConfigHandler(context.Background(), tc.id)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
repoCall.Unset()
})
}
}
func TestConnectClientHandler(t *testing.T) {
svc := newService()
cases := []struct {
desc string
clientID string
channelID string
err error
}{
{
desc: "connect",
channelID: channel.ID,
clientID: config.ClientID,
err: nil,
},
{
desc: "connect connected",
channelID: channel.ID,
clientID: config.ClientID,
err: svcerr.ErrAddPolicies,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
repoCall := boot.On("ConnectClient", context.Background(), mock.Anything, mock.Anything).Return(tc.err)
err := svc.ConnectClientHandler(context.Background(), tc.channelID, tc.clientID)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
repoCall.Unset()
})
}
}
func TestDisconnectClientsHandler(t *testing.T) {
svc := newService()
cases := []struct {
desc string
clientID string
channelID string
err error
}{
{
desc: "disconnect",
channelID: channel.ID,
clientID: config.ClientID,
err: nil,
},
{
desc: "disconnect disconnected",
channelID: channel.ID,
clientID: config.ClientID,
err: nil,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
repoCall := boot.On("DisconnectClient", context.Background(), mock.Anything, mock.Anything).Return(tc.err)
err := svc.DisconnectClientHandler(context.Background(), tc.channelID, tc.clientID)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
repoCall.Unset()
})
}
}