Initial commit

This commit is contained in:
Ronald Huynen
2026-03-23 21:37:59 +01:00
commit 2547717edb
2193 changed files with 972171 additions and 0 deletions

View File

@@ -0,0 +1,567 @@
<?php
namespace Tests\Feature\Security\Authorization;
use App\Models\Account;
use App\Models\Bank;
use App\Models\Organization;
use App\Models\Tag;
use App\Models\Transaction;
use App\Models\TransactionType;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Namu\WireChat\Models\Conversation;
use Namu\WireChat\Models\Message;
use Tests\TestCase;
/**
* Export Profile Data Authorization Tests
*
* Tests that users can only export their own profile data and cannot export
* data from profiles they don't own/manage.
*
* @group security
* @group authorization
* @group export
* @group critical
*/
class ExportProfileDataAuthorizationTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
// Create transaction type for transaction exports
\DB::table('transaction_types')->insert([
'id' => 1,
'name' => 'worked_hours',
'label' => 'Worked Hours',
'icon' => 'clock',
]);
}
/**
* Test user can export their own transactions
*
* @test
*/
public function user_can_export_own_transactions()
{
$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::factory()->create([
'from_account_id' => $userAccount->id,
'to_account_id' => $recipientAccount->id,
'amount' => 60,
]);
$this->actingAs($user, 'web');
session(['activeProfileType' => User::class, 'activeProfileId' => $user->id]);
$response = Livewire::test(\App\Http\Livewire\Profile\ExportProfileData::class)
->call('exportTransactions', 'json');
$response->assertStatus(200);
}
/**
* Test user cannot export another user's transactions via session manipulation
*
* @test
*/
public function user_cannot_export_another_users_transactions()
{
$user1 = User::factory()->create();
$user2 = User::factory()->create();
$user2Account = Account::factory()->create([
'accountable_type' => User::class,
'accountable_id' => $user2->id,
]);
// Logged in as user1
$this->actingAs($user1, 'web');
// Malicious: manipulate session to target user2
session(['activeProfileType' => User::class, 'activeProfileId' => $user2->id]);
$response = Livewire::test(\App\Http\Livewire\Profile\ExportProfileData::class)
->call('exportTransactions', 'json');
$response->assertStatus(403);
}
/**
* Test organization can export own transactions
*
* @test
*/
public function organization_can_export_own_transactions()
{
$user = User::factory()->create();
$organization = Organization::factory()->create();
$organization->users()->attach($user->id);
$orgAccount = Account::factory()->create([
'accountable_type' => Organization::class,
'accountable_id' => $organization->id,
]);
$this->actingAs($organization, 'organization');
session(['activeProfileType' => Organization::class, 'activeProfileId' => $organization->id]);
$response = Livewire::test(\App\Http\Livewire\Profile\ExportProfileData::class)
->call('exportTransactions', 'json');
$response->assertStatus(200);
}
/**
* Test organization cannot export another organization's transactions
*
* @test
*/
public function organization_cannot_export_another_organizations_transactions()
{
$user = User::factory()->create();
$org1 = Organization::factory()->create();
$org2 = Organization::factory()->create();
$org1->users()->attach($user->id);
// Logged in as both web user and organization
$this->actingAs($user, 'web');
$this->actingAs($org1, 'organization');
// Malicious: manipulate session to target org2
session(['activeProfileType' => Organization::class, 'activeProfileId' => $org2->id]);
$response = Livewire::test(\App\Http\Livewire\Profile\ExportProfileData::class)
->call('exportTransactions', 'json');
$response->assertStatus(403);
}
/**
* Test user can export own profile data
*
* @test
*/
public function user_can_export_own_profile_data()
{
$user = User::factory()->create();
$this->actingAs($user, 'web');
session(['activeProfileType' => User::class, 'activeProfileId' => $user->id]);
$response = Livewire::test(\App\Http\Livewire\Profile\ExportProfileData::class)
->call('exportProfileData', 'json');
$response->assertStatus(200);
}
/**
* Test user cannot export another user's profile data
*
* @test
*/
public function user_cannot_export_another_users_profile_data()
{
$user1 = User::factory()->create();
$user2 = User::factory()->create();
// Logged in as user1
$this->actingAs($user1, 'web');
// Malicious: manipulate session to target user2
session(['activeProfileType' => User::class, 'activeProfileId' => $user2->id]);
$response = Livewire::test(\App\Http\Livewire\Profile\ExportProfileData::class)
->call('exportProfileData', 'json');
$response->assertStatus(403);
}
/**
* Test user can export own messages
*
* @test
*/
public function user_can_export_own_messages()
{
$user = User::factory()->create();
$recipient = User::factory()->create();
// Create a conversation using sendMessageTo
$user->sendMessageTo($recipient, 'Test message');
$this->actingAs($user, 'web');
session(['activeProfileType' => User::class, 'activeProfileId' => $user->id]);
$response = Livewire::test(\App\Http\Livewire\Profile\ExportProfileData::class)
->call('exportMessages', 'json');
$response->assertStatus(200);
}
/**
* Test user cannot export another user's messages
*
* @test
*/
public function user_cannot_export_another_users_messages()
{
$user1 = User::factory()->create();
$user2 = User::factory()->create();
$recipient = User::factory()->create();
// Create messages for user2
$user2->sendMessageTo($recipient, 'User2 private message');
// Logged in as user1
$this->actingAs($user1, 'web');
// Malicious: manipulate session to target user2
session(['activeProfileType' => User::class, 'activeProfileId' => $user2->id]);
$response = Livewire::test(\App\Http\Livewire\Profile\ExportProfileData::class)
->call('exportMessages', 'json');
$response->assertStatus(403);
}
/**
* Test user can export own tags
*
* @test
*/
public function user_can_export_own_tags()
{
$user = User::factory()->create();
// Create a tag and attach to user
$tag = Tag::factory()->create();
$user->tags()->attach($tag->id);
$this->actingAs($user, 'web');
session(['activeProfileType' => User::class, 'activeProfileId' => $user->id]);
$response = Livewire::test(\App\Http\Livewire\Profile\ExportProfileData::class)
->call('exportTags', 'json');
$response->assertStatus(200);
}
/**
* Test user cannot export another user's tags
*
* @test
*/
public function user_cannot_export_another_users_tags()
{
$user1 = User::factory()->create();
$user2 = User::factory()->create();
// Create tags for user2
$tag = Tag::factory()->create();
$user2->tags()->attach($tag->id);
// Logged in as user1
$this->actingAs($user1, 'web');
// Malicious: manipulate session to target user2
session(['activeProfileType' => User::class, 'activeProfileId' => $user2->id]);
$response = Livewire::test(\App\Http\Livewire\Profile\ExportProfileData::class)
->call('exportTags', 'json');
$response->assertStatus(403);
}
/**
* Test user can export own contacts
*
* @test
*/
public function user_can_export_own_contacts()
{
$user = User::factory()->create();
$contact = User::factory()->create();
// Create a transaction to establish contact
$userAccount = Account::factory()->create([
'accountable_type' => User::class,
'accountable_id' => $user->id,
]);
$contactAccount = Account::factory()->create([
'accountable_type' => User::class,
'accountable_id' => $contact->id,
]);
Transaction::factory()->create([
'from_account_id' => $userAccount->id,
'to_account_id' => $contactAccount->id,
'amount' => 60,
]);
$this->actingAs($user, 'web');
session(['activeProfileType' => User::class, 'activeProfileId' => $user->id]);
$response = Livewire::test(\App\Http\Livewire\Profile\ExportProfileData::class)
->call('exportContacts', 'json');
$response->assertStatus(200);
}
/**
* Test user cannot export another user's contacts
*
* @test
*/
public function user_cannot_export_another_users_contacts()
{
$user1 = User::factory()->create();
$user2 = User::factory()->create();
// Logged in as user1
$this->actingAs($user1, 'web');
// Malicious: manipulate session to target user2
session(['activeProfileType' => User::class, 'activeProfileId' => $user2->id]);
$response = Livewire::test(\App\Http\Livewire\Profile\ExportProfileData::class)
->call('exportContacts', 'json');
$response->assertStatus(403);
}
/**
* Test organization can export own messages
*
* @test
*/
public function organization_can_export_own_messages()
{
$user = User::factory()->create();
$organization = Organization::factory()->create();
$organization->users()->attach($user->id);
$recipient = User::factory()->create();
// Create a conversation from organization
$organization->sendMessageTo($recipient, 'Org message');
$this->actingAs($user, 'web');
$this->actingAs($organization, 'organization');
session(['activeProfileType' => Organization::class, 'activeProfileId' => $organization->id]);
$response = Livewire::test(\App\Http\Livewire\Profile\ExportProfileData::class)
->call('exportMessages', 'json');
$response->assertStatus(200);
}
/**
* Test organization cannot export another organization's messages
*
* @test
*/
public function organization_cannot_export_another_organizations_messages()
{
$user = User::factory()->create();
$org1 = Organization::factory()->create();
$org2 = Organization::factory()->create();
$org1->users()->attach($user->id);
$this->actingAs($user, 'web');
$this->actingAs($org1, 'organization');
// Malicious: manipulate session to target org2
session(['activeProfileType' => Organization::class, 'activeProfileId' => $org2->id]);
$response = Livewire::test(\App\Http\Livewire\Profile\ExportProfileData::class)
->call('exportMessages', 'json');
$response->assertStatus(403);
}
/**
* Test organization can export own tags
*
* @test
*/
public function organization_can_export_own_tags()
{
$user = User::factory()->create();
$organization = Organization::factory()->create();
$organization->users()->attach($user->id);
// Create a tag and attach to organization
$tag = Tag::factory()->create();
$organization->tags()->attach($tag->id);
$this->actingAs($user, 'web');
$this->actingAs($organization, 'organization');
session(['activeProfileType' => Organization::class, 'activeProfileId' => $organization->id]);
$response = Livewire::test(\App\Http\Livewire\Profile\ExportProfileData::class)
->call('exportTags', 'json');
$response->assertStatus(200);
}
/**
* Test organization cannot export another organization's tags
*
* @test
*/
public function organization_cannot_export_another_organizations_tags()
{
$user = User::factory()->create();
$org1 = Organization::factory()->create();
$org2 = Organization::factory()->create();
$org1->users()->attach($user->id);
$this->actingAs($user, 'web');
$this->actingAs($org1, 'organization');
// Malicious: manipulate session to target org2
session(['activeProfileType' => Organization::class, 'activeProfileId' => $org2->id]);
$response = Livewire::test(\App\Http\Livewire\Profile\ExportProfileData::class)
->call('exportTags', 'json');
$response->assertStatus(403);
}
/**
* Test organization can export own contacts
*
* @test
*/
public function organization_can_export_own_contacts()
{
$user = User::factory()->create();
$organization = Organization::factory()->create();
$organization->users()->attach($user->id);
$this->actingAs($user, 'web');
$this->actingAs($organization, 'organization');
session(['activeProfileType' => Organization::class, 'activeProfileId' => $organization->id]);
$response = Livewire::test(\App\Http\Livewire\Profile\ExportProfileData::class)
->call('exportContacts', 'json');
$response->assertStatus(200);
}
/**
* Test organization cannot export another organization's contacts
*
* @test
*/
public function organization_cannot_export_another_organizations_contacts()
{
$user = User::factory()->create();
$org1 = Organization::factory()->create();
$org2 = Organization::factory()->create();
$org1->users()->attach($user->id);
$this->actingAs($user, 'web');
$this->actingAs($org1, 'organization');
// Malicious: manipulate session to target org2
session(['activeProfileType' => Organization::class, 'activeProfileId' => $org2->id]);
$response = Livewire::test(\App\Http\Livewire\Profile\ExportProfileData::class)
->call('exportContacts', 'json');
$response->assertStatus(403);
}
/**
* Test cross-guard attack: web user cannot export bank data
*
* @test
*/
public function web_user_cannot_export_bank_data_cross_guard_attack()
{
$user = User::factory()->create();
$bank = Bank::factory()->create();
$bank->managers()->attach($user->id);
// Logged in as user (web guard)
$this->actingAs($user, 'web');
// Malicious: manipulate session to target bank profile
session(['activeProfileType' => Bank::class, 'activeProfileId' => $bank->id]);
$response = Livewire::test(\App\Http\Livewire\Profile\ExportProfileData::class)
->call('exportProfileData', 'json');
$response->assertStatus(403);
}
/**
* Test bank can export own data when properly authenticated
*
* @test
*/
public function bank_can_export_own_data_when_properly_authenticated()
{
$user = User::factory()->create();
$bank = Bank::factory()->create();
$bank->managers()->attach($user->id);
// Properly logged in as bank
$this->actingAs($bank, 'bank');
session(['activeProfileType' => Bank::class, 'activeProfileId' => $bank->id]);
$response = Livewire::test(\App\Http\Livewire\Profile\ExportProfileData::class)
->call('exportProfileData', 'json');
$response->assertStatus(200);
}
/**
* Test unauthenticated user cannot export any data
*
* @test
*/
public function unauthenticated_user_cannot_export_data()
{
$user = User::factory()->create();
// Not authenticated
session(['activeProfileType' => User::class, 'activeProfileId' => $user->id]);
$response = Livewire::test(\App\Http\Livewire\Profile\ExportProfileData::class)
->call('exportTransactions', 'json');
// Should return 401 (not authenticated) rather than 403 (authenticated but unauthorized)
$response->assertStatus(401);
}
}

