create()); $this->post('/api/certification/identity-document', [ 'document' => UploadedFile::fake()->image('identity.jpg'), ])->assertForbidden(); expect(IdentityVerificationRequest::query()->count())->toBe(0); }); it('stores identity documents privately without exposing their path', function () { Storage::fake('identity_documents'); $user = User::factory()->create([ 'certification_purchased_at' => now(), ]); Sanctum::actingAs($user); $response = $this->post('/api/certification/identity-document', [ 'document' => UploadedFile::fake()->image('my-passport.jpg'), ]); $response ->assertOk() ->assertJsonPath('data.entitlementPurchased', true) ->assertJsonPath('data.canSubmitIdentity', false) ->assertJsonPath('data.verification.status', IdentityVerificationStatus::PENDING->value) ->assertJsonMissingPath('data.verification.documentPath'); $verificationRequest = IdentityVerificationRequest::query()->sole(); Storage::disk('identity_documents')->assertExists($verificationRequest->document_path); expect($verificationRequest->document_path)->toStartWith('submissions/') ->not->toContain('my-passport') ->and(Storage::disk('identity_documents')->getVisibility($verificationRequest->document_path)) ->toBe('private'); }); it('prevents replacing a document while it is under review', function () { Storage::fake('identity_documents'); $user = User::factory()->create([ 'certification_purchased_at' => now(), ]); IdentityVerificationRequest::factory()->for($user)->create(); Sanctum::actingAs($user); $this ->withHeader('Accept', 'application/json') ->post('/api/certification/identity-document', [ 'document' => UploadedFile::fake()->image('identity.jpg'), ]) ->assertConflict() ->assertJsonPath('message', __('api.certification.already_pending')); }); it('approves a certification and removes the reviewed identity document', function () { Storage::fake('identity_documents'); $user = User::factory()->create([ 'certification_purchased_at' => now(), ]); $admin = User::factory()->create([ 'role' => UserRole::ADMIN, ]); $verificationRequest = IdentityVerificationRequest::factory()->for($user)->create(); Storage::disk('identity_documents')->put($verificationRequest->document_path, 'identity'); app(ReviewIdentityVerification::class)->execute( $verificationRequest, IdentityVerificationStatus::APPROVED, $admin, ); Storage::disk('identity_documents')->assertMissing($verificationRequest->document_path); expect($user->fresh()->account_verified_at)->not->toBeNull() ->and($verificationRequest->fresh()->status)->toBe(IdentityVerificationStatus::APPROVED) ->and($verificationRequest->fresh()->document_path)->toBeNull() ->and($verificationRequest->fresh()->document_deleted_at)->not->toBeNull(); }); it('allows a new submission after rejection without another purchase', function () { Storage::fake('identity_documents'); $user = User::factory()->create([ 'certification_purchased_at' => now(), ]); $admin = User::factory()->create([ 'role' => UserRole::ADMIN, ]); $verificationRequest = IdentityVerificationRequest::factory()->for($user)->create(); Storage::disk('identity_documents')->put($verificationRequest->document_path, 'identity'); app(ReviewIdentityVerification::class)->execute( $verificationRequest, IdentityVerificationStatus::REJECTED, $admin, 'Le document est illisible.', ); Sanctum::actingAs($user); $this->post('/api/certification/identity-document', [ 'document' => UploadedFile::fake()->image('identity-new.jpg'), ]) ->assertOk() ->assertJsonPath('data.verification.status', IdentityVerificationStatus::PENDING->value); expect($verificationRequest->fresh()->rejection_reason)->toBeNull() ->and($verificationRequest->fresh()->document_path)->not->toBeNull(); }); it('restricts identity review records to administrators', function () { $policy = new IdentityVerificationRequestPolicy; $request = IdentityVerificationRequest::factory()->make(); $admin = User::factory()->make(['role' => UserRole::ADMIN]); $moderator = User::factory()->make(['role' => UserRole::MODERATOR]); expect($policy->viewAny($admin))->toBeTrue() ->and($policy->view($admin, $request))->toBeTrue() ->and($policy->viewAny($moderator))->toBeFalse() ->and($policy->view($moderator, $request))->toBeFalse(); }); it('lets an administrator approve the identity request from filament', function () { app()->setLocale('fr'); Filament::setCurrentPanel(Filament::getPanel('dashboard')); Storage::fake('identity_documents'); $admin = User::factory()->create([ 'role' => UserRole::ADMIN, ]); $user = User::factory()->create([ 'certification_purchased_at' => now(), ]); $verificationRequest = IdentityVerificationRequest::factory()->for($user)->create(); Storage::disk('identity_documents')->put($verificationRequest->document_path, 'identity'); $this->actingAs($admin); Livewire::test(ListIdentityVerificationRequests::class) ->assertCanSeeTableRecords([$verificationRequest]) ->callTableAction('approve', $verificationRequest); expect($verificationRequest->fresh()->status)->toBe(IdentityVerificationStatus::APPROVED) ->and($user->fresh()->account_verified_at)->not->toBeNull(); }); it('grants the lifetime certification entitlement from RevenueCat without a trial', function () { config()->set('billing.revenuecat.secret_key', 'rc-secret'); config()->set('billing.revenuecat.certification_entitlement', 'certification'); $user = User::factory()->create(); $purchaseDate = now()->subDay()->startOfSecond(); Sanctum::actingAs($user); Http::fake([ "api.revenuecat.com/v1/subscribers/{$user->id}" => Http::response([ 'subscriber' => [ 'entitlements' => [ 'certification' => [ 'expires_date' => null, 'grace_period_expires_date' => null, 'product_identifier' => 'lifetime', 'purchase_date' => $purchaseDate->toISOString(), ], ], 'subscriptions' => [], ], ]), ]); $this->postJson('/api/billing/sync') ->assertOk() ->assertJsonPath('data.certificationPurchased', true) ->assertJsonPath('data.analysisAccessLevel', 'free'); expect($user->fresh()->certification_purchased_at?->equalTo($purchaseDate))->toBeTrue() ->and($user->fresh()->subscription_is_trial)->toBeFalse(); });