Bulk updates for task tests

This commit is contained in:
David Bomba 2025-09-02 09:27:56 +10:00
parent a27d311ae8
commit fb0692b91c
5 changed files with 1015 additions and 11 deletions

View File

@ -143,7 +143,7 @@ class NordigenClient
$params['offset'] = $offset; $params['offset'] = $offset;
} }
$response = $this->httpClient->get("{$this->baseUrl}/agreements/", $params); $response = $this->httpClient->get("{$this->baseUrl}/agreements/enduser", $params);
return $this->handlePaginatedResponse($response); return $this->handlePaginatedResponse($response);
} }
@ -153,7 +153,7 @@ class NordigenClient
*/ */
public function getAgreement(string $agreementId): ?array public function getAgreement(string $agreementId): ?array
{ {
$response = $this->httpClient->get("{$this->baseUrl}/agreements/{$agreementId}/"); $response = $this->httpClient->get("{$this->baseUrl}/agreements/enduser{$agreementId}/");
return $this->handleResponse($response); return $this->handleResponse($response);
} }
@ -163,7 +163,7 @@ class NordigenClient
*/ */
public function createAgreement(array $data): ?array public function createAgreement(array $data): ?array
{ {
$response = $this->httpClient->post("{$this->baseUrl}/agreements/", $data); $response = $this->httpClient->post("{$this->baseUrl}/agreements/enduser", $data);
return $this->handleResponse($response); return $this->handleResponse($response);
} }
@ -173,7 +173,7 @@ class NordigenClient
*/ */
public function updateAgreement(string $agreementId, array $data): ?array public function updateAgreement(string $agreementId, array $data): ?array
{ {
$response = $this->httpClient->put("{$this->baseUrl}/agreements/{$agreementId}/", $data); $response = $this->httpClient->put("{$this->baseUrl}/agreements/enduser/{$agreementId}/", $data);
return $this->handleResponse($response); return $this->handleResponse($response);
} }
@ -183,7 +183,7 @@ class NordigenClient
*/ */
public function deleteAgreement(string $agreementId): bool public function deleteAgreement(string $agreementId): bool
{ {
$response = $this->httpClient->delete("{$this->baseUrl}/agreements/{$agreementId}/"); $response = $this->httpClient->delete("{$this->baseUrl}/agreements/enduser/{$agreementId}/");
return $response->successful(); return $response->successful();
} }

View File

@ -180,6 +180,9 @@ class Nordigen
/** /**
* validAgreement * validAgreement
* @param string $institution_id
* @param array $_accounts
* @return array|null
* @todo - very expensive! * @todo - very expensive!
*/ */
public function validAgreement($institution_id, $_accounts) public function validAgreement($institution_id, $_accounts)
@ -188,11 +191,13 @@ class Nordigen
$nc = new \App\Helpers\Bank\Nordigen\Http\NordigenClient($this->client->getAccessToken()); $nc = new \App\Helpers\Bank\Nordigen\Http\NordigenClient($this->client->getAccessToken());
$requisitions = $nc->getAllRequisitions(); $requisitions = $nc->getAllRequisitions();
$requisitions->filter(function($requisition) use ($institution_id, $_accounts){ $requisition = $requisitions->filter(function($requisition) use ($institution_id, $_accounts){
if($requisition['institution_id'] == $institution_id && !empty(array_intersect($requisition['accounts'], $_accounts))){ if($requisition['institution_id'] == $institution_id && !empty(array_intersect($requisition['accounts'], $_accounts))){
return $requisition; return $requisition;
} }
}); });
return $requisition->first()->toArray() ?? null;
} }

View File

@ -82,11 +82,6 @@ class NordigenController extends BaseController
$agreement = $nordigen->createAgreement($institution, $institution['max_access_valid_for_days'], $txDays);//@2025-07-01: this is the correct way to get the access days $agreement = $nordigen->createAgreement($institution, $institution['max_access_valid_for_days'], $txDays);//@2025-07-01: this is the correct way to get the access days
// $agreement = $nordigen->createAgreement($institution, $data['access_days'] ?? 9999, $txDays);
//this does not work in a multi tenant environment, it simply grabs the first agreement, without differentiating between companies. we may need to store the current requistion...
// $agreement = $nordigen->firstValidAgreement($institution['id'], $data['access_days'] ?? 0, $txDays)
// ?? $nordigen->createAgreement($institution, $data['max_access_valid_for_days'] ?? 90, $txDays);
} catch (\Exception $e) { } catch (\Exception $e) {
$debug = "{$e->getMessage()} ({$e->getCode()})"; $debug = "{$e->getMessage()} ({$e->getCode()})";

View File

@ -0,0 +1,623 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Tests\Feature;
use App\Models\Client;
use App\Models\Project;
use App\Models\Task;
use App\Models\User;
use App\Utils\Traits\MakesHash;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Session;
use Tests\MockAccountData;
use Tests\TestCase;
/**
* Test Task API validation and status codes
*/
class TaskApiValidationTest extends TestCase
{
use MakesHash;
use DatabaseTransactions;
use MockAccountData;
private $faker;
private Client $testClient;
private Project $testProject;
private User $testUser;
protected function setUp(): void
{
parent::setUp();
$this->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);
}
}