View File

@@ -0,0 +1,326 @@
<?php
namespace Tests\Feature\Security\Authorization;
use App\Http\Livewire\Mailings\Manage as MailingsManage;
use App\Http\Livewire\Profiles\Create as ProfilesCreate;
use App\Http\Livewire\Tags\Create as TagsCreate;
use App\Models\Admin;
use App\Models\Bank;
use App\Models\Organization;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Tests\TestCase;
/**
* Livewire Method-Level Authorization Tests
*
* Tests critical method-level authorization to prevent Livewire direct method invocation attacks.
* Focuses on the two newly discovered critical vulnerabilities and authorization patterns.
*
* @group security
* @group authorization
* @group livewire
* @group critical
*/
class LivewireMethodAuthorizationTest extends TestCase
{
use RefreshDatabase;
// ===========================================
// TAGS/CREATE.PHP - AUTHORIZATION TESTS
// ===========================================
/** @test */
public function admin_can_call_tags_create_method()
{
$admin = Admin::factory()->create();
$this->actingAs($admin, 'admin');
session(['activeProfileType' => Admin::class, 'activeProfileId' => $admin->id]);
$component = Livewire::test(TagsCreate::class);
$component->assertStatus(200);
}
/** @test */
public function central_bank_can_call_tags_create_method()
{
$bank = Bank::factory()->create(['level' => 0]);
$this->actingAs($bank, 'bank');
session(['activeProfileType' => Bank::class, 'activeProfileId' => $bank->id]);
$component = Livewire::test(TagsCreate::class);
$component->assertStatus(200);
}
/** @test */
public function regular_bank_cannot_call_tags_create_method()
{
$bank = Bank::factory()->create(['level' => 1]);
$this->actingAs($bank, 'bank');
session(['activeProfileType' => Bank::class, 'activeProfileId' => $bank->id]);
$component = Livewire::test(TagsCreate::class);
$component->assertStatus(403);
}
/** @test */
public function user_cannot_call_tags_create_method()
{
$user = User::factory()->create();
$this->actingAs($user, 'web');
session(['activeProfileType' => User::class, 'activeProfileId' => $user->id]);
$component = Livewire::test(TagsCreate::class);
$component->assertStatus(403);
}
/** @test */
public function organization_cannot_call_tags_create_method()
{
$org = Organization::factory()->create();
$this->actingAs($org, 'organization');
session(['activeProfileType' => Organization::class, 'activeProfileId' => $org->id]);
$component = Livewire::test(TagsCreate::class);
$component->assertStatus(403);
}
// ===========================================
// PROFILES/CREATE.PHP - CRITICAL VULNERABILITY FIX
// This was a CRITICAL vulnerability - unauthorized profile creation
// ===========================================
/** @test */
public function admin_can_access_profiles_create_component()
{
$admin = Admin::factory()->create();
$this->actingAs($admin, 'admin');
session(['activeProfileType' => Admin::class, 'activeProfileId' => $admin->id]);
$component = Livewire::test(ProfilesCreate::class);
$component->assertStatus(200);
}
/** @test */
public function central_bank_can_access_profiles_create_component()
{
$bank = Bank::factory()->create(['level' => 0]);
$this->actingAs($bank, 'bank');
session(['activeProfileType' => Bank::class, 'activeProfileId' => $bank->id]);
$component = Livewire::test(ProfilesCreate::class);
$component->assertStatus(200);
}
/** @test */
public function user_cannot_access_profiles_create_component()
{
$user = User::factory()->create();
$this->actingAs($user, 'web');
session(['activeProfileType' => User::class, 'activeProfileId' => $user->id]);
$component = Livewire::test(ProfilesCreate::class);
$component->assertStatus(403);
}
/** @test */
public function organization_cannot_access_profiles_create_component()
{
$org = Organization::factory()->create();
$this->actingAs($org, 'organization');
session(['activeProfileType' => Organization::class, 'activeProfileId' => $org->id]);
$component = Livewire::test(ProfilesCreate::class);
$component->assertStatus(403);
}
// ===========================================
// MAILINGS/MANAGE.PHP - CRITICAL VULNERABILITY FIX
// bulkDeleteMailings() method was unprotected
// ===========================================
/** @test */
public function admin_can_access_mailings_manage_component()
{
$admin = Admin::factory()->create();
$this->actingAs($admin, 'admin');
session(['activeProfileType' => Admin::class, 'activeProfileId' => $admin->id]);
$component = Livewire::test(MailingsManage::class);
$component->assertStatus(200);
}
/** @test */
public function central_bank_can_access_mailings_manage_component()
{
$bank = Bank::factory()->create(['level' => 0]);
$this->actingAs($bank, 'bank');
session(['activeProfileType' => Bank::class, 'activeProfileId' => $bank->id]);
$component = Livewire::test(MailingsManage::class);
$component->assertStatus(200);
}
/** @test */
public function user_cannot_access_mailings_manage_component()
{
$user = User::factory()->create();
$this->actingAs($user, 'web');
session(['activeProfileType' => User::class, 'activeProfileId' => $user->id]);
$component = Livewire::test(MailingsManage::class);
$component->assertStatus(403);
}
/** @test */
public function organization_cannot_access_mailings_manage_component()
{
$org = Organization::factory()->create();
$this->actingAs($org, 'organization');
session(['activeProfileType' => Organization::class, 'activeProfileId' => $org->id]);
$component = Livewire::test(MailingsManage::class);
$component->assertStatus(403);
}
// ===========================================
// CROSS-GUARD ATTACK PREVENTION TESTS
// ===========================================
/** @test */
public function user_authenticated_on_wrong_guard_cannot_access_admin_components()
{
$user = User::factory()->create();
$this->actingAs($user, 'web');
// Try to fake being an admin by setting wrong session
$admin = Admin::factory()->create();
session(['activeProfileType' => Admin::class, 'activeProfileId' => $admin->id]);
// Should be blocked because guard doesn't match
$component = Livewire::test(TagsCreate::class);
$component->assertStatus(403);
}
/** @test */
public function admin_cannot_access_other_admins_session()
{
$admin1 = Admin::factory()->create();
$admin2 = Admin::factory()->create();
$this->actingAs($admin1, 'admin');
// Try to fake session to access admin2's context
session(['activeProfileType' => Admin::class, 'activeProfileId' => $admin2->id]);
// Should be blocked by ProfileAuthorizationHelper IDOR prevention
$component = Livewire::test(TagsCreate::class);
$component->assertStatus(403);
}
/** @test */
public function unauthenticated_user_cannot_access_admin_components()
{
// No authentication at all - expect redirect or 403
$component = Livewire::test(TagsCreate::class);
$component->assertStatus(403);
}
/** @test */
public function user_with_no_session_cannot_access_admin_components()
{
$user = User::factory()->create();
$this->actingAs($user, 'web');
// Don't set session variables - this simulates a corrupted or missing session
// Set minimal session to prevent "No active profile" error
// but make it point to a non-existent or wrong profile
session([
'activeProfileType' => User::class,
'activeProfileId' => $user->id,
'active_guard' => 'web',
]);
// User (web guard) should not be able to access admin components
$component = Livewire::test(TagsCreate::class);
$component->assertStatus(403);
}
// ===========================================
// AUTHORIZATION CACHING TESTS
// ===========================================
/** @test */
public function authorization_is_cached_within_same_request()
{
$admin = Admin::factory()->create();
$this->actingAs($admin, 'admin');
session(['activeProfileType' => Admin::class, 'activeProfileId' => $admin->id]);
$component = Livewire::test(TagsCreate::class);
// First check happens in mount()
$component->assertStatus(200);
// Component can be used multiple times - caching works
$component->assertStatus(200);
}
// ===========================================
// BANK LEVEL VALIDATION TESTS
// ===========================================
/** @test */
public function only_central_bank_level_zero_can_access_admin_functions()
{
// Test level 0 (central bank) - should work
$centralBank = Bank::factory()->create(['level' => 0]);
$this->actingAs($centralBank, 'bank');
session(['activeProfileType' => Bank::class, 'activeProfileId' => $centralBank->id]);
$component = Livewire::test(TagsCreate::class);
$component->assertStatus(200);
}
/** @test */
public function bank_level_one_cannot_access_admin_functions()
{
$bank = Bank::factory()->create(['level' => 1]);
$this->actingAs($bank, 'bank');
session(['activeProfileType' => Bank::class, 'activeProfileId' => $bank->id]);
$component = Livewire::test(TagsCreate::class);
$component->assertStatus(403);
}
/** @test */
public function bank_level_two_cannot_access_admin_functions()
{
$bank = Bank::factory()->create(['level' => 2]);
$this->actingAs($bank, 'bank');
session(['activeProfileType' => Bank::class, 'activeProfileId' => $bank->id]);
$component = Livewire::test(TagsCreate::class);
$component->assertStatus(403);
}
}

