/** * Минимальный mock MoySklad JSON-API 1.2 для e2e-теста импорта. * * Отдаёт ровно те эндпоинты, которые дёргает MoySkladClient: * GET entity/organization — для test-connection (WhoAmI) * GET entity/counterparty — список контрагентов * GET entity/product — список товаров * GET entity/productfolder — группы товаров * * Формы JSON повторяют реальные поля API (см. MoySkladDtos.cs): name, * legalTitle, companyType, inn, kpp, phone, email, actual/legalAddress, * description, archived; product.name/article/code/weighed/vat/vatEnabled/ * trackingType/barcodes/salePrices/buyPrice/productFolder/country. Значения в * деньгах — в minor units (как у MoySklad: клиент делит на 100). * * Пагинация: данных мало (<1000), поэтому клиент забирает всё одной страницей. * Запрос с filter=archived=true отдаём пустым (все наши записи активны). * * Данные мутабельны (setData) — между прогонами импорта меняем их, чтобы * проверить overwrite/идемпотентность. */ import { createServer, type Server } from 'node:http' export interface MsDataset { organization: { name: string; inn: string } counterparties: Record[] products: Record[] folders: Record[] } export interface MockHandle { baseUrl: string setData: (d: MsDataset) => void requestCount: () => number close: () => Promise } const BASE_PATH = '/api/remap/1.2/' export function startMoySkladMock(port: number): Promise { let data: MsDataset = { organization: { name: 'Mock Org', inn: '000000000000' }, counterparties: [], products: [], folders: [] } let requests = 0 const listBody = (rows: Record[]) => JSON.stringify({ meta: { size: rows.length, limit: 1000, offset: 0, href: 'mock' }, rows, }) const server = createServer((req, res) => { requests++ const url = req.url ?? '' const archivedOnly = url.includes('archived=true') const path = url.split('?')[0] const send = (rows: Record[]) => { res.writeHead(200, { 'Content-Type': 'application/json' }) res.end(listBody(archivedOnly ? [] : rows)) } if (!path.startsWith(BASE_PATH)) { res.writeHead(404, { 'Content-Type': 'application/json' }) res.end(JSON.stringify({ errors: [{ error: 'not found', code: 0 }] })) return } const entity = path.slice(BASE_PATH.length) switch (entity) { case 'entity/organization': res.writeHead(200, { 'Content-Type': 'application/json' }) res.end(listBody([data.organization as Record])) return case 'entity/counterparty': return send(data.counterparties) case 'entity/product': return send(data.products) case 'entity/productfolder': return send(data.folders) default: res.writeHead(404, { 'Content-Type': 'application/json' }) res.end(JSON.stringify({ errors: [{ error: `unhandled ${entity}`, code: 0 }] })) } }) return new Promise((resolve) => { server.listen(port, '127.0.0.1', () => { resolve({ baseUrl: `http://127.0.0.1:${port}${BASE_PATH}`, setData: (d) => { data = d }, requestCount: () => requests, close: () => new Promise((r) => server.close(() => r())), }) }) }) }