Integrate Oudelaa backend features, security, tests, and deployment updates
هذا الالتزام موجود في:
@@ -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
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: '',
|
||||
});
|
||||
المرجع في مشكلة جديدة
حظر مستخدم