forked from yvv/decision
v3 : vote par la chaîne, corrections de la relecture de 74f96be (interface)
- rien n'est parti (nœud muet avant la signature, refus, fenêtre fermée avant) : la page lâche le bulletin ; signé, ou montré en code Ğecko, il est gardé et le serveur le confirme sans la page ; le refus de la chaîne se dit (« chain ») ; - le reçu : téléchargeable depuis la fenêtre de vote, et la page « Tes bulletins » (/bulletins) qui garde l'historique et vérifie un reçu dans le navigateur, sans le serveur (lib/receipt.ts, mêmes vecteurs que le moteur) ; - le panier ne dit plus « Tes votes comptent dès l'envoi » quand le vote est fermé ; - @noble/hashes déclaré (utilisé par e2e/chain-mock.mjs et le reçu) ; - chaîne simulée : finalité, pages, filtres, nœud qui tombe ; contrôles C32, C33. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
// What the end-to-end tests import is declared: a package found only as the
|
||||
// dependency of another (as @noble/hashes under polkadot-api) may vanish with it.
|
||||
import { readdirSync, readFileSync } from 'node:fs';
|
||||
import { builtinModules } from 'node:module';
|
||||
import { join } from 'node:path';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const root = join(__dirname, '../..');
|
||||
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) as {
|
||||
dependencies?: Record<string, string>;
|
||||
devDependencies?: Record<string, string>;
|
||||
};
|
||||
const declared = new Set([...Object.keys(pkg.dependencies ?? {}), ...Object.keys(pkg.devDependencies ?? {})]);
|
||||
|
||||
/** `@scope/name/sub.js` → `@scope/name`; `name/sub` → `name`. */
|
||||
const packageOf = (spec: string) => spec.split('/').slice(0, spec.startsWith('@') ? 2 : 1).join('/');
|
||||
|
||||
describe('the end-to-end tests', () => {
|
||||
it('import only declared packages', () => {
|
||||
const dir = join(root, 'e2e');
|
||||
for (const file of readdirSync(dir).filter((f) => /\.(mjs|ts)$/.test(f))) {
|
||||
const text = readFileSync(join(dir, file), 'utf8');
|
||||
for (const [, spec] of text.matchAll(/^import[^'"]*['"]([^'"]+)['"]/gm)) {
|
||||
if (!spec || spec.startsWith('.') || spec.startsWith('node:') || builtinModules.includes(spec)) continue;
|
||||
expect(declared.has(packageOf(spec)), `${file}: ${spec}`).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
// The receipt of a vote on the Ğ1 chain, checked in the browser: the same
|
||||
// canonical text and the same seal as the engine (crates/ld-engine/src/ballot.rs,
|
||||
// whose vectors were computed apart with Python's hashlib.blake2b).
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { canonical, checkReceipt, seal } from '../../src/lib/receipt';
|
||||
|
||||
const SALT = '00112233445566778899aabbccddeeff';
|
||||
const LINES = [
|
||||
{ proposalId: 'p2', revision: 1, choice: 'against' as const },
|
||||
{ proposalId: 'p1', revision: 3, choice: 'for' as const },
|
||||
];
|
||||
|
||||
describe('the sealed ballot', () => {
|
||||
it('reads as the engine writes it, the lines in the order of their versions', () => {
|
||||
expect(canonical('d0c', null, LINES, SALT)).toBe(
|
||||
'ld1\ndoc d0c\nauthor -\nline p1 3 for\nline p2 1 against\nsalt 00112233445566778899aabbccddeeff\n',
|
||||
);
|
||||
});
|
||||
|
||||
it('seals to the remark of the engine', () => {
|
||||
const s = seal('d0c', null, LINES, SALT);
|
||||
expect(s?.digest).toBe('20448d496744292981c8586856afb58a86e37da2470f764b4e6ed84eade71340');
|
||||
expect(s?.comment).toBe('ld1:20448d496744292981c8586856afb58a86e37da2470f764b4e6ed84eade71340');
|
||||
expect(s?.remarkHash).toBe('0xe765221a75716dde863a7b53df3a97acf6db38a2118edd525d54c8a2979b7bc3');
|
||||
const bound = seal('d0c', 'g1NrqKgJW9FoFabaYW82zhS9tDWdQcL1DGpivaJdj7GW9M6iv', [{ proposalId: 'p1', revision: 3, choice: 'none' }], 'ffeeddccbbaa99887766554433221100');
|
||||
expect(bound?.digest).toBe('6a6d8a90b5003ff3c4d050dbd36199e8bbf34c4cc11620618399226c9237c05e');
|
||||
expect(bound?.remarkHash).toBe('0x846495fe62b9a5184a0d3090d4d26f621887c334890404169c00575ee74540e4');
|
||||
});
|
||||
|
||||
it('what cannot be sealed', () => {
|
||||
expect(seal('d0c', null, [], SALT)).toBeNull();
|
||||
expect(seal('d0c', null, [LINES[0]!, { ...LINES[0]!, revision: 2 }], SALT)).toBeNull();
|
||||
expect(seal('d 0c', null, LINES, SALT)).toBeNull();
|
||||
expect(seal('d0c', null, LINES, SALT.toUpperCase())).toBeNull();
|
||||
expect(seal('d0c', null, LINES, '0011')).toBeNull();
|
||||
expect(seal('d0c', null, [{ proposalId: 'p1', revision: 1.5, choice: 'for' }], SALT)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('a receipt', () => {
|
||||
const receipt = {
|
||||
format: 'ld1',
|
||||
doc: 'd0c',
|
||||
author: null,
|
||||
lines: LINES.map((l) => ({ ...l, article: 'E1', title: 'Unicité du compte' })),
|
||||
salt: SALT,
|
||||
comment: 'ld1:20448d496744292981c8586856afb58a86e37da2470f764b4e6ed84eade71340',
|
||||
block: 3_400_000,
|
||||
};
|
||||
|
||||
it('holds when its lines and its salt give its comment', () => {
|
||||
expect(checkReceipt(JSON.stringify(receipt))).toEqual({
|
||||
kind: 'ok',
|
||||
comment: receipt.comment,
|
||||
remarkHash: '0xe765221a75716dde863a7b53df3a97acf6db38a2118edd525d54c8a2979b7bc3',
|
||||
block: 3_400_000,
|
||||
lines: receipt.lines,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not hold once a choice is changed', () => {
|
||||
const changed = { ...receipt, lines: [{ ...receipt.lines[0]!, choice: 'for' }, receipt.lines[1]!] };
|
||||
const check = checkReceipt(JSON.stringify(changed));
|
||||
expect(check.kind).toBe('mismatch');
|
||||
if (check.kind === 'mismatch') expect(check.computed).not.toBe(receipt.comment);
|
||||
});
|
||||
|
||||
it('what is not a receipt', () => {
|
||||
for (const text of ['', 'nope', '[]', '{}', JSON.stringify({ ...receipt, format: 'ld2' }), JSON.stringify({ ...receipt, lines: 'x' })]) {
|
||||
expect(checkReceipt(text).kind, text).toBe('unreadable');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { amountText, geckoLink } from '../../src/lib/wallet/gecko';
|
||||
import { failure, injectedNames, testExtrinsic } from '../../src/lib/wallet/injected';
|
||||
import { failure, injectedNames, mayLetGo, testExtrinsic } from '../../src/lib/wallet/injected';
|
||||
|
||||
const ACCOUNT = 'g1L5EYvvSBaYuQCPW78bHxYcvHfmWAqHYe3Gsrp9SV4kWZaEM';
|
||||
const COMMENT = `ld1:${'ab'.repeat(32)}`;
|
||||
@@ -30,6 +30,17 @@ describe('wallets of the page', () => {
|
||||
expect(failure({ type: 'Invalid', value: { type: 'Payment' } })).toBe('funds');
|
||||
expect(failure(new Error('Inability to pay some fees (e.g. account balance too low)'))).toBe('funds');
|
||||
expect(failure(new Error('socket hang up'))).toBe('wallet');
|
||||
// Refused by the pool of the node: it will never be included.
|
||||
expect(failure({ type: 'Invalid', value: { type: 'Stale' } })).toBe('chain');
|
||||
expect(failure({ code: 1010, message: 'Invalid Transaction' })).toBe('chain');
|
||||
});
|
||||
|
||||
it('a ballot is let go when nothing left the page, or when the chain said no — kept when it may still land', () => {
|
||||
// Before the signature (the node down before sending, a refusal, the window closed): nothing left.
|
||||
for (const f of ['node', 'wallet', 'cancelled', 'funds', 'account', 'chain'] as const) expect(mayLetGo(f, false), f).toBe(true);
|
||||
// Signed and handed to the node: it may reach the chain, unless the chain refused it.
|
||||
for (const f of ['funds', 'account', 'chain'] as const) expect(mayLetGo(f, true), f).toBe(true);
|
||||
for (const f of ['node', 'wallet'] as const) expect(mayLetGo(f, true), f).toBe(false);
|
||||
});
|
||||
|
||||
it('in tests, the fake node receives what was signed, readable', () => {
|
||||
|
||||
Reference in New Issue
Block a user