mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-16 00:10:56 +00:00
326 lines
15 KiB
TypeScript
326 lines
15 KiB
TypeScript
import { timedTest } from "./boot.js";
|
|
import assert from "node:assert/strict";
|
|
import { type OrderRecord } from "../src/domain.js";
|
|
import { Store } from "../src/store.js";
|
|
|
|
const storeTest = timedTest(2000);
|
|
|
|
|
|
class MemoryStorage {
|
|
map = new Map<string, string>();
|
|
failWrites = false;
|
|
getItem(k: string) { return this.map.get(k) ?? null; }
|
|
setItem(k: string, v: string) { if (this.failWrites) throw new Error("QuotaExceeded"); this.map.set(k, v); }
|
|
removeItem(k: string) { this.map.delete(k); }
|
|
}
|
|
|
|
const order = (id: string, over: Partial<OrderRecord> = {}): OrderRecord => ({
|
|
orderId: id, badgeType: "legend", months: 12,
|
|
createdAt: new Date(Date.parse("2026-08-24T11:02:19Z") + Number(id)).toISOString(),
|
|
status: "open", ...over,
|
|
});
|
|
|
|
storeTest("store: session round-trips the step and the draft", () => {
|
|
const s = new Store(new MemoryStorage());
|
|
assert.equal(s.session().step, "tier");
|
|
s.saveSession({ step: "months", priceId: "price_legend" });
|
|
assert.equal(s.session().step, "months");
|
|
assert.equal(s.session().priceId, "price_legend");
|
|
s.clearSession();
|
|
assert.equal(s.session().priceId, undefined);
|
|
});
|
|
|
|
storeTest("store: orders upsert by id and keep a stored code", () => {
|
|
const s = new Store(new MemoryStorage());
|
|
s.saveOrder(order("1", { code: "SB-CODE" }));
|
|
s.saveOrder(order("1", { status: "paid" }));
|
|
const got = s.orders()[0]!;
|
|
assert.equal(got.status, "paid");
|
|
assert.equal(got.code, "SB-CODE", "a later record must not clear the code");
|
|
});
|
|
|
|
storeTest("store: the cap drops the oldest entry holding no code", () => {
|
|
const s = new Store(new MemoryStorage());
|
|
for (let i = 0; i < 50; i++) s.saveOrder(order(String(i), i < 10 ? {} : { code: "C" + i }));
|
|
s.saveOrder(order("999", { code: "NEW" }));
|
|
const ids = s.orders().map((o) => o.orderId);
|
|
assert.equal(ids.length, 50);
|
|
assert.ok(!ids.includes("0"), "the oldest codeless entry is dropped");
|
|
assert.ok(ids.includes("999"));
|
|
});
|
|
|
|
storeTest("store: with every entry holding a code the new one is not stored", () => {
|
|
const s = new Store(new MemoryStorage());
|
|
for (let i = 0; i < 50; i++) s.saveOrder(order(String(i), { code: "C" + i }));
|
|
assert.equal(s.saveOrder(order("999", { code: "NEW" })), false);
|
|
assert.equal(s.orders().length, 50);
|
|
});
|
|
|
|
storeTest("store: a failing storage degrades without throwing", () => {
|
|
const mem = new MemoryStorage();
|
|
const s = new Store(mem);
|
|
mem.failWrites = true;
|
|
assert.equal(s.saveOrder(order("1", { code: "C" })), false);
|
|
assert.equal(s.saveSession({ step: "months" }), false);
|
|
assert.deepEqual(s.orders(), []);
|
|
assert.equal(s.session().step, "tier");
|
|
});
|
|
|
|
storeTest("store: corruption is replaced, not parsed", () => {
|
|
const mem = new MemoryStorage();
|
|
mem.map.set("sb.orders.v1", "{not json");
|
|
const s = new Store(mem);
|
|
assert.deepEqual(s.orders(), []);
|
|
assert.equal(s.saveOrder(order("1")), true);
|
|
assert.equal(s.orders().length, 1);
|
|
});
|
|
|
|
storeTest("store: newestOpen finds the resumable order", () => {
|
|
const s = new Store(new MemoryStorage());
|
|
s.saveOrder(order("1", { status: "expired" }));
|
|
s.saveOrder(order("2", { status: "open" }));
|
|
s.saveOrder(order("3", { status: "paid", code: "C" }));
|
|
assert.equal(s.newestOpen()?.orderId, "2");
|
|
});
|
|
|
|
storeTest("store: shape-corrupted but parseable entries are dropped, not thrown", () => {
|
|
const mem = new MemoryStorage();
|
|
mem.map.set("sb.orders.v1", JSON.stringify([{ foo: 1 }, { bar: 2 }]));
|
|
const s = new Store(mem);
|
|
assert.deepEqual(s.orders(), [], "malformed elements are dropped rather than surfaced");
|
|
assert.equal(s.saveOrder(order("1")), true, "the write path must not throw on corrupted shape");
|
|
assert.equal(s.orders().length, 1);
|
|
});
|
|
|
|
storeTest("store: orders sort chronologically across second- and millisecond-precision timestamps", () => {
|
|
const s = new Store(new MemoryStorage());
|
|
const secOrder: OrderRecord = {
|
|
orderId: "sec", badgeType: "legend", months: 12,
|
|
createdAt: "2026-08-24T11:02:19Z", status: "open",
|
|
};
|
|
const msOrder: OrderRecord = {
|
|
orderId: "ms", badgeType: "legend", months: 12,
|
|
createdAt: "2026-08-24T11:02:19.900Z", status: "open",
|
|
};
|
|
s.saveOrder(secOrder);
|
|
s.saveOrder(msOrder);
|
|
assert.deepEqual(
|
|
s.orders().map((o) => o.orderId),
|
|
["ms", "sec"],
|
|
"the .900Z record is chronologically newer despite sorting lexicographically before the second-precision string",
|
|
);
|
|
});
|
|
|
|
storeTest("store: an unparseable createdAt sorts oldest instead of poisoning the comparator", () => {
|
|
// Date.parse answers NaN for an unreadable createdAt, and NaN makes every comparison false, leaving the sort order undefined.
|
|
const mem = new MemoryStorage();
|
|
mem.map.set("sb.orders.v1", JSON.stringify([
|
|
{ orderId: "undated", badgeType: "legend", months: 12, createdAt: "whenever", status: "open" },
|
|
{ orderId: "older", badgeType: "legend", months: 12, createdAt: "2026-08-20T00:00:00Z", status: "open" },
|
|
]));
|
|
const s = new Store(mem);
|
|
s.saveOrder(order("1", { status: "open", createdAt: "2026-08-26T00:00:00Z" }));
|
|
assert.deepEqual(s.orders().map((o) => o.orderId), ["1", "older", "undated"]);
|
|
assert.equal(s.newestOpen()?.orderId, "1", "the newest DATED open order is the one resumed");
|
|
});
|
|
|
|
storeTest("store: a stored record keeps the fields that read and drops the ones that do not", () => {
|
|
const mem = new MemoryStorage();
|
|
mem.map.set("sb.orders.v1", JSON.stringify([{
|
|
orderId: "corrupt", createdAt: "2026-08-24T11:00:00Z", status: "open",
|
|
code: "SB-KEEP-ME", badgeType: null, months: "12",
|
|
currency: 42, paidInFull: "yes", cryptoCurrency: "eth", amount: 42000,
|
|
}]));
|
|
const stored = new Store(mem).orders();
|
|
assert.equal(stored.length, 1, "a record with a code is never dropped over a bad field");
|
|
const o = stored[0]!;
|
|
assert.equal(o.code, "SB-KEEP-ME");
|
|
assert.equal(o.amount, 42000, "and what does read is kept");
|
|
for (const key of ["currency", "paidInFull", "cryptoCurrency"] as const) {
|
|
assert.equal(o[key], undefined, `${key} does not read, so it is not there to be believed`);
|
|
}
|
|
assert.equal(o.badgeType, "");
|
|
assert.equal(o.months, 0);
|
|
});
|
|
|
|
storeTest("store: a record with nothing to point at is dropped whole", () => {
|
|
const mem = new MemoryStorage();
|
|
mem.map.set("sb.orders.v1", JSON.stringify([
|
|
{ orderId: "no-status", createdAt: "2026-08-24T11:00:00Z" },
|
|
{ orderId: "bad-status", createdAt: "2026-08-24T11:00:00Z", status: "refunded" },
|
|
{ orderId: "", createdAt: "2026-08-24T11:00:00Z", status: "open" },
|
|
{ createdAt: "2026-08-24T11:00:00Z", status: "open" },
|
|
"not-an-object",
|
|
null,
|
|
]));
|
|
assert.deepEqual(new Store(mem).orders(), [], "an order with no id or no state has no row to draw");
|
|
mem.map.set("sb.orders.v1", JSON.stringify({ orderId: "not-a-list" }));
|
|
assert.deepEqual(new Store(mem).orders(), []);
|
|
});
|
|
|
|
storeTest("store: a session or theme outside the known set falls back to the default", () => {
|
|
const mem = new MemoryStorage();
|
|
mem.map.set("sb.session.v1", JSON.stringify({ step: "elsewhere", priceId: 12, method: "paypal" }));
|
|
mem.map.set("sb.theme.v1", JSON.stringify("neon"));
|
|
assert.deepEqual(new Store(mem).session(), { step: "tier" });
|
|
assert.equal(new Store(mem).theme(), "system");
|
|
|
|
mem.map.set("sb.session.v1", JSON.stringify({ step: "months", priceId: "price_legend", method: "paypal" }));
|
|
assert.deepEqual(new Store(mem).session(), { step: "months", priceId: "price_legend" },
|
|
"a good step keeps what reads beside it, and only that");
|
|
});
|
|
|
|
storeTest("store: saveOrder replaces the stored entry rather than merging into it", () => {
|
|
const s = new Store(new MemoryStorage());
|
|
s.saveOrder(order("1", { address: "48HqK2Xm", cryptoAmountPaid: "1.482", paidInFull: true, amount: 42000 }));
|
|
s.saveOrder(order("1", { status: "expired" }));
|
|
const stored = s.order("1")!;
|
|
for (const key of ["address", "cryptoAmountPaid", "paidInFull", "amount"] as const) {
|
|
assert.equal(stored[key], undefined, `${key} must not survive a write that leaves it out`);
|
|
}
|
|
assert.equal(stored.status, "expired");
|
|
});
|
|
|
|
storeTest("store: the two facts only this browser holds survive a write that omits them", () => {
|
|
const s = new Store(new MemoryStorage());
|
|
s.saveOrder(order("1", { code: "SB-CODE" }));
|
|
assert.equal(s.markSubmitted("1"), true);
|
|
s.saveOrder(order("1", { status: "paid" }));
|
|
assert.equal(s.order("1")!.code, "SB-CODE");
|
|
assert.equal(s.order("1")!.submitted, true);
|
|
});
|
|
|
|
storeTest("store: the app that opened the page round-trips the session, and a value outside the contract reads as none", () => {
|
|
const mem = new MemoryStorage();
|
|
const s = new Store(mem);
|
|
s.saveSession({ app: "mobile" });
|
|
s.saveSession({ step: "months", priceId: "price_legend" });
|
|
assert.equal(new Store(mem).session().app, "mobile", "a later step and a reload keep it");
|
|
mem.setItem("sb.session.v1", JSON.stringify({ step: "tier", app: "true" }));
|
|
assert.equal(s.session().app, undefined, "the stored value is the page's own word, never the URL's");
|
|
});
|
|
|
|
storeTest("store: an order keeps the app it was bought from through a write that omits it and a reload", () => {
|
|
const mem = new MemoryStorage();
|
|
const s = new Store(mem);
|
|
s.saveOrder(order("1", { app: "desktop" }));
|
|
s.saveOrder(order("1", { status: "paid" }));
|
|
assert.equal(s.order("1")!.app, "desktop", "the service never sends it, so its answer cannot clear it");
|
|
assert.equal(new Store(mem).order("1")!.app, "desktop");
|
|
s.saveOrder(order("2"));
|
|
assert.equal(s.order("2")!.app, undefined, "and it is not a page-wide flag");
|
|
});
|
|
|
|
storeTest("store: markSubmitted is per order, sticky, and survives everything but Forget", () => {
|
|
const mem = new MemoryStorage();
|
|
const s = new Store(mem);
|
|
s.saveOrder(order("1", { code: "SB-CODE" }));
|
|
s.saveOrder(order("2"));
|
|
assert.equal(s.markSubmitted("1"), true);
|
|
assert.equal(s.markSubmitted("absent"), false, "there is no order to mark");
|
|
|
|
s.clearSession();
|
|
s.saveOrder(order("1", { status: "open" })); // a later 200 for the same order
|
|
assert.equal(s.order("1")!.submitted, true, "a plain upsert must not take it back off");
|
|
assert.equal(s.order("1")!.code, "SB-CODE");
|
|
assert.equal(s.order("2")!.submitted, undefined, "and it is not a page-wide flag");
|
|
|
|
// A reload is a fresh Store over the same storage.
|
|
assert.equal(new Store(mem).order("1")!.submitted, true);
|
|
});
|
|
|
|
storeTest("store: markCanceled is per order, sticky, and survives a later server answer", () => {
|
|
const mem = new MemoryStorage();
|
|
const s = new Store(mem);
|
|
s.saveOrder(order("1"));
|
|
s.saveOrder(order("2"));
|
|
assert.equal(s.markCanceled("1"), true);
|
|
assert.equal(s.markCanceled("absent"), false, "there is no order to mark");
|
|
assert.equal(s.order("1")!.canceled, true);
|
|
|
|
s.saveOrder(order("1", { status: "expired" })); // the cancel's own 200, applied to the record
|
|
assert.equal(s.order("1")!.canceled, true, "a plain upsert must not take it back off");
|
|
assert.equal(s.order("2")!.canceled, undefined, "and it is not a page-wide flag");
|
|
assert.equal(new Store(mem).order("1")!.canceled, true, "and it survives a reload");
|
|
});
|
|
|
|
storeTest("store: the cap evicts the oldest codeless entry even when an older entry holds a code", () => {
|
|
const s = new Store(new MemoryStorage());
|
|
for (let i = 0; i < 50; i++) s.saveOrder(order(String(i), i % 2 === 0 ? { code: "C" + i } : {}));
|
|
s.saveOrder(order("999", { code: "NEW" }));
|
|
const ids = s.orders().map((o) => o.orderId);
|
|
assert.equal(ids.length, 50);
|
|
assert.ok(ids.includes("0"), "the oldest entry overall holds a code and must survive");
|
|
assert.ok(!ids.includes("1"), "the oldest codeless entry is evicted, not the oldest overall");
|
|
assert.ok(ids.includes("999"));
|
|
});
|
|
|
|
storeTest("store: a store that loses its writes never claims to hold the code", () => {
|
|
const durable = new Store(new MemoryStorage());
|
|
durable.saveOrder(order("1", { code: "SB-CODE" }));
|
|
assert.equal(durable.holdsCode("1", "SB-CODE"), true);
|
|
|
|
const losing = new Store(new MemoryStorage(), false);
|
|
losing.saveOrder(order("1", { code: "SB-CODE" }));
|
|
assert.equal(losing.order("1")?.code, "SB-CODE", "it answers with what it was handed");
|
|
assert.equal(losing.holdsCode("1", "SB-CODE"), false, "and still does not claim to hold it");
|
|
|
|
assert.equal(durable.holdsCode("1", undefined), false, "no code is not a held code");
|
|
assert.equal(durable.holdsCode("2", "SB-CODE"), false);
|
|
});
|
|
|
|
storeTest("store: a full list cannot hold another code, and says so before the money", () => {
|
|
const s = new Store(new MemoryStorage());
|
|
assert.equal(s.canHoldACode(), true, "an empty store has room");
|
|
for (let i = 0; i < 50; i++) s.saveOrder(order(String(i), { code: "C" + i }));
|
|
assert.equal(s.saveOrder(order("999", { code: "NEW" })), false, "the cap really is reached");
|
|
assert.equal(s.canHoldACode(), false, "and the buyer can be told before paying");
|
|
|
|
const roomy = new Store(new MemoryStorage());
|
|
for (let i = 0; i < 50; i++) roomy.saveOrder(order(String(i), i === 7 ? {} : { code: "C" + i }));
|
|
assert.equal(roomy.canHoldACode(), true, "one codeless entry is room enough");
|
|
|
|
assert.equal(new Store(new MemoryStorage(), false).canHoldACode(), false, "and a losing store never has room");
|
|
});
|
|
|
|
storeTest("store: forgetting one key does not depend on the other succeeding", () => {
|
|
class RefusesOrders extends MemoryStorage {
|
|
override removeItem(k: string): void {
|
|
if (k === "sb.orders.v1") throw new Error("SecurityError");
|
|
super.removeItem(k);
|
|
}
|
|
}
|
|
const storage = new RefusesOrders();
|
|
const s = new Store(storage);
|
|
s.saveOrder(order("1", { code: "SB-CODE" }));
|
|
s.saveSession({ step: "checkout" });
|
|
s.forgetEverything();
|
|
assert.equal(storage.getItem("sb.session.v1"), null, "the session goes even though the orders key threw");
|
|
});
|
|
|
|
storeTest("store: forgetting everything is countable, so a write awaited across it can be dropped", () => {
|
|
const s = new Store(new MemoryStorage());
|
|
const before = s.wipeCount;
|
|
s.saveOrder(order("1"));
|
|
assert.equal(s.wipeCount, before, "an ordinary write is not a wipe");
|
|
s.forgetEverything();
|
|
assert.equal(s.wipeCount, before + 1, "and the wipe is");
|
|
s.forgetEverything();
|
|
assert.equal(s.wipeCount, before + 2, "each one counts: two answers may be in flight");
|
|
});
|
|
|
|
storeTest("store: a card-return is returned within the window, cleared on read, and dropped when stale", () => {
|
|
const s = new Store(new MemoryStorage());
|
|
const NOW = 1_000_000;
|
|
const WINDOW = 60_000;
|
|
assert.equal(s.takeCardReturn(WINDOW, NOW), undefined, "nothing remembered yet");
|
|
s.rememberCardReturn("inv_1", NOW);
|
|
assert.equal(s.takeCardReturn(WINDOW, NOW + WINDOW), "inv_1", "at the edge of the window it is the return");
|
|
assert.equal(s.takeCardReturn(WINDOW, NOW + WINDOW), undefined, "and it was cleared on the read");
|
|
s.rememberCardReturn("inv_2", NOW);
|
|
assert.equal(s.takeCardReturn(WINDOW, NOW + WINDOW + 1), undefined, "past the window it is a stale attempt");
|
|
s.rememberCardReturn("inv_3", NOW);
|
|
s.clearCardReturn();
|
|
assert.equal(s.takeCardReturn(WINDOW, NOW), undefined, "an explicit clear forgets it");
|
|
});
|