Skip to content

Commit 311a814

Browse files
authored
test(cmd): add comprehensive unit tests for CLI commands
test(cmd): add comprehensive unit tests for CLI commands
2 parents a59717f + 515875f commit 311a814

11 files changed

Lines changed: 2417 additions & 1 deletion

.github/workflows/coverage.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ jobs:
2424
go mod download
2525
go generate ./...
2626
go test -coverpkg=./... -coverprofile=profile.cov ./...
27-
sed -i '/cmd/d' profile.cov # remove cmd package from coverage
2827
total=$(go tool cover -func profile.cov | grep '^total:' | awk '{print $3}' | sed "s/%//")
2928
rm profile.cov
3029
echo "COVERAGE_VALUE=${total}" >> $GITHUB_ENV

cmd/cmd_test_helpers.go

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
package cmd
2+
3+
import (
4+
"bytes"
5+
"crypto/ecdsa"
6+
"crypto/elliptic"
7+
"crypto/rand"
8+
"crypto/rsa"
9+
"crypto/x509"
10+
"encoding/pem"
11+
"fmt"
12+
"os"
13+
"path/filepath"
14+
"testing"
15+
16+
"github.com/spf13/cobra"
17+
)
18+
19+
// executeCommand executes a Cobra command and captures stdout/stderr.
20+
// This helper allows testing command execution without running the full CLI.
21+
//
22+
// Note: Since the actual commands use fmt.Println (which writes to os.Stdout)
23+
// instead of cmd.OutOrStdout(), we need to temporarily redirect os.Stdout
24+
// to capture the output.
25+
//
26+
//nolint:unused // Shared test helper used across multiple test files
27+
func executeCommand(cmd *cobra.Command, args ...string) (string, error) {
28+
// Create a pipe to capture stdout
29+
oldStdout := os.Stdout
30+
r, w, _ := os.Pipe()
31+
os.Stdout = w
32+
33+
// Also set the command output (for errors and usage)
34+
buf := new(bytes.Buffer)
35+
cmd.SetOut(buf)
36+
cmd.SetErr(buf)
37+
cmd.SetArgs(args)
38+
39+
// Execute the command
40+
err := cmd.Execute()
41+
42+
// Restore stdout
43+
_ = w.Close()
44+
os.Stdout = oldStdout
45+
46+
// Read captured output
47+
var capturedOutput bytes.Buffer
48+
_, _ = capturedOutput.ReadFrom(r)
49+
50+
// Return captured output (from stdout) plus command output (from SetOut)
51+
if err != nil {
52+
return capturedOutput.String() + buf.String(), fmt.Errorf("command execution failed: %w", err)
53+
}
54+
return capturedOutput.String() + buf.String(), nil
55+
}
56+
57+
// registerEncodeFlags registers all encoding-related flags on a command.
58+
// This mimics the flag registration done in root.go for encode commands.
59+
//nolint:unused // Shared test helper used across multiple test files
60+
func registerEncodeFlags(cmd *cobra.Command) {
61+
cmd.Flags().StringP("payload", "p", "", "JSON payload")
62+
cmd.Flags().StringP("secret", "s", "", "HMAC secret")
63+
cmd.Flags().String("private-key", "", "path to RSA/ECDSA private key file")
64+
cmd.Flags().Bool("allow-weak-secret", false, "allow weak secrets")
65+
// Deprecated flags for backward compatibility
66+
cmd.Flags().String("p", "", "")
67+
_ = cmd.Flags().MarkDeprecated("p", "use --payload or -p instead")
68+
cmd.Flags().String("s", "", "")
69+
_ = cmd.Flags().MarkDeprecated("s", "use --secret or -s instead")
70+
cmd.Flags().String("pk", "", "")
71+
_ = cmd.Flags().MarkDeprecated("pk", "use --private-key instead")
72+
}
73+
74+
// registerDecodeFlags registers all decoding-related flags on a command.
75+
// This mimics the flag registration done in root.go for decode commands.
76+
//nolint:unused // Shared test helper used across multiple test files
77+
func registerDecodeFlags(cmd *cobra.Command) {
78+
cmd.Flags().StringP("token", "t", "", "JWT token to decode")
79+
cmd.Flags().StringP("secret", "s", "", "HMAC secret")
80+
cmd.Flags().String("private-key", "", "path to RSA/ECDSA private key file")
81+
cmd.Flags().String("public-key", "", "path to RSA/ECDSA public key file")
82+
cmd.Flags().Bool("allow-weak-secret", false, "allow weak secrets")
83+
// Deprecated flags for backward compatibility
84+
cmd.Flags().String("t", "", "")
85+
_ = cmd.Flags().MarkDeprecated("t", "use --token or -t instead")
86+
cmd.Flags().String("s", "", "")
87+
_ = cmd.Flags().MarkDeprecated("s", "use --secret or -s instead")
88+
cmd.Flags().String("pk", "", "")
89+
_ = cmd.Flags().MarkDeprecated("pk", "use --private-key instead")
90+
cmd.Flags().String("pubk", "", "")
91+
_ = cmd.Flags().MarkDeprecated("pubk", "use --public-key instead")
92+
}
93+
94+
// createTempFile creates a temporary file with given content.
95+
// The file is created in t.TempDir() and will be automatically cleaned up.
96+
//nolint:unused // Shared test helper used across multiple test files
97+
func createTempFile(t *testing.T, content []byte) string {
98+
t.Helper()
99+
tmpFile, err := os.CreateTemp(t.TempDir(), "test-*.pem")
100+
if err != nil {
101+
t.Fatalf("Failed to create temp file: %v", err)
102+
}
103+
if _, err := tmpFile.Write(content); err != nil {
104+
t.Fatalf("Failed to write to temp file: %v", err)
105+
}
106+
if err := tmpFile.Close(); err != nil {
107+
t.Fatalf("Failed to close temp file: %v", err)
108+
}
109+
return tmpFile.Name()
110+
}
111+
112+
// generateRSAKeyPair generates a test RSA key pair and returns file paths.
113+
// Both private and public keys are written to temporary files in PEM format.
114+
//nolint:unused // Shared test helper used across multiple test files
115+
func generateRSAKeyPair(t *testing.T) (string, string) {
116+
t.Helper()
117+
privateKey, err := rsa.GenerateKey(rand.Reader, testRSAKeySize)
118+
if err != nil {
119+
t.Fatalf("Failed to generate RSA key: %v", err)
120+
}
121+
122+
privateKeyPEM := &pem.Block{
123+
Type: "RSA PRIVATE KEY",
124+
Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
125+
}
126+
privateKeyBytes := pem.EncodeToMemory(privateKeyPEM)
127+
128+
publicKeyBytes, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey)
129+
if err != nil {
130+
t.Fatalf("Failed to marshal public key: %v", err)
131+
}
132+
publicKeyPEM := &pem.Block{
133+
Type: "PUBLIC KEY",
134+
Bytes: publicKeyBytes,
135+
}
136+
publicKeyPEMBytes := pem.EncodeToMemory(publicKeyPEM)
137+
138+
privateKeyPath := createTempFile(t, privateKeyBytes)
139+
publicKeyPath := createTempFile(t, publicKeyPEMBytes)
140+
return privateKeyPath, publicKeyPath
141+
}
142+
143+
// generateECDSAKeyPair generates a test ECDSA key pair for the given curve.
144+
// Both private and public keys are written to temporary files in PEM format.
145+
//nolint:unused // Shared test helper used across multiple test files
146+
func generateECDSAKeyPair(t *testing.T, curve elliptic.Curve) (string, string) {
147+
t.Helper()
148+
privateKey, err := ecdsa.GenerateKey(curve, rand.Reader)
149+
if err != nil {
150+
t.Fatalf("Failed to generate ECDSA key: %v", err)
151+
}
152+
153+
privateKeyBytes, err := x509.MarshalECPrivateKey(privateKey)
154+
if err != nil {
155+
t.Fatalf("Failed to marshal EC private key: %v", err)
156+
}
157+
privateKeyPEM := &pem.Block{
158+
Type: "EC PRIVATE KEY",
159+
Bytes: privateKeyBytes,
160+
}
161+
privateKeyPEMBytes := pem.EncodeToMemory(privateKeyPEM)
162+
163+
publicKeyBytes, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey)
164+
if err != nil {
165+
t.Fatalf("Failed to marshal public key: %v", err)
166+
}
167+
publicKeyPEM := &pem.Block{
168+
Type: "PUBLIC KEY",
169+
Bytes: publicKeyBytes,
170+
}
171+
publicKeyPEMBytes := pem.EncodeToMemory(publicKeyPEM)
172+
173+
privateKeyPath := createTempFile(t, privateKeyPEMBytes)
174+
publicKeyPath := createTempFile(t, publicKeyPEMBytes)
175+
return privateKeyPath, publicKeyPath
176+
}
177+
178+
// createInvalidPEMFile creates a file with invalid PEM content for testing error handling.
179+
//nolint:unused // Shared test helper used across multiple test files
180+
func createInvalidPEMFile(t *testing.T) string {
181+
t.Helper()
182+
return createTempFile(t, []byte("invalid pem content"))
183+
}
184+
185+
// createWrongTypePEMFile creates a PEM file with wrong type for testing error handling.
186+
//nolint:unused // Shared test helper used across multiple test files
187+
func createWrongTypePEMFile(t *testing.T, pemType string) string {
188+
t.Helper()
189+
block := &pem.Block{
190+
Type: pemType,
191+
Bytes: []byte("some data"),
192+
}
193+
return createTempFile(t, pem.EncodeToMemory(block))
194+
}
195+
196+
// createMalformedRSAKeyFile creates a PEM file with malformed RSA key data.
197+
//nolint:unused // Shared test helper used across multiple test files
198+
func createMalformedRSAKeyFile(t *testing.T) string {
199+
t.Helper()
200+
block := &pem.Block{
201+
Type: "RSA PRIVATE KEY",
202+
Bytes: []byte("malformed rsa key data"),
203+
}
204+
return createTempFile(t, pem.EncodeToMemory(block))
205+
}
206+
207+
// createMalformedECKeyFile creates a PEM file with malformed EC key data.
208+
//nolint:unused // Shared test helper used across multiple test files
209+
func createMalformedECKeyFile(t *testing.T) string {
210+
t.Helper()
211+
block := &pem.Block{
212+
Type: "EC PRIVATE KEY",
213+
Bytes: []byte("malformed ec key data"),
214+
}
215+
return createTempFile(t, pem.EncodeToMemory(block))
216+
}
217+
218+
// getNonExistentPath returns a path that doesn't exist for testing file not found errors.
219+
//nolint:unused // Shared test helper used across multiple test files
220+
func getNonExistentPath(t *testing.T) string {
221+
t.Helper()
222+
return filepath.Join(t.TempDir(), "nonexistent.pem")
223+
}
224+
225+
// Test constants used across multiple test files.
226+
//nolint:unused // Shared test constants used across multiple test files
227+
const (
228+
// testRSAKeySize is the RSA key size used for test key generation (2048 bits).
229+
testRSAKeySize = 2048
230+
231+
// validPayload is a valid JSON payload for testing JWT encoding/decoding.
232+
validPayload = `{"sub":"1234567890","name":"John Doe","iat":1516239022}`
233+
234+
// invalidJSON is an invalid JSON string for testing error handling.
235+
invalidJSON = `{invalid json`
236+
237+
// complexPayload is a complex nested JSON for testing edge cases.
238+
complexPayload = `{"user":{"id":123,"name":"Alice","roles":["admin","user"]},"meta":{"created":"2024-01-01","nested":{"deep":true}}}`
239+
240+
// hs256Secret is a valid 32-byte secret for HS256 algorithm (exactly 32 bytes).
241+
hs256Secret = "12345678901234567890123456789012"
242+
243+
// hs384Secret is a valid 48-byte secret for HS384 algorithm (exactly 48 bytes).
244+
hs384Secret = "123456789012345678901234567890123456789012345678"
245+
246+
// hs512Secret is a valid 64-byte secret for HS512 algorithm (exactly 64 bytes).
247+
hs512Secret = "1234567890123456789012345678901234567890123456789012345678901234"
248+
249+
// weakSecret is a secret that doesn't meet minimum length requirements.
250+
weakSecret = "short"
251+
)

0 commit comments

Comments
 (0)