SMQ-3399 - Unify Magistrala and SuperMQ (#3400)

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>
Signed-off-by: dusan <borovcanindusan1@gmail.com>
Co-authored-by: Steve Munene <stevenyaga2014@gmail.com>
This commit is contained in:
Dušan Borovčanin
2026-04-01 09:55:11 +02:00
committed by GitHub
parent 08249c045b
commit ef5c253c51
549 changed files with 95880 additions and 12234 deletions
+216
View File
@@ -0,0 +1,216 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package cli
import (
"encoding/json"
mgsdk "github.com/absmach/supermq/pkg/sdk"
"github.com/spf13/cobra"
)
var cmdBootstrap = []cobra.Command{
{
Use: "create <JSON_config> <domain_id> <user_auth_token>",
Short: "Create config",
Long: `Create new Client Bootstrap Config to the user identified by the provided key`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 3 {
logUsageCmd(*cmd, cmd.Use)
return
}
var cfg mgsdk.BootstrapConfig
if err := json.Unmarshal([]byte(args[0]), &cfg); err != nil {
logErrorCmd(*cmd, err)
return
}
id, err := sdk.AddBootstrap(cmd.Context(), cfg, args[1], args[2])
if err != nil {
logErrorCmd(*cmd, err)
return
}
logCreatedCmd(*cmd, id)
},
},
{
Use: "get [all | <client_id>] <domain_id> <user_auth_token>",
Short: "Get config",
Long: `Get Client Config with given ID belonging to the user identified by the given key.
all - lists all config
<client_id> - view config of <client_id>`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 3 {
logUsageCmd(*cmd, cmd.Use)
return
}
pageMetadata := mgsdk.PageMetadata{
Offset: Offset,
Limit: Limit,
State: State,
Name: Name,
}
if args[0] == all {
l, err := sdk.Bootstraps(cmd.Context(), pageMetadata, args[1], args[2])
if err != nil {
logErrorCmd(*cmd, err)
return
}
logJSONCmd(*cmd, l)
return
}
c, err := sdk.ViewBootstrap(cmd.Context(), args[0], args[1], args[2])
if err != nil {
logErrorCmd(*cmd, err)
return
}
logJSONCmd(*cmd, c)
},
},
{
Use: "update [config <JSON_config> | connection <id> <channel_ids> | certs <id> <client_cert> <client_key> <ca> ] <domain_id> <user_auth_token>",
Short: "Update config",
Long: `Updates editable fields of the provided Config.
config <JSON_config> - Updates editable fields of the provided Config.
connection <id> <channel_ids> - Updates connections performs update of the channel list corresponding Client is connected to.
channel_ids - '["channel_id1", ...]'
certs <id> <client_cert> <client_key> <ca> - Update bootstrap config certificates.`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) < 4 {
logUsageCmd(*cmd, cmd.Use)
return
}
if args[0] == "config" {
var cfg mgsdk.BootstrapConfig
if err := json.Unmarshal([]byte(args[1]), &cfg); err != nil {
logErrorCmd(*cmd, err)
return
}
if err := sdk.UpdateBootstrap(cmd.Context(), cfg, args[1], args[2]); err != nil {
logErrorCmd(*cmd, err)
return
}
logOKCmd(*cmd)
return
}
if args[0] == "connection" {
var ids []string
if err := json.Unmarshal([]byte(args[2]), &ids); err != nil {
logErrorCmd(*cmd, err)
return
}
if err := sdk.UpdateBootstrapConnection(cmd.Context(), args[1], ids, args[3], args[4]); err != nil {
logErrorCmd(*cmd, err)
return
}
logOKCmd(*cmd)
return
}
if args[0] == "certs" {
cfg, err := sdk.UpdateBootstrapCerts(cmd.Context(), args[0], args[1], args[2], args[3], args[4], args[5])
if err != nil {
logErrorCmd(*cmd, err)
return
}
logJSONCmd(*cmd, cfg)
return
}
logUsageCmd(*cmd, cmd.Use)
},
},
{
Use: "remove <client_id> <domain_id> <user_auth_token>",
Short: "Remove config",
Long: `Removes Config with specified key that belongs to the user identified by the given key`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 3 {
logUsageCmd(*cmd, cmd.Use)
return
}
if err := sdk.RemoveBootstrap(cmd.Context(), args[0], args[1], args[2]); err != nil {
logErrorCmd(*cmd, err)
return
}
logOKCmd(*cmd)
},
},
{
Use: "bootstrap [<external_id> <external_key> | secure <external_id> <external_key> <crypto_key> ]",
Short: "Bootstrap config",
Long: `Returns Config to the Client with provided external ID using external key.
secure - Retrieves a configuration with given external ID and encrypted external key.`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) < 2 {
logUsageCmd(*cmd, cmd.Use)
return
}
if args[0] == "secure" {
c, err := sdk.BootstrapSecure(cmd.Context(), args[1], args[2], args[3])
if err != nil {
logErrorCmd(*cmd, err)
return
}
logJSONCmd(*cmd, c)
return
}
c, err := sdk.Bootstrap(cmd.Context(), args[0], args[1])
if err != nil {
logErrorCmd(*cmd, err)
return
}
logJSONCmd(*cmd, c)
},
},
{
Use: "whitelist <JSON_config> <domain_id> <user_auth_token>",
Short: "Whitelist config",
Long: `Whitelist updates client state config with given id from the authenticated user`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 3 {
logUsageCmd(*cmd, cmd.Use)
return
}
var cfg mgsdk.BootstrapConfig
if err := json.Unmarshal([]byte(args[0]), &cfg); err != nil {
logErrorCmd(*cmd, err)
return
}
if err := sdk.Whitelist(cmd.Context(), cfg.ClientID, cfg.State, args[1], args[2]); err != nil {
logErrorCmd(*cmd, err)
return
}
logOKCmd(*cmd)
},
},
}
// NewBootstrapCmd returns bootstrap command.
func NewBootstrapCmd() *cobra.Command {
cmd := cobra.Command{
Use: "bootstrap [create | get | update | remove | bootstrap | whitelist]",
Short: "Bootstrap management",
Long: `Bootstrap management: create, get, update, delete or whitelist Bootstrap config`,
}
for i := range cmdBootstrap {
cmd.AddCommand(&cmdBootstrap[i])
}
return &cmd
}
+633
View File
@@ -0,0 +1,633 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package cli_test
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"testing"
"github.com/absmach/supermq/cli"
"github.com/absmach/supermq/internal/testsutil"
"github.com/absmach/supermq/pkg/errors"
svcerr "github.com/absmach/supermq/pkg/errors/service"
mgsdk "github.com/absmach/supermq/pkg/sdk"
sdkmocks "github.com/absmach/supermq/pkg/sdk/mocks"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
var (
clientID = testsutil.GenerateUUID(&testing.T{})
channelID = testsutil.GenerateUUID(&testing.T{})
domainID = testsutil.GenerateUUID(&testing.T{})
bootConfig = mgsdk.BootstrapConfig{
ClientID: clientID,
Channels: []string{channelID},
Name: "Test Bootstrap",
ExternalID: "09:6:0:sb:sa",
ExternalKey: "key",
}
validToken = "validToken"
invalidToken = "invalidToken"
extraArg = "extra-arg"
invalidID = "invalidID"
all = "all"
)
func TestCreateBootstrapConfigCmd(t *testing.T) {
sdkMock := new(sdkmocks.SDK)
cli.SetSDK(sdkMock)
bootCmd := cli.NewBootstrapCmd()
rootCmd := setFlags(bootCmd)
jsonConfig := fmt.Sprintf("{\"external_id\":\"09:6:0:sb:sa\", \"client_id\": \"%s\", \"external_key\":\"key\", \"name\": \"%s\", \"channels\":[\"%s\"]}", clientID, "Test Bootstrap", channelID)
invalidJson := fmt.Sprintf("{\"external_id\":\"09:6:0:sb:sa\", \"client_id\": \"%s\", \"external_key\":\"key\", \"name\": \"%s\", \"channels\":[\"%s\"]", clientID, "Test Bootstrap", channelID)
cases := []struct {
desc string
args []string
logType outputLog
response string
sdkErr errors.SDKError
errLogMessage string
id string
}{
{
desc: "create bootstrap config successfully",
args: []string{
jsonConfig,
domainID,
validToken,
},
logType: createLog,
id: clientID,
response: fmt.Sprintf("\ncreated: %s\n\n", clientID),
},
{
desc: "create bootstrap config with invald args",
args: []string{
jsonConfig,
domainID,
validToken,
extraArg,
},
logType: usageLog,
},
{
desc: "create bootstrap config with invald json",
args: []string{
invalidJson,
domainID,
validToken,
},
sdkErr: errors.NewSDKError(errors.New("unexpected end of JSON input")),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.New("unexpected end of JSON input")),
logType: errLog,
},
{
desc: "create bootstrap config with invald token",
args: []string{
jsonConfig,
domainID,
invalidToken,
},
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusUnauthorized),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusUnauthorized)),
logType: errLog,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
sdkCall := sdkMock.On("AddBootstrap", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.id, tc.sdkErr)
out := executeCommand(t, rootCmd, append([]string{createCmd}, tc.args...)...)
switch tc.logType {
case createLog:
assert.Equal(t, tc.response, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.response, out))
case errLog:
assert.Equal(t, tc.errLogMessage, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.errLogMessage, out))
case usageLog:
assert.False(t, strings.Contains(out, rootCmd.Use), fmt.Sprintf("%s invalid usage: %s", tc.desc, out))
}
sdkCall.Unset()
})
}
}
func TestGetBootstrapConfigCmd(t *testing.T) {
sdkMock := new(sdkmocks.SDK)
cli.SetSDK(sdkMock)
bootCmd := cli.NewBootstrapCmd()
rootCmd := setFlags(bootCmd)
var boot mgsdk.BootstrapConfig
var page mgsdk.BootstrapPage
cases := []struct {
desc string
args []string
sdkErr errors.SDKError
page mgsdk.BootstrapPage
boot mgsdk.BootstrapConfig
logType outputLog
errLogMessage string
}{
{
desc: "get all bootstrap config successfully",
args: []string{
all,
domainID,
validToken,
},
page: mgsdk.BootstrapPage{
PageRes: mgsdk.PageRes{
Total: 1,
Offset: 0,
Limit: 10,
},
Configs: []mgsdk.BootstrapConfig{bootConfig},
},
logType: entityLog,
},
{
desc: "get bootstrap config with id",
args: []string{
channelID,
domainID,
validToken,
},
logType: entityLog,
boot: bootConfig,
},
{
desc: "get bootstrap config with invalid args",
args: []string{
all,
domainID,
validToken,
extraArg,
},
logType: usageLog,
},
{
desc: "get all bootstrap config with invalid token",
args: []string{
all,
domainID,
invalidToken,
},
logType: errLog,
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden)),
},
{
desc: "get bootstrap config with invalid id",
args: []string{
invalidID,
domainID,
validToken,
},
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden)),
logType: errLog,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
sdkCall := sdkMock.On("ViewBootstrap", mock.Anything, tc.args[0], tc.args[1], tc.args[2]).Return(tc.boot, tc.sdkErr)
sdkCall1 := sdkMock.On("Bootstraps", mock.Anything, mock.Anything, tc.args[1], tc.args[2]).Return(tc.page, tc.sdkErr)
out := executeCommand(t, rootCmd, append([]string{getCmd}, tc.args...)...)
switch tc.logType {
case entityLog:
if tc.args[0] == all {
err := json.Unmarshal([]byte(out), &page)
assert.Nil(t, err)
assert.Equal(t, tc.page, page, fmt.Sprintf("%v unexpected response, expected: %v, got: %v", tc.desc, tc.page, page))
} else {
err := json.Unmarshal([]byte(out), &boot)
assert.Nil(t, err)
assert.Equal(t, tc.boot, boot, fmt.Sprintf("%v unexpected response, expected: %v, got: %v", tc.desc, tc.boot, boot))
}
case errLog:
assert.Equal(t, tc.errLogMessage, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.errLogMessage, out))
case usageLog:
assert.False(t, strings.Contains(out, rootCmd.Use), fmt.Sprintf("%s invalid usage: %s", tc.desc, out))
}
sdkCall.Unset()
sdkCall1.Unset()
})
}
}
func TestRemoveBootstrapConfigCmd(t *testing.T) {
sdkMock := new(sdkmocks.SDK)
cli.SetSDK(sdkMock)
bootCmd := cli.NewBootstrapCmd()
rootCmd := setFlags(bootCmd)
cases := []struct {
desc string
args []string
sdkErr errors.SDKError
logType outputLog
errLogMessage string
}{
{
desc: "remove bootstrap config successfully",
args: []string{
clientID,
domainID,
validToken,
},
logType: okLog,
},
{
desc: "remove bootstrap config with invalid args",
args: []string{
clientID,
domainID,
validToken,
extraArg,
},
logType: usageLog,
},
{
desc: "remove bootstrap config with invalid client id",
args: []string{
invalidID,
domainID,
validToken,
},
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden)),
logType: errLog,
},
{
desc: "remove bootstrap config with invalid token",
args: []string{
clientID,
domainID,
invalidToken,
},
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden)),
logType: errLog,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
sdkCall := sdkMock.On("RemoveBootstrap", mock.Anything, tc.args[0], tc.args[1], tc.args[2]).Return(tc.sdkErr)
out := executeCommand(t, rootCmd, append([]string{rmCmd}, tc.args...)...)
switch tc.logType {
case okLog:
assert.True(t, strings.Contains(out, "ok"), fmt.Sprintf("%s unexpected response: expected success message, got: %v", tc.desc, out))
case errLog:
assert.Equal(t, tc.errLogMessage, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.errLogMessage, out))
case usageLog:
assert.False(t, strings.Contains(out, rootCmd.Use), fmt.Sprintf("%s invalid usage: %s", tc.desc, out))
}
sdkCall.Unset()
})
}
}
func TestUpdateBootstrapConfigCmd(t *testing.T) {
sdkMock := new(sdkmocks.SDK)
cli.SetSDK(sdkMock)
bootCmd := cli.NewBootstrapCmd()
rootCmd := setFlags(bootCmd)
config := "config"
connection := "connection"
newConfigJson := "{\"name\" : \"New Bootstrap\"}"
chanIDsJson := fmt.Sprintf("[\"%s\"]", channelID)
cases := []struct {
desc string
args []string
boot mgsdk.BootstrapConfig
sdkErr errors.SDKError
errLogMessage string
logType outputLog
}{
{
desc: "update bootstrap config successfully",
args: []string{
config,
newConfigJson,
domainID,
validToken,
},
logType: okLog,
},
{
desc: "update bootstrap config with invalid token",
args: []string{
config,
newConfigJson,
domainID,
invalidToken,
},
logType: errLog,
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden)),
},
{
desc: "update bootstrap connections successfully",
args: []string{
connection,
clientID,
chanIDsJson,
domainID,
validToken,
},
logType: okLog,
},
{
desc: "update bootstrap connections with invalid json",
args: []string{
connection,
clientID,
fmt.Sprintf("[\"%s\"", clientID),
domainID,
validToken,
},
sdkErr: errors.NewSDKError(errors.New("unexpected end of JSON input")),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.New("unexpected end of JSON input")),
logType: errLog,
},
{
desc: "update bootstrap connections with invalid token",
args: []string{
connection,
clientID,
chanIDsJson,
domainID,
invalidToken,
},
logType: errLog,
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden)),
},
{
desc: "update bootstrap certs successfully",
args: []string{
"certs",
clientID,
"client cert",
"client key",
"ca",
domainID,
validToken,
},
boot: bootConfig,
logType: entityLog,
},
{
desc: "update bootstrap certs with invalid token",
args: []string{
"certs",
clientID,
"client cert",
"client key",
"ca",
domainID,
invalidToken,
},
logType: errLog,
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden)),
},
{
desc: "update bootstrap config with invalid args",
args: []string{
newConfigJson,
domainID,
validToken,
},
logType: usageLog,
},
{
desc: "update bootstrap config with invalid json",
args: []string{
config,
"{\"name\" : \"New Bootstrap\"",
domainID,
validToken,
},
sdkErr: errors.NewSDKError(errors.New("unexpected end of JSON input")),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.New("unexpected end of JSON input")),
logType: errLog,
},
{
desc: "update bootstrap with invalid args",
args: []string{
extraArg,
extraArg,
extraArg,
extraArg,
extraArg,
},
logType: usageLog,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
var boot mgsdk.BootstrapConfig
sdkCall := sdkMock.On("UpdateBootstrap", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.sdkErr)
sdkCall1 := sdkMock.On("UpdateBootstrapConnection", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.sdkErr)
sdkCall2 := sdkMock.On("UpdateBootstrapCerts", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.boot, tc.sdkErr)
out := executeCommand(t, rootCmd, append([]string{updCmd}, tc.args...)...)
switch tc.logType {
case entityLog:
err := json.Unmarshal([]byte(out), &boot)
assert.Nil(t, err)
assert.Equal(t, tc.boot, boot, fmt.Sprintf("%s unexpected response: expected: %v, got: %v", tc.desc, tc.boot, boot))
case okLog:
assert.True(t, strings.Contains(out, "ok"), fmt.Sprintf("%s unexpected response: expected success message, got: %v", tc.desc, out))
case usageLog:
assert.False(t, strings.Contains(out, rootCmd.Use), fmt.Sprintf("%s invalid usage: %s", tc.desc, out))
case errLog:
assert.Equal(t, tc.errLogMessage, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.errLogMessage, out))
}
sdkCall.Unset()
sdkCall1.Unset()
sdkCall2.Unset()
})
}
}
func TestWhitelistConfigCmd(t *testing.T) {
sdkMock := new(sdkmocks.SDK)
cli.SetSDK(sdkMock)
bootCmd := cli.NewBootstrapCmd()
rootCmd := setFlags(bootCmd)
jsonConfig := fmt.Sprintf("{\"client_id\": \"%s\", \"state\":%d}", clientID, 1)
cases := []struct {
desc string
args []string
logType outputLog
errLogMessage string
sdkErr errors.SDKError
}{
{
desc: "whitelist config successfully",
args: []string{
jsonConfig,
domainID,
validToken,
},
logType: okLog,
},
{
desc: "whitelist config with invalid args",
args: []string{
jsonConfig,
domainID,
validToken,
extraArg,
},
logType: usageLog,
},
{
desc: "whitelist config with invalid json",
args: []string{
fmt.Sprintf("{\"client_id\": \"%s\", \"state\":%d", clientID, 1),
domainID,
validToken,
},
sdkErr: errors.NewSDKError(errors.New("unexpected end of JSON input")),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.New("unexpected end of JSON input")),
logType: errLog,
},
{
desc: "whitelist config with invalid token",
args: []string{
jsonConfig,
domainID,
invalidToken,
},
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusUnauthorized),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusUnauthorized)),
logType: errLog,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
sdkCall := sdkMock.On("Whitelist", mock.Anything, mock.Anything, mock.Anything, tc.args[1], tc.args[2]).Return(tc.sdkErr)
out := executeCommand(t, rootCmd, append([]string{whitelistCmd}, tc.args...)...)
switch tc.logType {
case okLog:
assert.True(t, strings.Contains(out, "ok"), fmt.Sprintf("%s unexpected response: expected success message, got: %v", tc.desc, out))
case usageLog:
assert.False(t, strings.Contains(out, rootCmd.Use), fmt.Sprintf("%s invalid usage: %s", tc.desc, out))
case errLog:
assert.Equal(t, tc.errLogMessage, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.errLogMessage, out))
}
sdkCall.Unset()
})
}
}
func TestBootstrapConfigCmd(t *testing.T) {
sdkMock := new(sdkmocks.SDK)
cli.SetSDK(sdkMock)
bootCmd := cli.NewBootstrapCmd()
rootCmd := setFlags(bootCmd)
var boot mgsdk.BootstrapConfig
cryptoKey := "v7aT0HGxJxt2gULzr3RHwf4WIf6DusPp"
invalidKey := "invalid key"
cases := []struct {
desc string
args []string
logType outputLog
errLogMessage string
sdkErr errors.SDKError
boot mgsdk.BootstrapConfig
}{
{
desc: "bootstrap secure config successfully",
args: []string{
"secure",
bootConfig.ExternalID,
bootConfig.ExternalKey,
cryptoKey,
},
boot: bootConfig,
logType: entityLog,
},
{
desc: "bootstrap config successfully",
args: []string{
bootConfig.ExternalID,
bootConfig.ExternalKey,
},
boot: bootConfig,
logType: entityLog,
},
{
desc: "bootstrap secure config with invalid args",
args: []string{
cryptoKey,
},
logType: usageLog,
},
{
desc: "bootstrap secure config with invalid key",
args: []string{
"secure",
bootConfig.ExternalID,
invalidKey,
cryptoKey,
},
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusUnauthorized),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusUnauthorized)),
logType: errLog,
},
{
desc: "bootstrap config with invalid key",
args: []string{
bootConfig.ExternalID,
invalidKey,
},
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusUnauthorized),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusUnauthorized)),
logType: errLog,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
sdkCall := sdkMock.On("BootstrapSecure", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.boot, tc.sdkErr)
sdkCall1 := sdkMock.On("Bootstrap", mock.Anything, mock.Anything, mock.Anything).Return(tc.boot, tc.sdkErr)
out := executeCommand(t, rootCmd, append([]string{bootStrapCmd}, tc.args...)...)
switch tc.logType {
case entityLog:
err := json.Unmarshal([]byte(out), &boot)
assert.Nil(t, err)
assert.Equal(t, tc.boot, boot, fmt.Sprintf("%s unexpected response: expected: %v, got: %v", tc.desc, tc.boot, boot))
case usageLog:
assert.False(t, strings.Contains(out, rootCmd.Use), fmt.Sprintf("%s invalid usage: %s", tc.desc, out))
case errLog:
assert.Equal(t, tc.errLogMessage, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.errLogMessage, out))
}
sdkCall.Unset()
sdkCall1.Unset()
})
}
}
+342
View File
@@ -0,0 +1,342 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package cli
import (
"encoding/json"
"os"
"github.com/absmach/supermq/certs"
smqsdk "github.com/absmach/supermq/pkg/sdk"
"github.com/spf13/cobra"
)
var cmdCerts = []cobra.Command{
{
Use: "get [all | <entity_id>] <domain_id> <token>",
Short: "Get certificate",
Long: `Gets a certificate for a given entity ID or all certificates.`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 3 {
logUsageCmd(*cmd, cmd.Use)
return
}
if args[0] == all {
pm := smqsdk.PageMetadata{
Limit: Limit,
Offset: Offset,
}
page, err := sdk.ListCerts(cmd.Context(), pm, args[1], args[2])
if err != nil {
logErrorCmd(*cmd, err)
return
}
logJSONCmd(*cmd, page)
return
}
pm := smqsdk.PageMetadata{
EntityID: args[0],
Limit: Limit,
Offset: Offset,
}
page, err := sdk.ListCerts(cmd.Context(), pm, args[1], args[2])
if err != nil {
logErrorCmd(*cmd, err)
return
}
logJSONCmd(*cmd, page)
},
},
{
Use: "revoke <serial_number> <domain_id> <token>",
Short: "Revoke certificate",
Long: `Revokes a certificate for a given serial number.`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 3 {
logUsageCmd(*cmd, cmd.Use)
return
}
err := sdk.RevokeCert(cmd.Context(), args[0], args[1], args[2])
if err != nil {
logErrorCmd(*cmd, err)
return
}
logOKCmd(*cmd)
},
},
{
Use: "delete <entity_id> <domain_id> <token>",
Short: "Delete certificate",
Long: `Deletes certificates for a given entity id.`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 3 {
logUsageCmd(*cmd, cmd.Use)
return
}
err := sdk.DeleteCert(cmd.Context(), args[0], args[1], args[2])
if err != nil {
logErrorCmd(*cmd, err)
return
}
logOKCmd(*cmd)
},
},
{
Use: "renew <serial_number> <domain_id> <token>",
Short: "Renew certificate",
Long: `Renews a certificate for a given serial number.`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 3 {
logUsageCmd(*cmd, cmd.Use)
return
}
_, err := sdk.RenewCert(cmd.Context(), args[0], args[1], args[2])
if err != nil {
logErrorCmd(*cmd, err)
return
}
logOKCmd(*cmd)
},
},
{
Use: "ocsp <serial_number_or_certificate_path>",
Short: "OCSP",
Long: `OCSP for a given serial number or certificate.`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 1 {
logUsageCmd(*cmd, cmd.Use)
return
}
var serialNumber, certContent string
if _, statErr := os.Stat(args[0]); statErr == nil {
certBytes, err := os.ReadFile(args[0])
if err != nil {
logErrorCmd(*cmd, err)
return
}
certContent = string(certBytes)
} else {
serialNumber = args[0]
}
response, err := sdk.OCSP(cmd.Context(), serialNumber, certContent)
if err != nil {
logErrorCmd(*cmd, err)
return
}
logJSONCmd(*cmd, response)
},
},
{
Use: "view <serial_number> <domain_id> <token>",
Short: "View certificate",
Long: `Views a certificate for a given serial number.`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 3 {
logUsageCmd(*cmd, cmd.Use)
return
}
cert, err := sdk.ViewCert(cmd.Context(), args[0], args[1], args[2])
if err != nil {
logErrorCmd(*cmd, err)
return
}
logJSONCmd(*cmd, cert)
},
},
{
Use: "view-ca",
Short: "View-ca certificate",
Long: `Views ca certificate.`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 0 {
logUsageCmd(*cmd, cmd.Use)
return
}
cert, err := sdk.ViewCA(cmd.Context())
if err != nil {
logErrorCmd(*cmd, err)
return
}
logJSONCmd(*cmd, cert)
},
},
{
Use: "download-ca",
Short: "Download signing CA",
Long: `Download intermediate cert and ca.`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 0 {
logUsageCmd(*cmd, cmd.Use)
return
}
bundle, err := sdk.DownloadCA(cmd.Context())
if err != nil {
logErrorCmd(*cmd, err)
return
}
logSaveCAFiles(*cmd, bundle)
},
},
{
Use: "csr <metadata> <private_key_path>",
Short: "Create CSR",
Long: `Creates a CSR.`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsageCmd(*cmd, cmd.Use)
return
}
var pm certs.CSRMetadata
if err := json.Unmarshal([]byte(args[0]), &pm); err != nil {
logErrorCmd(*cmd, err)
return
}
data, err := os.ReadFile(args[1])
if err != nil {
logErrorCmd(*cmd, err)
return
}
csr, err := sdk.CreateCSR(cmd.Context(), pm, data)
if err != nil {
logErrorCmd(*cmd, err)
return
}
logSaveCSRFiles(*cmd, csr)
},
},
{
Use: "issue-csr <entity_id> <ttl> <path_to_csr> <domain_id> <token>",
Short: "Issue from CSR",
Long: `issues a certificate for a given csr.`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 5 {
logUsageCmd(*cmd, cmd.Use)
return
}
csrData, err := os.ReadFile(args[2])
if err != nil {
logErrorCmd(*cmd, err)
return
}
cert, err := sdk.IssueFromCSR(cmd.Context(), args[0], args[1], string(csrData), args[3], args[4])
if err != nil {
logErrorCmd(*cmd, err)
return
}
logJSONCmd(*cmd, cert)
logSaveCertFiles(*cmd, cert)
},
},
{
Use: "issue-csr-internal <entity_id> <ttl> <path_to_csr> <agent_token>",
Short: "Issue from CSR Internal (Agent)",
Long: `Issues a certificate for a given CSR using agent authentication.`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 4 {
logUsageCmd(*cmd, cmd.Use)
return
}
csrData, err := os.ReadFile(args[2])
if err != nil {
logErrorCmd(*cmd, err)
return
}
cert, err := sdk.IssueFromCSRInternal(cmd.Context(), args[0], args[1], string(csrData), args[3])
if err != nil {
logErrorCmd(*cmd, err)
return
}
logJSONCmd(*cmd, cert)
logSaveCertFiles(*cmd, cert)
},
},
{
Use: "crl",
Short: "Generate CRL",
Long: `Generates a Certificate Revocation List (CRL).`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 0 {
logUsageCmd(*cmd, cmd.Use)
return
}
crlBytes, err := sdk.GenerateCRL(cmd.Context())
if err != nil {
logErrorCmd(*cmd, err)
return
}
logSaveCRLFile(*cmd, crlBytes)
},
},
{
Use: "entity-id <serial_number> <domain_id> <token>",
Short: "Get entity ID by serial number",
Long: `Gets the entity ID for a certificate by its serial number.`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 3 {
logUsageCmd(*cmd, cmd.Use)
return
}
entityID, err := sdk.EntityID(cmd.Context(), args[0], args[1], args[2])
if err != nil {
logErrorCmd(*cmd, err)
return
}
logJSONCmd(*cmd, map[string]string{"entity_id": entityID})
},
},
}
// NewCertsCmd returns certificate command.
func NewCertsCmd() *cobra.Command {
var ttl string
issueCmd := cobra.Command{
Use: "issue <entity_id> <common_name> <ip_addrs_json> [<options_json>] <domain_id> <token> [--ttl=8760h]",
Short: "Issue certificate",
Long: `Issues a certificate for a given entity ID.`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) < 5 || len(args) > 6 {
logUsageCmd(*cmd, cmd.Use)
return
}
var ipAddrs []string
if err := json.Unmarshal([]byte(args[2]), &ipAddrs); err != nil {
logErrorCmd(*cmd, err)
return
}
var option smqsdk.Options
option.CommonName = args[1]
var domainID, token string
if len(args) == 5 {
domainID = args[3]
token = args[4]
} else {
if err := json.Unmarshal([]byte(args[3]), &option); err != nil {
logErrorCmd(*cmd, err)
return
}
domainID = args[4]
token = args[5]
}
cert, err := sdk.IssueCert(cmd.Context(), args[0], ttl, ipAddrs, option, domainID, token)
if err != nil {
logErrorCmd(*cmd, err)
return
}
logJSONCmd(*cmd, cert)
logSaveCertFiles(*cmd, cert)
},
}
issueCmd.Flags().StringVar(&ttl, "ttl", "8760h", "certificate time to live in duration")
cmd := cobra.Command{
Use: "certs [issue | get | revoke | renew | ocsp | view | download-ca | view-ca | csr | issue-csr | issue-csr-internal | crl | entity-id]",
Short: "Certificates management",
Long: `Certificates management: issue, get all, get by entity ID, revoke, renew, OCSP, view, CRL generation, entity ID lookup, agent CSR issuing, and CA operations.`,
}
cmd.AddCommand(&issueCmd)
for i := range cmdCerts {
cmd.AddCommand(&cmdCerts[i])
}
return &cmd
}
+905
View File
@@ -0,0 +1,905 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package cli_test
import (
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
"testing"
"github.com/absmach/supermq/certs"
"github.com/absmach/supermq/cli"
"github.com/absmach/supermq/pkg/errors"
"github.com/absmach/supermq/pkg/sdk"
sdkmocks "github.com/absmach/supermq/pkg/sdk/mocks"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
const (
revokeCmd = "revoke"
deleteCmd = "delete"
issueCmd = "issue"
renewCmd = "renew"
certsListCmd = "get"
downloadCACmd = "download-ca"
CATokenCmd = "certsToken-ca"
viewCACmd = "view-ca"
filePermission = 0o644
)
var (
serialNumber = "39054620502613157373429341617471746606"
id = "5b4c9ee3-e719-4a0a-9ee5-354932c5e6a4"
commonName = "test-name"
certsToken = "certsToken"
certsDomainID = "domain-id"
)
func TestIssueCertCmd(t *testing.T) {
sdkMock := new(sdkmocks.SDK)
cli.SetSDK(sdkMock)
certCmd := cli.NewCertsCmd()
rootCmd := setFlags(certCmd)
ipAddrs := "[\"192.168.100.22\"]"
var cert sdk.Certificate
cases := []struct {
desc string
args []string
sdkErr errors.SDKError
errLogMessage string
logType outputLog
cert sdk.Certificate
}{
{
desc: "issue cert successfully",
args: []string{
id,
commonName,
ipAddrs,
certsDomainID,
certsToken,
},
logType: entityLog,
cert: sdk.Certificate{SerialNumber: serialNumber},
},
{
desc: "issue cert with invalid args",
args: []string{
id,
ipAddrs,
},
logType: usageLog,
},
{
desc: "issue cert failed",
args: []string{
id,
commonName,
ipAddrs,
certsDomainID,
certsToken,
},
sdkErr: errors.NewSDKErrorWithStatus(certs.ErrCreateEntity, http.StatusUnprocessableEntity),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(certs.ErrCreateEntity, http.StatusUnprocessableEntity)),
logType: errLog,
},
{
desc: "issue cert with 6 args",
args: []string{
id,
commonName,
ipAddrs,
"{\"organization\":[\"organization_name\"]}",
certsDomainID,
certsToken,
},
logType: entityLog,
cert: sdk.Certificate{SerialNumber: serialNumber},
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
defer func() {
cleanupFiles(t, []string{"cert.pem", "key.pem"})
}()
sdkCall := sdkMock.On("IssueCert", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.cert, tc.sdkErr)
out := executeCommand(t, rootCmd, append([]string{issueCmd}, tc.args...)...)
switch tc.logType {
case entityLog:
lines := strings.Split(out, "\n")
var jsonLines []string
var inJSON bool
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "{") {
inJSON = true
jsonLines = append(jsonLines, line)
} else if inJSON && strings.HasSuffix(line, "}") {
jsonLines = append(jsonLines, line)
break
} else if inJSON {
jsonLines = append(jsonLines, line)
}
}
if len(jsonLines) == 0 {
t.Fatalf("No JSON found in output: %s", out)
}
jsonPart := strings.Join(jsonLines, "")
err := json.Unmarshal([]byte(jsonPart), &cert)
assert.Nil(t, err)
assert.Equal(t, tc.cert, cert, fmt.Sprintf("%s unexpected response: expected: %v, got: %v", tc.desc, tc.cert, cert))
assert.True(t, strings.Contains(out, "All certificate files have been saved successfully"), fmt.Sprintf("%s should save files", tc.desc))
case errLog:
assert.Equal(t, tc.errLogMessage, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.errLogMessage, out))
case usageLog:
assert.False(t, strings.Contains(out, rootCmd.Use), fmt.Sprintf("%s invalid usage: %s", tc.desc, out))
}
sdkCall.Unset()
})
}
}
func TestRevokeCertCmd(t *testing.T) {
sdkMock := new(sdkmocks.SDK)
cli.SetSDK(sdkMock)
certCmd := cli.NewCertsCmd()
rootCmd := setFlags(certCmd)
cases := []struct {
desc string
args []string
sdkErr errors.SDKError
errLogMessage string
logType outputLog
}{
{
desc: "revoke cert successfully",
args: []string{
serialNumber,
certsDomainID,
certsToken,
},
logType: okLog,
},
{
desc: "revoke cert with invalid args",
args: []string{
serialNumber,
extraArg,
},
logType: usageLog,
},
{
desc: "revoke cert failed",
args: []string{
serialNumber,
certsDomainID,
certsToken,
},
sdkErr: errors.NewSDKErrorWithStatus(certs.ErrUpdateEntity, http.StatusUnprocessableEntity),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(certs.ErrUpdateEntity, http.StatusUnprocessableEntity)),
logType: errLog,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
sdkCall := sdkMock.On("RevokeCert", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.sdkErr)
out := executeCommand(t, rootCmd, append([]string{revokeCmd}, tc.args...)...)
switch tc.logType {
case okLog:
assert.True(t, strings.Contains(out, "ok"), fmt.Sprintf("%s unexpected response: expected success message, got: %v", tc.desc, out))
case errLog:
assert.Equal(t, tc.errLogMessage, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.errLogMessage, out))
case usageLog:
assert.False(t, strings.Contains(out, rootCmd.Use), fmt.Sprintf("%s invalid usage: %s", tc.desc, out))
}
sdkCall.Unset()
})
}
}
func TestDeleteCertCmd(t *testing.T) {
sdkMock := new(sdkmocks.SDK)
cli.SetSDK(sdkMock)
certCmd := cli.NewCertsCmd()
rootCmd := setFlags(certCmd)
cases := []struct {
desc string
args []string
sdkErr errors.SDKError
errLogMessage string
logType outputLog
}{
{
desc: "delete certs successfully",
args: []string{
id,
certsDomainID,
certsToken,
},
logType: okLog,
},
{
desc: "delete certs with invalid args",
args: []string{
id,
extraArg,
},
logType: usageLog,
},
{
desc: "delete certs failed",
args: []string{
id,
certsDomainID,
certsToken,
},
sdkErr: errors.NewSDKErrorWithStatus(certs.ErrUpdateEntity, http.StatusUnprocessableEntity),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(certs.ErrUpdateEntity, http.StatusUnprocessableEntity)),
logType: errLog,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
sdkCall := sdkMock.On("DeleteCert", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.sdkErr)
out := executeCommand(t, rootCmd, append([]string{deleteCmd}, tc.args...)...)
switch tc.logType {
case okLog:
assert.True(t, strings.Contains(out, "ok"), fmt.Sprintf("%s unexpected response: expected success message, got: %v", tc.desc, out))
case errLog:
assert.Equal(t, tc.errLogMessage, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.errLogMessage, out))
case usageLog:
assert.False(t, strings.Contains(out, rootCmd.Use), fmt.Sprintf("%s invalid usage: %s", tc.desc, out))
}
sdkCall.Unset()
})
}
}
func TestRenewCertCmd(t *testing.T) {
sdkMock := new(sdkmocks.SDK)
cli.SetSDK(sdkMock)
certCmd := cli.NewCertsCmd()
rootCmd := setFlags(certCmd)
cases := []struct {
desc string
args []string
sdkErr errors.SDKError
errLogMessage string
logType outputLog
}{
{
desc: "renew cert successfully",
args: []string{
serialNumber,
certsDomainID,
certsToken,
},
logType: okLog,
},
{
desc: "renew cert with invalid args",
args: []string{
serialNumber,
extraArg,
},
logType: usageLog,
},
{
desc: "renew cert failed",
args: []string{
serialNumber,
certsDomainID,
certsToken,
},
sdkErr: errors.NewSDKErrorWithStatus(certs.ErrUpdateEntity, http.StatusUnprocessableEntity),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(certs.ErrUpdateEntity, http.StatusUnprocessableEntity)),
logType: errLog,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
sdkCall := sdkMock.On("RenewCert", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(sdk.Certificate{}, tc.sdkErr)
out := executeCommand(t, rootCmd, append([]string{renewCmd}, tc.args...)...)
switch tc.logType {
case okLog:
assert.True(t, strings.Contains(out, "ok"), fmt.Sprintf("%s unexpected response: expected success message, got: %v", tc.desc, out))
case errLog:
assert.Equal(t, tc.errLogMessage, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.errLogMessage, out))
case usageLog:
assert.False(t, strings.Contains(out, rootCmd.Use), fmt.Sprintf("%s invalid usage: %s", tc.desc, out))
}
sdkCall.Unset()
})
}
}
func TestListCertsCmd(t *testing.T) {
sdkMock := new(sdkmocks.SDK)
cli.SetSDK(sdkMock)
certCmd := cli.NewCertsCmd()
rootCmd := setFlags(certCmd)
var page sdk.CertificatePage
cases := []struct {
desc string
args []string
sdkErr errors.SDKError
errLogMessage string
logType outputLog
page sdk.CertificatePage
}{
{
desc: "list certs successfully",
args: []string{
all,
certsDomainID,
certsToken,
},
logType: entityLog,
page: sdk.CertificatePage{
Total: 1,
Offset: 0,
Limit: 10,
Certificates: []sdk.Certificate{
{SerialNumber: serialNumber},
},
},
},
{
desc: "list certs successfully with entity ID",
args: []string{
id,
certsDomainID,
certsToken,
},
logType: entityLog,
page: sdk.CertificatePage{
Total: 1,
Offset: 0,
Limit: 10,
Certificates: []sdk.Certificate{
{SerialNumber: serialNumber},
},
},
},
{
desc: "list certs with invalid args",
args: []string{
all,
extraArg,
},
logType: usageLog,
},
{
desc: "failed list certs with all",
args: []string{
all,
certsDomainID,
certsToken,
},
sdkErr: errors.NewSDKErrorWithStatus(certs.ErrViewEntity, http.StatusUnprocessableEntity),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(certs.ErrViewEntity, http.StatusUnprocessableEntity)),
logType: errLog,
},
{
desc: "failed list certs with entity ID",
args: []string{
id,
certsDomainID,
certsToken,
},
sdkErr: errors.NewSDKErrorWithStatus(certs.ErrViewEntity, http.StatusUnprocessableEntity),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(certs.ErrViewEntity, http.StatusUnprocessableEntity)),
logType: errLog,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
sdkCall := sdkMock.On("ListCerts", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.page, tc.sdkErr)
out := executeCommand(t, rootCmd, append([]string{certsListCmd}, tc.args...)...)
switch tc.logType {
case errLog:
assert.Equal(t, tc.errLogMessage, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.errLogMessage, out))
case usageLog:
assert.False(t, strings.Contains(out, rootCmd.Use), fmt.Sprintf("%s invalid usage: %s", tc.desc, out))
case entityLog:
err := json.Unmarshal([]byte(out), &page)
if err != nil {
t.Fatalf("Failed to unmarshal JSON: %v", err)
}
assert.Equal(t, tc.page, page, fmt.Sprintf("%v unexpected response, expected: %v, got: %v", tc.desc, tc.page, page))
}
sdkCall.Unset()
})
}
}
func TestDownloadCACmd(t *testing.T) {
sdkMock := new(sdkmocks.SDK)
cli.SetSDK(sdkMock)
certCmd := cli.NewCertsCmd()
rootCmd := setFlags(certCmd)
cases := []struct {
desc string
args []string
sdkErr errors.SDKError
errLogMessage string
logMessage string
logType outputLog
certBundle sdk.CertificateBundle
}{
{
desc: "download CA successfully",
args: []string{},
logType: entityLog,
certBundle: sdk.CertificateBundle{
Certificate: []byte("certificate"),
},
logMessage: "Saved ca.crt\n\nAll certificate files have been saved successfully.\n",
},
{
desc: "download CA with invalid args",
args: []string{
extraArg,
},
logType: usageLog,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
defer func() {
cleanupFiles(t, []string{"ca.crt"})
}()
sdkCall := sdkMock.On("DownloadCA", mock.Anything).Return(tc.certBundle, tc.sdkErr)
out := executeCommand(t, rootCmd, append([]string{downloadCACmd}, tc.args...)...)
switch tc.logType {
case entityLog:
assert.True(t, strings.Contains(out, "Saved ca.crt"), fmt.Sprintf("%s invalid output: %s", tc.desc, out))
case usageLog:
assert.False(t, strings.Contains(out, rootCmd.Use), fmt.Sprintf("%s invalid usage: %s", tc.desc, out))
case errLog:
assert.Equal(t, tc.errLogMessage, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.errLogMessage, out))
}
sdkCall.Unset()
})
}
}
func TestViewCACmd(t *testing.T) {
sdkMock := new(sdkmocks.SDK)
cli.SetSDK(sdkMock)
certCmd := cli.NewCertsCmd()
rootCmd := setFlags(certCmd)
var cert sdk.Certificate
cases := []struct {
desc string
args []string
sdkErr errors.SDKError
errLogMessage string
logType outputLog
cert sdk.Certificate
}{
{
desc: "view cert successfully",
args: []string{},
logType: entityLog,
cert: sdk.Certificate{
Certificate: "certificate",
Key: "privatekey",
},
},
{
desc: "view cert failed",
args: []string{},
sdkErr: errors.NewSDKErrorWithStatus(certs.ErrUpdateEntity, http.StatusUnprocessableEntity),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(certs.ErrUpdateEntity, http.StatusUnprocessableEntity)),
logType: errLog,
cert: sdk.Certificate{},
},
{
desc: "view cert with invalid args",
args: []string{extraArg},
logType: usageLog,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
sdkCall := sdkMock.On("ViewCA", mock.Anything).Return(tc.cert, tc.sdkErr)
out := executeCommand(t, rootCmd, append([]string{viewCACmd}, tc.args...)...)
switch tc.logType {
case entityLog:
err := json.Unmarshal([]byte(out), &cert)
assert.Nil(t, err)
assert.Equal(t, tc.cert, cert, fmt.Sprintf("%s unexpected response: expected: %v, got: %v", tc.desc, tc.cert, cert))
case usageLog:
assert.False(t, strings.Contains(out, rootCmd.Use), fmt.Sprintf("%s invalid usage: %s", tc.desc, out))
case errLog:
assert.Equal(t, tc.errLogMessage, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.errLogMessage, out))
}
sdkCall.Unset()
})
}
}
func TestGenerateCRLCmd(t *testing.T) {
sdkMock := new(sdkmocks.SDK)
cli.SetSDK(sdkMock)
certCmd := cli.NewCertsCmd()
rootCmd := setFlags(certCmd)
cases := []struct {
desc string
args []string
sdkErr errors.SDKError
errLogMessage string
logType outputLog
crlBytes []byte
}{
{
desc: "generate CRL successfully",
args: []string{},
logType: entityLog,
crlBytes: []byte("crl-data"),
},
{
desc: "generate CRL failed",
args: []string{},
sdkErr: errors.NewSDKErrorWithStatus(certs.ErrFailedCertCreation, http.StatusUnprocessableEntity),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(certs.ErrFailedCertCreation, http.StatusUnprocessableEntity)),
logType: errLog,
},
{
desc: "generate CRL with invalid args",
args: []string{"invalid"},
logType: usageLog,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
defer func() {
cleanupFiles(t, []string{"ca.crl"})
}()
sdkCall := sdkMock.On("GenerateCRL", mock.Anything).Return(tc.crlBytes, tc.sdkErr)
defer sdkCall.Unset()
out := executeCommand(t, rootCmd, append([]string{"crl"}, tc.args...)...)
switch tc.logType {
case entityLog:
assert.True(t, strings.Contains(out, "CRL file has been saved successfully"), fmt.Sprintf("%s invalid output: %s", tc.desc, out))
case usageLog:
assert.False(t, strings.Contains(out, rootCmd.Use), fmt.Sprintf("%s invalid usage: %s", tc.desc, out))
case errLog:
assert.Equal(t, tc.errLogMessage, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.errLogMessage, out))
}
})
}
}
func TestGetEntityIDCmd(t *testing.T) {
sdkMock := new(sdkmocks.SDK)
cli.SetSDK(sdkMock)
certCmd := cli.NewCertsCmd()
rootCmd := setFlags(certCmd)
entityID := "test-entity-id"
cases := []struct {
desc string
args []string
sdkErr errors.SDKError
errLogMessage string
logType outputLog
entityID string
}{
{
desc: "get entity ID successfully",
args: []string{serialNumber, certsDomainID, certsToken},
logType: entityLog,
entityID: entityID,
},
{
desc: "get entity ID with invalid args",
args: []string{serialNumber, extraArg},
logType: usageLog,
},
{
desc: "get entity ID failed",
args: []string{serialNumber, certsDomainID, certsToken},
sdkErr: errors.NewSDKErrorWithStatus(certs.ErrViewEntity, http.StatusUnprocessableEntity),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(certs.ErrViewEntity, http.StatusUnprocessableEntity)),
logType: errLog,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
sdkCall := sdkMock.On("EntityID", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.entityID, tc.sdkErr)
out := executeCommand(t, rootCmd, append([]string{"entity-id"}, tc.args...)...)
switch tc.logType {
case entityLog:
assert.True(t, strings.Contains(out, tc.entityID), fmt.Sprintf("%s invalid output: %s", tc.desc, out))
case usageLog:
assert.False(t, strings.Contains(out, rootCmd.Use), fmt.Sprintf("%s invalid usage: %s", tc.desc, out))
case errLog:
assert.Equal(t, tc.errLogMessage, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.errLogMessage, out))
}
sdkCall.Unset()
})
}
}
func cleanupFiles(t *testing.T, filenames []string) {
for _, filename := range filenames {
err := os.Remove(filename)
if err != nil && !os.IsNotExist(err) {
t.Logf("Failed to remove file %s: %v", filename, err)
}
}
}
func TestIssueFromCSRInternalCmd(t *testing.T) {
sdkMock := new(sdkmocks.SDK)
cli.SetSDK(sdkMock)
certCmd := cli.NewCertsCmd()
rootCmd := setFlags(certCmd)
agentToken := "agent-certsToken-123"
csrPath := "test.csr"
bytes := []byte("-----BEGIN CERTIFICATE REQUEST-----\n-csr-content\n-----END CERTIFICATE REQUEST-----")
err := os.WriteFile(csrPath, bytes, filePermission)
if err != nil {
t.Fatalf("Failed to create test CSR file: %v", err)
}
defer os.Remove(csrPath)
var cert sdk.Certificate
cases := []struct {
desc string
args []string
sdkErr errors.SDKError
errLogMessage string
logType outputLog
cert sdk.Certificate
}{
{
desc: "issue cert from CSR internal successfully",
args: []string{
id,
"10h",
csrPath,
agentToken,
},
logType: entityLog,
cert: sdk.Certificate{SerialNumber: serialNumber},
},
{
desc: "issue cert from CSR internal with invalid args",
args: []string{
id,
extraArg,
},
logType: usageLog,
},
{
desc: "issue cert from CSR internal failed",
args: []string{
id,
"10h",
csrPath,
agentToken,
},
sdkErr: errors.NewSDKErrorWithStatus(certs.ErrFailedCertCreation, http.StatusUnprocessableEntity),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(certs.ErrFailedCertCreation, http.StatusUnprocessableEntity)),
logType: errLog,
},
{
desc: "issue cert from CSR internal with non-existent file",
args: []string{
id,
"10h",
"non-existent.csr",
agentToken,
},
logType: errLog,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
defer func() {
cleanupFiles(t, []string{"cert.pem", "key.pem"})
}()
sdkCall := sdkMock.On("IssueFromCSRInternal", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.cert, tc.sdkErr)
out := executeCommand(t, rootCmd, append([]string{"issue-csr-internal"}, tc.args...)...)
switch tc.logType {
case entityLog:
lines := strings.Split(out, "\n")
var jsonLines []string
var inJSON bool
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "{") {
inJSON = true
jsonLines = append(jsonLines, line)
} else if inJSON && strings.HasSuffix(line, "}") {
jsonLines = append(jsonLines, line)
break
} else if inJSON {
jsonLines = append(jsonLines, line)
}
}
if len(jsonLines) == 0 {
t.Fatalf("No JSON found in output: %s", out)
}
jsonPart := strings.Join(jsonLines, "")
err := json.Unmarshal([]byte(jsonPart), &cert)
assert.Nil(t, err)
assert.Equal(t, tc.cert, cert, fmt.Sprintf("%s unexpected response: expected: %v, got: %v", tc.desc, tc.cert, cert))
assert.True(t, strings.Contains(out, "All certificate files have been saved successfully"), fmt.Sprintf("%s should save files", tc.desc))
case errLog:
if tc.errLogMessage != "" {
assert.Equal(t, tc.errLogMessage, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.errLogMessage, out))
} else {
assert.True(t, strings.Contains(out, "error"), fmt.Sprintf("%s should contain error message: %s", tc.desc, out))
}
case usageLog:
assert.False(t, strings.Contains(out, rootCmd.Use), fmt.Sprintf("%s invalid usage: %s", tc.desc, out))
}
sdkCall.Unset()
})
}
}
func TestIssueFromCSRCmd(t *testing.T) {
sdkMock := new(sdkmocks.SDK)
cli.SetSDK(sdkMock)
certCmd := cli.NewCertsCmd()
rootCmd := setFlags(certCmd)
csrPath := "test.csr"
bytes := []byte("-----BEGIN CERTIFICATE REQUEST-----\n-csr-content\n-----END CERTIFICATE REQUEST-----")
err := os.WriteFile(csrPath, bytes, filePermission)
if err != nil {
t.Fatalf("Failed to create test CSR file: %v", err)
}
defer os.Remove(csrPath)
var cert sdk.Certificate
cases := []struct {
desc string
args []string
sdkErr errors.SDKError
errLogMessage string
logType outputLog
cert sdk.Certificate
}{
{
desc: "issue cert from CSR successfully",
args: []string{
id,
"10h",
csrPath,
certsDomainID,
certsToken,
},
logType: entityLog,
cert: sdk.Certificate{SerialNumber: serialNumber},
},
{
desc: "issue cert from CSR with invalid args",
args: []string{
id,
extraArg,
},
logType: usageLog,
},
{
desc: "issue cert from CSR failed",
args: []string{
id,
"10h",
csrPath,
certsDomainID,
certsToken,
},
sdkErr: errors.NewSDKErrorWithStatus(certs.ErrFailedCertCreation, http.StatusUnprocessableEntity),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(certs.ErrFailedCertCreation, http.StatusUnprocessableEntity)),
logType: errLog,
},
{
desc: "issue cert from CSR with non-existent file",
args: []string{
id,
"10h",
"non-existent.csr",
certsDomainID,
certsToken,
},
logType: errLog,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
defer func() {
cleanupFiles(t, []string{"cert.pem", "key.pem"})
}()
sdkCall := sdkMock.On("IssueFromCSR", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.cert, tc.sdkErr)
out := executeCommand(t, rootCmd, append([]string{"issue-csr"}, tc.args...)...)
switch tc.logType {
case entityLog:
lines := strings.Split(out, "\n")
var jsonLines []string
var inJSON bool
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "{") {
inJSON = true
jsonLines = append(jsonLines, line)
} else if inJSON && strings.HasSuffix(line, "}") {
jsonLines = append(jsonLines, line)
break
} else if inJSON {
jsonLines = append(jsonLines, line)
}
}
if len(jsonLines) == 0 {
t.Fatalf("No JSON found in output: %s", out)
}
jsonPart := strings.Join(jsonLines, "")
err := json.Unmarshal([]byte(jsonPart), &cert)
assert.Nil(t, err)
assert.Equal(t, tc.cert, cert, fmt.Sprintf("%s unexpected response: expected: %v, got: %v", tc.desc, tc.cert, cert))
assert.True(t, strings.Contains(out, "All certificate files have been saved successfully"), fmt.Sprintf("%s should save files", tc.desc))
case errLog:
if tc.errLogMessage != "" {
assert.Equal(t, tc.errLogMessage, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.errLogMessage, out))
} else {
assert.True(t, strings.Contains(out, "error"), fmt.Sprintf("%s should contain error message: %s", tc.desc, out))
}
case usageLog:
assert.False(t, strings.Contains(out, rootCmd.Use), fmt.Sprintf("%s invalid usage: %s", tc.desc, out))
}
sdkCall.Unset()
})
}
}
-2
View File
@@ -24,9 +24,7 @@ import (
var (
token = "valid" + "domaintoken"
domainID = "domain-id"
relation = "administrator"
all = "all"
conntype = `["publish","subscribe"]`
errEndJSONInput = errors.New("unexpected end of JSON input")
+8
View File
@@ -51,3 +51,11 @@ const (
listCmd = "list"
membersCmd = "members"
)
// Bootstrap commands
const (
updCmd = "update"
rmCmd = "remove"
whitelistCmd = "whitelist"
bootStrapCmd = "bootstrap"
)
+100
View File
@@ -0,0 +1,100 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package cli
import (
mgsdk "github.com/absmach/supermq/pkg/sdk"
"github.com/spf13/cobra"
)
var cmdSubscription = []cobra.Command{
{
Use: "create <topic> <contact> <user_auth_token>",
Short: "Create subscription",
Long: `Create new subscription`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 3 {
logUsageCmd(*cmd, cmd.Use)
return
}
id, err := sdk.CreateSubscription(cmd.Context(), args[0], args[1], args[2])
if err != nil {
logErrorCmd(*cmd, err)
return
}
logCreatedCmd(*cmd, id)
},
},
{
Use: "get [all | <sub_id>] <user_auth_token>",
Short: "Get subscription",
Long: `Get subscription.
all - lists all subscriptions
<sub_id> - view subscription of <sub_id>`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsageCmd(*cmd, cmd.Use)
return
}
pageMetadata := mgsdk.PageMetadata{
Offset: Offset,
Limit: Limit,
Topic: Topic,
Contact: Contact,
}
if args[0] == all {
sub, err := sdk.ListSubscriptions(cmd.Context(), pageMetadata, args[1])
if err != nil {
logErrorCmd(*cmd, err)
return
}
logJSONCmd(*cmd, sub)
return
}
c, err := sdk.ViewSubscription(cmd.Context(), args[0], args[1])
if err != nil {
logErrorCmd(*cmd, err)
return
}
logJSONCmd(*cmd, c)
},
},
{
Use: "remove <sub_id> <user_auth_token>",
Short: "Remove subscription",
Long: `Removes removes a subscription with the provided id`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsageCmd(*cmd, cmd.Use)
return
}
if err := sdk.DeleteSubscription(cmd.Context(), args[0], args[1]); err != nil {
logErrorCmd(*cmd, err)
return
}
logOKCmd(*cmd)
},
},
}
// NewSubscriptionCmd returns subscription command.
func NewSubscriptionCmd() *cobra.Command {
cmd := cobra.Command{
Use: "subscription [create | get | remove ]",
Short: "Subscription management",
Long: `Subscription management: create, get, or delete subscription`,
}
for i := range cmdSubscription {
cmd.AddCommand(&cmdSubscription[i])
}
return &cmd
}
+266
View File
@@ -0,0 +1,266 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package cli_test
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"testing"
"github.com/absmach/supermq/cli"
"github.com/absmach/supermq/internal/testsutil"
"github.com/absmach/supermq/pkg/errors"
svcerr "github.com/absmach/supermq/pkg/errors/service"
mgsdk "github.com/absmach/supermq/pkg/sdk"
sdkmocks "github.com/absmach/supermq/pkg/sdk/mocks"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
var (
userID = testsutil.GenerateUUID(&testing.T{})
subscription = mgsdk.Subscription{
ID: testsutil.GenerateUUID(&testing.T{}),
OwnerID: userID,
Topic: "topic",
Contact: "identity@example.com",
}
)
func TestCreateSubscriptionCmd(t *testing.T) {
sdkMock := new(sdkmocks.SDK)
cli.SetSDK(sdkMock)
subCmd := cli.NewSubscriptionCmd()
rootCmd := setFlags(subCmd)
cases := []struct {
desc string
args []string
logType outputLog
errLogMessage string
sdkErr errors.SDKError
response string
id string
}{
{
desc: "create subscription successfully",
args: []string{
subscription.Topic,
subscription.Contact,
validToken,
},
id: userID,
response: fmt.Sprintf("\ncreated: %s\n\n", userID),
logType: createLog,
},
{
desc: "create subscription with invalid args",
args: []string{
subscription.Topic,
subscription.Contact,
validToken,
extraArg,
},
logType: usageLog,
},
{
desc: "create subscription with invalid token",
args: []string{
subscription.Topic,
subscription.Contact,
invalidToken,
},
logType: errLog,
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden)),
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
sdkCall := sdkMock.On("CreateSubscription", mock.Anything, tc.args[0], tc.args[1], tc.args[2]).Return(tc.id, tc.sdkErr)
out := executeCommand(t, rootCmd, append([]string{createCmd}, tc.args...)...)
switch tc.logType {
case usageLog:
assert.False(t, strings.Contains(out, rootCmd.Use), fmt.Sprintf("%s invalid usage: %s", tc.desc, out))
case errLog:
assert.Equal(t, tc.errLogMessage, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.errLogMessage, out))
case createLog:
assert.Equal(t, tc.response, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.response, out))
}
sdkCall.Unset()
})
}
}
func TestGetSubscriptionsCmd(t *testing.T) {
sdkMock := new(sdkmocks.SDK)
cli.SetSDK(sdkMock)
subCmd := cli.NewSubscriptionCmd()
rootCmd := setFlags(subCmd)
var sub mgsdk.Subscription
var page mgsdk.SubscriptionPage
cases := []struct {
desc string
args []string
sdkErr errors.SDKError
page mgsdk.SubscriptionPage
subscription mgsdk.Subscription
logType outputLog
errLogMessage string
}{
{
desc: "get all subscriptions successfully",
args: []string{
all,
validToken,
},
page: mgsdk.SubscriptionPage{
Subscriptions: []mgsdk.Subscription{subscription},
},
logType: entityLog,
},
{
desc: "get subscription with id",
args: []string{
subscription.ID,
validToken,
},
logType: entityLog,
subscription: subscription,
},
{
desc: "get subscriptions with invalid args",
args: []string{
all,
validToken,
extraArg,
},
logType: usageLog,
},
{
desc: "get all subscriptions with invalid token",
args: []string{
all,
invalidToken,
},
logType: errLog,
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden)),
},
{
desc: "get subscription with invalid id",
args: []string{
invalidID,
validToken,
},
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden)),
logType: errLog,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
sdkCall := sdkMock.On("ViewSubscription", mock.Anything, tc.args[0], tc.args[1]).Return(tc.subscription, tc.sdkErr)
sdkCall1 := sdkMock.On("ListSubscriptions", mock.Anything, mock.Anything, tc.args[1]).Return(tc.page, tc.sdkErr)
out := executeCommand(t, rootCmd, append([]string{getCmd}, tc.args...)...)
switch tc.logType {
case entityLog:
if tc.args[1] == all {
err := json.Unmarshal([]byte(out), &page)
assert.Nil(t, err)
assert.Equal(t, tc.page, page, fmt.Sprintf("%v unexpected response, expected: %v, got: %v", tc.desc, tc.page, page))
} else {
err := json.Unmarshal([]byte(out), &sub)
assert.Nil(t, err)
assert.Equal(t, tc.subscription, sub, fmt.Sprintf("%v unexpected response, expected: %v, got: %v", tc.desc, tc.subscription, sub))
}
case errLog:
assert.Equal(t, tc.errLogMessage, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.errLogMessage, out))
case usageLog:
assert.False(t, strings.Contains(out, rootCmd.Use), fmt.Sprintf("%s invalid usage: %s", tc.desc, out))
}
sdkCall.Unset()
sdkCall1.Unset()
})
}
}
func TestRemoveSubscriptionCmd(t *testing.T) {
sdkMock := new(sdkmocks.SDK)
cli.SetSDK(sdkMock)
subCmd := cli.NewSubscriptionCmd()
rootCmd := setFlags(subCmd)
cases := []struct {
desc string
args []string
sdkErr errors.SDKError
logType outputLog
errLogMessage string
}{
{
desc: "remove subscription successfully",
args: []string{
subscription.ID,
validToken,
},
logType: okLog,
},
{
desc: "remove subscription with invalid args",
args: []string{
subscription.ID,
validToken,
extraArg,
},
logType: usageLog,
},
{
desc: "remove subscription with invalid subscription id",
args: []string{
invalidID,
validToken,
},
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden)),
logType: errLog,
},
{
desc: "remove subscription with invalid token",
args: []string{
subscription.ID,
invalidToken,
},
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden),
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden)),
logType: errLog,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
sdkCall := sdkMock.On("DeleteSubscription", mock.Anything, tc.args[0], tc.args[1]).Return(tc.sdkErr)
out := executeCommand(t, rootCmd, append([]string{rmCmd}, tc.args...)...)
switch tc.logType {
case okLog:
assert.True(t, strings.Contains(out, "ok"), fmt.Sprintf("%s unexpected response: expected success message, got: %v", tc.desc, out))
case errLog:
assert.Equal(t, tc.errLogMessage, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.errLogMessage, out))
case usageLog:
assert.False(t, strings.Contains(out, rootCmd.Use), fmt.Sprintf("%s invalid usage: %s", tc.desc, out))
}
sdkCall.Unset()
})
}
}
+1 -1
View File
@@ -31,7 +31,7 @@ func NewMessagesCmd() *cobra.Command {
cmd := cobra.Command{
Use: "messages [send]",
Short: "Send messages",
Long: `Send messages using the http-adapter`,
Long: `Send messages using the HTTP API`,
}
for i := range cmdMessages {
+410
View File
@@ -0,0 +1,410 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package cli
import (
"encoding/csv"
"encoding/json"
"errors"
"fmt"
"io"
"math/rand"
"os"
"path/filepath"
"time"
"github.com/0x6flab/namegenerator"
smqsdk "github.com/absmach/supermq/pkg/sdk"
"github.com/spf13/cobra"
)
const (
jsonExt = ".json"
csvExt = ".csv"
PublishType = "publish"
SubscribeType = "subscribe"
)
var (
msgFormat = `[{"bn":"provision:", "bu":"V", "t": %d, "bver":5, "n":"voltage", "u":"V", "v":%d}]`
namesgenerator = namegenerator.NewGenerator()
)
var cmdProvision = []cobra.Command{
{
Use: "clients <clients_file> <domain_id> <user_token>",
Short: "Provision clients",
Long: `Bulk create clients`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 3 {
logUsageCmd(*cmd, cmd.Use)
return
}
if _, err := os.Stat(args[0]); os.IsNotExist(err) {
logErrorCmd(*cmd, err)
return
}
clients, err := clientsFromFile(args[0])
if err != nil {
logErrorCmd(*cmd, err)
return
}
clients, err = sdk.CreateClients(cmd.Context(), clients, args[1], args[2])
if err != nil {
logErrorCmd(*cmd, err)
return
}
logJSONCmd(*cmd, clients)
},
},
{
Use: "channels <channels_file> <domain_id> <user_token>",
Short: "Provision channels",
Long: `Bulk create channels`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 3 {
logUsageCmd(*cmd, cmd.Use)
return
}
channels, err := channelsFromFile(args[0])
if err != nil {
logErrorCmd(*cmd, err)
return
}
var chs []smqsdk.Channel
for _, c := range channels {
c, err = sdk.CreateChannel(cmd.Context(), c, args[1], args[2])
if err != nil {
logErrorCmd(*cmd, err)
return
}
chs = append(chs, c)
}
channels = chs
logJSONCmd(*cmd, channels)
},
},
{
Use: "connect <connections_file> <domain_id> <user_token>",
Short: "Provision connections",
Long: `Bulk connect clients to channels`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 3 {
logUsageCmd(*cmd, cmd.Use)
return
}
connIDs, err := connectionsFromFile(args[0])
if err != nil {
logErrorCmd(*cmd, err)
return
}
for _, conn := range connIDs {
if err := sdk.Connect(cmd.Context(), conn, args[1], args[2]); err != nil {
logErrorCmd(*cmd, err)
return
}
}
logOKCmd(*cmd)
},
},
{
Use: "test",
Short: "test",
Long: `Provisions test setup: one test user, two clients and two channels. \
Connect both clients to one of the channels, \
and only on client to other channel.`,
Run: func(cmd *cobra.Command, args []string) {
numClients := 2
numChan := 2
clients := []smqsdk.Client{}
channels := []smqsdk.Channel{}
if len(args) != 0 {
logUsageCmd(*cmd, cmd.Use)
return
}
// Create test user
name := namesgenerator.Generate()
user := smqsdk.User{
FirstName: name,
Email: fmt.Sprintf("%s@email.com", name),
Credentials: smqsdk.Credentials{
Username: name,
Secret: "12345678",
},
Status: smqsdk.EnabledStatus,
}
user, err := sdk.CreateUser(cmd.Context(), user, "")
if err != nil {
logErrorCmd(*cmd, err)
return
}
ut, err := sdk.CreateToken(cmd.Context(), smqsdk.Login{Username: user.Credentials.Username, Password: user.Credentials.Secret})
if err != nil {
logErrorCmd(*cmd, err)
return
}
// create domain
domain := smqsdk.Domain{
Name: fmt.Sprintf("%s-domain", name),
Status: smqsdk.EnabledStatus,
}
domain, err = sdk.CreateDomain(cmd.Context(), domain, ut.AccessToken)
if err != nil {
logErrorCmd(*cmd, err)
return
}
ut, err = sdk.CreateToken(cmd.Context(), smqsdk.Login{Username: user.Email, Password: user.Credentials.Secret})
if err != nil {
logErrorCmd(*cmd, err)
return
}
// Create clients
for i := 0; i < numClients; i++ {
t := smqsdk.Client{
Name: fmt.Sprintf("%s-client-%d", name, i),
Status: smqsdk.EnabledStatus,
}
clients = append(clients, t)
}
clients, err = sdk.CreateClients(cmd.Context(), clients, domain.ID, ut.AccessToken)
if err != nil {
logErrorCmd(*cmd, err)
return
}
// Create channels
for i := 0; i < numChan; i++ {
c := smqsdk.Channel{
Name: fmt.Sprintf("%s-channel-%d", name, i),
Status: smqsdk.EnabledStatus,
}
c, err = sdk.CreateChannel(cmd.Context(), c, domain.ID, ut.AccessToken)
if err != nil {
logErrorCmd(*cmd, err)
return
}
channels = append(channels, c)
}
// Connect clients to channels - first client to both channels, second only to first
conIDs := smqsdk.Connection{
ChannelIDs: []string{channels[0].ID},
ClientIDs: []string{clients[0].ID},
Types: []string{PublishType, SubscribeType},
}
if err := sdk.Connect(cmd.Context(), conIDs, domain.ID, ut.AccessToken); err != nil {
logErrorCmd(*cmd, err)
return
}
conIDs = smqsdk.Connection{
ChannelIDs: []string{channels[1].ID},
ClientIDs: []string{clients[0].ID},
Types: []string{PublishType, SubscribeType},
}
if err := sdk.Connect(cmd.Context(), conIDs, domain.ID, ut.AccessToken); err != nil {
logErrorCmd(*cmd, err)
return
}
conIDs = smqsdk.Connection{
ChannelIDs: []string{channels[0].ID},
ClientIDs: []string{clients[1].ID},
Types: []string{PublishType, SubscribeType},
}
if err := sdk.Connect(cmd.Context(), conIDs, domain.ID, ut.AccessToken); err != nil {
logErrorCmd(*cmd, err)
return
}
// send message to test connectivity
if err := sdk.SendMessage(cmd.Context(), domain.ID, channels[0].ID, clients[0].Credentials.Secret, fmt.Sprintf(msgFormat, time.Now().Unix(), rand.Int())); err != nil {
logErrorCmd(*cmd, err)
return
}
if err := sdk.SendMessage(cmd.Context(), domain.ID, channels[0].ID, clients[1].Credentials.Secret, fmt.Sprintf(msgFormat, time.Now().Unix(), rand.Int())); err != nil {
logErrorCmd(*cmd, err)
return
}
if err := sdk.SendMessage(cmd.Context(), domain.ID, channels[1].ID, clients[0].Credentials.Secret, fmt.Sprintf(msgFormat, time.Now().Unix(), rand.Int())); err != nil {
logErrorCmd(*cmd, err)
return
}
logJSONCmd(*cmd, user, ut, clients, channels)
},
},
}
// NewProvisionCmd returns provision command.
func NewProvisionCmd() *cobra.Command {
cmd := cobra.Command{
Use: "provision [clients | channels | connect | test]",
Short: "Provision clients and channels from a config file",
Long: `Provision clients and channels: use json or csv file to bulk provision clients and channels`,
}
for i := range cmdProvision {
cmd.AddCommand(&cmdProvision[i])
}
return &cmd
}
func clientsFromFile(path string) ([]smqsdk.Client, error) {
if _, err := os.Stat(path); os.IsNotExist(err) {
return []smqsdk.Client{}, err
}
file, err := os.OpenFile(path, os.O_RDONLY, os.ModePerm)
if err != nil {
return []smqsdk.Client{}, err
}
defer file.Close()
clients := []smqsdk.Client{}
switch filepath.Ext(path) {
case csvExt:
reader := csv.NewReader(file)
for {
l, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
return []smqsdk.Client{}, err
}
if len(l) < 1 {
return []smqsdk.Client{}, errors.New("empty line found in file")
}
client := smqsdk.Client{
Name: l[0],
}
clients = append(clients, client)
}
case jsonExt:
err := json.NewDecoder(file).Decode(&clients)
if err != nil {
return []smqsdk.Client{}, err
}
default:
return []smqsdk.Client{}, err
}
return clients, nil
}
func channelsFromFile(path string) ([]smqsdk.Channel, error) {
if _, err := os.Stat(path); os.IsNotExist(err) {
return []smqsdk.Channel{}, err
}
file, err := os.OpenFile(path, os.O_RDONLY, os.ModePerm)
if err != nil {
return []smqsdk.Channel{}, err
}
defer file.Close()
channels := []smqsdk.Channel{}
switch filepath.Ext(path) {
case csvExt:
reader := csv.NewReader(file)
for {
l, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
return []smqsdk.Channel{}, err
}
if len(l) < 1 {
return []smqsdk.Channel{}, errors.New("empty line found in file")
}
channel := smqsdk.Channel{
Name: l[0],
}
channels = append(channels, channel)
}
case jsonExt:
err := json.NewDecoder(file).Decode(&channels)
if err != nil {
return []smqsdk.Channel{}, err
}
default:
return []smqsdk.Channel{}, err
}
return channels, nil
}
func connectionsFromFile(path string) ([]smqsdk.Connection, error) {
if _, err := os.Stat(path); os.IsNotExist(err) {
return []smqsdk.Connection{}, err
}
file, err := os.OpenFile(path, os.O_RDONLY, os.ModePerm)
if err != nil {
return []smqsdk.Connection{}, err
}
defer file.Close()
connections := []smqsdk.Connection{}
switch filepath.Ext(path) {
case csvExt:
reader := csv.NewReader(file)
for {
l, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
return []smqsdk.Connection{}, err
}
if len(l) < 1 {
return []smqsdk.Connection{}, errors.New("empty line found in file")
}
connections = append(connections, smqsdk.Connection{
ClientIDs: []string{l[0]},
ChannelIDs: []string{l[1]},
Types: []string{PublishType, SubscribeType},
})
}
case jsonExt:
err := json.NewDecoder(file).Decode(&connections)
if err != nil {
return []smqsdk.Connection{}, err
}
default:
return []smqsdk.Connection{}, err
}
return connections, nil
}
-7
View File
@@ -33,13 +33,6 @@ var user = mgsdk.User{
Status: users.EnabledStatus.String(),
}
var (
validToken = "valid"
invalidToken = ""
invalidID = "invalidID"
extraArg = "extra-arg"
)
func TestCreateUsersCmd(t *testing.T) {
sdkMock := new(sdkmocks.SDK)
cli.SetSDK(sdkMock)
+81
View File
@@ -6,7 +6,11 @@ package cli
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"github.com/absmach/supermq/certs"
smqsdk "github.com/absmach/supermq/pkg/sdk"
"github.com/fatih/color"
"github.com/hokaccha/go-prettyjson"
"github.com/spf13/cobra"
@@ -76,6 +80,14 @@ func logOKCmd(cmd cobra.Command) {
fmt.Fprintf(cmd.OutOrStdout(), "\n%s\n\n", color.BlueString("ok"))
}
func logCreatedCmd(cmd cobra.Command, e string) {
if RawOutput {
fmt.Fprintln(cmd.OutOrStdout(), e)
} else {
fmt.Fprintf(cmd.OutOrStdout(), color.BlueString("\ncreated: %s\n\n"), e)
}
}
func convertMetadata(m string) (map[string]any, error) {
var metadata map[string]any
if m == "" {
@@ -86,3 +98,72 @@ func convertMetadata(m string) (map[string]any, error) {
}
return nil, nil
}
const certFileMode = 0o644
func logSaveCertFiles(cmd cobra.Command, cert smqsdk.Certificate) {
files := map[string][]byte{
"cert.pem": []byte(cert.Certificate),
}
if cert.Key != "" {
files["key.pem"] = []byte(cert.Key)
}
for filename, content := range files {
if err := saveToFile(filename, content); err != nil {
logErrorCmd(cmd, err)
return
}
fmt.Fprintf(cmd.OutOrStdout(), "Saved %s\n", filename)
}
fmt.Fprintf(cmd.OutOrStdout(), "\nAll certificate files have been saved successfully.\n")
}
func logSaveCAFiles(cmd cobra.Command, certBundle smqsdk.CertificateBundle) {
files := map[string][]byte{
"ca.crt": certBundle.Certificate,
}
for filename, content := range files {
if err := saveToFile(filename, content); err != nil {
logErrorCmd(cmd, err)
return
}
fmt.Fprintf(cmd.OutOrStdout(), "Saved %s\n", filename)
}
fmt.Fprintf(cmd.OutOrStdout(), "\nAll certificate files have been saved successfully.\n")
}
func logSaveCSRFiles(cmd cobra.Command, csr certs.CSR) {
files := map[string][]byte{
"file.csr": csr.CSR,
}
for filename, content := range files {
if err := saveToFile(filename, content); err != nil {
logErrorCmd(cmd, err)
return
}
fmt.Fprintf(cmd.OutOrStdout(), "Saved %s\n", filename)
}
fmt.Fprintf(cmd.OutOrStdout(), "\nCSR file have been saved successfully.\n")
}
func logSaveCRLFile(cmd cobra.Command, crlBytes []byte) {
filename := "ca.crl"
if err := saveToFile(filename, crlBytes); err != nil {
logErrorCmd(cmd, err)
return
}
fmt.Fprintf(cmd.OutOrStdout(), "Saved %s\n", filename)
fmt.Fprintf(cmd.OutOrStdout(), "\nCRL file has been saved successfully.\n")
}
func saveToFile(filename string, content []byte) error {
cwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to get current working directory: %w", err)
}
filePath := filepath.Join(cwd, filename)
if err := os.WriteFile(filePath, content, certFileMode); err != nil {
return fmt.Errorf("failed to write file %s: %w", filename, err)
}
return nil
}