set('services.etsy', ['keystring' => 'key', 'shared_secret' => 'secret', 'shop_name' => 'HickLab3D']); return EtsyConnection::create([ 'shop_id' => 42, 'shop_name' => 'HickLab3D', 'access_token' => 'token', 'refresh_token' => 'refresh', 'expires_at' => now()->addHour(), 'scopes' => 'listings_r listings_w transactions_r transactions_w shops_r', ]); } public function test_admin_can_attach_a_verified_listing_by_reading_it_from_etsy(): void { Http::preventStrayRequests(); $this->connectEtsy(); Http::fake(['https://api.etsy.com/v3/application/listings/999*' => Http::response([ 'listing_id' => 999, 'shop_id' => 42, 'title' => 'Dragon figurine', 'state' => 'active', 'last_modified_timestamp' => now()->timestamp, ])]); $admin = User::factory()->create(['role' => 'admin']); $project = Project::factory()->create(); $this->actingAs($admin)->post(route('projects.listings.store', $project), ['listing_id' => 999]) ->assertRedirect(); $this->assertDatabaseHas('project_listing_links', [ 'project_id' => $project->id, 'shop_id' => 42, 'listing_id' => 999, 'title_snapshot' => 'Dragon figurine', 'state_snapshot' => 'active', ]); } public function test_rejects_a_listing_that_belongs_to_a_different_shop(): void { Http::preventStrayRequests(); $this->connectEtsy(); Http::fake(['https://api.etsy.com/v3/application/listings/555*' => Http::response([ 'listing_id' => 555, 'shop_id' => 999999, 'title' => 'Not ours', 'state' => 'active', ])]); $admin = User::factory()->create(['role' => 'admin']); $project = Project::factory()->create(); $this->actingAs($admin)->post(route('projects.listings.store', $project), ['listing_id' => 555]) ->assertSessionHasErrors('listing_id'); $this->assertDatabaseCount('project_listing_links', 0); } public function test_rejects_a_nonexistent_listing(): void { Http::preventStrayRequests(); $this->connectEtsy(); Http::fake(['https://api.etsy.com/v3/application/listings/404*' => Http::response(['error' => 'not found'], 404)]); $admin = User::factory()->create(['role' => 'admin']); $project = Project::factory()->create(); $this->actingAs($admin)->post(route('projects.listings.store', $project), ['listing_id' => 404]) ->assertSessionHasErrors('listing_id'); } public function test_cannot_attach_the_same_listing_to_a_different_project(): void { Http::preventStrayRequests(); $this->connectEtsy(); Http::fake(['https://api.etsy.com/v3/application/listings/999*' => Http::response([ 'listing_id' => 999, 'shop_id' => 42, 'title' => 'Dragon figurine', 'state' => 'active', ])]); $admin = User::factory()->create(['role' => 'admin']); $projectA = Project::factory()->create(); $projectB = Project::factory()->create(); ProjectListingLink::factory()->for($projectA)->create(['shop_id' => 42, 'listing_id' => 999, 'variation_key' => 'all']); $this->actingAs($admin)->post(route('projects.listings.store', $projectB), ['listing_id' => 999]) ->assertSessionHasErrors('listing_id'); } public function test_reattaching_same_listing_to_same_project_does_not_duplicate(): void { Http::preventStrayRequests(); $this->connectEtsy(); Http::fake(['https://api.etsy.com/v3/application/listings/999*' => Http::response([ 'listing_id' => 999, 'shop_id' => 42, 'title' => 'Dragon figurine v2', 'state' => 'active', ])]); $admin = User::factory()->create(['role' => 'admin']); $project = Project::factory()->create(); ProjectListingLink::factory()->for($project)->create(['shop_id' => 42, 'listing_id' => 999, 'variation_key' => 'all', 'title_snapshot' => 'old title']); $this->actingAs($admin)->post(route('projects.listings.store', $project), ['listing_id' => 999])->assertRedirect(); $this->assertDatabaseCount('project_listing_links', 1); $this->assertDatabaseHas('project_listing_links', ['listing_id' => 999, 'title_snapshot' => 'Dragon figurine v2']); } public function test_detaching_a_listing_preserves_its_release_history(): void { $admin = User::factory()->create(['role' => 'admin']); $project = Project::factory()->create(); $link = ProjectListingLink::factory()->for($project)->create(); $this->actingAs($admin)->delete(route('projects.listings.destroy', [$project, $link]))->assertRedirect(); $link->refresh(); $this->assertNotNull($link->detached_at); $this->assertModelExists($link); } public function test_attaching_a_new_unready_mapping_to_an_already_live_project_drops_it_back_to_design(): void { Http::preventStrayRequests(); $this->connectEtsy(); Http::fake(['https://api.etsy.com/v3/application/listings/999*' => Http::response([ 'listing_id' => 999, 'shop_id' => 42, 'title' => 'Dragon figurine', 'state' => 'active', ])]); $admin = User::factory()->create(['role' => 'admin']); $project = Project::factory()->create(['stage' => 'live']); $existingLink = ProjectListingLink::factory()->for($project)->create(['live_release_id' => null]); $release = ProjectRelease::factory()->for($project)->create(); $existingLink->update(['live_release_id' => $release->id]); $this->actingAs($admin)->post(route('projects.listings.store', $project), ['listing_id' => 999]) ->assertRedirect(); $this->assertSame('design', $project->fresh()->stage); $this->assertDatabaseHas('project_events', ['project_id' => $project->id, 'event_type' => 'stage_changed']); } public function test_rejects_an_arbitrary_variation_key_since_per_variant_mapping_is_not_implemented(): void { Http::preventStrayRequests(); $this->connectEtsy(); Http::fake(['https://api.etsy.com/v3/application/listings/999*' => Http::response([ 'listing_id' => 999, 'shop_id' => 42, 'title' => 'Dragon figurine', 'state' => 'active', ])]); $admin = User::factory()->create(['role' => 'admin']); $project = Project::factory()->create(); $this->actingAs($admin)->post(route('projects.listings.store', $project), [ 'listing_id' => 999, 'variation_key' => 'color:red', ])->assertSessionHasErrors('variation_key'); $this->assertDatabaseCount('project_listing_links', 0); } public function test_rejects_a_listing_that_is_inactive_rather_than_live_or_draft(): void { Http::preventStrayRequests(); $this->connectEtsy(); Http::fake(['https://api.etsy.com/v3/application/listings/777*' => Http::response([ 'listing_id' => 777, 'shop_id' => 42, 'title' => 'Old item', 'state' => 'inactive', ])]); $admin = User::factory()->create(['role' => 'admin']); $project = Project::factory()->create(); $this->actingAs($admin)->post(route('projects.listings.store', $project), ['listing_id' => 777]) ->assertSessionHasErrors('listing_id'); $this->assertDatabaseCount('project_listing_links', 0); } public function test_rejects_a_sold_out_listing(): void { Http::preventStrayRequests(); $this->connectEtsy(); Http::fake(['https://api.etsy.com/v3/application/listings/778*' => Http::response([ 'listing_id' => 778, 'shop_id' => 42, 'title' => 'Sold out item', 'state' => 'sold_out', ])]); $admin = User::factory()->create(['role' => 'admin']); $project = Project::factory()->create(); $this->actingAs($admin)->post(route('projects.listings.store', $project), ['listing_id' => 778]) ->assertSessionHasErrors('listing_id'); $this->assertDatabaseCount('project_listing_links', 0); } public function test_non_admin_cannot_attach_listings(): void { $project = Project::factory()->create(); $this->actingAs(User::factory()->create())->post(route('projects.listings.store', $project), ['listing_id' => 1]) ->assertForbidden(); } }