From a320c050055c631a690abd0dbaadcc3ce68725ab Mon Sep 17 00:00:00 2001 From: GyeongHo Kim Date: Tue, 6 Jan 2026 20:33:20 +0900 Subject: [PATCH 1/9] test: update fixtures to bru-lang MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 테스트에 사용되는 fixture들이 공식문서의 bru-lang을 따르지 않던 문제 - 모든 HTTP Method를 사용하도록 fixtures 추가 --- .../auth-test-collection/expected-openapi.yml | 114 +++ .../request-with-basic-auth.bru | 16 + .../request-with-bearer-auth.bru | 15 + .../collection-with-bru-file/bruno.json | 6 + .../collection-with-bru-file/collection.bru | 5 + .../expected-openapi.yml | 60 ++ .../simple-request.bru | 10 + .../another-request.bru | 10 + .../expected-openapi.yml | 112 ++ .../simple-request.bru | 10 + tests/fixtures/empty-bru-file/empty.bru | 0 .../empty-bru-file/expected-openapi.yml | 6 + .../empty-collection/expected-openapi.yml | 6 + .../invalid-bruno-json-collection/bruno.json | 5 + .../minimal-bru-file/expected-openapi.yml | 6 + tests/fixtures/minimal-bru-file/minimal.bru | 5 + .../bruno.json | 2 + .../expected-openapi.yml | 7 + .../create-user-form.bru | 21 + .../sample-bruno-collection/create-user.bru | 31 +- .../create-xml-data.bru | 23 + .../sample-bruno-collection/delete-user.bru | 14 + .../expected-openapi.yml | 963 ++++++++++++++++++ .../get-user-by-id.bru | 15 + .../sample-bruno-collection/get-users.bru | 29 +- .../sample-bruno-collection/head-user.bru | 14 + .../sample-bruno-collection/options-user.bru | 14 + .../sample-bruno-collection/patch-user.bru | 21 + .../sample-bruno-collection/search-users.bru | 22 + .../sample-bruno-collection/send-message.bru | 19 + .../sample-bruno-collection/update-user.bru | 23 + .../sample-bruno-collection/upload-file.bru | 19 + 32 files changed, 1597 insertions(+), 26 deletions(-) create mode 100644 tests/fixtures/auth-test-collection/expected-openapi.yml create mode 100644 tests/fixtures/auth-test-collection/request-with-basic-auth.bru create mode 100644 tests/fixtures/auth-test-collection/request-with-bearer-auth.bru create mode 100644 tests/fixtures/collection-with-bru-file/bruno.json create mode 100644 tests/fixtures/collection-with-bru-file/collection.bru create mode 100644 tests/fixtures/collection-with-bru-file/expected-openapi.yml create mode 100644 tests/fixtures/collection-with-bru-file/simple-request.bru create mode 100644 tests/fixtures/collection-without-bruno-json/another-request.bru create mode 100644 tests/fixtures/collection-without-bruno-json/expected-openapi.yml create mode 100644 tests/fixtures/collection-without-bruno-json/simple-request.bru create mode 100644 tests/fixtures/empty-bru-file/empty.bru create mode 100644 tests/fixtures/empty-bru-file/expected-openapi.yml create mode 100644 tests/fixtures/empty-collection/expected-openapi.yml create mode 100644 tests/fixtures/invalid-bruno-json-collection/bruno.json create mode 100644 tests/fixtures/minimal-bru-file/expected-openapi.yml create mode 100644 tests/fixtures/minimal-bru-file/minimal.bru create mode 100644 tests/fixtures/non-object-bruno-json-collection/bruno.json create mode 100644 tests/fixtures/non-object-bruno-json-collection/expected-openapi.yml create mode 100644 tests/fixtures/sample-bruno-collection/create-user-form.bru create mode 100644 tests/fixtures/sample-bruno-collection/create-xml-data.bru create mode 100644 tests/fixtures/sample-bruno-collection/delete-user.bru create mode 100644 tests/fixtures/sample-bruno-collection/expected-openapi.yml create mode 100644 tests/fixtures/sample-bruno-collection/get-user-by-id.bru create mode 100644 tests/fixtures/sample-bruno-collection/head-user.bru create mode 100644 tests/fixtures/sample-bruno-collection/options-user.bru create mode 100644 tests/fixtures/sample-bruno-collection/patch-user.bru create mode 100644 tests/fixtures/sample-bruno-collection/search-users.bru create mode 100644 tests/fixtures/sample-bruno-collection/send-message.bru create mode 100644 tests/fixtures/sample-bruno-collection/update-user.bru create mode 100644 tests/fixtures/sample-bruno-collection/upload-file.bru diff --git a/tests/fixtures/auth-test-collection/expected-openapi.yml b/tests/fixtures/auth-test-collection/expected-openapi.yml new file mode 100644 index 0000000..4d49bbc --- /dev/null +++ b/tests/fixtures/auth-test-collection/expected-openapi.yml @@ -0,0 +1,114 @@ +openapi: 3.0.0 +info: + title: auth-test-collection + version: 1.0.0 + description: 'OpenAPI specification generated from Bruno collection: auth-test-collection' +paths: + /protected: + get: + summary: Request with Basic Auth + description: Operation for Request with Basic Auth + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + description: Default response schema + '400': + description: Bad Request - The request was invalid + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '401': + description: Unauthorized - Authentication required + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '404': + description: Not Found - The requested resource does not exist + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '500': + description: Internal Server Error + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + security: + - basicAuth: [] + /api/data: + get: + summary: Request with Bearer Auth + description: Operation for Request with Bearer Auth + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + description: Default response schema + '400': + description: Bad Request - The request was invalid + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '401': + description: Unauthorized - Authentication required + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '404': + description: Not Found - The requested resource does not exist + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '500': + description: Internal Server Error + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + security: + - bearerAuth: [] diff --git a/tests/fixtures/auth-test-collection/request-with-basic-auth.bru b/tests/fixtures/auth-test-collection/request-with-basic-auth.bru new file mode 100644 index 0000000..5818b83 --- /dev/null +++ b/tests/fixtures/auth-test-collection/request-with-basic-auth.bru @@ -0,0 +1,16 @@ +meta { + name: Request with Basic Auth + type: http + seq: 1 +} + +get { + url: https://api.example.com/protected +} + +auth { + mode: basic + username: admin + password: secret123 +} + diff --git a/tests/fixtures/auth-test-collection/request-with-bearer-auth.bru b/tests/fixtures/auth-test-collection/request-with-bearer-auth.bru new file mode 100644 index 0000000..b75caff --- /dev/null +++ b/tests/fixtures/auth-test-collection/request-with-bearer-auth.bru @@ -0,0 +1,15 @@ +meta { + name: Request with Bearer Auth + type: http + seq: 2 +} + +get { + url: https://api.example.com/api/data +} + +auth { + mode: bearer + token: {{bearerToken}} +} + diff --git a/tests/fixtures/collection-with-bru-file/bruno.json b/tests/fixtures/collection-with-bru-file/bruno.json new file mode 100644 index 0000000..97699fa --- /dev/null +++ b/tests/fixtures/collection-with-bru-file/bruno.json @@ -0,0 +1,6 @@ +{ + "name": "Collection with .bru file", + "version": "1", + "type": "collection" +} + diff --git a/tests/fixtures/collection-with-bru-file/collection.bru b/tests/fixtures/collection-with-bru-file/collection.bru new file mode 100644 index 0000000..493cf81 --- /dev/null +++ b/tests/fixtures/collection-with-bru-file/collection.bru @@ -0,0 +1,5 @@ +meta { + name: Collection Level Config + version: 1.0 +} + diff --git a/tests/fixtures/collection-with-bru-file/expected-openapi.yml b/tests/fixtures/collection-with-bru-file/expected-openapi.yml new file mode 100644 index 0000000..caad3d5 --- /dev/null +++ b/tests/fixtures/collection-with-bru-file/expected-openapi.yml @@ -0,0 +1,60 @@ +openapi: 3.0.0 +info: + title: Collection with .bru file + version: '1' + description: >- + OpenAPI specification generated from Bruno collection: Collection with .bru + file +paths: + /simple: + get: + summary: Simple Request + description: Operation for Simple Request + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + description: Default response schema + '400': + description: Bad Request - The request was invalid + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '401': + description: Unauthorized - Authentication required + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '404': + description: Not Found - The requested resource does not exist + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '500': + description: Internal Server Error + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message diff --git a/tests/fixtures/collection-with-bru-file/simple-request.bru b/tests/fixtures/collection-with-bru-file/simple-request.bru new file mode 100644 index 0000000..cfa2689 --- /dev/null +++ b/tests/fixtures/collection-with-bru-file/simple-request.bru @@ -0,0 +1,10 @@ +meta { + name: Simple Request + type: http + seq: 1 +} + +get { + url: https://api.example.com/simple +} + diff --git a/tests/fixtures/collection-without-bruno-json/another-request.bru b/tests/fixtures/collection-without-bruno-json/another-request.bru new file mode 100644 index 0000000..b99d766 --- /dev/null +++ b/tests/fixtures/collection-without-bruno-json/another-request.bru @@ -0,0 +1,10 @@ +meta { + name: Another Request + type: http + seq: 2 +} + +post { + url: https://api.example.com/another +} + diff --git a/tests/fixtures/collection-without-bruno-json/expected-openapi.yml b/tests/fixtures/collection-without-bruno-json/expected-openapi.yml new file mode 100644 index 0000000..a3d88f3 --- /dev/null +++ b/tests/fixtures/collection-without-bruno-json/expected-openapi.yml @@ -0,0 +1,112 @@ +openapi: 3.0.0 +info: + title: collection-without-bruno-json + version: 1.0.0 + description: >- + OpenAPI specification generated from Bruno collection: + collection-without-bruno-json +paths: + /another: + post: + summary: Another Request + description: Operation for Another Request + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + description: Default response schema + '400': + description: Bad Request - The request was invalid + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '401': + description: Unauthorized - Authentication required + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '404': + description: Not Found - The requested resource does not exist + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '500': + description: Internal Server Error + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + /simple: + get: + summary: Simple Request + description: Operation for Simple Request + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + description: Default response schema + '400': + description: Bad Request - The request was invalid + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '401': + description: Unauthorized - Authentication required + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '404': + description: Not Found - The requested resource does not exist + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '500': + description: Internal Server Error + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message diff --git a/tests/fixtures/collection-without-bruno-json/simple-request.bru b/tests/fixtures/collection-without-bruno-json/simple-request.bru new file mode 100644 index 0000000..cfa2689 --- /dev/null +++ b/tests/fixtures/collection-without-bruno-json/simple-request.bru @@ -0,0 +1,10 @@ +meta { + name: Simple Request + type: http + seq: 1 +} + +get { + url: https://api.example.com/simple +} + diff --git a/tests/fixtures/empty-bru-file/empty.bru b/tests/fixtures/empty-bru-file/empty.bru new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/empty-bru-file/expected-openapi.yml b/tests/fixtures/empty-bru-file/expected-openapi.yml new file mode 100644 index 0000000..02acd45 --- /dev/null +++ b/tests/fixtures/empty-bru-file/expected-openapi.yml @@ -0,0 +1,6 @@ +openapi: 3.0.0 +info: + title: empty-bru-file + version: 1.0.0 + description: 'OpenAPI specification generated from Bruno collection: empty-bru-file' +paths: {} diff --git a/tests/fixtures/empty-collection/expected-openapi.yml b/tests/fixtures/empty-collection/expected-openapi.yml new file mode 100644 index 0000000..dcb253e --- /dev/null +++ b/tests/fixtures/empty-collection/expected-openapi.yml @@ -0,0 +1,6 @@ +openapi: 3.0.0 +info: + title: empty-collection + version: 1.0.0 + description: 'OpenAPI specification generated from Bruno collection: empty-collection' +paths: {} diff --git a/tests/fixtures/invalid-bruno-json-collection/bruno.json b/tests/fixtures/invalid-bruno-json-collection/bruno.json new file mode 100644 index 0000000..c51422e --- /dev/null +++ b/tests/fixtures/invalid-bruno-json-collection/bruno.json @@ -0,0 +1,5 @@ +{ + "name": "Invalid Collection", + "version": "1", + "type": "collection" + // This is intentionally invalid JSON - missing closing brace diff --git a/tests/fixtures/minimal-bru-file/expected-openapi.yml b/tests/fixtures/minimal-bru-file/expected-openapi.yml new file mode 100644 index 0000000..8e486e5 --- /dev/null +++ b/tests/fixtures/minimal-bru-file/expected-openapi.yml @@ -0,0 +1,6 @@ +openapi: 3.0.0 +info: + title: minimal-bru-file + version: 1.0.0 + description: 'OpenAPI specification generated from Bruno collection: minimal-bru-file' +paths: {} diff --git a/tests/fixtures/minimal-bru-file/minimal.bru b/tests/fixtures/minimal-bru-file/minimal.bru new file mode 100644 index 0000000..fa4ec8f --- /dev/null +++ b/tests/fixtures/minimal-bru-file/minimal.bru @@ -0,0 +1,5 @@ +meta { + name: Minimal Request + type: http +} + diff --git a/tests/fixtures/non-object-bruno-json-collection/bruno.json b/tests/fixtures/non-object-bruno-json-collection/bruno.json new file mode 100644 index 0000000..b276969 --- /dev/null +++ b/tests/fixtures/non-object-bruno-json-collection/bruno.json @@ -0,0 +1,2 @@ +["this", "is", "an", "array", "not", "an", "object"] + diff --git a/tests/fixtures/non-object-bruno-json-collection/expected-openapi.yml b/tests/fixtures/non-object-bruno-json-collection/expected-openapi.yml new file mode 100644 index 0000000..4d97416 --- /dev/null +++ b/tests/fixtures/non-object-bruno-json-collection/expected-openapi.yml @@ -0,0 +1,7 @@ +openapi: 3.0.0 +info: + title: non-object-bruno-json-collection + version: 1.0.0 + description: | + OpenAPI specification generated from Bruno collection: non-object-bruno-json-collection +paths: diff --git a/tests/fixtures/sample-bruno-collection/create-user-form.bru b/tests/fixtures/sample-bruno-collection/create-user-form.bru new file mode 100644 index 0000000..cad2972 --- /dev/null +++ b/tests/fixtures/sample-bruno-collection/create-user-form.bru @@ -0,0 +1,21 @@ +meta { + name: Create User Form + type: http + seq: 8 +} + +post { + url: https://api.example.com/users/form +} + +headers { + Content-Type: application/x-www-form-urlencoded + Authorization: Bearer {{token}} +} + +body:form-urlencoded { + name: John Doe + email: john@example.com + age: 25 +} + diff --git a/tests/fixtures/sample-bruno-collection/create-user.bru b/tests/fixtures/sample-bruno-collection/create-user.bru index 4b5e5fe..2f55bcc 100644 --- a/tests/fixtures/sample-bruno-collection/create-user.bru +++ b/tests/fixtures/sample-bruno-collection/create-user.bru @@ -1,16 +1,21 @@ -[meta] -name=Create User -type=http-request -seq=2 +meta { + name: Create User + type: http + seq: 2 +} -[request] -method=POST -url=https://api.example.com/users +post { + url: https://api.example.com/users +} -[headers] -Content-Type=application/json -Authorization=Bearer {{token}} +headers { + Content-Type: application/json + Authorization: Bearer {{token}} +} -[body] -mode=json -json={"name": "John Doe", "email": "john@example.com"} \ No newline at end of file +body:json { + { + "name": "John Doe", + "email": "john@example.com" + } +} diff --git a/tests/fixtures/sample-bruno-collection/create-xml-data.bru b/tests/fixtures/sample-bruno-collection/create-xml-data.bru new file mode 100644 index 0000000..53b1ddc --- /dev/null +++ b/tests/fixtures/sample-bruno-collection/create-xml-data.bru @@ -0,0 +1,23 @@ +meta { + name: Create XML Data + type: http + seq: 13 +} + +post { + url: https://api.example.com/data +} + +headers { + Content-Type: application/xml + Authorization: Bearer {{token}} +} + +body:xml { + + John Doe + john@example.com + 25 + +} + diff --git a/tests/fixtures/sample-bruno-collection/delete-user.bru b/tests/fixtures/sample-bruno-collection/delete-user.bru new file mode 100644 index 0000000..836e049 --- /dev/null +++ b/tests/fixtures/sample-bruno-collection/delete-user.bru @@ -0,0 +1,14 @@ +meta { + name: Delete User + type: http + seq: 5 +} + +delete { + url: https://api.example.com/users/123 +} + +headers { + Authorization: Bearer {{token}} +} + diff --git a/tests/fixtures/sample-bruno-collection/expected-openapi.yml b/tests/fixtures/sample-bruno-collection/expected-openapi.yml new file mode 100644 index 0000000..41c70ed --- /dev/null +++ b/tests/fixtures/sample-bruno-collection/expected-openapi.yml @@ -0,0 +1,963 @@ +openapi: 3.0.0 +info: + title: Test API Collection + version: '1' + description: 'OpenAPI specification generated from Bruno collection: Test API Collection' +paths: + /users/form: + post: + summary: Create User Form + description: Operation for Create User Form + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + description: Default response schema + '400': + description: Bad Request - The request was invalid + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '401': + description: Unauthorized - Authentication required + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '404': + description: Not Found - The requested resource does not exist + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '500': + description: Internal Server Error + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + parameters: + - name: Content-Type + in: header + required: true + schema: + type: string + example: application/x-www-form-urlencoded + description: Content-Type + - name: Authorization + in: header + required: true + schema: + type: string + example: Bearer {{token}} + description: Authorization + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + name: + type: string + example: John Doe + email: + type: string + example: john@example.com + age: + type: string + example: '25' + /users: + post: + summary: Create User + description: Operation for Create User + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + description: Default response schema + '400': + description: Bad Request - The request was invalid + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '401': + description: Unauthorized - Authentication required + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '404': + description: Not Found - The requested resource does not exist + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '500': + description: Internal Server Error + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + parameters: + - name: Content-Type + in: header + required: true + schema: + type: string + example: application/json + description: Content-Type + - name: Authorization + in: header + required: true + schema: + type: string + example: Bearer {{token}} + description: Authorization + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + email: + type: string + example: + name: John Doe + email: john@example.com + get: + summary: Get Users + description: Operation for Get Users + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + description: Default response schema + '400': + description: Bad Request - The request was invalid + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '401': + description: Unauthorized - Authentication required + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '404': + description: Not Found - The requested resource does not exist + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '500': + description: Internal Server Error + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + parameters: + - name: limit + in: query + required: true + schema: + type: string + example: '10' + description: limit + - name: offset + in: query + required: true + schema: + type: string + example: '0' + description: offset + - name: Content-Type + in: header + required: true + schema: + type: string + example: application/json + description: Content-Type + - name: Authorization + in: header + required: true + schema: + type: string + example: Bearer {{token}} + description: Authorization + /data: + post: + summary: Create XML Data + description: Operation for Create XML Data + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + description: Default response schema + '400': + description: Bad Request - The request was invalid + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '401': + description: Unauthorized - Authentication required + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '404': + description: Not Found - The requested resource does not exist + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '500': + description: Internal Server Error + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + parameters: + - name: Content-Type + in: header + required: true + schema: + type: string + example: application/xml + description: Content-Type + - name: Authorization + in: header + required: true + schema: + type: string + example: Bearer {{token}} + description: Authorization + requestBody: + content: + application/xml: + schema: + type: string + example: |- + + John Doe + john@example.com + 25 + + /users/123: + delete: + summary: Delete User + description: Operation for Delete User + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + description: Default response schema + '400': + description: Bad Request - The request was invalid + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '401': + description: Unauthorized - Authentication required + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '404': + description: Not Found - The requested resource does not exist + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '500': + description: Internal Server Error + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + parameters: + - name: Authorization + in: header + required: true + schema: + type: string + example: Bearer {{token}} + description: Authorization + head: + summary: Head User + description: Operation for Head User + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + description: Default response schema + '400': + description: Bad Request - The request was invalid + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '401': + description: Unauthorized - Authentication required + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '404': + description: Not Found - The requested resource does not exist + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '500': + description: Internal Server Error + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + parameters: + - name: Authorization + in: header + required: true + schema: + type: string + example: Bearer {{token}} + description: Authorization + options: + summary: Options User + description: Operation for Options User + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + description: Default response schema + '400': + description: Bad Request - The request was invalid + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '401': + description: Unauthorized - Authentication required + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '404': + description: Not Found - The requested resource does not exist + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '500': + description: Internal Server Error + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + parameters: + - name: Authorization + in: header + required: true + schema: + type: string + example: Bearer {{token}} + description: Authorization + patch: + summary: Patch User + description: Operation for Patch User + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + description: Default response schema + '400': + description: Bad Request - The request was invalid + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '401': + description: Unauthorized - Authentication required + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '404': + description: Not Found - The requested resource does not exist + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '500': + description: Internal Server Error + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + parameters: + - name: Content-Type + in: header + required: true + schema: + type: string + example: application/json + description: Content-Type + - name: Authorization + in: header + required: true + schema: + type: string + example: Bearer {{token}} + description: Authorization + requestBody: + content: + application/json: + schema: + type: object + properties: + email: + type: string + example: + email: newemail@example.com + put: + summary: Update User + description: Operation for Update User + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + description: Default response schema + '400': + description: Bad Request - The request was invalid + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '401': + description: Unauthorized - Authentication required + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '404': + description: Not Found - The requested resource does not exist + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '500': + description: Internal Server Error + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + parameters: + - name: Content-Type + in: header + required: true + schema: + type: string + example: application/json + description: Content-Type + - name: Authorization + in: header + required: true + schema: + type: string + example: Bearer {{token}} + description: Authorization + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + email: + type: string + age: + type: integer + example: + name: Jane Doe + email: jane@example.com + age: 30 + /users/%7B%7BuserId%7D%7D: + get: + summary: Get User By ID + description: Operation for Get User By ID + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + description: Default response schema + '400': + description: Bad Request - The request was invalid + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '401': + description: Unauthorized - Authentication required + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '404': + description: Not Found - The requested resource does not exist + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '500': + description: Internal Server Error + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + parameters: + - name: Content-Type + in: header + required: true + schema: + type: string + example: application/json + description: Content-Type + - name: Authorization + in: header + required: true + schema: + type: string + example: Bearer {{token}} + description: Authorization + /users/search: + get: + summary: Search Users + description: Operation for Search Users + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + description: Default response schema + '400': + description: Bad Request - The request was invalid + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '401': + description: Unauthorized - Authentication required + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '404': + description: Not Found - The requested resource does not exist + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '500': + description: Internal Server Error + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + parameters: + - name: q + in: query + required: true + schema: + type: string + example: john + description: q + - name: status + in: query + required: true + schema: + type: string + example: active + description: status + - name: sort + in: query + required: true + schema: + type: string + example: name + description: sort + - name: order + in: query + required: true + schema: + type: string + example: asc + description: order + - name: Content-Type + in: header + required: true + schema: + type: string + example: application/json + description: Content-Type + - name: Authorization + in: header + required: true + schema: + type: string + example: Bearer {{token}} + description: Authorization + /messages: + post: + summary: Send Message + description: Operation for Send Message + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + description: Default response schema + '400': + description: Bad Request - The request was invalid + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '401': + description: Unauthorized - Authentication required + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '404': + description: Not Found - The requested resource does not exist + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '500': + description: Internal Server Error + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + parameters: + - name: Content-Type + in: header + required: true + schema: + type: string + example: application/json + description: Content-Type + - name: Authorization + in: header + required: true + schema: + type: string + example: Bearer {{token}} + description: Authorization + requestBody: + content: + text/plain: + schema: + type: string + example: This is a plain text message body + /upload: + post: + summary: Upload File + description: Operation for Upload File + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + description: Default response schema + '400': + description: Bad Request - The request was invalid + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '401': + description: Unauthorized - Authentication required + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '404': + description: Not Found - The requested resource does not exist + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + '500': + description: Internal Server Error + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + parameters: + - name: Authorization + in: header + required: true + schema: + type: string + example: Bearer {{token}} + description: Authorization + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: {} diff --git a/tests/fixtures/sample-bruno-collection/get-user-by-id.bru b/tests/fixtures/sample-bruno-collection/get-user-by-id.bru new file mode 100644 index 0000000..f1b5ccf --- /dev/null +++ b/tests/fixtures/sample-bruno-collection/get-user-by-id.bru @@ -0,0 +1,15 @@ +meta { + name: Get User By ID + type: http + seq: 11 +} + +get { + url: https://api.example.com/users/{{userId}} +} + +headers { + Content-Type: application/json + Authorization: Bearer {{token}} +} + diff --git a/tests/fixtures/sample-bruno-collection/get-users.bru b/tests/fixtures/sample-bruno-collection/get-users.bru index 9b6ba31..52392a8 100644 --- a/tests/fixtures/sample-bruno-collection/get-users.bru +++ b/tests/fixtures/sample-bruno-collection/get-users.bru @@ -1,16 +1,19 @@ -[meta] -name=Get Users -type=http-request -seq=1 +meta { + name: Get Users + type: http + seq: 1 +} -[request] -method=GET -url=https://api.example.com/users +get { + url: https://api.example.com/users +} -[headers] -Content-Type=application/json -Authorization=Bearer {{token}} +headers { + Content-Type: application/json + Authorization: Bearer {{token}} +} -[params] -limit=10 -offset=0 \ No newline at end of file +params { + limit: 10 + offset: 0 +} diff --git a/tests/fixtures/sample-bruno-collection/head-user.bru b/tests/fixtures/sample-bruno-collection/head-user.bru new file mode 100644 index 0000000..dd18a52 --- /dev/null +++ b/tests/fixtures/sample-bruno-collection/head-user.bru @@ -0,0 +1,14 @@ +meta { + name: Head User + type: http + seq: 6 +} + +head { + url: https://api.example.com/users/123 +} + +headers { + Authorization: Bearer {{token}} +} + diff --git a/tests/fixtures/sample-bruno-collection/options-user.bru b/tests/fixtures/sample-bruno-collection/options-user.bru new file mode 100644 index 0000000..28ce80d --- /dev/null +++ b/tests/fixtures/sample-bruno-collection/options-user.bru @@ -0,0 +1,14 @@ +meta { + name: Options User + type: http + seq: 7 +} + +options { + url: https://api.example.com/users/123 +} + +headers { + Authorization: Bearer {{token}} +} + diff --git a/tests/fixtures/sample-bruno-collection/patch-user.bru b/tests/fixtures/sample-bruno-collection/patch-user.bru new file mode 100644 index 0000000..fbeedc0 --- /dev/null +++ b/tests/fixtures/sample-bruno-collection/patch-user.bru @@ -0,0 +1,21 @@ +meta { + name: Patch User + type: http + seq: 4 +} + +patch { + url: https://api.example.com/users/123 +} + +headers { + Content-Type: application/json + Authorization: Bearer {{token}} +} + +body:json { + { + "email": "newemail@example.com" + } +} + diff --git a/tests/fixtures/sample-bruno-collection/search-users.bru b/tests/fixtures/sample-bruno-collection/search-users.bru new file mode 100644 index 0000000..f0178b4 --- /dev/null +++ b/tests/fixtures/sample-bruno-collection/search-users.bru @@ -0,0 +1,22 @@ +meta { + name: Search Users + type: http + seq: 10 +} + +get { + url: https://api.example.com/users/search +} + +headers { + Content-Type: application/json + Authorization: Bearer {{token}} +} + +params { + q: john + status: active + sort: name + order: asc +} + diff --git a/tests/fixtures/sample-bruno-collection/send-message.bru b/tests/fixtures/sample-bruno-collection/send-message.bru new file mode 100644 index 0000000..30d2954 --- /dev/null +++ b/tests/fixtures/sample-bruno-collection/send-message.bru @@ -0,0 +1,19 @@ +meta { + name: Send Message + type: http + seq: 12 +} + +post { + url: https://api.example.com/messages +} + +headers { + Content-Type: application/json + Authorization: Bearer {{token}} +} + +body:text { + This is a plain text message body +} + diff --git a/tests/fixtures/sample-bruno-collection/update-user.bru b/tests/fixtures/sample-bruno-collection/update-user.bru new file mode 100644 index 0000000..24da147 --- /dev/null +++ b/tests/fixtures/sample-bruno-collection/update-user.bru @@ -0,0 +1,23 @@ +meta { + name: Update User + type: http + seq: 3 +} + +put { + url: https://api.example.com/users/123 +} + +headers { + Content-Type: application/json + Authorization: Bearer {{token}} +} + +body:json { + { + "name": "Jane Doe", + "email": "jane@example.com", + "age": 30 + } +} + diff --git a/tests/fixtures/sample-bruno-collection/upload-file.bru b/tests/fixtures/sample-bruno-collection/upload-file.bru new file mode 100644 index 0000000..fd622bb --- /dev/null +++ b/tests/fixtures/sample-bruno-collection/upload-file.bru @@ -0,0 +1,19 @@ +meta { + name: Upload File + type: http + seq: 9 +} + +post { + url: https://api.example.com/upload +} + +headers { + Authorization: Bearer {{token}} +} + +body:multipart-form { + file: @/path/to/file.pdf + description: User profile picture +} + From 97af2d921e95993716af551052b9246d0dc8c349 Mon Sep 17 00:00:00 2001 From: GyeongHo Kim Date: Tue, 6 Jan 2026 20:54:46 +0900 Subject: [PATCH 2/9] test: make parser test code to use fixtures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - bruno-parser.test.ts가 실제 bru-lang fixture들을 사용하도록 수정 --- tests/unit/utils/bruno-parser.test.ts | 337 ++++++++++++++++++++------ 1 file changed, 266 insertions(+), 71 deletions(-) diff --git a/tests/unit/utils/bruno-parser.test.ts b/tests/unit/utils/bruno-parser.test.ts index a7082b7..024f02f 100644 --- a/tests/unit/utils/bruno-parser.test.ts +++ b/tests/unit/utils/bruno-parser.test.ts @@ -1,81 +1,107 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import path from 'node:path' +import { describe, expect, it } from 'vitest' +import { + BruFileParseError, + InvalidBrunoJsonError, + InvalidCollectionPathError, + InvalidJsonError, +} from '../../../src/models/errors.js' import { BrunoParser } from '../../../src/utils/bruno-parser.js' import { FileReader } from '../../../src/utils/file-reader.js' -// Mock the FileReader -vi.mock('../../../src/utils/file-reader.js', () => ({ - FileReader: { - isDirectory: vi.fn(), - fileExists: vi.fn(), - readFile: vi.fn(), - getBruFiles: vi.fn(), - }, -})) +// Get fixture paths +const fixturesDir = path.join(process.cwd(), 'tests', 'fixtures') +const sampleCollectionPath = path.join(fixturesDir, 'sample-bruno-collection') +const collectionWithoutBrunoJsonPath = path.join(fixturesDir, 'collection-without-bruno-json') +const emptyCollectionPath = path.join(fixturesDir, 'empty-collection') +const invalidBrunoJsonPath = path.join(fixturesDir, 'invalid-bruno-json-collection') +const nonObjectBrunoJsonPath = path.join(fixturesDir, 'non-object-bruno-json-collection') +const collectionWithBruFilePath = path.join(fixturesDir, 'collection-with-bru-file') +const authTestCollectionPath = path.join(fixturesDir, 'auth-test-collection') +const emptyBruFilePath = path.join(fixturesDir, 'empty-bru-file', 'empty.bru') +const minimalBruFilePath = path.join(fixturesDir, 'minimal-bru-file', 'minimal.bru') describe('BrunoParser', () => { - beforeEach(() => { - vi.clearAllMocks() - }) - describe('parseCollection', () => { it('should parse a Bruno collection successfully', async () => { - vi.mocked(FileReader.isDirectory).mockResolvedValue(true) - vi.mocked(FileReader.fileExists).mockResolvedValue(true) - vi.mocked(FileReader.readFile).mockResolvedValue( - JSON.stringify({ name: 'Test Collection', version: '1' }), - ) - vi.mocked(FileReader.getBruFiles).mockResolvedValue([]) + const result = await BrunoParser.parseCollection(sampleCollectionPath) - const result = await BrunoParser.parseCollection('/test/collection') - - expect(result.name).toBe('Test Collection') + expect(result.name).toBe('Test API Collection') expect(result.version).toBe('1') expect(Array.isArray(result.items)).toBe(true) + expect(result.items.length).toBeGreaterThan(0) }) it('should handle collection without bruno.json', async () => { - vi.mocked(FileReader.isDirectory).mockResolvedValue(true) - vi.mocked(FileReader.fileExists).mockResolvedValue(false) // bruno.json doesn't exist - vi.mocked(FileReader.getBruFiles).mockResolvedValue([]) - - const result = await BrunoParser.parseCollection('/test/collection') + const result = await BrunoParser.parseCollection(collectionWithoutBrunoJsonPath) - expect(result.name).toBe('collection') // Should use directory name + expect(result.name).toBe('collection-without-bruno-json') // Should use directory name expect(result.version).toBe('1') + expect(Array.isArray(result.items)).toBe(true) + expect(result.items.length).toBeGreaterThan(0) }) - it('should throw an error for invalid collection path', async () => { - vi.mocked(FileReader.isDirectory).mockResolvedValue(false) + it('should throw InvalidCollectionPathError for invalid collection path', async () => { + await expect( + BrunoParser.parseCollection('/invalid/path/that/does/not/exist'), + ).rejects.toBeInstanceOf(InvalidCollectionPathError) + }) + + it('should throw InvalidJsonError for invalid JSON in bruno.json', async () => { + await expect(BrunoParser.parseCollection(invalidBrunoJsonPath)).rejects.toBeInstanceOf( + InvalidJsonError, + ) + }) - await expect(BrunoParser.parseCollection('/invalid/path')).rejects.toThrow( - 'Collection path does not exist or is not a directory: /invalid/path', + it('should throw InvalidBrunoJsonError when bruno.json is not an object', async () => { + await expect(BrunoParser.parseCollection(nonObjectBrunoJsonPath)).rejects.toBeInstanceOf( + InvalidBrunoJsonError, ) }) + + it('should handle collection with collection.bru file', async () => { + const result = await BrunoParser.parseCollection(collectionWithBruFilePath) + + expect(result.name).toBe('Collection with .bru file') + expect(result.root).toBeDefined() + expect(result.root?.type).toBe('collection') + expect(result.items.length).toBeGreaterThan(0) + }) + }) + + describe('parseBruFiles', () => { + it('should parse multiple .bru files', async () => { + const bruFiles = await FileReader.getBruFiles(sampleCollectionPath) + const result = await BrunoParser.parseBruFiles(bruFiles, sampleCollectionPath) + + expect(result.length).toBeGreaterThan(1) + expect(result.every(item => item.type === 'http-request')).toBe(true) + }) + + it('should throw BruFileParseError when a .bru file fails to parse', async () => { + const invalidBruFiles = ['/nonexistent/file.bru'] + await expect( + BrunoParser.parseBruFiles(invalidBruFiles, sampleCollectionPath), + ).rejects.toBeInstanceOf(BruFileParseError) + }) }) describe('isValidCollection', () => { it('should return true for a collection with bruno.json', async () => { - vi.mocked(FileReader.fileExists).mockResolvedValue(true) - - const result = await BrunoParser.isValidCollection('/test/collection') + const result = await BrunoParser.isValidCollection(sampleCollectionPath) expect(result).toBe(true) }) it('should return true for a collection with .bru files', async () => { - vi.mocked(FileReader.fileExists).mockResolvedValue(false) // No bruno.json - vi.mocked(FileReader.getBruFiles).mockResolvedValue(['/test/file.bru']) - - const result = await BrunoParser.isValidCollection('/test/collection') + const result = await BrunoParser.isValidCollection(collectionWithoutBrunoJsonPath) expect(result).toBe(true) }) it('should return false for a collection without bruno.json and no .bru files', async () => { - vi.mocked(FileReader.fileExists).mockResolvedValue(false) // No bruno.json - vi.mocked(FileReader.getBruFiles).mockResolvedValue([]) // No .bru files - - const result = await BrunoParser.isValidCollection('/test/collection') + // Create empty directory if it doesn't exist + const result = await BrunoParser.isValidCollection(emptyCollectionPath) expect(result).toBe(false) }) @@ -83,57 +109,226 @@ describe('BrunoParser', () => { describe('parseBruContent', () => { it('should parse a basic .bru file content correctly', async () => { - const content = `[meta] -name=Test Request -seq=0 - -[request] -method=GET -url=https://api.example.com/users - -[headers] -Content-Type=application/json -Authorization=Bearer token123 -` + const bruFilePath = path.join(sampleCollectionPath, 'get-users.bru') + const content = await FileReader.readFile(bruFilePath) - const result = await BrunoParser.parseBruContent(content, 'test.bru') + const result = await BrunoParser.parseBruContent(content, 'get-users.bru') - expect(result.name).toBe('Test Request') + expect(result.name).toBe('Get Users') expect(result.type).toBe('http-request') expect(result.request?.method).toBe('GET') expect(result.request?.url).toBe('https://api.example.com/users') expect(result.request?.headers).toHaveLength(2) - expect(result.request?.headers[0]).toMatchObject({ + expect(result.request?.headers?.[0]).toMatchObject({ name: 'Content-Type', value: 'application/json', enabled: true, }) - expect(result.request?.headers[1]).toMatchObject({ + expect(result.request?.headers?.[1]).toMatchObject({ name: 'Authorization', - value: 'Bearer token123', + value: 'Bearer {{token}}', enabled: true, }) }) it('should parse a .bru file with params section', async () => { - const content = `[params] -userId=123 -status=active -` + const bruFilePath = path.join(sampleCollectionPath, 'search-users.bru') + const content = await FileReader.readFile(bruFilePath) - const result = await BrunoParser.parseBruContent(content, 'test.bru') + const result = await BrunoParser.parseBruContent(content, 'search-users.bru') - expect(result.request?.params).toHaveLength(2) - expect(result.request?.params[0]).toMatchObject({ - name: 'userId', - value: '123', + expect(result.request?.params).toHaveLength(4) + expect(result.request?.params?.[0]).toMatchObject({ + name: 'q', + value: 'john', enabled: true, }) - expect(result.request?.params[1]).toMatchObject({ + expect(result.request?.params?.[1]).toMatchObject({ name: 'status', value: 'active', enabled: true, }) + expect(result.request?.params?.[2]).toMatchObject({ + name: 'sort', + value: 'name', + enabled: true, + }) + expect(result.request?.params?.[3]).toMatchObject({ + name: 'order', + value: 'asc', + enabled: true, + }) + }) + + it('should parse a .bru file with JSON body', async () => { + const bruFilePath = path.join(sampleCollectionPath, 'create-user.bru') + const content = await FileReader.readFile(bruFilePath) + + const result = await BrunoParser.parseBruContent(content, 'create-user.bru') + + expect(result.name).toBe('Create User') + expect(result.request?.method).toBe('POST') + expect(result.request?.body).toBeDefined() + expect(result.request?.body?.mode).toBe('json') + expect(result.request?.body?.json).toBeDefined() + }) + + it('should parse different HTTP methods correctly', async () => { + // Test PUT + const putFilePath = path.join(sampleCollectionPath, 'update-user.bru') + const putContent = await FileReader.readFile(putFilePath) + const putResult = await BrunoParser.parseBruContent(putContent, 'update-user.bru') + expect(putResult.request?.method).toBe('PUT') + + // Test PATCH + const patchFilePath = path.join(sampleCollectionPath, 'patch-user.bru') + const patchContent = await FileReader.readFile(patchFilePath) + const patchResult = await BrunoParser.parseBruContent(patchContent, 'patch-user.bru') + expect(patchResult.request?.method).toBe('PATCH') + + // Test DELETE + const deleteFilePath = path.join(sampleCollectionPath, 'delete-user.bru') + const deleteContent = await FileReader.readFile(deleteFilePath) + const deleteResult = await BrunoParser.parseBruContent(deleteContent, 'delete-user.bru') + expect(deleteResult.request?.method).toBe('DELETE') + + // Test HEAD + const headFilePath = path.join(sampleCollectionPath, 'head-user.bru') + const headContent = await FileReader.readFile(headFilePath) + const headResult = await BrunoParser.parseBruContent(headContent, 'head-user.bru') + expect(headResult.request?.method).toBe('HEAD') + + // Test OPTIONS + const optionsFilePath = path.join(sampleCollectionPath, 'options-user.bru') + const optionsContent = await FileReader.readFile(optionsFilePath) + const optionsResult = await BrunoParser.parseBruContent(optionsContent, 'options-user.bru') + expect(optionsResult.request?.method).toBe('OPTIONS') + }) + + it('should parse a .bru file with form-urlencoded body', async () => { + const bruFilePath = path.join(sampleCollectionPath, 'create-user-form.bru') + const content = await FileReader.readFile(bruFilePath) + + const result = await BrunoParser.parseBruContent(content, 'create-user-form.bru') + + expect(result.request?.body).toBeDefined() + expect(result.request?.body?.mode).toBe('formUrlEncoded') + }) + + it('should parse a .bru file with text body', async () => { + const bruFilePath = path.join(sampleCollectionPath, 'send-message.bru') + const content = await FileReader.readFile(bruFilePath) + + const result = await BrunoParser.parseBruContent(content, 'send-message.bru') + + expect(result.request?.body).toBeDefined() + expect(result.request?.body?.mode).toBe('text') + expect(result.request?.body?.text).toBeDefined() + }) + + it('should parse a .bru file with XML body', async () => { + const bruFilePath = path.join(sampleCollectionPath, 'create-xml-data.bru') + const content = await FileReader.readFile(bruFilePath) + + const result = await BrunoParser.parseBruContent(content, 'create-xml-data.bru') + + expect(result.request?.body).toBeDefined() + expect(result.request?.body?.mode).toBe('xml') + expect(result.request?.body?.xml).toBeDefined() + }) + + it('should parse a .bru file with basic auth', async () => { + const bruFilePath = path.join(authTestCollectionPath, 'request-with-basic-auth.bru') + const content = await FileReader.readFile(bruFilePath) + + const result = await BrunoParser.parseBruContent(content, 'request-with-basic-auth.bru') + + expect(result.request?.auth).toBeDefined() + expect(result.request?.auth?.mode).toBe('basic') + expect(result.request?.auth?.basic).toBeDefined() + expect(result.request?.auth?.basic?.username).toBe('admin') + expect(result.request?.auth?.basic?.password).toBe('secret123') + }) + + it('should parse a .bru file with bearer auth', async () => { + const bruFilePath = path.join(authTestCollectionPath, 'request-with-bearer-auth.bru') + const content = await FileReader.readFile(bruFilePath) + + const result = await BrunoParser.parseBruContent(content, 'request-with-bearer-auth.bru') + + expect(result.request?.auth).toBeDefined() + expect(result.request?.auth?.mode).toBe('bearer') + expect(result.request?.auth?.bearer).toBeDefined() + expect(result.request?.auth?.bearer?.token).toBe('{{bearerToken}}') + }) + + it('should handle empty .bru file', async () => { + const content = await FileReader.readFile(emptyBruFilePath) + + const result = await BrunoParser.parseBruContent(content, 'empty.bru') + + expect(result.name).toBe('empty') + expect(result.type).toBe('http-request') + }) + + it('should handle minimal .bru file with only meta section', async () => { + const content = await FileReader.readFile(minimalBruFilePath) + + const result = await BrunoParser.parseBruContent(content, 'minimal.bru') + + expect(result.name).toBe('Minimal Request') + expect(result.type).toBe('http-request') + }) + }) + + describe('parseCollectionSync', () => { + it('should parse a Bruno collection synchronously', () => { + const result = BrunoParser.parseCollectionSync(sampleCollectionPath) + + expect(result.name).toBe('Test API Collection') + expect(result.version).toBe('1') + expect(Array.isArray(result.items)).toBe(true) + }) + + it('should throw InvalidCollectionPathError for invalid collection path synchronously', () => { + expect(() => BrunoParser.parseCollectionSync('/invalid/path/that/does/not/exist')).toThrow( + InvalidCollectionPathError, + ) + }) + }) + + describe('parseBruFilesSync', () => { + it('should parse multiple .bru files synchronously', () => { + const bruFiles = FileReader.getBruFilesSync(sampleCollectionPath) + const result = BrunoParser.parseBruFilesSync(bruFiles, sampleCollectionPath) + + expect(result.length).toBeGreaterThan(1) + }) + }) + + describe('parseBruContentSync', () => { + it('should parse .bru file content synchronously', () => { + const bruFilePath = path.join(sampleCollectionPath, 'get-users.bru') + const content = FileReader.readFileSync(bruFilePath) + + const result = BrunoParser.parseBruContentSync(content, 'get-users.bru') + + expect(result.name).toBe('Get Users') + expect(result.request?.method).toBe('GET') + }) + }) + + describe('isValidCollectionSync', () => { + it('should return true for a valid collection synchronously', () => { + const result = BrunoParser.isValidCollectionSync(sampleCollectionPath) + + expect(result).toBe(true) + }) + + it('should return false for an invalid collection synchronously', () => { + const result = BrunoParser.isValidCollectionSync(emptyCollectionPath) + + expect(result).toBe(false) }) }) }) From 9dc67880da6dc70804e9b9da4e18d3a5136ac590 Mon Sep 17 00:00:00 2001 From: GyeongHo Kim Date: Tue, 6 Jan 2026 20:56:58 +0900 Subject: [PATCH 3/9] feat: add custom error classes - InvalidCollectionPathError for existance of collection path - InvalidJsonError for json parse error - InvalidBrunoJsonError for existing bruno.json but invalid json inside - BruFileParseError for parse error --- src/models/errors.ts | 60 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 src/models/errors.ts diff --git a/src/models/errors.ts b/src/models/errors.ts new file mode 100644 index 0000000..c5c5f2c --- /dev/null +++ b/src/models/errors.ts @@ -0,0 +1,60 @@ +/** + * Custom error classes for Bruno collection parsing + */ + +/** + * Error thrown when a collection path is invalid (does not exist or is not a directory) + */ +export class InvalidCollectionPathError extends Error { + constructor(collectionPath: string) { + super(`Collection path does not exist or is not a directory: ${collectionPath}`) + this.name = 'InvalidCollectionPathError' + // Maintains proper stack trace for where our error was thrown (only available on V8) + if (Error.captureStackTrace) { + Error.captureStackTrace(this, InvalidCollectionPathError) + } + } +} + +/** + * Error thrown when JSON parsing fails + */ +export class InvalidJsonError extends Error { + constructor(filePath: string, parseError: unknown) { + const message = + parseError instanceof Error ? parseError.message : 'Unknown error' + super(`Invalid JSON in ${filePath}: ${message}`) + this.name = 'InvalidJsonError' + if (Error.captureStackTrace) { + Error.captureStackTrace(this, InvalidJsonError) + } + } +} + +/** + * Error thrown when bruno.json exists but does not contain a valid JSON object + */ +export class InvalidBrunoJsonError extends Error { + constructor(filePath: string) { + super(`bruno.json must contain a valid JSON object: ${filePath}`) + this.name = 'InvalidBrunoJsonError' + if (Error.captureStackTrace) { + Error.captureStackTrace(this, InvalidBrunoJsonError) + } + } +} + +/** + * Error thrown when a .bru file fails to parse + */ +export class BruFileParseError extends Error { + constructor(bruFilePath: string, cause?: unknown) { + const causeMessage = cause instanceof Error ? cause.message : String(cause) + super(`Failed to parse .bru file ${bruFilePath}: ${causeMessage}`) + this.name = 'BruFileParseError' + if (Error.captureStackTrace) { + Error.captureStackTrace(this, BruFileParseError) + } + } +} + From af0be76844a63358f2049be4a48edda3b96fb11d Mon Sep 17 00:00:00 2001 From: GyeongHo Kim Date: Wed, 7 Jan 2026 00:00:42 +0900 Subject: [PATCH 4/9] test: update bruno-parser test to use actual fixture files --- tests/unit/utils/bruno-parser.test.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/tests/unit/utils/bruno-parser.test.ts b/tests/unit/utils/bruno-parser.test.ts index 024f02f..5f63a64 100644 --- a/tests/unit/utils/bruno-parser.test.ts +++ b/tests/unit/utils/bruno-parser.test.ts @@ -58,15 +58,6 @@ describe('BrunoParser', () => { InvalidBrunoJsonError, ) }) - - it('should handle collection with collection.bru file', async () => { - const result = await BrunoParser.parseCollection(collectionWithBruFilePath) - - expect(result.name).toBe('Collection with .bru file') - expect(result.root).toBeDefined() - expect(result.root?.type).toBe('collection') - expect(result.items.length).toBeGreaterThan(0) - }) }) describe('parseBruFiles', () => { From 407025d9464f3997af3dca55db4db60ff9031e63 Mon Sep 17 00:00:00 2001 From: GyeongHo Kim Date: Wed, 7 Jan 2026 00:01:00 +0900 Subject: [PATCH 5/9] test: update index.test.ts to snapshot test --- tests/unit/__snapshots__/index.test.ts.snap | 4009 +++++++++++++++++++ tests/unit/index.test.ts | 571 ++- 2 files changed, 4461 insertions(+), 119 deletions(-) create mode 100644 tests/unit/__snapshots__/index.test.ts.snap diff --git a/tests/unit/__snapshots__/index.test.ts.snap b/tests/unit/__snapshots__/index.test.ts.snap new file mode 100644 index 0000000..e3bd8bb --- /dev/null +++ b/tests/unit/__snapshots__/index.test.ts.snap @@ -0,0 +1,4009 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`convertBrunoCollectionToOpenAPI - Snapshot Tests > auth-test-collection > should match expected OpenAPI specification snapshot 1`] = ` +"{ + "openapi": "3.0.0", + "info": { + "title": "auth-test-collection", + "version": "1.0.0", + "description": "OpenAPI specification generated from Bruno collection: auth-test-collection" + }, + "paths": { + "/protected": { + "get": { + "summary": "Request with Basic Auth", + "description": "Operation for Request with Basic Auth", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Default response schema" + } + } + } + }, + "400": { + "description": "Bad Request - The request was invalid", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Authentication required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "404": { + "description": "Not Found - The requested resource does not exist", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + } + }, + "security": [ + { + "basicAuth": [] + } + ] + } + }, + "/api/data": { + "get": { + "summary": "Request with Bearer Auth", + "description": "Operation for Request with Bearer Auth", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Default response schema" + } + } + } + }, + "400": { + "description": "Bad Request - The request was invalid", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Authentication required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "404": { + "description": "Not Found - The requested resource does not exist", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + } + } +}" +`; + +exports[`convertBrunoCollectionToOpenAPI - Snapshot Tests > collection-with-bru-file > should match expected OpenAPI specification snapshot 1`] = ` +"{ + "openapi": "3.0.0", + "info": { + "title": "Collection with .bru file", + "version": "1", + "description": "OpenAPI specification generated from Bruno collection: Collection with .bru file" + }, + "paths": { + "/simple": { + "get": { + "summary": "Simple Request", + "description": "Operation for Simple Request", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Default response schema" + } + } + } + }, + "400": { + "description": "Bad Request - The request was invalid", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Authentication required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "404": { + "description": "Not Found - The requested resource does not exist", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + } + } + } + } + } +}" +`; + +exports[`convertBrunoCollectionToOpenAPI - Snapshot Tests > collection-without-bruno-json > should match expected OpenAPI specification snapshot 1`] = ` +"{ + "openapi": "3.0.0", + "info": { + "title": "collection-without-bruno-json", + "version": "1.0.0", + "description": "OpenAPI specification generated from Bruno collection: collection-without-bruno-json" + }, + "paths": { + "/another": { + "post": { + "summary": "Another Request", + "description": "Operation for Another Request", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Default response schema" + } + } + } + }, + "400": { + "description": "Bad Request - The request was invalid", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Authentication required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "404": { + "description": "Not Found - The requested resource does not exist", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + } + } + } + }, + "/simple": { + "get": { + "summary": "Simple Request", + "description": "Operation for Simple Request", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Default response schema" + } + } + } + }, + "400": { + "description": "Bad Request - The request was invalid", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Authentication required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "404": { + "description": "Not Found - The requested resource does not exist", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + } + } + } + } + } +}" +`; + +exports[`convertBrunoCollectionToOpenAPI - Snapshot Tests > empty-bru-file > should match expected OpenAPI specification snapshot 1`] = ` +"{ + "openapi": "3.0.0", + "info": { + "title": "empty-bru-file", + "version": "1.0.0", + "description": "OpenAPI specification generated from Bruno collection: empty-bru-file" + }, + "paths": {} +}" +`; + +exports[`convertBrunoCollectionToOpenAPI - Snapshot Tests > empty-collection > should match expected OpenAPI specification snapshot 1`] = ` +"{ + "openapi": "3.0.0", + "info": { + "title": "empty-collection", + "version": "1.0.0", + "description": "OpenAPI specification generated from Bruno collection: empty-collection" + }, + "paths": {} +}" +`; + +exports[`convertBrunoCollectionToOpenAPI - Snapshot Tests > minimal-bru-file > should match expected OpenAPI specification snapshot 1`] = ` +"{ + "openapi": "3.0.0", + "info": { + "title": "minimal-bru-file", + "version": "1.0.0", + "description": "OpenAPI specification generated from Bruno collection: minimal-bru-file" + }, + "paths": {} +}" +`; + +exports[`convertBrunoCollectionToOpenAPI - Snapshot Tests > sample-bruno-collection > should match expected OpenAPI specification snapshot 1`] = ` +"{ + "openapi": "3.0.0", + "info": { + "title": "Test API Collection", + "version": "1", + "description": "OpenAPI specification generated from Bruno collection: Test API Collection" + }, + "paths": { + "/users/form": { + "post": { + "summary": "Create User Form", + "description": "Operation for Create User Form", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Default response schema" + } + } + } + }, + "400": { + "description": "Bad Request - The request was invalid", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Authentication required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "404": { + "description": "Not Found - The requested resource does not exist", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "Content-Type", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "application/x-www-form-urlencoded" + }, + "description": "Content-Type" + }, + { + "name": "Authorization", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "Bearer {{token}}" + }, + "description": "Authorization" + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "John Doe" + }, + "email": { + "type": "string", + "example": "john@example.com" + }, + "age": { + "type": "string", + "example": "25" + } + } + } + } + } + } + } + }, + "/users": { + "post": { + "summary": "Create User", + "description": "Operation for Create User", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Default response schema" + } + } + } + }, + "400": { + "description": "Bad Request - The request was invalid", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Authentication required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "404": { + "description": "Not Found - The requested resource does not exist", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "Content-Type", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "application/json" + }, + "description": "Content-Type" + }, + { + "name": "Authorization", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "Bearer {{token}}" + }, + "description": "Authorization" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "email": { + "type": "string" + } + } + }, + "example": { + "name": "John Doe", + "email": "john@example.com" + } + } + } + } + }, + "get": { + "summary": "Get Users", + "description": "Operation for Get Users", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Default response schema" + } + } + } + }, + "400": { + "description": "Bad Request - The request was invalid", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Authentication required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "404": { + "description": "Not Found - The requested resource does not exist", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "limit", + "in": "query", + "required": true, + "schema": { + "type": "string", + "example": "10" + }, + "description": "limit" + }, + { + "name": "offset", + "in": "query", + "required": true, + "schema": { + "type": "string", + "example": "0" + }, + "description": "offset" + }, + { + "name": "Content-Type", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "application/json" + }, + "description": "Content-Type" + }, + { + "name": "Authorization", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "Bearer {{token}}" + }, + "description": "Authorization" + } + ] + } + }, + "/data": { + "post": { + "summary": "Create XML Data", + "description": "Operation for Create XML Data", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Default response schema" + } + } + } + }, + "400": { + "description": "Bad Request - The request was invalid", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Authentication required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "404": { + "description": "Not Found - The requested resource does not exist", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "Content-Type", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "application/xml" + }, + "description": "Content-Type" + }, + { + "name": "Authorization", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "Bearer {{token}}" + }, + "description": "Authorization" + } + ], + "requestBody": { + "content": { + "application/xml": { + "schema": { + "type": "string" + }, + "example": "\\n John Doe\\n john@example.com\\n 25\\n " + } + } + } + } + }, + "/users/123": { + "delete": { + "summary": "Delete User", + "description": "Operation for Delete User", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Default response schema" + } + } + } + }, + "400": { + "description": "Bad Request - The request was invalid", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Authentication required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "404": { + "description": "Not Found - The requested resource does not exist", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "Authorization", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "Bearer {{token}}" + }, + "description": "Authorization" + } + ] + }, + "head": { + "summary": "Head User", + "description": "Operation for Head User", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Default response schema" + } + } + } + }, + "400": { + "description": "Bad Request - The request was invalid", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Authentication required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "404": { + "description": "Not Found - The requested resource does not exist", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "Authorization", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "Bearer {{token}}" + }, + "description": "Authorization" + } + ] + }, + "options": { + "summary": "Options User", + "description": "Operation for Options User", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Default response schema" + } + } + } + }, + "400": { + "description": "Bad Request - The request was invalid", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Authentication required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "404": { + "description": "Not Found - The requested resource does not exist", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "Authorization", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "Bearer {{token}}" + }, + "description": "Authorization" + } + ] + }, + "patch": { + "summary": "Patch User", + "description": "Operation for Patch User", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Default response schema" + } + } + } + }, + "400": { + "description": "Bad Request - The request was invalid", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Authentication required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "404": { + "description": "Not Found - The requested resource does not exist", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "Content-Type", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "application/json" + }, + "description": "Content-Type" + }, + { + "name": "Authorization", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "Bearer {{token}}" + }, + "description": "Authorization" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "email": { + "type": "string" + } + } + }, + "example": { + "email": "newemail@example.com" + } + } + } + } + }, + "put": { + "summary": "Update User", + "description": "Operation for Update User", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Default response schema" + } + } + } + }, + "400": { + "description": "Bad Request - The request was invalid", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Authentication required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "404": { + "description": "Not Found - The requested resource does not exist", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "Content-Type", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "application/json" + }, + "description": "Content-Type" + }, + { + "name": "Authorization", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "Bearer {{token}}" + }, + "description": "Authorization" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "email": { + "type": "string" + }, + "age": { + "type": "integer" + } + } + }, + "example": { + "name": "Jane Doe", + "email": "jane@example.com", + "age": 30 + } + } + } + } + } + }, + "/users/%7B%7BuserId%7D%7D": { + "get": { + "summary": "Get User By ID", + "description": "Operation for Get User By ID", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Default response schema" + } + } + } + }, + "400": { + "description": "Bad Request - The request was invalid", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Authentication required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "404": { + "description": "Not Found - The requested resource does not exist", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "Content-Type", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "application/json" + }, + "description": "Content-Type" + }, + { + "name": "Authorization", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "Bearer {{token}}" + }, + "description": "Authorization" + } + ] + } + }, + "/users/search": { + "get": { + "summary": "Search Users", + "description": "Operation for Search Users", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Default response schema" + } + } + } + }, + "400": { + "description": "Bad Request - The request was invalid", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Authentication required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "404": { + "description": "Not Found - The requested resource does not exist", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "q", + "in": "query", + "required": true, + "schema": { + "type": "string", + "example": "john" + }, + "description": "q" + }, + { + "name": "status", + "in": "query", + "required": true, + "schema": { + "type": "string", + "example": "active" + }, + "description": "status" + }, + { + "name": "sort", + "in": "query", + "required": true, + "schema": { + "type": "string", + "example": "name" + }, + "description": "sort" + }, + { + "name": "order", + "in": "query", + "required": true, + "schema": { + "type": "string", + "example": "asc" + }, + "description": "order" + }, + { + "name": "Content-Type", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "application/json" + }, + "description": "Content-Type" + }, + { + "name": "Authorization", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "Bearer {{token}}" + }, + "description": "Authorization" + } + ] + } + }, + "/messages": { + "post": { + "summary": "Send Message", + "description": "Operation for Send Message", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Default response schema" + } + } + } + }, + "400": { + "description": "Bad Request - The request was invalid", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Authentication required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "404": { + "description": "Not Found - The requested resource does not exist", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "Content-Type", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "application/json" + }, + "description": "Content-Type" + }, + { + "name": "Authorization", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "Bearer {{token}}" + }, + "description": "Authorization" + } + ], + "requestBody": { + "content": { + "text/plain": { + "schema": { + "type": "string" + }, + "example": "This is a plain text message body" + } + } + } + } + }, + "/upload": { + "post": { + "summary": "Upload File", + "description": "Operation for Upload File", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Default response schema" + } + } + } + }, + "400": { + "description": "Bad Request - The request was invalid", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Authentication required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "404": { + "description": "Not Found - The requested resource does not exist", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "Authorization", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "Bearer {{token}}" + }, + "description": "Authorization" + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": {} + } + } + } + } + } + } + } +}" +`; + +exports[`convertBrunoCollectionToOpenAPISync - Snapshot Tests > auth-test-collection > should match expected OpenAPI specification snapshot 1`] = ` +"{ + "openapi": "3.0.0", + "info": { + "title": "auth-test-collection", + "version": "1.0.0", + "description": "OpenAPI specification generated from Bruno collection: auth-test-collection" + }, + "paths": { + "/protected": { + "get": { + "summary": "Request with Basic Auth", + "description": "Operation for Request with Basic Auth", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Default response schema" + } + } + } + }, + "400": { + "description": "Bad Request - The request was invalid", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Authentication required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "404": { + "description": "Not Found - The requested resource does not exist", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + } + }, + "security": [ + { + "basicAuth": [] + } + ] + } + }, + "/api/data": { + "get": { + "summary": "Request with Bearer Auth", + "description": "Operation for Request with Bearer Auth", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Default response schema" + } + } + } + }, + "400": { + "description": "Bad Request - The request was invalid", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Authentication required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "404": { + "description": "Not Found - The requested resource does not exist", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + } + } +}" +`; + +exports[`convertBrunoCollectionToOpenAPISync - Snapshot Tests > collection-with-bru-file > should match expected OpenAPI specification snapshot 1`] = ` +"{ + "openapi": "3.0.0", + "info": { + "title": "Collection with .bru file", + "version": "1", + "description": "OpenAPI specification generated from Bruno collection: Collection with .bru file" + }, + "paths": { + "/simple": { + "get": { + "summary": "Simple Request", + "description": "Operation for Simple Request", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Default response schema" + } + } + } + }, + "400": { + "description": "Bad Request - The request was invalid", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Authentication required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "404": { + "description": "Not Found - The requested resource does not exist", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + } + } + } + } + } +}" +`; + +exports[`convertBrunoCollectionToOpenAPISync - Snapshot Tests > collection-without-bruno-json > should match expected OpenAPI specification snapshot 1`] = ` +"{ + "openapi": "3.0.0", + "info": { + "title": "collection-without-bruno-json", + "version": "1.0.0", + "description": "OpenAPI specification generated from Bruno collection: collection-without-bruno-json" + }, + "paths": { + "/another": { + "post": { + "summary": "Another Request", + "description": "Operation for Another Request", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Default response schema" + } + } + } + }, + "400": { + "description": "Bad Request - The request was invalid", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Authentication required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "404": { + "description": "Not Found - The requested resource does not exist", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + } + } + } + }, + "/simple": { + "get": { + "summary": "Simple Request", + "description": "Operation for Simple Request", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Default response schema" + } + } + } + }, + "400": { + "description": "Bad Request - The request was invalid", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Authentication required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "404": { + "description": "Not Found - The requested resource does not exist", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + } + } + } + } + } +}" +`; + +exports[`convertBrunoCollectionToOpenAPISync - Snapshot Tests > empty-bru-file > should match expected OpenAPI specification snapshot 1`] = ` +"{ + "openapi": "3.0.0", + "info": { + "title": "empty-bru-file", + "version": "1.0.0", + "description": "OpenAPI specification generated from Bruno collection: empty-bru-file" + }, + "paths": {} +}" +`; + +exports[`convertBrunoCollectionToOpenAPISync - Snapshot Tests > empty-collection > should match expected OpenAPI specification snapshot 1`] = ` +"{ + "openapi": "3.0.0", + "info": { + "title": "empty-collection", + "version": "1.0.0", + "description": "OpenAPI specification generated from Bruno collection: empty-collection" + }, + "paths": {} +}" +`; + +exports[`convertBrunoCollectionToOpenAPISync - Snapshot Tests > minimal-bru-file > should match expected OpenAPI specification snapshot 1`] = ` +"{ + "openapi": "3.0.0", + "info": { + "title": "minimal-bru-file", + "version": "1.0.0", + "description": "OpenAPI specification generated from Bruno collection: minimal-bru-file" + }, + "paths": {} +}" +`; + +exports[`convertBrunoCollectionToOpenAPISync - Snapshot Tests > sample-bruno-collection > should match expected OpenAPI specification snapshot 1`] = ` +"{ + "openapi": "3.0.0", + "info": { + "title": "Test API Collection", + "version": "1", + "description": "OpenAPI specification generated from Bruno collection: Test API Collection" + }, + "paths": { + "/users/form": { + "post": { + "summary": "Create User Form", + "description": "Operation for Create User Form", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Default response schema" + } + } + } + }, + "400": { + "description": "Bad Request - The request was invalid", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Authentication required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "404": { + "description": "Not Found - The requested resource does not exist", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "Content-Type", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "application/x-www-form-urlencoded" + }, + "description": "Content-Type" + }, + { + "name": "Authorization", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "Bearer {{token}}" + }, + "description": "Authorization" + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "John Doe" + }, + "email": { + "type": "string", + "example": "john@example.com" + }, + "age": { + "type": "string", + "example": "25" + } + } + } + } + } + } + } + }, + "/users": { + "post": { + "summary": "Create User", + "description": "Operation for Create User", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Default response schema" + } + } + } + }, + "400": { + "description": "Bad Request - The request was invalid", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Authentication required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "404": { + "description": "Not Found - The requested resource does not exist", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "Content-Type", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "application/json" + }, + "description": "Content-Type" + }, + { + "name": "Authorization", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "Bearer {{token}}" + }, + "description": "Authorization" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "email": { + "type": "string" + } + } + }, + "example": { + "name": "John Doe", + "email": "john@example.com" + } + } + } + } + }, + "get": { + "summary": "Get Users", + "description": "Operation for Get Users", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Default response schema" + } + } + } + }, + "400": { + "description": "Bad Request - The request was invalid", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Authentication required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "404": { + "description": "Not Found - The requested resource does not exist", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "limit", + "in": "query", + "required": true, + "schema": { + "type": "string", + "example": "10" + }, + "description": "limit" + }, + { + "name": "offset", + "in": "query", + "required": true, + "schema": { + "type": "string", + "example": "0" + }, + "description": "offset" + }, + { + "name": "Content-Type", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "application/json" + }, + "description": "Content-Type" + }, + { + "name": "Authorization", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "Bearer {{token}}" + }, + "description": "Authorization" + } + ] + } + }, + "/data": { + "post": { + "summary": "Create XML Data", + "description": "Operation for Create XML Data", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Default response schema" + } + } + } + }, + "400": { + "description": "Bad Request - The request was invalid", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Authentication required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "404": { + "description": "Not Found - The requested resource does not exist", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "Content-Type", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "application/xml" + }, + "description": "Content-Type" + }, + { + "name": "Authorization", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "Bearer {{token}}" + }, + "description": "Authorization" + } + ], + "requestBody": { + "content": { + "application/xml": { + "schema": { + "type": "string" + }, + "example": "\\n John Doe\\n john@example.com\\n 25\\n " + } + } + } + } + }, + "/users/123": { + "delete": { + "summary": "Delete User", + "description": "Operation for Delete User", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Default response schema" + } + } + } + }, + "400": { + "description": "Bad Request - The request was invalid", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Authentication required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "404": { + "description": "Not Found - The requested resource does not exist", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "Authorization", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "Bearer {{token}}" + }, + "description": "Authorization" + } + ] + }, + "head": { + "summary": "Head User", + "description": "Operation for Head User", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Default response schema" + } + } + } + }, + "400": { + "description": "Bad Request - The request was invalid", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Authentication required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "404": { + "description": "Not Found - The requested resource does not exist", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "Authorization", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "Bearer {{token}}" + }, + "description": "Authorization" + } + ] + }, + "options": { + "summary": "Options User", + "description": "Operation for Options User", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Default response schema" + } + } + } + }, + "400": { + "description": "Bad Request - The request was invalid", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Authentication required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "404": { + "description": "Not Found - The requested resource does not exist", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "Authorization", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "Bearer {{token}}" + }, + "description": "Authorization" + } + ] + }, + "patch": { + "summary": "Patch User", + "description": "Operation for Patch User", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Default response schema" + } + } + } + }, + "400": { + "description": "Bad Request - The request was invalid", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Authentication required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "404": { + "description": "Not Found - The requested resource does not exist", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "Content-Type", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "application/json" + }, + "description": "Content-Type" + }, + { + "name": "Authorization", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "Bearer {{token}}" + }, + "description": "Authorization" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "email": { + "type": "string" + } + } + }, + "example": { + "email": "newemail@example.com" + } + } + } + } + }, + "put": { + "summary": "Update User", + "description": "Operation for Update User", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Default response schema" + } + } + } + }, + "400": { + "description": "Bad Request - The request was invalid", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Authentication required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "404": { + "description": "Not Found - The requested resource does not exist", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "Content-Type", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "application/json" + }, + "description": "Content-Type" + }, + { + "name": "Authorization", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "Bearer {{token}}" + }, + "description": "Authorization" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "email": { + "type": "string" + }, + "age": { + "type": "integer" + } + } + }, + "example": { + "name": "Jane Doe", + "email": "jane@example.com", + "age": 30 + } + } + } + } + } + }, + "/users/%7B%7BuserId%7D%7D": { + "get": { + "summary": "Get User By ID", + "description": "Operation for Get User By ID", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Default response schema" + } + } + } + }, + "400": { + "description": "Bad Request - The request was invalid", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Authentication required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "404": { + "description": "Not Found - The requested resource does not exist", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "Content-Type", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "application/json" + }, + "description": "Content-Type" + }, + { + "name": "Authorization", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "Bearer {{token}}" + }, + "description": "Authorization" + } + ] + } + }, + "/users/search": { + "get": { + "summary": "Search Users", + "description": "Operation for Search Users", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Default response schema" + } + } + } + }, + "400": { + "description": "Bad Request - The request was invalid", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Authentication required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "404": { + "description": "Not Found - The requested resource does not exist", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "q", + "in": "query", + "required": true, + "schema": { + "type": "string", + "example": "john" + }, + "description": "q" + }, + { + "name": "status", + "in": "query", + "required": true, + "schema": { + "type": "string", + "example": "active" + }, + "description": "status" + }, + { + "name": "sort", + "in": "query", + "required": true, + "schema": { + "type": "string", + "example": "name" + }, + "description": "sort" + }, + { + "name": "order", + "in": "query", + "required": true, + "schema": { + "type": "string", + "example": "asc" + }, + "description": "order" + }, + { + "name": "Content-Type", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "application/json" + }, + "description": "Content-Type" + }, + { + "name": "Authorization", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "Bearer {{token}}" + }, + "description": "Authorization" + } + ] + } + }, + "/messages": { + "post": { + "summary": "Send Message", + "description": "Operation for Send Message", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Default response schema" + } + } + } + }, + "400": { + "description": "Bad Request - The request was invalid", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Authentication required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "404": { + "description": "Not Found - The requested resource does not exist", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "Content-Type", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "application/json" + }, + "description": "Content-Type" + }, + { + "name": "Authorization", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "Bearer {{token}}" + }, + "description": "Authorization" + } + ], + "requestBody": { + "content": { + "text/plain": { + "schema": { + "type": "string" + }, + "example": "This is a plain text message body" + } + } + } + } + }, + "/upload": { + "post": { + "summary": "Upload File", + "description": "Operation for Upload File", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Default response schema" + } + } + } + }, + "400": { + "description": "Bad Request - The request was invalid", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Authentication required", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "404": { + "description": "Not Found - The requested resource does not exist", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "Authorization", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "Bearer {{token}}" + }, + "description": "Authorization" + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": {} + } + } + } + } + } + } + } +}" +`; diff --git a/tests/unit/index.test.ts b/tests/unit/index.test.ts index 36603b9..359a1f8 100644 --- a/tests/unit/index.test.ts +++ b/tests/unit/index.test.ts @@ -1,145 +1,478 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { join } from 'node:path' +import SwaggerParser from '@apidevtools/swagger-parser' +import type { OpenAPI } from 'openapi-types' +import { describe, expect, it } from 'vitest' import { convertBrunoCollectionToOpenAPI, convertBrunoCollectionToOpenAPISync, - isValidBrunoCollection, } from '../../src/index.js' -import { BrunoParser } from '../../src/utils/bruno-parser.js' -import { OpenApiGenerator } from '../../src/utils/openapi-generator.js' - -// Mock the utility classes -vi.mock('../../src/utils/bruno-parser.js', () => ({ - BrunoParser: { - parseCollection: vi.fn(), - parseCollectionSync: vi.fn(), - isValidCollection: vi.fn(), - }, -})) - -vi.mock('../../src/utils/openapi-generator.js', () => ({ - OpenApiGenerator: { - generateOpenApiSpec: vi.fn(), - }, -})) - -describe('index.ts functions', () => { - beforeEach(() => { - vi.clearAllMocks() - }) +import type { OpenAPIObject } from '../../src/types/openapi.js' +import type { ConvertResult } from '../../src/types/result.js' - describe('convertBrunoCollectionToOpenAPI', () => { - it('should convert a Bruno collection to OpenAPI successfully', async () => { - const mockCollection = { - name: 'Test Collection', - version: '1' as const, - items: [], - uid: 'test-uid', - pathname: '/test/path', - } - - const mockResult = { - spec: { - openapi: '3.0.0' as const, - info: { title: 'Test', version: '1.0.0' }, - paths: {}, - }, - warnings: [], - content: JSON.stringify({ - openapi: '3.0.0', - info: { title: 'Test', version: '1.0.0' }, - paths: {}, - }), - } - - vi.mocked(BrunoParser.parseCollection).mockResolvedValue(mockCollection) - vi.mocked(OpenApiGenerator.generateOpenApiSpec).mockReturnValue(mockResult) - - const result = await convertBrunoCollectionToOpenAPI('/test/collection') - - expect(BrunoParser.parseCollection).toHaveBeenCalledWith('/test/collection') - expect(OpenApiGenerator.generateOpenApiSpec).toHaveBeenCalledWith(mockCollection) - expect(result).toEqual(mockResult) - }) +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} - it('should throw an error when collection parsing fails', async () => { - vi.mocked(BrunoParser.parseCollection).mockRejectedValue(new Error('Parse error')) +function isArrayOfUnknown(value: unknown): value is Array { + return Array.isArray(value) +} - await expect(convertBrunoCollectionToOpenAPI('/test/collection')).rejects.toThrow( - 'Failed to convert Bruno collection to OpenAPI: Parse error', - ) - }) - }) +function isError(error: unknown): error is Error { + return error instanceof Error +} - describe('isValidBrunoCollection', () => { - it('should return true for a valid Bruno collection', async () => { - vi.mocked(BrunoParser.isValidCollection).mockResolvedValue(true) +function hasProperty(obj: unknown, prop: T): obj is Record { + return isRecord(obj) && prop in obj +} - const result = await isValidBrunoCollection('/test/collection') +function hasNameProperty(obj: unknown): obj is Record & { name: unknown } { + return hasProperty(obj, 'name') +} - expect(BrunoParser.isValidCollection).toHaveBeenCalledWith('/test/collection') - expect(result).toBe(true) - }) +function toOpenAPIDocument(spec: OpenAPIObject): OpenAPI.Document { + if ( + typeof spec !== 'object' || + spec === null || + !('openapi' in spec) || + !('info' in spec) || + !('paths' in spec) || + typeof spec.openapi !== 'string' || + typeof spec.info !== 'object' || + spec.info === null || + typeof spec.paths !== 'object' || + spec.paths === null + ) { + throw new Error('Invalid OpenAPI specification structure') + } + return spec +} - it('should return false for an invalid Bruno collection', async () => { - vi.mocked(BrunoParser.isValidCollection).mockResolvedValue(false) +function validateOpenAPIStructure(spec: OpenAPIObject): void { + expect(spec).toBeDefined() + expect(typeof spec).toBe('object') + expect(spec).not.toBeNull() - const result = await isValidBrunoCollection('/test/collection') + expect(spec.openapi).toBe('3.0.0') + expect(spec.info).toBeDefined() + expect(typeof spec.info).toBe('object') + expect(spec.info.title).toBeDefined() + expect(typeof spec.info.title).toBe('string') + expect(spec.info.version).toBeDefined() + expect(typeof spec.info.version).toBe('string') - expect(BrunoParser.isValidCollection).toHaveBeenCalledWith('/test/collection') - expect(result).toBe(false) - }) + expect(spec.paths).toBeDefined() + expect(typeof spec.paths).toBe('object') +} + +function validateParameter(param: unknown, expectedName: string): void { + expect(param).toBeDefined() + expect(param).not.toBeNull() + expect(typeof param).toBe('object') + + expect(isRecord(param)).toBe(true) + if (!isRecord(param)) return + + expect(param).toHaveProperty('name', expectedName) + expect(typeof param.name).toBe('string') + expect(param).toHaveProperty('in') + expect(['query', 'header', 'path', 'cookie']).toContain(param.in) +} + +function validateOperation(operation: unknown): void { + expect(operation).toBeDefined() + expect(operation).not.toBeNull() + expect(typeof operation).toBe('object') + + expect(isRecord(operation)).toBe(true) + if (!isRecord(operation)) return + + expect(operation).toHaveProperty('summary') + expect(typeof operation.summary).toBe('string') + expect(operation).toHaveProperty('description') + expect(typeof operation.description).toBe('string') + expect(operation).toHaveProperty('responses') + expect(typeof operation.responses).toBe('object') - it('should return false when validation throws an error', async () => { - vi.mocked(BrunoParser.isValidCollection).mockRejectedValue(new Error('Validation error')) + expect(isRecord(operation.responses)).toBe(true) + if (!isRecord(operation.responses)) return + + const responses = operation.responses + expect(responses).toHaveProperty('200') + expect(responses).toHaveProperty('400') + expect(responses).toHaveProperty('401') + expect(responses).toHaveProperty('404') + expect(responses).toHaveProperty('500') + + for (const code of ['200', '400', '401', '404', '500']) { + const responseValue = responses[code] + expect(isRecord(responseValue)).toBe(true) + if (!isRecord(responseValue)) continue + + expect(responseValue).toHaveProperty('description') + expect(typeof responseValue.description).toBe('string') + } +} + +function validateWarnings(result: ConvertResult): void { + expect(result).toBeDefined() + expect(result.spec).toBeDefined() + expect(result.warnings).toBeDefined() + expect(Array.isArray(result.warnings)).toBe(true) + + for (const warning of result.warnings) { + expect(warning).toBeDefined() + expect(typeof warning).toBe('object') + expect(warning.message).toBeDefined() + expect(typeof warning.message).toBe('string') + expect(warning.itemName).toBeDefined() + expect(typeof warning.itemName).toBe('string') + } +} + +async function validateWithSwaggerParser(spec: OpenAPIObject): Promise { + let validationError: Error | null = null + try { + const result = await SwaggerParser.validate(toOpenAPIDocument(spec)) + expect(result).toBeDefined() + } catch (error) { + if (isError(error)) { + validationError = error + } + } + + expect(validationError).toBeNull() +} + +const fixturesDir = join(__dirname, '../fixtures') + +const fixtureDirs = [ + 'auth-test-collection', + 'collection-with-bru-file', + 'collection-without-bruno-json', + 'empty-bru-file', + 'empty-collection', + 'minimal-bru-file', + 'sample-bruno-collection', +] + +describe('convertBrunoCollectionToOpenAPI - Snapshot Tests', () => { + for (const fixtureDir of fixtureDirs) { + describe(fixtureDir, () => { + it('should generate a valid OpenAPI specification', async () => { + const fixturePath = join(fixturesDir, fixtureDir) + const result = await convertBrunoCollectionToOpenAPI(fixturePath) + + validateWarnings(result) + validateOpenAPIStructure(result.spec) + await validateWithSwaggerParser(result.spec) + }) - const result = await isValidBrunoCollection('/test/collection') + it('should match expected OpenAPI specification snapshot', async () => { + const fixturePath = join(fixturesDir, fixtureDir) + const result = await convertBrunoCollectionToOpenAPI(fixturePath) - expect(result).toBe(false) // Should return false on error, not throw + validateWarnings(result) + expect(JSON.stringify(result.spec, null, 2)).toMatchSnapshot() + }) + }) + } + + describe('invalid-bruno-json-collection', () => { + it('should throw an error for invalid JSON', async () => { + const fixturePath = join(fixturesDir, 'invalid-bruno-json-collection') + await expect(convertBrunoCollectionToOpenAPI(fixturePath)).rejects.toThrow() }) }) - describe('convertBrunoCollectionToOpenAPISync', () => { - it('should convert a Bruno collection to OpenAPI synchronously', () => { - const mockCollection = { - name: 'Test Collection', - version: '1' as const, - items: [], - uid: 'test-uid', - pathname: '/test/path', - } - - const mockResult = { - spec: { - openapi: '3.0.0' as const, - info: { title: 'Test', version: '1.0.0' }, - paths: {}, - }, - warnings: [], - content: JSON.stringify({ - openapi: '3.0.0', - info: { title: 'Test', version: '1.0.0' }, - paths: {}, - }), - } - - vi.mocked(BrunoParser.parseCollectionSync).mockReturnValue(mockCollection) - vi.mocked(OpenApiGenerator.generateOpenApiSpec).mockReturnValue(mockResult) - - const result = convertBrunoCollectionToOpenAPISync('/test/collection') - - expect(BrunoParser.parseCollectionSync).toHaveBeenCalledWith('/test/collection') - expect(OpenApiGenerator.generateOpenApiSpec).toHaveBeenCalledWith(mockCollection) - expect(result).toEqual(mockResult) + describe('non-object-bruno-json-collection', () => { + it('should throw an error when bruno.json is not an object', async () => { + const fixturePath = join(fixturesDir, 'non-object-bruno-json-collection') + await expect(convertBrunoCollectionToOpenAPI(fixturePath)).rejects.toThrow() }) + }) +}) + +describe('convertBrunoCollectionToOpenAPISync - Snapshot Tests', () => { + for (const fixtureDir of fixtureDirs) { + describe(fixtureDir, () => { + it('should generate a valid OpenAPI specification', async () => { + const fixturePath = join(fixturesDir, fixtureDir) + const result = convertBrunoCollectionToOpenAPISync(fixturePath) - it('should throw an error when collection parsing fails synchronously', () => { - vi.mocked(BrunoParser.parseCollectionSync).mockImplementation(() => { - throw new Error('Parse error') + validateWarnings(result) + validateOpenAPIStructure(result.spec) + await validateWithSwaggerParser(result.spec) }) - expect(() => convertBrunoCollectionToOpenAPISync('/test/collection')).toThrow( - 'Failed to convert Bruno collection to OpenAPI: Parse error', - ) + it('should match expected OpenAPI specification snapshot', () => { + const fixturePath = join(fixturesDir, fixtureDir) + const result = convertBrunoCollectionToOpenAPISync(fixturePath) + + validateWarnings(result) + expect(JSON.stringify(result.spec, null, 2)).toMatchSnapshot() + }) + }) + } + + describe('invalid-bruno-json-collection', () => { + it('should throw an error for invalid JSON', () => { + const fixturePath = join(fixturesDir, 'invalid-bruno-json-collection') + expect(() => { + convertBrunoCollectionToOpenAPISync(fixturePath) + }).toThrow() }) }) + + describe('non-object-bruno-json-collection', () => { + it('should throw an error when bruno.json is not an object', () => { + const fixturePath = join(fixturesDir, 'non-object-bruno-json-collection') + expect(() => { + convertBrunoCollectionToOpenAPISync(fixturePath) + }).toThrow() + }) + }) +}) + +describe('sample-bruno-collection - Field Validation', () => { + const fixturePath = join(fixturesDir, 'sample-bruno-collection') + + it('should contain all expected fields in the parsed OpenAPI spec', async () => { + const result = await convertBrunoCollectionToOpenAPI(fixturePath) + + validateWarnings(result) + await validateWithSwaggerParser(result.spec) + + const spec = result.spec + validateOpenAPIStructure(spec) + + const paths = spec.paths + expect(paths).toHaveProperty('/users') + expect(typeof paths['/users']).toBe('object') + const usersPath = paths['/users'] + expect(usersPath).toBeDefined() + expect(usersPath).not.toBeNull() + if (!usersPath) return + + expect(usersPath).toHaveProperty('get') + expect(usersPath).toHaveProperty('post') + + const getUsers = usersPath.get + expect(getUsers).toBeDefined() + expect(getUsers).not.toBeNull() + if (!getUsers) return + + validateOperation(getUsers) + + expect(getUsers).toHaveProperty('parameters') + expect(Array.isArray(getUsers.parameters)).toBe(true) + expect(isArrayOfUnknown(getUsers.parameters)).toBe(true) + + if (!isArrayOfUnknown(getUsers.parameters)) return + const getUsersParams = getUsers.parameters + + const limitParam = getUsersParams.find(p => hasNameProperty(p) && p.name === 'limit') + const offsetParam = getUsersParams.find(p => hasNameProperty(p) && p.name === 'offset') + + expect(limitParam).toBeDefined() + expect(offsetParam).toBeDefined() + validateParameter(limitParam, 'limit') + validateParameter(offsetParam, 'offset') + + const postUsers = usersPath.post + expect(postUsers).toBeDefined() + expect(postUsers).not.toBeNull() + if (!postUsers) return + + validateOperation(postUsers) + expect(postUsers).toHaveProperty('requestBody') + + expect(paths).toHaveProperty('/users/123') + expect(typeof paths['/users/123']).toBe('object') + const users123Path = paths['/users/123'] + expect(users123Path).toBeDefined() + expect(users123Path).not.toBeNull() + if (!users123Path) return + + expect(users123Path).toHaveProperty('put') + expect(users123Path).toHaveProperty('patch') + expect(users123Path).toHaveProperty('delete') + expect(users123Path).toHaveProperty('head') + expect(users123Path).toHaveProperty('options') + + const putUser = users123Path.put + expect(putUser).toBeDefined() + expect(putUser).not.toBeNull() + if (!putUser) return + + expect(putUser).toHaveProperty('requestBody') + expect(putUser).toHaveProperty('responses') + + const userByIdPathKey = '/users/%7B%7BuserId%7D%7D' + expect(paths).toHaveProperty(userByIdPathKey) + expect(typeof paths[userByIdPathKey]).toBe('object') + const userByIdPath = paths[userByIdPathKey] + expect(userByIdPath).toBeDefined() + expect(userByIdPath).not.toBeNull() + if (!userByIdPath) return + + expect(userByIdPath).toHaveProperty('get') + const getUserById = userByIdPath.get + expect(getUserById).toBeDefined() + expect(getUserById).not.toBeNull() + if (!getUserById) return + + validateOperation(getUserById) + + expect(paths).toHaveProperty('/users/search') + expect(typeof paths['/users/search']).toBe('object') + const searchPath = paths['/users/search'] + expect(searchPath).toBeDefined() + expect(searchPath).not.toBeNull() + if (!searchPath) return + + expect(searchPath).toHaveProperty('get') + const searchUsers = searchPath.get + expect(searchUsers).toBeDefined() + expect(searchUsers).not.toBeNull() + if (!searchUsers) return + + validateOperation(searchUsers) + + expect(searchUsers).toHaveProperty('parameters') + expect(Array.isArray(searchUsers.parameters)).toBe(true) + expect(isArrayOfUnknown(searchUsers.parameters)).toBe(true) + + if (!isArrayOfUnknown(searchUsers.parameters)) return + const searchParams = searchUsers.parameters + + const qParam = searchParams.find(p => hasNameProperty(p) && p.name === 'q') + const statusParam = searchParams.find(p => hasNameProperty(p) && p.name === 'status') + const sortParam = searchParams.find(p => hasNameProperty(p) && p.name === 'sort') + const orderParam = searchParams.find(p => hasNameProperty(p) && p.name === 'order') + + expect(qParam).toBeDefined() + expect(statusParam).toBeDefined() + expect(sortParam).toBeDefined() + expect(orderParam).toBeDefined() + validateParameter(qParam, 'q') + validateParameter(statusParam, 'status') + validateParameter(sortParam, 'sort') + validateParameter(orderParam, 'order') + + expect(paths).toHaveProperty('/users/form') + expect(typeof paths['/users/form']).toBe('object') + const formPath = paths['/users/form'] + expect(formPath).toBeDefined() + expect(formPath).not.toBeNull() + if (!formPath) return + + expect(formPath).toHaveProperty('post') + const postForm = formPath.post + expect(postForm).toBeDefined() + expect(postForm).not.toBeNull() + if (!postForm) return + + validateOperation(postForm) + expect(postForm).toHaveProperty('requestBody') + + expect(paths).toHaveProperty('/messages') + expect(typeof paths['/messages']).toBe('object') + const messagesPath = paths['/messages'] + expect(messagesPath).toBeDefined() + expect(messagesPath).not.toBeNull() + if (!messagesPath) return + + expect(messagesPath).toHaveProperty('post') + const postMessage = messagesPath.post + expect(postMessage).toBeDefined() + expect(postMessage).not.toBeNull() + if (!postMessage) return + + validateOperation(postMessage) + expect(postMessage).toHaveProperty('requestBody') + + expect(paths).toHaveProperty('/data') + expect(typeof paths['/data']).toBe('object') + const dataPath = paths['/data'] + expect(dataPath).toBeDefined() + expect(dataPath).not.toBeNull() + if (!dataPath) return + + expect(dataPath).toHaveProperty('post') + const postData = dataPath.post + expect(postData).toBeDefined() + expect(postData).not.toBeNull() + if (!postData) return + + validateOperation(postData) + expect(postData).toHaveProperty('requestBody') + + expect(paths).toHaveProperty('/upload') + expect(typeof paths['/upload']).toBe('object') + const uploadPath = paths['/upload'] + expect(uploadPath).toBeDefined() + expect(uploadPath).not.toBeNull() + + expect(uploadPath).toHaveProperty('post') + }) + + it('should have proper content types for different body types', async () => { + const result = await convertBrunoCollectionToOpenAPI(fixturePath) + + validateWarnings(result) + + const spec = result.spec + const paths = spec.paths + + const formPath = paths['/users/form'] + expect(formPath).toBeDefined() + expect(formPath).toHaveProperty('post') + + if (formPath?.post?.requestBody && typeof formPath.post.requestBody === 'object') { + const requestBody = formPath.post.requestBody + expect(isRecord(requestBody)).toBe(true) + if (!isRecord(requestBody)) return + + expect(requestBody).toHaveProperty('content') + expect(typeof requestBody.content).toBe('object') + expect(isRecord(requestBody.content)).toBe(true) + if (!isRecord(requestBody.content)) return + + expect(requestBody.content).toHaveProperty('application/x-www-form-urlencoded') + } + + const dataPath = paths['/data'] + expect(dataPath).toBeDefined() + expect(dataPath).toHaveProperty('post') + + if (dataPath?.post?.requestBody && typeof dataPath.post.requestBody === 'object') { + const requestBody = dataPath.post.requestBody + expect(isRecord(requestBody)).toBe(true) + if (!isRecord(requestBody)) return + + expect(requestBody).toHaveProperty('content') + expect(typeof requestBody.content).toBe('object') + expect(isRecord(requestBody.content)).toBe(true) + if (!isRecord(requestBody.content)) return + + expect(requestBody.content).toHaveProperty('application/xml') + } + + const messagesPath = paths['/messages'] + expect(messagesPath).toBeDefined() + expect(messagesPath).toHaveProperty('post') + + if (messagesPath?.post?.requestBody && typeof messagesPath.post.requestBody === 'object') { + const requestBody = messagesPath.post.requestBody + expect(isRecord(requestBody)).toBe(true) + if (!isRecord(requestBody)) return + + expect(requestBody).toHaveProperty('content') + expect(typeof requestBody.content).toBe('object') + expect(isRecord(requestBody.content)).toBe(true) + if (!isRecord(requestBody.content)) return + + expect(requestBody.content).toHaveProperty('text/plain') + } + }) }) From 6b45e91491e9c756c5d6ab9e10857b89ef06226b Mon Sep 17 00:00:00 2001 From: GyeongHo Kim Date: Wed, 7 Jan 2026 21:29:37 +0900 Subject: [PATCH 6/9] feat: support bru-lang MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - OpenAPI 관련 타입을 openapi-types 라이브러리를 re-export하는 식으로 변경 - 기존에 *.bru 파일들 파싱을 INI 형식으로 착각한 점, 공식문서 참고하여 bru-lang을 지원하게 변경 --- package-lock.json | 12 +- package.json | 3 + src/models/bruno-collection.ts | 4 +- src/models/errors.ts | 4 +- src/types/openapi.ts | 263 ++------- src/utils/bruno-parser.ts | 1015 ++++++++++++++++++++++---------- src/utils/openapi-generator.ts | 60 +- 7 files changed, 802 insertions(+), 559 deletions(-) diff --git a/package-lock.json b/package-lock.json index 18e751f..d6e56b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "@gyeonghokim/bruno-to-openapi", "version": "0.0.0", "license": "MIT", + "dependencies": { + "openapi-types": "^12.1.3" + }, "devDependencies": { "@apidevtools/swagger-parser": "^12.1.0", "@biomejs/biome": "^1.0.0", @@ -771,6 +774,7 @@ "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", @@ -1684,6 +1688,7 @@ "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~5.26.4" } @@ -1828,6 +1833,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -3485,6 +3491,7 @@ "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", "dev": true, "license": "MIT", + "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -5738,6 +5745,7 @@ "dev": true, "inBundle": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -5900,7 +5908,6 @@ "version": "12.1.3", "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", - "dev": true, "license": "MIT", "peer": true }, @@ -6522,6 +6529,7 @@ "integrity": "sha512-6qGjWccl5yoyugHt3jTgztJ9Y0JVzyH8/Voc/D8PlLat9pwxQYXz7W1Dpnq5h0/G5GCYGUaDSlYcyk3AMh5A6g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@semantic-release/commit-analyzer": "^13.0.1", "@semantic-release/error": "^4.0.0", @@ -7281,6 +7289,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -7376,6 +7385,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 6b109df..169104d 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,9 @@ "typescript": "^5.9.3", "vitest": "^1.0.0" }, + "dependencies": { + "openapi-types": "^12.1.3" + }, "engines": { "node": ">=18.0.0" } diff --git a/src/models/bruno-collection.ts b/src/models/bruno-collection.ts index 586da80..78d66de 100644 --- a/src/models/bruno-collection.ts +++ b/src/models/bruno-collection.ts @@ -61,8 +61,6 @@ export class BrunoCollectionModel implements BrunoCollection { * Generates a unique identifier */ private generateUid(): string { - // In a real implementation, you might want to use nanoid or similar - // For now, we'll create a simple UID based on timestamp and random number - return Date.now().toString(36) + Math.random().toString(36).substr(2, 5) + return crypto.randomUUID() } } diff --git a/src/models/errors.ts b/src/models/errors.ts index c5c5f2c..d5bb2a6 100644 --- a/src/models/errors.ts +++ b/src/models/errors.ts @@ -21,8 +21,7 @@ export class InvalidCollectionPathError extends Error { */ export class InvalidJsonError extends Error { constructor(filePath: string, parseError: unknown) { - const message = - parseError instanceof Error ? parseError.message : 'Unknown error' + const message = parseError instanceof Error ? parseError.message : 'Unknown error' super(`Invalid JSON in ${filePath}: ${message}`) this.name = 'InvalidJsonError' if (Error.captureStackTrace) { @@ -57,4 +56,3 @@ export class BruFileParseError extends Error { } } } - diff --git a/src/types/openapi.ts b/src/types/openapi.ts index c682590..f91d3e4 100644 --- a/src/types/openapi.ts +++ b/src/types/openapi.ts @@ -1,231 +1,34 @@ // Type definitions for OpenAPI entities -// Based on the OpenAPI 3.0 specification - -export interface OpenAPIObject { - openapi: '3.0.0' - info: InfoObject - paths: { [path: string]: PathItemObject } - servers?: ServerObject[] - components?: ComponentsObject - security?: SecurityRequirementObject[] - tags?: TagObject[] - externalDocs?: ExternalDocumentationObject -} - -export interface InfoObject { - title: string - description?: string - termsOfService?: string - contact?: ContactObject - license?: LicenseObject - version: string -} - -export interface ContactObject { - name?: string - url?: string - email?: string -} - -export interface LicenseObject { - name: string - url?: string -} - -export interface ServerObject { - url: string - description?: string - variables?: { [name: string]: ServerVariableObject } -} - -export interface ServerVariableObject { - enum?: string[] - default: string - description?: string -} - -export interface PathItemObject { - $ref?: string - summary?: string - description?: string - get?: OperationObject - put?: OperationObject - post?: OperationObject - delete?: OperationObject - options?: OperationObject - head?: OperationObject - patch?: OperationObject - trace?: OperationObject - servers?: ServerObject[] - parameters?: (ParameterObject | ReferenceObject)[] -} - -export interface OperationObject { - tags?: string[] - summary?: string - description?: string - externalDocs?: ExternalDocumentationObject - operationId?: string - parameters?: (ParameterObject | ReferenceObject)[] - requestBody?: RequestBodyObject | ReferenceObject - responses: ResponsesObject - callbacks?: { [name: string]: CallbackObject | ReferenceObject } - deprecated?: boolean - security?: SecurityRequirementObject[] - servers?: ServerObject[] -} - -export interface ExternalDocumentationObject { - description?: string - url: string -} - -export interface ParameterObject { - name: string - in: 'query' | 'header' | 'path' | 'cookie' - description?: string - required?: boolean - deprecated?: boolean - allowEmptyValue?: boolean - style?: string - explode?: boolean - allowReserved?: boolean - schema?: SchemaObject | ReferenceObject - example?: string | number | boolean | object | null - examples?: { [name: string]: ExampleObject | ReferenceObject } -} - -export interface RequestBodyObject { - description?: string - content: { [contentType: string]: MediaTypeObject } - required?: boolean -} - -export interface MediaTypeObject { - schema?: SchemaObject | ReferenceObject - example?: string | number | boolean | object | null - examples?: { [name: string]: ExampleObject | ReferenceObject } - encoding?: { [name: string]: EncodingObject } -} - -export interface EncodingObject { - contentType?: string - headers?: { [name: string]: HeaderObject | ReferenceObject } - style?: string - explode?: boolean - allowReserved?: boolean -} - -export interface HeaderObject { - description?: string - required?: boolean - deprecated?: boolean - allowEmptyValue?: boolean - style?: string - explode?: boolean - allowReserved?: boolean - schema?: SchemaObject | ReferenceObject - example?: string | number | boolean | object | null - examples?: { [name: string]: ExampleObject | ReferenceObject } -} - -export interface ResponsesObject { - [status: string]: ResponseObject | ReferenceObject -} - -export interface ResponseObject { - description: string - headers?: { [name: string]: HeaderObject | ReferenceObject } - content?: { [contentType: string]: MediaTypeObject } - links?: { [name: string]: LinkObject | ReferenceObject } -} - -export interface CallbackObject { - [name: string]: PathItemObject -} - -export interface ExampleObject { - summary?: string - description?: string - value?: string | number | boolean | object | null - externalValue?: string -} - -export interface LinkObject { - operationRef?: string - operationId?: string - parameters?: { [name: string]: string | number | boolean | object | null } - requestBody?: string | number | boolean | object | null - description?: string - server?: ServerObject -} - -export interface SchemaObject { - type?: string - properties?: { [name: string]: SchemaObject | ReferenceObject } - items?: SchemaObject | ReferenceObject - required?: string[] - description?: string - format?: string - default?: string | number | boolean | object | null - nullable?: boolean - enum?: (string | number | boolean | object | null)[] - minimum?: number - maximum?: number - minLength?: number - maxLength?: number - pattern?: string - [property: string]: unknown -} - -export interface ReferenceObject { - $ref: string -} - -export interface ComponentsObject { - schemas?: { [name: string]: SchemaObject | ReferenceObject } - responses?: { [name: string]: ResponseObject | ReferenceObject } - parameters?: { [name: string]: ParameterObject | ReferenceObject } - examples?: { [name: string]: ExampleObject | ReferenceObject } - requestBodies?: { [name: string]: RequestBodyObject | ReferenceObject } - headers?: { [name: string]: HeaderObject | ReferenceObject } - securitySchemes?: { [name: string]: SecuritySchemeObject | ReferenceObject } - links?: { [name: string]: LinkObject | ReferenceObject } - callbacks?: { [name: string]: CallbackObject | ReferenceObject } - [key: string]: unknown // Index signature to allow additional properties -} - -export interface SecuritySchemeObject { - type: 'apiKey' | 'http' | 'oauth2' | 'openIdConnect' - description?: string - name?: string - in?: 'query' | 'header' | 'cookie' - scheme?: string - bearerFormat?: string - flows?: OAuthFlowsObject - openIdConnectUrl?: string -} - -export interface OAuthFlowsObject { - implicit?: OAuthFlowObject - password?: OAuthFlowObject - clientCredentials?: OAuthFlowObject - authorizationCode?: OAuthFlowObject -} - -export interface OAuthFlowObject { - authorizationUrl?: string - tokenUrl?: string - refreshUrl?: string - scopes: { [scope: string]: string } -} - -export interface SecurityRequirementObject { - [name: string]: string[] -} - -export interface TagObject { - name: string - description?: string - externalDocs?: ExternalDocumentationObject -} +// Re-export from openapi-types for accuracy and compatibility with swagger-parser + +import type { OpenAPIV3 } from 'openapi-types' + +// Use OpenAPIV3.Document as the base type for OpenAPIObject +// This ensures compatibility with swagger-parser and other OpenAPI tools +export type OpenAPIObject = OpenAPIV3.Document + +// Re-export commonly used types from openapi-types +export type InfoObject = OpenAPIV3.InfoObject +export type ContactObject = OpenAPIV3.ContactObject +export type LicenseObject = OpenAPIV3.LicenseObject +export type ServerObject = OpenAPIV3.ServerObject +export type ServerVariableObject = OpenAPIV3.ServerVariableObject +export type PathItemObject = OpenAPIV3.PathItemObject +export type OperationObject = OpenAPIV3.OperationObject +export type ExternalDocumentationObject = OpenAPIV3.ExternalDocumentationObject +export type ParameterObject = OpenAPIV3.ParameterObject +export type RequestBodyObject = OpenAPIV3.RequestBodyObject +export type MediaTypeObject = OpenAPIV3.MediaTypeObject +export type EncodingObject = OpenAPIV3.EncodingObject +export type HeaderObject = OpenAPIV3.HeaderObject +export type ResponsesObject = OpenAPIV3.ResponsesObject +export type ResponseObject = OpenAPIV3.ResponseObject +export type CallbackObject = OpenAPIV3.CallbackObject +export type ExampleObject = OpenAPIV3.ExampleObject +export type LinkObject = OpenAPIV3.LinkObject +export type SchemaObject = OpenAPIV3.SchemaObject +export type ReferenceObject = OpenAPIV3.ReferenceObject +export type ComponentsObject = OpenAPIV3.ComponentsObject +export type SecuritySchemeObject = OpenAPIV3.SecuritySchemeObject +export type SecurityRequirementObject = OpenAPIV3.SecurityRequirementObject +export type TagObject = OpenAPIV3.TagObject diff --git a/src/utils/bruno-parser.ts b/src/utils/bruno-parser.ts index 24aa0d4..b400609 100644 --- a/src/utils/bruno-parser.ts +++ b/src/utils/bruno-parser.ts @@ -1,47 +1,266 @@ import path from 'node:path' +import { + BruFileParseError, + InvalidBrunoJsonError, + InvalidCollectionPathError, + InvalidJsonError, +} from '../models/errors.js' import type { BrunoAuth, BrunoBody, BrunoCollection, - BrunoEnvironment, BrunoHeader, BrunoItem, BrunoKeyValue, BrunoParam, BrunoRequest, - BrunoVariable, - FolderRoot, } from '../types/bruno.js' import { FileReader } from './file-reader.js' -/** - * Utility functions for parsing Bruno collection structures - */ +function hasNameProperty(obj: unknown): obj is { name: string } { + if (typeof obj !== 'object' || obj === null) { + return false + } + if (!('name' in obj)) { + return false + } + if (!Object.prototype.hasOwnProperty.call(obj, 'name')) { + return false + } + const nameValue = (obj as { name: unknown }).name + return typeof nameValue === 'string' && nameValue.length > 0 +} + +function hasVersionProperty(obj: unknown): obj is { version: string } { + if (typeof obj !== 'object' || obj === null) { + return false + } + if (!('version' in obj)) { + return false + } + if (!Object.prototype.hasOwnProperty.call(obj, 'version')) { + return false + } + const versionValue = (obj as { version: unknown }).version + return typeof versionValue === 'string' +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +function isError(value: unknown): value is Error { + return value instanceof Error +} + +const VALID_AUTH_MODES = [ + 'inherit', + 'none', + 'awsv4', + 'basic', + 'bearer', + 'digest', + 'ntlm', + 'oauth2', + 'wsse', + 'apikey', +] as const + +type AuthMode = (typeof VALID_AUTH_MODES)[number] + +function isValidAuthMode(value: string): value is AuthMode { + return VALID_AUTH_MODES.includes(value as AuthMode) +} + +const VALID_ITEM_TYPES = [ + 'folder', + 'http-request', + 'graphql-request', + 'js', + 'grpc-request', + 'ws-request', +] as const + +type ItemType = (typeof VALID_ITEM_TYPES)[number] + +function isValidItemType(value: string): value is ItemType { + return VALID_ITEM_TYPES.includes(value as ItemType) +} + +const VALID_HTTP_METHODS = [ + 'get', + 'post', + 'put', + 'patch', + 'delete', + 'head', + 'options', + 'trace', + 'connect', +] as const + +type HttpMethod = (typeof VALID_HTTP_METHODS)[number] + +const VALID_BODY_MODES = [ + 'none', + 'json', + 'text', + 'xml', + 'formUrlEncoded', + 'multipartForm', + 'graphql', + 'sparql', + 'file', +] as const + +type BodyMode = (typeof VALID_BODY_MODES)[number] + +function hasRequiredBrunoRequestFields( + partial: Partial, +): partial is Required> & + Partial { + return ( + typeof partial.url === 'string' && + typeof partial.method === 'string' && + Array.isArray(partial.headers) && + Array.isArray(partial.params) + ) +} + +function createBrunoRequest(partial: Partial = {}): BrunoRequest { + if (!hasRequiredBrunoRequestFields(partial)) { + throw new Error('BrunoRequest requires url, method, headers, and params fields') + } + return { + url: partial.url, + method: partial.method, + headers: partial.headers, + params: partial.params, + auth: partial.auth ?? null, + body: partial.body, + script: partial.script, + vars: partial.vars ?? null, + assertions: partial.assertions ?? null, + tests: partial.tests ?? null, + docs: partial.docs ?? null, + tags: partial.tags ?? null, + } +} + +function hasRequiredBrunoBodyFields( + partial: Partial, +): partial is Required> & Partial { + return typeof partial.mode === 'string' +} + +function createBrunoBody(partial: Partial): BrunoBody { + if (!hasRequiredBrunoBodyFields(partial)) { + throw new Error('BrunoBody requires mode field') + } + return { + mode: partial.mode, + json: partial.json, + text: partial.text, + xml: partial.xml, + sparql: partial.sparql, + formUrlEncoded: partial.formUrlEncoded, + multipartForm: partial.multipartForm, + graphql: partial.graphql, + file: partial.file, + } +} + +function hasRequiredBrunoAuthFields( + partial: Partial, +): partial is Required> & Partial { + return typeof partial.mode === 'string' +} + +function createBrunoAuth(partial: Partial): BrunoAuth { + if (!hasRequiredBrunoAuthFields(partial)) { + throw new Error('BrunoAuth requires mode field') + } + return { + mode: partial.mode, + awsv4: partial.awsv4, + basic: partial.basic, + bearer: partial.bearer, + ntlm: partial.ntlm, + digest: partial.digest, + oauth2: partial.oauth2, + wsse: partial.wsse, + apikey: partial.apikey, + } +} + +function hasRequiredBrunoItemFields( + partial: Partial, + relativePath: string, +): partial is Required> & Partial { + return ( + typeof partial.uid === 'string' && + typeof partial.type === 'string' && + typeof partial.name === 'string' + ) +} + +function createBrunoItem(partial: Partial, relativePath: string): BrunoItem { + const uid = partial.uid ?? BrunoParser.generateUid() + const type = partial.type ?? 'http-request' + const name = partial.name ?? path.basename(relativePath, '.bru') + const pathname = partial.pathname ?? relativePath + + if (!hasRequiredBrunoItemFields({ ...partial, uid, type, name }, relativePath)) { + throw new Error('BrunoItem requires uid, type, and name fields') + } + + return { + uid, + type, + name, + pathname, + depth: partial.depth ?? relativePath.split('/').length - 1, + request: partial.request ?? null, + seq: partial.seq ?? null, + tags: partial.tags ?? null, + settings: partial.settings ?? null, + fileContent: partial.fileContent ?? null, + root: partial.root ?? null, + items: partial.items ?? null, + examples: partial.examples ?? null, + filename: partial.filename ?? null, + } +} + export class BrunoParser { /** - * Parses a Bruno collection from a directory path + * Parses a Bruno collection from a directory path. + * + * @param collectionPath - Path to the Bruno collection directory + * @returns Parsed Bruno collection + * @throws {InvalidCollectionPathError} If the path is not a valid directory + * @throws {InvalidJsonError} If bruno.json is invalid + * @throws {InvalidBrunoJsonError} If bruno.json is not a valid object + * @throws {BruFileParseError} If any .bru file fails to parse */ static async parseCollection(collectionPath: string): Promise { - // Check if the path exists and is a directory const isDir = await FileReader.isDirectory(collectionPath) if (!isDir) { - throw new Error(`Collection path does not exist or is not a directory: ${collectionPath}`) + throw new InvalidCollectionPathError(collectionPath) } // Look for the required collection files const brunoJsonPath = path.join(collectionPath, 'bruno.json') - const collectionBruPath = path.join(collectionPath, 'collection.bru') let collection: BrunoCollection = { version: '1', - uid: BrunoParser.generateUid(), // We'll generate a UID if not provided - name: path.basename(collectionPath), // Default to directory name + uid: BrunoParser.generateUid(), + name: path.basename(collectionPath), items: [], pathname: collectionPath, brunoConfig: undefined, } - // Try to read bruno.json if it exists if (await FileReader.fileExists(brunoJsonPath)) { try { const brunoJsonContent = await FileReader.readFile(brunoJsonPath) @@ -50,57 +269,36 @@ export class BrunoParser { try { brunoJson = JSON.parse(brunoJsonContent) } catch (parseError) { - throw new Error( - `Invalid JSON in bruno.json: ${parseError instanceof Error ? parseError.message : 'Unknown error'}`, - ) + throw new InvalidJsonError('bruno.json', parseError) } - // Validate that brunoJson is an object - if (typeof brunoJson !== 'object' || brunoJson === null) { - throw new Error('bruno.json must contain a valid JSON object') + // Validate that brunoJson is an object (not an array or null) + if (typeof brunoJson !== 'object' || brunoJson === null || Array.isArray(brunoJson)) { + throw new InvalidBrunoJsonError(brunoJsonPath) } // Type guard to check if it has the required properties - if ( - typeof (brunoJson as { name?: unknown }).name === 'string' && - (brunoJson as { name?: unknown }).name - ) { - collection = { ...collection, name: (brunoJson as { name: string }).name } + if (hasNameProperty(brunoJson)) { + collection = { ...collection, name: brunoJson.name } } // Validate version - if ( - typeof (brunoJson as { version?: unknown }).version === 'string' && - ['1'].includes((brunoJson as { version: string }).version) - ) { - collection = { ...collection, version: '1' as const } + if (hasVersionProperty(brunoJson) && brunoJson.version === '1') { + collection = { ...collection, version: '1' } } // Store bruno config - if (typeof brunoJson === 'object' && brunoJson !== null) { - collection = { ...collection, brunoConfig: brunoJson as Record } + if (isRecord(brunoJson)) { + collection = { ...collection, brunoConfig: brunoJson } } } catch (error) { - if (error instanceof Error && error.message.includes('Invalid JSON')) { + if (error instanceof InvalidJsonError || error instanceof InvalidBrunoJsonError) { throw error // Re-throw specific JSON errors } - throw new Error(`Failed to parse bruno.json: ${(error as Error).message}`) + throw new InvalidJsonError(brunoJsonPath, error) } } - // Try to read collection.bru if it exists - if (await FileReader.fileExists(collectionBruPath)) { - try { - const collectionBruContent = await FileReader.readFile(collectionBruPath) - // TODO: Parse collection.bru content properly using bruno-lang if needed - // For now, we'll just acknowledge its existence - collection.root = { type: 'collection', name: collection.name } - } catch (error) { - throw new Error(`Failed to parse collection.bru: ${(error as Error).message}`) - } - } - - // Parse all .bru files in the collection directory const bruFiles = await FileReader.getBruFiles(collectionPath) if (bruFiles.length === 0) { @@ -115,7 +313,12 @@ export class BrunoParser { } /** - * Parses individual .bru files into BrunoItem structures + * Parses individual .bru files into BrunoItem structures. + * + * @param bruFilePaths - Array of paths to .bru files + * @param collectionPath - Base path of the collection + * @returns Array of parsed Bruno items + * @throws {BruFileParseError} If any file fails to parse */ static async parseBruFiles(bruFilePaths: string[], collectionPath: string): Promise { const items: BrunoItem[] = [] @@ -127,7 +330,7 @@ export class BrunoParser { const parsedItem = await BrunoParser.parseBruContent(content, relativePath) items.push(parsedItem) } catch (error) { - throw new Error(`Failed to parse .bru file ${bruPath}: ${(error as Error).message}`) + throw new BruFileParseError(bruPath, error) } } @@ -135,96 +338,81 @@ export class BrunoParser { } /** - * Parses the content of a single .bru file into a BrunoItem + * Parses the content of a single .bru file into a BrunoItem. + * Supports bru-lang format: meta { ... }, get { ... }, headers { ... }, etc. + * + * @param content - Content of the .bru file + * @param relativePath - Relative path of the file within the collection + * @returns Parsed Bruno item */ static async parseBruContent(content: string, relativePath: string): Promise { - // This is a more comprehensive parser for .bru files - const lines = content.split('\n') const item: Partial = { name: path.basename(relativePath, '.bru'), pathname: relativePath, - type: 'http-request', // Default type - depth: relativePath.split('/').length - 1 || 0, - request: {} as BrunoRequest, // Initialize with empty request + type: 'http-request', + depth: relativePath.split('/').length - 1, } - // Parse the .bru content into sections - const sections: { [key: string]: string[] } = {} - let currentSection: string | null = null - let currentSectionContent: string[] = [] + const blocks = BrunoParser.parseBruLangBlocks(content) - for (const line of lines) { - const trimmedLine = line.trim() - - // Check if this is a section header like [sectionName] - if (trimmedLine.startsWith('[') && trimmedLine.endsWith(']')) { - // Save previous section if exists - if (currentSection) { - sections[currentSection] = currentSectionContent - } - - // Start new section - currentSection = trimmedLine.substring(1, trimmedLine.length - 1) - currentSectionContent = [] - } else { - // Add line to current section - currentSectionContent.push(line) - } - } - - // Save the last section - if (currentSection) { - sections[currentSection] = currentSectionContent - } - - // Process each section - for (const [sectionName, sectionLines] of Object.entries(sections)) { - switch (sectionName) { + for (const block of blocks) { + switch (block.type) { case 'meta': - BrunoParser.parseMetaSection(sectionLines, item) + BrunoParser.parseMetaBlock(block.content, item) break - case 'request': - BrunoParser.parseRequestSection(sectionLines, item) + case 'http-method': + BrunoParser.parseHttpMethodBlock(block.name, block.content, item) break case 'headers': - if (!item.request) item.request = {} as BrunoRequest - item.request.headers = BrunoParser.parseHeadersSection(sectionLines) + if (!item.request) { + // Create request with required fields if it doesn't exist + item.request = createBrunoRequest({ + method: 'GET', + url: '', + headers: BrunoParser.parseHeadersBlock(block.content), + params: [], + }) + } else { + item.request.headers = BrunoParser.parseHeadersBlock(block.content) + } break case 'params': - if (!item.request) item.request = {} as BrunoRequest - item.request.params = BrunoParser.parseParamsSection(sectionLines) + if (!item.request) { + // Create request with required fields if it doesn't exist + item.request = createBrunoRequest({ + method: 'GET', + url: '', + headers: [], + params: BrunoParser.parseParamsBlock(block.content), + }) + } else { + item.request.params = BrunoParser.parseParamsBlock(block.content) + } break case 'body': - if (!item.request) item.request = {} as BrunoRequest - item.request.body = BrunoParser.parseBodySection(sectionLines) + if (!item.request) { + // Create request with required fields if it doesn't exist + item.request = createBrunoRequest({ + method: 'GET', + url: '', + headers: [], + params: [], + }) + } + item.request.body = BrunoParser.parseBodyBlock(block.name, block.content) break case 'auth': - if (!item.request) item.request = {} as BrunoRequest - item.request.auth = BrunoParser.parseAuthSection(sectionLines) - break - default: - // Process general properties not in sections - for (const line of sectionLines) { - if (line.includes('=')) { - const parts = line.split('=', 2) - const key = parts[0] - const value = parts[1] - const trimmedKey = key?.trim() - const trimmedValue = value ? value.trim() : '' - - if (trimmedKey === 'name') { - item.name = trimmedValue - } else if (trimmedKey === 'type' && trimmedValue) { - item.type = trimmedValue as - | 'folder' - | 'http-request' - | 'graphql-request' - | 'js' - | 'grpc-request' - | 'ws-request' - } - } + if (!item.request) { + // Create request with required fields if it doesn't exist + item.request = createBrunoRequest({ + method: 'GET', + url: '', + headers: [], + params: [], + }) } + item.request.auth = BrunoParser.parseAuthBlock(block.content) + break } } @@ -233,12 +421,301 @@ export class BrunoParser { item.name = path.basename(relativePath, '.bru') } - return item as BrunoItem + return createBrunoItem(item, relativePath) + } + + private static parseBruLangBlocks(content: string): Array<{ + type: 'meta' | 'http-method' | 'headers' | 'params' | 'body' | 'auth' + name: string + content: string + }> { + const blocks: Array<{ + type: 'meta' | 'http-method' | 'headers' | 'params' | 'body' | 'auth' + name: string + content: string + }> = [] + + const lines = content.split('\n') + let i = 0 + + while (i < lines.length) { + const line = lines[i]?.trim() + if (!line || line.startsWith('//')) { + i++ + continue + } + + const blockMatch = line.match(/^(\w+)(?::([\w-]+))?\s*\{$/) + if (blockMatch?.[1]) { + const blockName = blockMatch[1] + const subtype = blockMatch[2] + + let blockType: 'meta' | 'http-method' | 'headers' | 'params' | 'body' | 'auth' + if (blockName === 'meta') { + blockType = 'meta' + } else if (blockName === 'headers') { + blockType = 'headers' + } else if (blockName === 'params') { + blockType = 'params' + } else if (blockName === 'auth') { + blockType = 'auth' + } else if (blockName === 'body') { + blockType = 'body' + } else if (VALID_HTTP_METHODS.includes(blockName as HttpMethod)) { + blockType = 'http-method' + } else { + i++ + continue + } + + i++ + const blockContent: string[] = [] + let braceDepth = 1 + + while (i < lines.length && braceDepth > 0) { + const currentLine = lines[i] + if (!currentLine) { + i++ + continue + } + + for (const char of currentLine) { + if (char === '{') braceDepth++ + if (char === '}') braceDepth-- + } + + if (braceDepth > 0) { + blockContent.push(currentLine) + } + i++ + } + + const blockNameValue = subtype ?? blockName ?? '' + blocks.push({ + type: blockType, + name: blockNameValue, + content: blockContent.join('\n'), + }) + } else { + i++ + } + } + + return blocks + } + + private static parseMetaBlock(content: string, item: Partial): void { + const lines = content.split('\n') + for (const line of lines) { + const trimmedLine = line.trim() + if (!trimmedLine || trimmedLine.startsWith('//')) continue + + const colonIndex = trimmedLine.indexOf(':') + if (colonIndex > 0) { + const key = trimmedLine.substring(0, colonIndex).trim() + const value = trimmedLine.substring(colonIndex + 1).trim() + + if (key === 'name') { + item.name = value + } else if (key === 'seq') { + const parsedSeq = Number.parseInt(value, 10) + item.depth = Number.isNaN(parsedSeq) ? undefined : parsedSeq + } else if (key === 'type') { + if (value === 'http') { + item.type = 'http-request' + } else if (isValidItemType(value)) { + item.type = value + } + } + } + } + } + + private static parseHttpMethodBlock( + method: string, + content: string, + item: Partial, + ): void { + const httpMethod = method.toUpperCase() + let url = '' + + const lines = content.split('\n') + for (const line of lines) { + const trimmedLine = line.trim() + if (!trimmedLine || trimmedLine.startsWith('//')) continue + + const colonIndex = trimmedLine.indexOf(':') + if (colonIndex > 0) { + const key = trimmedLine.substring(0, colonIndex).trim() + const value = trimmedLine.substring(colonIndex + 1).trim() + + if (key === 'url') { + url = value + } + } + } + + // Create request with required fields + item.request = createBrunoRequest({ + method: httpMethod, + url, + headers: [], + params: [], + }) + } + + private static parseHeadersBlock(content: string): BrunoHeader[] { + const headers: BrunoHeader[] = [] + const lines = content.split('\n') + + for (const line of lines) { + const trimmedLine = line.trim() + if (!trimmedLine || trimmedLine.startsWith('//')) continue + + const colonIndex = trimmedLine.indexOf(':') + if (colonIndex > 0) { + const key = trimmedLine.substring(0, colonIndex).trim() + const value = trimmedLine.substring(colonIndex + 1).trim() + + headers.push({ + uid: BrunoParser.generateUid(), + name: key, + value, + enabled: true, + }) + } + } + + return headers + } + + private static parseParamsBlock(content: string): BrunoParam[] { + const params: BrunoParam[] = [] + const lines = content.split('\n') + + for (const line of lines) { + const trimmedLine = line.trim() + if (!trimmedLine || trimmedLine.startsWith('//')) continue + + const colonIndex = trimmedLine.indexOf(':') + if (colonIndex > 0) { + const key = trimmedLine.substring(0, colonIndex).trim() + const value = trimmedLine.substring(colonIndex + 1).trim() + + params.push({ + uid: BrunoParser.generateUid(), + name: key, + value, + type: 'query', + enabled: true, + }) + } + } + + return params + } + + private static parseBodyBlock(subtype: string, content: string): BrunoBody { + const body: Partial = {} + + if (subtype === 'json') { + body.mode = 'json' + body.json = content.trim() + } else if (subtype === 'text') { + body.mode = 'text' + body.text = content.trim() + } else if (subtype === 'xml') { + body.mode = 'xml' + body.xml = content.trim() + } else if (subtype === 'form-urlencoded') { + body.mode = 'formUrlEncoded' + const formData: BrunoKeyValue[] = [] + const lines = content.split('\n') + for (const line of lines) { + const trimmedLine = line.trim() + if (!trimmedLine || trimmedLine.startsWith('//')) continue + const colonIndex = trimmedLine.indexOf(':') + if (colonIndex > 0) { + const key = trimmedLine.substring(0, colonIndex).trim() + const value = trimmedLine.substring(colonIndex + 1).trim() + formData.push({ + uid: BrunoParser.generateUid(), + name: key, + value, + enabled: true, + }) + } + } + body.formUrlEncoded = formData + } else if (subtype === 'multipart-form') { + body.mode = 'multipartForm' + const formData: BrunoKeyValue[] = [] + const lines = content.split('\n') + for (const line of lines) { + const trimmedLine = line.trim() + if (!trimmedLine || trimmedLine.startsWith('//')) continue + const colonIndex = trimmedLine.indexOf(':') + if (colonIndex > 0) { + const key = trimmedLine.substring(0, colonIndex).trim() + const value = trimmedLine.substring(colonIndex + 1).trim() + formData.push({ + uid: BrunoParser.generateUid(), + name: key, + value, + enabled: true, + }) + } + } + } else { + body.mode = 'none' + } + + return createBrunoBody(body) + } + + private static parseAuthBlock(content: string): BrunoAuth { + const auth: Partial = { mode: 'none' } + const lines = content.split('\n') + + for (const line of lines) { + const trimmedLine = line.trim() + if (!trimmedLine || trimmedLine.startsWith('//')) continue + + const colonIndex = trimmedLine.indexOf(':') + if (colonIndex > 0) { + const key = trimmedLine.substring(0, colonIndex).trim() + const value = trimmedLine.substring(colonIndex + 1).trim() + + if (key === 'mode') { + if (isValidAuthMode(value)) { + auth.mode = value + } else { + auth.mode = 'none' + } + } else if (key === 'username') { + if (auth.mode === 'basic' || auth.mode === 'digest') { + if (!auth.basic) auth.basic = { username: '', password: '' } + auth.basic.username = value + } else if (auth.mode === 'oauth2') { + if (!auth.oauth2) auth.oauth2 = { grantType: 'password', accessTokenUrl: '' } + } + } else if (key === 'password') { + if (auth.mode === 'basic' || auth.mode === 'digest') { + if (!auth.basic) auth.basic = { username: '', password: '' } + auth.basic.password = value + } + } else if (key === 'token') { + if (auth.mode === 'bearer') { + if (!auth.bearer) auth.bearer = { token: '' } + auth.bearer.token = value + } + } + } + } + + return createBrunoAuth(auth) } - /** - * Parses the meta section of a .bru file - */ private static parseMetaSection(lines: string[], item: Partial): void { for (const line of lines) { if (line.includes('=')) { @@ -251,17 +728,16 @@ export class BrunoParser { if (trimmedKey === 'name') { item.name = trimmedValue } else if (trimmedKey === 'seq') { - item.depth = Number.parseInt(trimmedValue, 10) || 0 + const parsedSeq = Number.parseInt(trimmedValue, 10) + item.depth = Number.isNaN(parsedSeq) ? undefined : parsedSeq } } } } - /** - * Parses the request section of a .bru file - */ private static parseRequestSection(lines: string[], item: Partial): void { - if (!item.request) item.request = {} as BrunoRequest + let method = 'GET' + let url = '' for (const line of lines) { if (line.includes('=')) { @@ -272,17 +748,21 @@ export class BrunoParser { const trimmedValue = value ? value.trim() : '' if (trimmedKey === 'method') { - item.request.method = trimmedValue + method = trimmedValue } else if (trimmedKey === 'url') { - item.request.url = trimmedValue + url = trimmedValue } } } + + item.request = createBrunoRequest({ + method, + url, + headers: [], + params: [], + }) } - /** - * Parses the headers section of a .bru file - */ private static parseHeadersSection(lines: string[]): BrunoHeader[] { const headers: BrunoHeader[] = [] @@ -295,7 +775,6 @@ export class BrunoParser { const trimmedValue = value ? value.trim() : '' if (trimmedKey && trimmedKey !== 'enabled') { - // Skip the 'enabled' line if it's not part of a header headers.push({ uid: BrunoParser.generateUid(), name: trimmedKey, @@ -309,9 +788,6 @@ export class BrunoParser { return headers } - /** - * Parses the params section of a .bru file - */ private static parseParamsSection(lines: string[]): BrunoParam[] { const params: BrunoParam[] = [] @@ -324,12 +800,11 @@ export class BrunoParser { const trimmedValue = value ? value.trim() : '' if (trimmedKey && trimmedKey !== 'enabled') { - // Skip the 'enabled' line if it's not part of a param params.push({ uid: BrunoParser.generateUid(), name: trimmedKey, value: trimmedValue, - type: 'query', // default type + type: 'query', enabled: true, }) } @@ -339,9 +814,6 @@ export class BrunoParser { return params } - /** - * Parses the body section of a .bru file - */ private static parseBodySection(lines: string[]): BrunoBody { const body: Partial = {} @@ -354,16 +826,7 @@ export class BrunoParser { const trimmedValue = value ? value.trim() : '' if (trimmedKey === 'mode') { - body.mode = trimmedValue as - | 'none' - | 'json' - | 'text' - | 'xml' - | 'formUrlEncoded' - | 'multipartForm' - | 'graphql' - | 'sparql' - | 'file' + body.mode = trimmedValue as BodyMode } else if (trimmedKey === 'json') { body.json = trimmedValue } else if (trimmedKey === 'xml') { @@ -374,12 +837,9 @@ export class BrunoParser { } } - return body as BrunoBody + return createBrunoBody(body) } - /** - * Parses the auth section of a .bru file - */ private static parseAuthSection(lines: string[]): BrunoAuth { const auth: Partial = { mode: 'none' } @@ -392,24 +852,13 @@ export class BrunoParser { const trimmedValue = value ? value.trim() : '' if (trimmedKey === 'mode') { - auth.mode = trimmedValue as - | 'inherit' - | 'none' - | 'awsv4' - | 'basic' - | 'bearer' - | 'digest' - | 'ntlm' - | 'oauth2' - | 'wsse' - | 'apikey' + auth.mode = trimmedValue as AuthMode } else if (trimmedKey === 'username') { if (auth.mode === 'basic' || auth.mode === 'digest') { if (!auth.basic) auth.basic = { username: '', password: '' } auth.basic.username = trimmedValue } else if (auth.mode === 'oauth2') { if (!auth.oauth2) auth.oauth2 = { grantType: 'password', accessTokenUrl: '' } - // Note: OAuth2 has more complex structure that needs to be handled properly } } else if (trimmedKey === 'password') { if (auth.mode === 'basic' || auth.mode === 'digest') { @@ -425,17 +874,17 @@ export class BrunoParser { } } - return auth as BrunoAuth + return createBrunoAuth(auth) } /** - * Validates if a given path contains a valid Bruno collection + * Validates if a given path contains a valid Bruno collection. + * + * @param collectionPath - Path to check + * @returns True if the path contains a valid Bruno collection */ static async isValidCollection(collectionPath: string): Promise { try { - // A Bruno collection should have either: - // 1. A bruno.json file, OR - // 2. At least one .bru file const brunoJsonPath = path.join(collectionPath, 'bruno.json') const hasBrunoJson = await FileReader.fileExists(brunoJsonPath) @@ -443,7 +892,6 @@ export class BrunoParser { return true } - // Check for .bru files in the directory const bruFiles = await FileReader.getBruFiles(collectionPath) return bruFiles.length > 0 } catch (error) { @@ -452,38 +900,41 @@ export class BrunoParser { } /** - * Generates a unique identifier + * Generates a unique identifier. + * + * @returns A unique string identifier */ - private static generateUid(): string { - // In a real implementation, you might want to use nanoid or similar - // For now, we'll create a simple UID based on timestamp and random number + static generateUid(): string { return Date.now().toString(36) + Math.random().toString(36).substr(2, 5) } /** - * Parses a Bruno collection from a directory path synchronously + * Parses a Bruno collection from a directory path synchronously. + * + * @param collectionPath - Path to the Bruno collection directory + * @returns Parsed Bruno collection + * @throws {InvalidCollectionPathError} If the path is not a valid directory + * @throws {InvalidJsonError} If bruno.json is invalid + * @throws {InvalidBrunoJsonError} If bruno.json is not a valid object + * @throws {BruFileParseError} If any .bru file fails to parse */ static parseCollectionSync(collectionPath: string): BrunoCollection { - // Check if the path exists and is a directory const isDir = FileReader.isDirectorySync(collectionPath) if (!isDir) { - throw new Error(`Collection path does not exist or is not a directory: ${collectionPath}`) + throw new InvalidCollectionPathError(collectionPath) } - // Look for the required collection files const brunoJsonPath = path.join(collectionPath, 'bruno.json') - const collectionBruPath = path.join(collectionPath, 'collection.bru') let collection: BrunoCollection = { version: '1', - uid: BrunoParser.generateUid(), // We'll generate a UID if not provided - name: path.basename(collectionPath), // Default to directory name + uid: BrunoParser.generateUid(), + name: path.basename(collectionPath), items: [], pathname: collectionPath, brunoConfig: undefined, } - // Try to read bruno.json if it exists if (FileReader.fileExistsSync(brunoJsonPath)) { try { const brunoJsonContent = FileReader.readFileSync(brunoJsonPath) @@ -492,57 +943,36 @@ export class BrunoParser { try { brunoJson = JSON.parse(brunoJsonContent) } catch (parseError) { - throw new Error( - `Invalid JSON in bruno.json: ${parseError instanceof Error ? parseError.message : 'Unknown error'}`, - ) + throw new InvalidJsonError('bruno.json', parseError) } - // Validate that brunoJson is an object - if (typeof brunoJson !== 'object' || brunoJson === null) { - throw new Error('bruno.json must contain a valid JSON object') + // Validate that brunoJson is an object (not an array or null) + if (typeof brunoJson !== 'object' || brunoJson === null || Array.isArray(brunoJson)) { + throw new InvalidBrunoJsonError(brunoJsonPath) } // Type guard to check if it has the required properties - if ( - typeof (brunoJson as { name?: unknown }).name === 'string' && - (brunoJson as { name?: unknown }).name - ) { - collection = { ...collection, name: (brunoJson as { name: string }).name } + if (hasNameProperty(brunoJson)) { + collection = { ...collection, name: brunoJson.name } } // Validate version - if ( - typeof (brunoJson as { version?: unknown }).version === 'string' && - ['1'].includes((brunoJson as { version: string }).version) - ) { - collection = { ...collection, version: '1' as const } + if (hasVersionProperty(brunoJson) && brunoJson.version === '1') { + collection = { ...collection, version: '1' } } // Store bruno config - if (typeof brunoJson === 'object' && brunoJson !== null) { - collection = { ...collection, brunoConfig: brunoJson as Record } + if (isRecord(brunoJson)) { + collection = { ...collection, brunoConfig: brunoJson } } } catch (error) { - if (error instanceof Error && error.message.includes('Invalid JSON')) { + if (error instanceof InvalidJsonError || error instanceof InvalidBrunoJsonError) { throw error // Re-throw specific JSON errors } - throw new Error(`Failed to parse bruno.json: ${(error as Error).message}`) + throw new InvalidJsonError(brunoJsonPath, error) } } - // Try to read collection.bru if it exists - if (FileReader.fileExistsSync(collectionBruPath)) { - try { - const collectionBruContent = FileReader.readFileSync(collectionBruPath) - // TODO: Parse collection.bru content properly using bruno-lang if needed - // For now, we'll just acknowledge its existence - collection.root = { type: 'collection', name: collection.name } - } catch (error) { - throw new Error(`Failed to parse collection.bru: ${(error as Error).message}`) - } - } - - // Parse all .bru files in the collection directory const bruFiles = FileReader.getBruFilesSync(collectionPath) if (bruFiles.length === 0) { @@ -557,7 +987,12 @@ export class BrunoParser { } /** - * Parses individual .bru files into BrunoItem structures synchronously + * Parses individual .bru files into BrunoItem structures synchronously. + * + * @param bruFilePaths - Array of paths to .bru files + * @param collectionPath - Base path of the collection + * @returns Array of parsed Bruno items + * @throws {BruFileParseError} If any file fails to parse */ static parseBruFilesSync(bruFilePaths: string[], collectionPath: string): BrunoItem[] { const items: BrunoItem[] = [] @@ -569,7 +1004,7 @@ export class BrunoParser { const parsedItem = BrunoParser.parseBruContentSync(content, relativePath) items.push(parsedItem) } catch (error) { - throw new Error(`Failed to parse .bru file ${bruPath}: ${(error as Error).message}`) + throw new BruFileParseError(bruPath, error) } } @@ -577,96 +1012,81 @@ export class BrunoParser { } /** - * Parses the content of a single .bru file into a BrunoItem synchronously + * Parses the content of a single .bru file into a BrunoItem synchronously. + * Supports bru-lang format: meta { ... }, get { ... }, headers { ... }, etc. + * + * @param content - Content of the .bru file + * @param relativePath - Relative path of the file within the collection + * @returns Parsed Bruno item */ static parseBruContentSync(content: string, relativePath: string): BrunoItem { - // This is a more comprehensive parser for .bru files - const lines = content.split('\n') const item: Partial = { name: path.basename(relativePath, '.bru'), pathname: relativePath, - type: 'http-request', // Default type - depth: relativePath.split('/').length - 1 || 0, - request: {} as BrunoRequest, // Initialize with empty request + type: 'http-request', + depth: relativePath.split('/').length - 1, } - // Parse the .bru content into sections - const sections: { [key: string]: string[] } = {} - let currentSection: string | null = null - let currentSectionContent: string[] = [] - - for (const line of lines) { - const trimmedLine = line.trim() + const blocks = BrunoParser.parseBruLangBlocks(content) - // Check if this is a section header like [sectionName] - if (trimmedLine.startsWith('[') && trimmedLine.endsWith(']')) { - // Save previous section if exists - if (currentSection) { - sections[currentSection] = currentSectionContent - } - - // Start new section - currentSection = trimmedLine.substring(1, trimmedLine.length - 1) - currentSectionContent = [] - } else { - // Add line to current section - currentSectionContent.push(line) - } - } - - // Save the last section - if (currentSection) { - sections[currentSection] = currentSectionContent - } - - // Process each section - for (const [sectionName, sectionLines] of Object.entries(sections)) { - switch (sectionName) { + for (const block of blocks) { + switch (block.type) { case 'meta': - BrunoParser.parseMetaSection(sectionLines, item) + BrunoParser.parseMetaBlock(block.content, item) break - case 'request': - BrunoParser.parseRequestSection(sectionLines, item) + case 'http-method': + BrunoParser.parseHttpMethodBlock(block.name, block.content, item) break case 'headers': - if (!item.request) item.request = {} as BrunoRequest - item.request.headers = BrunoParser.parseHeadersSection(sectionLines) + if (!item.request) { + // Create request with required fields if it doesn't exist + item.request = createBrunoRequest({ + method: 'GET', + url: '', + headers: BrunoParser.parseHeadersBlock(block.content), + params: [], + }) + } else { + item.request.headers = BrunoParser.parseHeadersBlock(block.content) + } break case 'params': - if (!item.request) item.request = {} as BrunoRequest - item.request.params = BrunoParser.parseParamsSection(sectionLines) + if (!item.request) { + // Create request with required fields if it doesn't exist + item.request = createBrunoRequest({ + method: 'GET', + url: '', + headers: [], + params: BrunoParser.parseParamsBlock(block.content), + }) + } else { + item.request.params = BrunoParser.parseParamsBlock(block.content) + } break case 'body': - if (!item.request) item.request = {} as BrunoRequest - item.request.body = BrunoParser.parseBodySection(sectionLines) + if (!item.request) { + // Create request with required fields if it doesn't exist + item.request = createBrunoRequest({ + method: 'GET', + url: '', + headers: [], + params: [], + }) + } + item.request.body = BrunoParser.parseBodyBlock(block.name, block.content) break case 'auth': - if (!item.request) item.request = {} as BrunoRequest - item.request.auth = BrunoParser.parseAuthSection(sectionLines) - break - default: - // Process general properties not in sections - for (const line of sectionLines) { - if (line.includes('=')) { - const parts = line.split('=', 2) - const key = parts[0] - const value = parts[1] - const trimmedKey = key?.trim() - const trimmedValue = value ? value.trim() : '' - - if (trimmedKey === 'name') { - item.name = trimmedValue - } else if (trimmedKey === 'type' && trimmedValue) { - item.type = trimmedValue as - | 'folder' - | 'http-request' - | 'graphql-request' - | 'js' - | 'grpc-request' - | 'ws-request' - } - } + if (!item.request) { + // Create request with required fields if it doesn't exist + item.request = createBrunoRequest({ + method: 'GET', + url: '', + headers: [], + params: [], + }) } + item.request.auth = BrunoParser.parseAuthBlock(block.content) + break } } @@ -675,17 +1095,17 @@ export class BrunoParser { item.name = path.basename(relativePath, '.bru') } - return item as BrunoItem + return createBrunoItem(item, relativePath) } /** - * Validates if a given path contains a valid Bruno collection synchronously + * Validates if a given path contains a valid Bruno collection synchronously. + * + * @param collectionPath - Path to check + * @returns True if the path contains a valid Bruno collection */ static isValidCollectionSync(collectionPath: string): boolean { try { - // A Bruno collection should have either: - // 1. A bruno.json file, OR - // 2. At least one .bru file const brunoJsonPath = path.join(collectionPath, 'bruno.json') const hasBrunoJson = FileReader.fileExistsSync(brunoJsonPath) @@ -693,7 +1113,6 @@ export class BrunoParser { return true } - // Check for .bru files in the directory const bruFiles = FileReader.getBruFilesSync(collectionPath) return bruFiles.length > 0 } catch (error) { diff --git a/src/utils/openapi-generator.ts b/src/utils/openapi-generator.ts index e7ea35b..f8ba8af 100644 --- a/src/utils/openapi-generator.ts +++ b/src/utils/openapi-generator.ts @@ -491,38 +491,33 @@ export class OpenApiGenerator { * Infers a schema from a JavaScript value */ static inferSchemaFromValue(value: unknown): SchemaObject { - const schema: SchemaObject = { - type: typeof value as - | 'string' - | 'number' - | 'boolean' - | 'object' - | 'null' - | 'undefined' - | 'symbol' - | 'function', - } - + // Handle null values - use nullable instead of type: 'null' if (value === null) { - schema.type = 'null' - return schema + return { type: 'object', nullable: true } } + // Handle arrays - must use ArraySchemaObject type if (Array.isArray(value)) { - schema.type = 'array' if (value.length > 0) { // Use the first element to infer item schema - schema.items = OpenApiGenerator.inferSchemaFromValue(value[0]) - } else { - // For empty arrays, provide a generic item schema - schema.items = { type: 'object' } + return { + type: 'array', + items: OpenApiGenerator.inferSchemaFromValue(value[0]), + } + } + // For empty arrays, provide a generic item schema + return { + type: 'array', + items: { type: 'object' }, } - return schema } + // Handle objects if (typeof value === 'object' && value !== null) { - schema.type = 'object' - schema.properties = {} + const schema: SchemaObject = { + type: 'object', + properties: {}, + } // Use for...of to avoid forEach and non-null assertion for (const [key, propertyValue] of Object.entries(value)) { @@ -535,8 +530,25 @@ export class OpenApiGenerator { return schema } - // For primitive types, just return the basic schema - return schema + // For primitive types, map JavaScript types to OpenAPI types + const jsType = typeof value + if (jsType === 'string') { + return { type: 'string' } + } + if (jsType === 'number') { + // Check if it's an integer + if (Number.isInteger(value)) { + return { type: 'integer' } + } + return { type: 'number' } + } + if (jsType === 'boolean') { + return { type: 'boolean' } + } + + // For other types (undefined, symbol, function), default to string + // For other types (undefined, symbol, function), default to string + return { type: 'string' } } /** From ffcbb62474b42933449a4d0fe3cd2446a1b27abc Mon Sep 17 00:00:00 2001 From: GyeongHo Kim Date: Wed, 7 Jan 2026 21:30:58 +0900 Subject: [PATCH 7/9] test: apply openapi-types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 변경된 타입을 테스트 코드에 적용 --- .../unit/services/conversion-service.test.ts | 31 +++++++++++++++++-- tests/unit/utils/openapi-generator.test.ts | 20 +++++++++--- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/tests/unit/services/conversion-service.test.ts b/tests/unit/services/conversion-service.test.ts index c3eddb6..de487ce 100644 --- a/tests/unit/services/conversion-service.test.ts +++ b/tests/unit/services/conversion-service.test.ts @@ -1,7 +1,35 @@ import SwaggerParser from '@apidevtools/swagger-parser' +import type { OpenAPI } from 'openapi-types' import { describe, expect, it } from 'vitest' import { ConversionService } from '../../../src/services/conversion-service.js' import type { BrunoCollection } from '../../../src/types/bruno.js' +import type { OpenAPIObject } from '../../../src/types/openapi.js' + +/** + * Converts OpenAPIObject to OpenAPI.Document format for SwaggerParser + * Since OpenAPIObject is now a type alias for OpenAPIV3.Document, + * and OpenAPI.Document is a union type that includes OpenAPIV3.Document, + * we can safely return the spec directly after validation + */ +function toOpenAPIDocument(spec: OpenAPIObject): OpenAPI.Document { + // Validate basic structure + if ( + typeof spec !== 'object' || + spec === null || + !('openapi' in spec) || + !('info' in spec) || + !('paths' in spec) || + typeof spec.openapi !== 'string' || + typeof spec.info !== 'object' || + spec.info === null || + typeof spec.paths !== 'object' || + spec.paths === null + ) { + throw new Error('Invalid OpenAPI specification structure') + } + // OpenAPIObject is OpenAPIV3.Document, which is compatible with OpenAPI.Document + return spec +} describe('ConversionService', () => { const service = new ConversionService() @@ -39,8 +67,7 @@ describe('ConversionService', () => { // Validate the spec with external library try { - // biome-ignore lint/suspicious/noExplicitAny: External library type incompatibility - await SwaggerParser.validate(result.spec as any) + await SwaggerParser.validate(toOpenAPIDocument(result.spec)) expect(true).toBe(true) // If validation doesn't throw, it's valid } catch (error) { console.error('OpenAPI validation failed:', error) diff --git a/tests/unit/utils/openapi-generator.test.ts b/tests/unit/utils/openapi-generator.test.ts index 2ea06e5..aa29c10 100644 --- a/tests/unit/utils/openapi-generator.test.ts +++ b/tests/unit/utils/openapi-generator.test.ts @@ -58,7 +58,8 @@ describe('OpenApiGenerator', () => { const value = 42 const schema = OpenApiGenerator.inferSchemaFromValue(value) - expect(schema.type).toBe('number') + // OpenAPI 3.0 distinguishes between integer and number + expect(schema.type).toBe('integer') }) it('should infer schema for boolean value', () => { @@ -72,7 +73,9 @@ describe('OpenApiGenerator', () => { const value = null const schema = OpenApiGenerator.inferSchemaFromValue(value) - expect(schema.type).toBe('null') + // OpenAPI 3.0 uses nullable: true instead of type: 'null' + expect(schema.type).toBe('object') + expect('nullable' in schema && schema.nullable).toBe(true) }) it('should infer schema for array value', () => { @@ -80,8 +83,14 @@ describe('OpenApiGenerator', () => { const schema = OpenApiGenerator.inferSchemaFromValue(value) expect(schema.type).toBe('array') - expect(schema.items).toBeDefined() - expect((schema.items as { type: string }).type).toBe('number') + // SchemaObject is a union type, so we need to check if it's an ArraySchemaObject + if (schema.type === 'array' && 'items' in schema) { + expect(schema.items).toBeDefined() + // OpenAPI 3.0 distinguishes between integer and number + expect((schema.items as { type: string }).type).toBe('integer') + } else { + throw new Error('Expected array schema with items') + } }) it('should infer schema for object value', () => { @@ -92,7 +101,8 @@ describe('OpenApiGenerator', () => { expect(schema.properties).toBeDefined() const properties = schema.properties as { [key: string]: { type: string } } | undefined expect(properties?.name?.type).toBe('string') - expect(properties?.count?.type).toBe('number') + // OpenAPI 3.0 distinguishes between integer and number + expect(properties?.count?.type).toBe('integer') }) }) From 6f78dd4942f0ebd4768f837bdf9ccde4d7a48ac9 Mon Sep 17 00:00:00 2001 From: GyeongHo Kim Date: Wed, 7 Jan 2026 21:31:46 +0900 Subject: [PATCH 8/9] docs: udpate readme with newest interface, fix examples --- README.md | 44 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9e6b278..95cb34b 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ async function convertCollection() { const result = await convertBrunoCollectionToOpenAPI('./path-to-your-bruno-collection-directory'); // Save as JSON file - fs.writeFileSync('openapi-spec.json', JSON.stringify(result.openapi, null, 2)); + fs.writeFileSync('openapi-spec.json', JSON.stringify(result.spec, null, 2)); console.log('OpenAPI specification saved as openapi-spec.json'); } @@ -50,7 +50,45 @@ async function convertCollection() { const result = await convertBrunoCollectionToOpenAPI('./path-to-your-bruno-collection-directory'); // Convert to YAML and save - const yamlString = yaml.dump(result.openapi, { indent: 2 }); + const yamlString = yaml.dump(result.spec, { indent: 2 }); + fs.writeFileSync('openapi-spec.yaml', yamlString); + console.log('OpenAPI specification saved as openapi-spec.yaml'); +} + +convertCollection(); +``` + +### JSON Export (Sync) + +```javascript +import { convertBrunoCollectionToOpenAPISync } from '@gyeonghokim/bruno-to-openapi'; +import fs from 'fs'; + +function convertCollection() { + // Convert Bruno collection directory to OpenAPI specification synchronously + const result = convertBrunoCollectionToOpenAPISync('./path-to-your-bruno-collection-directory'); + + // Save as JSON file + fs.writeFileSync('openapi-spec.json', JSON.stringify(result.spec, null, 2)); + console.log('OpenAPI specification saved as openapi-spec.json'); +} + +convertCollection(); +``` + +### YAML Export (Sync) + +```javascript +import { convertBrunoCollectionToOpenAPISync } from '@gyeonghokim/bruno-to-openapi'; +import yaml from 'js-yaml'; +import fs from 'fs'; + +function convertCollection() { + // Convert Bruno collection directory to OpenAPI specification synchronously + const result = convertBrunoCollectionToOpenAPISync('./path-to-your-bruno-collection-directory'); + + // Convert to YAML and save + const yamlString = yaml.dump(result.spec, { indent: 2 }); fs.writeFileSync('openapi-spec.yaml', yamlString); console.log('OpenAPI specification saved as openapi-spec.yaml'); } @@ -74,7 +112,7 @@ convertCollection(); The library exports the following functions: - `convertBrunoCollectionToOpenAPI(collectionPath)`: Asynchronously takes a Bruno collection directory path and returns a Promise resolving to the conversion result containing the OpenAPI specification. -- `convertBrunoCollectionToAPISync(collectionPath)`: Synchronously takes a Bruno collection directory path and returns the conversion result containing the OpenAPI specification (Note: The full synchronous implementation has limitations and it's recommended to use the async version). +- `convertBrunoCollectionToOpenAPISync(collectionPath)`: Synchronously takes a Bruno collection directory path and returns the conversion result containing the OpenAPI specification (Note: The full synchronous implementation has limitations and it's recommended to use the async version). - `isValidBrunoCollection(collectionPath)`: Asynchronously validates if a given path contains a valid Bruno collection and returns a Promise resolving to true if valid. ## Contributing From 6d49dd2954f862ac7eefea4445b3f5594a81084f Mon Sep 17 00:00:00 2001 From: GyeongHo Kim Date: Wed, 7 Jan 2026 21:49:00 +0900 Subject: [PATCH 9/9] chore: biome format fixed --- tests/unit/index.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/index.test.ts b/tests/unit/index.test.ts index 359a1f8..e6eab03 100644 --- a/tests/unit/index.test.ts +++ b/tests/unit/index.test.ts @@ -70,9 +70,9 @@ function validateParameter(param: unknown, expectedName: string): void { expect(typeof param).toBe('object') expect(isRecord(param)).toBe(true) - if (!isRecord(param)) return + if (!isRecord(param)) return - expect(param).toHaveProperty('name', expectedName) + expect(param).toHaveProperty('name', expectedName) expect(typeof param.name).toBe('string') expect(param).toHaveProperty('in') expect(['query', 'header', 'path', 'cookie']).toContain(param.in)