Files
headroom/backend/tests/Feature/TeamMember/TeamMemberTest.php
Santhosh Janardhanan 3173d4250c feat(team-member): Complete Team Member Management capability
Implement full CRUD operations for team members with TDD approach:

Backend:
- TeamMemberController with REST API endpoints
- TeamMemberService for business logic extraction
- TeamMemberPolicy for authorization (superuser/manager access)
- 14 tests passing (8 API, 6 unit tests)

Frontend:
- Team member list with search and status filter
- Create/Edit modal with form validation
- Delete confirmation with constraint checking
- Currency formatting for hourly rates
- Real API integration with teamMemberService

Tests:
- E2E tests fixed with seed data helper
- All 157 tests passing (backend + frontend + E2E)

Closes #22
2026-02-18 22:01:57 -05:00

239 lines
7.5 KiB
PHP

<?php
namespace Tests\Feature\TeamMember;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
use App\Models\User;
use App\Models\TeamMember;
use App\Models\Role;
use App\Models\Allocation;
use App\Models\Project;
class TeamMemberTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
}
protected function loginAsManager()
{
$user = User::factory()->create([
'email' => 'manager@example.com',
'password' => bcrypt('password123'),
'role' => 'manager',
'active' => true,
]);
$response = $this->postJson('/api/auth/login', [
'email' => 'manager@example.com',
'password' => 'password123',
]);
return $response->json('access_token');
}
// 2.1.9 API test: POST /api/team-members creates member
public function test_post_team_members_creates_member()
{
$token = $this->loginAsManager();
$role = Role::factory()->create(['name' => 'Backend Developer']);
$response = $this->withHeader('Authorization', "Bearer {$token}")
->postJson('/api/team-members', [
'name' => 'John Doe',
'role_id' => $role->id,
'hourly_rate' => 150.00,
'active' => true,
]);
$response->assertStatus(201);
$response->assertJson([
'name' => 'John Doe',
'role_id' => $role->id,
'hourly_rate' => '150.00',
'active' => true,
]);
$this->assertDatabaseHas('team_members', [
'name' => 'John Doe',
'role_id' => $role->id,
'active' => true,
]);
}
// 2.1.10 API test: Validate hourly_rate > 0
public function test_validate_hourly_rate_must_be_greater_than_zero()
{
$token = $this->loginAsManager();
$role = Role::factory()->create(['name' => 'Backend Developer']);
// Test with zero
$response = $this->withHeader('Authorization', "Bearer {$token}")
->postJson('/api/team-members', [
'name' => 'John Doe',
'role_id' => $role->id,
'hourly_rate' => 0,
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors(['hourly_rate']);
// Test with negative
$response = $this->withHeader('Authorization', "Bearer {$token}")
->postJson('/api/team-members', [
'name' => 'John Doe',
'role_id' => $role->id,
'hourly_rate' => -50,
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors(['hourly_rate']);
$response->assertJsonFragment([
'hourly_rate' => ['Hourly rate must be greater than 0'],
]);
}
// 2.1.11 API test: Validate required fields
public function test_validate_required_fields()
{
$token = $this->loginAsManager();
$response = $this->withHeader('Authorization', "Bearer {$token}")
->postJson('/api/team-members', []);
$response->assertStatus(422);
$response->assertJsonValidationErrors(['name', 'role_id', 'hourly_rate']);
}
// 2.1.12 API test: GET /api/team-members returns all members
public function test_get_team_members_returns_all_members()
{
$token = $this->loginAsManager();
$role = Role::factory()->create();
// Create active and inactive team members
TeamMember::factory()->count(2)->create(['role_id' => $role->id, 'active' => true]);
TeamMember::factory()->create(['role_id' => $role->id, 'active' => false]);
$response = $this->withHeader('Authorization', "Bearer {$token}")
->getJson('/api/team-members');
$response->assertStatus(200);
$response->assertJsonCount(3);
}
// 2.1.13 API test: Filter by active status
public function test_filter_by_active_status()
{
$token = $this->loginAsManager();
$role = Role::factory()->create();
// Create active and inactive team members
TeamMember::factory()->count(2)->create(['role_id' => $role->id, 'active' => true]);
TeamMember::factory()->create(['role_id' => $role->id, 'active' => false]);
// Get only active
$response = $this->withHeader('Authorization', "Bearer {$token}")
->getJson('/api/team-members?active=true');
$response->assertStatus(200);
$response->assertJsonCount(2);
// Get only inactive
$response = $this->withHeader('Authorization', "Bearer {$token}")
->getJson('/api/team-members?active=false');
$response->assertStatus(200);
$response->assertJsonCount(1);
}
// 2.1.14 API test: PUT /api/team-members/{id} updates member
public function test_put_team_members_updates_member()
{
$token = $this->loginAsManager();
$role = Role::factory()->create();
$teamMember = TeamMember::factory()->create([
'role_id' => $role->id,
'hourly_rate' => 150.00,
]);
$response = $this->withHeader('Authorization', "Bearer {$token}")
->putJson("/api/team-members/{$teamMember->id}", [
'hourly_rate' => 175.00,
]);
$response->assertStatus(200);
$response->assertJson([
'id' => $teamMember->id,
'hourly_rate' => '175.00',
]);
$this->assertDatabaseHas('team_members', [
'id' => $teamMember->id,
'hourly_rate' => '175.00',
]);
}
// 2.1.15 API test: Deactivate sets active=false
public function test_deactivate_sets_active_to_false()
{
$token = $this->loginAsManager();
$role = Role::factory()->create();
$teamMember = TeamMember::factory()->create([
'role_id' => $role->id,
'active' => true,
]);
$response = $this->withHeader('Authorization', "Bearer {$token}")
->putJson("/api/team-members/{$teamMember->id}", [
'active' => false,
]);
$response->assertStatus(200);
$response->assertJson([
'id' => $teamMember->id,
'active' => false,
]);
$this->assertDatabaseHas('team_members', [
'id' => $teamMember->id,
'active' => false,
]);
}
// 2.1.16 API test: DELETE rejected if allocations exist
public function test_delete_rejected_if_allocations_exist()
{
$token = $this->loginAsManager();
$role = Role::factory()->create();
$teamMember = TeamMember::factory()->create(['role_id' => $role->id]);
$project = Project::factory()->create();
// Create an allocation for the team member
Allocation::factory()->create([
'team_member_id' => $teamMember->id,
'project_id' => $project->id,
'month' => '2024-01',
'allocated_hours' => 40,
]);
$response = $this->withHeader('Authorization', "Bearer {$token}")
->deleteJson("/api/team-members/{$teamMember->id}");
$response->assertStatus(422);
$response->assertJson([
'message' => 'Cannot delete team member with active allocations',
'suggestion' => 'Consider deactivating the team member instead',
]);
// Verify the team member still exists
$this->assertDatabaseHas('team_members', [
'id' => $teamMember->id,
]);
}
}