import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import request from 'supertest'; import type { FastifyInstance } from 'fastify'; import { buildApp } from '../src/app.js'; import { loadConfig } from '../src/config.js'; import { pool, closePool } from '../src/db/pool.js'; import { drizzle } from 'drizzle-orm/node-postgres'; import { agents, apiTokens, auditEvents, socialChannels, socialPosts, messages, roomMembers, rooms, } from '../src/db/schema.js'; describe('Social API', () => { let app: FastifyInstance; let adminId: string; let agentId: string; beforeAll(async () => { const config = loadConfig({ ...process.env, NODE_ENV: 'test', JWT_SECRET: 'test-secret-with-at-least-32-chars-for-jwt-security', }); app = await buildApp({ config }); await app.ready(); const db = drizzle(pool); await db.delete(socialPosts); await db.delete(socialChannels); await db.delete(auditEvents); await db.delete(messages); await db.delete(roomMembers); await db.delete(rooms); await db.delete(apiTokens); await db.delete(agents); // Create admin agent const adminRes = await request(app.server) .post('/api/v1/agents') .send({ name: 'social-admin', displayName: 'Social Admin', role: 'admin' }); adminId = adminRes.body.id; // Create regular agent const agentRes = await request(app.server) .post('/api/v1/agents') .send({ name: 'social-agent', displayName: 'Social Agent', role: 'agent' }); agentId = agentRes.body.id; }); afterAll(async () => { await app.close(); await closePool(); }); describe('Channels', () => { let channelId: string; it('should create a channel (admin)', async () => { const res = await request(app.server) .post('/api/v1/social/channels') .set('x-agent-id', adminId) .send({ slug: 'general', name: 'General', description: 'Main channel' }) .expect(201); expect(res.body).toHaveProperty('id'); expect(res.body.slug).toBe('general'); expect(res.body.name).toBe('General'); expect(res.body.description).toBe('Main channel'); channelId = res.body.id; }); it('should reject channel creation by non-admin', async () => { await request(app.server) .post('/api/v1/social/channels') .set('x-agent-id', agentId) .send({ slug: 'hacker', name: 'Hacker' }) .expect(403); }); it('should reject duplicate slug', async () => { await request(app.server) .post('/api/v1/social/channels') .set('x-agent-id', adminId) .send({ slug: 'general', name: 'General 2' }) .expect(409); }); it('should list channels', async () => { const res = await request(app.server) .get('/api/v1/social/channels') .set('x-agent-id', adminId) .expect(200); expect(res.body.channels).toHaveLength(1); expect(res.body.channels[0].slug).toBe('general'); }); it('should get channel by id with post count', async () => { const res = await request(app.server) .get(`/api/v1/social/channels/${channelId}`) .set('x-agent-id', adminId) .expect(200); expect(res.body.slug).toBe('general'); expect(res.body.postCount).toBe(0); }); it('should 404 for unknown channel', async () => { await request(app.server) .get('/api/v1/social/channels/00000000-0000-0000-0000-000000000000') .set('x-agent-id', adminId) .expect(404); }); }); describe('Posts', () => { let channelId: string; let postId: string; beforeAll(async () => { // Create a channel for post tests const res = await request(app.server) .post('/api/v1/social/channels') .set('x-agent-id', adminId) .send({ slug: 'philosophy', name: 'Philosophy' }); channelId = res.body.id; }); it('should create a post (any agent)', async () => { const res = await request(app.server) .post(`/api/v1/social/channels/${channelId}/posts`) .set('x-agent-id', agentId) .send({ body: 'I think, therefore I am.' }) .expect(201); expect(res.body).toHaveProperty('id'); expect(res.body.body).toBe('I think, therefore I am.'); expect(res.body.authorAgentId).toBe(agentId); expect(res.body.authorName).toBe('Social Agent'); expect(res.body.channelSlug).toBe('philosophy'); postId = res.body.id; }); it('should 404 when posting to unknown channel', async () => { await request(app.server) .post('/api/v1/social/channels/00000000-0000-0000-0000-000000000000/posts') .set('x-agent-id', agentId) .send({ body: 'Hello void' }) .expect(404); }); it('should reject empty body', async () => { await request(app.server) .post(`/api/v1/social/channels/${channelId}/posts`) .set('x-agent-id', agentId) .send({ body: '' }) .expect(400); }); it('should get a post by id', async () => { const res = await request(app.server) .get(`/api/v1/social/posts/${postId}`) .set('x-agent-id', agentId) .expect(200); expect(res.body.id).toBe(postId); expect(res.body.body).toBe('I think, therefore I am.'); expect(res.body.channelSlug).toBe('philosophy'); }); it('should list posts in channel', async () => { // Add another post await request(app.server) .post(`/api/v1/social/channels/${channelId}/posts`) .set('x-agent-id', adminId) .send({ body: 'To be or not to be.' }); const res = await request(app.server) .get(`/api/v1/social/channels/${channelId}/posts`) .set('x-agent-id', agentId) .expect(200); expect(res.body.posts.length).toBe(2); expect(res.body.channel.slug).toBe('philosophy'); // Newest first expect(res.body.posts[0].body).toBe('To be or not to be.'); }); it('should show post count on channel', async () => { const res = await request(app.server) .get(`/api/v1/social/channels/${channelId}`) .set('x-agent-id', agentId) .expect(200); expect(res.body.postCount).toBe(2); }); it('should delete own post', async () => { await request(app.server) .delete(`/api/v1/social/posts/${postId}`) .set('x-agent-id', agentId) .expect(204); await request(app.server) .get(`/api/v1/social/posts/${postId}`) .set('x-agent-id', agentId) .expect(404); }); it('should not delete another agents post (non-admin)', async () => { // Create post as admin const res = await request(app.server) .post(`/api/v1/social/channels/${channelId}/posts`) .set('x-agent-id', adminId) .send({ body: 'Admin thought.' }); await request(app.server) .delete(`/api/v1/social/posts/${res.body.id}`) .set('x-agent-id', agentId) .expect(403); }); }); describe('Feed', () => { it('should return global feed across channels', async () => { const res = await request(app.server) .get('/api/v1/social/feed') .set('x-agent-id', agentId) .expect(200); expect(res.body.posts.length).toBeGreaterThan(0); expect(res.body.posts[0]).toHaveProperty('channelSlug'); expect(res.body.posts[0]).toHaveProperty('authorName'); }); it('should paginate feed with cursor', async () => { const res = await request(app.server) .get('/api/v1/social/feed?limit=1') .set('x-agent-id', agentId) .expect(200); expect(res.body.posts).toHaveLength(1); expect(res.body.hasMore).toBe(true); expect(res.body.cursor).toBeTruthy(); // Fetch next page const res2 = await request(app.server) .get(`/api/v1/social/feed?limit=1&before=${res.body.cursor}`) .set('x-agent-id', agentId) .expect(200); expect(res2.body.posts).toHaveLength(1); expect(res2.body.posts[0].id).not.toBe(res.body.posts[0].id); }); }); });