diff --git a/app/Http/Controllers/Api/V1/Category/GetCategoriesController.php b/app/Http/Controllers/Api/V1/Category/GetCategoriesController.php new file mode 100644 index 0000000..83c9ec9 --- /dev/null +++ b/app/Http/Controllers/Api/V1/Category/GetCategoriesController.php @@ -0,0 +1,44 @@ +articleService->getAllCategories(); + + return response()->apiSuccess( + CategoryResource::collection($categories), + __('common.success') + ); + } catch (\Throwable $e) { + return response()->apiError( + __('common.error'), + Response::HTTP_INTERNAL_SERVER_ERROR, + null, + $e->getMessage() + ); + } + } +} diff --git a/app/Http/Controllers/Api/V1/Tag/GetTagsController.php b/app/Http/Controllers/Api/V1/Tag/GetTagsController.php new file mode 100644 index 0000000..157a89b --- /dev/null +++ b/app/Http/Controllers/Api/V1/Tag/GetTagsController.php @@ -0,0 +1,44 @@ +articleService->getAllTags(); + + return response()->apiSuccess( + TagResource::collection($tags), + __('common.success') + ); + } catch (\Throwable $e) { + return response()->apiError( + __('common.error'), + Response::HTTP_INTERNAL_SERVER_ERROR, + null, + $e->getMessage() + ); + } + } +} diff --git a/app/Http/Resources/Api/V1/Category/CategoryResource.php b/app/Http/Resources/Api/V1/Category/CategoryResource.php new file mode 100644 index 0000000..cfe120a --- /dev/null +++ b/app/Http/Resources/Api/V1/Category/CategoryResource.php @@ -0,0 +1,28 @@ + + */ + public function toArray($request): array + { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'slug' => $this->slug, + ]; + } +} diff --git a/app/Http/Resources/Api/V1/Tag/TagResource.php b/app/Http/Resources/Api/V1/Tag/TagResource.php new file mode 100644 index 0000000..88be863 --- /dev/null +++ b/app/Http/Resources/Api/V1/Tag/TagResource.php @@ -0,0 +1,28 @@ + + */ + public function toArray($request): array + { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'slug' => $this->slug, + ]; + } +} diff --git a/app/Services/ArticleService.php b/app/Services/ArticleService.php index b45d7b4..2ca864c 100644 --- a/app/Services/ArticleService.php +++ b/app/Services/ArticleService.php @@ -5,6 +5,8 @@ namespace App\Services; use App\Models\Article; +use App\Models\Category; +use App\Models\Tag; use Illuminate\Database\Eloquent\Builder; use Illuminate\Pagination\LengthAwarePaginator; @@ -123,4 +125,24 @@ private function applyFilters(Builder $query, array $params): void ->where('published_at', '<=', now()); } } + + /** + * Get all categories + * + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getAllCategories() + { + return Category::query()->get(['id', 'name', 'slug']); + } + + /** + * Get all tags + * + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getAllTags() + { + return Tag::query()->get(['id', 'name', 'slug']); + } } diff --git a/routes/api_v1.php b/routes/api_v1.php index 5aae028..a97186a 100644 --- a/routes/api_v1.php +++ b/routes/api_v1.php @@ -24,4 +24,10 @@ Route::get('/', \App\Http\Controllers\Api\V1\Article\GetArticlesController::class)->name('api.v1.articles.index'); Route::get('/{slug}', \App\Http\Controllers\Api\V1\Article\ShowArticleController::class)->name('api.v1.articles.show'); }); + + // Category Routes (Public) + Route::get('categories', \App\Http\Controllers\Api\V1\Category\GetCategoriesController::class)->name('api.v1.categories.index'); + + // Tag Routes (Public) + Route::get('tags', \App\Http\Controllers\Api\V1\Tag\GetTagsController::class)->name('api.v1.tags.index'); }); diff --git a/tests/Feature/API/V1/Category/GetCategoriesControllerTest.php b/tests/Feature/API/V1/Category/GetCategoriesControllerTest.php new file mode 100644 index 0000000..04b5984 --- /dev/null +++ b/tests/Feature/API/V1/Category/GetCategoriesControllerTest.php @@ -0,0 +1,45 @@ +count(3)->create(); + + $response = $this->getJson('/api/v1/categories'); + + $response->assertStatus(200) + ->assertJson(['status' => true]) + ->assertJson(['message' => __('common.success')]) + ->assertJsonStructure([ + 'data' => [ + '*' => [ + 'id', + 'name', + 'slug', + ], + ], + ]); + + expect($response->json('data'))->toHaveCount(3); + }); + + it('returns error if service throws', function () { + $this->mock(\App\Services\ArticleService::class, function ($mock) { + $mock->shouldReceive('getAllCategories') + ->andThrow(new Exception('fail')); + }); + + $response = $this->getJson('/api/v1/categories'); + + $response->assertStatus(500) + ->assertJson(['status' => false]) + ->assertJson(['message' => __('common.error')]) + ->assertJsonStructure([ + 'data', + 'error', + ]); + }); +}); diff --git a/tests/Feature/API/V1/Tag/GetTagsControllerTest.php b/tests/Feature/API/V1/Tag/GetTagsControllerTest.php new file mode 100644 index 0000000..ed71aff --- /dev/null +++ b/tests/Feature/API/V1/Tag/GetTagsControllerTest.php @@ -0,0 +1,45 @@ +count(4)->create(); + + $response = $this->getJson('/api/v1/tags'); + + $response->assertStatus(200) + ->assertJson(['status' => true]) + ->assertJson(['message' => __('common.success')]) + ->assertJsonStructure([ + 'data' => [ + '*' => [ + 'id', + 'name', + 'slug', + ], + ], + ]); + + expect($response->json('data'))->toHaveCount(4); + }); + + it('returns error if service throws', function () { + $this->mock(\App\Services\ArticleService::class, function ($mock) { + $mock->shouldReceive('getAllTags') + ->andThrow(new Exception('fail')); + }); + + $response = $this->getJson('/api/v1/tags'); + + $response->assertStatus(500) + ->assertJson(['status' => false]) + ->assertJson(['message' => __('common.error')]) + ->assertJsonStructure([ + 'data', + 'error', + ]); + }); +});