|
1 | 1 | import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; |
2 | 2 | import { |
3 | 3 | discoverMantleModels, |
| 4 | + generateBearerTokenFromIam, |
4 | 5 | mergeImplicitMantleProvider, |
| 6 | + resetIamTokenCacheForTest, |
5 | 7 | resetMantleDiscoveryCacheForTest, |
6 | 8 | resolveMantleBearerToken, |
7 | 9 | resolveImplicitMantleProvider, |
8 | 10 | } from "./api.js"; |
9 | 11 |
|
| 12 | +const mocks = vi.hoisted(() => ({ |
| 13 | + getTokenProvider: vi.fn(), |
| 14 | +})); |
| 15 | + |
| 16 | +vi.mock("@aws/bedrock-token-generator", () => ({ |
| 17 | + getTokenProvider: mocks.getTokenProvider, |
| 18 | +})); |
| 19 | + |
10 | 20 | describe("bedrock mantle discovery", () => { |
11 | 21 | const originalEnv = process.env; |
12 | 22 |
|
13 | 23 | beforeEach(() => { |
14 | 24 | process.env = { ...originalEnv }; |
15 | 25 | vi.restoreAllMocks(); |
| 26 | + mocks.getTokenProvider.mockReset(); |
16 | 27 | resetMantleDiscoveryCacheForTest(); |
| 28 | + resetIamTokenCacheForTest(); |
17 | 29 | }); |
18 | 30 |
|
19 | 31 | afterEach(() => { |
@@ -44,6 +56,68 @@ describe("bedrock mantle discovery", () => { |
44 | 56 | ).toBe("my-token"); |
45 | 57 | }); |
46 | 58 |
|
| 59 | + // --------------------------------------------------------------------------- |
| 60 | + // IAM token generation |
| 61 | + // --------------------------------------------------------------------------- |
| 62 | + |
| 63 | + it("generates token from IAM credentials when token generation succeeds", async () => { |
| 64 | + const tokenProvider = vi.fn(async () => "bedrock-api-key-generated"); // pragma: allowlist secret |
| 65 | + mocks.getTokenProvider.mockReturnValue(tokenProvider); |
| 66 | + |
| 67 | + const token = await generateBearerTokenFromIam({ region: "us-east-1" }); |
| 68 | + |
| 69 | + expect(token).toBe("bedrock-api-key-generated"); |
| 70 | + expect(mocks.getTokenProvider).toHaveBeenCalledWith({ |
| 71 | + region: "us-east-1", |
| 72 | + expiresInSeconds: 7200, |
| 73 | + }); |
| 74 | + expect(tokenProvider).toHaveBeenCalledTimes(1); |
| 75 | + }); |
| 76 | + |
| 77 | + it("caches generated IAM tokens within TTL", async () => { |
| 78 | + const tokenProvider = vi.fn(async () => "bedrock-api-key-cached"); // pragma: allowlist secret |
| 79 | + mocks.getTokenProvider.mockReturnValue(tokenProvider); |
| 80 | + let now = 1000; |
| 81 | + |
| 82 | + const t1 = await generateBearerTokenFromIam({ region: "us-east-1", now: () => now }); |
| 83 | + now += 1800_000; // 30 min — within 1hr cache TTL |
| 84 | + const t2 = await generateBearerTokenFromIam({ region: "us-east-1", now: () => now }); |
| 85 | + |
| 86 | + expect(t1).toEqual(t2); |
| 87 | + expect(tokenProvider).toHaveBeenCalledTimes(1); |
| 88 | + }); |
| 89 | + |
| 90 | + it("does not reuse an IAM token across regions", async () => { |
| 91 | + const tokenProvider = vi |
| 92 | + .fn<() => Promise<string>>() |
| 93 | + .mockResolvedValueOnce("bedrock-api-key-east") // pragma: allowlist secret |
| 94 | + .mockResolvedValueOnce("bedrock-api-key-west"); // pragma: allowlist secret |
| 95 | + mocks.getTokenProvider.mockReturnValue(tokenProvider); |
| 96 | + |
| 97 | + const east = await generateBearerTokenFromIam({ region: "us-east-1", now: () => 1000 }); |
| 98 | + const west = await generateBearerTokenFromIam({ region: "us-west-2", now: () => 2000 }); |
| 99 | + |
| 100 | + expect(east).toBe("bedrock-api-key-east"); |
| 101 | + expect(west).toBe("bedrock-api-key-west"); |
| 102 | + expect(mocks.getTokenProvider).toHaveBeenNthCalledWith(1, { |
| 103 | + region: "us-east-1", |
| 104 | + expiresInSeconds: 7200, |
| 105 | + }); |
| 106 | + expect(mocks.getTokenProvider).toHaveBeenNthCalledWith(2, { |
| 107 | + region: "us-west-2", |
| 108 | + expiresInSeconds: 7200, |
| 109 | + }); |
| 110 | + expect(tokenProvider).toHaveBeenCalledTimes(2); |
| 111 | + }); |
| 112 | + |
| 113 | + it("returns undefined when IAM token generation fails", async () => { |
| 114 | + mocks.getTokenProvider.mockImplementation(() => { |
| 115 | + throw new Error("no credentials"); |
| 116 | + }); |
| 117 | + |
| 118 | + await expect(generateBearerTokenFromIam({ region: "us-east-1" })).resolves.toBeUndefined(); |
| 119 | + }); |
| 120 | + |
47 | 121 | // --------------------------------------------------------------------------- |
48 | 122 | // Model discovery |
49 | 123 | // --------------------------------------------------------------------------- |
@@ -278,23 +352,47 @@ describe("bedrock mantle discovery", () => { |
278 | 352 | expect(provider?.models).toHaveLength(1); |
279 | 353 | }); |
280 | 354 |
|
281 | | - it("returns null when no bearer token is available", async () => { |
| 355 | + it("returns null when no auth is available", async () => { |
| 356 | + mocks.getTokenProvider.mockImplementation(() => { |
| 357 | + throw new Error("no credentials"); |
| 358 | + }); |
| 359 | + |
282 | 360 | const provider = await resolveImplicitMantleProvider({ |
283 | 361 | env: {} as NodeJS.ProcessEnv, |
284 | 362 | }); |
285 | 363 |
|
286 | 364 | expect(provider).toBeNull(); |
287 | 365 | }); |
288 | 366 |
|
289 | | - it("does not infer Mantle auth from plain IAM env vars alone", async () => { |
| 367 | + it("uses a generated IAM token when no explicit token is set", async () => { |
| 368 | + const tokenProvider = vi.fn(async () => "bedrock-api-key-iam"); // pragma: allowlist secret |
| 369 | + const mockFetch = vi.fn().mockResolvedValue({ |
| 370 | + ok: true, |
| 371 | + json: async () => ({ |
| 372 | + data: [{ id: "openai.gpt-oss-120b", object: "model" }], |
| 373 | + }), |
| 374 | + }); |
| 375 | + mocks.getTokenProvider.mockReturnValue(tokenProvider); |
| 376 | + |
290 | 377 | const provider = await resolveImplicitMantleProvider({ |
291 | 378 | env: { |
292 | 379 | AWS_PROFILE: "default", |
293 | 380 | AWS_REGION: "us-east-1", |
294 | 381 | } as NodeJS.ProcessEnv, |
| 382 | + fetchFn: mockFetch as unknown as typeof fetch, |
295 | 383 | }); |
296 | 384 |
|
297 | | - expect(provider).toBeNull(); |
| 385 | + expect(provider).not.toBeNull(); |
| 386 | + expect(provider?.apiKey).toBe("bedrock-api-key-iam"); |
| 387 | + expect(tokenProvider).toHaveBeenCalledTimes(1); |
| 388 | + expect(mockFetch).toHaveBeenCalledWith( |
| 389 | + "https://bedrock-mantle.us-east-1.api.aws/v1/models", |
| 390 | + expect.objectContaining({ |
| 391 | + headers: expect.objectContaining({ |
| 392 | + Authorization: "Bearer bedrock-api-key-iam", |
| 393 | + }), |
| 394 | + }), |
| 395 | + ); |
298 | 396 | }); |
299 | 397 |
|
300 | 398 | it("returns null for unsupported regions", async () => { |
|
0 commit comments