Skip to content

Commit 27d4519

Browse files
authored
Merge branch 'main' into osterman/migrate-uber-mock
2 parents 7681d3e + 8012b2a commit 27d4519

40 files changed

+1774
-120
lines changed

cmd/auth_login.go

Lines changed: 83 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,20 @@ package cmd
33
import (
44
"context"
55
"fmt"
6+
"os"
7+
"time"
68

9+
"github.com/charmbracelet/lipgloss"
10+
"github.com/charmbracelet/lipgloss/table"
711
"github.com/spf13/cobra"
812

913
"github.com/cloudposse/atmos/pkg/auth"
1014
"github.com/cloudposse/atmos/pkg/auth/credentials"
15+
authTypes "github.com/cloudposse/atmos/pkg/auth/types"
1116
"github.com/cloudposse/atmos/pkg/auth/validation"
1217
cfg "github.com/cloudposse/atmos/pkg/config"
1318
"github.com/cloudposse/atmos/pkg/schema"
14-
u "github.com/cloudposse/atmos/pkg/utils"
19+
"github.com/cloudposse/atmos/pkg/ui/theme"
1520
)
1621

1722
// authLoginCmd logs in using a configured identity.
@@ -50,19 +55,8 @@ func executeAuthLoginCommand(cmd *cobra.Command, args []string) error {
5055
return fmt.Errorf("authentication failed: %w", err)
5156
}
5257

53-
// Display success message
54-
u.PrintfMessageToTUI("**Authentication successful!**\n")
55-
u.PrintfMessageToTUI("Provider: %s\n", whoami.Provider)
56-
u.PrintfMessageToTUI("Identity: %s\n", whoami.Identity)
57-
if whoami.Account != "" {
58-
u.PrintfMessageToTUI("Account: %s\n", whoami.Account)
59-
}
60-
if whoami.Region != "" {
61-
u.PrintfMessageToTUI("Region: %s\n", whoami.Region)
62-
}
63-
if whoami.Expiration != nil {
64-
u.PrintfMessageToTUI("Expires: %s\n", whoami.Expiration.Format("2006-01-02 15:04:05 MST"))
65-
}
58+
// Display success message using Atmos theme.
59+
displayAuthSuccess(whoami)
6660

6761
return nil
6862
}
@@ -75,6 +69,81 @@ func createAuthManager(authConfig *schema.AuthConfig) (auth.AuthManager, error)
7569
return auth.NewAuthManager(authConfig, credStore, validator, nil)
7670
}
7771

72+
const (
73+
secondsPerMinute = 60
74+
minutesPerHour = 60
75+
)
76+
77+
// formatDuration formats a duration into a human-readable string.
78+
func formatDuration(d time.Duration) string {
79+
if d < 0 {
80+
return "expired"
81+
}
82+
83+
hours := int(d.Hours())
84+
minutes := int(d.Minutes()) % minutesPerHour
85+
seconds := int(d.Seconds()) % secondsPerMinute
86+
87+
if hours > 0 {
88+
return fmt.Sprintf("%dh %dm", hours, minutes)
89+
}
90+
if minutes > 0 {
91+
return fmt.Sprintf("%dm %ds", minutes, seconds)
92+
}
93+
return fmt.Sprintf("%ds", seconds)
94+
}
95+
96+
// displayAuthSuccess displays a styled success message with authentication details.
97+
func displayAuthSuccess(whoami *authTypes.WhoamiInfo) {
98+
// Display checkmark with success message.
99+
checkMark := theme.Styles.Checkmark
100+
fmt.Fprintf(os.Stderr, "\n%s Authentication successful!\n\n", checkMark)
101+
102+
// Build table rows.
103+
var rows [][]string
104+
rows = append(rows, []string{"Provider", whoami.Provider})
105+
rows = append(rows, []string{"Identity", whoami.Identity})
106+
107+
if whoami.Account != "" {
108+
rows = append(rows, []string{"Account", whoami.Account})
109+
}
110+
111+
if whoami.Region != "" {
112+
rows = append(rows, []string{"Region", whoami.Region})
113+
}
114+
115+
if whoami.Expiration != nil {
116+
expiresStr := whoami.Expiration.Format("2006-01-02 15:04:05 MST")
117+
duration := formatDuration(time.Until(*whoami.Expiration))
118+
// Style duration with darker gray.
119+
durationStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#808080"))
120+
expiresStr = fmt.Sprintf("%s %s", expiresStr, durationStyle.Render(fmt.Sprintf("(%s)", duration)))
121+
rows = append(rows, []string{"Expires", expiresStr})
122+
}
123+
124+
// Create minimal charmbracelet table.
125+
t := table.New().
126+
Rows(rows...).
127+
BorderTop(false).
128+
BorderBottom(false).
129+
BorderLeft(false).
130+
BorderRight(false).
131+
BorderRow(false).
132+
BorderColumn(false).
133+
StyleFunc(func(row, col int) lipgloss.Style {
134+
if col == 0 {
135+
// Key column - use cyan color.
136+
return lipgloss.NewStyle().
137+
Foreground(lipgloss.Color(theme.ColorCyan)).
138+
Padding(0, 1, 0, 2)
139+
}
140+
// Value column - default color with padding.
141+
return lipgloss.NewStyle().Padding(0, 1)
142+
})
143+
144+
fmt.Fprintf(os.Stderr, "%s\n\n", t)
145+
}
146+
78147
func init() {
79148
authCmd.AddCommand(authLoginCmd)
80149
}

