@@ -2,87 +2,94 @@ package cli_test
22
33import (
44 "bytes"
5+ "crypto/rand"
6+ "encoding/binary"
7+ "fmt"
58 "net/url"
69 "os"
710 "path"
811 "runtime"
912 "testing"
13+ "time"
1014
1115 "github.com/stretchr/testify/assert"
1216 "github.com/stretchr/testify/require"
1317
1418 "github.com/coder/coder/v2/cli"
1519 "github.com/coder/coder/v2/cli/clitest"
20+ "github.com/coder/coder/v2/cli/sessionstore"
1621 "github.com/coder/coder/v2/coderd/coderdtest"
1722 "github.com/coder/coder/v2/pty/ptytest"
23+ "github.com/coder/serpent"
1824)
1925
20- // mockKeyring is a mock sessionstore.Backend implementation.
21- type mockKeyring struct {
22- credentials map [string ]string // service name -> credential
26+ // keyringTestServiceName generates a unique service name for keyring tests
27+ // using the test name and a nanosecond timestamp to prevent collisions.
28+ func keyringTestServiceName (t * testing.T ) string {
29+ t .Helper ()
30+ var n uint32
31+ err := binary .Read (rand .Reader , binary .BigEndian , & n )
32+ if err != nil {
33+ t .Fatal (err )
34+ }
35+ return fmt .Sprintf ("%s_%v_%d" , t .Name (), time .Now ().UnixNano (), n )
2336}
2437
25- const mockServiceName = "mock-service-name"
38+ // instrumentKeyring sets up the CLI invocation to use the actual OS keyring
39+ // with a unique test service name to allow test parallelization. It returns
40+ // the backend and URL for verification of keyring contents in tests.
41+ func instrumentKeyring (t * testing.T , inv * serpent.Invocation , serverURL string ) (sessionstore.Backend , * url.URL ) {
42+ t .Helper ()
2643
27- func newMockKeyring () * mockKeyring {
28- return & mockKeyring {credentials : make (map [string ]string )}
29- }
44+ serviceName := keyringTestServiceName (t )
45+ backend := sessionstore .NewKeyringWithService (serviceName )
3046
31- func (m * mockKeyring ) Read (_ * url.URL ) (string , error ) {
32- cred , ok := m .credentials [mockServiceName ]
33- if ! ok {
34- return "" , os .ErrNotExist
35- }
36- return cred , nil
37- }
47+ srvURL , err := url .Parse (serverURL )
48+ require .NoError (t , err )
3849
39- func (m * mockKeyring ) Write (_ * url.URL , token string ) error {
40- m .credentials [mockServiceName ] = token
41- return nil
42- }
50+ t .Cleanup (func () {
51+ _ = backend .Delete (srvURL )
52+ })
4353
44- func ( m * mockKeyring ) Delete ( _ * url. URL ) error {
45- _ , ok := m . credentials [ mockServiceName ]
46- if ! ok {
47- return os . ErrNotExist
48- }
49- delete ( m . credentials , mockServiceName )
50- return nil
54+ var root cli. RootCmd
55+ cmd , err := root . Command ( root . AGPL ())
56+ require . NoError ( t , err )
57+ root . WithSessionStorageBackend ( backend )
58+ inv . Command = cmd
59+
60+ return backend , srvURL
5161}
5262
5363func TestUseKeyring (t * testing.T ) {
54- // Verify that the --use-keyring flag opts into using a keyring backend for
55- // storing session tokens instead of plain text files.
64+ // Verify that the --use-keyring flag default opts into using a keyring backend
65+ // for storing session tokens instead of plain text files.
5666 t .Parallel ()
5767
5868 t .Run ("Login" , func (t * testing.T ) {
5969 t .Parallel ()
6070
71+ if runtime .GOOS != "windows" && runtime .GOOS != "darwin" {
72+ t .Skip ("keyring is not supported on this OS" )
73+ }
74+
6175 // Create a test server
6276 client := coderdtest .New (t , nil )
6377 coderdtest .CreateFirstUser (t , client )
6478
6579 // Create a pty for interactive prompts
6680 pty := ptytest .New (t )
6781
68- // Create CLI invocation with --use-keyring flag
82+ // Create CLI invocation which defaults to using the keyring
6983 inv , cfg := clitest .New (t ,
7084 "login" ,
7185 "--force-tty" ,
72- "--use-keyring" ,
7386 "--no-open" ,
7487 client .URL .String (),
7588 )
7689 inv .Stdin = pty .Input ()
7790 inv .Stdout = pty .Output ()
7891
79- // Inject the mock backend before running the command
80- var root cli.RootCmd
81- cmd , err := root .Command (root .AGPL ())
82- require .NoError (t , err )
83- mockBackend := newMockKeyring ()
84- root .WithSessionStorageBackend (mockBackend )
85- inv .Command = cmd
92+ backend , srvURL := instrumentKeyring (t , inv , client .URL .String ())
8693
8794 // Run login in background
8895 doneChan := make (chan struct {})
@@ -100,43 +107,40 @@ func TestUseKeyring(t *testing.T) {
100107
101108 // Verify that session file was NOT created (using keyring instead)
102109 sessionFile := path .Join (string (cfg ), "session" )
103- _ , err = os .Stat (sessionFile )
110+ _ , err : = os .Stat (sessionFile )
104111 require .True (t , os .IsNotExist (err ), "session file should not exist when using keyring" )
105112
106- // Verify that the credential IS stored in mock keyring
107- cred , err := mockBackend .Read (nil )
108- require .NoError (t , err , "credential should be stored in mock keyring" )
113+ // Verify that the credential IS stored in OS keyring
114+ cred , err := backend .Read (srvURL )
115+ require .NoError (t , err , "credential should be stored in OS keyring" )
109116 require .Equal (t , client .SessionToken (), cred , "stored token should match login token" )
110117 })
111118
112119 t .Run ("Logout" , func (t * testing.T ) {
113120 t .Parallel ()
114121
122+ if runtime .GOOS != "windows" && runtime .GOOS != "darwin" {
123+ t .Skip ("keyring is not supported on this OS" )
124+ }
125+
115126 // Create a test server
116127 client := coderdtest .New (t , nil )
117128 coderdtest .CreateFirstUser (t , client )
118129
119130 // Create a pty for interactive prompts
120131 pty := ptytest .New (t )
121132
122- // First, login with --use- keyring
133+ // First, login with the keyring (default)
123134 loginInv , cfg := clitest .New (t ,
124135 "login" ,
125136 "--force-tty" ,
126- "--use-keyring" ,
127137 "--no-open" ,
128138 client .URL .String (),
129139 )
130140 loginInv .Stdin = pty .Input ()
131141 loginInv .Stdout = pty .Output ()
132142
133- // Inject the mock backend
134- var loginRoot cli.RootCmd
135- loginCmd , err := loginRoot .Command (loginRoot .AGPL ())
136- require .NoError (t , err )
137- mockBackend := newMockKeyring ()
138- loginRoot .WithSessionStorageBackend (mockBackend )
139- loginInv .Command = loginCmd
143+ backend , srvURL := instrumentKeyring (t , loginInv , client .URL .String ())
140144
141145 doneChan := make (chan struct {})
142146 go func () {
@@ -150,24 +154,23 @@ func TestUseKeyring(t *testing.T) {
150154 pty .ExpectMatch ("Welcome to Coder" )
151155 <- doneChan
152156
153- // Verify credential exists in mock keyring
154- cred , err := mockBackend .Read (nil )
157+ // Verify credential exists in OS keyring
158+ cred , err := backend .Read (srvURL )
155159 require .NoError (t , err , "read credential should succeed before logout" )
156- require .NotEmpty (t , cred , "credential should exist after logout" )
160+ require .NotEmpty (t , cred , "credential should exist before logout" )
157161
158- // Now run logout with --use-keyring
162+ // Now logout
159163 logoutInv , _ := clitest .New (t ,
160164 "logout" ,
161- "--use-keyring" ,
162165 "--yes" ,
163166 "--global-config" , string (cfg ),
164167 )
165168
166- // Inject the same mock backend
169+ // Instrument logout with the same backend
167170 var logoutRoot cli.RootCmd
168171 logoutCmd , err := logoutRoot .Command (logoutRoot .AGPL ())
169172 require .NoError (t , err )
170- logoutRoot .WithSessionStorageBackend (mockBackend )
173+ logoutRoot .WithSessionStorageBackend (backend )
171174 logoutInv .Command = logoutCmd
172175
173176 var logoutOut bytes.Buffer
@@ -176,22 +179,25 @@ func TestUseKeyring(t *testing.T) {
176179 err = logoutInv .Run ()
177180 require .NoError (t , err , "logout should succeed" )
178181
179- // Verify the credential was deleted from mock keyring
180- _ , err = mockBackend .Read (nil )
182+ // Verify the credential was deleted from OS keyring
183+ _ , err = backend .Read (srvURL )
181184 require .ErrorIs (t , err , os .ErrNotExist , "credential should be deleted from keyring after logout" )
182185 })
183186
184- t .Run ("OmitFlag " , func (t * testing.T ) {
187+ t .Run ("DefaultFileStorage " , func (t * testing.T ) {
185188 t .Parallel ()
186189
190+ if runtime .GOOS != "linux" {
191+ t .Skip ("file storage is the default for Linux" )
192+ }
193+
187194 // Create a test server
188195 client := coderdtest .New (t , nil )
189196 coderdtest .CreateFirstUser (t , client )
190197
191198 // Create a pty for interactive prompts
192199 pty := ptytest .New (t )
193200
194- // --use-keyring flag omitted (should use file-based storage)
195201 inv , cfg := clitest .New (t ,
196202 "login" ,
197203 "--force-tty" ,
@@ -216,7 +222,7 @@ func TestUseKeyring(t *testing.T) {
216222 // Verify that session file WAS created (not using keyring)
217223 sessionFile := path .Join (string (cfg ), "session" )
218224 _ , err := os .Stat (sessionFile )
219- require .NoError (t , err , "session file should exist when NOT using --use-keyring" )
225+ require .NoError (t , err , "session file should exist when NOT using --use-keyring on Linux " )
220226
221227 // Read and verify the token from file
222228 content , err := os .ReadFile (sessionFile )
@@ -234,7 +240,8 @@ func TestUseKeyring(t *testing.T) {
234240 // Create a pty for interactive prompts
235241 pty := ptytest .New (t )
236242
237- // Login using CODER_USE_KEYRING environment variable instead of flag
243+ // Login using CODER_USE_KEYRING environment variable set to disable keyring usage,
244+ // which should have the same behavior on all platforms.
238245 inv , cfg := clitest .New (t ,
239246 "login" ,
240247 "--force-tty" ,
@@ -243,15 +250,49 @@ func TestUseKeyring(t *testing.T) {
243250 )
244251 inv .Stdin = pty .Input ()
245252 inv .Stdout = pty .Output ()
246- inv .Environ .Set ("CODER_USE_KEYRING" , "true " )
253+ inv .Environ .Set ("CODER_USE_KEYRING" , "false " )
247254
248- // Inject the mock backend
249- var root cli.RootCmd
250- cmd , err := root .Command (root .AGPL ())
251- require .NoError (t , err )
252- mockBackend := newMockKeyring ()
253- root .WithSessionStorageBackend (mockBackend )
254- inv .Command = cmd
255+ doneChan := make (chan struct {})
256+ go func () {
257+ defer close (doneChan )
258+ err := inv .Run ()
259+ assert .NoError (t , err )
260+ }()
261+
262+ pty .ExpectMatch ("Paste your token here:" )
263+ pty .WriteLine (client .SessionToken ())
264+ pty .ExpectMatch ("Welcome to Coder" )
265+ <- doneChan
266+
267+ // Verify that session file WAS created (not using keyring)
268+ sessionFile := path .Join (string (cfg ), "session" )
269+ _ , err := os .Stat (sessionFile )
270+ require .NoError (t , err , "session file should exist when CODER_USE_KEYRING set to false" )
271+
272+ // Read and verify the token from file
273+ content , err := os .ReadFile (sessionFile )
274+ require .NoError (t , err , "should be able to read session file" )
275+ require .Equal (t , client .SessionToken (), string (content ), "file should contain the session token" )
276+ })
277+
278+ t .Run ("DisableKeyringWithFlag" , func (t * testing.T ) {
279+ t .Parallel ()
280+
281+ client := coderdtest .New (t , nil )
282+ coderdtest .CreateFirstUser (t , client )
283+ pty := ptytest .New (t )
284+
285+ // Login with --use-keyring=false to explicitly disable keyring usage, which
286+ // should have the same behavior on all platforms.
287+ inv , cfg := clitest .New (t ,
288+ "login" ,
289+ "--use-keyring=false" ,
290+ "--force-tty" ,
291+ "--no-open" ,
292+ client .URL .String (),
293+ )
294+ inv .Stdin = pty .Input ()
295+ inv .Stdout = pty .Output ()
255296
256297 doneChan := make (chan struct {})
257298 go func () {
@@ -265,15 +306,15 @@ func TestUseKeyring(t *testing.T) {
265306 pty .ExpectMatch ("Welcome to Coder" )
266307 <- doneChan
267308
268- // Verify that session file was NOT created (using keyring via env var )
309+ // Verify that session file WAS created (not using keyring)
269310 sessionFile := path .Join (string (cfg ), "session" )
270- _ , err = os .Stat (sessionFile )
271- require .True (t , os . IsNotExist ( err ) , "session file should not exist when using keyring via env var " )
311+ _ , err : = os .Stat (sessionFile )
312+ require .NoError (t , err , "session file should exist when --use- keyring=false is specified " )
272313
273- // Verify credential is in mock keyring
274- cred , err := mockBackend . Read ( nil )
275- require .NoError (t , err , "credential should be stored in keyring when CODER_USE_KEYRING=true " )
276- require .NotEmpty (t , cred )
314+ // Read and verify the token from file
315+ content , err := os . ReadFile ( sessionFile )
316+ require .NoError (t , err , "should be able to read session file " )
317+ require .Equal (t , client . SessionToken (), string ( content ), "file should contain the session token" )
277318 })
278319}
279320
@@ -287,7 +328,7 @@ func TestUseKeyringUnsupportedOS(t *testing.T) {
287328 t .Skipf ("Skipping unsupported OS test on %s where keyring is supported" , runtime .GOOS )
288329 }
289330
290- const expMessage = "keyring storage is not supported on this operating system; remove the --use-keyring flag "
331+ const expMessage = "keyring storage is not supported on this operating system; omit --use-keyring to use file-based storage "
291332
292333 t .Run ("LoginWithUnsupportedKeyring" , func (t * testing.T ) {
293334 t .Parallel ()
@@ -317,7 +358,7 @@ func TestUseKeyringUnsupportedOS(t *testing.T) {
317358 coderdtest .CreateFirstUser (t , client )
318359 pty := ptytest .New (t )
319360
320- // First login without keyring to create a session
361+ // First login without keyring to create a session (default behavior)
321362 loginInv , cfg := clitest .New (t ,
322363 "login" ,
323364 "--force-tty" ,
0 commit comments