added various tests for playwright
This commit is contained in:
@@ -68,6 +68,45 @@ class PaddleWebhookControllerTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
public function test_duplicate_transaction_is_idempotent(): void
|
||||
{
|
||||
config(['paddle.webhook_secret' => 'test_secret']);
|
||||
|
||||
[$tenant, $package, $session] = $this->prepareSession();
|
||||
|
||||
$payload = [
|
||||
'event_type' => 'transaction.completed',
|
||||
'data' => [
|
||||
'id' => 'txn_dup',
|
||||
'status' => 'completed',
|
||||
'checkout_id' => 'chk_dup',
|
||||
'metadata' => [
|
||||
'checkout_session_id' => $session->id,
|
||||
'tenant_id' => (string) $tenant->id,
|
||||
'package_id' => (string) $package->id,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$signature = hash_hmac('sha256', json_encode($payload), 'test_secret');
|
||||
|
||||
$first = $this->withHeader('Paddle-Webhook-Signature', $signature)
|
||||
->postJson('/paddle/webhook', $payload);
|
||||
|
||||
$first->assertOk()->assertJson(['status' => 'processed']);
|
||||
|
||||
$second = $this->withHeader('Paddle-Webhook-Signature', $signature)
|
||||
->postJson('/paddle/webhook', $payload);
|
||||
|
||||
$second->assertStatus(200)->assertJson(['status' => 'processed']);
|
||||
|
||||
$this->assertSame(1, PackagePurchase::query()->count());
|
||||
|
||||
$session->refresh();
|
||||
$this->assertEquals(CheckoutSession::STATUS_COMPLETED, $session->status);
|
||||
$this->assertEquals('txn_dup', Arr::get($session->provider_metadata, 'paddle_transaction_id'));
|
||||
}
|
||||
|
||||
public function test_rejects_invalid_signature(): void
|
||||
{
|
||||
config(['paddle.webhook_secret' => 'secret']);
|
||||
|
||||
103
tests/Feature/SecurityHeadersTest.php
Normal file
103
tests/Feature/SecurityHeadersTest.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Fruitcake\Cors\CorsService;
|
||||
use Illuminate\Http\Middleware\HandleCors;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Tests\TestCase;
|
||||
|
||||
class SecurityHeadersTest extends TestCase
|
||||
{
|
||||
public function test_marketing_and_auth_responses_have_security_headers_and_csrf_cookie(): void
|
||||
{
|
||||
$originalEnv = app()->environment();
|
||||
app()->detectEnvironment(fn () => 'production');
|
||||
config([
|
||||
'app.debug' => false,
|
||||
'app.url' => 'https://test-y0k0.fotospiel.app',
|
||||
'security_headers.force_hsts' => true,
|
||||
'cors.allowed_origins' => ['https://test-y0k0.fotospiel.app'],
|
||||
'cors.paths' => ['*'],
|
||||
]);
|
||||
|
||||
Route::middleware('web')->get('/__test/marketing', fn () => response('ok'));
|
||||
Route::middleware('web')->get('/__test/auth', fn () => response('ok'));
|
||||
|
||||
try {
|
||||
$response = $this->withServerVariables([
|
||||
'HTTPS' => 'on',
|
||||
'HTTP_ORIGIN' => 'https://test-y0k0.fotospiel.app',
|
||||
])->get('/__test/marketing');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertHeader('X-Frame-Options', 'SAMEORIGIN');
|
||||
$response->assertHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
||||
$response->assertHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
|
||||
$response->assertHeader('Content-Security-Policy');
|
||||
$response->assertHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
||||
$response->assertCookie('XSRF-TOKEN');
|
||||
|
||||
$login = $this->withServerVariables([
|
||||
'HTTPS' => 'on',
|
||||
'HTTP_ORIGIN' => 'https://test-y0k0.fotospiel.app',
|
||||
])->get('/__test/auth');
|
||||
|
||||
$login->assertOk();
|
||||
$login->assertHeader('Content-Security-Policy');
|
||||
$login->assertHeader('X-Frame-Options', 'SAMEORIGIN');
|
||||
$login->assertCookie('XSRF-TOKEN');
|
||||
} finally {
|
||||
app()->detectEnvironment(fn () => $originalEnv);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_cors_headers_present_for_api_requests_and_error_responses_keep_headers(): void
|
||||
{
|
||||
$originalEnv = app()->environment();
|
||||
app()->detectEnvironment(fn () => 'production');
|
||||
$corsConfig = config('cors');
|
||||
$corsConfig['paths'] = ['*'];
|
||||
$corsConfig['allowed_origins'] = ['https://test-y0k0.fotospiel.app'];
|
||||
|
||||
config([
|
||||
'app.debug' => false,
|
||||
'app.url' => 'https://test-y0k0.fotospiel.app',
|
||||
'cors' => $corsConfig,
|
||||
]);
|
||||
|
||||
Route::middleware('web')->get('/__test/error', fn () => throw new \RuntimeException('boom'));
|
||||
|
||||
try {
|
||||
$corsMiddleware = new HandleCors(app(), new CorsService(config('cors')));
|
||||
$this->assertContains('*', config('cors.paths'));
|
||||
$this->assertContains('https://test-y0k0.fotospiel.app', config('cors.allowed_origins'));
|
||||
$corsRequest = Request::create(
|
||||
uri: '/api/__test/cors',
|
||||
method: 'OPTIONS',
|
||||
server: [
|
||||
'HTTP_ORIGIN' => 'https://test-y0k0.fotospiel.app',
|
||||
'HTTPS' => 'on',
|
||||
'HTTP_ACCESS_CONTROL_REQUEST_METHOD' => 'GET',
|
||||
],
|
||||
);
|
||||
|
||||
$this->assertSame('https://test-y0k0.fotospiel.app', $corsRequest->headers->get('Origin'));
|
||||
|
||||
$corsResponse = $corsMiddleware->handle($corsRequest, fn () => response()->noContent());
|
||||
|
||||
$this->assertSame('https://test-y0k0.fotospiel.app', $corsResponse->headers->get('Access-Control-Allow-Origin'));
|
||||
|
||||
$error = $this->withServerVariables([
|
||||
'HTTPS' => 'on',
|
||||
])->get('/__test/error');
|
||||
|
||||
$error->assertStatus(500);
|
||||
$error->assertHeader('Content-Security-Policy');
|
||||
$error->assertHeader('X-Frame-Options', 'SAMEORIGIN');
|
||||
} finally {
|
||||
app()->detectEnvironment(fn () => $originalEnv);
|
||||
}
|
||||
}
|
||||
}
|
||||
58
tests/Feature/SentryReportingTest.php
Normal file
58
tests/Feature/SentryReportingTest.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Tests\TestCase;
|
||||
|
||||
class SentryReportingTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
config(['sentry.dsn' => 'https://example@sentry.test/1']);
|
||||
}
|
||||
|
||||
public function test_reports_server_errors_to_sentry(): void
|
||||
{
|
||||
$fake = new class
|
||||
{
|
||||
public int $captured = 0;
|
||||
|
||||
public function captureException(mixed $exception = null): void
|
||||
{
|
||||
$this->captured++;
|
||||
}
|
||||
};
|
||||
|
||||
$this->app->instance('sentry', $fake);
|
||||
|
||||
$handler = $this->app->make(\App\Exceptions\Handler::class);
|
||||
|
||||
$handler->report(new \RuntimeException('boom'));
|
||||
|
||||
$this->assertSame(1, $fake->captured, 'Sentry should receive server errors');
|
||||
}
|
||||
|
||||
public function test_ignores_client_errors_for_sentry(): void
|
||||
{
|
||||
$fake = new class
|
||||
{
|
||||
public int $captured = 0;
|
||||
|
||||
public function captureException(mixed $exception = null): void
|
||||
{
|
||||
$this->captured++;
|
||||
}
|
||||
};
|
||||
|
||||
$this->app->instance('sentry', $fake);
|
||||
|
||||
$handler = $this->app->make(\App\Exceptions\Handler::class);
|
||||
|
||||
$handler->report(new HttpException(404));
|
||||
|
||||
$this->assertSame(0, $fake->captured, 'Sentry should ignore 4xx errors');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user