food-market/tests/e2e/lib/moysklad-mock.ts
nns c7ecc39590 test(e2e): scenario moysklad-import + mock-сервер MoySklad
lib/moysklad-mock.ts — минимальный mock JSON-API remap 1.2 (organization/
counterparty/product/productfolder) с полями по MoySkladDtos. Сценарий (7
шагов): сохранение/маскирование токена, test-connection, импорт контрагентов
и товаров через фоновый job, идемпотентность повторного импорта
(overwrite=false → Skipped), обновление по ключу (overwrite=true → Updated),
и проверка маппинга в БД (BIN/тип/адрес контрагента; артикул/НДС/упаковка/
цена/штрихкод/группа/страна товара).

Требует запуск API с MoySklad__BaseUrl=http://127.0.0.1:5099/api/remap/1.2/.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 11:27:16 +05:00

92 lines
3.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Минимальный 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<string, unknown>[]
products: Record<string, unknown>[]
folders: Record<string, unknown>[]
}
export interface MockHandle {
baseUrl: string
setData: (d: MsDataset) => void
requestCount: () => number
close: () => Promise<void>
}
const BASE_PATH = '/api/remap/1.2/'
export function startMoySkladMock(port: number): Promise<MockHandle> {
let data: MsDataset = { organization: { name: 'Mock Org', inn: '000000000000' }, counterparties: [], products: [], folders: [] }
let requests = 0
const listBody = (rows: Record<string, unknown>[]) => 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<string, unknown>[]) => {
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<string, unknown>]))
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())),
})
})
})
}