Initial commit
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
570
tests/Feature/Security/Authorization/PaymentMultiAuthTest.php
Normal file
570
tests/Feature/Security/Authorization/PaymentMultiAuthTest.php
Normal 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');
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
421
tests/Feature/Security/Authorization/WireChatMultiAuthTest.php
Normal file
421
tests/Feature/Security/Authorization/WireChatMultiAuthTest.php
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user