mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-08 00:02:15 +01:00
Added ability to archive projects and clients, fixes ST-37
This commit is contained in:
committed by
Gregor Vostrak
parent
f21a2d4bdd
commit
a69d1cb4c4
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
35
app/Http/Requests/V1/Client/ClientIndexRequest.php
Normal file
35
app/Http/Requests/V1/Client/ClientIndexRequest.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\V1\Client;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class ClientIndexRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, array<string|ValidationRule>>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'page' => [
|
||||
'integer',
|
||||
'min:1',
|
||||
],
|
||||
'archived' => [
|
||||
'string',
|
||||
'in:true,false,all',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function getFilterArchived(): string
|
||||
{
|
||||
return $this->input('archived', 'false');
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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<bool, never>
|
||||
*/
|
||||
protected function isArchived(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn (mixed $value, array $attributes) => isset($attributes['archived_at']),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<int, Task> $tasks
|
||||
@@ -105,4 +111,14 @@ class Project extends Model
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Attribute<bool, never>
|
||||
*/
|
||||
protected function isArchived(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn (mixed $value, array $attributes) => isset($attributes['archived_at']),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('projects', function (Blueprint $table): void {
|
||||
$table->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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user