fdee3e42d6
Measured: a session lasted <= 6 h (2026-10-11), not the 8 h assumed, so a clock cannot predict the end. token-providers 3.1127.0 refreshes only inside a 5-min window and, on refusal, silently returns the still-valid token. The keeper now makes that attempt itself (pi's own SDK, same cache) when the window opens, and if expiresAt did not advance, logs in while the old token still works. Clock-based renewal, signed-in and hours commands removed.
152 lines
7.4 KiB
JavaScript
152 lines
7.4 KiB
JavaScript
// Two-sided tests for the pure parts of extensions/aws-sso.ts (node >= 23 strips
|
|
// the types; the only pi import is type-only). Shapes observed 2026-10-10/11 on
|
|
// mbp-m1-2020.
|
|
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import {
|
|
fingerprint, cacheKeyFor, parseIni, resolveSsoSession, parseDeviceOutput,
|
|
isSsoExpiredError, reanchor, fmtLeft, probeDue, probeWith, REFRESH_WINDOW_MS,
|
|
} from "../extensions/aws-sso.ts";
|
|
|
|
const START = "https://d-1234567890.awsapps.com/start";
|
|
const HOST = "d-1234567890.awsapps.com";
|
|
const CONFIG = `
|
|
[profile Base]
|
|
sso_session = MySession
|
|
sso_account_id = 111111111111
|
|
sso_role_name = Dev
|
|
[sso-session MySession]
|
|
sso_start_url = ${START}
|
|
sso_region = eu-west-1
|
|
[profile Bedrock]
|
|
role_arn = arn:aws:iam::111111111111:role/X
|
|
source_profile = Base
|
|
[profile LoopA]
|
|
source_profile = LoopB
|
|
[profile LoopB]
|
|
source_profile = LoopA
|
|
[profile Legacy]
|
|
sso_start_url = ${START}
|
|
[profile Dangling]
|
|
sso_session = Nope
|
|
`;
|
|
|
|
test("fingerprint: 8 hex chars, stable, never the value", () => {
|
|
const v = "aoaAAAAAGh0dHBzOi8vZXhhbXBsZS5jb20v";
|
|
assert.match(fingerprint(v), /^[0-9a-f]{8}$/);
|
|
assert.equal(fingerprint(v), fingerprint(v));
|
|
assert.notEqual(fingerprint(v), fingerprint(v + "x"));
|
|
assert.ok(!v.includes(fingerprint(v)));
|
|
assert.equal(fingerprint(""), "-");
|
|
});
|
|
|
|
test("cache key = sha1(session name) — the measured botocore file name", () => {
|
|
assert.equal(cacheKeyFor("GFTLIntelligenceOnTap"), "c8ef84539c753bc7f3a20b5dceef5c2f03e744a7");
|
|
});
|
|
|
|
test("resolveSsoSession follows source_profile to the sso-session", () => {
|
|
const r = resolveSsoSession(CONFIG, "Bedrock");
|
|
assert.equal(r.sessionName, "MySession");
|
|
assert.equal(r.startUrl, START);
|
|
assert.deepEqual(r.chain, ["Bedrock", "Base"]);
|
|
});
|
|
|
|
test("resolveSsoSession refuses broken configs with a reason", () => {
|
|
assert.throws(() => resolveSsoSession(CONFIG, "LoopA"), /loop/);
|
|
assert.throws(() => resolveSsoSession(CONFIG, "Missing"), /not found/);
|
|
assert.throws(() => resolveSsoSession(CONFIG, "Legacy"), /legacy/);
|
|
assert.throws(() => resolveSsoSession(CONFIG, "Dangling"), /sso-session Nope/);
|
|
assert.deepEqual(parseIni("# c\n[profile A]\nk = v = w\n"), { "profile A": { k: "v = w" } });
|
|
});
|
|
|
|
const CLI = (code, host = HOST) => `Browser will not be automatically opened.
|
|
Please visit the following URL:
|
|
|
|
https://${host}/start/#/device
|
|
|
|
Then enter the code:
|
|
|
|
${code}
|
|
|
|
Alternatively, you may visit the following URL which will autofill the code upon loading:
|
|
https://${host}/start/#/device?user_code=${code}
|
|
`;
|
|
|
|
test("parseDeviceOutput: real shape accepted; foreign host / bad code refused", () => {
|
|
const r = parseDeviceOutput(CLI("PCDB-WSGH"), START);
|
|
assert.equal(r.code, "PCDB-WSGH");
|
|
assert.equal(new URL(r.url).host, HOST);
|
|
assert.equal(parseDeviceOutput(CLI("PCDB-WSGH", "evil.example.com"), START), null);
|
|
assert.equal(parseDeviceOutput(CLI("PCDB-WSGH", `${HOST}.evil.example.com`), START), null);
|
|
assert.equal(parseDeviceOutput(CLI("pcdb-wsgh"), START), null);
|
|
assert.equal(parseDeviceOutput("Browser will not be automatically opened.\n", START), null);
|
|
});
|
|
|
|
test("isSsoExpiredError: the real pi error matches; unrelated failures do not", () => {
|
|
assert.ok(isSsoExpiredError("Token is expired. To refresh this SSO session run 'aws sso login' with the corresponding profile.")); // measured 2026-10-11T14:18:08Z
|
|
assert.ok(isSsoExpiredError("The SSO session associated with this profile has expired or is otherwise invalid"));
|
|
assert.ok(isSsoExpiredError("ExpiredTokenException: The security token included in the request is expired"));
|
|
for (const m of ["Provider stopped with: content_filtered", "getaddrinfo ENOTFOUND bedrock-runtime.eu-west-1.amazonaws.com", "AccessDeniedException: not authorized", "ThrottlingException: Too many requests"]) {
|
|
assert.ok(!isSsoExpiredError(m), m);
|
|
}
|
|
});
|
|
|
|
const H = 3600e3;
|
|
const T0 = Date.UTC(2026, 9, 11, 14, 19, 0);
|
|
const tok = (fp, accessExp) => ({ refreshFp: fp, accessExpiresAt: accessExp });
|
|
|
|
test("probeDue: only inside the SDK's 5-min window; 60 s apart while valid, 10 min apart once expired", () => {
|
|
const exp = T0 + H;
|
|
assert.equal(REFRESH_WINDOW_MS, 5 * 60e3); // token-providers 3.1127.0 EXPIRE_WINDOW_MS
|
|
assert.equal(probeDue(exp, exp - 6 * 60e3, 0), false); // outside: the SDK would not even try
|
|
assert.equal(probeDue(exp, exp - 5 * 60e3, 0), true);
|
|
assert.equal(probeDue(exp, exp - 4 * 60e3, exp - 4.5 * 60e3), false); // 30 s after the last attempt
|
|
assert.equal(probeDue(exp, exp - 3 * 60e3, exp - 4.5 * 60e3), true);
|
|
assert.equal(probeDue(exp, exp + 5 * 60e3, exp + 1 * 60e3), false); // expired: back off
|
|
assert.equal(probeDue(exp, exp + 12 * 60e3, exp + 1 * 60e3), true);
|
|
});
|
|
|
|
// probeWith: a fake SDK provider and a fake cache, mirroring token-providers' real behaviour.
|
|
const cacheSim = (exp) => ({ exp, read() { return this.exp; } });
|
|
test("probeWith: refreshed when the cache advances; refused when the SDK silently keeps the old token", async () => {
|
|
const now = () => T0 + H - 3 * 60e3;
|
|
const ok = cacheSim(T0 + H);
|
|
let r = await probeWith(() => () => async () => { ok.exp = T0 + 2 * H; return { token: "x" }; }, () => ok.read(), "Base", now);
|
|
assert.equal(r.result, "refreshed");
|
|
const ended = cacheSim(T0 + H); // session over: refresh fails inside the window, SDK returns the old token, no throw
|
|
r = await probeWith(() => () => async () => ({ token: "old" }), () => ended.read(), "Base", now);
|
|
assert.equal(r.result, "refused");
|
|
r = await probeWith(() => () => async () => { throw new Error("Token is expired. To refresh this SSO session run 'aws sso login'"); }, () => ended.read(), "Base", () => T0 + 2 * H);
|
|
assert.equal(r.result, "refused");
|
|
assert.match(r.error, /expired/);
|
|
});
|
|
|
|
test("probeWith: never calls the SDK outside the window; a missing SDK is 'unavailable', not 'session ended'", async () => {
|
|
let called = 0;
|
|
const c = cacheSim(T0 + H);
|
|
let r = await probeWith(() => () => async () => { called++; }, () => c.read(), "Base", () => T0);
|
|
assert.equal(r.result, "fresh");
|
|
assert.equal(called, 0);
|
|
r = await probeWith(() => { throw new Error("Cannot find module '@aws-sdk/token-providers'"); }, () => c.read(), "Base", () => T0 + H - 60e3);
|
|
assert.equal(r.result, "unavailable");
|
|
let seen;
|
|
await probeWith(() => (init) => { seen = init; return async () => {}; }, () => c.read(), "GFTLIoT-Base", () => T0 + H - 60e3);
|
|
assert.deepEqual(seen, { profile: "GFTLIoT-Base", ignoreCache: true }); // the SSO profile at the end of the chain
|
|
});
|
|
|
|
test("reanchor: a new token BEFORE the end inherits it (silent renewal); AFTER the end or an expiry starts fresh", () => {
|
|
const st = { anchor: { refreshFp: "aaaa1111", since: T0 } };
|
|
assert.equal(reanchor(st, "aaaa1111", T0 + 3 * H, T0 + 3 * H), null); // hourly access refresh: same refresh token
|
|
assert.deepEqual(reanchor(st, "bbbb2222", T0 + 1 * H, T0 + 1 * H), { refreshFp: "bbbb2222", since: T0 }); // measured 2026-10-11: green box, same session
|
|
assert.deepEqual(reanchor(st, "bbbb2222", T0 + 8.05 * H, T0 + 8.05 * H), { refreshFp: "bbbb2222", since: T0 + 8.05 * H });
|
|
assert.deepEqual(reanchor({ ...st, sessionEnded: true }, "bbbb2222", T0 + 2 * H, T0 + 2 * H), { refreshFp: "bbbb2222", since: T0 + 2 * H });
|
|
assert.deepEqual(reanchor({ ...st, sessionHours: 1 }, "bbbb2222", T0 + 2 * H, T0 + 2 * H), { refreshFp: "bbbb2222", since: T0 + 2 * H });
|
|
assert.deepEqual(reanchor({}, "aaaa1111", T0, T0), { refreshFp: "aaaa1111", since: T0 });
|
|
});
|
|
|
|
test("fmtLeft", () => {
|
|
assert.equal(fmtLeft(-5), "0m");
|
|
assert.equal(fmtLeft(25 * 60e3), "25m");
|
|
assert.equal(fmtLeft(7 * H + 5 * 60e3), "7h05m");
|
|
});
|