Files
biiproject-kit-v1/tests/Feature/System/NotificationCenterTest.php
T

245 lines
8.5 KiB
PHP

<?php
use App\Models\Notification;
use App\Models\Permission;
use App\Models\Role;
use App\Models\User;
use App\Services\SystemConfig\SystemConfigService;
use Illuminate\Support\Facades\Cache;
beforeEach(function () {
$ref = new ReflectionClass(SystemConfigService::class);
$prop = $ref->getProperty('resolvedSettings');
$prop->setAccessible(true);
$prop->setValue(null, null);
Cache::flush();
});
function makeNotificationAdmin(): User
{
$role = Role::firstOrCreate(['name' => 'Developer', 'guard_name' => 'web']);
$user = User::factory()->create();
$user->assignRole($role);
foreach (['view notification center', 'manage notification center'] as $name) {
$perm = Permission::firstOrCreate(['name' => $name, 'guard_name' => 'web']);
$user->givePermissionTo($perm);
}
return $user;
}
function makeNotificationViewer(): User
{
$user = User::factory()->create();
$perm = Permission::firstOrCreate(['name' => 'view notification center', 'guard_name' => 'web']);
$user->givePermissionTo($perm);
return $user;
}
function makeNotificationForUser(User $user): Notification
{
$role = Role::firstOrCreate(['name' => 'User', 'guard_name' => 'web']);
$notification = Notification::create([
'title' => 'Test Notice',
'message' => 'Hello',
'recipient' => 'all',
'type' => 'info',
'created_by' => $user->id,
]);
// Attach pivot row so the user sees it
\DB::table('system_notification_user')->insert([
'notification_id' => $notification->id,
'user_id' => $user->id,
'read_at' => null,
'deleted_at' => null,
]);
return $notification;
}
// ── Access control ────────────────────────────────────────────────────────────
test('guest cannot view notification center', function () {
$this->get('/notification-center')->assertRedirect('/login');
});
test('user without permission is forbidden from notification center', function () {
$user = User::factory()->create();
$this->actingAs($user)->get('/notification-center')->assertForbidden();
});
test('user with view permission can access notification center', function () {
$user = makeNotificationViewer();
$this->actingAs($user)->get('/notification-center')->assertOk();
});
// ── Store (broadcast) ─────────────────────────────────────────────────────────
test('viewer without manage permission cannot broadcast notifications', function () {
$viewer = makeNotificationViewer();
Role::firstOrCreate(['name' => 'User', 'guard_name' => 'web']);
$this->actingAs($viewer)
->postJson('/notification-center', [
'title' => 'Test',
'message' => 'Msg',
'recipient' => 'User',
'type' => 'info',
])
->assertForbidden();
});
test('admin can broadcast a notification', function () {
$admin = makeNotificationAdmin();
Role::firstOrCreate(['name' => 'User', 'guard_name' => 'web']);
$this->actingAs($admin)
->postJson('/notification-center', [
'title' => 'Important Update',
'message' => 'Please read.',
'recipient' => 'User',
'type' => 'info',
])
->assertOk()
->assertJsonPath('success', true);
expect(Notification::where('title', 'Important Update')->count())->toBe(1);
});
test('store rejects unknown notification type', function () {
$admin = makeNotificationAdmin();
Role::firstOrCreate(['name' => 'User', 'guard_name' => 'web']);
$this->actingAs($admin)
->postJson('/notification-center', [
'title' => 'Bad Type',
'message' => 'Msg',
'recipient' => 'User',
'type' => 'xss-alert',
])
->assertStatus(422);
});
test('store requires title and message', function () {
$admin = makeNotificationAdmin();
$this->actingAs($admin)
->postJson('/notification-center', ['type' => 'info'])
->assertStatus(422);
});
// ── Mark as read ──────────────────────────────────────────────────────────────
test('user can mark a notification as read', function () {
$user = makeNotificationViewer();
$notification = makeNotificationForUser($user);
$this->actingAs($user)
->patchJson(route('notification-center.read', $notification->id))
->assertOk()
->assertJsonPath('success', true);
});
test('guest cannot mark notifications as read', function () {
$notification = Notification::create([
'title' => 'X', 'message' => 'Y', 'recipient' => 'all', 'type' => 'info',
]);
// JSON requests return 401 Unauthorized (not a redirect) when unauthenticated
$this->patchJson(route('notification-center.read', $notification->id))
->assertUnauthorized();
});
// ── Mark all as read ──────────────────────────────────────────────────────────
test('user can mark all notifications as read', function () {
$user = makeNotificationViewer();
$this->actingAs($user)
->patchJson(route('notification-center.read-all'))
->assertOk()
->assertJsonPath('success', true);
});
// ── Personal delete ───────────────────────────────────────────────────────────
test('user can delete (hide) a notification from their view', function () {
$user = makeNotificationViewer();
$notification = makeNotificationForUser($user);
$this->actingAs($user)
->deleteJson(route('notification-center.destroy', $notification->id))
->assertOk()
->assertJsonPath('success', true);
});
// ── Feature flag ──────────────────────────────────────────────────────────────
test('feature disabled blocks regular viewers but allows manage-global-settings users', function () {
app(SystemConfigService::class)->update(['feature_notification_center' => false]);
$viewer = makeNotificationViewer();
$this->actingAs($viewer)
->get('/notification-center')
->assertForbidden();
});
test('feature flag disabled still allows users with manage global settings', function () {
app(SystemConfigService::class)->update(['feature_notification_center' => false]);
$admin = makeNotificationViewer();
$perm = Permission::firstOrCreate(['name' => 'manage global settings', 'guard_name' => 'web']);
$admin->givePermissionTo($perm);
$this->actingAs($admin)
->get('/notification-center')
->assertOk();
});
// ── Recent notifications API ──────────────────────────────────────────────────
test('recent notifications endpoint returns json with unread count', function () {
$user = makeNotificationViewer();
$this->actingAs($user)
->getJson('/notification-center/api/recent')
->assertOk()
->assertJsonStructure(['success', 'unread_count', 'notifications', 'has_more']);
});
test('recent notifications content is escaped', function () {
$user = makeNotificationViewer();
$notification = Notification::create([
'title' => '<script>alert(1)</script>',
'message' => '<b>bold</b>',
'recipient' => 'all',
'type' => 'info',
'created_by' => $user->id,
]);
\DB::table('system_notification_user')->insert([
'notification_id' => $notification->id,
'user_id' => $user->id,
'read_at' => null,
'deleted_at' => null,
]);
$response = $this->actingAs($user)
->getJson('/notification-center/api/recent')
->json();
$titles = collect($response['notifications'])->pluck('title')->all();
// Title must be HTML-entity-escaped, not raw script tag
foreach ($titles as $t) {
expect($t)->not->toContain('<script>');
}
});