mirror of
https://code.castopod.org/adaures/castopod
synced 2025-04-19 13:01:19 +00:00
feat(rest-api): add endpoints for episodes and full text search for podcasts and episodes
closes #296
This commit is contained in:
parent
2b516fee14
commit
85505d4b31
@ -63,3 +63,7 @@ cache.handler="file"
|
||||
# REST API configuration
|
||||
#--------------------------------------------------------------------
|
||||
# restapi.enabled=true
|
||||
# restapi.basicAuthUsername=castopod
|
||||
# restapi.basicAuthPassword=password
|
||||
# restapi.basicAuth=true
|
||||
|
||||
|
@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
class AddFullTextSearchIndexes extends BaseMigration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$prefix = $this->db->getPrefix();
|
||||
|
||||
$createQuery = <<<CODE_SAMPLE
|
||||
ALTER TABLE {$prefix}episodes DROP INDEX IF EXISTS title;
|
||||
CODE_SAMPLE;
|
||||
|
||||
$this->db->query($createQuery);
|
||||
|
||||
$createQuery = <<<CODE_SAMPLE
|
||||
ALTER TABLE {$prefix}episodes
|
||||
ADD FULLTEXT episodes_search (title, description_markdown, slug, location_name);
|
||||
CODE_SAMPLE;
|
||||
|
||||
$this->db->query($createQuery);
|
||||
|
||||
$createQuery = <<<CODE_SAMPLE
|
||||
ALTER TABLE {$prefix}podcasts
|
||||
ADD FULLTEXT podcasts_search (title, description_markdown, handle, location_name);
|
||||
CODE_SAMPLE;
|
||||
|
||||
$this->db->query($createQuery);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$prefix = $this->db->getPrefix();
|
||||
|
||||
$createQuery = <<<CODE_SAMPLE
|
||||
ALTER TABLE {$prefix}episodes
|
||||
DROP INDEX IF EXISTS episodes_search;
|
||||
CODE_SAMPLE;
|
||||
|
||||
$this->db->query($createQuery);
|
||||
|
||||
$createQuery = <<<CODE_SAMPLE
|
||||
ALTER TABLE {$prefix}podcasts
|
||||
DROP INDEX IF EXISTS podcasts_search;
|
||||
CODE_SAMPLE;
|
||||
|
||||
$this->db->query($createQuery);
|
||||
}
|
||||
}
|
@ -8,6 +8,27 @@ use CodeIgniter\Database\Seeder;
|
||||
|
||||
class FakeSinglePodcastApiSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* @return array{id: int, file_key: string, file_size: string, file_mimetype: string, file_metadata: string, type: string, description: null, language_code: string, uploaded_by: string, updated_by: string, uploaded_at: string, updated_at: string}
|
||||
*/
|
||||
public static function audio(): array
|
||||
{
|
||||
return [
|
||||
'id' => 3,
|
||||
'file_key' => 'podcasts/test/1685531765_84fb3309111ece22ca37.mp3',
|
||||
'file_size' => '2737773',
|
||||
'file_mimetype' => 'audio/mpeg',
|
||||
'file_metadata' => '{"GETID3_VERSION":"2.0.x-202207161647","filesize":2737773,"filepath":"\\/tmp","filename":"php76vXQR","filenamepath":"\\/tmp\\/php76vXQR","avdataoffset":45,"avdataend":2737773,"fileformat":"mp3","audio":{"dataformat":"mp3","channels":2,"sample_rate":48000,"bitrate":128008.9774161874,"channelmode":"stereo","bitrate_mode":"cbr","lossless":false,"encoder_options":"CBR128","compression_ratio":0.08333917800533033,"streams":[{"dataformat":"mp3","channels":2,"sample_rate":48000,"bitrate":128008.9774161874,"channelmode":"stereo","bitrate_mode":"cbr","lossless":false,"encoder_options":"CBR128","compression_ratio":0.08333917800533033}]},"tags":{"id3v2":{"encoder_settings":["Lavf58.29.100"]}},"encoding":"UTF-8","id3v2":{"header":true,"flags":{"unsynch":false,"exthead":false,"experim":false,"isfooter":false},"majorversion":4,"minorversion":0,"headerlength":45,"tag_offset_start":0,"tag_offset_end":45,"encoding":"UTF-8","comments":{"encoder_settings":["Lavf58.29.100"]},"TSSE":[{"frame_name":"TSSE","frame_flags_raw":0,"data":"Lavf58.29.100","datalength":15,"dataoffset":10,"framenamelong":"Software\\/Hardware and settings used for encoding","framenameshort":"encoder_settings","flags":{"TagAlterPreservation":false,"FileAlterPreservation":false,"ReadOnly":false,"GroupingIdentity":false,"compression":false,"Encryption":false,"Unsynchronisation":false,"DataLengthIndicator":false},"encodingid":3,"encoding":"UTF-8"}],"padding":{"start":35,"length":10,"valid":true}},"mime_type":"audio\\/mpeg","mpeg":{"audio":{"raw":{"synch":4094,"version":3,"layer":1,"protection":1,"bitrate":5,"sample_rate":1,"padding":0,"private":0,"channelmode":0,"modeextension":0,"copyright":0,"original":0,"emphasis":0},"version":"1","layer":3,"channelmode":"stereo","channels":2,"sample_rate":48000,"protection":false,"private":false,"modeextension":"","copyright":false,"original":false,"emphasis":"none","padding":false,"bitrate":128008.9774161874,"framelength":384,"bitrate_mode":"cbr","VBR_method":"Xing","xing_flags_raw":15,"xing_flags":{"frames":true,"bytes":true,"toc":true,"vbr_scale":true},"VBR_frames":7129,"VBR_bytes":2737728,"toc":[0,3,5,8,10,13,16,18,20,22,26,28,31,33,36,39,41,43,45,49,51,54,56,59,62,64,66,68,72,74,77,79,82,85,87,89,91,95,97,99,102,105,108,110,112,114,118,120,122,125,128,131,133,135,137,141,143,145,148,150,153,156,158,160,164,166,168,171,173,176,179,181,183,187,189,191,194,196,199,202,204,206,210,212,214,217,219,222,225,227,229,233,235,237,240,242,245,248,250,252],"VBR_scale":0,"VBR_bitrate":128008.9774161874}},"playtime_seconds":171.0720016831475,"tags_html":{"id3v2":{"encoder_settings":["Lavf58.29.100"]}},"bitrate":128008.9774161874,"playtime_string":"2:51"}',
|
||||
'type' => 'audio',
|
||||
'description' => null,
|
||||
'language_code' => 'pl',
|
||||
'uploaded_by' => '1',
|
||||
'updated_by' => '1',
|
||||
'uploaded_at' => '2023-05-31 11:16:05',
|
||||
'updated_at' => '2023-05-31 11:16:05',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{id: int, file_key: string, file_size: int, file_mimetype: string, file_metadata: string, type: string, description: null, language_code: null, uploaded_by: int, updated_by: int, uploaded_at: string, updated_at: string}
|
||||
*/
|
||||
@ -125,6 +146,46 @@ class FakeSinglePodcastApiSeeder extends Seeder
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{id: int, podcast_id: int, guid: string, title: string, slug: string, audio_id: int, description_markdown: string, description_html: string, cover_id: int, transcript_id: null, transcript_remote_url: null, chapters_id: null, chapters_remote_url: null, parental_advisory: null, number: int, season_number: null, type: string, is_blocked: false, location_name: null, location_geo: null, location_osm: null, custom_rss: null, is_published_on_hubs: false, posts_count: int, comments_count: int, is_premium: false, created_by: int, updated_by: int, published_at: null, created_at: string, updated_at: string}
|
||||
*/
|
||||
public static function episode(): array
|
||||
{
|
||||
return [
|
||||
'id' => 1,
|
||||
'podcast_id' => 1,
|
||||
'guid' => 'http://localhost:8080/@test/episodes/muzyka-marzen',
|
||||
'title' => 'Episode title',
|
||||
'slug' => 'episode-slug',
|
||||
'audio_id' => 3,
|
||||
'description_markdown' => '123',
|
||||
'description_html' => '<p>123</p>',
|
||||
'cover_id' => 1,
|
||||
'transcript_id' => null,
|
||||
'transcript_remote_url' => null,
|
||||
'chapters_id' => null,
|
||||
'chapters_remote_url' => null,
|
||||
'parental_advisory' => null,
|
||||
'number' => 1,
|
||||
'season_number' => null,
|
||||
'type' => 'full',
|
||||
'is_blocked' => false,
|
||||
'location_name' => null,
|
||||
'location_geo' => null,
|
||||
'location_osm' => null,
|
||||
'custom_rss' => null,
|
||||
'is_published_on_hubs' => false,
|
||||
'posts_count' => 0,
|
||||
'comments_count' => 0,
|
||||
'is_premium' => false,
|
||||
'created_by' => 1,
|
||||
'updated_by' => 1,
|
||||
'published_at' => null,
|
||||
'created_at' => '2023-05-31 11:16:06',
|
||||
'updated_at' => '2023-05-31 11:16:06',
|
||||
];
|
||||
}
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$this->call(AppSeeder::class);
|
||||
@ -133,9 +194,13 @@ class FakeSinglePodcastApiSeeder extends Seeder
|
||||
->insert(self::cover());
|
||||
$this->db->table('media')
|
||||
->insert(self::banner());
|
||||
$this->db->table('media')
|
||||
->insert(self::audio());
|
||||
$this->db->table('fediverse_actors')
|
||||
->insert(self::actor());
|
||||
$this->db->table('podcasts')
|
||||
->insert(self::podcast());
|
||||
$this->db->table('episodes')
|
||||
->insert(self::episode());
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ declare(strict_types=1);
|
||||
namespace App\Models;
|
||||
|
||||
use App\Entities\Episode;
|
||||
use CodeIgniter\Database\BaseBuilder;
|
||||
use CodeIgniter\Database\BaseResult;
|
||||
use CodeIgniter\I18n\Time;
|
||||
use CodeIgniter\Model;
|
||||
@ -434,6 +435,37 @@ class EpisodeModel extends Model
|
||||
])->countAllResults() > 0;
|
||||
}
|
||||
|
||||
public function fullTextSearch(string $query): ?BaseBuilder
|
||||
{
|
||||
$prefix = $this->db->getPrefix();
|
||||
$episodeTable = $prefix . $this->builder()->getTable();
|
||||
|
||||
$podcastModel = (new PodcastModel());
|
||||
|
||||
$podcastTable = $podcastModel->db->getPrefix() . $podcastModel->builder()->getTable();
|
||||
|
||||
$this->builder()
|
||||
->select('' . $episodeTable . '.*')
|
||||
->select('
|
||||
' . $this->getFullTextMatchClauseForEpisodes($episodeTable, $query) . ' as episodes_score,
|
||||
' . $podcastModel->getFullTextMatchClauseForPodcasts($podcastTable, $query) . ' as podcasts_score,
|
||||
')
|
||||
->select("{$podcastTable}.created_at AS podcast_created_at")
|
||||
->select(
|
||||
"{$podcastTable}.title as podcast_title, {$podcastTable}.handle as podcast_handle, {$podcastTable}.description_markdown as podcast_description_markdown"
|
||||
)
|
||||
->join($podcastTable, "{$podcastTable} on {$podcastTable}.id = {$episodeTable}.podcast_id")
|
||||
->where('
|
||||
(' .
|
||||
$this->getFullTextMatchClauseForEpisodes($episodeTable, $query)
|
||||
. 'OR' .
|
||||
$podcastModel->getFullTextMatchClauseForPodcasts($podcastTable, $query)
|
||||
. ')
|
||||
');
|
||||
|
||||
return $this->builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $data
|
||||
*
|
||||
@ -462,4 +494,17 @@ class EpisodeModel extends Model
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function getFullTextMatchClauseForEpisodes(string $table, string $value): string
|
||||
{
|
||||
return '
|
||||
MATCH (
|
||||
' . $table . '.title,
|
||||
' . $table . '.description_markdown,
|
||||
' . $table . '.slug,
|
||||
' . $table . '.location_name
|
||||
)
|
||||
AGAINST("*' . preg_replace('/[^\p{L}\p{N}_]+/u', ' ', $value) . '*" IN BOOLEAN MODE)
|
||||
';
|
||||
}
|
||||
}
|
||||
|
@ -384,6 +384,19 @@ class PodcastModel extends Model
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function getFullTextMatchClauseForPodcasts(string $table, string $value): string
|
||||
{
|
||||
return '
|
||||
MATCH (
|
||||
' . $table . '.title ,
|
||||
' . $table . '.description_markdown,
|
||||
' . $table . '.handle,
|
||||
' . $table . '.location_name
|
||||
)
|
||||
AGAINST("*' . preg_replace('/[^\p{L}\p{N}_]+/u', ' ', $value) . '*" IN BOOLEAN MODE)
|
||||
';
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an actor linked to the podcast (Triggered before insert)
|
||||
*
|
||||
|
@ -86,7 +86,7 @@ class EpisodeController extends BaseController
|
||||
->select('episodes.*, IFNULL(SUM(ape.hits),0) as downloads')
|
||||
->join('analytics_podcasts_by_episode ape', 'episodes.id=ape.episode_id', 'left')
|
||||
->where('episodes.podcast_id', $this->podcast->id)
|
||||
->where("MATCH (title, description_markdown) AGAINST ('{$query}')")
|
||||
->where("MATCH (title, description_markdown, slug, location_name) AGAINST ('{$query}')")
|
||||
->groupBy('episodes.id');
|
||||
}
|
||||
} else {
|
||||
|
@ -15,6 +15,17 @@ class RestApi extends BaseConfig
|
||||
*/
|
||||
public bool $enabled = false;
|
||||
|
||||
public bool $basicAuth = false;
|
||||
|
||||
public ?string $basicAuthUsername = null;
|
||||
|
||||
public ?string $basicAuthPassword = null;
|
||||
|
||||
/**
|
||||
* Default results limit.
|
||||
*/
|
||||
public int $limit = 10;
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Rest API gateway
|
||||
|
@ -19,3 +19,17 @@ $routes->group(
|
||||
$routes->get('(:any)', 'ExceptionController::notFound');
|
||||
}
|
||||
);
|
||||
|
||||
$routes->group(
|
||||
config('RestApi')
|
||||
->gateway . 'episodes',
|
||||
[
|
||||
'namespace' => 'Modules\Api\Rest\V1\Controllers',
|
||||
'filter' => 'rest-api',
|
||||
],
|
||||
static function ($routes): void {
|
||||
$routes->get('/', 'EpisodeController::list');
|
||||
$routes->get('(:num)', 'EpisodeController::view/$1');
|
||||
$routes->get('(:any)', 'ExceptionController::notFound');
|
||||
}
|
||||
);
|
||||
|
79
modules/Api/Rest/V1/Controllers/EpisodeController.php
Normal file
79
modules/Api/Rest/V1/Controllers/EpisodeController.php
Normal file
@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Api\Rest\V1\Controllers;
|
||||
|
||||
use App\Entities\Episode;
|
||||
use App\Models\EpisodeModel;
|
||||
use CodeIgniter\API\ResponseTrait;
|
||||
use CodeIgniter\Controller;
|
||||
use CodeIgniter\HTTP\Response;
|
||||
use Modules\Api\Rest\V1\Config\Services;
|
||||
|
||||
class EpisodeController extends Controller
|
||||
{
|
||||
use ResponseTrait;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
Services::restApiExceptions()->initialize();
|
||||
}
|
||||
|
||||
public function list(): Response
|
||||
{
|
||||
$query = $this->request->getGet('query');
|
||||
$order = $this->request->getGet('order') ?? 'newest';
|
||||
$podcastIds = $this->request->getGet('podcastIds');
|
||||
|
||||
$builder = (new EpisodeModel());
|
||||
|
||||
if ($podcastIds !== null) {
|
||||
$builder->whereIn('podcast_id', explode(',', (string) $podcastIds));
|
||||
}
|
||||
|
||||
if ($query !== null) {
|
||||
$builder->fullTextSearch($query);
|
||||
|
||||
if ($order === 'query') {
|
||||
$builder->orderBy('(episodes_score + podcasts_score)', 'desc');
|
||||
}
|
||||
}
|
||||
|
||||
if ($order === 'newest') {
|
||||
$builder->orderBy($builder->db->getPrefix() . $builder->getTable() . '.created_at', 'desc');
|
||||
}
|
||||
|
||||
$data = $builder->findAll(
|
||||
(int) ($this->request->getGet('limit') ?? config('RestApi')->limit),
|
||||
(int) $this->request->getGet('offset')
|
||||
);
|
||||
|
||||
array_map(static function ($episode): void {
|
||||
self::mapEpisode($episode);
|
||||
}, $data);
|
||||
|
||||
return $this->respond($data);
|
||||
}
|
||||
|
||||
public function view(int $id): Response
|
||||
{
|
||||
$episode = (new EpisodeModel())->getEpisodeById($id);
|
||||
|
||||
if (! $episode instanceof Episode) {
|
||||
return $this->failNotFound('Episode not found');
|
||||
}
|
||||
|
||||
return $this->respond($this->mapEpisode($episode));
|
||||
}
|
||||
|
||||
protected static function mapEpisode(Episode $episode): Episode
|
||||
{
|
||||
$episode->cover_url = $episode->getCover()
|
||||
->file_url;
|
||||
$episode->audio_url = $episode->getAudioUrl();
|
||||
$episode->duration = round($episode->audio->duration);
|
||||
|
||||
return $episode;
|
||||
}
|
||||
}
|
@ -24,19 +24,37 @@ class PodcastController extends Controller
|
||||
{
|
||||
$data = (new PodcastModel())->findAll();
|
||||
array_map(static function ($podcast): void {
|
||||
$podcast->feed_url = $podcast->getFeedUrl();
|
||||
self::mapPodcast($podcast);
|
||||
}, $data);
|
||||
return $this->respond($data);
|
||||
}
|
||||
|
||||
public function view(int $id): Response
|
||||
{
|
||||
$data = (new PodcastModel())->getPodcastById($id);
|
||||
if (! $data instanceof Podcast) {
|
||||
$podcast = (new PodcastModel())->getPodcastById($id);
|
||||
if (! $podcast instanceof Podcast) {
|
||||
return $this->failNotFound('Podcast not found');
|
||||
}
|
||||
|
||||
$data->feed_url = $data->getFeedUrl();
|
||||
return $this->respond($data);
|
||||
return $this->respond(self::mapPodcast($podcast));
|
||||
}
|
||||
|
||||
protected static function mapPodcast(Podcast $podcast): Podcast
|
||||
{
|
||||
$podcast->feed_url = $podcast->getFeedUrl();
|
||||
$podcast->actor_display_name = $podcast->getActor()
|
||||
->display_name;
|
||||
$podcast->cover_url = $podcast->getCover()
|
||||
->file_url;
|
||||
|
||||
$categories = [$podcast->getCategory(), ...$podcast->getOtherCategories()];
|
||||
|
||||
foreach ($categories as $category) {
|
||||
$category->translated = lang('Podcast.category_options.' . $category->code, [], null, false);
|
||||
}
|
||||
|
||||
$podcast->categories = $categories;
|
||||
|
||||
return $podcast;
|
||||
}
|
||||
}
|
||||
|
@ -6,16 +6,52 @@ namespace Modules\Api\Rest\V1\Filters;
|
||||
|
||||
use CodeIgniter\Exceptions\PageNotFoundException;
|
||||
use CodeIgniter\Filters\FilterInterface;
|
||||
use CodeIgniter\HTTP\Request;
|
||||
use CodeIgniter\HTTP\RequestInterface;
|
||||
use CodeIgniter\HTTP\Response;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
use Modules\Api\Rest\V1\Config\RestApi;
|
||||
|
||||
class ApiFilter implements FilterInterface
|
||||
{
|
||||
public function before(RequestInterface $request, $arguments = null): void
|
||||
/**
|
||||
* @param Request $request
|
||||
*/
|
||||
public function before(RequestInterface $request, $arguments = null)
|
||||
{
|
||||
if (! config('RestApi')->enabled) {
|
||||
/** @var RestApi $restApiConfig */
|
||||
$restApiConfig = config('RestApi');
|
||||
|
||||
if (! $restApiConfig->enabled) {
|
||||
throw PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
|
||||
if ($restApiConfig->basicAuth) {
|
||||
/** @var Response $response */
|
||||
$response = service('response');
|
||||
if (! $request->hasHeader('Authorization')) {
|
||||
$response->setStatusCode(401);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
$authHeader = $request->getHeaderLine('Authorization');
|
||||
if (substr($authHeader, 0, 6) !== 'Basic ') {
|
||||
$response->setStatusCode(401);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
$auth_token = base64_decode(substr($authHeader, 6), true);
|
||||
|
||||
list($username, $password) = explode(':', (string) $auth_token);
|
||||
|
||||
if (! ($username === $restApiConfig->basicAuthUsername && $password === $restApiConfig->basicAuthPassword)) {
|
||||
$response->setStatusCode(401);
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null): void
|
||||
|
108
tests/modules/Api/Rest/V1/EpisodeTest.php
Normal file
108
tests/modules/Api/Rest/V1/EpisodeTest.php
Normal file
@ -0,0 +1,108 @@
|
||||
<?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 EpisodeTest 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<string, mixed>
|
||||
*/
|
||||
private array $episode = [];
|
||||
|
||||
private readonly string $apiUrl;
|
||||
|
||||
public function __construct(?string $name = null)
|
||||
{
|
||||
parent::__construct($name);
|
||||
|
||||
$this->episode = FakeSinglePodcastApiSeeder::episode();
|
||||
|
||||
$this->episode['created_at'] = [];
|
||||
$this->episode['updated_at'] = [];
|
||||
$this->apiUrl = config('RestApi')
|
||||
->gateway;
|
||||
}
|
||||
|
||||
public function testList(): void
|
||||
{
|
||||
$result = $this->call('get', $this->apiUrl . 'episodes');
|
||||
$result->assertStatus(200);
|
||||
$result->assertHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
$result->assertJSONFragment([
|
||||
0 => $this->episode,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testView(): void
|
||||
{
|
||||
$result = $this->call('get', $this->apiUrl . 'episodes/1');
|
||||
$result->assertStatus(200);
|
||||
$result->assertHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
$result->assertJSONFragment($this->episode);
|
||||
}
|
||||
|
||||
public function testViewNotFound(): void
|
||||
{
|
||||
$result = $this->call('get', $this->apiUrl . 'episodes/2');
|
||||
$result->assertStatus(404);
|
||||
$result->assertJSONExact(
|
||||
[
|
||||
'status' => 404,
|
||||
'error' => 404,
|
||||
'messages' => [
|
||||
'error' => 'Episode not found',
|
||||
],
|
||||
]
|
||||
);
|
||||
$result->assertHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
}
|
||||
|
||||
/*
|
||||
* Refreshing database to fetch empty array of episodes
|
||||
*/
|
||||
public function testListEmpty(): void
|
||||
{
|
||||
$this->regressDatabase();
|
||||
$this->migrateDatabase();
|
||||
$result = $this->call('get', $this->apiUrl . 'episodes');
|
||||
$result->assertStatus(200);
|
||||
$result->assertHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
$result->assertJSONExact([]);
|
||||
$this->seed($this->seed);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user