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 @@ +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/Article.php b/code/app/Athenia/Models/Wiki/Article.php index fa510873..2a2649c3 100644 --- a/code/app/Athenia/Models/Wiki/Article.php +++ b/code/app/Athenia/Models/Wiki/Article.php @@ -15,11 +15,13 @@ use App\Models\User\User; use App\Models\Wiki\ArticleIteration; use App\Models\Wiki\ArticleModification; +use App\Models\Wiki\ArticleSummary; use App\Models\Wiki\ArticleVersion; use Eloquent; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\HasOne; /** * Class Article @@ -163,6 +165,16 @@ public function articleNotes() : HasMany return $this->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..2382459b --- /dev/null +++ b/code/app/Athenia/Models/Wiki/ArticleSummary.php @@ -0,0 +1,88 @@ +|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 => [ + 'content' => [ + 'string', + ], + ], + static::VALIDATION_RULES_UPDATE => [ + static::VALIDATION_PREPEND_REQUIRED => [ + ], + ], + static::VALIDATION_RULES_CREATE => [ + static::VALIDATION_PREPEND_REQUIRED => [ + '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/app/Policies/Wiki/ArticleSummaryPolicy.php b/code/app/Policies/Wiki/ArticleSummaryPolicy.php new file mode 100644 index 00000000..d704c820 --- /dev/null +++ b/code/app/Policies/Wiki/ArticleSummaryPolicy.php @@ -0,0 +1,64 @@ +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/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/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)); + } +} 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()); + } +}