Initial SMM panel implementation
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const http = require('node:http');
|
||||
const test = require('node:test');
|
||||
|
||||
const { closeDbPool, getDb } = require('../src/config/db');
|
||||
const {
|
||||
calculateOrderCharge,
|
||||
createOrder,
|
||||
getUserOrder,
|
||||
listUserOrders
|
||||
} = require('../src/services/orders.service');
|
||||
const { creditBalance } = require('../src/services/wallet.service');
|
||||
|
||||
const createdUserIds = [];
|
||||
const createdServiceIds = [];
|
||||
let providerCalls = [];
|
||||
let providerMode = 'success';
|
||||
let providerMock;
|
||||
|
||||
function unique(prefix) {
|
||||
return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
||||
}
|
||||
|
||||
function startProviderMock() {
|
||||
const server = http.createServer(async (req, res) => {
|
||||
const chunks = [];
|
||||
|
||||
for await (const chunk of req) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
|
||||
const body = new URLSearchParams(Buffer.concat(chunks).toString());
|
||||
providerCalls.push(Object.fromEntries(body.entries()));
|
||||
res.setHeader('content-type', 'application/json');
|
||||
|
||||
if (body.get('action') !== 'add') {
|
||||
res.statusCode = 400;
|
||||
res.end(JSON.stringify({ error: 'unsupported action' }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (providerMode === 'failure') {
|
||||
res.end(JSON.stringify({ error: 'provider rejected order' }));
|
||||
return;
|
||||
}
|
||||
|
||||
res.end(JSON.stringify({ order: `provider-${Date.now()}` }));
|
||||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
resolve({
|
||||
url: `http://127.0.0.1:${server.address().port}`,
|
||||
close: () => new Promise((done) => server.close(done))
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function createUser() {
|
||||
const db = getDb();
|
||||
const username = unique('order_user');
|
||||
const [id] = await db('users').insert({
|
||||
username,
|
||||
email: `${username}@example.com`,
|
||||
password_hash: 'hash',
|
||||
balance: '0.0000'
|
||||
});
|
||||
|
||||
createdUserIds.push(id);
|
||||
return id;
|
||||
}
|
||||
|
||||
async function createService(overrides = {}) {
|
||||
const [id] = await getDb()('services').insert({
|
||||
provider_service_id: unique('provider_service'),
|
||||
name: 'Instagram Followers',
|
||||
category: 'Instagram',
|
||||
provider_price: '4.0000',
|
||||
markup_percent: '25.0000',
|
||||
sell_price: '5.0000',
|
||||
min: 100,
|
||||
max: 10000,
|
||||
is_active: true,
|
||||
...overrides
|
||||
});
|
||||
|
||||
createdServiceIds.push(id);
|
||||
return id;
|
||||
}
|
||||
|
||||
async function readUser(userId) {
|
||||
return getDb()('users').where({ id: userId }).first();
|
||||
}
|
||||
|
||||
test.before(async () => {
|
||||
providerMock = await startProviderMock();
|
||||
process.env.PROVIDER_API_URL = providerMock.url;
|
||||
process.env.PROVIDER_API_KEY = 'mock-key';
|
||||
});
|
||||
|
||||
test.after(async () => {
|
||||
const db = getDb();
|
||||
|
||||
if (createdUserIds.length > 0) {
|
||||
await db('orders').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('password_reset_tokens').whereIn('user_id', createdUserIds).del();
|
||||
await db('refresh_tokens').whereIn('user_id', createdUserIds).del();
|
||||
await db('users').whereIn('id', createdUserIds).del();
|
||||
}
|
||||
|
||||
if (createdServiceIds.length > 0) {
|
||||
await db('services').whereIn('id', createdServiceIds).del();
|
||||
}
|
||||
|
||||
await closeDbPool();
|
||||
await providerMock.close();
|
||||
});
|
||||
|
||||
test('calculateOrderCharge uses server sell_price per 1000', () => {
|
||||
assert.equal(calculateOrderCharge('5.0000', 1000), '5.0000');
|
||||
assert.equal(calculateOrderCharge('5.0000', 250), '1.2500');
|
||||
assert.equal(calculateOrderCharge('1.3333', 3), '0.0040');
|
||||
});
|
||||
|
||||
test('successful order charges wallet, creates order, and sends provider order', async () => {
|
||||
providerMode = 'success';
|
||||
providerCalls = [];
|
||||
const userId = await createUser();
|
||||
const serviceId = await createService({ provider_service_id: 'provider-success-test' });
|
||||
|
||||
await creditBalance(userId, '20.0000', unique('seed_order_success'), 'seed balance');
|
||||
|
||||
const order = await createOrder(userId, {
|
||||
service_id: serviceId,
|
||||
link: 'https://example.com/post/1',
|
||||
quantity: 1500
|
||||
});
|
||||
const user = await readUser(userId);
|
||||
const transactions = await getDb()('transactions').where({ user_id: userId, type: 'order', reference: `order:${order.id}` });
|
||||
|
||||
assert.equal(order.status, 'processing');
|
||||
assert.equal(order.charge, '7.5000');
|
||||
assert.equal(user.balance, '12.5000');
|
||||
assert.equal(transactions.length, 1);
|
||||
assert.equal(providerCalls.length, 1);
|
||||
assert.equal(providerCalls[0].service, 'provider-success-test');
|
||||
assert.equal(providerCalls[0].quantity, '1500');
|
||||
});
|
||||
|
||||
test('failed provider order refunds wallet and marks order failed', async () => {
|
||||
providerMode = 'failure';
|
||||
providerCalls = [];
|
||||
const userId = await createUser();
|
||||
const serviceId = await createService({ provider_service_id: 'provider-failure-test' });
|
||||
|
||||
await creditBalance(userId, '10.0000', unique('seed_order_failure'), 'seed balance');
|
||||
|
||||
await assert.rejects(
|
||||
() => createOrder(userId, {
|
||||
service_id: serviceId,
|
||||
link: 'https://example.com/post/2',
|
||||
quantity: 1000
|
||||
}),
|
||||
(error) => error.code === 'PROVIDER_ERROR'
|
||||
);
|
||||
|
||||
const user = await readUser(userId);
|
||||
const [order] = await getDb()('orders').where({ user_id: userId, service_id: serviceId });
|
||||
const refund = await getDb()('transactions').where({ user_id: userId, type: 'refund', reference: `refund:order:${order.id}` }).first();
|
||||
|
||||
assert.equal(order.status, 'failed');
|
||||
assert.equal(user.balance, '10.0000');
|
||||
assert.equal(refund.amount, '5.0000');
|
||||
});
|
||||
|
||||
test('users can list and read only their own orders', async () => {
|
||||
providerMode = 'success';
|
||||
const ownerId = await createUser();
|
||||
const otherUserId = await createUser();
|
||||
const serviceId = await createService({ provider_service_id: 'provider-visibility-test' });
|
||||
|
||||
await creditBalance(ownerId, '10.0000', unique('seed_owner'), 'seed owner');
|
||||
await creditBalance(otherUserId, '10.0000', unique('seed_other'), 'seed other');
|
||||
|
||||
const ownerOrder = await createOrder(ownerId, {
|
||||
service_id: serviceId,
|
||||
link: 'https://example.com/owner',
|
||||
quantity: 1000
|
||||
});
|
||||
await createOrder(otherUserId, {
|
||||
service_id: serviceId,
|
||||
link: 'https://example.com/other',
|
||||
quantity: 1000
|
||||
});
|
||||
|
||||
const ownerOrders = await listUserOrders(ownerId);
|
||||
|
||||
assert.equal(ownerOrders.some((order) => order.id === ownerOrder.id), true);
|
||||
assert.equal(ownerOrders.every((order) => Number(order.user_id) === Number(ownerId)), true);
|
||||
await assert.rejects(
|
||||
() => getUserOrder(otherUserId, ownerOrder.id),
|
||||
(error) => error.code === 'ORDER_NOT_FOUND'
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user