import { INestApplication, ValidationPipe } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import * as request from 'supertest'; import { AppModule } from '../src/app.module'; Object.assign(process.env, { NODE_ENV: 'test', EMAIL_ENABLED: 'false', REDIS_ENABLED: 'false', REDIS_SOCKET_ADAPTER_ENABLED: 'false', QUEUE_ENABLED: 'false', REQUEST_LOGGING_ENABLED: 'false', FEED_CACHE_ENABLED: 'false', BCRYPT_SALT_ROUNDS: '8', PUBLIC_BASE_URL: process.env.PUBLIC_BASE_URL ?? 'http://127.0.0.1:4000', STORAGE_PUBLIC_BASE_URL: process.env.STORAGE_PUBLIC_BASE_URL ?? process.env.PUBLIC_BASE_URL ?? 'http://127.0.0.1:4000', MONGODB_URI: process.env.MONGODB_URI ?? 'mongodb://127.0.0.1:27017/oudelaa-e2e', JWT_ACCESS_SECRET: process.env.JWT_ACCESS_SECRET ?? 'test-access-secret-123456', JWT_REFRESH_SECRET: process.env.JWT_REFRESH_SECRET ?? 'test-refresh-secret-123456', SUPERADMIN_EMAIL: process.env.SUPERADMIN_EMAIL ?? 'superadmin@example.com', SUPERADMIN_PASSWORD: process.env.SUPERADMIN_PASSWORD ?? 'StrongPass123!', SUPERADMIN_ACCESS_SECRET: process.env.SUPERADMIN_ACCESS_SECRET ?? 'test-superadmin-access-123456', SUPERADMIN_REFRESH_SECRET: process.env.SUPERADMIN_REFRESH_SECRET ?? 'test-superadmin-refresh-123456', }); jest.setTimeout(120000); const tinyPngBuffer = Buffer.from( 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9WlH0i8AAAAASUVORK5CYII=', 'base64', ); describe('Oudelaa smoke (e2e)', () => { let app: INestApplication; const ts = Date.now(); const primary = { email: `smoke_primary_${ts}@example.com`, password: 'StrongPass123!', accessToken: '', userId: '', username: '', }; const secondary = { email: `smoke_secondary_${ts}@example.com`, password: 'StrongPass123!', accessToken: '', userId: '', username: '', }; const tertiary = { email: `smoke_tertiary_${ts}@example.com`, password: 'StrongPass123!', accessToken: '', userId: '', username: '', }; const superAdmin = { accessToken: '', refreshToken: '', secondAccessToken: '', secondRefreshToken: '', }; let postId = ''; let editableCommentId = ''; beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); app = moduleFixture.createNestApplication(); const configService = app.get(ConfigService); app.setGlobalPrefix(configService.get('globalPrefix', 'api/v1')); app.useGlobalPipes( new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true, transformOptions: { enableImplicitConversion: true }, }), ); await app.init(); }); afterAll(async () => { if (app) { await app.close(); } }); const registerAndVerify = async (user: { email: string; password: string; accessToken: string; userId: string; username: string; }) => { const registerResponse = await request(app.getHttpServer()) .post('/api/v1/auth/register-basic') .send({ email: user.email, password: user.password, confirmPassword: user.password, }) .expect(201); const verifyResponse = await request(app.getHttpServer()) .post('/api/v1/auth/verify-email') .send({ email: user.email, code: registerResponse.body.debugCode, }) .expect(200); user.accessToken = verifyResponse.body.accessToken; user.userId = verifyResponse.body.user._id || verifyResponse.body.user.id; user.username = verifyResponse.body.user.username; }; it('/api/v1/health (GET)', () => { return request(app.getHttpServer()).get('/api/v1/health').expect(200); }); it('registers and verifies smoke users', async () => { await registerAndVerify(primary); await registerAndVerify(secondary); await registerAndVerify(tertiary); expect(primary.accessToken).toBeTruthy(); expect(secondary.username).toBeTruthy(); expect(tertiary.userId).toBeTruthy(); }); it('updates profile setup with avatar upload', async () => { const response = await request(app.getHttpServer()) .patch('/api/v1/users/me/profile-setup') .set('Authorization', `Bearer ${primary.accessToken}`) .field('stageName', 'Smoke Artist') .field('bio', 'Smoke profile bio') .field('location', 'Riyadh, Saudi Arabia') .field('latitude', '24.7136') .field('longitude', '46.6753') .attach('avatarFile', tinyPngBuffer, { filename: 'avatar.png', contentType: 'image/png' }) .expect(200); expect(response.body.stageName).toBe('Smoke Artist'); expect(response.body.avatar).toMatch(/^https?:\/\//); }); it('updates music setup and discovers talents', async () => { await request(app.getHttpServer()) .patch('/api/v1/users/me/music-setup') .set('Authorization', `Bearer ${primary.accessToken}`) .send({ musicRoles: ['instrumentalist', 'composer'], musicGenres: ['Tarab'], experienceLevel: 'intermediate', favoriteInstruments: ['Oud'], favoriteMaqamat: ['Rast'], }) .expect(200); const discoverResponse = await request(app.getHttpServer()) .get('/api/v1/users/discover?musicRole=instrumentalist&hasAvatarOnly=true&limit=8') .set('Authorization', `Bearer ${secondary.accessToken}`) .expect(200); expect(discoverResponse.body.items).toBeInstanceOf(Array); expect(discoverResponse.body.roleBuckets).toBeInstanceOf(Array); expect(discoverResponse.body.pagination).toBeTruthy(); const overviewResponse = await request(app.getHttpServer()) .get(`/api/v1/users/${primary.userId}/profile-overview`) .set('Authorization', `Bearer ${secondary.accessToken}`) .expect(200); expect(overviewResponse.body.stats).toBeTruthy(); expect(overviewResponse.body.contentCounts).toBeTruthy(); expect(overviewResponse.body.viewerState).toBeTruthy(); }); it('creates an image post with tag and mention', async () => { const response = await request(app.getHttpServer()) .post('/api/v1/posts') .set('Authorization', `Bearer ${primary.accessToken}`) .field('content', `Smoke image post with @${secondary.username} #smoke`) .field('visibility', 'public') .field('taggedUserIds', secondary.userId) .field('mentionUsernames', secondary.username) .field('location', 'Riyadh, Saudi Arabia') .field('latitude', '24.7136') .field('longitude', '46.6753') .attach('imageFiles', tinyPngBuffer, { filename: 'post.png', contentType: 'image/png' }) .expect(201); postId = response.body._id || response.body.id; expect(postId).toBeTruthy(); expect(response.body.postType).toBe('image'); expect(response.body.imageUrls).toBeInstanceOf(Array); expect(response.body.imageUrls[0]).toMatch(/^https?:\/\//); expect(response.body.mentionUsernames).toContain(secondary.username.toLowerCase()); }); it('returns unified pagination in feed', async () => { const response = await request(app.getHttpServer()) .get('/api/v1/feed/me?includeSuggestions=false&limit=10') .set('Authorization', `Bearer ${primary.accessToken}`) .expect(200); expect(response.body.items).toBeInstanceOf(Array); expect(response.body.pagination).toEqual( expect.objectContaining({ mode: 'cursor', limit: 10, }), ); }); it('creates mention notification for mentioned post user', async () => { const response = await request(app.getHttpServer()) .get('/api/v1/notifications') .set('Authorization', `Bearer ${secondary.accessToken}`) .expect(200); const mention = (response.body.items ?? []).find( (item: any) => item.type === 'mention' && (item.referenceId === postId || item.deepLink === `/posts/${postId}`), ); expect(response.body.pagination).toBeTruthy(); expect(mention).toBeTruthy(); }); it('creates comment mention notification for a third user', async () => { await request(app.getHttpServer()) .post('/api/v1/comments') .set('Authorization', `Bearer ${secondary.accessToken}`) .send({ postId, content: `Great take @${tertiary.username}`, mentionUsernames: [tertiary.username], }) .expect(201); const commentsResponse = await request(app.getHttpServer()) .get(`/api/v1/comments/post/${postId}?page=1&limit=10`) .set('Authorization', `Bearer ${primary.accessToken}`) .expect(200); expect(commentsResponse.body.pagination).toEqual( expect.objectContaining({ mode: 'offset', limit: 10, }), ); const notificationsResponse = await request(app.getHttpServer()) .get('/api/v1/notifications') .set('Authorization', `Bearer ${tertiary.accessToken}`) .expect(200); const mention = (notificationsResponse.body.items ?? []).find( (item: any) => item.type === 'mention' && item.resourceType === 'comment', ); expect(mention).toBeTruthy(); }); it('updates own comment content and mention usernames', async () => { const createResponse = await request(app.getHttpServer()) .post('/api/v1/comments') .set('Authorization', `Bearer ${secondary.accessToken}`) .send({ postId, content: 'Needs edit', }) .expect(201); editableCommentId = createResponse.body._id || createResponse.body.id; const updatedContent = `Edited with @${primary.username}`; const updateResponse = await request(app.getHttpServer()) .patch(`/api/v1/comments/${editableCommentId}`) .set('Authorization', `Bearer ${secondary.accessToken}`) .send({ content: updatedContent, mentionUsernames: [primary.username], }) .expect(200); expect(updateResponse.body.content).toBe(updatedContent); expect(updateResponse.body.mentionUsernames).toContain(primary.username.toLowerCase()); const commentsResponse = await request(app.getHttpServer()) .get(`/api/v1/comments/post/${postId}?page=1&limit=20`) .set('Authorization', `Bearer ${primary.accessToken}`) .expect(200); const updatedComment = (commentsResponse.body.items ?? []).find( (item: any) => (item._id || item.id) === editableCommentId, ); expect(updatedComment?.content).toBe(updatedContent); const notificationsResponse = await request(app.getHttpServer()) .get('/api/v1/notifications?resourceType=comment') .set('Authorization', `Bearer ${primary.accessToken}`) .expect(200); const mention = (notificationsResponse.body.items ?? []).find( (item: any) => item.type === 'mention' && item.resourceType === 'comment' && item.previewText === updatedContent, ); expect(mention).toBeTruthy(); }); it('supports superadmin sessions refresh rotation and notifications access', async () => { const loginOne = await request(app.getHttpServer()) .post('/api/v1/auth/superadmin/login') .send({ email: process.env.SUPERADMIN_EMAIL, password: process.env.SUPERADMIN_PASSWORD, }) .expect(200); superAdmin.accessToken = loginOne.body.accessToken; superAdmin.refreshToken = loginOne.body.refreshToken; const sessionsAfterFirstLogin = await request(app.getHttpServer()) .get('/api/v1/auth/superadmin/sessions') .set('Authorization', `Bearer ${superAdmin.accessToken}`) .expect(200); const countAfterFirstLogin = (sessionsAfterFirstLogin.body.items ?? []).length; const loginTwo = await request(app.getHttpServer()) .post('/api/v1/auth/superadmin/login') .send({ email: process.env.SUPERADMIN_EMAIL, password: process.env.SUPERADMIN_PASSWORD, }) .expect(200); superAdmin.secondAccessToken = loginTwo.body.accessToken; superAdmin.secondRefreshToken = loginTwo.body.refreshToken; const initialSessions = await request(app.getHttpServer()) .get('/api/v1/auth/superadmin/sessions') .set('Authorization', `Bearer ${superAdmin.accessToken}`) .expect(200); const initialItems = initialSessions.body.items ?? []; expect(initialItems).toHaveLength(countAfterFirstLogin + 1); const recentSessionIds = initialItems .slice(0, 2) .map((item: { id?: string }) => item.id) .filter((id: string | undefined): id is string => Boolean(id)); const refreshResponse = await request(app.getHttpServer()) .post('/api/v1/auth/superadmin/refresh') .send({ refreshToken: superAdmin.refreshToken }) .expect(200); superAdmin.accessToken = refreshResponse.body.accessToken; superAdmin.refreshToken = refreshResponse.body.refreshToken; const refreshedSessions = await request(app.getHttpServer()) .get('/api/v1/auth/superadmin/sessions') .set('Authorization', `Bearer ${superAdmin.accessToken}`) .expect(200); const refreshedItems = refreshedSessions.body.items ?? []; expect(refreshedItems).toHaveLength(initialItems.length); const notificationsResponse = await request(app.getHttpServer()) .get('/api/v1/notifications/superadmin?limit=10') .set('Authorization', `Bearer ${superAdmin.accessToken}`) .expect(200); expect(notificationsResponse.body.items).toBeInstanceOf(Array); const secondarySession = refreshedItems.find( (item: { id?: string }) => item.id && recentSessionIds.includes(item.id), ); expect(secondarySession?.id).toBeTruthy(); await request(app.getHttpServer()) .post(`/api/v1/auth/superadmin/sessions/${secondarySession.id}/revoke`) .set('Authorization', `Bearer ${superAdmin.accessToken}`) .expect(201); const sessionsAfterRevoke = await request(app.getHttpServer()) .get('/api/v1/auth/superadmin/sessions') .set('Authorization', `Bearer ${superAdmin.accessToken}`) .expect(200); expect(sessionsAfterRevoke.body.items ?? []).toHaveLength(initialItems.length - 1); }); });