The dump worker becomes a page cache: a hit is served from DO SQLite storage with no diigo call; a miss asks the webapp instance in the cache's wiring and etches the page. - two independent caches, one behavior (PagedCache): DiigoCache keeps every item; PublicCache keeps shared items only, dropping private rows before they are stored; served at /public/<username> (HTML page form, or JSON) - create flow on one port: /_connect relays webapp's mint form, /_mint proxies the mint, /_wire wires a private or public cache; /_publish adds a public cache later - ids are webkeys: kept out of URLs (POST bodies), shown and logged only as a hash fingerprint; saved to the password manager under the holder's petname (never sent to the server) - items keyed on diigo's link_id; username from u_name; unkeyable items and every failure path are logged; each page read logs hit/miss - tests run the real worker + DOs in the vitest pool against a fake webapp (tests/fake-webapp.js) Generated-by: opencode (GLM) Generated-by: Claude Opus 5.5
63 lines
2.5 KiB
TypeScript
63 lines
2.5 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { pmSave } from "../src/pm-save.js";
|
|
import type { PmPowers } from "../src/pm-save.js";
|
|
|
|
/** a fake console that records what was logged, by level */
|
|
function makeLog() {
|
|
const entries: { level: string; args: unknown[] }[] = [];
|
|
const at = (level: string) => (...args: unknown[]) => entries.push({ level, args });
|
|
return { log: { info: at("info"), warn: at("warn"), error: at("error") }, entries };
|
|
}
|
|
|
|
class FakeCredential {
|
|
constructor(readonly init: { id: string; password: string }) {}
|
|
}
|
|
|
|
function setup(store: (c: unknown) => Promise<unknown>, petname = "my cache") {
|
|
const { log, entries } = makeLog();
|
|
const stored: unknown[] = [];
|
|
const powers: PmPowers = {
|
|
petname: { value: petname },
|
|
credentials: { store: (c) => (stored.push(c), store(c)) },
|
|
PasswordCredential: FakeCredential,
|
|
log,
|
|
};
|
|
const btn = { textContent: "save to password manager", disabled: false };
|
|
return { powers, btn, entries, stored };
|
|
}
|
|
|
|
describe("pmSave", () => {
|
|
it("stores under the petname, and reports a spec-conformant resolve (undefined) as offered, not failed", async () => {
|
|
const { powers, btn, entries, stored } = setup(() => Promise.resolve(undefined));
|
|
await pmSave(btn, "the-key", powers);
|
|
expect(stored).toEqual([new FakeCredential({ id: "my cache", password: "the-key" })]);
|
|
expect(btn.textContent).not.toMatch(/not saved|fail/);
|
|
expect(btn.textContent).toMatch(/password manager/);
|
|
expect(btn.disabled).toBe(false);
|
|
expect(entries.map((e) => e.level)).toEqual(["info"]);
|
|
});
|
|
|
|
it("logs, never silently, when the browser has no credential store", async () => {
|
|
const { powers, btn, entries } = setup(() => Promise.resolve(undefined));
|
|
delete powers.PasswordCredential;
|
|
await pmSave(btn, "the-key", powers);
|
|
expect(btn.textContent).toMatch(/no credential store/);
|
|
expect(entries.map((e) => e.level)).toEqual(["warn"]);
|
|
});
|
|
|
|
it("logs the error when the store rejects, without the key", async () => {
|
|
const { powers, btn, entries } = setup(() => Promise.reject(new Error("NotAllowedError")));
|
|
await pmSave(btn, "the-key", powers);
|
|
expect(btn.textContent).toMatch(/save failed/);
|
|
expect(entries.map((e) => e.level)).toEqual(["error"]);
|
|
expect(JSON.stringify(entries)).not.toContain("the-key");
|
|
});
|
|
|
|
it("asks for a petname rather than saving under an empty username", async () => {
|
|
const { powers, btn, stored } = setup(() => Promise.resolve(undefined), " ");
|
|
await pmSave(btn, "the-key", powers);
|
|
expect(stored).toEqual([]);
|
|
expect(btn.textContent).toMatch(/petname/);
|
|
});
|
|
});
|