/** * Workflows & Telegram notification parsing tests * * Tests for: * - workflows:// URL parsing and HTTP URL construction * - Telegram URL parsing with topic_id support * - Telegram link_preview_options body construction * * Run with: bun test tests/unit/workflows-notifications.test.ts */ import { describe, test, expect } from 'bun:test'; import { parseWorkflowsUrl, buildWorkflowsHttpUrl, resolveWorkflowsHttpUrl, parseTelegramUrl } from '../src/lib/utils/notification-parsers'; // Telegram request body construction (mirrors notifications.ts) function buildTelegramBody(chatId: string, text: string, topicId?: number) { return { chat_id: chatId, text, ...(topicId ? { message_thread_id: topicId } : {}), parse_mode: 'Markdown', link_preview_options: { is_disabled: true } }; } describe('Workflows URL Parsing', () => { test('parses valid workflows:// URL', () => { const result = parseWorkflowsUrl('workflows://prod.logic.azure.com/abc123/sig456'); expect(result).not.toBeNull(); expect(result!.hostname).toBe('prod.logic.azure.com'); expect(result!.workflow).toBe('abc123'); expect(result!.signature).toBe('sig456'); }); test('parses URL with complex workflow path', () => { const result = parseWorkflowsUrl('workflows://myhost.com/long-workflow-id-here/signature-value'); expect(result).not.toBeNull(); expect(result!.hostname).toBe('myhost.com'); expect(result!.workflow).toBe('long-workflow-id-here'); expect(result!.signature).toBe('signature-value'); }); test('parses workflow:// (singular) variant', () => { const result = parseWorkflowsUrl('workflow://host.com/wf/sig'); expect(result).not.toBeNull(); expect(result!.hostname).toBe('host.com'); }); test('returns null for missing signature', () => { expect(parseWorkflowsUrl('workflows://host.com/workflow-only')).toBeNull(); }); test('returns null for missing path', () => { expect(parseWorkflowsUrl('workflows://host.com')).toBeNull(); }); test('returns null for invalid protocol', () => { expect(parseWorkflowsUrl('http://host.com/wf/sig')).toBeNull(); }); test('returns null for empty string', () => { expect(parseWorkflowsUrl('')).toBeNull(); }); }); describe('Workflows HTTP URL Construction', () => { test('builds correct Power Automate URL', () => { const url = buildWorkflowsHttpUrl('prod.logic.azure.com', 'abc123', 'sig456'); expect(url).toBe( 'https://prod.logic.azure.com/powerautomate/automations/direct/workflows/abc123/triggers/manual/paths/invoke?api-version=1&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=sig456' ); }); test('URL starts with https', () => { const url = buildWorkflowsHttpUrl('any.host', 'wf', 'sig'); expect(url.startsWith('https://')).toBe(true); }); test('signature is placed in sig query param', () => { const url = buildWorkflowsHttpUrl('host', 'wf', 'my-signature'); expect(url).toContain('sig=my-signature'); }); }); describe('resolveWorkflowsHttpUrl (full-URL passthrough, #1512)', () => { test('new Power Automate URL with /cu// passes through verbatim (only scheme swapped)', () => { // The exact shape from #1512: powerplatform.com host, a /cu/11/ segment, // api-version=1. Rebuilding from parts drops those -> 401. Passthrough keeps them. const full = 'workflows://myenv.11.environment.api.powerplatform.com:443/powerautomate/automations/direct/cu/11/workflows/abc123/triggers/manual/paths/invoke?api-version=1&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=THE_SIGNATURE'; const url = resolveWorkflowsHttpUrl(full); expect(url).toBe( 'https://myenv.11.environment.api.powerplatform.com:443/powerautomate/automations/direct/cu/11/workflows/abc123/triggers/manual/paths/invoke?api-version=1&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=THE_SIGNATURE' ); }); test('legacy logic.azure.com full URL passes through verbatim', () => { const full = 'workflows://prod-161.westeurope.logic.azure.com:443/workflows/643e/triggers/manual/paths/invoke?api-version=2016-06-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=KODu'; const url = resolveWorkflowsHttpUrl(full)!; expect(url).toBe('https://' + full.slice('workflows://'.length)); expect(url).toContain('api-version=2016-06-01'); expect(url).toContain('sig=KODu'); }); test('short legacy form host/workflow/sig is still reconstructed (back-compat)', () => { const url = resolveWorkflowsHttpUrl('workflows://prod.logic.azure.com/abc123/sig456'); expect(url).toBe( 'https://prod.logic.azure.com/powerautomate/automations/direct/workflows/abc123/triggers/manual/paths/invoke?api-version=1&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=sig456' ); }); test('a full invoke path WITHOUT a sig is not treated as passthrough (falls back / rejects)', () => { // No sig= -> not a signed webhook; short-form parse also fails -> null. expect(resolveWorkflowsHttpUrl('workflows://host/workflows/x/triggers/manual/paths/invoke')).toBeNull(); }); test('non-workflows scheme returns null', () => { expect(resolveWorkflowsHttpUrl('https://host/x/y')).toBeNull(); expect(resolveWorkflowsHttpUrl('')).toBeNull(); }); test('workflow:// (singular) full URL also passes through', () => { const url = resolveWorkflowsHttpUrl('workflow://host:443/workflows/w/triggers/manual/paths/invoke?sig=s')!; expect(url).toBe('https://host:443/workflows/w/triggers/manual/paths/invoke?sig=s'); }); }); describe('Workflows Adaptive Card Payload', () => { test('constructs valid Adaptive Card body', () => { const title = 'Container Started'; const message = 'nginx started on production'; const environmentName = 'prod-cluster'; const titleWithEnv = environmentName ? `${title} [${environmentName}]` : title; const body = { type: 'message', attachments: [ { contentType: 'application/vnd.microsoft.card.adaptive', content: { $schema: 'https://adaptivecards.io/schemas/adaptive-card.json', type: 'AdaptiveCard', version: '1.2', body: [ { type: 'TextBlock', style: 'heading', wrap: true, text: titleWithEnv }, { type: 'TextBlock', style: 'default', wrap: true, text: message } ] } } ] }; expect(body.attachments[0].contentType).toBe('application/vnd.microsoft.card.adaptive'); expect(body.attachments[0].content.body[0].text).toBe('Container Started [prod-cluster]'); expect(body.attachments[0].content.body[1].text).toBe('nginx started on production'); }); test('title without environment has no brackets', () => { const title = 'License Expiring'; const environmentName = undefined; const titleWithEnv = environmentName ? `${title} [${environmentName}]` : title; expect(titleWithEnv).toBe('License Expiring'); }); }); describe('Telegram URL Parsing with Topic ID', () => { test('parses URL without topic ID', () => { const result = parseTelegramUrl('tgram://123456:ABC-token/-100999'); expect(result).not.toBeNull(); expect(result!.botToken).toBe('123456:ABC-token'); expect(result!.chatId).toBe('-100999'); expect(result!.topicId).toBeUndefined(); }); test('parses URL with topic ID', () => { const result = parseTelegramUrl('tgram://123456:ABC-token/-100999:42'); expect(result).not.toBeNull(); expect(result!.botToken).toBe('123456:ABC-token'); expect(result!.chatId).toBe('-100999'); expect(result!.topicId).toBe(42); }); test('topic ID is parsed as integer', () => { const result = parseTelegramUrl('tgram://bot/chat:999'); expect(result).not.toBeNull(); expect(typeof result!.topicId).toBe('number'); expect(result!.topicId).toBe(999); }); test('returns null for URL with no chat ID', () => { expect(parseTelegramUrl('tgram://bot-token-only')).toBeNull(); }); test('returns null for non-tgram protocol', () => { expect(parseTelegramUrl('discord://id/token')).toBeNull(); }); }); describe('Telegram Request Body', () => { test('includes link_preview_options with is_disabled: true', () => { const body = buildTelegramBody('-100123', 'test message'); expect(body.link_preview_options).toEqual({ is_disabled: true }); }); test('includes message_thread_id when topic ID provided', () => { const body = buildTelegramBody('-100123', 'test', 42); expect(body.message_thread_id).toBe(42); }); test('omits message_thread_id when no topic ID', () => { const body = buildTelegramBody('-100123', 'test'); expect(body).not.toHaveProperty('message_thread_id'); }); test('uses Markdown parse mode', () => { const body = buildTelegramBody('-100123', 'test'); expect(body.parse_mode).toBe('Markdown'); }); });