From 5bd255264109f0f6d6d6b184a1a2e061ecee8db8 Mon Sep 17 00:00:00 2001 From: Ruben Carlo Benante Date: Mon, 10 Aug 2026 14:30:18 -0300 Subject: [PATCH] test v0.1 --- src/config.test.ts | 110 +++++++++++++++++++++++++++++++++++++++++++++ src/format.test.ts | 94 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 src/config.test.ts create mode 100644 src/format.test.ts diff --git a/src/config.test.ts b/src/config.test.ts new file mode 100644 index 0000000..1722ce3 --- /dev/null +++ b/src/config.test.ts @@ -0,0 +1,110 @@ +// ************************************************************************* +// * (C)opyright 2026 by Ruben Carlo Benante * +// * * +// * This program is free software; you can redistribute it and/or modify * +// * it under the terms of the GNU General Public License as published by * +// * the Free Software Foundation, either version 3 of the License, or * +// * (at your option) any later version. * +// * * +// * This program is distributed in the hope that it will be useful, * +// * but WITHOUT ANY WARRANTY; without even the implied warranty of * +// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +// * GNU General Public License for more details. * +// * * +// * You should have received a copy of the GNU General Public License * +// * along with this program. If not, see http://www.gnu.org/licenses/. * +// * * +// * Contact author at: * +// * Ruben Carlo Benante * +// * rcb@beco.cc * +// ************************************************************************* + +import assert from 'node:assert/strict'; +import { test } from 'node:test'; + +import { + LOGOUT_COOLDOWN_MS, + SUBMIT_INTERVAL_MS, + decideSubmit, + isConfigured, + matchesSite, +} from './config.ts'; +import type { Config, State } from './config.ts'; + +const CONFIG: Config = { + url: 'https://portal.example.br/', + ra: '2000101010', + dn: '01/02/1999', + cpf: '123.456.789-01', + autoSubmit: true, +}; + +const FRESH: State = { lastSubmitAt: 0, logoutAt: 0 }; +const NOW = 1_000_000_000_000; + +test('isConfigured requires all four values', () => { + assert.equal(isConfigured(CONFIG), true); + assert.equal(isConfigured({ ...CONFIG, url: '' }), false); + assert.equal(isConfigured({ ...CONFIG, url: 'not a url' }), false); + assert.equal(isConfigured({ ...CONFIG, ra: '' }), false); + assert.equal(isConfigured({ ...CONFIG, dn: '' }), false); + assert.equal(isConfigured({ ...CONFIG, cpf: '' }), false); +}); + +test('matchesSite compares origins, not prefixes', () => { + assert.equal(matchesSite(CONFIG, 'https://portal.example.br/'), true); + assert.equal(matchesSite(CONFIG, 'https://portal.example.br/autenticacao/ap'), true); +}); + +test('matchesSite refuses lookalike hosts and scheme downgrades', () => { + // The content script runs on every page, so this guard is the only thing + // standing between the stored CPF and an attacker-chosen page. + assert.equal(matchesSite(CONFIG, 'https://portal.example.br.evil.tld/'), false); + assert.equal(matchesSite(CONFIG, 'https://evil.tld/portal.example.br'), false); + assert.equal(matchesSite(CONFIG, 'http://portal.example.br/'), false); + assert.equal(matchesSite(CONFIG, 'https://sub.portal.example.br/'), false); + assert.equal(matchesSite(CONFIG, 'about:blank'), false); +}); + +test('a fresh state submits', () => { + assert.deepEqual(decideSubmit(CONFIG, FRESH, NOW), { submit: true }); +}); + +test('the toggle wins over everything else', () => { + assert.deepEqual(decideSubmit({ ...CONFIG, autoSubmit: false }, FRESH, NOW), { + submit: false, + reason: 'disabled', + }); +}); + +test('one submit per hour, then autofill only', () => { + const justTried: State = { ...FRESH, lastSubmitAt: NOW - 1000 }; + assert.deepEqual(decideSubmit(CONFIG, justTried, NOW), { + submit: false, + reason: 'rate-limited', + }); + + const anHourAgo: State = { ...FRESH, lastSubmitAt: NOW - SUBMIT_INTERVAL_MS - 1 }; + assert.deepEqual(decideSubmit(CONFIG, anHourAgo, NOW), { submit: true }); +}); + +test('a recent logout suppresses the submit, and expires', () => { + const justLoggedOut: State = { ...FRESH, logoutAt: NOW - 1000 }; + assert.deepEqual(decideSubmit(CONFIG, justLoggedOut, NOW), { + submit: false, + reason: 'logout', + }); + + const staleLogout: State = { ...FRESH, logoutAt: NOW - LOGOUT_COOLDOWN_MS - 1 }; + assert.deepEqual(decideSubmit(CONFIG, staleLogout, NOW), { submit: true }); +}); + +test('logout is reported ahead of the rate limit', () => { + // Both apply after a login followed by a logout. The logout is the more + // useful explanation to show the user. + const both: State = { lastSubmitAt: NOW - 1000, logoutAt: NOW - 1000 }; + assert.deepEqual(decideSubmit(CONFIG, both, NOW), { + submit: false, + reason: 'logout', + }); +}); diff --git a/src/format.test.ts b/src/format.test.ts new file mode 100644 index 0000000..02a05ea --- /dev/null +++ b/src/format.test.ts @@ -0,0 +1,94 @@ +// ************************************************************************* +// * (C)opyright 2026 by Ruben Carlo Benante * +// * * +// * This program is free software; you can redistribute it and/or modify * +// * it under the terms of the GNU General Public License as published by * +// * the Free Software Foundation, either version 3 of the License, or * +// * (at your option) any later version. * +// * * +// * This program is distributed in the hope that it will be useful, * +// * but WITHOUT ANY WARRANTY; without even the implied warranty of * +// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +// * GNU General Public License for more details. * +// * * +// * You should have received a copy of the GNU General Public License * +// * along with this program. If not, see http://www.gnu.org/licenses/. * +// * * +// * Contact author at: * +// * Ruben Carlo Benante * +// * rcb@beco.cc * +// ************************************************************************* + +import assert from 'node:assert/strict'; +import { test } from 'node:test'; + +import { + formatCpf, + formatDate, + formatRa, + isCompleteCpf, + isCompleteDate, + toOrigin, +} from './format.ts'; + +test('formatDate builds dd/mm/yyyy progressively', () => { + assert.equal(formatDate('0'), '0'); + assert.equal(formatDate('01'), '01'); + assert.equal(formatDate('0102'), '01/02'); + assert.equal(formatDate('01021999'), '01/02/1999'); +}); + +test('formatDate is idempotent and tolerates a pasted separator', () => { + assert.equal(formatDate('01/02/1999'), '01/02/1999'); + assert.equal(formatDate(formatDate('01021999')), '01/02/1999'); + assert.equal(formatDate('1-2-1999'), '12/19/99'); +}); + +test('formatCpf builds 000.000.000-00 progressively', () => { + assert.equal(formatCpf('123'), '123'); + assert.equal(formatCpf('123456'), '123.456'); + assert.equal(formatCpf('12345678901'), '123.456.789-01'); + assert.equal(formatCpf('123.456.789-01'), '123.456.789-01'); +}); + +test('formatters drop overflow instead of growing without bound', () => { + assert.equal(formatCpf('123456789012345'), '123.456.789-01'); + assert.equal(formatRa('20001010109999'), '2000101010'); +}); + +test('isCompleteDate rejects impossible calendar dates', () => { + assert.equal(isCompleteDate('01/02/1999'), true); + assert.equal(isCompleteDate('29/02/2000'), true, 'leap year'); + assert.equal(isCompleteDate('29/02/1999'), false, 'not a leap year'); + assert.equal(isCompleteDate('31/04/1999'), false, 'April has 30 days'); + assert.equal(isCompleteDate('00/01/1999'), false); + assert.equal(isCompleteDate('01/13/1999'), false); + assert.equal(isCompleteDate('1/2/1999'), false, 'must be zero padded'); +}); + +test('isCompleteCpf counts digits, not characters', () => { + assert.equal(isCompleteCpf('123.456.789-01'), true); + assert.equal(isCompleteCpf('12345678901'), true); + assert.equal(isCompleteCpf('123.456.789-0'), false); +}); + +test('toOrigin assumes https and strips path, query and fragment', () => { + assert.equal(toOrigin('https://portal.example.br/'), 'https://portal.example.br'); + assert.equal(toOrigin('portal.example.br'), 'https://portal.example.br'); + assert.equal( + toOrigin('https://portal.example.br/login?a=1#x'), + 'https://portal.example.br', + ); + assert.equal(toOrigin(' portal.example.br '), 'https://portal.example.br'); +}); + +test('toOrigin keeps the port, which is part of the origin', () => { + assert.equal(toOrigin('http://localhost:8080/x'), 'http://localhost:8080'); +}); + +test('toOrigin rejects what cannot be a site', () => { + assert.equal(toOrigin(''), null); + assert.equal(toOrigin(' '), null); + assert.equal(toOrigin('javascript:alert(1)'), null); + assert.equal(toOrigin('file:///etc/passwd'), null); +});