Http::response([], 500)); $this->artisan('etsy:sync')->assertExitCode(0); Http::assertNothingSent(); } public function test_bundled_refresh_does_not_duplicate_the_independently_scheduled_etsy_sync(): void { $this->connectShop(); Http::fake(); $this->artisan('integrations:refresh')->assertExitCode(0); Http::assertNotSent(fn ($request): bool => str_contains($request->url(), 'etsy.com')); } public function test_etsy_sync_refreshes_receipts_and_listings(): void { $this->connectShop(); Http::fake([ '*/receipts*' => Http::response(['count' => 1, 'results' => [[ 'receipt_id' => 1, 'status' => 'open', 'was_paid' => true, 'was_shipped' => false, ]]]), '*/listings*' => Http::response(['count' => 1, 'results' => [['listing_id' => 11]]]), ]); $this->artisan('etsy:sync')->assertExitCode(0); Http::assertSentCount(2); } public function test_etsy_sync_returns_failure_if_a_required_refresh_fails(): void { $this->connectShop(); Http::fake([ '*/receipts*' => Http::response([], 503), '*/listings*' => Http::response(['results' => []]), ]); $this->artisan('etsy:sync')->assertExitCode(1); Http::assertSentCount(2); } public function test_etsy_sync_reports_failure_instead_of_claiming_stale_cache_was_refreshed(): void { $this->connectShop(); Http::fake([ '*/receipts*' => Http::sequence()->push(['results' => []])->push([], 503), '*/listings*' => Http::sequence()->push(['results' => []])->push([], 503), ]); $this->artisan('etsy:sync')->assertExitCode(0); $this->artisan('etsy:sync')->assertExitCode(1); } public function test_etsy_refresh_token_skips_cleanly_when_shop_is_not_connected(): void { Http::fake(fn () => Http::response([], 500)); $this->artisan('etsy:refresh-token')->assertExitCode(0); Http::assertNothingSent(); } public function test_etsy_refresh_token_proactively_refreshes_an_expired_token(): void { $connection = $this->connectShop(expiresAt: now()->subMinute()); Http::fake([ '*/oauth/token' => Http::response([ 'access_token' => 'new-access-token', 'refresh_token' => 'new-refresh-token', 'expires_in' => 3600, ]), ]); $this->artisan('etsy:refresh-token')->assertExitCode(0); Http::assertSentCount(1); $connection->refresh(); $this->assertSame('new-access-token', $connection->access_token); $this->assertTrue($connection->expires_at->isFuture()); } public function test_etsy_refresh_token_does_not_call_etsy_when_token_is_still_fresh(): void { $this->connectShop(expiresAt: now()->addMinutes(30)); Http::fake(); $this->artisan('etsy:refresh-token')->assertExitCode(0); Http::assertNothingSent(); } public function test_refreshes_before_expiry_and_does_not_reuse_a_stale_rotating_token(): void { $this->freezeTime(); $connection = $this->connectShop(expiresAt: now()->addMinutes(8)); $stale = EtsyConnection::findOrFail($connection->id); Http::preventStrayRequests(); Http::fake(['*/oauth/token' => Http::response([ 'access_token' => 'rotated-access', 'refresh_token' => 'rotated-refresh', 'expires_in' => 3600, ])]); $etsy = app(Etsy::class); $this->assertSame('rotated-access', $etsy->refreshAccessToken($connection)); $this->assertSame('rotated-access', $etsy->refreshAccessToken($stale)); Http::assertSentCount(1); $this->assertSame('rotated-refresh', $connection->fresh()->refresh_token); } private function connectShop(?Carbon $expiresAt = null): EtsyConnection { EtsyAppCredential::create([ 'id' => 1, 'keystring' => 'key', 'shared_secret' => 'secret', 'shop_name' => 'HickLab3D', ]); return EtsyConnection::create([ 'shop_id' => 123, 'shop_name' => 'HickLab3D', 'access_token' => 'token', 'refresh_token' => 'refresh', 'expires_at' => $expiresAt ?? now()->addHour(), 'scopes' => 'listings_r listings_w transactions_r transactions_w shops_r', ]); } }