Reduce cognitive complexity and fix code smells across multiple files: - Extract helper methods in DocuSealService, ForgotPasswordController, WebhookDocuSealController - Reduce MembresController.persistLocalUser from 8 to 3 parameters using typed array - Replace chained if/returns with ROLE_ROUTES map in LoginSuccessHandler - Add 100% test coverage for AnalyticsCryptoService (15 tests) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
151 lines
4.8 KiB
PHP
151 lines
4.8 KiB
PHP
<?php
|
|
|
|
namespace App\Tests\Service;
|
|
|
|
use App\Service\AnalyticsCryptoService;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
class AnalyticsCryptoServiceTest extends TestCase
|
|
{
|
|
private AnalyticsCryptoService $service;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->service = new AnalyticsCryptoService('test-secret-key');
|
|
}
|
|
|
|
public function testEncryptReturnsBase64String(): void
|
|
{
|
|
$data = ['page' => '/home', 'visits' => 42];
|
|
|
|
$encrypted = $this->service->encrypt($data);
|
|
|
|
$this->assertNotEmpty($encrypted);
|
|
$this->assertNotFalse(base64_decode($encrypted, true));
|
|
}
|
|
|
|
public function testEncryptThenDecryptReturnsOriginalData(): void
|
|
{
|
|
$data = ['page' => '/stats', 'count' => 10, 'nested' => ['a' => 1]];
|
|
|
|
$encrypted = $this->service->encrypt($data);
|
|
$decrypted = $this->service->decrypt($encrypted);
|
|
|
|
$this->assertSame($data, $decrypted);
|
|
}
|
|
|
|
public function testDecryptInvalidBase64ReturnsNull(): void
|
|
{
|
|
$this->assertNull($this->service->decrypt('!!!not-base64!!!'));
|
|
}
|
|
|
|
public function testDecryptTooShortPayloadReturnsNull(): void
|
|
{
|
|
// Less than 28 bytes after decode
|
|
$this->assertNull($this->service->decrypt(base64_encode('short')));
|
|
}
|
|
|
|
public function testDecryptCorruptedPayloadReturnsNull(): void
|
|
{
|
|
$data = ['test' => true];
|
|
$encrypted = $this->service->encrypt($data);
|
|
|
|
// Corrupt the payload by flipping bytes
|
|
$raw = base64_decode($encrypted, true);
|
|
$corrupted = $raw;
|
|
$corrupted[20] = chr(ord($corrupted[20]) ^ 0xFF);
|
|
$corrupted[21] = chr(ord($corrupted[21]) ^ 0xFF);
|
|
|
|
$this->assertNull($this->service->decrypt(base64_encode($corrupted)));
|
|
}
|
|
|
|
public function testDecryptJsFormatWithShortCiphertextReturnsNull(): void
|
|
{
|
|
// 28 bytes of zeros — neither JS nor PHP format can decrypt
|
|
$raw = str_repeat("\x00", 28);
|
|
|
|
$this->assertNull($this->service->decrypt(base64_encode($raw)));
|
|
}
|
|
|
|
public function testTryDecryptJsFormatWithTooShortInput(): void
|
|
{
|
|
$method = new \ReflectionMethod(AnalyticsCryptoService::class, 'tryDecryptJsFormat');
|
|
|
|
$result = $method->invoke($this->service, str_repeat("\x00", 12), 'short');
|
|
|
|
$this->assertNull($result);
|
|
}
|
|
|
|
public function testDecryptPhpFormatFallback(): void
|
|
{
|
|
// Build a PHP-format payload: iv (12) + tag (16) + ciphertext
|
|
$data = ['fallback' => true];
|
|
$json = json_encode($data);
|
|
$key = substr(hash('sha256', 'test-secret-key', true), 0, 32);
|
|
$iv = random_bytes(12);
|
|
$encrypted = openssl_encrypt($json, 'aes-256-gcm', $key, \OPENSSL_RAW_DATA, $iv, $tag, '', 16);
|
|
|
|
// PHP format: iv + tag + ciphertext
|
|
$phpFormat = base64_encode($iv . $tag . $encrypted);
|
|
|
|
$this->assertSame($data, $this->service->decrypt($phpFormat));
|
|
}
|
|
|
|
public function testGenerateVisitorHashReturnsDeterministicHash(): void
|
|
{
|
|
$hash1 = $this->service->generateVisitorHash('user-123');
|
|
$hash2 = $this->service->generateVisitorHash('user-123');
|
|
|
|
$this->assertSame($hash1, $hash2);
|
|
$this->assertSame(64, \strlen($hash1)); // SHA-256 hex = 64 chars
|
|
}
|
|
|
|
public function testGenerateVisitorHashDiffersForDifferentUids(): void
|
|
{
|
|
$hash1 = $this->service->generateVisitorHash('user-123');
|
|
$hash2 = $this->service->generateVisitorHash('user-456');
|
|
|
|
$this->assertNotSame($hash1, $hash2);
|
|
}
|
|
|
|
public function testVerifyVisitorHashReturnsTrueForValidHash(): void
|
|
{
|
|
$uid = 'visitor-abc';
|
|
$hash = $this->service->generateVisitorHash($uid);
|
|
|
|
$this->assertTrue($this->service->verifyVisitorHash($uid, $hash));
|
|
}
|
|
|
|
public function testVerifyVisitorHashReturnsFalseForInvalidHash(): void
|
|
{
|
|
$this->assertFalse($this->service->verifyVisitorHash('visitor-abc', 'wrong-hash'));
|
|
}
|
|
|
|
public function testGetKeyForJsReturnsBase64EncodedKey(): void
|
|
{
|
|
$jsKey = $this->service->getKeyForJs();
|
|
|
|
$this->assertNotEmpty($jsKey);
|
|
$decoded = base64_decode($jsKey, true);
|
|
$this->assertNotFalse($decoded);
|
|
$this->assertSame(32, \strlen($decoded)); // AES-256 key = 32 bytes
|
|
}
|
|
|
|
public function testDifferentSecretsProduceDifferentKeys(): void
|
|
{
|
|
$service2 = new AnalyticsCryptoService('different-secret');
|
|
|
|
$this->assertNotSame($this->service->getKeyForJs(), $service2->getKeyForJs());
|
|
}
|
|
|
|
public function testEncryptedByOneServiceCannotBeDecryptedByAnother(): void
|
|
{
|
|
$service2 = new AnalyticsCryptoService('different-secret');
|
|
$data = ['secret' => 'data'];
|
|
|
|
$encrypted = $this->service->encrypt($data);
|
|
|
|
$this->assertNull($service2->decrypt($encrypted));
|
|
}
|
|
}
|