Files
mail/tests/php/Unit/CompositionStoreTest.php
T

154 lines
4.4 KiB
PHP

<?php
declare(strict_types=1);
namespace KTXT\Mail\Tests\Unit;
use JsonException;
use KTXM\Mail\Stores\CompositionStore;
use PHPUnit\Framework\TestCase;
final class CompositionStoreTest extends TestCase {
private const TENANT_ID = 'tenant-1';
private const USER_ID = 'user-1';
private const COMPOSITION_ID = 'draft-1';
private string $rootDir;
private CompositionStore $store;
protected function setUp(): void {
$this->rootDir = sys_get_temp_dir() . '/ktrix-mail-store-test-' . uniqid('', true);
mkdir($this->rootDir, 0755, true);
$this->store = new CompositionStore($this->rootDir);
}
protected function tearDown(): void {
$this->deleteDir($this->rootDir);
}
public function testFailedReplacementPreservesPreviousSnapshot(): void {
$initial = ['identifier' => self::COMPOSITION_ID, 'revision' => 1];
$this->saveSnapshot($initial);
$resource = fopen('php://memory', 'r');
try {
$saveInvalidSnapshot = static fn(): array => [
'identifier' => self::COMPOSITION_ID,
'invalid' => $resource,
];
$this->store->compositionSave(
self::TENANT_ID,
self::USER_ID,
self::COMPOSITION_ID,
$saveInvalidSnapshot,
);
$this->fail('Saving a value that cannot be encoded as JSON should fail');
} catch (JsonException) {
$this->assertSame(
$initial,
$this->store->compositionFetch(self::TENANT_ID, self::USER_ID, self::COMPOSITION_ID),
);
} finally {
fclose($resource);
}
}
public function testUpdateCanLeaveSnapshotUnchanged(): void {
$initial = ['identifier' => self::COMPOSITION_ID, 'revision' => 1];
$this->saveSnapshot($initial);
$leaveUnchanged = static fn(?array $snapshot): ?array => null;
$updated = $this->store->compositionSave(
self::TENANT_ID,
self::USER_ID,
self::COMPOSITION_ID,
$leaveUnchanged,
);
$this->assertNull($updated);
$this->assertSame(
$initial,
$this->store->compositionFetch(self::TENANT_ID, self::USER_ID, self::COMPOSITION_ID),
);
}
public function testConcurrentUpdatesDoNotLoseChanges(): void {
if (!function_exists('pcntl_fork')) {
$this->markTestSkipped('The pcntl extension is required for the concurrency test');
}
$this->saveSnapshot([
'identifier' => self::COMPOSITION_ID,
'updates' => 0,
]);
$childPid = pcntl_fork();
if ($childPid === -1) {
$this->fail('Unable to fork the concurrency test process');
}
if ($childPid === 0) {
$this->incrementAfterDelay();
exit(0);
}
$this->incrementAfterDelay();
pcntl_waitpid($childPid, $status);
$this->assertTrue(pcntl_wifexited($status));
$this->assertSame(0, pcntl_wexitstatus($status));
$this->assertSame(
2,
$this->store->compositionFetch(self::TENANT_ID, self::USER_ID, self::COMPOSITION_ID)['updates'],
);
}
private function incrementAfterDelay(): void {
$incrementSnapshot = static function (?array $snapshot): array {
usleep(200_000);
$snapshot['updates']++;
return $snapshot;
};
$this->store->compositionSave(
self::TENANT_ID,
self::USER_ID,
self::COMPOSITION_ID,
$incrementSnapshot,
);
}
private function saveSnapshot(array $snapshot): void {
$saveSnapshot = static fn(): array => $snapshot;
$this->store->compositionSave(
self::TENANT_ID,
self::USER_ID,
self::COMPOSITION_ID,
$saveSnapshot,
);
}
private function deleteDir(string $path): void {
if (!is_dir($path)) {
return;
}
foreach (scandir($path) ?: [] as $entry) {
if ($entry === '.' || $entry === '..') {
continue;
}
$child = $path . '/' . $entry;
if (is_dir($child)) {
$this->deleteDir($child);
continue;
}
unlink($child);
}
rmdir($path);
}
}