feat(api): adding artist resource pivot endpoints (#528)

This commit is contained in:
paranarimasu
2023-02-25 22:47:04 -06:00
committed by GitHub
parent 3a9d78f122
commit ecf9b83997
15 changed files with 1348 additions and 0 deletions
@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Field\Pivot\Wiki\ArtistResource;
use App\Contracts\Http\Api\Field\CreatableField;
use App\Contracts\Http\Api\Field\SelectableField;
use App\Http\Api\Field\Field;
use App\Http\Api\Query\Query;
use App\Http\Api\Schema\Schema;
use App\Models\Wiki\Artist;
use App\Pivots\Wiki\ArtistResource;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
/**
* Class ArtistResourceArtistIdField.
*/
class ArtistResourceArtistIdField extends Field implements CreatableField, SelectableField
{
/**
* Create a new field instance.
*
* @param Schema $schema
*/
public function __construct(Schema $schema)
{
parent::__construct($schema, ArtistResource::ATTRIBUTE_ARTIST);
}
/**
* Set the creation validation rules for the field.
*
* @param Request $request
* @return array
*/
public function getCreationRules(Request $request): array
{
return [
'required',
'integer',
Rule::exists(Artist::TABLE, Artist::ATTRIBUTE_ID),
];
}
/**
* Determine if the field should be included in the select clause of our query.
*
* @param Query $query
* @param Schema $schema
* @return bool
*/
public function shouldSelect(Query $query, Schema $schema): bool
{
// Needed to match artist relation.
return true;
}
}
@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Field\Pivot\Wiki\ArtistResource;
use App\Contracts\Http\Api\Field\CreatableField;
use App\Contracts\Http\Api\Field\UpdatableField;
use App\Http\Api\Field\StringField;
use App\Http\Api\Schema\Schema;
use App\Pivots\Wiki\ArtistResource;
use Illuminate\Http\Request;
/**
* Class ArtistResourceAsField.
*/
class ArtistResourceAsField extends StringField implements CreatableField, UpdatableField
{
/**
* Create a new field instance.
*
* @param Schema $schema
*/
public function __construct(Schema $schema)
{
parent::__construct($schema, ArtistResource::ATTRIBUTE_AS);
}
/**
* Set the creation validation rules for the field.
*
* @param Request $request
* @return array
*/
public function getCreationRules(Request $request): array
{
return [
'nullable',
'string',
'max:192',
];
}
/**
* Set the update validation rules for the field.
*
* @param Request $request
* @return array
*/
public function getUpdateRules(Request $request): array
{
return [
'nullable',
'string',
'max:192',
];
}
}
@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Field\Pivot\Wiki\ArtistResource;
use App\Contracts\Http\Api\Field\CreatableField;
use App\Contracts\Http\Api\Field\SelectableField;
use App\Http\Api\Field\Field;
use App\Http\Api\Query\Query;
use App\Http\Api\Schema\Schema;
use App\Models\Wiki\ExternalResource;
use App\Pivots\Wiki\ArtistResource;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
/**
* Class ArtistResourceResourceIdField.
*/
class ArtistResourceResourceIdField extends Field implements CreatableField, SelectableField
{
/**
* Create a new field instance.
*
* @param Schema $schema
*/
public function __construct(Schema $schema)
{
parent::__construct($schema, ArtistResource::ATTRIBUTE_RESOURCE);
}
/**
* Set the creation validation rules for the field.
*
* @param Request $request
* @return array
*/
public function getCreationRules(Request $request): array
{
return [
'required',
'integer',
Rule::exists(ExternalResource::TABLE, ExternalResource::ATTRIBUTE_ID),
];
}
/**
* Determine if the field should be included in the select clause of our query.
*
* @param Query $query
* @param Schema $schema
* @return bool
*/
public function shouldSelect(Query $query, Schema $schema): bool
{
// Needed to match resource relation.
return true;
}
}
@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Schema\Pivot\Wiki;
use App\Http\Api\Field\Base\CreatedAtField;
use App\Http\Api\Field\Base\UpdatedAtField;
use App\Http\Api\Field\Field;
use App\Http\Api\Field\Pivot\Wiki\ArtistResource\ArtistResourceArtistIdField;
use App\Http\Api\Field\Pivot\Wiki\ArtistResource\ArtistResourceAsField;
use App\Http\Api\Field\Pivot\Wiki\ArtistResource\ArtistResourceResourceIdField;
use App\Http\Api\Include\AllowedInclude;
use App\Http\Api\Schema\EloquentSchema;
use App\Http\Api\Schema\Wiki\ArtistSchema;
use App\Http\Api\Schema\Wiki\ExternalResourceSchema;
use App\Http\Resources\Pivot\Wiki\Resource\ArtistResourceResource;
use App\Pivots\Wiki\ArtistResource;
/**
* Class ArtistResourceSchema.
*/
class ArtistResourceSchema extends EloquentSchema
{
/**
* The model this schema represents.
*
* @return string
*/
public function model(): string
{
return ArtistResource::class;
}
/**
* Get the type of the resource.
*
* @return string
*/
public function type(): string
{
return ArtistResourceResource::$wrap;
}
/**
* Get the allowed includes.
*
* @return AllowedInclude[]
*/
public function allowedIncludes(): array
{
return [
new AllowedInclude(new ArtistSchema(), ArtistResource::RELATION_ARTIST),
new AllowedInclude(new ExternalResourceSchema(), ArtistResource::RELATION_RESOURCE),
];
}
/**
* Get the direct fields of the resource.
*
* @return Field[]
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function fields(): array
{
return [
new CreatedAtField($this),
new UpdatedAtField($this),
new ArtistResourceArtistIdField($this),
new ArtistResourceResourceIdField($this),
new ArtistResourceAsField($this),
];
}
}
@@ -0,0 +1,143 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api\Pivot\Wiki;
use App\Actions\Http\Api\DestroyAction;
use App\Actions\Http\Api\IndexAction;
use App\Actions\Http\Api\ShowAction;
use App\Actions\Http\Api\StoreAction;
use App\Actions\Http\Api\UpdateAction;
use App\Http\Api\Query\Query;
use App\Http\Controllers\Api\Pivot\PivotController;
use App\Http\Requests\Api\IndexRequest;
use App\Http\Requests\Api\ShowRequest;
use App\Http\Requests\Api\StoreRequest;
use App\Http\Requests\Api\UpdateRequest;
use App\Http\Resources\Pivot\Wiki\Collection\ArtistResourceCollection;
use App\Http\Resources\Pivot\Wiki\Resource\ArtistResourceResource;
use App\Models\Wiki\Artist;
use App\Models\Wiki\ExternalResource;
use App\Pivots\Wiki\ArtistResource;
use Illuminate\Http\JsonResponse;
/**
* Class ArtistResourceController.
*/
class ArtistResourceController extends PivotController
{
/**
* Create a new controller instance.
*/
public function __construct()
{
parent::__construct(Artist::class, 'artist', ExternalResource::class, 'resource');
}
/**
* Display a listing of the resource.
*
* @param IndexRequest $request
* @param IndexAction $action
* @return JsonResponse
*/
public function index(IndexRequest $request, IndexAction $action): JsonResponse
{
$query = new Query($request->validated());
$resources = $action->index(ArtistResource::query(), $query, $request->schema());
$collection = new ArtistResourceCollection($resources, $query);
return $collection->toResponse($request);
}
/**
* Store a newly created resource.
*
* @param StoreRequest $request
* @param StoreAction $action
* @return JsonResponse
*/
public function store(StoreRequest $request, StoreAction $action): JsonResponse
{
$artistResource = $action->store(ArtistResource::query(), $request->validated());
$resource = new ArtistResourceResource($artistResource, new Query());
return $resource->toResponse($request);
}
/**
* Display the specified resource.
*
* @param ShowRequest $request
* @param Artist $artist
* @param ExternalResource $resource
* @param ShowAction $action
* @return JsonResponse
*/
public function show(ShowRequest $request, Artist $artist, ExternalResource $resource, ShowAction $action): JsonResponse
{
$artistResource = ArtistResource::query()
->where(ArtistResource::ATTRIBUTE_ARTIST, $artist->getKey())
->where(ArtistResource::ATTRIBUTE_RESOURCE, $resource->getKey())
->firstOrFail();
$query = new Query($request->validated());
$show = $action->show($artistResource, $query, $request->schema());
$apiResource = new ArtistResourceResource($show, $query);
return $apiResource->toResponse($request);
}
/**
* Update the specified resource.
*
* @param UpdateRequest $request
* @param Artist $artist
* @param ExternalResource $resource
* @param UpdateAction $action
* @return JsonResponse
*/
public function update(UpdateRequest $request, Artist $artist, ExternalResource $resource, UpdateAction $action): JsonResponse
{
$artistResource = ArtistResource::query()
->where(ArtistResource::ATTRIBUTE_ARTIST, $artist->getKey())
->where(ArtistResource::ATTRIBUTE_RESOURCE, $resource->getKey())
->firstOrFail();
$query = new Query($request->validated());
$updated = $action->update($artistResource, $request->validated());
$apiResource = new ArtistResourceResource($updated, $query);
return $apiResource->toResponse($request);
}
/**
* Remove the specified resource.
*
* @param Artist $artist
* @param ExternalResource $resource
* @param DestroyAction $action
* @return JsonResponse
*/
public function destroy(Artist $artist, ExternalResource $resource, DestroyAction $action): JsonResponse
{
$artistResource = ArtistResource::query()
->where(ArtistResource::ATTRIBUTE_ARTIST, $artist->getKey())
->where(ArtistResource::ATTRIBUTE_RESOURCE, $resource->getKey())
->firstOrFail();
$action->destroy($artistResource);
return new JsonResponse([
'message' => "Resource '**{$resource->getName()}**' has been detached from Artist '**{$artist->getName()}**'.",
]);
}
}
@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources\Pivot\Wiki\Collection;
use App\Http\Resources\BaseCollection;
use App\Http\Resources\Pivot\Wiki\Resource\ArtistResourceResource;
use App\Pivots\Wiki\ArtistResource;
use Illuminate\Http\Request;
/**
* Class ArtistResourceCollection.
*/
class ArtistResourceCollection extends BaseCollection
{
/**
* The "data" wrapper that should be applied.
*
* @var string|null
*/
public static $wrap = 'artistresources';
/**
* Transform the resource into a JSON array.
*
* @param Request $request
* @return array
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function toArray($request): array
{
return $this->collection->map(fn (ArtistResource $artistResource) => new ArtistResourceResource($artistResource, $this->query))->all();
}
}
@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources\Pivot\Wiki\Resource;
use App\Http\Api\Schema\Pivot\Wiki\ArtistResourceSchema;
use App\Http\Api\Schema\Schema;
use App\Http\Resources\BaseResource;
use App\Http\Resources\Wiki\Resource\ArtistResource;
use App\Http\Resources\Wiki\Resource\ExternalResourceResource;
use App\Pivots\Wiki\ArtistResource as ArtistResourcePivot;
use Illuminate\Http\Request;
/**
* Class ArtistResourceResource.
*/
class ArtistResourceResource extends BaseResource
{
/**
* The "data" wrapper that should be applied.
*
* @var string|null
*/
public static $wrap = 'artistresource';
/**
* Transform the resource into an array.
*
* @param Request $request
* @return array
*/
public function toArray($request): array
{
$result = parent::toArray($request);
$result[ArtistResourcePivot::RELATION_ARTIST] = new ArtistResource($this->whenLoaded(ArtistResourcePivot::RELATION_ARTIST), $this->query);
$result[ArtistResourcePivot::RELATION_RESOURCE] = new ExternalResourceResource($this->whenLoaded(ArtistResourcePivot::RELATION_RESOURCE), $this->query);
return $result;
}
/**
* Get the resource schema.
*
* @return Schema
*/
protected function schema(): Schema
{
return new ArtistResourceSchema();
}
}
+7
View File
@@ -17,8 +17,10 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* Class ArtistResource.
*
* @property Artist $artist
* @property int $artist_id
* @property string $as
* @property ExternalResource $resource
* @property int $resource_id
*
* @method static ArtistResourceFactory factory(...$parameters)
*/
@@ -30,13 +32,18 @@ class ArtistResource extends BasePivot
final public const ATTRIBUTE_ARTIST = 'artist_id';
final public const ATTRIBUTE_RESOURCE = 'resource_id';
final public const RELATION_ARTIST = 'artist';
final public const RELATION_RESOURCE = 'resource';
/**
* The attributes that are mass assignable.
*
* @var string[]
*/
protected $fillable = [
ArtistResource::ATTRIBUTE_ARTIST,
ArtistResource::ATTRIBUTE_AS,
ArtistResource::ATTRIBUTE_RESOURCE,
];
/**
+2
View File
@@ -22,6 +22,7 @@ use App\Http\Controllers\Api\Pivot\Wiki\AnimeSeriesController;
use App\Http\Controllers\Api\Pivot\Wiki\AnimeStudioController;
use App\Http\Controllers\Api\Pivot\Wiki\AnimeThemeEntryVideoController;
use App\Http\Controllers\Api\Pivot\Wiki\ArtistImageController;
use App\Http\Controllers\Api\Pivot\Wiki\ArtistResourceController;
use App\Http\Controllers\Api\Wiki\Anime\SynonymController;
use App\Http\Controllers\Api\Wiki\Anime\Theme\EntryController;
use App\Http\Controllers\Api\Wiki\Anime\ThemeController;
@@ -244,6 +245,7 @@ apiPivotResource('animeseries', 'anime', 'series', AnimeSeriesController::class)
apiPivotResource('animestudio', 'anime', 'studio', AnimeStudioController::class);
apiPivotResource('animethemeentryvideo', 'animethemeentry', 'video', AnimeThemeEntryVideoController::class);
apiPivotResource('artistimage', 'artist', 'image', ArtistImageController::class);
apiEditablePivotResource('artistresource', 'artist', 'resource', ArtistResourceController::class);
// Wiki Routes
apiResource('anime', AnimeController::class);
@@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
namespace Http\Api\Pivot\Wiki\ArtistResource;
use App\Models\Auth\User;
use App\Models\Wiki\Artist;
use App\Models\Wiki\ExternalResource;
use App\Pivots\Wiki\ArtistResource;
use Illuminate\Foundation\Testing\WithoutEvents;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
/**
* Class ArtistResourceDestroyTest.
*/
class ArtistResourceDestroyTest extends TestCase
{
use WithoutEvents;
/**
* The Artist Resource Destroy Endpoint shall be protected by sanctum.
*
* @return void
*/
public function testProtected(): void
{
$artistResource = ArtistResource::factory()
->for(Artist::factory())
->for(ExternalResource::factory(), ArtistResource::RELATION_RESOURCE)
->createOne();
$response = $this->delete(route('api.artistresource.destroy', ['artist' => $artistResource->artist, 'resource' => $artistResource->resource]));
$response->assertUnauthorized();
}
/**
* The Artist Resource Destroy Endpoint shall forbid users without the delete artist & delete resource permissions.
*
* @return void
*/
public function testForbidden(): void
{
$artistResource = ArtistResource::factory()
->for(Artist::factory())
->for(ExternalResource::factory(), ArtistResource::RELATION_RESOURCE)
->createOne();
$user = User::factory()->createOne();
Sanctum::actingAs($user);
$response = $this->delete(route('api.artistresource.destroy', ['artist' => $artistResource->artist, 'resource' => $artistResource->resource]));
$response->assertForbidden();
}
/**
* The Artist Resource Destroy Endpoint shall return an error if the artist resource does not exist.
*
* @return void
*/
public function testNotFound(): void
{
$artist = Artist::factory()->createOne();
$resource = ExternalResource::factory()->createOne();
$user = User::factory()->withPermissions(['delete artist', 'delete external resource'])->createOne();
Sanctum::actingAs($user);
$response = $this->delete(route('api.artistresource.destroy', ['artist' => $artist, 'resource' => $resource]));
$response->assertNotFound();
}
/**
* The Artist Resource Destroy Endpoint shall delete the artist resource.
*
* @return void
*/
public function testDeleted(): void
{
$artistResource = ArtistResource::factory()
->for(Artist::factory())
->for(ExternalResource::factory(), ArtistResource::RELATION_RESOURCE)
->createOne();
$user = User::factory()->withPermissions(['delete artist', 'delete external resource'])->createOne();
Sanctum::actingAs($user);
$response = $this->delete(route('api.artistresource.destroy', ['artist' => $artistResource->artist, 'resource' => $artistResource->resource]));
$response->assertOk();
static::assertModelMissing($artistResource);
}
}
@@ -0,0 +1,373 @@
<?php
declare(strict_types=1);
namespace Http\Api\Pivot\Wiki\ArtistResource;
use App\Concerns\Actions\Http\Api\SortsModels;
use App\Contracts\Http\Api\Field\SortableField;
use App\Enums\Http\Api\Sort\Direction;
use App\Enums\Models\Wiki\ResourceSite;
use App\Http\Api\Criteria\Paging\Criteria;
use App\Http\Api\Criteria\Paging\OffsetCriteria;
use App\Http\Api\Field\Field;
use App\Http\Api\Include\AllowedInclude;
use App\Http\Api\Parser\FieldParser;
use App\Http\Api\Parser\FilterParser;
use App\Http\Api\Parser\IncludeParser;
use App\Http\Api\Parser\PagingParser;
use App\Http\Api\Parser\SortParser;
use App\Http\Api\Query\Query;
use App\Http\Api\Schema\Pivot\Wiki\ArtistResourceSchema;
use App\Http\Resources\Pivot\Wiki\Collection\ArtistResourceCollection;
use App\Http\Resources\Pivot\Wiki\Resource\ArtistResourceResource;
use App\Models\Wiki\Artist;
use App\Models\Wiki\ExternalResource;
use App\Pivots\BasePivot;
use App\Pivots\Wiki\ArtistResource;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\WithoutEvents;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Tests\TestCase;
/**
* Class ArtistResourceIndexTest.
*/
class ArtistResourceIndexTest extends TestCase
{
use SortsModels;
use WithFaker;
use WithoutEvents;
/**
* By default, the Artist Resource Index Endpoint shall return a collection of Artist Resource Resources.
*
* @return void
*/
public function testDefault(): void
{
Collection::times($this->faker->randomDigitNotNull(), function () {
ArtistResource::factory()
->for(Artist::factory())
->for(ExternalResource::factory(), ArtistResource::RELATION_RESOURCE)
->create();
});
$artistResources = ArtistResource::all();
$response = $this->get(route('api.artistresource.index'));
$response->assertJson(
json_decode(
json_encode(
(new ArtistResourceCollection($artistResources, new Query()))
->response()
->getData()
),
true
)
);
}
/**
* The Theme Index Endpoint shall be paginated.
*
* @return void
*/
public function testPaginated(): void
{
Collection::times($this->faker->randomDigitNotNull(), function () {
ArtistResource::factory()
->for(Artist::factory())
->for(ExternalResource::factory(), ArtistResource::RELATION_RESOURCE)
->create();
});
$response = $this->get(route('api.artistresource.index'));
$response->assertJsonStructure([
ArtistResourceCollection::$wrap,
'links',
'meta',
]);
}
/**
* The Artist Resource Index Endpoint shall allow inclusion of related resources.
*
* @return void
*/
public function testAllowedIncludePaths(): void
{
$schema = new ArtistResourceSchema();
$allowedIncludes = collect($schema->allowedIncludes());
$selectedIncludes = $allowedIncludes->random($this->faker->numberBetween(1, $allowedIncludes->count()));
$includedPaths = $selectedIncludes->map(fn (AllowedInclude $include) => $include->path());
$parameters = [
IncludeParser::param() => $includedPaths->join(','),
];
Collection::times($this->faker->randomDigitNotNull(), function () {
ArtistResource::factory()
->for(Artist::factory())
->for(ExternalResource::factory(), ArtistResource::RELATION_RESOURCE)
->create();
});
$response = $this->get(route('api.artistresource.index', $parameters));
$artistResources = ArtistResource::with($includedPaths->all())->get();
$response->assertJson(
json_decode(
json_encode(
(new ArtistResourceCollection($artistResources, new Query($parameters)))
->response()
->getData()
),
true
)
);
}
/**
* The Artist Resource Index Endpoint shall implement sparse fieldsets.
*
* @return void
*/
public function testSparseFieldsets(): void
{
$schema = new ArtistResourceSchema();
$fields = collect($schema->fields());
$includedFields = $fields->random($this->faker->numberBetween(1, $fields->count()));
$parameters = [
FieldParser::param() => [
ArtistResourceResource::$wrap => $includedFields->map(fn (Field $field) => $field->getKey())->join(','),
],
];
Collection::times($this->faker->randomDigitNotNull(), function () {
ArtistResource::factory()
->for(Artist::factory())
->for(ExternalResource::factory(), ArtistResource::RELATION_RESOURCE)
->create();
});
$response = $this->get(route('api.artistresource.index', $parameters));
$artistResources = ArtistResource::all();
$response->assertJson(
json_decode(
json_encode(
(new ArtistResourceCollection($artistResources, new Query($parameters)))
->response()
->getData()
),
true
)
);
}
/**
* The Artist Resource Index Endpoint shall support sorting resources.
*
* @return void
*/
public function testSorts(): void
{
$schema = new ArtistResourceSchema();
$sort = collect($schema->fields())
->filter(fn (Field $field) => $field instanceof SortableField)
->map(fn (SortableField $field) => $field->getSort())
->random();
$parameters = [
SortParser::param() => $sort->format(Direction::getRandomInstance()),
];
$query = new Query($parameters);
Collection::times($this->faker->randomDigitNotNull(), function () {
ArtistResource::factory()
->for(Artist::factory())
->for(ExternalResource::factory(), ArtistResource::RELATION_RESOURCE)
->create();
});
$response = $this->get(route('api.artistresource.index', $parameters));
$artistResources = $this->sort(ArtistResource::query(), $query, $schema)->get();
$response->assertJson(
json_decode(
json_encode(
(new ArtistResourceCollection($artistResources, $query))
->response()
->getData()
),
true
)
);
}
/**
* The Artist Resource Index Endpoint shall support filtering by created_at.
*
* @return void
*/
public function testCreatedAtFilter(): void
{
$createdFilter = $this->faker->date();
$excludedDate = $this->faker->date();
$parameters = [
FilterParser::param() => [
BasePivot::ATTRIBUTE_CREATED_AT => $createdFilter,
],
PagingParser::param() => [
OffsetCriteria::SIZE_PARAM => Criteria::MAX_RESULTS,
],
];
Carbon::withTestNow($createdFilter, function () {
Collection::times($this->faker->randomDigitNotNull(), function () {
ArtistResource::factory()
->for(Artist::factory())
->for(ExternalResource::factory(), ArtistResource::RELATION_RESOURCE)
->create();
});
});
Carbon::withTestNow($excludedDate, function () {
Collection::times($this->faker->randomDigitNotNull(), function () {
ArtistResource::factory()
->for(Artist::factory())
->for(ExternalResource::factory(), ArtistResource::RELATION_RESOURCE)
->create();
});
});
$artistResources = ArtistResource::query()->where(BasePivot::ATTRIBUTE_CREATED_AT, $createdFilter)->get();
$response = $this->get(route('api.artistresource.index', $parameters));
$response->assertJson(
json_decode(
json_encode(
(new ArtistResourceCollection($artistResources, new Query($parameters)))
->response()
->getData()
),
true
)
);
}
/**
* The Artist Resource Index Endpoint shall support filtering by updated_at.
*
* @return void
*/
public function testUpdatedAtFilter(): void
{
$updatedFilter = $this->faker->date();
$excludedDate = $this->faker->date();
$parameters = [
FilterParser::param() => [
BasePivot::ATTRIBUTE_UPDATED_AT => $updatedFilter,
],
PagingParser::param() => [
OffsetCriteria::SIZE_PARAM => Criteria::MAX_RESULTS,
],
];
Carbon::withTestNow($updatedFilter, function () {
Collection::times($this->faker->randomDigitNotNull(), function () {
ArtistResource::factory()
->for(Artist::factory())
->for(ExternalResource::factory(), ArtistResource::RELATION_RESOURCE)
->create();
});
});
Carbon::withTestNow($excludedDate, function () {
Collection::times($this->faker->randomDigitNotNull(), function () {
ArtistResource::factory()
->for(Artist::factory())
->for(ExternalResource::factory(), ArtistResource::RELATION_RESOURCE)
->create();
});
});
$artistResources = ArtistResource::query()->where(BasePivot::ATTRIBUTE_UPDATED_AT, $updatedFilter)->get();
$response = $this->get(route('api.artistresource.index', $parameters));
$response->assertJson(
json_decode(
json_encode(
(new ArtistResourceCollection($artistResources, new Query($parameters)))
->response()
->getData()
),
true
)
);
}
/**
* The Artist Resource Index Endpoint shall support constrained eager loading of resources by site.
*
* @return void
*/
public function testResourcesBySite(): void
{
$siteFilter = ResourceSite::getRandomInstance();
$parameters = [
FilterParser::param() => [
ExternalResource::ATTRIBUTE_SITE => $siteFilter->description,
],
IncludeParser::param() => ArtistResource::RELATION_RESOURCE,
];
Collection::times($this->faker->randomDigitNotNull(), function () {
ArtistResource::factory()
->for(Artist::factory())
->for(ExternalResource::factory(), ArtistResource::RELATION_RESOURCE)
->create();
});
$response = $this->get(route('api.artistresource.index', $parameters));
$artistResources = ArtistResource::with([
ArtistResource::RELATION_RESOURCE => function (BelongsTo $query) use ($siteFilter) {
$query->where(ExternalResource::ATTRIBUTE_SITE, $siteFilter->value);
},
])
->get();
$response->assertJson(
json_decode(
json_encode(
(new ArtistResourceCollection($artistResources, new Query($parameters)))
->response()
->getData()
),
true
)
);
}
}
@@ -0,0 +1,195 @@
<?php
declare(strict_types=1);
namespace Http\Api\Pivot\Wiki\ArtistResource;
use App\Enums\Models\Wiki\ResourceSite;
use App\Http\Api\Field\Field;
use App\Http\Api\Include\AllowedInclude;
use App\Http\Api\Parser\FieldParser;
use App\Http\Api\Parser\FilterParser;
use App\Http\Api\Parser\IncludeParser;
use App\Http\Api\Query\Query;
use App\Http\Api\Schema\Pivot\Wiki\ArtistResourceSchema;
use App\Http\Resources\Pivot\Wiki\Resource\ArtistResourceResource;
use App\Models\Wiki\Artist;
use App\Models\Wiki\ExternalResource;
use App\Pivots\Wiki\ArtistResource;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\WithoutEvents;
use Tests\TestCase;
/**
* Class ArtistResourceShowTest.
*/
class ArtistResourceShowTest extends TestCase
{
use WithFaker;
use WithoutEvents;
/**
* The Artist Resource Show Endpoint shall return an error if the artist resource does not exist.
*
* @return void
*/
public function testNotFound(): void
{
$artist = Artist::factory()->createOne();
$resource = ExternalResource::factory()->createOne();
$response = $this->get(route('api.artistresource.show', ['artist' => $artist, 'resource' => $resource]));
$response->assertNotFound();
}
/**
* By default, the Artist Resource Show Endpoint shall return an Artist Resource Resource.
*
* @return void
*/
public function testDefault(): void
{
$artistResource = ArtistResource::factory()
->for(Artist::factory())
->for(ExternalResource::factory(), ArtistResource::RELATION_RESOURCE)
->createOne();
$response = $this->get(route('api.artistresource.show', ['artist' => $artistResource->artist, 'resource' => $artistResource->resource]));
$artistResource->unsetRelations();
$response->assertJson(
json_decode(
json_encode(
(new ArtistResourceResource($artistResource, new Query()))
->response()
->getData()
),
true
)
);
}
/**
* The Artist Resource Show Endpoint shall allow inclusion of related resources.
*
* @return void
*/
public function testAllowedIncludePaths(): void
{
$schema = new ArtistResourceSchema();
$allowedIncludes = collect($schema->allowedIncludes());
$selectedIncludes = $allowedIncludes->random($this->faker->numberBetween(1, $allowedIncludes->count()));
$includedPaths = $selectedIncludes->map(fn (AllowedInclude $include) => $include->path());
$parameters = [
IncludeParser::param() => $includedPaths->join(','),
];
$artistResource = ArtistResource::factory()
->for(Artist::factory())
->for(ExternalResource::factory(), ArtistResource::RELATION_RESOURCE)
->createOne();
$response = $this->get(route('api.artistresource.show', ['artist' => $artistResource->artist, 'resource' => $artistResource->resource] + $parameters));
$artistResource->unsetRelations()->load($includedPaths->all());
$response->assertJson(
json_decode(
json_encode(
(new ArtistResourceResource($artistResource, new Query($parameters)))
->response()
->getData()
),
true
)
);
}
/**
* The Artist Resource Show Endpoint shall implement sparse fieldsets.
*
* @return void
*/
public function testSparseFieldsets(): void
{
$schema = new ArtistResourceSchema();
$fields = collect($schema->fields());
$includedFields = $fields->random($this->faker->numberBetween(1, $fields->count()));
$parameters = [
FieldParser::param() => [
ArtistResourceResource::$wrap => $includedFields->map(fn (Field $field) => $field->getKey())->join(','),
],
];
$artistResource = ArtistResource::factory()
->for(Artist::factory())
->for(ExternalResource::factory(), ArtistResource::RELATION_RESOURCE)
->createOne();
$response = $this->get(route('api.artistresource.show', ['artist' => $artistResource->artist, 'resource' => $artistResource->resource] + $parameters));
$artistResource->unsetRelations();
$response->assertJson(
json_decode(
json_encode(
(new ArtistResourceResource($artistResource, new Query($parameters)))
->response()
->getData()
),
true
)
);
}
/**
* The Artist Resource Show Endpoint shall support constrained eager loading of resources by site.
*
* @return void
*/
public function testResourcesBySite(): void
{
$siteFilter = ResourceSite::getRandomInstance();
$parameters = [
FilterParser::param() => [
ExternalResource::ATTRIBUTE_SITE => $siteFilter->description,
],
IncludeParser::param() => ArtistResource::RELATION_RESOURCE,
];
$artistResource = ArtistResource::factory()
->for(Artist::factory())
->for(ExternalResource::factory(), ArtistResource::RELATION_RESOURCE)
->createOne();
$response = $this->get(route('api.artistresource.show', ['artist' => $artistResource->artist, 'resource' => $artistResource->resource] + $parameters));
$artistResource->unsetRelations()->load([
ArtistResource::RELATION_RESOURCE => function (BelongsTo $query) use ($siteFilter) {
$query->where(ExternalResource::ATTRIBUTE_SITE, $siteFilter->value);
},
]);
$response->assertJson(
json_decode(
json_encode(
(new ArtistResourceResource($artistResource, new Query($parameters)))
->response()
->getData()
),
true
)
);
}
}
@@ -0,0 +1,101 @@
<?php
declare(strict_types=1);
namespace Http\Api\Pivot\Wiki\ArtistResource;
use App\Models\Auth\User;
use App\Models\Wiki\Artist;
use App\Models\Wiki\ExternalResource;
use App\Pivots\Wiki\ArtistResource;
use Illuminate\Foundation\Testing\WithoutEvents;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
/**
* Class ArtistResourceStoreTest.
*/
class ArtistResourceStoreTest extends TestCase
{
use WithoutEvents;
/**
* The Artist Resource Store Destroy Endpoint shall be protected by sanctum.
*
* @return void
*/
public function testProtected(): void
{
$artistResource = ArtistResource::factory()
->for(Artist::factory())
->for(ExternalResource::factory(), ArtistResource::RELATION_RESOURCE)
->makeOne();
$response = $this->post(route('api.artistresource.store', $artistResource->toArray()));
$response->assertUnauthorized();
}
/**
* The Artist Resource Store Endpoint shall forbid users without the create artist & create resource permissions.
*
* @return void
*/
public function testForbidden(): void
{
$artistResource = ArtistResource::factory()
->for(Artist::factory())
->for(ExternalResource::factory(), ArtistResource::RELATION_RESOURCE)
->makeOne();
$user = User::factory()->createOne();
Sanctum::actingAs($user);
$response = $this->post(route('api.artistresource.store', $artistResource->toArray()));
$response->assertForbidden();
}
/**
* The Artist Resource Store Endpoint shall require artist and resource fields.
*
* @return void
*/
public function testRequiredFields(): void
{
$user = User::factory()->withPermissions(['create artist', 'create external resource'])->createOne();
Sanctum::actingAs($user);
$response = $this->post(route('api.artistresource.store'));
$response->assertJsonValidationErrors([
ArtistResource::ATTRIBUTE_ARTIST,
ArtistResource::ATTRIBUTE_RESOURCE,
]);
}
/**
* The Artist Resource Store Endpoint shall create an artist resource.
*
* @return void
*/
public function testCreate(): void
{
$parameters = array_merge(
ArtistResource::factory()->raw(),
[ArtistResource::ATTRIBUTE_ARTIST => Artist::factory()->createOne()->getKey()],
[ArtistResource::ATTRIBUTE_RESOURCE => ExternalResource::factory()->createOne()->getKey()],
);
$user = User::factory()->withPermissions(['create artist', 'create external resource'])->createOne();
Sanctum::actingAs($user);
$response = $this->post(route('api.artistresource.store', $parameters));
$response->assertCreated();
static::assertDatabaseCount(ArtistResource::TABLE, 1);
}
}
@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace Http\Api\Pivot\Wiki\ArtistResource;
use App\Models\Auth\User;
use App\Models\Wiki\Artist;
use App\Models\Wiki\ExternalResource;
use App\Pivots\Wiki\ArtistResource;
use Illuminate\Foundation\Testing\WithoutEvents;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
/**
* Class ArtistResourceUpdateTest.
*/
class ArtistResourceUpdateTest extends TestCase
{
use WithoutEvents;
/**
* The Artist Resource Update Destroy Endpoint shall be protected by sanctum.
*
* @return void
*/
public function testProtected(): void
{
$artistResource = ArtistResource::factory()
->for(Artist::factory())
->for(ExternalResource::factory(), ArtistResource::RELATION_RESOURCE)
->createOne();
$parameters = ArtistResource::factory()->raw();
$response = $this->put(route('api.artistresource.update', ['artist' => $artistResource->artist, 'resource' => $artistResource->resource] + $parameters));
$response->assertUnauthorized();
}
/**
* The Artist Resource Update Endpoint shall forbid users without the update artist & update resource permissions.
*
* @return void
*/
public function testForbidden(): void
{
$artistResource = ArtistResource::factory()
->for(Artist::factory())
->for(ExternalResource::factory(), ArtistResource::RELATION_RESOURCE)
->createOne();
$parameters = ArtistResource::factory()->raw();
$user = User::factory()->createOne();
Sanctum::actingAs($user);
$response = $this->put(route('api.artistresource.update', ['artist' => $artistResource->artist, 'resource' => $artistResource->resource] + $parameters));
$response->assertForbidden();
}
/**
* The Artist Resource Update Endpoint shall update an artist resource.
*
* @return void
*/
public function testUpdate(): void
{
$artistResource = ArtistResource::factory()
->for(Artist::factory())
->for(ExternalResource::factory(), ArtistResource::RELATION_RESOURCE)
->createOne();
$parameters = ArtistResource::factory()->raw();
$user = User::factory()->withPermissions(['update artist', 'update external resource'])->createOne();
Sanctum::actingAs($user);
$response = $this->put(route('api.artistresource.update', ['artist' => $artistResource->artist, 'resource' => $artistResource->resource] + $parameters));
$response->assertOk();
}
}
@@ -123,6 +123,8 @@ class ThemeShowTest extends TestCase
)
->createOne();
$theme->unsetRelations()->load($includedPaths->all());
$response = $this->get(route('api.animetheme.show', ['animetheme' => $theme] + $parameters));
$response->assertJson(