feat: add routes, lang, tests, stubs, docs, and docker configurations
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
<?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>');
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user