Heimdall/tests/app.test.ts
2026-04-26 15:23:04 +01:00

348 lines
10 KiB
TypeScript

import { afterEach, describe, expect, it } from "vitest";
import { type FastifyInstance } from "fastify";
import { buildApp } from "../src/app.js";
import { type HeimdallConfig } from "../src/config.js";
import { entitlementFacts, grantFacts, identityFacts } from "../src/facts.js";
import { type OAuthProviderRuntime } from "../src/oauth.js";
import { verifyJwt } from "../src/signing.js";
function createTestConfig(): HeimdallConfig {
return {
serviceName: "heimdall",
host: "127.0.0.1",
port: 4100,
publicBaseUrl: "https://heimdall.gamecult.org",
issuer: "https://heimdall.gamecult.org",
sessionTtlSeconds: 3600,
stateTtlSeconds: 600,
completionTtlSeconds: 300,
storage: {
backend: "memory",
applySchemaOnStartup: true,
},
apps: {
repixelizer: {
discordGuildId: "gamecult-guild",
discordAllowedRoleIds: ["role-repixelizer"],
},
},
providers: {
discord: { clientId: "discord-client", clientSecret: "discord-secret" },
patreon: { clientId: "patreon-client", clientSecret: "patreon-secret" },
github: { clientId: "github-client", clientSecret: "github-secret" },
twitch: { clientId: "twitch-client", clientSecret: "twitch-secret" },
youtube: { clientId: "youtube-client", clientSecret: "youtube-secret" },
},
};
}
function createMockDiscordRuntime(): OAuthProviderRuntime {
return {
async exchangeAuthorizationCode() {
return {
accessToken: "discord-access-token",
refreshToken: "discord-refresh-token",
tokenType: "Bearer",
scope: ["identify", "email", "guilds.members.read"],
expiresAt: "2026-04-26T13:00:00.000Z",
raw: { source: "test" },
};
},
async resolveIdentity() {
return {
provider: "discord",
providerUserId: "discord-user-123",
username: "meta",
displayName: "Meta",
primaryEmail: "meta@gamecult.org",
profile: { id: "discord-user-123", username: "meta" },
};
},
async evaluateEntitlements({ callback }) {
return {
facts: [entitlementFacts.appAccess],
snapshots: [
{
accountId: callback.accountId,
provider: "discord",
scope: "repixelizer:discord_role_access:gamecult-guild",
evaluatedAt: "2026-04-26T12:00:00.000Z",
isAllowed: true,
reasonCode: "matched_role",
reasonDetail: "Matched Repixelizer access role.",
rawSummaryJson: {
guildId: "gamecult-guild",
matchedRoles: ["role-repixelizer"],
},
},
],
};
},
};
}
const apps: Awaited<ReturnType<typeof buildApp>>[] = [];
function getPublicKey(app: FastifyInstance) {
return (
app as FastifyInstance & {
heimdallContext: {
keys: {
publicKey: Parameters<typeof verifyJwt>[1];
};
};
}
).heimdallContext.keys.publicKey;
}
async function startDiscordSignIn(app: FastifyInstance): Promise<string> {
const response = await app.inject({
method: "POST",
url: "/v1/oauth/discord/start",
payload: {
appSlug: "repixelizer",
mode: "sign_in",
returnTo: "https://repixelizer.gamecult.org/app/",
},
});
expect(response.statusCode).toBe(201);
return response.json().stateToken as string;
}
afterEach(async () => {
while (apps.length) {
await apps.pop()?.close();
}
});
describe("Heimdall service", () => {
it("serves discovery and JWKS metadata", async () => {
const app = await buildApp({ config: createTestConfig() });
apps.push(app);
const discoveryResponse = await app.inject({
method: "GET",
url: "/.well-known/heimdall-configuration",
});
expect(discoveryResponse.statusCode).toBe(200);
expect(discoveryResponse.json()).toEqual(
expect.objectContaining({
issuer: "https://heimdall.gamecult.org",
jwksUri: "https://heimdall.gamecult.org/.well-known/jwks.json",
})
);
const jwksResponse = await app.inject({
method: "GET",
url: "/.well-known/jwks.json",
});
expect(jwksResponse.statusCode).toBe(200);
expect(jwksResponse.json()).toEqual(
expect.objectContaining({
keys: [
expect.objectContaining({
alg: "EdDSA",
kid: expect.any(String),
use: "sig",
}),
],
})
);
});
it("builds signed OAuth start state for supported providers", async () => {
const app = await buildApp({ config: createTestConfig() });
apps.push(app);
const response = await app.inject({
method: "POST",
url: "/v1/oauth/discord/start",
payload: {
appSlug: "repixelizer",
mode: "sign_in",
returnTo: "https://repixelizer.gamecult.org/app/",
},
});
expect(response.statusCode).toBe(201);
const payload = response.json();
expect(payload.authorizationUrl).toContain("discord.com/oauth2/authorize");
const verified = verifyJwt(payload.stateToken as string, getPublicKey(app));
expect(verified.valid).toBe(true);
if (verified.valid) {
expect(verified.payload).toEqual(
expect.objectContaining({
typ: "heimdall_oauth_state",
provider: "discord",
app_slug: "repixelizer",
})
);
}
});
it("completes a provider callback and issues a verified Repixelizer claim", async () => {
const app = await buildApp({
config: createTestConfig(),
oauthRuntimes: {
discord: createMockDiscordRuntime(),
},
});
apps.push(app);
const stateToken = await startDiscordSignIn(app);
const response = await app.inject({
method: "GET",
url: `/v1/oauth/discord/callback?code=test-code&state=${encodeURIComponent(stateToken)}`,
});
expect(response.statusCode).toBe(201);
const payload = response.json();
expect(payload.completion).toEqual(
expect.objectContaining({
code: expect.any(String),
redeemEndpoint: "https://heimdall.gamecult.org/v1/apps/repixelizer/auth-completions/redeem",
})
);
expect(payload.sharedCapabilities).toEqual(expect.arrayContaining(["app_access", "queue_submit"]));
expect(payload.account).toEqual(
expect.objectContaining({
displayName: "Meta",
primaryEmail: "meta@gamecult.org",
})
);
expect(payload.entitlements.facts).toEqual(expect.arrayContaining([entitlementFacts.appAccess]));
const verified = verifyJwt(payload.accessToken as string, getPublicKey(app));
expect(verified.valid).toBe(true);
if (verified.valid) {
expect(verified.payload).toEqual(
expect.objectContaining({
typ: "heimdall_access",
aud: "repixelizer",
account_id: payload.account.id,
})
);
expect(verified.payload.facts).toEqual(
expect.arrayContaining([identityFacts.authenticated, entitlementFacts.appAccess])
);
expect(verified.payload.capabilities).toEqual(expect.arrayContaining(["app_access", "queue_submit"]));
}
});
it("redeems a completion code exactly once", async () => {
const app = await buildApp({
config: createTestConfig(),
oauthRuntimes: {
discord: createMockDiscordRuntime(),
},
});
apps.push(app);
const stateToken = await startDiscordSignIn(app);
const callbackResponse = await app.inject({
method: "GET",
url: `/v1/oauth/discord/callback?code=test-code&state=${encodeURIComponent(stateToken)}`,
});
const completionCode = callbackResponse.json().completion.code as string;
const redeemResponse = await app.inject({
method: "POST",
url: "/v1/apps/repixelizer/auth-completions/redeem",
payload: {
completionCode,
},
});
expect(redeemResponse.statusCode).toBe(201);
expect(redeemResponse.json()).toEqual(
expect.objectContaining({
status: "success",
provider: "discord",
appSlug: "repixelizer",
})
);
const secondRedeemResponse = await app.inject({
method: "POST",
url: "/v1/apps/repixelizer/auth-completions/redeem",
payload: {
completionCode,
},
});
expect(secondRedeemResponse.statusCode).toBe(410);
});
it("renders a browser handoff page that posts a completion code instead of a token", async () => {
const app = await buildApp({
config: createTestConfig(),
oauthRuntimes: {
discord: createMockDiscordRuntime(),
},
});
apps.push(app);
const stateToken = await startDiscordSignIn(app);
const response = await app.inject({
method: "GET",
url: `/v1/oauth/discord/callback?code=test-code&state=${encodeURIComponent(stateToken)}`,
headers: {
accept: "text/html",
},
});
expect(response.statusCode).toBe(200);
expect(response.headers["content-type"]).toContain("text/html");
expect(response.body).toContain("window.opener.postMessage");
expect(response.body).toContain("heimdall_completion_code");
expect(response.body).not.toContain("heimdall_access_token");
});
it("issues direct claims from provider-agnostic entitlement facts", async () => {
const app = await buildApp({ config: createTestConfig() });
apps.push(app);
const response = await app.inject({
method: "POST",
url: "/v1/apps/repixelizer/claims/issue",
payload: {
accountId: "acct_repixelizer_001",
displayName: "Meta",
facts: [entitlementFacts.appAccess, grantFacts.operator],
linkedIdentities: [
{
provider: "discord",
providerUserId: "123456789",
username: "meta",
},
],
},
});
expect(response.statusCode).toBe(201);
const payload = response.json();
expect(payload.sharedCapabilities).toEqual(
expect.arrayContaining(["app_access", "queue_submit", "admin_access"])
);
});
it("exposes StreamPixels hybrid capability seams without granting them blindly", async () => {
const app = await buildApp({ config: createTestConfig() });
apps.push(app);
const profileResponse = await app.inject({
method: "GET",
url: "/v1/apps/streampixels",
});
expect(profileResponse.statusCode).toBe(200);
expect(profileResponse.json()).toEqual(
expect.objectContaining({
slug: "streampixels",
managedConnectionProviders: ["twitch", "youtube"],
})
);
});
});