@@ -123,12 +123,18 @@ func TestEnvVarOverride(t *testing.T) {
123123 t .Fatalf ("Load() error: %v" , err )
124124 }
125125
126- if cfg .AccountID != "FROM_ENV" {
127- t .Errorf ("AccountID = %q, want %q (env override)" , cfg .AccountID , "FROM_ENV" )
126+ // Env overlay is applied at read time via ActiveProfileConfig,
127+ // not mutated into stored fields during Load.
128+ p := cfg .ActiveProfileConfig ()
129+ if p .AccountID != "FROM_ENV" {
130+ t .Errorf ("ActiveProfileConfig().AccountID = %q, want %q (env override)" , p .AccountID , "FROM_ENV" )
131+ }
132+ if p .ClientID != "FROM_FILE" {
133+ t .Errorf ("ActiveProfileConfig().ClientID = %q, want %q" , p .ClientID , "FROM_FILE" )
128134 }
129- // Other fields should still come from file
130- if cfg .ClientID != "FROM_FILE " {
131- t .Errorf ("ClientID = %q, want %q" , cfg .ClientID , "FROM_FILE " )
135+ // Stored fields must remain untouched so Save can't leak env values to disk.
136+ if cfg .AccountID != "ACC_FROM_FILE " {
137+ t .Errorf ("stored cfg.AccountID = %q, want %q (Load must not mutate stored fields) " , cfg .AccountID , "ACC_FROM_FILE " )
132138 }
133139}
134140
@@ -326,30 +332,122 @@ func TestAllEnvVarOverrides(t *testing.T) {
326332 path := filepath .Join (dir , "config.json" )
327333
328334 base := & Config {Format : "json" }
335+ base .SetProfile ("default" , & Profile {ClientID : "fileclientid" , AccountID : "fileaccount" , Environment : "prod" })
329336 if err := Save (path , base ); err != nil {
330337 t .Fatalf ("Save() error: %v" , err )
331338 }
332339
333340 t .Setenv ("BW_CLIENT_ID" , "envclientid" )
334341 t .Setenv ("BW_ACCOUNT_ID" , "envaccount" )
335- t .Setenv ("BW_FORMAT" , "table" )
336342 t .Setenv ("BW_ENVIRONMENT" , "custom" )
337343
338344 cfg , err := Load (path )
339345 if err != nil {
340346 t .Fatalf ("Load() error: %v" , err )
341347 }
342348
343- if cfg .ClientID != "envclientid" {
344- t .Errorf ("ClientID = %q, want %q" , cfg .ClientID , "envclientid" )
349+ p := cfg .ActiveProfileConfig ()
350+ if p .ClientID != "envclientid" {
351+ t .Errorf ("ActiveProfileConfig().ClientID = %q, want %q" , p .ClientID , "envclientid" )
345352 }
346- if cfg .AccountID != "envaccount" {
347- t .Errorf ("AccountID = %q, want %q" , cfg .AccountID , "envaccount" )
353+ if p .AccountID != "envaccount" {
354+ t .Errorf ("ActiveProfileConfig(). AccountID = %q, want %q" , p .AccountID , "envaccount" )
348355 }
349- if cfg . Format != "table " {
350- t .Errorf ("Format = %q, want %q" , cfg . Format , "table " )
356+ if p . Environment != "custom " {
357+ t .Errorf ("ActiveProfileConfig().Environment = %q, want %q" , p . Environment , "custom " )
351358 }
352- if cfg .Environment != "custom" {
353- t .Errorf ("Environment = %q, want %q" , cfg .Environment , "custom" )
359+
360+ // Stored profile must remain untouched.
361+ stored := cfg .Profiles ["default" ]
362+ if stored .ClientID != "fileclientid" || stored .AccountID != "fileaccount" || stored .Environment != "prod" {
363+ t .Errorf ("stored profile mutated by Load: %+v" , stored )
364+ }
365+ }
366+
367+ // TestLoad_EnvOverlayDoesNotPersistOntoStoredProfiles guards against the
368+ // regression where Load applied env vars to the live *Profile pointer in
369+ // cfg.Profiles, so that any subsequent Save (login, switch, etc.) would
370+ // silently rewrite the previously-active profile on disk with env values.
371+ func TestLoad_EnvOverlayDoesNotPersistOntoStoredProfiles (t * testing.T ) {
372+ dir := t .TempDir ()
373+ path := filepath .Join (dir , "config.json" )
374+
375+ cfg := & Config {Format : "json" }
376+ cfg .SetProfile ("prod" , & Profile {ClientID : "prod-id" , AccountID : "ACCT_A" , Environment : "prod" })
377+ cfg .SetProfile ("dev" , & Profile {ClientID : "dev-id" , AccountID : "ACCT_B" , Environment : "test" })
378+ cfg .ActiveProfile = "prod"
379+ if err := Save (path , cfg ); err != nil {
380+ t .Fatal (err )
381+ }
382+
383+ t .Setenv ("BW_ACCOUNT_ID" , "ENV_ACCT_Z" )
384+ t .Setenv ("BW_CLIENT_ID" , "ENV_CLIENT_Z" )
385+ t .Setenv ("BW_ENVIRONMENT" , "ENV_HOST_Z" )
386+
387+ // Simulate a writer flow: Load → mutate something unrelated → Save.
388+ loaded , err := Load (path )
389+ if err != nil {
390+ t .Fatal (err )
391+ }
392+ loaded .ActiveProfile = "dev"
393+ if err := Save (path , loaded ); err != nil {
394+ t .Fatal (err )
395+ }
396+
397+ // Re-read with env vars cleared to see only what was persisted.
398+ t .Setenv ("BW_ACCOUNT_ID" , "" )
399+ t .Setenv ("BW_CLIENT_ID" , "" )
400+ t .Setenv ("BW_ENVIRONMENT" , "" )
401+
402+ fresh , err := Load (path )
403+ if err != nil {
404+ t .Fatal (err )
405+ }
406+
407+ prod := fresh .Profiles ["prod" ]
408+ if prod .AccountID != "ACCT_A" || prod .ClientID != "prod-id" || prod .Environment != "prod" {
409+ t .Errorf ("prod profile leaked env values: %+v" , prod )
410+ }
411+ dev := fresh .Profiles ["dev" ]
412+ if dev .AccountID != "ACCT_B" || dev .ClientID != "dev-id" || dev .Environment != "test" {
413+ t .Errorf ("dev profile leaked env values: %+v" , dev )
414+ }
415+ }
416+
417+ func TestActiveProfileConfig_AppliesEnvOverlay (t * testing.T ) {
418+ cfg := & Config {}
419+ cfg .SetProfile ("default" , & Profile {ClientID : "id1" , AccountID : "ACCT_A" , Environment : "prod" })
420+
421+ t .Setenv ("BW_ACCOUNT_ID" , "ENV_ACCT_Z" )
422+ t .Setenv ("BW_CLIENT_ID" , "ENV_CLIENT_Z" )
423+ t .Setenv ("BW_ENVIRONMENT" , "test" )
424+
425+ p := cfg .ActiveProfileConfig ()
426+ if p .AccountID != "ENV_ACCT_Z" {
427+ t .Errorf ("AccountID = %q, want %q" , p .AccountID , "ENV_ACCT_Z" )
428+ }
429+ if p .ClientID != "ENV_CLIENT_Z" {
430+ t .Errorf ("ClientID = %q, want %q" , p .ClientID , "ENV_CLIENT_Z" )
431+ }
432+ if p .Environment != "test" {
433+ t .Errorf ("Environment = %q, want %q" , p .Environment , "test" )
434+ }
435+
436+ // Stored profile must not be mutated by ActiveProfileConfig.
437+ stored := cfg .Profiles ["default" ]
438+ if stored .AccountID != "ACCT_A" || stored .ClientID != "id1" || stored .Environment != "prod" {
439+ t .Errorf ("stored profile mutated by ActiveProfileConfig: %+v" , stored )
440+ }
441+ }
442+
443+ func TestActiveProfileConfig_ReturnsCopySafeToMutate (t * testing.T ) {
444+ cfg := & Config {}
445+ cfg .SetProfile ("default" , & Profile {ClientID : "id1" , AccountID : "ACCT_A" })
446+
447+ p := cfg .ActiveProfileConfig ()
448+ p .AccountID = "MUTATED"
449+
450+ if cfg .Profiles ["default" ].AccountID != "ACCT_A" {
451+ t .Errorf ("mutating ActiveProfileConfig() result leaked into stored profile: %q" , cfg .Profiles ["default" ].AccountID )
354452 }
355453}
0 commit comments