From c731e8b43a5cb00b074e3c9872c8e6a8cf84966a Mon Sep 17 00:00:00 2001 From: Bryce Meyer Date: Sun, 23 Nov 2025 21:25:58 +0100 Subject: [PATCH 1/2] setup article summary model --- .../Wiki/ArticleSummaryRepositoryContract.php | 13 +++ code/app/Athenia/Models/Wiki/Article.php | 12 +++ .../Athenia/Models/Wiki/ArticleSummary.php | 93 ++++++++++++++++ .../Providers/BaseRepositoryProvider.php | 10 ++ .../Wiki/ArticleSummaryRepository.php | 26 +++++ code/app/Models/Wiki/ArticleSummary.php | 47 ++++++++ .../factories/Wiki/ArticleSummaryFactory.php | 31 ++++++ ..._201741_create_article_summaries_table.php | 43 ++++++++ .../Wiki/ArticleSummaryRepositoryTest.php | 102 ++++++++++++++++++ .../Unit/Models/Wiki/ArticleSummaryTest.php | 25 +++++ 10 files changed, 402 insertions(+) create mode 100644 code/app/Athenia/Contracts/Repositories/Wiki/ArticleSummaryRepositoryContract.php create mode 100644 code/app/Athenia/Models/Wiki/ArticleSummary.php create mode 100644 code/app/Athenia/Repositories/Wiki/ArticleSummaryRepository.php create mode 100644 code/app/Models/Wiki/ArticleSummary.php create mode 100644 code/database/factories/Wiki/ArticleSummaryFactory.php create mode 100644 code/database/migrations/2025_11_23_201741_create_article_summaries_table.php create mode 100644 code/tests/Athenia/Integration/Repositories/Wiki/ArticleSummaryRepositoryTest.php create mode 100644 code/tests/Athenia/Unit/Models/Wiki/ArticleSummaryTest.php diff --git a/code/app/Athenia/Contracts/Repositories/Wiki/ArticleSummaryRepositoryContract.php b/code/app/Athenia/Contracts/Repositories/Wiki/ArticleSummaryRepositoryContract.php new file mode 100644 index 00000000..f5ca8a40 --- /dev/null +++ b/code/app/Athenia/Contracts/Repositories/Wiki/ArticleSummaryRepositoryContract.php @@ -0,0 +1,13 @@ +hasMany(\App\Models\User\ArticleNote::class); } + /** + * The summary for this article + * + * @return HasOne + */ + public function articleSummary() : HasOne + { + return $this->hasOne(ArticleSummary::class); + } + /** * Gets the content of the article * diff --git a/code/app/Athenia/Models/Wiki/ArticleSummary.php b/code/app/Athenia/Models/Wiki/ArticleSummary.php new file mode 100644 index 00000000..3c125a67 --- /dev/null +++ b/code/app/Athenia/Models/Wiki/ArticleSummary.php @@ -0,0 +1,93 @@ +|ArticleSummary getAggregateMethod() + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary isAppendRelationsCount() + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary isLeftJoin() + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary isUseTableAlias() + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary joinRelations($relations, $leftJoin = null) + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary newModelQuery() + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary newQuery() + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary orWhereInJoin($column, $values) + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary orWhereJoin($column, $operator, $value) + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary orWhereNotInJoin($column, $values) + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary orderByJoin($column, $direction = 'asc', $aggregateMethod = null) + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary query() + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary setAggregateMethod(string $aggregateMethod) + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary setAppendRelationsCount(bool $appendRelationsCount) + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary setLeftJoin(bool $leftJoin) + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary setUseTableAlias(bool $useTableAlias) + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary whereArticleId($value) + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary whereContent($value) + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary whereCreatedAt($value) + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary whereId($value) + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary whereInJoin($column, $values, $boolean = 'and', $not = false) + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary whereJoin($column, $operator, $value, $boolean = 'and') + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary whereNotInJoin($column, $values, $boolean = 'and') + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary whereUpdatedAt($value) + * @mixin Eloquent + */ +class ArticleSummary extends BaseModelAbstract implements HasValidationRulesContract +{ + use HasValidationRules; + + /** + * The article this summary belongs to + * + * @return BelongsTo + */ + public function article(): BelongsTo + { + return $this->belongsTo(Article::class); + } + + /** + * Build the model validation rules + * @param array $params + * @return array + */ + public function buildModelValidationRules(...$params): array + { + return [ + static::VALIDATION_RULES_BASE => [ + 'article_id' => [ + 'integer', + Rule::exists('articles', 'id'), + ], + 'content' => [ + 'string', + ], + ], + static::VALIDATION_RULES_UPDATE => [ + static::VALIDATION_PREPEND_REQUIRED => [ + ], + ], + static::VALIDATION_RULES_CREATE => [ + static::VALIDATION_PREPEND_REQUIRED => [ + 'article_id', + 'content', + ], + ], + ]; + } +} diff --git a/code/app/Athenia/Providers/BaseRepositoryProvider.php b/code/app/Athenia/Providers/BaseRepositoryProvider.php index a60bc8ea..356e8beb 100644 --- a/code/app/Athenia/Providers/BaseRepositoryProvider.php +++ b/code/app/Athenia/Providers/BaseRepositoryProvider.php @@ -33,6 +33,7 @@ use App\Athenia\Contracts\Repositories\Wiki\ArticleIterationRepositoryContract; use App\Athenia\Contracts\Repositories\Wiki\ArticleModificationRepositoryContract; use App\Athenia\Contracts\Repositories\Wiki\ArticleRepositoryContract; +use App\Athenia\Contracts\Repositories\Wiki\ArticleSummaryRepositoryContract; use App\Athenia\Contracts\Repositories\Wiki\ArticleVersionRepositoryContract; use App\Athenia\Contracts\Repositories\Statistic\TargetStatisticRepositoryContract; use App\Athenia\Contracts\Repositories\Statistic\StatisticRepositoryContract; @@ -72,6 +73,7 @@ use App\Athenia\Repositories\Wiki\ArticleIterationRepository; use App\Athenia\Repositories\Wiki\ArticleModificationRepository; use App\Athenia\Repositories\Wiki\ArticleRepository; +use App\Athenia\Repositories\Wiki\ArticleSummaryRepository; use App\Athenia\Repositories\Wiki\ArticleVersionRepository; use App\Athenia\Repositories\Statistic\StatisticRepository; use App\Athenia\Repositories\Statistic\StatisticFilterRepository; @@ -106,6 +108,7 @@ use App\Models\Wiki\Article; use App\Models\Wiki\ArticleIteration; use App\Models\Wiki\ArticleModification; +use App\Models\Wiki\ArticleSummary; use App\Models\Wiki\ArticleVersion; use App\Models\Statistic\TargetStatistic; use App\Models\Statistic\Statistic; @@ -131,6 +134,7 @@ public final function provides(): array ArticleRepositoryContract::class, ArticleIterationRepositoryContract::class, ArticleModificationRepositoryContract::class, + ArticleSummaryRepositoryContract::class, ArticleVersionRepositoryContract::class, ArticleNoteRepositoryContract::class, AssetRepositoryContract::class, @@ -205,6 +209,12 @@ public final function register(): void $this->app->make('log'), ); }); + $this->app->bind(ArticleSummaryRepositoryContract::class, function() { + return new ArticleSummaryRepository( + new ArticleSummary(), + $this->app->make('log'), + ); + }); $this->app->bind(ArticleVersionRepositoryContract::class, function() { return new ArticleVersionRepository( new ArticleVersion(), diff --git a/code/app/Athenia/Repositories/Wiki/ArticleSummaryRepository.php b/code/app/Athenia/Repositories/Wiki/ArticleSummaryRepository.php new file mode 100644 index 00000000..c9fc7626 --- /dev/null +++ b/code/app/Athenia/Repositories/Wiki/ArticleSummaryRepository.php @@ -0,0 +1,26 @@ +|ArticleSummary getAggregateMethod() + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary isAppendRelationsCount() + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary isLeftJoin() + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary isUseTableAlias() + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary joinRelations($relations, $leftJoin = null) + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary newModelQuery() + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary newQuery() + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary orWhereInJoin($column, $values) + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary orWhereJoin($column, $operator, $value) + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary orWhereNotInJoin($column, $values) + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary orderByJoin($column, $direction = 'asc', $aggregateMethod = null) + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary query() + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary setAggregateMethod(string $aggregateMethod) + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary setAppendRelationsCount(bool $appendRelationsCount) + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary setLeftJoin(bool $leftJoin) + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary setUseTableAlias(bool $useTableAlias) + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary whereArticleId($value) + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary whereContent($value) + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary whereCreatedAt($value) + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary whereId($value) + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary whereInJoin($column, $values, $boolean = 'and', $not = false) + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary whereJoin($column, $operator, $value, $boolean = 'and') + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary whereNotInJoin($column, $values, $boolean = 'and') + * @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|ArticleSummary whereUpdatedAt($value) + * @mixin \Eloquent + */ +class ArticleSummary extends AtheniaArticleSummary +{ +} diff --git a/code/database/factories/Wiki/ArticleSummaryFactory.php b/code/database/factories/Wiki/ArticleSummaryFactory.php new file mode 100644 index 00000000..3a53cfbf --- /dev/null +++ b/code/database/factories/Wiki/ArticleSummaryFactory.php @@ -0,0 +1,31 @@ + Article::factory()->create()->id, + 'content' => $this->faker->paragraph(), + ]; + } +} diff --git a/code/database/migrations/2025_11_23_201741_create_article_summaries_table.php b/code/database/migrations/2025_11_23_201741_create_article_summaries_table.php new file mode 100644 index 00000000..24d0dd56 --- /dev/null +++ b/code/database/migrations/2025_11_23_201741_create_article_summaries_table.php @@ -0,0 +1,43 @@ +id(); + $table->unsignedInteger('article_id'); + $table->text('content'); + $table->timestamps(); + $table->softDeletes(); + + $table->foreign('article_id') + ->references('id') + ->on('articles') + ->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + Schema::dropIfExists('article_summaries'); + } +} diff --git a/code/tests/Athenia/Integration/Repositories/Wiki/ArticleSummaryRepositoryTest.php b/code/tests/Athenia/Integration/Repositories/Wiki/ArticleSummaryRepositoryTest.php new file mode 100644 index 00000000..312eefc2 --- /dev/null +++ b/code/tests/Athenia/Integration/Repositories/Wiki/ArticleSummaryRepositoryTest.php @@ -0,0 +1,102 @@ +setupDatabase(); + + $this->repository = new ArticleSummaryRepository( + new ArticleSummary(), + $this->getGenericLogMock() + ); + } + + public function testFindAllSuccess(): void + { + ArticleSummary::factory()->count(5)->create(); + $items = $this->repository->findAll(); + $this->assertCount(5, $items); + } + + public function testFindAllEmpty(): void + { + $items = $this->repository->findAll(); + $this->assertEmpty($items); + } + + public function testFindOrFailSuccess(): void + { + $model = ArticleSummary::factory()->create(); + + $foundModel = $this->repository->findOrFail($model->id); + $this->assertEquals($model->id, $foundModel->id); + } + + public function testFindOrFailFails(): void + { + ArticleSummary::factory()->create(['id' => 2]); + + $this->expectException(ModelNotFoundException::class); + $this->repository->findOrFail(1); + } + + public function testCreateSuccess(): void + { + $article = Article::factory()->create(); + + /** @var ArticleSummary $summary */ + $summary = $this->repository->create([ + 'article_id' => $article->id, + 'content' => 'This is a test summary.', + ]); + + $this->assertEquals('This is a test summary.', $summary->content); + $this->assertEquals($article->id, $summary->article_id); + } + + public function testUpdateSuccess(): void + { + $model = ArticleSummary::factory()->create([ + 'content' => 'Original summary' + ]); + $this->repository->update($model, [ + 'content' => 'Updated summary', + ]); + + $updated = ArticleSummary::find($model->id); + $this->assertEquals('Updated summary', $updated->content); + } + + public function testDeleteSuccess(): void + { + $model = ArticleSummary::factory()->create(); + + $this->repository->delete($model); + + $this->assertNull(ArticleSummary::find($model->id)); + } +} diff --git a/code/tests/Athenia/Unit/Models/Wiki/ArticleSummaryTest.php b/code/tests/Athenia/Unit/Models/Wiki/ArticleSummaryTest.php new file mode 100644 index 00000000..e4dc2317 --- /dev/null +++ b/code/tests/Athenia/Unit/Models/Wiki/ArticleSummaryTest.php @@ -0,0 +1,25 @@ + 324, + ]); + $relation = $summary->article(); + $this->assertEquals('article_id', $relation->getForeignKeyName()); + $this->assertInstanceOf(Article::class, $relation->getRelated()); + } +} From e65bae00dd4daa1c96f002814a906b0db5879136 Mon Sep 17 00:00:00 2001 From: Bryce Meyer Date: Sun, 23 Nov 2025 21:46:36 +0100 Subject: [PATCH 2/2] set up article summary endpoint --- .../Wiki/ArticleSummaryControllerAbstract.php | 118 ++++++++++++++++++ .../Wiki/ArticleSummary/DeleteRequest.php | 51 ++++++++ .../Wiki/ArticleSummary/StoreRequest.php | 59 +++++++++ .../Wiki/ArticleSummary/UpdateRequest.php | 60 +++++++++ .../Wiki/ArticleSummary/ViewRequest.php | 47 +++++++ .../Athenia/Models/Wiki/ArticleSummary.php | 5 - .../Wiki/ArticleSummaryController.php | 13 ++ .../Wiki/ArticleSummary/DeleteRequest.php | 13 ++ .../Wiki/ArticleSummary/StoreRequest.php | 13 ++ .../Wiki/ArticleSummary/UpdateRequest.php | 13 ++ .../Wiki/ArticleSummary/ViewRequest.php | 13 ++ .../Wiki/ArticleSummaryController.php | 13 ++ .../Policies/Wiki/ArticleSummaryPolicy.php | 64 ++++++++++ code/routes/core.php | 6 + .../ArticleSummaryCreateTest.php | 94 ++++++++++++++ .../ArticleSummaryDeleteTest.php | 87 +++++++++++++ .../ArticleSummaryUpdateTest.php | 113 +++++++++++++++++ .../ArticleSummary/ArticleSummaryViewTest.php | 69 ++++++++++ .../Wiki/ArticleSummaryPolicyTest.php | 99 +++++++++++++++ 19 files changed, 945 insertions(+), 5 deletions(-) create mode 100644 code/app/Athenia/Http/Core/Controllers/Wiki/ArticleSummaryControllerAbstract.php create mode 100644 code/app/Athenia/Http/Core/Requests/Wiki/ArticleSummary/DeleteRequest.php create mode 100644 code/app/Athenia/Http/Core/Requests/Wiki/ArticleSummary/StoreRequest.php create mode 100644 code/app/Athenia/Http/Core/Requests/Wiki/ArticleSummary/UpdateRequest.php create mode 100644 code/app/Athenia/Http/Core/Requests/Wiki/ArticleSummary/ViewRequest.php create mode 100644 code/app/Http/Core/Controllers/Wiki/ArticleSummaryController.php create mode 100644 code/app/Http/Core/Requests/Wiki/ArticleSummary/DeleteRequest.php create mode 100644 code/app/Http/Core/Requests/Wiki/ArticleSummary/StoreRequest.php create mode 100644 code/app/Http/Core/Requests/Wiki/ArticleSummary/UpdateRequest.php create mode 100644 code/app/Http/Core/Requests/Wiki/ArticleSummary/ViewRequest.php create mode 100644 code/app/Http/V1/Controllers/Wiki/ArticleSummaryController.php create mode 100644 code/app/Policies/Wiki/ArticleSummaryPolicy.php create mode 100644 code/tests/Athenia/Feature/Wiki/ArticleSummary/ArticleSummaryCreateTest.php create mode 100644 code/tests/Athenia/Feature/Wiki/ArticleSummary/ArticleSummaryDeleteTest.php create mode 100644 code/tests/Athenia/Feature/Wiki/ArticleSummary/ArticleSummaryUpdateTest.php create mode 100644 code/tests/Athenia/Feature/Wiki/ArticleSummary/ArticleSummaryViewTest.php create mode 100644 code/tests/Athenia/Integration/Policies/Wiki/ArticleSummaryPolicyTest.php diff --git a/code/app/Athenia/Http/Core/Controllers/Wiki/ArticleSummaryControllerAbstract.php b/code/app/Athenia/Http/Core/Controllers/Wiki/ArticleSummaryControllerAbstract.php new file mode 100644 index 00000000..6adade97 --- /dev/null +++ b/code/app/Athenia/Http/Core/Controllers/Wiki/ArticleSummaryControllerAbstract.php @@ -0,0 +1,118 @@ +repository = $repository; + } + + /** + * Display the summary for the article + * + * @param Requests\Wiki\ArticleSummary\ViewRequest $request + * @param Article $article + * @return JsonResponse + */ + public function show(Requests\Wiki\ArticleSummary\ViewRequest $request, Article $article): JsonResponse + { + $summary = $article->articleSummary; + + if (!$summary) { + return new JsonResponse([ + 'message' => 'Article summary not found.' + ], 404); + } + + return new JsonResponse($summary, 200); + } + + /** + * Create a new summary for the article + * + * @param Requests\Wiki\ArticleSummary\StoreRequest $request + * @param Article $article + * @return JsonResponse + */ + public function store(Requests\Wiki\ArticleSummary\StoreRequest $request, Article $article): JsonResponse + { + $data = $request->json()->all(); + $data['article_id'] = $article->id; + + /** @var ArticleSummary $model */ + $model = $this->repository->create($data); + + return new JsonResponse($model, 201); + } + + /** + * Update the article summary + * + * @param Requests\Wiki\ArticleSummary\UpdateRequest $request + * @param Article $article + * @return JsonResponse + */ + public function update(Requests\Wiki\ArticleSummary\UpdateRequest $request, Article $article): JsonResponse + { + $summary = $article->articleSummary; + + if (!$summary) { + return new JsonResponse([ + 'message' => 'Article summary not found.' + ], 404); + } + + $data = $request->json()->all(); + + /** @var ArticleSummary $updated */ + $updated = $this->repository->update($summary, $data); + + return new JsonResponse($updated, 200); + } + + /** + * Delete the article summary + * + * @param Requests\Wiki\ArticleSummary\DeleteRequest $request + * @param Article $article + * @return JsonResponse + */ + public function destroy(Requests\Wiki\ArticleSummary\DeleteRequest $request, Article $article): JsonResponse + { + $summary = $article->articleSummary; + + if (!$summary) { + return new JsonResponse([ + 'message' => 'Article summary not found.' + ], 404); + } + + $this->repository->delete($summary); + + return new JsonResponse(null, 204); + } +} diff --git a/code/app/Athenia/Http/Core/Requests/Wiki/ArticleSummary/DeleteRequest.php b/code/app/Athenia/Http/Core/Requests/Wiki/ArticleSummary/DeleteRequest.php new file mode 100644 index 00000000..061934ae --- /dev/null +++ b/code/app/Athenia/Http/Core/Requests/Wiki/ArticleSummary/DeleteRequest.php @@ -0,0 +1,51 @@ +route('article'), + ]; + } +} diff --git a/code/app/Athenia/Http/Core/Requests/Wiki/ArticleSummary/StoreRequest.php b/code/app/Athenia/Http/Core/Requests/Wiki/ArticleSummary/StoreRequest.php new file mode 100644 index 00000000..05efe699 --- /dev/null +++ b/code/app/Athenia/Http/Core/Requests/Wiki/ArticleSummary/StoreRequest.php @@ -0,0 +1,59 @@ +route('article'), + ]; + } + + /** + * @param ArticleSummary $model + * @return array + */ + public function rules(ArticleSummary $model) + { + return $model->getValidationRules(ArticleSummary::VALIDATION_RULES_CREATE); + } +} diff --git a/code/app/Athenia/Http/Core/Requests/Wiki/ArticleSummary/UpdateRequest.php b/code/app/Athenia/Http/Core/Requests/Wiki/ArticleSummary/UpdateRequest.php new file mode 100644 index 00000000..01f34aff --- /dev/null +++ b/code/app/Athenia/Http/Core/Requests/Wiki/ArticleSummary/UpdateRequest.php @@ -0,0 +1,60 @@ +route('article'), + ]; + } + + /** + * The rules for this request + * + * @param ArticleSummary $model + */ + public function rules(ArticleSummary $model) + { + return $model->getValidationRules(ArticleSummary::VALIDATION_RULES_UPDATE); + } +} diff --git a/code/app/Athenia/Http/Core/Requests/Wiki/ArticleSummary/ViewRequest.php b/code/app/Athenia/Http/Core/Requests/Wiki/ArticleSummary/ViewRequest.php new file mode 100644 index 00000000..df216a9a --- /dev/null +++ b/code/app/Athenia/Http/Core/Requests/Wiki/ArticleSummary/ViewRequest.php @@ -0,0 +1,47 @@ +route('article'), + ]; + } +} diff --git a/code/app/Athenia/Models/Wiki/ArticleSummary.php b/code/app/Athenia/Models/Wiki/ArticleSummary.php index 3c125a67..2382459b 100644 --- a/code/app/Athenia/Models/Wiki/ArticleSummary.php +++ b/code/app/Athenia/Models/Wiki/ArticleSummary.php @@ -70,10 +70,6 @@ public function buildModelValidationRules(...$params): array { return [ static::VALIDATION_RULES_BASE => [ - 'article_id' => [ - 'integer', - Rule::exists('articles', 'id'), - ], 'content' => [ 'string', ], @@ -84,7 +80,6 @@ public function buildModelValidationRules(...$params): array ], static::VALIDATION_RULES_CREATE => [ static::VALIDATION_PREPEND_REQUIRED => [ - 'article_id', 'content', ], ], diff --git a/code/app/Http/Core/Controllers/Wiki/ArticleSummaryController.php b/code/app/Http/Core/Controllers/Wiki/ArticleSummaryController.php new file mode 100644 index 00000000..357633c1 --- /dev/null +++ b/code/app/Http/Core/Controllers/Wiki/ArticleSummaryController.php @@ -0,0 +1,13 @@ +hasRole(Role::ARTICLE_EDITOR); + } + + /** + * Only users with the ARTICLE_EDITOR role can update article summaries + * + * @param User $user + * @param Article $article + * @return bool + */ + public function update(User $user, Article $article) + { + return $user->hasRole(Role::ARTICLE_EDITOR); + } + + /** + * Only users with the ARTICLE_EDITOR role can delete article summaries + * + * @param User $user + * @param Article $article + * @return bool + */ + public function delete(User $user, Article $article) + { + return $user->hasRole(Role::ARTICLE_EDITOR); + } +} diff --git a/code/routes/core.php b/code/routes/core.php index 32c86db2..da854c70 100644 --- a/code/routes/core.php +++ b/code/routes/core.php @@ -90,6 +90,12 @@ 'index', 'store', ], ]); + + // Article summary routes (singular resource pattern) + Route::get('article-summary', 'Wiki\ArticleSummaryController@show')->name('article-summary.show'); + Route::post('article-summary', 'Wiki\ArticleSummaryController@store')->name('article-summary.store'); + Route::put('article-summary', 'Wiki\ArticleSummaryController@update')->name('article-summary.update'); + Route::delete('article-summary', 'Wiki\ArticleSummaryController@destroy')->name('article-summary.destroy'); }); Route::resource('ballots', 'BallotController', [ diff --git a/code/tests/Athenia/Feature/Wiki/ArticleSummary/ArticleSummaryCreateTest.php b/code/tests/Athenia/Feature/Wiki/ArticleSummary/ArticleSummaryCreateTest.php new file mode 100644 index 00000000..1fe8b2fa --- /dev/null +++ b/code/tests/Athenia/Feature/Wiki/ArticleSummary/ArticleSummaryCreateTest.php @@ -0,0 +1,94 @@ +setupDatabase(); + $this->mockApplicationLog(); + $this->article = Article::factory()->create(); + $this->path = '/v1/articles/' . $this->article->id . '/article-summary'; + } + + public function testNotLoggedInUserBlocked(): void + { + $response = $this->json('POST', $this->path); + + $response->assertStatus(403); + } + + public function testUserWithoutRoleBlocked(): void + { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->json('POST', $this->path, [ + 'content' => 'Test summary content', + ]); + + $response->assertStatus(403); + } + + public function testCreateSuccessful(): void + { + $user = User::factory()->create(); + $user->roles()->attach(Role::ARTICLE_EDITOR); + $this->actingAs($user); + + $response = $this->json('POST', $this->path, [ + 'content' => 'This is a test summary for the article.', + ]); + + $response->assertStatus(201); + $response->assertJson([ + 'article_id' => $this->article->id, + 'content' => 'This is a test summary for the article.', + ]); + } + + public function testCreateFailsMissingRequiredFields(): void + { + $user = User::factory()->create(); + $user->roles()->attach(Role::ARTICLE_EDITOR); + $this->actingAs($user); + + $response = $this->json('POST', $this->path, []); + + $response->assertStatus(400); + $response->assertJsonValidationErrors(['content']); + } + + public function testCreateFailsInvalidStringFields(): void + { + $user = User::factory()->create(); + $user->roles()->attach(Role::ARTICLE_EDITOR); + $this->actingAs($user); + + $response = $this->json('POST', $this->path, [ + 'content' => 12345, + ]); + + $response->assertStatus(400); + $response->assertJsonValidationErrors(['content']); + } +} diff --git a/code/tests/Athenia/Feature/Wiki/ArticleSummary/ArticleSummaryDeleteTest.php b/code/tests/Athenia/Feature/Wiki/ArticleSummary/ArticleSummaryDeleteTest.php new file mode 100644 index 00000000..d75cc1e9 --- /dev/null +++ b/code/tests/Athenia/Feature/Wiki/ArticleSummary/ArticleSummaryDeleteTest.php @@ -0,0 +1,87 @@ +setupDatabase(); + $this->mockApplicationLog(); + $this->article = Article::factory()->create(); + $this->path = '/v1/articles/' . $this->article->id . '/article-summary'; + } + + public function testNotLoggedInUserBlocked(): void + { + ArticleSummary::factory()->create([ + 'article_id' => $this->article->id, + ]); + + $response = $this->json('DELETE', $this->path); + + $response->assertStatus(403); + } + + public function testUserWithoutRoleBlocked(): void + { + $user = User::factory()->create(); + $this->actingAs($user); + + ArticleSummary::factory()->create([ + 'article_id' => $this->article->id, + ]); + + $response = $this->json('DELETE', $this->path); + + $response->assertStatus(403); + } + + public function testNotFound(): void + { + $user = User::factory()->create(); + $user->roles()->attach(Role::ARTICLE_EDITOR); + $this->actingAs($user); + + $response = $this->json('DELETE', $this->path); + + $response->assertStatus(404); + } + + public function testDeleteSuccessful(): void + { + $user = User::factory()->create(); + $user->roles()->attach(Role::ARTICLE_EDITOR); + $this->actingAs($user); + + $summary = ArticleSummary::factory()->create([ + 'article_id' => $this->article->id, + ]); + + $response = $this->json('DELETE', $this->path); + + $response->assertStatus(204); + + // Verify the summary was soft-deleted + $this->assertNull(ArticleSummary::find($summary->id)); + } +} diff --git a/code/tests/Athenia/Feature/Wiki/ArticleSummary/ArticleSummaryUpdateTest.php b/code/tests/Athenia/Feature/Wiki/ArticleSummary/ArticleSummaryUpdateTest.php new file mode 100644 index 00000000..bee9d0d4 --- /dev/null +++ b/code/tests/Athenia/Feature/Wiki/ArticleSummary/ArticleSummaryUpdateTest.php @@ -0,0 +1,113 @@ +setupDatabase(); + $this->mockApplicationLog(); + $this->article = Article::factory()->create(); + $this->path = '/v1/articles/' . $this->article->id . '/article-summary'; + } + + public function testNotLoggedInUserBlocked(): void + { + ArticleSummary::factory()->create([ + 'article_id' => $this->article->id, + ]); + + $response = $this->json('PUT', $this->path); + + $response->assertStatus(403); + } + + public function testUserWithoutRoleBlocked(): void + { + $user = User::factory()->create(); + $this->actingAs($user); + + ArticleSummary::factory()->create([ + 'article_id' => $this->article->id, + ]); + + $response = $this->json('PUT', $this->path, [ + 'content' => 'Updated summary content', + ]); + + $response->assertStatus(403); + } + + public function testNotFound(): void + { + $user = User::factory()->create(); + $user->roles()->attach(Role::ARTICLE_EDITOR); + $this->actingAs($user); + + $response = $this->json('PUT', $this->path, [ + 'content' => 'Updated content', + ]); + + $response->assertStatus(404); + } + + public function testUpdateSuccessful(): void + { + $user = User::factory()->create(); + $user->roles()->attach(Role::ARTICLE_EDITOR); + $this->actingAs($user); + + ArticleSummary::factory()->create([ + 'article_id' => $this->article->id, + 'content' => 'Original summary content', + ]); + + $response = $this->json('PUT', $this->path, [ + 'content' => 'Updated summary content', + ]); + + $response->assertStatus(200); + $response->assertJson([ + 'article_id' => $this->article->id, + 'content' => 'Updated summary content', + ]); + } + + public function testUpdateFailsInvalidStringFields(): void + { + $user = User::factory()->create(); + $user->roles()->attach(Role::ARTICLE_EDITOR); + $this->actingAs($user); + + ArticleSummary::factory()->create([ + 'article_id' => $this->article->id, + ]); + + $response = $this->json('PUT', $this->path, [ + 'content' => 12345, + ]); + + $response->assertStatus(400); + $response->assertJsonValidationErrors(['content']); + } +} diff --git a/code/tests/Athenia/Feature/Wiki/ArticleSummary/ArticleSummaryViewTest.php b/code/tests/Athenia/Feature/Wiki/ArticleSummary/ArticleSummaryViewTest.php new file mode 100644 index 00000000..fa9fff0f --- /dev/null +++ b/code/tests/Athenia/Feature/Wiki/ArticleSummary/ArticleSummaryViewTest.php @@ -0,0 +1,69 @@ +setupDatabase(); + $this->mockApplicationLog(); + $this->article = Article::factory()->create(); + $this->path = '/v1/articles/' . $this->article->id . '/article-summary'; + } + + public function testNotLoggedInUserBlocked(): void + { + $response = $this->json('GET', $this->path); + + $response->assertStatus(403); + } + + public function testViewSuccessful(): void + { + $user = User::factory()->create(); + $this->actingAs($user); + + $summary = ArticleSummary::factory()->create([ + 'article_id' => $this->article->id, + 'content' => 'Test summary content', + ]); + + $response = $this->json('GET', $this->path); + + $response->assertStatus(200); + $response->assertJson([ + 'id' => $summary->id, + 'article_id' => $this->article->id, + 'content' => 'Test summary content', + ]); + } + + public function testViewNotFound(): void + { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->json('GET', $this->path); + + $response->assertStatus(404); + } +} diff --git a/code/tests/Athenia/Integration/Policies/Wiki/ArticleSummaryPolicyTest.php b/code/tests/Athenia/Integration/Policies/Wiki/ArticleSummaryPolicyTest.php new file mode 100644 index 00000000..77271352 --- /dev/null +++ b/code/tests/Athenia/Integration/Policies/Wiki/ArticleSummaryPolicyTest.php @@ -0,0 +1,99 @@ +setupDatabase(); + } + + public function testViewPasses(): void + { + $user = User::factory()->create(); + $article = Article::factory()->create(); + + $policy = new ArticleSummaryPolicy(); + + $this->assertTrue($policy->view($user, $article)); + } + + public function testCreatePassesWithRole(): void + { + $user = User::factory()->create(); + $user->roles()->attach(Role::ARTICLE_EDITOR); + $article = Article::factory()->create(); + + $policy = new ArticleSummaryPolicy(); + + $this->assertTrue($policy->create($user, $article)); + } + + public function testCreateFailsWithoutRole(): void + { + $user = User::factory()->create(); + $article = Article::factory()->create(); + + $policy = new ArticleSummaryPolicy(); + + $this->assertFalse($policy->create($user, $article)); + } + + public function testUpdatePassesWithRole(): void + { + $user = User::factory()->create(); + $user->roles()->attach(Role::ARTICLE_EDITOR); + $article = Article::factory()->create(); + + $policy = new ArticleSummaryPolicy(); + + $this->assertTrue($policy->update($user, $article)); + } + + public function testUpdateFailsWithoutRole(): void + { + $user = User::factory()->create(); + $article = Article::factory()->create(); + + $policy = new ArticleSummaryPolicy(); + + $this->assertFalse($policy->update($user, $article)); + } + + public function testDeletePassesWithRole(): void + { + $user = User::factory()->create(); + $user->roles()->attach(Role::ARTICLE_EDITOR); + $article = Article::factory()->create(); + + $policy = new ArticleSummaryPolicy(); + + $this->assertTrue($policy->delete($user, $article)); + } + + public function testDeleteFailsWithoutRole(): void + { + $user = User::factory()->create(); + $article = Article::factory()->create(); + + $policy = new ArticleSummaryPolicy(); + + $this->assertFalse($policy->delete($user, $article)); + } +}