Files
HickLab3D_Dashboard/tests/Feature/Parcel2GoOAuthTest.php
T
Hickmeister 1be66f099c
Deploy HickLab3D Dashboard / Deploy to production (push) Successful in 26s
Add Parcel2Go account consent with PKCE
2026-10-11 17:27:07 +01:00

134 lines
6.7 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\IntegrationSetting;
use App\Models\User;
use App\Services\Parcel2Go;
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
class Parcel2GoOAuthTest extends TestCase
{
use LazilyRefreshDatabase;
public function test_only_an_administrator_can_start_account_consent(): void
{
$this->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',
]);
}
}