Skip to content

Commit

Permalink
test: mock openlibrary.org
Browse files Browse the repository at this point in the history
  • Loading branch information
vincentchalamon committed Mar 13, 2024
1 parent 602d3cd commit 920ad63
Show file tree
Hide file tree
Showing 14 changed files with 11,775 additions and 174 deletions.
1 change: 1 addition & 0 deletions api/composer.json
Expand Up @@ -13,6 +13,7 @@
"myclabs/php-enum": "^1.8",
"nelmio/cors-bundle": "^2.4",
"runtime/frankenphp-symfony": "^0.2.0",
"seld/jsonlint": "^1.10",
"symfony/asset": "7.0.*",
"symfony/clock": "7.0.*",
"symfony/console": "7.0.*",
Expand Down
66 changes: 65 additions & 1 deletion api/composer.lock

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

3 changes: 2 additions & 1 deletion api/src/Command/BooksImportCommand.php
Expand Up @@ -13,6 +13,7 @@
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Serializer\Encoder\DecoderInterface;
use Symfony\Component\Serializer\Encoder\JsonEncode;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;

Expand Down Expand Up @@ -83,7 +84,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int

$io->progressFinish();

$output->write($this->serializer->serialize($data, 'json'));
$output->write($this->serializer->serialize($data, 'json', [JsonEncode::OPTIONS => JSON_PRETTY_PRINT]));

return Command::SUCCESS;
}
Expand Down
201 changes: 201 additions & 0 deletions api/src/Command/BooksMockCommand.php
@@ -0,0 +1,201 @@
<?php

declare(strict_types=1);

namespace App\Command;

use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Serializer\Encoder\DecoderInterface;
use Symfony\Component\Serializer\Encoder\JsonDecode;
use Symfony\Component\Serializer\Encoder\JsonEncode;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;

#[AsCommand(
name: 'app:books:mock',
description: 'Import books from Open Library and store their data in a JSON for fixtures loading.',
)]
final class BooksMockCommand extends Command
{
public function __construct(
private readonly SerializerInterface $serializer,
private readonly DecoderInterface $decoder,
private readonly HttpClientInterface $client,
private readonly LoggerInterface $logger,
#[Autowire('%kernel.project_dir%')]
private readonly string $projectDir,
) {
parent::__construct();
}

protected function configure(): void
{
$this
->addArgument(
name: 'source',
mode: InputArgument::REQUIRED,
description: 'Source file (created by app:books:import command)',
)
;
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);

$source = realpath($this->projectDir . '/' . $input->getArgument('source'));

$mocks = [];
foreach (['Foundation Isaac Asimov', 'Eon Greg Bear', 'Hyperion Dan Simmons'] as $query) {
$searchUri = 'https://openlibrary.org/search.json?q=' . $query . '&limit=10';
$this->logger->debug('Importing search query.', [
'query' => $searchUri,
]);
$docs = $this->getData($searchUri)['docs'];
$docs = array_map(static fn ($doc): array => array_filter(
$doc,
static fn (string $key): bool => \in_array($key, ['title', 'author_name', 'seed'], true),
\ARRAY_FILTER_USE_KEY
), $docs);
$mocks[] = $this->mockifyJson(
$searchUri,
$docs
);
}

$this->logger->debug('Importing source file.', [
'source' => $source,
]);
$data = $this->decoder->decode(file_get_contents($source), 'json', [JsonDecode::DETAILED_ERROR_MESSAGES => true]);
$io->progressStart(\count($data));

foreach ($data as $datum) {
$bookUri = $datum['book'];
$this->logger->info('Importing book.', ['book' => $bookUri]);
$book = $this->getData($bookUri);

$mocks[] = $this->mockifyJson(
$bookUri,
array_filter(
$book,
// only save required data (cf. pwa/utils/book.ts)
static fn (string $key): bool => \in_array($key, [
'publish_date',
'covers',
'description',
'works',
], true),
\ARRAY_FILTER_USE_KEY
),
);

$coverId = null;
if (isset($book['covers'][0])) {
$coverId = $book['covers'][0];
}

if (isset($book['works'][0]['key'])) {
$workUri = 'https://openlibrary.org' . $book['works'][0]['key'];
$this->logger->info('Importing work.', ['work' => $workUri]);
$work = $this->getData($workUri);
$mocks[] = $this->mockifyJson(
$workUri,
array_filter(
$work,
// only save required data (cf. pwa/utils/book.ts)
static fn (string $key): bool => \in_array($key, [
'covers',
'description',
], true),
\ARRAY_FILTER_USE_KEY
),
);

if (!isset($book['covers'][0]) && isset($work['covers'][0])) {
$coverId = $work['covers'][0];
}
}

if ($coverId) {
// no need to save all images for e2e tests (except for default book)
// all other books are only tested in BookList, using medium image only
$coverUri = 'https://covers.openlibrary.org/b/id/' . $coverId . '-M.jpg';
$this->logger->info('Downloading cover.', ['cover' => $coverUri]);
$response = $this->client->request('GET', $coverUri, [
'buffer' => false,
]);
if (200 !== $response->getStatusCode()) {
throw new \RuntimeException('An error occurred while downloading image "' . $coverUri . '".');
}
$cover = '';
foreach ($this->client->stream($response) as $chunk) {
$cover .= $chunk->getContent();
}
$mocks[] = $this->mockifyJpg($coverUri, base64_encode($cover));
}

$io->progressAdvance();
}

$io->progressFinish();

$output->write($this->serializer->serialize($mocks, 'json', [JsonEncode::OPTIONS => \JSON_PRETTY_PRINT]));

return Command::SUCCESS;
}

