Skip to content

Commit

Permalink
fix: e2e tests
Browse files Browse the repository at this point in the history
  • Loading branch information
vincentchalamon committed Sep 7, 2023
1 parent b42dfa5 commit 87f62bb
Show file tree
Hide file tree
Showing 53 changed files with 1,273 additions and 698 deletions.
10 changes: 5 additions & 5 deletions api/composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

39 changes: 26 additions & 13 deletions api/config/packages/api_platform.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ api_platform:
#when@prod:
# parameters:
# # The api url that is called to invalidate cached resources
# # Can't be set in .env file cause it's only available on prod env
# # Can't be set in .env file because it's only available on prod env
# env(SOUIN_API_URL): http://caddy/souin-api
#
# api_platform:
Expand All @@ -48,23 +48,36 @@ api_platform:
# shared_max_age: 3600

services:
app.filter.review.admin.search:
class: 'ApiPlatform\Doctrine\Orm\Filter\SearchFilter'
_defaults:
autowire: false
autoconfigure: false
public: false

app.filter.review.admin.user:
parent: 'api_platform.doctrine.orm.search_filter'
arguments:
$managerRegistry: '@doctrine'
$iriConverter: '@api_platform.iri_converter'
$propertyAccessor: '@property_accessor'
$logger: '@logger'
$properties: { user: 'exact', book: 'exact' } ]
$identifiersExtractor: '@api_platform.api.identifiers_extractor'
$nameConverter: '@?api_platform.name_converter'
$properties: { user: 'exact' } ]
tags: [ 'api_platform.filter' ]

app.filter.review.admin.book:
parent: 'api_platform.doctrine.orm.search_filter'
arguments:
$properties: { book: 'exact' } ]
tags: [ 'api_platform.filter' ]

app.filter.review.admin.rating:
parent: 'api_platform.doctrine.orm.numeric_filter'
arguments:
$properties: { rating: ~ } ]
tags: [ 'api_platform.filter' ]

