Files
HickLab3D_Dashboard/tests/Feature/IntegrationsTest.php
T
Hickmeister 55c987ef47
Deploy HickLab3D Dashboard / Deploy to production (push) Successful in 26s
Fall back when Parcel2Go profile scope is denied
2026-10-11 17:02:13 +01:00

253 lines
15 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\EtsyConnection;
use App\Models\User;
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
class IntegrationsTest extends TestCase
{
use LazilyRefreshDatabase;
public function test_regular_user_cannot_begin_etsy_oauth(): void
{
$this->actingAs(User::factory()->create())->post(route('etsy.connect'))->assertForbidden();
}
public function test_etsy_callback_rejects_mismatched_state_without_exchanging_token(): void
{
Http::preventStrayRequests();
$admin = User::factory()->create(['role' => 'admin']);
$this->actingAs($admin)->withSession(['etsy_oauth' => ['state' => 'expected', 'verifier' => 'verifier', 'started' => time()]])
->get(route('etsy.callback', ['state' => 'wrong', 'code' => 'code']))
->assertRedirect(route('etsy.index'))->assertSessionHasErrors('etsy');
Http::assertNothingSent();
}
public function test_etsy_callback_saves_encrypted_tokens_and_shows_orders(): void
{
Http::preventStrayRequests();
Http::fake([
'https://api.etsy.com/v3/public/oauth/token' => Http::response(['access_token' => '123.token', 'refresh_token' => '123.refresh', 'expires_in' => 3600]),
'https://api.etsy.com/v3/application/users/123/shops' => Http::response(['shop_id' => 42, 'shop_name' => 'HickLab3D']),
'https://api.etsy.com/v3/application/shops/42/receipts*' => Http::response(['results' => [['receipt_id' => 77, 'name' => 'Buyer']]]),
'https://api.etsy.com/v3/application/shops/42/listings*' => Http::response(['results' => [['title' => '3D print', 'quantity' => 5]]]),
]);
config()->set('services.etsy', ['keystring' => 'key', 'shared_secret' => 'secret', 'redirect_uri' => 'https://example.test/etsy/callback']);
$admin = User::factory()->create(['role' => 'admin']);
$this->actingAs($admin)->withSession(['etsy_oauth' => ['state' => 'expected', 'verifier' => 'verifier', 'started' => time()]])
->get(route('etsy.callback', ['state' => 'expected', 'code' => 'code']))
->assertRedirect(route('etsy.index'));
$connection = EtsyConnection::firstOrFail();
$this->assertSame('123.token', $connection->access_token);
$this->assertNotSame('123.token', $connection->getRawOriginal('access_token'));
$this->actingAs($admin)->get(route('etsy.index'))->assertOk()->assertSee('3D print')->assertSee('#77');
Http::assertSentCount(4);
}
public function test_bambuddy_replaces_local_printer_status(): void
{
Http::preventStrayRequests();
Http::fake([
'https://3d.example.test/api/v1/printers' => Http::response([['id' => 1, 'name' => 'X1 Carbon', 'status' => 'printing']]),
'https://3d.example.test/api/v1/printers/1/status' => Http::response(['connected' => true, 'state' => 'RUNNING', 'progress' => 45, 'temperatures' => ['nozzle' => 220, 'bed' => 60]]),
]);
config()->set('services.bambuddy', ['url' => 'https://3d.example.test', 'api_key' => 'read-key']);
$this->actingAs(User::factory()->create())->get(route('printers.index'))->assertOk()->assertSee('X1 Carbon')->assertSee('45%');
Http::assertSent(fn ($request) => $request->hasHeader('X-API-Key', 'read-key'));
}
public function test_shipping_quote_sends_parcel_to_parcel2go(): void
{
Http::preventStrayRequests();
Http::fake([
'https://auth.example.test/token' => Http::response(['access_token' => 'test-token']),
'https://shipping.example.test/api/quotes' => Http::response(['Quotes' => [['Service' => ['Name' => 'Evri', 'Slug' => 'evri-collection', 'CollectionType' => 'Collection'], 'TotalPrice' => 5.5]]]),
'https://shipping.example.test/api/me/detail' => Http::response([]),
'https://shipping.example.test/api/me/prepay/balance' => Http::response([]),
'https://shipping.example.test/api/me/orders/detail' => Http::response(['Orders' => []]),
]);
config()->set('services.parcel2go', ['client_id' => 'id', 'client_secret' => 'secret', 'auth_url' => 'https://auth.example.test/token', 'base_url' => 'https://shipping.example.test/api']);
$admin = User::factory()->create(['role' => 'admin']);
$details = ['collection_postcode' => 'SW1A 1AA', 'delivery_postcode' => 'M1 1AE', 'weight' => 2, 'length' => 10, 'width' => 10, 'height' => 10, 'value' => 30];
$this->actingAs($admin)->post(route('shipping.quote'), $details)->assertRedirect(route('shipping'));
$this->actingAs($admin)->get(route('shipping'))->assertOk()->assertSee('Evri')->assertSee('£5.50');
Http::assertSent(fn ($request) => $request->url() === 'https://shipping.example.test/api/quotes' && $request['Parcels'][0]['Weight'] === 2);
}
public function test_shipping_page_works_with_public_api_scope_without_profile_access(): void
{
Http::preventStrayRequests();
Http::fake(['https://auth.example.test/token' => function ($request) {
return $request['scope'] === 'public-api'
? Http::response(['access_token' => 'test-token'])
: Http::response(['error' => 'invalid_scope'], 400);
}]);
config()->set('services.parcel2go', [
'client_id' => 'id', 'client_secret' => 'secret',
'auth_url' => 'https://auth.example.test/token',
'base_url' => 'https://shipping.example.test/api',
'scope' => 'public-api',
]);
$this->actingAs(User::factory()->create(['role' => 'admin']))->get(route('shipping'))
->assertOk()->assertSee('Parcel2Go connected for quotes and collections')
->assertDontSee('Parcel2Go is unavailable');
Http::assertSentCount(1);
}
public function test_shipping_falls_back_to_basic_access_when_parcel2go_rejects_profile_scope(): void
{
Http::preventStrayRequests();
Http::fake(['https://auth.example.test/token' => function ($request) {
return $request['scope'] === 'public-api'
? Http::response(['access_token' => 'test-token'])
: Http::response(['error' => 'invalid_scope'], 400);
}]);
config()->set('services.parcel2go', [
'client_id' => 'id', 'client_secret' => 'secret',
'auth_url' => 'https://auth.example.test/token',
'base_url' => 'https://shipping.example.test/api',
'scope' => 'public-api my-profile',
]);
$this->actingAs(User::factory()->create(['role' => 'admin']))->get(route('shipping'))
->assertOk()->assertSee('Parcel2Go rejected the selected API access')
->assertDontSee('Parcel2Go connected, but account history is unavailable')
->assertDontSee('Parcel2Go is unavailable');
Http::assertSentCount(2);
}
public function test_shipping_label_is_downloaded_for_an_order_in_account_history(): void
{
Http::preventStrayRequests();
Http::fake([
'https://auth.example.test/token' => Http::response(['access_token' => 'test-token']),
'https://shipping.example.test/api/me/orders/detail' => Http::response(['Orders' => [['OrderLineId' => 123, 'Hash' => 'order-hash']]]),
'https://shipping.example.test/api/labels/123*' => Http::response(['Base64EncodedLabels' => [base64_encode('%PDF-1.4 test label')]]),
]);
config()->set('services.parcel2go', ['client_id' => 'id', 'client_secret' => 'secret', 'auth_url' => 'https://auth.example.test/token', 'base_url' => 'https://shipping.example.test/api']);
$this->actingAs(User::factory()->create(['role' => 'admin']))->get(route('shipping.label', 123))
->assertOk()->assertHeader('content-type', 'application/pdf')->assertSee('%PDF-1.4 test label');
Http::assertSent(fn ($request) => str_contains($request->url(), '/labels/123') && str_contains($request->url(), 'hash=order-hash'));
}
public function test_collection_booking_uses_quoted_service_and_available_date(): void
{
Http::preventStrayRequests();
Http::fake([
'https://auth.example.test/token' => Http::response(['access_token' => 'test-token']),
'https://shipping.example.test/api/orders' => Http::response(['OrderId' => '789', 'TotalPrice' => 8.5, 'Links' => ['payment' => 'https://www.parcel2go.com/order/payment?id=789']]),
'https://shipping.example.test/api/me/detail' => Http::response([]),
'https://shipping.example.test/api/me/prepay/balance' => Http::response([]),
'https://shipping.example.test/api/me/orders/detail' => Http::response(['Orders' => []]),
]);
config()->set('services.parcel2go', ['client_id' => 'id', 'client_secret' => 'secret', 'auth_url' => 'https://auth.example.test/token', 'base_url' => 'https://shipping.example.test/api']);
$session = [
'shipping_quote' => ['collection_postcode' => 'SW1A 1AA', 'delivery_postcode' => 'M1 1AE', 'weight' => 2, 'length' => 10, 'width' => 10, 'height' => 10, 'value' => 30],
'shipping_selected_service' => 'evri-collection',
'shipping_quotes' => [['Service' => ['Slug' => 'evri-collection', 'CollectionType' => 'Collection']]],
'shipping_collection_dates' => [['CollectionDate' => '2026-10-15T10:00:00Z']],
'shipping_sender_location' => ['sender_street' => 'Main Street', 'sender_town' => 'London'],
];
$booking = [
'collection_date' => '2026-10-15T10:00:00Z',
'sender_name' => 'Lee Hickman', 'sender_email' => 'lee@example.test', 'sender_phone' => '07123456789',
'sender_property' => '1', 'sender_street' => 'Main Street', 'sender_town' => 'London',
'recipient_name' => 'Buyer Name', 'recipient_email' => 'buyer@example.test', 'recipient_phone' => '07123456788',
'recipient_property' => '2', 'recipient_street' => 'Market Street', 'recipient_town' => 'Manchester', 'contents' => '3D printed part',
];
$this->actingAs(User::factory()->create(['role' => 'admin']))->withSession($session)
->post(route('shipping.book-collection'), $booking)->assertRedirect(route('shipping'))->assertSessionHas('shipping_booking_result.OrderId', '789');
$this->get(route('shipping'))->assertOk()->assertSee('Collection order #789')->assertSee('Complete payment');
Http::assertSent(fn ($request) => $request->url() === 'https://shipping.example.test/api/orders'
&& $request['Items'][0]['Service'] === 'evri-collection' && $request['Items'][0]['Parcels'][0]['ContentsSummary'] === '3D printed part');
}
public function test_collection_dates_only_accept_a_quoted_collection_service(): void
{
Http::preventStrayRequests();
$admin = User::factory()->create(['role' => 'admin']);
$session = [
'shipping_quote' => ['collection_postcode' => 'SW1A 1AA'],
'shipping_quotes' => [['Service' => ['Slug' => 'evri-collection', 'CollectionType' => 'Collection']]],
];
$this->actingAs($admin)->withSession($session)
->post(route('shipping.collection-dates'), ['service' => 'forged-service', 'sender_street' => 'Main Street', 'sender_town' => 'London'])
->assertSessionHasErrors('service');
Http::assertNothingSent();
}
public function test_collection_dates_are_loaded_for_a_quoted_service(): void
{
Http::preventStrayRequests();
Http::fake([
'https://auth.example.test/token' => Http::response(['access_token' => 'test-token']),
'https://shipping.example.test/api/collectiondates' => Http::response(['CollectionDates' => [['CollectionDate' => '2026-10-15T10:00:00Z']]]),
]);
config()->set('services.parcel2go', ['client_id' => 'id', 'client_secret' => 'secret', 'auth_url' => 'https://auth.example.test/token', 'base_url' => 'https://shipping.example.test/api']);
$session = [
'shipping_quote' => ['collection_postcode' => 'SW1A 1AA'],
'shipping_quotes' => [['Service' => ['Slug' => 'evri-collection', 'CollectionType' => 'Collection']]],
];
$this->actingAs(User::factory()->create(['role' => 'admin']))->withSession($session)
->post(route('shipping.collection-dates'), ['service' => 'evri-collection', 'sender_street' => 'Main Street', 'sender_town' => 'London'])
->assertRedirect(route('shipping'))->assertSessionHas('shipping_selected_service', 'evri-collection');
Http::assertSent(fn ($request) => $request->url() === 'https://shipping.example.test/api/collectiondates'
&& $request['ServiceSlug'] === 'evri-collection' && $request['Postcode'] === 'SW1A 1AA');
}
public function test_uncertain_booking_requires_account_check_before_a_new_quote(): void
{
Http::preventStrayRequests();
$admin = User::factory()->create(['role' => 'admin']);
$details = ['collection_postcode' => 'SW1A 1AA', 'delivery_postcode' => 'M1 1AE', 'weight' => 2, 'length' => 10, 'width' => 10, 'height' => 10, 'value' => 30];
$this->actingAs($admin)->withSession(['shipping_booking_pending' => true])
->post(route('shipping.quote'), $details)->assertSessionHasErrors('collection');
$this->actingAs($admin)->post(route('shipping.reset-booking'), [])->assertSessionHasErrors('checked_account');
$this->actingAs($admin)->post(route('shipping.reset-booking'), ['checked_account' => 1])
->assertRedirect(route('shipping'))->assertSessionMissing('shipping_booking_pending');
Http::assertNothingSent();
}
public function test_dryer_status_and_fault_acknowledgement_use_versioned_api(): void
{
Http::preventStrayRequests();
Http::fake([
'https://dryer.example.test/api/v1/status' => Http::response(['state' => 'idle', 'sample' => ['valid' => true, 'temperatureC' => 25]]),
'https://dryer.example.test/api/v1/history*' => Http::response(['samples' => []]),
'https://dryer.example.test/api/v1/faults/acknowledge' => Http::response(['ok' => true]),
]);
config()->set('services.dryer', ['url' => 'https://dryer.example.test/api/v1', 'api_key' => 'test-key']);
$admin = User::factory()->create(['role' => 'admin']);
$this->actingAs($admin)->get(route('dryer.status'))->assertOk()->assertJsonPath('status.state', 'idle');
$this->actingAs($admin)->postJson(route('dryer.acknowledge'))->assertOk()->assertJsonPath('ok', true);
Http::assertSent(fn ($request) => $request->url() === 'https://dryer.example.test/api/v1/faults/acknowledge'
&& $request->hasHeader('X-API-Key', 'test-key') && str_starts_with($request['requestId'], 'dashboard-'));
}
public function test_dryer_rejects_temperature_above_firmware_limit_without_contacting_device(): void
{
Http::preventStrayRequests();
$admin = User::factory()->create(['role' => 'admin']);
$this->actingAs($admin)->postJson(route('dryer.control'), [
'dryerOn' => true, 'profile' => 'PLA', 'targetTemperature' => 80, 'dryingTime' => 240,
])->assertUnprocessable()->assertJsonValidationErrors('targetTemperature');
Http::assertNothingSent();
}
}