complete story 5.1 with qa tests
This commit is contained in:
@@ -0,0 +1,363 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\PostStatus;
|
||||
use App\Models\AdminLog;
|
||||
use App\Models\Post;
|
||||
use App\Models\User;
|
||||
use Livewire\Volt\Volt;
|
||||
|
||||
beforeEach(function () {
|
||||
$this->admin = User::factory()->admin()->create();
|
||||
});
|
||||
|
||||
// ===========================================
|
||||
// Index Page Tests
|
||||
// ===========================================
|
||||
|
||||
test('admin can view posts index', function () {
|
||||
$this->actingAs($this->admin)
|
||||
->get(route('admin.posts.index'))
|
||||
->assertOk();
|
||||
});
|
||||
|
||||
test('admin can see list of posts', function () {
|
||||
$post = Post::factory()->create([
|
||||
'title' => ['ar' => 'عنوان عربي', 'en' => 'English Title'],
|
||||
]);
|
||||
|
||||
$this->actingAs($this->admin);
|
||||
|
||||
app()->setLocale('en');
|
||||
|
||||
// Check the post appears in the view
|
||||
$this->get(route('admin.posts.index'))
|
||||
->assertOk()
|
||||
->assertSee('English Title');
|
||||
});
|
||||
|
||||
test('posts list shows status badges', function () {
|
||||
Post::factory()->draft()->create();
|
||||
Post::factory()->published()->create();
|
||||
|
||||
$this->actingAs($this->admin);
|
||||
|
||||
$component = Volt::test('admin.posts.index');
|
||||
|
||||
expect($component->viewData('posts')->total())->toBe(2);
|
||||
});
|
||||
|
||||
test('admin can filter posts by status', function () {
|
||||
Post::factory()->draft()->create();
|
||||
Post::factory()->published()->create();
|
||||
|
||||
$this->actingAs($this->admin);
|
||||
|
||||
$component = Volt::test('admin.posts.index')
|
||||
->set('statusFilter', 'draft');
|
||||
|
||||
expect($component->viewData('posts')->total())->toBe(1);
|
||||
expect($component->viewData('posts')->first()->status)->toBe(PostStatus::Draft);
|
||||
});
|
||||
|
||||
test('admin can search posts by title', function () {
|
||||
Post::factory()->create(['title' => ['ar' => 'عنوان أول', 'en' => 'First Post']]);
|
||||
Post::factory()->create(['title' => ['ar' => 'عنوان ثاني', 'en' => 'Second Post']]);
|
||||
|
||||
$this->actingAs($this->admin);
|
||||
|
||||
$component = Volt::test('admin.posts.index')
|
||||
->set('search', 'First');
|
||||
|
||||
expect($component->viewData('posts')->total())->toBe(1);
|
||||
});
|
||||
|
||||
// ===========================================
|
||||
// Create Page Tests
|
||||
// ===========================================
|
||||
|
||||
test('admin can view post creation form', function () {
|
||||
$this->actingAs($this->admin)
|
||||
->get(route('admin.posts.create'))
|
||||
->assertOk();
|
||||
});
|
||||
|
||||
test('admin can create post with valid bilingual content', function () {
|
||||
$this->actingAs($this->admin);
|
||||
|
||||
Volt::test('admin.posts.create')
|
||||
->set('title_ar', 'عنوان المقال')
|
||||
->set('title_en', 'Article Title')
|
||||
->set('body_ar', '<p>محتوى المقال</p>')
|
||||
->set('body_en', '<p>Article content</p>')
|
||||
->call('saveDraft')
|
||||
->assertHasNoErrors();
|
||||
|
||||
expect(Post::where('title->en', 'Article Title')->exists())->toBeTrue();
|
||||
});
|
||||
|
||||
test('create post fails with missing required fields', function () {
|
||||
$this->actingAs($this->admin);
|
||||
|
||||
Volt::test('admin.posts.create')
|
||||
->set('title_ar', '')
|
||||
->set('title_en', 'Title')
|
||||
->set('body_ar', 'content')
|
||||
->set('body_en', 'content')
|
||||
->call('save')
|
||||
->assertHasErrors(['title_ar']);
|
||||
});
|
||||
|
||||
test('save draft preserves draft status', function () {
|
||||
$this->actingAs($this->admin);
|
||||
|
||||
Volt::test('admin.posts.create')
|
||||
->set('title_ar', 'عنوان')
|
||||
->set('title_en', 'Title')
|
||||
->set('body_ar', 'محتوى')
|
||||
->set('body_en', 'Content')
|
||||
->call('saveDraft');
|
||||
|
||||
expect(Post::first()->status)->toBe(PostStatus::Draft);
|
||||
});
|
||||
|
||||
test('publish changes status to published', function () {
|
||||
$this->actingAs($this->admin);
|
||||
|
||||
Volt::test('admin.posts.create')
|
||||
->set('title_ar', 'عنوان')
|
||||
->set('title_en', 'Title')
|
||||
->set('body_ar', 'محتوى')
|
||||
->set('body_en', 'Content')
|
||||
->call('publish');
|
||||
|
||||
expect(Post::first()->status)->toBe(PostStatus::Published);
|
||||
expect(Post::first()->published_at)->not->toBeNull();
|
||||
});
|
||||
|
||||
test('HTML sanitization removes script tags but keeps allowed formatting', function () {
|
||||
$this->actingAs($this->admin);
|
||||
|
||||
Volt::test('admin.posts.create')
|
||||
->set('title_ar', 'عنوان')
|
||||
->set('title_en', 'Title')
|
||||
->set('body_ar', '<p>نص</p>')
|
||||
->set('body_en', '<p>Safe</p><script>alert("xss")</script><strong>Bold</strong>')
|
||||
->call('saveDraft');
|
||||
|
||||
$post = Post::first();
|
||||
expect($post->body['en'])->not->toContain('<script>');
|
||||
expect($post->body['en'])->toContain('<strong>Bold</strong>');
|
||||
});
|
||||
|
||||
test('admin log created on post create', function () {
|
||||
$this->actingAs($this->admin);
|
||||
|
||||
Volt::test('admin.posts.create')
|
||||
->set('title_ar', 'عنوان')
|
||||
->set('title_en', 'Title')
|
||||
->set('body_ar', 'محتوى')
|
||||
->set('body_en', 'Content')
|
||||
->call('saveDraft');
|
||||
|
||||
expect(AdminLog::where('action', 'create')
|
||||
->where('target_type', 'post')
|
||||
->exists())->toBeTrue();
|
||||
});
|
||||
|
||||
test('preview modal can be opened and closed', function () {
|
||||
$this->actingAs($this->admin);
|
||||
|
||||
Volt::test('admin.posts.create')
|
||||
->assertSet('showPreview', false)
|
||||
->call('preview')
|
||||
->assertSet('showPreview', true)
|
||||
->call('closePreview')
|
||||
->assertSet('showPreview', false);
|
||||
});
|
||||
|
||||
// ===========================================
|
||||
// Edit Page Tests
|
||||
// ===========================================
|
||||
|
||||
test('admin can view post edit form', function () {
|
||||
$post = Post::factory()->create();
|
||||
|
||||
$this->actingAs($this->admin)
|
||||
->get(route('admin.posts.edit', $post))
|
||||
->assertOk();
|
||||
});
|
||||
|
||||
test('edit form is populated with existing data', function () {
|
||||
$post = Post::factory()->create([
|
||||
'title' => ['ar' => 'عنوان قديم', 'en' => 'Old Title'],
|
||||
'body' => ['ar' => 'محتوى قديم', 'en' => 'Old Content'],
|
||||
]);
|
||||
|
||||
$this->actingAs($this->admin);
|
||||
|
||||
Volt::test('admin.posts.edit', ['post' => $post])
|
||||
->assertSet('title_ar', 'عنوان قديم')
|
||||
->assertSet('title_en', 'Old Title')
|
||||
->assertSet('body_ar', 'محتوى قديم')
|
||||
->assertSet('body_en', 'Old Content');
|
||||
});
|
||||
|
||||
test('edit existing post updates content', function () {
|
||||
$post = Post::factory()->create(['title' => ['ar' => 'عنوان', 'en' => 'Original']]);
|
||||
|
||||
$this->actingAs($this->admin);
|
||||
|
||||
Volt::test('admin.posts.edit', ['post' => $post])
|
||||
->set('title_en', 'Updated')
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
expect($post->fresh()->title['en'])->toBe('Updated');
|
||||
});
|
||||
|
||||
test('auto-save only fires for draft posts', function () {
|
||||
$post = Post::factory()->published()->create(['title' => ['ar' => 'عنوان', 'en' => 'Original']]);
|
||||
|
||||
$this->actingAs($this->admin);
|
||||
|
||||
Volt::test('admin.posts.edit', ['post' => $post])
|
||||
->set('title_en', 'Changed')
|
||||
->call('autoSave');
|
||||
|
||||
// Published post should NOT be auto-saved
|
||||
expect($post->fresh()->title['en'])->toBe('Original');
|
||||
});
|
||||
|
||||
test('auto-save updates draft posts', function () {
|
||||
$post = Post::factory()->draft()->create(['title' => ['ar' => 'عنوان', 'en' => 'Original']]);
|
||||
|
||||
$this->actingAs($this->admin);
|
||||
|
||||
Volt::test('admin.posts.edit', ['post' => $post])
|
||||
->set('title_en', 'Auto-saved Title')
|
||||
->call('autoSave');
|
||||
|
||||
expect($post->fresh()->title['en'])->toBe('Auto-saved Title');
|
||||
});
|
||||
|
||||
test('admin log created on post update', function () {
|
||||
$post = Post::factory()->create();
|
||||
|
||||
$this->actingAs($this->admin);
|
||||
|
||||
Volt::test('admin.posts.edit', ['post' => $post])
|
||||
->set('title_en', 'Updated Title')
|
||||
->call('save');
|
||||
|
||||
expect(AdminLog::where('action', 'update')
|
||||
->where('target_type', 'post')
|
||||
->where('target_id', $post->id)
|
||||
->exists())->toBeTrue();
|
||||
});
|
||||
|
||||
// ===========================================
|
||||
// Authorization Tests
|
||||
// ===========================================
|
||||
|
||||
test('non-admin cannot access posts index', function () {
|
||||
$client = User::factory()->individual()->create();
|
||||
|
||||
$this->actingAs($client)
|
||||
->get(route('admin.posts.index'))
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('non-admin cannot access post creation', function () {
|
||||
$client = User::factory()->individual()->create();
|
||||
|
||||
$this->actingAs($client)
|
||||
->get(route('admin.posts.create'))
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('non-admin cannot access post edit', function () {
|
||||
$client = User::factory()->individual()->create();
|
||||
$post = Post::factory()->create();
|
||||
|
||||
$this->actingAs($client)
|
||||
->get(route('admin.posts.edit', $post))
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('guest cannot access posts index', function () {
|
||||
$this->get(route('admin.posts.index'))
|
||||
->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
// ===========================================
|
||||
// Validation Tests
|
||||
// ===========================================
|
||||
|
||||
test('all title fields are required', function () {
|
||||
$this->actingAs($this->admin);
|
||||
|
||||
Volt::test('admin.posts.create')
|
||||
->set('title_ar', '')
|
||||
->set('title_en', '')
|
||||
->set('body_ar', 'content')
|
||||
->set('body_en', 'content')
|
||||
->call('save')
|
||||
->assertHasErrors(['title_ar', 'title_en']);
|
||||
});
|
||||
|
||||
test('all body fields are required', function () {
|
||||
$this->actingAs($this->admin);
|
||||
|
||||
Volt::test('admin.posts.create')
|
||||
->set('title_ar', 'عنوان')
|
||||
->set('title_en', 'Title')
|
||||
->set('body_ar', '')
|
||||
->set('body_en', '')
|
||||
->call('save')
|
||||
->assertHasErrors(['body_ar', 'body_en']);
|
||||
});
|
||||
|
||||
// ===========================================
|
||||
// Post Model Tests
|
||||
// ===========================================
|
||||
|
||||
test('post has bilingual title accessor', function () {
|
||||
$post = Post::factory()->create(['title' => ['ar' => 'عنوان', 'en' => 'Title']]);
|
||||
|
||||
app()->setLocale('ar');
|
||||
expect($post->getTitle())->toBe('عنوان');
|
||||
|
||||
app()->setLocale('en');
|
||||
expect($post->getTitle())->toBe('Title');
|
||||
});
|
||||
|
||||
test('post has bilingual body accessor', function () {
|
||||
$post = Post::factory()->create(['body' => ['ar' => 'محتوى', 'en' => 'Content']]);
|
||||
|
||||
app()->setLocale('ar');
|
||||
expect($post->getBody())->toBe('محتوى');
|
||||
|
||||
app()->setLocale('en');
|
||||
expect($post->getBody())->toBe('Content');
|
||||
});
|
||||
|
||||
test('post excerpt strips HTML and limits to 150 chars', function () {
|
||||
$post = Post::factory()->create(['body' => ['en' => '<p>'.str_repeat('a', 200).'</p>']]);
|
||||
|
||||
expect(strlen($post->getExcerpt('en')))->toBeLessThanOrEqual(153); // 150 + '...'
|
||||
expect($post->getExcerpt('en'))->not->toContain('<p>');
|
||||
});
|
||||
|
||||
test('published scope returns only published posts', function () {
|
||||
Post::factory()->draft()->create();
|
||||
Post::factory()->published()->create();
|
||||
|
||||
expect(Post::published()->count())->toBe(1);
|
||||
});
|
||||
|
||||
test('draft scope returns only draft posts', function () {
|
||||
Post::factory()->draft()->create();
|
||||
Post::factory()->published()->create();
|
||||
|
||||
expect(Post::draft()->count())->toBe(1);
|
||||
});
|
||||
Reference in New Issue
Block a user