Add project listing selector, confirmed revision and private 3MF renders
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\EtsyConnection;
|
||||
use App\Models\Project;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ProjectListingOptionsTest extends TestCase
|
||||
{
|
||||
use LazilyRefreshDatabase;
|
||||
|
||||
private function connectEtsy(): void
|
||||
{
|
||||
config()->set('services.etsy', ['keystring' => 'key', 'shared_secret' => 'secret', 'shop_name' => 'HickLab3D']);
|
||||
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_dropdown_shows_live_and_draft_listings_together_in_optgroups(): void
|
||||
{
|
||||
Http::preventStrayRequests();
|
||||
$this->connectEtsy();
|
||||
Http::fake([
|
||||
'https://api.etsy.com/v3/application/shops/42/listings*' => Http::sequence()
|
||||
->push(['count' => 1, 'results' => [['listing_id' => 101, 'shop_id' => 42, 'title' => 'Dragon figurine', 'state' => 'active']]])
|
||||
->push(['count' => 1, 'results' => [['listing_id' => 202, 'shop_id' => 42, 'title' => 'Draft figurine', 'state' => 'draft']]]),
|
||||
]);
|
||||
$admin = User::factory()->create(['role' => 'admin']);
|
||||
$project = Project::factory()->create();
|
||||
|
||||
$this->actingAs($admin)->get(route('projects.show', $project))->assertOk()
|
||||
->assertSee('<optgroup label="Live">', false)
|
||||
->assertSee('<optgroup label="Draft">', false)
|
||||
->assertSee('Dragon figurine (#101)', false)
|
||||
->assertSee('Draft figurine (#202)', false);
|
||||
Http::assertSentCount(2);
|
||||
}
|
||||
|
||||
public function test_duplicate_listing_pages_never_masquerade_as_a_complete_dropdown(): void
|
||||
{
|
||||
Http::preventStrayRequests();
|
||||
$this->connectEtsy();
|
||||
$first = array_map(fn (int $i) => ['listing_id' => $i, 'shop_id' => 42, 'title' => "Item {$i}", 'state' => 'active'], range(1, 100));
|
||||
Http::fake(['https://api.etsy.com/v3/application/shops/42/listings*' => Http::sequence()
|
||||
->push(['count' => 150, 'results' => $first])
|
||||
->push(['count' => 150, 'results' => array_slice($first, 50)])
|
||||
->push(['count' => 0, 'results' => []])]);
|
||||
$this->actingAs(User::factory()->create(['role' => 'admin']))
|
||||
->get(route('projects.show', Project::factory()->create()))->assertOk()
|
||||
->assertSee('temporarily unavailable or incomplete')->assertDontSee('<optgroup', false);
|
||||
}
|
||||
|
||||
public function test_fetches_every_page_of_a_state_with_over_one_hundred_listings(): void
|
||||
{
|
||||
Http::preventStrayRequests();
|
||||
$this->connectEtsy();
|
||||
$page1 = array_map(fn (int $i) => ['listing_id' => $i, 'shop_id' => 42, 'title' => "Active Item {$i}", 'state' => 'active'], range(1, 100));
|
||||
$page2 = array_map(fn (int $i) => ['listing_id' => $i, 'shop_id' => 42, 'title' => "Active Item {$i}", 'state' => 'active'], range(101, 150));
|
||||
Http::fake([
|
||||
'https://api.etsy.com/v3/application/shops/42/listings*' => Http::sequence()
|
||||
->push(['count' => 150, 'results' => $page1])
|
||||
->push(['count' => 150, 'results' => $page2])
|
||||
->push(['count' => 0, 'results' => []]),
|
||||
]);
|
||||
$admin = User::factory()->create(['role' => 'admin']);
|
||||
$project = Project::factory()->create();
|
||||
|
||||
$this->actingAs($admin)->get(route('projects.show', $project))->assertOk()
|
||||
->assertSee('Active Item 150 (#150)', false);
|
||||
Http::assertSentCount(3);
|
||||
Http::assertSent(fn ($request) => ($request['offset'] ?? null) === 0 && str_contains($request->url(), '/listings'));
|
||||
Http::assertSent(fn ($request) => ($request['offset'] ?? null) === 100 && str_contains($request->url(), '/listings'));
|
||||
}
|
||||
|
||||
public function test_fails_explicitly_rather_than_silently_truncating_when_a_state_exceeds_the_page_bound(): void
|
||||
{
|
||||
Http::preventStrayRequests();
|
||||
$this->connectEtsy();
|
||||
// Every page claims 5000 total but only ever returns another 100 — never converges within the bound.
|
||||
$hugePage = array_map(fn (int $i) => ['listing_id' => $i, 'shop_id' => 42, 'title' => "Item {$i}", 'state' => 'active'], range(1, 100));
|
||||
Http::fake(['https://api.etsy.com/v3/application/shops/42/listings*' => Http::response(['count' => 5000, 'results' => $hugePage])]);
|
||||
$admin = User::factory()->create(['role' => 'admin']);
|
||||
$project = Project::factory()->create();
|
||||
|
||||
$this->actingAs($admin)->get(route('projects.show', $project))->assertOk()
|
||||
->assertSee('temporarily unavailable or incomplete')
|
||||
->assertDontSee('<optgroup', false);
|
||||
// 20 pages fetched (the bound) before failing explicitly — never an unbounded fetch loop.
|
||||
Http::assertSentCount(20);
|
||||
}
|
||||
|
||||
public function test_fails_explicitly_when_a_page_comes_back_short_of_the_reported_total(): void
|
||||
{
|
||||
Http::preventStrayRequests();
|
||||
$this->connectEtsy();
|
||||
Http::fake(['https://api.etsy.com/v3/application/shops/42/listings*' => Http::response(['count' => 200, 'results' => []])]);
|
||||
$admin = User::factory()->create(['role' => 'admin']);
|
||||
$project = Project::factory()->create();
|
||||
|
||||
$this->actingAs($admin)->get(route('projects.show', $project))->assertOk()
|
||||
->assertSee('temporarily unavailable or incomplete')
|
||||
->assertDontSee('<optgroup', false);
|
||||
}
|
||||
|
||||
public function test_zero_results_in_both_states_is_shown_as_an_explicit_empty_state_not_an_error(): void
|
||||
{
|
||||
Http::preventStrayRequests();
|
||||
$this->connectEtsy();
|
||||
Http::fake(['https://api.etsy.com/v3/application/shops/42/listings*' => Http::response(['count' => 0, 'results' => []])]);
|
||||
$admin = User::factory()->create(['role' => 'admin']);
|
||||
$project = Project::factory()->create();
|
||||
|
||||
$this->actingAs($admin)->get(route('projects.show', $project))->assertOk()
|
||||
->assertSee('No live or draft listings found')
|
||||
->assertDontSee('temporarily unavailable');
|
||||
}
|
||||
|
||||
public function test_no_etsy_connection_shows_an_explicit_message_instead_of_an_empty_dropdown(): void
|
||||
{
|
||||
$admin = User::factory()->create(['role' => 'admin']);
|
||||
$project = Project::factory()->create();
|
||||
|
||||
$this->actingAs($admin)->get(route('projects.show', $project))->assertOk()
|
||||
->assertSee('Connect the Etsy shop');
|
||||
}
|
||||
|
||||
public function test_show_page_never_makes_a_real_network_call(): void
|
||||
{
|
||||
Http::preventStrayRequests();
|
||||
$this->connectEtsy();
|
||||
Http::fake(['https://api.etsy.com/v3/application/shops/42/listings*' => Http::response(['count' => 0, 'results' => []])]);
|
||||
$admin = User::factory()->create(['role' => 'admin']);
|
||||
$project = Project::factory()->create();
|
||||
|
||||
$this->actingAs($admin)->get(route('projects.show', $project))->assertOk();
|
||||
}
|
||||
|
||||
/**
|
||||
* The dropdown's rendered options are just a convenience; the server
|
||||
* must never trust that a submitted listing_id was actually one of
|
||||
* them. A tampered id the dropdown never offered still goes through the
|
||||
* same live ownership/state re-verification as any other submission.
|
||||
*/
|
||||
public function test_submitting_a_listing_id_absent_from_the_rendered_dropdown_is_still_independently_verified(): void
|
||||
{
|
||||
Http::preventStrayRequests();
|
||||
$this->connectEtsy();
|
||||
Http::fake([
|
||||
'https://api.etsy.com/v3/application/shops/42/listings*' => Http::response(['count' => 1, 'results' => [
|
||||
['listing_id' => 101, 'shop_id' => 42, 'title' => 'Dragon figurine', 'state' => 'active'],
|
||||
]]),
|
||||
'https://api.etsy.com/v3/application/listings/999*' => Http::response([
|
||||
'listing_id' => 999, 'shop_id' => 999999, 'title' => 'Not ours', 'state' => 'active',
|
||||
]),
|
||||
]);
|
||||
$admin = User::factory()->create(['role' => 'admin']);
|
||||
$project = Project::factory()->create();
|
||||
$this->actingAs($admin)->get(route('projects.show', $project))->assertOk()->assertDontSee('#999', false);
|
||||
|
||||
$this->actingAs($admin)->post(route('projects.listings.store', $project), ['listing_id' => 999])
|
||||
->assertSessionHasErrors('listing_id');
|
||||
$this->assertDatabaseCount('project_listing_links', 0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user