FRE-4694: Add CLI command end-to-end tests
Add comprehensive e2e tests for all CLI commands with mocked API responses. Fix test infrastructure to handle global state (os.Stdout capture, HOME env var) and broken test parallelism in stdout-capturing tests. - Add testutil_test.go with runFreshCommand, setupE2E, mockAPIServer - Add e2e_full_test.go with ~40 tests covering auth, mail, contacts, attachments, folders, labels, drafts, help output - Add newRootCmdBase() for fresh command trees per test - Remove t.Parallel() from stdout-capturing and HOME-dependent tests - Fix SessionWithMockSession to use runFreshCommand (stdout capture) Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
152
cmd/auth_test.go
Normal file
152
cmd/auth_test.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestLoginCommand tests the login CLI command
|
||||
func TestLoginCommand(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create a temporary config file
|
||||
tmpDir := t.TempDir()
|
||||
configPath := tmpDir + "/config.yaml"
|
||||
|
||||
configContent := `
|
||||
api:
|
||||
base_url: "http://localhost:8080"
|
||||
timeout: 30s
|
||||
`
|
||||
err := os.WriteFile(configPath, []byte(configContent), 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to write config: %v", err)
|
||||
}
|
||||
|
||||
// Set config path environment variable
|
||||
os.Setenv("POP_CONFIG_PATH", configPath)
|
||||
defer os.Unsetenv("POP_CONFIG_PATH")
|
||||
|
||||
// Create root command with login subcommand
|
||||
rootCmd := NewRootCmd()
|
||||
rootCmd.SetArgs([]string{"login"})
|
||||
|
||||
// Capture output
|
||||
var buf bytes.Buffer
|
||||
rootCmd.SetOut(&buf)
|
||||
rootCmd.SetErr(&buf)
|
||||
|
||||
// Execute command
|
||||
err = rootCmd.Execute()
|
||||
if err != nil {
|
||||
// Login requires interactive input, so error is expected in non-interactive mode
|
||||
t.Logf("Login command executed with error (expected in non-interactive mode): %v", err)
|
||||
}
|
||||
|
||||
// Verify command ran
|
||||
output := buf.String()
|
||||
t.Logf("Command output: %s", output)
|
||||
}
|
||||
|
||||
// TestLogoutCommand tests the logout CLI command
|
||||
func TestLogoutCommand(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create a temporary config file
|
||||
tmpDir := t.TempDir()
|
||||
configPath := tmpDir + "/config.yaml"
|
||||
|
||||
err := os.WriteFile(configPath, []byte("{}"), 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to write config: %v", err)
|
||||
}
|
||||
|
||||
os.Setenv("POP_CONFIG_PATH", configPath)
|
||||
defer os.Unsetenv("POP_CONFIG_PATH")
|
||||
|
||||
// Create root command with logout subcommand
|
||||
rootCmd := NewRootCmd()
|
||||
rootCmd.SetArgs([]string{"logout"})
|
||||
|
||||
// Capture output
|
||||
var buf bytes.Buffer
|
||||
rootCmd.SetOut(&buf)
|
||||
rootCmd.SetErr(&buf)
|
||||
|
||||
// Execute command
|
||||
err = rootCmd.Execute()
|
||||
// Logout may fail if no session exists, which is expected
|
||||
if err != nil {
|
||||
t.Logf("Logout command executed with error (expected if no session): %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSessionCommand tests the session CLI command
|
||||
func TestSessionCommand(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create a temporary config file
|
||||
tmpDir := t.TempDir()
|
||||
configPath := tmpDir + "/config.yaml"
|
||||
|
||||
err := os.WriteFile(configPath, []byte("{}"), 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to write config: %v", err)
|
||||
}
|
||||
|
||||
os.Setenv("POP_CONFIG_PATH", configPath)
|
||||
defer os.Unsetenv("POP_CONFIG_PATH")
|
||||
|
||||
// Create root command with session subcommand
|
||||
rootCmd := NewRootCmd()
|
||||
rootCmd.SetArgs([]string{"session"})
|
||||
|
||||
// Capture output
|
||||
var buf bytes.Buffer
|
||||
rootCmd.SetOut(&buf)
|
||||
rootCmd.SetErr(&buf)
|
||||
|
||||
// Execute command
|
||||
err = rootCmd.Execute()
|
||||
// Session may fail if no active session, which is expected
|
||||
if err != nil {
|
||||
t.Logf("Session command executed with error (expected if no session): %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRootCommandHelp tests the help output
|
||||
func TestRootCommandHelp(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rootCmd := NewRootCmd()
|
||||
rootCmd.SetArgs([]string{"--help"})
|
||||
|
||||
var buf bytes.Buffer
|
||||
rootCmd.SetOut(&buf)
|
||||
|
||||
err := rootCmd.Execute()
|
||||
if err != nil {
|
||||
t.Fatalf("Help command failed: %v", err)
|
||||
}
|
||||
|
||||
output, _ := io.ReadAll(&buf)
|
||||
if len(output) == 0 {
|
||||
t.Error("Help output is empty")
|
||||
}
|
||||
|
||||
// Verify help contains expected commands
|
||||
helpText := string(output)
|
||||
expectedCommands := []string{"login", "logout", "session", "mail", "contact", "attachment", "folder", "draft"}
|
||||
for _, cmd := range expectedCommands {
|
||||
if !contains(helpText, cmd) {
|
||||
t.Errorf("Help output missing command: %s", cmd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to check if string contains substring
|
||||
func contains(s, substr string) bool {
|
||||
return len(s) > 0 && len(substr) > 0 && (s == substr || len(s) > len(substr) && (s[:len(substr)] == substr || contains(s[1:], substr)))
|
||||
}
|
||||
1475
cmd/e2e_full_test.go
Normal file
1475
cmd/e2e_full_test.go
Normal file
File diff suppressed because it is too large
Load Diff
190
cmd/e2e_test.go
Normal file
190
cmd/e2e_test.go
Normal file
@@ -0,0 +1,190 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestMailCommand tests the mail CLI command structure
|
||||
func TestMailCommand(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rootCmd := newRootCmdBase()
|
||||
rootCmd.SetArgs([]string{"mail", "--help"})
|
||||
|
||||
var buf bytes.Buffer
|
||||
rootCmd.SetOut(&buf)
|
||||
|
||||
err := rootCmd.Execute()
|
||||
if err != nil {
|
||||
t.Fatalf("Mail help command failed: %v", err)
|
||||
}
|
||||
|
||||
output, _ := io.ReadAll(&buf)
|
||||
helpText := string(output)
|
||||
|
||||
// Verify mail subcommands are present
|
||||
expectedSubcommands := []string{"list", "read", "send", "delete", "trash", "draft", "search"}
|
||||
for _, subcmd := range expectedSubcommands {
|
||||
if !contains(helpText, subcmd) {
|
||||
t.Errorf("Mail help missing subcommand: %s", subcmd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestMailListCommand tests the mail list subcommand
|
||||
func TestMailListCommand(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rootCmd := newRootCmdBase()
|
||||
rootCmd.SetArgs([]string{"mail", "list", "--help"})
|
||||
|
||||
var buf bytes.Buffer
|
||||
rootCmd.SetOut(&buf)
|
||||
|
||||
err := rootCmd.Execute()
|
||||
if err != nil {
|
||||
t.Fatalf("Mail list help command failed: %v", err)
|
||||
}
|
||||
|
||||
output, _ := io.ReadAll(&buf)
|
||||
if len(output) == 0 {
|
||||
t.Error("Mail list help output is empty")
|
||||
}
|
||||
}
|
||||
|
||||
// TestContactCommand tests the contact CLI command structure
|
||||
func TestContactCommand(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rootCmd := newRootCmdBase()
|
||||
rootCmd.SetArgs([]string{"contact", "--help"})
|
||||
|
||||
var buf bytes.Buffer
|
||||
rootCmd.SetOut(&buf)
|
||||
|
||||
err := rootCmd.Execute()
|
||||
if err != nil {
|
||||
t.Fatalf("Contact help command failed: %v", err)
|
||||
}
|
||||
|
||||
output, _ := io.ReadAll(&buf)
|
||||
helpText := string(output)
|
||||
|
||||
// Verify contact subcommands are present
|
||||
expectedSubcommands := []string{"list", "add", "edit", "delete"}
|
||||
for _, subcmd := range expectedSubcommands {
|
||||
if !contains(helpText, subcmd) {
|
||||
t.Errorf("Contact help missing subcommand: %s", subcmd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestAttachmentCommand tests the attachment CLI command structure
|
||||
func TestAttachmentCommand(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rootCmd := newRootCmdBase()
|
||||
rootCmd.SetArgs([]string{"attachment", "--help"})
|
||||
|
||||
var buf bytes.Buffer
|
||||
rootCmd.SetOut(&buf)
|
||||
|
||||
err := rootCmd.Execute()
|
||||
if err != nil {
|
||||
t.Fatalf("Attachment help command failed: %v", err)
|
||||
}
|
||||
|
||||
output, _ := io.ReadAll(&buf)
|
||||
helpText := string(output)
|
||||
|
||||
// Verify attachment subcommands are present
|
||||
expectedSubcommands := []string{"upload", "download", "list"}
|
||||
for _, subcmd := range expectedSubcommands {
|
||||
if !contains(helpText, subcmd) {
|
||||
t.Errorf("Attachment help missing subcommand: %s", subcmd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestFolderCommand tests the folder CLI command structure
|
||||
func TestFolderCommand(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rootCmd := newRootCmdBase()
|
||||
rootCmd.SetArgs([]string{"folder", "--help"})
|
||||
|
||||
var buf bytes.Buffer
|
||||
rootCmd.SetOut(&buf)
|
||||
|
||||
err := rootCmd.Execute()
|
||||
if err != nil {
|
||||
t.Fatalf("Folder help command failed: %v", err)
|
||||
}
|
||||
|
||||
output, _ := io.ReadAll(&buf)
|
||||
helpText := string(output)
|
||||
|
||||
// Verify folder subcommands are present
|
||||
expectedSubcommands := []string{"list", "create", "update", "delete"}
|
||||
for _, subcmd := range expectedSubcommands {
|
||||
if !contains(helpText, subcmd) {
|
||||
t.Errorf("Folder help missing subcommand: %s", subcmd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestLabelCommand tests the label CLI command structure
|
||||
func TestLabelCommand(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rootCmd := newRootCmdBase()
|
||||
rootCmd.SetArgs([]string{"label", "--help"})
|
||||
|
||||
var buf bytes.Buffer
|
||||
rootCmd.SetOut(&buf)
|
||||
|
||||
err := rootCmd.Execute()
|
||||
if err != nil {
|
||||
t.Fatalf("Label help command failed: %v", err)
|
||||
}
|
||||
|
||||
output, _ := io.ReadAll(&buf)
|
||||
helpText := string(output)
|
||||
|
||||
// Verify label subcommands are present
|
||||
expectedSubcommands := []string{"list", "create", "update", "delete", "apply", "remove"}
|
||||
for _, subcmd := range expectedSubcommands {
|
||||
if !contains(helpText, subcmd) {
|
||||
t.Errorf("Label help missing subcommand: %s", subcmd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestDraftCommand tests the draft CLI command structure
|
||||
func TestDraftCommand(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rootCmd := newRootCmdBase()
|
||||
rootCmd.SetArgs([]string{"draft", "--help"})
|
||||
|
||||
var buf bytes.Buffer
|
||||
rootCmd.SetOut(&buf)
|
||||
|
||||
err := rootCmd.Execute()
|
||||
if err != nil {
|
||||
t.Fatalf("Draft help command failed: %v", err)
|
||||
}
|
||||
|
||||
output, _ := io.ReadAll(&buf)
|
||||
helpText := string(output)
|
||||
|
||||
// Verify draft subcommands are present
|
||||
expectedSubcommands := []string{"list", "save", "edit", "send"}
|
||||
for _, subcmd := range expectedSubcommands {
|
||||
if !contains(helpText, subcmd) {
|
||||
t.Errorf("Draft help missing subcommand: %s", subcmd)
|
||||
}
|
||||
}
|
||||
}
|
||||
22
cmd/root.go
22
cmd/root.go
@@ -16,6 +16,27 @@ It provides commands for managing emails, contacts, and attachments
|
||||
with full PGP encryption support.`,
|
||||
}
|
||||
|
||||
func newRootCmdBase() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "pop",
|
||||
Short: "ProtonMail CLI tool",
|
||||
Long: `pop is a CLI tool for interacting with ProtonMail.
|
||||
|
||||
It provides commands for managing emails, contacts, and attachments
|
||||
with full PGP encryption support.`,
|
||||
}
|
||||
cmd.AddCommand(loginCmd())
|
||||
cmd.AddCommand(logoutCmd())
|
||||
cmd.AddCommand(sessionCmd())
|
||||
cmd.AddCommand(mailCmd())
|
||||
cmd.AddCommand(mailDraftCmd())
|
||||
cmd.AddCommand(contactCmd())
|
||||
cmd.AddCommand(attachmentCmd())
|
||||
cmd.AddCommand(folderCmd())
|
||||
cmd.AddCommand(labelCmd())
|
||||
return cmd
|
||||
}
|
||||
|
||||
func NewRootCmd() *cobra.Command {
|
||||
rootCmd.AddCommand(loginCmd())
|
||||
rootCmd.AddCommand(logoutCmd())
|
||||
@@ -26,7 +47,6 @@ func NewRootCmd() *cobra.Command {
|
||||
rootCmd.AddCommand(attachmentCmd())
|
||||
rootCmd.AddCommand(folderCmd())
|
||||
rootCmd.AddCommand(labelCmd())
|
||||
|
||||
return rootCmd
|
||||
}
|
||||
|
||||
|
||||
213
cmd/testutil_test.go
Normal file
213
cmd/testutil_test.go
Normal file
@@ -0,0 +1,213 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var stdoutMu sync.Mutex
|
||||
|
||||
// e2eTestEnv provides a self-contained test environment with a temp config dir
|
||||
// and a mock API server. All CLI commands that use config.NewConfigManager() or
|
||||
// auth.NewSessionManager() will resolve to the temp directory.
|
||||
type e2eTestEnv struct {
|
||||
t *testing.T
|
||||
tempDir string
|
||||
mockServer *mockAPIServer
|
||||
origHome string
|
||||
}
|
||||
|
||||
// mockAPIServer wraps httptest.Server with dynamic handler registration.
|
||||
type mockAPIServer struct {
|
||||
mux *http.ServeMux
|
||||
server *httptest.Server
|
||||
handlers sync.Map
|
||||
}
|
||||
|
||||
func newMockAPIServer(t *testing.T) *mockAPIServer {
|
||||
t.Helper()
|
||||
mux := http.NewServeMux()
|
||||
srv := httptest.NewServer(mux)
|
||||
|
||||
ms := &mockAPIServer{mux: mux, server: srv}
|
||||
|
||||
// Register catch-all patterns for all API endpoints used by the CLI
|
||||
mux.HandleFunc("POST /auth", ms.resolve)
|
||||
mux.HandleFunc("POST /auth/verify", ms.resolve)
|
||||
mux.HandleFunc("POST /api/messages", ms.resolve)
|
||||
mux.HandleFunc("POST /api/messages/search", ms.resolve)
|
||||
mux.HandleFunc("POST /api/messages/{id}", ms.resolve)
|
||||
mux.HandleFunc("POST /api/messages/{id}/movetotrash", ms.resolve)
|
||||
mux.HandleFunc("POST /api/messages/{id}/delete", ms.resolve)
|
||||
mux.HandleFunc("POST /api/messages/{id}/send", ms.resolve)
|
||||
mux.HandleFunc("GET /api/folders", ms.resolve)
|
||||
mux.HandleFunc("POST /api/folders", ms.resolve)
|
||||
mux.HandleFunc("POST /api/folders/{id}", ms.resolve)
|
||||
mux.HandleFunc("GET /api/folders/{id}", ms.resolve)
|
||||
mux.HandleFunc("GET /api/labels", ms.resolve)
|
||||
mux.HandleFunc("POST /api/labels", ms.resolve)
|
||||
mux.HandleFunc("POST /api/labels/{id}", ms.resolve)
|
||||
mux.HandleFunc("POST /api/messages/{id}/setlabel", ms.resolve)
|
||||
mux.HandleFunc("POST /api/messages/{id}/clearlabel", ms.resolve)
|
||||
|
||||
return ms
|
||||
}
|
||||
|
||||
func (ms *mockAPIServer) URL() string { return ms.server.URL }
|
||||
func (ms *mockAPIServer) Close() { ms.server.Close() }
|
||||
|
||||
func (ms *mockAPIServer) Handle(key string, handler http.HandlerFunc) {
|
||||
ms.handlers.Store(key, handler)
|
||||
}
|
||||
|
||||
func (ms *mockAPIServer) resolve(w http.ResponseWriter, r *http.Request) {
|
||||
key := r.Method + " " + r.URL.Path
|
||||
if h, loaded := ms.handlers.Load(key); loaded {
|
||||
h.(http.HandlerFunc)(w, r)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
fmt.Fprintf(w, `{"Code":404,"Message":"not found"}`)
|
||||
}
|
||||
|
||||
// setupE2E creates a fresh test environment. Returns a cleanup func.
|
||||
func setupE2E(t *testing.T) *e2eTestEnv {
|
||||
t.Helper()
|
||||
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Create Pop config directory structure
|
||||
popDir := filepath.Join(tempDir, ".config", "pop")
|
||||
if err := os.MkdirAll(popDir, 0700); err != nil {
|
||||
t.Fatalf("create config dir: %v", err)
|
||||
}
|
||||
keyringDir := filepath.Join(popDir, "keyring")
|
||||
if err := os.MkdirAll(keyringDir, 0700); err != nil {
|
||||
t.Fatalf("create keyring dir: %v", err)
|
||||
}
|
||||
|
||||
// Override HOME so config.NewConfigManager() resolves to our temp dir
|
||||
origHome := os.Getenv("HOME")
|
||||
os.Setenv("HOME", tempDir)
|
||||
t.Cleanup(func() { os.Setenv("HOME", origHome) })
|
||||
|
||||
srv := newMockAPIServer(t)
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
return &e2eTestEnv{
|
||||
t: t,
|
||||
tempDir: tempDir,
|
||||
mockServer: srv,
|
||||
origHome: origHome,
|
||||
}
|
||||
}
|
||||
|
||||
// writeEncryptedSession writes an encrypted session to both the keyring file
|
||||
// and session.json, simulating a successful login.
|
||||
func (env *e2eTestEnv) writeEncryptedSession(uid, accessToken, refreshToken string, expiresAt int64) {
|
||||
sessionData, _ := json.Marshal(map[string]interface{}{
|
||||
"uid": uid,
|
||||
"access_token": accessToken,
|
||||
"refresh_token": refreshToken,
|
||||
"expires_at": expiresAt,
|
||||
"two_factor_enabled": false,
|
||||
"mail_passphrase": "test-passphrase",
|
||||
})
|
||||
|
||||
// Encrypt with AES-256-GCM (same scheme as auth.encryptSession)
|
||||
key := make([]byte, 32)
|
||||
for i := range key {
|
||||
key[i] = byte('k' + i%16)
|
||||
}
|
||||
nonce := make([]byte, 12)
|
||||
for i := range nonce {
|
||||
nonce[i] = byte('n' + i%8)
|
||||
}
|
||||
|
||||
block, _ := aes.NewCipher(key)
|
||||
aead, _ := cipher.NewGCM(block)
|
||||
sealed := aead.Seal(nil, nonce, sessionData, nil)
|
||||
|
||||
encrypted := fmt.Sprintf("%s|%s|%s",
|
||||
base64.StdEncoding.EncodeToString(key),
|
||||
base64.StdEncoding.EncodeToString(nonce),
|
||||
base64.StdEncoding.EncodeToString(sealed),
|
||||
)
|
||||
|
||||
// Write to session.json
|
||||
sessionFile := filepath.Join(env.tempDir, ".config", "pop", "session.json")
|
||||
os.WriteFile(sessionFile, []byte(encrypted), 0600)
|
||||
|
||||
// Write to keyring (file-based keyring stores in keyring/ directory)
|
||||
keyringFile := filepath.Join(env.tempDir, ".config", "pop", "keyring", "session")
|
||||
os.WriteFile(keyringFile, []byte(encrypted), 0600)
|
||||
}
|
||||
|
||||
// jsonResp writes a JSON response with the given status code.
|
||||
func jsonResp(t *testing.T, w http.ResponseWriter, code int, v interface{}) {
|
||||
t.Helper()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(code)
|
||||
json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
|
||||
// runCommand executes a cobra command with the given args, capturing stdout/stderr.
|
||||
func runCommand(root *cobra.Command, args []string) (string, string, error) {
|
||||
bufOut, bufErr := &bytes.Buffer{}, &bytes.Buffer{}
|
||||
root.SetOut(bufOut)
|
||||
root.SetErr(bufErr)
|
||||
root.SetArgs(args)
|
||||
|
||||
// Disable os.Exit on error by setting SilenceErrors
|
||||
root.SilenceErrors = true
|
||||
err := root.Execute()
|
||||
|
||||
return bufOut.String(), bufErr.String(), err
|
||||
}
|
||||
|
||||
// runFreshCommand creates a fresh root command tree and executes the given args.
|
||||
// Captures both cobra output and os.Stdout (since CLI commands use fmt.Printf).
|
||||
func runFreshCommand(args []string) (string, string, error) {
|
||||
stdoutMu.Lock()
|
||||
defer stdoutMu.Unlock()
|
||||
|
||||
root := newRootCmdBase()
|
||||
root.SetArgs(args)
|
||||
root.SilenceErrors = true
|
||||
|
||||
// Capture os.Stdout since CLI commands use fmt.Printf directly
|
||||
origStdout := os.Stdout
|
||||
origStderr := os.Stderr
|
||||
|
||||
rOut, wOut, _ := os.Pipe()
|
||||
os.Stdout = wOut
|
||||
rErr, wErr, _ := os.Pipe()
|
||||
os.Stderr = wErr
|
||||
|
||||
err := root.Execute()
|
||||
|
||||
wOut.Close()
|
||||
wErr.Close()
|
||||
os.Stdout = origStdout
|
||||
os.Stderr = origStderr
|
||||
|
||||
outBytes, _ := io.ReadAll(rOut)
|
||||
errBytes, _ := io.ReadAll(rErr)
|
||||
rOut.Close()
|
||||
rErr.Close()
|
||||
|
||||
return string(outBytes), string(errBytes), err
|
||||
}
|
||||
Reference in New Issue
Block a user