From e64001d00604bcf587ec5e9a631282f212df450d Mon Sep 17 00:00:00 2001 From: Sebastian Janik <sebastian.janik@deravesoftware.com> Date: Wed, 22 Jun 2022 10:48:58 +0000 Subject: [PATCH] feat(api): add rest api with podcasts read endpoints relates to #210 --- .env.example | 5 + .gitlab-ci.yml | 12 + app/Config/Autoload.php | 1 + app/Config/Filters.php | 2 + .../Seeds/FakeSinglePodcastApiSeeder.php | 141 ++++++++ modules/Api/Rest/V1/Config/Api.php | 19 + modules/Api/Rest/V1/Config/Routes.php | 21 ++ modules/Api/Rest/V1/Config/Services.php | 20 ++ .../V1/Controllers/ExceptionController.php | 19 + .../Rest/V1/Controllers/PodcastController.php | 42 +++ modules/Api/Rest/V1/Core/Exceptions.php | 32 ++ modules/Api/Rest/V1/Filters/ApiFilter.php | 25 ++ modules/Api/Rest/V1/podcast.json | 328 ++++++++++++++++++ phpunit.xml.dist | 9 +- tests/modules/Api/Rest/V1/PodcastTest.php | 109 ++++++ 15 files changed, 780 insertions(+), 5 deletions(-) create mode 100644 app/Database/Seeds/FakeSinglePodcastApiSeeder.php create mode 100644 modules/Api/Rest/V1/Config/Api.php create mode 100644 modules/Api/Rest/V1/Config/Routes.php create mode 100644 modules/Api/Rest/V1/Config/Services.php create mode 100644 modules/Api/Rest/V1/Controllers/ExceptionController.php create mode 100644 modules/Api/Rest/V1/Controllers/PodcastController.php create mode 100644 modules/Api/Rest/V1/Core/Exceptions.php create mode 100644 modules/Api/Rest/V1/Filters/ApiFilter.php create mode 100644 modules/Api/Rest/V1/podcast.json create mode 100644 tests/modules/Api/Rest/V1/PodcastTest.php diff --git a/.env.example b/.env.example index 578e0e0cb3..2844aa83b1 100644 --- a/.env.example +++ b/.env.example @@ -41,3 +41,8 @@ cache.handler="file" # cache.redis.password=null # cache.redis.port=6379 # cache.redis.database=0 + +#REST API configuration +#-------------------------------------------------------------------- +# 0/1 Disabled/Enabled +REST_API_ENABLED=1 \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b344654b40..578045a96d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -65,7 +65,19 @@ lint-js: tests: stage: quality + services: + - mariadb + variables: + MYSQL_DATABASE: "tests" + MYSQL_ROOT_PASSWORD: "R00Tp4ssW0RD" + MYSQL_USER: "tests_user" + MYSQL_PASSWORD: "password" + script: + - apt-get install -y mariadb-client libmariadb-dev + + - echo "SHOW DATABASES;" | mysql --user=root --password="$MYSQL_ROOT_PASSWORD" --host=mariadb "$MYSQL_DATABASE" + # run phpunit without code coverage # TODO: add code coverage - vendor/bin/phpunit --no-coverage diff --git a/app/Config/Autoload.php b/app/Config/Autoload.php index f68f50debd..8c5f6f0138 100644 --- a/app/Config/Autoload.php +++ b/app/Config/Autoload.php @@ -50,6 +50,7 @@ class Autoload extends AutoloadConfig 'Modules\Install' => ROOTPATH . 'modules/Install/', 'Modules\Fediverse' => ROOTPATH . 'modules/Fediverse/', 'Modules\WebSub' => ROOTPATH . 'modules/WebSub/', + 'Modules\Api\Rest\V1' => ROOTPATH . 'modules/Api/Rest/V1', 'Config' => APPPATH . 'Config/', 'ViewComponents' => APPPATH . 'Libraries/ViewComponents/', 'ViewThemes' => APPPATH . 'Libraries/ViewThemes/', diff --git a/app/Config/Filters.php b/app/Config/Filters.php index f7088509bd..8d893b746d 100644 --- a/app/Config/Filters.php +++ b/app/Config/Filters.php @@ -10,6 +10,7 @@ use CodeIgniter\Filters\DebugToolbar; use CodeIgniter\Filters\Honeypot; use CodeIgniter\Filters\InvalidChars; use CodeIgniter\Filters\SecureHeaders; +use Modules\Api\Rest\V1\Filters\ApiFilter; use Modules\Auth\Filters\PermissionFilter; use Modules\Fediverse\Filters\AllowCorsFilter; use Modules\Fediverse\Filters\FediverseFilter; @@ -34,6 +35,7 @@ class Filters extends BaseConfig 'permission' => PermissionFilter::class, 'fediverse' => FediverseFilter::class, 'allow-cors' => AllowCorsFilter::class, + 'rest-api' => ApiFilter::class, ]; /** diff --git a/app/Database/Seeds/FakeSinglePodcastApiSeeder.php b/app/Database/Seeds/FakeSinglePodcastApiSeeder.php new file mode 100644 index 0000000000..b2bd6a3d36 --- /dev/null +++ b/app/Database/Seeds/FakeSinglePodcastApiSeeder.php @@ -0,0 +1,141 @@ +<?php + +declare(strict_types=1); + +namespace App\Database\Seeds; + +use CodeIgniter\Database\Seeder; + +class FakeSinglePodcastApiSeeder extends Seeder +{ + /** + * @return array<mixed> + */ + public static function cover(): array + { + return [ + 'id' => 1, + 'file_path' => 'podcasts/Handle/cover.jpg', + 'file_size' => 400000, + 'file_mimetype' => 'image/jpeg', + 'file_metadata' => '{"FILE":{"FileName":"cover.jpg","FileDateTime":1654861723,"FileSize":468541,"FileType":2,"MimeType":"image\/jpeg","SectionsFound":"COMMENT"},"COMPUTED":{"html":"width=\"1400\" height=\"1400\"","Height":1400,"Width":1400,"IsColor":1},"COMMENT":["CREATOR: gd-jpeg v1.0 (using IJG JPEG v62), quality = 90\n"],"sizes":{"tiny":{"width":40,"height":40,"mimetype":"image\/webp","extension":"webp"},"thumbnail":{"width":150,"height":150,"mimetype":"image\/webp","extension":"webp"},"medium":{"width":320,"height":320,"mimetype":"image\/webp","extension":"webp"},"large":{"width":1024,"height":1024,"mimetype":"image\/webp","extension":"webp"},"feed":{"width":1400,"height":1400},"id3":{"width":500,"height":500},"og":{"width":1200,"height":1200},"federation":{"width":400,"height":400},"webmanifest192":{"width":192,"height":192,"mimetype":"image\/png","extension":"png"},"webmanifest512":{"width":512,"height":512,"mimetype":"image\/png","extension":"png"}}}', + 'type' => 'image', + 'description' => null, + 'language_code' => null, + 'uploaded_by' => 1, + 'updated_by' => 1, + 'uploaded_at' => '2022-06-13 8:00:00', + 'updated_at' => '2022-06-13 8:00:00', + ]; + } + + /** + * @return array<mixed> + */ + public static function banner(): array + { + return [ + 'id' => 2, + 'file_path' => 'podcasts/Handle/banner.jpg', + 'file_size' => 400000, + 'file_mimetype' => 'image/jpeg', + 'file_metadata' => '{"FILE":{"FileName":"banner.jpg","FileDateTime":1654861724,"FileSize":98209,"FileType":2,"MimeType":"image\/jpeg","SectionsFound":""},"COMPUTED":{"html":"width=\"1500\" height=\"500\"","Height":500,"Width":1500,"IsColor":1},"sizes":{"small":{"width":320,"height":128,"mimetype":"image\/webp","extension":"webp"},"medium":{"width":960,"height":320,"mimetype":"image\/webp","extension":"webp"},"federation":{"width":1500,"height":500}}}', + 'type' => 'image', + 'description' => null, + 'language_code' => null, + 'uploaded_by' => 1, + 'updated_by' => 1, + 'uploaded_at' => '2022-06-13 8:00:00', + 'updated_at' => '2022-06-13 8:00:00', + ]; + } + + /** + * @return array<mixed> + */ + public static function actor(): array + { + return [ + 'id' => 1, + 'uri' => getenv('app_baseURL') . '@Handle', + 'username' => 'Handle', + 'domain' => getenv('app_baseURL'), + 'private_key' => 'private_key', + 'public_key' => 'public_key', + 'display_name' => 'Title', + 'summary' => '<p>description</p>', + 'avatar_image_url' => getenv('app_baseURL') . 'media/podcasts/Handle', + 'avatar_image_mimetype' => 'image/webp', + 'cover_image_url' => null, + 'cover_image_mimetype' => null, + 'inbox_url' => getenv('app_baseURL') . '@Handle/inbox', + 'outbox_url' => getenv('app_baseURL') . '@Handle/outbox', + 'followers_url' => getenv('app_baseURL') . '@Handle/followers', + 'followers_count' => 0, + 'posts_count' => 0, + 'is_blocked' => 0, + 'created_at' => '2022-06-13 8:00:00', + 'updated_at' => '2022-06-13 8:00:00', + ]; + } + + /** + * @return array<mixed> + */ + public static function podcast(): array + { + return [ + 'id' => 1, + 'guid' => '0d341200-0234-5de7-99a6-a7d02bea4ce2', + 'actor_id' => 1, + 'handle' => 'Handle', + 'title' => 'Title', + 'description_markdown' => 'description', + 'description_html' => '<p>description</p>', + 'cover_id' => 1, + 'banner_id' => 2, + 'language_code' => 'en', + 'category_id' => 1, + 'parental_advisory' => null, + 'owner_name' => 'Owner', + 'owner_email' => 'Owner@gmail.com', + 'publisher' => '', + 'type' => 'episodic', + 'copyright' => '', + 'episode_description_footer_markdown' => null, + 'episode_description_footer_html' => null, + 'is_blocked' => 0, + 'is_completed' => 0, + 'is_locked' => 1, + 'imported_feed_url' => null, + 'new_feed_url' => null, + 'payment_pointer' => null, + 'location_name' => null, + 'location_geo' => null, + 'location_osm' => null, + 'custom_rss' => null, + 'is_published_on_hubs' => 0, + 'partner_id' => null, + 'partner_link_url' => null, + 'partner_image_url' => null, + 'created_by' => 1, + 'updated_by' => 1, + 'created_at' => '2022-06-13 8:00:00', + 'updated_at' => '2022-06-13 8:00:00', + ]; + } + + public function run(): void + { + $this->call(AppSeeder::class); + $this->call(TestSeeder::class); + $this->db->table('media') + ->insert(self::cover()); + $this->db->table('media') + ->insert(self::banner()); + $this->db->table('fediverse_actors') + ->insert(self::actor()); + $this->db->table('podcasts') + ->insert(self::podcast()); + } +} diff --git a/modules/Api/Rest/V1/Config/Api.php b/modules/Api/Rest/V1/Config/Api.php new file mode 100644 index 0000000000..849732e05c --- /dev/null +++ b/modules/Api/Rest/V1/Config/Api.php @@ -0,0 +1,19 @@ +<?php + + +declare(strict_types=1); + +namespace Modules\Api\Rest\V1\Config; + +use CodeIgniter\Config\BaseConfig; + +class Api extends BaseConfig +{ + /** + * -------------------------------------------------------------------------- + * Rest API gateway + * -------------------------------------------------------------------------- + * Defines a base route for all API pages + */ + public string $gateway = 'api/rest/v1/'; +} diff --git a/modules/Api/Rest/V1/Config/Routes.php b/modules/Api/Rest/V1/Config/Routes.php new file mode 100644 index 0000000000..f40467e03f --- /dev/null +++ b/modules/Api/Rest/V1/Config/Routes.php @@ -0,0 +1,21 @@ +<?php + +declare(strict_types=1); + +namespace Modules\Api\Rest\V1\Config; + +$routes = service('routes'); + +$routes->group( + config('Api') + ->gateway . 'podcasts', + [ + 'namespace' => 'Modules\Api\Rest\V1\Controllers', + 'filter' => 'rest-api', + ], + function ($routes): void { + $routes->get('/', 'PodcastController::list'); + $routes->get('(:num)', 'PodcastController::view/$1'); + $routes->get('(:any)', 'ExceptionController::notFound'); + } +); diff --git a/modules/Api/Rest/V1/Config/Services.php b/modules/Api/Rest/V1/Config/Services.php new file mode 100644 index 0000000000..c4a3d72c67 --- /dev/null +++ b/modules/Api/Rest/V1/Config/Services.php @@ -0,0 +1,20 @@ +<?php + +declare(strict_types=1); + +namespace Modules\Api\Rest\V1\Config; + +use CodeIgniter\Config\BaseService; +use Modules\Api\Rest\V1\Core\Exceptions; + +class Services extends BaseService +{ + public static function restApiExceptions(bool $getShared = true) + { + if ($getShared) { + return static::getSharedInstance('restApiExceptions'); + } + + return new Exceptions(config('Exceptions'), static::request(), static::response()); + } +} diff --git a/modules/Api/Rest/V1/Controllers/ExceptionController.php b/modules/Api/Rest/V1/Controllers/ExceptionController.php new file mode 100644 index 0000000000..50dd786333 --- /dev/null +++ b/modules/Api/Rest/V1/Controllers/ExceptionController.php @@ -0,0 +1,19 @@ +<?php + +declare(strict_types=1); + +namespace Modules\Api\Rest\V1\Controllers; + +use CodeIgniter\API\ResponseTrait; +use CodeIgniter\Controller; +use CodeIgniter\HTTP\Response; + +class ExceptionController extends Controller +{ + use ResponseTrait; + + public function notFound(): Response + { + return $this->failNotFound('Podcast not found'); + } +} diff --git a/modules/Api/Rest/V1/Controllers/PodcastController.php b/modules/Api/Rest/V1/Controllers/PodcastController.php new file mode 100644 index 0000000000..abfab63898 --- /dev/null +++ b/modules/Api/Rest/V1/Controllers/PodcastController.php @@ -0,0 +1,42 @@ +<?php + +declare(strict_types=1); + +namespace Modules\Api\Rest\V1\Controllers; + +use App\Entities\Podcast; +use App\Models\PodcastModel; +use CodeIgniter\API\ResponseTrait; +use CodeIgniter\Controller; +use CodeIgniter\HTTP\Response; +use Modules\Api\Rest\V1\Config\Services; + +class PodcastController extends Controller +{ + use ResponseTrait; + + public function __construct() + { + Services::restApiExceptions()->initialize(); + } + + public function list(): Response + { + $data = (new PodcastModel())->findAll(); + array_map(function ($podcast): void { + $podcast->feed_url = $podcast->getFeedUrl(); + }, $data); + return $this->respond($data); + } + + public function view(int $id): Response + { + $data = (new PodcastModel())->getPodcastById($id); + if (! $data instanceof Podcast) { + return $this->failNotFound('Podcast not found'); + } + + $data->feed_url = $data->getFeedUrl(); + return $this->respond($data); + } +} diff --git a/modules/Api/Rest/V1/Core/Exceptions.php b/modules/Api/Rest/V1/Core/Exceptions.php new file mode 100644 index 0000000000..4dae50e580 --- /dev/null +++ b/modules/Api/Rest/V1/Core/Exceptions.php @@ -0,0 +1,32 @@ +<?php + +declare(strict_types=1); + +namespace Modules\Api\Rest\V1\Core; + +use Throwable; + +class Exceptions extends \CodeIgniter\Debug\Exceptions +{ + protected function render(Throwable $exception, int $statusCode): void + { + header('Content-Type: application/json'); + $data = [ + 'status' => $statusCode, + 'error' => $statusCode, + 'messages' => [ + 'error' => 'Unexpected error', + ], + ]; + if (ENVIRONMENT === 'development') { + $data['messages'] = array_merge($data['messages'], [ + 'message' => $exception->getMessage(), + 'file' => $exception->getFile(), + 'line' => $exception->getLine(), + 'trace' => $exception->getTrace(), + ]); + } + + echo json_encode($data); + } +} diff --git a/modules/Api/Rest/V1/Filters/ApiFilter.php b/modules/Api/Rest/V1/Filters/ApiFilter.php new file mode 100644 index 0000000000..efd23f3533 --- /dev/null +++ b/modules/Api/Rest/V1/Filters/ApiFilter.php @@ -0,0 +1,25 @@ +<?php + +declare(strict_types=1); + +namespace Modules\Api\Rest\V1\Filters; + +use CodeIgniter\Exceptions\PageNotFoundException; +use CodeIgniter\Filters\FilterInterface; +use CodeIgniter\HTTP\RequestInterface; +use CodeIgniter\HTTP\ResponseInterface; + +class ApiFilter implements FilterInterface +{ + public function before(RequestInterface $request, $arguments = null): void + { + if (! getenv('REST_API_ENABLED')) { + throw PageNotFoundException::forPageNotFound(); + } + } + + public function after(RequestInterface $request, ResponseInterface $response, $arguments = null): void + { + // Do something here + } +} diff --git a/modules/Api/Rest/V1/podcast.json b/modules/Api/Rest/V1/podcast.json new file mode 100644 index 0000000000..4fe37fdeac --- /dev/null +++ b/modules/Api/Rest/V1/podcast.json @@ -0,0 +1,328 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "1.0.0", + "title": "Castopod podcasts" + }, + "paths": { + "/api/rest/v1/podcasts": { + "get": { + "summary": "List all podcasts", + "responses": { + "200": { + "description": "Object of podcasts", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Podcasts" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/api/rest/v1/podcasts/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "The id of the podcast to retrieve", + "schema": { + "type": "integer", + "format": "int64", + "minimum": 1, + "maxLength": 10 + } + } + ], + "get": { + "summary": "Info for a specific podcast", + "responses": { + "200": { + "description": "Expected response to a valid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Podcast" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Podcast": { + "type": "object", + "required": [ + "id", + "guid", + "actor_id", + "handle", + "title", + "description_markdown", + "description_html", + "cover_id", + "language_code", + "category_id", + "owner_name", + "owner_email", + "type", + "is_blocked", + "is_completed", + "is_locked", + "is_published_on_hubs", + "created_by", + "updated_by", + "created_at", + "updated_at", + "feed_url" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64", + "minimum": 1, + "maxLength": 10 + }, + "guid": { + "type": "string", + "maxLength": 36 + }, + "actor_id": { + "type": "integer", + "format": "int64", + "minimum": 1, + "maxLength": 10 + }, + "handle": { + "type": "string", + "maxLength": 32 + }, + "title": { + "type": "string", + "maxLength": 128 + }, + "description_markdown": { + "type": "string" + }, + "description_html": { + "type": "string" + }, + "cover_id": { + "type": "integer", + "format": "int64", + "minimum": 1, + "maxLength": 10 + }, + "banner_id": { + "type": "integer", + "format": "int64", + "minimum": 1, + "maxLength": 10 + }, + "language_code": { + "type": "string", + "maxLength": 2 + }, + "category_id": { + "type": "integer", + "format": "int64", + "minimum": 1 + }, + "parental_advisory": { + "type": "string", + "enum": ["clean", "explicit"] + }, + "owner_name": { + "type": "string", + "maxLength": 128 + }, + "owner_email": { + "type": "string", + "maxLength": 255 + }, + "publisher": { + "type": "string", + "maxLength": 128 + }, + "type": { + "type": "string", + "enum": ["episodic", "serial"] + }, + "copyright": { + "type": "string", + "maxLength": 128 + }, + "episode_description_footer_markdown": { + "type": "string" + }, + "episode_description_footer_html": { + "type": "string" + }, + "is_blocked": { + "type": "integer", + "format": "int32", + "enum": [0, 1], + "minLength": 1 + }, + "is_completed": { + "type": "integer", + "format": "int32", + "enum": [0, 1], + "minLength": 1 + }, + "is_locked": { + "type": "integer", + "format": "int32", + "enum": [0, 1], + "minLength": 1 + }, + "imported_feed_url": { + "type": "string", + "maxLength": 512 + }, + "new_feed_url": { + "type": "string", + "maxLength": 512 + }, + "payment_pointer": { + "type": "string", + "maxLength": 128 + }, + "location_name": { + "type": "string", + "maxLength": 128 + }, + "location_geo": { + "type": "string", + "maxLength": 32 + }, + "location_osm": { + "type": "string", + "maxLength": 12 + }, + "custom_rss": { + "type": "string" + }, + "is_published_on_hubs": { + "type": "integer", + "format": "int32", + "enum": [0, 1], + "minLength": 1 + }, + "partner_id": { + "type": "string", + "maxLength": 32 + }, + "partner_link_url": { + "type": "string", + "maxLength": 512 + }, + "partner_image_url": { + "type": "string", + "maxLength": 512 + }, + "created_by": { + "type": "integer", + "format": "int64", + "minimum": 1, + "maxLength": 10 + }, + "updated_by": { + "type": "integer", + "format": "int64", + "minimum": 1, + "maxLength": 10 + }, + "created_at": { + "type": "object", + "properties": { + "date": { + "type": "string", + "format": "date-time" + }, + "timezone_type": { + "type": "integer", + "format": "int32" + }, + "timezone": { + "type": "string" + } + } + }, + "updated_at": { + "type": "object", + "properties": { + "date": { + "type": "string", + "format": "date-time" + }, + "timezone_type": { + "type": "integer", + "format": "int32" + }, + "timezone": { + "type": "string" + } + } + }, + "feed_url": { + "type": "string" + } + } + }, + "Podcasts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Podcast" + } + }, + "Error": { + "type": "object", + "properties": { + "status": { + "type": "integer", + "format": "int32" + }, + "error": { + "type": "integer", + "format": "int32" + }, + "messages": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + } + } + } + } + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist index c05e50146f..c6b5aa99f2 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -45,13 +45,12 @@ <!-- Directory containing the front controller (index.php) --> <const name="PUBLICPATH" value="./public/"/> <!-- Database configuration --> - <!-- Uncomment to provide your own database for testing - <env name="database.tests.hostname" value="localhost"/> + <env name="database.tests.hostname" value="mariadb"/> <env name="database.tests.database" value="tests"/> <env name="database.tests.username" value="tests_user"/> - <env name="database.tests.password" value=""/> + <env name="database.tests.password" value="password"/> <env name="database.tests.DBDriver" value="MySQLi"/> <env name="database.tests.DBPrefix" value="tests_"/> - --> + <env name="REST_API_ENABLED" value="1"/> </php> -</phpunit> \ No newline at end of file +</phpunit> diff --git a/tests/modules/Api/Rest/V1/PodcastTest.php b/tests/modules/Api/Rest/V1/PodcastTest.php new file mode 100644 index 0000000000..56a2f9c4ff --- /dev/null +++ b/tests/modules/Api/Rest/V1/PodcastTest.php @@ -0,0 +1,109 @@ +<?php + +declare(strict_types=1); + +namespace modules\Api\Rest\V1; + +use App\Database\Seeds\FakeSinglePodcastApiSeeder; +use CodeIgniter\Test\CIUnitTestCase; +use CodeIgniter\Test\DatabaseTestTrait; +use CodeIgniter\Test\FeatureTestTrait; + +class PodcastTest extends CIUnitTestCase +{ + use FeatureTestTrait; + use DatabaseTestTrait; + + /** + * @var bool + */ + protected $migrate = true; + + /** + * @var bool + */ + protected $migrateOnce = false; + + /** + * @var string|null + */ + protected $namespace; + + /** + * @var string + */ + protected $seed = 'FakeSinglePodcastApiSeeder'; + + /** + * @var string + */ + protected $basePath = 'app/Database'; + + /** + * @var array<mixed> + */ + private array $podcast = []; + + private string $podcastApiUrl; + + /** + * @param array<mixed> $data + */ + public function __construct(?string $name = null, array $data = [], $dataName = '') + { + parent::__construct($name, $data, $dataName); + $this->podcast = FakeSinglePodcastApiSeeder::podcast(); + $this->podcast['created_at'] = []; + $this->podcast['updated_at'] = []; + $this->podcastApiUrl = config('Api') + ->gateway; + } + + public function testList(): void + { + $result = $this->call('get', $this->podcastApiUrl . 'podcasts'); + $result->assertStatus(200); + $result->assertHeader('Content-Type', 'application/json; charset=UTF-8'); + $result->assertJSONFragment([ + 0 => $this->podcast, + ]); + } + + public function testView(): void + { + $result = $this->call('get', $this->podcastApiUrl . 'podcasts/1'); + $result->assertStatus(200); + $result->assertHeader('Content-Type', 'application/json; charset=UTF-8'); + $result->assertJSONFragment($this->podcast); + } + + public function testViewNotFound(): void + { + $result = $this->call('get', $this->podcastApiUrl . 'podcasts/2'); + $result->assertStatus(404); + $result->assertJSONExact( + [ + 'status' => 404, + 'error' => 404, + 'messages' => [ + 'error' => 'Podcast not found', + ], + ] + ); + $result->assertHeader('Content-Type', 'application/json; charset=UTF-8'); + } + + /* + * Refreshing database to fetch empty array of podcasts + */ + public function testListEmpty(): void + { + $this->regressDatabase(); + $this->migrateDatabase(); + $result = $this->call('get', $this->podcastApiUrl . 'podcasts'); + $result->assertStatus(200); + $result->assertHeader('Content-Type', 'application/json; charset=UTF-8'); + $result->assertJSONExact([]); + $this->seed($this->seed); + } +} -- GitLab