From f01a4111b6adf4b461fd8211e2b721008b3182c2 Mon Sep 17 00:00:00 2001 From: Constantin Graf Date: Thu, 8 Oct 2026 12:57:56 +0200 Subject: [PATCH] Fix Scramble API docs warnings Remove redundant @var annotations from API resources and defer the user access in the email unique rule of UserUpdateRequest, so rules() can be evaluated by Scramble without route model binding. Add a test that fails on any Scramble diagnostic and tests for the email unique rule. --- .../Requests/V1/User/UserUpdateRequest.php | 5 ++- .../V1/ApiToken/ApiTokenResource.php | 8 ++-- .../ApiTokenWithAccessTokenResource.php | 8 ++-- .../Resources/V1/Client/ClientResource.php | 10 ++--- .../V1/Invitation/InvitationResource.php | 6 +-- .../Resources/V1/Member/MemberResource.php | 14 +++---- .../V1/Member/PersonalMembershipResource.php | 10 ++--- .../V1/Organization/OrganizationResource.php | 18 ++++----- .../Resources/V1/Project/ProjectResource.php | 18 ++++----- .../ProjectMember/ProjectMemberResource.php | 8 ++-- .../V1/Report/DetailedReportResource.php | 34 ++++++++--------- .../Report/DetailedWithDataReportResource.php | 16 ++++---- .../Resources/V1/Report/ReportResource.php | 12 +++--- app/Http/Resources/V1/Tag/TagResource.php | 8 ++-- app/Http/Resources/V1/Task/TaskResource.php | 16 ++++---- .../V1/TimeEntry/TimeEntryResource.php | 18 ++++----- app/Http/Resources/V1/User/UserResource.php | 14 +++---- tests/Unit/ApiDocs/ApiDocsExportTest.php | 22 +++++++++++ .../Unit/Endpoint/Api/V1/UserEndpointTest.php | 38 +++++++++++++++++++ 19 files changed, 172 insertions(+), 111 deletions(-) diff --git a/app/Http/Requests/V1/User/UserUpdateRequest.php b/app/Http/Requests/V1/User/UserUpdateRequest.php index 7c911396..e167b8ea 100644 --- a/app/Http/Requests/V1/User/UserUpdateRequest.php +++ b/app/Http/Requests/V1/User/UserUpdateRequest.php @@ -43,9 +43,10 @@ class UserUpdateRequest extends BaseFormRequest 'email' => [ 'email:rfc,strict', 'max:255', - UniqueEloquent::make(User::class, 'email')->ignore($this->user->id)->query(function (Builder $query) { + UniqueEloquent::make(User::class, 'email')->query(function (Builder $query) { /** @var Builder $query */ - return $query->where('is_placeholder', '=', false); + return $query->where('is_placeholder', '=', false) + ->whereKeyNot($this->user->getKey()); }), ], 'photo' => [ diff --git a/app/Http/Resources/V1/ApiToken/ApiTokenResource.php b/app/Http/Resources/V1/ApiToken/ApiTokenResource.php index 7a55b4cd..28f1f245 100644 --- a/app/Http/Resources/V1/ApiToken/ApiTokenResource.php +++ b/app/Http/Resources/V1/ApiToken/ApiTokenResource.php @@ -21,15 +21,15 @@ class ApiTokenResource extends BaseResource public function toArray(Request $request): array { return [ - /** @var string $id ID of the API token, this ID is NOT a UUID */ + /** ID of the API token, this ID is NOT a UUID */ 'id' => $this->resource->id, /** @var string $name Name of the API token */ 'name' => $this->resource->name, - /** @var bool $revoked Whether the API token is revoked */ + /** Whether the API token is revoked */ 'revoked' => $this->resource->revoked, - /** @var array $scopes List of scopes that the API token has */ + /** List of scopes that the API token has */ 'scopes' => $this->resource->scopes, - /** @var string $created_at When the API token was created (ISO 8601 format, UTC timezone, example: 2024-02-26T17:17:17Z) */ + /** When the API token was created (ISO 8601 format, UTC timezone, example: 2024-02-26T17:17:17Z) */ 'created_at' => $this->formatDateTime($this->resource->created_at), /** @var string|null $expires_at At what time the API token expires (ISO 8601 format, UTC timezone, example: 2024-02-26T17:17:17Z) */ 'expires_at' => $this->formatDateTime($this->resource->expires_at), diff --git a/app/Http/Resources/V1/ApiToken/ApiTokenWithAccessTokenResource.php b/app/Http/Resources/V1/ApiToken/ApiTokenWithAccessTokenResource.php index 8da5150f..655eff45 100644 --- a/app/Http/Resources/V1/ApiToken/ApiTokenWithAccessTokenResource.php +++ b/app/Http/Resources/V1/ApiToken/ApiTokenWithAccessTokenResource.php @@ -29,15 +29,15 @@ class ApiTokenWithAccessTokenResource extends BaseResource public function toArray(Request $request): array { return [ - /** @var string $id ID of the API token, this ID is NOT a UUID */ + /** ID of the API token, this ID is NOT a UUID */ 'id' => $this->resource->id, /** @var string $name Name of the API token */ 'name' => $this->resource->name, - /** @var bool $revoked Whether the API token is revoked */ + /** Whether the API token is revoked */ 'revoked' => $this->resource->revoked, - /** @var array $scopes List of scopes that the API token has */ + /** List of scopes that the API token has */ 'scopes' => $this->resource->scopes, - /** @var string $created_at When the API token was created (ISO 8601 format, UTC timezone, example: 2024-02-26T17:17:17Z) */ + /** When the API token was created (ISO 8601 format, UTC timezone, example: 2024-02-26T17:17:17Z) */ 'created_at' => $this->formatDateTime($this->resource->created_at), /** @var string|null $expires_at At what time the API token expires (ISO 8601 format, UTC timezone, example: 2024-02-26T17:17:17Z) */ 'expires_at' => $this->formatDateTime($this->resource->expires_at), diff --git a/app/Http/Resources/V1/Client/ClientResource.php b/app/Http/Resources/V1/Client/ClientResource.php index e5265555..e005b8d6 100644 --- a/app/Http/Resources/V1/Client/ClientResource.php +++ b/app/Http/Resources/V1/Client/ClientResource.php @@ -21,15 +21,15 @@ class ClientResource extends BaseResource public function toArray(Request $request): array { return [ - /** @var string $id ID */ + /** ID */ 'id' => $this->resource->id, - /** @var string $name Name */ + /** Name */ 'name' => $this->resource->name, - /** @var bool $is_archived Whether the client is archived */ + /** Whether the client is archived */ 'is_archived' => $this->resource->is_archived, - /** @var string $created_at When the tag was created */ + /** When the tag was created */ 'created_at' => $this->formatDateTime($this->resource->created_at), - /** @var string $updated_at When the tag was last updated */ + /** When the tag was last updated */ 'updated_at' => $this->formatDateTime($this->resource->updated_at), ]; } diff --git a/app/Http/Resources/V1/Invitation/InvitationResource.php b/app/Http/Resources/V1/Invitation/InvitationResource.php index 0c36a48c..d82797f9 100644 --- a/app/Http/Resources/V1/Invitation/InvitationResource.php +++ b/app/Http/Resources/V1/Invitation/InvitationResource.php @@ -21,11 +21,11 @@ class InvitationResource extends BaseResource public function toArray(Request $request): array { return [ - /** @var string $id ID of the invitation */ + /** ID of the invitation */ 'id' => $this->resource->id, - /** @var string $email Email */ + /** Email */ 'email' => $this->resource->email, - /** @var string $role Role */ + /** Role */ 'role' => $this->resource->role, ]; } diff --git a/app/Http/Resources/V1/Member/MemberResource.php b/app/Http/Resources/V1/Member/MemberResource.php index d5b8b087..12a89ebc 100644 --- a/app/Http/Resources/V1/Member/MemberResource.php +++ b/app/Http/Resources/V1/Member/MemberResource.php @@ -22,19 +22,19 @@ class MemberResource extends BaseResource public function toArray(Request $request): array { return [ - /** @var string $id ID of membership */ + /** ID of membership */ 'id' => $this->resource->id, - /** @var string $id ID of user */ + /** ID of user */ 'user_id' => $this->resource->user->id, - /** @var string $name Name */ + /** Name */ 'name' => $this->resource->user->name, - /** @var string $email Email */ + /** Email */ 'email' => $this->resource->user->email, - /** @var string $role Role */ + /** Role */ 'role' => $this->resource->role, - /** @var bool $is_placeholder Placeholder user for imports, user might not really exist and does not know about this placeholder membership */ + /** Placeholder user for imports, user might not really exist and does not know about this placeholder membership */ 'is_placeholder' => $this->resource->user->is_placeholder, - /** @var int|null $billable_rate Billable rate in cents per hour */ + /** Billable rate in cents per hour */ 'billable_rate' => $this->resource->billable_rate, ]; } diff --git a/app/Http/Resources/V1/Member/PersonalMembershipResource.php b/app/Http/Resources/V1/Member/PersonalMembershipResource.php index 917d7c69..6e01a967 100644 --- a/app/Http/Resources/V1/Member/PersonalMembershipResource.php +++ b/app/Http/Resources/V1/Member/PersonalMembershipResource.php @@ -21,17 +21,17 @@ class PersonalMembershipResource extends BaseResource public function toArray(Request $request): array { return [ - /** @var string $id ID of membership */ + /** ID of membership */ 'id' => $this->resource->id, 'organization' => [ - /** @var string $id ID of organization */ + /** ID of organization */ 'id' => $this->resource->organization->id, - /** @var string $name Name of organization */ + /** Name of organization */ 'name' => $this->resource->organization->name, - /** @var string $currency Currency code (ISO 4217) of organization */ + /** Currency code (ISO 4217) of organization */ 'currency' => $this->resource->organization->currency, ], - /** @var string $role Role */ + /** Role */ 'role' => $this->resource->role, ]; } diff --git a/app/Http/Resources/V1/Organization/OrganizationResource.php b/app/Http/Resources/V1/Organization/OrganizationResource.php index bb5075b6..860f9f49 100644 --- a/app/Http/Resources/V1/Organization/OrganizationResource.php +++ b/app/Http/Resources/V1/Organization/OrganizationResource.php @@ -43,23 +43,23 @@ class OrganizationResource extends BaseResource $currencyService = app(CurrencyService::class); return [ - /** @var string $id ID */ + /** ID */ 'id' => $this->resource->id, - /** @var string $name Name */ + /** Name */ 'name' => $this->resource->name, - /** @var bool $color Personal organizations automatically created after registration */ + /** Personal organizations automatically created after registration */ 'is_personal' => $this->resource->personal_team, - /** @var int|null $billable_rate Billable rate in cents per hour */ + /** Billable rate in cents per hour */ 'billable_rate' => $this->showBillableRate ? $this->resource->billable_rate : null, - /** @var bool $employees_can_see_billable_rates Can members of the organization with role "employee" see the billable rates */ + /** Can members of the organization with role "employee" see the billable rates */ 'employees_can_see_billable_rates' => $this->resource->employees_can_see_billable_rates, - /** @var bool $employees_can_manage_tasks Can members of the organization with role "employee" manage tasks in public projects and projects they are assigned to */ + /** Can members of the organization with role "employee" manage tasks in public projects and projects they are assigned to */ 'employees_can_manage_tasks' => $this->resource->employees_can_manage_tasks, - /** @var bool $prevent_overlapping_time_entries Prevent creating overlapping time entries (only new entries) */ + /** Prevent creating overlapping time entries (only new entries) */ 'prevent_overlapping_time_entries' => $this->resource->prevent_overlapping_time_entries, - /** @var bool $breaks_enabled Whether members of the organization can track breaks */ + /** Whether members of the organization can track breaks */ 'breaks_enabled' => $this->resource->breaks_enabled, - /** @var string $currency Currency code (ISO 4217) */ + /** Currency code (ISO 4217) */ 'currency' => $this->resource->currency, /** @var string $currency_symbol Currency symbol */ 'currency_symbol' => $currencyService->getCurrencySymbol($this->resource->currency), diff --git a/app/Http/Resources/V1/Project/ProjectResource.php b/app/Http/Resources/V1/Project/ProjectResource.php index 91cf27bb..ba7bd1b0 100644 --- a/app/Http/Resources/V1/Project/ProjectResource.php +++ b/app/Http/Resources/V1/Project/ProjectResource.php @@ -30,25 +30,25 @@ class ProjectResource extends BaseResource public function toArray(Request $request): array { return [ - /** @var string $id ID of project */ + /** ID of project */ 'id' => $this->resource->id, - /** @var string $name Name of project */ + /** Name of project */ 'name' => $this->resource->name, - /** @var string $color Color of project */ + /** Color of project */ '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 */ + /** Whether the client is archived */ 'is_archived' => $this->resource->is_archived, - /** @var int|null $billable_rate Billable rate in cents per hour */ + /** Billable rate in cents per hour */ 'billable_rate' => $this->showBillableRate ? $this->resource->billable_rate : null, - /** @var bool $is_billable Project time entries billable default */ + /** Project time entries billable default */ 'is_billable' => $this->resource->is_billable, - /** @var int|null $estimated_time Estimated time in seconds */ + /** Estimated time in seconds */ 'estimated_time' => $this->resource->estimated_time, - /** @var int $spent_time Spent time on this project in seconds (sum of the duration of all associated time entries, excl. still running time entries) */ + /** Spent time on this project in seconds (sum of the duration of all associated time entries, excl. still running time entries) */ 'spent_time' => $this->resource->spent_time, - /** @var bool $is_public Whether the project is public */ + /** Whether the project is public */ 'is_public' => $this->resource->is_public, ]; } diff --git a/app/Http/Resources/V1/ProjectMember/ProjectMemberResource.php b/app/Http/Resources/V1/ProjectMember/ProjectMemberResource.php index 8e986b25..419002f7 100644 --- a/app/Http/Resources/V1/ProjectMember/ProjectMemberResource.php +++ b/app/Http/Resources/V1/ProjectMember/ProjectMemberResource.php @@ -21,13 +21,13 @@ class ProjectMemberResource extends BaseResource public function toArray(Request $request): array { return [ - /** @var string $id ID of project member */ + /** ID of project member */ 'id' => $this->resource->id, - /** @var int|null $billable_rate Billable rate in cents per hour */ + /** Billable rate in cents per hour */ 'billable_rate' => $this->resource->billable_rate, - /** @var string $member_id ID of the organization member */ + /** ID of the organization member */ 'member_id' => $this->resource->member_id, - /** @var string $project_id ID of the project */ + /** ID of the project */ 'project_id' => $this->resource->project_id, ]; } diff --git a/app/Http/Resources/V1/Report/DetailedReportResource.php b/app/Http/Resources/V1/Report/DetailedReportResource.php index fd029e34..bb93f7a5 100644 --- a/app/Http/Resources/V1/Report/DetailedReportResource.php +++ b/app/Http/Resources/V1/Report/DetailedReportResource.php @@ -21,36 +21,36 @@ class DetailedReportResource extends BaseResource public function toArray(Request $request): array { return [ - /** @var string $id ID of the report */ + /** ID of the report */ 'id' => $this->resource->id, - /** @var string $name Name */ + /** Name */ 'name' => $this->resource->name, - /** @var string|null $email Description */ + /** Description */ 'description' => $this->resource->description, - /** @var bool $is_public Whether the report can be accessed via an external link */ + /** Whether the report can be accessed via an external link */ 'is_public' => $this->resource->is_public, /** @var string|null $public_until Date until the report is public */ 'public_until' => $this->formatDateTime($this->resource->public_until), /** @var string|null $shareable_link Get link to access the report externally, not set if the report is private */ 'shareable_link' => $this->resource->getShareableLink(), 'properties' => [ - /** @var string $group Type of first grouping */ + /** Type of first grouping */ 'group' => $this->resource->properties->group->value, - /** @var string $sub_group Type of second grouping */ + /** Type of second grouping */ 'sub_group' => $this->resource->properties->subGroup->value, - /** @var string $history_group Type of grouping of the historic aggregation (time chart) */ + /** Type of grouping of the historic aggregation (time chart) */ 'history_group' => $this->resource->properties->historyGroup->value, - /** @var string $start Start date of the report */ + /** Start date of the report */ 'start' => $this->formatDateTime($this->resource->properties->start), - /** @var string $end End date of the report */ + /** End date of the report */ 'end' => $this->formatDateTime($this->resource->properties->end), - /** @var bool|null $active Whether the report is active */ + /** Whether the report is active */ 'active' => $this->resource->properties->active, /** @var array|null $member_ids Filter by multiple member IDs, member IDs are OR combined */ 'member_ids' => $this->resource->properties->memberIds?->toArray(), - /** @var bool|null $billable Filter by billable status */ + /** Filter by billable status */ 'billable' => $this->resource->properties->billable, - /** @var string|null $time_entry_type Filter by time entry type */ + /** Filter by time entry type */ 'time_entry_type' => $this->resource->properties->timeEntryType?->value, /** @var array|null $client_ids Filter by client IDs, client IDs are OR combined */ 'client_ids' => $this->resource->properties->clientIds?->toArray(), @@ -58,18 +58,18 @@ class DetailedReportResource extends BaseResource 'project_ids' => $this->resource->properties->projectIds?->toArray(), /** @var array|null $tags_ids Filter by tag IDs, tag IDs are OR combined */ 'tag_ids' => $this->resource->properties->tagIds?->toArray(), - /** @var string|null $tag_match_type Tag match type */ + /** Tag match type */ 'tag_match_type' => $this->resource->properties->tagMatchType?->value, /** @var array|null $task_ids Filter by task IDs, task IDs are OR combined */ 'task_ids' => $this->resource->properties->taskIds?->toArray(), - /** @var string|null $rounding_type Rounding type for time entries */ + /** Rounding type for time entries */ 'rounding_type' => $this->resource->properties->roundingType?->value, - /** @var int|null $rounding_minutes Rounding minutes for time entries */ + /** Rounding minutes for time entries */ 'rounding_minutes' => $this->resource->properties->roundingMinutes, ], - /** @var string $created_at Date when the report was created */ + /** Date when the report was created */ 'created_at' => $this->formatDateTime($this->resource->created_at), - /** @var string $updated_at Date when the report was last updated */ + /** Date when the report was last updated */ 'updated_at' => $this->formatDateTime($this->resource->updated_at), ]; } diff --git a/app/Http/Resources/V1/Report/DetailedWithDataReportResource.php b/app/Http/Resources/V1/Report/DetailedWithDataReportResource.php index 3129f45f..f6f13f44 100644 --- a/app/Http/Resources/V1/Report/DetailedWithDataReportResource.php +++ b/app/Http/Resources/V1/Report/DetailedWithDataReportResource.php @@ -73,13 +73,13 @@ class DetailedWithDataReportResource extends BaseResource $currencyService = app(CurrencyService::class); return [ - /** @var string $name Name */ + /** Name */ 'name' => $this->resource->name, - /** @var string|null $email Description */ + /** Description */ 'description' => $this->resource->description, /** @var string|null $public_until Date until the report is public */ 'public_until' => $this->formatDateTime($this->resource->public_until), - /** @var string $currency Currency code (ISO 4217) */ + /** Currency code (ISO 4217) */ 'currency' => $this->resource->organization->currency, /** @var NumberFormat $number_format Number format */ 'number_format' => $this->resource->organization->number_format->value, @@ -94,15 +94,15 @@ class DetailedWithDataReportResource extends BaseResource /** @var TimeFormat $time_format Time format */ 'time_format' => $this->resource->organization->time_format->value, 'properties' => [ - /** @var string $group Type of first grouping */ + /** Type of first grouping */ 'group' => $this->resource->properties->group->value, - /** @var string $sub_group Type of second grouping */ + /** Type of second grouping */ 'sub_group' => $this->resource->properties->subGroup->value, - /** @var string $history_group Type of grouping of the historic aggregation (time chart) */ + /** Type of grouping of the historic aggregation (time chart) */ 'history_group' => $this->resource->properties->historyGroup->value, - /** @var string $start Start date of the report */ + /** Start date of the report */ 'start' => $this->formatDateTime($this->resource->properties->start), - /** @var string $end End date of the report */ + /** End date of the report */ 'end' => $this->formatDateTime($this->resource->properties->end), ], /** @var array{ diff --git a/app/Http/Resources/V1/Report/ReportResource.php b/app/Http/Resources/V1/Report/ReportResource.php index 23872ce8..16e8b34e 100644 --- a/app/Http/Resources/V1/Report/ReportResource.php +++ b/app/Http/Resources/V1/Report/ReportResource.php @@ -21,21 +21,21 @@ class ReportResource extends BaseResource public function toArray(Request $request): array { return [ - /** @var string $id ID of the report */ + /** ID of the report */ 'id' => $this->resource->id, - /** @var string $name Name */ + /** Name */ 'name' => $this->resource->name, - /** @var string|null $email Description */ + /** Description */ 'description' => $this->resource->description, - /** @var bool $is_public Whether the report can be accessed via an external link */ + /** Whether the report can be accessed via an external link */ 'is_public' => $this->resource->is_public, /** @var string|null $public_until Date until the report is public */ 'public_until' => $this->formatDateTime($this->resource->public_until), /** @var string|null $shareable_link Get link to access the report externally, not set if the report is private */ 'shareable_link' => $this->resource->getShareableLink(), - /** @var string $created_at Date when the report was created */ + /** Date when the report was created */ 'created_at' => $this->formatDateTime($this->resource->created_at), - /** @var string $updated_at Date when the report was last updated */ + /** Date when the report was last updated */ 'updated_at' => $this->formatDateTime($this->resource->updated_at), ]; } diff --git a/app/Http/Resources/V1/Tag/TagResource.php b/app/Http/Resources/V1/Tag/TagResource.php index ce6558a3..e5ca0607 100644 --- a/app/Http/Resources/V1/Tag/TagResource.php +++ b/app/Http/Resources/V1/Tag/TagResource.php @@ -21,13 +21,13 @@ class TagResource extends BaseResource public function toArray(Request $request): array { return [ - /** @var string $id ID */ + /** ID */ 'id' => $this->resource->id, - /** @var string $name Name */ + /** Name */ 'name' => $this->resource->name, - /** @var string $created_at When the tag was created */ + /** When the tag was created */ 'created_at' => $this->formatDateTime($this->resource->created_at), - /** @var string $updated_at When the tag was last updated */ + /** When the tag was last updated */ 'updated_at' => $this->formatDateTime($this->resource->updated_at), ]; } diff --git a/app/Http/Resources/V1/Task/TaskResource.php b/app/Http/Resources/V1/Task/TaskResource.php index 67411b00..fcb73091 100644 --- a/app/Http/Resources/V1/Task/TaskResource.php +++ b/app/Http/Resources/V1/Task/TaskResource.php @@ -22,21 +22,21 @@ class TaskResource extends BaseResource public function toArray(Request $request): array { return [ - /** @var string $id ID */ + /** ID */ 'id' => $this->resource->id, - /** @var string $name Name */ + /** Name */ 'name' => $this->resource->name, - /** @var bool $is_done Whether the task is done */ + /** Whether the task is done */ 'is_done' => $this->resource->is_done, - /** @var string $project_id ID of the project */ + /** ID of the project */ 'project_id' => $this->resource->project_id, - /** @var int|null $estimated_time Estimated time in seconds */ + /** Estimated time in seconds */ 'estimated_time' => $this->resource->estimated_time, - /** @var int $spent_time Spent time on this task in seconds (sum of the duration of all associated time entries, excl. still running time entries) */ + /** Spent time on this task in seconds (sum of the duration of all associated time entries, excl. still running time entries) */ 'spent_time' => $this->resource->spent_time, - /** @var string $created_at When the tag was created */ + /** When the tag was created */ 'created_at' => $this->formatDateTime($this->resource->created_at), - /** @var string $updated_at When the tag was last updated */ + /** When the tag was last updated */ 'updated_at' => $this->formatDateTime($this->resource->updated_at), ]; } diff --git a/app/Http/Resources/V1/TimeEntry/TimeEntryResource.php b/app/Http/Resources/V1/TimeEntry/TimeEntryResource.php index 2702751b..d9d9d627 100644 --- a/app/Http/Resources/V1/TimeEntry/TimeEntryResource.php +++ b/app/Http/Resources/V1/TimeEntry/TimeEntryResource.php @@ -21,10 +21,10 @@ class TimeEntryResource extends BaseResource public function toArray(Request $request): array { return [ - /** @var string $id ID of time entry */ + /** ID of time entry */ 'id' => $this->resource->id, /** - * @var string $start Start of time entry (ISO 8601 format, UTC timezone, example: 2024-02-26T17:17:17Z) + * Start of time entry (ISO 8601 format, UTC timezone, example: 2024-02-26T17:17:17Z) */ 'start' => $this->formatDateTime($this->resource->start), /** @@ -35,19 +35,19 @@ class TimeEntryResource extends BaseResource 'duration' => (int) $this->resource->getDuration()?->totalSeconds, /** @var string|null $description Description of time entry */ 'description' => $this->resource->description, - /** @var string|null $task_id ID of task */ + /** ID of task */ 'task_id' => $this->resource->task_id, - /** @var string|null $project_id ID of project */ + /** ID of project */ 'project_id' => $this->resource->project_id, - /** @var string $organization_id ID of organization */ + /** ID of organization */ 'organization_id' => $this->resource->organization_id, - /** @var string $user_id ID of user */ + /** ID of user */ 'user_id' => $this->resource->user_id, - /** @var array $tags List of tag IDs */ + /** List of tag IDs */ 'tags' => $this->resource->tags ?? [], - /** @var bool $billable Whether time entry is billable */ + /** Whether time entry is billable */ 'billable' => $this->resource->billable, - /** @var string $type Type of the time entry (`work` time or a `break`) */ + /** Type of the time entry (`work` time or a `break`) */ 'type' => $this->resource->type->value, ]; } diff --git a/app/Http/Resources/V1/User/UserResource.php b/app/Http/Resources/V1/User/UserResource.php index 980c9335..fb94ed17 100644 --- a/app/Http/Resources/V1/User/UserResource.php +++ b/app/Http/Resources/V1/User/UserResource.php @@ -22,21 +22,21 @@ class UserResource extends BaseResource public function toArray(Request $request): array { return [ - /** @var string $id ID of user */ + /** ID of user */ 'id' => $this->resource->id, - /** @var string $name Name of user */ + /** Name of user */ 'name' => $this->resource->name, - /** @var string $email Email of user */ + /** Email of user */ 'email' => $this->resource->email, - /** @var string|null $pending_email Email address awaiting verification (set when the user has requested an email change but not yet verified the new address) */ + /** Email address awaiting verification (set when the user has requested an email change but not yet verified the new address) */ 'pending_email' => $this->resource->pending_email, - /** @var string $profile_photo_url Profile photo URL */ + /** Profile photo URL */ 'profile_photo_url' => $this->resource->profile_photo_url, - /** @var string $timezone Timezone (f.e. Europe/Berlin or America/New_York) */ + /** Timezone (f.e. Europe/Berlin or America/New_York) */ 'timezone' => $this->resource->timezone, /** @var Weekday $week_start Starting day of the week */ 'week_start' => $this->resource->week_start->value, - /** @var bool $send_time_entry_still_running_email Whether to email the user when a time entry has been running for more than 8 hours */ + /** Whether to email the user when a time entry has been running for more than 8 hours */ 'send_time_entry_still_running_email' => $this->resource->send_time_entry_still_running_email, ]; } diff --git a/tests/Unit/ApiDocs/ApiDocsExportTest.php b/tests/Unit/ApiDocs/ApiDocsExportTest.php index 351b6879..c4aab21b 100644 --- a/tests/Unit/ApiDocs/ApiDocsExportTest.php +++ b/tests/Unit/ApiDocs/ApiDocsExportTest.php @@ -5,7 +5,10 @@ declare(strict_types=1); namespace Tests\Unit\ApiDocs; use App\Http\Controllers\Api\FallbackController; +use Dedoc\Scramble\Contracts\Diagnostics\Diagnostic; +use Dedoc\Scramble\Generator; use Dedoc\Scramble\Infer\Context; +use Dedoc\Scramble\Scramble; use Illuminate\Console\Command; use Illuminate\Support\Facades\File; use PHPUnit\Framework\Attributes\CoversClass; @@ -71,6 +74,25 @@ class ApiDocsExportTest extends TestCase $this->assertNotEmpty($docs['paths']); } + public function test_api_docs_are_generated_without_diagnostics(): void + { + // Act + $result = app(Generator::class)->generate(Scramble::getGeneratorConfig('default')); + + // Assert + $diagnostics = $result->diagnostics()->map(function (Diagnostic $diagnostic): string { + $location = $diagnostic->codeLocation(); + + return sprintf( + '[%s] %s%s', + $diagnostic->code(), + $diagnostic->message(), + $location !== null ? ' ('.$location->file.':'.$location->line.')' : '' + ); + })->all(); + $this->assertSame([], $diagnostics); + } + public function test_paginated_endpoints_are_documented_with_pagination(): void { // Act diff --git a/tests/Unit/Endpoint/Api/V1/UserEndpointTest.php b/tests/Unit/Endpoint/Api/V1/UserEndpointTest.php index 8c87b729..93210347 100644 --- a/tests/Unit/Endpoint/Api/V1/UserEndpointTest.php +++ b/tests/Unit/Endpoint/Api/V1/UserEndpointTest.php @@ -245,6 +245,44 @@ class UserEndpointTest extends ApiEndpointTestAbstract Mail::assertNothingSent(); } + public function test_update_fails_if_email_is_already_used_by_another_user(): void + { + // Arrange + Mail::fake(); + $data = $this->createUserWithPermission(); + User::factory()->create(['email' => 'taken@example.com']); + Passport::actingAs($data->user); + + // Act + $response = $this->putJson(route('api.v1.users.update', $data->user->getKey()), [ + 'email' => 'taken@example.com', + ]); + + // Assert + $response->assertUnprocessable(); + $response->assertJsonValidationErrors(['email']); + $this->assertNull($data->user->fresh()->pending_email); + Mail::assertNothingSent(); + } + + public function test_update_email_succeeds_if_email_is_only_used_by_a_placeholder_user(): void + { + // Arrange + Mail::fake(); + $data = $this->createUserWithPermission(); + User::factory()->placeholder()->create(['email' => 'placeholder@example.com']); + Passport::actingAs($data->user); + + // Act + $response = $this->putJson(route('api.v1.users.update', $data->user->getKey()), [ + 'email' => 'placeholder@example.com', + ]); + + // Assert + $response->assertSuccessful(); + $this->assertSame('placeholder@example.com', $data->user->fresh()->pending_email); + } + public function test_update_fails_if_email_format_is_invalid(): void { // Arrange