Add comprehensive test coverage for core site features (content, navigation, language, FAQ, news, petition, newsletter) using Pest browser tests and unit tests for custom plugins. Includes test infrastructure (Containerfile.test, compose.test.yaml), test documentation, and test files covering petition form logic, CSV handling, translation, date formatting, rate limiting, and map data building.
366 lines
13 KiB
PHP
366 lines
13 KiB
PHP
<?php
|
|
|
|
// PetitionFormTest.php (loads first) already required petition-form.php and
|
|
// defined the Context stub. This file only adds CSV-function tests.
|
|
|
|
const PETITION_CSV_HEADER = [
|
|
'timestamp', 'email', 'firstname', 'surname',
|
|
'region', 'display', 'status', 'token', 'token_created', 'ip_hash',
|
|
];
|
|
|
|
/**
|
|
* Write rows to a temp CSV and return its path. Caller must unlink().
|
|
*/
|
|
function petitionTestCsv(array $rows): string
|
|
{
|
|
$path = tempnam(sys_get_temp_dir(), 'petition_form_test_');
|
|
$fp = fopen($path, 'w');
|
|
foreach ($rows as $row) {
|
|
fputcsv($fp, $row, ',', '"', '');
|
|
}
|
|
fclose($fp);
|
|
return $path;
|
|
}
|
|
|
|
/**
|
|
* Build a minimal signature data array for petitionAppendSignature.
|
|
*/
|
|
function sigData(string $email, string $status = 'pending', string $token = 'tok'): array
|
|
{
|
|
return [
|
|
'timestamp' => time(),
|
|
'email' => $email,
|
|
'firstname' => 'Test',
|
|
'surname' => 'User',
|
|
'region' => 'oslo',
|
|
'display' => 'semi',
|
|
'status' => $status,
|
|
'token' => $token,
|
|
'token_created' => time(),
|
|
'ip_hash' => 'hash',
|
|
];
|
|
}
|
|
|
|
// --- petitionEmailExists ---
|
|
|
|
it('returns false when the CSV does not exist', function () {
|
|
expect(petitionEmailExists('/tmp/nonexistent-petition-form-test.csv', 'a@b.com'))->toBeFalse();
|
|
});
|
|
|
|
it('returns false when the email is not in the CSV', function () {
|
|
$path = petitionTestCsv([
|
|
PETITION_CSV_HEADER,
|
|
[time(), 'other@b.com', 'Ola', 'Hansen', 'oslo', 'semi', 'confirmed', 't1', time(), 'h1'],
|
|
]);
|
|
expect(petitionEmailExists($path, 'nothere@b.com'))->toBeFalse();
|
|
unlink($path);
|
|
});
|
|
|
|
it('returns true when the email exists', function () {
|
|
$path = petitionTestCsv([
|
|
PETITION_CSV_HEADER,
|
|
[time(), 'kari@b.com', 'Kari', 'Hansen', 'oslo', 'semi', 'confirmed', 't1', time(), 'h1'],
|
|
]);
|
|
expect(petitionEmailExists($path, 'kari@b.com'))->toBeTrue();
|
|
unlink($path);
|
|
});
|
|
|
|
it('matches the email case-insensitively', function () {
|
|
$path = petitionTestCsv([
|
|
PETITION_CSV_HEADER,
|
|
[time(), 'kari@b.com', 'Kari', 'Hansen', 'oslo', 'semi', 'confirmed', 't1', time(), 'h1'],
|
|
]);
|
|
expect(petitionEmailExists($path, 'KARI@B.COM'))->toBeTrue();
|
|
unlink($path);
|
|
});
|
|
|
|
// --- petitionGetConfirmedSignatures ---
|
|
|
|
it('returns an empty array when the CSV does not exist', function () {
|
|
expect(petitionGetConfirmedSignatures('/tmp/nonexistent-petition-form-test2.csv'))->toBe([]);
|
|
});
|
|
|
|
it('returns only confirmed signatures', function () {
|
|
$path = petitionTestCsv([
|
|
PETITION_CSV_HEADER,
|
|
[time(), 'a@b.com', 'Kari', 'Hansen', 'oslo', 'semi', 'confirmed', 't1', time(), 'h1'],
|
|
[time(), 'b@b.com', 'Ola', 'Nilsen', 'oslo', 'semi', 'pending', 't2', time(), 'h2'],
|
|
[time(), 'c@b.com', 'Per', 'Olsen', 'oslo', 'semi', 'deleted', 't3', time(), 'h3'],
|
|
]);
|
|
$sigs = petitionGetConfirmedSignatures($path);
|
|
expect(count($sigs))->toBe(1);
|
|
expect($sigs[0]['firstname'])->toBe('Kari');
|
|
unlink($path);
|
|
});
|
|
|
|
it('sorts confirmed signatures newest-first', function () {
|
|
$older = time() - 100;
|
|
$newer = time();
|
|
$path = petitionTestCsv([
|
|
PETITION_CSV_HEADER,
|
|
[$older, 'a@b.com', 'Older', 'Hansen', 'oslo', 'semi', 'confirmed', 't1', $older, 'h1'],
|
|
[$newer, 'b@b.com', 'Newer', 'Nilsen', 'oslo', 'semi', 'confirmed', 't2', $newer, 'h2'],
|
|
]);
|
|
$sigs = petitionGetConfirmedSignatures($path);
|
|
expect($sigs[0]['firstname'])->toBe('Newer');
|
|
expect($sigs[1]['firstname'])->toBe('Older');
|
|
unlink($path);
|
|
});
|
|
|
|
it('does not include email or token in the returned signature data', function () {
|
|
$path = petitionTestCsv([
|
|
PETITION_CSV_HEADER,
|
|
[time(), 'secret@b.com', 'Lars', 'Berg', 'oslo', 'semi', 'confirmed', 'secret-tok', time(), 'h1'],
|
|
]);
|
|
$sig = petitionGetConfirmedSignatures($path)[0];
|
|
expect($sig)->not->toHaveKey('email');
|
|
expect($sig)->not->toHaveKey('token');
|
|
unlink($path);
|
|
});
|
|
|
|
// --- petitionGetSignatureByToken ---
|
|
|
|
it('returns null when the CSV does not exist', function () {
|
|
expect(petitionGetSignatureByToken('/tmp/nonexistent.csv', 'tok'))->toBeNull();
|
|
});
|
|
|
|
it('returns null for an unknown token', function () {
|
|
$path = petitionTestCsv([
|
|
PETITION_CSV_HEADER,
|
|
[time(), 'a@b.com', 'Kari', 'Hansen', 'oslo', 'semi', 'pending', 'real-tok', time(), 'h1'],
|
|
]);
|
|
expect(petitionGetSignatureByToken($path, 'wrong-tok'))->toBeNull();
|
|
unlink($path);
|
|
});
|
|
|
|
it('returns email, firstname, and surname for a matching token', function () {
|
|
$path = petitionTestCsv([
|
|
PETITION_CSV_HEADER,
|
|
[time(), 'kari@b.com', 'Kari', 'Hansen', 'oslo', 'semi', 'pending', 'my-tok', time(), 'h1'],
|
|
]);
|
|
$sig = petitionGetSignatureByToken($path, 'my-tok');
|
|
expect($sig)->not->toBeNull();
|
|
expect($sig['email'])->toBe('kari@b.com');
|
|
expect($sig['firstname'])->toBe('Kari');
|
|
expect($sig['surname'])->toBe('Hansen');
|
|
unlink($path);
|
|
});
|
|
|
|
// --- petitionGetPendingSignatureByEmail ---
|
|
|
|
it('returns null when the CSV does not exist for pending lookup', function () {
|
|
expect(petitionGetPendingSignatureByEmail('/tmp/nonexistent.csv', 'a@b.com'))->toBeNull();
|
|
});
|
|
|
|
it('returns null when the email is not present', function () {
|
|
$path = petitionTestCsv([
|
|
PETITION_CSV_HEADER,
|
|
[time(), 'other@b.com', 'Ola', 'Hansen', 'oslo', 'semi', 'pending', 't1', time(), 'h1'],
|
|
]);
|
|
expect(petitionGetPendingSignatureByEmail($path, 'missing@b.com'))->toBeNull();
|
|
unlink($path);
|
|
});
|
|
|
|
it('returns the signature when the email matches', function () {
|
|
$path = petitionTestCsv([
|
|
PETITION_CSV_HEADER,
|
|
[time(), 'kari@b.com', 'Kari', 'Hansen', 'oslo', 'semi', 'pending', 'tok1', time(), 'h1'],
|
|
]);
|
|
$sig = petitionGetPendingSignatureByEmail($path, 'kari@b.com');
|
|
expect($sig)->not->toBeNull();
|
|
expect($sig['email'])->toBe('kari@b.com');
|
|
expect($sig['status'])->toBe('pending');
|
|
unlink($path);
|
|
});
|
|
|
|
it('matches the email case-insensitively for pending lookup', function () {
|
|
$path = petitionTestCsv([
|
|
PETITION_CSV_HEADER,
|
|
[time(), 'kari@b.com', 'Kari', 'Hansen', 'oslo', 'semi', 'pending', 'tok1', time(), 'h1'],
|
|
]);
|
|
expect(petitionGetPendingSignatureByEmail($path, 'KARI@B.COM'))->not->toBeNull();
|
|
unlink($path);
|
|
});
|
|
|
|
// --- petitionAppendSignature ---
|
|
|
|
it('creates a new CSV with a header and appends the first signature', function () {
|
|
$path = tempnam(sys_get_temp_dir(), 'petition_append_test_');
|
|
unlink($path); // petitionAppendSignature creates it
|
|
|
|
$result = petitionAppendSignature($path, sigData('new@b.com'));
|
|
expect($result)->toBeTrue();
|
|
expect(file_exists($path))->toBeTrue();
|
|
|
|
$rows = array_filter(explode("\n", file_get_contents($path)));
|
|
expect(count($rows))->toBe(2); // header + 1 row
|
|
unlink($path);
|
|
});
|
|
|
|
it('appends a signature to an existing CSV', function () {
|
|
$path = petitionTestCsv([
|
|
PETITION_CSV_HEADER,
|
|
[time(), 'first@b.com', 'First', 'User', 'oslo', 'semi', 'pending', 't1', time(), 'h1'],
|
|
]);
|
|
petitionAppendSignature($path, sigData('second@b.com', 'pending', 't2'));
|
|
|
|
$fp = fopen($path, 'r');
|
|
fgetcsv($fp, null, ',', '"', ''); // skip header
|
|
$rows = [];
|
|
while (($row = fgetcsv($fp, null, ',', '"', '')) !== false) $rows[] = $row;
|
|
fclose($path);
|
|
|
|
expect(count($rows))->toBe(2);
|
|
unlink($path);
|
|
});
|
|
|
|
it('rejects a duplicate email and returns false', function () {
|
|
$path = petitionTestCsv([
|
|
PETITION_CSV_HEADER,
|
|
[time(), 'existing@b.com', 'Kari', 'Hansen', 'oslo', 'semi', 'pending', 't1', time(), 'h1'],
|
|
]);
|
|
$result = petitionAppendSignature($path, sigData('existing@b.com', 'pending', 't2'));
|
|
expect($result)->toBeFalse();
|
|
unlink($path);
|
|
});
|
|
|
|
it('rejects a duplicate email case-insensitively', function () {
|
|
$path = petitionTestCsv([
|
|
PETITION_CSV_HEADER,
|
|
[time(), 'kari@b.com', 'Kari', 'Hansen', 'oslo', 'semi', 'pending', 't1', time(), 'h1'],
|
|
]);
|
|
$result = petitionAppendSignature($path, sigData('KARI@B.COM', 'pending', 't2'));
|
|
expect($result)->toBeFalse();
|
|
unlink($path);
|
|
});
|
|
|
|
// --- petitionConfirmSignature ---
|
|
|
|
it('returns error when the CSV does not exist', function () {
|
|
expect(petitionConfirmSignature('/tmp/nonexistent.csv', 'tok'))->toBe('error');
|
|
});
|
|
|
|
it('returns error for an unknown token', function () {
|
|
$path = petitionTestCsv([
|
|
PETITION_CSV_HEADER,
|
|
[time(), 'a@b.com', 'Kari', 'Hansen', 'oslo', 'semi', 'pending', 'real-tok', time(), 'h1'],
|
|
]);
|
|
expect(petitionConfirmSignature($path, 'wrong-tok'))->toBe('error');
|
|
unlink($path);
|
|
});
|
|
|
|
it('returns success and sets status to confirmed for a valid pending token', function () {
|
|
$path = petitionTestCsv([
|
|
PETITION_CSV_HEADER,
|
|
[time(), 'a@b.com', 'Kari', 'Hansen', 'oslo', 'semi', 'pending', 'valid-tok', time(), 'h1'],
|
|
]);
|
|
expect(petitionConfirmSignature($path, 'valid-tok'))->toBe('success');
|
|
|
|
// Verify the status was actually updated in the file
|
|
$fp = fopen($path, 'r');
|
|
fgetcsv($fp, null, ',', '"', ''); // header
|
|
$row = fgetcsv($fp, null, ',', '"', '');
|
|
fclose($fp);
|
|
expect($row[6])->toBe('confirmed');
|
|
unlink($path);
|
|
});
|
|
|
|
it('returns already when the token is already confirmed', function () {
|
|
$path = petitionTestCsv([
|
|
PETITION_CSV_HEADER,
|
|
[time(), 'a@b.com', 'Kari', 'Hansen', 'oslo', 'semi', 'confirmed', 'valid-tok', time(), 'h1'],
|
|
]);
|
|
expect(petitionConfirmSignature($path, 'valid-tok'))->toBe('already');
|
|
unlink($path);
|
|
});
|
|
|
|
it('returns expired when the token is older than 30 days', function () {
|
|
$oldTime = time() - (31 * 86400);
|
|
$path = petitionTestCsv([
|
|
PETITION_CSV_HEADER,
|
|
[$oldTime, 'a@b.com', 'Kari', 'Hansen', 'oslo', 'semi', 'pending', 'old-tok', $oldTime, 'h1'],
|
|
]);
|
|
expect(petitionConfirmSignature($path, 'old-tok'))->toBe('expired');
|
|
unlink($path);
|
|
});
|
|
|
|
// --- petitionDeleteSignature ---
|
|
|
|
it('returns error when the CSV does not exist for delete', function () {
|
|
expect(petitionDeleteSignature('/tmp/nonexistent.csv', 'tok'))->toBe('error');
|
|
});
|
|
|
|
it('returns error when the token is not found for delete', function () {
|
|
$path = petitionTestCsv([
|
|
PETITION_CSV_HEADER,
|
|
[time(), 'a@b.com', 'Kari', 'Hansen', 'oslo', 'semi', 'confirmed', 'real-tok', time(), 'h1'],
|
|
]);
|
|
expect(petitionDeleteSignature($path, 'wrong-tok'))->toBe('error');
|
|
unlink($path);
|
|
});
|
|
|
|
it('returns success and removes the row for a matching token', function () {
|
|
$path = petitionTestCsv([
|
|
PETITION_CSV_HEADER,
|
|
[time(), 'a@b.com', 'Kari', 'Hansen', 'oslo', 'semi', 'confirmed', 'del-tok', time(), 'h1'],
|
|
[time(), 'b@b.com', 'Other', 'Nilsen', 'oslo', 'semi', 'confirmed', 'keep-tok', time(), 'h2'],
|
|
]);
|
|
expect(petitionDeleteSignature($path, 'del-tok'))->toBe('success');
|
|
|
|
$fp = fopen($path, 'r');
|
|
fgetcsv($fp, null, ',', '"', ''); // header
|
|
$rows = [];
|
|
while (($row = fgetcsv($fp, null, ',', '"', '')) !== false) $rows[] = $row;
|
|
fclose($fp);
|
|
|
|
expect(count($rows))->toBe(1);
|
|
expect($rows[0][7])->toBe('keep-tok');
|
|
unlink($path);
|
|
});
|
|
|
|
// --- petitionUpdateSignatureToken ---
|
|
|
|
it('returns false when the CSV does not exist for token update', function () {
|
|
expect(petitionUpdateSignatureToken('/tmp/nonexistent.csv', 'a@b.com', 'new-tok'))->toBeFalse();
|
|
});
|
|
|
|
it('returns false when the email has no pending entry', function () {
|
|
$path = petitionTestCsv([
|
|
PETITION_CSV_HEADER,
|
|
[time(), 'a@b.com', 'Kari', 'Hansen', 'oslo', 'semi', 'confirmed', 'old-tok', time(), 'h1'],
|
|
]);
|
|
expect(petitionUpdateSignatureToken($path, 'a@b.com', 'new-tok'))->toBeFalse();
|
|
unlink($path);
|
|
});
|
|
|
|
it('returns true and updates the token for a pending entry', function () {
|
|
$path = petitionTestCsv([
|
|
PETITION_CSV_HEADER,
|
|
[time(), 'a@b.com', 'Kari', 'Hansen', 'oslo', 'semi', 'pending', 'old-tok', time(), 'h1'],
|
|
]);
|
|
expect(petitionUpdateSignatureToken($path, 'a@b.com', 'new-tok'))->toBeTrue();
|
|
|
|
$fp = fopen($path, 'r');
|
|
fgetcsv($fp, null, ',', '"', ''); // header
|
|
$row = fgetcsv($fp, null, ',', '"', '');
|
|
fclose($fp);
|
|
expect($row[7])->toBe('new-tok');
|
|
unlink($path);
|
|
});
|
|
|
|
it('only updates pending entries, not confirmed ones', function () {
|
|
$path = petitionTestCsv([
|
|
PETITION_CSV_HEADER,
|
|
[time(), 'a@b.com', 'Kari', 'Hansen', 'oslo', 'semi', 'confirmed', 'conf-tok', time(), 'h1'],
|
|
]);
|
|
$result = petitionUpdateSignatureToken($path, 'a@b.com', 'new-tok');
|
|
expect($result)->toBeFalse();
|
|
|
|
// Confirmed token should be unchanged
|
|
$fp = fopen($path, 'r');
|
|
fgetcsv($fp, null, ',', '"', '');
|
|
$row = fgetcsv($fp, null, ',', '"', '');
|
|
fclose($fp);
|
|
expect($row[7])->toBe('conf-tok');
|
|
unlink($path);
|
|
});
|