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 diff --git a/packages/http-specs/spec-summary.md b/packages/http-specs/spec-summary.md index a25c9665dbe..32c198bfff1 100644 --- a/packages/http-specs/spec-summary.md +++ b/packages/http-specs/spec-summary.md @@ -2050,6 +2050,68 @@ 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/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-- +``` + +### Payload_MultiPart_FormData_File_uploadFileRequiredFilename + +- Endpoint: `post /multipart/form-data/file/required-filename` + +Test File type in multipart form data with required filename. +Expected request: + +``` +POST /multipart/form-data/file/required-filename 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_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/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-- +``` + ### Payload_MultiPart_FormData_fileArrayAndBasic - Endpoint: `post /multipart/form-data/complex-parts` @@ -5210,6 +5272,90 @@ 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. +The File type accepts any content type. For testing, server will return image/png. +Expected response: + +- Content-Type header: image/png +- 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` + +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. +The File type accepts any content type. For testing, sender will use image/png. +Expected request: + +- Content-Type header: image/png +- 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` + +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..062a8b12e00 100644 --- a/packages/http-specs/specs/payload/multipart/main.tsp +++ b/packages/http-specs/specs/payload/multipart/main.tsp @@ -608,4 +608,91 @@ namespace FormData { ): NoContentResponse; } } + + @route("/file") + namespace File { + model FileWithRequiredFilename extends TypeSpec.Http.File<"image/png"> { + filename: 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 required filename. + Expected request: + ``` + POST /multipart/form-data/file/required-filename 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("/required-filename") + op uploadFileRequiredFilename( + @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..3ac7dbda70a 100644 --- a/packages/http-specs/specs/payload/multipart/mockapi.ts +++ b/packages/http-specs/specs/payload/multipart/mockapi.ts @@ -414,3 +414,92 @@ 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_uploadFileRequiredFilename = passOnSuccess({ + uri: "/multipart/form-data/file/required-filename", + 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_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 new file mode 100644 index 00000000000..a977b8e6a47 --- /dev/null +++ b/packages/http-specs/specs/type/file/main.tsp @@ -0,0 +1,109 @@ +import "@typespec/http"; +import "@typespec/spector"; + +using Http; +using Spector; + +@doc("Test for File type usage in request and response bodies") +@scenarioService("/type/file") +namespace Type.File; + +/** + * 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: 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. + 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(): Http.File<"image/png">; + + @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: Http.File<"image/png" | "image/jpeg">, + ): 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(): Http.File<"image/png" | "image/jpeg">; + + @scenario + @scenarioDoc(""" + 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: image/png + - Body: binary content matching packages/http-specs/assets/image.png + """) + @post + @route("/request/default-content-type") + op uploadFileDefaultContentType(@bodyRoot file: Http.File): NoContentResponse; + + @scenario + @scenarioDoc(""" + 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: image/png + - Body: binary content matching packages/http-specs/assets/image.png + """) + @get + @route("/response/default-content-type") + op downloadFileDefaultContentType(): Http.File; +} 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..41e6be7ef1d --- /dev/null +++ b/packages/http-specs/specs/type/file/mockapi.ts @@ -0,0 +1,200 @@ +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); +} + +// 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 - 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", + 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: "image/png", + rawContent: pngFile, + }, + }, + response: { + status: 204, + }, + handler(req: MockRequest) { + // 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 }; + }, + 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: "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: "image/png", + rawContent: pngFile, + }, + }; + }, + kind: "MockApiDefinition", +});