Pin the year boundary, the typed exchange failure and the start refusal's code

Closes StrykerJS survivors on the diff: the cap at exactly 365 days against a
second over, the rotating-exchange failure's typed code (and that a
non-rotating provider is unaffected), and the start refusal's error word.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-19 14:45:35 +02:00
commit 6db84bfae8
2 changed files with 33 additions and 5 deletions

View file

@ -1367,14 +1367,15 @@ describe("Heimdall service", () => {
}
it.each([
["no connection", {}],
["an account connection", { connection: { kind: "account", targetId: "victim-1" } }],
["an empty targetId", { connection: { kind: "creator", targetId: "" } }],
])("refuses a joystick connect with %s at start, before any redirect", async (_name, extra) => {
["no connection", {}, "creator_connection_required"],
["an account connection", { connection: { kind: "account", targetId: "victim-1" } }, "creator_connection_required"],
["an empty targetId", { connection: { kind: "creator", targetId: "" } }, "invalid_request"],
])("refuses a joystick connect with %s at start, before any redirect", async (_name, extra, error) => {
const app = await buildApp({ config: createTestConfig() });
apps.push(app);
const start = await connect(app, extra);
expect(start.statusCode).toBe(400);
expect(start.json().error).toBe(error);
expect(start.json().authorizationUrl).toBeUndefined();
expect(start.json().stateToken).toBeUndefined();
});

View file

@ -2,7 +2,7 @@ import { inspect } from "node:util";
import { afterEach, describe, expect, it } from "vitest";
import { type HeimdallConfig } from "../src/config.js";
import { providers, type Provider } from "../src/contracts.js";
import { requestProviderTokens } from "../src/oauth.js";
import { createOAuthRuntimeRegistry, requestProviderTokens } from "../src/oauth.js";
import { providerCatalog } from "../src/providers.js";
const originalFetch = globalThis.fetch;
@ -138,6 +138,33 @@ describe("requestProviderTokens follows the provider descriptor", () => {
expect(Date.parse(tokens.expiresAt ?? "") - before).toBeLessThan(3600 * 1000 + 5_000);
});
it.each([
["exactly a year", 31_536_000, 31_536_000],
["a second over a year", 31_536_001, 3600],
["3e11", 3e11, 3600],
["3e11 as a string", "300000000000", 3600],
])("takes an expires_in of %s as %i seconds' worth of lifetime", async (_n, expiresIn, seconds) => {
capture(() => new Response(JSON.stringify({ access_token: "a", expires_in: expiresIn }), { status: 200 }));
const before = Date.now();
const tokens = await requestProviderTokens("twitch", configFor(), { grant_type: "refresh_token", refresh_token: "R" });
expect(Date.parse(tokens.expiresAt ?? "") - before).toBeGreaterThanOrEqual(seconds * 1000 - 1000);
expect(Date.parse(tokens.expiresAt ?? "") - before).toBeLessThan(seconds * 1000 + 5_000);
});
it.each([
["no refresh_token", {}],
["an empty refresh_token", { refresh_token: "" }],
])("a rotating provider's code exchange with %s is a typed malformed token set; a non-rotating one is not", async (_n, extra) => {
capture(() => new Response(JSON.stringify({ access_token: "a", expires_in: 3600, ...extra }), { status: 200 }));
const runtimes = createOAuthRuntimeRegistry();
const exchange = { config: configFor(), code: "c", redirectUri: "https://h/cb" };
await expect(runtimes.joystick.exchangeAuthorizationCode(exchange)).rejects.toMatchObject({
name: "ProviderResponseError",
code: "provider_body_not_token_set",
});
expect((await runtimes.twitch.exchangeAuthorizationCode(exchange)).refreshToken).toBeUndefined();
});
it.each([
["a number", 42],
["an object", { a: 1 }],