Files
Tarnax/appstore/website/tests/Feature/CommerceTest.php
T
2026-11-04 16:47:55 +00:00

64 lines
9.6 KiB
PHP

<?php
namespace Tests\Feature;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use App\Models\User;
use App\Services\LicenseService;
use App\Livewire\SupportDesk;
use App\Livewire\Admin\Cms;
use Livewire\Livewire;
class CommerceTest extends TestCase {
use RefreshDatabase;
protected function setUp(): void {parent::setUp();$this->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());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']);}
}