From 69247b090197cb49b159dd68277002417fbab84f Mon Sep 17 00:00:00 2001 From: Theo Nam Truong Date: Wed, 29 May 2024 16:09:22 -0600 Subject: [PATCH] Spec Test Framework (#299) Signed-off-by: Theo Truong --- .github/opensearch-cluster/docker-compose.yml | 11 + .github/workflows/test-spec.yaml | 45 ++ json_schemas/test_story.schema.yaml | 146 ++++ package-lock.json | 691 +++++++++++++++++- package.json | 4 +- spec/namespaces/indices.yaml | 6 + tests/index_lifecycle.yaml | 75 ++ tools/src/tester/ChapterEvaluator.ts | 77 ++ tools/src/tester/ChapterReader.ts | 54 ++ tools/src/tester/ResultsDisplayer.ts | 116 +++ tools/src/tester/SchemaValidator.ts | 24 + tools/src/tester/SharedResources.ts | 32 + tools/src/tester/SpecParser.ts | 40 + tools/src/tester/StoryEvaluator.ts | 82 +++ tools/src/tester/TestsRunner.ts | 59 ++ tools/src/tester/_generate_story_types.ts | 29 + tools/src/tester/helpers.ts | 8 + tools/src/tester/start.ts | 23 + tools/src/tester/types/eval.types.ts | 38 + tools/src/tester/types/spec.types.ts | 26 + tools/src/tester/types/story.types.ts | 111 +++ 21 files changed, 1693 insertions(+), 4 deletions(-) create mode 100644 .github/opensearch-cluster/docker-compose.yml create mode 100644 .github/workflows/test-spec.yaml create mode 100644 json_schemas/test_story.schema.yaml create mode 100644 tests/index_lifecycle.yaml create mode 100644 tools/src/tester/ChapterEvaluator.ts create mode 100644 tools/src/tester/ChapterReader.ts create mode 100644 tools/src/tester/ResultsDisplayer.ts create mode 100644 tools/src/tester/SchemaValidator.ts create mode 100644 tools/src/tester/SharedResources.ts create mode 100644 tools/src/tester/SpecParser.ts create mode 100644 tools/src/tester/StoryEvaluator.ts create mode 100644 tools/src/tester/TestsRunner.ts create mode 100644 tools/src/tester/_generate_story_types.ts create mode 100644 tools/src/tester/helpers.ts create mode 100644 tools/src/tester/start.ts create mode 100644 tools/src/tester/types/eval.types.ts create mode 100644 tools/src/tester/types/spec.types.ts create mode 100644 tools/src/tester/types/story.types.ts diff --git a/.github/opensearch-cluster/docker-compose.yml b/.github/opensearch-cluster/docker-compose.yml new file mode 100644 index 000000000..8b87ae8d6 --- /dev/null +++ b/.github/opensearch-cluster/docker-compose.yml @@ -0,0 +1,11 @@ +version: '3' + +services: + opensearch-cluster: + image: opensearchproject/opensearch:${OPENSEARCH_VERSION} + ports: + - "9200:9200" + - "9600:9600" + environment: + - "discovery.type=single-node" + - "OPENSEARCH_INITIAL_ADMIN_PASSWORD=${OPENSEARCH_PASSWORD}" \ No newline at end of file diff --git a/.github/workflows/test-spec.yaml b/.github/workflows/test-spec.yaml new file mode 100644 index 000000000..64f27393b --- /dev/null +++ b/.github/workflows/test-spec.yaml @@ -0,0 +1,45 @@ +name: Test the OS cluster against the OpenSearch Spec + +on: + push: + branches: [ '**' ] + paths: + - 'package*.json' + - 'tsconfig.json' + - 'tools/tester/**' + - 'spec/**' + pull_request: + branches: [ '**' ] + paths: + - 'package*.json' + - 'tsconfig.json' + - 'tools/tester/**' + - 'spec/**' + +jobs: + test-opensearch-spec: + runs-on: ubuntu-latest + env: + OPENSEARCH_VERSION: 2.12.0 + OPENSEARCH_PASSWORD: myStrongPassword123! + OPENSEARCH_URL: https://localhost:9200 + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '20' + + - name: Install Dependencies + run: npm install + + - name: Run OpenSearch Cluster + working-directory: .github/opensearch-cluster + run: | + docker-compose up -d + sleep 60 + + - name: Run Tests + run: npm run test:spec \ No newline at end of file diff --git a/json_schemas/test_story.schema.yaml b/json_schemas/test_story.schema.yaml new file mode 100644 index 000000000..55fcb6cc1 --- /dev/null +++ b/json_schemas/test_story.schema.yaml @@ -0,0 +1,146 @@ +$schema: http://json-schema.org/draft-07/schema# + +type: object +properties: + $schema: + type: string + skip: + type: boolean + description: If true, the story will be skipped. + default: false + description: + type: string + prologues: + type: array + items: + $ref: '#/definitions/SupplementalChapter' + epilogues: + type: array + items: + $ref: '#/definitions/SupplementalChapter' + chapters: + type: array + items: + $ref: '#/definitions/Chapter' + minItems: 1 +required: [ description, chapters] +additionalProperties: false + +definitions: + Chapter: + type: object + allOf: + - $ref: '#/definitions/ChapterRequest' + - properties: + synopsis: + type: string + description: A brief description of the chapter. + response: + $ref: '#/definitions/ExpectedResponse' + required: [ synopsis ] + additionalProperties: false + + ReadChapter: + allOf: + - $ref: '#/definitions/Chapter' + - type: object + properties: + response: + $ref: '#/definitions/ActualResponse' + required: [ response ] + additionalProperties: false + + SupplementalChapter: + allOf: + - $ref: '#/definitions/ChapterRequest' + - type: object + properties: + status: + description: Array of success HTTP status codes. Default to [200, 201]. + type: array + default: [200, 201] + items: + type: integer + additionalProperties: false + + ChapterRequest: + type: object + properties: + path: + type: string + method: + type: string + enum: [ GET, PUT, POST, DELETE, PATCH, HEAD, OPTIONS ] + parameters: + type: object + additionalProperties: + $ref: '#/definitions/Parameter' + request_body: + $ref: '#/definitions/RequestBody' + required: [ path, method ] + additionalProperties: false + + + RequestBody: + type: object + properties: + content_type: + type: string + default: application/json + payload: + $ref: '#/definitions/Payload' + required: [ payload ] + additionalProperties: false + + ExpectedResponse: + type: object + properties: + status: + type: integer + description: The expected HTTP status code. Default to 200. + default: 200 + content_type: + type: string + default: application/json + payload: + $ref: '#/definitions/Payload' + required: [ status ] + additionalProperties: false + + ActualResponse: + type: object + properties: + status: + type: integer + content_type: + type: string + payload: + $ref: '#/definitions/Payload' + message: + type: string + description: Error message for non 2XX responses. + error: + type: object + description: Error object. + required: [ status, content_type, payload ] + additionalProperties: false + + Payload: + anyOf: + - type: object + - type: array + - type: string + - type: number + - type: boolean + + Parameter: + anyOf: + - type: array + items: + oneOf: + - type: string + - type: number + - type: boolean + - type: string + - type: number + - type: boolean \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index ab4e82682..f4b1b6149 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "eslint-plugin-promise": "^6.1.1", "globals": "^15.0.0", "jest": "^29.7.0", + "json-schema-to-typescript": "^14.0.4", "ts-jest": "^29.1.2" } }, @@ -899,6 +900,102 @@ "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "dev": true }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -1288,6 +1385,16 @@ "node": ">= 8" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -1429,9 +1536,9 @@ "dev": true }, "node_modules/@types/lodash": { - "version": "4.14.202", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", - "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==" + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha512-wYCP26ZLxaT3R39kiN2+HcJ4kTd3U1waI/cY7ivWYqFP6pW3ZNpvi6Wd6PHZx7T/t8z0vlkXMg3QYLa7DZ/IJQ==" }, "node_modules/@types/node": { "version": "20.11.20", @@ -1898,6 +2005,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -2433,6 +2546,22 @@ "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", "dev": true }, + "node_modules/cli-color": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.4.tgz", + "integrity": "sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==", + "dev": true, + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.64", + "es6-iterator": "^2.0.3", + "memoizee": "^0.4.15", + "timers-ext": "^0.1.7" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -2552,6 +2681,28 @@ "node": ">= 8" } }, + "node_modules/d": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", + "dev": true, + "dependencies": { + "es5-ext": "^0.10.64", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, "node_modules/data-view-buffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", @@ -2741,6 +2892,12 @@ "node": ">=6.0.0" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, "node_modules/electron-to-chromium": { "version": "1.4.683", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.683.tgz", @@ -2907,6 +3064,58 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es5-ext": { + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "dev": true, + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", + "dev": true, + "dependencies": { + "d": "^1.0.2", + "ext": "^1.7.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/es6-weak-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "dev": true, + "dependencies": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, "node_modules/escalade": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", @@ -3496,6 +3705,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "dev": true, + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/espree": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/espree/-/espree-10.0.1.tgz", @@ -3579,6 +3803,16 @@ "node": ">=0.10.0" } }, + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "dev": true, + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -3627,6 +3861,15 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "dev": true, + "dependencies": { + "type": "^2.7.2" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3690,6 +3933,29 @@ "bser": "2.1.1" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -3775,6 +4041,34 @@ "is-callable": "^1.1.3" } }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -3788,6 +4082,18 @@ "node": ">= 6" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -4425,6 +4731,12 @@ "node": ">=8" } }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "dev": true + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -4636,6 +4948,24 @@ "node": ">=8" } }, + "node_modules/jackspeak": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.1.2.tgz", + "integrity": "sha512-kWmLKn2tRtfYMF/BakihVVRzBKOxz4gJMiL2Rj91WnAB5TPZumSH99R/Yf1qE1u4uRimvCSJfm6hnxohXeEXjQ==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", @@ -5266,6 +5596,114 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true }, + "node_modules/json-schema-to-typescript": { + "version": "14.0.4", + "resolved": "https://registry.npmjs.org/json-schema-to-typescript/-/json-schema-to-typescript-14.0.4.tgz", + "integrity": "sha512-covPOp3hrbD+oEcMvDxP5Rh6xNZj7lOTZkXAeQoDyu1PuEl1A6oRZ3Sy05HN11vXXmdJ6gLh5P3Qz0mgMPTzzw==", + "dev": true, + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^11.5.5", + "@types/json-schema": "^7.0.15", + "@types/lodash": "^4.17.0", + "cli-color": "^2.0.4", + "glob": "^10.3.12", + "is-glob": "^4.0.3", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "mkdirp": "^3.0.1", + "mz": "^2.7.0", + "node-fetch": "^3.3.2", + "prettier": "^3.2.5" + }, + "bin": { + "json2ts": "dist/src/cli.js" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/json-schema-to-typescript/node_modules/@apidevtools/json-schema-ref-parser": { + "version": "11.6.2", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.6.2.tgz", + "integrity": "sha512-ENUdLLT04aDbbHCRwfKf8gR67AhV0CdFrOAtk+FcakBAgaq6ds3HLK9X0BCyiFUz8pK9uP+k6YZyJaGG7Mt7vQ==", + "dev": true, + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.15", + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/philsturgeon" + } + }, + "node_modules/json-schema-to-typescript/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/json-schema-to-typescript/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/json-schema-to-typescript/node_modules/glob": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", + "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/json-schema-to-typescript/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-to-typescript/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -5373,6 +5811,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lru-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", + "integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==", + "dev": true, + "dependencies": { + "es5-ext": "~0.10.2" + } + }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -5435,6 +5882,25 @@ "tmpl": "1.0.5" } }, + "node_modules/memoizee": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.17.tgz", + "integrity": "sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==", + "dev": true, + "dependencies": { + "d": "^1.0.2", + "es5-ext": "^0.10.64", + "es6-weak-map": "^2.0.3", + "event-emitter": "^0.3.5", + "is-promise": "^2.2.2", + "lru-queue": "^0.1.0", + "next-tick": "^1.1.0", + "timers-ext": "^0.1.7" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -5512,18 +5978,96 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "dev": true + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dev": true, + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -5557,6 +6101,15 @@ "node": ">=8" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", @@ -5803,6 +6356,31 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -5869,6 +6447,21 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -6301,6 +6894,21 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.trim": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", @@ -6362,6 +6970,19 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", @@ -6436,6 +7057,37 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/timers-ext": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.7.tgz", + "integrity": "sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==", + "dev": true, + "dependencies": { + "es5-ext": "~0.10.46", + "next-tick": "1" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -6626,6 +7278,12 @@ "node": ">=4" } }, + "node_modules/type": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz", + "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==", + "dev": true + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -6830,6 +7488,15 @@ "makeerror": "1.0.12" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6897,6 +7564,24 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index 72263f34a..0c14289ab 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "lint": "eslint .", "lint--fix": "eslint . --fix", "merge": "ts-node tools/src/merger/merge.ts", - "test": "jest" + "test": "jest", + "test:spec": "ts-node tools/src/tester/start.ts" }, "dependencies": { "@apidevtools/swagger-parser": "^10.1.0", @@ -39,6 +40,7 @@ "eslint-plugin-promise": "^6.1.1", "globals": "^15.0.0", "jest": "^29.7.0", + "json-schema-to-typescript": "^14.0.4", "ts-jest": "^29.1.2" } } diff --git a/spec/namespaces/indices.yaml b/spec/namespaces/indices.yaml index 0e10a7a87..dda8a0687 100644 --- a/spec/namespaces/indices.yaml +++ b/spec/namespaces/indices.yaml @@ -918,6 +918,8 @@ paths: responses: '200': $ref: '#/components/responses/indices.exists@200' + '404': + $ref: '#/components/responses/indices.exists@404' put: operationId: indices.create.0 x-operation-group: indices.create @@ -2274,6 +2276,10 @@ components: description: '' content: application/json: {} + indices.exists@404: + description: '' + content: + application/json: {} indices.exists_alias@200: description: '' content: diff --git a/tests/index_lifecycle.yaml b/tests/index_lifecycle.yaml new file mode 100644 index 000000000..4e5bd943d --- /dev/null +++ b/tests/index_lifecycle.yaml @@ -0,0 +1,75 @@ +$schema: ../json_schemas/test_story.schema.yaml + +skip: false +description: This story tests all endpoints relevant the lifecycle of an index, from creation to deletion. +epilogues: + - path: /books + method: DELETE + status: [200, 404] + - path: /games + method: DELETE + status: [200, 404] +chapters: + - synopsis: Create an index named `books` with mappings and settings. + path: /{index} + method: PUT + parameters: + index: books + request_body: + payload: + mappings: + properties: + name: + type: keyword + age: + type: integer + settings: + number_of_shards: 5 + number_of_replicas: 2 + response: + status: 200 + + - synopsis: Create an index named `games` with default settings, + path: /{index} + method: PUT + parameters: + index: games + + - synopsis: Check if the index `books` exists. It should. + path: /{index} + method: HEAD + parameters: + index: books + + - synopsis: Check if the index `movies` exists. It should not. + path: /{index} + method: HEAD + parameters: + index: movies + response: + status: 404 + + - synopsis: Retrieve the mappings and settings of the `books` and `games` indices. + path: /{index} + method: GET + parameters: + index: books,games + flat_settings: true + + - synopsis: Close the `books` index. + path: /{index}/_close + method: POST + parameters: + index: books + + - synopsis: Open the `books` index. + path: /{index}/_open + method: POST + parameters: + index: books + + - synopsis: Delete the `books` and `games` indices. + path: /{index} + method: DELETE + parameters: + index: books,games diff --git a/tools/src/tester/ChapterEvaluator.ts b/tools/src/tester/ChapterEvaluator.ts new file mode 100644 index 000000000..03b3c6497 --- /dev/null +++ b/tools/src/tester/ChapterEvaluator.ts @@ -0,0 +1,77 @@ +import { type Chapter, type ActualResponse } from './types/story.types' +import { type ChapterEvaluation, type Evaluation, Result } from './types/eval.types' +import { type ParsedOperation } from './types/spec.types' +import { overall_result } from './helpers' +import type ChapterReader from './ChapterReader' +import SharedResources from './SharedResources' +import type SpecParser from './SpecParser' +import type SchemaValidator from './SchemaValidator' + +export default class ChapterEvaluator { + chapter: Chapter + skip_payload_evaluation: boolean = false + spec_parser: SpecParser + chapter_reader: ChapterReader + schema_validator: SchemaValidator + + constructor (chapter: Chapter) { + this.chapter = chapter + this.spec_parser = SharedResources.get_instance().spec_parser + this.chapter_reader = SharedResources.get_instance().chapter_reader + this.schema_validator = SharedResources.get_instance().schema_validator + } + + async evaluate (skipped: boolean): Promise { + if (skipped) return { title: this.chapter.synopsis, overall: { result: Result.SKIPPED } } + const operation = this.spec_parser.locate_operation(this.chapter) + const response = await this.chapter_reader.read(this.chapter) + const params = this.#evaluate_parameters(operation) + const request_body = this.#evaluate_request_body(operation) + const status = this.#evaluate_status(response) + const payload = this.#evaluate_payload(operation, response) + return { + title: this.chapter.synopsis, + overall: { result: overall_result(Object.values(params).concat([request_body, status, payload])) }, + request: { parameters: params, requestBody: request_body }, + response: { status, payload } + } + } + + #evaluate_parameters (operation: ParsedOperation): Record { + return Object.fromEntries(Object.entries(this.chapter.parameters ?? {}).map(([name, parameter]) => { + const schema = operation.parameters[name]?.schema + if (schema == null) return [name, { result: Result.FAILED, message: `Schema for "${name}" parameter not found.` }] + const evaluation = this.schema_validator.validate(schema, parameter) + return [name, evaluation] + })) + } + + #evaluate_request_body (operation: ParsedOperation): Evaluation { + if (!this.chapter.request_body) return { result: Result.PASSED } + const content_type = this.chapter.request_body.content_type ?? 'application/json' + const schema = operation.requestBody?.content[content_type]?.schema + if (schema == null) return { result: Result.FAILED, message: `Schema for "${content_type}" request body not found in the spec.` } + return this.schema_validator.validate(schema, this.chapter.request_body?.payload ?? {}) + } + + #evaluate_status (response: ActualResponse): Evaluation { + const expected_status = this.chapter.response?.status ?? 200 + if (response.status === expected_status) return { result: Result.PASSED } + this.skip_payload_evaluation = true + return { + result: Result.ERROR, + message: `Expected status ${expected_status}, but received ${response.status}: ${response.content_type}. ${response.message}`, + error: response.error as Error + } + } + + #evaluate_payload (operation: ParsedOperation, response: ActualResponse): Evaluation { + if (this.skip_payload_evaluation) return { result: Result.SKIPPED } + const content_type = response.content_type ?? 'application/json' + const content = operation.responses[response.status]?.content[content_type] + const schema = content?.schema + if (schema == null && content != null) return { result: Result.PASSED } + if (schema == null) return { result: Result.FAILED, message: `Schema for "${response.status}: ${response.content_type}" response not found in the spec.` } + return this.schema_validator.validate(schema, response.payload) + } +} diff --git a/tools/src/tester/ChapterReader.ts b/tools/src/tester/ChapterReader.ts new file mode 100644 index 000000000..f8a962709 --- /dev/null +++ b/tools/src/tester/ChapterReader.ts @@ -0,0 +1,54 @@ +import axios from 'axios' +import { type ChapterRequest, type ActualResponse, type Parameter } from './types/story.types' +import { Agent } from 'https' + +// A lightweight client for testing the API +export default class ChapterReader { + url: string + admin_password: string + + constructor () { + this.url = process.env.OPENSEARCH_URL ?? 'https://localhost:9200' + if (process.env.OPENSEARCH_PASSWORD == null) throw new Error('OPENSEARCH_PASSWORD is not set') + this.admin_password = process.env.OPENSEARCH_PASSWORD + } + + async read (chapter: ChapterRequest): Promise { + const response: Record = {} + const [url, params] = this.#parse_url(chapter.path, chapter.parameters ?? {}) + await axios.request({ + url, + auth: { + username: 'admin', + password: this.admin_password + }, + httpsAgent: new Agent({ rejectUnauthorized: false }), + method: chapter.method, + params, + data: chapter.request_body?.payload + }).then(r => { + response.status = r.status + response.content_type = r.headers['content-type'].split(';')[0] + response.payload = r.data + }).catch(e => { + if (e.response == null) throw e + response.status = e.response.status + response.content_type = e.response.headers['content-type'].split(';')[0] + response.payload = e.response.data?.error + response.message = e.response.data?.error?.reason + response.error = e + }) + return response as ActualResponse + } + + #parse_url (path: string, parameters: Record): [string, Record] { + const path_params = new Set() + const parsed_path = path.replace(/{(\w+)}/g, (_, key) => { + path_params.add(key as string) + return parameters[key] as string + }) + const query_params = Object.fromEntries(Object.entries(parameters).filter(([key]) => !path_params.has(key))) + const url = this.url + parsed_path + return [url, query_params] + } +} diff --git a/tools/src/tester/ResultsDisplayer.ts b/tools/src/tester/ResultsDisplayer.ts new file mode 100644 index 000000000..feb3a6eb1 --- /dev/null +++ b/tools/src/tester/ResultsDisplayer.ts @@ -0,0 +1,116 @@ +import { type ChapterEvaluation, type Evaluation, Result, type StoryEvaluation } from './types/eval.types' +import { overall_result } from './helpers' + +function b (text: string): string { return `\x1b[1m${text}\x1b[0m` } +function i (text: string): string { return `\x1b[3m${text}\x1b[0m` } + +function padding (text: string, length: number, prefix: number = 0): string { + const spaces = length - text.length > 0 ? ' '.repeat(length - text.length) : '' + return `${' '.repeat(prefix)}${text}${spaces}` +} + +function green (text: string): string { return `\x1b[32m${text}\x1b[0m` } +function red (text: string): string { return `\x1b[31m${text}\x1b[0m` } +function yellow (text: string): string { return `\x1b[33m${text}\x1b[0m` } +function cyan (text: string): string { return `\x1b[36m${text}\x1b[0m` } +function gray (text: string): string { return `\x1b[90m${text}\x1b[0m` } +function magenta (text: string): string { return `\x1b[35m${text}\x1b[0m` } + +export interface DisplayOptions { + tab_size?: number + verbose?: boolean +} + +export default class ResultsDisplayer { + evaluation: StoryEvaluation + skip_components: boolean + tab_size: number + verbose: boolean + + constructor (evaluation: StoryEvaluation, opts: DisplayOptions) { + this.evaluation = evaluation + this.skip_components = [Result.PASSED, Result.SKIPPED].includes(evaluation.result) + this.tab_size = opts.tab_size ?? 4 + this.verbose = opts.verbose ?? false + } + + display (): void { + this.#display_story() + this.#display_chapters(this.evaluation.prologues ?? [], 'PROLOGUES') + this.#display_chapters(this.evaluation.chapters ?? [], 'CHAPTERS') + this.#display_chapters(this.evaluation.epilogues ?? [], 'EPILOGUES') + console.log('\n') + } + + #display_story (): void { + const result = this.evaluation.result + const message = this.evaluation.full_path + const title = cyan(b(this.evaluation.display_path)) + this.#display_evaluation({ result, message }, title) + } + + #display_chapters (evaluations: ChapterEvaluation[], title: string): void { + if (this.skip_components || evaluations.length === 0) return + const result = overall_result(evaluations.map(e => e.overall)) + this.#display_evaluation({ result }, title, this.tab_size) + if (result === Result.PASSED) return + for (const evaluation of evaluations) this.#display_chapter(evaluation) + } + + #display_chapter (chapter: ChapterEvaluation): void { + this.#display_evaluation(chapter.overall, i(chapter.title), this.tab_size * 2) + if (chapter.overall.result === Result.PASSED || chapter.overall.result === Result.SKIPPED) return + + this.#display_parameters(chapter.request?.parameters ?? {}) + this.#display_request_body(chapter.request?.requestBody) + this.#display_status(chapter.response?.status) + this.#display_payload(chapter.response?.payload) + } + + #display_parameters (parameters: Record): void { + if (Object.keys(parameters).length === 0) return + const result = overall_result(Object.values(parameters)) + this.#display_evaluation({ result }, 'PARAMETERS', this.tab_size * 3) + if (result === Result.PASSED) return + for (const [name, evaluation] of Object.entries(parameters)) { + this.#display_evaluation(evaluation, name, this.tab_size * 4) + } + } + + #display_request_body (evaluation: Evaluation | undefined): void { + if (evaluation == null) return + this.#display_evaluation(evaluation, 'REQUEST BODY', this.tab_size * 3) + } + + #display_status (evaluation: Evaluation | undefined): void { + if (evaluation == null) return + this.#display_evaluation(evaluation, 'RESPONSE STATUS', this.tab_size * 3) + } + + #display_payload (evaluation: Evaluation | undefined): void { + if (evaluation == null) return + this.#display_evaluation(evaluation, 'RESPONSE PAYLOAD', this.tab_size * 3) + } + + #display_evaluation (evaluation: Evaluation, title: string, prefix: number = 0): void { + const result = padding(this.#result(evaluation.result), 0, prefix) + const message = evaluation.message != null ? `${gray('(' + evaluation.message + ')')}` : '' + console.log(`${result} ${title} ${message}`) + if (evaluation.error && this.verbose) { + console.log('-'.repeat(100)) + console.error(evaluation.error) + console.log('-'.repeat(100)) + } + } + + #result (r: Result): string { + const text = padding(r, 7) + switch (r) { + case Result.PASSED: return green(text) + case Result.SKIPPED: return yellow(text) + case Result.FAILED: return magenta(text) + case Result.ERROR: return red(text) + default: return gray(text) + } + } +} diff --git a/tools/src/tester/SchemaValidator.ts b/tools/src/tester/SchemaValidator.ts new file mode 100644 index 000000000..4f9c91896 --- /dev/null +++ b/tools/src/tester/SchemaValidator.ts @@ -0,0 +1,24 @@ +import AJV from 'ajv' +import addFormats from 'ajv-formats' +import { type OpenAPIV3 } from 'openapi-types' +import { type Evaluation, Result } from './types/eval.types' + +export default class SchemaValidator { + private readonly ajv: AJV + constructor (spec: OpenAPIV3.Document) { + this.ajv = new AJV() + addFormats(this.ajv) + this.ajv.addKeyword('discriminator') + const schemas = spec.components?.schemas ?? {} + for (const key in schemas) this.ajv.addSchema(schemas[key], `#/components/schemas/${key}`) + } + + validate (schema: OpenAPIV3.SchemaObject, data: any): Evaluation { + const validate = this.ajv.compile(schema) + const valid = validate(data) + return { + result: valid ? Result.PASSED : Result.FAILED, + message: valid ? undefined : this.ajv.errorsText(validate.errors) + } + } +} diff --git a/tools/src/tester/SharedResources.ts b/tools/src/tester/SharedResources.ts new file mode 100644 index 000000000..c03a3fb13 --- /dev/null +++ b/tools/src/tester/SharedResources.ts @@ -0,0 +1,32 @@ +import type ChapterReader from './ChapterReader' +import type SchemaValidator from './SchemaValidator' +import type SpecParser from './SpecParser' + +interface Resources { + chapter_reader: ChapterReader + schema_validator: SchemaValidator + spec_parser: SpecParser +} + +export default class SharedResources { + private static instance: SharedResources | undefined + chapter_reader: ChapterReader + schema_validator: SchemaValidator + spec_parser: SpecParser + + private constructor (resources: Resources) { + this.chapter_reader = resources.chapter_reader + this.schema_validator = resources.schema_validator + this.spec_parser = resources.spec_parser + } + + public static create_instance (resources: Resources): void { + if (SharedResources.instance) throw new Error('SharedResources instance has already been created.') + SharedResources.instance = new SharedResources(resources) + } + + public static get_instance (): SharedResources { + if (SharedResources.instance) return SharedResources.instance + throw new Error('SharedResources instance has not been created.') + } +} diff --git a/tools/src/tester/SpecParser.ts b/tools/src/tester/SpecParser.ts new file mode 100644 index 000000000..ecc171f8e --- /dev/null +++ b/tools/src/tester/SpecParser.ts @@ -0,0 +1,40 @@ +import { type OpenAPIV3 } from 'openapi-types' +import { resolve_ref } from '../../helpers' +import { type Chapter } from './types/story.types' +import { type ParsedOperation } from './types/spec.types' +import _ from 'lodash' + +export default class SpecParser { + private readonly spec: OpenAPIV3.Document + private cached_operations: Record = {} + + constructor (spec: OpenAPIV3.Document) { + this.spec = spec + } + + locate_operation (chapter: Chapter): ParsedOperation { + const path = chapter.path + const method = chapter.method.toLowerCase() as OpenAPIV3.HttpMethods + const cache_key = path + method + if (this.cached_operations[cache_key] != null) return this.cached_operations[cache_key] + const operation = this.spec.paths[path]?.[method] + if (operation == null) throw new Error(`Operation "${method.toUpperCase()} ${path}" not found in the spec.`) + this.#deref(operation) + const parameters = _.keyBy(operation.parameters ?? [], 'name') + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + this.cached_operations[cache_key] = { ...operation, parameters } as ParsedOperation + return this.cached_operations[cache_key] + } + + // Normalize the operation object by dereferencing all $ref properties of an operation object. + // This includes the parameters, the requestBody, and the responses of the operation. + // This will result in an operation object with only references to #/components/schemas. + // We do not dereference $ref's to #components/schemas themselves, due to cyclic references. + #deref (obj: any): any { + if (obj == null) return obj + if (obj.$ref != null) return resolve_ref(obj.$ref as string, this.spec) + if (Array.isArray(obj)) return obj.map((item) => this.#deref(item)) + if (typeof obj === 'object') for (const key in obj) obj[key] = this.#deref(obj[key]) + return obj + } +} diff --git a/tools/src/tester/StoryEvaluator.ts b/tools/src/tester/StoryEvaluator.ts new file mode 100644 index 000000000..7c049125c --- /dev/null +++ b/tools/src/tester/StoryEvaluator.ts @@ -0,0 +1,82 @@ +import { type Chapter, type Story, type SupplementalChapter } from './types/story.types' +import { type ChapterEvaluation, Result, type StoryEvaluation } from './types/eval.types' +import ChapterEvaluator from './ChapterEvaluator' +import type ChapterReader from './ChapterReader' +import SharedResources from './SharedResources' +import { overall_result } from './helpers' + +export interface StoryFile { + display_path: string + full_path: string + story: Story +} + +export default class StoryEvaluator { + story: Story + display_path: string + full_path: string + has_errors: boolean = false + chapter_reader: ChapterReader + + constructor (story_file: StoryFile) { + this.story = story_file.story + this.display_path = story_file.display_path + this.full_path = story_file.full_path + this.chapter_reader = SharedResources.get_instance().chapter_reader + } + + async evaluate (): Promise { + if (this.story.skip) { + return { + result: Result.SKIPPED, + display_path: this.display_path, + full_path: this.full_path, + description: this.story.description, + chapters: [] + } + } + const prologues = await this.#evaluate_supplemental_chapters(this.story.prologues ?? []) + const chapters = await this.#evaluate_chapters(this.story.chapters) + const epilogues = await this.#evaluate_supplemental_chapters(this.story.epilogues ?? []) + return { + display_path: this.display_path, + full_path: this.full_path, + description: this.story.description, + chapters, + prologues, + epilogues, + result: overall_result(prologues.concat(chapters).concat(epilogues).concat(prologues).map(e => e.overall)) + } + } + + async #evaluate_chapters (chapters: Chapter[]): Promise { + if (this.has_errors) return [] + let has_errors: boolean = this.has_errors + + const evaluations: ChapterEvaluation[] = [] + + for (const chapter of chapters) { + const evaluator = new ChapterEvaluator(chapter) + const evaluation = await evaluator.evaluate(has_errors) + has_errors = has_errors || evaluation.overall.result === Result.ERROR + evaluations.push(evaluation) + } + + return evaluations + } + + async #evaluate_supplemental_chapters (chapters: SupplementalChapter[]): Promise { + const evaluations: ChapterEvaluation[] = [] + for (const chapter of chapters) { + const title = `${chapter.method} ${chapter.path}` + const response = await this.chapter_reader.read(chapter) + const status = chapter.status ?? [] + if (status.includes(response.status)) evaluations.push({ title, overall: { result: Result.PASSED } }) + else { + this.has_errors = true + evaluations.push({ title, overall: { result: Result.ERROR, message: response.message, error: response.error as Error } }) + } + } + return evaluations + } +} diff --git a/tools/src/tester/TestsRunner.ts b/tools/src/tester/TestsRunner.ts new file mode 100644 index 000000000..a5ba0689f --- /dev/null +++ b/tools/src/tester/TestsRunner.ts @@ -0,0 +1,59 @@ +import { type OpenAPIV3 } from 'openapi-types' +import SpecParser from './SpecParser' +import ChapterReader from './ChapterReader' +import SchemaValidator from './SchemaValidator' +import StoryEvaluator, { type StoryFile } from './StoryEvaluator' +import fs from 'fs' +import { type Story } from './types/story.types' +import { read_yaml } from '../../helpers' +import { Result } from './types/eval.types' +import ResultsDisplayer, { type DisplayOptions } from './ResultsDisplayer' +import SharedResources from './SharedResources' +import { resolve, basename } from 'path' + +type TestsRunnerOptions = DisplayOptions & Record + +export default class TestsRunner { + path: string // Path to a story file or a directory containing story files + opts: TestsRunnerOptions + + constructor (spec: OpenAPIV3.Document, path: string, opts: TestsRunnerOptions) { + this.path = resolve(path) + this.opts = opts + + const chapter_reader = new ChapterReader() + const spec_parser = new SpecParser(spec) + const schema_validator = new SchemaValidator(spec) + SharedResources.create_instance({ chapter_reader, schema_validator, spec_parser }) + } + + async run (): Promise { + let failed = false + const story_files = this.#collect_story_files(this.path, '', '').sort((a, b) => a.display_path.localeCompare(b.display_path)) + for (const story_file of story_files) { + const evaluator = new StoryEvaluator(story_file) + const evaluation = await evaluator.evaluate() + const displayer = new ResultsDisplayer(evaluation, this.opts) + displayer.display() + if ([Result.ERROR, Result.FAILED].includes(evaluation.result)) failed = true + } + if (failed) process.exit(1) + } + + #collect_story_files (folder: string, file: string, prefix: string): StoryFile[] { + const path = file === '' ? folder : `${folder}/${file}` + const next_prefix = prefix === '' ? file : `${prefix}/${file}` + if (fs.statSync(path).isFile()) { + const story: Story = read_yaml(path) + return [{ + display_path: next_prefix === '' ? basename(path) : next_prefix, + full_path: path, + story + }] + } else { + return fs.readdirSync(path).flatMap(next_file => { + return this.#collect_story_files(path, next_file, next_prefix) + }) + } + } +} diff --git a/tools/src/tester/_generate_story_types.ts b/tools/src/tester/_generate_story_types.ts new file mode 100644 index 000000000..dc497d288 --- /dev/null +++ b/tools/src/tester/_generate_story_types.ts @@ -0,0 +1,29 @@ +import * as js2ts from 'json-schema-to-typescript' +import fs from 'fs' +import { read_yaml } from '../../helpers' + +const schema = read_yaml('json_schemas/test_story.schema.yaml') +void js2ts.compile(schema, 'Story', + { + cwd: 'json_schemas/', + additionalProperties: false, + ignoreMinAndMaxItems: true, + unreachableDefinitions: true, + declareExternallyReferenced: true, + unknownAny: false, + style: { + singleQuote: true, + singleAttributePerLine: true + }, + // multiline comment + bannerComment: `/* eslint-disable */ + +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file by running: + * "npx ts-node tools/src/tester/_generate_story_types.ts" in a terminal. + */` + + }) + .then(ts => { fs.writeFileSync('tools/src/tester/types/story.types.ts', ts) }) diff --git a/tools/src/tester/helpers.ts b/tools/src/tester/helpers.ts new file mode 100644 index 000000000..1c5e78257 --- /dev/null +++ b/tools/src/tester/helpers.ts @@ -0,0 +1,8 @@ +import { type Evaluation, Result } from './types/eval.types' + +export function overall_result (evaluations: Evaluation[]): Result { + if (evaluations.some(e => e.result === Result.ERROR)) return Result.ERROR + if (evaluations.some(e => e.result === Result.FAILED)) return Result.FAILED + if (evaluations.some(e => e.result === Result.SKIPPED)) return Result.SKIPPED + return Result.PASSED +} diff --git a/tools/src/tester/start.ts b/tools/src/tester/start.ts new file mode 100644 index 000000000..c366a8926 --- /dev/null +++ b/tools/src/tester/start.ts @@ -0,0 +1,23 @@ +import OpenApiMerger from '../merger/OpenApiMerger' +import { LogLevel } from '../Logger' +import TestsRunner from './TestsRunner' +import { Command, Option } from '@commander-js/extra-typings' +import _ from 'lodash' + +const command = new Command() + .description('Run test stories against the OpenSearch spec.') + .addOption(new Option('--spec, --spec_path ', 'path to the root folder of the multi-file spec').default('./spec')) + .addOption(new Option('--tests, --tests_path ', 'path to the root folder of the tests').default('./tests')) + .addOption(new Option('--tab_size ', 'tab size for displayed results').default('4')) + .addOption(new Option('--verbose', 'whether to print the full stack trace of errors')) + .allowExcessArguments(false) + .parse() + +const opts = command.opts() +const display_options = { + verbose: opts.verbose ?? false, + tab_size: Number.parseInt(opts.tab_size) +} +const spec = (new OpenApiMerger(opts.spec_path, LogLevel.error)).merge() +const runner = new TestsRunner(spec, opts.tests_path, display_options) +void runner.run().then(() => { _.noop() }) diff --git a/tools/src/tester/types/eval.types.ts b/tools/src/tester/types/eval.types.ts new file mode 100644 index 000000000..95f9e7af7 --- /dev/null +++ b/tools/src/tester/types/eval.types.ts @@ -0,0 +1,38 @@ +export type LibraryEvaluation = StoryEvaluation[] + +export interface StoryEvaluation { + result: Result + display_path: string + full_path: string + description: string + message?: string + chapters: ChapterEvaluation[] + epilogues?: ChapterEvaluation[] + prologues?: ChapterEvaluation[] +} + +export interface ChapterEvaluation { + title: string + overall: Evaluation + request?: { + parameters?: Record + requestBody?: Evaluation + } + response?: { + status: Evaluation + payload: Evaluation + } +} + +export interface Evaluation { + result: Result + message?: string + error?: Error +} + +export enum Result { + PASSED = 'PASSED', + FAILED = 'FAILED', + SKIPPED = 'SKIPPED', + ERROR = 'ERROR', +} diff --git a/tools/src/tester/types/spec.types.ts b/tools/src/tester/types/spec.types.ts new file mode 100644 index 000000000..6a34db327 --- /dev/null +++ b/tools/src/tester/types/spec.types.ts @@ -0,0 +1,26 @@ +import { type OpenAPIV3 } from 'openapi-types' + +export type ParsedOperation = OpenAPIV3.OperationObject & { + parameters: Record + requestBody: ParsedRequestBody + responses: Record +} + +export type ParsedRequestBody = OpenAPIV3.RequestBodyObject & { + content: Record +} + +export type ParsedResponse = OpenAPIV3.ResponseObject & { + content: Record +} + +export type ParsedMediaType = OpenAPIV3.MediaTypeObject & { + schema: OpenAPIV3.SchemaObject +} + +export interface ParsedParameter { + name: string + in: string + required: boolean + schema: OpenAPIV3.SchemaObject +} diff --git a/tools/src/tester/types/story.types.ts b/tools/src/tester/types/story.types.ts new file mode 100644 index 000000000..344a15880 --- /dev/null +++ b/tools/src/tester/types/story.types.ts @@ -0,0 +1,111 @@ +/* eslint-disable */ + +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file by running: + * "npx ts-node tools/src/tester/_generate_story_types.ts" in a terminal. + */ + +/** + * This interface was referenced by `Story`'s JSON-Schema + * via the `definition` "SupplementalChapter". + */ +export type SupplementalChapter = ChapterRequest & { + /** + * Array of success HTTP status codes. Default to [200, 201]. + */ + status?: number[]; +}; +/** + * This interface was referenced by `Story`'s JSON-Schema + * via the `definition` "Parameter". + */ +export type Parameter = (string | number | boolean)[] | string | number | boolean; +/** + * This interface was referenced by `Story`'s JSON-Schema + * via the `definition` "Payload". + */ +export type Payload = {} | any[] | string | number | boolean; +/** + * This interface was referenced by `Story`'s JSON-Schema + * via the `definition` "Chapter". + */ +export type Chapter = ChapterRequest & { + /** + * A brief description of the chapter. + */ + synopsis: string; + response?: ExpectedResponse; +}; +/** + * This interface was referenced by `Story`'s JSON-Schema + * via the `definition` "ReadChapter". + */ +export type ReadChapter = Chapter & { + response: ActualResponse; +}; + +export interface Story { + $schema?: string; + /** + * If true, the story will be skipped. + */ + skip?: boolean; + description: string; + prologues?: SupplementalChapter[]; + epilogues?: SupplementalChapter[]; + /** + * @minItems 1 + */ + chapters: Chapter[]; +} +/** + * This interface was referenced by `Story`'s JSON-Schema + * via the `definition` "ChapterRequest". + */ +export interface ChapterRequest { + path: string; + method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS'; + parameters?: { + [k: string]: Parameter; + }; + request_body?: RequestBody; +} +/** + * This interface was referenced by `Story`'s JSON-Schema + * via the `definition` "RequestBody". + */ +export interface RequestBody { + content_type?: string; + payload: Payload; +} +/** + * This interface was referenced by `Story`'s JSON-Schema + * via the `definition` "ExpectedResponse". + */ +export interface ExpectedResponse { + /** + * The expected HTTP status code. Default to 200. + */ + status: number; + content_type?: string; + payload?: Payload; +} +/** + * This interface was referenced by `Story`'s JSON-Schema + * via the `definition` "ActualResponse". + */ +export interface ActualResponse { + status: number; + content_type: string; + payload: Payload; + /** + * Error message for non 2XX responses. + */ + message?: string; + /** + * Error object. + */ + error?: {}; +}