Skip to content

Commit b9ce699

Browse files
committed
update the testcases accordingly
1 parent c865c2a commit b9ce699

File tree

2 files changed

+163
-11
lines changed

2 files changed

+163
-11
lines changed

packages/credential-provider-imds/src/fromInstanceMetadata.spec.ts

Lines changed: 150 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,27 @@
1+
import { loadConfig } from "@smithy/node-config-provider";
12
import { CredentialsProviderError } from "@smithy/property-provider";
23
import { afterEach, beforeEach, describe, expect, test as it, vi } from "vitest";
3-
44
import { InstanceMetadataV1FallbackError } from "./error/InstanceMetadataV1FallbackError";
5-
import { fromInstanceMetadata } from "./fromInstanceMetadata";
5+
import { checkIfImdsDisabled, fromInstanceMetadata } from "./fromInstanceMetadata";
6+
import * as fromInstanceMetadataModule from "./fromInstanceMetadata";
67
import { httpRequest } from "./remoteProvider/httpRequest";
78
import { fromImdsCredentials, isImdsCredentials } from "./remoteProvider/ImdsCredentials";
89
import { providerConfigFromInit } from "./remoteProvider/RemoteProviderInit";
910
import { retry } from "./remoteProvider/retry";
1011
import { getInstanceMetadataEndpoint } from "./utils/getInstanceMetadataEndpoint";
1112
import { staticStabilityProvider } from "./utils/staticStabilityProvider";
1213

14+
15+
16+
vi.mock("@smithy/node-config-provider");
1317
vi.mock("./remoteProvider/httpRequest");
1418
vi.mock("./remoteProvider/ImdsCredentials");
1519
vi.mock("./remoteProvider/retry");
1620
vi.mock("./remoteProvider/RemoteProviderInit");
1721
vi.mock("./utils/getInstanceMetadataEndpoint");
1822
vi.mock("./utils/staticStabilityProvider");
1923

