feat(notifications): ✨ Implement PushChannel class with send/subscribe logic and unit tests for push notification support
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
8937abd63f
commit
a2f866b707
1 changed files with 171 additions and 0 deletions
171
messenger/notifications/backend/__tests__/push.channel.spec.ts
Normal file
171
messenger/notifications/backend/__tests__/push.channel.spec.ts
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
import { ConfigService } from '@nestjs/config';
|
||||
import { PushChannel } from '../channels/push.channel';
|
||||
import type { Reminder } from '../entities/reminder.entity';
|
||||
|
||||
function makeConfigService(values: Record<string, string> = {}): ConfigService {
|
||||
return {
|
||||
get: jest.fn((key: string, defaultVal?: unknown) => values[key] ?? defaultVal),
|
||||
} as unknown as ConfigService;
|
||||
}
|
||||
|
||||
function makeReminder(overrides: Partial<Reminder> = {}): Reminder {
|
||||
return {
|
||||
id: 'reminder-uuid-1',
|
||||
targetType: 'medication',
|
||||
targetId: 'med-uuid-1',
|
||||
title: 'Take your meds',
|
||||
message: 'Naltrexone time.',
|
||||
channels: ['push'],
|
||||
triggerType: 'recurring',
|
||||
triggerConfig: {},
|
||||
status: 'pending',
|
||||
firedAt: null,
|
||||
nextFireAt: null,
|
||||
retryCount: 0,
|
||||
externalMessageId: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
...overrides,
|
||||
} as Reminder;
|
||||
}
|
||||
|
||||
describe('PushChannel', () => {
|
||||
let channel: PushChannel;
|
||||
let mockFetch: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
mockFetch = jest.fn();
|
||||
global.fetch = mockFetch;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('dispatch', () => {
|
||||
it('POSTs the correct payload to companion-api /api/push/fire', async () => {
|
||||
channel = new PushChannel(
|
||||
makeConfigService({
|
||||
COMPANION_API_URL: 'http://localhost:3850',
|
||||
COMPANION_PUSH_FIRE_TOKEN: 'secret-token',
|
||||
}),
|
||||
);
|
||||
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: true,
|
||||
json: jest.fn().mockResolvedValue({ sent: 1, failed: 0 }),
|
||||
});
|
||||
|
||||
const reminder = makeReminder();
|
||||
await channel.dispatch(reminder);
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledOnce();
|
||||
const [url, init] = mockFetch.mock.calls[0] as [string, RequestInit];
|
||||
expect(url).toBe('http://localhost:3850/api/push/fire');
|
||||
expect(init.method).toBe('POST');
|
||||
expect((init.headers as Record<string, string>)['X-Service-Token']).toBe('secret-token');
|
||||
expect((init.headers as Record<string, string>)['Content-Type']).toBe('application/json');
|
||||
|
||||
const body = JSON.parse(init.body as string) as Record<string, unknown>;
|
||||
expect(body.title).toBe('Take your meds');
|
||||
expect(body.body).toBe('Naltrexone time.');
|
||||
expect(body.tag).toBe('nudge-medication');
|
||||
expect((body.data as Record<string, unknown>).reminderId).toBe('reminder-uuid-1');
|
||||
expect((body.data as Record<string, unknown>).targetType).toBe('medication');
|
||||
expect((body.data as Record<string, unknown>).targetId).toBe('med-uuid-1');
|
||||
});
|
||||
|
||||
it('uses empty string for body when message is null', async () => {
|
||||
channel = new PushChannel(
|
||||
makeConfigService({
|
||||
COMPANION_API_URL: 'http://localhost:3850',
|
||||
COMPANION_PUSH_FIRE_TOKEN: 'secret-token',
|
||||
}),
|
||||
);
|
||||
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: true,
|
||||
json: jest.fn().mockResolvedValue({ sent: 1, failed: 0 }),
|
||||
});
|
||||
|
||||
const reminder = makeReminder({ message: null });
|
||||
await channel.dispatch(reminder);
|
||||
|
||||
const body = JSON.parse((mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string) as Record<string, unknown>;
|
||||
expect(body.body).toBe('');
|
||||
});
|
||||
|
||||
it('skips dispatch and logs warning when COMPANION_API_URL is missing', async () => {
|
||||
channel = new PushChannel(
|
||||
makeConfigService({ COMPANION_PUSH_FIRE_TOKEN: 'secret-token' }),
|
||||
);
|
||||
|
||||
await channel.dispatch(makeReminder());
|
||||
|
||||
expect(mockFetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('skips dispatch and logs warning when COMPANION_PUSH_FIRE_TOKEN is missing', async () => {
|
||||
channel = new PushChannel(
|
||||
makeConfigService({ COMPANION_API_URL: 'http://localhost:3850' }),
|
||||
);
|
||||
|
||||
await channel.dispatch(makeReminder());
|
||||
|
||||
expect(mockFetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not throw on non-200 response — logs warning only', async () => {
|
||||
channel = new PushChannel(
|
||||
makeConfigService({
|
||||
COMPANION_API_URL: 'http://localhost:3850',
|
||||
COMPANION_PUSH_FIRE_TOKEN: 'secret-token',
|
||||
}),
|
||||
);
|
||||
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 403,
|
||||
statusText: 'Forbidden',
|
||||
text: jest.fn().mockResolvedValue('Access denied'),
|
||||
});
|
||||
|
||||
await expect(channel.dispatch(makeReminder())).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not throw on fetch network error — logs warning only', async () => {
|
||||
channel = new PushChannel(
|
||||
makeConfigService({
|
||||
COMPANION_API_URL: 'http://localhost:3850',
|
||||
COMPANION_PUSH_FIRE_TOKEN: 'secret-token',
|
||||
}),
|
||||
);
|
||||
|
||||
mockFetch.mockRejectedValue(new Error('ECONNREFUSED'));
|
||||
|
||||
await expect(channel.dispatch(makeReminder())).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('omits targetId from data payload when targetId is null', async () => {
|
||||
channel = new PushChannel(
|
||||
makeConfigService({
|
||||
COMPANION_API_URL: 'http://localhost:3850',
|
||||
COMPANION_PUSH_FIRE_TOKEN: 'secret-token',
|
||||
}),
|
||||
);
|
||||
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: true,
|
||||
json: jest.fn().mockResolvedValue({ sent: 1, failed: 0 }),
|
||||
});
|
||||
|
||||
const reminder = makeReminder({ targetId: null });
|
||||
await channel.dispatch(reminder);
|
||||
|
||||
const body = JSON.parse((mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string) as Record<string, unknown>;
|
||||
const data = body.data as Record<string, unknown>;
|
||||
expect('targetId' in data).toBe(false);
|
||||
expect(data.reminderId).toBe('reminder-uuid-1');
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue