mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-10-14 00:33:17 +01:00
Compare commits
3 Commits
feature/ad
...
fix/neutra
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44c36fdba1 | ||
|
|
4a2f78931a | ||
|
|
ed2f19f58b |
@@ -1,107 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Extensions\Scramble;
|
|
||||||
|
|
||||||
use App\Http\Resources\PaginatedResourceCollection;
|
|
||||||
use App\Http\Resources\V1\TimeEntry\TimeEntryCollection;
|
|
||||||
use Dedoc\Scramble\Extensions\TypeToSchemaExtension;
|
|
||||||
use Dedoc\Scramble\Support\Generator\Response;
|
|
||||||
use Dedoc\Scramble\Support\Generator\Schema;
|
|
||||||
use Dedoc\Scramble\Support\Generator\Types\ArrayType;
|
|
||||||
use Dedoc\Scramble\Support\Generator\Types\BooleanType;
|
|
||||||
use Dedoc\Scramble\Support\Generator\Types\IntegerType;
|
|
||||||
use Dedoc\Scramble\Support\Generator\Types\ObjectType as OpenApiObjectType;
|
|
||||||
use Dedoc\Scramble\Support\Generator\Types\StringType;
|
|
||||||
use Dedoc\Scramble\Support\Type\Generic;
|
|
||||||
use Dedoc\Scramble\Support\Type\ObjectType;
|
|
||||||
use Dedoc\Scramble\Support\Type\Type;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
|
||||||
|
|
||||||
class PaginatedResourceCollectionTypeToSchema extends TypeToSchemaExtension
|
|
||||||
{
|
|
||||||
public function shouldHandle(Type $type): bool
|
|
||||||
{
|
|
||||||
return $type instanceof ObjectType
|
|
||||||
&& $type->isInstanceOf(PaginatedResourceCollection::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function toSchema(Type $type): ?OpenApiObjectType
|
|
||||||
{
|
|
||||||
/** @var Type|null $collectingClassType */
|
|
||||||
$collectingClassType = $type->templateTypes[0] ?? null;
|
|
||||||
|
|
||||||
if (! $collectingClassType instanceof ObjectType) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $collectingClassType->isInstanceOf(JsonResource::class) && ! $collectingClassType->isInstanceOf(Model::class)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$collectingType = $this->openApiTransformer->transform($collectingClassType);
|
|
||||||
|
|
||||||
$newType = new OpenApiObjectType;
|
|
||||||
$newType->addProperty('data', (new ArrayType)->setItems($collectingType));
|
|
||||||
if ($type instanceof ObjectType && $type->isInstanceOf(TimeEntryCollection::class)) {
|
|
||||||
$newType->addProperty(
|
|
||||||
'meta',
|
|
||||||
(new OpenApiObjectType)
|
|
||||||
->addProperty('total', (new IntegerType)->setDescription('Total number of items being paginated.'))
|
|
||||||
->setRequired(['total'])
|
|
||||||
);
|
|
||||||
$newType->setRequired(['data', 'meta']);
|
|
||||||
} else {
|
|
||||||
$newType->addProperty(
|
|
||||||
'links',
|
|
||||||
(new OpenApiObjectType)
|
|
||||||
->addProperty('first', (new StringType)->nullable(true))
|
|
||||||
->addProperty('last', (new StringType)->nullable(true))
|
|
||||||
->addProperty('prev', (new StringType)->nullable(true))
|
|
||||||
->addProperty('next', (new StringType)->nullable(true))
|
|
||||||
->setRequired(['first', 'last', 'prev', 'next'])
|
|
||||||
);
|
|
||||||
$newType->addProperty(
|
|
||||||
'meta',
|
|
||||||
(new OpenApiObjectType)
|
|
||||||
->addProperty('current_page', new IntegerType)
|
|
||||||
->addProperty('from', (new IntegerType)->nullable(true))
|
|
||||||
->addProperty('last_page', new IntegerType)
|
|
||||||
->addProperty('links', (new ArrayType)->setItems(
|
|
||||||
(new OpenApiObjectType)
|
|
||||||
->addProperty('url', (new StringType)->nullable(true))
|
|
||||||
->addProperty('label', new StringType)
|
|
||||||
->addProperty('active', new BooleanType)
|
|
||||||
->setRequired(['url', 'label', 'active'])
|
|
||||||
)->setDescription('Generated paginator links.'))
|
|
||||||
->addProperty('path', (new StringType)->nullable(true)->setDescription('Base path for paginator generated URLs.'))
|
|
||||||
->addProperty('per_page', (new IntegerType)->setDescription('Number of items shown per page.'))
|
|
||||||
->addProperty('to', (new IntegerType)->nullable(true)->setDescription('Number of the last item in the slice.'))
|
|
||||||
->addProperty('total', (new IntegerType)->setDescription('Total number of items being paginated.'))
|
|
||||||
->setRequired(['current_page', 'from', 'last_page', 'links', 'path', 'per_page', 'to', 'total'])
|
|
||||||
);
|
|
||||||
$newType->setRequired(['data', 'links', 'meta']);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $newType;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param Generic $type
|
|
||||||
*/
|
|
||||||
public function toResponse(Type $type): ?Response
|
|
||||||
{
|
|
||||||
/** @var ObjectType|null $collectingClassType */
|
|
||||||
$collectingClassType = $type->templateTypes[0] ?? null;
|
|
||||||
if (! $collectingClassType instanceof ObjectType) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
$type = $this->toSchema($type);
|
|
||||||
|
|
||||||
return Response::make(200)
|
|
||||||
->description('Paginated set of `'.$this->components->uniqueSchemaName($collectingClassType->name).'`')
|
|
||||||
->setContent('application/json', Schema::fromType($type));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
21
app/Http/Controllers/Api/FallbackController.php
Normal file
21
app/Http/Controllers/Api/FallbackController.php
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Dedoc\Scramble\Attributes\ExcludeAllRoutesFromDocs;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fallback for unknown /api/* routes, to prevent a rendered HTML page
|
||||||
|
*/
|
||||||
|
#[ExcludeAllRoutesFromDocs]
|
||||||
|
class FallbackController extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(): never
|
||||||
|
{
|
||||||
|
throw new NotFoundHttpException('API resource not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,8 +29,6 @@ class ClientController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Get clients
|
* Get clients
|
||||||
*
|
*
|
||||||
* @return ClientCollection<ClientResource>
|
|
||||||
*
|
|
||||||
* @throws AuthorizationException
|
* @throws AuthorizationException
|
||||||
*
|
*
|
||||||
* @operationId getClients
|
* @operationId getClients
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ use App\Exceptions\Api\UserIsAlreadyMemberOfOrganizationApiException;
|
|||||||
use App\Http\Requests\V1\Invitation\InvitationIndexRequest;
|
use App\Http\Requests\V1\Invitation\InvitationIndexRequest;
|
||||||
use App\Http\Requests\V1\Invitation\InvitationStoreRequest;
|
use App\Http\Requests\V1\Invitation\InvitationStoreRequest;
|
||||||
use App\Http\Resources\V1\Invitation\InvitationCollection;
|
use App\Http\Resources\V1\Invitation\InvitationCollection;
|
||||||
use App\Http\Resources\V1\Invitation\InvitationResource;
|
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use App\Models\OrganizationInvitation;
|
use App\Models\OrganizationInvitation;
|
||||||
use App\Service\InvitationService;
|
use App\Service\InvitationService;
|
||||||
@@ -30,8 +29,6 @@ class InvitationController extends Controller
|
|||||||
/**
|
/**
|
||||||
* List all invitations of an organization
|
* List all invitations of an organization
|
||||||
*
|
*
|
||||||
* @return InvitationCollection<InvitationResource>
|
|
||||||
*
|
|
||||||
* @throws AuthorizationException
|
* @throws AuthorizationException
|
||||||
*
|
*
|
||||||
* @operationId getInvitations
|
* @operationId getInvitations
|
||||||
|
|||||||
@@ -48,8 +48,6 @@ class MemberController extends Controller
|
|||||||
/**
|
/**
|
||||||
* List all members of an organization
|
* List all members of an organization
|
||||||
*
|
*
|
||||||
* @return MemberCollection<MemberResource>
|
|
||||||
*
|
|
||||||
* @throws AuthorizationException
|
* @throws AuthorizationException
|
||||||
*
|
*
|
||||||
* @operationId getMembers
|
* @operationId getMembers
|
||||||
|
|||||||
@@ -35,8 +35,6 @@ class ProjectController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Get projects visible to the current user
|
* Get projects visible to the current user
|
||||||
*
|
*
|
||||||
* @return ProjectCollection<ProjectResource>
|
|
||||||
*
|
|
||||||
* @throws AuthorizationException
|
* @throws AuthorizationException
|
||||||
*
|
*
|
||||||
* @operationId getProjects
|
* @operationId getProjects
|
||||||
|
|||||||
@@ -36,8 +36,6 @@ class ProjectMemberController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Get project members for project
|
* Get project members for project
|
||||||
*
|
*
|
||||||
* @return ProjectMemberCollection<ProjectMemberResource>
|
|
||||||
*
|
|
||||||
* @throws AuthorizationException
|
* @throws AuthorizationException
|
||||||
*
|
*
|
||||||
* @operationId getProjectMembers
|
* @operationId getProjectMembers
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ use App\Http\Requests\V1\Report\ReportStoreRequest;
|
|||||||
use App\Http\Requests\V1\Report\ReportUpdateRequest;
|
use App\Http\Requests\V1\Report\ReportUpdateRequest;
|
||||||
use App\Http\Resources\V1\Report\DetailedReportResource;
|
use App\Http\Resources\V1\Report\DetailedReportResource;
|
||||||
use App\Http\Resources\V1\Report\ReportCollection;
|
use App\Http\Resources\V1\Report\ReportCollection;
|
||||||
use App\Http\Resources\V1\Report\ReportResource;
|
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use App\Models\Report;
|
use App\Models\Report;
|
||||||
use App\Service\Dto\ReportPropertiesDto;
|
use App\Service\Dto\ReportPropertiesDto;
|
||||||
@@ -35,8 +34,6 @@ class ReportController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Get reports
|
* Get reports
|
||||||
*
|
*
|
||||||
* @return ReportCollection<ReportResource>
|
|
||||||
*
|
|
||||||
* @throws AuthorizationException
|
* @throws AuthorizationException
|
||||||
*
|
*
|
||||||
* @operationId getReports
|
* @operationId getReports
|
||||||
|
|||||||
@@ -29,8 +29,6 @@ class TagController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Get tags
|
* Get tags
|
||||||
*
|
*
|
||||||
* @return TagCollection<TagResource>
|
|
||||||
*
|
|
||||||
* @operationId getTags
|
* @operationId getTags
|
||||||
*
|
*
|
||||||
* @throws AuthorizationException
|
* @throws AuthorizationException
|
||||||
|
|||||||
@@ -51,8 +51,6 @@ class TaskController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Get tasks
|
* Get tasks
|
||||||
*
|
*
|
||||||
* @return TaskCollection<TaskResource>
|
|
||||||
*
|
|
||||||
* @throws AuthorizationException
|
* @throws AuthorizationException
|
||||||
*
|
*
|
||||||
* @operationId getTasks
|
* @operationId getTasks
|
||||||
|
|||||||
@@ -117,8 +117,6 @@ class TimeEntryController extends Controller
|
|||||||
* Results are paginated with `limit` (default 100, max 500) and `offset`; check `meta.total` and fetch further pages when needed.
|
* Results are paginated with `limit` (default 100, max 500) and `offset`; check `meta.total` and fetch further pages when needed.
|
||||||
* To find the running timer, use `active=true` (or `GET /v1/users/me/time-entries/active`).
|
* To find the running timer, use `active=true` (or `GET /v1/users/me/time-entries/active`).
|
||||||
*
|
*
|
||||||
* @return TimeEntryCollection<TimeEntryResource>
|
|
||||||
*
|
|
||||||
* @throws AuthorizationException
|
* @throws AuthorizationException
|
||||||
*
|
*
|
||||||
* @operationId getTimeEntries
|
* @operationId getTimeEntries
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Http\Resources;
|
|
||||||
|
|
||||||
interface PaginatedResourceCollection {}
|
|
||||||
@@ -4,10 +4,9 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Resources\V1\Client;
|
namespace App\Http\Resources\V1\Client;
|
||||||
|
|
||||||
use App\Http\Resources\PaginatedResourceCollection;
|
|
||||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||||
|
|
||||||
class ClientCollection extends ResourceCollection implements PaginatedResourceCollection
|
class ClientCollection extends ResourceCollection
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The resource that this resource collects.
|
* The resource that this resource collects.
|
||||||
|
|||||||
@@ -4,10 +4,9 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Resources\V1\Invitation;
|
namespace App\Http\Resources\V1\Invitation;
|
||||||
|
|
||||||
use App\Http\Resources\PaginatedResourceCollection;
|
|
||||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||||
|
|
||||||
class InvitationCollection extends ResourceCollection implements PaginatedResourceCollection
|
class InvitationCollection extends ResourceCollection
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The resource that this resource collects.
|
* The resource that this resource collects.
|
||||||
|
|||||||
@@ -4,10 +4,9 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Resources\V1\Member;
|
namespace App\Http\Resources\V1\Member;
|
||||||
|
|
||||||
use App\Http\Resources\PaginatedResourceCollection;
|
|
||||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||||
|
|
||||||
class MemberCollection extends ResourceCollection implements PaginatedResourceCollection
|
class MemberCollection extends ResourceCollection
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The resource that this resource collects.
|
* The resource that this resource collects.
|
||||||
|
|||||||
@@ -4,10 +4,9 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Resources\V1\Member;
|
namespace App\Http\Resources\V1\Member;
|
||||||
|
|
||||||
use App\Http\Resources\PaginatedResourceCollection;
|
|
||||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||||
|
|
||||||
class PersonalMembershipCollection extends ResourceCollection implements PaginatedResourceCollection
|
class PersonalMembershipCollection extends ResourceCollection
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The resource that this resource collects.
|
* The resource that this resource collects.
|
||||||
|
|||||||
@@ -4,12 +4,11 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Resources\V1\Project;
|
namespace App\Http\Resources\V1\Project;
|
||||||
|
|
||||||
use App\Http\Resources\PaginatedResourceCollection;
|
|
||||||
use App\Models\Project;
|
use App\Models\Project;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||||
|
|
||||||
class ProjectCollection extends ResourceCollection implements PaginatedResourceCollection
|
class ProjectCollection extends ResourceCollection
|
||||||
{
|
{
|
||||||
private bool $showBillableRates;
|
private bool $showBillableRates;
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,9 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Resources\V1\ProjectMember;
|
namespace App\Http\Resources\V1\ProjectMember;
|
||||||
|
|
||||||
use App\Http\Resources\PaginatedResourceCollection;
|
|
||||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||||
|
|
||||||
class ProjectMemberCollection extends ResourceCollection implements PaginatedResourceCollection
|
class ProjectMemberCollection extends ResourceCollection
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The resource that this resource collects.
|
* The resource that this resource collects.
|
||||||
|
|||||||
@@ -4,10 +4,9 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Resources\V1\Report;
|
namespace App\Http\Resources\V1\Report;
|
||||||
|
|
||||||
use App\Http\Resources\PaginatedResourceCollection;
|
|
||||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||||
|
|
||||||
class ReportCollection extends ResourceCollection implements PaginatedResourceCollection
|
class ReportCollection extends ResourceCollection
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The resource that this resource collects.
|
* The resource that this resource collects.
|
||||||
|
|||||||
@@ -4,10 +4,9 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Resources\V1\Tag;
|
namespace App\Http\Resources\V1\Tag;
|
||||||
|
|
||||||
use App\Http\Resources\PaginatedResourceCollection;
|
|
||||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||||
|
|
||||||
class TagCollection extends ResourceCollection implements PaginatedResourceCollection
|
class TagCollection extends ResourceCollection
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The resource that this resource collects.
|
* The resource that this resource collects.
|
||||||
|
|||||||
@@ -4,10 +4,9 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Resources\V1\Task;
|
namespace App\Http\Resources\V1\Task;
|
||||||
|
|
||||||
use App\Http\Resources\PaginatedResourceCollection;
|
|
||||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||||
|
|
||||||
class TaskCollection extends ResourceCollection implements PaginatedResourceCollection
|
class TaskCollection extends ResourceCollection
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The resource that this resource collects.
|
* The resource that this resource collects.
|
||||||
|
|||||||
@@ -4,10 +4,9 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Resources\V1\TimeEntry;
|
namespace App\Http\Resources\V1\TimeEntry;
|
||||||
|
|
||||||
use App\Http\Resources\PaginatedResourceCollection;
|
|
||||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||||
|
|
||||||
class TimeEntryCollection extends ResourceCollection implements PaginatedResourceCollection
|
class TimeEntryCollection extends ResourceCollection
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The resource that this resource collects.
|
* The resource that this resource collects.
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use App\Extensions\Scramble\ApiExceptionTypeToSchema;
|
use App\Extensions\Scramble\ApiExceptionTypeToSchema;
|
||||||
use App\Extensions\Scramble\PaginatedResourceCollectionTypeToSchema;
|
|
||||||
use Dedoc\Scramble\Http\Middleware\RestrictedDocsAccess;
|
use Dedoc\Scramble\Http\Middleware\RestrictedDocsAccess;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -101,6 +100,5 @@ MD,
|
|||||||
|
|
||||||
'extensions' => [
|
'extensions' => [
|
||||||
ApiExceptionTypeToSchema::class,
|
ApiExceptionTypeToSchema::class,
|
||||||
PaginatedResourceCollectionTypeToSchema::class,
|
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -66,13 +66,13 @@
|
|||||||
:root.light {
|
:root.light {
|
||||||
--color-bg-primary: #ffffff;
|
--color-bg-primary: #ffffff;
|
||||||
--color-bg-secondary: #fcfcfc;
|
--color-bg-secondary: #fcfcfc;
|
||||||
--color-bg-tertiary: #eeeeef;
|
--color-bg-tertiary: #eeeeee;
|
||||||
--color-bg-quaternary: #e1e1e3;
|
--color-bg-quaternary: #e1e1e1;
|
||||||
--color-bg-background: #f5f5f5;
|
--color-bg-background: #f5f5f5;
|
||||||
--color-text-primary: #18181b;
|
--color-text-primary: #181818;
|
||||||
--color-text-secondary: #3f3f46;
|
--color-text-secondary: #404040;
|
||||||
--color-text-tertiary: #57575c;
|
--color-text-tertiary: #575757;
|
||||||
--color-text-quaternary: #a1a1aa;
|
--color-text-quaternary: #a2a2a2;
|
||||||
--color-border-primary: #e7e7e7;
|
--color-border-primary: #e7e7e7;
|
||||||
--color-border-secondary: #e5e5e5;
|
--color-border-secondary: #e5e5e5;
|
||||||
--color-border-tertiary: #dfdfdf;
|
--color-border-tertiary: #dfdfdf;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Http\Controllers\Api\FallbackController;
|
||||||
use App\Http\Controllers\Api\V1\ApiTokenController;
|
use App\Http\Controllers\Api\V1\ApiTokenController;
|
||||||
use App\Http\Controllers\Api\V1\ChartController;
|
use App\Http\Controllers\Api\V1\ChartController;
|
||||||
use App\Http\Controllers\Api\V1\ClientController;
|
use App\Http\Controllers\Api\V1\ClientController;
|
||||||
@@ -23,7 +24,6 @@ use App\Http\Controllers\Api\V1\UserController;
|
|||||||
use App\Http\Controllers\Api\V1\UserMembershipController;
|
use App\Http\Controllers\Api\V1\UserMembershipController;
|
||||||
use App\Http\Controllers\Api\V1\UserTimeEntryController;
|
use App\Http\Controllers\Api\V1\UserTimeEntryController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
@@ -200,9 +200,5 @@ Route::prefix('v1')->name('v1.')->group(static function (): void {
|
|||||||
* Fallback routes, to prevent a rendered HTML page in /api/* routes
|
* Fallback routes, to prevent a rendered HTML page in /api/* routes
|
||||||
* The / route is also included since the fallback is not triggered on the root route
|
* The / route is also included since the fallback is not triggered on the root route
|
||||||
*/
|
*/
|
||||||
Route::get('/', function (): void {
|
Route::get('/', FallbackController::class);
|
||||||
throw new NotFoundHttpException('API resource not found');
|
Route::fallback(FallbackController::class);
|
||||||
});
|
|
||||||
Route::fallback(function (): void {
|
|
||||||
throw new NotFoundHttpException('API resource not found');
|
|
||||||
});
|
|
||||||
|
|||||||
113
tests/Unit/ApiDocs/ApiDocsExportTest.php
Normal file
113
tests/Unit/ApiDocs/ApiDocsExportTest.php
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Tests\Unit\ApiDocs;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Api\FallbackController;
|
||||||
|
use Dedoc\Scramble\Infer\Context;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
#[CoversClass(FallbackController::class)]
|
||||||
|
class ApiDocsExportTest extends TestCase
|
||||||
|
{
|
||||||
|
private string $path;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
// Scramble keeps its inference context in a static property, which would otherwise outlive the refreshed application
|
||||||
|
Context::reset();
|
||||||
|
$this->path = storage_path('framework/testing/api-docs-'.uniqid().'.json');
|
||||||
|
File::ensureDirectoryExists(dirname($this->path));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
File::delete($this->path);
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function exportApiDocs(): array
|
||||||
|
{
|
||||||
|
$exitCode = $this->withoutMockingConsoleOutput()->artisan('scramble:export', [
|
||||||
|
'--path' => $this->path,
|
||||||
|
]);
|
||||||
|
$this->assertSame(Command::SUCCESS, $exitCode);
|
||||||
|
$this->assertFileExists($this->path);
|
||||||
|
|
||||||
|
return json_decode(File::get($this->path), true, flags: JSON_THROW_ON_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $docs
|
||||||
|
* @param array<string, mixed> $schema
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function resolveSchema(array $docs, array $schema): array
|
||||||
|
{
|
||||||
|
if (isset($schema['$ref'])) {
|
||||||
|
$name = str_replace('#/components/schemas/', '', $schema['$ref']);
|
||||||
|
|
||||||
|
return $this->resolveSchema($docs, $docs['components']['schemas'][$name]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_api_docs_can_be_exported(): void
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
$docs = $this->exportApiDocs();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$this->assertArrayHasKey('paths', $docs);
|
||||||
|
$this->assertNotEmpty($docs['paths']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_paginated_endpoints_are_documented_with_pagination(): void
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
$docs = $this->exportApiDocs();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$tagsSchema = $docs['paths']['/v1/organizations/{organization}/tags']['get']['responses']['200']['content']['application/json']['schema'];
|
||||||
|
$this->assertSame(['data', 'links', 'meta'], $tagsSchema['required']);
|
||||||
|
$tagsData = $this->resolveSchema($docs, $tagsSchema['properties']['data']);
|
||||||
|
$this->assertSame('array', $tagsData['type']);
|
||||||
|
$this->assertSame('#/components/schemas/TagResource', $tagsData['items']['$ref']);
|
||||||
|
|
||||||
|
$timeEntriesSchema = $docs['paths']['/v1/organizations/{organization}/time-entries']['get']['responses']['200']['content']['application/json']['schema'];
|
||||||
|
$this->assertSame(['data', 'meta'], $timeEntriesSchema['required']);
|
||||||
|
$this->assertSame(['total'], $timeEntriesSchema['properties']['meta']['required']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_fallback_routes_are_excluded_from_api_docs(): void
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
$docs = $this->exportApiDocs();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$this->assertArrayNotHasKey('/', $docs['paths']);
|
||||||
|
$this->assertArrayNotHasKey('/{fallbackPlaceholder}', $docs['paths']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_fallback_routes_return_not_found(): void
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
$rootResponse = $this->getJson('/api');
|
||||||
|
$unknownResponse = $this->getJson('/api/does-not-exist');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$rootResponse->assertNotFound();
|
||||||
|
$rootResponse->assertJsonPath('message', 'API resource not found');
|
||||||
|
$unknownResponse->assertNotFound();
|
||||||
|
$unknownResponse->assertJsonPath('message', 'API resource not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user