From 9676e7a617586fed94b6627120a90d5fed44525b Mon Sep 17 00:00:00 2001 From: Bowen Date: Fri, 21 Feb 2025 21:31:39 +0800 Subject: [PATCH] test: optimize test cases of env commands --- foundation/console/env_decrypt_command.go | 22 +++-- .../console/env_decrypt_command_test.go | 85 +++++++++++++++---- foundation/console/env_encrypt_command.go | 16 ++-- .../console/env_encrypt_command_test.go | 82 ++++++++++++++---- tests/go.mod | 2 +- tests/go.sum | 4 +- 6 files changed, 163 insertions(+), 48 deletions(-) diff --git a/foundation/console/env_decrypt_command.go b/foundation/console/env_decrypt_command.go index d2dd65955..20b95dc94 100644 --- a/foundation/console/env_decrypt_command.go +++ b/foundation/console/env_decrypt_command.go @@ -10,6 +10,8 @@ import ( "github.com/goravel/framework/contracts/console" "github.com/goravel/framework/contracts/console/command" "github.com/goravel/framework/errors" + "github.com/goravel/framework/support/convert" + "github.com/goravel/framework/support/file" ) type EnvDecryptCommand struct { @@ -46,22 +48,21 @@ func (r *EnvDecryptCommand) Extend() command.Extend { // Handle Execute the console command. func (r *EnvDecryptCommand) Handle(ctx console.Context) error { - key := ctx.Option("key") - if key == "" { - key = os.Getenv("GORAVEL_ENV_ENCRYPTION_KEY") - } + key := convert.Default(ctx.Option("key"), os.Getenv("GORAVEL_ENV_ENCRYPTION_KEY")) if key == "" { ctx.Error("A decryption key is required.") return nil } + ciphertext, err := os.ReadFile(".env.encrypted") if err != nil { ctx.Error("Encrypted environment file not found.") return nil } - if _, err = os.Stat(".env"); err == nil { + + if file.Exists(".env") { ok, _ := ctx.Confirm("Environment file already exists, are you sure to overwrite?", console.ConfirmOption{ - Default: true, + Default: false, Affirmative: "Yes", Negative: "No", }) @@ -69,16 +70,19 @@ func (r *EnvDecryptCommand) Handle(ctx console.Context) error { return nil } } + plaintext, err := r.decrypt(ciphertext, []byte(key)) if err != nil { ctx.Error(fmt.Sprintf("Decrypt error: %v", err)) return nil } + err = os.WriteFile(".env", plaintext, 0644) if err != nil { ctx.Error(fmt.Sprintf("Writer error: %v", err)) return nil } + ctx.Success("Encrypted environment successfully decrypted.") return nil } @@ -88,18 +92,22 @@ func (r *EnvDecryptCommand) decrypt(ciphertext []byte, key []byte) ([]byte, erro if err != nil { return nil, err } + iv := ciphertext[:aes.BlockSize] ciphertext = ciphertext[aes.BlockSize:] block, err := aes.NewCipher(key) if err != nil { return nil, err } + if len(ciphertext)%aes.BlockSize != 0 { return nil, errors.AesCiphertextInvalid } + mode := cipher.NewCBCDecrypter(block, iv) plaintext := make([]byte, len(ciphertext)) mode.CryptBlocks(plaintext, ciphertext) + return r.pkcs7Unpad(plaintext) } @@ -108,9 +116,11 @@ func (r *EnvDecryptCommand) pkcs7Unpad(data []byte) ([]byte, error) { if length == 0 { return nil, errors.AesCiphertextInvalid } + padding := int(data[length-1]) if padding < 1 || length < padding { return nil, errors.AesPaddingInvalid } + return data[:length-padding], nil } diff --git a/foundation/console/env_decrypt_command_test.go b/foundation/console/env_decrypt_command_test.go index b370bd540..d93443bac 100644 --- a/foundation/console/env_decrypt_command_test.go +++ b/foundation/console/env_decrypt_command_test.go @@ -1,7 +1,6 @@ package console import ( - "os" "reflect" "testing" @@ -27,11 +26,9 @@ func TestEnvDecryptCommandTestSuite(t *testing.T) { } func (s *EnvDecryptCommandTestSuite) SetupSuite() { - s.Nil(file.PutContent(".env.encrypted", EnvDecryptCiphertext)) } func (s *EnvDecryptCommandTestSuite) TearDownSuite() { - s.Nil(file.Remove(".env.encrypted")) } func (s *EnvDecryptCommandTestSuite) TestSignature() { @@ -84,30 +81,85 @@ func (s *EnvDecryptCommandTestSuite) TestHandle() { envDecryptCommand := NewEnvDecryptCommand() mockContext := mocksconsole.NewContext(s.T()) - if env, err := os.ReadFile(".env"); err == nil { + s.Run("empty key", func() { + mockContext.EXPECT().Option("key").Return("").Once() + mockContext.EXPECT().Error("A decryption key is required.").Once() + s.Nil(envDecryptCommand.Handle(mockContext)) + }) + + s.Run("invalid key", func() { + s.Nil(file.PutContent(".env.encrypted", EnvDecryptCiphertext)) + defer func() { + s.Nil(file.Remove(".env.encrypted")) + }() + + mockContext.EXPECT().Option("key").Return(EnvDecryptInvalidKey).Once() + mockContext.EXPECT().Error("Decrypt error: crypto/aes: invalid key size 4").Once() + + s.Nil(envDecryptCommand.Handle(mockContext)) + }) + + s.Run(".env.encrypted is not found", func() { + mockContext.EXPECT().Option("key").Return(EnvDecryptValidKey).Once() + mockContext.EXPECT().Error("Encrypted environment file not found.").Once() + s.Nil(envDecryptCommand.Handle(mockContext)) + }) + + s.Run(".env exists and confirm failed", func() { + s.Nil(file.PutContent(".env.encrypted", EnvDecryptCiphertext)) + s.Nil(file.PutContent(".env", EnvDecryptPlaintext)) + defer func() { + s.Nil(file.Remove(".env.encrypted")) + s.Nil(file.Remove(".env")) + }() + + mockContext.EXPECT().Option("key").Return(EnvDecryptValidKey).Once() mockContext.EXPECT().Confirm("Environment file already exists, are you sure to overwrite?", console.ConfirmOption{ - Default: true, + Default: false, Affirmative: "Yes", Negative: "No", - }).Return(true, nil).Once() - s.Require().Equal(EnvDecryptPlaintext, string(env)) - } - - s.Run("valid key", func() { - mockContext.EXPECT().Option("key").Return(EnvDecryptValidKey).Once() - mockContext.EXPECT().Success("Encrypted environment successfully decrypted.").Once() + }).Return(false, nil).Once() s.Nil(envDecryptCommand.Handle(mockContext)) }) - s.Run("invalid key", func() { - mockContext.EXPECT().Option("key").Return(EnvDecryptInvalidKey).Once() + s.Run("success when .env exists", func() { + s.Nil(file.PutContent(".env.encrypted", EnvDecryptCiphertext)) + s.Nil(file.PutContent(".env", EnvDecryptPlaintext)) + defer func() { + s.Nil(file.Remove(".env")) + s.Nil(file.Remove(".env.encrypted")) + }() + + mockContext.EXPECT().Option("key").Return(EnvDecryptValidKey).Once() mockContext.EXPECT().Confirm("Environment file already exists, are you sure to overwrite?", console.ConfirmOption{ - Default: true, + Default: false, Affirmative: "Yes", Negative: "No", }).Return(true, nil).Once() - mockContext.EXPECT().Error("Decrypt error: crypto/aes: invalid key size 4").Once() + mockContext.EXPECT().Success("Encrypted environment successfully decrypted.").Once() + s.Nil(envDecryptCommand.Handle(mockContext)) + s.True(file.Exists(".env")) + content, err := file.GetContent(".env") + s.Nil(err) + s.Equal(EnvDecryptPlaintext, content) + }) + + s.Run("success when .env not exists", func() { + s.Nil(file.PutContent(".env.encrypted", EnvDecryptCiphertext)) + defer func() { + s.Nil(file.Remove(".env.encrypted")) + s.Nil(file.Remove(".env")) + }() + + mockContext.EXPECT().Option("key").Return(EnvDecryptValidKey).Once() + mockContext.EXPECT().Success("Encrypted environment successfully decrypted.").Once() + + s.Nil(envDecryptCommand.Handle(mockContext)) + s.True(file.Exists(".env")) + content, err := file.GetContent(".env") + s.Nil(err) + s.Equal(EnvDecryptPlaintext, content) }) } @@ -119,6 +171,7 @@ func (s *EnvDecryptCommandTestSuite) TestDecrypt() { s.Equal(EnvDecryptPlaintext, string(decrypted)) s.Nil(err) }) + s.Run("invalid key", func() { _, err := envDecryptCommand.decrypt([]byte(EnvDecryptCiphertext), []byte(EnvDecryptInvalidKey)) s.Error(err) diff --git a/foundation/console/env_encrypt_command.go b/foundation/console/env_encrypt_command.go index cc6613fdc..0b0931e40 100644 --- a/foundation/console/env_encrypt_command.go +++ b/foundation/console/env_encrypt_command.go @@ -10,6 +10,8 @@ import ( "github.com/goravel/framework/contracts/console" "github.com/goravel/framework/contracts/console/command" + "github.com/goravel/framework/support/convert" + "github.com/goravel/framework/support/file" "github.com/goravel/framework/support/str" ) @@ -47,18 +49,15 @@ func (r *EnvEncryptCommand) Extend() command.Extend { // Handle Execute the console command. func (r *EnvEncryptCommand) Handle(ctx console.Context) error { - key := ctx.Option("key") - if key == "" { - key = str.Random(32) - } + key := convert.Default(ctx.Option("key"), str.Random(32)) plaintext, err := os.ReadFile(".env") if err != nil { ctx.Error("Environment file not found.") return nil } - if _, err = os.Stat(".env.encrypted"); err == nil { + if file.Exists(".env.encrypted") { ok, _ := ctx.Confirm("Encrypted environment file already exists, are you sure to overwrite?", console.ConfirmOption{ - Default: true, + Default: false, Affirmative: "Yes", Negative: "No", }) @@ -66,11 +65,13 @@ func (r *EnvEncryptCommand) Handle(ctx console.Context) error { return nil } } + ciphertext, err := r.encrypt(plaintext, []byte(key)) if err != nil { ctx.Error(fmt.Sprintf("Encrypt error: %v", err)) return nil } + base64Data := base64.StdEncoding.EncodeToString(ciphertext) err = os.WriteFile(".env.encrypted", []byte(base64Data), 0644) if err != nil { @@ -91,16 +92,19 @@ func (r *EnvEncryptCommand) encrypt(plaintext []byte, key []byte) ([]byte, error if err != nil { return nil, err } + iv := key[:aes.BlockSize] plaintext = r.pkcs7Pad(plaintext) mode := cipher.NewCBCEncrypter(block, iv) ciphertext := make([]byte, len(plaintext)) mode.CryptBlocks(ciphertext, plaintext) + return append(iv, ciphertext...), nil } func (r *EnvEncryptCommand) pkcs7Pad(data []byte) []byte { padding := aes.BlockSize - len(data)%aes.BlockSize padText := bytes.Repeat([]byte{byte(padding)}, padding) + return append(data, padText...) } diff --git a/foundation/console/env_encrypt_command_test.go b/foundation/console/env_encrypt_command_test.go index 8cf275f2a..2332d1488 100644 --- a/foundation/console/env_encrypt_command_test.go +++ b/foundation/console/env_encrypt_command_test.go @@ -2,7 +2,6 @@ package console import ( "encoding/base64" - "os" "reflect" "testing" @@ -28,11 +27,9 @@ func TestEnvEncryptCommandTestSuite(t *testing.T) { } func (s *EnvEncryptCommandTestSuite) SetupSuite() { - s.Nil(file.PutContent(".env", EnvEncryptPlaintext)) } func (s *EnvEncryptCommandTestSuite) TearDownSuite() { - s.Nil(file.Remove(".env")) } func (s *EnvEncryptCommandTestSuite) TestSignature() { @@ -85,35 +82,86 @@ func (s *EnvEncryptCommandTestSuite) TestHandle() { envEncryptCommand := NewEnvEncryptCommand() mockContext := mocksconsole.NewContext(s.T()) - if _, err := os.Stat(".env.encrypted"); err == nil { + s.Run(".env not exists", func() { + mockContext.EXPECT().Option("key").Return(EnvEncryptValidKey).Once() + mockContext.EXPECT().Error("Environment file not found.").Once() + + s.Nil(envEncryptCommand.Handle(mockContext)) + }) + + s.Run(".env.encrypted exists and confirm failed", func() { + s.Nil(file.PutContent(".env", EnvEncryptPlaintext)) + s.Nil(file.PutContent(".env.encrypted", EnvEncryptCiphertext)) + defer func() { + s.Nil(file.Remove(".env")) + s.Nil(file.Remove(".env.encrypted")) + }() + + mockContext.EXPECT().Option("key").Return(EnvEncryptValidKey).Once() mockContext.EXPECT().Confirm("Encrypted environment file already exists, are you sure to overwrite?", console.ConfirmOption{ - Default: true, + Default: false, Affirmative: "Yes", Negative: "No", - }).Return(true, nil).Once() - } - - s.Run("valid key", func() { - mockContext.EXPECT().Option("key").Return(EnvEncryptValidKey).Once() - mockContext.EXPECT().Success("Environment successfully encrypted.").Once() - mockContext.EXPECT().TwoColumnDetail("Key", EnvEncryptValidKey).Once() - mockContext.EXPECT().TwoColumnDetail("Cipher", "AES-256-CBC").Once() - mockContext.EXPECT().TwoColumnDetail("Encrypted file", ".env.encrypted").Once() + }).Return(false, nil).Once() s.Nil(envEncryptCommand.Handle(mockContext)) }) s.Run("invalid key", func() { + s.Nil(file.PutContent(".env", EnvEncryptPlaintext)) + defer func() { + s.Nil(file.Remove(".env")) + }() + mockContext.EXPECT().Option("key").Return(EnvEncryptInvalidKey).Once() + mockContext.EXPECT().Error("Encrypt error: crypto/aes: invalid key size 4").Once() + + s.Nil(envEncryptCommand.Handle(mockContext)) + }) + + s.Run("success when .env.encrypted exists", func() { + s.Nil(file.PutContent(".env", EnvEncryptPlaintext)) + s.Nil(file.PutContent(".env.encrypted", EnvEncryptCiphertext)) + defer func() { + s.Nil(file.Remove(".env")) + s.Nil(file.Remove(".env.encrypted")) + }() + + mockContext.EXPECT().Option("key").Return(EnvEncryptValidKey).Once() mockContext.EXPECT().Confirm("Encrypted environment file already exists, are you sure to overwrite?", console.ConfirmOption{ - Default: true, + Default: false, Affirmative: "Yes", Negative: "No", }).Return(true, nil).Once() - mockContext.EXPECT().Error("Encrypt error: crypto/aes: invalid key size 4").Once() + mockContext.EXPECT().Success("Environment successfully encrypted.").Once() + mockContext.EXPECT().TwoColumnDetail("Key", EnvEncryptValidKey).Once() + mockContext.EXPECT().TwoColumnDetail("Cipher", "AES-256-CBC").Once() + mockContext.EXPECT().TwoColumnDetail("Encrypted file", ".env.encrypted").Once() + s.Nil(envEncryptCommand.Handle(mockContext)) + content, err := file.GetContent(".env.encrypted") + s.Nil(err) + s.Equal(EnvEncryptCiphertext, content) }) + s.Run("success when .env.encrypted not exists", func() { + s.Nil(file.PutContent(".env", EnvEncryptPlaintext)) + defer func() { + s.Nil(file.Remove(".env")) + s.Nil(file.Remove(".env.encrypted")) + }() + + mockContext.EXPECT().Option("key").Return(EnvEncryptValidKey).Once() + mockContext.EXPECT().Success("Environment successfully encrypted.").Once() + mockContext.EXPECT().TwoColumnDetail("Key", EnvEncryptValidKey).Once() + mockContext.EXPECT().TwoColumnDetail("Cipher", "AES-256-CBC").Once() + mockContext.EXPECT().TwoColumnDetail("Encrypted file", ".env.encrypted").Once() + + s.Nil(envEncryptCommand.Handle(mockContext)) + content, err := file.GetContent(".env.encrypted") + s.Nil(err) + s.Equal(EnvEncryptCiphertext, content) + }) } func (s *EnvDecryptCommandTestSuite) TestEncrypt() { @@ -124,9 +172,9 @@ func (s *EnvDecryptCommandTestSuite) TestEncrypt() { s.Equal(EnvEncryptCiphertext, base64Data) s.Nil(err) }) + s.Run("invalid key", func() { _, err := envEncryptCommand.encrypt([]byte(EnvEncryptPlaintext), []byte(EnvEncryptInvalidKey)) s.Error(err) }) - } diff --git a/tests/go.mod b/tests/go.mod index b21658796..a220faa64 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -64,7 +64,7 @@ require ( github.com/stretchr/objx v0.5.2 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/crypto v0.33.0 // indirect - golang.org/x/exp v0.0.0-20250215185904-eff6e970281f // indirect + golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa // indirect golang.org/x/net v0.35.0 // indirect golang.org/x/sync v0.11.0 // indirect golang.org/x/sys v0.30.0 // indirect diff --git a/tests/go.sum b/tests/go.sum index 63f4a1f4b..976586c6c 100644 --- a/tests/go.sum +++ b/tests/go.sum @@ -248,8 +248,8 @@ golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= -golang.org/x/exp v0.0.0-20250215185904-eff6e970281f h1:oFMYAjX0867ZD2jcNiLBrI9BdpmEkvPyi5YrBGXbamg= -golang.org/x/exp v0.0.0-20250215185904-eff6e970281f/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk= +golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4= +golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=