diff --git a/README.md b/README.md index e6412c2..061a189 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,37 @@ -# @apilki/yandex-market — JavaScript/TypeScript клиент +# @apilki/yandex-market — JavaScript/TypeScript client -**Версия: 0.2.0 · API от 2026-08-25** +**Version: 0.3.0 · API as of 2026-08-25** -JavaScript/TypeScript клиент для **Yandex Market Partner API** (продавец-сторона: заказы, каталог, цены, отчёты). Построен на базе `fetch` API. Работает и в Node.js, и в браузере. Публикуется в npm-скоупе `@apilki` (GitHub-орг `apilki`). +A **curated generated TypeScript client** for the Yandex Market Partner API — +generated from the official API specification, with edge-cases and conflicts +resolved. Fully typed, fetch-based, zero runtime dependencies, works in +Node.js ≥ 18 and browsers. Covers all 161 operations (43 API classes) and is +regenerated on every upstream change. -## Особенности сборки -- **Типизация**: Полная поддержка TypeScript (`.d.ts` включены). -- **Стиль имен**: - - Методы и классы: `camelCase` (например, `orderApi.getOrders`). - - Свойства объектов (JSON): **Original** (сохранено именование из спецификации Яндекса, обычно это `camelCase`). -- **Аргументы методов**: Плоский список (позиционный). Первым параметром в большинстве методов идет `campaignId`. +```bash +npm install @apilki/yandex-market +``` + +Docs: https://yandex.ru/dev/market/partner-api/doc/ru/ · Full description (RU) below. + +## Что это + +Готовый TypeScript-клиент для **Yandex Market Partner API** — заказы, каталог, +цены, отчёты (продавец-сторона). Сгенерирован из официальной документации +Яндекса и **доведён до ума**: неоднозначности и конфликты документации +разбираются агентами и автоматическими рецептами, результат проверяется +тестами — вы получаете клиент, который просто работает. + +- **Полное покрытие** — все 161 операция, 43 API-класса, ~757 моделей; +- **Всегда свежий** — перегенерируется при каждом изменении API Яндекса; +- **Никаких зависимостей** — на чистом `fetch`, работает в Node.js ≥ 18 и в браузере; +- **Типизация / Ноль сюрпризов** — полные `.d.ts`, оригинальные имена полей из спецификации. + +## Установка + +```bash +npm install @apilki/yandex-market +``` ## Быстрый старт @@ -32,32 +54,43 @@ const orderApi = new OrderApi(config); const campaignsApi = new CampaignsApi(config); ``` -## 2. Работа с несколькими кабинетами или магазинами +### 2. Несколько кабинетов или магазинов -В зависимости от метода API Яндекса, первым аргументом может выступать либо **campaignId** (идентификатор магазина), либо **businessId** (идентификатор бизнеса/кабинета). Ключ авторизации `Api-Key` уже находится в заголовках конфигурации, поэтому он не требуется в аргументах. +В зависимости от метода API Яндекса первым аргументом выступает либо +**campaignId** (идентификатор магазина), либо **businessId** (идентификатор +бизнеса/кабинета). Ключ авторизации `Api-Key` уже в заголовках конфигурации — +в аргументах он не требуется. ```javascript -const campaignId = 12345678; +const campaignId = 12345678; const businessId = 987654; async function fetchData() { - try { - // Пример метода с campaignId (Заказы) - const orders = await orderApi.getOrders(campaignId, { status: 'PROCESSING' }); + // Метод с campaignId (Заказы) + const orders = await orderApi.getOrders(campaignId, { status: 'PROCESSING' }); - // Пример метода с businessId (Каталог/Цены) - const offers = await assortmentApi.getOfferMappings(businessId, { limit: 100 }); - - console.log('Данные получены'); - } catch (error) { - console.error('Yandex API Error:', error); - } + // Метод с businessId (Каталог/Цены) + const offers = await assortmentApi.getOfferMappings(businessId, { limit: 100 }); + + console.log('Данные получены'); } ``` -### 3. Обработка ошибок +## Особенности сборки -В случае ошибок 4xx/5xx клиент выбрасывает ResponseError. Ответ Яндекса обычно содержит объект с массивом ошибок. +- **Типизация**: полная поддержка TypeScript (`.d.ts` включены). +- **Стиль имён**: + - Методы и классы: `camelCase` (например, `orderApi.getOrders`); + - Свойства объектов (JSON): **original** — сохранено именование из спецификации Яндекса. +- **Аргументы методов**: плоский список (позиционный). Первым параметром в большинстве + методов идёт `campaignId`, в методах каталога/финтеха — `businessId`. Проверяйте + сигнатуру через автодополнение в IDE. +- **Fetch**: в среде Node.js < 18 требуется полифил `node-fetch`. + +## Ошибки + +При ответах 4xx/5xx клиент выбрасывает `ResponseError`. Ответ Яндекса обычно +содержит объект с массивом ошибок. ```javascript try { @@ -69,7 +102,7 @@ try { } ``` -### 4. Отладка (Middleware) +## Отладка (Middleware) Для отладки сетевых запросов используйте Middleware: @@ -84,21 +117,52 @@ const config = new Configuration({ }); ``` -### Важные замечания +## Покрытие API -Порядок аргументов: В большинстве методов Яндекса первым аргументом идет campaignId. Всегда проверяйте сигнатуру метода через автодополнение в IDE. +Клиент покрывает **161 операцию** в **43 API-классах** (~757 моделей данных) — +полный охват Yandex Market Partner API. Покрытие поддерживается пайплайном: +каждое изменение спецификации проверяется автоматическими тестами на полноту. -**Идентификаторы (ID):** Внимательно следите за сигнатурой метода в IDE. -* Если метод относится к операциям магазина — первым аргументом идет campaignId. -* Если к настройкам каталога или финтеху — первым аргументом идет businessId. +## Postman-коллекция -**JSON:** Мы сохранили оригинальное именование полей. Если поле в документации Яндекса называется deliveryServiceId, в коде оно будет точно таким же. +Готовая [Postman Collection](https://github.com/apilki/yandex-market-postman) +v2.1 — отдельный артефакт `apilki/yandex-market-postman` +(`postman/collection.json`). Импортируйте коллекцию и работайте с API без кода. -**Fetch:** В среде Node.js < 18 требуется полифил node-fetch. +## Тесты -**Авторизация:** Если Api-Key не задан в Configuration, он может потребоваться первым аргументом в каждом методе. Проверяйте подсказки IDE. +После установки выполните `npm test` — юнит-набор по ключевым путям +(запросы, сериализация, обработка ошибок, middleware). Прогоняется перед +каждой публикацией, работает офлайн — без зависимостей и ключей доступа. -### Ссылки +## Известные ограничения -* [Официальная документация API Яндекс Маркета для продавцов](https://yandex.ru/dev/market/partner-api/doc/ru/) -* [Спецификация API Яндекс Маркета для продавцов](https://github.com/yandex-market/yandex-market-partner-api) \ No newline at end of file +- **Свежесть API — дата коммита спецификации, а не номер версии.** Апстрим + публикует спецификацию без семантических версий (`LATEST`): номер версии + есть только у клиента. Поэтому актуальность данных отражается датой в шапке + README (строка «API от …») и в CHANGELOG релизов — сопоставление по версиям + спецификации невозможно. Breaking-изменения анонсируются в ⚠-секции + CHANGELOG и в [MIGRATION.md](MIGRATION.md) (в пакете и GH-дистро). +- **Node.js < 18 — нужен полифил `fetch`.** Клиент работает на встроенном + `fetch` (ноль зависимостей), нативно доступном с Node 18. На более старых + версиях установите `node-fetch` и передайте его через + `Configuration.fetchApi` (подробнее — «Особенности сборки»). +- **Типы коллекционных параметров различаются (`Array` / `Set`)** — наследие + генерации: часть методов принимает массивы, часть `Set`. Ориентируйтесь на + сигнатуру метода (подсказки IDE); конвертировать вручную не нужно. + +## Языки + +- **TypeScript** — этот клиент (JavaScript/TypeScript); +- **Python / Go** — в планах (тот же скоуп `@apilki`, отдельные репозитории). + +## Лицензия + +MIT © apilki. Клиент сгенерирован из спецификации Yandex Market Partner API +(https://github.com/yandex-market/yandex-market-partner-api), Copyright (c) 2023 +YANDEX LLC, лицензирована по BSD 3-Clause. Полный текст — в [LICENSE](LICENSE). + +## Ссылки + +- [Официальная документация API Яндекс Маркета для продавцов](https://yandex.ru/dev/market/partner-api/doc/ru/) +- [Спецификация API Яндекс Маркета для продавцов](https://github.com/yandex-market/yandex-market-partner-api) diff --git a/dist/esm/models/Echo200Response.d.ts b/dist/esm/models/Echo200Response.d.ts deleted file mode 100644 index 4b70afd..0000000 --- a/dist/esm/models/Echo200Response.d.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * API Яндекс Маркета для продавцов - * API Яндекс Маркета помогает продавцам автоматизировать и упростить работу с маркетплейсом. В числе возможностей интеграции: * управление каталогом товаров и витриной, * обработка заказов, * изменение настроек магазина, * получение отчетов. - * - * The version of the OpenAPI document: LATEST - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ -/** - * - * @export - * @interface Echo200Response - */ -export interface Echo200Response { - /** - * - * @type {string} - * @memberof Echo200Response - */ - message?: string; -} -/** - * Check if a given object implements the Echo200Response interface. - */ -export declare function instanceOfEcho200Response(value: object): value is Echo200Response; -export declare function Echo200ResponseFromJSON(json: any): Echo200Response; -export declare function Echo200ResponseFromJSONTyped(json: any, ignoreDiscriminator: boolean): Echo200Response; -export declare function Echo200ResponseToJSON(value?: Echo200Response | null): any; diff --git a/dist/esm/models/Echo200Response.js b/dist/esm/models/Echo200Response.js deleted file mode 100644 index f6f6ee8..0000000 --- a/dist/esm/models/Echo200Response.js +++ /dev/null @@ -1,38 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * API Яндекс Маркета для продавцов - * API Яндекс Маркета помогает продавцам автоматизировать и упростить работу с маркетплейсом. В числе возможностей интеграции: * управление каталогом товаров и витриной, * обработка заказов, * изменение настроек магазина, * получение отчетов. - * - * The version of the OpenAPI document: LATEST - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ -/** - * Check if a given object implements the Echo200Response interface. - */ -export function instanceOfEcho200Response(value) { - return true; -} -export function Echo200ResponseFromJSON(json) { - return Echo200ResponseFromJSONTyped(json, false); -} -export function Echo200ResponseFromJSONTyped(json, ignoreDiscriminator) { - if (json == null) { - return json; - } - return { - 'message': json['message'] == null ? undefined : json['message'], - }; -} -export function Echo200ResponseToJSON(value) { - if (value == null) { - return value; - } - return { - 'message': value['message'], - }; -} diff --git a/dist/models/Echo200Response.d.ts b/dist/models/Echo200Response.d.ts deleted file mode 100644 index 4b70afd..0000000 --- a/dist/models/Echo200Response.d.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * API Яндекс Маркета для продавцов - * API Яндекс Маркета помогает продавцам автоматизировать и упростить работу с маркетплейсом. В числе возможностей интеграции: * управление каталогом товаров и витриной, * обработка заказов, * изменение настроек магазина, * получение отчетов. - * - * The version of the OpenAPI document: LATEST - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ -/** - * - * @export - * @interface Echo200Response - */ -export interface Echo200Response { - /** - * - * @type {string} - * @memberof Echo200Response - */ - message?: string; -} -/** - * Check if a given object implements the Echo200Response interface. - */ -export declare function instanceOfEcho200Response(value: object): value is Echo200Response; -export declare function Echo200ResponseFromJSON(json: any): Echo200Response; -export declare function Echo200ResponseFromJSONTyped(json: any, ignoreDiscriminator: boolean): Echo200Response; -export declare function Echo200ResponseToJSON(value?: Echo200Response | null): any; diff --git a/dist/models/Echo200Response.js b/dist/models/Echo200Response.js deleted file mode 100644 index df52db3..0000000 --- a/dist/models/Echo200Response.js +++ /dev/null @@ -1,44 +0,0 @@ -"use strict"; -/* tslint:disable */ -/* eslint-disable */ -/** - * API Яндекс Маркета для продавцов - * API Яндекс Маркета помогает продавцам автоматизировать и упростить работу с маркетплейсом. В числе возможностей интеграции: * управление каталогом товаров и витриной, * обработка заказов, * изменение настроек магазина, * получение отчетов. - * - * The version of the OpenAPI document: LATEST - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.instanceOfEcho200Response = instanceOfEcho200Response; -exports.Echo200ResponseFromJSON = Echo200ResponseFromJSON; -exports.Echo200ResponseFromJSONTyped = Echo200ResponseFromJSONTyped; -exports.Echo200ResponseToJSON = Echo200ResponseToJSON; -/** - * Check if a given object implements the Echo200Response interface. - */ -function instanceOfEcho200Response(value) { - return true; -} -function Echo200ResponseFromJSON(json) { - return Echo200ResponseFromJSONTyped(json, false); -} -function Echo200ResponseFromJSONTyped(json, ignoreDiscriminator) { - if (json == null) { - return json; - } - return { - 'message': json['message'] == null ? undefined : json['message'], - }; -} -function Echo200ResponseToJSON(value) { - if (value == null) { - return value; - } - return { - 'message': value['message'], - }; -} diff --git a/package.json b/package.json index 76aa5ac..75ab45e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apilki/yandex-market", - "version": "0.2.0", + "version": "0.3.0", "description": "JavaScript/TypeScript client for Yandex Market Partner API", "author": "vitya.kuznetsov@gmail.com", "repository": { @@ -13,7 +13,8 @@ "sideEffects": false, "scripts": { "build": "tsc && tsc -p tsconfig.esm.json", - "prepare": "npm run build" + "prepare": "npm run build", + "test": "node --test" }, "devDependencies": { "typescript": "^4.0 || ^5.0" diff --git a/test/api-surface.test.js b/test/api-surface.test.js new file mode 100755 index 0000000..d83e509 --- /dev/null +++ b/test/api-surface.test.js @@ -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`); + } +}); diff --git a/test/client-path.js b/test/client-path.js new file mode 100755 index 0000000..8c2635b --- /dev/null +++ b/test/client-path.js @@ -0,0 +1,16 @@ +// Единый резолвер расположения скомпилированного клиента для юнит-тестов: +// dev-репо: tests/client/*.test.js → ../../output/typescript/dist +// пакет: test/*.test.js → ../dist (dist всегда в пакете) +// Стадия test копирует tests/client → /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) }; diff --git a/test/request-shape.test.js b/test/request-shape.test.js new file mode 100755 index 0000000..f8b7a7d --- /dev/null +++ b/test/request-shape.test.js @@ -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 кампаний'); +}); diff --git a/test/response-error.test.js b/test/response-error.test.js new file mode 100755 index 0000000..9df4fc9 --- /dev/null +++ b/test/response-error.test.js @@ -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); +});