insert([ ['id' => 1, 'name' => 'worked_hours', 'label' => 'Worked Hours', 'icon' => 'clock'], ['id' => 2, 'name' => 'gift', 'label' => 'Gift', 'icon' => 'gift'], ['id' => 3, 'name' => 'donation', 'label' => 'Donation', 'icon' => 'hand-thumb-up'], ]); } /** * Test user can make payment from their own account * * @test */ public function user_can_make_payment_from_own_account() { $user = User::factory()->create(); $recipient = User::factory()->create(); $userAccount = Account::factory()->create([ 'accountable_type' => User::class, 'accountable_id' => $user->id, 'limit_min' => -1000, 'limit_max' => 1000, ]); $recipientAccount = Account::factory()->create([ 'accountable_type' => User::class, 'accountable_id' => $recipient->id, 'limit_min' => -1000, 'limit_max' => 1000, ]); $this->actingAs($user, 'web'); session(['activeProfileType' => User::class]); session(['activeProfileId' => $user->id]); $response = Livewire::test(\App\Http\Livewire\Pay::class) ->set('fromAccountId', $userAccount->id) ->set('toAccountId', $recipientAccount->id) ->set('amount', 60) ->set('description', 'Test payment') ->set('transactionTypeSelected', ['id' => 1, 'name' => 'worked_hours']) ->call('doTransfer'); $response->assertHasNoErrors(); $this->assertDatabaseHas('transactions', [ 'from_account_id' => $userAccount->id, 'to_account_id' => $recipientAccount->id, 'amount' => 60, ]); } /** * Test user cannot make payment from another user's account * * @test */ public function user_cannot_make_payment_from_another_users_account() { $user1 = User::factory()->create(); $user2 = User::factory()->create(); $recipient = User::factory()->create(); $user1Account = Account::factory()->create([ 'accountable_type' => User::class, 'accountable_id' => $user1->id, ]); $user2Account = Account::factory()->create([ 'accountable_type' => User::class, 'accountable_id' => $user2->id, ]); $recipientAccount = Account::factory()->create([ 'accountable_type' => User::class, 'accountable_id' => $recipient->id, ]); // Login as user1 $this->actingAs($user1, 'web'); session(['activeProfileType' => User::class]); session(['activeProfileId' => $user1->id]); // Try to make payment from user2's account $response = Livewire::test(\App\Http\Livewire\Pay::class) ->set('fromAccountId', $user2Account->id) // Unauthorized account! ->set('toAccountId', $recipientAccount->id) ->set('amount', 60) ->set('description', 'Unauthorized payment') ->set('transactionTypeSelected', ['id' => 1, 'name' => 'worked_hours']) ->call('doTransfer'); // Should redirect back with error $response->assertRedirect(); // Transaction should NOT be created $this->assertDatabaseMissing('transactions', [ 'from_account_id' => $user2Account->id, 'amount' => 60, 'description' => 'Unauthorized payment', ]); } /** * Test organization can make payment from their own account * * @test */ public function organization_can_make_payment_from_own_account() { $user = User::factory()->create(); $organization = Organization::factory()->create(); $organization->users()->attach($user->id); $recipient = User::factory()->create(); $orgAccount = Account::factory()->create([ 'accountable_type' => Organization::class, 'accountable_id' => $organization->id, 'limit_min' => -1000, 'limit_max' => 1000, ]); $recipientAccount = Account::factory()->create([ 'accountable_type' => User::class, 'accountable_id' => $recipient->id, 'limit_min' => -1000, 'limit_max' => 1000, ]); $this->actingAs($organization, 'organization'); session(['activeProfileType' => Organization::class]); session(['activeProfileId' => $organization->id]); $response = Livewire::test(\App\Http\Livewire\Pay::class) ->set('fromAccountId', $orgAccount->id) ->set('toAccountId', $recipientAccount->id) ->set('amount', 120) ->set('description', 'Organization payment') ->set('transactionTypeSelected', ['id' => 1, 'name' => 'worked_hours']) ->call('doTransfer'); $response->assertHasNoErrors(); $this->assertDatabaseHas('transactions', [ 'from_account_id' => $orgAccount->id, 'to_account_id' => $recipientAccount->id, 'amount' => 120, ]); } /** * Test organization cannot make payment from another organization's account * * @test */ public function organization_cannot_make_payment_from_another_organizations_account() { $user = User::factory()->create(); $org1 = Organization::factory()->create(); $org2 = Organization::factory()->create(); $org1->users()->attach($user->id); $recipient = User::factory()->create(); $org1Account = Account::factory()->create([ 'accountable_type' => Organization::class, 'accountable_id' => $org1->id, ]); $org2Account = Account::factory()->create([ 'accountable_type' => Organization::class, 'accountable_id' => $org2->id, ]); $recipientAccount = Account::factory()->create([ 'accountable_type' => User::class, 'accountable_id' => $recipient->id, ]); // Login as org1 $this->actingAs($org1, 'organization'); session(['activeProfileType' => Organization::class]); session(['activeProfileId' => $org1->id]); // Try to make payment from org2's account $response = Livewire::test(\App\Http\Livewire\Pay::class) ->set('fromAccountId', $org2Account->id) // Unauthorized! ->set('toAccountId', $recipientAccount->id) ->set('amount', 120) ->set('description', 'Unauthorized org payment') ->set('transactionTypeSelected', ['id' => 1, 'name' => 'worked_hours']) ->call('doTransfer'); $response->assertRedirect(); $this->assertDatabaseMissing('transactions', [ 'from_account_id' => $org2Account->id, 'description' => 'Unauthorized org payment', ]); } /** * Test bank can make payment from their own account * * @test */ public function bank_can_make_payment_from_own_account() { $user = User::factory()->create(); $bank = Bank::factory()->create(); $bank->managers()->attach($user->id); $recipient = User::factory()->create(); $bankAccount = Account::factory()->create([ 'accountable_type' => Bank::class, 'accountable_id' => $bank->id, 'limit_min' => -10000, 'limit_max' => 10000, ]); $recipientAccount = Account::factory()->create([ 'accountable_type' => User::class, 'accountable_id' => $recipient->id, 'limit_min' => -1000, 'limit_max' => 1000, ]); $this->actingAs($bank, 'bank'); session(['activeProfileType' => Bank::class]); session(['activeProfileId' => $bank->id]); $response = Livewire::test(\App\Http\Livewire\Pay::class) ->set('fromAccountId', $bankAccount->id) ->set('toAccountId', $recipientAccount->id) ->set('amount', 200) ->set('description', 'Bank payment') ->set('transactionTypeSelected', ['id' => 1, 'name' => 'worked_hours']) ->call('doTransfer'); $response->assertHasNoErrors(); $this->assertDatabaseHas('transactions', [ 'from_account_id' => $bankAccount->id, 'to_account_id' => $recipientAccount->id, 'amount' => 200, ]); } /** * Test bank cannot make payment from another bank's account * * @test */ public function bank_cannot_make_payment_from_another_banks_account() { $user = User::factory()->create(); $bank1 = Bank::factory()->create(); $bank2 = Bank::factory()->create(); $bank1->users()->attach($user->id); $recipient = User::factory()->create(); $bank1Account = Account::factory()->create([ 'accountable_type' => Bank::class, 'accountable_id' => $bank1->id, ]); $bank2Account = Account::factory()->create([ 'accountable_type' => Bank::class, 'accountable_id' => $bank2->id, ]); $recipientAccount = Account::factory()->create([ 'accountable_type' => User::class, 'accountable_id' => $recipient->id, ]); // Login as bank1 $this->actingAs($bank1, 'bank'); session(['activeProfileType' => Bank::class]); session(['activeProfileId' => $bank1->id]); // Try to make payment from bank2's account $response = Livewire::test(\App\Http\Livewire\Pay::class) ->set('fromAccountId', $bank2Account->id) // Unauthorized! ->set('toAccountId', $recipientAccount->id) ->set('amount', 200) ->set('description', 'Unauthorized bank payment') ->set('transactionTypeSelected', ['id' => 1, 'name' => 'worked_hours']) ->call('doTransfer'); $response->assertRedirect(); $this->assertDatabaseMissing('transactions', [ 'from_account_id' => $bank2Account->id, 'description' => 'Unauthorized bank payment', ]); } /** * Test session manipulation attack prevention * * @test */ public function session_manipulation_attack_is_prevented() { $user1 = User::factory()->create(); $user2 = User::factory()->create(); $recipient = User::factory()->create(); $user1Account = Account::factory()->create([ 'accountable_type' => User::class, 'accountable_id' => $user1->id, ]); $user2Account = Account::factory()->create([ 'accountable_type' => User::class, 'accountable_id' => $user2->id, ]); $recipientAccount = Account::factory()->create([ 'accountable_type' => User::class, 'accountable_id' => $recipient->id, ]); // Login as user1 $this->actingAs($user1, 'web'); // Malicious: manipulate session to impersonate user2 session(['activeProfileType' => User::class]); session(['activeProfileId' => $user2->id]); // Attacker sets this to user2! // Try to make payment from user2's account $response = Livewire::test(\App\Http\Livewire\Pay::class) ->set('fromAccountId', $user2Account->id) ->set('toAccountId', $recipientAccount->id) ->set('amount', 60) ->set('description', 'Session manipulation attack') ->set('transactionTypeSelected', ['id' => 1, 'name' => 'worked_hours']) ->call('doTransfer'); // Should be blocked because doTransfer() checks getAccountsInfo() // which validates against the session's active profile $response->assertRedirect(); // Transaction should NOT be created $this->assertDatabaseMissing('transactions', [ 'from_account_id' => $user2Account->id, 'description' => 'Session manipulation attack', ]); } /** * Test cross-guard attack prevention (user trying to use org account) * * @test */ public function cross_guard_attack_is_prevented() { $user = User::factory()->create(); $organization = Organization::factory()->create(); $organization->users()->attach($user->id); $recipient = User::factory()->create(); $userAccount = Account::factory()->create([ 'accountable_type' => User::class, 'accountable_id' => $user->id, ]); $orgAccount = Account::factory()->create([ 'accountable_type' => Organization::class, 'accountable_id' => $organization->id, ]); $recipientAccount = Account::factory()->create([ 'accountable_type' => User::class, 'accountable_id' => $recipient->id, ]); // Login as user (web guard) $this->actingAs($user, 'web'); session(['activeProfileType' => User::class]); session(['activeProfileId' => $user->id]); // Try to make payment from organization's account while logged in as user $response = Livewire::test(\App\Http\Livewire\Pay::class) ->set('fromAccountId', $orgAccount->id) // Org account, but user guard! ->set('toAccountId', $recipientAccount->id) ->set('amount', 120) ->set('description', 'Cross-guard attack') ->set('transactionTypeSelected', ['id' => 1, 'name' => 'worked_hours']) ->call('doTransfer'); $response->assertRedirect(); $this->assertDatabaseMissing('transactions', [ 'from_account_id' => $orgAccount->id, 'description' => 'Cross-guard attack', ]); } /** * Test payment to same account is prevented * * @test */ public function payment_to_same_account_is_prevented() { $user = User::factory()->create(); $userAccount = Account::factory()->create([ 'accountable_type' => User::class, 'accountable_id' => $user->id, ]); $this->actingAs($user, 'web'); session(['activeProfileType' => User::class]); session(['activeProfileId' => $user->id]); $response = Livewire::test(\App\Http\Livewire\Pay::class) ->set('fromAccountId', $userAccount->id) ->set('toAccountId', $userAccount->id) // Same account! ->set('amount', 60) ->set('description', 'Self payment') ->set('transactionTypeSelected', ['id' => 1, 'name' => 'worked_hours']) ->call('doTransfer'); $response->assertRedirect(); $this->assertDatabaseMissing('transactions', [ 'from_account_id' => $userAccount->id, 'to_account_id' => $userAccount->id, ]); } /** * Test payment to non-existent account is prevented * * @test */ public function payment_to_nonexistent_account_is_prevented() { $user = User::factory()->create(); $userAccount = Account::factory()->create([ 'accountable_type' => User::class, 'accountable_id' => $user->id, ]); $this->actingAs($user, 'web'); session(['activeProfileType' => User::class]); session(['activeProfileId' => $user->id]); $response = Livewire::test(\App\Http\Livewire\Pay::class) ->set('fromAccountId', $userAccount->id) ->set('toAccountId', 99999) // Non-existent account ->set('amount', 60) ->set('description', 'Payment to nowhere') ->set('transactionTypeSelected', ['id' => 1, 'name' => 'worked_hours']) ->call('doTransfer'); $response->assertRedirect(); $this->assertDatabaseMissing('transactions', [ 'to_account_id' => 99999, ]); } /** * Test unauthenticated user cannot make payments * * @test */ public function unauthenticated_user_cannot_make_payments() { $user = User::factory()->create(); $recipient = User::factory()->create(); $userAccount = Account::factory()->create([ 'accountable_type' => User::class, 'accountable_id' => $user->id, ]); $recipientAccount = Account::factory()->create([ 'accountable_type' => User::class, 'accountable_id' => $recipient->id, ]); // Not authenticated session(['activeProfileType' => User::class]); session(['activeProfileId' => $user->id]); $response = Livewire::test(\App\Http\Livewire\Pay::class) ->set('fromAccountId', $userAccount->id) ->set('toAccountId', $recipientAccount->id) ->set('amount', 60) ->set('description', 'Unauthenticated payment') ->set('transactionTypeSelected', ['id' => 1, 'name' => 'worked_hours']) ->call('doTransfer'); // Should fail due to lack of authentication $this->assertDatabaseMissing('transactions', [ 'description' => 'Unauthenticated payment', ]); } /** * Test admin profile without account cannot make payments * * @test */ public function admin_without_account_cannot_make_payments() { $user = User::factory()->create(); $admin = Admin::factory()->create(); $admin->users()->attach($user->id); $recipient = User::factory()->create(); // Admin has no account $recipientAccount = Account::factory()->create([ 'accountable_type' => User::class, 'accountable_id' => $recipient->id, ]); $this->actingAs($admin, 'admin'); session(['activeProfileType' => Admin::class]); session(['activeProfileId' => $admin->id]); $response = Livewire::test(\App\Http\Livewire\Pay::class) ->set('fromAccountId', null) // No account ->set('toAccountId', $recipientAccount->id) ->set('amount', 60) ->set('description', 'Admin payment without account') ->set('transactionTypeSelected', ['id' => 1, 'name' => 'worked_hours']) ->call('doTransfer'); // Should show error notification $response->assertDispatched('notification'); } }