1
+ import { loadConfig } from "@smithy/node-config-provider" ;
1
2
import { CredentialsProviderError } from "@smithy/property-provider" ;
2
3
import { afterEach , beforeEach , describe , expect , test as it , vi } from "vitest" ;
3
-
4
4
import { InstanceMetadataV1FallbackError } from "./error/InstanceMetadataV1FallbackError" ;
5
- import { fromInstanceMetadata } from "./fromInstanceMetadata" ;
5
+ import { checkIfImdsDisabled , fromInstanceMetadata } from "./fromInstanceMetadata" ;
6
+ import * as fromInstanceMetadataModule from "./fromInstanceMetadata" ;
6
7
import { httpRequest } from "./remoteProvider/httpRequest" ;
7
8
import { fromImdsCredentials , isImdsCredentials } from "./remoteProvider/ImdsCredentials" ;
8
9
import { providerConfigFromInit } from "./remoteProvider/RemoteProviderInit" ;
9
10
import { retry } from "./remoteProvider/retry" ;
10
11
import { getInstanceMetadataEndpoint } from "./utils/getInstanceMetadataEndpoint" ;
11
12
import { staticStabilityProvider } from "./utils/staticStabilityProvider" ;
12
13
14
+
15
+
16
+ vi . mock ( "@smithy/node-config-provider" ) ;
13
17
vi . mock ( "./remoteProvider/httpRequest" ) ;
14
18
vi . mock ( "./remoteProvider/ImdsCredentials" ) ;
15
19
vi . mock ( "./remoteProvider/retry" ) ;
16
20
vi . mock ( "./remoteProvider/RemoteProviderInit" ) ;
17
21
vi . mock ( "./utils/getInstanceMetadataEndpoint" ) ;
18
22
vi . mock ( "./utils/staticStabilityProvider" ) ;
19
23
24
+
20
25
describe ( "fromInstanceMetadata" , ( ) => {
21
26
const hostname = "127.0.0.1" ;
22
27
const mockTimeout = 1000 ;
@@ -36,7 +41,7 @@ describe("fromInstanceMetadata", () => {
36
41
37
42
const mockProfileRequestOptions = {
38
43
hostname,
39
- path : "/latest/meta-data/iam/security-credentials/" ,
44
+ path : "/latest/meta-data/iam/security-credentials-extended /" ,
40
45
timeout : mockTimeout ,
41
46
headers : {
42
47
"x-aws-ec2-metadata-token" : mockToken ,
@@ -49,18 +54,22 @@ describe("fromInstanceMetadata", () => {
49
54
SecretAccessKey : "bar" ,
50
55
Token : "baz" ,
51
56
Expiration : ONE_HOUR_IN_FUTURE . toISOString ( ) ,
57
+ AccountId : "123456789012"
52
58
} ) ;
53
59
54
60
const mockCreds = Object . freeze ( {
55
61
accessKeyId : mockImdsCreds . AccessKeyId ,
56
62
secretAccessKey : mockImdsCreds . SecretAccessKey ,
57
63
sessionToken : mockImdsCreds . Token ,
58
64
expiration : new Date ( mockImdsCreds . Expiration ) ,
65
+ accountId : mockImdsCreds . AccountId
59
66
} ) ;
60
67
61
68
beforeEach ( ( ) => {
62
69
vi . mocked ( staticStabilityProvider ) . mockImplementation ( ( input ) => input ) ;
63
70
vi . mocked ( getInstanceMetadataEndpoint ) . mockResolvedValue ( { hostname } as any ) ;
71
+ vi . mocked ( loadConfig ) . mockReturnValue ( ( ) => Promise . resolve ( false ) ) ;
72
+ vi . spyOn ( fromInstanceMetadataModule , "checkIfImdsDisabled" ) . mockResolvedValue ( undefined ) ;
64
73
( isImdsCredentials as unknown as any ) . mockReturnValue ( true ) ;
65
74
vi . mocked ( providerConfigFromInit ) . mockReturnValue ( {
66
75
timeout : mockTimeout ,
@@ -72,6 +81,67 @@ describe("fromInstanceMetadata", () => {
72
81
vi . resetAllMocks ( ) ;
73
82
} ) ;
74
83
84
+ it ( "returns no credentials when AWS_EC2_METADATA_DISABLED=true" , async ( ) => {
85
+ vi . mocked ( loadConfig ) . mockReturnValueOnce ( ( ) => Promise . resolve ( true ) ) ;
86
+ vi . mocked ( fromInstanceMetadataModule . checkIfImdsDisabled ) . mockRejectedValueOnce (
87
+ new CredentialsProviderError ( "IMDS credential fetching is disabled" )
88
+ ) ;
89
+ const provider = fromInstanceMetadata ( { } ) ;
90
+
91
+ await expect ( provider ( ) ) . rejects . toEqual (
92
+ new CredentialsProviderError ( "IMDS credential fetching is disabled" )
93
+ ) ;
94
+ expect ( httpRequest ) . not . toHaveBeenCalled ( ) ;
95
+ } ) ;
96
+
97
+ it ( "returns valid credentials with account ID when ec2InstanceProfileName is provided" , async ( ) => {
98
+ const profileName = "my-profile-0002" ;
99
+
100
+ vi . mocked ( httpRequest )
101
+ . mockResolvedValueOnce ( mockToken as any )
102
+ . mockResolvedValueOnce ( JSON . stringify ( mockImdsCreds ) as any ) ;
103
+
104
+ vi . mocked ( retry ) . mockImplementation ( ( fn : any ) => fn ( ) ) ;
105
+ vi . mocked ( fromImdsCredentials ) . mockReturnValue ( mockCreds ) ;
106
+
107
+ const result = await fromInstanceMetadata ( { ec2InstanceProfileName : profileName } ) ( ) ;
108
+
109
+ expect ( result ) . toEqual ( mockCreds ) ;
110
+ expect ( result . accountId ) . toBe ( mockCreds . accountId ) ;
111
+
112
+ expect ( httpRequest ) . toHaveBeenCalledTimes ( 2 ) ;
113
+ expect ( httpRequest ) . toHaveBeenNthCalledWith ( 1 , mockTokenRequestOptions ) ;
114
+ expect ( httpRequest ) . toHaveBeenNthCalledWith ( 2 , {
115
+ ...mockProfileRequestOptions ,
116
+ path : `${ mockProfileRequestOptions . path } ${ profileName } ` ,
117
+ } ) ;
118
+ } ) ;
119
+
120
+ it ( "returns valid credentials with account ID when profile is discovered from IMDS" , async ( ) => {
121
+ vi . mocked ( httpRequest )
122
+ . mockResolvedValueOnce ( mockToken as any )
123
+ . mockResolvedValueOnce ( mockProfile as any )
124
+ . mockResolvedValueOnce ( JSON . stringify ( mockImdsCreds ) as any ) ;
125
+
126
+ vi . mocked ( retry ) . mockImplementation ( ( fn : any ) => fn ( ) ) ;
127
+ vi . mocked ( fromImdsCredentials ) . mockReturnValue ( mockCreds ) ;
128
+
129
+ const provider = fromInstanceMetadata ( { } ) ;
130
+
131
+ const result = await provider ( ) ;
132
+
133
+ expect ( result ) . toEqual ( mockCreds ) ;
134
+ expect ( result . accountId ) . toBe ( mockCreds . accountId ) ;
135
+
136
+ expect ( httpRequest ) . toHaveBeenCalledTimes ( 3 ) ;
137
+ expect ( httpRequest ) . toHaveBeenNthCalledWith ( 1 , mockTokenRequestOptions ) ;
138
+ expect ( httpRequest ) . toHaveBeenNthCalledWith ( 2 , mockProfileRequestOptions ) ;
139
+ expect ( httpRequest ) . toHaveBeenNthCalledWith ( 3 , {
140
+ ...mockProfileRequestOptions ,
141
+ path : `${ mockProfileRequestOptions . path } ${ mockProfile } ` ,
142
+ } ) ;
143
+ } ) ;
144
+
75
145
it ( "gets token and profile name to fetch credentials" , async ( ) => {
76
146
vi . mocked ( httpRequest )
77
147
. mockResolvedValueOnce ( mockToken as any )
@@ -99,6 +169,7 @@ describe("fromInstanceMetadata", () => {
99
169
100
170
vi . mocked ( retry ) . mockImplementation ( ( fn : any ) => fn ( ) ) ;
101
171
vi . mocked ( fromImdsCredentials ) . mockReturnValue ( mockCreds ) ;
172
+ vi . mocked ( checkIfImdsDisabled ) . mockReturnValueOnce ( Promise . resolve ( ) ) ;
102
173
103
174
await expect ( fromInstanceMetadata ( ) ( ) ) . resolves . toEqual ( mockCreds ) ;
104
175
expect ( httpRequest ) . toHaveBeenNthCalledWith ( 3 , {
@@ -109,6 +180,7 @@ describe("fromInstanceMetadata", () => {
109
180
110
181
it ( "passes {} to providerConfigFromInit if init not defined" , async ( ) => {
111
182
vi . mocked ( retry ) . mockResolvedValueOnce ( mockProfile ) . mockResolvedValueOnce ( mockCreds ) ;
183
+ vi . mocked ( loadConfig ) . mockReturnValueOnce ( ( ) => Promise . resolve ( false ) ) ;
112
184
113
185
await expect ( fromInstanceMetadata ( ) ( ) ) . resolves . toEqual ( mockCreds ) ;
114
186
expect ( providerConfigFromInit ) . toHaveBeenCalledTimes ( 1 ) ;
@@ -117,6 +189,7 @@ describe("fromInstanceMetadata", () => {
117
189
118
190
it ( "passes init to providerConfigFromInit" , async ( ) => {
119
191
vi . mocked ( retry ) . mockResolvedValueOnce ( mockProfile ) . mockResolvedValueOnce ( mockCreds ) ;
192
+ vi . mocked ( loadConfig ) . mockReturnValueOnce ( ( ) => Promise . resolve ( false ) ) ;
120
193
121
194
const init = { maxRetries : 5 , timeout : 1213 } ;
122
195
await expect ( fromInstanceMetadata ( init ) ( ) ) . resolves . toEqual ( mockCreds ) ;
@@ -213,6 +286,79 @@ describe("fromInstanceMetadata", () => {
213
286
expect ( vi . mocked ( staticStabilityProvider ) ) . toBeCalledTimes ( 1 ) ;
214
287
} ) ;
215
288
289
+ describe ( "getImdsProfileHelper" , ( ) => {
290
+ beforeEach ( ( ) => {
291
+ vi . mocked ( httpRequest ) . mockClear ( ) ;
292
+ vi . mocked ( loadConfig ) . mockClear ( ) ;
293
+ vi . mocked ( retry ) . mockImplementation ( ( fn : any ) => fn ( ) ) ;
294
+ } ) ;
295
+
296
+ it ( "uses ec2InstanceProfileName from init if provided" , async ( ) => {
297
+ const profileName = "profile-from-init" ;
298
+ const options = { hostname } as any ;
299
+
300
+ // Only use vi.spyOn for imported functions
301
+ vi . spyOn ( fromInstanceMetadataModule , "getConfiguredProfileName" ) . mockResolvedValueOnce ( profileName ) ;
302
+
303
+ const result = await fromInstanceMetadataModule . getImdsProfileHelper (
304
+ options , mockMaxRetries , { ec2InstanceProfileName : profileName }
305
+ ) ;
306
+
307
+ expect ( result ) . toBe ( profileName ) ;
308
+ expect ( httpRequest ) . not . toHaveBeenCalled ( ) ;
309
+ } ) ;
310
+
311
+ it ( "uses environment variable if ec2InstanceProfileName not provided" , async ( ) => {
312
+ const envProfileName = "profile-from-env" ;
313
+ const options = { hostname } as any ;
314
+
315
+ // Mock loadConfig to simulate env variable present
316
+ vi . mocked ( loadConfig ) . mockReturnValue ( ( ) => Promise . resolve ( envProfileName ) ) ;
317
+
318
+ const result = await fromInstanceMetadataModule . getImdsProfileHelper (
319
+ options , mockMaxRetries , { }
320
+ ) ;
321
+
322
+ expect ( result ) . toBe ( envProfileName ) ;
323
+ expect ( httpRequest ) . not . toHaveBeenCalled ( ) ;
324
+ } ) ;
325
+
326
+ it ( "uses profile from config file if present, otherwise falls back to IMDS (extended then legacy)" , async ( ) => {
327
+ const configProfileName = "profile-from-config" ;
328
+ const legacyProfileName = "profile-from-legacy" ;
329
+ const options = { hostname } as any ;
330
+
331
+ // 1. Simulate config file present: should return configProfileName, no IMDS call
332
+ vi . mocked ( loadConfig ) . mockReturnValue ( ( ) => Promise . resolve ( configProfileName ) ) ;
333
+
334
+ let result = await fromInstanceMetadataModule . getImdsProfileHelper (
335
+ options , mockMaxRetries , { }
336
+ ) ;
337
+ expect ( result ) . toBe ( configProfileName ) ;
338
+ expect ( httpRequest ) . not . toHaveBeenCalled ( ) ;
339
+
340
+ // 2. Simulate config file missing: should call IMDS (extended fails, legacy succeeds)
341
+ vi . mocked ( loadConfig ) . mockReturnValue ( ( ) => Promise . resolve ( null ) ) ;
342
+ vi . mocked ( httpRequest )
343
+ . mockRejectedValueOnce ( Object . assign ( new Error ( ) , { statusCode : 404 } ) )
344
+ . mockResolvedValueOnce ( legacyProfileName as any ) ;
345
+
346
+ result = await fromInstanceMetadataModule . getImdsProfileHelper (
347
+ options , mockMaxRetries , { }
348
+ ) ;
349
+ expect ( result ) . toBe ( legacyProfileName ) ;
350
+ expect ( httpRequest ) . toHaveBeenCalledTimes ( 2 ) ;
351
+ expect ( httpRequest ) . toHaveBeenNthCalledWith ( 1 , {
352
+ ...options ,
353
+ path : "/latest/meta-data/iam/security-credentials-extended/"
354
+ } ) ;
355
+ expect ( httpRequest ) . toHaveBeenNthCalledWith ( 2 , {
356
+ ...options ,
357
+ path : "/latest/meta-data/iam/security-credentials/"
358
+ } ) ;
359
+ } ) ;
360
+ } ) ;
361
+
216
362
describe ( "disables fetching of token" , ( ) => {
217
363
beforeEach ( ( ) => {
218
364
vi . mocked ( retry ) . mockImplementation ( ( fn : any ) => fn ( ) ) ;
@@ -303,4 +449,4 @@ describe("fromInstanceMetadata", () => {
303
449
} ) ;
304
450
await expect ( ( ) => fromInstanceMetadataFunc ( ) ) . rejects . toBeInstanceOf ( InstanceMetadataV1FallbackError ) ;
305
451
} ) ;
306
- } ) ;
452
+ } ) ;
0 commit comments