24+
2025
describe("fromInstanceMetadata", () => {
2126
const hostname = "127.0.0.1";
2227
const mockTimeout = 1000;
@@ -36,7 +41,7 @@ describe("fromInstanceMetadata", () => {
3641

3742
const mockProfileRequestOptions = {
3843
hostname,
39-
path: "/latest/meta-data/iam/security-credentials/",
44+
path: "/latest/meta-data/iam/security-credentials-extended/",
4045
timeout: mockTimeout,
4146
headers: {
4247
"x-aws-ec2-metadata-token": mockToken,
@@ -49,18 +54,22 @@ describe("fromInstanceMetadata", () => {
4954
SecretAccessKey: "bar",
5055
Token: "baz",
5156
Expiration: ONE_HOUR_IN_FUTURE.toISOString(),
57+
AccountId: "123456789012"
5258
});
5359

5460
const mockCreds = Object.freeze({
5561
accessKeyId: mockImdsCreds.AccessKeyId,
5662
secretAccessKey: mockImdsCreds.SecretAccessKey,
5763
sessionToken: mockImdsCreds.Token,
5864
expiration: new Date(mockImdsCreds.Expiration),
65+
accountId: mockImdsCreds.AccountId
5966
});
6067

6168
beforeEach(() => {
6269
vi.mocked(staticStabilityProvider).mockImplementation((input) => input);
6370
vi.mocked(getInstanceMetadataEndpoint).mockResolvedValue({ hostname } as any);
71+
vi.mocked(loadConfig).mockReturnValue(() => Promise.resolve(false));
72+
vi.spyOn(fromInstanceMetadataModule, "checkIfImdsDisabled").mockResolvedValue(undefined);
6473
(isImdsCredentials as unknown as any).mockReturnValue(true);
6574
vi.mocked(providerConfigFromInit).mockReturnValue({
6675
timeout: mockTimeout,
@@ -72,6 +81,67 @@ describe("fromInstanceMetadata", () => {
7281
vi.resetAllMocks();
7382
});
7483

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+
75145
it("gets token and profile name to fetch credentials", async () => {
76146
vi.mocked(httpRequest)
77147
.mockResolvedValueOnce(mockToken as any)
@@ -99,6 +169,7 @@ describe("fromInstanceMetadata", () => {
99169

100170
vi.mocked(retry).mockImplementation((fn: any) => fn());
101171
vi.mocked(fromImdsCredentials).mockReturnValue(mockCreds);
172+
vi.mocked(checkIfImdsDisabled).mockReturnValueOnce(Promise.resolve());
102173

103174
await expect(fromInstanceMetadata()()).resolves.toEqual(mockCreds);
104175
expect(httpRequest).toHaveBeenNthCalledWith(3, {
@@ -109,6 +180,7 @@ describe("fromInstanceMetadata", () => {
109180

110181
it("passes {} to providerConfigFromInit if init not defined", async () => {
111182
vi.mocked(retry).mockResolvedValueOnce(mockProfile).mockResolvedValueOnce(mockCreds);
183+
vi.mocked(loadConfig).mockReturnValueOnce(() => Promise.resolve(false));
112184

113185
await expect(fromInstanceMetadata()()).resolves.toEqual(mockCreds);
114186
expect(providerConfigFromInit).toHaveBeenCalledTimes(1);
@@ -117,6 +189,7 @@ describe("fromInstanceMetadata", () => {
117189

118190
it("passes init to providerConfigFromInit", async () => {
119191
vi.mocked(retry).mockResolvedValueOnce(mockProfile).mockResolvedValueOnce(mockCreds);
192+
vi.mocked(loadConfig).mockReturnValueOnce(() => Promise.resolve(false));
120193

121194
const init = { maxRetries: 5, timeout: 1213 };
122195
await expect(fromInstanceMetadata(init)()).resolves.toEqual(mockCreds);
@@ -213,6 +286,79 @@ describe("fromInstanceMetadata", () => {
213286
expect(vi.mocked(staticStabilityProvider)).toBeCalledTimes(1);
214287
});
215288

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+
216362
describe("disables fetching of token", () => {
217363
beforeEach(() => {
218364
vi.mocked(retry).mockImplementation((fn: any) => fn());
@@ -303,4 +449,4 @@ describe("fromInstanceMetadata", () => {
303449
});
304450
await expect(() => fromInstanceMetadataFunc()).rejects.toBeInstanceOf(InstanceMetadataV1FallbackError);
305451
});
306-
});
452+
});

packages/credential-provider-imds/src/fromInstanceMetadata.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -147,15 +147,21 @@ const getInstanceMetadataProvider = (init: RemoteProviderInit = {}) => {
147147
* @internal
148148
* Gets IMDS profile with proper error handling and retries
149149
*/
150-
const getImdsProfileHelper = async (
150+
export const getImdsProfileHelper = async (
151151
options: RequestOptions,
152152
maxRetries: number,
153153
init: RemoteProviderInit = {},
154-
profile?: string
154+
profile?: string,
155+
resetCache?: boolean
155156
): Promise<string> => {
156157
let apiVersion: "unknown" | "extended" | "legacy" = "unknown";
157158
let resolvedProfile: string | null = null;
158-
159+
160+
// If resetCache is true, clear the cached profile name
161+
if (resetCache) {
162+
resolvedProfile = null;
163+
}
164+
159165
return retry<string>(async () => {
160166
// First check if a profile name is configured
161167
const configuredName = await getConfiguredProfileName(init, profile);
@@ -206,7 +212,7 @@ const getMetadataToken = async (options: RequestOptions) =>
206212
* @internal
207213
* Checks if IMDS credential fetching is disabled through configuration
208214
*/
209-
const checkIfImdsDisabled = async (profile?: string, logger?: any): Promise<void> => {
215+
export const checkIfImdsDisabled = async (profile?: string, logger?: any): Promise<void> => {
210216
// Load configuration in priority order
211217
const disableImds = await loadConfig(
212218
{
@@ -235,7 +241,7 @@ const checkIfImdsDisabled = async (profile?: string, logger?: any): Promise<void
235241
* @internal
236242
* Gets configured profile name from various sources
237243
*/
238-
const getConfiguredProfileName = async (init: RemoteProviderInit, profile?: string): Promise<string | null> => {
244+
export const getConfiguredProfileName = async (init: RemoteProviderInit, profile?: string): Promise<string | null> => {
239245
// Load configuration in priority order
240246
const profileName = await loadConfig(
241247
{
@@ -278,8 +284,8 @@ const getCredentialsFromProfile = async (profile: string, options: RequestOption
278284
// If legacy API also returns 404 and we're using a cached profile name,
279285
// the profile might have changed - clear cache and retry
280286
const resolvedProfile = null;
281-
const newProfileName = await getImdsProfileHelper(options, init.maxRetries ?? 3, init, profile);
282-
return getCredentialsFromProfile(newProfileName, options, init);
287+
const newProfileName = await getImdsProfileHelper(options, init.maxRetries ?? 3, init, profile, true);
288+
return getCredentialsFromProfile(newProfileName, options, init);
283289
}
284290
throw legacyError;
285291
}

0 commit comments

Comments
 (0)