feat: push upozornění na doručení objednávky Bolt Food
CI / Generate TypeScript types (push) Successful in 12s
CI / Server unit tests (push) Successful in 22s
CI / Build server (push) Successful in 28s
CI / Build client (push) Successful in 41s
CI / Playwright E2E tests (push) Successful in 1m48s
CI / Build and push Docker image (push) Successful in 53s
CI / Notify (push) Successful in 2s
CI / Generate TypeScript types (push) Successful in 12s
CI / Server unit tests (push) Successful in 22s
CI / Build server (push) Successful in 28s
CI / Build client (push) Successful in 41s
CI / Playwright E2E tests (push) Successful in 1m48s
CI / Build and push Docker image (push) Successful in 53s
CI / Notify (push) Successful in 2s
Nové zaškrtávátko v Nastavení → Notifikace pod výběrem času připomínky. Když skupinová objednávka sledovaná přes Bolt Food přejde do stavu delivered/finished, dostanou push členové skupiny, kteří si to zapnuli. Zakladatel skupiny notifikaci nedostává — ten je informován aplikací Bolt. Push subscription byla dosud svázaná s časem připomínky (subscribe endpoint bez reminderTime vracel 400), takže by nový přepínač samostatně nefungoval. Čas připomínky je nově v registru volitelný a scheduler připomínek záznamy bez času přeskakuje. Service worker nemá natvrdo tag a akci "Mám vlastní/neobědvám" — obojí se řídí payloadem, aby je doručovací notifikace nedědila.
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
[
|
||||
"Upozornění na doručení skupinové objednávky sledované přes Bolt Food (zapíná se v Nastavení → Notifikace)"
|
||||
]
|
||||
@@ -7,6 +7,7 @@ import { formatDate } from './utils';
|
||||
import { getWebsocket } from './websocket';
|
||||
import { ClientData, GroupState } from '../../types/gen/types.gen';
|
||||
import { isBoltSimulated, getSimulatedBoltOrder } from './boltSimulator';
|
||||
import { notifyBoltDelivered } from './notifikace';
|
||||
|
||||
const storage = getStorage();
|
||||
const lease = createLeaderLease('luncher:bolt:leader');
|
||||
@@ -14,6 +15,7 @@ const lease = createLeaderLease('luncher:bolt:leader');
|
||||
const BOLT_POLLING_URL = 'https://deliveryuser.live.boltsvc.net/deliveryClient/public/getOrderPolling';
|
||||
const BOLT_TOKEN_REGEX = /^[0-9a-f]{64}$/i;
|
||||
const TERMINAL_STATE_REGEX = /delivered|finished|cancelled|rejected|failed/i;
|
||||
const DELIVERED_STATE_REGEX = /delivered|finished/i;
|
||||
const MAX_CONSECUTIVE_FAILURES = 10;
|
||||
|
||||
/** Identifikátor zařízení pro Bolt API — generuje se jednou na proces. */
|
||||
@@ -143,6 +145,13 @@ export async function checkBoltTracking(): Promise<void> {
|
||||
const courierChanged = courierState !== group.boltCourierState && !clearToken;
|
||||
if (!clearToken && !timeChanged && !stateChanged && !courierChanged) continue;
|
||||
|
||||
// Notifikujeme jen skutečný přechod do doručeného stavu. Porovnání s předchozím
|
||||
// stavem hlídá duplicity jak u přechodu delivered → finished, tak u tiku, kdy
|
||||
// objednávka zmizí z API poté, co už byla označena za doručenou.
|
||||
const deliveredNow = stateChanged
|
||||
&& DELIVERED_STATE_REGEX.test(orderState ?? '')
|
||||
&& !DELIVERED_STATE_REGEX.test(group.boltOrderState ?? '');
|
||||
|
||||
// Log každého přechodu stavu — Bolt API není dokumentované, takže si takhle
|
||||
// průběžně mapujeme jeho stavový automat (viz mapování v BoltOrderProgress.tsx).
|
||||
if (stateChanged || courierChanged) {
|
||||
@@ -166,6 +175,16 @@ export async function checkBoltTracking(): Promise<void> {
|
||||
if (clearToken) {
|
||||
console.log(`Bolt tracking: sledování skupiny "${group.name}" ukončeno`);
|
||||
}
|
||||
|
||||
// Až po uložení stavu, aby pád neposlal notifikaci podruhé.
|
||||
// Selhání push nesmí shodit celý tik sledování.
|
||||
if (deliveredNow) {
|
||||
try {
|
||||
await notifyBoltDelivered(group.name, Object.keys(group.members ?? {}), group.creatorLogin);
|
||||
} catch (e) {
|
||||
console.error(`Bolt tracking: chyba při odesílání notifikace o doručení skupiny "${group.name}"`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (updated) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getClientData, getToday } from "./service";
|
||||
import { getUsersByLocation, getHumanTime } from "./utils";
|
||||
import { NotifikaceData, NotifikaceInput, NotificationSettings } from '../../types';
|
||||
import getStorage from "./storage";
|
||||
import { sendPushToLogins } from "./pushReminder";
|
||||
|
||||
const ENVIRONMENT = process.env.NODE_ENV ?? 'production';
|
||||
dotenv.config({ path: path.resolve(__dirname, `../.env.${ENVIRONMENT}`) });
|
||||
@@ -223,3 +224,28 @@ export const callNotifikace = async ({ input, teams = true, gotify = false, ntfy
|
||||
console.error("Error in callNotifikace: ", error);
|
||||
}
|
||||
};
|
||||
|
||||
/** Cesta na stránku skupinových objednávek (viz OBJEDNANI_URL v klientovi). */
|
||||
const OBJEDNANI_PATH = '/objednani';
|
||||
|
||||
/**
|
||||
* Odešle push notifikaci o doručení skupinové objednávky členům skupiny,
|
||||
* kteří si to zapnuli v nastavení. Zakladatel skupiny notifikaci nedostává —
|
||||
* ten je o doručení informován přímo aplikací Bolt.
|
||||
*/
|
||||
export async function notifyBoltDelivered(groupName: string, memberLogins: string[], creatorLogin: string): Promise<void> {
|
||||
const recipients: string[] = [];
|
||||
for (const login of memberLogins) {
|
||||
if (login === creatorLogin) continue;
|
||||
const settings = await getNotificationSettings(login);
|
||||
if (settings.boltDeliveredPush) recipients.push(login);
|
||||
}
|
||||
if (recipients.length === 0) return;
|
||||
|
||||
await sendPushToLogins(recipients, {
|
||||
title: 'Luncher',
|
||||
body: `Objednávka „${groupName}" byla doručena!`,
|
||||
tag: `bolt-delivered-${groupName}`,
|
||||
url: OBJEDNANI_PATH,
|
||||
});
|
||||
}
|
||||
|
||||
+61
-12
@@ -13,10 +13,21 @@ const lease = createLeaderLease('luncher:reminder:leader');
|
||||
const POD_ID = process.env.POD_ID ?? `local-${process.pid}`;
|
||||
|
||||
interface RegistryEntry {
|
||||
time: string;
|
||||
/** Čas připomínky výběru oběda (HH:MM). Nevyplněno = uživatel odebírá jen ostatní push notifikace. */
|
||||
time?: string;
|
||||
subscription: webpush.PushSubscription;
|
||||
}
|
||||
|
||||
/** Data odesílaná do service workeru. */
|
||||
export interface PushPayload {
|
||||
title: string;
|
||||
body: string;
|
||||
/** Seskupovací tag notifikace (výchozí v service workeru je připomínka oběda). */
|
||||
tag?: string;
|
||||
/** Cesta, která se otevře po kliknutí na notifikaci. */
|
||||
url?: string;
|
||||
}
|
||||
|
||||
type Registry = Record<string, RegistryEntry>;
|
||||
|
||||
/** Mapa login → timestamp (ms) posledního odeslání připomínky. */
|
||||
@@ -55,14 +66,18 @@ export function stopReminderScheduler(): void {
|
||||
}
|
||||
}
|
||||
|
||||
/** Přidá nebo aktualizuje push subscription pro uživatele. */
|
||||
export async function subscribePush(login: string, subscription: webpush.PushSubscription, reminderTime: string): Promise<void> {
|
||||
/**
|
||||
* Přidá nebo aktualizuje push subscription pro uživatele.
|
||||
* `reminderTime` je volitelný — bez něj uživatel dostává jen ostatní push notifikace
|
||||
* (např. doručení objednávky Bolt Food), ale ne připomínku výběru oběda.
|
||||
*/
|
||||
export async function subscribePush(login: string, subscription: webpush.PushSubscription, reminderTime?: string): Promise<void> {
|
||||
await storage.updateData<Registry>(REGISTRY_KEY, (current) => {
|
||||
const registry = current ?? {};
|
||||
registry[login] = { time: reminderTime, subscription };
|
||||
return registry;
|
||||
});
|
||||
console.log(`Push reminder: uživatel ${login} přihlášen k připomínkám v ${reminderTime}`);
|
||||
console.log(`Push reminder: uživatel ${login} přihlášen k push notifikacím${reminderTime ? `, připomínka v ${reminderTime}` : ' (bez připomínky oběda)'}`);
|
||||
}
|
||||
|
||||
/** Odebere push subscription pro uživatele. */
|
||||
@@ -94,6 +109,45 @@ export function verifyQuickChoiceToken(login: string, token: string): boolean {
|
||||
return crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(token, 'hex'));
|
||||
}
|
||||
|
||||
/** Odebere z registru subscriptions, které push služba označila za neplatné (404/410). */
|
||||
async function pruneExpiredSubscriptions(expiredLogins: string[]): Promise<void> {
|
||||
if (expiredLogins.length === 0) return;
|
||||
await storage.updateData<Registry>(REGISTRY_KEY, (current) => {
|
||||
const registry = current ?? {};
|
||||
for (const login of expiredLogins) delete registry[login];
|
||||
return registry;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Odešle push notifikaci daným uživatelům. Loginy bez registrované subscription
|
||||
* se tiše přeskočí, expirované subscriptions se z registru odeberou.
|
||||
*/
|
||||
export async function sendPushToLogins(logins: string[], payload: PushPayload): Promise<void> {
|
||||
if (logins.length === 0) return;
|
||||
|
||||
const registry = await storage.getData<Registry>(REGISTRY_KEY) ?? {};
|
||||
const expiredLogins: string[] = [];
|
||||
|
||||
for (const login of logins) {
|
||||
const entry = registry[login];
|
||||
if (!entry) continue;
|
||||
try {
|
||||
await webpush.sendNotification(entry.subscription, JSON.stringify(payload));
|
||||
console.log(`Push: odeslána notifikace "${payload.title}" uživateli ${login}`);
|
||||
} catch (error: any) {
|
||||
if (error.statusCode === 410 || error.statusCode === 404) {
|
||||
console.log(`Push: subscription uživatele ${login} expirovala, odebírám`);
|
||||
expiredLogins.push(login);
|
||||
} else {
|
||||
console.error(`Push: chyba při odesílání notifikace uživateli ${login}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await pruneExpiredSubscriptions(expiredLogins);
|
||||
}
|
||||
|
||||
/** Zkontroluje a odešle připomínky uživatelům, kteří si nezvolili oběd. */
|
||||
async function checkAndSendReminders(): Promise<void> {
|
||||
if (getIsWeekend(getToday())) return;
|
||||
@@ -119,7 +173,8 @@ async function checkAndSendReminders(): Promise<void> {
|
||||
const expiredLogins: string[] = [];
|
||||
|
||||
for (const [login, entry] of entries) {
|
||||
if (currentTime < entry.time) continue;
|
||||
// Uživatelé bez nastaveného času připomínky odebírají jen ostatní push notifikace
|
||||
if (!entry.time || currentTime < entry.time) continue;
|
||||
|
||||
const last = lastReminded.get(login) ?? 0;
|
||||
if (Date.now() - last < REMINDER_COOLDOWN_MS) continue;
|
||||
@@ -148,13 +203,7 @@ async function checkAndSendReminders(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
if (expiredLogins.length > 0) {
|
||||
await storage.updateData<Registry>(REGISTRY_KEY, (current) => {
|
||||
const r = current ?? {};
|
||||
for (const login of expiredLogins) delete r[login];
|
||||
return r;
|
||||
});
|
||||
}
|
||||
await pruneExpiredSubscriptions(expiredLogins);
|
||||
}
|
||||
|
||||
/** Spustí scheduler pro kontrolu a odesílání připomínek každou minutu. */
|
||||
|
||||
@@ -26,6 +26,7 @@ router.post("/settings", async (req: Request<{}, any, UpdateNotificationSettings
|
||||
teamsWebhookUrl: req.body.teamsWebhookUrl,
|
||||
enabledEvents: req.body.enabledEvents,
|
||||
reminderTime: req.body.reminderTime,
|
||||
boltDeliveredPush: req.body.boltDeliveredPush,
|
||||
});
|
||||
res.status(200).json(settings);
|
||||
} catch (e: any) { next(e) }
|
||||
@@ -40,22 +41,19 @@ router.get("/push/vapidKey", (req, res) => {
|
||||
res.status(200).json({ key });
|
||||
});
|
||||
|
||||
/** Přihlásí uživatele k push připomínkám. */
|
||||
/** Přihlásí uživatele k push notifikacím. Čas připomínky výběru oběda je volitelný. */
|
||||
router.post("/push/subscribe", async (req, res, next) => {
|
||||
const login = getLogin(parseToken(req));
|
||||
try {
|
||||
if (!req.body.subscription) {
|
||||
return res.status(400).json({ error: "Nebyla předána push subscription" });
|
||||
}
|
||||
if (!req.body.reminderTime) {
|
||||
return res.status(400).json({ error: "Nebyl předán čas připomínky" });
|
||||
}
|
||||
await subscribePush(login, req.body.subscription, req.body.reminderTime);
|
||||
res.status(200).json({});
|
||||
} catch (e: any) { next(e) }
|
||||
});
|
||||
|
||||
/** Odhlásí uživatele z push připomínek. */
|
||||
/** Odhlásí uživatele z push notifikací. */
|
||||
router.post("/push/unsubscribe", async (req, res, next) => {
|
||||
const login = getLogin(parseToken(req));
|
||||
try {
|
||||
|
||||
@@ -2,7 +2,9 @@ import axios from 'axios';
|
||||
import { resetMemoryStorage } from '../storage/memory';
|
||||
import getStorage from '../storage';
|
||||
import { addStore } from '../stores';
|
||||
import { createGroup, setGroupState, setGroupBoltTracking } from '../groups';
|
||||
import { createGroup, setGroupState, setGroupBoltTracking, addGroupMember } from '../groups';
|
||||
import { saveNotificationSettings } from '../notifikace';
|
||||
import { sendPushToLogins } from '../pushReminder';
|
||||
import { extractBoltToken, computeDeliveryHHMM, checkBoltTracking } from '../boltTracking';
|
||||
import { startBoltSimulation, advanceBoltSimulation, setBoltSimulationStep, stopBoltSimulationByGroup } from '../boltSimulator';
|
||||
import { ClientData, GroupState } from '../../../types/gen/types.gen';
|
||||
@@ -16,6 +18,9 @@ jest.mock('../websocket', () => ({
|
||||
getWebsocket: () => ({ emit: mockEmit }),
|
||||
}));
|
||||
|
||||
jest.mock('../pushReminder', () => ({ sendPushToLogins: jest.fn() }));
|
||||
const mockedSendPush = sendPushToLogins as jest.MockedFunction<typeof sendPushToLogins>;
|
||||
|
||||
const storage = getStorage();
|
||||
|
||||
const CREATOR = 'tomas';
|
||||
@@ -321,3 +326,91 @@ describe('DEV simulace (boltSimulator + checkBoltTracking)', () => {
|
||||
expect(g.boltTrackingToken).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('notifikace o doručení objednávky', () => {
|
||||
const MEMBER = 'petr';
|
||||
const OTHER = 'jana';
|
||||
let groupId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
const d = await createGroup(CREATOR, STORE);
|
||||
groupId = d.groups![0].id;
|
||||
await addGroupMember(CREATOR, groupId, MEMBER);
|
||||
await addGroupMember(CREATOR, groupId, OTHER);
|
||||
await setGroupState(CREATOR, groupId, GroupState.LOCKED);
|
||||
await setGroupState(CREATOR, groupId, GroupState.ORDERED);
|
||||
await setGroupBoltTracking(CREATOR, groupId, SHARE_URL);
|
||||
});
|
||||
|
||||
/** Posune objednávku do stavu "na cestě", aby další poll byl skutečný přechod. */
|
||||
async function tickInDelivery() {
|
||||
mockedAxios.post.mockResolvedValue(boltResponse({ order_state: 'in_delivery', expected_time_to_client_in_seconds: 600 }));
|
||||
await checkBoltTracking();
|
||||
}
|
||||
|
||||
test('doručení notifikuje členy, kteří to mají zapnuté', async () => {
|
||||
await saveNotificationSettings(MEMBER, { boltDeliveredPush: true });
|
||||
await saveNotificationSettings(OTHER, { boltDeliveredPush: false });
|
||||
await tickInDelivery();
|
||||
|
||||
mockedAxios.post.mockResolvedValue(boltResponse({ order_state: 'delivered' }));
|
||||
await checkBoltTracking();
|
||||
|
||||
expect(mockedSendPush).toHaveBeenCalledTimes(1);
|
||||
expect(mockedSendPush).toHaveBeenCalledWith(
|
||||
[MEMBER],
|
||||
expect.objectContaining({ body: expect.stringContaining(STORE), url: '/objednani' }),
|
||||
);
|
||||
});
|
||||
|
||||
test('zakladatel notifikaci nedostane, ani když ji má zapnutou', async () => {
|
||||
await saveNotificationSettings(CREATOR, { boltDeliveredPush: true });
|
||||
await saveNotificationSettings(MEMBER, { boltDeliveredPush: true });
|
||||
await tickInDelivery();
|
||||
|
||||
mockedAxios.post.mockResolvedValue(boltResponse({ order_state: 'delivered' }));
|
||||
await checkBoltTracking();
|
||||
|
||||
expect(mockedSendPush).toHaveBeenCalledWith([MEMBER], expect.anything());
|
||||
});
|
||||
|
||||
test('bez zapnutého nastavení se neposílá nic', async () => {
|
||||
await tickInDelivery();
|
||||
mockedAxios.post.mockResolvedValue(boltResponse({ order_state: 'delivered' }));
|
||||
await checkBoltTracking();
|
||||
expect(mockedSendPush).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('notifikace se neposílá opakovaně (delivered → finished → objednávka zmizí)', async () => {
|
||||
await saveNotificationSettings(MEMBER, { boltDeliveredPush: true });
|
||||
await tickInDelivery();
|
||||
|
||||
mockedAxios.post.mockResolvedValue(boltResponse({ order_state: 'delivered' }));
|
||||
await checkBoltTracking();
|
||||
expect(mockedSendPush).toHaveBeenCalledTimes(1);
|
||||
|
||||
mockedAxios.post.mockResolvedValue(boltResponse({ order_state: 'finished' }));
|
||||
await checkBoltTracking();
|
||||
mockedAxios.post.mockResolvedValue(boltResponse(null));
|
||||
await checkBoltTracking();
|
||||
expect(mockedSendPush).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('zmizelá objednávka bez předchozího doručení notifikuje', async () => {
|
||||
await saveNotificationSettings(MEMBER, { boltDeliveredPush: true });
|
||||
await tickInDelivery();
|
||||
|
||||
mockedAxios.post.mockResolvedValue(boltResponse(null));
|
||||
await checkBoltTracking();
|
||||
expect(mockedSendPush).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('zrušená objednávka notifikaci neposílá', async () => {
|
||||
await saveNotificationSettings(MEMBER, { boltDeliveredPush: true });
|
||||
await tickInDelivery();
|
||||
|
||||
mockedAxios.post.mockResolvedValue(boltResponse({ order_state: 'cancelled' }));
|
||||
await checkBoltTracking();
|
||||
expect(mockedSendPush).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import webpush from 'web-push';
|
||||
import { resetMemoryStorage } from '../storage/memory';
|
||||
import getStorage from '../storage';
|
||||
import { subscribePush, unsubscribePush, sendPushToLogins } from '../pushReminder';
|
||||
|
||||
jest.mock('web-push');
|
||||
const mockedWebpush = webpush as jest.Mocked<typeof webpush>;
|
||||
|
||||
const storage = getStorage();
|
||||
const REGISTRY_KEY = 'push_reminder_registry';
|
||||
|
||||
const SUB_A = { endpoint: 'https://push.example/a', keys: { p256dh: 'a', auth: 'a' } } as any;
|
||||
const SUB_B = { endpoint: 'https://push.example/b', keys: { p256dh: 'b', auth: 'b' } } as any;
|
||||
|
||||
async function getRegistry(): Promise<Record<string, { time?: string; subscription: unknown }>> {
|
||||
return await storage.getData(REGISTRY_KEY) ?? {};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
resetMemoryStorage();
|
||||
jest.clearAllMocks();
|
||||
mockedWebpush.sendNotification.mockResolvedValue({} as any);
|
||||
});
|
||||
|
||||
describe('subscribePush', () => {
|
||||
test('uloží subscription i bez času připomínky', async () => {
|
||||
await subscribePush('petr', SUB_A);
|
||||
const entry = (await getRegistry())['petr'];
|
||||
expect(entry.subscription).toEqual(SUB_A);
|
||||
expect(entry.time).toBeUndefined();
|
||||
});
|
||||
|
||||
test('uloží čas připomínky, když je zadán', async () => {
|
||||
await subscribePush('petr', SUB_A, '10:30');
|
||||
expect((await getRegistry())['petr'].time).toBe('10:30');
|
||||
});
|
||||
|
||||
test('opakované přihlášení přepíše předchozí záznam', async () => {
|
||||
await subscribePush('petr', SUB_A, '10:30');
|
||||
await subscribePush('petr', SUB_B);
|
||||
const entry = (await getRegistry())['petr'];
|
||||
expect(entry.subscription).toEqual(SUB_B);
|
||||
expect(entry.time).toBeUndefined();
|
||||
});
|
||||
|
||||
test('unsubscribePush záznam odebere', async () => {
|
||||
await subscribePush('petr', SUB_A);
|
||||
await unsubscribePush('petr');
|
||||
expect(await getRegistry()).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendPushToLogins', () => {
|
||||
const PAYLOAD = { title: 'Luncher', body: 'Objednávka byla doručena!', url: '/objednani' };
|
||||
|
||||
test('odešle notifikaci registrovaným uživatelům', async () => {
|
||||
await subscribePush('petr', SUB_A);
|
||||
await subscribePush('jana', SUB_B, '11:00');
|
||||
|
||||
await sendPushToLogins(['petr', 'jana'], PAYLOAD);
|
||||
|
||||
expect(mockedWebpush.sendNotification).toHaveBeenCalledTimes(2);
|
||||
expect(mockedWebpush.sendNotification).toHaveBeenCalledWith(SUB_A, JSON.stringify(PAYLOAD));
|
||||
expect(mockedWebpush.sendNotification).toHaveBeenCalledWith(SUB_B, JSON.stringify(PAYLOAD));
|
||||
});
|
||||
|
||||
test('přeskočí uživatele bez registrované subscription', async () => {
|
||||
await subscribePush('petr', SUB_A);
|
||||
await sendPushToLogins(['petr', 'nikdo'], PAYLOAD);
|
||||
expect(mockedWebpush.sendNotification).toHaveBeenCalledTimes(1);
|
||||
expect(mockedWebpush.sendNotification).toHaveBeenCalledWith(SUB_A, expect.any(String));
|
||||
});
|
||||
|
||||
test('prázdný seznam neodesílá nic', async () => {
|
||||
await subscribePush('petr', SUB_A);
|
||||
await sendPushToLogins([], PAYLOAD);
|
||||
expect(mockedWebpush.sendNotification).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('expirovanou subscription (410) odebere z registru', async () => {
|
||||
await subscribePush('petr', SUB_A);
|
||||
await subscribePush('jana', SUB_B);
|
||||
mockedWebpush.sendNotification.mockImplementation(async (sub: any) => {
|
||||
if (sub.endpoint === SUB_A.endpoint) throw Object.assign(new Error('Gone'), { statusCode: 410 });
|
||||
return {} as any;
|
||||
});
|
||||
|
||||
await sendPushToLogins(['petr', 'jana'], PAYLOAD);
|
||||
|
||||
const registry = await getRegistry();
|
||||
expect(registry['petr']).toBeUndefined();
|
||||
expect(registry['jana']).toBeDefined();
|
||||
});
|
||||
|
||||
test('ostatní chyby subscription nemažou a nešíří se dál', async () => {
|
||||
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
await subscribePush('petr', SUB_A);
|
||||
mockedWebpush.sendNotification.mockRejectedValue(Object.assign(new Error('Boom'), { statusCode: 500 }));
|
||||
|
||||
await expect(sendPushToLogins(['petr'], PAYLOAD)).resolves.toBeUndefined();
|
||||
expect((await getRegistry())['petr']).toBeDefined();
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user