diff --git a/app/Livewire/Backend/Article/Form.php b/app/Livewire/Backend/Article/Form.php index 7fbeba14..efd8d320 100644 --- a/app/Livewire/Backend/Article/Form.php +++ b/app/Livewire/Backend/Article/Form.php @@ -33,16 +33,16 @@ class Form extends Component 'articleData.meta.image_url' => 'nullable|url', ]; - public function mount(?Article $article = null): void + public function mount($article = null): void { - if ($article->id) { + if ($article?->id) { $this->originalArticle = $article; $this->articleData = $article->toArray(); $this->articleData['keywords'] = $article->keywords->pluck('name')->implode(' '); $this->articleData['meta'] = $article->meta ?: []; } - $this->method = $article->id ? 'put' : 'post'; + $this->method = $article?->id ? 'put' : 'post'; } public function render(): View @@ -75,7 +75,7 @@ protected function store(array $articleData): void $newArticle = Article::query()->create($articleData); //add keywords - $keywordsToAttach = array_unique(explode(' ', Arr::get($this->article, 'keywords'))); + $keywordsToAttach = array_unique(explode(' ', Arr::get($this->articleData, 'keywords'))); foreach ($keywordsToAttach as $keywordToAttach) { if (empty($keywordToAttach)) { diff --git a/app/Livewire/Backend/Config/Index.php b/app/Livewire/Backend/Config/Index.php index 3f418a89..ef420e38 100644 --- a/app/Livewire/Backend/Config/Index.php +++ b/app/Livewire/Backend/Config/Index.php @@ -34,6 +34,6 @@ public function update(): void Config::query()->findOrFail($this->editingConfig['id']) ->update(Arr::get($data, 'editingConfig')); - $this->reset(['editingConfig', 'editingConfigId']); + $this->reset(['editingConfig']); } } diff --git a/app/Models/Feedback.php b/app/Models/Feedback.php index 774fb044..2b582f9f 100644 --- a/app/Models/Feedback.php +++ b/app/Models/Feedback.php @@ -2,11 +2,13 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Feedback extends Model { use CanFormatDates; + use HasFactory; protected $table = 'feedbacks'; diff --git a/database/factories/ArticleFactory.php b/database/factories/ArticleFactory.php index accecf85..28e9115a 100644 --- a/database/factories/ArticleFactory.php +++ b/database/factories/ArticleFactory.php @@ -3,6 +3,7 @@ namespace Database\Factories; use App\Models\Article; +use App\Models\Category; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Str; @@ -26,7 +27,7 @@ public function definition(): array 'user_id' => null, 'language' => $language = $this->faker->randomElement(['ben', 'eng']), 'slug' => Str::slug($heading, '-', $language), - 'category_id' => null, + 'category_id' => Category::factory()->create()->id, ]; } diff --git a/database/factories/FeedbackFactory.php b/database/factories/FeedbackFactory.php new file mode 100644 index 00000000..225a0294 --- /dev/null +++ b/database/factories/FeedbackFactory.php @@ -0,0 +1,29 @@ + + */ +class FeedbackFactory extends Factory +{ + + protected $model = Feedback::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => $this->faker->name(), + 'email' => $this->faker->unique()->safeEmail(), + 'content' => $this->faker->text(), + ]; + } +} diff --git a/tests/Feature/Controllers/ArticleControllerTest.php b/tests/Feature/Controllers/ArticleControllerTest.php index 5f9f5fdd..8092787f 100644 --- a/tests/Feature/Controllers/ArticleControllerTest.php +++ b/tests/Feature/Controllers/ArticleControllerTest.php @@ -93,7 +93,7 @@ public function testShowById() $this->get("article/{$article->id}/{$article->heading}")->assertOk(); - $this->get('article/'.Str::random()."/{$article->heading}") + $this->get('article/'.Str::random().time()."/{$article->heading}") ->assertRedirectToRoute('home'); } diff --git a/tests/Feature/Controllers/CommentControllerTest.php b/tests/Feature/Controllers/CommentControllerTest.php index b42d3025..16b59bda 100644 --- a/tests/Feature/Controllers/CommentControllerTest.php +++ b/tests/Feature/Controllers/CommentControllerTest.php @@ -2,10 +2,13 @@ namespace Tests\Feature\Controllers; +use App\Mail\NotifyCommentThread; use App\Models\Article; use App\Models\Category; use App\Models\Comment; +use App\Models\Reader; use App\Models\User; +use Illuminate\Support\Facades\Mail; use Illuminate\Support\Str; use Spatie\Permission\Models\Role; use Tests\TestCase; @@ -27,6 +30,12 @@ public function setUp(): void $this->user = User::factory() ->create(['name' => 'Example User', 'email' => 'example@test.com']); + Reader::query()->create([ + 'user_id' => $this->user->id, + 'is_verified' => false, + 'notify' => true, + ]); + $this->category = Category::factory()->create(); $this->article = Article::factory()->published()->create([ @@ -38,6 +47,11 @@ public function setUp(): void public function testConfirmComment() { + $parentComment = Comment::factory()->create([ + 'user_id' => $this->user->id, + 'article_id' => $this->article->id, + ]); + $comment = Comment::factory() ->create([ 'user_id' => $this->user->id, @@ -45,16 +59,27 @@ public function testConfirmComment() 'is_published' => 0, 'is_confirmed' => 0, 'token' => 'test-token', + 'parent_comment_id' => $parentComment->id, ]); + Mail::fake(); + $this->get("comment/{$comment->id}/confirm/?token={$comment->token}") ->assertRedirect(route('get-article', [$comment->article->id])) ->assertSessionHas('successMsg'); - $comment = Comment::query()->find($comment->id); + $this->assertDatabaseHas('comments', [ + 'id' => $comment->id, + 'is_published' => 1, + 'is_confirmed' => 1, + ]); + + $this->assertDatabaseHas('readers', [ + 'user_id' => $this->user->id, + 'is_verified' => 1, + ]); - $this->assertEquals(1, $comment->is_published); - $this->assertEquals(1, $comment->is_confirmed); + Mail::assertQueued(NotifyCommentThread::class); } public function testConfirmCommentFailsWithInvalidToken() @@ -72,8 +97,11 @@ public function testConfirmCommentFailsWithInvalidToken() ->assertRedirectToRoute('home') ->assertSessionHas('errorMsg'); - $this->assertEquals(0, $comment->is_published); - $this->assertEquals(0, $comment->is_confirmed); + $this->assertDatabaseHas('comments', [ + 'id' => $comment->id, + 'is_published' => 0, + 'is_confirmed' => 0, + ]); } public function testConfirmCommentFailsWithAlreadyPublishedComment() @@ -91,7 +119,10 @@ public function testConfirmCommentFailsWithAlreadyPublishedComment() ->assertRedirectToRoute('get-article', [$comment->article->id]) ->assertSessionHas('warningMsg'); - $this->assertEquals(1, $comment->is_published); - $this->assertEquals(1, $comment->is_confirmed); + $this->assertDatabaseHas('comments', [ + 'id' => $comment->id, + 'is_published' => 1, + 'is_confirmed' => 1, + ]); } } diff --git a/tests/Feature/Controllers/HomeControllerTest.php b/tests/Feature/Controllers/HomeControllerTest.php index 24f9a6dc..c6c23d15 100644 --- a/tests/Feature/Controllers/HomeControllerTest.php +++ b/tests/Feature/Controllers/HomeControllerTest.php @@ -9,6 +9,7 @@ use App\Models\User; use Exception; use Illuminate\Validation\ValidationException; +use Spatie\Permission\Models\Role; use Tests\TestCase; class HomeControllerTest extends TestCase @@ -46,6 +47,21 @@ public function testHomePage() ->assertSee(Config::get('site_title')); } + public function testAdminDashboard() + { + Category::factory()->create(); + $admin = Role::findOrCreate('admin'); + $user = User::factory()->create(); + $user->assignRole($admin); + + $this->actingAs($user) + ->get('/') + ->assertViewIs('backend.dashboard') + ->assertViewHas('categories') + ->assertViewHas('latestComments') + ->assertViewHas('latestFeedbacks'); + } + public function testGetMessage() { $controller = new HomeController(); diff --git a/tests/Feature/Livewire/Backend/Article/FormTest.php b/tests/Feature/Livewire/Backend/Article/FormTest.php new file mode 100644 index 00000000..c7a7b224 --- /dev/null +++ b/tests/Feature/Livewire/Backend/Article/FormTest.php @@ -0,0 +1,135 @@ +assertStatus(Response::HTTP_OK) + ->assertViewIs('livewire.backend.article.form') + ->assertViewHas('categories'); + } + + public function test_initializes_correctly_without_article() + { + Livewire::test(Form::class) + ->assertSet('method', 'post') + ->assertSet('articleData', []); + } + + public function test_initializes_correctly_with_article() + { + $article = Article::factory()->create(); + Livewire::test(Form::class, ['article' => $article]) + ->assertSet('method', 'put') + ->assertSet('articleData.heading', $article->heading) + ->assertSet('articleData.slug', $article->slug) + ->assertSet('articleData.category_id', $article->category_id); + } + + public function test_generates_slug_when_heading_changes() + { + Livewire::test(Form::class) + ->set('articleData.heading', 'New Article') + ->set('articleData.language', 'en') + ->assertSet('articleData.slug', Str::slug('New Article')); + } + + public function test_validates_article_data_correctly() + { + Livewire::test(Form::class) + ->set('articleData.heading', '') + ->set('articleData.slug', '') + ->set('articleData.category_id', null) + ->set('articleData.content', '') + ->set('articleData.language', '') + ->call('submit') + ->assertHasErrors([ + 'articleData.heading' => 'required', + 'articleData.slug' => 'required', + 'articleData.category_id' => 'required', + 'articleData.content' => 'required', + 'articleData.language' => 'required', + ]); + } + + public function test_stores_a_new_article_correctly() + { + Mail::fake(); + Auth::loginUsingId(User::factory()->create()->id); + + $category = Category::factory()->create(); + $data = [ + 'heading' => 'Test Article', + 'slug' => 'test-article', + 'category_id' => $category->id, + 'content' => 'This is a test article.', + 'language' => 'en', + 'is_comment_enabled' => true, + 'meta' => [ + 'description' => 'Test description', + 'image_url' => 'http://example.com/image.jpg', + ], + 'keywords' => 'test article', + ]; + + Livewire::test(Form::class, ['article' => null]) + ->set('articleData', $data) + ->call('submit') + ->assertSessionHas('success', 'Article published successfully!'); + + $this->assertDatabaseHas('articles', ['heading' => 'Test Article']); + $this->assertDatabaseHas('keywords', ['name' => 'test']); + $this->assertDatabaseHas('keywords', ['name' => 'article']); + Mail::assertQueued(NotifySubscriberForNewArticle::class); + } + + public function test_updates_an_existing_article_correctly() + { + Mail::fake(); + Auth::loginUsingId(User::factory()->create()->id); + + $article = Article::factory()->create(); + $data = [ + 'heading' => 'Updated Article', + 'slug' => 'updated-article', + 'category_id' => $article->category_id, + 'content' => 'This is an updated article.', + 'language' => 'en', + 'is_comment_enabled' => true, + 'meta' => [ + 'description' => 'Updated description', + 'image_url' => 'http://example.com/updated-image.jpg', + ], + 'keywords' => 'updated article', + ]; + + Livewire::test(Form::class, ['article' => $article]) + ->set('articleData', $data) + ->call('submit') + ->assertSessionHas('successMsg', 'Article updated successfully!'); + + $this->assertDatabaseHas('articles', [ + 'id' => $article->id, + 'heading' => 'Updated Article' + ]); + $this->assertDatabaseHas('keywords', ['name' => 'updated']); + $this->assertDatabaseHas('keywords', ['name' => 'article']); + } +} diff --git a/tests/Feature/Livewire/Backend/Article/IndexTest.php b/tests/Feature/Livewire/Backend/Article/IndexTest.php new file mode 100644 index 00000000..e3a8a46e --- /dev/null +++ b/tests/Feature/Livewire/Backend/Article/IndexTest.php @@ -0,0 +1,76 @@ +create(); + $author = Role::findOrCreate('author'); + $user->assignRole($author); + Auth::loginUsingId($user->id); + } + public function testItRendersCorrectly() + { + Auth::loginUsingId(User::factory()->create()->id); + + Livewire::test(Index::class) + ->assertStatus(200) + ->assertViewIs('livewire.backend.article.index'); + } + + public function testItFiltersArticlesBasedOnCategory() + { + $category = Category::factory()->create(); + Article::factory()->count(3)->create(['category_id' => $category->id]); + Article::factory()->count(2)->create(); // Articles in different categories + + Livewire::test(Index::class) + ->set('category', $category->id) + ->assertSeeInOrder([$category->name], 'category') + ->assertViewHas('articles', function ($articles) use ($category) { + return $articles->every(fn($article) => $article->category_id === $category->id); + }); + } + + public function testItFiltersArticlesBasedOnKeyword() + { + $keyword = Keyword::factory()->create(); + $articleWithKeyword = Article::factory()->create(); + $articleWithKeyword->keywords()->attach($keyword); + + Article::factory()->count(2)->create(); // Articles without the keyword + + Livewire::test(Index::class) + ->set('keyword', $keyword->name) + ->assertSeeInOrder([$keyword->name], 'keywords') + ->assertViewHas('articles', function ($articles) use ($keyword) { + return $articles->every(fn($article) => $article->keywords->contains($keyword)); + }); + } + + public function testItFiltersArticlesBasedOnSearchQuery() + { + $query = 'Unique Title'; + Article::factory()->create(['heading' => $query]); + Article::factory()->count(2)->create(['heading' => 'Other Title']); + + Livewire::test(Index::class) + ->set('query', $query) + ->assertViewHas('articles', function ($articles) use ($query) { + return $articles->every(fn($article) => stripos($article->heading, $query) !== false); + }); + } +} diff --git a/tests/Feature/Livewire/Backend/Config/IndexTest.php b/tests/Feature/Livewire/Backend/Config/IndexTest.php new file mode 100644 index 00000000..44b9f685 --- /dev/null +++ b/tests/Feature/Livewire/Backend/Config/IndexTest.php @@ -0,0 +1,47 @@ +assertStatus(Response::HTTP_OK) + ->assertViewIs('livewire.backend.config.index') + ->assertViewHas('configs'); + } + + public function testStartEditing() + { + $config = Config::query()->create(['name' => 'test_config', 'value' => Str::random()]); + + Livewire::test(Index::class) + ->call('startEditing', ['config' => $config->id]) + ->assertSee('editingConfig') + ->assertReturned(null); + } + + public function testUpdate() + { + $config = Config::query()->create(['name' => $name = 'test_config', 'value' => Str::random()]); + + Livewire::test(Index::class, ['editingConfig' => []]) + ->set('editingConfig', ['id' => $config->id, 'value' => $value = Str::random()]) + ->call('update', ['config' => $config->id]) + ->assertReturned(null); + + $this->assertDatabaseHas('configs', [ + 'id' => $config->id, + 'name' => $name, + 'value' => $value, + ]); + } +} diff --git a/tests/Feature/Livewire/Backend/Feedback/IndexTest.php b/tests/Feature/Livewire/Backend/Feedback/IndexTest.php new file mode 100644 index 00000000..50f3eaf5 --- /dev/null +++ b/tests/Feature/Livewire/Backend/Feedback/IndexTest.php @@ -0,0 +1,49 @@ +assertStatus(Response::HTTP_OK) + ->assertViewIs('livewire.backend.feedback.index') + ->assertViewHas('feedbacks'); + } + + public function testToggleResolvedTrue() + { + $feedback = Feedback::factory()->create(['is_resolved' => false,]); + Livewire::test(Index::class) + ->call('toggleResolved', ['feedback' => $feedback->id]); + + $this->assertDatabaseHas('feedbacks', ['id' => $feedback->id, 'is_resolved' => 1]); + } + + public function testToggleResolvedFalse() + { + $feedback = Feedback::factory()->create(['is_resolved' => true,]); + + Livewire::test(Index::class) + ->call('toggleResolved', ['feedback' => $feedback->id]); + + $this->assertDatabaseHas('feedbacks', ['id' => $feedback->id, 'is_resolved' => 0]); + } + + public function testClose() + { + $feedback = Feedback::factory()->create(['is_closed' => false,]); + + Livewire::test(Index::class) + ->call('close', ['feedback' => $feedback->id]); + + $this->assertDatabaseHas('feedbacks', ['id' => $feedback->id, 'is_closed' => 1]); + } +} diff --git a/tests/Feature/Livewire/Article/CommentTest.php b/tests/Feature/Livewire/Frontend/Article/CommentTest.php similarity index 97% rename from tests/Feature/Livewire/Article/CommentTest.php rename to tests/Feature/Livewire/Frontend/Article/CommentTest.php index 3108f145..da1f7eb2 100644 --- a/tests/Feature/Livewire/Article/CommentTest.php +++ b/tests/Feature/Livewire/Frontend/Article/CommentTest.php @@ -1,6 +1,6 @@