seed([ ProjectStatusSeeder::class, ProjectTypeSeeder::class, ]); } 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'); } private function projectPayload(array $overrides = []): array { $type = ProjectType::first(); return array_merge([ 'code' => 'TEST-'.strtoupper(Str::random(4)), 'title' => 'New Project', 'type_id' => $type->id, ], $overrides); } private function statusId(string $name): int { return ProjectStatus::where('name', $name)->value('id'); } private function transitionProjectStatus(string $projectId, string $statusName, string $token) { return $this->withToken($token)->putJson("/api/projects/{$projectId}/status", [ 'status_id' => $this->statusId($statusName), ]); } // 3.1.13 API test: POST /api/projects creates project public function test_post_projects_creates_project() { $token = $this->loginAsManager(); $payload = $this->projectPayload(); $response = $this->withToken($token) ->postJson('/api/projects', $payload); $response->assertStatus(201) ->assertJsonFragment([ 'code' => $payload['code'], 'title' => $payload['title'], ]); $this->assertDatabaseHas('projects', [ 'code' => $payload['code'], 'title' => $payload['title'], ]); } // 3.1.14 API test: Project code must be unique public function test_project_code_must_be_unique() { $token = $this->loginAsManager(); $payload = $this->projectPayload(); $this->withToken($token)->postJson('/api/projects', $payload) ->assertStatus(201); $this->withToken($token)->postJson('/api/projects', $payload) ->assertStatus(422) ->assertJsonStructure([ 'message', 'errors' => ['code'], ]); } // 3.1.15 API test: Status transition validation public function test_status_transition_validation() { $token = $this->loginAsManager(); $payload = $this->projectPayload(); $projectId = $this->withToken($token) ->postJson('/api/projects', $payload) ->json('id'); $invalidStatus = $this->statusId('In Progress'); $this->withToken($token) ->putJson("/api/projects/{$projectId}/status", ['status_id' => $invalidStatus]) ->assertStatus(422) ->assertJsonFragment([ 'message' => 'Cannot transition from Pre-sales to In Progress', ]); $this->transitionProjectStatus($projectId, 'SOW Approval', $token) ->assertStatus(200) ->assertJsonPath('status.name', 'SOW Approval'); } // 3.1.16 API test: Estimate approved requires estimate value public function test_estimate_approved_requires_estimate_value() { $token = $this->loginAsManager(); $projectId = $this->withToken($token) ->postJson('/api/projects', $this->projectPayload()) ->json('id'); $this->transitionProjectStatus($projectId, 'SOW Approval', $token) ->assertStatus(200); $this->transitionProjectStatus($projectId, 'Estimation', $token) ->assertStatus(200); $this->transitionProjectStatus($projectId, 'Estimate Approved', $token) ->assertStatus(422) ->assertJsonFragment([ 'message' => 'Cannot transition to Estimate Approved without an approved estimate', ]); } // 3.1.17 API test: Full workflow state machine public function test_full_workflow_state_machine() { $token = $this->loginAsManager(); $payload = $this->projectPayload(['approved_estimate' => 120]); $projectId = $this->withToken($token) ->postJson('/api/projects', $payload) ->json('id'); $workflow = [ 'Pre-sales', 'SOW Approval', 'Estimation', 'Estimate Approved', 'Resource Allocation', 'Sprint 0', 'In Progress', 'UAT', 'Handover / Sign-off', 'Closed', ]; foreach (array_slice($workflow, 1) as $statusName) { $this->transitionProjectStatus($projectId, $statusName, $token) ->assertStatus(200) ->assertJsonPath('status.name', $statusName); } } // 3.1.18 API test: PUT /api/projects/{id}/status transitions public function test_put_projects_status_transitions() { $token = $this->loginAsManager(); $projectId = $this->withToken($token) ->postJson('/api/projects', $this->projectPayload()) ->json('id'); $this->transitionProjectStatus($projectId, 'SOW Approval', $token) ->assertStatus(200) ->assertJsonPath('status.name', 'SOW Approval'); $this->assertDatabaseHas('projects', [ 'id' => $projectId, 'status_id' => $this->statusId('SOW Approval'), ]); } // 3.1.19 API test: PUT /api/projects/{id}/estimate sets approved public function test_put_projects_estimate_sets_approved() { $token = $this->loginAsManager(); $projectId = $this->withToken($token) ->postJson('/api/projects', $this->projectPayload()) ->json('id'); $this->withToken($token) ->putJson("/api/projects/{$projectId}/estimate", ['approved_estimate' => 275]) ->assertStatus(200) ->assertJsonPath('approved_estimate', '275.00'); $this->assertSame('275.00', (string) Project::find($projectId)->approved_estimate); } // 3.1.20 API test: PUT /api/projects/{id}/forecast updates effort public function test_put_projects_forecast_updates_effort() { $token = $this->loginAsManager(); $projectId = $this->withToken($token) ->postJson('/api/projects', $this->projectPayload(['approved_estimate' => 100])) ->json('id'); $forecast = ['2025-01' => 33, '2025-02' => 33, '2025-03' => 34]; $this->withToken($token) ->putJson("/api/projects/{$projectId}/forecast", ['forecasted_effort' => $forecast]) ->assertStatus(200) ->assertJsonFragment(['forecasted_effort' => $forecast]); $this->assertSame($forecast, Project::find($projectId)->forecasted_effort); } // 3.1.21 API test: Validate forecasted sum equals approved public function test_validate_forecasted_sum_equals_approved() { $token = $this->loginAsManager(); $projectId = $this->withToken($token) ->postJson('/api/projects', $this->projectPayload(['approved_estimate' => 100])) ->json('id'); $forecast = ['2025-01' => 50, '2025-02' => 50, '2025-03' => 50]; $this->withToken($token) ->putJson("/api/projects/{$projectId}/forecast", ['forecasted_effort' => $forecast]) ->assertStatus(422) ->assertJsonFragment([ 'message' => 'Forecasted effort (150.00 h) exceeds approved estimate (100.00 h) by 50.00 hours (50.00%). Forecasted effort must be between 95.00 and 105.00 hours for a 100.00 hour estimate.', ]); $this->assertNull(Project::find($projectId)->forecasted_effort); } }