diff --git a/CHANGELOG.md b/CHANGELOG.md index cac4676e6..83e417c0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,10 +26,18 @@ - **`ServiceRegistry`**: Access and manage attributes related to Jira Service Management’s service registry, which helps organize and maintain services ([documentation](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-service-registry/#api-group-service-registry)). - **`TeamsInPlan`**: Configure settings for Atlassian and custom teams within advanced roadmaps plans, including creating, updating, and deleting team configurations ([documentation](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-teams-in-plan/#api-group-teams-in-plan)). +### 4.0.6 + +- **#347:** Fixed an issue with adding attachments of type `Readable` or `ReadableStream` (e.g., `fs.createReadStream`). Thanks to [Lunatic174](https://github.com/Lunatic174) for [reporting the issue](https://github.com/MrRefactoring/jira.js/issues/347). + +--- + ### 4.0.5 - **#344:** Replaced the `mime-types` library with `mime` to ensure browser compatibility, as `mime-types` relies on the `path` module from Node.js. Thanks to [kang](https://github.com/kang8) for [reporting the issue](https://github.com/MrRefactoring/jira.js/issues/344) and proposing the fix. +--- + ### 4.0.4 - **#320:** Resolved a tree-shaking issue where importing a single client would still include all clients in the output bundle when using bundlers. Now, only the required client code is included. Thanks to [Nao Yonashiro](https://github.com/orisano) for [reporting the issue](https://github.com/MrRefactoring/jira.js/issues/320) and proposing a fix. @@ -72,12 +80,16 @@ console.log(attachment[0].mimeType); // Will be 'application/typescript' ``` +--- + ### 4.0.3 - **Bug Fix:** Fixed an issue with the `Users.createUser` method by adding the required `products` property. Thanks to [Appelberg-s](https://github.com/Appelberg-s) for the [fix](https://github.com/MrRefactoring/jira.js/commit/362918093c20036049db334743e2a0f5f41cbcd4#diff-6960050bc2a3d9ffad9eb5e307145969dc4a38eb5434eebf39da545fd18e01b7R12). - **Documentation Update:** Corrected an error in `README.md`. Thanks to [Maurice de Bruyn](https://github.com/ueberBrot) for the [contribution](https://github.com/MrRefactoring/jira.js/commit/fb6151e1a0c7953b9447aaaf99caea5c2f93bb96). - **Dependencies:** Updated all dependencies to their latest versions. +--- + ### 4.0.2 - `getAllProjects` in README and examples replaced to `searchProjects`. Thanks to [Alexander Pivovarov](https://github.com/bladerunner2020) for reporting [the issue](https://github.com/MrRefactoring/jira.js/issues/323). diff --git a/package-lock.json b/package-lock.json index 194913b06..ec9cde6c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,7 @@ "prettier-plugin-jsdoc": "^1.3.2", "sinon": "^18.0.1", "typedoc": "^0.27.6", - "typescript": "^5.7.2", + "typescript": "^5.7.3", "vite-tsconfig-paths": "^5.1.4", "vitest": "^2.1.8" } @@ -5799,9 +5799,9 @@ } }, "node_modules/typescript": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", - "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/package.json b/package.json index 6d64ddc3e..680838f96 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "test": "npm run test:unit && npm run test:integration", "test:unit": "vitest run tests/unit --maxWorkers=8 --sequence.concurrent", "test:integration": "vitest run tests/integration --bail=1 --no-file-parallelism --max-concurrency 1 -c vitest.config.mts --hookTimeout 100000 --testTimeout 100000", - "replace:all": "npm run replace:permissions:version2 && npm run replace:permissions:version3 && npm run replace:pagination:version2 && npm run replace:pagination:version3 && npm run replace:async:version2 && npm run replace:async:version3 && npm run replace:expansion:version2 && npm run replace:expansion:version3 && npm run replace:ordering:version2 && npm run replace:ordering:version3 && npm run replace:groupMember:version2 && npm run replace:workflowPaginated:version2", + "replace:all": "npm run replace:permissions:version2 && npm run replace:permissions:version3 && npm run replace:pagination:version2 && npm run replace:pagination:version3 && npm run replace:async:version2 && npm run replace:async:version3 && npm run replace:expansion:version2 && npm run replace:expansion:version3 && npm run replace:ordering:version2 && npm run replace:ordering:version3 && npm run replace:groupMember:version2 && npm run replace:workflowPaginated:version2 && npm run replace:attachment:serviceDesk", "replace:permissions:version2": "grep -rl \"(#permissions)\" ./src/version2 | xargs sed -i '' 's/(#permissions)/(https:\\/\\/developer.atlassian.com\\/cloud\\/jira\\/platform\\/rest\\/v2\\/intro\\/#permissions)/g'", "replace:permissions:version3": "grep -rl \"(#permissions)\" ./src/version3 | xargs sed -i '' 's/(#permissions)/(https:\\/\\/developer.atlassian.com\\/cloud\\/jira\\/platform\\/rest\\/v3\\/intro\\/#permissions)/g'", "replace:pagination:version2": "grep -rl \"(#pagination)\" ./src/version2 | xargs sed -i '' 's/(#pagination)/(https:\\/\\/developer.atlassian.com\\/cloud\\/jira\\/platform\\/rest\\/v2\\/intro\\/#pagination)/g'", @@ -50,6 +50,7 @@ "replace:ordering:version3": "grep -rl \"(#ordering)\" ./src/version3 | xargs sed -i '' 's/(#ordering)/(https:\\/\\/developer.atlassian.com\\/cloud\\/jira\\/platform\\/rest\\/v3\\/intro\\/#ordering)/g'", "replace:groupMember:version2": "grep -rl \"(#api-rest-api-2-group-member-get)\" ./src/version2 | xargs sed -i '' 's/(#api-rest-api-2-group-member-get)/(https:\\/\\/developer.atlassian.com\\/cloud\\/jira\\/platform\\/rest\\/v2\\/api-group-groups\\/#api-rest-api-2-group-member-get)/g'", "replace:workflowPaginated:version2": "grep -rl \"(#api-rest-api-2-workflow-search-get)\" ./src/version2 | xargs sed -i '' 's/(#api-rest-api-2-workflow-search-get)/(https:\\/\\/developer.atlassian.com\\/cloud\\/jira\\/platform\\/rest\\/v2\\/api-group-workflows\\/#api-rest-api-2-workflow-search-get)/g'", + "replace:attachment:serviceDesk": "grep -rl \"(#api-request-issueIdOrKey-attachment-post)\" ./src/serviceDesk | xargs sed -i '' 's/(#api-request-issueIdOrKey-attachment-post)/(https:\\/\\/developer.atlassian.com\\/cloud\\/jira\\/service-desk\\/rest\\/api-group-servicedesk\\/#api-rest-servicedeskapi-servicedesk-servicedeskid-attachtemporaryfile-post)/g'", "code:formatting": "npm run replace:all && npm run prettier && npm run lint:fix" }, "devDependencies": { @@ -66,7 +67,7 @@ "prettier-plugin-jsdoc": "^1.3.2", "sinon": "^18.0.1", "typedoc": "^0.27.6", - "typescript": "^5.7.2", + "typescript": "^5.7.3", "vite-tsconfig-paths": "^5.1.4", "vitest": "^2.1.8" }, diff --git a/src/serviceDesk/parameters/attachTemporaryFile.ts b/src/serviceDesk/parameters/attachTemporaryFile.ts index e9d0d7a7c..d68513d96 100644 --- a/src/serviceDesk/parameters/attachTemporaryFile.ts +++ b/src/serviceDesk/parameters/attachTemporaryFile.ts @@ -1,3 +1,5 @@ +import type { Readable } from 'node:stream'; + /** * Represents an attachment to be temporarily attached to a Service Desk. * @@ -35,7 +37,7 @@ export interface Attachment { * const fileContent = Buffer.from('Example content here'); * ``` */ - file: Buffer | ReadableStream | string | Blob | File; + file: Buffer | ReadableStream | Readable | string | Blob | File; /** * Optional MIME type of the attachment. Example values include: diff --git a/src/serviceDesk/serviceDesk.ts b/src/serviceDesk/serviceDesk.ts index f7ec53e7a..ebd4b60e4 100644 --- a/src/serviceDesk/serviceDesk.ts +++ b/src/serviceDesk/serviceDesk.ts @@ -1,4 +1,5 @@ import { FormData, File } from 'formdata-node'; +import type { Mime } from 'mime' with { 'resolution-mode': 'import' }; import * as Models from './models'; import * as Parameters from './parameters'; import { Callback } from '../callback'; @@ -85,7 +86,7 @@ export class ServiceDesk { /** * This method adds one or more temporary attachments to a service desk, which can then be permanently attached to a * customer request using - * [servicedeskapi/request/{issueIdOrKey}/attachment](#api-request-issueIdOrKey-attachment-post). + * [servicedeskapi/request/{issueIdOrKey}/attachment](https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-servicedesk/#api-rest-servicedeskapi-servicedesk-servicedeskid-attachtemporaryfile-post). * * **Note**: It is possible for a service desk administrator to turn off the ability to add attachments to a service * desk. @@ -100,7 +101,7 @@ export class ServiceDesk { /** * This method adds one or more temporary attachments to a service desk, which can then be permanently attached to a * customer request using - * [servicedeskapi/request/{issueIdOrKey}/attachment](#api-request-issueIdOrKey-attachment-post). + * [servicedeskapi/request/{issueIdOrKey}/attachment](https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-servicedesk/#api-rest-servicedeskapi-servicedesk-servicedeskid-attachtemporaryfile-post). * * **Note**: It is possible for a service desk administrator to turn off the ability to add attachments to a service * desk. @@ -118,14 +119,24 @@ export class ServiceDesk { const { default: mime } = await import('mime'); - attachments.forEach(attachment => { - const mimeType = attachment.mimeType ?? (mime.getType(attachment.filename) || undefined); - const file = Buffer.isBuffer(attachment.file) - ? new File([attachment.file], attachment.filename, { type: mimeType }) - : attachment.file; + let Readable: typeof import('stream').Readable | undefined; + + if (typeof window === 'undefined') { + const { Readable: NodeReadable } = await import('stream'); + + Readable = NodeReadable; + } + + // eslint-disable-next-line no-restricted-syntax + for await (const attachment of attachments) { + const file = await this._convertToFile(attachment, mime, Readable); + + if (!(file instanceof File || file instanceof Blob)) { + throw new Error(`Unsupported file type for attachment: ${typeof file}`); + } formData.append('file', file, attachment.filename); - }); + } const config: RequestConfig = { url: `/rest/servicedeskapi/servicedesk/${parameters.serviceDeskId}/attachTemporaryFile`, @@ -808,4 +819,75 @@ export class ServiceDesk { return this.client.sendRequest(config, callback); } + + private async _convertToFile( + attachment: Parameters.Attachment, + mime: Mime, + Readable?: typeof import('stream').Readable, + ): Promise { + const mimeType = attachment.mimeType ?? (mime.getType(attachment.filename) || undefined); + + if (attachment.file instanceof Blob || attachment.file instanceof File) { + return attachment.file; + } + + if (typeof attachment.file === 'string') { + return new File([attachment.file], attachment.filename, { type: mimeType }); + } + + if (Readable && attachment.file instanceof Readable) { + return this._streamToBlob(attachment.file, attachment.filename, mimeType); + } + + if (attachment.file instanceof ReadableStream) { + return this._streamToBlob(attachment.file, attachment.filename, mimeType); + } + + if (ArrayBuffer.isView(attachment.file) || attachment.file instanceof ArrayBuffer) { + return new File([attachment.file], attachment.filename, { type: mimeType }); + } + + throw new Error('Unsupported attachment file type.'); + } + + private async _streamToBlob( + stream: import('stream').Readable | ReadableStream, + filename: string, + mimeType?: string, + ): Promise { + if (typeof window === 'undefined' && stream instanceof (await import('stream')).Readable) { + return new Promise((resolve, reject) => { + const chunks: Uint8Array[] = []; + + stream.on('data', chunk => chunks.push(chunk)); + stream.on('end', () => { + const blob = new Blob(chunks, { type: mimeType }); + + resolve(new File([blob], filename, { type: mimeType })); + }); + stream.on('error', reject); + }); + } + + if (stream instanceof ReadableStream) { + const reader = stream.getReader(); + const chunks: Uint8Array[] = []; + + let done = false; + + while (!done) { + // eslint-disable-next-line no-await-in-loop + const { value, done: streamDone } = await reader.read(); + + if (value) chunks.push(value); + done = streamDone; + } + + const blob = new Blob(chunks, { type: mimeType }); + + return new File([blob], filename, { type: mimeType }); + } + + throw new Error('Unsupported stream type.'); + } } diff --git a/src/version2/issueAttachments.ts b/src/version2/issueAttachments.ts index 8598fd9f4..8400766bd 100644 --- a/src/version2/issueAttachments.ts +++ b/src/version2/issueAttachments.ts @@ -1,4 +1,5 @@ import { FormData, File } from 'formdata-node'; +import type { Mime } from 'mime' with { 'resolution-mode': 'import' }; import * as Models from './models'; import * as Parameters from './parameters'; import { Client } from '../clients'; @@ -379,12 +380,6 @@ export class IssueAttachments { * Adds one or more attachments to an issue. Attachments are posted as multipart/form-data ([RFC * 1867](https://www.ietf.org/rfc/rfc1867.txt)). * - * Note that: - * - * - The request must have a `X-Atlassian-Token: no-check` header, if not it is blocked. See [Special - * headers](#special-request-headers) for more information. - * - The name of the multipart/form-data parameter that contains the attachments must be `file`. - * * This operation can be accessed anonymously. * * **[Permissions](https://developer.atlassian.com/cloud/jira/platform/rest/v2/intro/#permissions) required:** @@ -402,12 +397,6 @@ export class IssueAttachments { * Adds one or more attachments to an issue. Attachments are posted as multipart/form-data ([RFC * 1867](https://www.ietf.org/rfc/rfc1867.txt)). * - * Note that: - * - * - The request must have a `X-Atlassian-Token: no-check` header, if not it is blocked. See [Special - * headers](#special-request-headers) for more information. - * - The name of the multipart/form-data parameter that contains the attachments must be `file`. - * * This operation can be accessed anonymously. * * **[Permissions](https://developer.atlassian.com/cloud/jira/platform/rest/v2/intro/#permissions) required:** @@ -427,14 +416,24 @@ export class IssueAttachments { const { default: mime } = await import('mime'); - attachments.forEach(attachment => { - const mimeType = attachment.mimeType ?? (mime.getType(attachment.filename) || undefined); - const file = Buffer.isBuffer(attachment.file) - ? new File([attachment.file], attachment.filename, { type: mimeType }) - : attachment.file; + let Readable: typeof import('stream').Readable | undefined; + + if (typeof window === 'undefined') { + const { Readable: NodeReadable } = await import('stream'); + + Readable = NodeReadable; + } + + // eslint-disable-next-line no-restricted-syntax + for await (const attachment of attachments) { + const file = await this._convertToFile(attachment, mime, Readable); + + if (!(file instanceof File || file instanceof Blob)) { + throw new Error(`Unsupported file type for attachment: ${typeof file}`); + } formData.append('file', file, attachment.filename); - }); + } const config: RequestConfig = { url: `/rest/api/2/issue/${parameters.issueIdOrKey}/attachments`, @@ -450,4 +449,75 @@ export class IssueAttachments { return this.client.sendRequest(config, callback); } + + private async _convertToFile( + attachment: Parameters.Attachment, + mime: Mime, + Readable?: typeof import('stream').Readable, + ): Promise { + const mimeType = attachment.mimeType ?? (mime.getType(attachment.filename) || undefined); + + if (attachment.file instanceof Blob || attachment.file instanceof File) { + return attachment.file; + } + + if (typeof attachment.file === 'string') { + return new File([attachment.file], attachment.filename, { type: mimeType }); + } + + if (Readable && attachment.file instanceof Readable) { + return this._streamToBlob(attachment.file, attachment.filename, mimeType); + } + + if (attachment.file instanceof ReadableStream) { + return this._streamToBlob(attachment.file, attachment.filename, mimeType); + } + + if (ArrayBuffer.isView(attachment.file) || attachment.file instanceof ArrayBuffer) { + return new File([attachment.file], attachment.filename, { type: mimeType }); + } + + throw new Error('Unsupported attachment file type.'); + } + + private async _streamToBlob( + stream: import('stream').Readable | ReadableStream, + filename: string, + mimeType?: string, + ): Promise { + if (typeof window === 'undefined' && stream instanceof (await import('stream')).Readable) { + return new Promise((resolve, reject) => { + const chunks: Uint8Array[] = []; + + stream.on('data', chunk => chunks.push(chunk)); + stream.on('end', () => { + const blob = new Blob(chunks, { type: mimeType }); + + resolve(new File([blob], filename, { type: mimeType })); + }); + stream.on('error', reject); + }); + } + + if (stream instanceof ReadableStream) { + const reader = stream.getReader(); + const chunks: Uint8Array[] = []; + + let done = false; + + while (!done) { + // eslint-disable-next-line no-await-in-loop + const { value, done: streamDone } = await reader.read(); + + if (value) chunks.push(value); + done = streamDone; + } + + const blob = new Blob(chunks, { type: mimeType }); + + return new File([blob], filename, { type: mimeType }); + } + + throw new Error('Unsupported stream type.'); + } } diff --git a/src/version2/parameters/addAttachment.ts b/src/version2/parameters/addAttachment.ts index 5caab857b..7a0682f58 100644 --- a/src/version2/parameters/addAttachment.ts +++ b/src/version2/parameters/addAttachment.ts @@ -1,3 +1,5 @@ +import type { Readable } from 'node:stream'; + /** * Represents an attachment to be added to an issue. * @@ -35,7 +37,7 @@ export interface Attachment { * const fileContent = fs.readFileSync('./document.pdf'); * ``` */ - file: Buffer | ReadableStream | string | Blob | File; + file: Buffer | ReadableStream | Readable | string | Blob | File; /** * Optional MIME type of the attachment. Example values include: diff --git a/src/version3/issueAttachments.ts b/src/version3/issueAttachments.ts index 8da71b34a..f0cf74cb3 100644 --- a/src/version3/issueAttachments.ts +++ b/src/version3/issueAttachments.ts @@ -1,4 +1,5 @@ import { FormData, File } from 'formdata-node'; +import type { Mime } from 'mime' with { 'resolution-mode': 'import' }; import * as Models from './models'; import * as Parameters from './parameters'; import { Client } from '../clients'; @@ -437,14 +438,24 @@ export class IssueAttachments { const { default: mime } = await import('mime'); - attachments.forEach(attachment => { - const mimeType = attachment.mimeType ?? (mime.getType(attachment.filename) || undefined); - const file = Buffer.isBuffer(attachment.file) - ? new File([attachment.file], attachment.filename, { type: mimeType }) - : attachment.file; + let Readable: typeof import('stream').Readable | undefined; + + if (typeof window === 'undefined') { + const { Readable: NodeReadable } = await import('stream'); + + Readable = NodeReadable; + } + + // eslint-disable-next-line no-restricted-syntax + for await (const attachment of attachments) { + const file = await this._convertToFile(attachment, mime, Readable); + + if (!(file instanceof File || file instanceof Blob)) { + throw new Error(`Unsupported file type for attachment: ${typeof file}`); + } formData.append('file', file, attachment.filename); - }); + } const config: RequestConfig = { url: `/rest/api/3/issue/${parameters.issueIdOrKey}/attachments`, @@ -460,4 +471,75 @@ export class IssueAttachments { return this.client.sendRequest(config, callback); } + + private async _convertToFile( + attachment: Parameters.Attachment, + mime: Mime, + Readable?: typeof import('stream').Readable, + ): Promise { + const mimeType = attachment.mimeType ?? (mime.getType(attachment.filename) || undefined); + + if (attachment.file instanceof Blob || attachment.file instanceof File) { + return attachment.file; + } + + if (typeof attachment.file === 'string') { + return new File([attachment.file], attachment.filename, { type: mimeType }); + } + + if (Readable && attachment.file instanceof Readable) { + return this._streamToBlob(attachment.file, attachment.filename, mimeType); + } + + if (attachment.file instanceof ReadableStream) { + return this._streamToBlob(attachment.file, attachment.filename, mimeType); + } + + if (ArrayBuffer.isView(attachment.file) || attachment.file instanceof ArrayBuffer) { + return new File([attachment.file], attachment.filename, { type: mimeType }); + } + + throw new Error('Unsupported attachment file type.'); + } + + private async _streamToBlob( + stream: import('stream').Readable | ReadableStream, + filename: string, + mimeType?: string, + ): Promise { + if (typeof window === 'undefined' && stream instanceof (await import('stream')).Readable) { + return new Promise((resolve, reject) => { + const chunks: Uint8Array[] = []; + + stream.on('data', chunk => chunks.push(chunk)); + stream.on('end', () => { + const blob = new Blob(chunks, { type: mimeType }); + + resolve(new File([blob], filename, { type: mimeType })); + }); + stream.on('error', reject); + }); + } + + if (stream instanceof ReadableStream) { + const reader = stream.getReader(); + const chunks: Uint8Array[] = []; + + let done = false; + + while (!done) { + // eslint-disable-next-line no-await-in-loop + const { value, done: streamDone } = await reader.read(); + + if (value) chunks.push(value); + done = streamDone; + } + + const blob = new Blob(chunks, { type: mimeType }); + + return new File([blob], filename, { type: mimeType }); + } + + throw new Error('Unsupported stream type.'); + } } diff --git a/src/version3/parameters/addAttachment.ts b/src/version3/parameters/addAttachment.ts index e372d3896..d30367c87 100644 --- a/src/version3/parameters/addAttachment.ts +++ b/src/version3/parameters/addAttachment.ts @@ -1,3 +1,5 @@ +import type { Readable } from 'node:stream'; + /** * Represents an attachment to be added to an issue. * @@ -35,7 +37,7 @@ export interface Attachment { * const fileContent = fs.readFileSync('./document.pdf'); * ``` */ - file: Buffer | ReadableStream | string | Blob | File; + file: Buffer | ReadableStream | Readable | string | Blob | File; /** * Optional MIME type of the attachment. Example values include: diff --git a/tests/integration/version2/issueAttachments.test.ts b/tests/integration/version2/issueAttachments.test.ts index 195d035a8..8b5b22724 100644 --- a/tests/integration/version2/issueAttachments.test.ts +++ b/tests/integration/version2/issueAttachments.test.ts @@ -1,8 +1,9 @@ -import * as fs from 'fs'; +import * as fs from 'node:fs'; import { afterAll, beforeAll, test } from 'vitest'; import type { Attachment, Issue } from '@jirajs/version2/models'; import { Constants } from '@tests/integration/constants'; import { cleanupEnvironment, getVersion2Client, prepareEnvironment } from '@tests/integration/utils'; +import { Readable } from 'node:stream'; const client = getVersion2Client({ noCheckAtlassianToken: true }); @@ -62,6 +63,40 @@ test.sequential('should add attachment with custom MIME type', async ({ expect } expect(customAttachment[0].mimeType).toBe(customMimeType); }); +test.sequential('should add attachment with ReadableStream', async ({ expect }) => { + const readableStream = Readable.from(['This is a test content for ReadableStream.']); + + attachments = await client.issueAttachments.addAttachment({ + issueIdOrKey: issue.key, + attachment: { + filename: 'readableStreamAttachment.txt', + file: readableStream, + }, + }); + + expect(!!attachments).toBeTruthy(); + expect(attachments[0].filename).toBe('readableStreamAttachment.txt'); + expect(attachments[0].mimeType).toBe('text/plain'); +}); + +test.sequential('should add attachment with fs.createReadStream', async ({ expect }) => { + const customMimeType = 'application/typescript'; + const fileStream = fs.createReadStream('./tests/integration/version2/issueAttachments.test.ts'); + + attachments = await client.issueAttachments.addAttachment({ + issueIdOrKey: issue.key, + attachment: { + filename: 'fsReadStreamAttachment.ts', + file: fileStream, + mimeType: customMimeType, + }, + }); + + expect(!!attachments).toBeTruthy(); + expect(attachments[0].filename).toBe('fsReadStreamAttachment.ts'); + expect(attachments[0].mimeType).toBe(customMimeType); +}); + test.sequential('should getAttachmentContent', async ({ expect }) => { const content = await client.issueAttachments.getAttachmentContent({ id: attachments[0].id }); diff --git a/tests/integration/version3/issueAttachments.test.ts b/tests/integration/version3/issueAttachments.test.ts index 4a2f21043..863e41131 100644 --- a/tests/integration/version3/issueAttachments.test.ts +++ b/tests/integration/version3/issueAttachments.test.ts @@ -3,6 +3,7 @@ import { afterAll, beforeAll, test } from 'vitest'; import type { Attachment, Issue } from '@jirajs/version3/models'; import { Constants } from '@tests/integration/constants'; import { cleanupEnvironment, getVersion3Client, prepareEnvironment } from '@tests/integration/utils'; +import { Readable } from 'node:stream'; const client = getVersion3Client({ noCheckAtlassianToken: true }); @@ -62,6 +63,40 @@ test.sequential('should add attachment with custom MIME type', async ({ expect } expect(customAttachment[0].mimeType).toBe(customMimeType); }); +test.sequential('should add attachment with ReadableStream', async ({ expect }) => { + const readableStream = Readable.from(['This is a test content for ReadableStream.']); + + attachments = await client.issueAttachments.addAttachment({ + issueIdOrKey: issue.key, + attachment: { + filename: 'readableStreamAttachment.txt', + file: readableStream, + }, + }); + + expect(!!attachments).toBeTruthy(); + expect(attachments[0].filename).toBe('readableStreamAttachment.txt'); + expect(attachments[0].mimeType).toBe('text/plain'); +}); + +test.sequential('should add attachment with fs.createReadStream', async ({ expect }) => { + const customMimeType = 'application/typescript'; + const fileStream = fs.createReadStream('./tests/integration/version2/issueAttachments.test.ts'); + + attachments = await client.issueAttachments.addAttachment({ + issueIdOrKey: issue.key, + attachment: { + filename: 'fsReadStreamAttachment.ts', + file: fileStream, + mimeType: customMimeType, + }, + }); + + expect(!!attachments).toBeTruthy(); + expect(attachments[0].filename).toBe('fsReadStreamAttachment.ts'); + expect(attachments[0].mimeType).toBe(customMimeType); +}); + test.sequential('should getAttachmentContent', async ({ expect }) => { const content = await client.issueAttachments.getAttachmentContent({ id: attachments[0].id });