makeTestData(); Session::start(); $this->faker = \Faker\Factory::create(); Model::reguard(); // Create test data $this->testClient = Client::factory()->create([ 'user_id' => $this->user->id, 'company_id' => $this->company->id, ]); $this->testProject = Project::factory()->create([ 'user_id' => $this->user->id, 'company_id' => $this->company->id, 'client_id' => $this->testClient->id, ]); $this->testUser = User::factory()->create([ 'account_id' => $this->account->id, ]); } // ==================== VALID PAYLOADS (200 STATUS) ==================== public function testCreateTaskWithValidPayloadReturns200() { $data = [ 'client_id' => $this->testClient->hashed_id, 'description' => 'Test Task Description', 'time_log' => json_encode([ [time() - 3600, time(), 'Working on task', true] ]), ]; $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token, ])->postJson("/api/v1/tasks", $data); $response->assertStatus(200); $response->assertJsonStructure([ 'data' => [ 'id', 'description', 'client_id', 'time_log', ] ]); } public function testCreateTaskWithProjectReturns200() { $data = [ 'client_id' => $this->testClient->hashed_id, 'project_id' => $this->testProject->hashed_id, 'description' => 'Test Task with Project', 'time_log' => json_encode([]), ]; $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token, ])->postJson("/api/v1/tasks", $data); $response->assertStatus(200); $response->assertJson([ 'data' => [ 'description' => 'Test Task with Project', 'project_id' => $this->testProject->hashed_id, ] ]); } public function testUpdateTaskWithValidPayloadReturns200() { $task = Task::factory()->create([ 'user_id' => $this->user->id, 'company_id' => $this->company->id, 'client_id' => $this->testClient->id, 'description' => 'Original Description', ]); $data = [ 'description' => 'Updated Description', 'time_log' => json_encode([ [time() - 1800, time(), 'Updated time log', true] ]), ]; $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token, ])->putJson("/api/v1/tasks/{$task->hashed_id}", $data); $response->assertStatus(200); $response->assertJson([ 'data' => [ 'description' => 'Updated Description', ] ]); } public function testCreateTaskWithValidTimeLogArrayReturns200() { $data = [ 'client_id' => $this->testClient->hashed_id, 'description' => 'Test Task with Array Time Log', 'time_log' => [ [time() - 3600, time() - 1800, 'Working on task', true], [time() - 1800, time() - 900, 'Break time', false], ], ]; $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token, ])->postJson("/api/v1/tasks", $data); $response->assertStatus(200); } public function testCreateTaskWithEmptyTimeLogReturns200() { $data = [ 'client_id' => $this->testClient->hashed_id, 'description' => 'Test Task with Empty Time Log', 'time_log' => json_encode([]), ]; $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token, ])->postJson("/api/v1/tasks", $data); $response->assertStatus(200); } // ==================== INVALID PAYLOADS (422 STATUS) ==================== public function testCreateTaskWithoutClientIdReturns200() { $data = [ 'description' => 'Test Task without Client', 'time_log' => json_encode([]), ]; $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token, ])->postJson("/api/v1/tasks", $data); $response->assertStatus(200); $response->assertJsonStructure([ 'data' => [ 'id', 'description', 'client_id', // Should be null ] ]); } public function testCreateTaskWithInvalidClientIdReturns422() { $data = [ 'client_id' => 'invalid-client-id', 'description' => 'Test Task with Invalid Client', 'time_log' => json_encode([]), ]; $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token, ])->postJson("/api/v1/tasks", $data); $response->assertStatus(422); $response->assertJsonValidationErrors(['client_id']); } public function testCreateTaskWithNonExistentClientIdReturns422() { $data = [ 'client_id' => $this->encodePrimaryKey(99999), 'description' => 'Test Task with Non-existent Client', 'time_log' => json_encode([]), ]; $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token, ])->postJson("/api/v1/tasks", $data); $response->assertStatus(422); $response->assertJsonValidationErrors(['client_id']); } public function testCreateTaskWithInvalidProjectIdReturns200() { $data = [ 'client_id' => $this->testClient->hashed_id, 'project_id' => 'invalid-project-id', 'description' => 'Test Task with Invalid Project', 'time_log' => json_encode([]), ]; $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token, ])->postJson("/api/v1/tasks", $data); $response->assertStatus(200); // Invalid project_id should be silently removed $response->assertJsonMissing(['project_id']); } public function testCreateTaskWithNonExistentProjectIdReturns200() { $data = [ 'client_id' => $this->testClient->hashed_id, 'project_id' => $this->encodePrimaryKey(99999), 'description' => 'Test Task with Non-existent Project', 'time_log' => json_encode([]), ]; $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token, ])->postJson("/api/v1/tasks", $data); $response->assertStatus(200); // Non-existent project_id should be silently removed $response->assertJsonMissing(['project_id']); } // ==================== TIME_LOG VALIDATION TESTS ==================== public function testCreateTaskWithInvalidTimeLogFormatReturns200() { $data = [ 'client_id' => $this->testClient->hashed_id, 'description' => 'Test Task with Invalid Time Log', 'time_log' => 'invalid-json-string', ]; $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token, ])->postJson("/api/v1/tasks", $data); $response->assertStatus(200); // Invalid JSON should be converted to empty array $response->assertJson([ 'data' => [ 'time_log' => '[]' ] ]); } public function testCreateTaskWithTimeLogTooManyElementsReturns422() { $data = [ 'client_id' => $this->testClient->hashed_id, 'description' => 'Test Task with Too Many Time Log Elements', 'time_log' => json_encode([ [time() - 3600, time(), 'Working', true, 'extra-element'] ]), ]; $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token, ])->postJson("/api/v1/tasks", $data); $response->assertStatus(422); $response->assertJsonValidationErrors(['time_log']); } public function testCreateTaskWithNonIntegerTimestampsReturns200() { $data = [ 'client_id' => $this->testClient->hashed_id, 'description' => 'Test Task with Non-integer Timestamps', 'time_log' => json_encode([ ['not-a-timestamp', time(), 'Working', true] ]), ]; $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token, ])->postJson("/api/v1/tasks", $data); $response->assertStatus(200); // Non-integer timestamps should be converted to integers $response->assertJsonStructure([ 'data' => [ 'time_log' ] ]); } public function testCreateTaskWithNonBooleanBillableFlagReturns200() { $data = [ 'client_id' => $this->testClient->hashed_id, 'description' => 'Test Task with Non-boolean Billable', 'time_log' => json_encode([ [time() - 3600, time(), 'Working', 'not-a-boolean'] ]), ]; $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token, ])->postJson("/api/v1/tasks", $data); $response->assertStatus(200); // Non-boolean billable flag should be converted to true $response->assertJsonStructure([ 'data' => [ 'time_log' ] ]); } public function testCreateTaskWithOverlappingTimeLogReturns422() { $startTime = time() - 3600; $endTime = time() - 1800; $overlapStart = time() - 2700; // Overlaps with first entry $overlapEnd = time() - 900; $data = [ 'client_id' => $this->testClient->hashed_id, 'description' => 'Test Task with Overlapping Time Log', 'time_log' => json_encode([ [$startTime, $endTime, 'First session', true], [$overlapStart, $overlapEnd, 'Overlapping session', true], ]), ]; $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token, ])->postJson("/api/v1/tasks", $data); $response->assertStatus(422); $response->assertJsonValidationErrors(['time_log']); } public function testCreateTaskWithStartTimeAfterEndTimeReturns422() { $data = [ 'client_id' => $this->testClient->hashed_id, 'description' => 'Test Task with Invalid Time Order', 'time_log' => json_encode([ [time(), time() - 3600, 'Start after end', true] // Start time after end time ]), ]; $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token, ])->postJson("/api/v1/tasks", $data); $response->assertStatus(422); $response->assertJsonValidationErrors(['time_log']); } // ==================== UPDATE VALIDATION TESTS ==================== public function testUpdateTaskWithInvalidClientIdReturns422() { $task = Task::factory()->create([ 'user_id' => $this->user->id, 'company_id' => $this->company->id, 'client_id' => $this->testClient->id, ]); $data = [ 'client_id' => 'invalid-client-id', 'description' => 'Updated Description', ]; $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token, ])->putJson("/api/v1/tasks/{$task->hashed_id}", $data); $response->assertStatus(422); $response->assertJsonValidationErrors(['client_id']); } public function testUpdateTaskWithInvalidProjectIdReturns200() { $task = Task::factory()->create([ 'user_id' => $this->user->id, 'company_id' => $this->company->id, 'client_id' => $this->testClient->id, ]); $data = [ 'project_id' => 'invalid-project-id', 'description' => 'Updated Description', ]; $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token, ])->putJson("/api/v1/tasks/{$task->hashed_id}", $data); $response->assertStatus(200); // Invalid project_id should be silently removed $response->assertJsonMissing(['project_id']); } public function testUpdateTaskWithInvalidTimeLogReturns200() { $task = Task::factory()->create([ 'user_id' => $this->user->id, 'company_id' => $this->company->id, 'client_id' => $this->testClient->id, ]); $data = [ 'time_log' => json_encode([ [time() - 3600, time(), 'Working', 'not-a-boolean'] ]), ]; $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token, ])->putJson("/api/v1/tasks/{$task->hashed_id}", $data); $response->assertStatus(200); // Invalid data should be sanitized $response->assertJsonStructure([ 'data' => [ 'time_log' ] ]); } // ==================== EDGE CASES ==================== public function testCreateTaskWithProjectFromDifferentClientReturns200() { // Create a different client and project $otherClient = Client::factory()->create([ 'user_id' => $this->user->id, 'company_id' => $this->company->id, ]); $otherProject = Project::factory()->create([ 'user_id' => $this->user->id, 'company_id' => $this->company->id, 'client_id' => $otherClient->id, ]); $data = [ 'client_id' => $this->testClient->hashed_id, 'project_id' => $otherProject->hashed_id, // Project belongs to different client 'description' => 'Test Task with Mismatched Project', 'time_log' => json_encode([]), ]; $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token, ])->postJson("/api/v1/tasks", $data); $response->assertStatus(200); // Project from different client should be silently removed $response->assertJsonMissing(['project_id']); } public function testCreateTaskWithDeletedClientReturns422() { $deletedClient = Client::factory()->create([ 'user_id' => $this->user->id, 'company_id' => $this->company->id, 'is_deleted' => true, ]); $data = [ 'client_id' => $deletedClient->hashed_id, 'description' => 'Test Task with Deleted Client', 'time_log' => json_encode([]), ]; $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token, ])->postJson("/api/v1/tasks", $data); $response->assertStatus(422); $response->assertJsonValidationErrors(['client_id']); } public function testCreateTaskWithDeletedProjectReturns422() { $deletedProject = Project::factory()->create([ 'user_id' => $this->user->id, 'company_id' => $this->company->id, 'client_id' => $this->testClient->id, 'is_deleted' => true, ]); $data = [ 'client_id' => $this->testClient->hashed_id, 'project_id' => $deletedProject->hashed_id, 'description' => 'Test Task with Deleted Project', 'time_log' => json_encode([]), ]; $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token, ])->postJson("/api/v1/tasks", $data); $response->assertStatus(422); $response->assertJsonValidationErrors(['project_id']); } public function testCreateTaskWithValidTimeLogWithZeroEndTimeReturns200() { $data = [ 'client_id' => $this->testClient->hashed_id, 'description' => 'Test Task with Running Timer', 'time_log' => json_encode([ [time() - 3600, 0, 'Currently running', true] // 0 means timer is running ]), ]; $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token, ])->postJson("/api/v1/tasks", $data); $response->assertStatus(200); } public function testCreateTaskWithValidTimeLogWithOnlyTwoElementsReturns200() { $data = [ 'client_id' => $this->testClient->hashed_id, 'description' => 'Test Task with Minimal Time Log', 'time_log' => json_encode([ [time() - 3600, time()] // Only start and end time ]), ]; $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token, ])->postJson("/api/v1/tasks", $data); $response->assertStatus(200); } public function testCreateTaskWithValidTimeLogWithThreeElementsReturns200() { $data = [ 'client_id' => $this->testClient->hashed_id, 'description' => 'Test Task with Three Element Time Log', 'time_log' => json_encode([ [time() - 3600, time(), 'Working on task'] // Start, end, description ]), ]; $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token, ])->postJson("/api/v1/tasks", $data); $response->assertStatus(200); } }