configureParcel2Go(); Http::preventStrayRequests(); $this->post(route('parcel2go.connect'))->assertRedirect(route('login')); $this->actingAs(User::factory()->create())->post(route('parcel2go.connect'))->assertForbidden(); Http::assertNothingSent(); } public function test_consent_uses_pkce_and_exact_callback_url(): void { $this->configureParcel2Go(); Http::preventStrayRequests(); $response = $this->actingAs(User::factory()->create(['role' => 'admin']))->post(route('parcel2go.connect')); $query = []; parse_str(parse_url($response->headers->get('Location'), PHP_URL_QUERY), $query); $pending = session('parcel2go_oauth'); $this->assertSame('https://auth.example.test/connect/authorize', strtok($response->headers->get('Location'), '?')); $this->assertSame('profile-client', $query['client_id']); $this->assertSame(route('parcel2go.callback'), $query['redirect_uri']); $this->assertSame('public-api my-profile offline_access', $query['scope']); $this->assertSame('S256', $query['code_challenge_method']); $this->assertSame($pending['state'], $query['state']); $this->assertSame(rtrim(strtr(base64_encode(hash('sha256', $pending['verifier'], true)), '+/', '-_'), '='), $query['code_challenge']); Http::assertNothingSent(); } public function test_callback_rejects_bad_state_without_exchanging_a_code(): void { $this->configureParcel2Go(); Http::preventStrayRequests(); $this->actingAs(User::factory()->create(['role' => 'admin'])) ->withSession(['parcel2go_oauth' => ['state' => 'expected', 'verifier' => 'verifier', 'started' => time()]]) ->get(route('parcel2go.callback', ['state' => 'wrong', 'code' => 'code'])) ->assertRedirect(route('settings.index'))->assertSessionHasErrors('parcel2go'); $this->assertDatabaseMissing('integration_settings', ['provider' => 'parcel2go_oauth']); Http::assertNothingSent(); } public function test_callback_stores_encrypted_tokens_and_uses_them_for_account_history(): void { $this->configureParcel2Go(); Http::preventStrayRequests(); Http::fake([ 'https://auth.example.test/connect/token' => Http::response([ 'access_token' => 'profile-token', 'refresh_token' => 'refresh-token', 'expires_in' => 3600, ]), 'https://shipping.example.test/api/me/orders/detail' => Http::response(['Orders' => [['OrderLineId' => 123]]]), ]); $this->actingAs(User::factory()->create(['role' => 'admin'])) ->withSession(['parcel2go_oauth' => ['state' => 'expected', 'verifier' => 'verifier', 'started' => time()]]) ->get(route('parcel2go.callback', ['state' => 'expected', 'code' => 'code'])) ->assertRedirect(route('settings.index'))->assertSessionHas('status'); $connection = IntegrationSetting::findOrFail('parcel2go_oauth'); $this->assertSame('profile-token', $connection->settings['access_token']); $this->assertStringNotContainsString('profile-token', $connection->getRawOriginal('settings')); Http::assertSent(fn ($request) => $request->url() === 'https://auth.example.test/connect/token' && $request['grant_type'] === 'authorization_code' && $request['code_verifier'] === 'verifier'); $this->assertSame([['OrderLineId' => 123]], app(Parcel2Go::class)->fetch('me/orders/detail')['Orders']); Http::assertSent(fn ($request) => $request->url() === 'https://shipping.example.test/api/me/orders/detail' && $request->hasHeader('Authorization', 'Bearer profile-token')); } public function test_expired_account_token_is_refreshed_before_history_request(): void { $this->configureParcel2Go(); IntegrationSetting::create(['provider' => 'parcel2go_oauth', 'settings' => [ 'client_id' => 'profile-client', 'access_token' => 'old-token', 'refresh_token' => 'old-refresh', 'expires_at' => now()->subMinute()->toIso8601String(), ]]); Http::preventStrayRequests(); Http::fake([ 'https://auth.example.test/connect/token' => Http::response([ 'access_token' => 'new-token', 'refresh_token' => 'new-refresh', 'expires_in' => 3600, ]), 'https://shipping.example.test/api/me/orders/detail' => Http::response(['Orders' => []]), ]); $this->assertSame([], app(Parcel2Go::class)->fetch('me/orders/detail')['Orders']); $this->assertSame('new-refresh', IntegrationSetting::findOrFail('parcel2go_oauth')->settings['refresh_token']); Http::assertSent(fn ($request) => $request->url() === 'https://auth.example.test/connect/token' && $request['grant_type'] === 'refresh_token' && $request['refresh_token'] === 'old-refresh'); Http::assertSent(fn ($request) => $request->url() === 'https://shipping.example.test/api/me/orders/detail' && $request->hasHeader('Authorization', 'Bearer new-token')); } public function test_callback_does_not_save_a_token_without_history_permission(): void { $this->configureParcel2Go(); Http::preventStrayRequests(); Http::fake([ 'https://auth.example.test/connect/token' => Http::response(['access_token' => 'limited-token', 'expires_in' => 3600]), 'https://shipping.example.test/api/me/orders/detail' => Http::response([], 403), ]); $this->actingAs(User::factory()->create(['role' => 'admin'])) ->withSession(['parcel2go_oauth' => ['state' => 'expected', 'verifier' => 'verifier', 'started' => time()]]) ->get(route('parcel2go.callback', ['state' => 'expected', 'code' => 'code'])) ->assertRedirect(route('settings.index'))->assertSessionHasErrors('parcel2go'); $this->assertDatabaseMissing('integration_settings', ['provider' => 'parcel2go_oauth']); } private function configureParcel2Go(): void { config()->set('services.parcel2go', [ 'client_id' => 'basic-client', 'client_secret' => 'basic-secret', 'profile_client_id' => 'profile-client', 'auth_url' => 'https://auth.example.test/connect/token', 'base_url' => 'https://shipping.example.test/api', 'scope' => 'public-api', ]); } }