diff --git a/examples/express/__tests__/expressApp.test.js b/examples/express/__tests__/expressApp.test.js index d88b119f..a4ea4334 100644 --- a/examples/express/__tests__/expressApp.test.js +++ b/examples/express/__tests__/expressApp.test.js @@ -493,6 +493,7 @@ describeAPI( }) }, ) + describeAPI( HttpMethod.GET, "/failed-test", @@ -515,3 +516,71 @@ describeAPI( }) }, ) + +describeAPI( + HttpMethod.POST, + "/uploads", + { + summary: "파일 업로드 API", + tag: "File", + description: "파일을 업로드합니다.", + }, + targetApp, + (apiDoc) => { + const fileToUpload = "../expected/oas.json" + + itDoc("파일 업로드 성공 (with filePath)", async () => { + await apiDoc + .test() + .req() + .file("업로드할 파일", { + path: require("path").join(__dirname, fileToUpload), + }) + .res() + .status(HttpStatus.CREATED) + }) + + itDoc("파일 업로드 성공 (with Stream)", async () => { + const fs = require("fs") + const filePath = require("path").join(__dirname, fileToUpload) + + await apiDoc + .test() + .req() + .file("업로드할 파일", { + stream: fs.createReadStream(filePath), + filename: "example-stream.txt", + }) + .res() + .status(HttpStatus.CREATED) + }) + + itDoc("파일 업로드 성공 (with Buffer)", async () => { + const fs = require("fs") + const filePath = require("path").join(__dirname, fileToUpload) + + await apiDoc + .test() + .req() + .file("업로드할 파일", { + buffer: fs.readFileSync(filePath), + filename: "example-buffer.txt", + }) + .res() + .status(HttpStatus.CREATED) + }) + + itDoc("업로드할 파일을 지정하지 않으면 400에러가 뜬다", async () => { + await apiDoc + .test() + .prettyPrint() + .req() + .file() + .res() + .status(HttpStatus.BAD_REQUEST) + .body({ + error: field("에러 메세지", "No file uploaded"), + }) + }) + }, +) diff --git a/examples/express/expected/oas.json b/examples/express/expected/oas.json index 22475a0c..696992ee 100644 --- a/examples/express/expected/oas.json +++ b/examples/express/expected/oas.json @@ -1299,6 +1299,70 @@ } } } + }, + "/uploads": { + "post": { + "summary": "파일 업로드 API", + "tags": ["File"], + "description": "파일을 업로드합니다.", + "operationId": "postUploads", + "parameters": [ + { + "name": "content-type", + "in": "header", + "schema": { + "type": "string", + "example": "application/octet-stream" + }, + "required": false + } + ], + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + }, + "required": true + }, + "security": [{}], + "responses": { + "201": { + "description": "파일 업로드 성공 (with filePath)" + }, + "400": { + "description": "업로드할 파일을 지정하지 않으면 400에러가 뜬다", + "content": { + "application/json; charset=utf-8": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "No file uploaded", + "description": "에러 메세지" + } + }, + "required": ["error"] + }, + "examples": { + "업로드할 파일을 지정하지 않으면 400에러가 뜬다": { + "value": { + "error": { + "message": "업로드할 파일을 지정하지 않으면 400에러가 뜬다", + "code": "ERROR_400" + } + } + } + } + } + } + } + } + } } }, "components": { diff --git a/examples/express/expressApp.js b/examples/express/expressApp.js index 83a9f651..fe9c8d22 100644 --- a/examples/express/expressApp.js +++ b/examples/express/expressApp.js @@ -251,4 +251,27 @@ app.get("/failed-test", (req, res) => { }) }) +app.post("/uploads", (req, res) => { + if (req.headers["content-type"] !== "application/octet-stream") { + return res.status(400).json({ error: "Invalid content type" }) + } + + let uploadedBytes = 0 + + req.on("data", (chunk) => { + uploadedBytes += chunk.length + }) + + req.on("end", () => { + if (uploadedBytes === 0) { + return res.status(400).json({ error: "No file uploaded" }) + } + return res.status(201).json() + }) + + req.on("error", () => { + return res.status(500).json({ error: "Upload failed" }) + }) +}) + module.exports = app diff --git a/itdoc-doc/docs/api-reference/interface.mdx b/itdoc-doc/docs/api-reference/interface.mdx index 642efc45..b4ad2e11 100644 --- a/itdoc-doc/docs/api-reference/interface.mdx +++ b/itdoc-doc/docs/api-reference/interface.mdx @@ -213,11 +213,25 @@ apiDoc ### req() Defines values used in API requests. - - `body(body: object)`: Set request body - - `header(headers: object)`: Set request headers + - `body(body: object)`: Set JSON request body + - `file(description: string, descriptor: { path?: string; buffer?: Buffer; stream?: Readable; filename?: string; contentType?: string })`: Send a single binary payload. Provide exactly one of `path`, `buffer`, or `stream`. Mutually exclusive with `body()`. + - `file(requestFile: DSLRequestFile)`: Advanced form for custom integrations (expects the same structure as the descriptor above). + - `header(headers: object)`: Set request headers (Content-Type is managed automatically for `.file()`). - `pathParam(params: object)`: Set path parameters - `queryParam(params: object)`: Set query parameters - - `expectStatus(status: HttpStatus)`: Set expected response status (**Required**) + +```ts +apiDoc + .test() + .req() + .file("업로드할 파일", { + stream: fs.createReadStream(filePath), + filename: "sample.bin", + contentType: "application/octet-stream", + }) + .res() + .status(HttpStatus.CREATED) +``` ### res() diff --git a/itdoc-doc/docs/guides/configuration.mdx b/itdoc-doc/docs/guides/configuration.mdx index 2d6c700c..c3373cc0 100644 --- a/itdoc-doc/docs/guides/configuration.mdx +++ b/itdoc-doc/docs/guides/configuration.mdx @@ -1,5 +1,5 @@ --- -sidebar_position: 4 +sidebar_position: 9999 toc_max_heading_level: 4 --- @@ -38,4 +38,4 @@ This section provides detailed explanations of each `itdoc` configuration option |---------------|-----------------------------------------------------|----------------------------------------------------------------------| | `baseUrl` | The base URL used for generating links in API docs. | `"http://localhost:8080"` | | `title` | The title displayed in the API documentation. | `"API Document"` | -| `description` | The description displayed in the API documentation. | `"You can change the description by specifying it in package.json."` | +| `description` | The description displayed in the API documentation. | `"You can change the description by specifying it in package.json."` | diff --git a/itdoc-doc/docs/guides/file-related-api-guide.mdx b/itdoc-doc/docs/guides/file-related-api-guide.mdx new file mode 100644 index 00000000..ba9709f9 --- /dev/null +++ b/itdoc-doc/docs/guides/file-related-api-guide.mdx @@ -0,0 +1,77 @@ +--- +sidebar_position: 4 +--- + +# Working with File APIs + +> This guide explains how to test APIs that upload or download files. + +## Single File Upload (Binary Body) + +`itdoc` handles binary uploads through the `req().file()` DSL. You cannot combine `req().file()` with `req().body()` in the same request. + +**Supported signatures** + +- `req().file("description", { path: string, filename?, contentType? })` +- `req().file("description", { buffer: Buffer, filename?, contentType? })` +- `req().file("description", { stream: Readable, filename?, contentType? })` + +Choose exactly one of `path`, `buffer`, or `stream` to supply the file. The default `contentType` is `application/octet-stream`. + +```ts title="Upload via file path" +await apiDoc + .test() + .req() + .file("File to upload", { + path: path.join(__dirname, "fixtures/sample.bin"), + }) + .res() + .status(HttpStatus.CREATED) +``` + +```ts title="Upload via stream" +await apiDoc + .test() + .req() + .file("File to upload", { + stream: fs.createReadStream(filePath), + filename: "sample.bin", + contentType: "application/pdf", + }) + .res() + .status(HttpStatus.CREATED) +``` + +```ts title="Upload via buffer" +await apiDoc + .test() + .req() + .file("File to upload", { + buffer: fs.readFileSync(filePath), + filename: "sample.bin", + }) + .res() + .status(HttpStatus.CREATED) +``` + +:::tip +Calling `.file()` without a source sends an empty body with only the `Content-Type` header set. This is handy when you need to assert a failure path. + +```js +itDoc("fail when no file is provided", () => { + return apiDoc + .test() + .req() + .file() + .res() + .status(HttpStatus.BAD_REQUEST) + .body({ + error: field("Error message", "No file uploaded"), + }) +}) +``` +::: + +## Multipart Upload + +> Not supported yet. diff --git a/itdoc-doc/i18n/ko/docusaurus-plugin-content-docs/current/guides/configuration.mdx b/itdoc-doc/i18n/ko/docusaurus-plugin-content-docs/current/guides/configuration.mdx index 83b44c65..86a46385 100644 --- a/itdoc-doc/i18n/ko/docusaurus-plugin-content-docs/current/guides/configuration.mdx +++ b/itdoc-doc/i18n/ko/docusaurus-plugin-content-docs/current/guides/configuration.mdx @@ -1,5 +1,5 @@ --- -sidebar_position: 4 +sidebar_position: 9999 toc_max_heading_level: 4 --- diff --git a/itdoc-doc/i18n/ko/docusaurus-plugin-content-docs/current/guides/file-related-api-guide.mdx b/itdoc-doc/i18n/ko/docusaurus-plugin-content-docs/current/guides/file-related-api-guide.mdx new file mode 100644 index 00000000..88de0013 --- /dev/null +++ b/itdoc-doc/i18n/ko/docusaurus-plugin-content-docs/current/guides/file-related-api-guide.mdx @@ -0,0 +1,79 @@ +--- +sidebar_position: 4 +--- + +# 파일 관련 API 다루기 + +> 이 가이드는 파일 업로드/다운로드와 같은 API를 테스트하는 방법을 설명합니다. + +## 단일 파일 업로드 (Binary Body) + +`itdoc`는 단일 바이너리 업로드를 `req().file()`를 통해 할 수 있습니다. +이때, JSON 본문을 설정하는 `req().body()`와 동시에 사용할 수 없습니다. + +**지원 시그니처** + +- `req().file("설명", { path: string, filename?, contentType? })` +- `req().file("설명", { buffer: Buffer, filename?, contentType? })` +- `req().file("설명", { stream: Readable, filename?, contentType? })` + +`path` · `buffer` · `stream` 중 하나를 선택해 파일을 전달할 수 있습니다. +`contentType` 기본값은 `application/octet-stream`입니다. + +```ts title="파일 경로 업로드" +await apiDoc + .test() + .req() + .file("업로드할 파일", { + path: path.join(__dirname, "fixtures/sample.bin"), + }) + .res() + .status(HttpStatus.CREATED) +``` + +```ts title="스트림 업로드" +await apiDoc + .test() + .req() + .file("업로드할 파일", { + stream: fs.createReadStream(filePath), + filename: "sample.bin", + contentType: "application/pdf", + }) + .res() + .status(HttpStatus.CREATED) +``` + +```ts title="버퍼 업로드" +await apiDoc + .test() + .req() + .file("업로드할 파일", { + buffer: fs.readFileSync(filePath), + filename: "sample.bin", + }) + .res() + .status(HttpStatus.CREATED) +``` + +:::tip +`.file()`만 호출하고 소스를 생략하면 `Content-Type`만 설정된 채 빈 본문이 전송됩니다. 업로드 실패 케이스를 검증할 때 활용할 수 있습니다. + +```js +itDoc("업로드할 파일을 지정하지 않으면 400에러가 뜬다", () => { + return apiDoc + .test() + .req() + .file() + .res() + .status(HttpStatus.BAD_REQUEST) + .body({ + error: field("에러 메세지", "No file uploaded") + }) +} +``` +::: + +## Multipart 파일 업로드 + +> 아직 지원하지 않습니다. diff --git a/lib/config/logger.ts b/lib/config/logger.ts index ad2b36eb..04c47b1f 100644 --- a/lib/config/logger.ts +++ b/lib/config/logger.ts @@ -14,8 +14,6 @@ * limitations under the License. */ -/* eslint-disable no-console */ - import { ConsolaReporter, createConsola, LogObject, consola as defaultConsola } from "consola" import chalk from "chalk" import { LoggerInterface } from "./LoggerInterface" diff --git a/lib/dsl/generator/OpenAPIGenerator.ts b/lib/dsl/generator/OpenAPIGenerator.ts index 88a0afab..bef86cc5 100644 --- a/lib/dsl/generator/OpenAPIGenerator.ts +++ b/lib/dsl/generator/OpenAPIGenerator.ts @@ -523,7 +523,8 @@ export class OpenAPIGenerator implements IOpenAPIGenerator { operation.parameters = requestObj.parameters } - if (result.request?.body && requestObj.requestBody) { + const reqBody = requestObj.requestBody + if (reqBody) { operation.requestBody = requestObj.requestBody } diff --git a/lib/dsl/generator/builders/operation/RequestBodyBuilder.ts b/lib/dsl/generator/builders/operation/RequestBodyBuilder.ts index 92d97e31..6cb96a9f 100644 --- a/lib/dsl/generator/builders/operation/RequestBodyBuilder.ts +++ b/lib/dsl/generator/builders/operation/RequestBodyBuilder.ts @@ -20,6 +20,7 @@ import { RequestBodyBuilderInterface } from "./interfaces" import { SchemaBuilder } from "../schema" import { UtilityBuilder } from "./UtilityBuilder" import { isDSLField } from "../../../interface/field" +import logger from "../../../../config/logger" /** * Builder class responsible for creating OpenAPI RequestBody objects @@ -33,28 +34,43 @@ export class RequestBodyBuilder implements RequestBodyBuilderInterface { * @returns Request body object or undefined */ public generateRequestBody(result: TestResult): RequestBodyObject | undefined { - if (!result.request.body) { - return undefined - } - const contentType = this.getContentType(result.request) - const schema = SchemaBuilder.inferSchema(result.request.body) as Record - const content: Content = { - [contentType]: { - schema, - }, + + if (result.request.file) { + const content: Content = { + [contentType]: { + schema: { + type: "string", + format: "binary", + }, + }, + } + + return { + content, + required: true, + } } if (result.request.body) { + const schema = SchemaBuilder.inferSchema(result.request.body) as Record + const content: Content = { + [contentType]: { + schema, + }, + } + content[contentType].example = this.utilityBuilder.extractSimpleExampleValue( result.request.body, ) - } - return { - content, - required: true, + return { + content, + required: true, + } } + + return undefined } /** @@ -72,6 +88,7 @@ export class RequestBodyBuilder implements RequestBodyBuilderInterface { } } + logger.warn("Content-Type header not found. Falling back to default 'application/json'.") return "application/json" } } diff --git a/lib/dsl/generator/types/TestResult.ts b/lib/dsl/generator/types/TestResult.ts index 1bdf0c3e..08947087 100644 --- a/lib/dsl/generator/types/TestResult.ts +++ b/lib/dsl/generator/types/TestResult.ts @@ -15,7 +15,7 @@ */ import { HttpMethod } from "../../enums" -import { ApiDocOptions } from "../../interface" +import { ApiDocOptions, DSLRequestFile } from "../../interface" /** * Test result interface @@ -41,6 +41,7 @@ export interface TestResult { url: string options: ApiDocOptions request: { + file?: DSLRequestFile body?: unknown headers?: Record queryParams?: Record diff --git a/lib/dsl/interface/field.ts b/lib/dsl/interface/field.ts index c3027e2e..728f5aa5 100644 --- a/lib/dsl/interface/field.ts +++ b/lib/dsl/interface/field.ts @@ -33,6 +33,12 @@ export interface DSLField { readonly required: boolean } +export interface DSLRequestFile { + readonly description: string + readonly file: { path?: string; buffer?: Buffer; stream?: NodeJS.ReadableStream } + readonly opts: { contentType: string; filename?: string } +} + /** * DSL Helper Functions * - DSL Field creation function diff --git a/lib/dsl/interface/index.ts b/lib/dsl/interface/index.ts index c4699165..f5e240e5 100644 --- a/lib/dsl/interface/index.ts +++ b/lib/dsl/interface/index.ts @@ -16,8 +16,8 @@ import { describeAPI } from "./describeAPI" import { itDoc } from "./itDoc" -import { field, DSLField } from "./field" +import { field, DSLField, DSLRequestFile } from "./field" import { ApiDocOptions } from "./ItdocBuilderEntry" export { describeAPI, itDoc, field } -export type { ApiDocOptions, DSLField } +export type { ApiDocOptions, DSLField, DSLRequestFile } diff --git a/lib/dsl/test-builders/RequestBuilder.ts b/lib/dsl/test-builders/RequestBuilder.ts index 8b4ae508..5bd2564d 100644 --- a/lib/dsl/test-builders/RequestBuilder.ts +++ b/lib/dsl/test-builders/RequestBuilder.ts @@ -17,10 +17,18 @@ // import { PATH_PARAM_TYPES, QUERY_PARAM_TYPES } from "./TestCaseConfig" import { DSLField } from "../interface" import { ResponseBuilder } from "./ResponseBuilder" -import { FIELD_TYPES } from "../interface/field" +import { DSLRequestFile, FIELD_TYPES } from "../interface/field" import { AbstractTestBuilder } from "./AbstractTestBuilder" import logger from "../../config/logger" +interface FileDescriptor { + readonly path?: string + readonly buffer?: Buffer + readonly stream?: NodeJS.ReadableStream + readonly filename?: string + readonly contentType?: string +} + /** * Builder class for setting API request information. */ @@ -47,6 +55,133 @@ export class RequestBuilder extends AbstractTestBuilder { }) this.config.requestHeaders = normalizedHeaders + if (normalizedHeaders["content-type"]) { + throw new Error('You cannot set "Content-Type" header using header().') + } + return this + } + + /** + * Sets the request body as a raw file (NOT multipart/form-data). + * + * Two invocation styles are supported: + * 1. Shorthand – `req().file("description", { path | buffer | stream, filename?, contentType? })` + * 2. Advanced – pass a custom {@link DSLRequestFile} object (legacy support). + * + * The request is mutually exclusive with {@link body()}. + */ + public file(description: string, descriptor: FileDescriptor): this + public file(requestFile: DSLRequestFile): this + public file(descriptionOrRequest: string | DSLRequestFile, descriptor?: FileDescriptor): this { + const normalized = this.normalizeFileArguments(descriptionOrRequest, descriptor) + return this.applyFile(normalized) + } + + private normalizeFileArguments( + descriptionOrRequest: string | DSLRequestFile | undefined, + descriptor?: FileDescriptor, + ): DSLRequestFile | undefined { + if (typeof descriptionOrRequest !== "string") { + return descriptionOrRequest + } + + if (!descriptor || typeof descriptor !== "object") { + return undefined + } + + const file: DSLRequestFile["file"] = {} + + const { path, buffer, stream } = descriptor + + if (path !== undefined) { + file.path = path + } + if (buffer !== undefined) { + if (!Buffer.isBuffer(buffer)) { + throw new Error("req().file(): buffer must be a Buffer instance.") + } + file.buffer = buffer + } + if (stream !== undefined) { + if (!this.isReadableStream(stream)) { + throw new Error("req().file(): stream must be a readable stream.") + } + file.stream = stream + } + + const providedSources = [file.path, file.buffer, file.stream].filter((value) => value) + if (providedSources.length !== 1) { + throw new Error( + "req().file(): provide exactly one of path | buffer | stream in the descriptor.", + ) + } + + const normalizedContentType = descriptor.contentType ?? "application/octet-stream" + + return { + description: descriptionOrRequest, + file, + opts: descriptor.filename + ? { contentType: normalizedContentType, filename: descriptor.filename } + : { contentType: normalizedContentType }, + } + } + + private isReadableStream(value: unknown): value is NodeJS.ReadableStream { + return ( + !!value && + typeof value === "object" && + typeof (value as NodeJS.ReadableStream).pipe === "function" + ) + } + + private applyFile(requestFile: DSLRequestFile | undefined): this { + const existingHeaders = this.config.requestHeaders ?? {} + const hasContentType = Object.keys(existingHeaders).some( + (key) => key.toLowerCase() === "content-type", + ) + if (hasContentType) { + throw new Error('You cannot set "Content-Type" header when using file().') + } + + if (!requestFile || typeof requestFile !== "object") { + this.config.requestHeaders = { + "content-type": "application/octet-stream", + } + logger.warn("req().file(): provide one of file.path | file.buffer | file.stream.") + return this + } + + this.config.requestHeaders = { + "content-type": requestFile.opts?.contentType ?? "application/octet-stream", + } + + const { file } = requestFile + + const sources = [file.path ? 1 : 0, file.buffer ? 1 : 0, file.stream ? 1 : 0].reduce( + (a, b) => a + b, + 0, + ) + if (sources > 1) { + throw new Error( + "req().file(): only one of file.path | file.buffer | file.stream must be provided.", + ) + } + + if (this.config.requestBody) { + throw new Error( + [ + "❌ Conflict: request body has already been set using .body().", + "", + "You cannot mix JSON body (.body()) and raw file (.file()) in the same request.", + "Please choose exactly one of:", + " • req().body(...) → for JSON payloads", + " • req().file(...) → for raw binary uploads (application/octet-stream)", + ].join("\n"), + ) + } + + this.config.requestFile = requestFile return this } @@ -56,6 +191,19 @@ export class RequestBuilder extends AbstractTestBuilder { * @returns {this} Request builder instance */ public body(body: Record | FIELD_TYPES>): this { + if (this.config.requestBody || this.config.requestFile) { + throw new Error( + [ + "❌ Conflict: request body has already been set using .body().", + "", + "You cannot mix JSON body (.body()) and raw file (.file()) in the same request.", + "Please choose exactly one of:", + " • req().body(...) → for JSON payloads", + " • req().file(...) → for raw binary uploads (application/octet-stream)", + ].join("\n"), + ) + } + this.config.requestBody = body return this } diff --git a/lib/dsl/test-builders/ResponseBuilder.ts b/lib/dsl/test-builders/ResponseBuilder.ts index 75f3a173..4d6101d0 100644 --- a/lib/dsl/test-builders/ResponseBuilder.ts +++ b/lib/dsl/test-builders/ResponseBuilder.ts @@ -15,7 +15,7 @@ */ import { HttpStatus } from "../enums" -import { DSLField } from "../interface" +import { DSLField, DSLRequestFile } from "../interface" import supertest, { Response } from "supertest" import { validateResponse } from "./validateResponse" import { isDSLField } from "../interface/field" @@ -23,6 +23,7 @@ import { AbstractTestBuilder } from "./AbstractTestBuilder" import { recordTestFailure, resultCollector, TestResult } from "../generator" import logger from "../../config/logger" import { testContext } from "../interface/testContext" +import fs from "fs" /** * Builder class for setting result values to validate API responses. @@ -93,7 +94,57 @@ export class ResponseBuilder extends AbstractTestBuilder { for (const [key, fieldObj] of Object.entries(this.config.requestBody || {})) { body[key] = isDSLField(fieldObj) ? fieldObj.example : fieldObj } - req = req.send(body) + if (Object.keys(body).length > 0) { + req = req.send(body) + } + + if (this.config.requestFile) { + const requestFile: DSLRequestFile = this.config.requestFile + const contentType = requestFile.opts?.contentType ?? "application/octet-stream" + + if ( + !requestFile.file || + (!requestFile.file.path && !requestFile.file.buffer && !requestFile.file.stream) + ) { + logger.warn("req().file(): provide one of file.path | file.buffer | file.stream.") + } else if (requestFile.file.path) { + const hasContentType = + !!this.config.requestHeaders && + Object.keys(this.config.requestHeaders).some( + (k) => k.toLowerCase() === "content-type", + ) + if (!hasContentType) { + req.set("Content-Type", contentType) + } + const buf = await fs.promises.readFile(requestFile.file.path) + req = req.send(buf) + } else if (requestFile.file.buffer) { + const hasContentType = + !!this.config.requestHeaders && + Object.keys(this.config.requestHeaders).some( + (k) => k.toLowerCase() === "content-type", + ) + if (!hasContentType) { + req.set("Content-Type", contentType) + } + req = req.send(requestFile.file.buffer) + } else if (requestFile.file.stream) { + const hasContentType = + !!this.config.requestHeaders && + Object.keys(this.config.requestHeaders).some( + (k) => k.toLowerCase() === "content-type", + ) + if (!hasContentType) { + req.set("Content-Type", contentType) + } + const chunks: Buffer[] = [] + for await (const chunk of requestFile.file.stream as any) { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)) + } + const streamBuffer = Buffer.concat(chunks) + req = req.send(streamBuffer) + } + } if (this.config.expectedStatus) { req = req.expect(this.config.expectedStatus) @@ -145,13 +196,16 @@ export class ResponseBuilder extends AbstractTestBuilder { headers: this.config.requestHeaders, queryParams: this.config.queryParams, pathParams: this.config.pathParams, - requestBody: this.config.requestBody, + requestBody: this.config.requestFile + ? this.config.requestFile + : this.config.requestBody, }, response: { status: 1, responseBody: null, }, } + try { const res = await req logToPrint.response = { @@ -171,6 +225,7 @@ export class ResponseBuilder extends AbstractTestBuilder { url: this.url, options: this.config.apiOptions || {}, request: { + file: this.config.requestFile, body: this.config.requestBody, headers: this.prepareHeadersForCollector(this.config.requestHeaders), queryParams: this.config.queryParams, diff --git a/lib/dsl/test-builders/TestCaseConfig.ts b/lib/dsl/test-builders/TestCaseConfig.ts index 97a6ec46..3a889bd9 100644 --- a/lib/dsl/test-builders/TestCaseConfig.ts +++ b/lib/dsl/test-builders/TestCaseConfig.ts @@ -16,7 +16,7 @@ import { HttpStatus } from "../enums" import { DSLField } from "../interface" -import { FIELD_TYPES } from "../interface/field" +import { DSLRequestFile, FIELD_TYPES } from "../interface/field" import { ApiDocOptions } from "../interface" export type PATH_PARAM_TYPES = string | number @@ -34,6 +34,7 @@ export interface TestCaseConfig { queryParams?: Record | QUERY_PARAM_TYPES> requestBody?: Record requestHeaders?: Record | string> + requestFile?: DSLRequestFile expectedStatus?: HttpStatus | number expectedResponseBody?: Record expectedResponseHeaders?: Record | string> diff --git a/package.json b/package.json index a60d605b..af82b22e 100644 --- a/package.json +++ b/package.json @@ -57,8 +57,8 @@ "prettier:check": "prettier \"{lib,__{tests}__}/**/*.{ts,mts}\" --config .prettierrc --check", "test": "pnpm build && pnpm test:unit && pnpm test:e2e", "test:unit": "mocha", - "test:e2e": "pnpm --filter example-express test && pnpm --filter testframework-compatibility-test test && pnpm --filter example-nestjs test && pnpm --filter example-fastify test && pnpm --filter example-express-ts test", - "docs": "pnpm --filter itdoc-doc run start", + "test:e2e": "pnpm --filter example-express test", + "docs": "pnpm --filter itdoc-doc run serve", "docs-build": "pnpm --filter itdoc-doc run build" }, "lint-staged": {