View File

@@ -0,0 +1,284 @@
<?php
namespace Tests\Feature\Security\Authorization;
use App\Models\Admin;
use App\Models\Bank;
use App\Models\Organization;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Tests\TestCase;
/**
* Message Settings Authorization Tests
*
* Tests that users can only modify their own profile message settings
* and cannot manipulate session to modify other profiles' settings.
*
* @group security
* @group authorization
* @group idor
* @group critical
*/
class MessageSettingsAuthorizationTest extends TestCase
{
use RefreshDatabase;
/**
* Test user can access their own message settings
*
* @test
*/
public function user_can_access_own_message_settings()
{
$user = User::factory()->create();
$this->actingAs($user, 'web');
session(['activeProfileType' => User::class]);
session(['activeProfileId' => $user->id]);
$response = Livewire::test(\App\Http\Livewire\Profile\UpdateMessageSettingsForm::class);
$response->assertStatus(200);
$response->assertSet('systemMessage', true); // Default value
}
/**
* Test user cannot access another user's message settings via session manipulation
*
* @test
*/
public function user_cannot_access_another_users_message_settings()
{
$user1 = User::factory()->create();
$user2 = User::factory()->create();
$this->actingAs($user1, 'web');
// Malicious attempt: manipulate session to access user2's settings
session(['activeProfileType' => User::class]);
session(['activeProfileId' => $user2->id]); // Different user!
$response = Livewire::test(\App\Http\Livewire\Profile\UpdateMessageSettingsForm::class);
$response->assertStatus(403);
}
/**
* Test user cannot update another user's message settings
*
* @test
*/
public function user_cannot_update_another_users_message_settings()
{
$user1 = User::factory()->create();
$user2 = User::factory()->create();
$this->actingAs($user1, 'web');
// Malicious attempt: manipulate session to update user2's settings
session(['activeProfileType' => User::class]);
session(['activeProfileId' => $user2->id]);
$response = Livewire::test(\App\Http\Livewire\Profile\UpdateMessageSettingsForm::class)
->set('systemMessage', false)
->call('updateMessageSettings');
$response->assertStatus(403);
}
/**
* Test organization can access their own message settings
*
* @test
*/
public function organization_can_access_own_message_settings()
{
$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 = Livewire::test(\App\Http\Livewire\Profile\UpdateMessageSettingsForm::class);
$response->assertStatus(200);
}
/**
* Test organization cannot access another organization's message settings
*
* @test
*/
public function organization_cannot_access_another_organizations_message_settings()
{
$user = User::factory()->create();
$org1 = Organization::factory()->create();
$org2 = Organization::factory()->create();
$org1->users()->attach($user->id);
// User is NOT linked to org2
$this->actingAs($org1, 'organization');
// Malicious attempt: manipulate session to access org2's settings
session(['activeProfileType' => Organization::class]);
session(['activeProfileId' => $org2->id]);
$response = Livewire::test(\App\Http\Livewire\Profile\UpdateMessageSettingsForm::class);
$response->assertStatus(403);
}
/**
* Test admin can access their own message settings
*
* @test
*/
public function admin_can_access_own_message_settings()
{
$user = User::factory()->create();
$admin = Admin::factory()->create();
$admin->users()->attach($user->id);
$this->actingAs($admin, 'admin');
session(['activeProfileType' => Admin::class]);
session(['activeProfileId' => $admin->id]);
$response = Livewire::test(\App\Http\Livewire\Profile\UpdateMessageSettingsForm::class);
$response->assertStatus(200);
}
/**
* Test bank can access their own message settings
*
* @test
*/
public function bank_can_access_own_message_settings()
{
$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 = Livewire::test(\App\Http\Livewire\Profile\UpdateMessageSettingsForm::class);
$response->assertStatus(200);
}
/**
* Test user can update their own message settings
*
* @test
*/
public function user_can_update_own_message_settings()
{
$user = User::factory()->create();
$this->actingAs($user, 'web');
session(['activeProfileType' => User::class]);
session(['activeProfileId' => $user->id]);
$response = Livewire::test(\App\Http\Livewire\Profile\UpdateMessageSettingsForm::class)
->set('systemMessage', false)
->set('paymentReceived', true)
->set('chatUnreadDelay', 24)
->call('updateMessageSettings');
$response->assertStatus(200);
$response->assertDispatched('saved');
// Verify settings were saved
$this->assertDatabaseHas('message_settings', [
'messageable_type' => User::class,
'messageable_id' => $user->id,
'system_message' => false,
'payment_received' => true,
'chat_unread_delay' => 24,
]);
}
/**
* Test cross-profile access: user logged in as user trying to access org settings
*
* @test
*/
public function user_cannot_access_organization_message_settings_via_session()
{
$user = User::factory()->create();
$organization = Organization::factory()->create();
$user->organizations()->attach($organization->id);
// Login as regular user (web guard)
$this->actingAs($user, 'web');
// Try to access organization settings via session manipulation
session(['activeProfileType' => Organization::class]);
session(['activeProfileId' => $organization->id]);
$response = Livewire::test(\App\Http\Livewire\Profile\UpdateMessageSettingsForm::class);
// Should fail because authenticated as User but trying to access Organization profile
$response->assertStatus(403);
}
/**
* Test unauthenticated user cannot access message settings
*
* @test
*/
public function unauthenticated_user_cannot_access_message_settings()
{
$user = User::factory()->create();
session(['activeProfileType' => User::class]);
session(['activeProfileId' => $user->id]);
$response = Livewire::test(\App\Http\Livewire\Profile\UpdateMessageSettingsForm::class);
$response->assertStatus(401);
}
/**
* Test message settings default values are created on first access
*
* @test
*/
public function message_settings_default_values_created_on_first_access()
{
$user = User::factory()->create();
$this->actingAs($user, 'web');
session(['activeProfileType' => User::class]);
session(['activeProfileId' => $user->id]);
// User has no message settings yet
$this->assertDatabaseMissing('message_settings', [
'messageable_type' => User::class,
'messageable_id' => $user->id,
]);
$response = Livewire::test(\App\Http\Livewire\Profile\UpdateMessageSettingsForm::class);
$response->assertStatus(200);
// Default settings should be created
$this->assertDatabaseHas('message_settings', [
'messageable_type' => User::class,
'messageable_id' => $user->id,
'system_message' => true,
'payment_received' => true,
'star_received' => true,
]);
}
}

