1+ import { loadConfig } from "@smithy/node-config-provider" ;
12import { CredentialsProviderError } from "@smithy/property-provider" ;
23import { afterEach , beforeEach , describe , expect , test as it , vi } from "vitest" ;
3-
44import { InstanceMetadataV1FallbackError } from "./error/InstanceMetadataV1FallbackError" ;
5- import { fromInstanceMetadata } from "./fromInstanceMetadata" ;
5+ import { checkIfImdsDisabled , fromInstanceMetadata } from "./fromInstanceMetadata" ;
6+ import * as fromInstanceMetadataModule from "./fromInstanceMetadata" ;
67import { httpRequest } from "./remoteProvider/httpRequest" ;
78import { fromImdsCredentials , isImdsCredentials } from "./remoteProvider/ImdsCredentials" ;
89import { providerConfigFromInit } from "./remoteProvider/RemoteProviderInit" ;
910import { retry } from "./remoteProvider/retry" ;
1011import { getInstanceMetadataEndpoint } from "./utils/getInstanceMetadataEndpoint" ;
1112import { staticStabilityProvider } from "./utils/staticStabilityProvider" ;
1213
14+ vi . mock ( "@smithy/node-config-provider" ) ;
1315vi . mock ( "./remoteProvider/httpRequest" ) ;
1416vi . mock ( "./remoteProvider/ImdsCredentials" ) ;
1517vi . mock ( "./remoteProvider/retry" ) ;
@@ -36,7 +38,7 @@ describe("fromInstanceMetadata", () => {
3638
3739 const mockProfileRequestOptions = {
3840 hostname,
39- path : "/latest/meta-data/iam/security-credentials/" ,
41+ path : "/latest/meta-data/iam/security-credentials-extended /" ,
4042 timeout : mockTimeout ,
4143 headers : {
4244 "x-aws-ec2-metadata-token" : mockToken ,
@@ -49,18 +51,22 @@ describe("fromInstanceMetadata", () => {
4951 SecretAccessKey : "bar" ,
5052 Token : "baz" ,
5153 Expiration : ONE_HOUR_IN_FUTURE . toISOString ( ) ,
54+ AccountId : "123456789012" ,
5255 } ) ;
5356
5457 const mockCreds = Object . freeze ( {
5558 accessKeyId : mockImdsCreds . AccessKeyId ,
5659 secretAccessKey : mockImdsCreds . SecretAccessKey ,
5760 sessionToken : mockImdsCreds . Token ,
5861 expiration : new Date ( mockImdsCreds . Expiration ) ,
62+ accountId : mockImdsCreds . AccountId ,
5963 } ) ;
6064
6165 beforeEach ( ( ) => {
6266 vi . mocked ( staticStabilityProvider ) . mockImplementation ( ( input ) => input ) ;
6367 vi . mocked ( getInstanceMetadataEndpoint ) . mockResolvedValue ( { hostname } as any ) ;
68+ vi . mocked ( loadConfig ) . mockReturnValue ( ( ) => Promise . resolve ( false ) ) ;
69+ vi . spyOn ( fromInstanceMetadataModule , "checkIfImdsDisabled" ) . mockResolvedValue ( undefined ) ;
6470 ( isImdsCredentials as unknown as any ) . mockReturnValue ( true ) ;
6571 vi . mocked ( providerConfigFromInit ) . mockReturnValue ( {
6672 timeout : mockTimeout ,
@@ -72,6 +78,65 @@ describe("fromInstanceMetadata", () => {
7278 vi . resetAllMocks ( ) ;
7379 } ) ;
7480
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+
75140 it ( "gets token and profile name to fetch credentials" , async ( ) => {
76141 vi . mocked ( httpRequest )
77142 . mockResolvedValueOnce ( mockToken as any )
@@ -99,6 +164,7 @@ describe("fromInstanceMetadata", () => {
99164
100165 vi . mocked ( retry ) . mockImplementation ( ( fn : any ) => fn ( ) ) ;
101166 vi . mocked ( fromImdsCredentials ) . mockReturnValue ( mockCreds ) ;
167+ vi . mocked ( checkIfImdsDisabled ) . mockReturnValueOnce ( Promise . resolve ( ) ) ;
102168
103169 await expect ( fromInstanceMetadata ( ) ( ) ) . resolves . toEqual ( mockCreds ) ;
104170 expect ( httpRequest ) . toHaveBeenNthCalledWith ( 3 , {
@@ -109,6 +175,7 @@ describe("fromInstanceMetadata", () => {
109175
110176 it ( "passes {} to providerConfigFromInit if init not defined" , async ( ) => {
111177 vi . mocked ( retry ) . mockResolvedValueOnce ( mockProfile ) . mockResolvedValueOnce ( mockCreds ) ;
178+ vi . mocked ( loadConfig ) . mockReturnValueOnce ( ( ) => Promise . resolve ( false ) ) ;
112179
113180 await expect ( fromInstanceMetadata ( ) ( ) ) . resolves . toEqual ( mockCreds ) ;
114181 expect ( providerConfigFromInit ) . toHaveBeenCalledTimes ( 1 ) ;
@@ -117,6 +184,7 @@ describe("fromInstanceMetadata", () => {
117184
118185 it ( "passes init to providerConfigFromInit" , async ( ) => {
119186 vi . mocked ( retry ) . mockResolvedValueOnce ( mockProfile ) . mockResolvedValueOnce ( mockCreds ) ;
187+ vi . mocked ( loadConfig ) . mockReturnValueOnce ( ( ) => Promise . resolve ( false ) ) ;
120188
121189 const init = { maxRetries : 5 , timeout : 1213 } ;
122190 await expect ( fromInstanceMetadata ( init ) ( ) ) . resolves . toEqual ( mockCreds ) ;
@@ -213,6 +281,73 @@ describe("fromInstanceMetadata", () => {
213281 expect ( vi . mocked ( staticStabilityProvider ) ) . toBeCalledTimes ( 1 ) ;
214282 } ) ;
215283
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+
216351 describe ( "disables fetching of token" , ( ) => {
217352 beforeEach ( ( ) => {
218353 vi . mocked ( retry ) . mockImplementation ( ( fn : any ) => fn ( ) ) ;
0 commit comments