Features: - Support login via phone number or email (LoginRequest) - Add members:import-roster command for Excel roster import - Merge survey emails with roster data Code Quality (Phase 1-4): - Add database locking for balance calculation - Add self-approval checks for finance workflow - Create service layer (FinanceDocumentApprovalService, PaymentVerificationService) - Add HasAccountingEntries and HasApprovalWorkflow traits - Create FormRequest classes for validation - Add status-badge component - Define authorization gates in AuthServiceProvider - Add accounting config file Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
181 lines
5.5 KiB
PHP
181 lines
5.5 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature\Roles;
|
|
|
|
use App\Models\FinanceDocument;
|
|
use App\Models\Member;
|
|
use App\Models\MembershipPayment;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Spatie\Permission\Models\Permission;
|
|
use Spatie\Permission\Models\Role;
|
|
use Tests\TestCase;
|
|
use Tests\Traits\CreatesFinanceData;
|
|
use Tests\Traits\CreatesMemberData;
|
|
use Tests\Traits\SeedsRolesAndPermissions;
|
|
|
|
/**
|
|
* Role Permission Tests
|
|
*
|
|
* Tests role-based access control and permissions.
|
|
* Uses new workflow: Secretary → Chair → Board
|
|
*/
|
|
class RolePermissionTest extends TestCase
|
|
{
|
|
use RefreshDatabase, SeedsRolesAndPermissions, CreatesMemberData, CreatesFinanceData;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
Storage::fake('private');
|
|
Storage::fake('local');
|
|
$this->seedRolesAndPermissions();
|
|
}
|
|
|
|
/**
|
|
* Test admin can access admin dashboard
|
|
*/
|
|
public function test_admin_can_access_admin_dashboard(): void
|
|
{
|
|
$admin = $this->createAdmin();
|
|
|
|
$response = $this->actingAs($admin)->get(route('admin.dashboard'));
|
|
|
|
$response->assertStatus(200);
|
|
}
|
|
|
|
/**
|
|
* Test member cannot access admin dashboard
|
|
*/
|
|
public function test_member_cannot_access_admin_dashboard(): void
|
|
{
|
|
$user = $this->createMemberUser();
|
|
|
|
$response = $this->actingAs($user)->get(route('admin.dashboard'));
|
|
|
|
$response->assertStatus(403);
|
|
}
|
|
|
|
/**
|
|
* Test cashier can approve membership payments (first tier)
|
|
*/
|
|
public function test_cashier_can_approve_membership_payments(): void
|
|
{
|
|
$cashier = $this->createCashier();
|
|
$data = $this->createMemberWithPendingPayment();
|
|
|
|
$response = $this->actingAs($cashier)->post(
|
|
route('admin.payment-verifications.approve-cashier', $data['payment'])
|
|
);
|
|
|
|
$data['payment']->refresh();
|
|
$this->assertEquals(MembershipPayment::STATUS_APPROVED_CASHIER, $data['payment']->status);
|
|
}
|
|
|
|
/**
|
|
* Test accountant cannot approve pending payment directly (needs cashier first)
|
|
*/
|
|
public function test_accountant_cannot_approve_pending_payment_directly(): void
|
|
{
|
|
$accountant = $this->createAccountant();
|
|
$data = $this->createMemberWithPendingPayment();
|
|
|
|
$response = $this->actingAs($accountant)->post(
|
|
route('admin.payment-verifications.approve-accountant', $data['payment'])
|
|
);
|
|
|
|
// Should remain pending (workflow requires cashier first)
|
|
$data['payment']->refresh();
|
|
$this->assertEquals(MembershipPayment::STATUS_PENDING, $data['payment']->status);
|
|
}
|
|
|
|
/**
|
|
* Test chair can approve after accountant
|
|
*/
|
|
public function test_chair_can_approve_after_accountant(): void
|
|
{
|
|
$chair = $this->createChair();
|
|
$data = $this->createMemberWithPaymentAtStage('accountant_approved');
|
|
|
|
$response = $this->actingAs($chair)->post(
|
|
route('admin.payment-verifications.approve-chair', $data['payment'])
|
|
);
|
|
|
|
$data['payment']->refresh();
|
|
$this->assertEquals(MembershipPayment::STATUS_APPROVED_CHAIR, $data['payment']->status);
|
|
}
|
|
|
|
/**
|
|
* Test secretary can approve finance documents (new workflow first stage)
|
|
*/
|
|
public function test_secretary_can_approve_finance_documents(): void
|
|
{
|
|
$secretary = $this->createSecretary();
|
|
$document = $this->createFinanceDocument([
|
|
'status' => FinanceDocument::STATUS_PENDING,
|
|
]);
|
|
|
|
$response = $this->actingAs($secretary)->post(
|
|
route('admin.finance.approve', $document)
|
|
);
|
|
|
|
$document->refresh();
|
|
$this->assertEquals(FinanceDocument::STATUS_APPROVED_SECRETARY, $document->status);
|
|
}
|
|
|
|
/**
|
|
* Test unauthorized user cannot approve
|
|
*/
|
|
public function test_unauthorized_user_cannot_approve(): void
|
|
{
|
|
$user = $this->createMemberUser();
|
|
$data = $this->createMemberWithPendingPayment();
|
|
|
|
$response = $this->actingAs($user)->post(
|
|
route('admin.payment-verifications.approve-cashier', $data['payment'])
|
|
);
|
|
|
|
$response->assertStatus(403);
|
|
}
|
|
|
|
/**
|
|
* Test super admin has all permissions
|
|
*/
|
|
public function test_super_admin_has_all_permissions(): void
|
|
{
|
|
$superAdmin = $this->createSuperAdmin();
|
|
|
|
// Super admin should have various permissions
|
|
$this->assertTrue($superAdmin->hasRole('super_admin'));
|
|
$this->assertTrue($superAdmin->can('view_finance_documents'));
|
|
$this->assertTrue($superAdmin->can('approve_finance_secretary'));
|
|
}
|
|
|
|
/**
|
|
* Test role hierarchy for approvals
|
|
*/
|
|
public function test_role_hierarchy_for_approvals(): void
|
|
{
|
|
// Chair should have the finance_chair role
|
|
$chair = $this->createChair();
|
|
$this->assertTrue($chair->hasRole('finance_chair'));
|
|
|
|
// Secretary should have the secretary_general role
|
|
$secretary = $this->createSecretary();
|
|
$this->assertTrue($secretary->hasRole('secretary_general'));
|
|
}
|
|
|
|
/**
|
|
* Test finance approval roles exist
|
|
*/
|
|
public function test_finance_approval_roles_exist(): void
|
|
{
|
|
$this->assertNotNull(Role::findByName('secretary_general'));
|
|
$this->assertNotNull(Role::findByName('finance_chair'));
|
|
$this->assertNotNull(Role::findByName('finance_board_member'));
|
|
$this->assertNotNull(Role::findByName('finance_cashier'));
|
|
$this->assertNotNull(Role::findByName('finance_accountant'));
|
|
}
|
|
}
|