View File

@@ -0,0 +1,570 @@
<?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\TransactionType;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Tests\TestCase;
/**
* Payment Multi-Auth Tests (Livewire Pay Component)
*
* Tests that the Livewire Pay component correctly validates account ownership
* and prevents unauthorized payments across all authentication guards.
*
* @group security
* @group authorization
* @group multi-guard
* @group payment
* @group critical
*/
class PaymentMultiAuthTest 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'],
['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');
}
}

View File

@@ -0,0 +1,235 @@
<?php
namespace Tests\Feature\Security\Authorization;
use App\Models\Admin;
use App\Models\Bank;
use App\Models\Organization;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Tests\TestCase;
/**
* Posts Management Authorization Tests
*
* Tests that only admins and central banks can access post management,
* and prevents IDOR/cross-guard attacks.
*
* @group security
* @group authorization
* @group admin
* @group critical
*/
class PostsManageAuthorizationTest extends TestCase
{
use RefreshDatabase;
/**
* Test admin can access posts management
*
* @test
*/
public function admin_can_access_posts_management()
{
$admin = Admin::factory()->create();
$this->actingAs($admin, 'admin');
session(['activeProfileType' => Admin::class, 'activeProfileId' => $admin->id]);
$response = Livewire::test(\App\Http\Livewire\Posts\Manage::class);
$response->assertStatus(200);
}
/**
* Test central bank (level 0) can access posts management
*
* @test
*/
public function central_bank_can_access_posts_management()
{
$bank = Bank::factory()->create(['level' => 0]);
$this->actingAs($bank, 'bank');
session(['activeProfileType' => Bank::class, 'activeProfileId' => $bank->id]);
$response = Livewire::test(\App\Http\Livewire\Posts\Manage::class);
$response->assertStatus(200);
}
/**
* Test regular bank (level 1) CANNOT access posts management
*
* @test
*/
public function regular_bank_cannot_access_posts_management()
{
$bank = Bank::factory()->create(['level' => 1]);
$this->actingAs($bank, 'bank');
session(['activeProfileType' => Bank::class, 'activeProfileId' => $bank->id]);
$response = Livewire::test(\App\Http\Livewire\Posts\Manage::class);
$response->assertStatus(403);
}
/**
* Test user CANNOT access posts management
*
* @test
*/
public function user_cannot_access_posts_management()
{
$user = User::factory()->create();
$this->actingAs($user, 'web');
session(['activeProfileType' => User::class, 'activeProfileId' => $user->id]);
$response = Livewire::test(\App\Http\Livewire\Posts\Manage::class);
$response->assertStatus(403);
}
/**
* Test organization CANNOT access posts management
*
* @test
*/
public function organization_cannot_access_posts_management()
{
$user = User::factory()->create();
$organization = Organization::factory()->create();
$organization->users()->attach($user->id);
$this->actingAs($user, 'web');
$this->actingAs($organization, 'organization');
session(['activeProfileType' => Organization::class, 'activeProfileId' => $organization->id]);
$response = Livewire::test(\App\Http\Livewire\Posts\Manage::class);
$response->assertStatus(403);
}
/**
* Test web user CANNOT access posts via cross-guard attack (targeting admin profile)
*
* @test
*/
public function web_user_cannot_access_posts_via_cross_guard_admin_attack()
{
$user = User::factory()->create();
$admin = Admin::factory()->create();
$admin->users()->attach($user->id); // User is linked to admin
// User authenticated on 'web' guard
$this->actingAs($user, 'web');
// Malicious: manipulate session to target admin profile
session(['activeProfileType' => Admin::class, 'activeProfileId' => $admin->id]);
$response = Livewire::test(\App\Http\Livewire\Posts\Manage::class);
// Should be blocked by ProfileAuthorizationHelper (cross-guard validation)
$response->assertStatus(403);
}
/**
* Test web user CANNOT access posts via cross-guard attack (targeting bank profile)
*
* @test
*/
public function web_user_cannot_access_posts_via_cross_guard_bank_attack()
{
$user = User::factory()->create();
$bank = Bank::factory()->create(['level' => 0]);
$bank->managers()->attach($user->id); // User is manager of bank
// User authenticated on 'web' guard
$this->actingAs($user, 'web');
// Malicious: manipulate session to target bank profile
session(['activeProfileType' => Bank::class, 'activeProfileId' => $bank->id]);
$response = Livewire::test(\App\Http\Livewire\Posts\Manage::class);
// Should be blocked by ProfileAuthorizationHelper (cross-guard validation)
$response->assertStatus(403);
}
/**
* Test unauthenticated user CANNOT access posts management
*
* @test
*/
public function unauthenticated_user_cannot_access_posts_management()
{
$admin = Admin::factory()->create();
// Not authenticated
session(['activeProfileType' => Admin::class, 'activeProfileId' => $admin->id]);
$response = Livewire::test(\App\Http\Livewire\Posts\Manage::class);
// Should return 401 (not authenticated)
$response->assertStatus(401);
}
/**
* Test admin CANNOT access posts when session has no active profile
*
* @test
*/
public function admin_cannot_access_posts_without_active_profile()
{
$admin = Admin::factory()->create();
$this->actingAs($admin, 'admin');
// NO session activeProfileType/activeProfileId set
$response = Livewire::test(\App\Http\Livewire\Posts\Manage::class);
$response->assertStatus(403);
}
/**
* Test admin CANNOT access posts when session has invalid profile ID
*
* @test
*/
public function admin_cannot_access_posts_with_invalid_profile_id()
{
$admin = Admin::factory()->create();
$this->actingAs($admin, 'admin');
session(['activeProfileType' => Admin::class, 'activeProfileId' => 99999]); // Non-existent ID
$response = Livewire::test(\App\Http\Livewire\Posts\Manage::class);
$response->assertStatus(403);
}
/**
* Test admin CANNOT access posts management for different admin profile (IDOR)
*
* @test
*/
public function admin_cannot_access_posts_as_different_admin()
{
$admin1 = Admin::factory()->create();
$admin2 = Admin::factory()->create();
// Authenticated as admin1
$this->actingAs($admin1, 'admin');
// Malicious: manipulate session to target admin2 profile
session(['activeProfileType' => Admin::class, 'activeProfileId' => $admin2->id]);
$response = Livewire::test(\App\Http\Livewire\Posts\Manage::class);
// Should be blocked by ProfileAuthorizationHelper (different admin)
$response->assertStatus(403);
}
}

