186 lines
6.3 KiB
PHP
186 lines
6.3 KiB
PHP
<?php
|
|
|
|
use App\Enums\SocialProvider;
|
|
use App\Models\MealPosts;
|
|
use App\Models\SocialAccount;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Http\Client\Request as ClientRequest;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Str;
|
|
use Laravel\Sanctum\Sanctum;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
function configureDeletionAppleOAuthClient(): void
|
|
{
|
|
$key = openssl_pkey_new([
|
|
'curve_name' => '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);
|
|
});
|