test v0.1
This commit is contained in:
110
src/config.test.ts
Normal file
110
src/config.test.ts
Normal file
@@ -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',
|
||||||
|
});
|
||||||
|
});
|
||||||
94
src/format.test.ts
Normal file
94
src/format.test.ts
Normal file
@@ -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);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user