From 3cd60471fc662fee2b0e3777a6d2847f9cb8515b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 15:30:46 +0000 Subject: [PATCH 01/10] Initial plan From 229eb6e02210d92c59747583fb7b628c1c7b7ff7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 15:44:36 +0000 Subject: [PATCH 02/10] Add File type spector tests for body and multipart scenarios Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- packages/http-specs/specs/type/file/main.tsp | 219 +++++++++++++ .../http-specs/specs/type/file/mockapi.ts | 291 ++++++++++++++++++ 2 files changed, 510 insertions(+) create mode 100644 packages/http-specs/specs/type/file/main.tsp create mode 100644 packages/http-specs/specs/type/file/mockapi.ts diff --git a/packages/http-specs/specs/type/file/main.tsp b/packages/http-specs/specs/type/file/main.tsp new file mode 100644 index 00000000000..786f426d3fb --- /dev/null +++ b/packages/http-specs/specs/type/file/main.tsp @@ -0,0 +1,219 @@ +import "@typespec/http"; +import "@typespec/spector"; + +using TypeSpec.Http; +using Spector; + +@doc("Test for File type usage in request and response bodies") +@scenarioService("/type/file") +namespace Type.File; + +// Define custom File types for testing +model PngImageFile extends TypeSpec.Http.File { + contentType: "image/png"; +} + +model ImageFile extends TypeSpec.Http.File { + contentType: "image/png" | "image/jpeg"; +} + +/** + * Test File as request and response body with specific content type + */ +@route("/body") +namespace Body { + @scenario + @scenarioDoc(""" + Test File type as request body with specific content type. + Expected request: + - Content-Type header: image/png + - Body: binary content matching packages/http-specs/assets/image.png + """) + @post + @route("/request/specific-content-type") + op uploadFileSpecificContentType(@bodyRoot file: PngImageFile): NoContentResponse; + + @scenario + @scenarioDoc(""" + Test File type as response body with specific content type. + Expected response: + - Content-Type header: image/png + - Body: binary content matching packages/http-specs/assets/image.png + """) + @get + @route("/response/specific-content-type") + op downloadFileSpecificContentType(): PngImageFile; + + @scenario + @scenarioDoc(""" + Test File type as request body with multiple allowed content types (image/png or image/jpeg). + Client should send image/png. + Expected request: + - Content-Type header: image/png + - Body: binary content matching packages/http-specs/assets/image.png + """) + @post + @route("/request/multiple-content-types") + op uploadFileMultipleContentTypes(@bodyRoot file: ImageFile): NoContentResponse; + + @scenario + @scenarioDoc(""" + Test File type as response body with multiple allowed content types. + Service will return image/png. + Expected response: + - Content-Type header: image/png + - Body: binary content matching packages/http-specs/assets/image.png + """) + @get + @route("/response/multiple-content-types") + op downloadFileMultipleContentTypes(): ImageFile; + + @scenario + @scenarioDoc(""" + Test File type as request body with unspecified content type (defaults to application/octet-stream). + Expected request: + - Content-Type header: application/octet-stream (or not specified) + - Body: binary content matching packages/http-specs/assets/image.png + """) + @post + @route("/request/default-content-type") + op uploadFileDefaultContentType(@bodyRoot file: TypeSpec.Http.File): NoContentResponse; + + @scenario + @scenarioDoc(""" + Test File type as response body with unspecified content type (defaults to application/octet-stream). + Expected response: + - Content-Type header: application/octet-stream + - Body: binary content matching packages/http-specs/assets/image.png + """) + @get + @route("/response/default-content-type") + op downloadFileDefaultContentType(): TypeSpec.Http.File; +} + +/** + * Test File in multipart form data scenarios + */ +@route("/multipart") +namespace MultiPart { + model FileWithSpecificContentType extends TypeSpec.Http.File { + filename: string; + contentType: "image/png"; + } + + model FileWithMultipleContentTypes extends TypeSpec.Http.File { + filename: string; + contentType: "image/png" | "image/jpeg"; + } + + model FileWithRequiredMetadata extends TypeSpec.Http.File { + filename: string; + contentType: string; + } + + @scenario + @scenarioDoc(""" + Test File type in multipart form data with specific content type. + Expected request: + ``` + POST /multipart/specific-content-type HTTP/1.1 + Content-Type: multipart/form-data; boundary=abcde12345 + + --abcde12345 + Content-Disposition: form-data; name="file"; filename="image.png" + Content-Type: image/png + + {…file content of image.png…} + --abcde12345-- + ``` + """) + @post + @route("/specific-content-type") + op uploadFileSpecificContentType( + @header contentType: "multipart/form-data", + @multipartBody body: { + file: HttpPart; + }, + ): NoContentResponse; + + @scenario + @scenarioDoc(""" + Test File type in multipart form data with multiple allowed content types. + Client should send image/png. + Expected request: + ``` + POST /multipart/multiple-content-types HTTP/1.1 + Content-Type: multipart/form-data; boundary=abcde12345 + + --abcde12345 + Content-Disposition: form-data; name="file"; filename="image.png" + Content-Type: image/png + + {…file content of image.png…} + --abcde12345-- + ``` + """) + @post + @route("/multiple-content-types") + op uploadFileMultipleContentTypes( + @header contentType: "multipart/form-data", + @multipartBody body: { + file: HttpPart; + }, + ): NoContentResponse; + + @scenario + @scenarioDoc(""" + Test File type in multipart form data with required content type metadata. + Expected request: + ``` + POST /multipart/required-content-type HTTP/1.1 + Content-Type: multipart/form-data; boundary=abcde12345 + + --abcde12345 + Content-Disposition: form-data; name="file"; filename="image.png" + Content-Type: application/octet-stream + + {…file content of image.png…} + --abcde12345-- + ``` + """) + @post + @route("/required-content-type") + op uploadFileRequiredContentType( + @header contentType: "multipart/form-data", + @multipartBody body: { + file: HttpPart; + }, + ): NoContentResponse; + + @scenario + @scenarioDoc(""" + Test multiple File instances in multipart form data. + Expected request: + ``` + POST /multipart/file-array HTTP/1.1 + Content-Type: multipart/form-data; boundary=abcde12345 + + --abcde12345 + Content-Disposition: form-data; name="files"; filename="image1.png" + Content-Type: image/png + + {…file content of image.png…} + --abcde12345 + Content-Disposition: form-data; name="files"; filename="image2.png" + Content-Type: image/png + + {…file content of image.png…} + --abcde12345-- + ``` + """) + @post + @route("/file-array") + op uploadFileArray( + @header contentType: "multipart/form-data", + @multipartBody body: { + files: HttpPart[]; + }, + ): NoContentResponse; +} diff --git a/packages/http-specs/specs/type/file/mockapi.ts b/packages/http-specs/specs/type/file/mockapi.ts new file mode 100644 index 00000000000..b1d5c889a2d --- /dev/null +++ b/packages/http-specs/specs/type/file/mockapi.ts @@ -0,0 +1,291 @@ +import { + MockRequest, + passOnSuccess, + ScenarioMockApi, + ValidationError, +} from "@typespec/spec-api"; +import { pngFile } from "../../helper.js"; + +export const Scenarios: Record = {}; + +// Helper function to check file content +function checkFileContent(req: MockRequest, expectedFile: Buffer) { + req.expect.rawBodyEquals(expectedFile); +} + +// Helper function to check file in multipart +function checkMultipartFile( + req: MockRequest, + file: Record, + expectedContent: Buffer, + expectedContentType: string, + expectedFileName?: string, +) { + req.expect.deepEqual(file.mimetype, expectedContentType); + req.expect.deepEqual(file.buffer, expectedContent); + if (expectedFileName) { + req.expect.deepEqual(file.originalname, expectedFileName); + } +} + +// Body tests - Request with specific content type +Scenarios.Type_File_Body_uploadFileSpecificContentType = passOnSuccess({ + uri: "/type/file/body/request/specific-content-type", + method: "post", + request: { + body: { + contentType: "image/png", + rawContent: pngFile, + }, + }, + response: { + status: 204, + }, + handler(req: MockRequest) { + req.expect.containsHeader("content-type", "image/png"); + checkFileContent(req, pngFile); + return { status: 204 }; + }, + kind: "MockApiDefinition", +}); + +// Body tests - Response with specific content type +Scenarios.Type_File_Body_downloadFileSpecificContentType = passOnSuccess({ + uri: "/type/file/body/response/specific-content-type", + method: "get", + request: {}, + response: { + status: 200, + body: { + contentType: "image/png", + rawContent: pngFile, + }, + }, + handler(req: MockRequest) { + return { + status: 200, + body: { + contentType: "image/png", + rawContent: pngFile, + }, + }; + }, + kind: "MockApiDefinition", +}); + +// Body tests - Request with multiple content types +Scenarios.Type_File_Body_uploadFileMultipleContentTypes = passOnSuccess({ + uri: "/type/file/body/request/multiple-content-types", + method: "post", + request: { + body: { + contentType: "image/png", + rawContent: pngFile, + }, + }, + response: { + status: 204, + }, + handler(req: MockRequest) { + // Client should send image/png (one of the allowed types) + const contentType = req.headers["content-type"]; + if (contentType !== "image/png" && contentType !== "image/jpeg") { + throw new ValidationError( + "Expected content-type to be image/png or image/jpeg", + "image/png or image/jpeg", + contentType, + ); + } + checkFileContent(req, pngFile); + return { status: 204 }; + }, + kind: "MockApiDefinition", +}); + +// Body tests - Response with multiple content types +Scenarios.Type_File_Body_downloadFileMultipleContentTypes = passOnSuccess({ + uri: "/type/file/body/response/multiple-content-types", + method: "get", + request: {}, + response: { + status: 200, + body: { + contentType: "image/png", + rawContent: pngFile, + }, + }, + handler(req: MockRequest) { + // Server returns image/png (one of the allowed types) + return { + status: 200, + body: { + contentType: "image/png", + rawContent: pngFile, + }, + }; + }, + kind: "MockApiDefinition", +}); + +// Body tests - Request with default content type +Scenarios.Type_File_Body_uploadFileDefaultContentType = passOnSuccess({ + uri: "/type/file/body/request/default-content-type", + method: "post", + request: { + body: { + contentType: "application/octet-stream", + rawContent: pngFile, + }, + }, + response: { + status: 204, + }, + handler(req: MockRequest) { + // Content-Type should be application/octet-stream or not specified + const contentType = req.headers["content-type"]; + if (contentType && contentType !== "application/octet-stream") { + throw new ValidationError( + "Expected content-type to be application/octet-stream or not specified", + "application/octet-stream", + contentType, + ); + } + checkFileContent(req, pngFile); + return { status: 204 }; + }, + kind: "MockApiDefinition", +}); + +// Body tests - Response with default content type +Scenarios.Type_File_Body_downloadFileDefaultContentType = passOnSuccess({ + uri: "/type/file/body/response/default-content-type", + method: "get", + request: {}, + response: { + status: 200, + body: { + contentType: "application/octet-stream", + rawContent: pngFile, + }, + }, + handler(req: MockRequest) { + return { + status: 200, + body: { + contentType: "application/octet-stream", + rawContent: pngFile, + }, + }; + }, + kind: "MockApiDefinition", +}); + +// Multipart tests - Specific content type +Scenarios.Type_File_MultiPart_uploadFileSpecificContentType = passOnSuccess({ + uri: "/type/file/multipart/specific-content-type", + method: "post", + request: { + headers: { + "content-type": "multipart/form-data", + }, + }, + response: { + status: 204, + }, + handler(req: MockRequest) { + if (req.files instanceof Array && req.files.length === 1) { + const file = req.files[0]; + req.expect.deepEqual(file.fieldname, "file"); + checkMultipartFile(req, file, pngFile, "image/png", "image.png"); + return { status: 204 }; + } else { + throw new ValidationError("Expected exactly one file", "1 file", req.files); + } + }, + kind: "MockApiDefinition", +}); + +// Multipart tests - Multiple content types +Scenarios.Type_File_MultiPart_uploadFileMultipleContentTypes = passOnSuccess({ + uri: "/type/file/multipart/multiple-content-types", + method: "post", + request: { + headers: { + "content-type": "multipart/form-data", + }, + }, + response: { + status: 204, + }, + handler(req: MockRequest) { + if (req.files instanceof Array && req.files.length === 1) { + const file = req.files[0]; + req.expect.deepEqual(file.fieldname, "file"); + // Client should send image/png (one of the allowed types) + if (file.mimetype !== "image/png" && file.mimetype !== "image/jpeg") { + throw new ValidationError( + "Expected mimetype to be image/png or image/jpeg", + "image/png or image/jpeg", + file.mimetype, + ); + } + req.expect.deepEqual(file.buffer, pngFile); + req.expect.deepEqual(file.originalname, "image.png"); + return { status: 204 }; + } else { + throw new ValidationError("Expected exactly one file", "1 file", req.files); + } + }, + kind: "MockApiDefinition", +}); + +// Multipart tests - Required content type +Scenarios.Type_File_MultiPart_uploadFileRequiredContentType = passOnSuccess({ + uri: "/type/file/multipart/required-content-type", + method: "post", + request: { + headers: { + "content-type": "multipart/form-data", + }, + }, + response: { + status: 204, + }, + handler(req: MockRequest) { + if (req.files instanceof Array && req.files.length === 1) { + const file = req.files[0]; + req.expect.deepEqual(file.fieldname, "file"); + checkMultipartFile(req, file, pngFile, "application/octet-stream", "image.png"); + return { status: 204 }; + } else { + throw new ValidationError("Expected exactly one file", "1 file", req.files); + } + }, + kind: "MockApiDefinition", +}); + +// Multipart tests - File array +Scenarios.Type_File_MultiPart_uploadFileArray = passOnSuccess({ + uri: "/type/file/multipart/file-array", + method: "post", + request: { + headers: { + "content-type": "multipart/form-data", + }, + }, + response: { + status: 204, + }, + handler(req: MockRequest) { + if (req.files instanceof Array && req.files.length === 2) { + for (const file of req.files) { + req.expect.deepEqual(file.fieldname, "files"); + checkMultipartFile(req, file, pngFile, "image/png"); + } + return { status: 204 }; + } else { + throw new ValidationError("Expected exactly two files", "2 files", req.files); + } + }, + kind: "MockApiDefinition", +}); From 3721792e147ac6fc3d073e070705eb0f24abfacf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 15:51:43 +0000 Subject: [PATCH 03/10] Add comment explaining TypeSpec.Http.File usage and address code review Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- packages/http-specs/specs/type/file/main.tsp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/http-specs/specs/type/file/main.tsp b/packages/http-specs/specs/type/file/main.tsp index 786f426d3fb..70b29440539 100644 --- a/packages/http-specs/specs/type/file/main.tsp +++ b/packages/http-specs/specs/type/file/main.tsp @@ -1,13 +1,16 @@ import "@typespec/http"; import "@typespec/spector"; -using TypeSpec.Http; +using Http; using Spector; @doc("Test for File type usage in request and response bodies") @scenarioService("/type/file") namespace Type.File; +// Note: Using TypeSpec.Http.File fully qualified because "File" in the namespace name +// would shadow the File type from the Http namespace. + // Define custom File types for testing model PngImageFile extends TypeSpec.Http.File { contentType: "image/png"; From 361156f92cc9f9147750415d69fed087a774bcce Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 16:18:09 +0000 Subject: [PATCH 04/10] Address PR feedback: use inline File syntax and move multipart tests to multipart spec Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- packages/http-specs/spec-summary.md | 144 +++++++++++++++++ .../specs/payload/multipart/main.tsp | 124 ++++++++++++++ .../specs/payload/multipart/mockapi.ts | 122 ++++++++++++++ packages/http-specs/specs/type/file/main.tsp | 153 +----------------- .../http-specs/specs/type/file/mockapi.ts | 125 -------------- .../specs/type/file/test-inline.tsp | 8 + 6 files changed, 406 insertions(+), 270 deletions(-) create mode 100644 packages/http-specs/specs/type/file/test-inline.tsp diff --git a/packages/http-specs/spec-summary.md b/packages/http-specs/spec-summary.md index a25c9665dbe..7d445b29ea3 100644 --- a/packages/http-specs/spec-summary.md +++ b/packages/http-specs/spec-summary.md @@ -2050,6 +2050,88 @@ Content-Type: image/jpg --abcde12345-- ``` +### Payload_MultiPart_FormData_File_uploadFileArray + +- Endpoint: `post /multipart/form-data/file/file-array` + +Test multiple File instances in multipart form data. +Expected request: + +``` +POST /multipart/file/file-array HTTP/1.1 +Content-Type: multipart/form-data; boundary=abcde12345 + +--abcde12345 +Content-Disposition: form-data; name="files"; filename="image1.png" +Content-Type: image/png + +{…file content of image.png…} +--abcde12345 +Content-Disposition: form-data; name="files"; filename="image2.png" +Content-Type: image/png + +{…file content of image.png…} +--abcde12345-- +``` + +### Payload_MultiPart_FormData_File_uploadFileMultipleContentTypes + +- Endpoint: `post /multipart/form-data/file/multiple-content-types` + +Test File type in multipart form data with multiple allowed content types. +Client should send image/png. +Expected request: + +``` +POST /multipart/file/multiple-content-types HTTP/1.1 +Content-Type: multipart/form-data; boundary=abcde12345 + +--abcde12345 +Content-Disposition: form-data; name="file"; filename="image.png" +Content-Type: image/png + +{…file content of image.png…} +--abcde12345-- +``` + +### Payload_MultiPart_FormData_File_uploadFileRequiredContentType + +- Endpoint: `post /multipart/form-data/file/required-content-type` + +Test File type in multipart form data with required content type metadata. +Expected request: + +``` +POST /multipart/file/required-content-type HTTP/1.1 +Content-Type: multipart/form-data; boundary=abcde12345 + +--abcde12345 +Content-Disposition: form-data; name="file"; filename="image.png" +Content-Type: application/octet-stream + +{…file content of image.png…} +--abcde12345-- +``` + +### Payload_MultiPart_FormData_File_uploadFileSpecificContentType + +- Endpoint: `post /multipart/form-data/file/specific-content-type` + +Test File type in multipart form data with specific content type. +Expected request: + +``` +POST /multipart/file/specific-content-type HTTP/1.1 +Content-Type: multipart/form-data; boundary=abcde12345 + +--abcde12345 +Content-Disposition: form-data; name="file"; filename="image.png" +Content-Type: image/png + +{…file content of image.png…} +--abcde12345-- +``` + ### Payload_MultiPart_FormData_fileArrayAndBasic - Endpoint: `post /multipart/form-data/complex-parts` @@ -5210,6 +5292,68 @@ Expect to send a known value. Mock api expect to receive 'Monday' Expect to handle an unknown value. Mock api expect to receive 'Weekend' +### Type_File_Body_downloadFileDefaultContentType + +- Endpoint: `get /type/file/body/response/default-content-type` + +Test File type as response body with unspecified content type (defaults to application/octet-stream). +Expected response: + +- Content-Type header: application/octet-stream +- Body: binary content matching packages/http-specs/assets/image.png + +### Type_File_Body_downloadFileMultipleContentTypes + +- Endpoint: `get /type/file/body/response/multiple-content-types` + +Test File type as response body with multiple allowed content types. +Service will return image/png. +Expected response: + +- Content-Type header: image/png +- Body: binary content matching packages/http-specs/assets/image.png + +### Type_File_Body_downloadFileSpecificContentType + +- Endpoint: `get /type/file/body/response/specific-content-type` + +Test File type as response body with specific content type. +Expected response: + +- Content-Type header: image/png +- Body: binary content matching packages/http-specs/assets/image.png + +### Type_File_Body_uploadFileDefaultContentType + +- Endpoint: `post /type/file/body/request/default-content-type` + +Test File type as request body with unspecified content type (defaults to application/octet-stream). +Expected request: + +- Content-Type header: application/octet-stream (or not specified) +- Body: binary content matching packages/http-specs/assets/image.png + +### Type_File_Body_uploadFileMultipleContentTypes + +- Endpoint: `post /type/file/body/request/multiple-content-types` + +Test File type as request body with multiple allowed content types (image/png or image/jpeg). +Client should send image/png. +Expected request: + +- Content-Type header: image/png +- Body: binary content matching packages/http-specs/assets/image.png + +### Type_File_Body_uploadFileSpecificContentType + +- Endpoint: `post /type/file/body/request/specific-content-type` + +Test File type as request body with specific content type. +Expected request: + +- Content-Type header: image/png +- Body: binary content matching packages/http-specs/assets/image.png + ### Type_Model_Empty_getEmpty - Endpoint: `get /type/model/empty/alone` diff --git a/packages/http-specs/specs/payload/multipart/main.tsp b/packages/http-specs/specs/payload/multipart/main.tsp index 87a8f46f6c3..ea8eee08dc0 100644 --- a/packages/http-specs/specs/payload/multipart/main.tsp +++ b/packages/http-specs/specs/payload/multipart/main.tsp @@ -608,4 +608,128 @@ namespace FormData { ): NoContentResponse; } } + + @route("/file") + namespace File { + model FileWithSpecificContentType extends TypeSpec.Http.File { + filename: string; + contentType: "image/png"; + } + + model FileWithMultipleContentTypes extends TypeSpec.Http.File { + filename: string; + contentType: "image/png" | "image/jpeg"; + } + + model FileWithRequiredMetadata extends TypeSpec.Http.File { + filename: string; + contentType: string; + } + + @scenario + @scenarioDoc(""" + Test File type in multipart form data with specific content type. + Expected request: + ``` + POST /multipart/form-data/file/specific-content-type HTTP/1.1 + Content-Type: multipart/form-data; boundary=abcde12345 + + --abcde12345 + Content-Disposition: form-data; name="file"; filename="image.png" + Content-Type: image/png + + {…file content of image.png…} + --abcde12345-- + ``` + """) + @post + @route("/specific-content-type") + op uploadFileSpecificContentType( + @header contentType: "multipart/form-data", + @multipartBody body: { + file: HttpPart; + }, + ): NoContentResponse; + + @scenario + @scenarioDoc(""" + Test File type in multipart form data with multiple allowed content types. + Client should send image/png. + Expected request: + ``` + POST /multipart/form-data/file/multiple-content-types HTTP/1.1 + Content-Type: multipart/form-data; boundary=abcde12345 + + --abcde12345 + Content-Disposition: form-data; name="file"; filename="image.png" + Content-Type: image/png + + {…file content of image.png…} + --abcde12345-- + ``` + """) + @post + @route("/multiple-content-types") + op uploadFileMultipleContentTypes( + @header contentType: "multipart/form-data", + @multipartBody body: { + file: HttpPart; + }, + ): NoContentResponse; + + @scenario + @scenarioDoc(""" + Test File type in multipart form data with required content type metadata. + Expected request: + ``` + POST /multipart/form-data/file/required-content-type HTTP/1.1 + Content-Type: multipart/form-data; boundary=abcde12345 + + --abcde12345 + Content-Disposition: form-data; name="file"; filename="image.png" + Content-Type: application/octet-stream + + {…file content of image.png…} + --abcde12345-- + ``` + """) + @post + @route("/required-content-type") + op uploadFileRequiredContentType( + @header contentType: "multipart/form-data", + @multipartBody body: { + file: HttpPart; + }, + ): NoContentResponse; + + @scenario + @scenarioDoc(""" + Test multiple File instances in multipart form data. + Expected request: + ``` + POST /multipart/form-data/file/file-array HTTP/1.1 + Content-Type: multipart/form-data; boundary=abcde12345 + + --abcde12345 + Content-Disposition: form-data; name="files"; filename="image1.png" + Content-Type: image/png + + {…file content of image.png…} + --abcde12345 + Content-Disposition: form-data; name="files"; filename="image2.png" + Content-Type: image/png + + {…file content of image.png…} + --abcde12345-- + ``` + """) + @post + @route("/file-array") + op uploadFileArray( + @header contentType: "multipart/form-data", + @multipartBody body: { + files: HttpPart[]; + }, + ): NoContentResponse; + } } diff --git a/packages/http-specs/specs/payload/multipart/mockapi.ts b/packages/http-specs/specs/payload/multipart/mockapi.ts index 4c190ff8ad9..ee7f42e6cb6 100644 --- a/packages/http-specs/specs/payload/multipart/mockapi.ts +++ b/packages/http-specs/specs/payload/multipart/mockapi.ts @@ -414,3 +414,125 @@ Scenarios.Payload_MultiPart_FormData_HttpParts_NonString_float = passOnSuccess({ handler: (req: MockRequest) => createHandler(req, [checkFloat]), kind: "MockApiDefinition", }); + +// Helper function to check file in multipart for File type tests +function checkMultipartFile( + req: MockRequest, + file: Record, + expectedContent: Buffer, + expectedContentType: string, + fieldName: string = "file", + expectedFileName?: string, +) { + req.expect.deepEqual(file.fieldname, fieldName); + req.expect.deepEqual(file.mimetype, expectedContentType); + req.expect.deepEqual(file.buffer, expectedContent); + if (expectedFileName) { + req.expect.deepEqual(file.originalname, expectedFileName); + } +} + +// Multipart File type tests +Scenarios.Payload_MultiPart_FormData_File_uploadFileSpecificContentType = passOnSuccess({ + uri: "/multipart/form-data/file/specific-content-type", + method: "post", + request: { + headers: { + "content-type": "multipart/form-data", + }, + }, + response: { + status: 204, + }, + handler(req: MockRequest) { + if (req.files instanceof Array && req.files.length === 1) { + const file = req.files[0]; + checkMultipartFile(req, file, pngFile, "image/png", "file", "image.png"); + return { status: 204 }; + } else { + throw new ValidationError("Expected exactly one file", "1 file", req.files); + } + }, + kind: "MockApiDefinition", +}); + +Scenarios.Payload_MultiPart_FormData_File_uploadFileMultipleContentTypes = passOnSuccess({ + uri: "/multipart/form-data/file/multiple-content-types", + method: "post", + request: { + headers: { + "content-type": "multipart/form-data", + }, + }, + response: { + status: 204, + }, + handler(req: MockRequest) { + if (req.files instanceof Array && req.files.length === 1) { + const file = req.files[0]; + // Client should send image/png (one of the allowed types) + if (file.mimetype !== "image/png" && file.mimetype !== "image/jpeg") { + throw new ValidationError( + "Expected mimetype to be image/png or image/jpeg", + "image/png or image/jpeg", + file.mimetype, + ); + } + req.expect.deepEqual(file.fieldname, "file"); + req.expect.deepEqual(file.buffer, pngFile); + req.expect.deepEqual(file.originalname, "image.png"); + return { status: 204 }; + } else { + throw new ValidationError("Expected exactly one file", "1 file", req.files); + } + }, + kind: "MockApiDefinition", +}); + +Scenarios.Payload_MultiPart_FormData_File_uploadFileRequiredContentType = passOnSuccess({ + uri: "/multipart/form-data/file/required-content-type", + method: "post", + request: { + headers: { + "content-type": "multipart/form-data", + }, + }, + response: { + status: 204, + }, + handler(req: MockRequest) { + if (req.files instanceof Array && req.files.length === 1) { + const file = req.files[0]; + checkMultipartFile(req, file, pngFile, "application/octet-stream", "file", "image.png"); + return { status: 204 }; + } else { + throw new ValidationError("Expected exactly one file", "1 file", req.files); + } + }, + kind: "MockApiDefinition", +}); + +Scenarios.Payload_MultiPart_FormData_File_uploadFileArray = passOnSuccess({ + uri: "/multipart/form-data/file/file-array", + method: "post", + request: { + headers: { + "content-type": "multipart/form-data", + }, + }, + response: { + status: 204, + }, + handler(req: MockRequest) { + if (req.files instanceof Array && req.files.length === 2) { + for (const file of req.files) { + req.expect.deepEqual(file.fieldname, "files"); + checkMultipartFile(req, file, pngFile, "image/png", "files"); + } + return { status: 204 }; + } else { + throw new ValidationError("Expected exactly two files", "2 files", req.files); + } + }, + kind: "MockApiDefinition", +}); diff --git a/packages/http-specs/specs/type/file/main.tsp b/packages/http-specs/specs/type/file/main.tsp index 70b29440539..4631b10c086 100644 --- a/packages/http-specs/specs/type/file/main.tsp +++ b/packages/http-specs/specs/type/file/main.tsp @@ -8,18 +8,6 @@ using Spector; @scenarioService("/type/file") namespace Type.File; -// Note: Using TypeSpec.Http.File fully qualified because "File" in the namespace name -// would shadow the File type from the Http namespace. - -// Define custom File types for testing -model PngImageFile extends TypeSpec.Http.File { - contentType: "image/png"; -} - -model ImageFile extends TypeSpec.Http.File { - contentType: "image/png" | "image/jpeg"; -} - /** * Test File as request and response body with specific content type */ @@ -34,7 +22,7 @@ namespace Body { """) @post @route("/request/specific-content-type") - op uploadFileSpecificContentType(@bodyRoot file: PngImageFile): NoContentResponse; + op uploadFileSpecificContentType(@bodyRoot file: Http.File<"image/png">): NoContentResponse; @scenario @scenarioDoc(""" @@ -45,7 +33,7 @@ namespace Body { """) @get @route("/response/specific-content-type") - op downloadFileSpecificContentType(): PngImageFile; + op downloadFileSpecificContentType(): Http.File<"image/png">; @scenario @scenarioDoc(""" @@ -57,7 +45,9 @@ namespace Body { """) @post @route("/request/multiple-content-types") - op uploadFileMultipleContentTypes(@bodyRoot file: ImageFile): NoContentResponse; + op uploadFileMultipleContentTypes( + @bodyRoot file: Http.File<"image/png" | "image/jpeg">, + ): NoContentResponse; @scenario @scenarioDoc(""" @@ -69,7 +59,7 @@ namespace Body { """) @get @route("/response/multiple-content-types") - op downloadFileMultipleContentTypes(): ImageFile; + op downloadFileMultipleContentTypes(): Http.File<"image/png" | "image/jpeg">; @scenario @scenarioDoc(""" @@ -80,7 +70,7 @@ namespace Body { """) @post @route("/request/default-content-type") - op uploadFileDefaultContentType(@bodyRoot file: TypeSpec.Http.File): NoContentResponse; + op uploadFileDefaultContentType(@bodyRoot file: Http.File): NoContentResponse; @scenario @scenarioDoc(""" @@ -91,132 +81,5 @@ namespace Body { """) @get @route("/response/default-content-type") - op downloadFileDefaultContentType(): TypeSpec.Http.File; -} - -/** - * Test File in multipart form data scenarios - */ -@route("/multipart") -namespace MultiPart { - model FileWithSpecificContentType extends TypeSpec.Http.File { - filename: string; - contentType: "image/png"; - } - - model FileWithMultipleContentTypes extends TypeSpec.Http.File { - filename: string; - contentType: "image/png" | "image/jpeg"; - } - - model FileWithRequiredMetadata extends TypeSpec.Http.File { - filename: string; - contentType: string; - } - - @scenario - @scenarioDoc(""" - Test File type in multipart form data with specific content type. - Expected request: - ``` - POST /multipart/specific-content-type HTTP/1.1 - Content-Type: multipart/form-data; boundary=abcde12345 - - --abcde12345 - Content-Disposition: form-data; name="file"; filename="image.png" - Content-Type: image/png - - {…file content of image.png…} - --abcde12345-- - ``` - """) - @post - @route("/specific-content-type") - op uploadFileSpecificContentType( - @header contentType: "multipart/form-data", - @multipartBody body: { - file: HttpPart; - }, - ): NoContentResponse; - - @scenario - @scenarioDoc(""" - Test File type in multipart form data with multiple allowed content types. - Client should send image/png. - Expected request: - ``` - POST /multipart/multiple-content-types HTTP/1.1 - Content-Type: multipart/form-data; boundary=abcde12345 - - --abcde12345 - Content-Disposition: form-data; name="file"; filename="image.png" - Content-Type: image/png - - {…file content of image.png…} - --abcde12345-- - ``` - """) - @post - @route("/multiple-content-types") - op uploadFileMultipleContentTypes( - @header contentType: "multipart/form-data", - @multipartBody body: { - file: HttpPart; - }, - ): NoContentResponse; - - @scenario - @scenarioDoc(""" - Test File type in multipart form data with required content type metadata. - Expected request: - ``` - POST /multipart/required-content-type HTTP/1.1 - Content-Type: multipart/form-data; boundary=abcde12345 - - --abcde12345 - Content-Disposition: form-data; name="file"; filename="image.png" - Content-Type: application/octet-stream - - {…file content of image.png…} - --abcde12345-- - ``` - """) - @post - @route("/required-content-type") - op uploadFileRequiredContentType( - @header contentType: "multipart/form-data", - @multipartBody body: { - file: HttpPart; - }, - ): NoContentResponse; - - @scenario - @scenarioDoc(""" - Test multiple File instances in multipart form data. - Expected request: - ``` - POST /multipart/file-array HTTP/1.1 - Content-Type: multipart/form-data; boundary=abcde12345 - - --abcde12345 - Content-Disposition: form-data; name="files"; filename="image1.png" - Content-Type: image/png - - {…file content of image.png…} - --abcde12345 - Content-Disposition: form-data; name="files"; filename="image2.png" - Content-Type: image/png - - {…file content of image.png…} - --abcde12345-- - ``` - """) - @post - @route("/file-array") - op uploadFileArray( - @header contentType: "multipart/form-data", - @multipartBody body: { - files: HttpPart[]; - }, - ): NoContentResponse; + op downloadFileDefaultContentType(): Http.File; } diff --git a/packages/http-specs/specs/type/file/mockapi.ts b/packages/http-specs/specs/type/file/mockapi.ts index b1d5c889a2d..fcc260b1c9c 100644 --- a/packages/http-specs/specs/type/file/mockapi.ts +++ b/packages/http-specs/specs/type/file/mockapi.ts @@ -13,21 +13,6 @@ function checkFileContent(req: MockRequest, expectedFile: Buffer) { req.expect.rawBodyEquals(expectedFile); } -// Helper function to check file in multipart -function checkMultipartFile( - req: MockRequest, - file: Record, - expectedContent: Buffer, - expectedContentType: string, - expectedFileName?: string, -) { - req.expect.deepEqual(file.mimetype, expectedContentType); - req.expect.deepEqual(file.buffer, expectedContent); - if (expectedFileName) { - req.expect.deepEqual(file.originalname, expectedFileName); - } -} - // Body tests - Request with specific content type Scenarios.Type_File_Body_uploadFileSpecificContentType = passOnSuccess({ uri: "/type/file/body/request/specific-content-type", @@ -179,113 +164,3 @@ Scenarios.Type_File_Body_downloadFileDefaultContentType = passOnSuccess({ }, kind: "MockApiDefinition", }); - -// Multipart tests - Specific content type -Scenarios.Type_File_MultiPart_uploadFileSpecificContentType = passOnSuccess({ - uri: "/type/file/multipart/specific-content-type", - method: "post", - request: { - headers: { - "content-type": "multipart/form-data", - }, - }, - response: { - status: 204, - }, - handler(req: MockRequest) { - if (req.files instanceof Array && req.files.length === 1) { - const file = req.files[0]; - req.expect.deepEqual(file.fieldname, "file"); - checkMultipartFile(req, file, pngFile, "image/png", "image.png"); - return { status: 204 }; - } else { - throw new ValidationError("Expected exactly one file", "1 file", req.files); - } - }, - kind: "MockApiDefinition", -}); - -// Multipart tests - Multiple content types -Scenarios.Type_File_MultiPart_uploadFileMultipleContentTypes = passOnSuccess({ - uri: "/type/file/multipart/multiple-content-types", - method: "post", - request: { - headers: { - "content-type": "multipart/form-data", - }, - }, - response: { - status: 204, - }, - handler(req: MockRequest) { - if (req.files instanceof Array && req.files.length === 1) { - const file = req.files[0]; - req.expect.deepEqual(file.fieldname, "file"); - // Client should send image/png (one of the allowed types) - if (file.mimetype !== "image/png" && file.mimetype !== "image/jpeg") { - throw new ValidationError( - "Expected mimetype to be image/png or image/jpeg", - "image/png or image/jpeg", - file.mimetype, - ); - } - req.expect.deepEqual(file.buffer, pngFile); - req.expect.deepEqual(file.originalname, "image.png"); - return { status: 204 }; - } else { - throw new ValidationError("Expected exactly one file", "1 file", req.files); - } - }, - kind: "MockApiDefinition", -}); - -// Multipart tests - Required content type -Scenarios.Type_File_MultiPart_uploadFileRequiredContentType = passOnSuccess({ - uri: "/type/file/multipart/required-content-type", - method: "post", - request: { - headers: { - "content-type": "multipart/form-data", - }, - }, - response: { - status: 204, - }, - handler(req: MockRequest) { - if (req.files instanceof Array && req.files.length === 1) { - const file = req.files[0]; - req.expect.deepEqual(file.fieldname, "file"); - checkMultipartFile(req, file, pngFile, "application/octet-stream", "image.png"); - return { status: 204 }; - } else { - throw new ValidationError("Expected exactly one file", "1 file", req.files); - } - }, - kind: "MockApiDefinition", -}); - -// Multipart tests - File array -Scenarios.Type_File_MultiPart_uploadFileArray = passOnSuccess({ - uri: "/type/file/multipart/file-array", - method: "post", - request: { - headers: { - "content-type": "multipart/form-data", - }, - }, - response: { - status: 204, - }, - handler(req: MockRequest) { - if (req.files instanceof Array && req.files.length === 2) { - for (const file of req.files) { - req.expect.deepEqual(file.fieldname, "files"); - checkMultipartFile(req, file, pngFile, "image/png"); - } - return { status: 204 }; - } else { - throw new ValidationError("Expected exactly two files", "2 files", req.files); - } - }, - kind: "MockApiDefinition", -}); diff --git a/packages/http-specs/specs/type/file/test-inline.tsp b/packages/http-specs/specs/type/file/test-inline.tsp new file mode 100644 index 00000000000..d79977fd36d --- /dev/null +++ b/packages/http-specs/specs/type/file/test-inline.tsp @@ -0,0 +1,8 @@ +import "@typespec/http"; + +using Http; + +namespace Test.FileInline; + +@post +op upload(@bodyRoot file: Http.File<"image/png">): void; From a69b9f8cbb327e2edebd075815ca2c8e56d6b889 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 16:18:34 +0000 Subject: [PATCH 05/10] Remove accidentally committed test file Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- packages/http-specs/specs/type/file/test-inline.tsp | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 packages/http-specs/specs/type/file/test-inline.tsp diff --git a/packages/http-specs/specs/type/file/test-inline.tsp b/packages/http-specs/specs/type/file/test-inline.tsp deleted file mode 100644 index d79977fd36d..00000000000 --- a/packages/http-specs/specs/type/file/test-inline.tsp +++ /dev/null @@ -1,8 +0,0 @@ -import "@typespec/http"; - -using Http; - -namespace Test.FileInline; - -@post -op upload(@bodyRoot file: Http.File<"image/png">): void; From 107bfbd35181d665228a163c0eb138d63938e860 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 16:35:40 +0000 Subject: [PATCH 06/10] Address feedback: use inline File type, rename to FileWithRequiredFilename, remove FileWithRequiredMetadata Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- .../specs/payload/multipart/main.tsp | 53 +++---------------- .../specs/payload/multipart/mockapi.ts | 39 ++------------ 2 files changed, 11 insertions(+), 81 deletions(-) diff --git a/packages/http-specs/specs/payload/multipart/main.tsp b/packages/http-specs/specs/payload/multipart/main.tsp index ea8eee08dc0..062a8b12e00 100644 --- a/packages/http-specs/specs/payload/multipart/main.tsp +++ b/packages/http-specs/specs/payload/multipart/main.tsp @@ -611,19 +611,8 @@ namespace FormData { @route("/file") namespace File { - model FileWithSpecificContentType extends TypeSpec.Http.File { + model FileWithRequiredFilename extends TypeSpec.Http.File<"image/png"> { filename: string; - contentType: "image/png"; - } - - model FileWithMultipleContentTypes extends TypeSpec.Http.File { - filename: string; - contentType: "image/png" | "image/jpeg"; - } - - model FileWithRequiredMetadata extends TypeSpec.Http.File { - filename: string; - contentType: string; } @scenario @@ -647,17 +636,16 @@ namespace FormData { op uploadFileSpecificContentType( @header contentType: "multipart/form-data", @multipartBody body: { - file: HttpPart; + file: HttpPart>; }, ): NoContentResponse; @scenario @scenarioDoc(""" - Test File type in multipart form data with multiple allowed content types. - Client should send image/png. + Test File type in multipart form data with required filename. Expected request: ``` - POST /multipart/form-data/file/multiple-content-types HTTP/1.1 + POST /multipart/form-data/file/required-filename HTTP/1.1 Content-Type: multipart/form-data; boundary=abcde12345 --abcde12345 @@ -669,36 +657,11 @@ namespace FormData { ``` """) @post - @route("/multiple-content-types") - op uploadFileMultipleContentTypes( - @header contentType: "multipart/form-data", - @multipartBody body: { - file: HttpPart; - }, - ): NoContentResponse; - - @scenario - @scenarioDoc(""" - Test File type in multipart form data with required content type metadata. - Expected request: - ``` - POST /multipart/form-data/file/required-content-type HTTP/1.1 - Content-Type: multipart/form-data; boundary=abcde12345 - - --abcde12345 - Content-Disposition: form-data; name="file"; filename="image.png" - Content-Type: application/octet-stream - - {…file content of image.png…} - --abcde12345-- - ``` - """) - @post - @route("/required-content-type") - op uploadFileRequiredContentType( + @route("/required-filename") + op uploadFileRequiredFilename( @header contentType: "multipart/form-data", @multipartBody body: { - file: HttpPart; + file: HttpPart; }, ): NoContentResponse; @@ -728,7 +691,7 @@ namespace FormData { op uploadFileArray( @header contentType: "multipart/form-data", @multipartBody body: { - files: HttpPart[]; + files: HttpPart>[]; }, ): NoContentResponse; } diff --git a/packages/http-specs/specs/payload/multipart/mockapi.ts b/packages/http-specs/specs/payload/multipart/mockapi.ts index ee7f42e6cb6..3ac7dbda70a 100644 --- a/packages/http-specs/specs/payload/multipart/mockapi.ts +++ b/packages/http-specs/specs/payload/multipart/mockapi.ts @@ -456,8 +456,8 @@ Scenarios.Payload_MultiPart_FormData_File_uploadFileSpecificContentType = passOn kind: "MockApiDefinition", }); -Scenarios.Payload_MultiPart_FormData_File_uploadFileMultipleContentTypes = passOnSuccess({ - uri: "/multipart/form-data/file/multiple-content-types", +Scenarios.Payload_MultiPart_FormData_File_uploadFileRequiredFilename = passOnSuccess({ + uri: "/multipart/form-data/file/required-filename", method: "post", request: { headers: { @@ -470,40 +470,7 @@ Scenarios.Payload_MultiPart_FormData_File_uploadFileMultipleContentTypes = passO handler(req: MockRequest) { if (req.files instanceof Array && req.files.length === 1) { const file = req.files[0]; - // Client should send image/png (one of the allowed types) - if (file.mimetype !== "image/png" && file.mimetype !== "image/jpeg") { - throw new ValidationError( - "Expected mimetype to be image/png or image/jpeg", - "image/png or image/jpeg", - file.mimetype, - ); - } - req.expect.deepEqual(file.fieldname, "file"); - req.expect.deepEqual(file.buffer, pngFile); - req.expect.deepEqual(file.originalname, "image.png"); - return { status: 204 }; - } else { - throw new ValidationError("Expected exactly one file", "1 file", req.files); - } - }, - kind: "MockApiDefinition", -}); - -Scenarios.Payload_MultiPart_FormData_File_uploadFileRequiredContentType = passOnSuccess({ - uri: "/multipart/form-data/file/required-content-type", - method: "post", - request: { - headers: { - "content-type": "multipart/form-data", - }, - }, - response: { - status: 204, - }, - handler(req: MockRequest) { - if (req.files instanceof Array && req.files.length === 1) { - const file = req.files[0]; - checkMultipartFile(req, file, pngFile, "application/octet-stream", "file", "image.png"); + checkMultipartFile(req, file, pngFile, "image/png", "file", "image.png"); return { status: 204 }; } else { throw new ValidationError("Expected exactly one file", "1 file", req.files); From aa7271116d581df165677e453a915dfe732e379f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 16:48:42 +0000 Subject: [PATCH 07/10] Add tests for File with application/json content type Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- packages/http-specs/spec-summary.md | 52 +++++++++---------- packages/http-specs/specs/type/file/main.tsp | 22 ++++++++ .../http-specs/specs/type/file/mockapi.ts | 45 ++++++++++++++++ 3 files changed, 93 insertions(+), 26 deletions(-) diff --git a/packages/http-specs/spec-summary.md b/packages/http-specs/spec-summary.md index 7d445b29ea3..1a74bf9766b 100644 --- a/packages/http-specs/spec-summary.md +++ b/packages/http-specs/spec-summary.md @@ -2058,7 +2058,7 @@ Test multiple File instances in multipart form data. Expected request: ``` -POST /multipart/file/file-array HTTP/1.1 +POST /multipart/form-data/file/file-array HTTP/1.1 Content-Type: multipart/form-data; boundary=abcde12345 --abcde12345 @@ -2074,16 +2074,15 @@ Content-Type: image/png --abcde12345-- ``` -### Payload_MultiPart_FormData_File_uploadFileMultipleContentTypes +### Payload_MultiPart_FormData_File_uploadFileRequiredFilename -- Endpoint: `post /multipart/form-data/file/multiple-content-types` +- Endpoint: `post /multipart/form-data/file/required-filename` -Test File type in multipart form data with multiple allowed content types. -Client should send image/png. +Test File type in multipart form data with required filename. Expected request: ``` -POST /multipart/file/multiple-content-types HTTP/1.1 +POST /multipart/form-data/file/required-filename HTTP/1.1 Content-Type: multipart/form-data; boundary=abcde12345 --abcde12345 @@ -2094,25 +2093,6 @@ Content-Type: image/png --abcde12345-- ``` -### Payload_MultiPart_FormData_File_uploadFileRequiredContentType - -- Endpoint: `post /multipart/form-data/file/required-content-type` - -Test File type in multipart form data with required content type metadata. -Expected request: - -``` -POST /multipart/file/required-content-type HTTP/1.1 -Content-Type: multipart/form-data; boundary=abcde12345 - ---abcde12345 -Content-Disposition: form-data; name="file"; filename="image.png" -Content-Type: application/octet-stream - -{…file content of image.png…} ---abcde12345-- -``` - ### Payload_MultiPart_FormData_File_uploadFileSpecificContentType - Endpoint: `post /multipart/form-data/file/specific-content-type` @@ -2121,7 +2101,7 @@ Test File type in multipart form data with specific content type. Expected request: ``` -POST /multipart/file/specific-content-type HTTP/1.1 +POST /multipart/form-data/file/specific-content-type HTTP/1.1 Content-Type: multipart/form-data; boundary=abcde12345 --abcde12345 @@ -5302,6 +5282,16 @@ Expected response: - Content-Type header: application/octet-stream - Body: binary content matching packages/http-specs/assets/image.png +### Type_File_Body_downloadFileJsonContentType + +- Endpoint: `get /type/file/body/response/json-content-type` + +Test File type as response body with JSON content type. +Expected response: + +- Content-Type header: application/json +- Body: JSON content with file data + ### Type_File_Body_downloadFileMultipleContentTypes - Endpoint: `get /type/file/body/response/multiple-content-types` @@ -5333,6 +5323,16 @@ Expected request: - Content-Type header: application/octet-stream (or not specified) - Body: binary content matching packages/http-specs/assets/image.png +### Type_File_Body_uploadFileJsonContentType + +- Endpoint: `post /type/file/body/request/json-content-type` + +Test File type as request body with JSON content type. +Expected request: + +- Content-Type header: application/json +- Body: JSON content with file data + ### Type_File_Body_uploadFileMultipleContentTypes - Endpoint: `post /type/file/body/request/multiple-content-types` diff --git a/packages/http-specs/specs/type/file/main.tsp b/packages/http-specs/specs/type/file/main.tsp index 4631b10c086..ae445eb9c98 100644 --- a/packages/http-specs/specs/type/file/main.tsp +++ b/packages/http-specs/specs/type/file/main.tsp @@ -24,6 +24,28 @@ namespace Body { @route("/request/specific-content-type") op uploadFileSpecificContentType(@bodyRoot file: Http.File<"image/png">): NoContentResponse; + @scenario + @scenarioDoc(""" + Test File type as request body with JSON content type. + Expected request: + - Content-Type header: application/json + - Body: JSON content with file data + """) + @post + @route("/request/json-content-type") + op uploadFileJsonContentType(@bodyRoot file: Http.File<"application/json">): NoContentResponse; + + @scenario + @scenarioDoc(""" + Test File type as response body with JSON content type. + Expected response: + - Content-Type header: application/json + - Body: JSON content with file data + """) + @get + @route("/response/json-content-type") + op downloadFileJsonContentType(): Http.File<"application/json">; + @scenario @scenarioDoc(""" Test File type as response body with specific content type. diff --git a/packages/http-specs/specs/type/file/mockapi.ts b/packages/http-specs/specs/type/file/mockapi.ts index fcc260b1c9c..42fb499aaf7 100644 --- a/packages/http-specs/specs/type/file/mockapi.ts +++ b/packages/http-specs/specs/type/file/mockapi.ts @@ -34,6 +34,51 @@ Scenarios.Type_File_Body_uploadFileSpecificContentType = passOnSuccess({ kind: "MockApiDefinition", }); +// Body tests - Request with JSON content type +Scenarios.Type_File_Body_uploadFileJsonContentType = passOnSuccess({ + uri: "/type/file/body/request/json-content-type", + method: "post", + request: { + body: { + contentType: "application/json", + rawContent: JSON.stringify({ message: "test file content" }), + }, + }, + response: { + status: 204, + }, + handler(req: MockRequest) { + req.expect.containsHeader("content-type", "application/json"); + req.expect.rawBodyEquals(JSON.stringify({ message: "test file content" })); + return { status: 204 }; + }, + kind: "MockApiDefinition", +}); + +// Body tests - Response with JSON content type +Scenarios.Type_File_Body_downloadFileJsonContentType = passOnSuccess({ + uri: "/type/file/body/response/json-content-type", + method: "get", + request: {}, + response: { + status: 200, + body: { + contentType: "application/json", + rawContent: JSON.stringify({ message: "test file content" }), + }, + }, + handler(req: MockRequest) { + return { + status: 200, + body: { + contentType: "application/json", + rawContent: JSON.stringify({ message: "test file content" }), + }, + }; + }, + kind: "MockApiDefinition", +}); + // Body tests - Response with specific content type Scenarios.Type_File_Body_downloadFileSpecificContentType = passOnSuccess({ uri: "/type/file/body/response/specific-content-type", From 0994f42868417b445e915f5cb08bd1fc15c0f17c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 17:57:01 +0000 Subject: [PATCH 08/10] Fix default content type tests to expect image/png instead of application/octet-stream Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- packages/http-specs/spec-summary.md | 10 ++++++---- packages/http-specs/specs/type/file/main.tsp | 10 ++++++---- packages/http-specs/specs/type/file/mockapi.ts | 18 ++++++------------ 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/packages/http-specs/spec-summary.md b/packages/http-specs/spec-summary.md index 1a74bf9766b..32c198bfff1 100644 --- a/packages/http-specs/spec-summary.md +++ b/packages/http-specs/spec-summary.md @@ -5276,10 +5276,11 @@ Expect to handle an unknown value. Mock api expect to receive 'Weekend' - Endpoint: `get /type/file/body/response/default-content-type` -Test File type as response body with unspecified content type (defaults to application/octet-stream). +Test File type as response body with unspecified content type. +The File type accepts any content type. For testing, server will return image/png. Expected response: -- Content-Type header: application/octet-stream +- Content-Type header: image/png - Body: binary content matching packages/http-specs/assets/image.png ### Type_File_Body_downloadFileJsonContentType @@ -5317,10 +5318,11 @@ Expected response: - Endpoint: `post /type/file/body/request/default-content-type` -Test File type as request body with unspecified content type (defaults to application/octet-stream). +Test File type as request body with unspecified content type. +The File type accepts any content type. For testing, sender will use image/png. Expected request: -- Content-Type header: application/octet-stream (or not specified) +- Content-Type header: image/png - Body: binary content matching packages/http-specs/assets/image.png ### Type_File_Body_uploadFileJsonContentType diff --git a/packages/http-specs/specs/type/file/main.tsp b/packages/http-specs/specs/type/file/main.tsp index ae445eb9c98..a977b8e6a47 100644 --- a/packages/http-specs/specs/type/file/main.tsp +++ b/packages/http-specs/specs/type/file/main.tsp @@ -85,9 +85,10 @@ namespace Body { @scenario @scenarioDoc(""" - Test File type as request body with unspecified content type (defaults to application/octet-stream). + Test File type as request body with unspecified content type. + The File type accepts any content type. For testing, sender will use image/png. Expected request: - - Content-Type header: application/octet-stream (or not specified) + - Content-Type header: image/png - Body: binary content matching packages/http-specs/assets/image.png """) @post @@ -96,9 +97,10 @@ namespace Body { @scenario @scenarioDoc(""" - Test File type as response body with unspecified content type (defaults to application/octet-stream). + Test File type as response body with unspecified content type. + The File type accepts any content type. For testing, server will return image/png. Expected response: - - Content-Type header: application/octet-stream + - Content-Type header: image/png - Body: binary content matching packages/http-specs/assets/image.png """) @get diff --git a/packages/http-specs/specs/type/file/mockapi.ts b/packages/http-specs/specs/type/file/mockapi.ts index 42fb499aaf7..6d3c8d9b6d8 100644 --- a/packages/http-specs/specs/type/file/mockapi.ts +++ b/packages/http-specs/specs/type/file/mockapi.ts @@ -163,7 +163,7 @@ Scenarios.Type_File_Body_uploadFileDefaultContentType = passOnSuccess({ method: "post", request: { body: { - contentType: "application/octet-stream", + contentType: "image/png", rawContent: pngFile, }, }, @@ -171,15 +171,8 @@ Scenarios.Type_File_Body_uploadFileDefaultContentType = passOnSuccess({ status: 204, }, handler(req: MockRequest) { - // Content-Type should be application/octet-stream or not specified - const contentType = req.headers["content-type"]; - if (contentType && contentType !== "application/octet-stream") { - throw new ValidationError( - "Expected content-type to be application/octet-stream or not specified", - "application/octet-stream", - contentType, - ); - } + // File type accepts any content type, but for testing we expect image/png + req.expect.containsHeader("content-type", "image/png"); checkFileContent(req, pngFile); return { status: 204 }; }, @@ -194,15 +187,16 @@ Scenarios.Type_File_Body_downloadFileDefaultContentType = passOnSuccess({ response: { status: 200, body: { - contentType: "application/octet-stream", + contentType: "image/png", rawContent: pngFile, }, }, handler(req: MockRequest) { + // File type accepts any content type, but for testing we return image/png return { status: 200, body: { - contentType: "application/octet-stream", + contentType: "image/png", rawContent: pngFile, }, }; From 651bc95ef62a413ed3641a9d5a53f68b1f0ff1a5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 18:50:33 +0000 Subject: [PATCH 09/10] Run pnpm format to fix code formatting Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- packages/http-specs/specs/type/file/mockapi.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/http-specs/specs/type/file/mockapi.ts b/packages/http-specs/specs/type/file/mockapi.ts index 6d3c8d9b6d8..41e6be7ef1d 100644 --- a/packages/http-specs/specs/type/file/mockapi.ts +++ b/packages/http-specs/specs/type/file/mockapi.ts @@ -1,9 +1,4 @@ -import { - MockRequest, - passOnSuccess, - ScenarioMockApi, - ValidationError, -} from "@typespec/spec-api"; +import { MockRequest, passOnSuccess, ScenarioMockApi, ValidationError } from "@typespec/spec-api"; import { pngFile } from "../../helper.js"; export const Scenarios: Record = {}; From 4d4afdabef3fed9b2d5f017a61e34286970bbff2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 19:05:04 +0000 Subject: [PATCH 10/10] Add changelog entry for File type spector tests Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- ...add-spector-test-file-content-type-2026-0-29-19-4-24.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .chronus/changes/copilot-add-spector-test-file-content-type-2026-0-29-19-4-24.md diff --git a/.chronus/changes/copilot-add-spector-test-file-content-type-2026-0-29-19-4-24.md b/.chronus/changes/copilot-add-spector-test-file-content-type-2026-0-29-19-4-24.md new file mode 100644 index 00000000000..c62fc041965 --- /dev/null +++ b/.chronus/changes/copilot-add-spector-test-file-content-type-2026-0-29-19-4-24.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@typespec/http-specs" +--- + +Add Spector tests for File type with various content types (body and multipart) \ No newline at end of file