cmd/auth_login_test.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package cmd
33
import (
44
"bytes"
55
"fmt"
6-
"os"
76
"testing"
87

98
"github.com/spf13/cobra"
@@ -127,10 +126,6 @@ func TestAuthLoginCmd(t *testing.T) {
127126
t.Run(tt.name, func(t *testing.T) {
128127
_ = NewTestKit(t)
129128

130-
// Setup test environment
131-
originalArgs := os.Args
132-
defer func() { os.Args = originalArgs }()
133-
134129
// Create a new command instance for testing
135130
cmd := &cobra.Command{
136131
Use: "login",

cmd/auth_whoami.go

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ import (
66
"fmt"
77
"os"
88
"strings"
9+
"time"
910

11+
"github.com/charmbracelet/lipgloss"
12+
"github.com/charmbracelet/lipgloss/table"
1013
homedir "github.com/mitchellh/go-homedir"
1114
"github.com/spf13/cobra"
1215
"github.com/spf13/viper"
@@ -16,6 +19,7 @@ import (
1619
cfg "github.com/cloudposse/atmos/pkg/config"
1720
log "github.com/cloudposse/atmos/pkg/logger"
1821
"github.com/cloudposse/atmos/pkg/schema"
22+
"github.com/cloudposse/atmos/pkg/ui/theme"
1923
)
2024

2125
// authWhoamiCmd shows current authentication status.
@@ -100,22 +104,71 @@ func printWhoamiJSON(whoami *authTypes.WhoamiInfo) error {
100104
}
101105

102106
func printWhoamiHuman(whoami *authTypes.WhoamiInfo) {
107+
const (
108+
expiringThresholdMinutes = 15
109+
)
110+
103111
fmt.Fprintf(os.Stderr, "Current Authentication Status\n\n")
104-
fmt.Fprintf(os.Stderr, "Provider: %s\n", whoami.Provider)
105-
fmt.Fprintf(os.Stderr, "Identity: %s\n", whoami.Identity)
112+
113+
// Build table rows.
114+
var rows [][]string
115+
rows = append(rows, []string{"Provider", whoami.Provider})
116+
rows = append(rows, []string{"Identity", whoami.Identity})
117+
106118
if whoami.Principal != "" {
107-
fmt.Fprintf(os.Stderr, "Principal: %s\n", whoami.Principal)
119+
rows = append(rows, []string{"Principal", whoami.Principal})
108120
}
121+
109122
if whoami.Account != "" {
110-
fmt.Fprintf(os.Stderr, "Account: %s\n", whoami.Account)
123+
rows = append(rows, []string{"Account", whoami.Account})
111124
}
125+
112126
if whoami.Region != "" {
113-
fmt.Fprintf(os.Stderr, "Region: %s\n", whoami.Region)
127+
rows = append(rows, []string{"Region", whoami.Region})
114128
}
129+
115130
if whoami.Expiration != nil {
116-
fmt.Fprintf(os.Stderr, "Expires: %s\n", whoami.Expiration.Format("2006-01-02 15:04:05 MST"))
117-
}
118-
fmt.Fprintf(os.Stderr, "Last Updated: %s\n", whoami.LastUpdated.Format("2006-01-02 15:04:05 MST"))
131+
expiresStr := whoami.Expiration.Format("2006-01-02 15:04:05 MST")
132+
duration := formatDuration(time.Until(*whoami.Expiration))
133+
134+
// Check if expiring within threshold and style duration accordingly.
135+
timeUntilExpiration := time.Until(*whoami.Expiration)
136+
var durationStyle lipgloss.Style
137+
if timeUntilExpiration > 0 && timeUntilExpiration < expiringThresholdMinutes*time.Minute {
138+
// Expiring soon - use red for duration.
139+
durationStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(theme.ColorRed))
140+
} else {
141+
// Normal - use darker gray for duration.
142+
durationStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#808080"))
143+
}
144+
expiresStr = fmt.Sprintf("%s %s", expiresStr, durationStyle.Render(fmt.Sprintf("(%s)", duration)))
145+
146+
rows = append(rows, []string{"Expires", expiresStr})
147+
}
148+
149+
rows = append(rows, []string{"Last Updated", whoami.LastUpdated.Format("2006-01-02 15:04:05 MST")})
150+
151+
// Create minimal charmbracelet table.
152+
t := table.New().
153+
Rows(rows...).
154+
BorderTop(false).
155+
BorderBottom(false).
156+
BorderLeft(false).
157+
BorderRight(false).
158+
BorderRow(false).
159+
BorderColumn(false).
160+
StyleFunc(func(row, col int) lipgloss.Style {
161+
if col == 0 {
162+
// Key column - use cyan color.
163+
return lipgloss.NewStyle().
164+
Foreground(lipgloss.Color(theme.ColorCyan)).
165+
Padding(0, 1, 0, 2)
166+
}
167+
// Value column - default color with padding.
168+
return lipgloss.NewStyle().Padding(0, 1)
169+
})
170+
171+
fmt.Fprintf(os.Stderr, "%s\n\n", t)
119172
}
120173

121174
// redactHomeDir replaces occurrences of the homeDir at the start of v with "~" to avoid leaking user paths.

cmd/cmd_utils_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,9 @@ func TestIsVersionCommand(t *testing.T) {
171171

172172
for _, tt := range tests {
173173
t.Run(tt.name, func(t *testing.T) {
174-
// Save original os.Args
174+
// isVersionCommand() reads os.Args directly, so tests must manipulate it.
175+
// This is acceptable because isVersionCommand() is called early in init
176+
// before Cobra command parsing happens.
175177
oldArgs := os.Args
176178
defer func() { os.Args = oldArgs }()
177179

cmd/root_test.go

Lines changed: 3 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,8 @@ func TestNoColorLog(t *testing.T) {
5454
var buf bytes.Buffer
5555
log.SetOutput(&buf)
5656

57-
oldArgs := os.Args
58-
defer func() {
59-
os.Args = oldArgs
60-
}()
61-
// Set the arguments for the command
62-
os.Args = []string{"atmos", "about"}
57+
// Use SetArgs - TestKit handles cleanup automatically.
58+
RootCmd.SetArgs([]string{"about"})
6359

6460
// Reset buffer to ensure clean state (previous tests may have written to logger).
6561
buf.Reset()
@@ -77,20 +73,7 @@ func TestNoColorLog(t *testing.T) {
7773
}
7874

7975
func TestInitFunction(t *testing.T) {
80-
// Save the original state
81-
originalArgs := os.Args
82-
originalEnvVars := make(map[string]string)
83-
defer func() {
84-
// Restore original state
85-
os.Args = originalArgs
86-
for k, v := range originalEnvVars {
87-
if v == "" {
88-
os.Unsetenv(k)
89-
} else {
90-
os.Setenv(k, v)
91-
}
92-
}
93-
}()
76+
// Test doesn't modify os.Args, so no need to save/restore.
9477

9578
// Test cases
9679
tests := []struct {
@@ -459,12 +442,6 @@ func TestVersionFlagExecutionPath(t *testing.T) {
459442

460443
// TestIsCompletionCommand tests the isCompletionCommand function.
461444
func TestIsCompletionCommand(t *testing.T) {
462-
// Save original args.
463-
originalArgs := os.Args
464-
defer func() {
465-
os.Args = originalArgs
466-
}()
467-
468445
tests := []struct {
469446
name string
470447
args []string

0 commit comments

Comments
 (0)