Upload files to "tests/providers/azure"
This commit is contained in:
@@ -0,0 +1,276 @@
|
||||
import { beforeEach, describe, expect, it, mock } from "bun:test";
|
||||
|
||||
// ── Mock the auth module ────────────────────────────────────────────────────
|
||||
const mockResolveAllIdentities = mock();
|
||||
const mockResolveToken = mock();
|
||||
|
||||
mock.module("../../../src/providers/azure/auth", () => ({
|
||||
resolveAllIdentities: mockResolveAllIdentities,
|
||||
resolveToken: mockResolveToken,
|
||||
}));
|
||||
|
||||
// ── Mock the client module ──────────────────────────────────────────────────
|
||||
const mockAzureFetch = mock();
|
||||
|
||||
mock.module("../../../src/providers/azure/client", () => ({
|
||||
azureFetch: mockAzureFetch,
|
||||
}));
|
||||
|
||||
// ── Mock global fetch for MS Graph calls ────────────────────────────────────
|
||||
const mockFetch = mock();
|
||||
globalThis.fetch = mockFetch as unknown as typeof fetch;
|
||||
|
||||
import { AzureIdentityService } from "../../../src/providers/azure/identity";
|
||||
|
||||
// ── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function jwtEncode(payload: Record<string, unknown>): string {
|
||||
const header = Buffer.from(JSON.stringify({ alg: "none" })).toString("base64url");
|
||||
const body = Buffer.from(JSON.stringify(payload)).toString("base64url");
|
||||
return `${header}.${body}.signature`;
|
||||
}
|
||||
|
||||
function makeTokenResponse(overrides: Record<string, unknown> = {}): string {
|
||||
return JSON.stringify({
|
||||
access_token: jwtEncode({
|
||||
oid: overrides.oid ?? "obj-123",
|
||||
tid: overrides.tid ?? "tenant-456",
|
||||
appid: overrides.appid ?? "app-789",
|
||||
sub: overrides.sub ?? "sub-user",
|
||||
...overrides,
|
||||
}),
|
||||
expires_on: String(Date.now() / 1000 + 3600),
|
||||
tenant_id: overrides.tid ?? "tenant-456",
|
||||
});
|
||||
}
|
||||
|
||||
function makeMsGraphUserResponse(displayName: string) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
id: "user-1",
|
||||
displayName,
|
||||
userPrincipalName: "user@contoso.com",
|
||||
}),
|
||||
{ status: 200 },
|
||||
);
|
||||
}
|
||||
|
||||
function makeMsGraphSpResponse(displayName: string) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
value: [
|
||||
{
|
||||
id: "sp-1",
|
||||
appId: "app-789",
|
||||
displayName,
|
||||
servicePrincipalType: "Application",
|
||||
},
|
||||
],
|
||||
}),
|
||||
{ status: 200 },
|
||||
);
|
||||
}
|
||||
|
||||
// ── Tests ───────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("AzureIdentityService", () => {
|
||||
let service: AzureIdentityService;
|
||||
|
||||
beforeEach(() => {
|
||||
mockResolveAllIdentities.mockReset();
|
||||
mockResolveToken.mockReset();
|
||||
mockAzureFetch.mockReset();
|
||||
mockFetch.mockReset();
|
||||
service = new AzureIdentityService();
|
||||
});
|
||||
|
||||
// ── No identities ─────────────────────────────────────────────────────
|
||||
|
||||
it("returns failure when no identities found", async () => {
|
||||
mockResolveAllIdentities.mockResolvedValue([]);
|
||||
|
||||
const result = await service.execute();
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error?.message).toContain("No Azure identities found");
|
||||
});
|
||||
|
||||
// ── Single identity from env service principal ─────────────────────────
|
||||
|
||||
it("resolves identity from env service principal", async () => {
|
||||
mockResolveAllIdentities.mockResolvedValue([
|
||||
{
|
||||
source: "env-service-principal",
|
||||
token: {
|
||||
token: jwtEncode({
|
||||
oid: "obj-001",
|
||||
tid: "tenant-abc",
|
||||
appid: "app-xyz",
|
||||
}),
|
||||
expiresOn: Date.now() / 1000 + 3600,
|
||||
tenantId: "tenant-abc",
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// MS Graph user lookup fails → falls back to service principal lookup
|
||||
mockFetch.mockRejectedValueOnce(new Error("Graph not scoped"));
|
||||
mockFetch.mockResolvedValueOnce(makeMsGraphSpResponse("MyApp"));
|
||||
|
||||
// ARM subscription list
|
||||
mockAzureFetch.mockResolvedValue({
|
||||
value: [
|
||||
{
|
||||
subscriptionId: "sub-1",
|
||||
displayName: "Pay-As-You-Go",
|
||||
state: "Enabled",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const result = await service.execute();
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
const identities = result.data as any[];
|
||||
expect(identities).toHaveLength(1);
|
||||
expect(identities[0].source).toBe("env-service-principal");
|
||||
expect(identities[0].tenantId).toBe("tenant-abc");
|
||||
expect(identities[0].objectId).toBe("obj-001");
|
||||
expect(identities[0].clientId).toBe("app-xyz");
|
||||
expect(identities[0].displayName).toBe("MyApp");
|
||||
expect(identities[0].subscriptionIds).toEqual(["sub-1"]);
|
||||
});
|
||||
|
||||
// ── Multiple identities ────────────────────────────────────────────────
|
||||
|
||||
it("resolves multiple identities from different sources", async () => {
|
||||
mockResolveAllIdentities.mockResolvedValue([
|
||||
{
|
||||
source: "env-service-principal",
|
||||
token: {
|
||||
token: jwtEncode({ oid: "obj-1", tid: "t1", appid: "a1" }),
|
||||
expiresOn: Date.now() / 1000 + 3600,
|
||||
},
|
||||
},
|
||||
{
|
||||
source: "managed-identity",
|
||||
token: {
|
||||
token: jwtEncode({ oid: "obj-2", tid: "t2", appid: "a2" }),
|
||||
expiresOn: Date.now() / 1000 + 3600,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
mockFetch.mockRejectedValue(new Error("no graph")); // both fail
|
||||
mockAzureFetch.mockResolvedValue({ value: [] });
|
||||
|
||||
const result = await service.execute();
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
const identities = result.data as any[];
|
||||
expect(identities).toHaveLength(2);
|
||||
expect(identities[0].source).toBe("env-service-principal");
|
||||
expect(identities[1].source).toBe("managed-identity");
|
||||
});
|
||||
|
||||
// ── MS Graph user info ────────────────────────────────────────────────
|
||||
|
||||
it("resolves display name from MS Graph user endpoint", async () => {
|
||||
mockResolveAllIdentities.mockResolvedValue([
|
||||
{
|
||||
source: "env-service-principal",
|
||||
token: {
|
||||
token: jwtEncode({ oid: "obj-1", tid: "t1" }),
|
||||
expiresOn: Date.now() / 1000 + 3600,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
mockFetch.mockResolvedValueOnce(makeMsGraphUserResponse("Jane Doe"));
|
||||
mockAzureFetch.mockResolvedValue({ value: [] });
|
||||
|
||||
const result = await service.execute();
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
const identities = result.data as any[];
|
||||
expect(identities[0].displayName).toBe("Jane Doe");
|
||||
});
|
||||
|
||||
// ── JWT claim fallbacks ────────────────────────────────────────────────
|
||||
|
||||
it("falls back to sub claim when oid missing", async () => {
|
||||
mockResolveAllIdentities.mockResolvedValue([
|
||||
{
|
||||
source: "env-service-principal",
|
||||
token: {
|
||||
token: jwtEncode({ tid: "t1", sub: "fallback-sub" }),
|
||||
expiresOn: Date.now() / 1000 + 3600,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
mockFetch.mockRejectedValue(new Error("no graph"));
|
||||
mockAzureFetch.mockResolvedValue({ value: [] });
|
||||
|
||||
const result = await service.execute();
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
const identities = result.data as any[];
|
||||
expect(identities[0].objectId).toBe("fallback-sub");
|
||||
});
|
||||
|
||||
// ── ARM subscription filtering ─────────────────────────────────────────
|
||||
|
||||
it("filters out disabled subscriptions", async () => {
|
||||
mockResolveAllIdentities.mockResolvedValue([
|
||||
{
|
||||
source: "env-service-principal",
|
||||
token: {
|
||||
token: jwtEncode({ oid: "obj-1", tid: "t1" }),
|
||||
expiresOn: Date.now() / 1000 + 3600,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
mockFetch.mockRejectedValue(new Error("no graph"));
|
||||
mockAzureFetch.mockResolvedValue({
|
||||
value: [
|
||||
{ subscriptionId: "enabled-1", displayName: "A", state: "Enabled" },
|
||||
{ subscriptionId: "disabled-1", displayName: "B", state: "Disabled" },
|
||||
{ subscriptionId: "pastdue-1", displayName: "C", state: "PastDue" },
|
||||
],
|
||||
});
|
||||
|
||||
const result = await service.execute();
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
const identities = result.data as any[];
|
||||
expect(identities[0].subscriptionIds).toEqual(["enabled-1"]);
|
||||
expect(identities[0].subscriptionIds).not.toContain("disabled-1");
|
||||
expect(identities[0].subscriptionIds).not.toContain("pastdue-1");
|
||||
});
|
||||
|
||||
// ── Falls back to token claims when tenantId not in token object ───────
|
||||
|
||||
it("uses token.tenantId when tid claim is absent", async () => {
|
||||
mockResolveAllIdentities.mockResolvedValue([
|
||||
{
|
||||
source: "managed-identity",
|
||||
token: {
|
||||
token: jwtEncode({ oid: "obj-1" }),
|
||||
expiresOn: Date.now() / 1000 + 3600,
|
||||
tenantId: "from-token-obj",
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
mockFetch.mockRejectedValue(new Error("no graph"));
|
||||
mockAzureFetch.mockResolvedValue({ value: [] });
|
||||
|
||||
const result = await service.execute();
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
const identities = result.data as any[];
|
||||
expect(identities[0].tenantId).toBe("from-token-obj");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user