410 أسطر
14 KiB
TypeScript
410 أسطر
14 KiB
TypeScript
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<string>('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);
|
|
});
|
|
});
|