Files
SS/tests/orders.test.js

208 lines
6.4 KiB
JavaScript

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'
);
});