Skip to content

Commit b9b5842

Browse files
authored
Merge pull request #6 from tstromberg/main
Implement race-proof mocking
2 parents bf862b6 + c167d7c commit b9b5842

26 files changed

+4290
-4296
lines changed

.golangci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,8 @@ linters:
214214
- r
215215
- w
216216
- f
217+
- h
218+
- q
217219
- err
218220

219221
exclusions:

auth/auth.go

Lines changed: 40 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -22,38 +22,52 @@ const (
2222
maxBodySize = 10 * 1024 * 1024 // 10MB
2323
defaultTimeout = 30 * time.Second
2424
metadataFlavor = "Google"
25+
//nolint:revive // GCP metadata server only supports HTTP
26+
defaultMetadataURL = "http://metadata.google.internal/computeMetadata/v1"
2527
)
2628

27-
var (
28-
metadataURL = "http://metadata.google.internal/computeMetadata/v1" //nolint:revive // GCP metadata server only supports HTTP
29-
isTestMode = false
29+
var httpClient = &http.Client{
30+
Timeout: defaultTimeout,
31+
}
3032

31-
httpClient = &http.Client{
32-
Timeout: defaultTimeout,
33-
}
34-
)
33+
// Config holds auth configuration.
34+
type Config struct {
35+
// MetadataURL is the URL for the GCP metadata server.
36+
// Defaults to the production metadata server if empty.
37+
MetadataURL string
38+
39+
// SkipADC skips Application Default Credentials and goes straight to metadata server.
40+
// Useful for testing to ensure mock servers are used.
41+
SkipADC bool
42+
}
3543