View File

@@ -0,0 +1,367 @@
<?php
namespace Tests\Feature\Security\Authorization;
use App\Helpers\ProfileAuthorizationHelper;
use App\Models\Admin;
use App\Models\Bank;
use App\Models\Organization;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
/**
* ProfileAuthorizationHelper Multi-Guard Tests
*
* Tests that ProfileAuthorizationHelper correctly validates profile access
* across all authentication guards (web, admin, organization, bank).
*
* @group security
* @group authorization
* @group multi-guard
* @group critical
*/
class ProfileAuthorizationHelperTest extends TestCase
{
use RefreshDatabase;
/**
* Test user can access their own user profile
*
* @test
*/
public function user_can_access_own_user_profile()
{
$user = User::factory()->create();
$this->actingAs($user, 'web');
$result = ProfileAuthorizationHelper::can($user);
$this->assertTrue($result);
}
/**
* Test user cannot access another user's profile
*
* @test
*/
public function user_cannot_access_another_users_profile()
{
$user1 = User::factory()->create();
$user2 = User::factory()->create();
$this->actingAs($user1, 'web');
$result = ProfileAuthorizationHelper::can($user2);
$this->assertFalse($result);
}
/**
* Test admin can access their own admin profile
*
* @test
*/
public function admin_can_access_own_admin_profile()
{
// Create admin and link to user
$user = User::factory()->create();
$admin = Admin::factory()->create();
$admin->users()->attach($user->id);
$this->actingAs($admin, 'admin');
$result = ProfileAuthorizationHelper::can($admin);
$this->assertTrue($result, 'Admin should be able to access their own admin profile');
}
/**
* Test organization can access their own organization profile
*
* @test
*/
public function organization_can_access_own_organization_profile()
{
// Create organization and link to user
$user = User::factory()->create();
$organization = Organization::factory()->create();
$organization->users()->attach($user->id);
$this->actingAs($organization, 'organization');
$result = ProfileAuthorizationHelper::can($organization);
$this->assertTrue($result, 'Organization should be able to access their own profile');
}
/**
* Test bank can access their own bank profile
*
* @test
*/
public function bank_can_access_own_bank_profile()
{
// Create bank and link to user
$user = User::factory()->create();
$bank = Bank::factory()->create();
$bank->managers()->attach($user->id);
$this->actingAs($bank, 'bank');
$result = ProfileAuthorizationHelper::can($bank);
$this->assertTrue($result, 'Bank should be able to access their own bank profile');
}
/**
* Test user can access organization they are member of (for profile switching)
*
* @test
*/
public function user_can_access_organization_they_are_member_of()
{
$user = User::factory()->create();
$organization = Organization::factory()->create();
$user->organizations()->attach($organization->id);
$this->actingAs($user, 'web');
// Use userOwnsProfile() for cross-guard ownership checks (profile switching scenario)
$result = ProfileAuthorizationHelper::userOwnsProfile($organization);
$this->assertTrue($result);
}
/**
* Test user cannot access organization they are not member of
*
* @test
*/
public function user_cannot_access_organization_they_are_not_member_of()
{
$user = User::factory()->create();
$organization = Organization::factory()->create();
// User is NOT attached to organization
$this->actingAs($user, 'web');
$result = ProfileAuthorizationHelper::can($organization);
$this->assertFalse($result);
}
/**
* Test user can access bank they manage (for profile switching)
*
* @test
*/
public function user_can_access_bank_they_manage()
{
$user = User::factory()->create();
$bank = Bank::factory()->create();
$user->banksManaged()->attach($bank->id);
$this->actingAs($user, 'web');
// Use userOwnsProfile() for cross-guard ownership checks (profile switching scenario)
$result = ProfileAuthorizationHelper::userOwnsProfile($bank);
$this->assertTrue($result);
}
/**
* Test user cannot access bank they don't manage
*
* @test
*/
public function user_cannot_access_bank_they_dont_manage()
{
$user = User::factory()->create();
$bank = Bank::factory()->create();
// User is NOT attached to bank
$this->actingAs($user, 'web');
$result = ProfileAuthorizationHelper::can($bank);
$this->assertFalse($result);
}
/**
* Test user can access admin profile they are linked to (for profile switching)
*
* @test
*/
public function user_can_access_admin_profile_they_are_linked_to()
{
$user = User::factory()->create();
$admin = Admin::factory()->create();
$user->admins()->attach($admin->id);
$this->actingAs($user, 'web');
// Use userOwnsProfile() for cross-guard ownership checks (profile switching scenario)
$result = ProfileAuthorizationHelper::userOwnsProfile($admin);
$this->assertTrue($result);
}
/**
* Test user cannot access admin profile they are not linked to
*
* @test
*/
public function user_cannot_access_admin_profile_they_are_not_linked_to()
{
$user = User::factory()->create();
$admin = Admin::factory()->create();
// User is NOT attached to admin
$this->actingAs($user, 'web');
$result = ProfileAuthorizationHelper::can($admin);
$this->assertFalse($result);
}
/**
* Test admin cannot directly switch to organization (must go through web user)
*
* In the application, profile switching flow is: User Admin back to User Organization
* Direct Admin Organization switching is not supported.
*
* @test
*/
public function admin_can_access_organization_via_linked_user()
{
// Create user linked to both admin and organization
$user = User::factory()->create();
$admin = Admin::factory()->create();
$organization = Organization::factory()->create();
$admin->users()->attach($user->id);
$user->organizations()->attach($organization->id);
$this->actingAs($admin, 'admin');
// userOwnsProfile() only checks web guard, so this should return false
// Profile switching requires being on web guard first
$result = ProfileAuthorizationHelper::userOwnsProfile($organization);
$this->assertFalse($result, 'Admin cannot directly switch to organization without going through web user first');
}
/**
* Test organization cannot directly switch to bank (must go through web user)
*
* In the application, profile switching flow is: User Organization back to User Bank
* Direct Organization Bank switching is not supported.
*
* @test
*/
public function organization_can_access_bank_via_linked_user()
{
// Create user linked to both organization and bank
$user = User::factory()->create();
$organization = Organization::factory()->create();
$bank = Bank::factory()->create();
$organization->users()->attach($user->id);
$user->banksManaged()->attach($bank->id);
$this->actingAs($organization, 'organization');
// userOwnsProfile() only checks web guard, so this should return false
// Profile switching requires being on web guard first
$result = ProfileAuthorizationHelper::userOwnsProfile($bank);
$this->assertFalse($result, 'Organization cannot directly switch to bank without going through web user first');
}
/**
* Test admin cannot access unrelated organization
*
* @test
*/
public function admin_cannot_access_unrelated_organization()
{
$user = User::factory()->create();
$admin = Admin::factory()->create();
$organization = Organization::factory()->create();
$admin->users()->attach($user->id);
// User is NOT linked to organization
$this->actingAs($admin, 'admin');
$result = ProfileAuthorizationHelper::can($organization);
$this->assertFalse($result, 'Admin should NOT access unrelated organization');
}
/**
* Test authorize method throws 403 for unauthorized access
*
* @test
*/
public function authorize_method_throws_403_for_unauthorized_access()
{
$this->expectException(\Symfony\Component\HttpKernel\Exception\HttpException::class);
$this->expectExceptionMessage('Unauthorized');
$user1 = User::factory()->create();
$user2 = User::factory()->create();
$this->actingAs($user1, 'web');
ProfileAuthorizationHelper::authorize($user2);
}
/**
* Test unauthenticated access is denied
*
* @test
*/
public function unauthenticated_access_is_denied()
{
$user = User::factory()->create();
$result = ProfileAuthorizationHelper::can($user);
$this->assertFalse($result);
}
/**
* Test authorize method throws 401 for unauthenticated access
*
* @test
*/
public function authorize_method_throws_401_for_unauthenticated()
{
$this->expectException(\Symfony\Component\HttpKernel\Exception\HttpException::class);
$this->expectExceptionMessage('Authentication required');
$user = User::factory()->create();
ProfileAuthorizationHelper::authorize($user);
}
/**
* Test admin cannot access another admin profile
*
* @test
*/
public function admin_cannot_access_another_admin_profile()
{
$user1 = User::factory()->create();
$admin1 = Admin::factory()->create();
$admin2 = Admin::factory()->create();
$admin1->users()->attach($user1->id);
$this->actingAs($admin1, 'admin');
$result = ProfileAuthorizationHelper::can($admin2);
$this->assertFalse($result, 'Admin should NOT access another admin profile');
}
}

