245 lines
8.5 KiB
PHP
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>');
|
|
}
|
|
});
|