Files
securebit-chat/tests/qr-scan-single-frame.test.mjs
T
lockbitchat 556727eb6f
CodeQL Analysis / Analyze CodeQL (push) Canceled after 0s
Deploy Application / deploy (push) Canceled after 0s
Mirror to Codeberg / mirror (push) Canceled after 0s
Mirror to PrivacyGuides / mirror (push) Canceled after 0s
fix(qr): accept a one-frame invitation; release v5.9.1
The scanner waited for four frames when shown a single one. Its chunk assembler
was written for SB1, which the generator always cuts into exactly four frames,
and its fallback branch claims any non-JSON string longer than 100 characters —
which a 151-character SBQ2 invitation is. A complete invitation was filed as
chunk 1 of 4, and the scan never finished.

SBQ2 payloads are now recognised as complete before any assembly runs, in both
the text and raw-byte forms, and the hard-coded frame count is marked as
belonging to SB1 so it is not read as a general rule.
2026-08-06 23:32:20 -04:00

72 lines
3.7 KiB
JavaScript

// An SBQ2 invitation is one QR frame. The scanner must treat it as complete on
// sight.
//
// This test exists because of a real bug. The chunk assembler in handleQRScan
// was written for SB1, which the generator always cuts into exactly four frames,
// so the expected count is hard-coded to 4. Its fallback branch claims *any*
// non-JSON string longer than 100 characters — and an `SB2:` payload is 151 to
// 170 characters. A single, complete invitation was therefore filed as chunk 1
// of 4, and the scanner sat waiting for three frames that do not exist.
//
// The assertions are structural rather than behavioural: handleQRScan lives
// inside a 5,900-line JSX component and cannot be invoked in isolation, but the
// ordering that makes it correct can still be pinned.
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
const src = readFileSync(new URL('../src/app.jsx', import.meta.url), 'utf8');
const scanIdx = src.indexOf('const handleQRScan = async (scannedData) => {');
assert.notEqual(scanIdx, -1, 'handleQRScan must exist in src/app.jsx');
// Everything from the start of the handler to the end of the file is enough:
// what matters is the order of the branches inside it.
const body = src.slice(scanIdx);
const sbq2Idx = body.indexOf("scannedData.startsWith('SB2:')");
assert.notEqual(sbq2Idx, -1, 'handleQRScan must recognise an SB2 payload');
// The raw-byte form must be recognised too — the two families never collide,
// since any SB1 payload begins with ASCII 'S'.
assert.ok(/scannedData\.charCodeAt\(0\) === 0x02/.test(body.slice(sbq2Idx, sbq2Idx + 400)),
'the raw-byte SBQ2 form must be recognised alongside the text form');
// The SBQ2 branch must come before BOTH chunk-assembly branches: the explicit
// SB1:bin: one and the "long non-JSON string" fallback that actually caught it.
const binChunkIdx = body.indexOf("scannedData.startsWith('SB1:bin:')");
const lengthHeuristicIdx = body.indexOf('scannedData.length > 100');
assert.notEqual(binChunkIdx, -1, 'the SB1 chunk branch must still exist');
assert.notEqual(lengthHeuristicIdx, -1, 'the long-string fallback must still exist');
assert.ok(sbq2Idx < binChunkIdx,
'SBQ2 must be handled before the SB1:bin chunk assembler');
assert.ok(sbq2Idx < lengthHeuristicIdx,
'SBQ2 must be handled before the "long non-JSON string" fallback, which a ' +
'151-character SB2 payload otherwise matches');
// The branch must terminate the scan rather than fall through into assembly.
const sbq2Branch = body.slice(sbq2Idx, binChunkIdx);
assert.ok(/setShowQRScannerModal\(false\)/.test(sbq2Branch),
'capturing a single-frame invitation must close the scanner');
assert.ok(/return Promise\.resolve\(true\)/.test(sbq2Branch),
'the SBQ2 branch must report completion, not "waiting for more"');
assert.ok(/qrChunksBufferRef\.current = \{ id: null/.test(sbq2Branch),
'any partial chunk buffer must be cleared, so a stray earlier frame cannot ' +
'be spliced onto a complete invitation');
// Both destinations must be fed, because the same scanner serves both steps.
assert.ok(/setAnswerInput\(scannedData\)/.test(sbq2Branch) &&
/setOfferInput\(scannedData\)/.test(sbq2Branch),
'the scanned code must reach the answer field on the offering side and the ' +
'offer field on the joining side');
// The hard-coded four must stay explicitly tied to SB1, so it is not read as a
// general rule the next time the format changes.
for (const m of body.matchAll(/total: 4,/g)) {
const context = body.slice(Math.max(0, m.index - 400), m.index);
assert.ok(/SB1/.test(context),
'a hard-coded frame count must say which format it belongs to');
}
console.log('qr-scan-single-frame: all assertions passed');