Files
timebank-cc-public/tests/Feature/Security/Authorization/TransactionViewAuthorizationTest.php
Ronald Huynen 2547717edb Initial commit
2026-03-23 21:37:59 +01:00

551 lines
17 KiB
PHP

<?php
namespace Tests\Feature\Security\Authorization;
use App\Models\Account;
use App\Models\Admin;
use App\Models\Bank;
use App\Models\Organization;
use App\Models\Transaction;
use App\Models\TransactionType;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Tests\TestCase;
/**
* Transaction View Authorization Tests
*
* Tests that users can only view transactions involving their own accounts
* and cannot view other users' transactions across all authentication guards.
*
* @group security
* @group authorization
* @group multi-guard
* @group transaction-view
* @group critical
*/
class TransactionViewAuthorizationTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
// Create transaction types (using insert to avoid mass assignment protection)
\DB::table('transaction_types')->insert([
'id' => 1,
'name' => 'worked_hours',
'label' => 'Worked Hours',
'icon' => 'clock',
]);
}
/**
* Test user can view transaction they are involved in (sender)
*
* @test
*/
public function user_can_view_transaction_as_sender()
{
$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,
]);
$transaction = Transaction::factory()->create([
'from_account_id' => $userAccount->id,
'to_account_id' => $recipientAccount->id,
'amount' => 60,
]);
$this->actingAs($user, 'web');
session(['activeProfileType' => User::class]);
session(['activeProfileId' => $user->id]);
$response = $this->get(route('transaction.show', ['transactionId' => $transaction->id]));
$response->assertStatus(200);
}
/**
* Test user can view transaction they are involved in (recipient)
*
* @test
*/
public function user_can_view_transaction_as_recipient()
{
$sender = User::factory()->create();
$user = User::factory()->create();
$senderAccount = Account::factory()->create([
'accountable_type' => User::class,
'accountable_id' => $sender->id,
]);
$userAccount = Account::factory()->create([
'accountable_type' => User::class,
'accountable_id' => $user->id,
]);
$transaction = Transaction::factory()->create([
'from_account_id' => $senderAccount->id,
'to_account_id' => $userAccount->id,
'amount' => 60,
]);
$this->actingAs($user, 'web');
session(['activeProfileType' => User::class]);
session(['activeProfileId' => $user->id]);
$response = $this->get(route('transaction.show', ['transactionId' => $transaction->id]));
$response->assertStatus(200);
}
/**
* Test user cannot view transaction they are not involved in
*
* @test
*/
public function user_cannot_view_transaction_they_are_not_involved_in()
{
$user = User::factory()->create();
$sender = User::factory()->create();
$recipient = User::factory()->create();
$senderAccount = Account::factory()->create([
'accountable_type' => User::class,
'accountable_id' => $sender->id,
]);
$recipientAccount = Account::factory()->create([
'accountable_type' => User::class,
'accountable_id' => $recipient->id,
]);
$transaction = Transaction::factory()->create([
'from_account_id' => $senderAccount->id,
'to_account_id' => $recipientAccount->id,
'amount' => 60,
]);
// User is NOT involved in this transaction
$this->actingAs($user, 'web');
session(['activeProfileType' => User::class]);
session(['activeProfileId' => $user->id]);
$response = $this->get(route('transaction.show', ['transactionId' => $transaction->id]));
$response->assertStatus(403);
}
/**
* Test organization can view transaction they are involved in
*
* @test
*/
public function organization_can_view_transaction_they_are_involved_in()
{
$orgUser = User::factory()->create();
$organization = Organization::factory()->create();
$organization->users()->attach($orgUser->id);
$recipient = User::factory()->create();
$orgAccount = Account::factory()->create([
'accountable_type' => Organization::class,
'accountable_id' => $organization->id,
]);
$recipientAccount = Account::factory()->create([
'accountable_type' => User::class,
'accountable_id' => $recipient->id,
]);
$transaction = Transaction::factory()->create([
'from_account_id' => $orgAccount->id,
'to_account_id' => $recipientAccount->id,
'amount' => 120,
]);
$this->actingAs($organization, 'organization');
session(['activeProfileType' => Organization::class]);
session(['activeProfileId' => $organization->id]);
$response = $this->get(route('transaction.show', ['transactionId' => $transaction->id]));
$response->assertStatus(200);
}
/**
* Test organization cannot view transaction of another organization
*
* @test
*/
public function organization_cannot_view_another_organizations_transaction()
{
$user = User::factory()->create();
$org1 = Organization::factory()->create();
$org2 = Organization::factory()->create();
$org1->users()->attach($user->id);
$recipient = User::factory()->create();
$org2Account = Account::factory()->create([
'accountable_type' => Organization::class,
'accountable_id' => $org2->id,
]);
$recipientAccount = Account::factory()->create([
'accountable_type' => User::class,
'accountable_id' => $recipient->id,
]);
$transaction = Transaction::factory()->create([
'from_account_id' => $org2Account->id,
'to_account_id' => $recipientAccount->id,
'amount' => 120,
]);
// Logged in as org1, trying to view org2's transaction
$this->actingAs($org1, 'organization');
session(['activeProfileType' => Organization::class]);
session(['activeProfileId' => $org1->id]);
$response = $this->get(route('transaction.show', ['transactionId' => $transaction->id]));
$response->assertStatus(403);
}
/**
* Test bank can view transaction they are involved in
*
* @test
*/
public function bank_can_view_transaction_they_are_involved_in()
{
$bankUser = User::factory()->create();
$bank = Bank::factory()->create();
$bank->managers()->attach($bankUser->id);
$recipient = User::factory()->create();
$bankAccount = Account::factory()->create([
'accountable_type' => Bank::class,
'accountable_id' => $bank->id,
]);
$recipientAccount = Account::factory()->create([
'accountable_type' => User::class,
'accountable_id' => $recipient->id,
]);
$transaction = Transaction::factory()->create([
'from_account_id' => $bankAccount->id,
'to_account_id' => $recipientAccount->id,
'amount' => 200,
]);
$this->actingAs($bank, 'bank');
session(['activeProfileType' => Bank::class]);
session(['activeProfileId' => $bank->id]);
$response = $this->get(route('transaction.show', ['transactionId' => $transaction->id]));
$response->assertStatus(200);
}
/**
* Test session manipulation to view unauthorized transaction is blocked
*
* @test
*/
public function session_manipulation_to_view_transaction_is_blocked()
{
$user1 = User::factory()->create();
$user2 = User::factory()->create();
$recipient = User::factory()->create();
$user2Account = Account::factory()->create([
'accountable_type' => User::class,
'accountable_id' => $user2->id,
]);
$recipientAccount = Account::factory()->create([
'accountable_type' => User::class,
'accountable_id' => $recipient->id,
]);
$transaction = Transaction::factory()->create([
'from_account_id' => $user2Account->id,
'to_account_id' => $recipientAccount->id,
'amount' => 60,
]);
// Logged in as user1
$this->actingAs($user1, 'web');
// Malicious: manipulate session to impersonate user2
session(['activeProfileType' => User::class]);
session(['activeProfileId' => $user2->id]); // Attacker sets this!
$response = $this->get(route('transaction.show', ['transactionId' => $transaction->id]));
// Should be blocked by statement() query which checks session against database
$response->assertStatus(403);
}
/**
* Test cross-guard attack to view transaction is blocked
*
* @test
*/
public function cross_guard_attack_to_view_transaction_is_blocked()
{
$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,
]);
$recipientAccount = Account::factory()->create([
'accountable_type' => User::class,
'accountable_id' => $recipient->id,
]);
$transaction = Transaction::factory()->create([
'from_account_id' => $orgAccount->id,
'to_account_id' => $recipientAccount->id,
'amount' => 120,
]);
// Logged in as user (web guard)
$this->actingAs($user, 'web');
// Malicious: manipulate session to view org's transaction
session(['activeProfileType' => Organization::class]);
session(['activeProfileId' => $organization->id]);
$response = $this->get(route('transaction.show', ['transactionId' => $transaction->id]));
// Should be blocked because auth guard doesn't match session
$response->assertStatus(403);
}
/**
* Test unauthenticated user cannot view transactions
*
* @test
*/
public function unauthenticated_user_cannot_view_transactions()
{
$sender = User::factory()->create();
$recipient = User::factory()->create();
$senderAccount = Account::factory()->create([
'accountable_type' => User::class,
'accountable_id' => $sender->id,
]);
$recipientAccount = Account::factory()->create([
'accountable_type' => User::class,
'accountable_id' => $recipient->id,
]);
$transaction = Transaction::factory()->create([
'from_account_id' => $senderAccount->id,
'to_account_id' => $recipientAccount->id,
'amount' => 60,
]);
// Not authenticated
$response = $this->get(route('transaction.show', ['transactionId' => $transaction->id]));
$response->assertRedirect(route('login'));
}
/**
* Test user can access transactions list page
*
* @test
*/
public function user_can_access_transactions_list_page()
{
$user = User::factory()->create();
$this->actingAs($user, 'web');
session(['activeProfileType' => User::class]);
session(['activeProfileId' => $user->id]);
$response = $this->get(route('transactions'));
$response->assertStatus(200);
}
/**
* Test organization can access transactions list page
*
* @test
*/
public function organization_can_access_transactions_list_page()
{
$user = User::factory()->create();
$organization = Organization::factory()->create();
$organization->users()->attach($user->id);
$this->actingAs($organization, 'organization');
session(['activeProfileType' => Organization::class]);
session(['activeProfileId' => $organization->id]);
$response = $this->get(route('transactions'));
$response->assertStatus(200);
}
/**
* Test bank can access transactions list page
*
* @test
*/
public function bank_can_access_transactions_list_page()
{
$user = User::factory()->create();
$bank = Bank::factory()->create();
$bank->managers()->attach($user->id);
$this->actingAs($bank, 'bank');
session(['activeProfileType' => Bank::class]);
session(['activeProfileId' => $bank->id]);
$response = $this->get(route('transactions'));
$response->assertStatus(200);
}
/**
* Test non-existent transaction returns 403
*
* @test
*/
public function non_existent_transaction_returns_403()
{
$user = User::factory()->create();
$this->actingAs($user, 'web');
session(['activeProfileType' => User::class]);
session(['activeProfileId' => $user->id]);
$response = $this->get(route('transaction.show', ['transactionId' => 99999]));
$response->assertStatus(403);
}
/**
* Test TransactionsTable Livewire component loads for user
*
* @test
*/
public function transactions_table_livewire_component_loads_for_user()
{
$user = User::factory()->create();
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\TransactionsTable::class);
$response->assertStatus(200);
}
/**
* Test TransactionsTable Livewire component loads for organization
*
* @test
*/
public function transactions_table_livewire_component_loads_for_organization()
{
$user = User::factory()->create();
$organization = Organization::factory()->create();
$organization->users()->attach($user->id);
Account::factory()->create([
'accountable_type' => Organization::class,
'accountable_id' => $organization->id,
]);
$this->actingAs($organization, 'organization');
session(['activeProfileType' => Organization::class]);
session(['activeProfileId' => $organization->id]);
$response = Livewire::test(\App\Http\Livewire\TransactionsTable::class);
$response->assertStatus(200);
}
/**
* Test TransactionsTable filters transactions by active profile
*
* @test
*/
public function transactions_table_filters_by_active_profile()
{
$user1 = User::factory()->create();
$user2 = 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,
]);
// Transaction involving user1
Transaction::factory()->create([
'from_account_id' => $user1Account->id,
'to_account_id' => $user2Account->id,
'amount' => 60,
'description' => 'User1 transaction',
]);
// Transaction NOT involving user1
Transaction::factory()->create([
'from_account_id' => $user2Account->id,
'to_account_id' => $user2Account->id, // Self transaction for testing
'amount' => 30,
'description' => 'User2 only transaction',
]);
$this->actingAs($user1, 'web');
session(['activeProfileType' => User::class]);
session(['activeProfileId' => $user1->id]);
$response = Livewire::test(\App\Http\Livewire\TransactionsTable::class);
// Should see user1's transaction
$response->assertSee('User1 transaction');
// Should NOT see user2's private transaction
$response->assertDontSee('User2 only transaction');
}
}