@@ -2,27 +2,37 @@ package mock
22
33import (
44 "context"
5+ "encoding/json"
56 "errors"
67 "fmt"
8+ "os"
9+ "path/filepath"
710 "time"
811
912 "github.com/cloudposse/atmos/pkg/auth/types"
1013 "github.com/cloudposse/atmos/pkg/perf"
1114 "github.com/cloudposse/atmos/pkg/schema"
1215)
1316
17+ const (
18+ // MockRegion is the default AWS region for mock credentials.
19+ MockRegion = "us-east-1"
20+
21+ // MockFilePermissions are the file permissions for credential files (owner read/write only).
22+ MockFilePermissions = 0o600
23+ )
24+
1425// ErrNoStoredCredentials indicates storage is supported but currently empty.
1526// This error is returned when LoadCredentials is called before authentication.
1627var ErrNoStoredCredentials = errors .New ("mock identity has no stored credentials" )
1728
1829// Identity is a mock authentication identity for testing purposes only.
19- // It simulates provider-agnostic credential storage behavior by tracking whether
20- // credentials have been persisted (like AWS writing to ~/.aws/credentials, or
21- // GitHub storing a token in an environment variable/ file).
30+ // It simulates provider-agnostic credential storage behavior by persisting
31+ // credentials to disk (like AWS writing to ~/.aws/credentials, or GitHub storing
32+ // a token in a file). This allows credentials to persist across process invocations .
2233type Identity struct {
23- name string
24- config * schema.Identity
25- hasStoredCredentials bool // Tracks if credentials have been written to "storage"
34+ name string
35+ config * schema.Identity
2636}
2737
2838// NewIdentity creates a new mock identity.
@@ -35,6 +45,15 @@ func NewIdentity(name string, config *schema.Identity) *Identity {
3545 }
3646}
3747
48+ // getCredentialsFilePath returns the path where mock credentials are stored.
49+ // This simulates how real providers persist credentials to disk.
50+ func (i * Identity ) getCredentialsFilePath () string {
51+ // Use a temp directory that's cleaned up by the OS.
52+ // In production, real providers would use XDG directories like ~/.config/atmos/aws/{provider}/.
53+ tmpDir := os .TempDir ()
54+ return filepath .Join (tmpDir , "atmos-mock-" + i .name + ".json" )
55+ }
56+
3857// Kind returns the identity kind.
3958func (i * Identity ) Kind () string {
4059 defer perf .Track (nil , "mock.Identity.Kind" )()
@@ -65,7 +84,7 @@ func (i *Identity) Authenticate(ctx context.Context, baseCreds types.ICredential
6584 AccessKeyID : fmt .Sprintf ("MOCK_KEY_%s" , i .name ),
6685 SecretAccessKey : fmt .Sprintf ("MOCK_SECRET_%s" , i .name ),
6786 SessionToken : fmt .Sprintf ("MOCK_TOKEN_%s" , i .name ),
68- Region : "us-east-1" ,
87+ Region : MockRegion ,
6988 Expiration : fixedExpiration ,
7089 }, nil
7190}
@@ -92,8 +111,8 @@ func (i *Identity) Environment() (map[string]string, error) {
92111 env ["AWS_SHARED_CREDENTIALS_FILE" ] = "/tmp/mock-credentials"
93112 env ["AWS_CONFIG_FILE" ] = "/tmp/mock-config"
94113 env ["AWS_PROFILE" ] = i .name
95- env ["AWS_REGION" ] = "us-east-1"
96- env ["AWS_DEFAULT_REGION" ] = "us-east-1"
114+ env ["AWS_REGION" ] = MockRegion
115+ env ["AWS_DEFAULT_REGION" ] = MockRegion
97116
98117 return env , nil
99118}
@@ -110,15 +129,35 @@ func (i *Identity) PrepareEnvironment(_ context.Context, environ map[string]stri
110129}
111130
112131// PostAuthenticate simulates writing credentials to persistent storage.
113- // For mock identities, this tracks that credentials have been "stored" after authentication .
132+ // For mock identities, this writes credentials to a temporary file to persist them .
114133// This mimics real provider behavior where authentication results in credentials being written
115134// to disk (AWS ~/.aws/credentials), environment variables (GitHub token), or other storage.
116135func (i * Identity ) PostAuthenticate (ctx context.Context , params * types.PostAuthenticateParams ) error {
117136 defer perf .Track (nil , "mock.Identity.PostAuthenticate" )()
118137
119- // Mark that credentials have been written to "storage".
120- // This allows LoadCredentials to succeed on subsequent calls.
121- i .hasStoredCredentials = true
138+ // Write credentials to disk to simulate persistent storage.
139+ // Use a fixed timestamp for deterministic testing.
140+ fixedExpiration := time .Date (MockExpirationYear , MockExpirationMonth , MockExpirationDay , MockExpirationHour , MockExpirationMinute , MockExpirationSecond , 0 , time .UTC )
141+
142+ creds := & Credentials {
143+ AccessKeyID : "mock-access-key" ,
144+ SecretAccessKey : "mock-secret-key" ,
145+ SessionToken : "mock-session-token" ,
146+ Region : MockRegion ,
147+ Expiration : fixedExpiration ,
148+ }
149+
150+ // Serialize credentials to JSON.
151+ data , err := json .Marshal (creds )
152+ if err != nil {
153+ return fmt .Errorf ("failed to marshal mock credentials: %w" , err )
154+ }
155+
156+ // Write to temp file (simulates writing to XDG directory).
157+ credPath := i .getCredentialsFilePath ()
158+ if err := os .WriteFile (credPath , data , MockFilePermissions ); err != nil {
159+ return fmt .Errorf ("failed to write mock credentials to %s: %w" , credPath , err )
160+ }
122161
123162 return nil
124163}
@@ -145,35 +184,37 @@ func (i *Identity) CredentialsExist() (bool, error) {
145184func (i * Identity ) LoadCredentials (ctx context.Context ) (types.ICredentials , error ) {
146185 defer perf .Track (nil , "mock.Identity.LoadCredentials" )()
147186
148- // Check if credentials have been stored (via PostAuthenticate).
149- if ! i .hasStoredCredentials {
150- // Return a typed error to indicate credentials must be obtained via authentication.
151- return nil , fmt .Errorf ("%w: %q — use 'atmos auth login' to authenticate" , ErrNoStoredCredentials , i .name )
187+ // Check if credentials file exists.
188+ credPath := i .getCredentialsFilePath ()
189+ data , err := os .ReadFile (credPath )
190+ if err != nil {
191+ if os .IsNotExist (err ) {
192+ // Return typed error to indicate credentials must be obtained via authentication.
193+ return nil , fmt .Errorf ("%w: %q — use 'atmos auth login' to authenticate" , ErrNoStoredCredentials , i .name )
194+ }
195+ return nil , fmt .Errorf ("failed to read mock credentials from %s: %w" , credPath , err )
152196 }
153197
154- // Use a fixed timestamp far in the future for deterministic testing and snapshot stability.
155- // This ensures tests don't become flaky due to expiration checks.
156- fixedExpiration := time .Date (MockExpirationYear , MockExpirationMonth , MockExpirationDay , MockExpirationHour , MockExpirationMinute , MockExpirationSecond , 0 , time .UTC )
198+ // Deserialize credentials from JSON.
199+ var creds Credentials
200+ if err := json .Unmarshal (data , & creds ); err != nil {
201+ return nil , fmt .Errorf ("failed to unmarshal mock credentials: %w" , err )
202+ }
157203
158- // Return stored credentials.
159- // In a real provider, this would read from disk/environment/etc.
160- return & Credentials {
161- AccessKeyID : "mock-access-key" ,
162- SecretAccessKey : "mock-secret-key" ,
163- SessionToken : "mock-session-token" ,
164- Region : "us-east-1" ,
165- Expiration : fixedExpiration ,
166- }, nil
204+ return & creds , nil
167205}
168206
169207// Logout simulates removing credentials from persistent storage.
170- // This clears the stored credentials state , requiring re-authentication.
208+ // This deletes the credentials file , requiring re-authentication.
171209func (i * Identity ) Logout (ctx context.Context ) error {
172210 defer perf .Track (nil , "mock.Identity.Logout" )()
173211
174- // Clear the stored credentials flag.
175- // This simulates removing credentials from disk/environment/etc.
176- i .hasStoredCredentials = false
212+ // Delete the credentials file to simulate removal from disk/environment/etc.
213+ credPath := i .getCredentialsFilePath ()
214+ err := os .Remove (credPath )
215+ if err != nil && ! os .IsNotExist (err ) {
216+ return fmt .Errorf ("failed to delete mock credentials file %s: %w" , credPath , err )
217+ }
177218
178219 return nil
179220}
0 commit comments