mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-09-28 06:59:42 +00:00
* badges: webapp (#7433) * badges: service migrations, store and catalog * badges: BTCPay provider and settlement poller * badges: web listener and /api endpoints * web: checkout single-page app * badges: tests and BTCPay fixtures * badges: README and ini reference * badges: fix hex16 build on GHC 8.10.7 * badges: Stripe card lane * badges: fix Stripe card checkout, add theming * badges: add a discount row to the order summary * badges: site navbar, embedding, theme, Forget move * badges: use SB code prefix in web checkout * badges: rename sxb app namespace to sb * badges: embed checkout nav via site; keep original app navbar * badges: post iframe height, apply site background when embedded * badges: embed dark surfaces, steadier iframe height * badges: hide app footer when embedded * badges: size embedded body to content, not viewport * badges: declare color-scheme to stop reload flash * badges: fade shell in on load, no reload blank * badges: prerender app shell into index.html * badges: pre-paint theme, hide shell on deep reload * badges: logo returns to landing client-side * badges: embedded wizard back, buy-a-code, resume * badges: signal app-managed screens, resume across reload * badges: rebuild wizard history on deep load so Back walks it * badges: carry welcome-page height as the iframe floor * badges: keep selection on Buy a code; rename to Your codes * badges: read web shell as UTF-8, not locale * badges: resume the exact paid order after Stripe card redirect * badges: move docker deploy under scripts * badges: add serve_webapp toggle and webapp export * badges: wire split webapp deploy in docker config * badges: quiet agent logs by default * badges: resume card redirect in the embedded frame * badges: migrate Stripe adapter to PaymentIntents * badges: correct Stripe restricted key scopes in ini example * badges: card via Payment Element and PaymentIntents * badges: fix stale Checkout Session wording in Stripe adapter * badges: fix stale CheckoutActions reference in card comment * badges: order shell stylesheet before bootstrap script * badges: remove development card stand-in * badges: theme the Stripe card form with the site palette * badges: exclude web from the Haskell build stage * badges: unify invoice cancel and mark canceled * badges: default log level to info * badges: unify closed-invoice buy-again button * badges: mute agent connection logs at info level * badges: show purchase time in local timezone in Your codes * badges: log service events on own channel, quiet agent * badges: fold service migrations into one baseline * badges: run compose on postgres over host network * badges: use high-res hero art * badges: add web CI to catch stale builds * badges: rebuild web shell from committed source * badges: normalize invoice-code link and columns * badges: drop unused columns, rename index * badges: note deferred receipt_hash in migrations * badges: apply code-review fixes * badges: reduce comments across service and web --------- Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com> Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com> * badges: improve web page (#7546) * badges: improve web page * improve layout * improve layout * fix * small changes --------- Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com> * badges: read one issuer key from the ini * badges: move and group the service tests * badges: service fixes (#7567) * badges: match the redeem error wording in tests * badges: drop unused imports in the bot tests * badges: cancel Stripe orders when they expire * badges: correct the Stripe config and docs * badges: refuse to revoke a redeemed code * badges: make the fake Stripe cancel like Stripe * badges: limit replayed webhook deliveries --------- Co-authored-by: sh <37271604+shumvgolove@users.noreply.github.com> Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com> Co-authored-by: shum <github.shum@liber.li> Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
118 lines
6.6 KiB
TypeScript
118 lines
6.6 KiB
TypeScript
import { timedTest } from "./boot.js";
|
|
import assert from "node:assert/strict";
|
|
import { readFileSync, readdirSync } from "node:fs";
|
|
|
|
const buildTest = timedTest(2000);
|
|
|
|
interface BuildModule {
|
|
HASH_CHARS: number;
|
|
ASSET_PATTERN: RegExp;
|
|
BUILD_PATTERN: RegExp;
|
|
served(source: string): string;
|
|
hashOf(files: Array<readonly [string, string | Buffer]>): string;
|
|
assets(compiled?: string, stylesheet?: string, images?: string): Array<[string, string | Buffer]>;
|
|
retarget(text: string, pattern: RegExp, replacement: string, what: string): string;
|
|
withBuild(html: string, build: string): string;
|
|
withBuildId(js: string, build: string): string;
|
|
}
|
|
|
|
// A non-literal specifier keeps tsc from resolving build.js, which this project does not compile.
|
|
const build = (await import(new URL("../../build.js", import.meta.url).href)) as BuildModule;
|
|
|
|
const indexHtml = readFileSync(new URL("../../public/index.html", import.meta.url), "utf8");
|
|
const workerJs = readFileSync(new URL("../../public/sw.js", import.meta.url), "utf8");
|
|
|
|
function declaredIn(text: string): string[] {
|
|
return [...text.matchAll(/[0-9a-f]{16}/g)].map((m) => m[0]);
|
|
}
|
|
|
|
// Fails when the committed shell or worker names a build other than the one on disk, forcing a rebuild before commit.
|
|
buildTest("build: the committed shell and worker name the build that is on disk", () => {
|
|
const current = build.hashOf(build.assets());
|
|
assert.equal(new Set(declaredIn(indexHtml)).size, 1, "the shell names exactly one build");
|
|
assert.equal(declaredIn(indexHtml)[0], current,
|
|
"public/index.html names a build that is not this one — run `npm run build` and commit the result");
|
|
assert.ok(workerJs.includes(`const BUILD = "${current}";`),
|
|
"public/sw.js names a build that is not this one — run `npm run build` and commit the result");
|
|
});
|
|
|
|
buildTest("build: the hash is derived from the bytes that are served, and from nothing else", () => {
|
|
const files: Array<readonly [string, string]> = [["main.js", "a"], ["styles.css", "b"]];
|
|
assert.equal(build.hashOf(files), build.hashOf(files), "same content, same hash — twice in a row");
|
|
assert.equal(build.hashOf(files), build.hashOf([...files].reverse()), "and independent of the order read");
|
|
assert.notEqual(build.hashOf(files), build.hashOf([["main.js", "a "], ["styles.css", "b"]]),
|
|
"one byte of one module changes it: this is what stops a cache-first page serving last week's code");
|
|
assert.notEqual(build.hashOf(files), build.hashOf([["main.js", "a"], ["styles.css", "b "]]),
|
|
"the stylesheet is served under the same hash, so it counts too");
|
|
assert.notEqual(build.hashOf(files), build.hashOf([["other.js", "a"], ["styles.css", "b"]]),
|
|
"and so does the name each is served under");
|
|
assert.match(build.hashOf(files), new RegExp(`^[0-9a-f]{${build.HASH_CHARS}}$`));
|
|
});
|
|
|
|
buildTest("build: a rebuild of unchanged sources produces the same hash and the same files", () => {
|
|
const first = build.assets();
|
|
const second = build.assets();
|
|
assert.deepEqual(first, second);
|
|
assert.equal(build.hashOf(first), build.hashOf(second), "idempotent: nothing changed, nothing moves");
|
|
});
|
|
|
|
buildTest("build: what is hashed is what is served — every module, the stylesheet, the images, the fonts, no maps", () => {
|
|
const names = build.assets().map(([name]) => name).sort();
|
|
const modules = readdirSync(new URL("../../src", import.meta.url))
|
|
.filter((f) => f.endsWith(".ts")).map((f) => f.replace(/\.ts$/, ".js"));
|
|
const images = readdirSync(new URL("../../public/img", import.meta.url))
|
|
.filter((f) => f.endsWith(".png") || f.endsWith(".svg"));
|
|
const fonts = readdirSync(new URL("../../public/fonts", import.meta.url)).filter((f) => f.endsWith(".woff2"));
|
|
assert.ok(images.some((f) => f.endsWith(".png")), "the hero has to be somewhere for the stylesheet to point at");
|
|
assert.ok(images.some((f) => f.endsWith(".svg")), "and so does the wordmark");
|
|
assert.ok(fonts.length > 0, "and the faces the stylesheet declares");
|
|
assert.deepEqual(names, [...modules, "styles.css", "init.js", ...images, ...fonts].sort());
|
|
assert.ok(!names.some((n) => n.endsWith(".map")),
|
|
"a source map under the hash would 404 on its sources, which are not served");
|
|
});
|
|
|
|
buildTest("build: an image's BYTES move the hash, so a redrawn hero is a new build", () => {
|
|
// Read as bytes, since a PNG decoded as UTF-8 is a lossy string two different images can collide on.
|
|
const [, content] = build.assets().find(([name]) => name.endsWith(".png"))!;
|
|
assert.ok(Buffer.isBuffer(content), "images are hashed and written as bytes");
|
|
const base: Array<readonly [string, string | Buffer]> = [["styles.css", "a"], ["hero-light.png", content]];
|
|
const flipped = Buffer.from(content);
|
|
const last = flipped.length - 1;
|
|
flipped[last] = flipped[last]! ^ 0xff;
|
|
assert.notEqual(build.hashOf(base), build.hashOf([["styles.css", "a"], ["hero-light.png", flipped]]),
|
|
"one byte of the hero is a different build");
|
|
});
|
|
|
|
buildTest("build: the sourceMappingURL comment is stripped, because the map is not published", () => {
|
|
assert.equal(build.served("const a = 1;\n//# sourceMappingURL=a.js.map\n"), "const a = 1;\n");
|
|
assert.equal(build.served("const a = 1;\n"), "const a = 1;\n", "a module without one is untouched");
|
|
for (const [, content] of build.assets()) {
|
|
assert.ok(!content.includes("sourceMappingURL"), "no served file may point at a map");
|
|
}
|
|
});
|
|
|
|
buildTest("build: both files are retargeted in one pass, and each really changes", () => {
|
|
const to = "0123456789abcdef";
|
|
const html = build.withBuild(indexHtml, to);
|
|
assert.deepEqual([...new Set(declaredIn(html))], [to], "every asset path in the shell moves together");
|
|
assert.ok(html.includes(`/assets/${to}/main.js`) && html.includes(`/assets/${to}/styles.css`));
|
|
const js = build.withBuildId(workerJs, to);
|
|
assert.ok(js.includes(`const BUILD = "${to}";`));
|
|
assert.ok(!js.includes(declaredIn(workerJs)[0]!), "the old build id is gone from the worker");
|
|
});
|
|
|
|
buildTest("build: retargeting is idempotent", () => {
|
|
const to = "0123456789abcdef";
|
|
assert.equal(build.withBuild(build.withBuild(indexHtml, to), to), build.withBuild(indexHtml, to));
|
|
assert.equal(build.withBuildId(build.withBuildId(workerJs, to), to), build.withBuildId(workerJs, to));
|
|
});
|
|
|
|
buildTest("build: retargeting nothing FAILS rather than quietly shipping the old hash", () => {
|
|
assert.throws(() => build.withBuild('<script src="/js/main.js"></script>', "0123456789abcdef"),
|
|
/no asset path to rewrite/);
|
|
assert.throws(() => build.withBuildId('const VERSION = "1";', "0123456789abcdef"),
|
|
/no BUILD constant to rewrite/);
|
|
assert.doesNotThrow(() => build.withBuild(indexHtml, "0123456789abcdef"));
|
|
assert.doesNotThrow(() => build.withBuildId(workerJs, "0123456789abcdef"));
|
|
});
|