efdaba606e
Like Claude Code's awsAuthRefresh: the user still approves each login. mode mac runs it on the Docker host over SSH (own browser); mode device shows the device code in pi for SSH/tmux use. Off by default (/aws-sso on). Session start inferred from refresh-token changes; 8 h default, measured.
128 lines
5.6 KiB
JavaScript
128 lines
5.6 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, assess, reanchor, fmtLeft, DEFAULTS,
|
|
} 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("assess: ok → due (lead window) → expired; unknown start stays ok", () => {
|
|
const st = { anchor: { refreshFp: "aaaa1111", since: T0 } };
|
|
assert.equal(assess(st, tok("aaaa1111", T0 + H), T0 + 1 * H).status, "ok");
|
|
assert.equal(assess(st, tok("aaaa1111", T0 + 8 * H), T0 + 7.5 * H).status, "ok");
|
|
assert.equal(assess(st, tok("aaaa1111", T0 + 8 * H), T0 + 8 * H - DEFAULTS.leadMinutes * 60e3).status, "due");
|
|
assert.equal(assess(st, tok("aaaa1111", T0 + 8.5 * H), T0 + 8.2 * H).status, "due"); // past end but access token still valid
|
|
assert.equal(assess(st, tok("aaaa1111", T0 + 8 * H), T0 + 8.2 * H).status, "expired");
|
|
assert.equal(assess(st, null, T0).status, "none");
|
|
assert.equal(assess({}, tok("aaaa1111", T0 + H), T0 + 30 * H).status, "ok"); // no anchor: never invent an end
|
|
assert.equal(assess(st, tok("bbbb2222", T0 + H), T0 + 30 * H).endsAt, null); // anchor belongs to another token
|
|
});
|
|
|
|
test("assess honours a configured session length", () => {
|
|
const st = { sessionHours: 72, anchor: { refreshFp: "aaaa1111", since: T0 } };
|
|
assert.equal(assess(st, tok("aaaa1111", T0 + 9 * H), T0 + 8.2 * H).status, "ok");
|
|
assert.equal(assess(st, tok("aaaa1111", T0 + 72 * H), T0 + 71.8 * H).status, "due");
|
|
});
|
|
|
|
test("reanchor: only when the refresh token changes (hourly access refresh keeps it)", () => {
|
|
const st = { anchor: { refreshFp: "aaaa1111", since: T0 } };
|
|
assert.equal(reanchor(st, "aaaa1111", T0 + 3 * H), null);
|
|
assert.deepEqual(reanchor(st, "bbbb2222", T0 + 3 * H), { refreshFp: "bbbb2222", since: T0 + 3 * H });
|
|
assert.deepEqual(reanchor({}, "aaaa1111", 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");
|
|
});
|