From a69d1cb4c45bc6d61ceae14c6527cc87c051f3a1 Mon Sep 17 00:00:00 2001 From: Constantin Graf Date: Mon, 24 Jun 2024 13:36:30 +0200 Subject: [PATCH] Added ability to archive projects and clients, fixes ST-37 --- .../Controllers/Api/V1/ClientController.php | 21 ++- .../Controllers/Api/V1/ProjectController.php | 11 +- .../Requests/V1/Client/ClientIndexRequest.php | 35 ++++ .../V1/Client/ClientUpdateRequest.php | 10 ++ .../V1/Project/ProjectIndexRequest.php | 9 + .../V1/Project/ProjectUpdateRequest.php | 10 ++ .../Resources/V1/Client/ClientResource.php | 2 + .../Resources/V1/Project/ProjectResource.php | 2 + app/Models/Client.php | 13 ++ app/Models/Project.php | 16 ++ database/factories/ClientFactory.php | 10 ++ database/factories/ProjectFactory.php | 10 ++ ..._columns_to_projects_and_clients_table.php | 36 ++++ .../Endpoint/Api/V1/ClientEndpointTest.php | 137 +++++++++++++++ .../Endpoint/Api/V1/ProjectEndpointTest.php | 160 +++++++++++++++++- tests/Unit/Model/ClientModelTest.php | 26 +++ tests/Unit/Model/ProjectModelTest.php | 26 +++ 17 files changed, 524 insertions(+), 10 deletions(-) create mode 100644 app/Http/Requests/V1/Client/ClientIndexRequest.php create mode 100644 database/migrations/2024_06_21_122754_add_is_archived_columns_to_projects_and_clients_table.php diff --git a/app/Http/Controllers/Api/V1/ClientController.php b/app/Http/Controllers/Api/V1/ClientController.php index e150a33e..57c29a1b 100644 --- a/app/Http/Controllers/Api/V1/ClientController.php +++ b/app/Http/Controllers/Api/V1/ClientController.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\Http\Controllers\Api\V1; use App\Exceptions\Api\EntityStillInUseApiException; +use App\Http\Requests\V1\Client\ClientIndexRequest; use App\Http\Requests\V1\Client\ClientStoreRequest; use App\Http\Requests\V1\Client\ClientUpdateRequest; use App\Http\Resources\V1\Client\ClientCollection; @@ -13,6 +14,7 @@ use App\Models\Client; use App\Models\Organization; use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Http\JsonResponse; +use Illuminate\Support\Carbon; class ClientController extends Controller { @@ -33,14 +35,22 @@ class ClientController extends Controller * * @operationId getClients */ - public function index(Organization $organization): ClientCollection + public function index(Organization $organization, ClientIndexRequest $request): ClientCollection { $this->checkPermission($organization, 'clients:view'); - $clients = Client::query() + $clientsQuery = Client::query() ->whereBelongsTo($organization, 'organization') - ->orderBy('created_at', 'desc') - ->paginate(config('app.pagination_per_page_default')); + ->orderBy('created_at', 'desc'); + + $filterArchived = $request->getFilterArchived(); + if ($filterArchived === 'true') { + $clientsQuery->whereNotNull('archived_at'); + } elseif ($filterArchived === 'false') { + $clientsQuery->whereNull('archived_at'); + } + + $clients = $clientsQuery->paginate(config('app.pagination_per_page_default')); return new ClientCollection($clients); } @@ -76,6 +86,9 @@ class ClientController extends Controller $this->checkPermission($organization, 'clients:update', $client); $client->name = $request->input('name'); + if ($request->has('is_archived')) { + $client->archived_at = $request->getIsArchived() ? Carbon::now() : null; + } $client->save(); return new ClientResource($client); diff --git a/app/Http/Controllers/Api/V1/ProjectController.php b/app/Http/Controllers/Api/V1/ProjectController.php index 2cdfca5a..8e53c603 100644 --- a/app/Http/Controllers/Api/V1/ProjectController.php +++ b/app/Http/Controllers/Api/V1/ProjectController.php @@ -13,11 +13,11 @@ use App\Http\Resources\V1\Project\ProjectResource; use App\Models\Organization; use App\Models\Project; use App\Models\ProjectMember; -use App\Models\User; use App\Service\BillableRateService; use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Http\JsonResponse; use Illuminate\Http\Resources\Json\JsonResource; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; class ProjectController extends Controller @@ -51,6 +51,12 @@ class ProjectController extends Controller if (! $canViewAllProjects) { $projectsQuery->visibleByEmployee($user); } + $filterArchived = $request->getFilterArchived(); + if ($filterArchived === 'true') { + $projectsQuery->whereNotNull('archived_at'); + } elseif ($filterArchived === 'false') { + $projectsQuery->whereNull('archived_at'); + } $projects = $projectsQuery->paginate(config('app.pagination_per_page_default')); @@ -108,6 +114,9 @@ class ProjectController extends Controller $project->name = $request->input('name'); $project->color = $request->input('color'); $project->is_billable = (bool) $request->input('is_billable'); + if ($request->has('is_archived')) { + $project->archived_at = $request->getIsArchived() ? Carbon::now() : null; + } $project->billable_rate = $request->getBillableRate(); $project->client_id = $request->input('client_id'); $project->save(); diff --git a/app/Http/Requests/V1/Client/ClientIndexRequest.php b/app/Http/Requests/V1/Client/ClientIndexRequest.php new file mode 100644 index 00000000..e7947dd1 --- /dev/null +++ b/app/Http/Requests/V1/Client/ClientIndexRequest.php @@ -0,0 +1,35 @@ +> + */ + public function rules(): array + { + return [ + 'page' => [ + 'integer', + 'min:1', + ], + 'archived' => [ + 'string', + 'in:true,false,all', + ], + ]; + } + + public function getFilterArchived(): string + { + return $this->input('archived', 'false'); + } +} diff --git a/app/Http/Requests/V1/Client/ClientUpdateRequest.php b/app/Http/Requests/V1/Client/ClientUpdateRequest.php index a9b9f5c6..5b035bb5 100644 --- a/app/Http/Requests/V1/Client/ClientUpdateRequest.php +++ b/app/Http/Requests/V1/Client/ClientUpdateRequest.php @@ -35,6 +35,16 @@ class ClientUpdateRequest extends FormRequest return $builder->whereBelongsTo($this->organization, 'organization'); }))->ignore($this->client->getKey())->withCustomTranslation('validation.client_name_already_exists'), ], + 'is_archived' => [ + 'boolean', + ], ]; } + + public function getIsArchived(): bool + { + assert($this->has('is_archived')); + + return (bool) $this->input('is_archived'); + } } diff --git a/app/Http/Requests/V1/Project/ProjectIndexRequest.php b/app/Http/Requests/V1/Project/ProjectIndexRequest.php index f71b3e16..44f96e8a 100644 --- a/app/Http/Requests/V1/Project/ProjectIndexRequest.php +++ b/app/Http/Requests/V1/Project/ProjectIndexRequest.php @@ -21,6 +21,15 @@ class ProjectIndexRequest extends FormRequest 'integer', 'min:1', ], + 'archived' => [ + 'string', + 'in:true,false,all', + ], ]; } + + public function getFilterArchived(): string + { + return $this->input('archived', 'false'); + } } diff --git a/app/Http/Requests/V1/Project/ProjectUpdateRequest.php b/app/Http/Requests/V1/Project/ProjectUpdateRequest.php index c3692e00..ed163c92 100644 --- a/app/Http/Requests/V1/Project/ProjectUpdateRequest.php +++ b/app/Http/Requests/V1/Project/ProjectUpdateRequest.php @@ -47,6 +47,9 @@ class ProjectUpdateRequest extends FormRequest 'required', 'boolean', ], + 'is_archived' => [ + 'boolean', + ], 'client_id' => [ 'nullable', new ExistsEloquent(Client::class, null, function (Builder $builder): Builder { @@ -66,6 +69,13 @@ class ProjectUpdateRequest extends FormRequest ]; } + public function getIsArchived(): bool + { + assert($this->has('is_archived')); + + return (bool) $this->input('is_archived'); + } + public function getBillableRate(): ?int { $input = $this->input('billable_rate'); diff --git a/app/Http/Resources/V1/Client/ClientResource.php b/app/Http/Resources/V1/Client/ClientResource.php index 7a9b2556..e5265555 100644 --- a/app/Http/Resources/V1/Client/ClientResource.php +++ b/app/Http/Resources/V1/Client/ClientResource.php @@ -25,6 +25,8 @@ class ClientResource extends BaseResource 'id' => $this->resource->id, /** @var string $name Name */ 'name' => $this->resource->name, + /** @var bool $is_archived Whether the client is archived */ + 'is_archived' => $this->resource->is_archived, /** @var string $created_at When the tag was created */ 'created_at' => $this->formatDateTime($this->resource->created_at), /** @var string $updated_at When the tag was last updated */ diff --git a/app/Http/Resources/V1/Project/ProjectResource.php b/app/Http/Resources/V1/Project/ProjectResource.php index 9c7bad4f..7f9069ca 100644 --- a/app/Http/Resources/V1/Project/ProjectResource.php +++ b/app/Http/Resources/V1/Project/ProjectResource.php @@ -29,6 +29,8 @@ class ProjectResource extends BaseResource 'color' => $this->resource->color, /** @var string|null $client_id ID of client */ 'client_id' => $this->resource->client_id, + /** @var bool $is_archived Whether the client is archived */ + 'is_archived' => $this->resource->is_archived, /** @var int|null $billable_rate Billable rate in cents per hour */ 'billable_rate' => $this->resource->billable_rate, /** @var bool $is_billable Project time entries billable default */ diff --git a/app/Models/Client.php b/app/Models/Client.php index cb692e2a..750607fa 100644 --- a/app/Models/Client.php +++ b/app/Models/Client.php @@ -6,6 +6,7 @@ namespace App\Models; use App\Models\Concerns\HasUuids; use Database\Factories\ClientFactory; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -16,6 +17,8 @@ use Illuminate\Support\Carbon; * @property string $id * @property string $name * @property string $organization_id + * @property-read bool $is_archived + * @property Carbon|null $archived_at * @property Carbon|null $created_at * @property Carbon|null $updated_at * @property-read Organization $organization @@ -51,4 +54,14 @@ class Client extends Model { return $this->hasMany(Project::class, 'client_id'); } + + /** + * @return Attribute + */ + protected function isArchived(): Attribute + { + return Attribute::make( + get: fn (mixed $value, array $attributes) => isset($attributes['archived_at']), + ); + } } diff --git a/app/Models/Project.php b/app/Models/Project.php index a4d15e27..5dd75bb0 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -7,11 +7,13 @@ namespace App\Models; use App\Models\Concerns\HasUuids; use Database\Factories\ProjectFactory; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Support\Carbon; /** * @property string $id @@ -21,6 +23,10 @@ use Illuminate\Database\Eloquent\Relations\HasMany; * @property string $client_id * @property int|null $billable_rate * @property bool $is_billable + * @property-read bool $is_archived + * @property Carbon|null $archived_at + * @property Carbon|null $created_at + * @property Carbon|null $updated_at * @property-read Organization $organization * @property-read Client|null $client * @property-read Collection $tasks @@ -105,4 +111,14 @@ class Project extends Model }); }); } + + /** + * @return Attribute + */ + protected function isArchived(): Attribute + { + return Attribute::make( + get: fn (mixed $value, array $attributes) => isset($attributes['archived_at']), + ); + } } diff --git a/database/factories/ClientFactory.php b/database/factories/ClientFactory.php index 605c2e31..4a799a65 100644 --- a/database/factories/ClientFactory.php +++ b/database/factories/ClientFactory.php @@ -22,6 +22,7 @@ class ClientFactory extends Factory { return [ 'name' => $this->faker->company(), + 'archived_at' => null, 'organization_id' => Organization::factory(), ]; } @@ -43,4 +44,13 @@ class ClientFactory extends Factory ]; }); } + + public function archived(): self + { + return $this->state(function (array $attributes): array { + return [ + 'archived_at' => $this->faker->dateTime(), + ]; + }); + } } diff --git a/database/factories/ProjectFactory.php b/database/factories/ProjectFactory.php index 31dcd536..ff4eb278 100644 --- a/database/factories/ProjectFactory.php +++ b/database/factories/ProjectFactory.php @@ -30,6 +30,7 @@ class ProjectFactory extends Factory 'is_billable' => false, 'billable_rate' => null, 'is_public' => false, + 'archived_at' => null, 'client_id' => null, 'organization_id' => Organization::factory(), ]; @@ -45,6 +46,15 @@ class ProjectFactory extends Factory }); } + public function archived(): self + { + return $this->state(function (array $attributes): array { + return [ + 'archived_at' => $this->faker->dateTime(), + ]; + }); + } + public function forOrganization(Organization $organization): self { return $this->state(function (array $attributes) use ($organization): array { diff --git a/database/migrations/2024_06_21_122754_add_is_archived_columns_to_projects_and_clients_table.php b/database/migrations/2024_06_21_122754_add_is_archived_columns_to_projects_and_clients_table.php new file mode 100644 index 00000000..b95923a5 --- /dev/null +++ b/database/migrations/2024_06_21_122754_add_is_archived_columns_to_projects_and_clients_table.php @@ -0,0 +1,36 @@ +dateTime('archived_at')->nullable(); + }); + Schema::table('clients', function (Blueprint $table): void { + $table->dateTime('archived_at')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('projects', function (Blueprint $table): void { + $table->dropColumn('archived_at'); + }); + Schema::table('clients', function (Blueprint $table): void { + $table->dropColumn('archived_at'); + }); + } +}; diff --git a/tests/Unit/Endpoint/Api/V1/ClientEndpointTest.php b/tests/Unit/Endpoint/Api/V1/ClientEndpointTest.php index 89bdb611..faf2af53 100644 --- a/tests/Unit/Endpoint/Api/V1/ClientEndpointTest.php +++ b/tests/Unit/Endpoint/Api/V1/ClientEndpointTest.php @@ -57,6 +57,91 @@ class ClientEndpointTest extends ApiEndpointTestAbstract ); } + public function test_index_endpoint_without_filter_archived_returns_only_non_archived_clients(): void + { + // Arrange + $data = $this->createUserWithPermission([ + 'clients:view', + ]); + $archivedClients = Client::factory()->forOrganization($data->organization)->archived()->createMany(2); + $nonArchivedClients = Client::factory()->forOrganization($data->organization)->createMany(2); + Passport::actingAs($data->user); + + // Act + $response = $this->getJson(route('api.v1.clients.index', [$data->organization->getKey()])); + + // Assert + $response->assertStatus(200); + $response->assertJsonCount(2, 'data'); + $this->assertEqualsCanonicalizing($nonArchivedClients->pluck('id')->toArray(), $response->json('data.*.id')); + } + + public function test_index_endpoint_with_filter_archived_true_returns_only_archived_clients(): void + { + // Arrange + $data = $this->createUserWithPermission([ + 'clients:view', + ]); + $archivedClients = Client::factory()->forOrganization($data->organization)->archived()->createMany(2); + $nonArchivedClients = Client::factory()->forOrganization($data->organization)->createMany(2); + Passport::actingAs($data->user); + + // Act + $response = $this->getJson(route('api.v1.clients.index', [ + $data->organization->getKey(), + 'archived' => 'true', + ])); + + // Assert + $response->assertStatus(200); + $response->assertJsonCount(2, 'data'); + $this->assertEqualsCanonicalizing($archivedClients->pluck('id')->toArray(), $response->json('data.*.id')); + } + + public function test_index_endpoint_with_filter_archived_false_returns_only_non_archived_clients(): void + { + // Arrange + $data = $this->createUserWithPermission([ + 'clients:view', + ]); + $archivedClients = Client::factory()->forOrganization($data->organization)->archived()->createMany(2); + $nonArchivedClients = Client::factory()->forOrganization($data->organization)->createMany(2); + Passport::actingAs($data->user); + + // Act + $response = $this->getJson(route('api.v1.clients.index', [ + $data->organization->getKey(), + 'archived' => 'false', + ])); + + // Assert + $response->assertStatus(200); + $response->assertJsonCount(2, 'data'); + $this->assertEqualsCanonicalizing($nonArchivedClients->pluck('id')->toArray(), $response->json('data.*.id')); + } + + public function test_index_endpoint_with_filter_archived_all_returns_all_clients(): void + { + // Arrange + $data = $this->createUserWithPermission([ + 'clients:view', + ]); + $archivedClients = Client::factory()->forOrganization($data->organization)->archived()->createMany(2); + $nonArchivedClients = Client::factory()->forOrganization($data->organization)->createMany(2); + Passport::actingAs($data->user); + + // Act + $response = $this->getJson(route('api.v1.clients.index', [ + $data->organization->getKey(), + 'archived' => 'all', + ])); + + // Assert + $response->assertStatus(200); + $response->assertJsonCount(4, 'data'); + $this->assertEqualsCanonicalizing($archivedClients->merge($nonArchivedClients)->pluck('id')->toArray(), $response->json('data.*.id')); + } + public function test_store_endpoint_fails_if_user_has_no_permission_to_create_clients(): void { // Arrange @@ -257,6 +342,58 @@ class ClientEndpointTest extends ApiEndpointTestAbstract ]); } + public function test_update_endpoint_can_archive_a_client(): void + { + // Arrange + $data = $this->createUserWithPermission([ + 'clients:update', + ]); + $client = Client::factory()->forOrganization($data->organization)->create(); + $clientFake = Client::factory()->make(); + Passport::actingAs($data->user); + + // Act + $response = $this->putJson(route('api.v1.clients.update', [$data->organization->getKey(), $client->getKey()]), [ + 'name' => $clientFake->name, + 'is_archived' => true, + ]); + + // Assert + $response->assertStatus(200); + $response->assertJson(fn (AssertableJson $json) => $json + ->has('data') + ->where('data.is_archived', true) + ); + $client->refresh(); + $this->assertTrue($client->is_archived); + } + + public function test_update_endpoint_can_unarchive_a_client(): void + { + // Arrange + $data = $this->createUserWithPermission([ + 'clients:update', + ]); + $client = Client::factory()->forOrganization($data->organization)->archived()->create(); + $clientFake = Client::factory()->make(); + Passport::actingAs($data->user); + + // Act + $response = $this->putJson(route('api.v1.clients.update', [$data->organization->getKey(), $client->getKey()]), [ + 'name' => $clientFake->name, + 'is_archived' => false, + ]); + + // Assert + $response->assertStatus(200); + $response->assertJson(fn (AssertableJson $json) => $json + ->has('data') + ->where('data.is_archived', false) + ); + $client->refresh(); + $this->assertFalse($client->is_archived); + } + public function test_destroy_endpoint_fails_if_user_has_no_permission_to_delete_clients(): void { // Arrange diff --git a/tests/Unit/Endpoint/Api/V1/ProjectEndpointTest.php b/tests/Unit/Endpoint/Api/V1/ProjectEndpointTest.php index db871692..f9a5898b 100644 --- a/tests/Unit/Endpoint/Api/V1/ProjectEndpointTest.php +++ b/tests/Unit/Endpoint/Api/V1/ProjectEndpointTest.php @@ -12,6 +12,7 @@ use App\Models\ProjectMember; use App\Models\Task; use App\Models\TimeEntry; use App\Service\BillableRateService; +use Illuminate\Testing\Fluent\AssertableJson; use Laravel\Passport\Passport; use Mockery\MockInterface; use PHPUnit\Framework\Attributes\UsesClass; @@ -52,6 +53,93 @@ class ProjectEndpointTest extends ApiEndpointTestAbstract $response->assertJsonCount(4, 'data'); } + public function test_index_endpoint_without_filter_archived_returns_only_non_archived_projects(): void + { + // Arrange + $data = $this->createUserWithPermission([ + 'projects:view', + 'projects:view:all', + ]); + $archivedProjects = Project::factory()->forOrganization($data->organization)->archived()->createMany(2); + $nonArchivedProjects = Project::factory()->forOrganization($data->organization)->createMany(2); + Passport::actingAs($data->user); + + // Act + $response = $this->getJson(route('api.v1.projects.index', [$data->organization->getKey()])); + + // Assert + $response->assertStatus(200); + $response->assertJsonCount(2, 'data'); + $this->assertEqualsCanonicalizing($nonArchivedProjects->pluck('id')->toArray(), $response->json('data.*.id')); + } + + public function test_index_endpoint_with_filter_archived_true_returns_only_archived_projects(): void + { + // Arrange + $data = $this->createUserWithPermission([ + 'projects:view', + 'projects:view:all', + ]); + $archivedProjects = Project::factory()->forOrganization($data->organization)->archived()->createMany(2); + $nonArchivedProjects = Project::factory()->forOrganization($data->organization)->createMany(2); + Passport::actingAs($data->user); + + // Act + $response = $this->getJson(route('api.v1.projects.index', [ + $data->organization->getKey(), + 'archived' => 'true', + ])); + + // Assert + $response->assertStatus(200); + $response->assertJsonCount(2, 'data'); + $this->assertEqualsCanonicalizing($archivedProjects->pluck('id')->toArray(), $response->json('data.*.id')); + } + + public function test_index_endpoint_with_filter_archived_false_returns_only_non_archived_projects(): void + { + // Arrange + $data = $this->createUserWithPermission([ + 'projects:view', + 'projects:view:all', + ]); + $archivedProjects = Project::factory()->forOrganization($data->organization)->archived()->createMany(2); + $nonArchivedProjects = Project::factory()->forOrganization($data->organization)->createMany(2); + Passport::actingAs($data->user); + + // Act + $response = $this->getJson(route('api.v1.projects.index', [ + $data->organization->getKey(), + 'archived' => 'false', + ])); + // Assert + $response->assertStatus(200); + $response->assertJsonCount(2, 'data'); + $this->assertEqualsCanonicalizing($nonArchivedProjects->pluck('id')->toArray(), $response->json('data.*.id')); + } + + public function test_index_endpoint_with_filter_archived_all_returns_all_projects(): void + { + // Arrange + $data = $this->createUserWithPermission([ + 'projects:view', + 'projects:view:all', + ]); + $archivedProjects = Project::factory()->forOrganization($data->organization)->archived()->createMany(2); + $nonArchivedProjects = Project::factory()->forOrganization($data->organization)->createMany(2); + Passport::actingAs($data->user); + + // Act + $response = $this->getJson(route('api.v1.projects.index', [ + $data->organization->getKey(), + 'archived' => 'all', + ])); + + // Assert + $response->assertStatus(200); + $response->assertJsonCount(4, 'data'); + } + public function test_index_endpoint_returns_list_of_projects_of_organization_which_are_public_or_where_user_is_member_for_user_with_restricted_permission(): void { // Arrange @@ -406,11 +494,17 @@ class ProjectEndpointTest extends ApiEndpointTestAbstract // Assert $response->assertStatus(200); - $this->assertDatabaseHas(Project::class, [ - 'name' => $projectFake->name, - 'color' => $projectFake->color, - 'client_id' => $client->getKey(), - ]); + $project->refresh(); + $response->assertJson(fn (AssertableJson $json) => $json + ->has('data') + ->where('data.name', $projectFake->name) + ->where('data.color', $projectFake->color) + ->where('data.client_id', $client->getKey()) + ); + $this->assertSame($projectFake->name, $project->name); + $this->assertSame($projectFake->color, $project->color); + $this->assertSame($client->getKey(), $project->client_id); + $this->assertFalse($project->is_archived); } public function test_update_endpoint_can_update_projects_billable_rate(): void @@ -474,6 +568,62 @@ class ProjectEndpointTest extends ApiEndpointTestAbstract ]); } + public function test_update_endpoint_can_archive_a_project(): void + { + // Arrange + $data = $this->createUserWithPermission([ + 'projects:update', + ]); + $project = Project::factory()->forOrganization($data->organization)->create(); + $projectFake = Project::factory()->make(); + Passport::actingAs($data->user); + + // Act + $response = $this->putJson(route('api.v1.projects.update', [$data->organization->getKey(), $project->getKey()]), [ + 'name' => $projectFake->name, + 'color' => $projectFake->color, + 'is_billable' => $projectFake->is_billable, + 'is_archived' => true, + ]); + + // Assert + $response->assertStatus(200); + $response->assertJson(fn (AssertableJson $json) => $json + ->has('data') + ->where('data.is_archived', true) + ); + $project->refresh(); + $this->assertTrue($project->is_archived); + } + + public function test_update_endpoint_can_unarchive_a_project(): void + { + // Arrange + $data = $this->createUserWithPermission([ + 'projects:update', + ]); + $project = Project::factory()->forOrganization($data->organization)->archived()->create(); + $projectFake = Project::factory()->make(); + Passport::actingAs($data->user); + + // Act + $response = $this->putJson(route('api.v1.projects.update', [$data->organization->getKey(), $project->getKey()]), [ + 'name' => $projectFake->name, + 'color' => $projectFake->color, + 'is_billable' => $projectFake->is_billable, + 'is_archived' => false, + ]); + + // Assert + $response->assertStatus(200); + $response->assertJson(fn (AssertableJson $json) => $json + ->has('data') + ->where('data.is_archived', false) + ); + $project->refresh(); + $this->assertFalse($project->is_archived); + } + public function test_destroy_endpoint_fails_if_user_is_not_part_of_project_organization(): void { // Arrange diff --git a/tests/Unit/Model/ClientModelTest.php b/tests/Unit/Model/ClientModelTest.php index b193515c..b4b85f7e 100644 --- a/tests/Unit/Model/ClientModelTest.php +++ b/tests/Unit/Model/ClientModelTest.php @@ -46,4 +46,30 @@ class ClientModelTest extends ModelTestAbstract $this->assertCount(4, $projectsRel); $this->assertTrue($projectsRel->first()->is($projects->first())); } + + public function test_accessor_is_archived_is_true_if_archived_at_is_not_null(): void + { + // Arrange + $client = Client::factory()->archived()->create(); + + // Act + $client->refresh(); + $isArchived = $client->is_archived; + + // Assert + $this->assertTrue($isArchived); + } + + public function test_accessor_is_archived_is_false_if_archived_at_is_null(): void + { + // Arrange + $client = Client::factory()->create(); + + // Act + $client->refresh(); + $isArchived = $client->is_archived; + + // Assert + $this->assertFalse($isArchived); + } } diff --git a/tests/Unit/Model/ProjectModelTest.php b/tests/Unit/Model/ProjectModelTest.php index 33aec647..5bda407d 100644 --- a/tests/Unit/Model/ProjectModelTest.php +++ b/tests/Unit/Model/ProjectModelTest.php @@ -116,4 +116,30 @@ class ProjectModelTest extends ModelTestAbstract $projectPrivateButMember->getKey(), ], $allProjects); } + + public function test_accessor_is_archived_is_true_if_archived_at_is_not_null(): void + { + // Arrange + $project = Project::factory()->archived()->create(); + + // Act + $project->refresh(); + $isArchived = $project->is_archived; + + // Assert + $this->assertTrue($isArchived); + } + + public function test_accessor_is_archived_is_false_if_archived_at_is_null(): void + { + // Arrange + $project = Project::factory()->create(); + + // Act + $project->refresh(); + $isArchived = $project->is_archived; + + // Assert + $this->assertFalse($isArchived); + } }