seed(); Notification::fake(); config(['tarnax.paddle.webhook_secret' => 'test-secret', 'tarnax.paddle.price_id' => 'pri_test']); } private function customer(array $attributes = []): User { return User::factory()->create(['account_token' => (string) Str::uuid(), ...$attributes]); } private function event(User $user, string $id = 'evt_1'): array { return ['event_id' => $id, 'event_type' => 'transaction.completed', 'occurred_at' => now()->toISOString(), 'data' => ['id' => 'txn_1', 'status' => 'completed', 'custom_data' => ['account_token' => $user->account_token], 'items' => [['price' => ['id' => 'pri_test'], 'quantity' => 1]], 'details' => ['totals' => ['grand_total' => '4900']], 'currency_code' => 'USD']]; } private function deliver(array $event, ?int $timestamp = null, ?string $signature = null) { $body = json_encode($event); $ts = $timestamp ?? time(); $sig = $signature ?? hash_hmac('sha256', $ts.':'.$body, 'test-secret'); return $this->call('POST', '/api/webhooks/paddle', [], [], [], ['CONTENT_TYPE' => 'application/json', 'HTTP_ACCEPT' => 'application/json', 'HTTP_PADDLE_SIGNATURE' => 'ts='.$ts.';h1='.$sig], $body); } private function license(User $user): object { $this->deliver($this->event($user))->assertOk(); return DB::table('licenses')->first(); } public function test_public_pages_and_authentication_views_render(): void { foreach (['/', '/releases', '/pages/privacy', '/pages/terms', '/pages/features', '/login', '/register', '/forgot-password'] as $url) { $this->get($url)->assertOk(); }$this->get('/account')->assertRedirect('/login'); } public function test_registration_cannot_grant_administration(): void { $this->post('/register', ['name' => 'Customer', 'email' => 'new@example.com', 'password' => 'StrongPass123!', 'password_confirmation' => 'StrongPass123!', 'is_admin' => true])->assertRedirect(); $this->assertFalse(User::where('email', 'new@example.com')->first()->is_admin); } public function test_verified_payment_issues_only_one_license_on_retries(): void { $user = $this->customer(); $event = $this->event($user); $this->deliver($event)->assertOk(); $this->deliver($event)->assertOk(); $event['event_id'] = 'evt_2'; $this->deliver($event)->assertOk(); $this->assertDatabaseCount('purchases', 1); $this->assertDatabaseCount('licenses', 1); } public function test_invalid_signature_and_stale_replay_never_grant_licenses(): void { $user = $this->customer(); $this->deliver($this->event($user), null, 'invalid')->assertUnauthorized(); $this->deliver($this->event($user), time() - 600)->assertUnauthorized(); $this->assertDatabaseCount('licenses', 0); } public function test_wrong_product_does_not_grant_license(): void { $event = $this->event($this->customer()); $event['data']['items'][0]['price']['id'] = 'pri_other'; $this->deliver($event)->assertOk(); $this->assertDatabaseCount('licenses', 0); } public function test_unknown_customer_rolls_back_event_for_retry(): void { $event = $this->event($this->customer()); $event['data']['custom_data']['account_token'] = (string) Str::uuid(); $this->deliver($event)->assertUnprocessable(); $this->assertDatabaseCount('webhook_events', 0); } public function test_activation_limits_validation_and_deactivation(): void { $license = $this->license($this->customer()); $key = Crypt::decryptString($license->key_encrypted); $token = $this->postJson('/api/v1/licenses/activate', ['license_key' => $key, 'device_id' => str_repeat('a', 20)])->assertOk()->json('activation_token'); $this->postJson('/api/v1/licenses/activate', ['license_key' => $key, 'device_id' => str_repeat('b', 20)])->assertOk(); $this->postJson('/api/v1/licenses/activate', ['license_key' => $key, 'device_id' => str_repeat('c', 20)])->assertConflict(); $this->withToken($token)->postJson('/api/v1/licenses/validate')->assertOk()->assertJson(['valid' => true]); $this->withToken($token)->postJson('/api/v1/licenses/deactivate')->assertOk(); $this->withToken($token)->postJson('/api/v1/licenses/validate')->assertForbidden(); } public function test_refund_revokes_license_and_prevents_regrant(): void { $user = $this->customer(); $license = $this->license($user); $key = Crypt::decryptString($license->key_encrypted); $token = app(LicenseService::class)->activate($key, str_repeat('a', 20))['activation_token']; $refund = ['event_id' => 'evt_refund', 'event_type' => 'adjustment.updated', 'occurred_at' => now()->addSecond()->toISOString(), 'data' => ['status' => 'approved', 'action' => 'refund', 'transaction_id' => 'txn_1']]; $this->deliver($refund)->assertOk(); $this->withToken($token)->postJson('/api/v1/licenses/validate')->assertForbidden(); $this->deliver($this->event($user, 'evt_late'))->assertOk(); $this->assertDatabaseHas('licenses', ['status' => 'revoked']); } public function test_checkout_uses_server_selected_product_and_customer(): void { $user = $this->customer(); config(['tarnax.paddle.api_key' => 'secret', 'tarnax.paddle.client_token' => 'token']); Http::fake(['*' => Http::response(['data' => ['id' => 'txn_checkout']])]); $this->actingAs($user)->postJson('/checkout', ['price_id' => 'attacker', 'custom_data' => ['account_token' => 'other']])->assertOk()->assertJson(['transaction_id' => 'txn_checkout']); Http::assertSent(fn ($r) => $r['items'][0]['price_id'] === 'pri_test' && $r['custom_data']['account_token'] === $user->account_token); } public function test_unconfigured_checkout_fails_closed(): void { config(['tarnax.paddle.api_key' => null]); $this->actingAs($this->customer())->postJson('/checkout')->assertStatus(503); } public function test_downloads_require_license_signature_and_matching_customer(): void { Storage::fake('local'); Storage::disk('local')->put('releases/app.zip', 'binary'); $release = DB::table('releases')->insertGetId(['version' => '1.0.0', 'channel' => 'stable', 'platform' => 'macOS', 'notes' => 'Release', 'path' => 'releases/app.zip', 'sha256' => hash('sha256', 'binary'), 'bytes' => 6, 'published' => true, 'created_at' => now(), 'updated_at' => now()]); $user = $this->customer(); $other = $this->customer(); $this->actingAs($user)->get('/downloads/'.$release)->assertForbidden(); $this->license($user); $this->get('/downloads/'.$release)->assertRedirect(); $url = URL::temporarySignedRoute('download.file', now()->addMinutes(5), ['release' => $release, 'user' => $user->id]); $this->get($url)->assertOk(); $this->actingAs($other)->get($url)->assertForbidden(); $expired = URL::temporarySignedRoute('download.file', now()->subMinute(), ['release' => $release, 'user' => $user->id]); $this->actingAs($user)->get($expired)->assertForbidden(); } public function test_support_is_scoped_and_does_not_accept_another_ticket(): void { $owner = $this->customer(); $other = $this->customer(); $ticket = DB::table('tickets')->insertGetId(['user_id' => $owner->id, 'subject' => 'Private conversation', 'status' => 'open', 'created_at' => now(), 'updated_at' => now()]); Livewire::actingAs($other)->test(SupportDesk::class)->assertDontSee('Private conversation')->call('select', $ticket)->assertForbidden(); } public function test_customer_can_create_support_ticket(): void { Livewire::actingAs($this->customer())->test(SupportDesk::class)->set('subject', 'Connection help')->set('body', 'Please help me connect my server.')->call('create')->assertHasNoErrors(); $this->assertDatabaseCount('tickets', 1); $this->assertDatabaseCount('ticket_messages', 1); } public function test_admin_requires_role_and_confirmed_two_factor(): void { $this->actingAs($this->customer())->get('/admin')->assertForbidden(); $admin = $this->customer(['is_admin' => true]); $this->actingAs($admin)->get('/admin')->assertRedirect('/account/security'); Livewire::actingAs($admin)->test(Cms::class)->assertForbidden(); } public function test_admin_can_edit_cms_and_mutations_are_audited(): void { $admin = $this->customer(['is_admin' => true, 'two_factor_confirmed_at' => now()]); Livewire::actingAs($admin)->test(Cms::class)->set('form', ['slug' => 'new-page', 'title' => 'New page', 'body' => 'Safe content', 'published' => true])->call('save')->assertHasNoErrors(); $this->assertDatabaseHas('pages', ['slug' => 'new-page']); $this->assertDatabaseCount('audit_logs', 1); } public function test_consent_records_explicit_choices(): void { $this->postJson('/consent', ['analytics' => false, 'marketing' => false])->assertOk(); $this->assertDatabaseHas('consents', ['analytics' => false, 'marketing' => false]); } public function test_apple_verification_fails_closed_without_root(): void { config(['tarnax.apple.root_certificate' => null]); $this->actingAs($this->customer())->postJson('/account/apple', ['signed_transaction' => 'untrusted.payload.signature'])->assertStatus(503); $this->assertDatabaseCount('purchases', 0); } public function test_account_support_and_security_render_for_verified_customer(): void { $this->actingAs($this->customer())->withSession(['auth.password_confirmed_at' => time()]); foreach (['/account', '/support', '/account/security'] as $url) { $this->get($url)->assertOk(); } } public function test_erasure_revokes_access_and_anonymises_profile(): void { $user = $this->customer(); $this->license($user); $id = DB::table('privacy_requests')->insertGetId(['user_id' => $user->id, 'type' => 'deletion', 'status' => 'pending', 'created_at' => now(), 'updated_at' => now()]); $admin = $this->customer(['is_admin' => true, 'two_factor_confirmed_at' => now()]); Livewire::actingAs($admin)->test(Cms::class)->set('section', 'privacy_requests')->call('fulfillDeletion', $id)->assertHasNoErrors(); $this->assertDatabaseHas('users', ['id' => $user->id, 'name' => 'Deleted customer']); $this->assertDatabaseHas('licenses', ['user_id' => $user->id, 'status' => 'revoked']); $this->assertDatabaseHas('privacy_requests', ['id' => $id, 'status' => 'fulfilled']); } public function test_refund_before_purchase_prevents_late_grant(): void { $user = $this->customer(); $this->deliver(['event_id' => 'early_refund', 'event_type' => 'adjustment.created', 'occurred_at' => now()->toISOString(), 'data' => ['status' => 'approved', 'action' => 'refund', 'transaction_id' => 'txn_1']])->assertOk(); $this->deliver($this->event($user))->assertOk(); $this->assertDatabaseCount('licenses', 0); } public function test_subscription_events_revoke_and_restore_and_ignore_stale_events(): void { $user = $this->customer(); $event = $this->event($user); $event['data']['subscription_id'] = 'sub_1'; $this->deliver($event)->assertOk(); $send = function ($id, $status, $seconds) { return $this->deliver(['event_id' => $id, 'event_type' => 'subscription.updated', 'occurred_at' => now()->addSeconds($seconds)->toISOString(), 'data' => ['id' => 'sub_1', 'status' => $status]]); }; $send('pause', 'paused', 10)->assertOk(); $this->assertDatabaseHas('licenses', ['status' => 'revoked']); $send('resume', 'active', 20)->assertOk(); $this->assertDatabaseHas('licenses', ['status' => 'active']); $send('old_pause', 'paused', 5)->assertOk(); $this->assertDatabaseHas('licenses', ['status' => 'active']); } public function test_admin_can_manage_navigation_and_reject_external_redirects(): void { $admin = $this->customer(['is_admin' => true, 'two_factor_confirmed_at' => now()]); Livewire::actingAs($admin)->test(Cms::class)->set('section', 'navigation')->set('form', ['label' => 'Features', 'path' => '/pages/features', 'position' => 1, 'published' => true])->call('saveResource')->assertHasNoErrors(); $this->assertDatabaseHas('navigation', ['label' => 'Features']); Livewire::actingAs($admin)->test(Cms::class)->set('section', 'redirects')->set('form', ['source' => '/old', 'destination' => '//evil.example'])->call('saveResource')->assertHasErrors('form.destination'); } public function test_published_page_html_is_escaped(): void { DB::table('pages')->where('slug', 'features')->update(['body' => '']); $this->get('/pages/features')->assertSee('<script>', false)->assertDontSee('', false); } public function test_apple_notification_fails_closed_without_configuration(): void { config(['tarnax.apple.root_certificate' => null]); $this->postJson('/api/webhooks/apple', ['signedPayload' => 'untrusted.payload.signature'])->assertStatus(503); } public function test_erased_customer_cannot_use_an_old_activation_token(): void { $user = $this->customer(); $license = $this->license($user); $token = app(LicenseService::class)->activate(Crypt::decryptString($license->key_encrypted), str_repeat('x', 20))['activation_token']; $id = DB::table('privacy_requests')->insertGetId(['user_id' => $user->id, 'type' => 'deletion', 'status' => 'pending', 'created_at' => now(), 'updated_at' => now()]); $admin = $this->customer(['is_admin' => true, 'two_factor_confirmed_at' => now()]); Livewire::actingAs($admin)->test(Cms::class)->set('section', 'privacy_requests')->call('fulfillDeletion', $id); $this->withToken($token)->postJson('/api/v1/licenses/validate')->assertForbidden(); } public function test_unverified_accounts_cannot_checkout_or_open_portal(): void { $user = $this->customer(['email_verified_at' => null]); $this->actingAs($user)->get('/account')->assertRedirect('/email/verify'); $this->postJson('/checkout')->assertForbidden(); } public function test_private_media_is_safely_served(): void { Storage::fake('local'); Storage::disk('local')->put('media/test.png','test image'); $id = DB::table('media')->insertGetId(['title' => 'App', 'alt' => 'Icon', 'path' => 'media/test.png', 'mime' => 'image/png', 'bytes' => 10, 'created_at' => now(), 'updated_at' => now()]); $this->get('/media/'.$id)->assertOk()->assertHeader('X-Content-Type-Options','nosniff'); } }