| import { describe, it, expect, beforeAll, afterAll } from 'vitest'; |
| import { buildApp } from '../src/app'; |
|
|
| |
|
|
| function makeJwt(app: any, payload: object) { |
| return (app as any).jwt.sign(payload); |
| } |
|
|
| |
|
|
| describe('Route Input Validation', () => { |
| let app: any; |
|
|
| beforeAll(async () => { |
| app = await buildApp(); |
| }); |
|
|
| afterAll(async () => { |
| await app.close(); |
| }); |
|
|
| |
|
|
| describe('Campaign routes β auth required', () => { |
| const routes = [ |
| { method: 'POST' as const, url: '/v1/organizations/org1/campaigns/generate' }, |
| { method: 'POST' as const, url: '/v1/organizations/org1/campaigns/send' }, |
| { method: 'POST' as const, url: '/v1/organizations/org1/campaigns/broadcast' }, |
| ]; |
|
|
| for (const { method, url } of routes) { |
| it(`${method} ${url} returns 401 with no token`, async () => { |
| const res = await app.inject({ method, url, payload: {} }); |
| expect(res.statusCode).toBe(401); |
| }); |
| } |
| }); |
|
|
| |
| |
| |
| |
| |
|
|
| describe('Campaign /send β schema validation (pure logic)', () => { |
| const { z } = require('zod'); |
|
|
| const sendSchema = z.object({ |
| listId: z.string().uuid(), |
| message: z.string().min(1), |
| sendAt: z.string().datetime({ offset: true }).optional(), |
| }); |
|
|
| it('rejects missing listId', () => { |
| const r = sendSchema.safeParse({ message: 'hello' }); |
| expect(r.success).toBe(false); |
| }); |
|
|
| it('rejects non-uuid listId', () => { |
| const r = sendSchema.safeParse({ listId: 'not-a-uuid', message: 'hello' }); |
| expect(r.success).toBe(false); |
| }); |
|
|
| it('rejects empty message', () => { |
| const r = sendSchema.safeParse({ listId: '00000000-0000-0000-0000-000000000001', message: '' }); |
| expect(r.success).toBe(false); |
| }); |
|
|
| it('rejects invalid sendAt (not ISO datetime)', () => { |
| const r = sendSchema.safeParse({ |
| listId: '00000000-0000-0000-0000-000000000001', |
| message: 'hello', |
| sendAt: 'not-a-date', |
| }); |
| expect(r.success).toBe(false); |
| }); |
|
|
| it('accepts valid payload without sendAt', () => { |
| const r = sendSchema.safeParse({ |
| listId: '00000000-0000-0000-0000-000000000001', |
| message: 'Hello students!', |
| }); |
| expect(r.success).toBe(true); |
| }); |
|
|
| it('accepts valid payload with future sendAt', () => { |
| const future = new Date(Date.now() + 60_000).toISOString(); |
| const r = sendSchema.safeParse({ |
| listId: '00000000-0000-0000-0000-000000000001', |
| message: 'Hello students!', |
| sendAt: future, |
| }); |
| expect(r.success).toBe(true); |
| }); |
| }); |
|
|
| |
|
|
| describe('Campaign /broadcast β schema validation (pure logic)', () => { |
| const { z } = require('zod'); |
|
|
| const broadcastSchema = z.object({ |
| message: z.string().optional(), |
| listId: z.string().uuid().optional(), |
| templateName: z.string().optional(), |
| templateLanguage: z.string().optional(), |
| sendAt: z.string().datetime({ offset: true }).optional(), |
| }); |
|
|
| it('accepts empty body (schema is permissive β business logic handles missing content)', () => { |
| expect(broadcastSchema.safeParse({}).success).toBe(true); |
| }); |
|
|
| it('rejects non-uuid listId', () => { |
| expect(broadcastSchema.safeParse({ listId: 'bad-uuid' }).success).toBe(false); |
| }); |
|
|
| it('rejects invalid sendAt format', () => { |
| expect(broadcastSchema.safeParse({ sendAt: '2024-13-40' }).success).toBe(false); |
| }); |
|
|
| it('accepts valid broadcast with template', () => { |
| expect(broadcastSchema.safeParse({ |
| templateName: 'welcome_message', |
| templateLanguage: 'fr', |
| listId: '00000000-0000-0000-0000-000000000002', |
| }).success).toBe(true); |
| }); |
| }); |
|
|
| |
|
|
| describe('Campaign /generate β schema validation (pure logic)', () => { |
| const { z } = require('zod'); |
|
|
| const generateSchema = z.object({ |
| prompt: z.string().min(1).max(2000), |
| listId: z.string().uuid().optional(), |
| }); |
|
|
| it('rejects empty prompt', () => { |
| expect(generateSchema.safeParse({ prompt: '' }).success).toBe(false); |
| }); |
|
|
| it('rejects prompt over 2000 chars', () => { |
| expect(generateSchema.safeParse({ prompt: 'x'.repeat(2001) }).success).toBe(false); |
| }); |
|
|
| it('accepts valid prompt', () => { |
| expect(generateSchema.safeParse({ prompt: 'Create a motivational message for day 3' }).success).toBe(true); |
| }); |
| }); |
|
|
| |
|
|
| describe('Analytics /query β SQL injection patterns', () => { |
| |
| const DANGEROUS_PATTERNS = [/\bUNION\b/, /\bINSERT\b/, /\bUPDATE\b/, /\bDELETE\b/, /\bDROP\b/, /\bEXEC\b/, /\bEXECUTE\b/, /--/, /\/\*/, /;\s*SELECT/i]; |
| const isSafe = (sql: string) => !DANGEROUS_PATTERNS.some(p => p.test(sql.toUpperCase())); |
| const isDangerous = (sql: string) => DANGEROUS_PATTERNS.some(p => p.test(sql.toUpperCase())); |
|
|
| |
| it('blocks UNION SELECT', () => expect(isDangerous('SELECT * FROM x UNION SELECT * FROM y')).toBe(true)); |
| it('blocks DROP TABLE', () => expect(isDangerous('DROP TABLE users')).toBe(true)); |
| it('blocks DELETE statement', () => expect(isDangerous('DELETE FROM users WHERE 1=1')).toBe(true)); |
| it('blocks INSERT statement', () => expect(isDangerous("INSERT INTO users VALUES ('x')")).toBe(true)); |
| it('blocks UPDATE statement', () => expect(isDangerous("UPDATE users SET x='y'")).toBe(true)); |
| it('blocks EXEC call', () => expect(isDangerous('EXEC xp_cmdshell')).toBe(true)); |
| it('blocks EXECUTE call', () => expect(isDangerous('EXECUTE sp_executesql')).toBe(true)); |
| it('blocks -- comment', () => expect(isDangerous("SELECT 1 -- bypass")).toBe(true)); |
| it('blocks /* comment */', () => expect(isDangerous('SELECT 1 /* comment */')).toBe(true)); |
| it('blocks stacked ; SELECT', () => expect(isDangerous('SELECT 1; SELECT * FROM x')).toBe(true)); |
|
|
| |
| it('allows plain SELECT', () => expect(isSafe('SELECT COUNT(*) FROM "Message" WHERE "organizationId" = \'x\'')).toBe(true)); |
| it('allows SELECT with GROUP BY', () => expect(isSafe('SELECT feature, COUNT(*) FROM "UsageEvent" GROUP BY feature ORDER BY 2 DESC LIMIT 5')).toBe(true)); |
| it('allows SELECT with JOIN', () => expect(isSafe('SELECT u.name, COUNT(m.id) FROM "User" u LEFT JOIN "Message" m ON m."userId" = u.id GROUP BY u.name')).toBe(true)); |
| }); |
|
|
| |
|
|
| describe('Auth routes β public endpoints', () => { |
| it('POST /v1/auth/login with invalid credentials returns 400, 401, or 500 (no DB in test env)', async () => { |
| const res = await app.inject({ |
| method: 'POST', |
| url: '/v1/auth/login', |
| payload: { email: 'nobody@example.com', password: 'wrongpass' }, |
| }); |
| expect([400, 401, 500]).toContain(res.statusCode); |
| }); |
|
|
| it('POST /v1/auth/login with empty body returns 400', async () => { |
| const res = await app.inject({ |
| method: 'POST', |
| url: '/v1/auth/login', |
| payload: {}, |
| }); |
| expect(res.statusCode).toBe(400); |
| }); |
| }); |
| }); |
|
|