From 48ba4ffa490e608ddad3aab54eaf5648fa6d1fec Mon Sep 17 00:00:00 2001 From: Lindsey Cheng Date: Mon, 30 Dec 2024 16:39:49 +0800 Subject: [PATCH] feat: Add unit tests for new functions Relates to #5038. Add unit tests on the controller and app layers. Signed-off-by: Lindsey Cheng --- internal/pkg/utils/{ => crypto}/aes.go | 10 +- internal/pkg/utils/{ => crypto}/aes_test.go | 2 +- .../pkg/utils/crypto/interfaces/crypto.go | 13 ++ .../utils/crypto/interfaces/mocks/Crypto.go | 90 +++++++++ .../security/proxyauth/application/key.go | 10 +- .../proxyauth/application/key_test.go | 172 +++++++++++++++++ .../security/proxyauth/container/cryptor.go | 20 ++ .../proxyauth/controller/controller.go | 6 - .../proxyauth/controller/controller_test.go | 32 ++++ internal/security/proxyauth/controller/key.go | 20 +- .../security/proxyauth/controller/key_test.go | 179 ++++++++++++++++++ .../interfaces/mocks/DBClient.go | 128 +++++++++++++ internal/security/proxyauth/main.go | 4 + internal/security/proxyauth/utils/key_test.go | 28 +++ 14 files changed, 684 insertions(+), 30 deletions(-) rename internal/pkg/utils/{ => crypto}/aes.go (91%) rename internal/pkg/utils/{ => crypto}/aes_test.go (96%) create mode 100644 internal/pkg/utils/crypto/interfaces/crypto.go create mode 100644 internal/pkg/utils/crypto/interfaces/mocks/Crypto.go create mode 100644 internal/security/proxyauth/application/key_test.go create mode 100644 internal/security/proxyauth/container/cryptor.go create mode 100644 internal/security/proxyauth/controller/controller_test.go create mode 100644 internal/security/proxyauth/controller/key_test.go create mode 100644 internal/security/proxyauth/infrastructure/interfaces/mocks/DBClient.go create mode 100644 internal/security/proxyauth/utils/key_test.go diff --git a/internal/pkg/utils/aes.go b/internal/pkg/utils/crypto/aes.go similarity index 91% rename from internal/pkg/utils/aes.go rename to internal/pkg/utils/crypto/aes.go index 10b4aadce9..ebf313bd03 100644 --- a/internal/pkg/utils/aes.go +++ b/internal/pkg/utils/crypto/aes.go @@ -3,7 +3,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package utils +package crypto import ( "bytes" @@ -13,6 +13,8 @@ import ( "encoding/base64" "io" + "github.com/edgexfoundry/edgex-go/internal/pkg/utils/crypto/interfaces" + "github.com/edgexfoundry/go-mod-core-contracts/v4/errors" ) @@ -23,14 +25,14 @@ type AESCryptor struct { key []byte } -func NewAESCryptor() *AESCryptor { +func NewAESCryptor() interfaces.Crypto { return &AESCryptor{ key: []byte(aesKey), } } // Encrypt encrypts the given plaintext with AES-CBC mode and returns a string in base64 encoding -func (c AESCryptor) Encrypt(plaintext string) (string, errors.EdgeX) { +func (c *AESCryptor) Encrypt(plaintext string) (string, errors.EdgeX) { bytePlaintext := []byte(plaintext) block, err := aes.NewCipher(c.key) if err != nil { @@ -54,7 +56,7 @@ func (c AESCryptor) Encrypt(plaintext string) (string, errors.EdgeX) { } // Decrypt decrypts the given ciphertext with AES-CBC mode and returns the original value as string -func (c AESCryptor) Decrypt(ciphertext string) ([]byte, errors.EdgeX) { +func (c *AESCryptor) Decrypt(ciphertext string) ([]byte, errors.EdgeX) { decodedCipherText, err := base64.StdEncoding.DecodeString(ciphertext) if err != nil { return nil, errors.NewCommonEdgeX(errors.KindServerError, "decrypt failed", err) diff --git a/internal/pkg/utils/aes_test.go b/internal/pkg/utils/crypto/aes_test.go similarity index 96% rename from internal/pkg/utils/aes_test.go rename to internal/pkg/utils/crypto/aes_test.go index b5fcbfe8cd..d740215c97 100644 --- a/internal/pkg/utils/aes_test.go +++ b/internal/pkg/utils/crypto/aes_test.go @@ -3,7 +3,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package utils +package crypto import ( "testing" diff --git a/internal/pkg/utils/crypto/interfaces/crypto.go b/internal/pkg/utils/crypto/interfaces/crypto.go new file mode 100644 index 0000000000..adbaa3a547 --- /dev/null +++ b/internal/pkg/utils/crypto/interfaces/crypto.go @@ -0,0 +1,13 @@ +// +// Copyright (C) 2024 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package interfaces + +import "github.com/edgexfoundry/go-mod-core-contracts/v4/errors" + +type Crypto interface { + Encrypt(string) (string, errors.EdgeX) + Decrypt(string) ([]byte, errors.EdgeX) +} diff --git a/internal/pkg/utils/crypto/interfaces/mocks/Crypto.go b/internal/pkg/utils/crypto/interfaces/mocks/Crypto.go new file mode 100644 index 0000000000..c540f8ddfb --- /dev/null +++ b/internal/pkg/utils/crypto/interfaces/mocks/Crypto.go @@ -0,0 +1,90 @@ +// Code generated by mockery v2.49.0. DO NOT EDIT. + +package mocks + +import ( + errors "github.com/edgexfoundry/go-mod-core-contracts/v4/errors" + + mock "github.com/stretchr/testify/mock" +) + +// Crypto is an autogenerated mock type for the Crypto type +type Crypto struct { + mock.Mock +} + +// Decrypt provides a mock function with given fields: _a0 +func (_m *Crypto) Decrypt(_a0 string) ([]byte, errors.EdgeX) { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for Decrypt") + } + + var r0 []byte + var r1 errors.EdgeX + if rf, ok := ret.Get(0).(func(string) ([]byte, errors.EdgeX)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(string) []byte); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + if rf, ok := ret.Get(1).(func(string) errors.EdgeX); ok { + r1 = rf(_a0) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(errors.EdgeX) + } + } + + return r0, r1 +} + +// Encrypt provides a mock function with given fields: _a0 +func (_m *Crypto) Encrypt(_a0 string) (string, errors.EdgeX) { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for Encrypt") + } + + var r0 string + var r1 errors.EdgeX + if rf, ok := ret.Get(0).(func(string) (string, errors.EdgeX)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(string) string); ok { + r0 = rf(_a0) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(string) errors.EdgeX); ok { + r1 = rf(_a0) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(errors.EdgeX) + } + } + + return r0, r1 +} + +// NewCrypto creates a new instance of Crypto. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewCrypto(t interface { + mock.TestingT + Cleanup(func()) +}) *Crypto { + mock := &Crypto{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/security/proxyauth/application/key.go b/internal/security/proxyauth/application/key.go index 5c196c19dd..3153280cb2 100644 --- a/internal/security/proxyauth/application/key.go +++ b/internal/security/proxyauth/application/key.go @@ -7,11 +7,8 @@ package application import ( "fmt" - - "github.com/edgexfoundry/edgex-go/internal/pkg/utils" "github.com/edgexfoundry/edgex-go/internal/security/proxyauth/container" proxyAuthUtils "github.com/edgexfoundry/edgex-go/internal/security/proxyauth/utils" - "github.com/edgexfoundry/go-mod-bootstrap/v4/di" "github.com/edgexfoundry/go-mod-core-contracts/v4/common" "github.com/edgexfoundry/go-mod-core-contracts/v4/dtos" @@ -23,6 +20,7 @@ import ( // and then invokes AddKey function of infrastructure layer to add new user func AddKey(dic *di.Container, keyData models.KeyData) errors.EdgeX { dbClient := container.DBClientFrom(dic.Get) + cryptor := container.CryptoFrom(dic.Get) keyName := "" if len(keyData.Type) == 0 { @@ -39,7 +37,7 @@ func AddKey(dic *di.Container, keyData models.KeyData) errors.EdgeX { fmt.Sprintf("key type should be one of the '%s' or '%s'", common.VerificationKeyType, common.SigningKeyType), nil) } - encryptedKey, err := utils.NewAESCryptor().Encrypt(keyData.Key) + encryptedKey, err := cryptor.Encrypt(keyData.Key) if err != nil { return errors.NewCommonEdgeX(errors.Kind(err), "failed to encrypt the key", err) } @@ -69,12 +67,14 @@ func VerificationKeyByIssuer(dic *di.Container, issuer string) (dtos.KeyData, er } keyName := proxyAuthUtils.VerificationKeyName(issuer) dbClient := container.DBClientFrom(dic.Get) + cryptor := container.CryptoFrom(dic.Get) keyData, err := dbClient.ReadKeyContent(keyName) if err != nil { return dtos.KeyData{}, errors.NewCommonEdgeXWrapper(err) } - decryptedKey, err := utils.NewAESCryptor().Decrypt(keyData) + + decryptedKey, err := cryptor.Decrypt(keyData) if err != nil { return dtos.KeyData{}, errors.NewCommonEdgeX(errors.Kind(err), "failed to decrypt the key", err) } diff --git a/internal/security/proxyauth/application/key_test.go b/internal/security/proxyauth/application/key_test.go new file mode 100644 index 0000000000..26ee5e9168 --- /dev/null +++ b/internal/security/proxyauth/application/key_test.go @@ -0,0 +1,172 @@ +// +// Copyright (C) 2024 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package application + +import ( + "testing" + + cryptoMocks "github.com/edgexfoundry/edgex-go/internal/pkg/utils/crypto/interfaces/mocks" + "github.com/edgexfoundry/edgex-go/internal/security/proxyauth/container" + "github.com/edgexfoundry/edgex-go/internal/security/proxyauth/infrastructure/interfaces/mocks" + + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v4/di" + "github.com/edgexfoundry/go-mod-core-contracts/v4/clients/logger" + "github.com/edgexfoundry/go-mod-core-contracts/v4/common" + "github.com/edgexfoundry/go-mod-core-contracts/v4/dtos" + "github.com/edgexfoundry/go-mod-core-contracts/v4/errors" + "github.com/edgexfoundry/go-mod-core-contracts/v4/models" + + "github.com/stretchr/testify/require" +) + +func mockDic() *di.Container { + return di.NewContainer(di.ServiceConstructorMap{ + bootstrapContainer.LoggingClientInterfaceName: func(get di.Get) interface{} { + return logger.NewMockClient() + }, + }) +} + +func TestAddKey(t *testing.T) { + dic := mockDic() + + validNewKey := "validNewKey" + validIssuer := "testIssuer" + validKeyData := models.KeyData{ + Type: common.VerificationKeyType, + Issuer: validIssuer, + Key: validNewKey, + } + validKeyName := validKeyData.Issuer + "/" + validKeyData.Type + validEncryptedKey := "encryptedValidNewKey" + + validUpdateKey := "validUpdateKey" + updateKeyData := models.KeyData{ + Type: common.SigningKeyType, + Issuer: "issuer2", + Key: validUpdateKey, + } + validUpdateKeyName := updateKeyData.Issuer + "/" + updateKeyData.Type + validUpdateEncryptedKey := "encryptedValidUpdateKey" + + invalidKeyData := models.KeyData{ + Type: "invalidKeyType", + Issuer: "issuer2", + Key: validUpdateKey, + } + + encryptFailedKey := "encryptFailedKey" + encryptFailedKeyData := models.KeyData{ + Type: common.SigningKeyType, + Issuer: "issuer3", + Key: encryptFailedKey, + } + + dbClientMock := &mocks.DBClient{} + dbClientMock.On("KeyExists", validKeyName).Return(false, nil) + dbClientMock.On("AddKey", validKeyName, validEncryptedKey).Return(nil) + dbClientMock.On("KeyExists", validUpdateKeyName).Return(true, nil) + dbClientMock.On("UpdateKey", validUpdateKeyName, validUpdateEncryptedKey).Return(nil) + + cryptoMock := &cryptoMocks.Crypto{} + cryptoMock.On("Encrypt", validKeyData.Key).Return(validEncryptedKey, nil) + cryptoMock.On("Encrypt", updateKeyData.Key).Return(validUpdateEncryptedKey, nil) + cryptoMock.On("Encrypt", encryptFailedKeyData.Key).Return("", errors.NewCommonEdgeX(errors.KindServerError, "failed to encrypt the key", nil)) + + dic.Update(di.ServiceConstructorMap{ + container.DBClientInterfaceName: func(get di.Get) interface{} { + return dbClientMock + }, + container.CryptoInterfaceName: func(get di.Get) interface{} { + return cryptoMock + }, + }) + + tests := []struct { + name string + keyData models.KeyData + errorExpected bool + errKind errors.ErrKind + }{ + {"Valid - Add new verification key", validKeyData, false, ""}, + {"Valid - Update existing signing key", updateKeyData, false, ""}, + {"Invalid - Invalid key type", invalidKeyData, true, errors.KindContractInvalid}, + {"Invalid - Encryption Error", encryptFailedKeyData, true, errors.KindServerError}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := AddKey(dic, test.keyData) + if test.errorExpected { + require.Error(t, err) + require.Equal(t, test.errKind, errors.Kind(err)) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestVerificationKeyByIssuer(t *testing.T) { + dic := mockDic() + + validIssuer := "issuer1" + validEncryptedKey := "encryptedKey" + expectedKeyName := validIssuer + "/" + common.VerificationKeyType + expectedKeyData := dtos.KeyData{Issuer: validIssuer, Type: common.VerificationKeyType, Key: "decryptedKey"} + + invalidIssuer := "invalidIssuer" + invalidKeyName := invalidIssuer + "/" + common.VerificationKeyType + + decryptErrIssuer := "decryptErrIssuer" + decryptErrKeyName := decryptErrIssuer + "/" + common.VerificationKeyType + decryptErrKey := "decryptErrKey" + + dbClientMock := &mocks.DBClient{} + dbClientMock.On("ReadKeyContent", expectedKeyName).Return(validEncryptedKey, nil) + dbClientMock.On("ReadKeyContent", invalidKeyName).Return("", errors.NewCommonEdgeX(errors.KindServerError, "read key error", nil)) + dbClientMock.On("ReadKeyContent", decryptErrKeyName).Return(decryptErrKey, nil) + + cryptoMock := &cryptoMocks.Crypto{} + cryptoMock.On("Decrypt", validEncryptedKey).Return([]byte("decryptedKey"), nil) + cryptoMock.On("Decrypt", decryptErrKey).Return([]byte{}, errors.NewCommonEdgeX(errors.KindServerError, "decrypt key error", nil)) + + dic.Update(di.ServiceConstructorMap{ + container.DBClientInterfaceName: func(get di.Get) interface{} { + return dbClientMock + }, + container.CryptoInterfaceName: func(get di.Get) interface{} { + return cryptoMock + }, + }) + + tests := []struct { + name string + issuer string + expectedKeyData dtos.KeyData + errorExpected bool + errKind errors.ErrKind + }{ + {"Valid - Valid key", validIssuer, expectedKeyData, false, ""}, + {"Invalid - Empty issuer", "", dtos.KeyData{}, true, errors.KindContractInvalid}, + {"Invalid - Key read error", invalidIssuer, dtos.KeyData{}, true, errors.KindServerError}, + {"Invalid - Decryption error", decryptErrIssuer, dtos.KeyData{}, true, errors.KindServerError}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result, err := VerificationKeyByIssuer(dic, test.issuer) + if test.errorExpected { + require.Error(t, err) + require.Equal(t, test.errKind, errors.Kind(err)) + } else { + require.NoError(t, err) + require.Equal(t, test.expectedKeyData, result) + } + }) + } +} diff --git a/internal/security/proxyauth/container/cryptor.go b/internal/security/proxyauth/container/cryptor.go new file mode 100644 index 0000000000..b835b8a2d1 --- /dev/null +++ b/internal/security/proxyauth/container/cryptor.go @@ -0,0 +1,20 @@ +// +// Copyright (C) 2024 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package container + +import ( + "github.com/edgexfoundry/edgex-go/internal/pkg/utils/crypto/interfaces" + + "github.com/edgexfoundry/go-mod-bootstrap/v4/di" +) + +// CryptoInterfaceName contains the name of the interfaces.Crypto implementation in the DIC. +var CryptoInterfaceName = di.TypeInstanceToName((*interfaces.Crypto)(nil)) + +// CryptoFrom helper function queries the DIC and returns the interfaces.Cryptor implementation. +func CryptoFrom(get di.Get) interfaces.Crypto { + return get(CryptoInterfaceName).(interfaces.Crypto) +} diff --git a/internal/security/proxyauth/controller/controller.go b/internal/security/proxyauth/controller/controller.go index a3b9fa3d92..b33da03943 100644 --- a/internal/security/proxyauth/controller/controller.go +++ b/internal/security/proxyauth/controller/controller.go @@ -8,22 +8,16 @@ package controller import ( "github.com/edgexfoundry/edgex-go/internal/io" - "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/container" "github.com/edgexfoundry/go-mod-bootstrap/v4/di" - "github.com/edgexfoundry/go-mod-core-contracts/v4/clients/logger" ) type AuthController struct { dic *di.Container - lc logger.LoggingClient reader io.DtoReader } func NewAuthController(dic *di.Container) *AuthController { - lc := container.LoggingClientFrom(dic.Get) - return &AuthController{ - lc: lc, dic: dic, reader: io.NewJsonDtoReader(), } diff --git a/internal/security/proxyauth/controller/controller_test.go b/internal/security/proxyauth/controller/controller_test.go new file mode 100644 index 0000000000..797fa34eff --- /dev/null +++ b/internal/security/proxyauth/controller/controller_test.go @@ -0,0 +1,32 @@ +// +// Copyright (C) 2024 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package controller + +import ( + "testing" + + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v4/di" + "github.com/edgexfoundry/go-mod-core-contracts/v4/clients/logger" + + "github.com/stretchr/testify/require" +) + +func mockDic() *di.Container { + return di.NewContainer(di.ServiceConstructorMap{ + bootstrapContainer.LoggingClientInterfaceName: func(get di.Get) interface{} { + return logger.NewMockClient() + }, + }) +} + +func TestNewAuthController(t *testing.T) { + dic := mockDic() + controller := NewAuthController(dic) + + require.NotNil(t, controller) + require.NotNil(t, controller.reader) +} diff --git a/internal/security/proxyauth/controller/key.go b/internal/security/proxyauth/controller/key.go index 26f5c737df..4a0aa19cdf 100644 --- a/internal/security/proxyauth/controller/key.go +++ b/internal/security/proxyauth/controller/key.go @@ -9,7 +9,6 @@ import ( "net/http" "github.com/edgexfoundry/edgex-go/internal/pkg" - "github.com/edgexfoundry/edgex-go/internal/pkg/correlation" "github.com/edgexfoundry/edgex-go/internal/pkg/utils" "github.com/edgexfoundry/edgex-go/internal/security/proxyauth/application" @@ -32,7 +31,6 @@ func (a *AuthController) AddKey(c echo.Context) error { lc := bootstrapContainer.LoggingClientFrom(a.dic.Get) ctx := r.Context() - correlationId := correlation.FromContext(ctx) var req requests.AddKeyDataRequest err := a.reader.Read(r.Body, &req) @@ -45,20 +43,14 @@ func (a *AuthController) AddKey(c echo.Context) error { err = application.AddKey(a.dic, dtos.ToKeyDataModel(req.KeyData)) if err != nil { - lc.Error(err.Error(), common.CorrelationHeader, correlationId) - lc.Debug(err.DebugMessages(), common.CorrelationHeader, correlationId) - response = commonDTO.NewBaseResponse( - reqId, - err.Message(), - err.Code()) - } else { - response = commonDTO.NewBaseResponse( - reqId, - "", - http.StatusCreated) + return utils.WriteErrorResponse(w, ctx, lc, err, "") } - utils.WriteHttpHeader(w, ctx, http.StatusMultiStatus) + response = commonDTO.NewBaseResponse( + reqId, + "", + http.StatusCreated) + utils.WriteHttpHeader(w, ctx, http.StatusCreated) return pkg.EncodeAndWriteResponse(response, w, lc) } diff --git a/internal/security/proxyauth/controller/key_test.go b/internal/security/proxyauth/controller/key_test.go new file mode 100644 index 0000000000..2c5d5c9100 --- /dev/null +++ b/internal/security/proxyauth/controller/key_test.go @@ -0,0 +1,179 @@ +// +// Copyright (C) 2024 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package controller + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + + cryptoMocks "github.com/edgexfoundry/edgex-go/internal/pkg/utils/crypto/interfaces/mocks" + "github.com/edgexfoundry/edgex-go/internal/security/proxyauth/container" + "github.com/edgexfoundry/edgex-go/internal/security/proxyauth/infrastructure/interfaces/mocks" + + "github.com/edgexfoundry/go-mod-bootstrap/v4/di" + "github.com/edgexfoundry/go-mod-core-contracts/v4/common" + "github.com/edgexfoundry/go-mod-core-contracts/v4/dtos" + commonDTO "github.com/edgexfoundry/go-mod-core-contracts/v4/dtos/common" + "github.com/edgexfoundry/go-mod-core-contracts/v4/dtos/requests" + "github.com/edgexfoundry/go-mod-core-contracts/v4/dtos/responses" + "github.com/edgexfoundry/go-mod-core-contracts/v4/errors" + + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/require" +) + +func TestAuthController_AddKey(t *testing.T) { + dic := mockDic() + + e := echo.New() + controller := NewAuthController(dic) + + validNewKey := "validNewKey" + validIssuer := "testIssuer" + validKeyData := dtos.KeyData{ + Type: common.VerificationKeyType, + Issuer: validIssuer, + Key: validNewKey, + } + validKeyName := validKeyData.Issuer + "/" + validKeyData.Type + validEncryptedKey := "encryptedValidNewKey" + validReq := requests.AddKeyDataRequest{ + BaseRequest: commonDTO.BaseRequest{ + Versionable: commonDTO.NewVersionable(), + }, + KeyData: validKeyData, + } + + noIssuerReq := validReq + noIssuerReq.KeyData.Issuer = "" + + invalidTypeReq := validReq + invalidTypeReq.KeyData.Type = "invalidType" + + dbClientMock := &mocks.DBClient{} + dbClientMock.On("KeyExists", validKeyName).Return(false, nil) + dbClientMock.On("AddKey", validKeyName, validEncryptedKey).Return(nil) + dbClientMock.On("KeyExists", validKeyName).Return(false, nil) + + cryptoMock := &cryptoMocks.Crypto{} + cryptoMock.On("Encrypt", validKeyData.Key).Return(validEncryptedKey, nil) + + dic.Update(di.ServiceConstructorMap{ + container.DBClientInterfaceName: func(get di.Get) interface{} { + return dbClientMock + }, + container.CryptoInterfaceName: func(get di.Get) interface{} { + return cryptoMock + }, + }) + + tests := []struct { + name string + request requests.AddKeyDataRequest + expectedStatus int + }{ + {"Valid - Successful add key", validReq, http.StatusCreated}, + {"Invalid - no issuer request", noIssuerReq, http.StatusBadRequest}, + {"Invalid - invalid type", invalidTypeReq, http.StatusBadRequest}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + jsonBytes, err := json.Marshal(test.request) + require.NoError(t, err) + + reader := strings.NewReader(string(jsonBytes)) + req, err := http.NewRequest(http.MethodPost, common.ApiKeyRoute, reader) + require.NoError(t, err) + + recorder := httptest.NewRecorder() + ctx := e.NewContext(req, recorder) + + edgexErr := controller.AddKey(ctx) + require.NoError(t, edgexErr) + require.Equal(t, test.expectedStatus, recorder.Code) + + var res commonDTO.BaseResponse + err = json.Unmarshal(recorder.Body.Bytes(), &res) + require.NoError(t, err) + require.Equal(t, common.ApiVersion, res.ApiVersion, "API Version not as expected") + require.Equal(t, test.expectedStatus, res.StatusCode, "BaseResponse status code not as expected") + }) + } +} + +func TestAuthController_VerificationKeyByIssuer(t *testing.T) { + dic := mockDic() + controller := NewAuthController(dic) + + validIssuer := "issuer1" + validEncryptedKey := "encryptedKey" + expectedKeyName := validIssuer + "/" + common.VerificationKeyType + expectedKeyData := dtos.KeyData{Issuer: validIssuer, Type: common.VerificationKeyType, Key: "decryptedKey"} + + invalidIssuer := "invalidIssuer" + invalidKeyName := invalidIssuer + "/" + common.VerificationKeyType + + dbClientMock := &mocks.DBClient{} + dbClientMock.On("ReadKeyContent", expectedKeyName).Return(validEncryptedKey, nil) + dbClientMock.On("ReadKeyContent", invalidKeyName).Return("", errors.NewCommonEdgeX(errors.KindServerError, "read key error", nil)) + + cryptoMock := &cryptoMocks.Crypto{} + cryptoMock.On("Decrypt", validEncryptedKey).Return([]byte("decryptedKey"), nil) + + dic.Update(di.ServiceConstructorMap{ + container.DBClientInterfaceName: func(get di.Get) interface{} { + return dbClientMock + }, + container.CryptoInterfaceName: func(get di.Get) interface{} { + return cryptoMock + }, + }) + + tests := []struct { + name string + issuer string + expectedStatus int + expectedKeyData dtos.KeyData + }{ + {"Valid - valid issuer", validIssuer, http.StatusOK, expectedKeyData}, + {"Invalid - no issuer request", "", http.StatusBadRequest, dtos.KeyData{}}, + {"Invalid - failed to read key by issuer", invalidIssuer, http.StatusInternalServerError, dtos.KeyData{}}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + e := echo.New() + req, err := http.NewRequest(http.MethodGet, common.ApiVerificationKeyByIssuerRoute, http.NoBody) + require.NoError(t, err) + + recorder := httptest.NewRecorder() + c := e.NewContext(req, recorder) + c.SetParamNames(common.Issuer) + c.SetParamValues(test.issuer) + + edgexErr := controller.VerificationKeyByIssuer(c) + require.NoError(t, edgexErr) + require.Equal(t, test.expectedStatus, recorder.Code) + if test.expectedStatus == http.StatusOK { + var res responses.KeyDataResponse + err = json.Unmarshal(recorder.Body.Bytes(), &res) + require.NoError(t, err) + require.Equal(t, common.ApiVersion, res.ApiVersion, "API Version not as expected") + require.Equal(t, expectedKeyData, res.KeyData, "KeyData response not as expected") + } else { + var res commonDTO.BaseResponse + err = json.Unmarshal(recorder.Body.Bytes(), &res) + require.NoError(t, err) + require.Equal(t, common.ApiVersion, res.ApiVersion, "API Version not as expected") + require.Equal(t, test.expectedStatus, res.StatusCode, "BaseResponse status code not as expected") + } + }) + } +} diff --git a/internal/security/proxyauth/infrastructure/interfaces/mocks/DBClient.go b/internal/security/proxyauth/infrastructure/interfaces/mocks/DBClient.go new file mode 100644 index 0000000000..d65b72251f --- /dev/null +++ b/internal/security/proxyauth/infrastructure/interfaces/mocks/DBClient.go @@ -0,0 +1,128 @@ +// Code generated by mockery v2.49.0. DO NOT EDIT. + +package mocks + +import ( + errors "github.com/edgexfoundry/go-mod-core-contracts/v4/errors" + + mock "github.com/stretchr/testify/mock" +) + +// DBClient is an autogenerated mock type for the DBClient type +type DBClient struct { + mock.Mock +} + +// AddKey provides a mock function with given fields: name, content +func (_m *DBClient) AddKey(name string, content string) errors.EdgeX { + ret := _m.Called(name, content) + + if len(ret) == 0 { + panic("no return value specified for AddKey") + } + + var r0 errors.EdgeX + if rf, ok := ret.Get(0).(func(string, string) errors.EdgeX); ok { + r0 = rf(name, content) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(errors.EdgeX) + } + } + + return r0 +} + +// KeyExists provides a mock function with given fields: name +func (_m *DBClient) KeyExists(name string) (bool, errors.EdgeX) { + ret := _m.Called(name) + + if len(ret) == 0 { + panic("no return value specified for KeyExists") + } + + var r0 bool + var r1 errors.EdgeX + if rf, ok := ret.Get(0).(func(string) (bool, errors.EdgeX)); ok { + return rf(name) + } + if rf, ok := ret.Get(0).(func(string) bool); ok { + r0 = rf(name) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(string) errors.EdgeX); ok { + r1 = rf(name) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(errors.EdgeX) + } + } + + return r0, r1 +} + +// ReadKeyContent provides a mock function with given fields: name +func (_m *DBClient) ReadKeyContent(name string) (string, errors.EdgeX) { + ret := _m.Called(name) + + if len(ret) == 0 { + panic("no return value specified for ReadKeyContent") + } + + var r0 string + var r1 errors.EdgeX + if rf, ok := ret.Get(0).(func(string) (string, errors.EdgeX)); ok { + return rf(name) + } + if rf, ok := ret.Get(0).(func(string) string); ok { + r0 = rf(name) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(string) errors.EdgeX); ok { + r1 = rf(name) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(errors.EdgeX) + } + } + + return r0, r1 +} + +// UpdateKey provides a mock function with given fields: name, content +func (_m *DBClient) UpdateKey(name string, content string) errors.EdgeX { + ret := _m.Called(name, content) + + if len(ret) == 0 { + panic("no return value specified for UpdateKey") + } + + var r0 errors.EdgeX + if rf, ok := ret.Get(0).(func(string, string) errors.EdgeX); ok { + r0 = rf(name, content) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(errors.EdgeX) + } + } + + return r0 +} + +// NewDBClient creates a new instance of DBClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewDBClient(t interface { + mock.TestingT + Cleanup(func()) +}) *DBClient { + mock := &DBClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/security/proxyauth/main.go b/internal/security/proxyauth/main.go index df3037992e..f6aaa4d74d 100644 --- a/internal/security/proxyauth/main.go +++ b/internal/security/proxyauth/main.go @@ -29,6 +29,7 @@ import ( "github.com/edgexfoundry/edgex-go" pkgHandlers "github.com/edgexfoundry/edgex-go/internal/pkg/bootstrap/handlers" + "github.com/edgexfoundry/edgex-go/internal/pkg/utils/crypto" "github.com/edgexfoundry/edgex-go/internal/security/proxyauth/config" "github.com/edgexfoundry/edgex-go/internal/security/proxyauth/container" @@ -55,6 +56,9 @@ func Main(ctx context.Context, cancel context.CancelFunc, router *echo.Echo, arg container.ConfigurationName: func(get di.Get) interface{} { return configuration }, + container.CryptoInterfaceName: func(get di.Get) interface{} { + return crypto.NewAESCryptor() + }, }) httpServer := handlers.NewHttpServer(router, true, common.SecurityProxyAuthServiceKey) diff --git a/internal/security/proxyauth/utils/key_test.go b/internal/security/proxyauth/utils/key_test.go new file mode 100644 index 0000000000..3cc4a9dda3 --- /dev/null +++ b/internal/security/proxyauth/utils/key_test.go @@ -0,0 +1,28 @@ +// +// Copyright (C) 2024 IOTech Ltd +// +// SPDX-License-Identifier: Apache-2.0 + +package utils + +import ( + "testing" + + "github.com/edgexfoundry/go-mod-core-contracts/v4/common" + + "github.com/stretchr/testify/require" +) + +func TestSigningKeyName(t *testing.T) { + mockIssuer := "mockIssuer" + expected := mockIssuer + "/" + common.SigningKeyType + result := SigningKeyName(mockIssuer) + require.Equal(t, expected, result) +} + +func TestVerificationKeyName(t *testing.T) { + mockIssuer := "mockIssuer" + expected := mockIssuer + "/" + common.VerificationKeyType + result := VerificationKeyName(mockIssuer) + require.Equal(t, expected, result) +}