mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
- Administration.md: the createAccount example reads PDS_ADMIN_PASSWORD from the PDS container before calling curl and unsets it afterwards. The operator shell had no value, so curl sent empty Basic Auth credentials. - _appview.js: restorePreferences clears its deadline timer once the race settles, so a Playwright worker no longer stays alive until the 45 s timer fires. Addresses the CodeRabbit review on #917. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
137 lines
5.1 KiB
JavaScript
137 lines
5.1 KiB
JavaScript
const { expect } = require("@playwright/test");
|
|
const { resolveTimeout } = require("./timeouts");
|
|
const { apiFetchOnion, apiGetOnion } = require("./personas");
|
|
|
|
const LXM_PATTERN = /lxm|lexicon method/i;
|
|
const PROXIED_METHODS = ["app.bsky.feed.getTimeline", "app.bsky.notification.listNotifications"];
|
|
const PERSONAL_DETAILS = "app.bsky.actor.defs#personalDetailsPref";
|
|
const ADULT_BIRTHDATE = "1990-01-01";
|
|
|
|
async function expectPdsProxiesAppView(request, pdsBaseUrl, accessJwt) {
|
|
for (const nsid of PROXIED_METHODS) {
|
|
const response = await apiGetOnion(request, `${pdsBaseUrl}/xrpc/${nsid}`, {
|
|
headers: { authorization: `Bearer ${accessJwt}` },
|
|
timeout: resolveTimeout(30_000),
|
|
});
|
|
const body = await response.text();
|
|
expect(
|
|
response.status(),
|
|
`PDS must proxy ${nsid} to PDS_BSKY_APP_VIEW_URL with a service JWT the AppView accepts (got body: ${body})`,
|
|
).toBe(200);
|
|
}
|
|
}
|
|
|
|
async function brokerAccessJwt(page) {
|
|
let accessJwt = "";
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
accessJwt = await page
|
|
.evaluate(() => JSON.parse(localStorage.getItem("BSKY_STORAGE") || "{}").session?.currentAccount?.accessJwt || "")
|
|
.catch(() => "");
|
|
return accessJwt;
|
|
},
|
|
{ timeout: resolveTimeout(60_000), message: "broker handoff left no accessJwt in BSKY_STORAGE" },
|
|
)
|
|
.not.toBe("");
|
|
return accessJwt;
|
|
}
|
|
|
|
async function readPreferences(request, pdsBaseUrl, accessJwt) {
|
|
const response = await apiGetOnion(request, `${pdsBaseUrl}/xrpc/app.bsky.actor.getPreferences`, {
|
|
headers: { authorization: `Bearer ${accessJwt}` },
|
|
timeout: resolveTimeout(30_000),
|
|
});
|
|
expect(response.status(), `PDS getPreferences must return 200 (got body: ${await response.text()})`).toBe(200);
|
|
return (await response.json()).preferences;
|
|
}
|
|
|
|
async function restorePreferences(request, pdsBaseUrl, accessJwt, preferences) {
|
|
if (preferences.some((entry) => entry.$type === PERSONAL_DETAILS && entry.birthDate)) return;
|
|
const restore = apiFetchOnion(request, `${pdsBaseUrl}/xrpc/app.bsky.actor.putPreferences`, {
|
|
method: "POST",
|
|
headers: { authorization: `Bearer ${accessJwt}`, "content-type": "application/json" },
|
|
data: { preferences },
|
|
timeout: resolveTimeout(30_000),
|
|
}).then((response) => {
|
|
if (response.status() !== 200) throw new Error(`putPreferences answered ${response.status()}`);
|
|
});
|
|
let timer;
|
|
const deadline = new Promise((_, reject) => {
|
|
timer = setTimeout(() => reject(new Error("putPreferences timed out")), resolveTimeout(45_000));
|
|
});
|
|
await Promise.race([restore, deadline])
|
|
.catch((err) => console.warn(`bluesky: preferences not restored: ${err.message}`))
|
|
.finally(() => clearTimeout(timer));
|
|
}
|
|
|
|
function collectAppViewResponses(page) {
|
|
const seen = [];
|
|
page.on("response", (response) => {
|
|
const { pathname } = new URL(response.url());
|
|
if (!pathname.startsWith("/xrpc/app.bsky.") || response.request().method() !== "GET") return;
|
|
const status = response.status();
|
|
seen.push({
|
|
nsid: pathname.slice("/xrpc/".length),
|
|
status,
|
|
body: status === 200 ? Promise.resolve("") : response.text().catch((err) => `<body unavailable: ${err.message}>`),
|
|
});
|
|
});
|
|
return seen;
|
|
}
|
|
|
|
async function waitForResponseTo(seen, nsid, from = 0) {
|
|
await expect
|
|
.poll(() => seen.slice(from).some((entry) => entry.nsid === nsid), {
|
|
timeout: resolveTimeout(60_000),
|
|
message: `social-app never requested ${nsid}`,
|
|
})
|
|
.toBe(true);
|
|
}
|
|
|
|
async function expectNoFailures(seen, nsid) {
|
|
const failed = seen.filter((entry) => entry.nsid === nsid && entry.status !== 200);
|
|
expect(
|
|
await Promise.all(failed.map(async (entry) => `${entry.status} ${await entry.body}`)),
|
|
`PDS must proxy ${nsid} to the AppView with a service JWT the AppView accepts`,
|
|
).toEqual([]);
|
|
}
|
|
|
|
async function expectNoLxmRejections(seen, pageErrors) {
|
|
const lines = await Promise.all(seen.map(async (entry) => `${entry.nsid} ${entry.status} ${await entry.body}`));
|
|
expect(
|
|
lines.filter((line) => LXM_PATTERN.test(line)),
|
|
"no AppView call may be rejected for a missing lxm claim",
|
|
).toEqual([]);
|
|
expect(
|
|
pageErrors.filter((message) => LXM_PATTERN.test(message)),
|
|
"social-app must not raise lxm page errors after login",
|
|
).toEqual([]);
|
|
}
|
|
|
|
async function reachFollowingFeed(page) {
|
|
const home = page.getByTestId("followingFeedPage");
|
|
const birthdateGate = page.getByRole("button", { name: "Click here to update your birthdate" }).first();
|
|
await expect(async () => {
|
|
if (await birthdateGate.isVisible()) {
|
|
await birthdateGate.click();
|
|
const dialog = page.getByTestId("birthDateSettingsDialog");
|
|
await dialog.getByTestId("birthdayInput").fill(ADULT_BIRTHDATE);
|
|
await dialog.getByRole("button", { name: "Save birthdate" }).click();
|
|
}
|
|
await expect(home).toBeVisible({ timeout: resolveTimeout(5_000) });
|
|
}).toPass({ timeout: resolveTimeout(120_000) });
|
|
}
|
|
|
|
module.exports = {
|
|
brokerAccessJwt,
|
|
collectAppViewResponses,
|
|
expectNoFailures,
|
|
expectNoLxmRejections,
|
|
expectPdsProxiesAppView,
|
|
reachFollowingFeed,
|
|
readPreferences,
|
|
restorePreferences,
|
|
waitForResponseTo,
|
|
};
|