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