openvibers/openvibe.tips Commits
Tips moves between its pages without a reload (plan T11): openvibe-shared v2.2.0 boost swaps <main> styles-first and does a normal load across a deploy (the ov-boost marker carries the release); page scripts that wire <main> also run on ov:boost:load. Two test:pg flakes the verify found are fixed test-side: the hidden-pending-tip case waits for the settlement to reach the row instead of a fixed 50 ms, and the per-test database teardown waits for terminated sessions to leave and retries catalog DDL on 'tuple concurrently updated'. npm test 16/16; the three files green twice on PostgreSQL.
Changes
@@@ -46,7 +46,7 @@ from *interaction delivered*.(openvibe-sdk transactional outbox)- **OpenVibe.Network** — SSO for people, service tokens, JWKS, identity resolve- **OpenVibe.Live** — chat delivery through `/internal/tips/deliveries` (deployed in Live since `f11f809`; [docs/live-patch.diff](docs/live-patch.diff) is the original patch) until OpenVibe.Chat exposes paid messages; overlay consumer--- **OpenVibe.Shared** v2.0.0 (app icon, SSR footer, noscript nav, release manifest, legal pages) and++- **OpenVibe.Shared** v2.2.0 (app icon, SSR footer, noscript nav, release manifest, legal pages, boost page moves) andthe Network's `navbar.js`; **openvibe-contracts** v0.79.0 and **openvibe-sdk** v0.25.0 (service tokens,the async data layer `openvibe-sdk/db`, the PostgreSQL outbox and inbox — the events outbox is the SDK's`createServiceOutbox`, plan T1 — Valkey, pub/sub and the shared limit store), pinned by release tarball;@@@ -16,7 +16,7 @@"jsonwebtoken": "^9.0.2","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.79.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.25.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.0.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.2.0","pg": "^8.23.0"},"devDependencies": {@@@ -1162,9 +1162,9 @@}},"node_modules/openvibe-shared": {-- "version": "2.0.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.0.0",-- "integrity": "sha512-n5CHC0hfxOo0jo/3kS4O3KiwhirhZDlx5zZTIuPyIf3Dq0yI/R6R64btx7NYwZe5dI0NjvqzEtnv19m8unwLMg==",++ "version": "2.2.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.2.0",++ "integrity": "sha512-1q+iUuUxzPeylTYmithW4jR++Jccvn5GEOrB1B3D18Dgn6OXQhPpvhPQAa1D1k1p+dWIa1/SmggyJwSGFnTNtw==","license": "MIT","dependencies": {"jsonwebtoken": "^9.0.0"@@@ -28,7 +28,7 @@"jsonwebtoken": "^9.0.2","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.79.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.25.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.0.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.2.0","pg": "^8.23.0"},"engines": {@@@ -2,7 +2,7 @@(function () {'use strict';document.documentElement.classList.add('js');-- document.addEventListener('DOMContentLoaded', function () {++ function wire() {var form = document.querySelector('.tip-form');if (!form) return;function sync() {@@@ -11,5 +11,7 @@}form.addEventListener('change', sync);sync();-- });++ }++ document.addEventListener('DOMContentLoaded', wire);++ document.addEventListener('ov:boost:load', wire); // a swapped-in page's tip form wires the same way})();@@@ -57,6 +57,7 @@ function createApp(opts = {}) {const domain = createDomain({ db, config, outbox, billing, adapters, valkey, now, log });const apiAuth = createApiAuth({ config, keys, userAuth });const release = require('openvibe-shared/release').createRelease({ service: 'tips', root: path.join(__dirname, '..') });++ require('./web/layout').setRelease(release.release);const layout = createLayout({ config, release });const app = express();@@@ -31,6 +31,11 @@ function asset(rel) {}const assetVersion = (rel) => { asset(rel); return _hashes.get(rel); };++// The deployed release (app.js sets it from openvibe-shared/release): openvibe-shared/boost swaps a page in place only++// between pages of the same release, and does a normal load across a deploy.++let RELEASE = 'dev';++function setRelease(id) { if (id) RELEASE = String(id); }++const NAV_LINKS = [{ label: 'Dashboard', href: '/dashboard' },{ label: 'Receipts', href: '/receipts' },@@@ -54,7 +59,7 @@ function createLayout({ config, release }) {history: { type: 'page', title: o.title || SITE_NAME },silentLogin: `${config.baseUrl}/auth/login?silent=1&next={url}`,sessionUrl: '/auth/me',-- loginUrl: `/auth/login?next=${encodeURIComponent(o.canonicalPath || '/')}`,++ loginUrl: '/auth/login?next={path}', // filled from the current page (boost moves between pages)logoutUrl: '/auth/logout?next={path}', // Sign out in the shared navbar ends this site's session toonotificationsRealtime: true, // the bell hears new notifications over OpenVibe.Events (Shared 1.22.0)};@@@ -88,6 +93,8 @@ ${jsonLd}<script src="${ovServe.url('navbar.js')}" defer></script><script src="${ovServe.url('footer.js')}" defer></script><script src="${asset('js/tips.js')}" defer></script>++<meta name="ov-boost" content="tips@${esc(RELEASE)}">++<script src="${ovServe.url('boost.js')}" data-main="#main" defer></script></head><body><div id="navbar-mount"></div>@@@ -112,4 +119,4 @@ document.addEventListener('DOMContentLoaded', function () {return { page, abs };}--module.exports = { createLayout, esc, asset, assetVersion, SITE_NAME, DEFAULT_DESCRIPTION };++module.exports = { createLayout, setRelease, esc, asset, assetVersion, SITE_NAME, DEFAULT_DESCRIPTION };@@@ -56,8 +56,20 @@ async function testDb({ store = process.env.TIPS_TEST_STORE || 'pglite', max = 4// PgBouncer keeps its server connections after the client pool closes: end them here, or// every run would leave its roles' idle connections holding the server's slots.await su.query('SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE usename = ANY($1)', [[name, owner]]);-- await su.query(`DROP SCHEMA IF EXISTS ${name} CASCADE`);-- for (const r of [name, owner]) { await su.query(`DROP OWNED BY ${r}`); await su.query(`DROP ROLE ${r}`); }++ // Terminating is asynchronous: wait until the sessions are gone, then retry catalog DDL that races another++ // session's catalog write ('tuple concurrently updated', XX000) a few times.++ for (let i = 0; i < 100; i++) {++ const left = await su.query('SELECT count(*)::int AS n FROM pg_stat_activity WHERE usename = ANY($1)', [[name, owner]]);++ if (!Number(left.rows[0].n)) break;++ await new Promise((ok) => setTimeout(ok, 50));++ }++ const ddl = async (stmt) => {++ for (let i = 0; ; i++) {++ try { return await su.query(stmt); } catch (err) { if (err.code !== 'XX000' || i >= 5) throw err; await new Promise((ok) => setTimeout(ok, 100 * (i + 1))); }++ }++ };++ await ddl(`DROP SCHEMA IF EXISTS ${name} CASCADE`);++ for (const r of [name, owner]) { await ddl(`DROP OWNED BY ${r}`); await ddl(`DROP ROLE ${r}`); }} finally { await su.close(); }}let db;@@@ -230,7 +230,8 @@ const validModerated = payloadValidator('tips.interaction.moderated');const id = r.json.interaction.id;assert.strictEqual((await t.call('POST', `/api/v1/interactions/${id}/hide`, { user: alex })).status, 200);await t.deliver(billing.settlePurchase((await domain.interactions.get(t.db, id)).billing_intent_id));-- await new Promise((ok) => setTimeout(ok, 50));++ // The settlement reaches the row through the delivery; wait for it (bounded) instead of a fixed sleep.++ for (let k = 0; k < 100 && (await domain.interactions.get(t.db, id)).payment_state === 'pending'; k++) await new Promise((ok) => setTimeout(ok, 20));await domain.interactions.processDueTransfers();const i = await domain.interactions.get(t.db, id);assert.strictEqual(i.payment_state, 'settled');@@@ -33,6 +33,16 @@ const { boot, check, done } = require('./helpers/app');assert.ok(!r.text.includes('href="/bob"'), 'switched-off pages are not listed');});++ await check('every page carries the boost marker and script, and the navbar signs in back to the current page', async () => {++ for (const [p, u] of [['/', null], ['/alex', viewer], ['/dashboard', alex], ['/receipts', viewer], ['/updates', alex]]) {++ const r = await get(p, u);++ assert.strictEqual(r.status, 200, p);++ assert.match(r.text, /<meta name="ov-boost" content="tips@[^"]+">/, p);++ assert.match(r.text, /<script src="\/shared\/boost\.js\?v=[0-9a-f]{12}" data-main="#main" defer><\/script>/, p);++ assert.match(r.text, /"loginUrl":"\/auth\/login\?next=\{path\}"/, p);++ }++ });++await check('creator page: indexable when on, goals and a no-JS tip form', async () => {const r = await get('/alex');assert.strictEqual(r.status, 200);
@@ -46,7 +46,7 @@ from *interaction delivered*.
@@ -46,7 +46,7 @@ from *interaction delivered*.
(openvibe-sdk transactional outbox)
(openvibe-sdk transactional outbox)
- **OpenVibe.Network** — SSO for people, service tokens, JWKS, identity resolve
- **OpenVibe.Network** — SSO for people, service tokens, JWKS, identity resolve
- **OpenVibe.Live** — chat delivery through `/internal/tips/deliveries` (deployed in Live since `f11f809`; [docs/live-patch.diff](docs/live-patch.diff) is the original patch) until OpenVibe.Chat exposes paid messages; overlay consumer
- **OpenVibe.Live** — chat delivery through `/internal/tips/deliveries` (deployed in Live since `f11f809`; [docs/live-patch.diff](docs/live-patch.diff) is the original patch) until OpenVibe.Chat exposes paid messages; overlay consumer
-- **OpenVibe.Shared** v2.0.0 (app icon, SSR footer, noscript nav, release manifest, legal pages) and
+- **OpenVibe.Shared** v2.2.0 (app icon, SSR footer, noscript nav, release manifest, legal pages, boost page moves) and
the Network's `navbar.js`; **openvibe-contracts** v0.79.0 and **openvibe-sdk** v0.25.0 (service tokens,
the Network's `navbar.js`; **openvibe-contracts** v0.79.0 and **openvibe-sdk** v0.25.0 (service tokens,
the async data layer `openvibe-sdk/db`, the PostgreSQL outbox and inbox — the events outbox is the SDK's
the async data layer `openvibe-sdk/db`, the PostgreSQL outbox and inbox — the events outbox is the SDK's
`createServiceOutbox`, plan T1 — Valkey, pub/sub and the shared limit store), pinned by release tarball;
`createServiceOutbox`, plan T1 — Valkey, pub/sub and the shared limit store), pinned by release tarball;
@@ -16,7 +16,7 @@
@@ -16,7 +16,7 @@
"jsonwebtoken": "^9.0.2",
"jsonwebtoken": "^9.0.2",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.79.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.79.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.25.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.25.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.0.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.2.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
},
},
"devDependencies": {
"devDependencies": {
@@ -1162,9 +1162,9 @@
@@ -1162,9 +1162,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"node_modules/openvibe-shared": {
- "version": "2.0.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.0.0",
- "integrity": "sha512-n5CHC0hfxOo0jo/3kS4O3KiwhirhZDlx5zZTIuPyIf3Dq0yI/R6R64btx7NYwZe5dI0NjvqzEtnv19m8unwLMg==",
+ "version": "2.2.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.2.0",
+ "integrity": "sha512-1q+iUuUxzPeylTYmithW4jR++Jccvn5GEOrB1B3D18Dgn6OXQhPpvhPQAa1D1k1p+dWIa1/SmggyJwSGFnTNtw==",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0"
"jsonwebtoken": "^9.0.0"
@@ -28,7 +28,7 @@
@@ -28,7 +28,7 @@
"jsonwebtoken": "^9.0.2",
"jsonwebtoken": "^9.0.2",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.79.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.79.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.25.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.25.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.0.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.2.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
},
},
"engines": {
"engines": {
@@ -2,7 +2,7 @@
@@ -2,7 +2,7 @@
(function () {
(function () {
'use strict';
'use strict';
document.documentElement.classList.add('js');
document.documentElement.classList.add('js');
- document.addEventListener('DOMContentLoaded', function () {
+ function wire() {
var form = document.querySelector('.tip-form');
var form = document.querySelector('.tip-form');
if (!form) return;
if (!form) return;
function sync() {
function sync() {
@@ -11,5 +11,7 @@
@@ -11,5 +11,7 @@
}
}
form.addEventListener('change', sync);
form.addEventListener('change', sync);
sync();
sync();
- });
+ }
+ document.addEventListener('DOMContentLoaded', wire);
+ document.addEventListener('ov:boost:load', wire); // a swapped-in page's tip form wires the same way
})();
})();
@@ -57,6 +57,7 @@ function createApp(opts = {}) {
@@ -57,6 +57,7 @@ function createApp(opts = {}) {
const domain = createDomain({ db, config, outbox, billing, adapters, valkey, now, log });
const domain = createDomain({ db, config, outbox, billing, adapters, valkey, now, log });
const apiAuth = createApiAuth({ config, keys, userAuth });
const apiAuth = createApiAuth({ config, keys, userAuth });
const release = require('openvibe-shared/release').createRelease({ service: 'tips', root: path.join(__dirname, '..') });
const release = require('openvibe-shared/release').createRelease({ service: 'tips', root: path.join(__dirname, '..') });
+ require('./web/layout').setRelease(release.release);
const layout = createLayout({ config, release });
const layout = createLayout({ config, release });
const app = express();
const app = express();
@@ -31,6 +31,11 @@ function asset(rel) {
@@ -31,6 +31,11 @@ function asset(rel) {
}
}
const assetVersion = (rel) => { asset(rel); return _hashes.get(rel); };
const assetVersion = (rel) => { asset(rel); return _hashes.get(rel); };
+// The deployed release (app.js sets it from openvibe-shared/release): openvibe-shared/boost swaps a page in place only
+// between pages of the same release, and does a normal load across a deploy.
+let RELEASE = 'dev';
+function setRelease(id) { if (id) RELEASE = String(id); }
+
const NAV_LINKS = [
const NAV_LINKS = [
{ label: 'Dashboard', href: '/dashboard' },
{ label: 'Dashboard', href: '/dashboard' },
{ label: 'Receipts', href: '/receipts' },
{ label: 'Receipts', href: '/receipts' },
@@ -54,7 +59,7 @@ function createLayout({ config, release }) {
@@ -54,7 +59,7 @@ function createLayout({ config, release }) {
history: { type: 'page', title: o.title || SITE_NAME },
history: { type: 'page', title: o.title || SITE_NAME },
silentLogin: `${config.baseUrl}/auth/login?silent=1&next={url}`,
silentLogin: `${config.baseUrl}/auth/login?silent=1&next={url}`,
sessionUrl: '/auth/me',
sessionUrl: '/auth/me',
- loginUrl: `/auth/login?next=${encodeURIComponent(o.canonicalPath || '/')}`,
+ loginUrl: '/auth/login?next={path}', // filled from the current page (boost moves between pages)
logoutUrl: '/auth/logout?next={path}', // Sign out in the shared navbar ends this site's session too
logoutUrl: '/auth/logout?next={path}', // Sign out in the shared navbar ends this site's session too
notificationsRealtime: true, // the bell hears new notifications over OpenVibe.Events (Shared 1.22.0)
notificationsRealtime: true, // the bell hears new notifications over OpenVibe.Events (Shared 1.22.0)
};
};
@@ -88,6 +93,8 @@ ${jsonLd}
@@ -88,6 +93,8 @@ ${jsonLd}
<script src="${ovServe.url('navbar.js')}" defer></script>
<script src="${ovServe.url('navbar.js')}" defer></script>
<script src="${ovServe.url('footer.js')}" defer></script>
<script src="${ovServe.url('footer.js')}" defer></script>
<script src="${asset('js/tips.js')}" defer></script>
<script src="${asset('js/tips.js')}" defer></script>
+<meta name="ov-boost" content="tips@${esc(RELEASE)}">
+<script src="${ovServe.url('boost.js')}" data-main="#main" defer></script>
</head>
</head>
<body>
<body>
<div id="navbar-mount"></div>
<div id="navbar-mount"></div>
@@ -112,4 +119,4 @@ document.addEventListener('DOMContentLoaded', function () {
@@ -112,4 +119,4 @@ document.addEventListener('DOMContentLoaded', function () {
return { page, abs };
return { page, abs };
}
}
-module.exports = { createLayout, esc, asset, assetVersion, SITE_NAME, DEFAULT_DESCRIPTION };
+module.exports = { createLayout, setRelease, esc, asset, assetVersion, SITE_NAME, DEFAULT_DESCRIPTION };
@@ -56,8 +56,20 @@ async function testDb({ store = process.env.TIPS_TEST_STORE || 'pglite', max = 4
@@ -56,8 +56,20 @@ async function testDb({ store = process.env.TIPS_TEST_STORE || 'pglite', max = 4
// PgBouncer keeps its server connections after the client pool closes: end them here, or
// PgBouncer keeps its server connections after the client pool closes: end them here, or
// every run would leave its roles' idle connections holding the server's slots.
// every run would leave its roles' idle connections holding the server's slots.
await su.query('SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE usename = ANY($1)', [[name, owner]]);
await su.query('SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE usename = ANY($1)', [[name, owner]]);
- await su.query(`DROP SCHEMA IF EXISTS ${name} CASCADE`);
- for (const r of [name, owner]) { await su.query(`DROP OWNED BY ${r}`); await su.query(`DROP ROLE ${r}`); }
+ // Terminating is asynchronous: wait until the sessions are gone, then retry catalog DDL that races another
+ // session's catalog write ('tuple concurrently updated', XX000) a few times.
+ for (let i = 0; i < 100; i++) {
+ const left = await su.query('SELECT count(*)::int AS n FROM pg_stat_activity WHERE usename = ANY($1)', [[name, owner]]);
+ if (!Number(left.rows[0].n)) break;
+ await new Promise((ok) => setTimeout(ok, 50));
+ }
+ const ddl = async (stmt) => {
+ for (let i = 0; ; i++) {
+ try { return await su.query(stmt); } catch (err) { if (err.code !== 'XX000' || i >= 5) throw err; await new Promise((ok) => setTimeout(ok, 100 * (i + 1))); }
+ }
+ };
+ await ddl(`DROP SCHEMA IF EXISTS ${name} CASCADE`);
+ for (const r of [name, owner]) { await ddl(`DROP OWNED BY ${r}`); await ddl(`DROP ROLE ${r}`); }
} finally { await su.close(); }
} finally { await su.close(); }
}
}
let db;
let db;
@@ -230,7 +230,8 @@ const validModerated = payloadValidator('tips.interaction.moderated');
@@ -230,7 +230,8 @@ const validModerated = payloadValidator('tips.interaction.moderated');
const id = r.json.interaction.id;
const id = r.json.interaction.id;
assert.strictEqual((await t.call('POST', `/api/v1/interactions/${id}/hide`, { user: alex })).status, 200);
assert.strictEqual((await t.call('POST', `/api/v1/interactions/${id}/hide`, { user: alex })).status, 200);
await t.deliver(billing.settlePurchase((await domain.interactions.get(t.db, id)).billing_intent_id));
await t.deliver(billing.settlePurchase((await domain.interactions.get(t.db, id)).billing_intent_id));
- await new Promise((ok) => setTimeout(ok, 50));
+ // The settlement reaches the row through the delivery; wait for it (bounded) instead of a fixed sleep.
+ for (let k = 0; k < 100 && (await domain.interactions.get(t.db, id)).payment_state === 'pending'; k++) await new Promise((ok) => setTimeout(ok, 20));
await domain.interactions.processDueTransfers();
await domain.interactions.processDueTransfers();
const i = await domain.interactions.get(t.db, id);
const i = await domain.interactions.get(t.db, id);
assert.strictEqual(i.payment_state, 'settled');
assert.strictEqual(i.payment_state, 'settled');
@@ -33,6 +33,16 @@ const { boot, check, done } = require('./helpers/app');
@@ -33,6 +33,16 @@ const { boot, check, done } = require('./helpers/app');
assert.ok(!r.text.includes('href="/bob"'), 'switched-off pages are not listed');
assert.ok(!r.text.includes('href="/bob"'), 'switched-off pages are not listed');
});
});
+ await check('every page carries the boost marker and script, and the navbar signs in back to the current page', async () => {
+ for (const [p, u] of [['/', null], ['/alex', viewer], ['/dashboard', alex], ['/receipts', viewer], ['/updates', alex]]) {
+ const r = await get(p, u);
+ assert.strictEqual(r.status, 200, p);
+ assert.match(r.text, /<meta name="ov-boost" content="tips@[^"]+">/, p);
+ assert.match(r.text, /<script src="\/shared\/boost\.js\?v=[0-9a-f]{12}" data-main="#main" defer><\/script>/, p);
+ assert.match(r.text, /"loginUrl":"\/auth\/login\?next=\{path\}"/, p);
+ }
+ });
+
await check('creator page: indexable when on, goals and a no-JS tip form', async () => {
await check('creator page: indexable when on, goals and a no-JS tip form', async () => {
const r = await get('/alex');
const r = await get('/alex');
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);