- Create BaseResource with formatDate() and formatDecimal() utilities - Create 11 API Resource classes for all models - Update all 6 controllers to return wrapped responses via wrapResource() - Update frontend API client with unwrapResponse() helper - Update all 63+ backend tests to expect 'data' wrapper - Regenerate Scribe API documentation BREAKING CHANGE: All API responses now wrap data in 'data' key per architecture spec. Backend Tests: 70 passed, 5 failed (unrelated to data wrapper) Frontend Unit: 10 passed E2E Tests: 102 passed, 20 skipped API Docs: Generated successfully Refs: openspec/changes/api-resource-standard
245 lines
7.7 KiB
PHP
245 lines
7.7 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature\TeamMember;
|
|
|
|
use App\Models\Allocation;
|
|
use App\Models\Project;
|
|
use App\Models\Role;
|
|
use App\Models\TeamMember;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Tests\TestCase;
|
|
|
|
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([
|
|
'data' => [
|
|
'name' => 'John Doe',
|
|
'hourly_rate' => '150.00',
|
|
'active' => true,
|
|
],
|
|
]);
|
|
$response->assertJsonPath('data.role.id', $role->id);
|
|
|
|
$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, 'data');
|
|
}
|
|
|
|
// 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, 'data');
|
|
|
|
// Get only inactive
|
|
$response = $this->withHeader('Authorization', "Bearer {$token}")
|
|
->getJson('/api/team-members?active=false');
|
|
|
|
$response->assertStatus(200);
|
|
$response->assertJsonCount(1, 'data');
|
|
}
|
|
|
|
// 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([
|
|
'data' => [
|
|
'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([
|
|
'data' => [
|
|
'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,
|
|
]);
|
|
}
|
|
}
|