'prime256v1', 'private_key_type' => OPENSSL_KEYTYPE_EC, ]); if ($key === false || ! openssl_pkey_export($key, $privateKey)) { throw new RuntimeException('Unable to generate an Apple OAuth test key.'); } config()->set([ 'services.apple.client_id' => 'com.meal.daily', 'services.apple.issuer' => 'https://appleid.apple.com', 'services.apple.key_id' => 'APPLEKEY1', 'services.apple.private_key' => $privateKey, 'services.apple.private_key_base64' => null, 'services.apple.revoke_url' => 'https://appleid.apple.com/auth/revoke', 'services.apple.team_id' => 'TEAMID1234', ]); } it('deletes the authenticated account and related data', function () { config()->set('filesystems.default', 's3'); Storage::fake('s3'); $user = User::factory()->create([ 'avatar_url' => 'avatars/profile.jpg', ]); Storage::disk('s3')->put('avatars/profile.jpg', 'avatar'); Storage::disk('s3')->put('meal-posts/local.jpg', 'meal'); $localMealPost = MealPosts::factory()->for($user, 'user')->create([ 'image_url' => 'meal-posts/local.jpg', ]); $remoteMealPost = MealPosts::factory()->for($user, 'user')->create([ 'image_url' => 'https://example.com/meal.jpg', ]); $deviceToken = $user->deviceTokens()->create([ 'expo_push_token' => 'ExponentPushToken[account-deletion]', 'platform' => 'ios', ]); $user->createToken('api'); DB::table('notifications')->insert([ 'id' => (string) Str::uuid(), 'type' => 'test', 'notifiable_type' => User::class, 'notifiable_id' => $user->getKey(), 'data' => json_encode(['message' => 'test'], JSON_THROW_ON_ERROR), 'created_at' => now(), 'updated_at' => now(), ]); DB::table('sessions')->insert([ 'id' => 'account-deletion-session', 'user_id' => $user->getKey(), 'ip_address' => '127.0.0.1', 'user_agent' => 'Pest', 'payload' => 'payload', 'last_activity' => now()->timestamp, ]); DB::table('password_reset_tokens')->insert([ 'email' => $user->email, 'token' => 'reset-token', 'created_at' => now(), ]); Sanctum::actingAs($user); $this->deleteJson('/api/auth/me') ->assertOk() ->assertJsonPath('code', 'ACCOUNT_DELETED'); $this->assertDatabaseMissing('users', ['id' => $user->getKey()]); $this->assertDatabaseMissing('meal_posts', ['id' => $localMealPost->getKey()]); $this->assertDatabaseMissing('meal_posts', ['id' => $remoteMealPost->getKey()]); $this->assertDatabaseMissing('device_tokens', ['id' => $deviceToken->getKey()]); $this->assertDatabaseMissing('personal_access_tokens', ['tokenable_id' => $user->getKey()]); $this->assertDatabaseMissing('notifications', ['notifiable_id' => $user->getKey()]); $this->assertDatabaseMissing('sessions', ['user_id' => $user->getKey()]); $this->assertDatabaseMissing('password_reset_tokens', ['email' => $user->email]); Storage::disk('s3')->assertMissing('avatars/profile.jpg'); Storage::disk('s3')->assertMissing('meal-posts/local.jpg'); }); it('requires authentication to delete an account', function () { $this->deleteJson('/api/auth/me') ->assertUnauthorized(); }); it('revokes the Apple refresh token before deleting the account', function () { configureDeletionAppleOAuthClient(); Http::preventStrayRequests(); Http::fake([ 'https://appleid.apple.com/auth/revoke' => Http::response(status: 200), ]); $user = User::factory()->create(); $socialAccount = SocialAccount::factory()->for($user)->create([ 'provider' => SocialProvider::Apple, 'provider_refresh_token' => 'apple-refresh-token', ]); Sanctum::actingAs($user); $this->deleteJson('/api/auth/me') ->assertOk() ->assertJsonPath('code', 'ACCOUNT_DELETED'); Http::assertSent(function (ClientRequest $request): bool { $data = $request->data(); return $request->url() === 'https://appleid.apple.com/auth/revoke' && $request->isForm() && ($data['client_id'] ?? null) === 'com.meal.daily' && ($data['token'] ?? null) === 'apple-refresh-token' && ($data['token_type_hint'] ?? null) === 'refresh_token' && is_string($data['client_secret'] ?? null); }); $this->assertModelMissing($user); $this->assertModelMissing($socialAccount); }); it('keeps the account when Apple token revocation fails', function () { configureDeletionAppleOAuthClient(); Http::preventStrayRequests(); Http::fake([ 'https://appleid.apple.com/auth/revoke' => Http::response([ 'error' => 'invalid_client', ], 400), ]); $user = User::factory()->create(); $socialAccount = SocialAccount::factory()->for($user)->create([ 'provider' => SocialProvider::Apple, 'provider_refresh_token' => 'apple-refresh-token', ]); Sanctum::actingAs($user); $this->deleteJson('/api/auth/me') ->assertServiceUnavailable() ->assertJsonPath('code', 'SOCIAL_REVOCATION_FAILED'); $this->assertModelExists($user); $this->assertModelExists($socialAccount); expect($socialAccount->fresh()->provider_refresh_token)->toBe('apple-refresh-token'); }); it('keeps an Apple account that has no refresh token', function () { $user = User::factory()->create(); $socialAccount = SocialAccount::factory()->for($user)->create([ 'provider' => SocialProvider::Apple, 'provider_refresh_token' => null, ]); Sanctum::actingAs($user); $this->deleteJson('/api/auth/me') ->assertConflict() ->assertJsonPath('code', 'SOCIAL_REVOCATION_TOKEN_MISSING'); $this->assertModelExists($user); $this->assertModelExists($socialAccount); });