208 lines
6.4 KiB
JavaScript
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'
|
|
);
|
|
});
|