380 lines
13 KiB
JavaScript
380 lines
13 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const test = require('node:test');
|
|
const bcrypt = require('bcryptjs');
|
|
|
|
const { createApp } = require('../src/app');
|
|
const { closeDbPool, getDb } = require('../src/config/db');
|
|
const { validateEnv } = require('../src/config/env');
|
|
|
|
const createdUserIds = [];
|
|
const createdServiceIds = [];
|
|
const createdOrderIds = [];
|
|
const createdPaymentMethodIds = [];
|
|
|
|
function unique(prefix) {
|
|
return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
}
|
|
|
|
function listen(app) {
|
|
return new Promise((resolve) => {
|
|
const server = app.listen(0, '127.0.0.1', () => {
|
|
resolve({
|
|
baseUrl: `http://127.0.0.1:${server.address().port}`,
|
|
close: () => new Promise((done) => server.close(done))
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
async function request(baseUrl, path, options = {}) {
|
|
const response = await fetch(`${baseUrl}${path}`, {
|
|
...options,
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
...(options.headers || {})
|
|
}
|
|
});
|
|
const body = await response.json().catch(() => ({}));
|
|
|
|
return { response, body };
|
|
}
|
|
|
|
async function createAccount(baseUrl, prefix) {
|
|
const account = {
|
|
username: unique(prefix),
|
|
email: `${unique(prefix)}@example.com`,
|
|
password: 'StrongPass123'
|
|
};
|
|
const registered = await request(baseUrl, '/auth/register', {
|
|
method: 'POST',
|
|
body: JSON.stringify(account)
|
|
});
|
|
|
|
assert.equal(registered.response.status, 201);
|
|
createdUserIds.push(registered.body.user.id);
|
|
|
|
return {
|
|
...account,
|
|
id: registered.body.user.id
|
|
};
|
|
}
|
|
|
|
async function createDbAccount(prefix, overrides = {}) {
|
|
const db = getDb();
|
|
const password = 'StrongPass123';
|
|
const account = {
|
|
username: unique(prefix),
|
|
email: `${unique(prefix)}@example.com`,
|
|
password
|
|
};
|
|
const [id] = await db('users').insert({
|
|
username: account.username,
|
|
email: account.email,
|
|
password_hash: await bcrypt.hash(password, 12),
|
|
balance: '0.0000',
|
|
role: 'user',
|
|
status: 'active',
|
|
...overrides
|
|
});
|
|
|
|
createdUserIds.push(id);
|
|
|
|
return {
|
|
...account,
|
|
id
|
|
};
|
|
}
|
|
|
|
async function login(baseUrl, account) {
|
|
const loggedIn = await request(baseUrl, '/auth/login', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
email: account.email,
|
|
password: account.password
|
|
})
|
|
});
|
|
|
|
assert.equal(loggedIn.response.status, 200);
|
|
return loggedIn.body.accessToken;
|
|
}
|
|
|
|
test.after(async () => {
|
|
const db = getDb();
|
|
|
|
if (createdOrderIds.length > 0) {
|
|
await db('orders').whereIn('id', createdOrderIds).del();
|
|
}
|
|
|
|
if (createdServiceIds.length > 0) {
|
|
await db('services').whereIn('id', createdServiceIds).del();
|
|
}
|
|
|
|
if (createdPaymentMethodIds.length > 0) {
|
|
await db('payment_methods').whereIn('id', createdPaymentMethodIds).del();
|
|
}
|
|
|
|
if (createdUserIds.length > 0) {
|
|
await db('password_reset_tokens').whereIn('user_id', createdUserIds).del();
|
|
await db('refresh_tokens').whereIn('user_id', createdUserIds).del();
|
|
await db('deposits').whereIn('user_id', createdUserIds).orWhereIn('reviewed_by', createdUserIds).del();
|
|
await db('transactions').whereIn('user_id', createdUserIds).del();
|
|
await db('users').whereIn('id', createdUserIds).del();
|
|
}
|
|
|
|
await closeDbPool();
|
|
});
|
|
|
|
test('admin users and stats work, while normal users receive 403', async () => {
|
|
process.env.JWT_SECRET = process.env.JWT_SECRET || 'test_secret';
|
|
const api = await listen(createApp());
|
|
|
|
try {
|
|
const db = getDb();
|
|
const admin = await createAccount(api.baseUrl, 'admin_test_admin');
|
|
const user = await createAccount(api.baseUrl, 'admin_test_user');
|
|
|
|
await db('users').where({ id: admin.id }).update({ role: 'admin' });
|
|
|
|
const [serviceId] = await db('services').insert({
|
|
provider_service_id: unique('admin_provider_service'),
|
|
name: 'Admin Test Service',
|
|
category: 'Admin',
|
|
provider_price: '4.0000',
|
|
markup_percent: '25.0000',
|
|
sell_price: '5.0000',
|
|
min: 100,
|
|
max: 10000,
|
|
is_active: true
|
|
});
|
|
createdServiceIds.push(serviceId);
|
|
|
|
const [orderId] = await db('orders').insert({
|
|
user_id: user.id,
|
|
service_id: serviceId,
|
|
link: 'https://example.com/admin-order',
|
|
quantity: 1000,
|
|
charge: '5.0000',
|
|
provider_order_id: unique('admin_provider_order'),
|
|
status: 'completed'
|
|
});
|
|
createdOrderIds.push(orderId);
|
|
|
|
const adminToken = await login(api.baseUrl, admin);
|
|
const userToken = await login(api.baseUrl, user);
|
|
|
|
const users = await request(api.baseUrl, '/admin/users', {
|
|
headers: { authorization: `Bearer ${adminToken}` }
|
|
});
|
|
assert.equal(users.response.status, 200);
|
|
assert.equal(users.body.users.some((row) => Number(row.id) === Number(user.id)), true);
|
|
|
|
const stats = await request(api.baseUrl, '/admin/stats', {
|
|
headers: { authorization: `Bearer ${adminToken}` }
|
|
});
|
|
assert.equal(stats.response.status, 200);
|
|
assert.equal(stats.body.stats.gross_sales, '5.0000');
|
|
assert.equal(stats.body.stats.provider_cost, '4.0000');
|
|
assert.equal(stats.body.stats.profit, '1.0000');
|
|
|
|
const adjusted = await request(api.baseUrl, `/admin/users/${user.id}`, {
|
|
method: 'PATCH',
|
|
headers: { authorization: `Bearer ${adminToken}` },
|
|
body: JSON.stringify({
|
|
amount: '3.0000',
|
|
note: 'Admin test credit'
|
|
})
|
|
});
|
|
assert.equal(adjusted.response.status, 200);
|
|
assert.equal(adjusted.body.user.balance, '3.0000');
|
|
assert.equal(adjusted.body.wallet.balance, '3.0000');
|
|
|
|
const banned = await request(api.baseUrl, `/admin/users/${user.id}`, {
|
|
method: 'PATCH',
|
|
headers: { authorization: `Bearer ${adminToken}` },
|
|
body: JSON.stringify({ status: 'banned' })
|
|
});
|
|
assert.equal(banned.response.status, 200);
|
|
assert.equal(banned.body.user.status, 'banned');
|
|
|
|
const forbiddenChecks = [
|
|
request(api.baseUrl, '/admin/users', { headers: { authorization: `Bearer ${userToken}` } }),
|
|
request(api.baseUrl, '/admin/deposits', { headers: { authorization: `Bearer ${userToken}` } }),
|
|
request(api.baseUrl, '/admin/orders', { headers: { authorization: `Bearer ${userToken}` } }),
|
|
request(api.baseUrl, '/admin/services', { headers: { authorization: `Bearer ${userToken}` } }),
|
|
request(api.baseUrl, '/admin/payment-methods', { headers: { authorization: `Bearer ${userToken}` } }),
|
|
request(api.baseUrl, '/admin/stats', { headers: { authorization: `Bearer ${userToken}` } }),
|
|
request(api.baseUrl, `/admin/users/${user.id}`, {
|
|
method: 'PATCH',
|
|
headers: { authorization: `Bearer ${userToken}` },
|
|
body: JSON.stringify({ status: 'active' })
|
|
}),
|
|
request(api.baseUrl, '/admin/services/sync', {
|
|
method: 'POST',
|
|
headers: { authorization: `Bearer ${userToken}` },
|
|
body: JSON.stringify({})
|
|
}),
|
|
request(api.baseUrl, '/admin/deposits/1', {
|
|
method: 'PATCH',
|
|
headers: { authorization: `Bearer ${userToken}` },
|
|
body: JSON.stringify({ action: 'reject' })
|
|
}),
|
|
request(api.baseUrl, `/admin/services/${serviceId}`, {
|
|
method: 'PATCH',
|
|
headers: { authorization: `Bearer ${userToken}` },
|
|
body: JSON.stringify({ is_active: false })
|
|
}),
|
|
request(api.baseUrl, '/admin/payment-methods', {
|
|
method: 'POST',
|
|
headers: { authorization: `Bearer ${userToken}` },
|
|
body: JSON.stringify({
|
|
code: unique('normal_forbidden'),
|
|
label_ar: 'Forbidden',
|
|
instructions: 'Forbidden payment method'
|
|
})
|
|
})
|
|
];
|
|
const forbiddenResponses = await Promise.all(forbiddenChecks);
|
|
|
|
assert.equal(forbiddenResponses.every((result) => result.response.status === 403), true);
|
|
} finally {
|
|
await api.close();
|
|
}
|
|
});
|
|
|
|
test('admin can manage payment methods and active list hides disabled methods', async () => {
|
|
process.env.JWT_SECRET = process.env.JWT_SECRET || 'test_secret';
|
|
const api = await listen(createApp());
|
|
|
|
try {
|
|
const db = getDb();
|
|
const admin = await createAccount(api.baseUrl, 'payment_admin');
|
|
const user = await createAccount(api.baseUrl, 'payment_user');
|
|
|
|
await db('users').where({ id: admin.id }).update({ role: 'admin' });
|
|
|
|
const adminToken = await login(api.baseUrl, admin);
|
|
const userToken = await login(api.baseUrl, user);
|
|
const code = unique('payment_method').replace(/[^a-z0-9_]/g, '_').slice(0, 30);
|
|
|
|
const created = await request(api.baseUrl, '/admin/payment-methods', {
|
|
method: 'POST',
|
|
headers: { authorization: `Bearer ${adminToken}` },
|
|
body: JSON.stringify({
|
|
code,
|
|
label_ar: 'اختبار دفع',
|
|
instructions: 'تعليمات قناة الدفع الاختبارية',
|
|
destination: 'test@example.com',
|
|
sort_order: 999
|
|
})
|
|
});
|
|
assert.equal(created.response.status, 201);
|
|
createdPaymentMethodIds.push(created.body.paymentMethod.id);
|
|
|
|
const active = await request(api.baseUrl, '/payment-methods', {
|
|
headers: { authorization: `Bearer ${userToken}` }
|
|
});
|
|
assert.equal(active.response.status, 200);
|
|
assert.equal(active.body.paymentMethods.some((method) => method.code === code), true);
|
|
|
|
const disabled = await request(api.baseUrl, `/admin/payment-methods/${created.body.paymentMethod.id}`, {
|
|
method: 'PATCH',
|
|
headers: { authorization: `Bearer ${adminToken}` },
|
|
body: JSON.stringify({ is_active: false })
|
|
});
|
|
assert.equal(disabled.response.status, 200);
|
|
assert.equal(disabled.body.paymentMethod.is_active, 0);
|
|
|
|
const hidden = await request(api.baseUrl, '/payment-methods', {
|
|
headers: { authorization: `Bearer ${userToken}` }
|
|
});
|
|
assert.equal(hidden.response.status, 200);
|
|
assert.equal(hidden.body.paymentMethods.some((method) => method.code === code), false);
|
|
} finally {
|
|
await api.close();
|
|
}
|
|
});
|
|
|
|
test('admin user update refuses self-ban and blocks rate-limited sensitive routes', async () => {
|
|
process.env.JWT_SECRET = process.env.JWT_SECRET || 'test_secret';
|
|
const api = await listen(createApp());
|
|
|
|
try {
|
|
const admin = await createDbAccount('security_admin', { role: 'admin' });
|
|
const user = await createDbAccount('security_user');
|
|
|
|
const adminToken = await login(api.baseUrl, admin);
|
|
const userToken = await login(api.baseUrl, user);
|
|
|
|
const selfBan = await request(api.baseUrl, `/admin/users/${admin.id}`, {
|
|
method: 'PATCH',
|
|
headers: { authorization: `Bearer ${adminToken}` },
|
|
body: JSON.stringify({ status: 'banned' })
|
|
});
|
|
assert.equal(selfBan.response.status, 400);
|
|
assert.equal(selfBan.body.error.code, 'ADMIN_SELF_BAN_FORBIDDEN');
|
|
|
|
const forgotResponses = [];
|
|
for (let index = 0; index < 6; index += 1) {
|
|
forgotResponses.push(await request(api.baseUrl, '/auth/forgot-password', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ email: `missing_${index}@example.com` })
|
|
}));
|
|
}
|
|
assert.equal(forgotResponses.slice(0, 5).every((result) => result.response.status === 200), true);
|
|
assert.equal(forgotResponses[5].response.status, 429);
|
|
|
|
const depositResponses = [];
|
|
for (let index = 0; index < 11; index += 1) {
|
|
depositResponses.push(await request(api.baseUrl, '/deposits', {
|
|
method: 'POST',
|
|
headers: { authorization: `Bearer ${userToken}` },
|
|
body: JSON.stringify({
|
|
amount: '1.0000',
|
|
method: 'paypal',
|
|
tx_ref: unique(`rate_limited_deposit_${index}`)
|
|
})
|
|
}));
|
|
}
|
|
assert.equal(depositResponses.slice(0, 10).every((result) => result.response.status === 201), true);
|
|
assert.equal(depositResponses[10].response.status, 429);
|
|
} finally {
|
|
await api.close();
|
|
}
|
|
});
|
|
|
|
test('production environment requires CORS_ORIGIN', () => {
|
|
const previous = {
|
|
NODE_ENV: process.env.NODE_ENV,
|
|
DATABASE_URL: process.env.DATABASE_URL,
|
|
JWT_SECRET: process.env.JWT_SECRET,
|
|
PROVIDER_API_URL: process.env.PROVIDER_API_URL,
|
|
PROVIDER_API_KEY: process.env.PROVIDER_API_KEY,
|
|
CORS_ORIGIN: process.env.CORS_ORIGIN
|
|
};
|
|
|
|
try {
|
|
process.env.NODE_ENV = 'production';
|
|
process.env.DATABASE_URL = 'mysql://smm:smm_password@127.0.0.1:3306/smm';
|
|
process.env.JWT_SECRET = 'a'.repeat(32);
|
|
process.env.PROVIDER_API_URL = 'https://provider.real/api/v2';
|
|
process.env.PROVIDER_API_KEY = 'real_provider_key';
|
|
delete process.env.CORS_ORIGIN;
|
|
|
|
assert.throws(
|
|
() => validateEnv(),
|
|
/Missing required environment variables: CORS_ORIGIN/
|
|
);
|
|
|
|
process.env.CORS_ORIGIN = 'https://panel.example.com';
|
|
assert.doesNotThrow(() => validateEnv());
|
|
} finally {
|
|
Object.entries(previous).forEach(([key, value]) => {
|
|
if (value === undefined) {
|
|
delete process.env[key];
|
|
} else {
|
|
process.env[key] = value;
|
|
}
|
|
});
|
|
}
|
|
});
|