8fb3086792
Signed-off-by: Sebastian <krupinski01@gmail.com>
154 lines
4.4 KiB
PHP
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);
|
|
}
|
|
}
|