You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
114 lines
6.7 KiB
114 lines
6.7 KiB
// Tests of drand.ts: the release of a round from the public relays, raced,
|
|
// with a fetch that stands for the relays.
|
|
|
|
import { describe, expect, it } from 'vitest';
|
|
import { fetchRelease, RELAYS } from './drand.ts';
|
|
|
|
const CHAIN = '52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971';
|
|
// The release of round 32668196, as api.drand.sh gave it.
|
|
const SIG = '8d7fdb1526df070026a1da478690f84e39daf609407f9516096da4ed2ae43641f250b8fc470bebc0de133b8d0a76e38a';
|
|
const RANDOMNESS = '46b86c3aaf32c9f0695ec2cdf6fa4b77812791917ef74e6544b8473a2b9c6315';
|
|
const ROUND = 32668196;
|
|
|
|
type Answer = Response | Error | 'hang';
|
|
|
|
// A fetch that answers each relay as `answers` says, and records what it was asked.
|
|
function relays(answers: Record<string, Answer>, asked: string[] = []): typeof fetch {
|
|
return ((url: string, init?: RequestInit) => {
|
|
asked.push(url);
|
|
const a = answers[new URL(url).host]!;
|
|
if (a === 'hang') {
|
|
return new Promise((_, reject) => init!.signal!.addEventListener('abort', () => reject(init!.signal!.reason)));
|
|
}
|
|
return a instanceof Error ? Promise.reject(a) : Promise.resolve(a);
|
|
}) as typeof fetch;
|
|
}
|
|
|
|
const json = (body: unknown, status = 200) => new Response(JSON.stringify(body), { status });
|
|
const good = () => json({ round: ROUND, signature: SIG, randomness: RANDOMNESS });
|
|
|
|
describe('fetchRelease', () => {
|
|
it('asks every relay for the round of the chain, and takes the first release that holds together', async () => {
|
|
const asked: string[] = [];
|
|
const r = await fetchRelease(CHAIN, ROUND, relays({ 'api.drand.sh': new TypeError('offline'), 'api2.drand.sh': good(), 'api3.drand.sh': 'hang' }, asked));
|
|
expect(r).toEqual({ round: ROUND, signature: SIG, relay: 'https://api2.drand.sh' });
|
|
expect(asked).toEqual(RELAYS.map((h) => `${h}/v2/chains/${CHAIN}/rounds/${ROUND}`));
|
|
const noRandomness = await fetchRelease(CHAIN, ROUND, relays({ 'api.drand.sh': json({ round: ROUND, signature: SIG.toUpperCase() }), 'api2.drand.sh': 'hang', 'api3.drand.sh': 'hang' }));
|
|
expect(noRandomness.signature).toBe(SIG);
|
|
});
|
|
|
|
it('says when drand has not published the round yet', async () => {
|
|
const notYet = () => new Response('not found', { status: 404 });
|
|
await expect(fetchRelease(CHAIN, ROUND, relays({ 'api.drand.sh': notYet(), 'api2.drand.sh': notYet(), 'api3.drand.sh': notYet() }))).rejects.toThrow(
|
|
`drand aún no ha publicado la firma de la ronda ${ROUND}.`,
|
|
);
|
|
});
|
|
|
|
it('names what went wrong with each relay', async () => {
|
|
const answers: Record<string, Answer> = {
|
|
'api.drand.sh': new Response('nope', { status: 500 }),
|
|
'api2.drand.sh': new Response('<html>', { status: 200 }),
|
|
'api3.drand.sh': json({ round: ROUND + 1, signature: SIG }),
|
|
};
|
|
await expect(fetchRelease(CHAIN, ROUND, relays(answers))).rejects.toThrow(
|
|
`Ningún relay de drand dio la firma: api.drand.sh respondió HTTP 500; api2.drand.sh respondió algo que no es una firma; api3.drand.sh dio la ronda ${ROUND + 1}, no la ${ROUND}.`,
|
|
);
|
|
const worse: Record<string, Answer> = {
|
|
'api.drand.sh': json({ round: ROUND, signature: 'xyz' }),
|
|
'api2.drand.sh': json({ round: ROUND, signature: SIG, randomness: '00' }),
|
|
'api3.drand.sh': new Response('x'.repeat(9000), { status: 200 }),
|
|
};
|
|
await expect(fetchRelease(CHAIN, ROUND, relays(worse))).rejects.toThrow(
|
|
'Ningún relay de drand dio la firma: api.drand.sh dio una firma que no es hexadecimal; api2.drand.sh dio una aleatoriedad que no es la de su firma; api3.drand.sh respondió demasiado.',
|
|
);
|
|
const odd: Record<string, Answer> = {
|
|
'api.drand.sh': new Response(null, { status: 200 }),
|
|
'api2.drand.sh': json(null),
|
|
'api3.drand.sh': new TypeError('blocked'),
|
|
};
|
|
await expect(fetchRelease(CHAIN, ROUND, relays(odd))).rejects.toThrow(
|
|
'Ningún relay de drand dio la firma: api.drand.sh respondió algo que no es una firma; api2.drand.sh respondió algo que no es una firma; api3.drand.sh no se pudo conectar.',
|
|
);
|
|
});
|
|
|
|
// Spec v0.16, §47.1: the client of the reference reads the answers of the relays with the strict reader of drand's
|
|
// JSON, as a release that the caller gives: a name twice is malformed, and ROUND is another name, ignored.
|
|
it('reads an answer with the strict reader of drand JSON', async () => {
|
|
const text = (body: string): Response => new Response(body, { status: 200 });
|
|
const answers: Record<string, Answer> = {
|
|
'api.drand.sh': text(`{"round":${ROUND},"round":${ROUND},"signature":"${SIG}"}`),
|
|
'api2.drand.sh': text(`{"round":${ROUND},"signature":""}`),
|
|
'api3.drand.sh': text(`{"round":${ROUND},"signature":"${SIG}","randomness":null}`),
|
|
};
|
|
await expect(fetchRelease(CHAIN, ROUND, relays(answers))).rejects.toThrow(
|
|
'Ningún relay de drand dio la firma: api.drand.sh respondió algo que no es una firma; api2.drand.sh dio una firma que no es hexadecimal; api3.drand.sh respondió algo que no es una firma.',
|
|
);
|
|
const other = await fetchRelease(
|
|
CHAIN,
|
|
ROUND,
|
|
relays({ 'api.drand.sh': text(`{"\\u0072ound":${ROUND},"ROUND":${ROUND + 1},"signature":"${SIG}"}`), 'api2.drand.sh': 'hang', 'api3.drand.sh': 'hang' }),
|
|
);
|
|
expect(other).toEqual({ round: ROUND, signature: SIG, relay: 'https://api.drand.sh' });
|
|
});
|
|
|
|
// The client of the reference reads the body with provider.ParseDrandJSON, for which a byte order mark before the JSON
|
|
// is not a space of JSON.
|
|
it('refuses an answer that starts with a byte order mark, as the client of the reference does', async () => {
|
|
const body = new TextEncoder().encode(JSON.stringify({ round: ROUND, signature: SIG, randomness: RANDOMNESS }));
|
|
const bom = new Response(new Uint8Array([0xef, 0xbb, 0xbf, ...body]), { status: 200 });
|
|
const offline = (): Error => new TypeError('offline');
|
|
await expect(fetchRelease(CHAIN, ROUND, relays({ 'api.drand.sh': bom, 'api2.drand.sh': offline(), 'api3.drand.sh': offline() }))).rejects.toThrow(
|
|
'Ningún relay de drand dio la firma: api.drand.sh respondió algo que no es una firma; api2.drand.sh no se pudo conectar; api3.drand.sh no se pudo conectar.',
|
|
);
|
|
});
|
|
|
|
it('gives up after its timeout', async () => {
|
|
await expect(fetchRelease(CHAIN, ROUND, relays({ 'api.drand.sh': 'hang', 'api2.drand.sh': 'hang', 'api3.drand.sh': 'hang' }), 20)).rejects.toThrow(
|
|
'Ningún relay de drand dio la firma: api.drand.sh no contestó a tiempo; api2.drand.sh no contestó a tiempo; api3.drand.sh no contestó a tiempo.',
|
|
);
|
|
const late = ((_: string, init?: RequestInit) =>
|
|
new Promise((_, reject) => init!.signal!.addEventListener('abort', () => reject(new DOMException('aborted', 'AbortError'))))) as typeof fetch;
|
|
await expect(fetchRelease(CHAIN, ROUND, late, 20)).rejects.toThrow('api.drand.sh no contestó a tiempo');
|
|
});
|
|
});
|