whisper-money/tests/Feature/DeleteUserCommandTest.php

260 lines
12 KiB
PHP

<?php
use App\Contracts\BankingProviderInterface;
use App\Enums\BankingConnectionStatus;
use App\Models\Account;
use App\Models\AccountBalance;
use App\Models\AutomationRule;
use App\Models\Bank;
use App\Models\BankingConnection;
use App\Models\Category;
use App\Models\EncryptedMessage;
use App\Models\Label;
use App\Models\Transaction;
use App\Models\User;
use App\Models\UserMailLog;
use Laravel\Cashier\Subscription;
test('marks user as deleted, preserves data, and prefixes email with timestamp when confirmed', function () {
$this->travelTo(now()->setDate(2026, 4, 22)->setTime(10, 51, 24));
$user = User::factory()->onboarded()->create([
'email' => 'test@example.com',
'name' => 'Test User',
]);
$mockProvider = Mockery::mock(BankingProviderInterface::class);
$mockProvider->shouldNotReceive('revokeSession');
app()->instance(BankingProviderInterface::class, $mockProvider);
// Create associated data
EncryptedMessage::query()->create([
'user_id' => $user->id,
'encrypted_content' => 'test-content',
'iv' => 'test-iv',
]);
Transaction::factory()->count(3)->create(['user_id' => $user->id]);
$account = Account::factory()->create(['user_id' => $user->id]);
AccountBalance::factory()->count(2)->create(['account_id' => $account->id]);
Account::factory()->create(['user_id' => $user->id]);
Category::factory()->count(2)->create(['user_id' => $user->id]);
AutomationRule::factory()->count(1)->create(['user_id' => $user->id]);
Label::factory()->count(2)->create(['user_id' => $user->id]);
UserMailLog::factory()->count(1)->create(['user_id' => $user->id]);
Bank::factory()->count(2)->create(['user_id' => $user->id]);
// Confirm deletion
$this->artisan('user:delete', ['email' => 'test@example.com'])
->expectsConfirmation("Are you sure you want to mark user 'Test User' (test@example.com) as deleted? Their data will be preserved.", 'yes')
->expectsOutput("User 'test@example.com' has been marked as deleted. Their data remains in the database.")
->assertSuccessful();
$deletedUser = User::withTrashed()->find($user->id);
expect(User::query()->where('email', 'test@example.com')->exists())->toBeFalse();
expect($deletedUser?->deleted_at)->not->toBeNull();
expect($deletedUser?->email)->toBe('20260422105124_test@example.com');
expect(EncryptedMessage::query()->where('user_id', $user->id)->exists())->toBeTrue();
expect(Transaction::query()->where('user_id', $user->id)->exists())->toBeTrue();
expect(Account::query()->where('user_id', $user->id)->exists())->toBeTrue();
expect(AccountBalance::query()->where('account_id', $account->id)->exists())->toBeTrue();
expect(Category::query()->where('user_id', $user->id)->exists())->toBeTrue();
expect(AutomationRule::query()->where('user_id', $user->id)->exists())->toBeTrue();
expect(Label::query()->where('user_id', $user->id)->exists())->toBeTrue();
expect(UserMailLog::query()->where('user_id', $user->id)->exists())->toBeTrue();
expect(Bank::query()->where('user_id', $user->id)->exists())->toBeTrue();
});
test('shows error when user not found', function () {
$this->artisan('user:delete', ['email' => 'nonexistent@example.com'])
->expectsOutput("User with email 'nonexistent@example.com' not found.")
->assertFailed();
});
test('cancels deletion when not confirmed', function () {
$user = User::factory()->onboarded()->create([
'email' => 'test@example.com',
'name' => 'Test User',
]);
$this->artisan('user:delete', ['email' => 'test@example.com'])
->expectsConfirmation("Are you sure you want to mark user 'Test User' (test@example.com) as deleted? Their data will be preserved.", 'no')
->expectsOutput('Deletion cancelled.')
->assertSuccessful();
// Verify user still exists
expect(User::query()->where('email', 'test@example.com')->exists())->toBeTrue();
});
test('marks user as deleted without associated data', function () {
$this->travelTo(now()->setDate(2026, 4, 22)->setTime(10, 51, 24));
$user = User::factory()->onboarded()->create([
'email' => 'test@example.com',
'name' => 'Test User',
]);
$mockProvider = Mockery::mock(BankingProviderInterface::class);
$mockProvider->shouldNotReceive('revokeSession');
app()->instance(BankingProviderInterface::class, $mockProvider);
$this->artisan('user:delete', ['email' => 'test@example.com'])
->expectsConfirmation("Are you sure you want to mark user 'Test User' (test@example.com) as deleted? Their data will be preserved.", 'yes')
->expectsOutput("User 'test@example.com' has been marked as deleted. Their data remains in the database.")
->assertSuccessful();
$deletedUser = User::withTrashed()->find($user->id);
expect(User::query()->where('email', 'test@example.com')->exists())->toBeFalse();
expect($deletedUser?->deleted_at)->not->toBeNull();
expect($deletedUser?->email)->toBe('20260422105124_test@example.com');
});
test('does not delete other users data', function () {
$this->travelTo(now()->setDate(2026, 4, 22)->setTime(10, 51, 24));
$userToDelete = User::factory()->onboarded()->create([
'email' => 'delete@example.com',
]);
$mockProvider = Mockery::mock(BankingProviderInterface::class);
$mockProvider->shouldNotReceive('revokeSession');
app()->instance(BankingProviderInterface::class, $mockProvider);
$otherUser = User::factory()->onboarded()->create([
'email' => 'keep@example.com',
]);
// Create data for both users
Account::factory()->create(['user_id' => $userToDelete->id]);
Account::factory()->create(['user_id' => $otherUser->id]);
$this->artisan('user:delete', ['email' => 'delete@example.com'])
->expectsConfirmation("Are you sure you want to mark user '{$userToDelete->name}' (delete@example.com) as deleted? Their data will be preserved.", 'yes')
->assertSuccessful();
$deletedUser = User::withTrashed()->find($userToDelete->id);
expect(User::query()->where('email', 'delete@example.com')->exists())->toBeFalse();
expect($deletedUser?->deleted_at)->not->toBeNull();
expect($deletedUser?->email)->toBe('20260422105124_delete@example.com');
expect(User::query()->where('email', 'keep@example.com')->exists())->toBeTrue();
// Verify other user's data is intact
expect(Account::query()->where('user_id', $otherUser->id)->exists())->toBeTrue();
});
test('cancels active subscription before deleting user when confirmed', function () {
$this->travelTo(now()->setDate(2026, 4, 22)->setTime(10, 51, 24));
$user = User::factory()->onboarded()->create([
'email' => 'subscribed@example.com',
'name' => 'Subscribed User',
]);
$subscription = Subscription::query()->create([
'user_id' => $user->id,
'type' => 'default',
'stripe_id' => 'sub_test123',
'stripe_status' => 'active',
'stripe_price' => 'price_test123',
'quantity' => 1,
]);
$mockProvider = Mockery::mock(BankingProviderInterface::class);
$mockProvider->shouldNotReceive('revokeSession');
app()->instance(BankingProviderInterface::class, $mockProvider);
$this->artisan('user:delete', ['email' => 'subscribed@example.com'])
->expectsConfirmation("Are you sure you want to mark user 'Subscribed User' (subscribed@example.com) as deleted? Their data will be preserved.", 'yes')
->expectsConfirmation("User 'subscribed@example.com' has an active Stripe subscription. Cancel it before deleting the user?", 'yes')
->expectsOutput("Cancelled active Stripe subscription for 'subscribed@example.com'.")
->expectsOutput("User 'subscribed@example.com' has been marked as deleted. Their data remains in the database.")
->assertSuccessful();
expect($subscription->fresh()->stripe_status)->toBe('canceled');
expect($subscription->fresh()->ends_at)->not->toBeNull();
expect(User::withTrashed()->find($user->id)?->deleted_at)->not->toBeNull();
});
test('cancels deletion when subscription cancellation is not confirmed', function () {
$user = User::factory()->onboarded()->create([
'email' => 'subscribed@example.com',
'name' => 'Subscribed User',
]);
Subscription::query()->create([
'user_id' => $user->id,
'type' => 'default',
'stripe_id' => 'sub_test123',
'stripe_status' => 'active',
'stripe_price' => 'price_test123',
'quantity' => 1,
]);
$mockProvider = Mockery::mock(BankingProviderInterface::class);
$mockProvider->shouldNotReceive('revokeSession');
app()->instance(BankingProviderInterface::class, $mockProvider);
$this->artisan('user:delete', ['email' => 'subscribed@example.com'])
->expectsConfirmation("Are you sure you want to mark user 'Subscribed User' (subscribed@example.com) as deleted? Their data will be preserved.", 'yes')
->expectsConfirmation("User 'subscribed@example.com' has an active Stripe subscription. Cancel it before deleting the user?", 'no')
->expectsOutput('Deletion cancelled.')
->assertSuccessful();
expect(User::query()->where('email', 'subscribed@example.com')->exists())->toBeTrue();
expect(Subscription::query()->where('user_id', $user->id)->first()?->stripe_status)->toBe('active');
});
test('revokes enable banking connections before deleting user when confirmed', function () {
$this->travelTo(now()->setDate(2026, 4, 22)->setTime(10, 51, 24));
$user = User::factory()->onboarded()->create([
'email' => 'banking@example.com',
'name' => 'Banking User',
]);
$connection = BankingConnection::factory()->for($user)->create();
$account = Account::factory()->for($user)->create([
'banking_connection_id' => $connection->id,
'external_account_id' => 'ext-123',
]);
$mockProvider = Mockery::mock(BankingProviderInterface::class);
$mockProvider->shouldReceive('revokeSession')->once()->with($connection->session_id);
app()->instance(BankingProviderInterface::class, $mockProvider);
$this->artisan('user:delete', ['email' => 'banking@example.com'])
->expectsConfirmation("Are you sure you want to mark user 'Banking User' (banking@example.com) as deleted? Their data will be preserved.", 'yes')
->expectsConfirmation("User 'banking@example.com' has 1 Enable Banking connection(s). Revoke them and keep linked accounts as manual accounts?", 'yes')
->expectsOutput("Revoked 1 Enable Banking connection(s) for 'banking@example.com'.")
->expectsOutput("User 'banking@example.com' has been marked as deleted. Their data remains in the database.")
->assertSuccessful();
expect($connection->fresh()->status)->toBe(BankingConnectionStatus::Revoked);
expect($connection->fresh()->trashed())->toBeTrue();
expect($account->fresh()->banking_connection_id)->toBeNull();
expect($account->fresh()->external_account_id)->toBeNull();
expect(User::withTrashed()->find($user->id)?->deleted_at)->not->toBeNull();
});
test('cancels deletion when enable banking revocation is not confirmed', function () {
$user = User::factory()->onboarded()->create([
'email' => 'banking@example.com',
'name' => 'Banking User',
]);
BankingConnection::factory()->for($user)->create();
$mockProvider = Mockery::mock(BankingProviderInterface::class);
$mockProvider->shouldNotReceive('revokeSession');
app()->instance(BankingProviderInterface::class, $mockProvider);
$this->artisan('user:delete', ['email' => 'banking@example.com'])
->expectsConfirmation("Are you sure you want to mark user 'Banking User' (banking@example.com) as deleted? Their data will be preserved.", 'yes')
->expectsConfirmation("User 'banking@example.com' has 1 Enable Banking connection(s). Revoke them and keep linked accounts as manual accounts?", 'no')
->expectsOutput('Deletion cancelled.')
->assertSuccessful();
expect(User::query()->where('email', 'banking@example.com')->exists())->toBeTrue();
expect(BankingConnection::query()->where('user_id', $user->id)->first()?->trashed())->toBeFalse();
});