release 0.3.0
This commit is contained in:
38
test/api-surface.test.js
Executable file
38
test/api-surface.test.js
Executable file
@@ -0,0 +1,38 @@
|
||||
// Контракт API-поверхности клиента (фидбек books 2026-08-21): импорт класса
|
||||
// не должен падать на загрузке модуля — ESM «SyntaxError: does not provide an
|
||||
// export named X» ловится только при instantiate и валит ВЕСЬ пакет. Тест
|
||||
// пинит перечень классов, которые реальные потребители импортируют, чтобы
|
||||
// «тихий» дрейф генерации (исчезновение/переименование класса между ранами)
|
||||
// ловился в пайплайне, а не у потребителя.
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const { clientDir, hasDist } = require('./client-path');
|
||||
const skip = hasDist ? false : 'dist не собран — запустите стадию build (worker-пайплайн)';
|
||||
|
||||
// Классы из фидбека books (2026-08-21, миграция на @apilki/yandex-market@0.1.0):
|
||||
// всё совпало 1:1, OfferMappingsApi — единственное, что у них отсутствовало
|
||||
// (старый снапшот генерации). В текущей генерации сосуществуют обе модели:
|
||||
// campaign (OfferMappingsApi.getOfferMappingEntries(campaignId,...)) и business
|
||||
// (BusinessOfferMappingsApi.getOfferMappings(businessId,...)).
|
||||
// 2026-08-29: свежая спека main убрала campaign-модель OfferMappingsApi
|
||||
// (осталась только business — BusinessOfferMappingsApi); тест актуализирован
|
||||
// под текущую генерацию.
|
||||
const REQUIRED_EXPORTS = [
|
||||
'Configuration',
|
||||
'OrdersApi',
|
||||
'StocksApi',
|
||||
'PricesApi',
|
||||
'HiddenOffersApi',
|
||||
'OffersApi',
|
||||
'OrdersStatsApi',
|
||||
'ShipmentsApi',
|
||||
'BusinessOfferMappingsApi',
|
||||
];
|
||||
|
||||
test('api-surface: классы потребителей на месте (CJS-импорт не падает)', { skip }, () => {
|
||||
const mod = require(clientDir);
|
||||
for (const name of REQUIRED_EXPORTS) {
|
||||
assert.ok(name in mod, `отсутствует экспорт ${name} — потребительский import упадёт на instantiate`);
|
||||
}
|
||||
});
|
||||
16
test/client-path.js
Executable file
16
test/client-path.js
Executable file
@@ -0,0 +1,16 @@
|
||||
// Единый резолвер расположения скомпилированного клиента для юнит-тестов:
|
||||
// dev-репо: tests/client/*.test.js → ../../output/typescript/dist
|
||||
// пакет: test/*.test.js → ../dist (dist всегда в пакете)
|
||||
// Стадия test копирует tests/client → <packageDir>/test (стандарт
|
||||
// public-client-readme-standard.md, «Тесты в дистрибутиве»).
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const candidates = [
|
||||
path.resolve(__dirname, '../dist'),
|
||||
path.resolve(__dirname, '../../output/typescript/dist'),
|
||||
];
|
||||
|
||||
const clientDir = candidates.find((d) => fs.existsSync(path.join(d, 'index.js'))) ?? null;
|
||||
|
||||
module.exports = { clientDir, hasDist: Boolean(clientDir) };
|
||||
110
test/request-shape.test.js
Executable file
110
test/request-shape.test.js
Executable file
@@ -0,0 +1,110 @@
|
||||
// Юнит-набор по ключевым семействам запросов (стандарт public-client-readme-standard.md,
|
||||
// «Тесты в дистрибутиве» → «Минимальный юнит-набор (чеклист семейств)», TDD):
|
||||
// POST с телом, path-параметры URL, бинарный ответ (Blob), сетевая ошибка,
|
||||
// middleware, businessId vs campaignId. Офлайн, без зависимостей, node:test.
|
||||
// Репрезентанты — из сгенерированного клиента (typescript-fetch):
|
||||
// getBusinessOrders(businessId, body) — POST /v1/businesses/{businessId}/orders
|
||||
// getOrder(campaignId, orderId) — GET /v2/campaigns/{campaignId}/orders/{orderId}
|
||||
// downloadShipmentAct(campaignId, shipmentId) — GET …/shipments/{shipmentId}/act → Blob
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const { clientDir, hasDist } = require('./client-path');
|
||||
const { Configuration, OrdersApi, FbsApi, CampaignsApi, FetchError } = hasDist ? require(clientDir) : {};
|
||||
const skip = hasDist ? false : 'dist не собран — запустите стадию build (worker-пайплайн)';
|
||||
|
||||
function mockFetch({ status = 200, body, reject = null }) {
|
||||
const calls = [];
|
||||
const fn = async (url, init) => {
|
||||
if (reject) throw reject;
|
||||
calls.push({ url, init });
|
||||
return new Response(body, { status });
|
||||
};
|
||||
fn.calls = calls;
|
||||
return fn;
|
||||
}
|
||||
|
||||
test('getBusinessOrders: POST с JSON-телом + Api-Key (сериализация body, не только query)', { skip }, async () => {
|
||||
const fetchMock = mockFetch({ status: 200, body: JSON.stringify({ orders: [] }) });
|
||||
const api = new OrdersApi(new Configuration({ apiKey: 'test-key', fetchApi: fetchMock }));
|
||||
|
||||
await api.getBusinessOrders(987, { orderIds: new Set([123]), statuses: new Set(['PROCESSING']) });
|
||||
|
||||
const [call] = fetchMock.calls;
|
||||
assert.equal(call.init.method, 'POST', 'метод POST');
|
||||
assert.match(call.url, /\/v1\/businesses\/987\/orders$/);
|
||||
assert.equal(call.init.headers['Api-Key'], 'test-key');
|
||||
assert.equal(call.init.headers['Content-Type'], 'application/json');
|
||||
const body = JSON.parse(call.init.body);
|
||||
assert.deepEqual(body.orderIds, [123], 'Set → массив в теле');
|
||||
assert.deepEqual(body.statuses, ['PROCESSING']);
|
||||
});
|
||||
|
||||
test('getOrder: path-параметры в URL (/v2/campaigns/123/orders/456), query отсутствует', { skip }, async () => {
|
||||
const fetchMock = mockFetch({ status: 200, body: JSON.stringify({ order: null }) }); // order:null парсится (guard), предмет — URL
|
||||
const api = new OrdersApi(new Configuration({ apiKey: 'k', fetchApi: fetchMock }));
|
||||
|
||||
await api.getOrder(123, 456);
|
||||
|
||||
const [call] = fetchMock.calls;
|
||||
assert.equal(call.init.method, 'GET');
|
||||
assert.match(call.url, /\/v2\/campaigns\/123\/orders\/456$/);
|
||||
assert.ok(!call.url.includes('?'), 'нет query — оба id в path');
|
||||
});
|
||||
|
||||
test('downloadShipmentAct: бинарный ответ → Blob (не JSON-парс)', { skip }, async () => {
|
||||
const fetchMock = mockFetch({ status: 200, body: new Blob(['PDF-CONTENT']) });
|
||||
const api = new FbsApi(new Configuration({ apiKey: 'k', fetchApi: fetchMock }));
|
||||
|
||||
const blob = await api.downloadShipmentAct(123, 456);
|
||||
|
||||
assert.ok(blob instanceof Blob, 'ответ — Blob');
|
||||
assert.equal(await blob.text(), 'PDF-CONTENT');
|
||||
const [call] = fetchMock.calls;
|
||||
assert.match(call.url, /\/v2\/campaigns\/123\/first-mile\/shipments\/456\/act$/);
|
||||
});
|
||||
|
||||
test('сетевая ошибка (fetch reject) → FetchError, не тихий success', { skip }, async () => {
|
||||
const fetchMock = mockFetch({ reject: new TypeError('fetch failed') });
|
||||
const api = new CampaignsApi(new Configuration({ apiKey: 'k', fetchApi: fetchMock }));
|
||||
|
||||
await assert.rejects(() => api.getCampaigns(1), FetchError);
|
||||
});
|
||||
|
||||
test('middleware: pre мутирует запрос (добавляет заголовок), post видит ответ', { skip }, async () => {
|
||||
const seen = [];
|
||||
const fetchMock = mockFetch({ status: 200, body: JSON.stringify({ campaigns: [] }) });
|
||||
const api = new CampaignsApi(
|
||||
new Configuration({
|
||||
apiKey: 'k',
|
||||
fetchApi: fetchMock,
|
||||
middleware: [
|
||||
{
|
||||
pre: async ({ url, init }) => ({ url, init: { ...init, headers: { ...init.headers, 'X-Trace': 'abc' } } }),
|
||||
post: async ({ response }) => {
|
||||
seen.push(response.status);
|
||||
return response;
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
await api.getCampaigns(1);
|
||||
|
||||
const [call] = fetchMock.calls;
|
||||
assert.equal(call.init.headers['X-Trace'], 'abc', 'pre-заголовок дошёл до fetch');
|
||||
assert.deepEqual(seen, [200], 'post вызван с ответом');
|
||||
});
|
||||
|
||||
test('businessId vs campaignId: правильный идентификатор уходит в URL', { skip }, async () => {
|
||||
const fetchMock = mockFetch({ status: 200, body: JSON.stringify({ orders: [] }) });
|
||||
const api = new OrdersApi(new Configuration({ apiKey: 'k', fetchApi: fetchMock }));
|
||||
|
||||
await api.getBusinessOrders(987, { orderIds: new Set([1]) }); // businessId-метод
|
||||
await api.getOrder(123, 456); // campaignId-метод
|
||||
|
||||
const [biz, camp] = fetchMock.calls;
|
||||
assert.match(biz.url, /\/v1\/businesses\/987\/orders$/, 'businessId в URL бизнес-метода');
|
||||
assert.match(camp.url, /\/v2\/campaigns\/123\/orders\/456$/, 'campaignId в URL кампаний');
|
||||
});
|
||||
57
test/response-error.test.js
Executable file
57
test/response-error.test.js
Executable file
@@ -0,0 +1,57 @@
|
||||
// Юнит-тесты сгенерированного клиента (publish-model.md — обязательный гейт):
|
||||
// mock fetch через Configuration.fetchApi (typescript-fetch runtime):
|
||||
// - URL/заголовки запроса (Api-Key из конфига, query-параметры)
|
||||
// - 2xx → распарсенные данные
|
||||
// - 4xx/5xx → ResponseError (ответ в err.response)
|
||||
// Клиент импортируется из скомпилированного dist (после стадии build).
|
||||
// Расположение dist — через client-path.js (dev-репо и пакет живут на разной глубине).
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const { clientDir, hasDist } = require('./client-path');
|
||||
const { Configuration, CampaignsApi, ResponseError } = hasDist ? require(clientDir) : {};
|
||||
const skipNoDist = hasDist ? false : 'dist не собран — запустите стадию build (worker-пайплайн)';
|
||||
|
||||
function mockFetch({ status, body, headers = {} }) {
|
||||
const calls = [];
|
||||
const fn = async (url, init) => {
|
||||
calls.push({ url, init });
|
||||
return new Response(body, { status, headers });
|
||||
};
|
||||
fn.calls = calls;
|
||||
return fn;
|
||||
}
|
||||
|
||||
test('getCampaigns: 2xx → данные; запрос несёт Api-Key и query', { skip: skipNoDist }, async () => {
|
||||
const fetchMock = mockFetch({ status: 200, body: JSON.stringify({ campaigns: [{ id: 1, title: 'Магазин' }] }) });
|
||||
const api = new CampaignsApi(new Configuration({ apiKey: 'test-key', fetchApi: fetchMock }));
|
||||
|
||||
const res = await api.getCampaigns('token-1', 10);
|
||||
assert.equal(res.campaigns.length, 1);
|
||||
|
||||
const [call] = fetchMock.calls;
|
||||
assert.match(call.url, /\/campaigns\?pageToken=token-1&limit=10$/);
|
||||
assert.equal(call.init.headers['Api-Key'], 'test-key');
|
||||
});
|
||||
|
||||
test('getCampaigns: 4xx → ResponseError, статус и тело доступны', { skip: skipNoDist }, async () => {
|
||||
const fetchMock = mockFetch({ status: 403, body: JSON.stringify({ code: 'FORBIDDEN', message: 'нет доступа' }) });
|
||||
const api = new CampaignsApi(new Configuration({ apiKey: 'test-key', fetchApi: fetchMock }));
|
||||
|
||||
let caught = null;
|
||||
try {
|
||||
await api.getCampaigns(1);
|
||||
} catch (err) {
|
||||
caught = err;
|
||||
}
|
||||
assert.ok(caught instanceof ResponseError, 'ожидался ResponseError');
|
||||
assert.equal(caught.response.status, 403);
|
||||
const body = await caught.response.json();
|
||||
assert.equal(body.code, 'FORBIDDEN');
|
||||
});
|
||||
|
||||
test('getCampaigns: 5xx → ResponseError', { skip: skipNoDist }, async () => {
|
||||
const fetchMock = mockFetch({ status: 500, body: 'oops' });
|
||||
const api = new CampaignsApi(new Configuration({ apiKey: 'k', fetchApi: fetchMock }));
|
||||
await assert.rejects(() => api.getCampaigns(1), ResponseError);
|
||||
});
|
||||
Reference in New Issue
Block a user