View File

@ -0,0 +1,381 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Tests\Unit;
use Tests\TestCase;
use App\Models\Task;
use App\Models\Client;
use App\Models\Project;
use App\Models\User;
use App\Repositories\TaskRepository;
use Tests\MockAccountData;
use App\Utils\Traits\MakesHash;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Session;
use Illuminate\Foundation\Testing\DatabaseTransactions;
/**
* Test TaskRepository::bulkUpdate() method
*/
class TaskRepositoryBulkUpdateTest extends TestCase
{
use MakesHash;
use DatabaseTransactions;
use MockAccountData;
private TaskRepository $taskRepository;
private Client $testClient;
private Project $testProject;
private User $testUser;
protected function setUp(): void
{
parent::setUp();
$this->makeTestData();
Session::start();
Model::reguard();
$this->taskRepository = new TaskRepository();
// Create test client
$this->testClient = Client::factory()->create([
'user_id' => $this->user->id,
'company_id' => $this->company->id,
]);
// Create test project
$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,
]);
}
public function testBulkUpdateProjectIdUpdatesClientId()
{
// Create tasks with different clients
$task1 = Task::factory()->create([
'user_id' => $this->user->id,
'company_id' => $this->company->id,
'client_id' => $this->testClient->id,
'project_id' => null,
'invoice_id' => null,
]);
$task2 = Task::factory()->create([
'user_id' => $this->user->id,
'company_id' => $this->company->id,
'client_id' => $this->testClient->id,
'project_id' => null,
'invoice_id' => null,
]);
// 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,
]);
// Get the query builder for the tasks
$models = Task::whereIn('id', [$task1->id, $task2->id]);
// Bulk update project_id
$this->taskRepository->bulkUpdate($models, 'project_id', $otherProject->id);
// Refresh models from database
$task1->refresh();
$task2->refresh();
// Assert both tasks now have the new project and client
$this->assertEquals($otherProject->id, $task1->project_id);
$this->assertEquals($otherClient->id, $task1->client_id);
$this->assertEquals($otherProject->id, $task2->project_id);
$this->assertEquals($otherClient->id, $task2->client_id);
}
public function testBulkUpdateProjectIdWithNonExistentProject()
{
// Create a task
$task = Task::factory()->create([
'user_id' => $this->user->id,
'company_id' => $this->company->id,
'client_id' => $this->testClient->id,
'project_id' => null,
'invoice_id' => null,
]);
$originalClientId = $task->client_id;
$originalProjectId = $task->project_id;
// Get the query builder for the task
$models = Task::where('id', $task->id);
// Try to bulk update with non-existent project ID
$this->taskRepository->bulkUpdate($models, 'project_id', 99999);
// Refresh model from database
$task->refresh();
// Assert task remains unchanged
$this->assertEquals($originalClientId, $task->client_id);
$this->assertEquals($originalProjectId, $task->project_id);
}
public function testBulkUpdateClientIdUnsetsProjectId()
{
// Create a task with a project
$task = Task::factory()->create([
'user_id' => $this->user->id,
'company_id' => $this->company->id,
'client_id' => $this->testClient->id,
'project_id' => $this->project->id,
'invoice_id' => null,
]);
// Create a different client
$newClient = Client::factory()->create([
'user_id' => $this->user->id,
'company_id' => $this->company->id,
]);
// Get the query builder for the task
$models = Task::where('id', $task->id);
// Bulk update client_id
$this->taskRepository->bulkUpdate($models, 'client_id', $newClient->id);
// Refresh model from database
$task->refresh();
// Assert client_id is updated and project_id is null
$this->assertEquals($newClient->id, $task->client_id);
$this->assertNull($task->project_id);
}
public function testBulkUpdateAssignedUser()
{
// Create tasks
$task1 = Task::factory()->create([
'user_id' => $this->user->id,
'company_id' => $this->company->id,
'client_id' => $this->testClient->id,
'assigned_user_id' => null,
'invoice_id' => null,
]);
$task2 = Task::factory()->create([
'user_id' => $this->user->id,
'company_id' => $this->company->id,
'client_id' => $this->testClient->id,
'assigned_user_id' => null,
'invoice_id' => null,
]);
// Get the query builder for the tasks
$models = Task::whereIn('id', [$task1->id, $task2->id]);
// Bulk update assigned_user_id
$this->taskRepository->bulkUpdate($models, 'assigned_user_id', $this->testUser->id);
// Refresh models from database
$task1->refresh();
$task2->refresh();
// Assert both tasks now have the assigned user
$this->assertEquals($this->testUser->id, $task1->assigned_user_id);
$this->assertEquals($this->testUser->id, $task2->assigned_user_id);
}
public function testBulkUpdateSkipsInvoicedTasks()
{
// Create an invoice first
$invoice = \App\Models\Invoice::factory()->create([
'user_id' => $this->user->id,
'company_id' => $this->company->id,
'client_id' => $this->testClient->id,
]);
// Create tasks - one invoiced, one not
$invoicedTask = Task::factory()->create([
'user_id' => $this->user->id,
'company_id' => $this->company->id,
'client_id' => $this->testClient->id,
'assigned_user_id' => null,
'invoice_id' => $invoice->id, // This task is invoiced
]);
$regularTask = Task::factory()->create([
'user_id' => $this->user->id,
'company_id' => $this->company->id,
'client_id' => $this->testClient->id,
'assigned_user_id' => null,
'invoice_id' => null, // This task is not invoiced
]);
// Get the query builder for both tasks
$models = Task::whereIn('id', [$invoicedTask->id, $regularTask->id]);
// Bulk update assigned_user_id
$this->taskRepository->bulkUpdate($models, 'assigned_user_id', $this->testUser->id);
// Refresh models from database
$invoicedTask->refresh();
$regularTask->refresh();
// Assert invoiced task is unchanged
$this->assertNull($invoicedTask->assigned_user_id);
// Assert regular task is updated
$this->assertEquals($this->testUser->id, $regularTask->assigned_user_id);
}
public function testBulkUpdateWithSoftDeletedProject()
{
// Create a task
$task = Task::factory()->create([
'user_id' => $this->user->id,
'company_id' => $this->company->id,
'client_id' => $this->testClient->id,
'project_id' => null,
'invoice_id' => null,
]);
// Soft delete the project
$this->testProject->delete();
// Get the query builder for the task
$models = Task::where('id', $task->id);
// Bulk update project_id (should work with soft deleted project)
$this->taskRepository->bulkUpdate($models, 'project_id', $this->testProject->id);
// Refresh model from database
$task->refresh();
// Assert task is updated with the soft deleted project
$this->assertEquals($this->testProject->id, $task->project_id);
$this->assertEquals($this->testClient->id, $task->client_id);
}
public function testBulkUpdateWithDifferentColumnTypes()
{
// Create a task
$task = Task::factory()->create([
'user_id' => $this->user->id,
'company_id' => $this->company->id,
'client_id' => $this->testClient->id,
'description' => 'Original Description',
'rate' => 50.00,
'invoice_id' => null,
]);
// Test string column update
$models = Task::where('id', $task->id);
$this->taskRepository->bulkUpdate($models, 'description', 'New Description');
$task->refresh();
$this->assertEquals('New Description', $task->description);
// Test numeric column update
$models = Task::where('id', $task->id);
$this->taskRepository->bulkUpdate($models, 'rate', 75.50);
$task->refresh();
$this->assertEquals(75.50, $task->rate);
}
public function testBulkUpdatePerformanceWithLargeDataset()
{
// Create many tasks
$tasks = Task::factory()->count(100)->create([
'user_id' => $this->user->id,
'company_id' => $this->company->id,
'client_id' => $this->testClient->id,
'assigned_user_id' => null,
'invoice_id' => null,
]);
$taskIds = $tasks->pluck('id')->toArray();
// Get the query builder for all tasks
$models = Task::whereIn('id', $taskIds);
// Measure execution time
$startTime = microtime(true);
// Bulk update assigned_user_id
$this->taskRepository->bulkUpdate($models, 'assigned_user_id', $this->testUser->id);
$endTime = microtime(true);
$executionTime = $endTime - $startTime;
// Assert all tasks are updated
$updatedTasks = Task::whereIn('id', $taskIds)->get();
foreach ($updatedTasks as $task) {
$this->assertEquals($this->testUser->id, $task->assigned_user_id);
}
// Assert execution time is reasonable (less than 1 second for 100 records)
$this->assertLessThan(1.0, $executionTime, 'Bulk update should be fast for 100 records');
}
public function testBulkUpdateWithEmptyResultSet()
{
// Get query builder for non-existent tasks
$models = Task::where('id', 99999);
// This should not throw an error
$this->taskRepository->bulkUpdate($models, 'assigned_user_id', $this->testUser->id);
// No assertions needed - just ensuring no exceptions are thrown
$this->assertTrue(true);
}
public function testBulkUpdateProjectIdWithTrashedProject()
{
// Create a task
$task = Task::factory()->create([
'user_id' => $this->user->id,
'company_id' => $this->company->id,
'client_id' => $this->testClient->id,
'project_id' => null,
'invoice_id' => null,
]);
// Soft delete the project
$this->testProject->delete();
// Get the query builder for the task
$models = Task::where('id', $task->id);
// Bulk update project_id (should work with soft deleted project)
$this->taskRepository->bulkUpdate($models, 'project_id', $this->testProject->id);
// Refresh model from database
$task->refresh();
// Assert task is updated with the soft deleted project
$this->assertEquals($this->testProject->id, $task->project_id);
$this->assertEquals($this->testClient->id, $task->client_id);
}
}