feat: expand backend admin marketplace and scaling
فشلت بعض الفحوصات
/ deploy (push) Failing after 1m22s
فشلت بعض الفحوصات
/ deploy (push) Failing after 1m22s
هذا الالتزام موجود في:
@@ -1,21 +1,357 @@
|
||||
import { INestApplication, ValidationPipe } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import * as request from 'supertest';
|
||||
import { AppModule } from '../src/app.module';
|
||||
|
||||
describe('AppController (e2e)', () => {
|
||||
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;
|
||||
|
||||
beforeEach(async () => {
|
||||
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 = '';
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
it('/health (GET)', () => {
|
||||
return request(app.getHttpServer()).get('/health').expect(200);
|
||||
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('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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"moduleFileExtensions": ["js", "json", "ts"],
|
||||
"rootDir": ".",
|
||||
"testEnvironment": "node",
|
||||
"testTimeout": 120000,
|
||||
"testRegex": ".e2e-spec.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
|
||||
المرجع في مشكلة جديدة
حظر مستخدم