// Copyright (c) Mainflux // SPDX-License-Identifier: Apache-2.0 package bootstrap_test import ( "context" "crypto/aes" "crypto/cipher" "crypto/rand" "encoding/hex" "fmt" "io" "net/http/httptest" "sort" "testing" "github.com/go-chi/chi/v5" "github.com/mainflux/mainflux" authmocks "github.com/mainflux/mainflux/auth/mocks" "github.com/mainflux/mainflux/bootstrap" "github.com/mainflux/mainflux/bootstrap/mocks" "github.com/mainflux/mainflux/internal/groups" chmocks "github.com/mainflux/mainflux/internal/groups/mocks" mflog "github.com/mainflux/mainflux/logger" "github.com/mainflux/mainflux/pkg/errors" mfgroups "github.com/mainflux/mainflux/pkg/groups" mfsdk "github.com/mainflux/mainflux/pkg/sdk/go" "github.com/mainflux/mainflux/pkg/uuid" "github.com/mainflux/mainflux/things" thapi "github.com/mainflux/mainflux/things/api/http" thmocks "github.com/mainflux/mainflux/things/mocks" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const ( validToken = "validToken" invalidToken = "invalidToken" email = "test@example.com" unknown = "unknown" channelsNum = 3 instanceID = "5de9b29a-feb9-11ed-be56-0242ac120002" ) var ( encKey = []byte("1234567891011121") channel = bootstrap.Channel{ ID: "1", Name: "name", Metadata: map[string]interface{}{"name": "value"}, } config = bootstrap.Config{ ExternalID: "external_id", ExternalKey: "external_key", Channels: []bootstrap.Channel{channel}, Content: "config", } ) func newService(url string, auth mainflux.AuthServiceClient) bootstrap.Service { things := mocks.NewConfigsRepository() config := mfsdk.Config{ ThingsURL: url, } sdk := mfsdk.NewSDK(config) return bootstrap.New(auth, things, sdk, encKey) } func newThingsService() (things.Service, mfgroups.Service, mainflux.AuthServiceClient) { auth := new(authmocks.Service) thingCache := thmocks.NewCache() idProvider := uuid.NewMock() cRepo := new(thmocks.Repository) gRepo := new(chmocks.Repository) return things.NewService(auth, cRepo, gRepo, thingCache, idProvider), groups.NewService(gRepo, idProvider, auth), auth } func newThingsServer(tsvc things.Service, gsvc mfgroups.Service) *httptest.Server { logger := mflog.NewMock() mux := chi.NewRouter() thapi.MakeHandler(tsvc, gsvc, mux, logger, instanceID) return httptest.NewServer(mux) } 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) { tsvc, gsvc, auth := newThingsService() ts := newThingsServer(tsvc, gsvc) svc := newService(ts.URL, auth) neID := config neID.ThingID = "non-existent" wrongChannels := config ch := channel ch.ID = "invalid" wrongChannels.Channels = append(wrongChannels.Channels, ch) cases := []struct { desc string config bootstrap.Config token string err error }{ { desc: "add a new config", config: config, token: validToken, err: nil, }, { desc: "add a config with an invalid ID", config: neID, token: validToken, err: errors.ErrNotFound, }, { desc: "add a config with wrong credentials", config: config, token: invalidToken, err: errors.ErrAuthentication, }, { desc: "add a config with invalid list of channels", config: wrongChannels, token: validToken, err: errors.ErrMalformedEntity, }, } for _, tc := range cases { _, err := svc.Add(context.Background(), tc.token, tc.config) switch err { case nil: assert.Nil(t, err, fmt.Sprintf("%s: got unexpected error : %s", tc.desc, err)) default: assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) } } } func TestView(t *testing.T) { tsvc, gsvc, auth := newThingsService() ts := newThingsServer(tsvc, gsvc) svc := newService(ts.URL, auth) saved, err := svc.Add(context.Background(), validToken, config) assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err)) cases := []struct { desc string id string token string err error }{ { desc: "view an existing config", id: saved.ThingID, token: validToken, err: nil, }, { desc: "view a non-existing config", id: unknown, token: validToken, err: errors.ErrNotFound, }, { desc: "view a config with wrong credentials", id: config.ThingID, token: invalidToken, err: errors.ErrAuthentication, }, } for _, tc := range cases { _, err := svc.View(context.Background(), tc.token, tc.id) assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) } } func TestUpdate(t *testing.T) { tsvc, gsvc, auth := newThingsService() ts := newThingsServer(tsvc, gsvc) svc := newService(ts.URL, auth) c := config ch := channel ch.ID = "2" c.Channels = append(c.Channels, ch) saved, err := svc.Add(context.Background(), validToken, c) assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err)) modifiedCreated := saved modifiedCreated.Content = "new-config" modifiedCreated.Name = "new name" nonExisting := config nonExisting.ThingID = unknown cases := []struct { desc string config bootstrap.Config token string err error }{ { desc: "update a config with state Created", config: modifiedCreated, token: validToken, err: nil, }, { desc: "update a non-existing config", config: nonExisting, token: validToken, err: errors.ErrNotFound, }, { desc: "update a config with wrong credentials", config: saved, token: invalidToken, err: errors.ErrAuthentication, }, } for _, tc := range cases { err := svc.Update(context.Background(), 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)) } } func TestUpdateCert(t *testing.T) { tsvc, gsvc, auth := newThingsService() ts := newThingsServer(tsvc, gsvc) svc := newService(ts.URL, auth) c := config ch := channel ch.ID = "2" c.Channels = append(c.Channels, ch) saved, err := svc.Add(context.Background(), validToken, c) assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err)) cases := []struct { desc string token string thingKey string clientCert string clientKey string caCert string expectedConfig bootstrap.Config err error }{ { desc: "update certs for the valid config", thingKey: saved.ThingKey, clientCert: "newCert", clientKey: "newKey", caCert: "newCert", token: validToken, expectedConfig: bootstrap.Config{ Name: saved.Name, ThingKey: saved.ThingKey, Channels: saved.Channels, ExternalID: saved.ExternalID, ExternalKey: saved.ExternalKey, Content: saved.Content, State: saved.State, Owner: saved.Owner, ThingID: saved.ThingID, ClientCert: "newCert", CACert: "newCert", ClientKey: "newKey", }, err: nil, }, { desc: "update cert for a non-existing config", thingKey: "empty", clientCert: "newCert", clientKey: "newKey", caCert: "newCert", token: validToken, expectedConfig: bootstrap.Config{}, err: errors.ErrNotFound, }, { desc: "update config cert with wrong credentials", thingKey: saved.ThingKey, clientCert: "newCert", clientKey: "newKey", caCert: "newCert", token: invalidToken, expectedConfig: bootstrap.Config{}, err: errors.ErrAuthentication, }, } for _, tc := range cases { cfg, err := svc.UpdateCert(context.Background(), tc.token, tc.thingKey, 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)) } } func TestUpdateConnections(t *testing.T) { tsvc, gsvc, auth := newThingsService() ts := newThingsServer(tsvc, gsvc) svc := newService(ts.URL, auth) c := config ch := channel ch.ID = "2" c.Channels = append(c.Channels, ch) created, err := svc.Add(context.Background(), validToken, c) assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err)) externalID, err := uuid.New().ID() assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err)) c.ExternalID = externalID active, err := svc.Add(context.Background(), validToken, c) assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err)) err = svc.ChangeState(context.Background(), validToken, active.ThingID, bootstrap.Active) assert.Nil(t, err, fmt.Sprintf("Changing state expected to succeed: %s.\n", err)) nonExisting := config nonExisting.ThingID = unknown cases := []struct { desc string token string id string connections []string err error }{ { desc: "update connections for config with state Inactive", token: validToken, id: created.ThingID, connections: []string{"2"}, err: nil, }, { desc: "update connections for config with state Active", token: validToken, id: active.ThingID, connections: []string{"3"}, err: nil, }, { desc: "update connections for non-existing config", token: validToken, id: "", connections: []string{"3"}, err: errors.ErrNotFound, }, { desc: "update connections with invalid channels", token: validToken, id: created.ThingID, connections: []string{"wrong"}, err: errors.ErrMalformedEntity, }, { desc: "update connections a config with wrong credentials", token: invalidToken, id: created.ThingKey, connections: []string{"2", "3"}, err: errors.ErrAuthentication, }, } for _, tc := range cases { err := svc.UpdateConnections(context.Background(), 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)) } } func TestList(t *testing.T) { tsvc, gsvc, auth := newThingsService() ts := newThingsServer(tsvc, gsvc) svc := newService(ts.URL, auth) numThings := 101 var saved []bootstrap.Config for i := 0; i < numThings; i++ { c := config id, err := uuid.New().ID() assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err)) c.ExternalID = id c.ExternalKey = id c.Name = fmt.Sprintf("%s-%d", config.Name, i) s, err := svc.Add(context.Background(), validToken, c) saved = append(saved, s) assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err)) } // Set one Thing to the different state err := svc.ChangeState(context.Background(), validToken, "42", bootstrap.Active) assert.Nil(t, err, fmt.Sprintf("Changing config state expected to succeed: %s.\n", err)) saved[41].State = bootstrap.Active cases := []struct { desc string config bootstrap.ConfigsPage filter bootstrap.Filter offset uint64 limit uint64 token string err error }{ { desc: "list configs", config: bootstrap.ConfigsPage{ Total: uint64(len(saved)), Offset: 0, Limit: 10, Configs: saved[0:10], }, filter: bootstrap.Filter{}, token: validToken, offset: 0, limit: 10, err: nil, }, { desc: "list configs with specified name", config: bootstrap.ConfigsPage{ Total: 1, Offset: 0, Limit: 100, Configs: saved[95:96], }, filter: bootstrap.Filter{PartialMatch: map[string]string{"name": "95"}}, token: validToken, offset: 0, limit: 100, err: nil, }, { desc: "list configs with invalid token", config: bootstrap.ConfigsPage{}, filter: bootstrap.Filter{}, token: invalidToken, offset: 0, limit: 10, err: errors.ErrAuthentication, }, { desc: "list last page", config: bootstrap.ConfigsPage{ Total: uint64(len(saved)), Offset: 95, Limit: 10, Configs: saved[95:], }, filter: bootstrap.Filter{}, token: validToken, offset: 95, limit: 10, err: nil, }, { desc: "list configs with Active state", 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, offset: 35, limit: 20, err: nil, }, } for _, tc := range cases { result, err := svc.List(context.Background(), tc.token, 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)) } } func TestRemove(t *testing.T) { tsvc, gsvc, auth := newThingsService() ts := newThingsServer(tsvc, gsvc) svc := newService(ts.URL, auth) saved, err := svc.Add(context.Background(), validToken, config) assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err)) cases := []struct { desc string id string token string err error }{ { desc: "view a config with wrong credentials", id: saved.ThingID, token: invalidToken, err: errors.ErrAuthentication, }, { desc: "remove an existing config", id: saved.ThingID, token: validToken, err: nil, }, { desc: "remove removed config", id: saved.ThingID, token: validToken, err: nil, }, { desc: "remove non-existing config", id: unknown, token: validToken, err: nil, }, } for _, tc := range cases { err := svc.Remove(context.Background(), tc.token, tc.id) assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) } } func TestBootstrap(t *testing.T) { tsvc, gsvc, auth := newThingsService() ts := newThingsServer(tsvc, gsvc) svc := newService(ts.URL, auth) saved, err := svc.Add(context.Background(), validToken, config) assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err)) e, err := enc([]byte(saved.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 err error encrypted bool }{ { desc: "bootstrap using invalid external id", config: bootstrap.Config{}, externalID: "invalid", externalKey: saved.ExternalKey, err: errors.ErrNotFound, encrypted: false, }, { desc: "bootstrap using invalid external key", config: bootstrap.Config{}, externalID: saved.ExternalID, externalKey: "invalid", err: bootstrap.ErrExternalKey, encrypted: false, }, { desc: "bootstrap an existing config", config: saved, externalID: saved.ExternalID, externalKey: saved.ExternalKey, err: nil, encrypted: false, }, { desc: "bootstrap encrypted", config: saved, externalID: saved.ExternalID, externalKey: hex.EncodeToString(e), err: nil, encrypted: true, }, } for _, tc := range cases { 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)) } } func TestChangeState(t *testing.T) { tsvc, gsvc, auth := newThingsService() ts := newThingsServer(tsvc, gsvc) svc := newService(ts.URL, auth) saved, err := svc.Add(context.Background(), validToken, config) assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err)) cases := []struct { desc string state bootstrap.State id string token string err error }{ { desc: "change state with wrong credentials", state: bootstrap.Active, id: saved.ThingID, token: invalidToken, err: errors.ErrAuthentication, }, { desc: "change state of non-existing config", state: bootstrap.Active, id: unknown, token: validToken, err: errors.ErrNotFound, }, { desc: "change state to Active", state: bootstrap.Active, id: saved.ThingID, token: validToken, err: nil, }, { desc: "change state to current state", state: bootstrap.Active, id: saved.ThingID, token: validToken, err: nil, }, { desc: "change state to Inactive", state: bootstrap.Inactive, id: saved.ThingID, token: validToken, err: nil, }, } for _, tc := range cases { err := svc.ChangeState(context.Background(), 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)) } } func TestUpdateChannelHandler(t *testing.T) { tsvc, gsvc, auth := newThingsService() ts := newThingsServer(tsvc, gsvc) svc := newService(ts.URL, auth) _, err := svc.Add(context.Background(), validToken, config) assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err)) 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 { 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)) } } func TestRemoveChannelHandler(t *testing.T) { tsvc, gsvc, auth := newThingsService() ts := newThingsServer(tsvc, gsvc) svc := newService(ts.URL, auth) _, err := svc.Add(context.Background(), validToken, config) assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err)) cases := []struct { desc string id string err error }{ { desc: "remove an existing channel", id: channel.ID, err: nil, }, { desc: "remove a non-existing channel", id: "unknown", err: nil, }, } for _, tc := range cases { 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)) } } func TestRemoveCoinfigHandler(t *testing.T) { tsvc, gsvc, auth := newThingsService() ts := newThingsServer(tsvc, gsvc) svc := newService(ts.URL, auth) saved, err := svc.Add(context.Background(), validToken, config) assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err)) cases := []struct { desc string id string err error }{ { desc: "remove an existing config", id: saved.ThingID, err: nil, }, { desc: "remove a non-existing channel", id: "unknown", err: nil, }, } for _, tc := range cases { 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)) } } func TestDisconnectThingsHandler(t *testing.T) { tsvc, gsvc, auth := newThingsService() ts := newThingsServer(tsvc, gsvc) svc := newService(ts.URL, auth) saved, err := svc.Add(context.Background(), validToken, config) require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err)) cases := []struct { desc string thingID string channelID string err error }{ { desc: "disconnect", channelID: channel.ID, thingID: saved.ThingID, err: nil, }, { desc: "disconnect disconnected", channelID: channel.ID, thingID: saved.ThingID, err: nil, }, } for _, tc := range cases { err := svc.DisconnectThingHandler(context.Background(), tc.channelID, tc.thingID) assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) } }