As the API stabilizing, we started adding tests.
This commit is contained in:
@@ -5,3 +5,4 @@
|
||||
!./docker/config/.gitignore
|
||||
./var/*
|
||||
!./var/.gitignore
|
||||
.phpunit.result.cache
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
/.idea/*
|
||||
/vendor/*
|
||||
.phpunit.result.cache
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
"*": "dist"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"test": "vendor/bin/phpunit --colors=always"
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"ext-pdo": "*",
|
||||
@@ -33,7 +36,13 @@
|
||||
"require-dev": {
|
||||
"roave/security-advisories": "dev-latest",
|
||||
"symfony/var-dumper": "^6.0",
|
||||
"perftools/php-profiler": "^1.0"
|
||||
"perftools/php-profiler": "^1.0",
|
||||
"phpunit/phpunit": "^9.5"
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
|
||||
1955
composer.lock
generated
1955
composer.lock
generated
File diff suppressed because it is too large
Load Diff
11
phpunit.xml.dist
Normal file
11
phpunit.xml.dist
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.5/phpunit.xsd"
|
||||
bootstrap="tests/bootstrap.php" colors="true">
|
||||
<testsuites>
|
||||
<testsuite name="WatchState Test Suite">
|
||||
<directory>tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<logging/>
|
||||
</phpunit>
|
||||
@@ -2,15 +2,7 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Libs\Config;
|
||||
use App\Libs\Container;
|
||||
use App\Libs\HttpException;
|
||||
use App\Libs\Servers\ServerInterface;
|
||||
use App\Libs\Storage\StorageInterface;
|
||||
use Nyholm\Psr7\Response;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('error_reporting', 'On');
|
||||
@@ -41,82 +33,8 @@ if (!file_exists(__DIR__ . '/../vendor/autoload.php')) {
|
||||
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
$fn = function (ServerRequestInterface $request): ResponseInterface {
|
||||
try {
|
||||
if (true !== Config::get('webhook.enabled', false)) {
|
||||
throw new HttpException('Webhook is disabled via config.', 500);
|
||||
}
|
||||
|
||||
if (null === Config::get('webhook.apikey', null)) {
|
||||
throw new HttpException('No webhook.apikey is set in config.', 500);
|
||||
}
|
||||
|
||||
// -- get apikey from header or query.
|
||||
$apikey = $request->getHeaderLine('x-apikey');
|
||||
if (empty($apikey)) {
|
||||
$apikey = ag($request->getQueryParams(), 'apikey', '');
|
||||
if (empty($apikey)) {
|
||||
throw new HttpException('No API key was given.', 400);
|
||||
}
|
||||
}
|
||||
|
||||
if (!hash_equals(Config::get('webhook.apikey'), $apikey)) {
|
||||
throw new HttpException('Invalid API key was given.', 401);
|
||||
}
|
||||
|
||||
if (null === ($type = ag($request->getQueryParams(), 'type', null))) {
|
||||
throw new HttpException('No type was given via type= query.', 400);
|
||||
}
|
||||
|
||||
$types = Config::get('supported', []);
|
||||
|
||||
if (null === ($backend = ag($types, $type))) {
|
||||
throw new HttpException('Invalid server type was given.', 400);
|
||||
}
|
||||
|
||||
$class = new ReflectionClass($backend);
|
||||
|
||||
if (!$class->implementsInterface(ServerInterface::class)) {
|
||||
throw new HttpException('Invalid Parser Class.', 500);
|
||||
}
|
||||
|
||||
/** @var ServerInterface $backend */
|
||||
$entity = $backend::parseWebhook($request);
|
||||
|
||||
if (null === $entity || !$entity->hasGuids()) {
|
||||
return new Response(status: 200, headers: ['X-Status' => 'No GUIDs.']);
|
||||
}
|
||||
|
||||
$storage = Container::get(StorageInterface::class);
|
||||
|
||||
if (null === ($backend = $storage->get($entity))) {
|
||||
$storage->insert($entity);
|
||||
return jsonResponse(status: 200, body: $entity->getAll());
|
||||
}
|
||||
|
||||
if ($backend->updated > $entity->updated) {
|
||||
return new Response(
|
||||
status: 200,
|
||||
headers: ['X-Status' => 'Entity date is older than what available in storage.']
|
||||
);
|
||||
}
|
||||
|
||||
if ($backend->apply($entity)->isChanged()) {
|
||||
$backend = $storage->update($backend);
|
||||
|
||||
return jsonResponse(status: 200, body: $backend->getAll());
|
||||
}
|
||||
|
||||
return new Response(status: 200, headers: ['X-Status' => 'Entity is unchanged.']);
|
||||
} catch (HttpException $e) {
|
||||
Container::get(LoggerInterface::class)->error($e->getMessage());
|
||||
|
||||
if (200 === $e->getCode()) {
|
||||
return new Response(status: $e->getCode(), headers: ['X-Status' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
return jsonResponse(status: $e->getCode(), body: ['error' => true, 'message' => $e->getMessage()]);
|
||||
(new App\Libs\KernelConsole())->boot()->runHttp(
|
||||
function (ServerRequestInterface $request) {
|
||||
return serveHttpRequest($request);
|
||||
}
|
||||
};
|
||||
|
||||
(new App\Libs\KernelConsole())->boot()->runHttp($fn);
|
||||
);
|
||||
|
||||
@@ -136,6 +136,12 @@ final class StateEntity implements StateInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function updateOriginal(): StateInterface
|
||||
{
|
||||
$this->data = $this->getAll();
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function isEqual(StateInterface $entity): bool
|
||||
{
|
||||
foreach ($this->getAll() as $key => $val) {
|
||||
|
||||
@@ -81,4 +81,5 @@ interface StateInterface
|
||||
*/
|
||||
public function apply(StateInterface $entity, bool $guidOnly = false): StateInterface;
|
||||
|
||||
public function updateOriginal(): StateInterface;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace App\Libs\Storage\PDO;
|
||||
|
||||
use App\Libs\Container;
|
||||
use App\Libs\Entity\StateInterface;
|
||||
use App\Libs\Storage\StorageException;
|
||||
use App\Libs\Storage\StorageInterface;
|
||||
use Closure;
|
||||
use DateTimeInterface;
|
||||
@@ -14,7 +15,6 @@ use PDO;
|
||||
use PDOException;
|
||||
use PDOStatement;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use RuntimeException;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
@@ -28,38 +28,26 @@ final class PDOAdapter implements StorageInterface
|
||||
];
|
||||
|
||||
private PDO|null $pdo = null;
|
||||
private string|null $driver = null;
|
||||
private bool $viaCommit = false;
|
||||
|
||||
private PDOStatement|null $stmtInsert = null;
|
||||
private PDOStatement|null $stmtUpdate = null;
|
||||
private PDOStatement|null $stmtDelete = null;
|
||||
/**
|
||||
* Cache Prepared Statements.
|
||||
*
|
||||
* @var array<array-key, PDOStatement>
|
||||
*/
|
||||
private array $stmt = [
|
||||
'insert' => null,
|
||||
'update' => null,
|
||||
];
|
||||
|
||||
public function __construct(private LoggerInterface $logger)
|
||||
{
|
||||
}
|
||||
|
||||
public function getAll(DateTimeInterface|null $date = null): array
|
||||
{
|
||||
$arr = [];
|
||||
|
||||
$sql = "SELECT * FROM state";
|
||||
|
||||
if (null !== $date) {
|
||||
$sql .= ' WHERE updated > ' . $date->getTimestamp();
|
||||
}
|
||||
|
||||
foreach ($this->pdo->query($sql) as $row) {
|
||||
$arr[] = Container::get(StateInterface::class)::fromArray($row);
|
||||
}
|
||||
|
||||
return $arr;
|
||||
}
|
||||
|
||||
public function setUp(array $opts): StorageInterface
|
||||
{
|
||||
if (null === ($opts['dsn'] ?? null)) {
|
||||
throw new RuntimeException('No storage.opts.dsn (Data Source Name) was provided.');
|
||||
throw new StorageException('No storage.opts.dsn (Data Source Name) was provided.', 10);
|
||||
}
|
||||
|
||||
$this->pdo = new PDO(
|
||||
@@ -75,13 +63,13 @@ final class PDOAdapter implements StorageInterface
|
||||
)
|
||||
);
|
||||
|
||||
$this->driver = $this->getDriver();
|
||||
$driver = $this->getDriver();
|
||||
|
||||
if (!in_array($this->driver, $this->supported)) {
|
||||
throw new RuntimeException(sprintf('%s Driver is not supported.', $this->driver));
|
||||
if (!in_array($driver, $this->supported)) {
|
||||
throw new StorageException(sprintf('%s Driver is not supported.', $driver), 11);
|
||||
}
|
||||
|
||||
if (null !== ($exec = ag($opts, "exec.{$this->driver}")) && is_array($exec)) {
|
||||
if (null !== ($exec = ag($opts, "exec.{$driver}")) && is_array($exec)) {
|
||||
foreach ($exec as $cmd) {
|
||||
$this->pdo->exec($cmd);
|
||||
}
|
||||
@@ -90,17 +78,10 @@ final class PDOAdapter implements StorageInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setLogger(LoggerInterface $logger): StorageInterface
|
||||
{
|
||||
$this->logger = $logger;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function insert(StateInterface $entity): StateInterface
|
||||
{
|
||||
if (null === $this->pdo) {
|
||||
throw new RuntimeException('Setup(): method was not called.');
|
||||
throw new StorageException('Setup(): method was not called.', StorageException::SETUP_NOT_CALLED);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -111,24 +92,24 @@ final class PDOAdapter implements StorageInterface
|
||||
}
|
||||
|
||||
if (null !== $data['id']) {
|
||||
throw new RuntimeException(
|
||||
sprintf('Trying to insert already saved entity #%s', $data['id'])
|
||||
throw new StorageException(
|
||||
sprintf('Trying to insert already saved entity #%s', $data['id']), 21
|
||||
);
|
||||
}
|
||||
|
||||
unset($data['id']);
|
||||
|
||||
if (null === $this->stmtInsert) {
|
||||
$this->stmtInsert = $this->pdo->prepare(
|
||||
$this->pdoInsert('state', array_keys($data))
|
||||
if (null === ($this->stmt['insert'] ?? null)) {
|
||||
$this->stmt['insert'] = $this->pdo->prepare(
|
||||
$this->pdoInsert('state', StateInterface::ENTITY_KEYS)
|
||||
);
|
||||
}
|
||||
|
||||
$this->stmtInsert->execute($data);
|
||||
$this->stmt['insert']->execute($data);
|
||||
|
||||
$entity->id = (int)$this->pdo->lastInsertId();
|
||||
} catch (PDOException $e) {
|
||||
$this->stmtInsert = null;
|
||||
$this->stmt['insert'] = null;
|
||||
if (false === $this->viaCommit) {
|
||||
$this->logger->error($e->getMessage(), $entity->meta ?? []);
|
||||
return $entity;
|
||||
@@ -136,13 +117,56 @@ final class PDOAdapter implements StorageInterface
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return $entity;
|
||||
return $entity->updateOriginal();
|
||||
}
|
||||
|
||||
public function get(StateInterface $entity): StateInterface|null
|
||||
{
|
||||
if (null === $this->pdo) {
|
||||
throw new StorageException('Setup(): method was not called.', StorageException::SETUP_NOT_CALLED);
|
||||
}
|
||||
|
||||
$arr = array_intersect_key(
|
||||
$entity->getAll(),
|
||||
array_flip(StateInterface::ENTITY_GUIDS)
|
||||
);
|
||||
|
||||
if (null !== $entity->id) {
|
||||
$arr['id'] = $entity->id;
|
||||
}
|
||||
|
||||
return $this->matchAnyId($arr, $entity);
|
||||
}
|
||||
|
||||
public function getAll(DateTimeInterface|null $date = null, StateInterface|null $class = null): array
|
||||
{
|
||||
if (null === $this->pdo) {
|
||||
throw new StorageException('Setup(): method was not called.', StorageException::SETUP_NOT_CALLED);
|
||||
}
|
||||
|
||||
$arr = [];
|
||||
|
||||
$sql = 'SELECT * FROM state';
|
||||
|
||||
if (null !== $date) {
|
||||
$sql .= ' WHERE updated > ' . $date->getTimestamp();
|
||||
}
|
||||
|
||||
if (null === $class) {
|
||||
$class = Container::get(StateInterface::class);
|
||||
}
|
||||
|
||||
foreach ($this->pdo->query($sql) as $row) {
|
||||
$arr[] = $class::fromArray($row);
|
||||
}
|
||||
|
||||
return $arr;
|
||||
}
|
||||
|
||||
public function update(StateInterface $entity): StateInterface
|
||||
{
|
||||
if (null === $this->pdo) {
|
||||
throw new RuntimeException('Setup(): method was not called.');
|
||||
throw new StorageException('Setup(): method was not called.', StorageException::SETUP_NOT_CALLED);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -153,16 +177,18 @@ final class PDOAdapter implements StorageInterface
|
||||
}
|
||||
|
||||
if (null === $data['id']) {
|
||||
throw new RuntimeException('Trying to update unsaved entity');
|
||||
throw new StorageException('Trying to update unsaved entity', 51);
|
||||
}
|
||||
|
||||
if (null === $this->stmtUpdate) {
|
||||
$this->stmtUpdate = $this->pdo->prepare($this->pdoUpdate('state', array_keys($data)));
|
||||
if (null === ($this->stmt['update'] ?? null)) {
|
||||
$this->stmt['update'] = $this->pdo->prepare(
|
||||
$this->pdoUpdate('state', StateInterface::ENTITY_KEYS)
|
||||
);
|
||||
}
|
||||
|
||||
$this->stmtUpdate->execute($data);
|
||||
$this->stmt['update']->execute($data);
|
||||
} catch (PDOException $e) {
|
||||
$this->stmtUpdate = null;
|
||||
$this->stmt['update'] = null;
|
||||
if (false === $this->viaCommit) {
|
||||
$this->logger->error($e->getMessage(), $entity->meta ?? []);
|
||||
return $entity;
|
||||
@@ -170,31 +196,35 @@ final class PDOAdapter implements StorageInterface
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return $entity;
|
||||
return $entity->updateOriginal();
|
||||
}
|
||||
|
||||
public function get(StateInterface $entity): StateInterface|null
|
||||
public function matchAnyId(array $ids, StateInterface|null $class = null): StateInterface|null
|
||||
{
|
||||
if (null === $this->pdo) {
|
||||
throw new RuntimeException('Setup(): method was not called.');
|
||||
throw new StorageException('Setup(): method was not called.', StorageException::SETUP_NOT_CALLED);
|
||||
}
|
||||
|
||||
if (null !== $entity->id) {
|
||||
$stmt = $this->pdo->query("SELECT * FROM state WHERE id = " . (int)$entity->id);
|
||||
if (null === $class) {
|
||||
$class = Container::get(StateInterface::class);
|
||||
}
|
||||
|
||||
if (null !== ($ids['id'] ?? null)) {
|
||||
$stmt = $this->pdo->query("SELECT * FROM state WHERE id = " . (int)$ids['id']);
|
||||
|
||||
if (false === ($row = $stmt->fetch(PDO::FETCH_ASSOC))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Container::get(StateInterface::class)::fromArray($row);
|
||||
return $class::fromArray($row);
|
||||
}
|
||||
|
||||
$cond = $where = [];
|
||||
foreach (StateInterface::ENTITY_GUIDS as $key) {
|
||||
if (null === $entity->{$key}) {
|
||||
if (null === ($ids[$key] ?? null)) {
|
||||
continue;
|
||||
}
|
||||
$cond[$key] = $entity->{$key};
|
||||
$cond[$key] = $ids[$key];
|
||||
}
|
||||
|
||||
if (empty($cond)) {
|
||||
@@ -202,106 +232,47 @@ final class PDOAdapter implements StorageInterface
|
||||
}
|
||||
|
||||
foreach ($cond as $key => $_) {
|
||||
$where[] = $this->escapeIdentifier($key) . ' = :' . $key;
|
||||
$where[] = $key . ' = :' . $key;
|
||||
}
|
||||
|
||||
$sqlWhere = implode(' OR ', $where);
|
||||
|
||||
$stmt = $this->pdo->prepare(
|
||||
sprintf(
|
||||
"SELECT * FROM %s WHERE %s LIMIT 1",
|
||||
$this->escapeIdentifier('state'),
|
||||
$sqlWhere
|
||||
)
|
||||
);
|
||||
$cachedKey = md5($sqlWhere);
|
||||
|
||||
if (false === $stmt->execute($cond)) {
|
||||
throw new RuntimeException('Unable to prepare sql statement');
|
||||
}
|
||||
try {
|
||||
if (null === ($this->stmt[$cachedKey] ?? null)) {
|
||||
$this->stmt[$cachedKey] = $this->pdo->prepare("SELECT * FROM state WHERE {$sqlWhere}");
|
||||
}
|
||||
|
||||
if (false === ($row = $stmt->fetch(PDO::FETCH_ASSOC))) {
|
||||
return null;
|
||||
}
|
||||
if (false === $this->stmt[$cachedKey]->execute($cond)) {
|
||||
$this->stmt[$cachedKey] = null;
|
||||
throw new StorageException('Failed to execute sql query.', 61);
|
||||
}
|
||||
|
||||
return Container::get(StateInterface::class)::fromArray($row);
|
||||
}
|
||||
|
||||
public function matchAnyId(array $ids): StateInterface|null
|
||||
{
|
||||
if (null === $this->pdo) {
|
||||
throw new RuntimeException('Setup(): method was not called.');
|
||||
}
|
||||
|
||||
if (null !== ($ids['id'] ?? null)) {
|
||||
$stmt = $this->pdo->prepare(
|
||||
sprintf(
|
||||
'SELECT * FROM %s WHERE %s = :id LIMIT 1',
|
||||
$this->escapeIdentifier('state'),
|
||||
$this->escapeIdentifier('id'),
|
||||
)
|
||||
);
|
||||
|
||||
if (false === ($stmt->execute(['id' => $ids['id']]))) {
|
||||
if (false === ($row = $this->stmt[$cachedKey]->fetch(PDO::FETCH_ASSOC))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (false === ($row = $stmt->fetch(PDO::FETCH_ASSOC))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Container::get(StateInterface::class)::fromArray($row);
|
||||
return $class::fromArray($row);
|
||||
} catch (PDOException|StorageException $e) {
|
||||
$this->stmt[$cachedKey] = null;
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$cond = $where = [];
|
||||
|
||||
foreach ($ids as $_val) {
|
||||
if (null === $_val || !str_starts_with($_val, 'guid_')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
[$key, $val] = explode('://', $_val);
|
||||
|
||||
$cond[$key] = $val;
|
||||
}
|
||||
|
||||
if (empty($cond)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($cond as $key => $_) {
|
||||
$where[] = $this->escapeIdentifier($key) . ' = :' . $key;
|
||||
}
|
||||
|
||||
$sqlWhere = implode(' OR ', $where);
|
||||
|
||||
$stmt = $this->pdo->prepare(
|
||||
sprintf(
|
||||
"SELECT * FROM %s WHERE %s LIMIT 1",
|
||||
$this->escapeIdentifier('state'),
|
||||
$sqlWhere
|
||||
)
|
||||
);
|
||||
|
||||
if (false === $stmt->execute($cond)) {
|
||||
throw new RuntimeException('Unable to prepare sql statement');
|
||||
}
|
||||
|
||||
if (false === ($row = $stmt->fetch(PDO::FETCH_ASSOC))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Container::get(StateInterface::class)::fromArray($row);
|
||||
}
|
||||
|
||||
public function remove(StateInterface $entity): bool
|
||||
{
|
||||
if (null === $this->pdo) {
|
||||
throw new StorageException('Setup(): method was not called.', StorageException::SETUP_NOT_CALLED);
|
||||
}
|
||||
|
||||
if (null === $entity->id && !$entity->hasGuids()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
if (null === $entity->id) {
|
||||
if (null === $dbEntity = $this->get($entity)) {
|
||||
if (null === ($dbEntity = $this->get($entity))) {
|
||||
return false;
|
||||
}
|
||||
$id = $dbEntity->id;
|
||||
@@ -309,20 +280,9 @@ final class PDOAdapter implements StorageInterface
|
||||
$id = $entity->id;
|
||||
}
|
||||
|
||||
if (null === $this->stmtDelete) {
|
||||
$this->stmtDelete = $this->pdo->prepare(
|
||||
sprintf(
|
||||
'DELETE FROM %s WHERE %s = :id',
|
||||
$this->escapeIdentifier('state'),
|
||||
$this->escapeIdentifier('id'),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$this->stmtDelete->execute(['id' => $id]);
|
||||
$this->pdo->query('DELETE FROM state WHERE id = ' . (int)$id);
|
||||
} catch (PDOException $e) {
|
||||
$this->logger->error($e->getMessage());
|
||||
$this->stmtDelete = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -332,7 +292,7 @@ final class PDOAdapter implements StorageInterface
|
||||
public function commit(array $entities): array
|
||||
{
|
||||
if (null === $this->pdo) {
|
||||
throw new RuntimeException('Setup(): method was not called.');
|
||||
throw new StorageException('Setup(): method was not called.', StorageException::SETUP_NOT_CALLED);
|
||||
}
|
||||
|
||||
return $this->transactional(function () use ($entities) {
|
||||
@@ -377,6 +337,45 @@ final class PDOAdapter implements StorageInterface
|
||||
});
|
||||
}
|
||||
|
||||
public function migrations(string $dir, InputInterface $input, OutputInterface $output, array $opts = []): mixed
|
||||
{
|
||||
if (null === $this->pdo) {
|
||||
throw new StorageException('Setup(): method was not called.', StorageException::SETUP_NOT_CALLED);
|
||||
}
|
||||
|
||||
$class = new PDOMigrations($this->pdo);
|
||||
|
||||
return match (strtolower($dir)) {
|
||||
StorageInterface::MIGRATE_UP => $class->up($input, $output),
|
||||
StorageInterface::MIGRATE_DOWN => $class->down($output),
|
||||
default => throw new StorageException(sprintf('Unknown direction \'%s\' was given.', $dir), 91),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function makeMigration(string $name, OutputInterface $output, array $opts = []): mixed
|
||||
{
|
||||
if (null === $this->pdo) {
|
||||
throw new StorageException('Setup(): method was not called.');
|
||||
}
|
||||
|
||||
return (new PDOMigrations($this->pdo))->make($name, $output);
|
||||
}
|
||||
|
||||
public function maintenance(InputInterface $input, OutputInterface $output, array $opts = []): mixed
|
||||
{
|
||||
return (new PDOMigrations($this->pdo))->runMaintenance();
|
||||
}
|
||||
|
||||
public function setLogger(LoggerInterface $logger): StorageInterface
|
||||
{
|
||||
$this->logger = $logger;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap Transaction.
|
||||
*
|
||||
@@ -409,6 +408,22 @@ final class PDOAdapter implements StorageInterface
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get PDO Driver.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getDriver(): string
|
||||
{
|
||||
$driver = $this->pdo->getAttribute($this->pdo::ATTR_DRIVER_NAME);
|
||||
|
||||
if (empty($driver) || !is_string($driver)) {
|
||||
$driver = 'unknown';
|
||||
}
|
||||
|
||||
return strtolower($driver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate SQL Insert Statement.
|
||||
*
|
||||
@@ -418,7 +433,7 @@ final class PDOAdapter implements StorageInterface
|
||||
*/
|
||||
private function pdoInsert(string $table, array $columns): string
|
||||
{
|
||||
$queryString = 'INSERT INTO ' . $this->escapeIdentifier($table) . ' (%{columns}) VALUES(%{values})';
|
||||
$queryString = "INSERT INTO {$table} (%(columns)) VALUES(%(values))";
|
||||
|
||||
$sql_columns = $sql_placeholder = [];
|
||||
|
||||
@@ -427,12 +442,12 @@ final class PDOAdapter implements StorageInterface
|
||||
continue;
|
||||
}
|
||||
|
||||
$sql_columns[] = $this->escapeIdentifier($column, true);
|
||||
$sql_placeholder[] = ':' . $this->escapeIdentifier($column, false);
|
||||
$sql_columns[] = $column;
|
||||
$sql_placeholder[] = ':' . $column;
|
||||
}
|
||||
|
||||
$queryString = str_replace(
|
||||
['%{columns}', '%{values}'],
|
||||
['%(columns)', '%(values)'],
|
||||
[implode(', ', $sql_columns), implode(', ', $sql_placeholder)],
|
||||
$queryString
|
||||
);
|
||||
@@ -449,11 +464,8 @@ final class PDOAdapter implements StorageInterface
|
||||
*/
|
||||
private function pdoUpdate(string $table, array $columns): string
|
||||
{
|
||||
$queryString = sprintf(
|
||||
'UPDATE %s SET ${place} = ${holder} WHERE %s = :id',
|
||||
$this->escapeIdentifier($table, true),
|
||||
$this->escapeIdentifier('id', true)
|
||||
);
|
||||
/** @noinspection SqlWithoutWhere */
|
||||
$queryString = "UPDATE {$table} SET %(place) = %(holder) WHERE id = :id";
|
||||
|
||||
$placeholders = [];
|
||||
|
||||
@@ -461,95 +473,14 @@ final class PDOAdapter implements StorageInterface
|
||||
if ('id' === $column) {
|
||||
continue;
|
||||
}
|
||||
$placeholders[] = sprintf(
|
||||
'%1$s = :%2$s',
|
||||
$this->escapeIdentifier($column, true),
|
||||
$this->escapeIdentifier($column, false)
|
||||
);
|
||||
$placeholders[] = sprintf('%1$s = :%1$s', $column);
|
||||
}
|
||||
|
||||
return trim(str_replace('${place} = ${holder}', implode(', ', $placeholders), $queryString));
|
||||
}
|
||||
|
||||
private function escapeIdentifier(string $text, bool $quote = true): string
|
||||
{
|
||||
// table or column has to be valid ASCII name.
|
||||
// this is opinionated, but we only allow [a-zA-Z0-9_] in column/table name.
|
||||
if (!preg_match('#\w#', $text)) {
|
||||
throw new RuntimeException(
|
||||
sprintf(
|
||||
'Invalid identifier "%s": Column/table must be valid ASCII code.',
|
||||
$text
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// The first character cannot be [0-9]:
|
||||
if (preg_match('/^\d/', $text)) {
|
||||
throw new RuntimeException(
|
||||
sprintf(
|
||||
'Invalid identifier "%s": Must begin with a letter or underscore.',
|
||||
$text
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!$quote) {
|
||||
return $text;
|
||||
}
|
||||
|
||||
return match ($this->driver) {
|
||||
'mssql' => '[' . $text . ']',
|
||||
'mysql' => '`' . $text . '`',
|
||||
default => '"' . $text . '"',
|
||||
};
|
||||
return trim(str_replace('%(place) = %(holder)', implode(', ', $placeholders), $queryString));
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
$this->stmtDelete = $this->stmtUpdate = $this->stmtInsert = null;
|
||||
}
|
||||
|
||||
public function migrations(string $dir, InputInterface $input, OutputInterface $output, array $opts = []): mixed
|
||||
{
|
||||
if (null === $this->pdo) {
|
||||
throw new RuntimeException('Setup(): method was not called.');
|
||||
}
|
||||
|
||||
$class = new PDOMigrations($this->pdo);
|
||||
|
||||
return match ($dir) {
|
||||
StorageInterface::MIGRATE_UP => $class->up($input, $output),
|
||||
StorageInterface::MIGRATE_DOWN => $class->down($output),
|
||||
default => throw new RuntimeException(sprintf('Unknown direction \'%s\' was given.', $dir)),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function makeMigration(string $name, OutputInterface $output, array $opts = []): void
|
||||
{
|
||||
if (null === $this->pdo) {
|
||||
throw new RuntimeException('Setup(): method was not called.');
|
||||
}
|
||||
|
||||
(new PDOMigrations($this->pdo))->make($name, $output);
|
||||
}
|
||||
|
||||
public function maintenance(InputInterface $input, OutputInterface $output, array $opts = []): mixed
|
||||
{
|
||||
return (new PDOMigrations($this->pdo))->runMaintenance();
|
||||
}
|
||||
|
||||
private function getDriver(): string
|
||||
{
|
||||
$driver = $this->pdo->getAttribute($this->pdo::ATTR_DRIVER_NAME);
|
||||
|
||||
if (empty($driver) || !is_string($driver)) {
|
||||
$driver = 'unknown';
|
||||
}
|
||||
|
||||
return strtolower($driver);
|
||||
$this->stmt = [];
|
||||
}
|
||||
}
|
||||
|
||||
12
src/Libs/Storage/StorageException.php
Normal file
12
src/Libs/Storage/StorageException.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Libs\Storage;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class StorageException extends RuntimeException
|
||||
{
|
||||
public const SETUP_NOT_CALLED = 1;
|
||||
}
|
||||
@@ -25,14 +25,6 @@ interface StorageInterface
|
||||
*/
|
||||
public function setUp(array $opts): self;
|
||||
|
||||
/**
|
||||
* Inject Logger.
|
||||
*
|
||||
* @param LoggerInterface $logger
|
||||
* @return $this
|
||||
*/
|
||||
public function setLogger(LoggerInterface $logger): self;
|
||||
|
||||
/**
|
||||
* Insert Entity immediately.
|
||||
*
|
||||
@@ -42,6 +34,25 @@ interface StorageInterface
|
||||
*/
|
||||
public function insert(StateInterface $entity): StateInterface;
|
||||
|
||||
/**
|
||||
* Get Entity.
|
||||
*
|
||||
* @param StateInterface $entity
|
||||
*
|
||||
* @return StateInterface|null
|
||||
*/
|
||||
public function get(StateInterface $entity): StateInterface|null;
|
||||
|
||||
/**
|
||||
* Load entities from backend.
|
||||
*
|
||||
* @param DateTimeInterface|null $date Get Entities That has changed since given time, if null get all.
|
||||
* @param StateInterface|null $class Create objects based on given class, if null use default class.
|
||||
*
|
||||
* @return array<StateInterface>
|
||||
*/
|
||||
public function getAll(DateTimeInterface|null $date = null, StateInterface|null $class = null): array;
|
||||
|
||||
/**
|
||||
* Update Entity immediately.
|
||||
*
|
||||
@@ -52,13 +63,14 @@ interface StorageInterface
|
||||
public function update(StateInterface $entity): StateInterface;
|
||||
|
||||
/**
|
||||
* Get Entity.
|
||||
* Get Entity Using array of ids.
|
||||
*
|
||||
* @param StateInterface $entity
|
||||
* @param array $ids
|
||||
* @param StateInterface|null $class Create object based on given class, if null use default class.
|
||||
*
|
||||
* @return StateInterface|null
|
||||
*/
|
||||
public function get(StateInterface $entity): StateInterface|null;
|
||||
public function matchAnyId(array $ids, StateInterface|null $class = null): StateInterface|null;
|
||||
|
||||
/**
|
||||
* Remove Entity.
|
||||
@@ -78,24 +90,6 @@ interface StorageInterface
|
||||
*/
|
||||
public function commit(array $entities): array;
|
||||
|
||||
/**
|
||||
* Load All Entities From backend.
|
||||
*
|
||||
* @param DateTimeInterface|null $date Get Entities That has changed since given time.
|
||||
*
|
||||
* @return array<StateInterface>
|
||||
*/
|
||||
public function getAll(DateTimeInterface|null $date = null): array;
|
||||
|
||||
/**
|
||||
* Get Entity Using array of ids.
|
||||
*
|
||||
* @param array $ids
|
||||
*
|
||||
* @return StateInterface|null
|
||||
*/
|
||||
public function matchAnyId(array $ids): StateInterface|null;
|
||||
|
||||
/**
|
||||
* Migrate Backend Storage Schema.
|
||||
*
|
||||
@@ -124,6 +118,17 @@ interface StorageInterface
|
||||
* @param string $name
|
||||
* @param OutputInterface $output
|
||||
* @param array $opts
|
||||
*
|
||||
* @return mixed can return migration file name in supported cases.
|
||||
*/
|
||||
public function makeMigration(string $name, OutputInterface $output, array $opts = []): void;
|
||||
public function makeMigration(string $name, OutputInterface $output, array $opts = []): mixed;
|
||||
|
||||
/**
|
||||
* Inject Logger.
|
||||
*
|
||||
* @param LoggerInterface $logger
|
||||
* @return $this
|
||||
*/
|
||||
public function setLogger(LoggerInterface $logger): self;
|
||||
|
||||
}
|
||||
|
||||
@@ -3,10 +3,15 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Libs\Config;
|
||||
use App\Libs\Container;
|
||||
use App\Libs\Extends\Date;
|
||||
use App\Libs\HttpException;
|
||||
use App\Libs\Servers\ServerInterface;
|
||||
use App\Libs\Storage\StorageInterface;
|
||||
use Nyholm\Psr7\Response;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
|
||||
|
||||
@@ -112,7 +117,7 @@ if (!function_exists('ag_set')) {
|
||||
if (is_array($at)) {
|
||||
$at[array_shift($keys)] = $value;
|
||||
} else {
|
||||
throw new RuntimeException("Can not set value at this path ($path) because is not array.");
|
||||
throw new RuntimeException("Can not set value at this path ($path) because its not array.");
|
||||
}
|
||||
} else {
|
||||
$path = array_shift($keys);
|
||||
@@ -268,3 +273,87 @@ if (!function_exists('httpClientChunks')) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('serveHttpRequest')) {
|
||||
/**
|
||||
* @throws ReflectionException
|
||||
*/
|
||||
function serveHttpRequest(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
try {
|
||||
if (true !== Config::get('webhook.enabled', false)) {
|
||||
throw new HttpException('Webhook is disabled via config.', 500);
|
||||
}
|
||||
|
||||
if (null === Config::get('webhook.apikey', null)) {
|
||||
throw new HttpException('No webhook.apikey is set in config.', 500);
|
||||
}
|
||||
|
||||
// -- get apikey from header or query.
|
||||
$apikey = $request->getHeaderLine('x-apikey');
|
||||
if (empty($apikey)) {
|
||||
$apikey = ag($request->getQueryParams(), 'apikey', '');
|
||||
if (empty($apikey)) {
|
||||
throw new HttpException('No API key was given.', 400);
|
||||
}
|
||||
}
|
||||
|
||||
if (!hash_equals(Config::get('webhook.apikey'), $apikey)) {
|
||||
throw new HttpException('Invalid API key was given.', 401);
|
||||
}
|
||||
|
||||
if (null === ($type = ag($request->getQueryParams(), 'type', null))) {
|
||||
throw new HttpException('No type was given via type= query.', 400);
|
||||
}
|
||||
|
||||
$types = Config::get('supported', []);
|
||||
|
||||
if (null === ($backend = ag($types, $type))) {
|
||||
throw new HttpException('Invalid server type was given.', 400);
|
||||
}
|
||||
|
||||
$class = new ReflectionClass($backend);
|
||||
|
||||
if (!$class->implementsInterface(ServerInterface::class)) {
|
||||
throw new HttpException('Invalid Parser Class.', 500);
|
||||
}
|
||||
|
||||
/** @var ServerInterface $backend */
|
||||
$entity = $backend::parseWebhook($request);
|
||||
|
||||
if (null === $entity || !$entity->hasGuids()) {
|
||||
return new Response(status: 200, headers: ['X-Status' => 'No GUIDs.']);
|
||||
}
|
||||
|
||||
$storage = Container::get(StorageInterface::class);
|
||||
|
||||
if (null === ($backend = $storage->get($entity))) {
|
||||
$storage->insert($entity);
|
||||
return jsonResponse(status: 200, body: $entity->getAll());
|
||||
}
|
||||
|
||||
if ($backend->updated > $entity->updated) {
|
||||
return new Response(
|
||||
status: 200,
|
||||
headers: ['X-Status' => 'Entity date is older than what available in storage.']
|
||||
);
|
||||
}
|
||||
|
||||
if ($backend->apply($entity)->isChanged()) {
|
||||
$backend = $storage->update($backend);
|
||||
|
||||
return jsonResponse(status: 200, body: $backend->getAll());
|
||||
}
|
||||
|
||||
return new Response(status: 200, headers: ['X-Status' => 'Entity is unchanged.']);
|
||||
} catch (HttpException $e) {
|
||||
Container::get(LoggerInterface::class)->error($e->getMessage());
|
||||
|
||||
if (200 === $e->getCode()) {
|
||||
return new Response(status: $e->getCode(), headers: ['X-Status' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
return jsonResponse(status: $e->getCode(), body: ['error' => true, 'message' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
313
tests/Storage/PDOAdapterTest.php
Normal file
313
tests/Storage/PDOAdapterTest.php
Normal file
@@ -0,0 +1,313 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Storage;
|
||||
|
||||
use App\Libs\Entity\StateEntity;
|
||||
use App\Libs\Entity\StateInterface;
|
||||
use App\Libs\Extends\CliLogger;
|
||||
use App\Libs\Storage\PDO\PDOAdapter;
|
||||
use App\Libs\Storage\StorageException;
|
||||
use App\Libs\Storage\StorageInterface;
|
||||
use DateTimeImmutable;
|
||||
use Error;
|
||||
use PDOException;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Output\NullOutput;
|
||||
|
||||
class PDOAdapterTest extends TestCase
|
||||
{
|
||||
private array $testEpisode = [
|
||||
'id' => null,
|
||||
'type' => StateInterface::TYPE_EPISODE,
|
||||
'updated' => 0,
|
||||
'watched' => 1,
|
||||
'meta' => [],
|
||||
'guid_plex' => StateInterface::TYPE_EPISODE . '/1',
|
||||
'guid_imdb' => StateInterface::TYPE_EPISODE . '/2',
|
||||
'guid_tvdb' => StateInterface::TYPE_EPISODE . '/3',
|
||||
'guid_tmdb' => StateInterface::TYPE_EPISODE . '/4',
|
||||
'guid_tvmaze' => StateInterface::TYPE_EPISODE . '/5',
|
||||
'guid_tvrage' => StateInterface::TYPE_EPISODE . '/6',
|
||||
'guid_anidb' => StateInterface::TYPE_EPISODE . '/7',
|
||||
];
|
||||
|
||||
private array $testMovie = [
|
||||
'id' => null,
|
||||
'type' => StateInterface::TYPE_MOVIE,
|
||||
'updated' => 1,
|
||||
'watched' => 1,
|
||||
'meta' => [],
|
||||
'guid_plex' => StateInterface::TYPE_MOVIE . '/10',
|
||||
'guid_imdb' => StateInterface::TYPE_MOVIE . '/20',
|
||||
'guid_tvdb' => StateInterface::TYPE_MOVIE . '/30',
|
||||
'guid_tmdb' => StateInterface::TYPE_MOVIE . '/40',
|
||||
'guid_tvmaze' => StateInterface::TYPE_MOVIE . '/50',
|
||||
'guid_tvrage' => StateInterface::TYPE_MOVIE . '/60',
|
||||
'guid_anidb' => StateInterface::TYPE_MOVIE . '/70',
|
||||
];
|
||||
|
||||
private StorageInterface|null $storage = null;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->output = new NullOutput();
|
||||
$this->input = new ArrayInput([]);
|
||||
|
||||
$this->storage = new PDOAdapter(new CliLogger($this->output));
|
||||
$this->storage->setUp(['dsn' => 'sqlite::memory:']);
|
||||
$this->storage->migrations('up', $this->input, $this->output);
|
||||
}
|
||||
|
||||
/** StorageInterface::setUp */
|
||||
public function test_setup_throw_exception_if_no_dsn(): void
|
||||
{
|
||||
$this->expectException(StorageException::class);
|
||||
$this->expectExceptionCode(10);
|
||||
$storage = new PDOAdapter(new CliLogger($this->output));
|
||||
$storage->setUp([]);
|
||||
}
|
||||
|
||||
public function test_setup_throw_exception_if_invalid_dsn(): void
|
||||
{
|
||||
$this->expectException(PDOException::class);
|
||||
$storage = new PDOAdapter(new CliLogger($this->output));
|
||||
$storage->setUp(['dsn' => 'not_real_driver::foo']);
|
||||
}
|
||||
|
||||
/** StorageInterface::insert */
|
||||
public function test_insert_call_without_setup_exception(): void
|
||||
{
|
||||
$this->expectException(StorageException::class);
|
||||
$this->expectExceptionCode(StorageException::SETUP_NOT_CALLED);
|
||||
$storage = new PDOAdapter(new CliLogger($this->output));
|
||||
|
||||
$storage->insert(new StateEntity([]));
|
||||
}
|
||||
|
||||
public function test_insert_throw_exception_if_has_id(): void
|
||||
{
|
||||
$this->expectException(StorageException::class);
|
||||
$this->expectExceptionCode(21);
|
||||
$item = new StateEntity($this->testEpisode);
|
||||
$this->storage->insert($item);
|
||||
$this->storage->insert($item);
|
||||
}
|
||||
|
||||
public function test_insert_successful(): void
|
||||
{
|
||||
$item = $this->storage->insert(new StateEntity($this->testEpisode));
|
||||
$this->assertSame(1, $item->id);
|
||||
}
|
||||
|
||||
/** StorageInterface::get */
|
||||
public function test_get_call_without_setup_exception(): void
|
||||
{
|
||||
$this->expectException(StorageException::class);
|
||||
$this->expectExceptionCode(StorageException::SETUP_NOT_CALLED);
|
||||
$storage = new PDOAdapter(new CliLogger($this->output));
|
||||
$storage->get(new StateEntity([]));
|
||||
}
|
||||
|
||||
public function test_get_conditions(): void
|
||||
{
|
||||
$item = new StateEntity($this->testEpisode);
|
||||
|
||||
// -- db should be empty at this stage. as such we expect null.
|
||||
$this->assertNull($this->storage->get($item));
|
||||
|
||||
// -- insert and return object and assert it's the same
|
||||
$modified = $this->storage->insert(clone $item);
|
||||
|
||||
$this->assertSame($modified->getAll(), $this->storage->get($item)->getAll());
|
||||
|
||||
// -- look up based on id
|
||||
$this->assertSame($modified->getAll(), $this->storage->get($modified)->getAll());
|
||||
}
|
||||
|
||||
/** StorageInterface::getAll */
|
||||
public function test_getAll_call_without_setup_exception(): void
|
||||
{
|
||||
$this->expectException(StorageException::class);
|
||||
$this->expectExceptionCode(StorageException::SETUP_NOT_CALLED);
|
||||
$storage = new PDOAdapter(new CliLogger($this->output));
|
||||
$storage->getAll();
|
||||
}
|
||||
|
||||
public function test_getAll_call_without_initialized_container(): void
|
||||
{
|
||||
$this->expectException(Error::class);
|
||||
$this->expectExceptionMessage('Call to a member function');
|
||||
$this->storage->getAll();
|
||||
}
|
||||
|
||||
public function test_getAll_conditions(): void
|
||||
{
|
||||
$item = new StateEntity($this->testEpisode);
|
||||
|
||||
$this->assertSame([], $this->storage->getAll(class: $item));
|
||||
|
||||
$this->storage->insert($item);
|
||||
|
||||
$this->assertCount(1, $this->storage->getAll(class: $item));
|
||||
|
||||
// -- future date should be 0.
|
||||
$this->assertCount(0, $this->storage->getAll(date: new DateTimeImmutable('now'), class: $item));
|
||||
}
|
||||
|
||||
/** StorageInterface::update */
|
||||
public function test_update_call_without_setup_exception(): void
|
||||
{
|
||||
$this->expectException(StorageException::class);
|
||||
$this->expectExceptionCode(StorageException::SETUP_NOT_CALLED);
|
||||
$storage = new PDOAdapter(new CliLogger($this->output));
|
||||
$storage->update(new StateEntity([]));
|
||||
}
|
||||
|
||||
public function test_update_call_without_id_exception(): void
|
||||
{
|
||||
$this->expectException(StorageException::class);
|
||||
$this->expectExceptionCode(51);
|
||||
$item = new StateEntity($this->testEpisode);
|
||||
|
||||
$this->storage->update($item);
|
||||
}
|
||||
|
||||
public function test_update_conditions(): void
|
||||
{
|
||||
$item = $this->storage->insert(new StateEntity($this->testEpisode));
|
||||
$item->guid_plex = StateInterface::TYPE_EPISODE . '/1000';
|
||||
|
||||
$updatedItem = $this->storage->update($item);
|
||||
|
||||
$this->assertSame($item, $updatedItem);
|
||||
$this->assertSame($updatedItem->getAll(), $this->storage->get($item)->getAll());
|
||||
}
|
||||
|
||||
/** StorageInterface::update */
|
||||
public function test_matchAnyId_call_without_setup_exception(): void
|
||||
{
|
||||
$this->expectException(StorageException::class);
|
||||
$this->expectExceptionCode(StorageException::SETUP_NOT_CALLED);
|
||||
$storage = new PDOAdapter(new CliLogger($this->output));
|
||||
$storage->matchAnyId([]);
|
||||
}
|
||||
|
||||
public function test_matchAnyId_call_without_initialized_container(): void
|
||||
{
|
||||
$this->expectException(Error::class);
|
||||
$this->expectExceptionMessage('Call to a member function');
|
||||
$this->storage->matchAnyId([]);
|
||||
}
|
||||
|
||||
public function test_matchAnyId_conditions(): void
|
||||
{
|
||||
$item1 = new StateEntity($this->testEpisode);
|
||||
$item2 = new StateEntity($this->testMovie);
|
||||
|
||||
$this->assertNull(
|
||||
$this->storage->matchAnyId(
|
||||
array_intersect_key($item1->getAll(), array_flip(StateInterface::ENTITY_GUIDS)),
|
||||
$item1
|
||||
)
|
||||
);
|
||||
|
||||
$newItem1 = $this->storage->insert($item1);
|
||||
$newItem2 = $this->storage->insert($item2);
|
||||
|
||||
$this->assertSame(
|
||||
$newItem1->getAll(),
|
||||
$this->storage->matchAnyId(
|
||||
array_intersect_key($item1->getAll(), array_flip(StateInterface::ENTITY_GUIDS)),
|
||||
$item1
|
||||
)->getAll()
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
$newItem2->getAll(),
|
||||
$this->storage->matchAnyId(
|
||||
array_intersect_key($item2->getAll(), array_flip(StateInterface::ENTITY_GUIDS)),
|
||||
$item2
|
||||
)->getAll()
|
||||
);
|
||||
}
|
||||
|
||||
/** StorageInterface::remove */
|
||||
public function test_remove_call_without_setup_exception(): void
|
||||
{
|
||||
$this->expectException(StorageException::class);
|
||||
$this->expectExceptionCode(StorageException::SETUP_NOT_CALLED);
|
||||
$storage = new PDOAdapter(new CliLogger($this->output));
|
||||
$storage->remove(new StateEntity([]));
|
||||
}
|
||||
|
||||
public function test_remove_conditions(): void
|
||||
{
|
||||
$item1 = new StateEntity($this->testEpisode);
|
||||
$item2 = new StateEntity($this->testMovie);
|
||||
$item3 = new StateEntity([]);
|
||||
|
||||
$this->assertFalse($this->storage->remove($item1));
|
||||
|
||||
$item1 = $this->storage->insert($item1);
|
||||
$this->storage->insert($item2);
|
||||
|
||||
$this->assertTrue($this->storage->remove($item1));
|
||||
$this->assertInstanceOf(StateInterface::class, $this->storage->get($item2));
|
||||
|
||||
// -- remove without id pointer.
|
||||
$this->assertTrue($this->storage->remove($item2));
|
||||
$this->assertFalse($this->storage->remove($item3));
|
||||
}
|
||||
|
||||
/** StorageInterface::commit */
|
||||
public function test_commit_call_without_setup_exception(): void
|
||||
{
|
||||
$this->expectException(StorageException::class);
|
||||
$this->expectExceptionCode(StorageException::SETUP_NOT_CALLED);
|
||||
$storage = new PDOAdapter(new CliLogger($this->output));
|
||||
$storage->commit([]);
|
||||
}
|
||||
|
||||
public function test_commit_conditions(): void
|
||||
{
|
||||
$item1 = new StateEntity($this->testEpisode);
|
||||
$item2 = new StateEntity($this->testMovie);
|
||||
|
||||
$this->assertSame(
|
||||
[
|
||||
StateInterface::TYPE_MOVIE => ['added' => 1, 'updated' => 0, 'failed' => 0],
|
||||
StateInterface::TYPE_EPISODE => ['added' => 1, 'updated' => 0, 'failed' => 0],
|
||||
],
|
||||
$this->storage->commit([$item1, $item2])
|
||||
);
|
||||
|
||||
$item1->guid_anidb = StateInterface::TYPE_EPISODE . '/1';
|
||||
$item2->guid_anidb = StateInterface::TYPE_MOVIE . '/1';
|
||||
|
||||
$this->assertSame(
|
||||
[
|
||||
StateInterface::TYPE_MOVIE => ['added' => 0, 'updated' => 1, 'failed' => 0],
|
||||
StateInterface::TYPE_EPISODE => ['added' => 0, 'updated' => 1, 'failed' => 0],
|
||||
],
|
||||
$this->storage->commit([$item1, $item2])
|
||||
);
|
||||
}
|
||||
|
||||
/** StorageInterface::migrations */
|
||||
public function test_migrations_call_without_setup_exception(): void
|
||||
{
|
||||
$this->expectException(StorageException::class);
|
||||
$this->expectExceptionCode(StorageException::SETUP_NOT_CALLED);
|
||||
$storage = new PDOAdapter(new CliLogger($this->output));
|
||||
$storage->migrations('f', new ArrayInput([]), new NullOutput());
|
||||
}
|
||||
public function test_migrations_call_with_wrong_direction_exception(): void
|
||||
{
|
||||
$this->expectException(StorageException::class);
|
||||
$this->expectExceptionCode(91);
|
||||
$this->storage->migrations('not_dd', new ArrayInput([]), new NullOutput());
|
||||
}
|
||||
}
|
||||
15
tests/bootstrap.php
Normal file
15
tests/bootstrap.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require __DIR__ . '/../pre_init.php';
|
||||
|
||||
if (!file_exists(__DIR__ . '/../vendor/autoload.php')) {
|
||||
fwrite(STDERR, 'Composer dependencies are missing. Run the following commands.' . PHP_EOL);
|
||||
fwrite(STDERR, sprintf('cd %s', dirname(__DIR__)) . PHP_EOL);
|
||||
fwrite(STDERR, 'composer install --optimize-autoloader' . PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
Reference in New Issue
Block a user