This repository has no description
Something went wrong. Try again.
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211// SPDX-License-Identifier: AGPL-3.0-or-later
import type {WebhookTokenResponse} from '@fluxer/schema/src/domains/webhook/WebhookSchemas';import {afterEach, beforeEach, describe, expect, it} from 'vitest';import {createTestAccount} from '../../auth/tests/AuthTestUtils';import {createGuild} from '../../guild/tests/GuildTestUtils';import {type ApiTestHarness, createApiTestHarness} from '../../test/ApiTestHarness';import {HTTP_STATUS, TEST_IDS} from '../../test/TestConstants';import {createBuilderWithoutAuth} from '../../test/TestRequestBuilder';import {createWebhook, deleteWebhook} from './WebhookTestUtils';
describe('Webhook token authentication', () => { let harness: ApiTestHarness; beforeEach(async () => { harness = await createApiTestHarness(); }); afterEach(async () => { await harness?.shutdown(); }); describe('GET /webhooks/:webhook_id/:token', () => { it('returns webhook with valid token', async () => { const owner = await createTestAccount(harness); const guild = await createGuild(harness, owner.token, 'Token Auth Guild'); const channelId = guild.system_channel_id!; const webhook = await createWebhook(harness, channelId, owner.token, 'Token Auth Webhook'); const response = await createBuilderWithoutAuth<WebhookTokenResponse>(harness) .get(`/webhooks/${webhook.id}/${webhook.token}`) .expect(HTTP_STATUS.OK) .execute(); expect(response).not.toHaveProperty('user'); await deleteWebhook(harness, webhook.id, owner.token); }); it('rejects invalid token', async () => { const owner = await createTestAccount(harness); const guild = await createGuild(harness, owner.token, 'Token Auth Guild'); const channelId = guild.system_channel_id!; const webhook = await createWebhook(harness, channelId, owner.token, 'Token Auth Webhook'); await createBuilderWithoutAuth(harness) .get(`/webhooks/${webhook.id}/invalid_token_12345`) .expect(HTTP_STATUS.NOT_FOUND) .execute(); await deleteWebhook(harness, webhook.id, owner.token); }); it('rejects nonexistent webhook id', async () => { await createBuilderWithoutAuth(harness) .get(`/webhooks/${TEST_IDS.NONEXISTENT_WEBHOOK}/any_token`) .expect(HTTP_STATUS.NOT_FOUND) .execute(); }); it('rejects valid id with wrong token', async () => { const owner = await createTestAccount(harness); const guild = await createGuild(harness, owner.token, 'Token Auth Guild'); const channelId = guild.system_channel_id!; const webhook1 = await createWebhook(harness, channelId, owner.token, 'First Webhook'); const webhook2 = await createWebhook(harness, channelId, owner.token, 'Second Webhook'); await createBuilderWithoutAuth(harness) .get(`/webhooks/${webhook1.id}/${webhook2.token}`) .expect(HTTP_STATUS.NOT_FOUND) .execute(); await deleteWebhook(harness, webhook1.id, owner.token); await deleteWebhook(harness, webhook2.id, owner.token); }); }); describe('PATCH /webhooks/:webhook_id/:token', () => { it('updates webhook with valid token', async () => { const owner = await createTestAccount(harness); const guild = await createGuild(harness, owner.token, 'Token Auth Guild'); const channelId = guild.system_channel_id!; const webhook = await createWebhook(harness, channelId, owner.token, 'Original Name'); const response = await createBuilderWithoutAuth<WebhookTokenResponse>(harness) .patch(`/webhooks/${webhook.id}/${webhook.token}`) .body({name: 'Updated Name'}) .expect(HTTP_STATUS.OK) .execute(); expect(response).not.toHaveProperty('user'); await deleteWebhook(harness, webhook.id, owner.token); }); it('rejects channel_id updates with token', async () => { const owner = await createTestAccount(harness); const guild = await createGuild(harness, owner.token, 'Token Auth Guild'); const channelId = guild.system_channel_id!; const webhook = await createWebhook(harness, channelId, owner.token, 'Token Patch Webhook'); await createBuilderWithoutAuth(harness) .patch(`/webhooks/${webhook.id}/${webhook.token}`) .body({channel_id: channelId}) .expect(HTTP_STATUS.BAD_REQUEST, 'INVALID_FORM_BODY') .execute(); await deleteWebhook(harness, webhook.id, owner.token); }); it('rejects update with invalid token', async () => { const owner = await createTestAccount(harness); const guild = await createGuild(harness, owner.token, 'Token Auth Guild'); const channelId = guild.system_channel_id!; const webhook = await createWebhook(harness, channelId, owner.token, 'Token Patch Webhook'); await createBuilderWithoutAuth(harness) .patch(`/webhooks/${webhook.id}/wrong_token`) .body({name: 'Hacked Name'}) .expect(HTTP_STATUS.NOT_FOUND) .execute(); await deleteWebhook(harness, webhook.id, owner.token); }); }); describe('DELETE /webhooks/:webhook_id/:token', () => { it('deletes webhook with valid token', async () => { const owner = await createTestAccount(harness); const guild = await createGuild(harness, owner.token, 'Token Auth Guild'); const channelId = guild.system_channel_id!; const webhook = await createWebhook(harness, channelId, owner.token, 'Delete With Token'); await createBuilderWithoutAuth(harness) .delete(`/webhooks/${webhook.id}/${webhook.token}`) .expect(HTTP_STATUS.NO_CONTENT) .execute(); await createBuilderWithoutAuth(harness) .get(`/webhooks/${webhook.id}/${webhook.token}`) .expect(HTTP_STATUS.NOT_FOUND) .execute(); }); it('rejects delete with invalid token', async () => { const owner = await createTestAccount(harness); const guild = await createGuild(harness, owner.token, 'Token Auth Guild'); const channelId = guild.system_channel_id!; const webhook = await createWebhook(harness, channelId, owner.token, 'Protected Webhook'); await createBuilderWithoutAuth(harness) .delete(`/webhooks/${webhook.id}/bad_token`) .expect(HTTP_STATUS.NOT_FOUND) .execute(); await createBuilderWithoutAuth(harness) .get(`/webhooks/${webhook.id}/${webhook.token}`) .expect(HTTP_STATUS.OK) .execute(); await deleteWebhook(harness, webhook.id, owner.token); }); }); describe('POST /webhooks/:webhook_id/:token (execute)', () => { it('executes webhook with valid token', async () => { const owner = await createTestAccount(harness); const guild = await createGuild(harness, owner.token, 'Token Auth Guild'); const channelId = guild.system_channel_id!; const webhook = await createWebhook(harness, channelId, owner.token, 'Execute Webhook'); await createBuilderWithoutAuth(harness) .post(`/webhooks/${webhook.id}/${webhook.token}`) .body({content: 'Hello from webhook'}) .expect(HTTP_STATUS.NO_CONTENT) .execute(); await deleteWebhook(harness, webhook.id, owner.token); }); it('rejects execution with invalid token', async () => { const owner = await createTestAccount(harness); const guild = await createGuild(harness, owner.token, 'Token Auth Guild'); const channelId = guild.system_channel_id!; const webhook = await createWebhook(harness, channelId, owner.token, 'Execute Webhook'); await createBuilderWithoutAuth(harness) .post(`/webhooks/${webhook.id}/invalid_token`) .body({content: 'Should fail'}) .expect(HTTP_STATUS.NOT_FOUND) .execute(); await deleteWebhook(harness, webhook.id, owner.token); }); }); describe('CORS', () => { it('allows browser requests from arbitrary origins on token-authenticated webhook routes', async () => { const owner = await createTestAccount(harness); const guild = await createGuild(harness, owner.token, 'Token Auth Guild'); const channelId = guild.system_channel_id!; const webhook = await createWebhook(harness, channelId, owner.token, 'CORS Webhook'); const response = await harness.app.request(`/webhooks/${webhook.id}/${webhook.token}`, { headers: { origin: 'https://discohook.org', 'x-forwarded-for': '127.0.0.1', }, }); expect(response.status).toBe(HTTP_STATUS.OK); expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*'); await deleteWebhook(harness, webhook.id, owner.token); }); it('allows CORS preflight for webhook execution', async () => { const owner = await createTestAccount(harness); const guild = await createGuild(harness, owner.token, 'Token Auth Guild'); const channelId = guild.system_channel_id!; const webhook = await createWebhook(harness, channelId, owner.token, 'CORS Execute Webhook'); const response = await harness.app.request(`/webhooks/${webhook.id}/${webhook.token}`, { method: 'OPTIONS', headers: { origin: 'https://discohook.org', 'access-control-request-method': 'POST', 'access-control-request-headers': 'content-type', }, }); expect(response.status).toBe(HTTP_STATUS.NO_CONTENT); expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*'); expect(response.headers.get('Access-Control-Allow-Methods')).toContain('POST'); expect(response.headers.get('Access-Control-Allow-Headers')).toContain('Content-Type'); await deleteWebhook(harness, webhook.id, owner.token); }); it('keeps authenticated management routes on the standard API CORS allowlist', async () => { const owner = await createTestAccount(harness); const guild = await createGuild(harness, owner.token, 'Token Auth Guild'); const channelId = guild.system_channel_id!; const response = await harness.app.request(`/channels/${channelId}/webhooks`, { method: 'OPTIONS', headers: { origin: 'https://discohook.org', 'access-control-request-method': 'GET', }, }); expect(response.status).toBe(HTTP_STATUS.NO_CONTENT); expect(response.headers.get('Access-Control-Allow-Origin')).toBeNull(); }); });});