From 2ca2a3d6ba13142418052b0d5a859ce0615bf22c Mon Sep 17 00:00:00 2001 From: Nikolay Tkachenko Date: Fri, 28 Nov 2025 22:30:59 +0200 Subject: [PATCH 1/3] Bump Up APIFW version --- Makefile | 2 +- .../OWASP_CoreRuleSet/docker-compose.yml | 2 +- .../docker-compose-api-mode.yml | 2 +- .../docker-compose-graphql-mode.yml | 2 +- demo/docker-compose/docker-compose.yml | 2 +- .../kubernetes/volumes/helm/api-firewall.yaml | 2 +- docs/configuration-guides/allowlist.md | 2 +- docs/installation-guides/api-mode.md | 2 +- docs/installation-guides/docker-container.md | 4 +- .../graphql/docker-container.md | 4 +- docs/release-notes.md | 4 + helm/api-firewall/Chart.yaml | 2 +- resources/test/docker-compose-api-mode.yml | 2 +- resources/test/k6/30.js | 201 +++++++++++++++++ resources/test/k6/32.js | 107 +++++++++ resources/test/k6/33.js | 187 ++++++++++++++++ resources/test/k6/34.js | 211 ++++++++++++++++++ resources/test/k6/58.js | 154 +++++++++++++ resources/test/k6/59.js | 148 ++++++++++++ ..._account_authorization_validations.test.js | 85 +++++++ 20 files changed, 1111 insertions(+), 14 deletions(-) create mode 100644 resources/test/k6/30.js create mode 100644 resources/test/k6/32.js create mode 100644 resources/test/k6/33.js create mode 100644 resources/test/k6/34.js create mode 100644 resources/test/k6/58.js create mode 100644 resources/test/k6/59.js create mode 100644 resources/test/k6/doe_v2_account_authorization_validations.test.js diff --git a/Makefile b/Makefile index 81b4db0..fcb3378 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -VERSION := 0.9.3 +VERSION := 0.9.4 NAMESPACE := github.com/wallarm/api-firewall .DEFAULT_GOAL := build diff --git a/demo/docker-compose/OWASP_CoreRuleSet/docker-compose.yml b/demo/docker-compose/OWASP_CoreRuleSet/docker-compose.yml index b9f5106..12692c3 100644 --- a/demo/docker-compose/OWASP_CoreRuleSet/docker-compose.yml +++ b/demo/docker-compose/OWASP_CoreRuleSet/docker-compose.yml @@ -2,7 +2,7 @@ version: "3.8" services: api-firewall: container_name: api-firewall - image: wallarm/api-firewall:v0.9.3 + image: wallarm/api-firewall:v0.9.4 restart: on-failure environment: APIFW_URL: "http://0.0.0.0:8080" diff --git a/demo/docker-compose/docker-compose-api-mode.yml b/demo/docker-compose/docker-compose-api-mode.yml index 69416a5..aa2afc2 100644 --- a/demo/docker-compose/docker-compose-api-mode.yml +++ b/demo/docker-compose/docker-compose-api-mode.yml @@ -2,7 +2,7 @@ version: '3.8' services: api-firewall: container_name: api-firewall - image: wallarm/api-firewall:v0.9.3 + image: wallarm/api-firewall:v0.9.4 restart: on-failure environment: APIFW_MODE: "api" diff --git a/demo/docker-compose/docker-compose-graphql-mode.yml b/demo/docker-compose/docker-compose-graphql-mode.yml index a41590b..e71b445 100644 --- a/demo/docker-compose/docker-compose-graphql-mode.yml +++ b/demo/docker-compose/docker-compose-graphql-mode.yml @@ -2,7 +2,7 @@ version: '3.8' services: api-firewall: container_name: api-firewall - image: wallarm/api-firewall:v0.9.3 + image: wallarm/api-firewall:v0.9.4 restart: on-failure environment: APIFW_MODE: "graphql" diff --git a/demo/docker-compose/docker-compose.yml b/demo/docker-compose/docker-compose.yml index 7a197e1..1e72822 100644 --- a/demo/docker-compose/docker-compose.yml +++ b/demo/docker-compose/docker-compose.yml @@ -2,7 +2,7 @@ version: "3.8" services: api-firewall: container_name: api-firewall - image: wallarm/api-firewall:v0.9.3 + image: wallarm/api-firewall:v0.9.4 restart: on-failure environment: APIFW_URL: "http://0.0.0.0:8080" diff --git a/demo/kubernetes/volumes/helm/api-firewall.yaml b/demo/kubernetes/volumes/helm/api-firewall.yaml index 58f922a..3ab1196 100644 --- a/demo/kubernetes/volumes/helm/api-firewall.yaml +++ b/demo/kubernetes/volumes/helm/api-firewall.yaml @@ -10,7 +10,7 @@ manifest: "url": "https://kennethreitz.org", "email": "me@kennethreitz.org" }, - "version": "0.9.3" + "version": "0.9.4" }, "servers": [ { diff --git a/docs/configuration-guides/allowlist.md b/docs/configuration-guides/allowlist.md index b89006c..402a431 100644 --- a/docs/configuration-guides/allowlist.md +++ b/docs/configuration-guides/allowlist.md @@ -33,7 +33,7 @@ docker run --rm -it --network api-firewall-network --network-alias api-firewall -e APIFW_URL= -e APIFW_SERVER_URL= \ -e APIFW_REQUEST_VALIDATION= -e APIFW_RESPONSE_VALIDATION= \ -e APIFW_ALLOW_IP_FILE=/opt/ip-allowlist.txt -e APIFW_ALLOW_IP_HEADER_NAME="X-Real-IP" \ - -p 8088:8088 wallarm/api-firewall:v0.9.3 + -p 8088:8088 wallarm/api-firewall:v0.9.4 ``` | Environment variable | Description | diff --git a/docs/installation-guides/api-mode.md b/docs/installation-guides/api-mode.md index f7a5a0c..bf0b526 100644 --- a/docs/installation-guides/api-mode.md +++ b/docs/installation-guides/api-mode.md @@ -38,7 +38,7 @@ Use the following command to run the API Firewall container: ``` docker run --rm -it -v :/var/lib/wallarm-api/1/wallarm_api.db \ - -e APIFW_MODE=API -p 8282:8282 wallarm/api-firewall:v0.9.3 + -e APIFW_MODE=API -p 8282:8282 wallarm/api-firewall:v0.9.4 ``` You can pass to the container the following variables: diff --git a/docs/installation-guides/docker-container.md b/docs/installation-guides/docker-container.md index d22a6a9..fadc075 100644 --- a/docs/installation-guides/docker-container.md +++ b/docs/installation-guides/docker-container.md @@ -27,7 +27,7 @@ networks: services: api-firewall: container_name: api-firewall - image: wallarm/api-firewall:v0.9.3 + image: wallarm/api-firewall:v0.9.4 restart: on-failure volumes: - : @@ -171,6 +171,6 @@ To start API Firewall on Docker, you can also use regular Docker commands as in -v : -e APIFW_API_SPECS= \ -e APIFW_URL= -e APIFW_SERVER_URL= \ -e APIFW_REQUEST_VALIDATION= -e APIFW_RESPONSE_VALIDATION= \ - -p 8088:8088 wallarm/api-firewall:v0.9.3 + -p 8088:8088 wallarm/api-firewall:v0.9.4 ``` 4. When the environment is started, test it and enable traffic on API Firewall following steps 6 and 7. diff --git a/docs/installation-guides/graphql/docker-container.md b/docs/installation-guides/graphql/docker-container.md index fc2075d..02e353f 100644 --- a/docs/installation-guides/graphql/docker-container.md +++ b/docs/installation-guides/graphql/docker-container.md @@ -29,7 +29,7 @@ networks: services: api-firewall: container_name: api-firewall - image: wallarm/api-firewall:v0.9.3 + image: wallarm/api-firewall:v0.9.4 restart: on-failure volumes: - : @@ -200,6 +200,6 @@ To start API Firewall on Docker, you can also use regular Docker commands as in -e APIFW_GRAPHQL_MAX_QUERY_COMPLEXITY= \ -e APIFW_GRAPHQL_MAX_QUERY_DEPTH= -e APIFW_GRAPHQL_NODE_COUNT_LIMIT= \ -e APIFW_GRAPHQL_INTROSPECTION= \ - -p 8088:8088 wallarm/api-firewall:v0.9.3 + -p 8088:8088 wallarm/api-firewall:v0.9.4 ``` 4. When the environment is started, test it and enable traffic on API Firewall following steps 6 and 7. diff --git a/docs/release-notes.md b/docs/release-notes.md index 7b760d1..f4a5737 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,10 @@ This page describes new releases of Wallarm API Firewall. +## v0.9.4 (2025-11-28) + +* Dependency upgrade + ## v0.9.3 (2025-08-15) * Relaxed `content-type` handling: API Firewall no longer rejects requests with image MIME types (image/png, image/jpeg, image/gif, image/webp, image/avif, image/heic, image/heif, image/bmp, image/tiff, image/svg+xml) diff --git a/helm/api-firewall/Chart.yaml b/helm/api-firewall/Chart.yaml index f6cb13f..0e39a48 100644 --- a/helm/api-firewall/Chart.yaml +++ b/helm/api-firewall/Chart.yaml @@ -1,7 +1,7 @@ apiVersion: v1 name: api-firewall version: 0.7.2 -appVersion: 0.9.3 +appVersion: 0.9.4 description: Wallarm OpenAPI-based API Firewall home: https://github.com/wallarm/api-firewall icon: https://static.wallarm.com/wallarm-logo.svg diff --git a/resources/test/docker-compose-api-mode.yml b/resources/test/docker-compose-api-mode.yml index cbc7c94..35f403e 100644 --- a/resources/test/docker-compose-api-mode.yml +++ b/resources/test/docker-compose-api-mode.yml @@ -2,7 +2,7 @@ version: '3.8' services: api-firewall: container_name: api-firewall - image: wallarm/api-firewall:v0.9.3 + image: wallarm/api-firewall:v0.9.4 build: context: ../../ dockerfile: Dockerfile diff --git a/resources/test/k6/30.js b/resources/test/k6/30.js new file mode 100644 index 0000000..b77b652 --- /dev/null +++ b/resources/test/k6/30.js @@ -0,0 +1,201 @@ +// file: document_management_all_paths_with_negative.test.js +import http from 'k6/http'; +import { check, group } from 'k6'; +import { Trend } from 'k6/metrics'; + +export const options = { + vus: Number(__ENV.VUS || 1), + iterations: Number(__ENV.ITERATIONS || 1), + thresholds: { + http_req_failed: ['rate==0'], + http_req_duration: ['p(95)<1200'], + }, +}; + +const BASE_URL = __ENV.BASE_URL || 'http://localhost:8282'; +const H = { 'X-Wallarm-Schema-ID': '30' }; +const H_JSON = { ...H, 'Content-Type': 'application/json' }; +const H_BAD = { 'X-Wallarm-Schema-ID': '31' }; // неверное значение +const H_JSON_BAD = { ...H_BAD, 'Content-Type': 'application/json' }; // неверное значение + JSON +const EXPECTED = { summary: [{ schema_id: 30, status_code: 200 }] }; + +const opTrend = new Trend('dm_op_duration_ms'); + +function t(endpoint) { return `${BASE_URL}${endpoint}`; } +function j(r) { try { return r.json(); } catch { return null; } } +function eq(a, b) { return JSON.stringify(a) === JSON.stringify(b); } +function uuidv4() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { + const r = (Math.random() * 16) | 0, v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); +} + +// ========= минимальные payload'ы для позитивных запросов ========= +function payloadEmailBatch() { + return { + attachments: [], + bcc: [], + cc: [], + contentType: 'HTML', + distributionType: 'ATTACHMENT', + id: uuidv4(), + name: 'batch', + notifyAnyway: false, + recipientAttachments: [], + status: 'DRAFT', + to: [], + type: 'MARKETING_CAMPAIGN', + }; +} +function payloadFetchPaginatedQueryRequestV2() { + return { dataset: 'documents', fields: [{ id: 'id' }], pagination: { first: 1 } }; +} +function payloadFetchGroupAndAggregateQueryRequestV2() { + return { dataset: 'documents', countTotalVisible: false }; +} +function payloadCreateClientPortalDocumentInput() { + return { archived: false, documentName: 'doc', fileName: 'file.pdf' }; +} +function payloadEditDocumentsInput() { return {}; } +function payloadDownloadDocumentsInput() { return {}; } +function payloadEditClientPortalDocumentInput() { return { documentName: 'doc-updated' }; } +function payloadAddPortalUsersAccessInput() { return {}; } +function payloadRemovePortalUsersAccessInput() { return {}; } +function payloadAddBatchRecipientAttachmentRequestBody() { return {}; } + +// ========= позитивные кейсы по всей спеке ========= +function makePositiveCases() { + const docId = uuidv4(); + const docId2 = uuidv4(); + const batchId = uuidv4(); + const recId = uuidv4(); + + return [ + { m: 'PATCH', p: `/api-internal/document-management/v1/documents`, body: payloadEditDocumentsInput() }, + { m: 'POST', p: `/api-internal/document-management/v1/documents/query`, body: payloadFetchPaginatedQueryRequestV2() }, + { m: 'POST', p: `/api-internal/document-management/v1/documents/aggregate`, body: payloadFetchGroupAndAggregateQueryRequestV2() }, + { m: 'GET', p: `/api-internal/document-management/v1/documents/metadata` }, + { m: 'POST', p: `/api-internal/document-management/v1/documents/download`, body: payloadDownloadDocumentsInput() }, + { m: 'POST', p: `/api-internal/document-management/v1/documents/download/${docId}` }, + { m: 'PATCH', p: `/api-internal/document-management/v1/documents/${docId}`, body: payloadEditDocumentsInput() }, + { m: 'GET', p: `/api-internal/document-management/v1/documents/upload-status?documentIds=${docId}` }, + { m: 'GET', p: `/api-internal/document-management/v1/documents/${docId2}/upload-status` }, + + { m: 'GET', p: `/api-internal/document-management/v1/dataset/documents/metadata` }, + { m: 'POST', p: `/api-internal/document-management/v1/dataset/documents/paginated`, body: payloadFetchPaginatedQueryRequestV2() }, + { m: 'POST', p: `/api-internal/document-management/v1/dataset/documents/group-and-aggregate`, body: payloadFetchGroupAndAggregateQueryRequestV2() }, + + { m: 'POST', p: `/api-internal/document-management/v1/documents/portal`, body: payloadCreateClientPortalDocumentInput() }, + { m: 'POST', p: `/api-internal/document-management/v1/documents/portal/query`, body: payloadFetchPaginatedQueryRequestV2() }, + { m: 'GET', p: `/api-internal/document-management/v1/documents/portal/metadata` }, + { m: 'POST', p: `/api-internal/document-management/v1/documents/portal/aggregate`, body: payloadFetchGroupAndAggregateQueryRequestV2() }, + { m: 'PATCH', p: `/api-internal/document-management/v1/documents/portal/${docId}`, body: payloadEditClientPortalDocumentInput() }, + + { m: 'POST', p: `/api-internal/document-management/v1/documents/portal-user-access`, body: payloadAddPortalUsersAccessInput() }, + { m: 'POST', p: `/api-internal/document-management/v1/documents/portal-user-access/remove`, body: payloadRemovePortalUsersAccessInput() }, + { m: 'POST', p: `/api-internal/document-management/v1/documents/portal-user-access/notification`, body: payloadAddPortalUsersAccessInput() }, + + { m: 'POST', p: `/api-internal/document-management/v1/email-batches`, body: payloadEmailBatch() }, + { m: 'GET', p: `/api-internal/document-management/v1/email-batches/metadata` }, + { m: 'POST', p: `/api-internal/document-management/v1/email-batches/query`, body: payloadFetchPaginatedQueryRequestV2() }, + { m: 'POST', p: `/api-internal/document-management/v1/email-batches/aggregate`, body: payloadFetchGroupAndAggregateQueryRequestV2() }, + { m: 'GET', p: `/api-internal/document-management/v1/email-batches/${batchId}` }, + { m: 'POST', p: `/api-internal/document-management/v1/email-batches/${batchId}`, body: payloadEmailBatch() }, + { m: 'POST', p: `/api-internal/document-management/v1/email-batches/${batchId}/send`, body: payloadEmailBatch() }, + { m: 'GET', p: `/api-internal/document-management/v1/email-batches/${batchId}/preview` }, + { m: 'POST', p: `/api-internal/document-management/v1/email-batches/${batchId}/document-recipients`, body: payloadAddBatchRecipientAttachmentRequestBody() }, + { m: 'DELETE',p: `/api-internal/document-management/v1/email-batches/${batchId}/document-recipients` }, + { m: 'DELETE',p: `/api-internal/document-management/v1/email-batches/${batchId}/document-recipients/${recId}` }, + + { m: 'POST', p: `/api-internal/document-management/v1/email-recipients/query`, body: payloadFetchPaginatedQueryRequestV2() }, + { m: 'POST', p: `/api-internal/document-management/v1/email-recipients/aggregate`, body: payloadFetchGroupAndAggregateQueryRequestV2() }, + { m: 'GET', p: `/api-internal/document-management/v1/email-recipients/metadata` }, + + { m: 'POST', p: `/api-internal/document-management/v1/document-recipients/query`, body: payloadFetchPaginatedQueryRequestV2() }, + { m: 'POST', p: `/api-internal/document-management/v1/document-recipients/aggregate`, body: payloadFetchGroupAndAggregateQueryRequestV2() }, + { m: 'GET', p: `/api-internal/document-management/v1/document-recipients/metadata` }, + { m: 'POST', p: `/api-internal/document-management/v1/document-recipients/paginated`, body: payloadFetchPaginatedQueryRequestV2() }, + + { m: 'GET', p: `/api-internal/document-management/v1/dataset/document-recipients/metadata` }, + { m: 'POST', p: `/api-internal/document-management/v1/dataset/document-recipients/paginated`, body: payloadFetchPaginatedQueryRequestV2() }, + { m: 'POST', p: `/api-internal/document-management/v1/dataset/document-recipients/group-and-aggregate`, body: payloadFetchGroupAndAggregateQueryRequestV2() }, + ]; +} + +// ========= негативные кейсы ========= +// - отсутствие заголовка X-Wallarm-Schema-ID +// - неверный X-Wallarm-Schema-ID +// - пустые/некорректные тела для эндпоинтов с обязательным body +// - невалидные UUID в path +// - отсутствие обязательных query параметров +function makeNegativeCases() { + const badUUID = 'not-a-uuid'; + + return [ + // Нет заголовка X-Wallarm-Schema-ID + { title: 'missing header', m: 'GET', p: `/api-internal/document-management/v1/documents/metadata`, headers: {} }, + { title: 'missing header', m: 'POST', p: `/api-internal/document-management/v1/documents/query`, headers: { 'Content-Type': 'application/json' }, body: {} }, + + // Неверный заголовок X-Wallarm-Schema-ID + { title: 'wrong schema id', m: 'GET', p: `/api-internal/document-management/v1/email-batches/metadata`, headers: H_BAD }, + { title: 'wrong schema id', m: 'POST', p: `/api-internal/document-management/v1/email-batches`, headers: H_JSON_BAD, body: {} }, + + // Пустое тело там, где нужен обязательный body + { title: 'empty body', m: 'POST', p: `/api-internal/document-management/v1/dataset/documents/paginated`, headers: H_JSON, body: {} }, + { title: 'empty body', m: 'POST', p: `/api-internal/document-management/v1/documents/aggregate`, headers: H_JSON, body: {} }, + { title: 'empty body', m: 'POST', p: `/api-internal/document-management/v1/documents/portal`, headers: H_JSON, body: {} }, + + // Невалидный UUID в path + { title: 'invalid uuid', m: 'GET', p: `/api-internal/document-management/v1/email-batches/${badUUID}`, headers: H }, + { title: 'invalid uuid', m: 'PATCH',p: `/api-internal/document-management/v1/documents/${badUUID}`, headers: H_JSON, body: {} }, + { title: 'invalid uuid', m: 'GET', p: `/api-internal/document-management/v1/documents/${badUUID}/upload-status`, headers: H }, + + // Отсутствует обязательный query + { title: 'missing query', m: 'GET', p: `/api-internal/document-management/v1/documents/upload-status`, headers: H }, + + // Неверный метод (например, DELETE без id where path requires id) + { title: 'wrong method shape', m: 'DELETE', p: `/api-internal/document-management/v1/email-batches/document-recipients`, headers: H }, // нет batchId в path + ]; +} + +function doRequest(method, url, headers, body) { + if (method === 'GET') return http.get(url, { headers }); + if (method === 'DELETE')return http.del(url, null, { headers }); + if (method === 'POST') return http.post(url, body ? JSON.stringify(body) : '', { headers }); + if (method === 'PATCH') return http.patch(url, body ? JSON.stringify(body) : '', { headers }); + throw new Error(`Unsupported method ${method}`); +} + +export default function () { + const positives = makePositiveCases(); + + group('Document Management API – positive cases (expect 200 + stub body)', () => { + for (const c of positives) { + const url = t(c.p); + const headers = (c.m === 'GET' || c.m === 'DELETE') ? H : H_JSON; + const res = doRequest(c.m, url, headers, c.body); + + opTrend.add(res.timings.duration, { path: c.p, method: c.m, kind: 'positive' }); + + const body = j(res); + check(res, { [`${c.m} ${c.p} -> status 200`]: (r) => r.status === 200 }); + check(body, { [`${c.m} ${c.p} -> expected stub`]: (b) => eq(b, EXPECTED) }); + } + }); + + const negatives = makeNegativeCases(); + + group('Document Management API – negative cases (expect non-200 and body != stub)', () => { + for (const n of negatives) { + const url = t(n.p); + const res = doRequest(n.m, url, n.headers ?? H, n.body); + + opTrend.add(res.timings.duration, { path: n.p, method: n.m, kind: 'negative', title: n.title }); + + const body = j(res); + check(res, { [`NEG ${n.m} ${n.p} (${n.title}) -> status != 200`]: (r) => r.status !== 200 }); + check(body, { [`NEG ${n.m} ${n.p} (${n.title}) -> body != stub`]: (b) => !eq(b, EXPECTED) }); + } + }); +} \ No newline at end of file diff --git a/resources/test/k6/32.js b/resources/test/k6/32.js new file mode 100644 index 0000000..fcb0b37 --- /dev/null +++ b/resources/test/k6/32.js @@ -0,0 +1,107 @@ +// file: query_engine_all_paths_schema32.test.js +import http from 'k6/http'; +import { check, group } from 'k6'; +import { Trend } from 'k6/metrics'; + +export const options = { + vus: Number(__ENV.VUS || 1), + iterations: Number(__ENV.ITERATIONS || 1), + thresholds: { + http_req_failed: ['rate==0'], + http_req_duration: ['p(95)<1000'], + }, +}; + +// В спеке servers: [] — задаём вручную или через env +const BASE_URL = __ENV.BASE_URL || 'http://localhost:8282'; +const DATASET_ID = __ENV.DATASET_ID || 'test-dataset'; + +// Общие заголовки +const H = { 'X-Wallarm-Schema-ID': '32' }; +const H_JSON = { ...H, 'Content-Type': 'application/json' }; + +// Ожидаемый стаб-ответ +const EXPECTED = { summary: [{ schema_id: 32, status_code: 200 }] }; + +// Helpers +function j(r) { try { return r.json(); } catch { return null; } } +function eq(a, b) { return JSON.stringify(a) === JSON.stringify(b); } +function u(p) { return `${BASE_URL}${p}`; } + +const op = new Trend('qe_op_ms'); + +export default function () { + group('Query Engine API – all endpoints return stub (schema_id=32)', () => { + // 1) GET /api/v1/query-engine/datasets + { + const res = http.get(u(`/api/v1/query-engine/datasets`), { headers: H }); + op.add(res.timings.duration, { path: '/datasets', method: 'GET' }); + const body = j(res); + check(res, { 'GET /datasets -> 200': (r) => r.status === 200 }); + check(body, { 'GET /datasets -> expected body': (b) => eq(b, EXPECTED) }); + console.log('Response body:', body); + } + + // 2) GET /api/v1/query-engine/datasets/{id} + { + const res = http.get(u(`/api/v1/query-engine/datasets/${encodeURIComponent(DATASET_ID)}`), { headers: H }); + op.add(res.timings.duration, { path: '/datasets/{id}', method: 'GET' }); + const body = j(res); + check(res, { 'GET /datasets/{id} -> 200': (r) => r.status === 200 }); + check(body, { 'GET /datasets/{id} -> expected body': (b) => eq(b, EXPECTED) }); + console.log('Response body:', body); + } + + // 3) POST /api/v1/query-engine/datasets/{id}/query (без тела) + { + const res = http.post(u(`/api/v1/query-engine/datasets/${encodeURIComponent(DATASET_ID)}/query`), '', { headers: H_JSON }); + op.add(res.timings.duration, { path: '/datasets/{id}/query', method: 'POST' }); + const body = j(res); + check(res, { 'POST /datasets/{id}/query -> 200': (r) => r.status === 200 }); + check(body, { 'POST /datasets/{id}/query -> expected body': (b) => eq(b, EXPECTED) }); + console.log('Response body:', body); + } + + // 4) POST /api/v1/query-engine/datasets/{id}/query с query-параметрами + { + const params = { headers: H_JSON }; + const url = u(`/api/v1/query-engine/datasets/${encodeURIComponent(DATASET_ID)}/query?limit=1&cursor=abc&direction=NEXT`); + const res = http.post(url, '', params); + op.add(res.timings.duration, { path: '/datasets/{id}/query?params', method: 'POST' }); + const body = j(res); + check(res, { 'POST /datasets/{id}/query?params -> 200': (r) => r.status === 200 }); + check(body, { 'POST /datasets/{id}/query?params -> expected body': (b) => eq(b, EXPECTED) }); + console.log('Response body:', body); + } + + // 5) POST /api/v1/query-engine/datasets/{id}/export (без тела) + { + const res = http.post(u(`/api/v1/query-engine/datasets/${encodeURIComponent(DATASET_ID)}/export`), '', { headers: H_JSON }); + op.add(res.timings.duration, { path: '/datasets/{id}/export', method: 'POST' }); + const body = j(res); + check(res, { 'POST /datasets/{id}/export -> 200': (r) => r.status === 200 }); + check(body, { 'POST /datasets/{id}/export -> expected body': (b) => eq(b, EXPECTED) }); + console.log('Response body:', body); + } + + // 6) POST /api/v1/query-engine/datasets/{id}/aggregate (без тела) + { + const res = http.post(u(`/api/v1/query-engine/datasets/${encodeURIComponent(DATASET_ID)}/aggregate`), '', { headers: H_JSON }); + op.add(res.timings.duration, { path: '/datasets/{id}/aggregate', method: 'POST' }); + const body = j(res); + check(res, { 'POST /datasets/{id}/aggregate -> 200': (r) => r.status === 200 }); + check(body, { 'POST /datasets/{id}/aggregate -> expected body': (b) => eq(b, EXPECTED) }); + console.log('Response body:', body); + } + + // 7) POST /api/v1/query-engine/datasets/{id}/query/count (без тела) + { + const res = http.post(u(`/api/v1/query-engine/datasets/${encodeURIComponent(DATASET_ID)}/query/count`), '', { headers: H_JSON }); + op.add(res.timings.duration, { path: '/datasets/{id}/query/count', method: 'POST' }); + const body = j(res); + check(res, { 'POST /datasets/{id}/query/count -> 200': (r) => r.status === 200 }); + check(body, { 'POST /datasets/{id}/query/count -> expected body': (b) => eq(b, EXPECTED) }); + console.log('Response body:', body); + } + }); +} \ No newline at end of file diff --git a/resources/test/k6/33.js b/resources/test/k6/33.js new file mode 100644 index 0000000..8303274 --- /dev/null +++ b/resources/test/k6/33.js @@ -0,0 +1,187 @@ +// file: recon_v3_all_paths_schema33_with_negative.test.js +import http from 'k6/http'; +import { check, group } from 'k6'; +import { Trend } from 'k6/metrics'; + +export const options = { + vus: Number(__ENV.VUS || 1), + iterations: Number(__ENV.ITERATIONS || 1), + thresholds: { + http_req_failed: ['rate==0'], + http_req_duration: ['p(95)<1200'], + }, +}; + +const BASE_URL = __ENV.BASE_URL || 'http://localhost:8282'; + +// --- Headers +const H = { 'X-Wallarm-Schema-ID': '33' }; +const H_JSON = { ...H, 'Content-Type': 'application/json' }; +const H_WRONG = { 'X-Wallarm-Schema-ID': '33' }; +const H_JSON_WRONG = { ...H_WRONG, 'Content-Type': 'application/json' }; + +// --- Expected stub +const EXPECTED = { summary: [{ schema_id: 33, status_code: 200 }] }; + +// --- Helpers +function j(r) { try { return r.json(); } catch { return null; } } +function eq(a, b) { return JSON.stringify(a) === JSON.stringify(b); } +function u(p) { return `${BASE_URL}${p}`; } +function uuidv4() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { + const r = (Math.random() * 16) | 0, v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); +} +function doRequest(method, url, headers, body) { + if (method === 'GET') return http.get(url, { headers }); + if (method === 'DELETE') return http.del(url, null, { headers }); + if (method === 'POST') return http.post(url, body ? JSON.stringify(body) : '', { headers }); + if (method === 'PATCH') return http.patch(url, body ? JSON.stringify(body) : '', { headers }); + throw new Error(`Unsupported method ${method}`); +} + +const opTrend = new Trend('recon_op_ms'); + +// --- Minimal valid payloads (по required полям схем) +function minimalCustodianSetting() { + return { + customName: 'rs-min', + reconcileDate: 'TODAY', + updatePriceAtStartOfDay: false, + excludeInternalAssetTypes: [], + excludeInternalInstrumentTypes: [], + excludeInternalTransactionTypes: [], + positionReconcileDateType: 'TRADE_DATE', + positionReconcileFaceType: 'CURRENT_QUANTITY', + positionReconcileValueType: 'BOOK_VALUES', + transactionReconcileDateType: 'TRADE_DATE', + matchingThresholdsOverride: { value: {} }, + matchingThresholdsSettings: { value: {} }, + positionAutoReconcileOverride: { value: {} }, + positionAutoReconcileSettings: { value: {} }, + transactionAutoReconcileOverride: { value: {} }, + transactionAutoReconcileSettings: { value: {} }, + transactionAutoAcceptCustodianOverride: { value: {} }, + transactionAutoAcceptCustodianSettings: { value: {} }, + positionReconcileDateTypeOverrideInstrumentTypes: [], + }; +} +const payloadRuleSetInput = () => ({ ruleSet: minimalCustodianSetting() }); +const payloadUpdateGlobalRule = () => ({ globalRule: { reconcileDate: 'TODAY', updatePriceAtStartOfDay: false } }); +const payloadBulkFilters = () => ({ reconDate: '2025-01-01' }); +const payloadBulkPositions = () => ({ action: 'GET_COUNT', filters: payloadBulkFilters() }); +const payloadBulkMatched = () => ({ action: 'GET_COUNT', filters: payloadBulkFilters() }); +const payloadBulkUnmatched = () => ({ action: 'GET_COUNT', filters: payloadBulkFilters() }); +const payloadGetNextTrigger = () => ({ /* currentTrigger опционален */ }); +const payloadTxnIds = () => ({ ids: [uuidv4()] }); +const payloadRemoveAssignments = () => ({ custodianIds: ['c1'] }); +const payloadReplaceAssignments = () => ({ ruleSetId: uuidv4(), custodianIds: ['c1'] }); + +// --- Positive cases +function makePositiveCases() { + const rsId = uuidv4(); + const qInput = encodeURIComponent(JSON.stringify({})); // required query object + + return [ + { m: 'GET', p: `/api-internal/reconciliation/v1/reconciliation-rules` }, + { m: 'GET', p: `/api-internal/reconciliation/v1/reconciliation-rules/global-rule` }, + { m: 'PATCH', p: `/api-internal/reconciliation/v1/reconciliation-rules/global-rule`, body: payloadUpdateGlobalRule() }, + + { m: 'POST', p: `/api-internal/reconciliation/v1/reconciliation-rules/rule-sets`, body: payloadRuleSetInput() }, + { m: 'GET', p: `/api-internal/reconciliation/v1/reconciliation-rules/rule-sets/${rsId}` }, + { m: 'PATCH', p: `/api-internal/reconciliation/v1/reconciliation-rules/rule-sets/${rsId}`, body: payloadRuleSetInput() }, + { m: 'DELETE',p: `/api-internal/reconciliation/v1/reconciliation-rules/rule-sets/${rsId}` }, + + { m: 'GET', p: `/api-internal/reconciliation/v1/reconciliation-rules/rule-sets/assignments` }, + { m: 'GET', p: `/api-internal/reconciliation/v1/reconciliation-rules/rule-sets/${rsId}/assignments` }, + + { m: 'POST', p: `/api-internal/reconciliation/v1/reconciliation-rules/rule-sets/remove-assignments`, body: payloadRemoveAssignments() }, + { m: 'POST', p: `/api-internal/reconciliation/v1/reconciliation-rules/rule-sets/replace-assignments`, body: payloadReplaceAssignments() }, + + { m: 'POST', p: `/api-internal/reconciliation/v1/ai/get-next-agent-trigger`, body: payloadGetNextTrigger() }, + + { m: 'POST', p: `/api-internal/reconciliation/v1/reconciliation-positions/actions/bulk`, body: payloadBulkPositions() }, + { m: 'POST', p: `/api-internal/reconciliation/v1/reconciliation-transactions/actions/bulk-matched`, body: payloadBulkMatched() }, + { m: 'POST', p: `/api-internal/reconciliation/v1/reconciliation-transactions/actions/bulk-unmatched`, body: payloadBulkUnmatched() }, + + { m: 'GET', p: `/api-internal/reconciliation/v1/reconciliation-positions/object-labels?input=${qInput}` }, + + { m: 'POST', p: `/api-internal/reconciliation/v1/reconciliation-positions/transactions/query`, body: payloadTxnIds() }, + ]; +} + +// --- Negative cases +function makeNegativeCases() { + const badUUID = 'not-a-uuid'; + const rsId = uuidv4(); // будем использовать валидный для некоторых негативов + + return [ + // 1) Отсутствует заголовок X-Wallarm-Schema-ID + { title: 'missing schema header', m: 'GET', p: `/api-internal/reconciliation/v1/reconciliation-rules`, headers: {} }, + { title: 'missing schema header', m: 'POST', p: `/api-internal/reconciliation/v1/reconciliation-rules/rule-sets`, headers: { 'Content-Type': 'application/json' }, body: payloadRuleSetInput() }, + + // 2) Неверный X-Wallarm-Schema-ID + { title: 'wrong schema header', m: 'GET', p: `/api-internal/reconciliation/v1/reconciliation-rules/global-rule`, headers: H_WRONG }, + { title: 'wrong schema header', m: 'PATCH',p: `/api-internal/reconciliation/v1/reconciliation-rules/global-rule`, headers: H_JSON_WRONG, body: payloadUpdateGlobalRule() }, + + // 3) Невалидный UUID в path + { title: 'invalid uuid in path', m: 'GET', p: `/api-internal/reconciliation/v1/reconciliation-rules/rule-sets/${badUUID}`, headers: H }, + { title: 'invalid uuid in path', m: 'PATCH', p: `/api-internal/reconciliation/v1/reconciliation-rules/rule-sets/${badUUID}`, headers: H_JSON, body: payloadRuleSetInput() }, + { title: 'invalid uuid in path', m: 'DELETE',p: `/api-internal/reconciliation/v1/reconciliation-rules/rule-sets/${badUUID}`, headers: H }, + { title: 'invalid uuid in path', m: 'GET', p: `/api-internal/reconciliation/v1/reconciliation-rules/rule-sets/${badUUID}/assignments`, headers: H }, + + // 4) Пустой/битый body там, где он обязателен + { title: 'empty body', m: 'POST', p: `/api-internal/reconciliation/v1/reconciliation-rules/rule-sets`, headers: H_JSON, body: {} }, + { title: 'empty body', m: 'PATCH', p: `/api-internal/reconciliation/v1/reconciliation-rules/global-rule`, headers: H_JSON, body: {} }, + { title: 'empty body', m: 'PATCH', p: `/api-internal/reconciliation/v1/reconciliation-rules/rule-sets/${rsId}`, headers: H_JSON, body: {} }, + { title: 'empty body', m: 'POST', p: `/api-internal/reconciliation/v1/ai/get-next-agent-trigger`, headers: H_JSON, body: {} }, + { title: 'empty body', m: 'POST', p: `/api-internal/reconciliation/v1/reconciliation-positions/actions/bulk`, headers: H_JSON, body: {} }, + { title: 'empty body', m: 'POST', p: `/api-internal/reconciliation/v1/reconciliation-transactions/actions/bulk-matched`, headers: H_JSON, body: {} }, + { title: 'empty body', m: 'POST', p: `/api-internal/reconciliation/v1/reconciliation-transactions/actions/bulk-unmatched`, headers: H_JSON, body: {} }, + { title: 'empty body', m: 'POST', p: `/api-internal/reconciliation/v1/reconciliation-rules/rule-sets/remove-assignments`, headers: H_JSON, body: {} }, + { title: 'empty body', m: 'POST', p: `/api-internal/reconciliation/v1/reconciliation-rules/rule-sets/replace-assignments`, headers: H_JSON, body: {} }, + { title: 'empty body', m: 'POST', p: `/api-internal/reconciliation/v1/reconciliation-positions/transactions/query`, headers: H_JSON, body: {} }, + + // 5) Плохой query param: input обязателен, отсутствует + { title: 'missing required query param', m: 'GET', p: `/api-internal/reconciliation/v1/reconciliation-positions/object-labels`, headers: H }, + + // 6) Невалидные поля в body: невалидный UUID в ids + { title: 'invalid uuid in body ids', m: 'POST', p: `/api-internal/reconciliation/v1/reconciliation-positions/transactions/query`, headers: H_JSON, body: { ids: ['not-a-uuid'] } }, + + // 7) Отсутствующее required поле в bulk-параметрах (нет filters) + { title: 'bulk missing filters', m: 'POST', p: `/api-internal/reconciliation/v1/reconciliation-positions/actions/bulk`, headers: H_JSON, body: { action: 'GET_COUNT' } }, + { title: 'bulk missing action', m: 'POST', p: `/api-internal/reconciliation/v1/reconciliation-transactions/actions/bulk-matched`, headers: H_JSON, body: { filters: payloadBulkFilters() } }, + ]; +} + +export default function () { + // --- Positive run + group('Reconciliation API – positive cases (expect 200 + stub)', () => { + for (const c of makePositiveCases()) { + const url = u(c.p); + const headers = (c.m === 'GET' || c.m === 'DELETE') ? H : H_JSON; + const res = doRequest(c.m, url, headers, c.body); + opTrend.add(res.timings.duration, { path: c.p, method: c.m, kind: 'positive' }); + + const body = j(res); + check(res, { [`${c.m} ${c.p} -> 200`]: (r) => r.status === 200 }); + check(body, { [`${c.m} ${c.p} -> expected body`]: (b) => eq(b, EXPECTED) }); + console.log('Response body:', body); + } + }); + + // --- Negative run + group('Reconciliation API – negative cases (expect non-200 and/or body != stub)', () => { + for (const n of makeNegativeCases()) { + const url = u(n.p); + const res = doRequest(n.m, url, n.headers ?? H, n.body); + opTrend.add(res.timings.duration, { path: n.p, method: n.m, kind: 'negative', title: n.title }); + + const body = j(res); + check(res, { [`NEG ${n.m} ${n.p} (${n.title}) -> status != 200`]: (r) => r.status !== 200 }); + check(body, { [`NEG ${n.m} ${n.p} (${n.title}) -> body != stub`]: (b) => !eq(b, EXPECTED) }); + console.log('Response body:', body); + } + }); +} \ No newline at end of file diff --git a/resources/test/k6/34.js b/resources/test/k6/34.js new file mode 100644 index 0000000..8cfb31a --- /dev/null +++ b/resources/test/k6/34.js @@ -0,0 +1,211 @@ +// file: kotlin_service_template_schema34.test.js +import http from 'k6/http'; +import { check, group } from 'k6'; +import { Trend } from 'k6/metrics'; + +export const options = { + vus: Number(__ENV.VUS || 1), + iterations: Number(__ENV.ITERATIONS || 1), + thresholds: { + http_req_failed: ['rate==0'], + http_req_duration: ['p(95)<1200'], + }, +}; + +const BASE_URL = __ENV.BASE_URL || 'http://localhost:8282'; + +const REQUIRED_HEADERS = { + 'X-Wallarm-Schema-ID': '34', + 'rl-tenant-id': __ENV.RL_TENANT_ID || 'tenant-1', + 'rl-user-id': __ENV.RL_USER_ID || 'user-1', +}; +const H = { ...REQUIRED_HEADERS }; +const H_JSON = { ...H, 'Content-Type': 'application/json' }; + +const WRONG_SCHEMA_H = { ...REQUIRED_HEADERS, 'X-Wallarm-Schema-ID': '34' }; + +const EXPECTED = { summary: [{ schema_id: 34, status_code: 200 }] }; + +function j(r) { try { return r.json(); } catch { return null; } } +function eq(a, b) { return JSON.stringify(a) === JSON.stringify(b); } +function u(p) { return `${BASE_URL}${p}`; } +function uuid() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { + const r = (Math.random()*16)|0, v = c === 'x' ? r : (r&0x3|0x8); + return v.toString(16); + }); +} + +// ---- минимальные полезные данные ---- +function createWidgetBody() { + return { name: 'Widget A', type: 'BASIC', description: 'Example widget' }; +} +function updateWidgetBody() { + return { description: 'Updated description' }; +} + +const op = new Trend('kst_op_ms'); + +// ---- Позитивные тесты ---- +function positiveTests() { + group('POSITIVE /api-internal/v1/widgets (GET list)', () => { + const url = u('/api-internal/v1/widgets?limit=2&direction=STARTING_AFTER&ids=a&ids=b&types=BASIC'); + const res = http.get(url, { headers: H }); + op.add(res.timings.duration, { path: '/widgets', method: 'GET' }); + const body = j(res); + // допускаем 200; если есть тело — сверяем стаб + check(res, { 'GET /widgets -> 200': r => r.status === 200 }); + if (res.body && res.body.length) { + check(body, { 'GET /widgets -> expected stub': b => eq(b, EXPECTED) }); + console.log('Response body:', body); + } + }); + + group('POSITIVE /api-internal/v1/widgets (POST create)', () => { + const res = http.post(u('/api-internal/v1/widgets'), JSON.stringify(createWidgetBody()), { headers: H_JSON }); + op.add(res.timings.duration, { path: '/widgets', method: 'POST' }); + const body = j(res); + // по спеке 201; некоторые стабы могут вернуть 200 — примем оба + check(res, { 'POST /widgets -> 201 or 200': r => r.status === 201 || r.status === 200 }); + if (res.status === 200 || res.headers['Content-Type']?.includes('application/json')) { + check(body, { 'POST /widgets -> expected stub (if JSON body)': b => !res.body || eq(b, EXPECTED) }); + console.log('Response body:', body); + } + }); + + group('POSITIVE /api-internal/v1/widgets/{id} (GET item)', () => { + const id = uuid(); // произвольный + const res = http.get(u(`/api-internal/v1/widgets/${encodeURIComponent(id)}`), { headers: H }); + op.add(res.timings.duration, { path: '/widgets/{id}', method: 'GET' }); + const body = j(res); + check(res, { 'GET /widgets/{id} -> 200 or 404': r => r.status === 200 || r.status === 404 }); + if (res.status === 200) { + check(body, { 'GET /widgets/{id} -> expected stub': b => eq(b, EXPECTED) }); + console.log('Response body:', body); + } + }); + + group('POSITIVE /api-internal/v1/widgets/{id} (PATCH update)', () => { + const id = uuid(); + const res = http.patch(u(`/api-internal/v1/widgets/${encodeURIComponent(id)}`), JSON.stringify(updateWidgetBody()), { headers: H_JSON }); + op.add(res.timings.duration, { path: '/widgets/{id}', method: 'PATCH' }); + const body = j(res); + check(res, { 'PATCH /widgets/{id} -> 200 or 404': r => r.status === 200 || r.status === 404 }); + if (res.status === 200) { + check(body, { 'PATCH /widgets/{id} -> expected stub': b => eq(b, EXPECTED) }); + console.log('Response body:', body); + } + }); + + group('POSITIVE /api-internal/v1/widgets/{id} (DELETE)', () => { + const id = uuid(); + const res = http.del(u(`/api-internal/v1/widgets/${encodeURIComponent(id)}`), null, { headers: H }); + op.add(res.timings.duration, { path: '/widgets/{id}', method: 'DELETE' }); + // по спеке 204; стабы иногда возвращают 200 — примем оба + check(res, { 'DELETE /widgets/{id} -> 204 or 200 or 404': r => [204,200,404].includes(r.status) }); + if (res.status === 200 && res.body) { + check(j(res), { 'DELETE /widgets/{id} -> expected stub if body': b => eq(b, EXPECTED) }); + console.log('Response body:', res.body); + } + }); +} + +// ---- Негативные тесты ---- +function negativeTests() { + // 1) отсутствуют обязательные заголовки + group('NEG missing required headers', () => { + const url = u('/api-internal/v1/widgets'); + const res = http.get(url, { headers: { 'X-Wallarm-Schema-ID': '34' } }); // нет rl-tenant-id / rl-user-id + op.add(res.timings.duration, { path: '/widgets', method: 'GET', neg: 'missing headers' }); + check(res, { 'GET /widgets without rl headers -> non-2xx': r => r.status < 200 || r.status >= 300 }); + check(j(res), { 'body != stub': b => !eq(b, EXPECTED) }); + console.log('Response body:', res.body); + }); + + // 2) неверный X-Wallarm-Schema-ID + group('NEG wrong X-Wallarm-Schema-ID', () => { + const res = http.get(u('/api-internal/v1/widgets'), { headers: WRONG_SCHEMA_H }); + op.add(res.timings.duration, { path: '/widgets', method: 'GET', neg: 'wrong schema id' }); + check(res, { 'GET /widgets wrong schema -> non-2xx': r => r.status < 200 || r.status >= 300 }); + check(j(res), { 'body != stub': b => !eq(b, EXPECTED) }); + console.log('Response body:', res.body); + }); + + // 3) невалидные query-параметры (limit < minimum, direction неверный) + group('NEG invalid query params', () => { + const res1 = http.get(u('/api-internal/v1/widgets?limit=0'), { headers: H }); // minimum: 1 + op.add(res1.timings.duration, { path: '/widgets', method: 'GET', neg: 'limit=0' }); + check(res1, { 'GET /widgets limit=0 -> 400 (or non-2xx)': r => r.status === 400 || r.status < 200 || r.status >= 300 }); + check(j(res1), { 'body != stub': b => !eq(b, EXPECTED) }); + console.log('Response body:', res1.body); + + const res2 = http.get(u('/api-internal/v1/widgets?direction=SIDEWAYS'), { headers: H }); + op.add(res2.timings.duration, { path: '/widgets', method: 'GET', neg: 'bad direction' }); + check(res2, { 'GET /widgets direction invalid -> 400 (or non-2xx)': r => r.status === 400 || r.status < 200 || r.status >= 300 }); + check(j(res2), { 'body != stub': b => !eq(b, EXPECTED) }); + console.log('Response body:', res2.body); + }); + + // 4) create: отсутствует обязательное поле + group('NEG POST /widgets missing required body fields', () => { + const bad = { name: 'W', description: 'no type' }; // отсутствует type + const res = http.post(u('/api-internal/v1/widgets'), JSON.stringify(bad), { headers: H_JSON }); + op.add(res.timings.duration, { path: '/widgets', method: 'POST', neg: 'missing type' }); + check(res, { 'POST /widgets missing type -> 400 (or non-2xx)': r => r.status === 400 || r.status < 200 || r.status >= 300 }); + check(j(res), { 'body != stub': b => !eq(b, EXPECTED) }); + console.log('Response body:', res.body); + }); + + // 5) update: пустое тело + group('NEG PATCH /widgets/{id} empty body', () => { + const id = uuid(); + const res = http.patch(u(`/api-internal/v1/widgets/${id}`), '', { headers: H_JSON }); + op.add(res.timings.duration, { path: '/widgets/{id}', method: 'PATCH', neg: 'empty body' }); + check(res, { 'PATCH /widgets/{id} empty -> 400/404/non-2xx': r => [400,404].includes(r.status) || r.status < 200 || r.status >= 300 }); + check(j(res), { 'body != stub': b => !eq(b, EXPECTED) }); + console.log('Response body:', res.body); + }); + + // 6) get/delete несуществующего id (ожидаем 404) + group('NEG GET/DELETE nonexistent id -> 404', () => { + const id = 'does-not-exist'; + const resG = http.get(u(`/api-internal/v1/widgets/${encodeURIComponent(id)}`), { headers: H }); + op.add(resG.timings.duration, { path: '/widgets/{id}', method: 'GET', neg: 'nonexistent' }); + check(resG, { 'GET /widgets/{id} -> 404 or non-2xx': r => r.status === 404 || r.status < 200 || r.status >= 300 }); + check(j(resG), { 'body != stub': b => !eq(b, EXPECTED) }); + console.log('Response body:', resG.body); + + const resD = http.del(u(`/api-internal/v1/widgets/${encodeURIComponent(id)}`), null, { headers: H }); + op.add(resD.timings.duration, { path: '/widgets/{id}', method: 'DELETE', neg: 'nonexistent' }); + check(resD, { 'DELETE /widgets/{id} -> 404 or non-2xx': r => r.status === 404 || r.status < 200 || r.status >= 300 }); + if (resD.body) { + check(j(resD), { 'body != stub': b => !eq(b, EXPECTED) }); + console.log('Response body:', resD.body); + } + }); + + // 7) пропущенные обязательные заголовки на mutate-методах + group('NEG missing rl headers on POST/PATCH/DELETE', () => { + const id = uuid(); + + const resP = http.post(u('/api-internal/v1/widgets'), JSON.stringify(createWidgetBody()), { headers: { 'X-Wallarm-Schema-ID': '34', 'Content-Type': 'application/json' } }); + op.add(resP.timings.duration, { path: '/widgets', method: 'POST', neg: 'no rl headers' }); + check(resP, { 'POST /widgets without rl headers -> non-2xx': r => r.status < 200 || r.status >= 300 }); + console.log('Response body:', resP.body); + + const resU = http.patch(u(`/api-internal/v1/widgets/${id}`), JSON.stringify(updateWidgetBody()), { headers: { 'X-Wallarm-Schema-ID': '34', 'Content-Type': 'application/json' } }); + op.add(resU.timings.duration, { path: '/widgets/{id}', method: 'PATCH', neg: 'no rl headers' }); + check(resU, { 'PATCH /widgets/{id} without rl headers -> non-2xx': r => r.status < 200 || r.status >= 300 }); + console.log('Response body:', resU.body); + + const resD = http.del(u(`/api-internal/v1/widgets/${id}`), null, { headers: { 'X-Wallarm-Schema-ID': '34' } }); + op.add(resD.timings.duration, { path: '/widgets/{id}', method: 'DELETE', neg: 'no rl headers' }); + check(resD, { 'DELETE /widgets/{id} without rl headers -> non-2xx': r => r.status < 200 || r.status >= 300 }); + console.log('Response body:', resD.body); + }); +} + +export default function () { + positiveTests(); + negativeTests(); +} \ No newline at end of file diff --git a/resources/test/k6/58.js b/resources/test/k6/58.js new file mode 100644 index 0000000..b991ca0 --- /dev/null +++ b/resources/test/k6/58.js @@ -0,0 +1,154 @@ +// file: oas31_feature_showcase.test.js +import http from 'k6/http'; +import { check, group } from 'k6'; +import { Trend } from 'k6/metrics'; + +export const options = { + vus: Number(__ENV.VUS || 1), + iterations: Number(__ENV.ITERATIONS || 1), + thresholds: { + http_req_failed: ['rate==0'], + http_req_duration: ['p(95)<1000'], + }, +}; + +const BASE_URL = __ENV.BASE_URL || 'http://localhost:8282'; + +// общие заголовки и куки +const H = { + 'X-Wallarm-Schema-ID': '58', + 'Cookie': 'SESSIONID=dummy', +}; +const H_JSON = { ...H, 'Content-Type': 'application/json' }; + +// ожидаемый стаб +const EXPECTED = { summary: [{ schema_id: 58, status_code: 200 }] }; + +function j(r) { try { return r.json(); } catch { return null; } } +function eq(a, b) { return JSON.stringify(a) === JSON.stringify(b); } +function u(p) { return `${BASE_URL}${p}`; } + +const op = new Trend('oas31_op_ms'); + +// ------------------ Позитивные кейсы ------------------ +function positives() { + group('POSITIVE: /v1/orders (JSON Schema 2020-12 features)', () => { + const body = { + id: 'ord-1', + status: 'NEW', // const + note: 'deliver asap', // becomes required when flags.express = true (if/then) + items: [ + { sku: 'SKU-1', qty: 1 } // unevaluatedProperties: false enforced in item + ], + flags: { express: true } // triggers if/then -> note required + }; + const res = http.post(u('/v1/orders'), JSON.stringify(body), { headers: H_JSON }); + op.add(res.timings.duration, { path: '/v1/orders', method: 'POST', kind: 'positive' }); + const bodyJson = j(res); + check(res, { '200 /v1/orders': (r) => r.status === 200 }); + check(bodyJson, { 'expected stub /v1/orders': (b) => eq(b, EXPECTED) }); + console.log('Response body:', res.body); + }); + + group('POSITIVE: /v1/orders/{id} (deepObject + allowReserved)', () => { + const params = '?filter[date][gt]=2025-01-01&filter[date][lt]=2025-12-31&search=[abc]{def}'; + const res = http.get(u(`/v1/orders/ord-1${params}`), { headers: H }); + op.add(res.timings.duration, { path: '/v1/orders/{id}', method: 'GET', kind: 'positive' }); + const bodyJson = j(res); + check(res, { '200 /v1/orders/{id}': (r) => r.status === 200 }); + check(bodyJson, { 'expected stub /v1/orders/{id}': (b) => eq(b, EXPECTED) }); + console.log('Response body:', res.body); + }); + + group('POSITIVE: /v1/profile (JSON and merge-patch)', () => { + const resJson = http.patch(u('/v1/profile'), JSON.stringify({ nickname: 'Nick', age: 33 }), { headers: H_JSON }); + op.add(resJson.timings.duration, { path: '/v1/profile', method: 'PATCH', kind: 'positive-json' }); + check(resJson, { '200 /v1/profile JSON': (r) => r.status === 200 }); + check(j(resJson), { 'expected stub /v1/profile JSON': (b) => eq(b, EXPECTED) }); + console.log('Response body:', resJson.body); + + const resPatch = http.patch(u('/v1/profile'), JSON.stringify({ nickname: null }), { headers: { ...H, 'Content-Type': 'application/merge-patch+json' } }); + op.add(resPatch.timings.duration, { path: '/v1/profile', method: 'PATCH', kind: 'positive-merge' }); + check(resPatch, { '200 /v1/profile merge-patch': (r) => r.status === 200 }); + check(j(resPatch), { 'expected stub /v1/profile merge-patch': (b) => eq(b, EXPECTED) }); + console.log('Response body:', resPatch.body); + }); + + group('POSITIVE: /v1/refsibling ($ref with siblings)', () => { + // $ref -> codeBase + minLength sibling + const res = http.post(u('/v1/refsibling'), JSON.stringify({ code: 'ABC_1' }), { headers: H_JSON }); + op.add(res.timings.duration, { path: '/v1/refsibling', method: 'POST', kind: 'positive' }); + check(res, { '200 /v1/refsibling': (r) => r.status === 200 }); + check(j(res), { 'expected stub /v1/refsibling': (b) => eq(b, EXPECTED) }); + console.log('Response body:', res.body); + }); +} + +// ------------------ Негативные кейсы ------------------ +function negatives() { + group('NEGATIVE: /v1/orders -> const violation (status != NEW)', () => { + const body = { + id: 'ord-2', + status: 'OLD', // должно быть NEW + items: [{ sku: 'SKU-1', qty: 1 }], + flags: { express: false } + }; + const res = http.post(u('/v1/orders'), JSON.stringify(body), { headers: H_JSON }); + op.add(res.timings.duration, { path: '/v1/orders', method: 'POST', kind: 'neg-const' }); + const bj = j(res); + check(res, { 'non-200 on const violation': (r) => r.status !== 200 }); + check(bj, { 'body != stub on const violation': (b) => !eq(b, EXPECTED) }); + }); + + group('NEGATIVE: /v1/orders -> if/then/else (express=true but note missing)', () => { + const body = { + id: 'ord-3', + status: 'NEW', + items: [{ sku: 'SKU-1', qty: 1 }], + flags: { express: true } // note отсутствует + }; + const res = http.post(u('/v1/orders'), JSON.stringify(body), { headers: H_JSON }); + op.add(res.timings.duration, { path: '/v1/orders', method: 'POST', kind: 'neg-if-then' }); + check(res, { 'non-200 on if/then violation': (r) => r.status !== 200 }); + check(j(res), { 'body != stub on if/then violation': (b) => !eq(b, EXPECTED) }); + }); + + group('NEGATIVE: /v1/orders -> unevaluatedProperties (extra field in item)', () => { + const body = { + id: 'ord-4', + status: 'NEW', + items: [{ sku: 'SKU-1', qty: 1, extra: 'nope' }], // лишнее поле + }; + const res = http.post(u('/v1/orders'), JSON.stringify(body), { headers: H_JSON }); + op.add(res.timings.duration, { path: '/v1/orders', method: 'POST', kind: 'neg-unevaluated' }); + check(res, { 'non-200 on unevaluatedProperties': (r) => r.status !== 200 }); + check(j(res), { 'body != stub on unevaluatedProperties': (b) => !eq(b, EXPECTED) }); + }); + + group('NEGATIVE: /v1/profile -> wrong type (age < 0)', () => { + const res = http.patch(u('/v1/profile'), JSON.stringify({ age: -1 }), { headers: H_JSON }); + op.add(res.timings.duration, { path: '/v1/profile', method: 'PATCH', kind: 'neg-age' }); + check(res, { 'non-200 on min violation': (r) => r.status !== 200 }); + check(j(res), { 'body != stub on min violation': (b) => !eq(b, EXPECTED) }); + }); + + group('NEGATIVE: /v1/refsibling -> $ref sibling minLength not satisfied', () => { + const res = http.post(u('/v1/refsibling'), JSON.stringify({ code: 'A1' }), { headers: H_JSON }); // слишком короткий + op.add(res.timings.duration, { path: '/v1/refsibling', method: 'POST', kind: 'neg-ref-sibling' }); + check(res, { 'non-200 on ref+minLength violation': (r) => r.status !== 200 }); + check(j(res), { 'body != stub on ref+minLength violation': (b) => !eq(b, EXPECTED) }); + + }); + + group('NEGATIVE: missing Cookie SESSIONID (security)', () => { + const res = http.get(u('/v1/orders/ord-1?search=[abc]'), { headers: { 'X-Wallarm-Schema-ID': '999' } }); + op.add(res.timings.duration, { path: '/v1/orders/{id}', method: 'GET', kind: 'neg-missing-cookie' }); + check(res, { 'non-200 without cookie': (r) => r.status !== 200 }); + check(j(res), { 'body != stub without cookie': (b) => !eq(b, EXPECTED) }); + }); +} + +export default function () { + positives(); + negatives(); +} \ No newline at end of file diff --git a/resources/test/k6/59.js b/resources/test/k6/59.js new file mode 100644 index 0000000..433dc1a --- /dev/null +++ b/resources/test/k6/59.js @@ -0,0 +1,148 @@ +// file: kotlin_service_template_schema34_allof_oneof.test.js +import http from 'k6/http'; +import { check, group } from 'k6'; + +export const options = { + vus: Number(__ENV.VUS || 1), + iterations: Number(__ENV.ITERATIONS || 1), +}; + +const BASE_URL = __ENV.BASE_URL || 'http://localhost:8282'; +const H = { + 'X-Wallarm-Schema-ID': '59', + 'rl-tenant-id': __ENV.RL_TENANT_ID || 'tenant-1', + 'rl-user-id': __ENV.RL_USER_ID || 'user-1', +}; +const H_JSON = { ...H, 'Content-Type': 'application/json' }; + +// Стаб, который возвращают мок/фильтр Wallarm. Если видим его — пропускаем проверки формы виджета +const EXPECTED_STUB = { summary: [{ schema_id: 59, status_code: 200 }] }; + +function j(r) { try { return r.json(); } catch { return null; } } +function eq(a, b) { return JSON.stringify(a) === JSON.stringify(b); } +function u(p) { return `${BASE_URL}${p}`; } +function uuid() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c=>{ + const r=(Math.random()*16)|0, v=c==='x'?r:(r&0x3|0x8); return v.toString(16); + }); +} + +// -------- Helpers to validate allOf(widgetId + widgetProperties) -------- +function isString(x) { return typeof x === 'string' && x.length >= 0; } +function checkAllOfWidget(obj) { + // widget = allOf(widgetId, widgetProperties) + required: id, name, type, description + return obj + && isString(obj.id) + && isString(obj.name) + && isString(obj.type) + && isString(obj.description); +} + +// -------- Bodies -------- +const createBody = (overrides={}) => ({ + name: 'Widget A', + type: 'BASIC', + description: 'Example', + ...overrides, +}); +const updateBody = (overrides={}) => ({ + description: 'Updated', + ...overrides, +}); + +// =================== POSITIVE: проверка allOf =================== +export default function () { + group('ALLOF – Create -> Get -> Update -> Delete', () => { + // POST /widgets (Create) + const rCreate = http.post(u('/api-internal/v1/widgets'), JSON.stringify(createBody()), { headers: H_JSON }); + check(rCreate, { + 'POST /widgets 201/200': r => r.status === 201 || r.status === 200, + }); + console.log('Response body:', rCreate.body); + + const bjCreate = j(rCreate); + if (bjCreate && !eq(bjCreate, EXPECTED_STUB)) { + // Если не стаб — проверяем allOf + check(bjCreate, { 'create matches allOf widget': b => checkAllOfWidget(b) }); + } + + // Вытащим id если сервер реально вернул виджет, иначе сгенерим + const id = (bjCreate && bjCreate.id && !eq(bjCreate, EXPECTED_STUB)) ? bjCreate.id : uuid(); + + // GET /widgets/{id} + const rGet = http.get(u(`/api-internal/v1/widgets/${encodeURIComponent(id)}`), { headers: H }); + check(rGet, { 'GET /widgets/{id} 200/404': r => [200,404].includes(r.status) }); + const bjGet = j(rGet); + if (rGet.status === 200 && bjGet && !eq(bjGet, EXPECTED_STUB)) { + check(bjGet, { 'get matches allOf widget': b => checkAllOfWidget(b) }); + } + console.log('Response body:', rGet.body); + + // PATCH /widgets/{id} + const rPatch = http.patch(u(`/api-internal/v1/widgets/${encodeURIComponent(id)}`), JSON.stringify(updateBody()), { headers: H_JSON }); + check(rPatch, { 'PATCH /widgets/{id} 200/404': r => [200,404].includes(r.status) }); + const bjPatch = j(rPatch); + if (rPatch.status === 200 && bjPatch && !eq(bjPatch, EXPECTED_STUB)) { + check(bjPatch, { 'patch matches allOf widget': b => checkAllOfWidget(b) }); + } + console.log('Response body:', rPatch.body); + + // DELETE /widgets/{id} + const rDel = http.del(u(`/api-internal/v1/widgets/${encodeURIComponent(id)}`), null, { headers: H }); + check(rDel, { 'DELETE /widgets/{id} 204/200/404': r => [204,200,404].includes(r.status) }); + console.log('Response body:', rDel.body); + }); + + // =================== NEGATIVE: нарушаем allOf (required из обеих частей) =================== + group('ALLOF – Negative create (missing required fields)', () => { + // Нет type + const r1 = http.post(u('/api-internal/v1/widgets'), JSON.stringify(createBody({ type: undefined })), { headers: H_JSON }); + check(r1, { 'missing type -> 400/non-2xx': r => r.status === 400 || r.status < 200 || r.status >= 300 }); + console.log('Response body:', r1.body); + + // Нет name + const r2 = http.post(u('/api-internal/v1/widgets'), JSON.stringify(createBody({ name: undefined })), { headers: H_JSON }); + check(r2, { 'missing name -> 400/non-2xx': r => r.status === 400 || r.status < 200 || r.status >= 300 }); + console.log('Response body:', r2.body); + + // Нет description + const r3 = http.post(u('/api-internal/v1/widgets'), JSON.stringify(createBody({ description: undefined })), { headers: H_JSON }); + check(r3, { 'missing description -> 400/non-2xx': r => r.status === 400 || r.status < 200 || r.status >= 300 }); + console.log('Response body:', r3.body); + }); + + // =================== ONEOF: примеры (в текущей спеки НЕТ oneOf) =================== + // Ниже — шаблон, как тестировать oneOf, если добавишь схему с oneOf в components. + /* + // Пример схемы (для справки, не исполняется): + // components: + // schemas: + // widgetUpsert: + // oneOf: + // - $ref: '#/components/schemas/widgetCreate' # required: name,type,description + // - $ref: '#/components/schemas/widgetUpdate' # required: id + (любые свойства) + // + // Тесты: + group('ONEOF – Positive branch #1 (create variant)', () => { + const body = { name: 'W', type: 'BASIC', description: 'D' }; // соответствует widgetCreate + const r = http.post(u('/api-internal/v1/widgets/upsert'), JSON.stringify(body), { headers: H_JSON }); + check(r, { 'oneOf create branch -> 200/201': rr => [200,201].includes(rr.status) }); + }); + + group('ONEOF – Positive branch #2 (update variant)', () => { + const body = { id: uuid(), description: 'upd only' }; // соответствует widgetUpdate + const r = http.post(u('/api-internal/v1/widgets/upsert'), JSON.stringify(body), { headers: H_JSON }); + check(r, { 'oneOf update branch -> 200/201': rr => [200,201].includes(rr.status) }); + }); + + group('ONEOF – Negative (matches none or multiple)', () => { + // 1) Не подходит ни под одну ветку: нет обязательных полей обеих веток + const rBad1 = http.post(u('/api-internal/v1/widgets/upsert'), JSON.stringify({ foo: 'bar' }), { headers: H_JSON }); + check(rBad1, { 'oneOf none -> 400/non-2xx': rr => rr.status === 400 || rr.status < 200 || rr.status >= 300 }); + + // 2) Подходит под обе ветки (если такое возможно по схеме) — сервер должен отбраковать + const rBad2 = http.post(u('/api-internal/v1/widgets/upsert'), JSON.stringify({ id: uuid(), name: 'W', type: 'BASIC', description: 'D' }), { headers: H_JSON }); + check(rBad2, { 'oneOf multiple -> 400/non-2xx': rr => rr.status === 400 || rr.status < 200 || rr.status >= 300 }); + }); + */ +} \ No newline at end of file diff --git a/resources/test/k6/doe_v2_account_authorization_validations.test.js b/resources/test/k6/doe_v2_account_authorization_validations.test.js new file mode 100644 index 0000000..a6cde71 --- /dev/null +++ b/resources/test/k6/doe_v2_account_authorization_validations.test.js @@ -0,0 +1,85 @@ +// doe_v2_account_authorization_validations.test.js +import http from 'k6/http'; +import { check, group, sleep } from 'k6'; +import { Trend } from 'k6/metrics'; + +export const options = { + vus: Number(__ENV.VUS || 1), + iterations: Number(__ENV.ITERATIONS || 1), + thresholds: { + http_req_failed: ['rate==0'], + http_req_duration: ['p(95)<800'], + 'doe_v2_get_duration': ['p(95)<600'], + 'doe_v2_post_duration': ['p(95)<600'], + }, +}; + +const BASE_URL = __ENV.BASE_URL || 'http://localhost:8282'; +const TEST_DATE = __ENV.TEST_DATE || new Date().toISOString().slice(0, 10); + +const postTrend = new Trend('doe_v2_post_duration'); +const getTrend = new Trend('doe_v2_get_duration'); + +// Общие заголовки (по требованию) +const COMMON_HEADERS_JSON = { + 'Content-Type': 'application/json', + 'X-Wallarm-Schema-ID': '29', +}; +const COMMON_HEADERS = { + 'X-Wallarm-Schema-ID': '29', +}; + +function safeJson(res) { + try { return res.json(); } catch { return null; } +} +function isString(v) { return typeof v === 'string'; } +function looksLikeUrl(s) { return isString(s) && /^(https?:\/\/|\/)/i.test(s); } + +export default function () { + group('POST /api-internal/custodian-data/v1/account-authorization-validations', () => { + const endpoint = `${BASE_URL}/api-internal/custodian-data/v1/account-authorization-validations`; + const payload = { date: TEST_DATE }; + + const res = http.post(endpoint, JSON.stringify(payload), { + headers: COMMON_HEADERS_JSON, + tags: { service: 'doe-v2', operationId: 'account-authorization-validations-post' }, + }); + postTrend.add(res.timings.duration); + + check(res, { + 'POST status 200': (r) => r.status === 200, + 'POST content-type JSON': (r) => (r.headers['Content-Type'] || '').includes('application/json'), + }); + + const json = safeJson(res); + check(json, { + 'POST body is object': (j) => j && typeof j === 'object', + 'POST has date': (j) => j && isString(j.date), + 'POST date echoes input': (j) => j && j.date === TEST_DATE, + }); + }); + + sleep(0.2); + + group('GET /api-internal/custodian-data/v1/account-authorization-validations/{date}', () => { + const endpoint = `${BASE_URL}/api-internal/custodian-data/v1/account-authorization-validations/${encodeURIComponent(TEST_DATE)}`; + const res = http.get(endpoint, { + headers: COMMON_HEADERS, + tags: { service: 'doe-v2', operationId: 'account-authorization-validations-get' }, + }); + getTrend.add(res.timings.duration); + + check(res, { + 'GET status 200': (r) => r.status === 200, + 'GET content-type JSON': (r) => (r.headers['Content-Type'] || '').includes('application/json'), + }); + + const json = safeJson(res); + check(json, { + 'GET body is object': (j) => j && typeof j === 'object', + 'has failureReportUrl': (j) => j && looksLikeUrl(j.failureReportUrl), + 'has successReportUrl': (j) => j && looksLikeUrl(j.successReportUrl), + 'has summaryReportUrl': (j) => j && looksLikeUrl(j.summaryReportUrl), + }); + }); +} \ No newline at end of file From 874c542c018a71c36639e9564042125d9fd090ea Mon Sep 17 00:00:00 2001 From: Nikolay Tkachenko Date: Sat, 29 Nov 2025 01:08:50 +0200 Subject: [PATCH 2/3] Update Go version and dependencies --- .github/workflows/binaries.yml | 12 +- cmd/api-firewall/tests/wallarm_api2_update.db | Bin 98304 -> 98304 bytes go.mod | 109 +++---- go.sum | 267 ++++++++---------- internal/platform/validator/issue641_test.go | 2 +- internal/platform/validator/issue789_test.go | 2 +- internal/platform/validator/issue949_test.go | 116 ++++++++ .../platform/validator/req_resp_decoder.go | 17 +- .../validator/req_resp_decoder_test.go | 8 +- .../validator/zip_file_upload_test.go | 123 ++++++++ pkg/APIMode/apifw_test.go | 3 + resources/test/k6/30.js | 201 ------------- resources/test/k6/32.js | 107 ------- resources/test/k6/33.js | 187 ------------ resources/test/k6/34.js | 211 -------------- resources/test/k6/58.js | 154 ---------- resources/test/k6/59.js | 148 ---------- ..._account_authorization_validations.test.js | 85 ------ 18 files changed, 433 insertions(+), 1319 deletions(-) create mode 100644 internal/platform/validator/issue949_test.go create mode 100644 internal/platform/validator/zip_file_upload_test.go delete mode 100644 resources/test/k6/30.js delete mode 100644 resources/test/k6/32.js delete mode 100644 resources/test/k6/33.js delete mode 100644 resources/test/k6/34.js delete mode 100644 resources/test/k6/58.js delete mode 100644 resources/test/k6/59.js delete mode 100644 resources/test/k6/doe_v2_account_authorization_validations.test.js diff --git a/.github/workflows/binaries.yml b/.github/workflows/binaries.yml index 6a8dc2b..e5f3060 100644 --- a/.github/workflows/binaries.yml +++ b/.github/workflows/binaries.yml @@ -51,7 +51,7 @@ jobs: needs: - draft-release env: - X_GO_DISTRIBUTION: "https://go.dev/dl/go1.24.6.linux-amd64.tar.gz" + X_GO_DISTRIBUTION: "https://go.dev/dl/go1.24.10.linux-amd64.tar.gz" APIFIREWALL_NAMESPACE: "github.com/wallarm/api-firewall" strategy: matrix: @@ -162,7 +162,7 @@ jobs: needs: - draft-release env: - X_GO_VERSION: "1.24.6" + X_GO_VERSION: "1.24.10" APIFIREWALL_NAMESPACE: "github.com/wallarm/api-firewall" strategy: matrix: @@ -272,19 +272,19 @@ jobs: include: - arch: armv6 distro: bookworm - go_distribution: https://go.dev/dl/go1.24.6.linux-armv6l.tar.gz + go_distribution: https://go.dev/dl/go1.24.10.linux-armv6l.tar.gz artifact: armv6-libc - arch: aarch64 distro: bookworm - go_distribution: https://go.dev/dl/go1.24.6.linux-arm64.tar.gz + go_distribution: https://go.dev/dl/go1.24.10.linux-arm64.tar.gz artifact: arm64-libc - arch: armv6 distro: alpine_latest - go_distribution: https://go.dev/dl/go1.24.6.linux-armv6l.tar.gz + go_distribution: https://go.dev/dl/go1.24.10.linux-armv6l.tar.gz artifact: armv6-musl - arch: aarch64 distro: alpine_latest - go_distribution: https://go.dev/dl/go1.24.6.linux-arm64.tar.gz + go_distribution: https://go.dev/dl/go1.24.10.linux-arm64.tar.gz artifact: arm64-musl steps: - uses: actions/checkout@v4 diff --git a/cmd/api-firewall/tests/wallarm_api2_update.db b/cmd/api-firewall/tests/wallarm_api2_update.db index 2b22ece001d1bb2d961ae36ac87d94a828245d63..e258ef342193e9ac9e6e342a6756102fb9e406b6 100644 GIT binary patch delta 46 zcmZo@U~6b#n;^}&X`+lX { - const r = (Math.random() * 16) | 0, v = c === 'x' ? r : (r & 0x3 | 0x8); - return v.toString(16); - }); -} - -// ========= минимальные payload'ы для позитивных запросов ========= -function payloadEmailBatch() { - return { - attachments: [], - bcc: [], - cc: [], - contentType: 'HTML', - distributionType: 'ATTACHMENT', - id: uuidv4(), - name: 'batch', - notifyAnyway: false, - recipientAttachments: [], - status: 'DRAFT', - to: [], - type: 'MARKETING_CAMPAIGN', - }; -} -function payloadFetchPaginatedQueryRequestV2() { - return { dataset: 'documents', fields: [{ id: 'id' }], pagination: { first: 1 } }; -} -function payloadFetchGroupAndAggregateQueryRequestV2() { - return { dataset: 'documents', countTotalVisible: false }; -} -function payloadCreateClientPortalDocumentInput() { - return { archived: false, documentName: 'doc', fileName: 'file.pdf' }; -} -function payloadEditDocumentsInput() { return {}; } -function payloadDownloadDocumentsInput() { return {}; } -function payloadEditClientPortalDocumentInput() { return { documentName: 'doc-updated' }; } -function payloadAddPortalUsersAccessInput() { return {}; } -function payloadRemovePortalUsersAccessInput() { return {}; } -function payloadAddBatchRecipientAttachmentRequestBody() { return {}; } - -// ========= позитивные кейсы по всей спеке ========= -function makePositiveCases() { - const docId = uuidv4(); - const docId2 = uuidv4(); - const batchId = uuidv4(); - const recId = uuidv4(); - - return [ - { m: 'PATCH', p: `/api-internal/document-management/v1/documents`, body: payloadEditDocumentsInput() }, - { m: 'POST', p: `/api-internal/document-management/v1/documents/query`, body: payloadFetchPaginatedQueryRequestV2() }, - { m: 'POST', p: `/api-internal/document-management/v1/documents/aggregate`, body: payloadFetchGroupAndAggregateQueryRequestV2() }, - { m: 'GET', p: `/api-internal/document-management/v1/documents/metadata` }, - { m: 'POST', p: `/api-internal/document-management/v1/documents/download`, body: payloadDownloadDocumentsInput() }, - { m: 'POST', p: `/api-internal/document-management/v1/documents/download/${docId}` }, - { m: 'PATCH', p: `/api-internal/document-management/v1/documents/${docId}`, body: payloadEditDocumentsInput() }, - { m: 'GET', p: `/api-internal/document-management/v1/documents/upload-status?documentIds=${docId}` }, - { m: 'GET', p: `/api-internal/document-management/v1/documents/${docId2}/upload-status` }, - - { m: 'GET', p: `/api-internal/document-management/v1/dataset/documents/metadata` }, - { m: 'POST', p: `/api-internal/document-management/v1/dataset/documents/paginated`, body: payloadFetchPaginatedQueryRequestV2() }, - { m: 'POST', p: `/api-internal/document-management/v1/dataset/documents/group-and-aggregate`, body: payloadFetchGroupAndAggregateQueryRequestV2() }, - - { m: 'POST', p: `/api-internal/document-management/v1/documents/portal`, body: payloadCreateClientPortalDocumentInput() }, - { m: 'POST', p: `/api-internal/document-management/v1/documents/portal/query`, body: payloadFetchPaginatedQueryRequestV2() }, - { m: 'GET', p: `/api-internal/document-management/v1/documents/portal/metadata` }, - { m: 'POST', p: `/api-internal/document-management/v1/documents/portal/aggregate`, body: payloadFetchGroupAndAggregateQueryRequestV2() }, - { m: 'PATCH', p: `/api-internal/document-management/v1/documents/portal/${docId}`, body: payloadEditClientPortalDocumentInput() }, - - { m: 'POST', p: `/api-internal/document-management/v1/documents/portal-user-access`, body: payloadAddPortalUsersAccessInput() }, - { m: 'POST', p: `/api-internal/document-management/v1/documents/portal-user-access/remove`, body: payloadRemovePortalUsersAccessInput() }, - { m: 'POST', p: `/api-internal/document-management/v1/documents/portal-user-access/notification`, body: payloadAddPortalUsersAccessInput() }, - - { m: 'POST', p: `/api-internal/document-management/v1/email-batches`, body: payloadEmailBatch() }, - { m: 'GET', p: `/api-internal/document-management/v1/email-batches/metadata` }, - { m: 'POST', p: `/api-internal/document-management/v1/email-batches/query`, body: payloadFetchPaginatedQueryRequestV2() }, - { m: 'POST', p: `/api-internal/document-management/v1/email-batches/aggregate`, body: payloadFetchGroupAndAggregateQueryRequestV2() }, - { m: 'GET', p: `/api-internal/document-management/v1/email-batches/${batchId}` }, - { m: 'POST', p: `/api-internal/document-management/v1/email-batches/${batchId}`, body: payloadEmailBatch() }, - { m: 'POST', p: `/api-internal/document-management/v1/email-batches/${batchId}/send`, body: payloadEmailBatch() }, - { m: 'GET', p: `/api-internal/document-management/v1/email-batches/${batchId}/preview` }, - { m: 'POST', p: `/api-internal/document-management/v1/email-batches/${batchId}/document-recipients`, body: payloadAddBatchRecipientAttachmentRequestBody() }, - { m: 'DELETE',p: `/api-internal/document-management/v1/email-batches/${batchId}/document-recipients` }, - { m: 'DELETE',p: `/api-internal/document-management/v1/email-batches/${batchId}/document-recipients/${recId}` }, - - { m: 'POST', p: `/api-internal/document-management/v1/email-recipients/query`, body: payloadFetchPaginatedQueryRequestV2() }, - { m: 'POST', p: `/api-internal/document-management/v1/email-recipients/aggregate`, body: payloadFetchGroupAndAggregateQueryRequestV2() }, - { m: 'GET', p: `/api-internal/document-management/v1/email-recipients/metadata` }, - - { m: 'POST', p: `/api-internal/document-management/v1/document-recipients/query`, body: payloadFetchPaginatedQueryRequestV2() }, - { m: 'POST', p: `/api-internal/document-management/v1/document-recipients/aggregate`, body: payloadFetchGroupAndAggregateQueryRequestV2() }, - { m: 'GET', p: `/api-internal/document-management/v1/document-recipients/metadata` }, - { m: 'POST', p: `/api-internal/document-management/v1/document-recipients/paginated`, body: payloadFetchPaginatedQueryRequestV2() }, - - { m: 'GET', p: `/api-internal/document-management/v1/dataset/document-recipients/metadata` }, - { m: 'POST', p: `/api-internal/document-management/v1/dataset/document-recipients/paginated`, body: payloadFetchPaginatedQueryRequestV2() }, - { m: 'POST', p: `/api-internal/document-management/v1/dataset/document-recipients/group-and-aggregate`, body: payloadFetchGroupAndAggregateQueryRequestV2() }, - ]; -} - -// ========= негативные кейсы ========= -// - отсутствие заголовка X-Wallarm-Schema-ID -// - неверный X-Wallarm-Schema-ID -// - пустые/некорректные тела для эндпоинтов с обязательным body -// - невалидные UUID в path -// - отсутствие обязательных query параметров -function makeNegativeCases() { - const badUUID = 'not-a-uuid'; - - return [ - // Нет заголовка X-Wallarm-Schema-ID - { title: 'missing header', m: 'GET', p: `/api-internal/document-management/v1/documents/metadata`, headers: {} }, - { title: 'missing header', m: 'POST', p: `/api-internal/document-management/v1/documents/query`, headers: { 'Content-Type': 'application/json' }, body: {} }, - - // Неверный заголовок X-Wallarm-Schema-ID - { title: 'wrong schema id', m: 'GET', p: `/api-internal/document-management/v1/email-batches/metadata`, headers: H_BAD }, - { title: 'wrong schema id', m: 'POST', p: `/api-internal/document-management/v1/email-batches`, headers: H_JSON_BAD, body: {} }, - - // Пустое тело там, где нужен обязательный body - { title: 'empty body', m: 'POST', p: `/api-internal/document-management/v1/dataset/documents/paginated`, headers: H_JSON, body: {} }, - { title: 'empty body', m: 'POST', p: `/api-internal/document-management/v1/documents/aggregate`, headers: H_JSON, body: {} }, - { title: 'empty body', m: 'POST', p: `/api-internal/document-management/v1/documents/portal`, headers: H_JSON, body: {} }, - - // Невалидный UUID в path - { title: 'invalid uuid', m: 'GET', p: `/api-internal/document-management/v1/email-batches/${badUUID}`, headers: H }, - { title: 'invalid uuid', m: 'PATCH',p: `/api-internal/document-management/v1/documents/${badUUID}`, headers: H_JSON, body: {} }, - { title: 'invalid uuid', m: 'GET', p: `/api-internal/document-management/v1/documents/${badUUID}/upload-status`, headers: H }, - - // Отсутствует обязательный query - { title: 'missing query', m: 'GET', p: `/api-internal/document-management/v1/documents/upload-status`, headers: H }, - - // Неверный метод (например, DELETE без id where path requires id) - { title: 'wrong method shape', m: 'DELETE', p: `/api-internal/document-management/v1/email-batches/document-recipients`, headers: H }, // нет batchId в path - ]; -} - -function doRequest(method, url, headers, body) { - if (method === 'GET') return http.get(url, { headers }); - if (method === 'DELETE')return http.del(url, null, { headers }); - if (method === 'POST') return http.post(url, body ? JSON.stringify(body) : '', { headers }); - if (method === 'PATCH') return http.patch(url, body ? JSON.stringify(body) : '', { headers }); - throw new Error(`Unsupported method ${method}`); -} - -export default function () { - const positives = makePositiveCases(); - - group('Document Management API – positive cases (expect 200 + stub body)', () => { - for (const c of positives) { - const url = t(c.p); - const headers = (c.m === 'GET' || c.m === 'DELETE') ? H : H_JSON; - const res = doRequest(c.m, url, headers, c.body); - - opTrend.add(res.timings.duration, { path: c.p, method: c.m, kind: 'positive' }); - - const body = j(res); - check(res, { [`${c.m} ${c.p} -> status 200`]: (r) => r.status === 200 }); - check(body, { [`${c.m} ${c.p} -> expected stub`]: (b) => eq(b, EXPECTED) }); - } - }); - - const negatives = makeNegativeCases(); - - group('Document Management API – negative cases (expect non-200 and body != stub)', () => { - for (const n of negatives) { - const url = t(n.p); - const res = doRequest(n.m, url, n.headers ?? H, n.body); - - opTrend.add(res.timings.duration, { path: n.p, method: n.m, kind: 'negative', title: n.title }); - - const body = j(res); - check(res, { [`NEG ${n.m} ${n.p} (${n.title}) -> status != 200`]: (r) => r.status !== 200 }); - check(body, { [`NEG ${n.m} ${n.p} (${n.title}) -> body != stub`]: (b) => !eq(b, EXPECTED) }); - } - }); -} \ No newline at end of file diff --git a/resources/test/k6/32.js b/resources/test/k6/32.js deleted file mode 100644 index fcb0b37..0000000 --- a/resources/test/k6/32.js +++ /dev/null @@ -1,107 +0,0 @@ -// file: query_engine_all_paths_schema32.test.js -import http from 'k6/http'; -import { check, group } from 'k6'; -import { Trend } from 'k6/metrics'; - -export const options = { - vus: Number(__ENV.VUS || 1), - iterations: Number(__ENV.ITERATIONS || 1), - thresholds: { - http_req_failed: ['rate==0'], - http_req_duration: ['p(95)<1000'], - }, -}; - -// В спеке servers: [] — задаём вручную или через env -const BASE_URL = __ENV.BASE_URL || 'http://localhost:8282'; -const DATASET_ID = __ENV.DATASET_ID || 'test-dataset'; - -// Общие заголовки -const H = { 'X-Wallarm-Schema-ID': '32' }; -const H_JSON = { ...H, 'Content-Type': 'application/json' }; - -// Ожидаемый стаб-ответ -const EXPECTED = { summary: [{ schema_id: 32, status_code: 200 }] }; - -// Helpers -function j(r) { try { return r.json(); } catch { return null; } } -function eq(a, b) { return JSON.stringify(a) === JSON.stringify(b); } -function u(p) { return `${BASE_URL}${p}`; } - -const op = new Trend('qe_op_ms'); - -export default function () { - group('Query Engine API – all endpoints return stub (schema_id=32)', () => { - // 1) GET /api/v1/query-engine/datasets - { - const res = http.get(u(`/api/v1/query-engine/datasets`), { headers: H }); - op.add(res.timings.duration, { path: '/datasets', method: 'GET' }); - const body = j(res); - check(res, { 'GET /datasets -> 200': (r) => r.status === 200 }); - check(body, { 'GET /datasets -> expected body': (b) => eq(b, EXPECTED) }); - console.log('Response body:', body); - } - - // 2) GET /api/v1/query-engine/datasets/{id} - { - const res = http.get(u(`/api/v1/query-engine/datasets/${encodeURIComponent(DATASET_ID)}`), { headers: H }); - op.add(res.timings.duration, { path: '/datasets/{id}', method: 'GET' }); - const body = j(res); - check(res, { 'GET /datasets/{id} -> 200': (r) => r.status === 200 }); - check(body, { 'GET /datasets/{id} -> expected body': (b) => eq(b, EXPECTED) }); - console.log('Response body:', body); - } - - // 3) POST /api/v1/query-engine/datasets/{id}/query (без тела) - { - const res = http.post(u(`/api/v1/query-engine/datasets/${encodeURIComponent(DATASET_ID)}/query`), '', { headers: H_JSON }); - op.add(res.timings.duration, { path: '/datasets/{id}/query', method: 'POST' }); - const body = j(res); - check(res, { 'POST /datasets/{id}/query -> 200': (r) => r.status === 200 }); - check(body, { 'POST /datasets/{id}/query -> expected body': (b) => eq(b, EXPECTED) }); - console.log('Response body:', body); - } - - // 4) POST /api/v1/query-engine/datasets/{id}/query с query-параметрами - { - const params = { headers: H_JSON }; - const url = u(`/api/v1/query-engine/datasets/${encodeURIComponent(DATASET_ID)}/query?limit=1&cursor=abc&direction=NEXT`); - const res = http.post(url, '', params); - op.add(res.timings.duration, { path: '/datasets/{id}/query?params', method: 'POST' }); - const body = j(res); - check(res, { 'POST /datasets/{id}/query?params -> 200': (r) => r.status === 200 }); - check(body, { 'POST /datasets/{id}/query?params -> expected body': (b) => eq(b, EXPECTED) }); - console.log('Response body:', body); - } - - // 5) POST /api/v1/query-engine/datasets/{id}/export (без тела) - { - const res = http.post(u(`/api/v1/query-engine/datasets/${encodeURIComponent(DATASET_ID)}/export`), '', { headers: H_JSON }); - op.add(res.timings.duration, { path: '/datasets/{id}/export', method: 'POST' }); - const body = j(res); - check(res, { 'POST /datasets/{id}/export -> 200': (r) => r.status === 200 }); - check(body, { 'POST /datasets/{id}/export -> expected body': (b) => eq(b, EXPECTED) }); - console.log('Response body:', body); - } - - // 6) POST /api/v1/query-engine/datasets/{id}/aggregate (без тела) - { - const res = http.post(u(`/api/v1/query-engine/datasets/${encodeURIComponent(DATASET_ID)}/aggregate`), '', { headers: H_JSON }); - op.add(res.timings.duration, { path: '/datasets/{id}/aggregate', method: 'POST' }); - const body = j(res); - check(res, { 'POST /datasets/{id}/aggregate -> 200': (r) => r.status === 200 }); - check(body, { 'POST /datasets/{id}/aggregate -> expected body': (b) => eq(b, EXPECTED) }); - console.log('Response body:', body); - } - - // 7) POST /api/v1/query-engine/datasets/{id}/query/count (без тела) - { - const res = http.post(u(`/api/v1/query-engine/datasets/${encodeURIComponent(DATASET_ID)}/query/count`), '', { headers: H_JSON }); - op.add(res.timings.duration, { path: '/datasets/{id}/query/count', method: 'POST' }); - const body = j(res); - check(res, { 'POST /datasets/{id}/query/count -> 200': (r) => r.status === 200 }); - check(body, { 'POST /datasets/{id}/query/count -> expected body': (b) => eq(b, EXPECTED) }); - console.log('Response body:', body); - } - }); -} \ No newline at end of file diff --git a/resources/test/k6/33.js b/resources/test/k6/33.js deleted file mode 100644 index 8303274..0000000 --- a/resources/test/k6/33.js +++ /dev/null @@ -1,187 +0,0 @@ -// file: recon_v3_all_paths_schema33_with_negative.test.js -import http from 'k6/http'; -import { check, group } from 'k6'; -import { Trend } from 'k6/metrics'; - -export const options = { - vus: Number(__ENV.VUS || 1), - iterations: Number(__ENV.ITERATIONS || 1), - thresholds: { - http_req_failed: ['rate==0'], - http_req_duration: ['p(95)<1200'], - }, -}; - -const BASE_URL = __ENV.BASE_URL || 'http://localhost:8282'; - -// --- Headers -const H = { 'X-Wallarm-Schema-ID': '33' }; -const H_JSON = { ...H, 'Content-Type': 'application/json' }; -const H_WRONG = { 'X-Wallarm-Schema-ID': '33' }; -const H_JSON_WRONG = { ...H_WRONG, 'Content-Type': 'application/json' }; - -// --- Expected stub -const EXPECTED = { summary: [{ schema_id: 33, status_code: 200 }] }; - -// --- Helpers -function j(r) { try { return r.json(); } catch { return null; } } -function eq(a, b) { return JSON.stringify(a) === JSON.stringify(b); } -function u(p) { return `${BASE_URL}${p}`; } -function uuidv4() { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { - const r = (Math.random() * 16) | 0, v = c === 'x' ? r : (r & 0x3 | 0x8); - return v.toString(16); - }); -} -function doRequest(method, url, headers, body) { - if (method === 'GET') return http.get(url, { headers }); - if (method === 'DELETE') return http.del(url, null, { headers }); - if (method === 'POST') return http.post(url, body ? JSON.stringify(body) : '', { headers }); - if (method === 'PATCH') return http.patch(url, body ? JSON.stringify(body) : '', { headers }); - throw new Error(`Unsupported method ${method}`); -} - -const opTrend = new Trend('recon_op_ms'); - -// --- Minimal valid payloads (по required полям схем) -function minimalCustodianSetting() { - return { - customName: 'rs-min', - reconcileDate: 'TODAY', - updatePriceAtStartOfDay: false, - excludeInternalAssetTypes: [], - excludeInternalInstrumentTypes: [], - excludeInternalTransactionTypes: [], - positionReconcileDateType: 'TRADE_DATE', - positionReconcileFaceType: 'CURRENT_QUANTITY', - positionReconcileValueType: 'BOOK_VALUES', - transactionReconcileDateType: 'TRADE_DATE', - matchingThresholdsOverride: { value: {} }, - matchingThresholdsSettings: { value: {} }, - positionAutoReconcileOverride: { value: {} }, - positionAutoReconcileSettings: { value: {} }, - transactionAutoReconcileOverride: { value: {} }, - transactionAutoReconcileSettings: { value: {} }, - transactionAutoAcceptCustodianOverride: { value: {} }, - transactionAutoAcceptCustodianSettings: { value: {} }, - positionReconcileDateTypeOverrideInstrumentTypes: [], - }; -} -const payloadRuleSetInput = () => ({ ruleSet: minimalCustodianSetting() }); -const payloadUpdateGlobalRule = () => ({ globalRule: { reconcileDate: 'TODAY', updatePriceAtStartOfDay: false } }); -const payloadBulkFilters = () => ({ reconDate: '2025-01-01' }); -const payloadBulkPositions = () => ({ action: 'GET_COUNT', filters: payloadBulkFilters() }); -const payloadBulkMatched = () => ({ action: 'GET_COUNT', filters: payloadBulkFilters() }); -const payloadBulkUnmatched = () => ({ action: 'GET_COUNT', filters: payloadBulkFilters() }); -const payloadGetNextTrigger = () => ({ /* currentTrigger опционален */ }); -const payloadTxnIds = () => ({ ids: [uuidv4()] }); -const payloadRemoveAssignments = () => ({ custodianIds: ['c1'] }); -const payloadReplaceAssignments = () => ({ ruleSetId: uuidv4(), custodianIds: ['c1'] }); - -// --- Positive cases -function makePositiveCases() { - const rsId = uuidv4(); - const qInput = encodeURIComponent(JSON.stringify({})); // required query object - - return [ - { m: 'GET', p: `/api-internal/reconciliation/v1/reconciliation-rules` }, - { m: 'GET', p: `/api-internal/reconciliation/v1/reconciliation-rules/global-rule` }, - { m: 'PATCH', p: `/api-internal/reconciliation/v1/reconciliation-rules/global-rule`, body: payloadUpdateGlobalRule() }, - - { m: 'POST', p: `/api-internal/reconciliation/v1/reconciliation-rules/rule-sets`, body: payloadRuleSetInput() }, - { m: 'GET', p: `/api-internal/reconciliation/v1/reconciliation-rules/rule-sets/${rsId}` }, - { m: 'PATCH', p: `/api-internal/reconciliation/v1/reconciliation-rules/rule-sets/${rsId}`, body: payloadRuleSetInput() }, - { m: 'DELETE',p: `/api-internal/reconciliation/v1/reconciliation-rules/rule-sets/${rsId}` }, - - { m: 'GET', p: `/api-internal/reconciliation/v1/reconciliation-rules/rule-sets/assignments` }, - { m: 'GET', p: `/api-internal/reconciliation/v1/reconciliation-rules/rule-sets/${rsId}/assignments` }, - - { m: 'POST', p: `/api-internal/reconciliation/v1/reconciliation-rules/rule-sets/remove-assignments`, body: payloadRemoveAssignments() }, - { m: 'POST', p: `/api-internal/reconciliation/v1/reconciliation-rules/rule-sets/replace-assignments`, body: payloadReplaceAssignments() }, - - { m: 'POST', p: `/api-internal/reconciliation/v1/ai/get-next-agent-trigger`, body: payloadGetNextTrigger() }, - - { m: 'POST', p: `/api-internal/reconciliation/v1/reconciliation-positions/actions/bulk`, body: payloadBulkPositions() }, - { m: 'POST', p: `/api-internal/reconciliation/v1/reconciliation-transactions/actions/bulk-matched`, body: payloadBulkMatched() }, - { m: 'POST', p: `/api-internal/reconciliation/v1/reconciliation-transactions/actions/bulk-unmatched`, body: payloadBulkUnmatched() }, - - { m: 'GET', p: `/api-internal/reconciliation/v1/reconciliation-positions/object-labels?input=${qInput}` }, - - { m: 'POST', p: `/api-internal/reconciliation/v1/reconciliation-positions/transactions/query`, body: payloadTxnIds() }, - ]; -} - -// --- Negative cases -function makeNegativeCases() { - const badUUID = 'not-a-uuid'; - const rsId = uuidv4(); // будем использовать валидный для некоторых негативов - - return [ - // 1) Отсутствует заголовок X-Wallarm-Schema-ID - { title: 'missing schema header', m: 'GET', p: `/api-internal/reconciliation/v1/reconciliation-rules`, headers: {} }, - { title: 'missing schema header', m: 'POST', p: `/api-internal/reconciliation/v1/reconciliation-rules/rule-sets`, headers: { 'Content-Type': 'application/json' }, body: payloadRuleSetInput() }, - - // 2) Неверный X-Wallarm-Schema-ID - { title: 'wrong schema header', m: 'GET', p: `/api-internal/reconciliation/v1/reconciliation-rules/global-rule`, headers: H_WRONG }, - { title: 'wrong schema header', m: 'PATCH',p: `/api-internal/reconciliation/v1/reconciliation-rules/global-rule`, headers: H_JSON_WRONG, body: payloadUpdateGlobalRule() }, - - // 3) Невалидный UUID в path - { title: 'invalid uuid in path', m: 'GET', p: `/api-internal/reconciliation/v1/reconciliation-rules/rule-sets/${badUUID}`, headers: H }, - { title: 'invalid uuid in path', m: 'PATCH', p: `/api-internal/reconciliation/v1/reconciliation-rules/rule-sets/${badUUID}`, headers: H_JSON, body: payloadRuleSetInput() }, - { title: 'invalid uuid in path', m: 'DELETE',p: `/api-internal/reconciliation/v1/reconciliation-rules/rule-sets/${badUUID}`, headers: H }, - { title: 'invalid uuid in path', m: 'GET', p: `/api-internal/reconciliation/v1/reconciliation-rules/rule-sets/${badUUID}/assignments`, headers: H }, - - // 4) Пустой/битый body там, где он обязателен - { title: 'empty body', m: 'POST', p: `/api-internal/reconciliation/v1/reconciliation-rules/rule-sets`, headers: H_JSON, body: {} }, - { title: 'empty body', m: 'PATCH', p: `/api-internal/reconciliation/v1/reconciliation-rules/global-rule`, headers: H_JSON, body: {} }, - { title: 'empty body', m: 'PATCH', p: `/api-internal/reconciliation/v1/reconciliation-rules/rule-sets/${rsId}`, headers: H_JSON, body: {} }, - { title: 'empty body', m: 'POST', p: `/api-internal/reconciliation/v1/ai/get-next-agent-trigger`, headers: H_JSON, body: {} }, - { title: 'empty body', m: 'POST', p: `/api-internal/reconciliation/v1/reconciliation-positions/actions/bulk`, headers: H_JSON, body: {} }, - { title: 'empty body', m: 'POST', p: `/api-internal/reconciliation/v1/reconciliation-transactions/actions/bulk-matched`, headers: H_JSON, body: {} }, - { title: 'empty body', m: 'POST', p: `/api-internal/reconciliation/v1/reconciliation-transactions/actions/bulk-unmatched`, headers: H_JSON, body: {} }, - { title: 'empty body', m: 'POST', p: `/api-internal/reconciliation/v1/reconciliation-rules/rule-sets/remove-assignments`, headers: H_JSON, body: {} }, - { title: 'empty body', m: 'POST', p: `/api-internal/reconciliation/v1/reconciliation-rules/rule-sets/replace-assignments`, headers: H_JSON, body: {} }, - { title: 'empty body', m: 'POST', p: `/api-internal/reconciliation/v1/reconciliation-positions/transactions/query`, headers: H_JSON, body: {} }, - - // 5) Плохой query param: input обязателен, отсутствует - { title: 'missing required query param', m: 'GET', p: `/api-internal/reconciliation/v1/reconciliation-positions/object-labels`, headers: H }, - - // 6) Невалидные поля в body: невалидный UUID в ids - { title: 'invalid uuid in body ids', m: 'POST', p: `/api-internal/reconciliation/v1/reconciliation-positions/transactions/query`, headers: H_JSON, body: { ids: ['not-a-uuid'] } }, - - // 7) Отсутствующее required поле в bulk-параметрах (нет filters) - { title: 'bulk missing filters', m: 'POST', p: `/api-internal/reconciliation/v1/reconciliation-positions/actions/bulk`, headers: H_JSON, body: { action: 'GET_COUNT' } }, - { title: 'bulk missing action', m: 'POST', p: `/api-internal/reconciliation/v1/reconciliation-transactions/actions/bulk-matched`, headers: H_JSON, body: { filters: payloadBulkFilters() } }, - ]; -} - -export default function () { - // --- Positive run - group('Reconciliation API – positive cases (expect 200 + stub)', () => { - for (const c of makePositiveCases()) { - const url = u(c.p); - const headers = (c.m === 'GET' || c.m === 'DELETE') ? H : H_JSON; - const res = doRequest(c.m, url, headers, c.body); - opTrend.add(res.timings.duration, { path: c.p, method: c.m, kind: 'positive' }); - - const body = j(res); - check(res, { [`${c.m} ${c.p} -> 200`]: (r) => r.status === 200 }); - check(body, { [`${c.m} ${c.p} -> expected body`]: (b) => eq(b, EXPECTED) }); - console.log('Response body:', body); - } - }); - - // --- Negative run - group('Reconciliation API – negative cases (expect non-200 and/or body != stub)', () => { - for (const n of makeNegativeCases()) { - const url = u(n.p); - const res = doRequest(n.m, url, n.headers ?? H, n.body); - opTrend.add(res.timings.duration, { path: n.p, method: n.m, kind: 'negative', title: n.title }); - - const body = j(res); - check(res, { [`NEG ${n.m} ${n.p} (${n.title}) -> status != 200`]: (r) => r.status !== 200 }); - check(body, { [`NEG ${n.m} ${n.p} (${n.title}) -> body != stub`]: (b) => !eq(b, EXPECTED) }); - console.log('Response body:', body); - } - }); -} \ No newline at end of file diff --git a/resources/test/k6/34.js b/resources/test/k6/34.js deleted file mode 100644 index 8cfb31a..0000000 --- a/resources/test/k6/34.js +++ /dev/null @@ -1,211 +0,0 @@ -// file: kotlin_service_template_schema34.test.js -import http from 'k6/http'; -import { check, group } from 'k6'; -import { Trend } from 'k6/metrics'; - -export const options = { - vus: Number(__ENV.VUS || 1), - iterations: Number(__ENV.ITERATIONS || 1), - thresholds: { - http_req_failed: ['rate==0'], - http_req_duration: ['p(95)<1200'], - }, -}; - -const BASE_URL = __ENV.BASE_URL || 'http://localhost:8282'; - -const REQUIRED_HEADERS = { - 'X-Wallarm-Schema-ID': '34', - 'rl-tenant-id': __ENV.RL_TENANT_ID || 'tenant-1', - 'rl-user-id': __ENV.RL_USER_ID || 'user-1', -}; -const H = { ...REQUIRED_HEADERS }; -const H_JSON = { ...H, 'Content-Type': 'application/json' }; - -const WRONG_SCHEMA_H = { ...REQUIRED_HEADERS, 'X-Wallarm-Schema-ID': '34' }; - -const EXPECTED = { summary: [{ schema_id: 34, status_code: 200 }] }; - -function j(r) { try { return r.json(); } catch { return null; } } -function eq(a, b) { return JSON.stringify(a) === JSON.stringify(b); } -function u(p) { return `${BASE_URL}${p}`; } -function uuid() { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { - const r = (Math.random()*16)|0, v = c === 'x' ? r : (r&0x3|0x8); - return v.toString(16); - }); -} - -// ---- минимальные полезные данные ---- -function createWidgetBody() { - return { name: 'Widget A', type: 'BASIC', description: 'Example widget' }; -} -function updateWidgetBody() { - return { description: 'Updated description' }; -} - -const op = new Trend('kst_op_ms'); - -// ---- Позитивные тесты ---- -function positiveTests() { - group('POSITIVE /api-internal/v1/widgets (GET list)', () => { - const url = u('/api-internal/v1/widgets?limit=2&direction=STARTING_AFTER&ids=a&ids=b&types=BASIC'); - const res = http.get(url, { headers: H }); - op.add(res.timings.duration, { path: '/widgets', method: 'GET' }); - const body = j(res); - // допускаем 200; если есть тело — сверяем стаб - check(res, { 'GET /widgets -> 200': r => r.status === 200 }); - if (res.body && res.body.length) { - check(body, { 'GET /widgets -> expected stub': b => eq(b, EXPECTED) }); - console.log('Response body:', body); - } - }); - - group('POSITIVE /api-internal/v1/widgets (POST create)', () => { - const res = http.post(u('/api-internal/v1/widgets'), JSON.stringify(createWidgetBody()), { headers: H_JSON }); - op.add(res.timings.duration, { path: '/widgets', method: 'POST' }); - const body = j(res); - // по спеке 201; некоторые стабы могут вернуть 200 — примем оба - check(res, { 'POST /widgets -> 201 or 200': r => r.status === 201 || r.status === 200 }); - if (res.status === 200 || res.headers['Content-Type']?.includes('application/json')) { - check(body, { 'POST /widgets -> expected stub (if JSON body)': b => !res.body || eq(b, EXPECTED) }); - console.log('Response body:', body); - } - }); - - group('POSITIVE /api-internal/v1/widgets/{id} (GET item)', () => { - const id = uuid(); // произвольный - const res = http.get(u(`/api-internal/v1/widgets/${encodeURIComponent(id)}`), { headers: H }); - op.add(res.timings.duration, { path: '/widgets/{id}', method: 'GET' }); - const body = j(res); - check(res, { 'GET /widgets/{id} -> 200 or 404': r => r.status === 200 || r.status === 404 }); - if (res.status === 200) { - check(body, { 'GET /widgets/{id} -> expected stub': b => eq(b, EXPECTED) }); - console.log('Response body:', body); - } - }); - - group('POSITIVE /api-internal/v1/widgets/{id} (PATCH update)', () => { - const id = uuid(); - const res = http.patch(u(`/api-internal/v1/widgets/${encodeURIComponent(id)}`), JSON.stringify(updateWidgetBody()), { headers: H_JSON }); - op.add(res.timings.duration, { path: '/widgets/{id}', method: 'PATCH' }); - const body = j(res); - check(res, { 'PATCH /widgets/{id} -> 200 or 404': r => r.status === 200 || r.status === 404 }); - if (res.status === 200) { - check(body, { 'PATCH /widgets/{id} -> expected stub': b => eq(b, EXPECTED) }); - console.log('Response body:', body); - } - }); - - group('POSITIVE /api-internal/v1/widgets/{id} (DELETE)', () => { - const id = uuid(); - const res = http.del(u(`/api-internal/v1/widgets/${encodeURIComponent(id)}`), null, { headers: H }); - op.add(res.timings.duration, { path: '/widgets/{id}', method: 'DELETE' }); - // по спеке 204; стабы иногда возвращают 200 — примем оба - check(res, { 'DELETE /widgets/{id} -> 204 or 200 or 404': r => [204,200,404].includes(r.status) }); - if (res.status === 200 && res.body) { - check(j(res), { 'DELETE /widgets/{id} -> expected stub if body': b => eq(b, EXPECTED) }); - console.log('Response body:', res.body); - } - }); -} - -// ---- Негативные тесты ---- -function negativeTests() { - // 1) отсутствуют обязательные заголовки - group('NEG missing required headers', () => { - const url = u('/api-internal/v1/widgets'); - const res = http.get(url, { headers: { 'X-Wallarm-Schema-ID': '34' } }); // нет rl-tenant-id / rl-user-id - op.add(res.timings.duration, { path: '/widgets', method: 'GET', neg: 'missing headers' }); - check(res, { 'GET /widgets without rl headers -> non-2xx': r => r.status < 200 || r.status >= 300 }); - check(j(res), { 'body != stub': b => !eq(b, EXPECTED) }); - console.log('Response body:', res.body); - }); - - // 2) неверный X-Wallarm-Schema-ID - group('NEG wrong X-Wallarm-Schema-ID', () => { - const res = http.get(u('/api-internal/v1/widgets'), { headers: WRONG_SCHEMA_H }); - op.add(res.timings.duration, { path: '/widgets', method: 'GET', neg: 'wrong schema id' }); - check(res, { 'GET /widgets wrong schema -> non-2xx': r => r.status < 200 || r.status >= 300 }); - check(j(res), { 'body != stub': b => !eq(b, EXPECTED) }); - console.log('Response body:', res.body); - }); - - // 3) невалидные query-параметры (limit < minimum, direction неверный) - group('NEG invalid query params', () => { - const res1 = http.get(u('/api-internal/v1/widgets?limit=0'), { headers: H }); // minimum: 1 - op.add(res1.timings.duration, { path: '/widgets', method: 'GET', neg: 'limit=0' }); - check(res1, { 'GET /widgets limit=0 -> 400 (or non-2xx)': r => r.status === 400 || r.status < 200 || r.status >= 300 }); - check(j(res1), { 'body != stub': b => !eq(b, EXPECTED) }); - console.log('Response body:', res1.body); - - const res2 = http.get(u('/api-internal/v1/widgets?direction=SIDEWAYS'), { headers: H }); - op.add(res2.timings.duration, { path: '/widgets', method: 'GET', neg: 'bad direction' }); - check(res2, { 'GET /widgets direction invalid -> 400 (or non-2xx)': r => r.status === 400 || r.status < 200 || r.status >= 300 }); - check(j(res2), { 'body != stub': b => !eq(b, EXPECTED) }); - console.log('Response body:', res2.body); - }); - - // 4) create: отсутствует обязательное поле - group('NEG POST /widgets missing required body fields', () => { - const bad = { name: 'W', description: 'no type' }; // отсутствует type - const res = http.post(u('/api-internal/v1/widgets'), JSON.stringify(bad), { headers: H_JSON }); - op.add(res.timings.duration, { path: '/widgets', method: 'POST', neg: 'missing type' }); - check(res, { 'POST /widgets missing type -> 400 (or non-2xx)': r => r.status === 400 || r.status < 200 || r.status >= 300 }); - check(j(res), { 'body != stub': b => !eq(b, EXPECTED) }); - console.log('Response body:', res.body); - }); - - // 5) update: пустое тело - group('NEG PATCH /widgets/{id} empty body', () => { - const id = uuid(); - const res = http.patch(u(`/api-internal/v1/widgets/${id}`), '', { headers: H_JSON }); - op.add(res.timings.duration, { path: '/widgets/{id}', method: 'PATCH', neg: 'empty body' }); - check(res, { 'PATCH /widgets/{id} empty -> 400/404/non-2xx': r => [400,404].includes(r.status) || r.status < 200 || r.status >= 300 }); - check(j(res), { 'body != stub': b => !eq(b, EXPECTED) }); - console.log('Response body:', res.body); - }); - - // 6) get/delete несуществующего id (ожидаем 404) - group('NEG GET/DELETE nonexistent id -> 404', () => { - const id = 'does-not-exist'; - const resG = http.get(u(`/api-internal/v1/widgets/${encodeURIComponent(id)}`), { headers: H }); - op.add(resG.timings.duration, { path: '/widgets/{id}', method: 'GET', neg: 'nonexistent' }); - check(resG, { 'GET /widgets/{id} -> 404 or non-2xx': r => r.status === 404 || r.status < 200 || r.status >= 300 }); - check(j(resG), { 'body != stub': b => !eq(b, EXPECTED) }); - console.log('Response body:', resG.body); - - const resD = http.del(u(`/api-internal/v1/widgets/${encodeURIComponent(id)}`), null, { headers: H }); - op.add(resD.timings.duration, { path: '/widgets/{id}', method: 'DELETE', neg: 'nonexistent' }); - check(resD, { 'DELETE /widgets/{id} -> 404 or non-2xx': r => r.status === 404 || r.status < 200 || r.status >= 300 }); - if (resD.body) { - check(j(resD), { 'body != stub': b => !eq(b, EXPECTED) }); - console.log('Response body:', resD.body); - } - }); - - // 7) пропущенные обязательные заголовки на mutate-методах - group('NEG missing rl headers on POST/PATCH/DELETE', () => { - const id = uuid(); - - const resP = http.post(u('/api-internal/v1/widgets'), JSON.stringify(createWidgetBody()), { headers: { 'X-Wallarm-Schema-ID': '34', 'Content-Type': 'application/json' } }); - op.add(resP.timings.duration, { path: '/widgets', method: 'POST', neg: 'no rl headers' }); - check(resP, { 'POST /widgets without rl headers -> non-2xx': r => r.status < 200 || r.status >= 300 }); - console.log('Response body:', resP.body); - - const resU = http.patch(u(`/api-internal/v1/widgets/${id}`), JSON.stringify(updateWidgetBody()), { headers: { 'X-Wallarm-Schema-ID': '34', 'Content-Type': 'application/json' } }); - op.add(resU.timings.duration, { path: '/widgets/{id}', method: 'PATCH', neg: 'no rl headers' }); - check(resU, { 'PATCH /widgets/{id} without rl headers -> non-2xx': r => r.status < 200 || r.status >= 300 }); - console.log('Response body:', resU.body); - - const resD = http.del(u(`/api-internal/v1/widgets/${id}`), null, { headers: { 'X-Wallarm-Schema-ID': '34' } }); - op.add(resD.timings.duration, { path: '/widgets/{id}', method: 'DELETE', neg: 'no rl headers' }); - check(resD, { 'DELETE /widgets/{id} without rl headers -> non-2xx': r => r.status < 200 || r.status >= 300 }); - console.log('Response body:', resD.body); - }); -} - -export default function () { - positiveTests(); - negativeTests(); -} \ No newline at end of file diff --git a/resources/test/k6/58.js b/resources/test/k6/58.js deleted file mode 100644 index b991ca0..0000000 --- a/resources/test/k6/58.js +++ /dev/null @@ -1,154 +0,0 @@ -// file: oas31_feature_showcase.test.js -import http from 'k6/http'; -import { check, group } from 'k6'; -import { Trend } from 'k6/metrics'; - -export const options = { - vus: Number(__ENV.VUS || 1), - iterations: Number(__ENV.ITERATIONS || 1), - thresholds: { - http_req_failed: ['rate==0'], - http_req_duration: ['p(95)<1000'], - }, -}; - -const BASE_URL = __ENV.BASE_URL || 'http://localhost:8282'; - -// общие заголовки и куки -const H = { - 'X-Wallarm-Schema-ID': '58', - 'Cookie': 'SESSIONID=dummy', -}; -const H_JSON = { ...H, 'Content-Type': 'application/json' }; - -// ожидаемый стаб -const EXPECTED = { summary: [{ schema_id: 58, status_code: 200 }] }; - -function j(r) { try { return r.json(); } catch { return null; } } -function eq(a, b) { return JSON.stringify(a) === JSON.stringify(b); } -function u(p) { return `${BASE_URL}${p}`; } - -const op = new Trend('oas31_op_ms'); - -// ------------------ Позитивные кейсы ------------------ -function positives() { - group('POSITIVE: /v1/orders (JSON Schema 2020-12 features)', () => { - const body = { - id: 'ord-1', - status: 'NEW', // const - note: 'deliver asap', // becomes required when flags.express = true (if/then) - items: [ - { sku: 'SKU-1', qty: 1 } // unevaluatedProperties: false enforced in item - ], - flags: { express: true } // triggers if/then -> note required - }; - const res = http.post(u('/v1/orders'), JSON.stringify(body), { headers: H_JSON }); - op.add(res.timings.duration, { path: '/v1/orders', method: 'POST', kind: 'positive' }); - const bodyJson = j(res); - check(res, { '200 /v1/orders': (r) => r.status === 200 }); - check(bodyJson, { 'expected stub /v1/orders': (b) => eq(b, EXPECTED) }); - console.log('Response body:', res.body); - }); - - group('POSITIVE: /v1/orders/{id} (deepObject + allowReserved)', () => { - const params = '?filter[date][gt]=2025-01-01&filter[date][lt]=2025-12-31&search=[abc]{def}'; - const res = http.get(u(`/v1/orders/ord-1${params}`), { headers: H }); - op.add(res.timings.duration, { path: '/v1/orders/{id}', method: 'GET', kind: 'positive' }); - const bodyJson = j(res); - check(res, { '200 /v1/orders/{id}': (r) => r.status === 200 }); - check(bodyJson, { 'expected stub /v1/orders/{id}': (b) => eq(b, EXPECTED) }); - console.log('Response body:', res.body); - }); - - group('POSITIVE: /v1/profile (JSON and merge-patch)', () => { - const resJson = http.patch(u('/v1/profile'), JSON.stringify({ nickname: 'Nick', age: 33 }), { headers: H_JSON }); - op.add(resJson.timings.duration, { path: '/v1/profile', method: 'PATCH', kind: 'positive-json' }); - check(resJson, { '200 /v1/profile JSON': (r) => r.status === 200 }); - check(j(resJson), { 'expected stub /v1/profile JSON': (b) => eq(b, EXPECTED) }); - console.log('Response body:', resJson.body); - - const resPatch = http.patch(u('/v1/profile'), JSON.stringify({ nickname: null }), { headers: { ...H, 'Content-Type': 'application/merge-patch+json' } }); - op.add(resPatch.timings.duration, { path: '/v1/profile', method: 'PATCH', kind: 'positive-merge' }); - check(resPatch, { '200 /v1/profile merge-patch': (r) => r.status === 200 }); - check(j(resPatch), { 'expected stub /v1/profile merge-patch': (b) => eq(b, EXPECTED) }); - console.log('Response body:', resPatch.body); - }); - - group('POSITIVE: /v1/refsibling ($ref with siblings)', () => { - // $ref -> codeBase + minLength sibling - const res = http.post(u('/v1/refsibling'), JSON.stringify({ code: 'ABC_1' }), { headers: H_JSON }); - op.add(res.timings.duration, { path: '/v1/refsibling', method: 'POST', kind: 'positive' }); - check(res, { '200 /v1/refsibling': (r) => r.status === 200 }); - check(j(res), { 'expected stub /v1/refsibling': (b) => eq(b, EXPECTED) }); - console.log('Response body:', res.body); - }); -} - -// ------------------ Негативные кейсы ------------------ -function negatives() { - group('NEGATIVE: /v1/orders -> const violation (status != NEW)', () => { - const body = { - id: 'ord-2', - status: 'OLD', // должно быть NEW - items: [{ sku: 'SKU-1', qty: 1 }], - flags: { express: false } - }; - const res = http.post(u('/v1/orders'), JSON.stringify(body), { headers: H_JSON }); - op.add(res.timings.duration, { path: '/v1/orders', method: 'POST', kind: 'neg-const' }); - const bj = j(res); - check(res, { 'non-200 on const violation': (r) => r.status !== 200 }); - check(bj, { 'body != stub on const violation': (b) => !eq(b, EXPECTED) }); - }); - - group('NEGATIVE: /v1/orders -> if/then/else (express=true but note missing)', () => { - const body = { - id: 'ord-3', - status: 'NEW', - items: [{ sku: 'SKU-1', qty: 1 }], - flags: { express: true } // note отсутствует - }; - const res = http.post(u('/v1/orders'), JSON.stringify(body), { headers: H_JSON }); - op.add(res.timings.duration, { path: '/v1/orders', method: 'POST', kind: 'neg-if-then' }); - check(res, { 'non-200 on if/then violation': (r) => r.status !== 200 }); - check(j(res), { 'body != stub on if/then violation': (b) => !eq(b, EXPECTED) }); - }); - - group('NEGATIVE: /v1/orders -> unevaluatedProperties (extra field in item)', () => { - const body = { - id: 'ord-4', - status: 'NEW', - items: [{ sku: 'SKU-1', qty: 1, extra: 'nope' }], // лишнее поле - }; - const res = http.post(u('/v1/orders'), JSON.stringify(body), { headers: H_JSON }); - op.add(res.timings.duration, { path: '/v1/orders', method: 'POST', kind: 'neg-unevaluated' }); - check(res, { 'non-200 on unevaluatedProperties': (r) => r.status !== 200 }); - check(j(res), { 'body != stub on unevaluatedProperties': (b) => !eq(b, EXPECTED) }); - }); - - group('NEGATIVE: /v1/profile -> wrong type (age < 0)', () => { - const res = http.patch(u('/v1/profile'), JSON.stringify({ age: -1 }), { headers: H_JSON }); - op.add(res.timings.duration, { path: '/v1/profile', method: 'PATCH', kind: 'neg-age' }); - check(res, { 'non-200 on min violation': (r) => r.status !== 200 }); - check(j(res), { 'body != stub on min violation': (b) => !eq(b, EXPECTED) }); - }); - - group('NEGATIVE: /v1/refsibling -> $ref sibling minLength not satisfied', () => { - const res = http.post(u('/v1/refsibling'), JSON.stringify({ code: 'A1' }), { headers: H_JSON }); // слишком короткий - op.add(res.timings.duration, { path: '/v1/refsibling', method: 'POST', kind: 'neg-ref-sibling' }); - check(res, { 'non-200 on ref+minLength violation': (r) => r.status !== 200 }); - check(j(res), { 'body != stub on ref+minLength violation': (b) => !eq(b, EXPECTED) }); - - }); - - group('NEGATIVE: missing Cookie SESSIONID (security)', () => { - const res = http.get(u('/v1/orders/ord-1?search=[abc]'), { headers: { 'X-Wallarm-Schema-ID': '999' } }); - op.add(res.timings.duration, { path: '/v1/orders/{id}', method: 'GET', kind: 'neg-missing-cookie' }); - check(res, { 'non-200 without cookie': (r) => r.status !== 200 }); - check(j(res), { 'body != stub without cookie': (b) => !eq(b, EXPECTED) }); - }); -} - -export default function () { - positives(); - negatives(); -} \ No newline at end of file diff --git a/resources/test/k6/59.js b/resources/test/k6/59.js deleted file mode 100644 index 433dc1a..0000000 --- a/resources/test/k6/59.js +++ /dev/null @@ -1,148 +0,0 @@ -// file: kotlin_service_template_schema34_allof_oneof.test.js -import http from 'k6/http'; -import { check, group } from 'k6'; - -export const options = { - vus: Number(__ENV.VUS || 1), - iterations: Number(__ENV.ITERATIONS || 1), -}; - -const BASE_URL = __ENV.BASE_URL || 'http://localhost:8282'; -const H = { - 'X-Wallarm-Schema-ID': '59', - 'rl-tenant-id': __ENV.RL_TENANT_ID || 'tenant-1', - 'rl-user-id': __ENV.RL_USER_ID || 'user-1', -}; -const H_JSON = { ...H, 'Content-Type': 'application/json' }; - -// Стаб, который возвращают мок/фильтр Wallarm. Если видим его — пропускаем проверки формы виджета -const EXPECTED_STUB = { summary: [{ schema_id: 59, status_code: 200 }] }; - -function j(r) { try { return r.json(); } catch { return null; } } -function eq(a, b) { return JSON.stringify(a) === JSON.stringify(b); } -function u(p) { return `${BASE_URL}${p}`; } -function uuid() { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c=>{ - const r=(Math.random()*16)|0, v=c==='x'?r:(r&0x3|0x8); return v.toString(16); - }); -} - -// -------- Helpers to validate allOf(widgetId + widgetProperties) -------- -function isString(x) { return typeof x === 'string' && x.length >= 0; } -function checkAllOfWidget(obj) { - // widget = allOf(widgetId, widgetProperties) + required: id, name, type, description - return obj - && isString(obj.id) - && isString(obj.name) - && isString(obj.type) - && isString(obj.description); -} - -// -------- Bodies -------- -const createBody = (overrides={}) => ({ - name: 'Widget A', - type: 'BASIC', - description: 'Example', - ...overrides, -}); -const updateBody = (overrides={}) => ({ - description: 'Updated', - ...overrides, -}); - -// =================== POSITIVE: проверка allOf =================== -export default function () { - group('ALLOF – Create -> Get -> Update -> Delete', () => { - // POST /widgets (Create) - const rCreate = http.post(u('/api-internal/v1/widgets'), JSON.stringify(createBody()), { headers: H_JSON }); - check(rCreate, { - 'POST /widgets 201/200': r => r.status === 201 || r.status === 200, - }); - console.log('Response body:', rCreate.body); - - const bjCreate = j(rCreate); - if (bjCreate && !eq(bjCreate, EXPECTED_STUB)) { - // Если не стаб — проверяем allOf - check(bjCreate, { 'create matches allOf widget': b => checkAllOfWidget(b) }); - } - - // Вытащим id если сервер реально вернул виджет, иначе сгенерим - const id = (bjCreate && bjCreate.id && !eq(bjCreate, EXPECTED_STUB)) ? bjCreate.id : uuid(); - - // GET /widgets/{id} - const rGet = http.get(u(`/api-internal/v1/widgets/${encodeURIComponent(id)}`), { headers: H }); - check(rGet, { 'GET /widgets/{id} 200/404': r => [200,404].includes(r.status) }); - const bjGet = j(rGet); - if (rGet.status === 200 && bjGet && !eq(bjGet, EXPECTED_STUB)) { - check(bjGet, { 'get matches allOf widget': b => checkAllOfWidget(b) }); - } - console.log('Response body:', rGet.body); - - // PATCH /widgets/{id} - const rPatch = http.patch(u(`/api-internal/v1/widgets/${encodeURIComponent(id)}`), JSON.stringify(updateBody()), { headers: H_JSON }); - check(rPatch, { 'PATCH /widgets/{id} 200/404': r => [200,404].includes(r.status) }); - const bjPatch = j(rPatch); - if (rPatch.status === 200 && bjPatch && !eq(bjPatch, EXPECTED_STUB)) { - check(bjPatch, { 'patch matches allOf widget': b => checkAllOfWidget(b) }); - } - console.log('Response body:', rPatch.body); - - // DELETE /widgets/{id} - const rDel = http.del(u(`/api-internal/v1/widgets/${encodeURIComponent(id)}`), null, { headers: H }); - check(rDel, { 'DELETE /widgets/{id} 204/200/404': r => [204,200,404].includes(r.status) }); - console.log('Response body:', rDel.body); - }); - - // =================== NEGATIVE: нарушаем allOf (required из обеих частей) =================== - group('ALLOF – Negative create (missing required fields)', () => { - // Нет type - const r1 = http.post(u('/api-internal/v1/widgets'), JSON.stringify(createBody({ type: undefined })), { headers: H_JSON }); - check(r1, { 'missing type -> 400/non-2xx': r => r.status === 400 || r.status < 200 || r.status >= 300 }); - console.log('Response body:', r1.body); - - // Нет name - const r2 = http.post(u('/api-internal/v1/widgets'), JSON.stringify(createBody({ name: undefined })), { headers: H_JSON }); - check(r2, { 'missing name -> 400/non-2xx': r => r.status === 400 || r.status < 200 || r.status >= 300 }); - console.log('Response body:', r2.body); - - // Нет description - const r3 = http.post(u('/api-internal/v1/widgets'), JSON.stringify(createBody({ description: undefined })), { headers: H_JSON }); - check(r3, { 'missing description -> 400/non-2xx': r => r.status === 400 || r.status < 200 || r.status >= 300 }); - console.log('Response body:', r3.body); - }); - - // =================== ONEOF: примеры (в текущей спеки НЕТ oneOf) =================== - // Ниже — шаблон, как тестировать oneOf, если добавишь схему с oneOf в components. - /* - // Пример схемы (для справки, не исполняется): - // components: - // schemas: - // widgetUpsert: - // oneOf: - // - $ref: '#/components/schemas/widgetCreate' # required: name,type,description - // - $ref: '#/components/schemas/widgetUpdate' # required: id + (любые свойства) - // - // Тесты: - group('ONEOF – Positive branch #1 (create variant)', () => { - const body = { name: 'W', type: 'BASIC', description: 'D' }; // соответствует widgetCreate - const r = http.post(u('/api-internal/v1/widgets/upsert'), JSON.stringify(body), { headers: H_JSON }); - check(r, { 'oneOf create branch -> 200/201': rr => [200,201].includes(rr.status) }); - }); - - group('ONEOF – Positive branch #2 (update variant)', () => { - const body = { id: uuid(), description: 'upd only' }; // соответствует widgetUpdate - const r = http.post(u('/api-internal/v1/widgets/upsert'), JSON.stringify(body), { headers: H_JSON }); - check(r, { 'oneOf update branch -> 200/201': rr => [200,201].includes(rr.status) }); - }); - - group('ONEOF – Negative (matches none or multiple)', () => { - // 1) Не подходит ни под одну ветку: нет обязательных полей обеих веток - const rBad1 = http.post(u('/api-internal/v1/widgets/upsert'), JSON.stringify({ foo: 'bar' }), { headers: H_JSON }); - check(rBad1, { 'oneOf none -> 400/non-2xx': rr => rr.status === 400 || rr.status < 200 || rr.status >= 300 }); - - // 2) Подходит под обе ветки (если такое возможно по схеме) — сервер должен отбраковать - const rBad2 = http.post(u('/api-internal/v1/widgets/upsert'), JSON.stringify({ id: uuid(), name: 'W', type: 'BASIC', description: 'D' }), { headers: H_JSON }); - check(rBad2, { 'oneOf multiple -> 400/non-2xx': rr => rr.status === 400 || rr.status < 200 || rr.status >= 300 }); - }); - */ -} \ No newline at end of file diff --git a/resources/test/k6/doe_v2_account_authorization_validations.test.js b/resources/test/k6/doe_v2_account_authorization_validations.test.js deleted file mode 100644 index a6cde71..0000000 --- a/resources/test/k6/doe_v2_account_authorization_validations.test.js +++ /dev/null @@ -1,85 +0,0 @@ -// doe_v2_account_authorization_validations.test.js -import http from 'k6/http'; -import { check, group, sleep } from 'k6'; -import { Trend } from 'k6/metrics'; - -export const options = { - vus: Number(__ENV.VUS || 1), - iterations: Number(__ENV.ITERATIONS || 1), - thresholds: { - http_req_failed: ['rate==0'], - http_req_duration: ['p(95)<800'], - 'doe_v2_get_duration': ['p(95)<600'], - 'doe_v2_post_duration': ['p(95)<600'], - }, -}; - -const BASE_URL = __ENV.BASE_URL || 'http://localhost:8282'; -const TEST_DATE = __ENV.TEST_DATE || new Date().toISOString().slice(0, 10); - -const postTrend = new Trend('doe_v2_post_duration'); -const getTrend = new Trend('doe_v2_get_duration'); - -// Общие заголовки (по требованию) -const COMMON_HEADERS_JSON = { - 'Content-Type': 'application/json', - 'X-Wallarm-Schema-ID': '29', -}; -const COMMON_HEADERS = { - 'X-Wallarm-Schema-ID': '29', -}; - -function safeJson(res) { - try { return res.json(); } catch { return null; } -} -function isString(v) { return typeof v === 'string'; } -function looksLikeUrl(s) { return isString(s) && /^(https?:\/\/|\/)/i.test(s); } - -export default function () { - group('POST /api-internal/custodian-data/v1/account-authorization-validations', () => { - const endpoint = `${BASE_URL}/api-internal/custodian-data/v1/account-authorization-validations`; - const payload = { date: TEST_DATE }; - - const res = http.post(endpoint, JSON.stringify(payload), { - headers: COMMON_HEADERS_JSON, - tags: { service: 'doe-v2', operationId: 'account-authorization-validations-post' }, - }); - postTrend.add(res.timings.duration); - - check(res, { - 'POST status 200': (r) => r.status === 200, - 'POST content-type JSON': (r) => (r.headers['Content-Type'] || '').includes('application/json'), - }); - - const json = safeJson(res); - check(json, { - 'POST body is object': (j) => j && typeof j === 'object', - 'POST has date': (j) => j && isString(j.date), - 'POST date echoes input': (j) => j && j.date === TEST_DATE, - }); - }); - - sleep(0.2); - - group('GET /api-internal/custodian-data/v1/account-authorization-validations/{date}', () => { - const endpoint = `${BASE_URL}/api-internal/custodian-data/v1/account-authorization-validations/${encodeURIComponent(TEST_DATE)}`; - const res = http.get(endpoint, { - headers: COMMON_HEADERS, - tags: { service: 'doe-v2', operationId: 'account-authorization-validations-get' }, - }); - getTrend.add(res.timings.duration); - - check(res, { - 'GET status 200': (r) => r.status === 200, - 'GET content-type JSON': (r) => (r.headers['Content-Type'] || '').includes('application/json'), - }); - - const json = safeJson(res); - check(json, { - 'GET body is object': (j) => j && typeof j === 'object', - 'has failureReportUrl': (j) => j && looksLikeUrl(j.failureReportUrl), - 'has successReportUrl': (j) => j && looksLikeUrl(j.successReportUrl), - 'has summaryReportUrl': (j) => j && looksLikeUrl(j.summaryReportUrl), - }); - }); -} \ No newline at end of file From dd5f16273c9c3086bed9423ab012f756d0feaa4e Mon Sep 17 00:00:00 2001 From: Nikolay Tkachenko Date: Sat, 29 Nov 2025 01:39:06 +0200 Subject: [PATCH 3/3] Update release notes --- docs/release-notes.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index f4a5737..543fe36 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,7 +4,8 @@ This page describes new releases of Wallarm API Firewall. ## v0.9.4 (2025-11-28) -* Dependency upgrade +* Upgrade Go to 1.24.10 +* Upgrade dependencies ## v0.9.3 (2025-08-15)