dir = sys_get_temp_dir().'/fn-store-test-'.bin2hex(random_bytes(6)); mkdir($this->dir, 0700); } protected function tearDown(): void { $files = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->dir, \FilesystemIterator::SKIP_DOTS), \RecursiveIteratorIterator::CHILD_FIRST); foreach ($files as $f) { if ($f->isDir()) rmdir($f->getPathname()); else unlink($f->getPathname()); } rmdir($this->dir); } private function fixture(string $value, int $count = 10): array { return ['files' => ['site/v2ray' => $value], 'report' => ['counts' => ['v2ray' => $count]]]; } public function testRollbackRestoresPreviousReleaseAndPublicFile(): void { $store = new ReleaseStore($this->dir.'/private', $this->dir.'/public'); $a = $store->stage($this->fixture('first'), date('Y-m-d')); $store->publishPrepared(); $b = $store->stage($this->fixture('second'), date('Y-m-d')); $store->publishPrepared(); self::assertSame(['v2ray' => 'second'], $store->readSite('site', ['v2ray'])); $store->activate($store->previous()['id']); self::assertSame($a['id'], $store->current()['id']); self::assertSame('first', file_get_contents($this->dir.'/public/site/v2ray/'.date('Y-m-d').'.txt')); } public function testCountDropDoesNotReplaceCurrent(): void { $store = new ReleaseStore($this->dir.'/private', $this->dir.'/public'); $a = $store->stage($this->fixture('first'), date('Y-m-d')); $store->publishPrepared(); try { $store->stage($this->fixture('empty', 1), date('Y-m-d')); self::fail(); } catch (RuntimeException $e) { self::assertStringContainsString('count_drop_guard', $e->getMessage()); } self::assertSame($a['id'], $store->current()['id']); } public function testTamperedFilesCannotBePublished(): void { $store = new ReleaseStore($this->dir.'/private', $this->dir.'/public'); $a = $store->stage($this->fixture('first'), date('Y-m-d')); file_put_contents($this->dir.'/private/versions/'.$a['id'].'/site/v2ray/'.date('Y-m-d').'.txt', 'tampered'); $this->expectException(RuntimeException::class); $store->publishPrepared(); } public function testFirstPublishCapturesLegacyBytesForRollback(): void { $public = $this->dir.'/public/site/v2ray'; mkdir($public, 0700, true); file_put_contents($public.'/'.date('Y-m-d').'.txt', 'legacy'); $store = new ReleaseStore($this->dir.'/private', $this->dir.'/public'); $store->stage($this->fixture('new'), date('Y-m-d')); $store->publishPrepared(); $legacy = $store->previous(); self::assertFalse($legacy['validated']); $store->activate($legacy['id']); self::assertSame('legacy', file_get_contents($public.'/'.date('Y-m-d').'.txt')); } public function testPublicationFailureRestoresPublicBytesAndKeepsCurrent(): void { $store = new ReleaseStore($this->dir.'/private', $this->dir.'/public'); $a = $store->stage($this->fixture('first'), date('Y-m-d')); $store->publishPrepared(); $store->stage($this->fixture('second'), date('Y-m-d')); mkdir($this->dir.'/private/previous.json'); try { $store->publishPrepared(); self::fail(); } catch (RuntimeException $e) { self::assertSame('artifact_rename_failed', $e->getMessage()); } self::assertSame($a['id'], $store->current()['id']); self::assertSame('first', file_get_contents($this->dir.'/public/site/v2ray/'.date('Y-m-d').'.txt')); } }