app['env'] = 'production'; $this->dir = sys_get_temp_dir().'/delivery-fna-'.bin2hex(random_bytes(8)); mkdir($this->dir, 0700, true); mkdir($this->dir.'/public'); file_put_contents($this->dir.'/credential.json', json_encode(['project' => 'fna-v3', 'base_url' => 'https://resource-v2.gemcook.net', 'token' => 'rsv2_'.str_repeat('a', 64)])); config(['illustrations.enabled' => true, 'illustrations.local_enabled' => true, 'illustrations.delivery_enabled' => true, 'illustrations.base_url' => 'https://resource-v2.gemcook.net', 'illustrations.credential_file' => $this->dir.'/credential.json', 'illustrations.local_directory' => $this->dir.'/public', 'illustrations.local_manifest' => $this->dir.'/manifest.json']); $this->sha = hash('sha256', 'verified-copy'); file_put_contents($this->dir.'/public/'.$this->sha.'.webp', 'verified-copy'); file_put_contents($this->dir.'/manifest.json', json_encode(['version' => 1, 'assets' => [$this->asset => ['sha256' => $this->sha, 'mime' => 'image/webp', 'bytes' => strlen('verified-copy'), 'url' => '/images/illustrations/'.$this->sha.'.webp']], 'selections' => ['freenode' => ['old-article' => 'old-asset']]])); Http::preventStrayRequests(); Artisan::all(); \Illuminate\Support\Facades\Schema::create('articles', function ($t) { $t->id(); $t->integer('iCategoryId'); $t->integer('iStatus'); $t->string('sSlug'); $t->timestamp('created_at')->nullable(); }); } protected function tearDown(): void { File::deleteDirectory($this->dir); parent::tearDown(); } private function fake(int $sync = 0, int $ack = 200): void { Http::fake([ 'https://resource-v2.gemcook.net/api/v2/pools/*' => Http::response(['data' => ['items' => [['asset_id' => $this->asset, 'url' => 'https://resource-v2.gemcook.net/media/'.$this->id]], 'selections' => [], 'unavailable' => [], 'next' => null]], $sync ? 503 : 200), 'https://resource-v2.gemcook.net/api/v2/illustration-deliveries' => Http::response(['data' => [['id' => $this->id, 'state' => 'awaiting_sync', 'fingerprint' => str_repeat('b', 64), 'assets' => [['asset_id' => $this->asset, 'sha256' => $this->sha, 'pool' => 'freenode']]]]]), 'https://resource-v2.gemcook.net/api/v2/illustration-deliveries/*/ack' => Http::response(['data' => ['state' => 'completed']], $ack), ]); } public function test_verified_copy_is_acknowledged_and_lost_ack_can_retry(): void { $this->fake(0, 503); $this->artisan('illustrations:sync-deliveries')->assertExitCode(1); Http::swap(new \Illuminate\Http\Client\Factory); Http::preventStrayRequests(); $this->fake(); $this->artisan('illustrations:sync-deliveries')->assertExitCode(0); $this->assertSame('old-asset', json_decode(file_get_contents($this->dir.'/manifest.json'), true)['selections']['freenode']['old-article']); Http::assertSent(fn ($r) => str_ends_with($r->url(), '/ack') && $r['fingerprint'] === str_repeat('b', 64) && $r['manifest_sha256'] === hash_file('sha256', $this->dir.'/manifest.json')); } public function test_failed_sync_sends_no_ack(): void { $this->fake(1); $this->artisan('illustrations:sync-deliveries')->assertExitCode(1); Http::assertNotSent(fn ($r) => str_ends_with($r->url(), '/ack')); } public function test_corrupt_copy_sends_no_ack(): void { $this->fake(); file_put_contents($this->dir.'/public/'.$this->sha.'.webp', 'corrupt'); $this->artisan('illustrations:sync-deliveries')->assertExitCode(1); Http::assertNotSent(fn ($r) => str_ends_with($r->url(), '/ack')); } public function test_disabled_worker_does_not_contact_resource(): void { config(['illustrations.delivery_enabled' => false]); $this->artisan('illustrations:sync-deliveries')->assertExitCode(1); Http::assertNothingSent(); } }