private function getData(string $uri): array
{
return $this->decoder->decode($this->client->request(Request::METHOD_GET, $uri, [
'headers' => [
'Accept' => 'application/json',
],
])->getContent(), 'json');
}

private function mockifyJson(string $uri, array $body): array
{
return [
'httpRequest' => [
'method' => 'GET',
'path' => preg_replace('/^https:\/\/openlibrary\.org(.*)$/', '$1', $uri),
],
'httpResponse' => [
'headers' => [
'Content-Type' => ['application/json; charset=utf-8'],
],
'body' => $this->serializer->serialize($body, 'json'),
],
];
}

private function mockifyJpg(string $uri, string $body): array
{
$filename = pathinfo($uri, \PATHINFO_BASENAME);

return [
'httpRequest' => [
'method' => 'GET',
'path' => preg_replace('/^https:\/\/covers\.openlibrary\.org(.*)$/', '$1', $uri),
],
'httpResponse' => [
'headers' => [
'Content-Type' => ['image/jpg'],
'Content-Disposition' => ['form-data; name="' . $filename . '"; filename="' . $filename . '"'],
],
'body' => [
'type' => 'BINARY',
'base64Bytes' => $body,
],
],
];
}
}
5 changes: 4 additions & 1 deletion pwa/playwright.config.ts
Expand Up @@ -10,7 +10,10 @@ import { defineConfig, devices } from "@playwright/test";
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
timeout: 5 * 60 * 1000,
timeout: 45000,
expect: {
timeout: 10000,
},
testDir: "./tests",
/* Run tests in files in parallel */
fullyParallel: true,
Expand Down
21 changes: 21 additions & 0 deletions pwa/tests/admin/pages/AbstractPage.ts
Expand Up @@ -14,4 +14,25 @@ export abstract class AbstractPage {

return this.page;
}

protected async registerMock() {
await this.page.route(/^https:\/\/openlibrary\.org\/books\/(.+)\.json$/, (route) => route.fulfill({
path: "tests/mocks/openlibrary.org/books/OL2055137M.json"
}));
await this.page.route(/^https:\/\/openlibrary\.org\/works\/(.+)\.json$/, (route) => route.fulfill({
path: "tests/mocks/openlibrary.org/works/OL1963268W.json"
}));
await this.page.route("https://openlibrary.org/search.json?q=Foundation%20Isaac%20Asimov&limit=10", (route) => route.fulfill({
path: "tests/mocks/openlibrary.org/search/Foundation-Isaac-Asimov.json"
}));
await this.page.route("https://openlibrary.org/search.json?q=Eon%20Greg%20Bear&limit=10", (route) => route.fulfill({
path: "tests/mocks/openlibrary.org/search/Eon-Greg-Bear.json"
}));
await this.page.route("https://openlibrary.org/search.json?q=Hyperion%20Dan%20Simmons&limit=10", (route) => route.fulfill({
path: "tests/mocks/openlibrary.org/search/Hyperion-Dan-Simmons.json"
}));
await this.page.route(/^https:\/\/covers\.openlibrary.org\/b\/id\/(.+)\.jpg$/, (route) => route.fulfill({
path: "tests/mocks/covers.openlibrary.org/b/id/4066031-M.jpg",
}));
}
}
2 changes: 2 additions & 0 deletions pwa/tests/admin/pages/BookPage.ts
Expand Up @@ -8,6 +8,8 @@ interface FiltersProps {

export class BookPage extends AbstractPage {
public async gotoList() {
await this.registerMock();

await this.page.goto("/admin");
await this.login();
await this.page.waitForURL(/\/admin#\/admin/);
Expand Down
Binary file not shown.

0 comments on commit 920ad63

Please sign in to comment.