View File

@@ -0,0 +1,241 @@
<?php
namespace Tests\Feature\Security\Authorization;
use App\Models\User;
use App\Models\Organization;
use App\Models\Bank;
use App\Models\Admin;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Tests\TestCase;
/**
* Profile Deletion Authorization Tests
*
* Tests that users can only delete their own profiles and cannot delete
* profiles they don't own or have access to.
*
* @group security
* @group authorization
* @group critical
*/
class ProfileDeletionAuthorizationTest extends TestCase
{
use RefreshDatabase;
/**
* Test user cannot delete another user's profile
*
* @test
*/
public function user_cannot_delete_another_users_profile()
{
// Arrange: Create two users
$user1 = User::factory()->create(['password' => bcrypt('password123')]);
$user2 = User::factory()->create();
// Act: Login as user1 and try to delete user2's profile
$this->actingAs($user1);
// Create session as if user1 is trying to delete user2
session(['activeProfileType' => 'App\\Models\\User']);
session(['activeProfileId' => $user2->id]); // Malicious attempt
session(['active_guard' => 'web']);
// Attempt deletion
$response = Livewire::test(\App\Http\Livewire\Profile\DeleteUserForm::class)
->set('password', 'password123')
->call('deleteUser', request(), app(\Laravel\Jetstream\Contracts\DeletesUsers::class), auth()->guard());
// Assert: The operation should fail or delete user1 (not user2)
$this->assertDatabaseHas('users', [
'id' => $user2->id,
'deleted_at' => null,
]);
}
/**
* Test user cannot delete organization they don't have access to
*
* @test
*/
public function user_cannot_delete_organization_they_dont_own()
{
// Arrange: Create user and two organizations
$user = User::factory()->create(['password' => bcrypt('password123')]);
$org1 = Organization::factory()->create(['password' => bcrypt('orgpass123')]);
$org2 = Organization::factory()->create();
// Link user to org1 only
$user->organizations()->attach($org1->id);
// Act: Login as user and try to delete org2
$this->actingAs($user);
session(['activeProfileType' => 'App\\Models\\Organization']);
session(['activeProfileId' => $org2->id]); // Malicious attempt
session(['active_guard' => 'organization']);
// Attempt deletion
$response = Livewire::test(\App\Http\Livewire\Profile\DeleteUserForm::class)
->set('password', 'password123')
->call('deleteUser', request(), app(\Laravel\Jetstream\Contracts\DeletesUsers::class), auth()->guard('organization'));
// Assert: org2 should NOT be deleted
$this->assertDatabaseHas('organizations', [
'id' => $org2->id,
'deleted_at' => null,
]);
}
/**
* Test central bank (level 0) cannot be deleted
*
* @test
*/
public function central_bank_cannot_be_deleted()
{
// Arrange: Create central bank
$centralBank = Bank::factory()->create([
'level' => 0,
'password' => bcrypt('bankpass123')
]);
$user = User::factory()->create();
$user->banks()->attach($centralBank->id);
// Act: Login and try to delete central bank
$this->actingAs($user);
auth()->guard('bank')->login($centralBank);
session(['activeProfileType' => 'App\\Models\\Bank']);
session(['activeProfileId' => $centralBank->id]);
session(['active_guard' => 'bank']);
// Attempt deletion
$response = Livewire::test(\App\Http\Livewire\Profile\DeleteUserForm::class)
->set('password', 'bankpass123')
->call('deleteUser', request(), app(\Laravel\Jetstream\Contracts\DeletesUsers::class), auth()->guard('bank'));
// Assert: Deletion should fail with validation error
$response->assertHasErrors(['password']);
// Assert: Central bank still exists
$this->assertDatabaseHas('banks', [
'id' => $centralBank->id,
'level' => 0,
'deleted_at' => null,
]);
}
/**
* Test final admin cannot be deleted
*
* @test
*/
public function final_admin_cannot_be_deleted()
{
// Arrange: Create single admin
$admin = Admin::factory()->create(['password' => bcrypt('adminpass123')]);
$user = User::factory()->create();
$user->admins()->attach($admin->id);
// Act: Login and try to delete the only admin
$this->actingAs($user);
auth()->guard('admin')->login($admin);
session(['activeProfileType' => 'App\\Models\\Admin']);
session(['activeProfileId' => $admin->id]);
session(['active_guard' => 'admin']);
// Attempt deletion
$response = Livewire::test(\App\Http\Livewire\Profile\DeleteUserForm::class)
->set('password', 'adminpass123')
->call('deleteUser', request(), app(\Laravel\Jetstream\Contracts\DeletesUsers::class), auth()->guard('admin'));
// Assert: Deletion should fail with validation error
$response->assertHasErrors(['password']);
// Assert: Admin still exists
$this->assertDatabaseHas('admins', [
'id' => $admin->id,
'deleted_at' => null,
]);
}
/**
* Test user can delete their own profile with correct password
*
* @test
*/
public function user_can_delete_own_profile_with_correct_password()
{
// Arrange: Create user
$user = User::factory()->create(['password' => bcrypt('password123')]);
// Act: Login and delete own profile
$this->actingAs($user);
session(['activeProfileType' => 'App\\Models\\User']);
session(['activeProfileId' => $user->id]);
session(['active_guard' => 'web']);
// Attempt deletion with correct password
$response = Livewire::test(\App\Http\Livewire\Profile\DeleteUserForm::class)
->set('password', 'password123')
->call('deleteUser', request(), app(\Laravel\Jetstream\Contracts\DeletesUsers::class), auth()->guard());
// Assert: User is soft-deleted
$this->assertSoftDeleted('users', [
'id' => $user->id,
]);
}
/**
* Test user cannot delete profile with wrong password
*
* @test
*/
public function user_cannot_delete_profile_with_wrong_password()
{
// Arrange: Create user
$user = User::factory()->create(['password' => bcrypt('password123')]);
// Act: Login and try to delete with wrong password
$this->actingAs($user);
session(['activeProfileType' => 'App\\Models\\User']);
session(['activeProfileId' => $user->id]);
session(['active_guard' => 'web']);
// Attempt deletion with wrong password
$response = Livewire::test(\App\Http\Livewire\Profile\DeleteUserForm::class)
->set('password', 'wrongpassword')
->call('deleteUser', request(), app(\Laravel\Jetstream\Contracts\DeletesUsers::class), auth()->guard());
// Assert: Deletion should fail with validation error
$response->assertHasErrors(['password']);
// Assert: User still exists
$this->assertDatabaseHas('users', [
'id' => $user->id,
'deleted_at' => null,
]);
}
/**
* Test unauthenticated user cannot access delete form
*
* @test
*/
public function unauthenticated_user_cannot_access_delete_form()
{
// Act: Try to render delete form without authentication
$response = Livewire::test(\App\Http\Livewire\Profile\DeleteUserForm::class);
// Assert: Should fail (Laravel's auth middleware should prevent this)
// This test verifies the component requires authentication
$this->assertGuest();
}
}

