diff --git a/README.md b/README.md index 5276496c..ec5e6aef 100644 --- a/README.md +++ b/README.md @@ -1697,4 +1697,4 @@ EXAMPLES # Current Limitations -- We currently have a 40GB size upload limitation per file for both, CLI and WebDAV +- Upload size limits depend on the account's plan tier. Since the CLI is only available to Ultimate plan users, we currently have a 100GB size upload limitation per file for both, CLI and WebDAV diff --git a/WEBDAV.md b/WEBDAV.md index 0636ebce..a561cb44 100644 --- a/WEBDAV.md +++ b/WEBDAV.md @@ -15,7 +15,7 @@ No plain data is being sent or is being pulled from the Internxt servers, you ca ![image](https://raw.githubusercontent.com/internxt/cli/main/public/webdav-how-it-works.png) -_We currently have a 40GB size upload limitation per file for both, CLI and WebDAV_ +_Upload size limits depend on the account's plan tier. Since the CLI is only available to Ultimate plan users, we currently have a 100GB size upload limitation per file for both, CLI and WebDAV_ ## Officially supported WebDav clients diff --git a/package-lock.json b/package-lock.json index a50b373c..5a28d628 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,9 +14,9 @@ "@inquirer/prompts": "8.5.2", "@internxt/inxt-js": "3.3.5", "@internxt/lib": "1.5.2", - "@internxt/sdk": "1.19.0", + "@internxt/sdk": "1.20.1", "@oclif/core": "4.13.3", - "@oclif/plugin-autocomplete": "3.2.55", + "@oclif/plugin-autocomplete": "3.2.56", "axios": "1.19.0", "better-sqlite3": "12.11.1", "bip39": "3.1.0", @@ -26,11 +26,11 @@ "dotenv": "17.4.2", "express": "5.2.1", "express-async-handler": "1.2.0", - "fast-xml-builder": "1.3.0", + "fast-xml-builder": "1.3.1", "fast-xml-parser": "5.10.1", "hash-wasm": "4.12.0", "mime-types": "3.0.2", - "open": "11.0.0", + "open": "11.0.1", "openpgp": "6.3.1", "otpauth": "9.5.1", "pm2": "7.0.3", @@ -51,15 +51,15 @@ "@types/cli-progress": "^3.11.6", "@types/express": "^5.0.6", "@types/mime-types": "^3.0.1", - "@types/node": "^26.1.2", + "@types/node": "^26.2.0", "@types/range-parser": "^1.2.7", "@vitest/coverage-istanbul": "^4.1.10", "@vitest/spy": "^4.1.10", - "eslint": "^10.8.0", + "eslint": "^10.8.1", "husky": "^9.1.7", "lint-staged": "^17.3.0", "nodemon": "^3.1.14", - "oclif": "^4.23.29", + "oclif": "^4.23.30", "prettier": "^3.9.6", "rimraf": "^6.1.3", "sql.js": "^1.14.1", @@ -1895,9 +1895,9 @@ "license": "MIT" }, "node_modules/@internxt/sdk": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/@internxt/sdk/-/sdk-1.19.0.tgz", - "integrity": "sha512-u5s7+FSUwZSjW4vhLAnfnjRtqEdAYnxUmqNBY6YC3chap27e3kJUM9jfJhpxRNckmxYbaUrLG8l8wfo/dlWe/g==", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/@internxt/sdk/-/sdk-1.20.1.tgz", + "integrity": "sha512-Xfgz/dLncfQ81ytx+y69OVF+IJwpxxQPIIF/VdHWWQy3W5WiN3J6CmMRAb8J7AOPDy2UPVhVq4KM7PyOdP+QxQ==", "license": "MIT", "dependencies": { "axios": "^1.16.0" @@ -2036,9 +2036,9 @@ } }, "node_modules/@oclif/plugin-autocomplete": { - "version": "3.2.55", - "resolved": "https://registry.npmjs.org/@oclif/plugin-autocomplete/-/plugin-autocomplete-3.2.55.tgz", - "integrity": "sha512-3s3xLlfHarctGcZt8HlbDeEIoOCcsjAzHvvq8pHTy8LpQNag78DwXgvUwFE/RtTme8ahTZEBteD5IPN/B2NTgQ==", + "version": "3.2.56", + "resolved": "https://registry.npmjs.org/@oclif/plugin-autocomplete/-/plugin-autocomplete-3.2.56.tgz", + "integrity": "sha512-P96b6RZvQatlJ6pX6sgIFOQhgX2tFVhgSfw53GT/MajecCQNFGVqagJKo2CfcPsacDpuFuPRT6zji8h1oaqtrA==", "license": "MIT", "dependencies": { "@oclif/core": "^4", @@ -3389,9 +3389,9 @@ } }, "node_modules/@types/node": { - "version": "26.1.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.2.tgz", - "integrity": "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==", + "version": "26.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz", + "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==", "devOptional": true, "license": "MIT", "dependencies": { @@ -5599,9 +5599,9 @@ } }, "node_modules/eslint": { - "version": "10.8.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.0.tgz", - "integrity": "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==", + "version": "10.8.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.1.tgz", + "integrity": "sha512-wqA7W2jbsC/BnV9Iv1UZpKVFkO1AdNoSmYW8NWG4HNOBbkAMvIqDZ27pI2f07dqn583NcIC44ckjAcOXDL1QbQ==", "dev": true, "license": "MIT", "workspaces": [ @@ -5946,9 +5946,9 @@ } }, "node_modules/fast-xml-builder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.3.0.tgz", - "integrity": "sha512-F74cZEdCvuw9P41GAC3rod4X04jjWGM1JPEv/GWSqFTWLsdyMSBMBMlm9Hk3GLBgLBbdBNY8yee0pQh2RBVESQ==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.3.1.tgz", + "integrity": "sha512-pIM/1n3ntFXKYrUZwW7QCK0gAW7XY+wzj1YMIV3tLDvPj/V+zTGJK5e3/4WJfwj0qWw2ElNXiTixda/R+3YSug==", "funding": [ { "type": "github", @@ -8474,9 +8474,9 @@ } }, "node_modules/oclif": { - "version": "4.23.29", - "resolved": "https://registry.npmjs.org/oclif/-/oclif-4.23.29.tgz", - "integrity": "sha512-pUekmMh+wwIbFq3xX1EfkgZ7RuYuuSbdhJiB+20zqSfGE8Tt9T+K9br1/vmsaIOxfDo0Dw3jxLBk4JZOMFEMeQ==", + "version": "4.23.30", + "resolved": "https://registry.npmjs.org/oclif/-/oclif-4.23.30.tgz", + "integrity": "sha512-x8sMRv606XqZAWEfGh981PP2JM4G2CKt4/z2KDntoNHljD8slfJ5sHaDFUQya9w6W8xOJDMFF3UaLAFFYHo13w==", "dev": true, "license": "MIT", "dependencies": { @@ -8712,17 +8712,17 @@ } }, "node_modules/open": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/open/-/open-11.0.0.tgz", - "integrity": "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==", + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/open/-/open-11.0.1.tgz", + "integrity": "sha512-NzwMUB6C1D0+Kd+9iMS/H4k+Ck3cTX6Ckyfr/gAGlmvSE1LUQZnEZvWBi4PYmMwH/S5SMeTXnE+9uAz8uF+pWw==", "license": "MIT", "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", - "powershell-utils": "^0.1.0", - "wsl-utils": "^0.3.0" + "powershell-utils": "^0.2.0", + "wsl-utils": "^1.0.0" }, "engines": { "node": ">=20" @@ -9238,9 +9238,9 @@ } }, "node_modules/powershell-utils": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz", - "integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.2.0.tgz", + "integrity": "sha512-ZlsFlG7MtSFCoc5xreOvBAozCJ6Pf06opgJjh9ONEv418xpZSAzNjstD36C6+JwOnfSqOW/9uDkqKjezTdxZhw==", "license": "MIT", "engines": { "node": ">=20" @@ -12055,9 +12055,9 @@ } }, "node_modules/wsl-utils": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.3.1.tgz", - "integrity": "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-1.0.0.tgz", + "integrity": "sha512-Hl0ZOAs672vg+06kfujwRhoS6/jehvULrlFkuF2dRu6pHgA8U06h3xqNIqNNU1LTXPcedxByAR4GS6pwQK0mgA==", "license": "MIT", "dependencies": { "is-wsl": "^3.1.0", @@ -12085,6 +12085,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/wsl-utils/node_modules/powershell-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz", + "integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/xml-naming": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.3.0.tgz", diff --git a/package.json b/package.json index 14e6cad3..99358bf3 100644 --- a/package.json +++ b/package.json @@ -40,9 +40,9 @@ "@inquirer/prompts": "8.5.2", "@internxt/inxt-js": "3.3.5", "@internxt/lib": "1.5.2", - "@internxt/sdk": "1.19.0", + "@internxt/sdk": "1.20.1", "@oclif/core": "4.13.3", - "@oclif/plugin-autocomplete": "3.2.55", + "@oclif/plugin-autocomplete": "3.2.56", "axios": "1.19.0", "better-sqlite3": "12.11.1", "bip39": "3.1.0", @@ -52,11 +52,11 @@ "dotenv": "17.4.2", "express": "5.2.1", "express-async-handler": "1.2.0", - "fast-xml-builder": "1.3.0", + "fast-xml-builder": "1.3.1", "fast-xml-parser": "5.10.1", "hash-wasm": "4.12.0", "mime-types": "3.0.2", - "open": "11.0.0", + "open": "11.0.1", "openpgp": "6.3.1", "otpauth": "9.5.1", "pm2": "7.0.3", @@ -74,15 +74,15 @@ "@types/cli-progress": "^3.11.6", "@types/express": "^5.0.6", "@types/mime-types": "^3.0.1", - "@types/node": "^26.1.2", + "@types/node": "^26.2.0", "@types/range-parser": "^1.2.7", "@vitest/coverage-istanbul": "^4.1.10", "@vitest/spy": "^4.1.10", - "eslint": "^10.8.0", + "eslint": "^10.8.1", "husky": "^9.1.7", "lint-staged": "^17.3.0", "nodemon": "^3.1.14", - "oclif": "^4.23.29", + "oclif": "^4.23.30", "prettier": "^3.9.6", "rimraf": "^6.1.3", "sql.js": "^1.14.1", diff --git a/src/services/database/drive-item/drive-item.model.ts b/src/services/database/drive-item/drive-item.model.ts index 67ed721e..a6cd893c 100644 --- a/src/services/database/drive-item/drive-item.model.ts +++ b/src/services/database/drive-item/drive-item.model.ts @@ -1,11 +1,12 @@ import { DriveItemAttributes } from './drive-item.attributes'; -import { Column, Entity, PrimaryColumn } from 'typeorm'; +import { Column, Entity, Index, PrimaryColumn } from 'typeorm'; @Entity('drive_item') export class DriveItemModel implements DriveItemAttributes { @PrimaryColumn({ nullable: false, type: 'varchar' }) declare uuid: string; + @Index() @Column({ nullable: false, type: 'varchar' }) declare path: string; diff --git a/src/services/database/drive-item/drive-item.repository.ts b/src/services/database/drive-item/drive-item.repository.ts index 44a9e4f3..2e5c8a93 100644 --- a/src/services/database/drive-item/drive-item.repository.ts +++ b/src/services/database/drive-item/drive-item.repository.ts @@ -22,15 +22,27 @@ export class DriveItemRepository { const existingByUuid = new Map(existing.map((e) => [e.uuid, e])); const existingByPath = new Map(existing.map((e) => [e.path, e])); + const itemsToInsert: DriveItemModel[] = []; + const itemsToUpdate: { targetUuid: string; item: DriveItemModel }[] = []; + for (const item of items) { const match = existingByUuid.get(item.uuid) ?? existingByPath.get(item.path); if (match) { - await this.repository.update(match.uuid, item); + itemsToUpdate.push({ targetUuid: match.uuid, item }); } else { - await this.repository.insert(item); + itemsToInsert.push(item); } } + await this.repository.manager.transaction(async (transactionalEntityManager) => { + if (itemsToInsert.length > 0) { + await transactionalEntityManager.insert(DriveItemModel, itemsToInsert); + } + for (const { targetUuid, item } of itemsToUpdate) { + await transactionalEntityManager.update(DriveItemModel, targetUuid, item); + } + }); + return items.map((item) => new DriveItemBD(item)); } catch (error) { ErrorUtils.report(error, { createOrUpdate: items }); diff --git a/src/webdav/middewares/errors.middleware.ts b/src/webdav/middewares/errors.middleware.ts index acd60191..12c3dd7c 100644 --- a/src/webdav/middewares/errors.middleware.ts +++ b/src/webdav/middewares/errors.middleware.ts @@ -3,9 +3,43 @@ import { webdavLogger } from '../../utils/logger.utils'; import { XMLUtils } from '../../utils/xml.utils'; import { ErrorUtils } from '../../utils/errors.utils'; +/** + * SDK errors (AxiosResponseError/AxiosUnknownError) carry the upstream API's response + * body in `data`, which is discarded unless we read it explicitly. + */ +const getErrorDetail = (err: unknown): string | undefined => { + if (typeof err !== 'object' || err === null || !('data' in err)) return undefined; + + const data = (err as { data?: unknown }).data; + if (typeof data !== 'object' || data === null || !('message' in data)) return undefined; + + const message = (data as { message?: unknown }).message; + if (typeof message === 'string' && message.trim().length > 0) return message; + if (Array.isArray(message) && message.length > 0) return message.join(', '); + return undefined; +}; + +/** + * The CLI's own errors (BadRequestError, NotFoundError, ...) expose `statusCode`, + * but errors normalized by @internxt/sdk's HttpClient expose `status` instead. + */ +const getErrorStatusCode = (err: unknown): number | undefined => { + if (typeof err !== 'object' || err === null) return undefined; + + const { statusCode, status } = err as { statusCode?: unknown; status?: unknown }; + if (typeof statusCode === 'number' && !Number.isNaN(statusCode)) return statusCode; + if (typeof status === 'number' && !Number.isNaN(status)) return status; + return undefined; +}; + // eslint-disable-next-line @typescript-eslint/no-unused-vars export const ErrorHandlingMiddleware: ErrorRequestHandler = (err, req, res, _) => { - const message = ErrorUtils.isError(err) ? err.message : 'Something went wrong'; + let message = ErrorUtils.isError(err) ? err.message : 'Something went wrong'; + + const detail = getErrorDetail(err); + if (detail) { + message += ` [${detail}]`; + } if (ErrorUtils.isError(err) && err.stack) { webdavLogger.error(`[ERROR MIDDLEWARE] [${req.method.toUpperCase()} - ${req.url}] ${message}\nStack: ${err.stack}`); @@ -21,10 +55,7 @@ export const ErrorHandlingMiddleware: ErrorRequestHandler = (err, req, res, _) = 'error', ); - let statusCode = 500; - if ('statusCode' in err && !Number.isNaN(err.statusCode)) { - statusCode = err.statusCode; - } + const statusCode = getErrorStatusCode(err) ?? 500; res.set('Content-Type', 'application/xml; charset="utf-8"'); res.status(statusCode).send(errorBodyXML); diff --git a/test/services/network/network-facade.service.test.ts b/test/services/network/network-facade.service.test.ts index 7dd57dbb..31188c90 100644 --- a/test/services/network/network-facade.service.test.ts +++ b/test/services/network/network-facade.service.test.ts @@ -47,6 +47,7 @@ describe('Network Facade Service', () => { vi.spyOn(UsageService.instance, 'fetchLimits').mockResolvedValue({ maxUploadFileSize: null, versioning: { enabled: false, maxFileSize: 0, retentionDays: 0, maxVersions: 0 }, + photosAccess: false, }); await sut.uploadFile({ @@ -73,6 +74,7 @@ describe('Network Facade Service', () => { vi.spyOn(UsageService.instance, 'fetchLimits').mockResolvedValue({ maxUploadFileSize: 1024 * 1024, versioning: { enabled: false, maxFileSize: 0, retentionDays: 0, maxVersions: 0 }, + photosAccess: false, }); await expect(() => @@ -101,6 +103,7 @@ describe('Network Facade Service', () => { vi.spyOn(UsageService.instance, 'fetchLimits').mockResolvedValue({ maxUploadFileSize: null, versioning: { enabled: false, maxFileSize: 0, retentionDays: 0, maxVersions: 0 }, + photosAccess: false, }); await expect(() => @@ -129,6 +132,7 @@ describe('Network Facade Service', () => { vi.spyOn(UsageService.instance, 'fetchLimits').mockResolvedValue({ maxUploadFileSize: 1024 * 1024 * 1024, versioning: { enabled: false, maxFileSize: 0, retentionDays: 0, maxVersions: 0 }, + photosAccess: false, }); await expect(() => @@ -157,6 +161,7 @@ describe('Network Facade Service', () => { vi.spyOn(UsageService.instance, 'fetchLimits').mockResolvedValue({ maxUploadFileSize: 100 * 1024 * 1024, versioning: { enabled: false, maxFileSize: 0, retentionDays: 0, maxVersions: 0 }, + photosAccess: false, }); await sut.uploadFile({ diff --git a/test/utils/upload.utils.test.ts b/test/utils/upload.utils.test.ts index 153b2c5b..6bce10c2 100644 --- a/test/utils/upload.utils.test.ts +++ b/test/utils/upload.utils.test.ts @@ -29,6 +29,7 @@ describe('UploadUtils', () => { vi.spyOn(UsageService.instance, 'fetchLimits').mockResolvedValue({ maxUploadFileSize: 8 * 1024 * 1024 * 1024, versioning: { enabled: false, maxFileSize: 0, retentionDays: 0, maxVersions: 0 }, + photosAccess: false, }); await expect(UploadUtils.checkUploadSizeLimits(5 * 1024 * 1024 * 1024)).resolves.toBeUndefined(); @@ -40,6 +41,7 @@ describe('UploadUtils', () => { vi.spyOn(UsageService.instance, 'fetchLimits').mockResolvedValue({ maxUploadFileSize: limitBytes, versioning: { enabled: false, maxFileSize: 0, retentionDays: 0, maxVersions: 0 }, + photosAccess: false, }); await expect(UploadUtils.checkUploadSizeLimits(sizeBytes)).rejects.toThrow( @@ -53,6 +55,7 @@ describe('UploadUtils', () => { vi.spyOn(UsageService.instance, 'fetchLimits').mockResolvedValue({ maxUploadFileSize: limitBytes, versioning: { enabled: false, maxFileSize: 0, retentionDays: 0, maxVersions: 0 }, + photosAccess: false, }); await expect(UploadUtils.checkUploadSizeLimits(limitBytes)).resolves.toBeUndefined(); @@ -70,6 +73,7 @@ describe('UploadUtils', () => { vi.spyOn(UsageService.instance, 'fetchLimits').mockResolvedValue({ maxUploadFileSize: 200 * 1024 * 1024 * 1024, versioning: { enabled: false, maxFileSize: 0, retentionDays: 0, maxVersions: 0 }, + photosAccess: false, }); await expect(UploadUtils.checkUploadSizeLimits(150 * 1024 * 1024 * 1024)).rejects.toThrow( @@ -82,6 +86,7 @@ describe('UploadUtils', () => { vi.spyOn(UsageService.instance, 'fetchLimits').mockResolvedValue({ maxUploadFileSize: limitBytes, versioning: { enabled: false, maxFileSize: 0, retentionDays: 0, maxVersions: 0 }, + photosAccess: false, }); await expect(UploadUtils.checkUploadSizeLimits(9 * 1024 * 1024 * 1024)).rejects.toThrow( diff --git a/test/webdav/handlers/PUT.handler.test.ts b/test/webdav/handlers/PUT.handler.test.ts index fd5744e1..c33cc9b4 100644 --- a/test/webdav/handlers/PUT.handler.test.ts +++ b/test/webdav/handlers/PUT.handler.test.ts @@ -28,6 +28,7 @@ describe('PUT request handler', () => { vi.spyOn(UsageService.instance, 'fetchLimits').mockResolvedValue({ maxUploadFileSize: null, versioning: { enabled: false, maxFileSize: 0, retentionDays: 0, maxVersions: 0 }, + photosAccess: false, }); sut = new PUTRequestHandler(); @@ -83,6 +84,14 @@ describe('PUT request handler', () => { expect(getAuthDetailsStub).toHaveBeenCalledOnce(); expect(uploadStub).not.toHaveBeenCalled(); expect(createDriveFileStub).toHaveBeenCalledOnce(); + expect(createDriveFileStub).toHaveBeenCalledWith( + expect.objectContaining({ + fileId: undefined, + size: 0, + folderUuid: folderFixture.uuid, + plainName: requestedFileResource.path.name, + }), + ); }); test('when a file is uploaded to an existing folder, then the server stores it', async () => { diff --git a/test/webdav/middlewares/errors.middleware.test.ts b/test/webdav/middlewares/errors.middleware.test.ts index 91537fbd..12f3cef5 100644 --- a/test/webdav/middlewares/errors.middleware.test.ts +++ b/test/webdav/middlewares/errors.middleware.test.ts @@ -3,6 +3,8 @@ import { ErrorHandlingMiddleware } from '../../../src/webdav/middewares/errors.m import { createWebDavRequestFixture, createWebDavResponseFixture } from '../../fixtures/webdav.fixture'; import { BadRequestError, NotFoundError, NotImplementedError } from '../../../src/utils/errors.utils'; import { XMLUtils } from '../../../src/utils/xml.utils'; +import { AxiosResponseError, AxiosUnknownError } from '@internxt/sdk/dist/shared/types/errors'; +import { AxiosError } from 'axios'; describe('Error handling middleware', () => { test('when a not found error occurs, then the server responds with a 404 status', () => { @@ -104,4 +106,67 @@ describe('Error handling middleware', () => { ), ); }); + + test('when a Drive API request fails with a response, then the server forwards its status and detail', () => { + const error = new AxiosResponseError('Request failed with status code 400', 'POST /files', { + status: 400, + data: { message: 'fileId must not be provided when size is 0', statusCode: 400 }, + headers: {}, + statusText: 'Bad Request', + // @ts-expect-error partial AxiosResponse fixture, only the fields read by AxiosResponseError are needed + config: {}, + }); + const res = createWebDavResponseFixture({ + status: vi.fn().mockReturnValue({ send: vi.fn() }), + }); + const req = createWebDavRequestFixture({ + method: 'PUT', + url: '/test/empty.bin', + }); + + ErrorHandlingMiddleware(error, req, res, () => {}); + + expect(res.status).toHaveBeenCalledWith(400); + expect(res.send).toHaveBeenCalledWith( + XMLUtils.toWebDavXML( + { + [XMLUtils.addDefaultNamespace('responsedescription')]: + 'Request failed with status code 400 [fileId must not be provided when size is 0]', + }, + {}, + 'error', + ), + ); + }); + + test('when a Drive API request fails without a response, then the server responds with the normalized status and no detail suffix', () => { + const axiosError = { + message: 'Network Error', + code: 'ECONNABORTED', + // no `request` -> AxiosUnknownError normalizes this to status 400 + request: undefined, + config: {}, + } as AxiosError; + const error = new AxiosUnknownError('Network Error', 'POST /files', axiosError); + const res = createWebDavResponseFixture({ + status: vi.fn().mockReturnValue({ send: vi.fn() }), + }); + const req = createWebDavRequestFixture({ + method: 'PUT', + url: '/test/empty.bin', + }); + + ErrorHandlingMiddleware(error, req, res, () => {}); + + expect(res.status).toHaveBeenCalledWith(400); + expect(res.send).toHaveBeenCalledWith( + XMLUtils.toWebDavXML( + { + [XMLUtils.addDefaultNamespace('responsedescription')]: 'Network Error', + }, + {}, + 'error', + ), + ); + }); });