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>
92 lines
3.6 KiB
TypeScript
92 lines
3.6 KiB
TypeScript
/**
|
||
* Минимальный 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())),
|
||
})
|
||
})
|
||
})
|
||
}
|