View File

@@ -0,0 +1,550 @@
<?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');
}
}

View File

@@ -0,0 +1,421 @@
<?php
namespace Tests\Feature\Security\Authorization;
use App\Models\Admin;
use App\Models\Bank;
use App\Models\Organization;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Namu\WireChat\Models\Conversation;
use Tests\TestCase;
/**
* WireChat Multi-Auth Tests
*
* Tests that WireChat components work correctly with multi-guard authentication
* for User, Organization, Bank, and Admin profiles.
*
* @group security
* @group authorization
* @group multi-guard
* @group wirechat
*/
class WireChatMultiAuthTest extends TestCase
{
use RefreshDatabase;
/**
* Test user can access conversation they belong to
*
* @test
*/
public function user_can_access_conversation_they_belong_to()
{
$user = User::factory()->create();
$recipient = User::factory()->create();
$this->actingAs($user, 'web');
// Create conversation using sendMessageTo (same logic as Pay.php line 417)
$message = $user->sendMessageTo($recipient, 'Test message');
$conversation = $message->conversation;
$response = Livewire::test(
\App\Http\Livewire\WireChat\DisappearingMessagesSettings::class,
['conversationId' => $conversation->id]
);
$response->assertStatus(200);
}
/**
* Test user cannot access conversation they don't belong to
*
* @test
*/
public function user_cannot_access_conversation_they_dont_belong_to()
{
$user = User::factory()->create();
$otherUser = User::factory()->create();
$anotherUser = User::factory()->create();
$this->actingAs($user, 'web');
// Set active profile in session (required by getActiveProfile())
session([
'activeProfileType' => get_class($user),
'activeProfileId' => $user->id,
'active_guard' => 'web',
]);
// Create conversation between otherUser and anotherUser (not involving $user)
$message = $otherUser->sendMessageTo($anotherUser, 'Test message');
$conversation = $message->conversation;
$response = Livewire::test(
\App\Http\Livewire\WireChat\DisappearingMessagesSettings::class,
['conversationId' => $conversation->id]
);
$response->assertStatus(403);
}
/**
* Test organization can access conversation they belong to
*
* @test
*/
public function organization_can_access_conversation_they_belong_to()
{
$user = User::factory()->create();
$organization = Organization::factory()->create();
$organization->users()->attach($user->id);
$recipient = User::factory()->create();
$this->actingAs($organization, 'organization');
// Create conversation with organization as sender
$message = $organization->sendMessageTo($recipient, 'Test message');
$conversation = $message->conversation;
$response = Livewire::test(
\App\Http\Livewire\WireChat\DisappearingMessagesSettings::class,
['conversationId' => $conversation->id]
);
$response->assertStatus(200);
}
/**
* Test admin can access conversation they belong to
*
* @test
*/
public function admin_can_access_conversation_they_belong_to()
{
$user = User::factory()->create();
$admin = Admin::factory()->create();
$admin->users()->attach($user->id);
$recipient = User::factory()->create();
$this->actingAs($admin, 'admin');
// Create conversation with admin as sender
$message = $admin->sendMessageTo($recipient, 'Test message');
$conversation = $message->conversation;
$response = Livewire::test(
\App\Http\Livewire\WireChat\DisappearingMessagesSettings::class,
['conversationId' => $conversation->id]
);
$response->assertStatus(200);
}
/**
* Test bank can access conversation they belong to
*
* @test
*/
public function bank_can_access_conversation_they_belong_to()
{
$user = User::factory()->create();
$bank = Bank::factory()->create();
$bank->managers()->attach($user->id);
$recipient = User::factory()->create();
$this->actingAs($bank, 'bank');
// Create conversation with bank as sender
$message = $bank->sendMessageTo($recipient, 'Test message');
$conversation = $message->conversation;
$response = Livewire::test(
\App\Http\Livewire\WireChat\DisappearingMessagesSettings::class,
['conversationId' => $conversation->id]
);
$response->assertStatus(200);
}
/**
* Test organization cannot access conversation they don't belong to
*
* @test
*/
public function organization_cannot_access_conversation_they_dont_belong_to()
{
$user = User::factory()->create();
$org1 = Organization::factory()->create();
$org2 = Organization::factory()->create();
$recipient = User::factory()->create();
$org1->users()->attach($user->id);
$org2->users()->attach($recipient->id);
$this->actingAs($org1, 'organization');
// Set active profile in session (required by getActiveProfile())
session([
'activeProfileType' => get_class($org1),
'activeProfileId' => $org1->id,
'active_guard' => 'organization',
]);
// Create conversation with org2 (not org1)
$message = $org2->sendMessageTo($recipient, 'Test message');
$conversation = $message->conversation;
$response = Livewire::test(
\App\Http\Livewire\WireChat\DisappearingMessagesSettings::class,
['conversationId' => $conversation->id]
);
$response->assertStatus(403);
}
/**
* Test unauthenticated user cannot access conversations
*
* @test
*/
public function unauthenticated_user_cannot_access_conversations()
{
$user1 = User::factory()->create();
$user2 = User::factory()->create();
// Create conversation between two users
$message = $user1->sendMessageTo($user2, 'Test message');
$conversation = $message->conversation;
// No authentication - accessing as guest
$response = Livewire::test(
\App\Http\Livewire\WireChat\DisappearingMessagesSettings::class,
['conversationId' => $conversation->id]
);
$response->assertStatus(403);
}
/**
* Test multi-participant conversation access (User and Organization)
*
* @test
*/
public function multi_participant_conversation_allows_both_participants()
{
$user = User::factory()->create();
$organization = Organization::factory()->create();
$organization->users()->attach($user->id);
// Create conversation between user and organization
$message = $user->sendMessageTo($organization, 'Test message');
$conversation = $message->conversation;
// Test 1: User can access
$this->actingAs($user, 'web');
$response1 = Livewire::test(
\App\Http\Livewire\WireChat\DisappearingMessagesSettings::class,
['conversationId' => $conversation->id]
);
$response1->assertStatus(200);
// Test 2: Organization can access
$this->actingAs($organization, 'organization');
$response2 = Livewire::test(
\App\Http\Livewire\WireChat\DisappearingMessagesSettings::class,
['conversationId' => $conversation->id]
);
$response2->assertStatus(200);
}
/**
* Test disappearing messages can be enabled by organization
*
* @test
*/
public function organization_can_enable_disappearing_messages()
{
$user = User::factory()->create();
$organization = Organization::factory()->create();
$organization->users()->attach($user->id);
$recipient = User::factory()->create();
$this->actingAs($organization, 'organization');
// Create conversation with organization as sender
$message = $organization->sendMessageTo($recipient, 'Test message');
$conversation = $message->conversation;
$response = Livewire::test(
\App\Http\Livewire\WireChat\DisappearingMessagesSettings::class,
['conversationId' => $conversation->id]
);
// Check that component loaded successfully
$response->assertStatus(200);
$response->assertSet('conversationId', $conversation->id);
$response->assertSet('platformEnabled', true);
}
/**
* Test admin can access disappearing message settings
*
* @test
*/
public function admin_can_access_disappearing_message_settings()
{
$user = User::factory()->create();
$admin = Admin::factory()->create();
$admin->users()->attach($user->id);
$recipient = User::factory()->create();
$this->actingAs($admin, 'admin');
// Create conversation with admin as sender
$message = $admin->sendMessageTo($recipient, 'Test message');
$conversation = $message->conversation;
$response = Livewire::test(
\App\Http\Livewire\WireChat\DisappearingMessagesSettings::class,
['conversationId' => $conversation->id]
);
$response->assertStatus(200);
$response->assertViewHas('conversation');
}
/**
* Test bank can access disappearing message settings
*
* @test
*/
public function bank_can_access_disappearing_message_settings()
{
$user = User::factory()->create();
$bank = Bank::factory()->create();
$bank->managers()->attach($user->id);
$recipient = User::factory()->create();
$this->actingAs($bank, 'bank');
// Create conversation with bank as sender
$message = $bank->sendMessageTo($recipient, 'Test message');
$conversation = $message->conversation;
$response = Livewire::test(
\App\Http\Livewire\WireChat\DisappearingMessagesSettings::class,
['conversationId' => $conversation->id]
);
$response->assertStatus(200);
$response->assertSet('conversation', function ($conversation) {
return $conversation instanceof Conversation;
});
}
/**
* Test conversation access via route middleware (belongsToConversation)
*
* @test
*/
public function route_middleware_blocks_unauthorized_conversation_access()
{
$user = User::factory()->create();
$otherUser = User::factory()->create();
$anotherUser = User::factory()->create();
$this->actingAs($user, 'web');
// Set active profile in session (required by getActiveProfile())
session([
'activeProfileType' => get_class($user),
'activeProfileId' => $user->id,
'active_guard' => 'web',
]);
// Create conversation between otherUser and anotherUser (not involving $user)
$message = $otherUser->sendMessageTo($anotherUser, 'Test message');
$conversation = $message->conversation;
// Try to access via route (should be blocked)
$response = $this->get(route('chat', ['conversation' => $conversation->id]));
// Middleware may return 403 or redirect (302) when unauthorized
// Both are acceptable - what matters is user cannot access the conversation
$this->assertTrue(
in_array($response->status(), [302, 403]),
"Expected 302 redirect or 403 forbidden, but got {$response->status()}"
);
// If redirected, should not be to the chat page
if ($response->status() === 302) {
$this->assertNotEquals(
route('chat', ['conversation' => $conversation->id]),
$response->headers->get('Location'),
'User should not be redirected to the unauthorized conversation'
);
}
}
/**
* Test route middleware allows authorized conversation access
*
* @test
*/
public function route_middleware_allows_authorized_conversation_access()
{
$user = User::factory()->create();
$recipient = User::factory()->create();
$this->actingAs($user, 'web');
// Set active profile in session (required by getActiveProfile())
session([
'activeProfileType' => get_class($user),
'activeProfileId' => $user->id,
'active_guard' => 'web',
]);
// Create conversation with current user as sender
$message = $user->sendMessageTo($recipient, 'Test message');
$conversation = $message->conversation;
// Access via route (should be allowed by middleware)
$response = $this->get(route('chat', ['conversation' => $conversation->id]));
// Should either return 200 (success) or 302 redirect to valid location
// The Livewire component tests already verify authorization at component level
// Route level we just need to ensure it's not blocked entirely
$this->assertTrue(
in_array($response->status(), [200, 302]),
"Expected 200 success or 302 redirect, but got {$response->status()}"
);
// If successful (200), verify we're not getting an error page
if ($response->status() === 200) {
$response->assertDontSee('403');
$response->assertDontSee('Forbidden');
}
}
}