app.filter.review.admin.numeric:
class: 'ApiPlatform\Doctrine\Orm\Filter\NumericFilter'
# "name" is not a property, it's only a method "getName"
# Can't apply ApiFilter PHP attribute on method, so declare filter manually
app.filter.user.admin.name:
class: 'App\Doctrine\Orm\Filter\NameFilter'
arguments:
$managerRegistry: '@doctrine'
$logger: '@logger'
$properties: { rating: ~ } ]
$nameConverter: '@?api_platform.name_converter'
$properties: { name: 'ipartial' } ]
tags: [ 'api_platform.filter' ]
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20230825125251 extends AbstractMigration
final class Version20230906094949 extends AbstractMigration
{
public function getDescription(): string
{
Expand Down Expand Up @@ -49,7 +49,7 @@ public function up(Schema $schema): void
$this->addSql('ALTER TABLE bookmark ADD CONSTRAINT FK_DA62921DA76ED395 FOREIGN KEY (user_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE bookmark ADD CONSTRAINT FK_DA62921D16A2B381 FOREIGN KEY (book_id) REFERENCES book (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE review ADD CONSTRAINT FK_794381C6A76ED395 FOREIGN KEY (user_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE review ADD CONSTRAINT FK_794381C616A2B381 FOREIGN KEY (book_id) REFERENCES book (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE review ADD CONSTRAINT FK_794381C616A2B381 FOREIGN KEY (book_id) REFERENCES book (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
}

public function down(Schema $schema): void
Expand Down
6 changes: 3 additions & 3 deletions api/src/DataFixtures/Story/DefaultStory.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ public function build(): void
// Create default book (must be created first to appear first in list)
$defaultBook = BookFactory::createOne([
'condition' => BookCondition::UsedCondition,
'book' => 'https://openlibrary.org/books/OL25840917M.json',
'title' => 'The Three-Body Problem',
'author' => 'Liu Cixin',
'book' => 'https://openlibrary.org/books/OL2055137M.json',
'title' => 'Hyperion',
'author' => 'Dan Simmons',
]);

// Default book has reviews (new users are created)
Expand Down
78 changes: 78 additions & 0 deletions api/src/Doctrine/Orm/Filter/NameFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

declare(strict_types=1);

namespace App\Doctrine\Orm\Filter;

use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter;
use ApiPlatform\Doctrine\Orm\PropertyHelperTrait;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Exception\InvalidArgumentException;
use ApiPlatform\Metadata\Operation;
use Doctrine\ORM\QueryBuilder;

final class NameFilter extends AbstractFilter
{
use PropertyHelperTrait;

public function getDescription(string $resourceClass): array
{
return [
'name' => [
'property' => 'name',
'type' => 'string',
'required' => false,
'strategy' => 'ipartial',
'is_collection' => false,
],
];
}

protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, Operation $operation = null, array $context = []): void
{
if ('name' !== $property) {
return;
}

$values = $this->normalizeValues($value, $property);
if (null === $values) {
return;
}

$alias = $queryBuilder->getRootAliases()[0];
$expressions = [];
foreach ($values as $key => $value) {
$parameterName = $queryNameGenerator->generateParameterName("name$key");
$queryBuilder->setParameter($parameterName, "%$value%");
$expressions[] = $queryBuilder->expr()->orX(
$queryBuilder->expr()->like(sprintf('%s.firstName', $alias), ":$parameterName"),
$queryBuilder->expr()->like(sprintf('%s.lastName', $alias), ":$parameterName")
);
}
$queryBuilder->andWhere($queryBuilder->expr()->andX(...$expressions));
}

protected function normalizeValues($value, string $property): ?array
{
if (!\is_string($value) || empty(trim($value))) {
return null;
}

$values = explode(' ', $value);
foreach ($values as $key => $value) {
if (empty(trim($value))) {
unset($values[$key]);
}
}

if (empty($values)) {
$this->getLogger()->notice('Invalid filter ignored', [
'exception' => new InvalidArgumentException(sprintf('At least one value is required, multiple values should be in "%1$s[]=firstvalue&%1$s[]=secondvalue" format', $property)),
]);

return null;
}

return array_values($values);
}
}
9 changes: 5 additions & 4 deletions api/src/Entity/Book.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@
types: ['https://schema.org/Book', 'https://schema.org/Offer'],
operations: [
new GetCollection(
itemUriTemplate: '/admin/books/{id}{._format}'
itemUriTemplate: '/admin/books/{id}{._format}',
paginationClientItemsPerPage: true
),
new Post(
// Mercure publish is done manually in MercureProcessor through BookPersistProcessor
Expand Down Expand Up @@ -98,7 +99,7 @@ class Book
*/
#[ApiProperty(
types: ['https://schema.org/itemOffered', 'https://purl.org/dc/terms/BibliographicResource'],
example: 'https://openlibrary.org/books/OL25840917M.json'
example: 'https://openlibrary.org/books/OL2055137M.json'
)]
#[Assert\NotBlank(allowNull: false)]
#[Assert\Url(protocols: ['https'])]
Expand All @@ -114,7 +115,7 @@ class Book
#[ApiFilter(SearchFilter::class, strategy: 'i'.SearchFilterInterface::STRATEGY_PARTIAL)]
#[ApiProperty(
types: ['https://schema.org/name'],
example: 'The Three-Body Problem'
example: 'Hyperion'
)]
#[Groups(groups: ['Book:read', 'Book:read:admin', 'Bookmark:read', 'Review:read:admin'])]
#[ORM\Column(type: Types::TEXT)]
Expand All @@ -126,7 +127,7 @@ class Book
#[ApiFilter(SearchFilter::class, strategy: 'i'.SearchFilterInterface::STRATEGY_PARTIAL)]
#[ApiProperty(
types: ['https://schema.org/author'],
example: 'Liu Cixin'
example: 'Dan Simmons'
)]
#[Groups(groups: ['Book:read', 'Book:read:admin', 'Bookmark:read', 'Review:read:admin'])]
#[ORM\Column(nullable: true)]
Expand Down
9 changes: 7 additions & 2 deletions api/src/Entity/Review.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,12 @@
new GetCollection(
uriTemplate: '/admin/reviews{._format}',
itemUriTemplate: '/admin/reviews/{id}{._format}',
filters: ['app.filter.review.admin.search', 'app.filter.review.admin.numeric']
filters: [
'app.filter.review.admin.user',
'app.filter.review.admin.book',
'app.filter.review.admin.rating',
],
paginationClientItemsPerPage: true
),
new Get(
uriTemplate: '/admin/reviews/{id}{._format}'
Expand Down Expand Up @@ -156,7 +161,7 @@ class Review
#[Assert\NotNull]
#[Groups(groups: ['Review:read', 'Review:write:admin'])]
#[ORM\ManyToOne(targetEntity: Book::class)]
#[ORM\JoinColumn(nullable: false)]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
public ?Book $book = null;

/**
Expand Down
8 changes: 8 additions & 0 deletions api/src/Entity/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use App\Repository\UserRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Types\UuidType;
Expand All @@ -23,6 +24,13 @@
#[ApiResource(
types: ['https://schema.org/Person'],
operations: [
new GetCollection(
uriTemplate: '/admin/users{._format}',
itemUriTemplate: '/admin/users/{id}{._format}',
security: 'is_granted("ROLE_ADMIN")',
filters: ['app.filter.user.admin.name'],
paginationClientItemsPerPage: true
),
new Get(
uriTemplate: '/admin/users/{id}{._format}',
security: 'is_granted("ROLE_ADMIN")'
Expand Down
14 changes: 12 additions & 2 deletions api/src/State/Processor/ReviewPersistProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use ApiPlatform\Doctrine\Common\State\PersistProcessor;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Post;
use ApiPlatform\State\ProcessorInterface;
use App\Entity\Review;
use Psr\Clock\ClockInterface;
Expand All @@ -29,8 +30,17 @@ public function __construct(
*/
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): Review
{
$data->user = $this->security->getUser();
$data->publishedAt = $this->clock->now();
// standard PUT
if (isset($context['previous_data'])) {
$data->user = $context['previous_data']->user;
$data->publishedAt = $context['previous_data']->publishedAt;
}

// prevent overriding user, for instance from admin
if ($operation instanceof Post) {
$data->user = $this->security->getUser();
$data->publishedAt = $this->clock->now();
}

// save entity
$data = $this->persistProcessor->process($data, $operation, $uriVariables, $context);
Expand Down
26 changes: 16 additions & 10 deletions api/tests/Api/Admin/BookTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public function testAsNonAdminUserICannotGetACollectionOfBooks(int $expectedCode
/**
* @dataProvider getUrls
*/
public function testAsAdminUserICanGetACollectionOfBooks(FactoryCollection $factory, string $url, int $hydraTotalItems): void
public function testAsAdminUserICanGetACollectionOfBooks(FactoryCollection $factory, string $url, int $hydraTotalItems, int $itemsPerPage = null): void
{
// Cannot use Factory as data provider because BookFactory has a service dependency
$factory->create();
Expand All @@ -79,7 +79,7 @@ public function testAsAdminUserICanGetACollectionOfBooks(FactoryCollection $fact
self::assertJsonContains([
'hydra:totalItems' => $hydraTotalItems,
]);
self::assertCount(min($hydraTotalItems, 30), $response->toArray()['hydra:member']);
self::assertCount(min($itemsPerPage ?? $hydraTotalItems, 30), $response->toArray()['hydra:member']);
self::assertMatchesJsonSchema(file_get_contents(__DIR__.'/schemas/Book/collection.json'));
}

Expand All @@ -90,24 +90,30 @@ public function getUrls(): iterable
'/admin/books',
35,
];
yield 'all books using itemsPerPage' => [
BookFactory::new()->many(35),
'/admin/books?itemsPerPage=10',
35,
10,
];
yield 'books filtered by title' => [
BookFactory::new()->sequence(function () {
yield ['title' => 'The Three-Body Problem'];
yield ['title' => 'Hyperion'];
foreach (range(1, 10) as $i) {
yield [];
}
}),
'/admin/books?title=three-body',
'/admin/books?title=yperio',
1,
];
yield 'books filtered by author' => [
BookFactory::new()->sequence(function () {
yield ['author' => 'Liu Cixin'];
yield ['author' => 'Dan Simmons'];
foreach (range(1, 10) as $i) {
yield [];
}
}),
'/admin/books?author=liu',
'/admin/books?author=dan',
1,
];
yield 'books filtered by condition' => [
Expand All @@ -124,7 +130,7 @@ public function getUrls(): iterable

public function testAsAdminUserICanGetACollectionOfBooksOrderedByTitle(): void
{
BookFactory::createOne(['title' => 'The Three-Body Problem']);
BookFactory::createOne(['title' => 'Hyperion']);
BookFactory::createOne(['title' => 'The Wandering Earth']);
BookFactory::createOne(['title' => 'Ball Lightning']);

Expand All @@ -137,7 +143,7 @@ public function testAsAdminUserICanGetACollectionOfBooksOrderedByTitle(): void
self::assertResponseIsSuccessful();
self::assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8');
self::assertEquals('Ball Lightning', $response->toArray()['hydra:member'][0]['title']);
self::assertEquals('The Three-Body Problem', $response->toArray()['hydra:member'][1]['title']);
self::assertEquals('Hyperion', $response->toArray()['hydra:member'][1]['title']);
self::assertEquals('The Wandering Earth', $response->toArray()['hydra:member'][2]['title']);
self::assertMatchesJsonSchema(file_get_contents(__DIR__.'/schemas/Book/collection.json'));
}
Expand Down Expand Up @@ -573,7 +579,7 @@ public function testAsAdminUserICannotDeleteAnInvalidBook(): void
*/
public function testAsAdminUserICanDeleteABook(): void
{
$book = BookFactory::createOne(['title' => 'The Three-Body Problem']);
$book = BookFactory::createOne(['title' => 'Hyperion']);
self::getMercureHub()->reset();
$id = $book->getId();

Expand All @@ -585,7 +591,7 @@ public function testAsAdminUserICanDeleteABook(): void

self::assertResponseStatusCodeSame(Response::HTTP_NO_CONTENT);
self::assertEmpty($response->getContent());
BookFactory::assert()->notExists(['title' => 'The Three-Body Problem']);
BookFactory::assert()->notExists(['title' => 'Hyperion']);
self::assertCount(2, self::getMercureMessages());
// todo how to ensure it's a delete update
self::assertEquals(
Expand Down
Loading

0 comments on commit 87f62bb

Please sign in to comment.