36-
// SetMetadataURL sets a custom metadata server URL for testing.
37-
// Returns a function that restores the original URL.
38-
// WARNING: This function should only be called in test code.
39-
// Set DS9_ALLOW_TEST_OVERRIDES=true to enable in non-test environments.
40-
func SetMetadataURL(urlStr string) func() {
41-
old := metadataURL
42-
oldTestMode := isTestMode
43-
metadataURL = urlStr
44-
isTestMode = true // Enable test mode to skip ADC
45-
return func() {
46-
metadataURL = old
47-
isTestMode = oldTestMode
44+
// configKey is the key for storing Config in context.
45+
type configKey struct{}
46+
47+
// WithConfig returns a new context with the given auth config.
48+
func WithConfig(ctx context.Context, cfg *Config) context.Context {
49+
return context.WithValue(ctx, configKey{}, cfg)
50+
}
51+
52+
// getConfig retrieves the auth config from context, or returns defaults.
53+
func getConfig(ctx context.Context) *Config {
54+
if cfg, ok := ctx.Value(configKey{}).(*Config); ok && cfg != nil {
55+
return cfg
56+
}
57+
return &Config{
58+
MetadataURL: defaultMetadataURL,
59+
SkipADC: false,
4860
}
4961
}
5062

5163
// AccessToken retrieves a GCP access token.
5264
// It tries Application Default Credentials first, then falls back to the metadata server.
53-
// In test mode, ADC is skipped to ensure mock servers are used.
65+
// Configuration can be provided via auth.WithConfig in the context.
5466
func AccessToken(ctx context.Context) (string, error) {
55-
// Skip ADC in test mode to ensure tests use mock metadata server
56-
if !isTestMode {
67+
cfg := getConfig(ctx)
68+
69+
// Skip ADC if configured (useful for testing to ensure mock metadata server is used)
70+
if !cfg.SkipADC {
5771
// Try Application Default Credentials first (for local development)
5872
token, err := accessTokenFromADC(ctx)
5973
if err == nil {
@@ -165,7 +179,8 @@ func exchangeRefreshToken(ctx context.Context, clientID, clientSecret, refreshTo
165179
// accessTokenFromMetadata retrieves an access token from the GCP metadata server.
166180
// This is used when running on GCP (GCE, GKE, Cloud Run, etc.).
167181
func accessTokenFromMetadata(ctx context.Context) (string, error) {
168-
reqURL := metadataURL + "/instance/service-accounts/default/token"
182+
cfg := getConfig(ctx)
183+
reqURL := cfg.MetadataURL + "/instance/service-accounts/default/token"
169184

170185
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, http.NoBody)
171186
if err != nil {
@@ -206,7 +221,8 @@ func accessTokenFromMetadata(ctx context.Context) (string, error) {
206221

207222
// ProjectID retrieves the project ID from the GCP metadata server.
208223
func ProjectID(ctx context.Context) (string, error) {
209-
reqURL := metadataURL + "/project/project-id"
224+
cfg := getConfig(ctx)
225+
reqURL := cfg.MetadataURL + "/project/project-id"
210226

211227
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, http.NoBody)
212228
if err != nil {

auth/auth_test.go

Lines changed: 68 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -12,30 +12,27 @@ import (
1212
"testing"
1313
)
1414

15-
func TestSetMetadataURL(t *testing.T) {
16-
originalURL := metadataURL
17-
originalTestMode := isTestMode
18-
19-
// Set custom URL
20-
restore := SetMetadataURL("http://custom-metadata")
21-
22-
if metadataURL != "http://custom-metadata" {
23-
t.Errorf("expected metadataURL to be http://custom-metadata, got %s", metadataURL)
15+
func TestWithConfig(t *testing.T) {
16+
// Test that config can be set in context
17+
cfg := &Config{
18+
MetadataURL: "http://custom-metadata",
19+
SkipADC: true,
2420
}
21+
ctx := WithConfig(context.Background(), cfg)
2522

26-
if !isTestMode {
27-
t.Error("expected isTestMode to be true")
23+
// Context should be non-nil
24+
if ctx == nil {
25+
t.Fatal("expected non-nil context")
2826
}
2927

30-
// Restore
31-
restore()
32-
33-
if metadataURL != originalURL {
34-
t.Errorf("expected metadataURL to be restored to %s, got %s", originalURL, metadataURL)
28+
// Verify config is retrievable
29+
retrievedCfg := getConfig(ctx)
30+
if retrievedCfg.MetadataURL != "http://custom-metadata" {
31+
t.Errorf("expected MetadataURL to be http://custom-metadata, got %s", retrievedCfg.MetadataURL)
3532
}
3633

37-
if isTestMode != originalTestMode {
38-
t.Errorf("expected isTestMode to be restored to %v, got %v", originalTestMode, isTestMode)
34+
if !retrievedCfg.SkipADC {
35+
t.Error("expected SkipADC to be true")
3936
}
4037
}
4138

@@ -111,10 +108,11 @@ func TestAccessTokenFromMetadata(t *testing.T) {
111108
}))
112109
defer server.Close()
113110

114-
restore := SetMetadataURL(server.URL)
115-
defer restore()
111+
ctx := WithConfig(context.Background(), &Config{
112+
MetadataURL: server.URL,
113+
SkipADC: true,
114+
})
116115

117-
ctx := context.Background()
118116
token, err := accessTokenFromMetadata(ctx)
119117

120118
if tt.wantErr {
@@ -153,12 +151,12 @@ func TestAccessToken(t *testing.T) {
153151
}))
154152
defer server.Close()
155153

156-
restore := SetMetadataURL(server.URL)
157-
defer restore()
154+
ctx := WithConfig(context.Background(), &Config{
155+
MetadataURL: server.URL,
156+
SkipADC: true,
157+
})
158158

159-
ctx := context.Background()
160-
161-
// In test mode, should use metadata server
159+
// With SkipADC=true, should use metadata server
162160
token, err := AccessToken(ctx)
163161
if err != nil {
164162
t.Fatalf("AccessToken failed: %v", err)
@@ -353,10 +351,10 @@ func TestProjectID(t *testing.T) {
353351
}))
354352
defer server.Close()
355353

356-
restore := SetMetadataURL(server.URL)
357-
defer restore()
358-
359-
ctx := context.Background()
354+
ctx := WithConfig(context.Background(), &Config{
355+
MetadataURL: server.URL,
356+
SkipADC: true,
357+
})
360358
projectID, err := ProjectID(ctx)
361359

362360
if tt.wantErr {
@@ -379,10 +377,10 @@ func TestProjectID(t *testing.T) {
379377

380378
func TestAccessTokenMetadataServerDown(t *testing.T) {
381379
// Point to non-existent server
382-
restore := SetMetadataURL("http://localhost:59999")
383-
defer restore()
384-
385-
ctx := context.Background()
380+
ctx := WithConfig(context.Background(), &Config{
381+
MetadataURL: "http://localhost:59999",
382+
SkipADC: true,
383+
})
386384
_, err := accessTokenFromMetadata(ctx)
387385

388386
if err == nil {
@@ -396,10 +394,10 @@ func TestAccessTokenMetadataServerDown(t *testing.T) {
396394

397395
func TestProjectIDMetadataServerDown(t *testing.T) {
398396
// Point to non-existent server
399-
restore := SetMetadataURL("http://localhost:59998")
400-
defer restore()
401-
402-
ctx := context.Background()
397+
ctx := WithConfig(context.Background(), &Config{
398+
MetadataURL: "http://localhost:59998",
399+
SkipADC: true,
400+
})
403401
_, err := ProjectID(ctx)
404402

405403
if err == nil {
@@ -465,10 +463,10 @@ func TestAccessTokenFromMetadataReadError(t *testing.T) {
465463
}))
466464
defer server.Close()
467465

468-
restore := SetMetadataURL(server.URL)
469-
defer restore()
470-
471-
ctx := context.Background()
466+
ctx := WithConfig(context.Background(), &Config{
467+
MetadataURL: server.URL,
468+
SkipADC: true,
469+
})
472470
_, err := accessTokenFromMetadata(ctx)
473471

474472
if err == nil {
@@ -488,10 +486,10 @@ func TestProjectIDReadError(t *testing.T) {
488486
}))
489487
defer server.Close()
490488

491-
restore := SetMetadataURL(server.URL)
492-
defer restore()
493-
494-
ctx := context.Background()
489+
ctx := WithConfig(context.Background(), &Config{
490+
MetadataURL: server.URL,
491+
SkipADC: true,
492+
})
495493
_, err := ProjectID(ctx)
496494

497495
if err == nil {
@@ -521,10 +519,10 @@ func TestAccessTokenFromMetadataWithMalformedJSON(t *testing.T) {
521519
}))
522520
defer server.Close()
523521

524-
restore := SetMetadataURL(server.URL)
525-
defer restore()
526-
527-
ctx := context.Background()
522+
ctx := WithConfig(context.Background(), &Config{
523+
MetadataURL: server.URL,
524+
SkipADC: true,
525+
})
528526
_, err := accessTokenFromMetadata(ctx)
529527
// Should either succeed (if parser is lenient) or fail with parse error
530528
if err != nil {
@@ -547,10 +545,10 @@ func TestProjectIDWithEmptyResponse(t *testing.T) {
547545
}))
548546
defer server.Close()
549547

550-
restore := SetMetadataURL(server.URL)
551-
defer restore()
552-
553-
ctx := context.Background()
548+
ctx := WithConfig(context.Background(), &Config{
549+
MetadataURL: server.URL,
550+
SkipADC: true,
551+
})
554552
projectID, err := ProjectID(ctx)
555553
if err != nil {
556554
t.Fatalf("ProjectID with empty response failed: %v", err)
@@ -650,15 +648,16 @@ func TestAccessTokenFallbackToMetadata(t *testing.T) {
650648
}))
651649
defer server.Close()
652650

653-
restore := SetMetadataURL(server.URL)
654-
defer restore()
651+
ctx := WithConfig(context.Background(), &Config{
652+
MetadataURL: server.URL,
653+
SkipADC: true,
654+
})
655655

656656
// Ensure no ADC credentials are available
657657
if err := os.Unsetenv("GOOGLE_APPLICATION_CREDENTIALS"); err != nil {
658658
t.Fatalf("failed to unset env var: %v", err)
659659
}
660660

661-
ctx := context.Background()
662661
token, err := AccessToken(ctx)
663662
if err != nil {
664663
t.Fatalf("AccessToken failed: %v", err)
@@ -688,10 +687,10 @@ func TestProjectIDInvalidJSON(t *testing.T) {
688687
}))
689688
defer server.Close()
690689

691-
restore := SetMetadataURL(server.URL)
692-
defer restore()
693-
694-
ctx := context.Background()
690+
ctx := WithConfig(context.Background(), &Config{
691+
MetadataURL: server.URL,
692+
SkipADC: true,
693+
})
695694
projectID, err := ProjectID(ctx)
696695

697696
if err != nil {
@@ -750,10 +749,10 @@ func TestAccessTokenFromADCDefaultLocation(t *testing.T) {
750749
// Test ProjectID with request error
751750
func TestProjectIDRequestError(t *testing.T) {
752751
// Set invalid URL to trigger request error
753-
restore := SetMetadataURL("http://invalid-host-that-does-not-exist-12345")
754-
defer restore()
755-
756-
ctx := context.Background()
752+
ctx := WithConfig(context.Background(), &Config{
753+
MetadataURL: "http://invalid-host-that-does-not-exist-12345",
754+
SkipADC: true,
755+
})
757756
_, err := ProjectID(ctx)
758757

759758
if err == nil {
@@ -777,10 +776,10 @@ func TestAccessTokenFromMetadataTypeError(t *testing.T) {
777776
}))
778777
defer server.Close()
779778

780-
restore := SetMetadataURL(server.URL)
781-
defer restore()
782-
783-
ctx := context.Background()
779+
ctx := WithConfig(context.Background(), &Config{
780+
MetadataURL: server.URL,
781+
SkipADC: true,
782+
})
784783
_, err := accessTokenFromMetadata(ctx)
785784

786785
if err == nil {

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
module github.com/codeGROOVE-dev/ds9
22

3-
go 1.23
3+
go 1.25

0 commit comments

Comments
 (0)