mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +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:
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');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user