QA: end-to-end join-flow harness proving the post-registration sign-in, including the refuse regression
qa/harness-server.js boots the real server with chain reads stubbed (COLD=1 reproduces the cached-index state that left #787 without a profile). qa/join-flow-e2e.mjs drives the real /join-now page with a fake wallet producing genuine secp256k1 signatures. Three scenarios pass: signs on a cold index (9) - the gate appears on their own dashboard; refuses to sign (10) - REGRESSION, the join still completes and redirects with the page usable; signs on a warm index (9). Full set green: profiles-unit 28, signin-fallback 7, gate-e2e 47, join-flow 9/10/9, plus the live shared-link check. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
// End-to-end QA of the REAL join flow, with a fake wallet that produces genuine
|
||||
// secp256k1 signatures, against a server whose chain reads are stubbed cold —
|
||||
// exactly the state that left #787 without a profile.
|
||||
//
|
||||
// Two runs matter:
|
||||
// SCENARIO=sign member approves the signature -> profile gate must appear
|
||||
// SCENARIO=refuse member rejects the signature -> the JOIN MUST STILL COMPLETE
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { createRequire } from 'node:module';
|
||||
import crypto from 'node:crypto';
|
||||
const ROOT = 'D:/Projects/HighRisk/The RM Circle/promos/';
|
||||
const require = createRequire(ROOT + 'package.json');
|
||||
const secp = require(ROOT + 'vendor/secp256k1.js');
|
||||
const { keccak256 } = require(ROOT + 'vendor/sha3.js');
|
||||
secp.utils.hmacSha256Sync = (key, ...msgs) => {
|
||||
const h = crypto.createHmac('sha256', Buffer.from(key));
|
||||
msgs.forEach(m => h.update(Buffer.from(m)));
|
||||
return Uint8Array.from(h.digest());
|
||||
};
|
||||
const PW = 'D:/Projects/MarketingAgent/qa-tester/node_modules/playwright';
|
||||
const { chromium } = (await import(pathToFileURL(PW + '/index.js').href)).default;
|
||||
|
||||
const B = process.env.LOCAL || 'http://127.0.0.1:3399';
|
||||
const SCENARIO = process.env.SCENARIO || 'sign';
|
||||
const PRIV = Buffer.from(process.env.TEST_PRIV, 'hex');
|
||||
const ADDR = process.env.TEST_ADDR;
|
||||
const NEW_ID = Number(process.env.TEST_ID || 9001);
|
||||
const CONTRACT = '0x33bdaeefd6d17d80ae53816c916dfb26c4fb2daf';
|
||||
const T_REGISTERED = '0xe4a74887d749eb048f14bfef37b204477f3a5ff67055908b7c8cc62c202aef17';
|
||||
|
||||
const ok = [], bad = [];
|
||||
const t = (n, c, extra) => { (c ? ok : bad).push(n + (c || !extra ? '' : ' -> ' + extra)); };
|
||||
|
||||
const browser = await chromium.launch();
|
||||
const ctx = await browser.newContext({ viewport: { width: 1280, height: 950 } });
|
||||
const page = await ctx.newPage();
|
||||
|
||||
// real signing, called from the page (exposeFunction sidesteps the page's strict CSP)
|
||||
await page.exposeFunction('__qaSign', async (msg) => {
|
||||
const m = Buffer.from(msg, 'utf8');
|
||||
const pre = Buffer.from('\x19Ethereum Signed Message:\n' + m.length, 'utf8');
|
||||
const digest = Buffer.from(keccak256(Buffer.concat([pre, m])), 'hex');
|
||||
const [sig, rec] = secp.signSync(digest, PRIV, { recovered: true, der: false });
|
||||
return '0x' + Buffer.from(sig).toString('hex') + (27 + rec).toString(16).padStart(2, '0');
|
||||
});
|
||||
|
||||
await page.addInitScript(({ addr, newId, contract, topic, scenario }) => {
|
||||
const TX = '0x' + 'ab'.repeat(32);
|
||||
let sent = false;
|
||||
// counters live in sessionStorage so they survive the redirect to /my/<id>
|
||||
const bump = (k) => { try { const s = JSON.parse(sessionStorage.getItem('__qa') || '{}'); s[k] = (s[k] || 0) + 1; sessionStorage.setItem('__qa', JSON.stringify(s)); } catch (e) {} };
|
||||
window.__qaRead = () => { try { return JSON.parse(sessionStorage.getItem('__qa') || '{}'); } catch (e) { return {}; } };
|
||||
window.ethereum = {
|
||||
isMetaMask: true,
|
||||
request: async ({ method, params }) => {
|
||||
switch (method) {
|
||||
case 'eth_requestAccounts': case 'eth_accounts': return [addr];
|
||||
case 'eth_chainId': return '0x89';
|
||||
case 'eth_getBalance': return '0x' + (10n ** 21n).toString(16); // 1000 POL, plenty
|
||||
case 'eth_call': throw new Error('no node in the harness'); // page falls back to its constant
|
||||
case 'eth_sendTransaction': bump('txSent'); sent = true; return TX;
|
||||
case 'eth_getTransactionReceipt':
|
||||
if (!sent) return null;
|
||||
return { status: '0x1', transactionHash: TX, logs: [{ address: contract, topics: [topic, '0x' + newId.toString(16).padStart(64, '0')], data: '0x' }] };
|
||||
case 'personal_sign': {
|
||||
bump('signAsked');
|
||||
if (scenario === 'refuse') { bump('signRefused'); const e = new Error('User rejected the request.'); e.code = 4001; throw e; }
|
||||
const hex = String(params[0]).replace(/^0x/, '');
|
||||
let s = ''; for (let i = 0; i < hex.length; i += 2) s += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
|
||||
return await window.__qaSign(s);
|
||||
}
|
||||
case 'wallet_switchEthereumChain': case 'wallet_addEthereumChain': return null;
|
||||
default: return null;
|
||||
}
|
||||
},
|
||||
on: () => {}, removeListener: () => {}
|
||||
};
|
||||
}, { addr: ADDR, newId: NEW_ID, contract: CONTRACT, topic: T_REGISTERED, scenario: SCENARIO });
|
||||
|
||||
const errs = [];
|
||||
page.on('console', m => { if (m.type() === 'error') errs.push(m.text().slice(0, 120)); });
|
||||
page.on('pageerror', e => errs.push('PAGEERROR ' + String(e.message).slice(0, 120)));
|
||||
|
||||
await page.goto(B + '/join-now?ref=21&direct=1', { waitUntil: 'networkidle', timeout: 60000 });
|
||||
await page.waitForTimeout(1500);
|
||||
t('join page loads', await page.evaluate(() => !!document.getElementById('joinBtn')));
|
||||
|
||||
// the page auto-connects when the wallet already reports an account, which hides #connectBtn
|
||||
const needsConnect = await page.evaluate(() => { const b = document.getElementById('connectBtn'); return !!b && getComputedStyle(b).display !== 'none'; });
|
||||
if (needsConnect) { await page.click('#connectBtn'); }
|
||||
await page.waitForTimeout(2000);
|
||||
t('wallet connects', await page.evaluate(() => { const w = document.getElementById('wallet'); return !!(w && w.textContent && w.textContent.length > 6); }),
|
||||
await page.evaluate(() => { const w = document.getElementById('wallet'); return w ? w.textContent : 'no #wallet'; }));
|
||||
|
||||
await page.click('#joinBtn');
|
||||
// registration + report + (maybe) signature + redirect
|
||||
await page.waitForURL(u => /\/my\//.test(u.toString()), { timeout: 60000 }).catch(() => {});
|
||||
await page.waitForTimeout(3500);
|
||||
|
||||
const qa = await page.evaluate(() => (window.__qaRead ? window.__qaRead() : {})).catch(() => ({}));
|
||||
t('the registration transaction was sent', (qa.txSent || 0) >= 1 || /\/my\//.test(page.url()), JSON.stringify(qa));
|
||||
t('the member lands on their position page', page.url().includes('/my/'), page.url());
|
||||
|
||||
const cookies = await ctx.cookies();
|
||||
const hasSession = cookies.some(c => c.name === 'ctb.msid');
|
||||
|
||||
if (SCENARIO === 'sign') {
|
||||
t('a signature was requested right after the join', (qa.signAsked || 0) >= 1, JSON.stringify(qa));
|
||||
t('an inbox session was created', hasSession, cookies.map(c => c.name).join(','));
|
||||
await page.waitForTimeout(2500);
|
||||
const gate = await page.evaluate(() => !!document.querySelector('#pgCard'));
|
||||
t('the profile gate appears on their dashboard', gate);
|
||||
const card = await page.evaluate(() => { const c = document.querySelector('#pgCard'); return c ? c.innerText.replace(/\s+/g, ' ') : ''; });
|
||||
t('the gate is for THEIR position', new RegExp('position #' + NEW_ID, 'i').test(card), card.slice(0, 90));
|
||||
} else {
|
||||
t('the signature was refused by the wallet', (qa.signRefused || 0) >= 1, JSON.stringify(qa));
|
||||
t('REGRESSION: the join still completed and redirected', /\/my\//.test(page.url()), page.url());
|
||||
t('no inbox session, as expected', !hasSession);
|
||||
const gate = await page.evaluate(() => !!document.querySelector('#pgCard'));
|
||||
t('no gate without a session, page still usable', !gate);
|
||||
const txt = await page.evaluate(() => document.body.innerText);
|
||||
t('the dashboard still renders for them', txt.length > 300, 'len ' + txt.length);
|
||||
}
|
||||
|
||||
const fatal = errs.filter(e => /PAGEERROR/.test(e));
|
||||
t('no uncaught page errors', fatal.length === 0, fatal.join(' | '));
|
||||
|
||||
console.log('[' + SCENARIO + '] PASS ' + ok.length);
|
||||
for (const b of bad) console.log('[' + SCENARIO + '] FAIL ' + b);
|
||||
if (errs.length) console.log(' console noise:', errs.slice(0, 3));
|
||||
await browser.close();
|
||||
process.exit(bad.length ? 1 : 0);
|
||||
Reference in New Issue
Block a user