mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-06 00:47:52 +00:00
7d4e61625f
* fix(notifications): harden alert dispatch crash-safety and redact webhook secrets in logs Make NotificationService.dispatchAlert never reject so the many fire-and-forget callers (monitors, event streams, policy and image-update paths) cannot trigger an unhandledRejection on an unhealthy database. The whole dispatch body now sits inside a guard covering node resolution, the history insert, channel-table reads, and the WebSocket broadcast; a failure logs and drops the notification instead of crashing the process. An inner guard still splits the write-success and write-failure metrics. Also: - Redact webhook URLs in diagnostic logs via a new maskWebhookUrl helper; Discord/Slack/custom webhook URLs embed their token in the path, so only the origin is safe to emit. - Add error logging to four notification-history route handlers that previously swallowed database errors silently before returning 500. - Snapshot the subscriber set before broadcasting so a close/error handler firing mid-send cannot mutate the set during iteration. - Sanitize the admin-supplied route name in dispatch log lines. Adds tests for dispatch crash-safety (write failure, post-write routing failure, broadcast send failure), the success-path write metric, webhook URL masking, and history-route error logging. * fix(notifications): sanitize route name and patterns in create logs Apply sanitizeForLog to the admin-supplied route name and stack patterns in the route-creation log lines, matching the dispatch-site sanitization and closing the remaining log-injection path on this feature. Also extend tests: userinfo-stripping in maskWebhookUrl, subscriber-set snapshot behavior under an unsubscribe-during-send, and error logging on the mark-read, delete-one, and clear-all notification-history handlers.
50 lines
2.0 KiB
TypeScript
50 lines
2.0 KiB
TypeScript
/**
|
|
* Unit tests for notification channel helpers. Focused on maskWebhookUrl,
|
|
* which must never let a channel's embedded auth token reach a log line.
|
|
*/
|
|
import { describe, it, expect } from 'vitest';
|
|
import { maskWebhookUrl } from '../helpers/notificationChannels';
|
|
|
|
describe('maskWebhookUrl', () => {
|
|
it('redacts the token-bearing path of a Discord webhook URL', () => {
|
|
const masked = maskWebhookUrl('https://discord.com/api/webhooks/123456789/SuP3rS3cr3tT0k3n');
|
|
expect(masked).toBe('https://discord.com/<redacted>');
|
|
expect(masked).not.toContain('SuP3rS3cr3tT0k3n');
|
|
});
|
|
|
|
it('redacts the path of a Slack webhook URL', () => {
|
|
const masked = maskWebhookUrl('https://hooks.slack.com/services/T000/B000/XXXXSECRET');
|
|
expect(masked).toBe('https://hooks.slack.com/<redacted>');
|
|
expect(masked).not.toContain('XXXXSECRET');
|
|
});
|
|
|
|
it('redacts a secret carried in the query string', () => {
|
|
const masked = maskWebhookUrl('https://example.com/?token=abc123secret');
|
|
expect(masked).toBe('https://example.com/<redacted>');
|
|
expect(masked).not.toContain('abc123secret');
|
|
});
|
|
|
|
it('returns the bare origin when there is no path or query to hide', () => {
|
|
expect(maskWebhookUrl('https://example.com')).toBe('https://example.com');
|
|
expect(maskWebhookUrl('https://example.com/')).toBe('https://example.com');
|
|
});
|
|
|
|
it('strips embedded userinfo credentials (origin omits user:pass@)', () => {
|
|
const masked = maskWebhookUrl('https://user:s3cr3t@example.com/');
|
|
expect(masked).toBe('https://example.com');
|
|
expect(masked).not.toContain('s3cr3t');
|
|
expect(masked).not.toContain('user');
|
|
});
|
|
|
|
it('returns a placeholder for empty or non-string input', () => {
|
|
expect(maskWebhookUrl('')).toBe('<no url>');
|
|
expect(maskWebhookUrl(undefined)).toBe('<no url>');
|
|
expect(maskWebhookUrl(null)).toBe('<no url>');
|
|
expect(maskWebhookUrl(42)).toBe('<no url>');
|
|
});
|
|
|
|
it('returns a placeholder for an unparseable URL', () => {
|
|
expect(maskWebhookUrl('not a url')).toBe('<invalid url>');
|
|
});
|
|
});
|