Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions examples/express/__tests__/expressApp.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,7 @@ describeAPI(
})
},
)

describeAPI(
HttpMethod.GET,
"/failed-test",
Expand All @@ -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"),
})
})
},
)
64 changes: 64 additions & 0 deletions examples/express/expected/oas.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
23 changes: 23 additions & 0 deletions examples/express/expressApp.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
20 changes: 17 additions & 3 deletions itdoc-doc/docs/api-reference/interface.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
4 changes: 2 additions & 2 deletions itdoc-doc/docs/guides/configuration.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 4
sidebar_position: 9999
toc_max_heading_level: 4
---

Expand Down Expand Up @@ -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."` |
77 changes: 77 additions & 0 deletions itdoc-doc/docs/guides/file-related-api-guide.mdx
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 4
sidebar_position: 9999
toc_max_heading_level: 4
---

Expand Down
Original file line number Diff line number Diff line change
@@ -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 파일 업로드

> 아직 지원하지 않습니다.
2 changes: 0 additions & 2 deletions lib/config/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading
Loading