c413004930
Dependency: - minishlink/web-push v10 (VAPID JWT + aes128gcm payload encryption) - Dockerfile: add curl, mbstring, gmp extensions (web-push needs ext-curl) VAPID: - config/webpush.php from env; `php artisan push:vapid` generates keypair - Shared platform keypair; .env.example has empty placeholders Schema: - push_subscriptions (user/company, endpoint unique, p256dh, auth, encoding) WebPushService: - send / sendToUser / dispatch via WebPush::flush - Auto-prunes subscriptions reported expired (404/410) Subscribe flow: - POST /push/subscribe + /push/unsubscribe (auth, tenant) - Tenant panel JS subscribes after SW registration with VAPID public key Service worker (/sw.js): - Cache v2, push listener → showNotification, notificationclick → focus/open Install prompt: - Floating "Instalează aplicația" button wired to beforeinstallprompt Staff push: - WorkOrder master_id change → push to assigned mechanic - Settings "Test notificare push" action Tests (6 new): - subscribe stores + upserts; requires auth (401); validation (422); service configured; sendToUser with no subs returns zero Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
113 lines
3.8 KiB
PHP
113 lines
3.8 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\Central\Company;
|
|
use App\Models\Central\Plan;
|
|
use App\Models\Tenant\PushSubscription;
|
|
use App\Models\Tenant\User;
|
|
use App\Services\Notifications\WebPushService;
|
|
use App\Tenancy\TenantManager;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Tests\TestCase;
|
|
|
|
class PushSubscriptionTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
public function test_subscribe_stores_subscription_for_user(): void
|
|
{
|
|
[$company, $user] = $this->makeUser('sub1');
|
|
|
|
$this->actingAs($user)
|
|
->postJson('http://sub1.service.mir.md/push/subscribe', [
|
|
'endpoint' => 'https://push.example.com/abc123',
|
|
'keys' => ['p256dh' => 'PUBLICKEY', 'auth' => 'AUTHTOKEN'],
|
|
'contentEncoding' => 'aes128gcm',
|
|
])
|
|
->assertOk()
|
|
->assertJson(['ok' => true]);
|
|
|
|
$this->assertDatabaseHas('push_subscriptions', [
|
|
'endpoint' => 'https://push.example.com/abc123',
|
|
'user_id' => $user->id,
|
|
'public_key' => 'PUBLICKEY',
|
|
'content_encoding' => 'aes128gcm',
|
|
]);
|
|
}
|
|
|
|
public function test_subscribe_upserts_on_duplicate_endpoint(): void
|
|
{
|
|
[$company, $user] = $this->makeUser('sub2');
|
|
|
|
$payload = [
|
|
'endpoint' => 'https://push.example.com/dup',
|
|
'keys' => ['p256dh' => 'KEY1', 'auth' => 'AUTH1'],
|
|
];
|
|
|
|
$this->actingAs($user)->postJson('http://sub2.service.mir.md/push/subscribe', $payload)->assertOk();
|
|
$payload['keys']['p256dh'] = 'KEY2';
|
|
$this->actingAs($user)->postJson('http://sub2.service.mir.md/push/subscribe', $payload)->assertOk();
|
|
|
|
$this->assertEquals(1, PushSubscription::where('endpoint', 'https://push.example.com/dup')->count());
|
|
$this->assertEquals('KEY2', PushSubscription::where('endpoint', 'https://push.example.com/dup')->value('public_key'));
|
|
}
|
|
|
|
public function test_subscribe_requires_auth(): void
|
|
{
|
|
$this->makeUser('sub3');
|
|
$this->postJson('http://sub3.service.mir.md/push/subscribe', [
|
|
'endpoint' => 'https://push.example.com/noauth',
|
|
'keys' => ['p256dh' => 'X', 'auth' => 'Y'],
|
|
])->assertStatus(401);
|
|
}
|
|
|
|
public function test_validation_rejects_missing_keys(): void
|
|
{
|
|
[$company, $user] = $this->makeUser('sub4');
|
|
$this->actingAs($user)
|
|
->postJson('http://sub4.service.mir.md/push/subscribe', [
|
|
'endpoint' => 'https://push.example.com/x',
|
|
])
|
|
->assertStatus(422);
|
|
}
|
|
|
|
public function test_webpush_service_reports_configured(): void
|
|
{
|
|
$svc = app(WebPushService::class);
|
|
$this->assertTrue($svc->configured());
|
|
$this->assertNotEmpty($svc->publicKey());
|
|
}
|
|
|
|
public function test_send_to_user_with_no_subscriptions_returns_zero(): void
|
|
{
|
|
[$company, $user] = $this->makeUser('sub5');
|
|
$r = app(WebPushService::class)->sendToUser($user->id, 'T', 'B');
|
|
$this->assertEquals(0, $r['sent']);
|
|
$this->assertEquals(0, $r['pruned']);
|
|
}
|
|
|
|
private function makeUser(string $slug): array
|
|
{
|
|
$plan = Plan::firstOrCreate(['slug' => 'test'], ['name' => 'T', 'price' => 0, 'features' => []]);
|
|
$company = Company::create([
|
|
'plan_id' => $plan->id,
|
|
'slug' => $slug,
|
|
'name' => ucfirst($slug),
|
|
'status' => 'active',
|
|
]);
|
|
app(TenantManager::class)->setCurrent($company);
|
|
|
|
$user = User::create([
|
|
'company_id' => $company->id,
|
|
'name' => 'Push User',
|
|
'email' => $slug . '@example.com',
|
|
'password' => bcrypt('secret'),
|
|
'role' => 'admin',
|
|
'status' => 'active',
|
|
]);
|
|
|
|
return [$company, $user];
|
|
}
|
|
}
|