mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 08:09:18 +02:00
Add PHPUnit setup (Phase 0) and pure-logic tests (Phase 1): Crypto, EmailObfuscator, SystemController helpers, StudentEmail, TfeController OG tags
This commit is contained in:
17
tests/bootstrap.php
Normal file
17
tests/bootstrap.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* PHPUnit bootstrap for XAMXAM.
|
||||
*
|
||||
* Sets up autoloading, constants, and any environment needed before tests run.
|
||||
* Uses the production app/.env for APP_KEY (Crypto tests need a real key).
|
||||
*/
|
||||
|
||||
// Composer autoloader
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
// Define application root (points to app/)
|
||||
define('APP_ROOT', realpath(__DIR__ . '/../app'));
|
||||
|
||||
// Storage directory for tests — use app/storage/
|
||||
define('STORAGE_ROOT', APP_ROOT . '/storage');
|
||||
152
tests/phpunit/CryptoTest.php
Normal file
152
tests/phpunit/CryptoTest.php
Normal file
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* CryptoTest — Pure logic tests for the Crypto class (AES-256-GCM).
|
||||
*
|
||||
* Requires a valid APP_KEY in app/.env (set up by bootstrap.php).
|
||||
*/
|
||||
class CryptoTest extends TestCase
|
||||
{
|
||||
// ── Encrypt → Decrypt round-trip ──────────────────────────────────────────
|
||||
|
||||
public function testEncryptDecryptRoundTrip(): void
|
||||
{
|
||||
$plain = 'Hello, World!';
|
||||
$enc = Crypto::encrypt($plain);
|
||||
$dec = Crypto::decrypt($enc);
|
||||
|
||||
$this->assertSame($plain, $dec);
|
||||
$this->assertNotSame($plain, $enc, 'Ciphertext must differ from plaintext');
|
||||
}
|
||||
|
||||
public function testEncryptDecryptWithUnicode(): void
|
||||
{
|
||||
$plain = 'émoji 🎉 — français & 日本語';
|
||||
$enc = Crypto::encrypt($plain);
|
||||
$dec = Crypto::decrypt($enc);
|
||||
|
||||
$this->assertSame($plain, $dec);
|
||||
}
|
||||
|
||||
public function testEncryptDecryptMultiline(): void
|
||||
{
|
||||
$plain = "Line 1\nLine 2\r\nLine 3\n\n";
|
||||
$enc = Crypto::encrypt($plain);
|
||||
$dec = Crypto::decrypt($enc);
|
||||
|
||||
$this->assertSame($plain, $dec);
|
||||
}
|
||||
|
||||
// ── Different plaintexts → different ciphertexts ──────────────────────────
|
||||
|
||||
public function testDifferentPlaintextsProduceDifferentCiphertexts(): void
|
||||
{
|
||||
$a = Crypto::encrypt('alpha');
|
||||
$b = Crypto::encrypt('beta');
|
||||
|
||||
$this->assertNotSame($a, $b);
|
||||
}
|
||||
|
||||
public function testSamePlaintextProducesDifferentCiphertexts(): void
|
||||
{
|
||||
// IV is random every time — encrypting the same value twice should
|
||||
// never produce identical ciphertext (except in astronomically rare
|
||||
// collisions of a 12-byte IV).
|
||||
$a = Crypto::encrypt('same');
|
||||
$b = Crypto::encrypt('same');
|
||||
|
||||
$this->assertNotSame($a, $b, 'IV randomness → ciphertexts must differ');
|
||||
}
|
||||
|
||||
// ── isEncrypted() ─────────────────────────────────────────────────────────
|
||||
|
||||
public function testIsEncryptedRecognizesEncryptedValue(): void
|
||||
{
|
||||
$enc = Crypto::encrypt('secret');
|
||||
$this->assertTrue(Crypto::isEncrypted($enc));
|
||||
}
|
||||
|
||||
public function testIsEncryptedRejectsPlaintext(): void
|
||||
{
|
||||
$this->assertFalse(Crypto::isEncrypted('plaintext password'));
|
||||
}
|
||||
|
||||
public function testIsEncryptedReturnsFalseForEmptyString(): void
|
||||
{
|
||||
$this->assertFalse(Crypto::isEncrypted(''));
|
||||
}
|
||||
|
||||
public function testIsEncryptedRejectsInvalidBase64(): void
|
||||
{
|
||||
$this->assertFalse(Crypto::isEncrypted('not valid base64!!!'));
|
||||
}
|
||||
|
||||
// ── Empty string ──────────────────────────────────────────────────────────
|
||||
|
||||
public function testEncryptEmptyStringProducesCiphertext(): void
|
||||
{
|
||||
$enc = Crypto::encrypt('');
|
||||
$this->assertNotEmpty($enc, 'Encrypting empty string should still produce ciphertext');
|
||||
// Note: isEncrypted() returns false for the empty-string ciphertext
|
||||
// because the raw blob (IV+TAG+empty ciphertext) is exactly 28 bytes,
|
||||
// which is < IV_LEN + TAG_LEN + 1 (29). This is a known edge case.
|
||||
$raw = base64_decode($enc, true);
|
||||
$this->assertIsString($raw);
|
||||
$this->assertEquals(28, strlen($raw), 'IV(12) + TAG(16) + empty ciphertext(0)');
|
||||
}
|
||||
|
||||
public function testDecryptEmptyStringReturnsEmpty(): void
|
||||
{
|
||||
$this->assertSame('', Crypto::decrypt(''));
|
||||
}
|
||||
|
||||
// ── Invalid base64 input ──────────────────────────────────────────────────
|
||||
|
||||
public function testDecryptInvalidBase64ReturnsInputGracefully(): void
|
||||
{
|
||||
// Legacy fallback: non-base64 strings are returned as-is
|
||||
$input = 'plaintext-legacy-password';
|
||||
$result = Crypto::decrypt($input);
|
||||
$this->assertSame($input, $result);
|
||||
}
|
||||
|
||||
public function testDecryptTooShortBlobReturnsInputGracefully(): void
|
||||
{
|
||||
// Base64 blob that decodes to fewer bytes than IV + TAG + 1
|
||||
$tooShort = base64_encode('abc');
|
||||
$result = Crypto::decrypt($tooShort);
|
||||
$this->assertSame($tooShort, $result);
|
||||
}
|
||||
|
||||
// ── Wrong key / tampering ─────────────────────────────────────────────────
|
||||
|
||||
public function testDecryptWithTamperedCiphertextReturnsEmpty(): void
|
||||
{
|
||||
$enc = Crypto::encrypt('tamper me');
|
||||
$raw = base64_decode($enc, true);
|
||||
|
||||
// Flip a byte inside the ciphertext portion (after IV + tag)
|
||||
$tamperOffset = 12 + 16; // after 12-byte IV + 16-byte tag
|
||||
$raw[$tamperOffset] = chr(ord($raw[$tamperOffset]) ^ 0xFF);
|
||||
|
||||
$tampered = base64_encode($raw);
|
||||
$result = Crypto::decrypt($tampered);
|
||||
$this->assertSame('', $result, 'Tampered ciphertext must return empty string');
|
||||
}
|
||||
|
||||
public function testDecryptValidBlobTamperedTagReturnsEmpty(): void
|
||||
{
|
||||
$enc = Crypto::encrypt('test');
|
||||
$raw = base64_decode($enc, true);
|
||||
|
||||
// Tamper with the authentication tag (bytes 13-28, after the 12-byte IV)
|
||||
$raw[13] = chr(ord($raw[13]) ^ 0xFF);
|
||||
|
||||
$tampered = base64_encode($raw);
|
||||
$result = Crypto::decrypt($tampered);
|
||||
|
||||
$this->assertSame('', $result, 'Tag mismatch must return empty');
|
||||
}
|
||||
}
|
||||
166
tests/phpunit/EmailObfuscatorTest.php
Normal file
166
tests/phpunit/EmailObfuscatorTest.php
Normal file
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* EmailObfuscatorTest — Pure logic tests for EmailObfuscator.
|
||||
*/
|
||||
class EmailObfuscatorTest extends TestCase
|
||||
{
|
||||
// ── encode() ──────────────────────────────────────────────────────────────
|
||||
|
||||
public function testEncodeContainsNoAtSign(): void
|
||||
{
|
||||
$result = EmailObfuscator::email('test@example.com');
|
||||
|
||||
$this->assertStringNotContainsString('@', $result);
|
||||
$this->assertStringContainsString('@', $result); // decimal entity for @
|
||||
}
|
||||
|
||||
public function testEncodeOutputIsNumericEntities(): void
|
||||
{
|
||||
$result = EmailObfuscator::email('a@b.c');
|
||||
|
||||
// Every original character should be a &#xxx; entity
|
||||
$this->assertMatchesRegularExpression('/^(?:&#\d+;)+$/', $result);
|
||||
$this->assertNotEmpty($result);
|
||||
}
|
||||
|
||||
// ── email() ───────────────────────────────────────────────────────────────
|
||||
|
||||
public function testEmailReturnsObfuscatedAddress(): void
|
||||
{
|
||||
$result = EmailObfuscator::email('hello@world.org');
|
||||
|
||||
$this->assertStringNotContainsString('hello', $result);
|
||||
$this->assertStringNotContainsString('@', $result);
|
||||
$this->assertStringContainsString('h', $result); // 'h'
|
||||
$this->assertStringContainsString('@', $result); // '@'
|
||||
}
|
||||
|
||||
// ── mailto() ──────────────────────────────────────────────────────────────
|
||||
|
||||
public function testMailtoBuildsCorrectHrefStructure(): void
|
||||
{
|
||||
$result = EmailObfuscator::mailto('x@y.com');
|
||||
|
||||
$this->assertStringStartsWith('mailto:', $result);
|
||||
$this->assertStringNotContainsString('@', $result);
|
||||
$this->assertStringContainsString('@', $result);
|
||||
}
|
||||
|
||||
// ── emailText() ───────────────────────────────────────────────────────────
|
||||
|
||||
public function testEmailTextReplacesBareEmail(): void
|
||||
{
|
||||
$input = 'Contact xamxam@erg.be for help';
|
||||
$result = EmailObfuscator::emailText($input);
|
||||
|
||||
$this->assertStringNotContainsString('xamxam@erg.be', $result);
|
||||
$this->assertStringContainsString('@', $result); // @ entity
|
||||
$this->assertStringContainsString('Contact', $result); // surrounding text preserved
|
||||
// Entity-encoded chars: 120=x, 97=a, 109=m, so 'xamxam' becomes xam...
|
||||
$this->assertStringContainsString('x', $result); // 'x'
|
||||
$this->assertStringContainsString('a', $result); // 'a'
|
||||
$this->assertStringContainsString('m', $result); // 'm'
|
||||
}
|
||||
|
||||
public function testEmailTextReplacesMultipleEmails(): void
|
||||
{
|
||||
$input = 'a@b.com and c@d.org';
|
||||
$result = EmailObfuscator::emailText($input);
|
||||
|
||||
$this->assertStringNotContainsString('@', $result);
|
||||
$this->assertStringContainsString('@', $result);
|
||||
// Should have two occurrences of the @ entity
|
||||
$this->assertEquals(2, substr_count($result, '@'));
|
||||
}
|
||||
|
||||
// ── mailtoInText() ────────────────────────────────────────────────────────
|
||||
|
||||
public function testMailtoInTextReplacesMailtoLinks(): void
|
||||
{
|
||||
$input = '<a href="mailto:contact@example.com">contact</a>';
|
||||
$result = EmailObfuscator::mailtoInText($input);
|
||||
|
||||
$this->assertStringNotContainsString('contact@example.com', $result);
|
||||
$this->assertStringContainsString('mailto:', $result);
|
||||
$this->assertStringContainsString('@', $result);
|
||||
}
|
||||
|
||||
// ── obfuscateHtml() ───────────────────────────────────────────────────────
|
||||
|
||||
public function testObfuscateHtmlReplacesAnchorTag(): void
|
||||
{
|
||||
$input = '<a href="mailto:contact@example.com">contact@example.com</a>';
|
||||
$result = EmailObfuscator::obfuscateHtml($input);
|
||||
|
||||
$this->assertStringNotContainsString('contact@example.com', $result);
|
||||
$this->assertStringContainsString('<a ', $result);
|
||||
$this->assertStringContainsString('href="mailto:', $result);
|
||||
// @ entity should appear twice (href + link text)
|
||||
$this->assertGreaterThanOrEqual(2, substr_count($result, '@'));
|
||||
}
|
||||
|
||||
public function testObfuscateHtmlKeepsNonMailtoLinksUnchanged(): void
|
||||
{
|
||||
$input = '<a href="https://erg.be">ERG</a> and <a href="mailto:x@y.z">x@y.z</a>';
|
||||
$result = EmailObfuscator::obfuscateHtml($input);
|
||||
|
||||
$this->assertStringContainsString('https://erg.be', $result);
|
||||
$this->assertStringContainsString('ERG', $result);
|
||||
$this->assertStringNotContainsString('x@y.z', $result);
|
||||
}
|
||||
|
||||
public function testObfuscateHtmlPreservesCustomLinkText(): void
|
||||
{
|
||||
// When the link text differs from the email, only the href is obfuscated
|
||||
$input = '<a href="mailto:foobar@test.com">custom text</a>';
|
||||
$result = EmailObfuscator::obfuscateHtml($input);
|
||||
|
||||
$this->assertStringNotContainsString('foobar@test.com', $result);
|
||||
$this->assertStringContainsString('@', $result); // in href
|
||||
$this->assertStringContainsString('custom text', $result); // link text unchanged
|
||||
}
|
||||
|
||||
// ── Edge cases ────────────────────────────────────────────────────────────
|
||||
|
||||
public function testEmptyStringReturnsEmpty(): void
|
||||
{
|
||||
$this->assertSame('', EmailObfuscator::email(''));
|
||||
}
|
||||
|
||||
public function testStringWithNoEmailsIsUnchanged(): void
|
||||
{
|
||||
$input = 'Hello, world! No emails here.';
|
||||
$this->assertSame($input, EmailObfuscator::emailText($input));
|
||||
}
|
||||
|
||||
public function testAlreadyObfuscatedContentIsNotDoubleEncoded(): void
|
||||
{
|
||||
// Already entity-encoded email — no literal '@' to match
|
||||
$input = 'Contact xamxam@erg.be here';
|
||||
$result = EmailObfuscator::emailText($input);
|
||||
|
||||
// Should be unchanged (regex won't match entity-encoded @)
|
||||
$this->assertSame($input, $result);
|
||||
}
|
||||
|
||||
public function testMultipleEmailsInOneString(): void
|
||||
{
|
||||
$input = 'Mail to a@b.com or c@d.org pls';
|
||||
$result = EmailObfuscator::emailText($input);
|
||||
|
||||
$this->assertStringNotContainsString('a@b.com', $result);
|
||||
$this->assertStringNotContainsString('c@d.org', $result);
|
||||
// Two @ entities
|
||||
$this->assertEquals(2, substr_count($result, '@'));
|
||||
}
|
||||
|
||||
public function testEmailWithPlusSign(): void
|
||||
{
|
||||
$result = EmailObfuscator::email('user+tag@domain.com');
|
||||
$this->assertStringContainsString('+', $result); // '+'
|
||||
$this->assertStringContainsString('@', $result); // '@'
|
||||
}
|
||||
}
|
||||
140
tests/phpunit/StudentEmailTest.php
Normal file
140
tests/phpunit/StudentEmailTest.php
Normal file
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* StudentEmailTest — Pure logic tests for StudentEmail::buildHtml().
|
||||
*
|
||||
* buildHtml() is private but we can test it via reflection.
|
||||
*/
|
||||
class StudentEmailTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Invoke the private buildHtml() via reflection.
|
||||
*/
|
||||
private function buildHtml(array $thesis): string
|
||||
{
|
||||
$ref = new ReflectionMethod(StudentEmail::class, 'buildHtml');
|
||||
$ref->setAccessible(true);
|
||||
|
||||
return $ref->invoke(null, $thesis);
|
||||
}
|
||||
|
||||
// ── Basic output ──────────────────────────────────────────────────────────
|
||||
|
||||
public function testBuildHtmlReturnsNonEmptyString(): void
|
||||
{
|
||||
$thesis = [
|
||||
'identifier' => '2024-001',
|
||||
'title' => 'My Thesis',
|
||||
'authors' => 'John Doe',
|
||||
'year' => '2024',
|
||||
'synopsis' => 'A brief synopsis.',
|
||||
'keywords' => 'art, design',
|
||||
];
|
||||
|
||||
$html = $this->buildHtml($thesis);
|
||||
|
||||
$this->assertIsString($html);
|
||||
$this->assertNotEmpty($html);
|
||||
$this->assertStringContainsString('My Thesis', $html);
|
||||
$this->assertStringContainsString('2024-001', $html);
|
||||
$this->assertStringContainsString('John Doe', $html);
|
||||
}
|
||||
|
||||
public function testBuildHtmlContainsKeyFields(): void
|
||||
{
|
||||
$thesis = [
|
||||
'title' => 'Test Title',
|
||||
'authors' => 'Alice',
|
||||
'year' => '2025',
|
||||
'identifier' => '2025-042',
|
||||
'orientation' => 'BD',
|
||||
];
|
||||
|
||||
$html = $this->buildHtml($thesis);
|
||||
|
||||
$this->assertStringContainsString('Test Title', $html);
|
||||
$this->assertStringContainsString('Alice', $html);
|
||||
$this->assertStringContainsString('2025', $html);
|
||||
$this->assertStringContainsString('2025-042', $html);
|
||||
$this->assertStringContainsString('BD', $html);
|
||||
}
|
||||
|
||||
// ── HTML escaping ─────────────────────────────────────────────────────────
|
||||
|
||||
public function testBuildHtmlEscapesSpecialCharacters(): void
|
||||
{
|
||||
$thesis = [
|
||||
'identifier' => '2024-999',
|
||||
'title' => '<script>alert("xss")</script>',
|
||||
'authors' => 'Evil & Co.',
|
||||
'year' => '2024',
|
||||
];
|
||||
|
||||
$html = $this->buildHtml($thesis);
|
||||
|
||||
$this->assertStringNotContainsString('<script>', $html);
|
||||
$this->assertStringContainsString('<script>', $html);
|
||||
$this->assertStringContainsString('Evil & Co.', $html);
|
||||
}
|
||||
|
||||
// ── Missing / null optional fields ────────────────────────────────────────
|
||||
|
||||
public function testBuildHtmlHandlesMissingOptionalFields(): void
|
||||
{
|
||||
// Minimal thesis — only title and authors
|
||||
$thesis = [
|
||||
'title' => 'Solo',
|
||||
'authors' => 'One Author',
|
||||
];
|
||||
|
||||
$html = $this->buildHtml($thesis);
|
||||
|
||||
$this->assertIsString($html);
|
||||
$this->assertNotEmpty($html);
|
||||
// Should contain the "–" dash placeholders for empty fields
|
||||
$this->assertStringContainsString('Solo', $html);
|
||||
$this->assertStringContainsString('One Author', $html);
|
||||
}
|
||||
|
||||
public function testBuildHtmlHandlesNullFieldsGracefully(): void
|
||||
{
|
||||
$thesis = [
|
||||
'identifier' => null,
|
||||
'title' => 'Null Test',
|
||||
'authors' => null,
|
||||
'year' => null,
|
||||
'subtitle' => null,
|
||||
'orientation' => null,
|
||||
];
|
||||
|
||||
$html = $this->buildHtml($thesis);
|
||||
|
||||
$this->assertIsString($html);
|
||||
$this->assertNotEmpty($html);
|
||||
$this->assertStringContainsString('Null Test', $html);
|
||||
}
|
||||
|
||||
public function testBuildHtmlHandlesEmptyArray(): void
|
||||
{
|
||||
$html = $this->buildHtml([]);
|
||||
|
||||
$this->assertIsString($html);
|
||||
$this->assertNotEmpty($html);
|
||||
// Should not crash — just fill all fields with "–"
|
||||
}
|
||||
|
||||
public function testBuildHtmlContainsLabelFields(): void
|
||||
{
|
||||
$thesis = ['title' => 'X', 'authors' => 'Y'];
|
||||
|
||||
$html = $this->buildHtml($thesis);
|
||||
|
||||
$this->assertStringContainsString('Identifiant', $html);
|
||||
$this->assertStringContainsString('Titre', $html);
|
||||
$this->assertStringContainsString('Auteur·ice(s)', $html);
|
||||
$this->assertStringContainsString('Année', $html);
|
||||
$this->assertStringContainsString('Mots-clés', $html);
|
||||
}
|
||||
}
|
||||
195
tests/phpunit/SystemControllerHelpersTest.php
Normal file
195
tests/phpunit/SystemControllerHelpersTest.php
Normal file
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* SystemControllerHelpersTest — Pure logic tests for the static helper methods
|
||||
* in SystemController (humanBytes, diskColor, logLineClass, nginxLineClass,
|
||||
* statusLabel, statusClass).
|
||||
*
|
||||
* These are stateless, no-IO functions.
|
||||
*/
|
||||
class SystemControllerHelpersTest extends TestCase
|
||||
{
|
||||
// ── humanBytes() ──────────────────────────────────────────────────────────
|
||||
|
||||
public function testHumanBytesZero(): void
|
||||
{
|
||||
$this->assertSame('0.0 KB', SystemController::humanBytes(0));
|
||||
}
|
||||
|
||||
public function testHumanBytesBelowOneKB(): void
|
||||
{
|
||||
$this->assertSame('1.0 KB', SystemController::humanBytes(1023));
|
||||
}
|
||||
|
||||
public function testHumanBytesOneKB(): void
|
||||
{
|
||||
$this->assertSame('1.0 KB', SystemController::humanBytes(1024));
|
||||
}
|
||||
|
||||
public function testHumanBytesOneMB(): void
|
||||
{
|
||||
// threshold is > 1048576, so exactly 1048576 is still KB
|
||||
$this->assertSame('1,024.0 KB', SystemController::humanBytes(1048576));
|
||||
}
|
||||
|
||||
public function testHumanBytesOneGB(): void
|
||||
{
|
||||
// threshold is > 1073741824, so exactly that is still MB
|
||||
$this->assertSame('1,024.0 MB', SystemController::humanBytes(1073741824));
|
||||
}
|
||||
|
||||
public function testHumanBytes1523MB(): void
|
||||
{
|
||||
$bytes = (int)(1.5 * 1048576);
|
||||
$this->assertSame('1.5 MB', SystemController::humanBytes($bytes));
|
||||
}
|
||||
|
||||
public function testHumanBytes2500GB(): void
|
||||
{
|
||||
$bytes = (int)(2.5 * 1073741824);
|
||||
$this->assertSame('2.5 GB', SystemController::humanBytes($bytes));
|
||||
}
|
||||
|
||||
// ── diskColor() ───────────────────────────────────────────────────────────
|
||||
|
||||
public function testDiskColorBelowWarning(): void
|
||||
{
|
||||
$this->assertSame('#4caf50', SystemController::diskColor(0));
|
||||
$this->assertSame('#4caf50', SystemController::diskColor(50));
|
||||
$this->assertSame('#4caf50', SystemController::diskColor(70));
|
||||
}
|
||||
|
||||
public function testDiskColorWarning(): void
|
||||
{
|
||||
$this->assertSame('#ffc107', SystemController::diskColor(71));
|
||||
$this->assertSame('#ffc107', SystemController::diskColor(85));
|
||||
}
|
||||
|
||||
public function testDiskColorCritical(): void
|
||||
{
|
||||
$this->assertSame('#e05555', SystemController::diskColor(86));
|
||||
$this->assertSame('#e05555', SystemController::diskColor(99));
|
||||
$this->assertSame('#e05555', SystemController::diskColor(100));
|
||||
}
|
||||
|
||||
// ── logLineClass() ────────────────────────────────────────────────────────
|
||||
|
||||
public function testLogLineClassCrit(): void
|
||||
{
|
||||
$this->assertSame('log-crit', SystemController::logLineClass('[crit] Fatal'));
|
||||
$this->assertSame('log-crit', SystemController::logLineClass('[emerg] Emergency'));
|
||||
$this->assertSame('log-crit', SystemController::logLineClass('[alert] Alert'));
|
||||
}
|
||||
|
||||
public function testLogLineClassError(): void
|
||||
{
|
||||
$this->assertSame('log-error', SystemController::logLineClass('[error] An error'));
|
||||
}
|
||||
|
||||
public function testLogLineClassWarn(): void
|
||||
{
|
||||
$this->assertSame('log-warn', SystemController::logLineClass('[warn] Warning'));
|
||||
}
|
||||
|
||||
public function testLogLineClassNotice(): void
|
||||
{
|
||||
$this->assertSame('log-notice', SystemController::logLineClass('[notice] Notice'));
|
||||
}
|
||||
|
||||
public function testLogLineClassHttp500(): void
|
||||
{
|
||||
// Matches " 500 " pattern in nginx-style log
|
||||
$this->assertSame('log-error', SystemController::logLineClass('GET / " 500 123 "'));
|
||||
$this->assertSame('log-error', SystemController::logLineClass('GET / " 404 123 "'));
|
||||
}
|
||||
|
||||
public function testLogLineClassHttp300(): void
|
||||
{
|
||||
$this->assertSame('log-notice', SystemController::logLineClass('GET / " 302 456 "'));
|
||||
}
|
||||
|
||||
public function testLogLineClassDefault(): void
|
||||
{
|
||||
$this->assertSame('', SystemController::logLineClass('Just a plain log message'));
|
||||
$this->assertSame('', SystemController::logLineClass('GET / " 200 123 "'));
|
||||
}
|
||||
|
||||
// ── nginxLineClass() ──────────────────────────────────────────────────────
|
||||
|
||||
public function testNginxLineClassComment(): void
|
||||
{
|
||||
$this->assertSame('nginx-comment', SystemController::nginxLineClass('# comment'));
|
||||
$this->assertSame('nginx-comment', SystemController::nginxLineClass(' # indented comment'));
|
||||
$this->assertSame('nginx-comment', SystemController::nginxLineClass(''));
|
||||
$this->assertSame('nginx-comment', SystemController::nginxLineClass(' '));
|
||||
}
|
||||
|
||||
public function testNginxLineClassBlock(): void
|
||||
{
|
||||
$this->assertSame('nginx-block', SystemController::nginxLineClass('server {'));
|
||||
$this->assertSame('nginx-block', SystemController::nginxLineClass('location / {'));
|
||||
$this->assertSame('nginx-block', SystemController::nginxLineClass(' upstream backend {'));
|
||||
$this->assertSame('nginx-block', SystemController::nginxLineClass('events {'));
|
||||
$this->assertSame('nginx-block', SystemController::nginxLineClass('http {'));
|
||||
}
|
||||
|
||||
public function testNginxLineClassDirective(): void
|
||||
{
|
||||
$this->assertSame('nginx-directive', SystemController::nginxLineClass('listen 80;'));
|
||||
$this->assertSame('nginx-directive', SystemController::nginxLineClass('root /var/www;'));
|
||||
$this->assertSame('nginx-directive', SystemController::nginxLineClass(' server_name example.com;'));
|
||||
}
|
||||
|
||||
// ── statusLabel() ─────────────────────────────────────────────────────────
|
||||
|
||||
public function testStatusLabelActive(): void
|
||||
{
|
||||
$this->assertSame('● En ligne', SystemController::statusLabel('active'));
|
||||
}
|
||||
|
||||
public function testStatusLabelInactive(): void
|
||||
{
|
||||
$this->assertSame('○ Inactif', SystemController::statusLabel('inactive'));
|
||||
}
|
||||
|
||||
public function testStatusLabelFailed(): void
|
||||
{
|
||||
$this->assertSame('✕ Erreur', SystemController::statusLabel('failed'));
|
||||
}
|
||||
|
||||
public function testStatusLabelWarn(): void
|
||||
{
|
||||
$this->assertSame('⚠ Attention', SystemController::statusLabel('warn'));
|
||||
}
|
||||
|
||||
public function testStatusLabelUnknown(): void
|
||||
{
|
||||
$this->assertSame('? Inconnu', SystemController::statusLabel('unknown'));
|
||||
$this->assertSame('? Inconnu', SystemController::statusLabel('bogus'));
|
||||
}
|
||||
|
||||
// ── statusClass() ─────────────────────────────────────────────────────────
|
||||
|
||||
public function testStatusClassOk(): void
|
||||
{
|
||||
$this->assertSame('status-ok', SystemController::statusClass('active'));
|
||||
}
|
||||
|
||||
public function testStatusClassWarn(): void
|
||||
{
|
||||
$this->assertSame('status-warn', SystemController::statusClass('inactive'));
|
||||
$this->assertSame('status-warn', SystemController::statusClass('warn'));
|
||||
}
|
||||
|
||||
public function testStatusClassError(): void
|
||||
{
|
||||
$this->assertSame('status-err', SystemController::statusClass('failed'));
|
||||
}
|
||||
|
||||
public function testStatusClassUnknown(): void
|
||||
{
|
||||
$this->assertSame('status-unknown', SystemController::statusClass('bogus'));
|
||||
}
|
||||
}
|
||||
199
tests/phpunit/TfeControllerOgTest.php
Normal file
199
tests/phpunit/TfeControllerOgTest.php
Normal file
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* TfeControllerOgTest — Pure logic tests for TfeController::buildOgTags()
|
||||
* and buildMetaDescription().
|
||||
*
|
||||
* These methods are protected; we use a thin subclass to expose them.
|
||||
*/
|
||||
class TfeControllerOgTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Subclass that exposes protected methods for testing.
|
||||
*/
|
||||
private static function makeController(): object
|
||||
{
|
||||
return new class extends TfeController {
|
||||
public function __construct()
|
||||
{
|
||||
// Skip parent constructor — we don't need DB for these pure methods
|
||||
}
|
||||
|
||||
public function exposedBuildOgTags(array $data, int $thesisId, string $metaDescription): array
|
||||
{
|
||||
return $this->buildOgTags($data, $thesisId, $metaDescription);
|
||||
}
|
||||
|
||||
public function exposedBuildMetaDescription(string $synopsis): string
|
||||
{
|
||||
return $this->buildMetaDescription($synopsis);
|
||||
}
|
||||
|
||||
public function exposedResolveOgImage(array $files): string
|
||||
{
|
||||
return $this->resolveOgImage($files);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ── buildOgTags() ─────────────────────────────────────────────────────────
|
||||
|
||||
public function testBuildOgTagsReturnsAllRequiredKeys(): void
|
||||
{
|
||||
$ctrl = self::makeController();
|
||||
$data = [
|
||||
'title' => 'My Thesis',
|
||||
'authors' => 'Jane Doe',
|
||||
'year' => '2024',
|
||||
'files' => [],
|
||||
];
|
||||
|
||||
$tags = $ctrl->exposedBuildOgTags($data, 42, 'A description');
|
||||
|
||||
$requiredKeys = ['type', 'title', 'description', 'url', 'image', 'image_alt', 'site_name', 'article_author', 'article_published_time'];
|
||||
foreach ($requiredKeys as $key) {
|
||||
$this->assertArrayHasKey($key, $tags);
|
||||
$this->assertIsString($tags[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
public function testBuildOgTagsTitleIncludesAuthors(): void
|
||||
{
|
||||
$ctrl = self::makeController();
|
||||
$data = [
|
||||
'title' => 'The Title',
|
||||
'authors' => 'Author Name',
|
||||
'year' => '2024',
|
||||
'files' => [],
|
||||
];
|
||||
|
||||
$tags = $ctrl->exposedBuildOgTags($data, 1, 'desc');
|
||||
|
||||
$this->assertStringContainsString('The Title – Author Name', $tags['title']);
|
||||
|
||||
// Without authors, just the title
|
||||
$data['authors'] = '';
|
||||
$tags2 = $ctrl->exposedBuildOgTags($data, 1, 'desc');
|
||||
$this->assertSame('The Title', $tags2['title']);
|
||||
}
|
||||
|
||||
public function testBuildOgTagsImageEmptyWhenNoFiles(): void
|
||||
{
|
||||
$ctrl = self::makeController();
|
||||
$data = [
|
||||
'title' => 'No Files',
|
||||
'authors' => 'A',
|
||||
'year' => '2023',
|
||||
'files' => [],
|
||||
];
|
||||
|
||||
$tags = $ctrl->exposedBuildOgTags($data, 5, 'desc');
|
||||
$this->assertSame('', $tags['image']);
|
||||
}
|
||||
|
||||
public function testBuildOgTagsImageFromCover(): void
|
||||
{
|
||||
$ctrl = self::makeController();
|
||||
$data = [
|
||||
'title' => 'Cover Test',
|
||||
'authors' => 'Author',
|
||||
'year' => '2024',
|
||||
'files' => [
|
||||
['file_path' => 'documents/2024-001/cover.jpg', 'file_type' => 'cover'],
|
||||
],
|
||||
];
|
||||
|
||||
$tags = $ctrl->exposedBuildOgTags($data, 1, 'desc');
|
||||
|
||||
$this->assertStringContainsString('cover.jpg', $tags['image']);
|
||||
}
|
||||
|
||||
public function testBuildOgTagsImageFallbackToFirstImage(): void
|
||||
{
|
||||
$ctrl = self::makeController();
|
||||
$data = [
|
||||
'title' => 'Image Test',
|
||||
'authors' => 'B',
|
||||
'year' => '2024',
|
||||
'files' => [
|
||||
['file_path' => 'documents/2024-001/doc.pdf', 'file_type' => 'tfe'],
|
||||
['file_path' => 'documents/2024-001/screenshot.png', 'file_type' => 'tfe'],
|
||||
],
|
||||
];
|
||||
|
||||
$tags = $ctrl->exposedBuildOgTags($data, 2, 'desc');
|
||||
|
||||
$this->assertStringContainsString('screenshot.png', $tags['image']);
|
||||
}
|
||||
|
||||
public function testBuildOgTagsUrlIncludesThesisId(): void
|
||||
{
|
||||
$ctrl = self::makeController();
|
||||
$data = ['title' => 'URL Test', 'authors' => '', 'year' => '2024', 'files' => []];
|
||||
|
||||
$tags = $ctrl->exposedBuildOgTags($data, 99, 'desc');
|
||||
|
||||
$this->assertStringContainsString('tfe?id=99', $tags['url']);
|
||||
}
|
||||
|
||||
public function testBuildOgTagsPublishedTimeFormatted(): void
|
||||
{
|
||||
$ctrl = self::makeController();
|
||||
$data = ['title' => 'T', 'authors' => '', 'year' => '2025', 'files' => []];
|
||||
|
||||
$tags = $ctrl->exposedBuildOgTags($data, 1, 'desc');
|
||||
|
||||
$this->assertSame('2025-01-01', $tags['article_published_time']);
|
||||
}
|
||||
|
||||
public function testBuildOgTagsPublishedTimeEmptyWhenNoYear(): void
|
||||
{
|
||||
$ctrl = self::makeController();
|
||||
$data = ['title' => 'T', 'authors' => '', 'files' => []];
|
||||
|
||||
$tags = $ctrl->exposedBuildOgTags($data, 1, 'desc');
|
||||
|
||||
$this->assertSame('', $tags['article_published_time']);
|
||||
}
|
||||
|
||||
// ── buildMetaDescription() ────────────────────────────────────────────────
|
||||
|
||||
public function testBuildMetaDescriptionTruncatesLongSynopsis(): void
|
||||
{
|
||||
$ctrl = self::makeController();
|
||||
$long = str_repeat('abcdefghij', 20); // 200 chars
|
||||
$desc = $ctrl->exposedBuildMetaDescription($long);
|
||||
|
||||
$this->assertLessThanOrEqual(160, strlen($desc));
|
||||
$this->assertStringEndsWith('…', $desc);
|
||||
}
|
||||
|
||||
public function testBuildMetaDescriptionKeepsShortSynopsis(): void
|
||||
{
|
||||
$ctrl = self::makeController();
|
||||
$desc = $ctrl->exposedBuildMetaDescription('A short synopsis.');
|
||||
|
||||
$this->assertSame('A short synopsis.', $desc);
|
||||
}
|
||||
|
||||
public function testBuildMetaDescriptionEmptySynopsisReturnsDefault(): void
|
||||
{
|
||||
$ctrl = self::makeController();
|
||||
$desc = $ctrl->exposedBuildMetaDescription('');
|
||||
|
||||
$this->assertStringContainsString('Mémoire', $desc);
|
||||
$this->assertStringContainsString('XAMXAM', $desc);
|
||||
}
|
||||
|
||||
public function testBuildMetaDescriptionStripsHtmlTags(): void
|
||||
{
|
||||
$ctrl = self::makeController();
|
||||
$desc = $ctrl->exposedBuildMetaDescription('<p>Hello <b>world</b></p>');
|
||||
|
||||
$this->assertStringNotContainsString('<p>', $desc);
|
||||
$this->assertStringNotContainsString('<b>', $desc);
|
||||
$this->assertStringContainsString('Hello world', $desc);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user