Integrate Oudelaa backend features, security, tests, and deployment updates

هذا الالتزام موجود في:
boutmoun123
2026-07-26 16:58:53 +03:00
الأصل 1b24ca4294
التزام 2fd5322ef7
178 ملفات معدلة مع 19068 إضافات و2592 حذوفات

عرض الملف

@@ -2,6 +2,7 @@ import { INestApplication, ValidationPipe } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Test, TestingModule } from '@nestjs/testing';
import * as request from 'supertest';
import { io, Socket } from 'socket.io-client';
import { AppModule } from '../src/app.module';
Object.assign(process.env, {
@@ -84,7 +85,7 @@ describe('Oudelaa smoke (e2e)', () => {
transformOptions: { enableImplicitConversion: true },
}),
);
await app.init();
await app.listen(0, '127.0.0.1');
});
afterAll(async () => {
@@ -109,7 +110,7 @@ describe('Oudelaa smoke (e2e)', () => {
})
.expect(201);
const verifyResponse = await request(app.getHttpServer())
await request(app.getHttpServer())
.post('/api/v1/auth/verify-email')
.send({
email: user.email,
@@ -117,15 +118,31 @@ describe('Oudelaa smoke (e2e)', () => {
})
.expect(200);
user.accessToken = verifyResponse.body.accessToken;
user.userId = verifyResponse.body.user._id || verifyResponse.body.user.id;
user.username = verifyResponse.body.user.username;
const loginResponse = await request(app.getHttpServer())
.post('/api/v1/auth/login')
.send({ email: user.email, password: user.password })
.expect(200);
user.accessToken = loginResponse.body.accessToken;
user.userId = loginResponse.body.user._id || loginResponse.body.user.id;
user.username = loginResponse.body.user.username;
};
it('/api/v1/health (GET)', () => {
return request(app.getHttpServer()).get('/api/v1/health').expect(200);
});
it('/api/v1/health/ready validates MongoDB and storage', async () => {
const response = await request(app.getHttpServer()).get('/api/v1/health/ready').expect(200);
expect(response.body).toEqual(expect.objectContaining({
status: 'ready',
checks: expect.objectContaining({
mongodb: expect.objectContaining({ status: 'up' }),
storage: expect.objectContaining({ status: 'up' }),
}),
}));
});
it('registers and verifies smoke users', async () => {
await registerAndVerify(primary);
await registerAndVerify(secondary);
@@ -207,6 +224,59 @@ describe('Oudelaa smoke (e2e)', () => {
expect(response.body.mentionUsernames).toContain(secondary.username.toLowerCase());
});
it('authenticates realtime namespaces and rejects anonymous sockets', async () => {
const address = app.getHttpServer().address();
if (!address || typeof address === 'string') throw new Error('Test server address is unavailable');
const baseUrl = `http://127.0.0.1:${address.port}`;
const connectAuthenticated = (namespace: string) => new Promise<Socket>((resolve, reject) => {
const socket = io(`${baseUrl}/${namespace}`, {
auth: { token: primary.accessToken }, transports: ['websocket'], forceNew: true,
});
const timer = setTimeout(() => { socket.disconnect(); reject(new Error(`${namespace} connection timeout`)); }, 5000);
socket.once('connect', () => { clearTimeout(timer); resolve(socket); });
socket.once('connect_error', (error) => { clearTimeout(timer); socket.disconnect(); reject(error); });
});
const sockets = await Promise.all([
connectAuthenticated('notifications'),
connectAuthenticated('chat'),
]);
expect(sockets.every((socket) => socket.connected)).toBe(true);
sockets.forEach((socket) => socket.disconnect());
const anonymousRejected = await new Promise<boolean>((resolve) => {
const socket = io(`${baseUrl}/notifications`, { transports: ['websocket'], forceNew: true });
const timer = setTimeout(() => { socket.disconnect(); resolve(false); }, 5000);
socket.once('disconnect', () => { clearTimeout(timer); resolve(true); });
socket.once('connect_error', () => { clearTimeout(timer); socket.disconnect(); resolve(true); });
});
expect(anonymousRejected).toBe(true);
});
it('keeps likes idempotent under concurrent duplicate requests', async () => {
const responses = await Promise.all(
Array.from({ length: 20 }, () =>
request(app.getHttpServer())
.post('/api/v1/likes')
.set('Authorization', `Bearer ${secondary.accessToken}`)
.send({ targetType: 'post', targetId: postId }),
),
);
expect(responses.every((response) => response.status === 201)).toBe(true);
const postResponse = await request(app.getHttpServer())
.get(`/api/v1/posts/${postId}`)
.set('Authorization', `Bearer ${secondary.accessToken}`)
.expect(200);
expect(postResponse.body.likesCount).toBe(1);
await request(app.getHttpServer())
.delete(`/api/v1/likes/post/${postId}`)
.set('Authorization', `Bearer ${secondary.accessToken}`)
.expect(200);
});
it('returns unified pagination in feed', async () => {
const response = await request(app.getHttpServer())
.get('/api/v1/feed/me?includeSuggestions=false&limit=10')
@@ -222,6 +292,40 @@ describe('Oudelaa smoke (e2e)', () => {
);
});
it('tracks feed quality signals and supports not-interested recovery', async () => {
await request(app.getHttpServer())
.post('/api/v1/engagement/events/batch')
.set('Authorization', `Bearer ${secondary.accessToken}`)
.send({ events: [
{ postId, type: 'impression', sessionId: `smoke-${ts}` },
{ postId, type: 'watch', watchTimeMs: 4200, progressPercent: 85, sessionId: `smoke-${ts}` },
{ postId, type: 'complete', progressPercent: 100, sessionId: `smoke-${ts}` },
] })
.expect(202);
await request(app.getHttpServer())
.post(`/api/v1/engagement/posts/${postId}/not-interested`)
.set('Authorization', `Bearer ${secondary.accessToken}`)
.expect(201);
const feed = await request(app.getHttpServer())
.get('/api/v1/feed/me?includeSuggestions=false&limit=20')
.set('Authorization', `Bearer ${secondary.accessToken}`)
.expect(200);
expect(feed.body.items.some((item: any) => (item._id || item.id) === postId)).toBe(false);
await request(app.getHttpServer())
.delete(`/api/v1/engagement/posts/${postId}/not-interested`)
.set('Authorization', `Bearer ${secondary.accessToken}`)
.expect(200);
const summary = await request(app.getHttpServer())
.get('/api/v1/engagement/me/summary')
.set('Authorization', `Bearer ${secondary.accessToken}`)
.expect(200);
expect(summary.body).toEqual(expect.objectContaining({ periodDays: 30, byType: expect.any(Object) }));
});
it('creates mention notification for mentioned post user', async () => {
const response = await request(app.getHttpServer())
.get('/api/v1/notifications')

عرض الملف

@@ -2,6 +2,7 @@
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"setupFiles": ["<rootDir>/setup-env.ts"],
"testTimeout": 120000,
"testRegex": ".e2e-spec.ts$",
"transform": {

23
test/setup-env.ts Normal file
عرض الملف

@@ -0,0 +1,23 @@
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',
STORAGE_PROVIDER: 'local',
MEDIA_ACCESS_MODE: 'direct',
BCRYPT_SALT_ROUNDS: '8',
PUBLIC_BASE_URL: 'http://127.0.0.1:4000',
STORAGE_PUBLIC_BASE_URL: 'http://127.0.0.1:4000',
MONGODB_URI: process.env.E2E_MONGODB_URI ?? 'mongodb://127.0.0.1:27017/oudelaa-e2e',
JWT_ACCESS_SECRET: 'test-access-secret-123456',
JWT_REFRESH_SECRET: 'test-refresh-secret-123456',
SUPERADMIN_EMAIL: 'superadmin@example.com',
SUPERADMIN_PASSWORD: 'StrongPass123!',
SUPERADMIN_PASSWORD_HASH: '',
SUPERADMIN_ACCESS_SECRET: 'test-superadmin-access-123456',
SUPERADMIN_REFRESH_SECRET: 'test-superadmin-refresh-123456',
SUPERADMIN_TOTP_SECRET: '',
});