feat: Http curl wrapper and TokenStore with atomic write and flock
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Weerscherm\Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Weerscherm\TokenStore;
|
||||
|
||||
final class TokenStoreTest extends TestCase
|
||||
{
|
||||
private string $dir;
|
||||
private string $file;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->dir = sys_get_temp_dir() . '/weerscherm-token-' . uniqid();
|
||||
$this->file = $this->dir . '/netatmo-token.json';
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
array_map('unlink', glob($this->dir . '/*') ?: []);
|
||||
@rmdir($this->dir);
|
||||
}
|
||||
|
||||
public function testWriteReadRoundTripAndMode(): void
|
||||
{
|
||||
$store = new TokenStore($this->file);
|
||||
$store->write(['access_token' => 'a', 'refresh_token' => 'r', 'expires_at' => 123]);
|
||||
|
||||
self::assertSame(['access_token' => 'a', 'refresh_token' => 'r', 'expires_at' => 123], $store->read());
|
||||
self::assertSame('0600', substr(sprintf('%o', fileperms($this->file)), -4));
|
||||
self::assertSame([], glob($this->dir . '/*.tmp.*') ?: [], 'geen tijdelijke bestanden achtergelaten');
|
||||
}
|
||||
|
||||
public function testMissingOrInvalidFileReadsNull(): void
|
||||
{
|
||||
self::assertNull((new TokenStore($this->file))->read());
|
||||
|
||||
mkdir($this->dir, 0700, true);
|
||||
file_put_contents($this->file, '{"access_token":"a"}');
|
||||
self::assertNull((new TokenStore($this->file))->read(), 'zonder refresh token is het bestand onbruikbaar');
|
||||
}
|
||||
|
||||
public function testWithLockReturnsValueAndReleasesAfterException(): void
|
||||
{
|
||||
$store = new TokenStore($this->file);
|
||||
|
||||
self::assertSame(42, $store->withLock(fn() => 42));
|
||||
|
||||
try {
|
||||
$store->withLock(function (): void {
|
||||
throw new \RuntimeException('boem');
|
||||
});
|
||||
self::fail('exception verwacht');
|
||||
} catch (\RuntimeException) {
|
||||
}
|
||||
|
||||
$handle = fopen($this->file . '.lock', 'c');
|
||||
self::assertTrue(flock($handle, LOCK_EX | LOCK_NB), 'lock is weer vrij');
|
||||
fclose($handle);
|
||||
}
|
||||
|
||||
public function testConcurrentProcessesAreSerialised(): void
|
||||
{
|
||||
$store = new TokenStore($this->file);
|
||||
$script = sprintf(
|
||||
'<?php require %s; $s = new Weerscherm\TokenStore(%s); $s->withLock(function () { usleep(400000); });',
|
||||
var_export(__DIR__ . '/../vendor/autoload.php', true),
|
||||
var_export($this->file, true),
|
||||
);
|
||||
$scriptFile = $this->dir . '/hold.php';
|
||||
mkdir($this->dir, 0700, true);
|
||||
file_put_contents($scriptFile, $script);
|
||||
|
||||
$proc = proc_open([PHP_BINARY, $scriptFile], [], $pipes);
|
||||
usleep(150000); // geef het andere proces tijd om de lock te pakken
|
||||
|
||||
$start = microtime(true);
|
||||
$store->withLock(fn() => null);
|
||||
$waited = microtime(true) - $start;
|
||||
proc_close($proc);
|
||||
|
||||
self::assertGreaterThan(0.15, $waited, 'tweede proces moest wachten op de lock');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user