From 509a29e8dd7fd355b16bebe6874e3b00721aa036 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 6 Sep 2024 15:45:21 +0200 Subject: [PATCH 1/9] Add initial password-protected zip export support --- ...vidual-vault-export.service.abstraction.ts | 2 +- .../individual-vault-export.service.ts | 36 +++++++++------- .../src/services/vault-export.service.ts | 2 +- .../src/components/export.component.html | 42 +++++++++++++++++++ .../src/components/export.component.ts | 5 ++- package-lock.json | 17 +++++++- package.json | 1 + 7 files changed, 86 insertions(+), 19 deletions(-) diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.abstraction.ts b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.abstraction.ts index e792bef71b37..aed486378fa2 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.abstraction.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.abstraction.ts @@ -2,5 +2,5 @@ import { ExportFormat } from "./vault-export.service.abstraction"; export abstract class IndividualVaultExportServiceAbstraction { getExport: (format: ExportFormat) => Promise; - getPasswordProtectedExport: (password: string) => Promise; + getPasswordProtectedExport: (format: ExportFormat, password: string) => Promise; } diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts index 0dcb5927bc01..952251b672ff 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts @@ -1,4 +1,4 @@ -import * as JSZip from "jszip"; +import { BlobWriter, ZipWriter, TextReader, Uint8ArrayReader } from "@zip.js/zip.js"; import * as papa from "papaparse"; import { PinServiceAbstraction } from "@bitwarden/auth/common"; @@ -45,24 +45,28 @@ export class IndividualVaultExportService if (format === "encrypted_json") { return this.getEncryptedExport(); } else if (format === "zip") { - return this.getDecryptedExportZip(); + return this.getExportZip(null); } - return this.getDecryptedExport(format); + return this.getExportZip("abc"); } - async getPasswordProtectedExport(password: string): Promise { - const clearText = (await this.getExport("json")) as string; - return this.buildPasswordExport(clearText, password); + async getPasswordProtectedExport(format: ExportFormat, password: string): Promise { + if (format == "encrypted_json") { + const clearText = (await this.getExport("json")) as string; + return await this.buildPasswordExport(clearText, password); + } else if (format === "zip") { + return await this.getExportZip(password); + } else { + throw new Error("CSV does not support password protected export"); + } } - async getDecryptedExportZip(): Promise { - const zip = new JSZip(); + async getExportZip(password?: string): Promise { + const blobWriter = new BlobWriter("application/zip"); + const zipWriter = new ZipWriter(blobWriter, { bufferedWrite: true, password }); - // ciphers const dataJson = await this.getDecryptedExport("json"); - zip.file("data.json", dataJson); - - const attachmentsFolder = zip.folder("attachments"); + await zipWriter.add("data.json", new TextReader(dataJson)); // attachments for (const cipher of await this.cipherService.getAllDecrypted()) { @@ -70,7 +74,6 @@ export class IndividualVaultExportService continue; } - const cipherFolder = attachmentsFolder.folder(cipher.id); for (const attachment of cipher.attachments) { const response = await fetch(new Request(attachment.url, { cache: "no-store" })); const encBuf = await EncArrayBuffer.fromResponse(response); @@ -79,11 +82,14 @@ export class IndividualVaultExportService ? attachment.key : await this.cryptoService.getOrgKey(cipher.organizationId); const decBuf = await this.cryptoService.decryptFromBytes(encBuf, key); - cipherFolder.file(attachment.fileName, decBuf); + await zipWriter.add(`${cipher.id}/${attachment.fileName}`, new Uint8ArrayReader(decBuf)); } } - return zip.generateAsync({ type: "blob" }); + await zipWriter.close(); + const zipFileBlob = await blobWriter.getData(); + + return zipFileBlob; } private async getDecryptedExport(format: "json" | "csv"): Promise { diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.ts b/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.ts index b77ee4d41c5e..7da81a299fbd 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.ts @@ -17,7 +17,7 @@ export class VaultExportService implements VaultExportServiceAbstraction { throw new Error("CSV does not support password protected export"); } - return this.individualVaultExportService.getPasswordProtectedExport(password); + return this.individualVaultExportService.getPasswordProtectedExport(format, password); } return this.individualVaultExportService.getExport(format); } diff --git a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.html b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.html index 07606add8b43..e603c4e6d2f5 100644 --- a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.html +++ b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.html @@ -98,4 +98,46 @@ + + +
+ + {{ "filePassword" | i18n }} + + + {{ "exportPasswordDescription" | i18n }} + + + +
+ + {{ "confirmFilePassword" | i18n }} + + + +
diff --git a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts index 7ae741a8a93e..a338685704c2 100644 --- a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts +++ b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts @@ -405,7 +405,10 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { this.exportForm.get("confirmFilePassword").reset(); this.exportForm.get("filePassword").reset(); - if (this.encryptedFormat && this.fileEncryptionType == EncryptedExportType.FileEncrypted) { + if ( + (this.encryptedFormat && this.fileEncryptionType == EncryptedExportType.FileEncrypted) || + this.format === "zip" + ) { this.exportForm.controls.filePassword.enable(); this.exportForm.controls.confirmFilePassword.enable(); } else { diff --git a/package-lock.json b/package-lock.json index 0362185a1a7d..b4d1f495193a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "@microsoft/signalr": "8.0.7", "@microsoft/signalr-protocol-msgpack": "8.0.7", "@ng-select/ng-select": "11.2.0", + "@zip.js/zip.js": "^2.7.52", "argon2": "0.40.1", "argon2-browser": "1.18.0", "big-integer": "1.6.51", @@ -249,7 +250,10 @@ }, "apps/web": { "name": "@bitwarden/web-vault", - "version": "2024.7.3" + "version": "2024.7.3", + "dependencies": { + "@zip.js/zip.js": "^2.7.52" + } }, "libs/admin-console": { "name": "@bitwarden/admin-console", @@ -13331,6 +13335,17 @@ "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==" }, + "node_modules/@zip.js/zip.js": { + "version": "2.7.52", + "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.7.52.tgz", + "integrity": "sha512-+5g7FQswvrCHwYKNMd/KFxZSObctLSsQOgqBSi0LzwHo3li9Eh1w5cF5ndjQw9Zbr3ajVnd2+XyiX85gAetx1Q==", + "license": "BSD-3-Clause", + "engines": { + "bun": ">=0.7.0", + "deno": ">=1.0.0", + "node": ">=16.5.0" + } + }, "node_modules/7zip-bin": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", diff --git a/package.json b/package.json index 2af0969680a0..870b43966703 100644 --- a/package.json +++ b/package.json @@ -165,6 +165,7 @@ "@microsoft/signalr": "8.0.7", "@microsoft/signalr-protocol-msgpack": "8.0.7", "@ng-select/ng-select": "11.2.0", + "@zip.js/zip.js": "^2.7.52", "argon2": "0.40.1", "argon2-browser": "1.18.0", "big-integer": "1.6.51", From b80cbfdbd989d7a8a1ffef3ba57b9f78bc8a93d1 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 6 Sep 2024 16:45:24 +0200 Subject: [PATCH 2/9] Add optional password protection --- apps/web/src/locales/en/messages.json | 6 ++ .../src/components/export.component.html | 59 +++++++++++-------- .../src/components/export.component.ts | 14 ++++- 3 files changed, 51 insertions(+), 28 deletions(-) diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index fd2924badb4e..3f1468f741dc 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -1254,6 +1254,12 @@ "passwordProtectedOptionDescription": { "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." }, + "zipPasswordProtectedOptionTitle": { + "message": "Encrypt exported zip archive" + }, + "zipPasswordProtectedOptionDescription": { + "message": "Set a file password for the zip archive to encrypt the exported vault items and attachments, and import it to any Bitwarden account using the password for decryption." + }, "exportTypeHeading": { "message": "Export type" }, diff --git a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.html b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.html index e603c4e6d2f5..2415256ce986 100644 --- a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.html +++ b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.html @@ -100,15 +100,42 @@ -
+ + {{ "zipPasswordProtectedOptionTitle" | i18n }} + + {{ "zipPasswordProtectedOptionDescription" | i18n }} + + +
+ + {{ "filePassword" | i18n }} + + + {{ "exportPasswordDescription" | i18n }} + + + +
- {{ "filePassword" | i18n }} + {{ "confirmFilePassword" | i18n }} - {{ "exportPasswordDescription" | i18n }} - - -
- - {{ "confirmFilePassword" | i18n }} - - - +
diff --git a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts index a338685704c2..3638c0af9e1b 100644 --- a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts +++ b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts @@ -31,6 +31,7 @@ import { BitSubmitDirective, ButtonModule, CalloutModule, + CheckboxModule, DialogService, FormFieldModule, IconButtonModule, @@ -50,6 +51,7 @@ import { ExportScopeCalloutComponent } from "./export-scope-callout.component"; standalone: true, imports: [ CommonModule, + CheckboxModule, ReactiveFormsModule, JslibModule, FormFieldModule, @@ -137,6 +139,7 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { }, ], format: ["json", Validators.required], + passwordProtected: [true], secret: [""], filePassword: ["", Validators.required], confirmFilePassword: ["", Validators.required], @@ -185,6 +188,7 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { merge( this.exportForm.get("format").valueChanges, this.exportForm.get("fileEncryptionType").valueChanges, + this.exportForm.get("passwordProtected").valueChanges, ) .pipe(startWith(0), takeUntil(this.destroy$)) .subscribe(() => this.adjustValidators()); @@ -200,6 +204,7 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { return; } else { this.formatOptions.push({ name: ".zip (With Attachments)", value: "zip" }); + this.exportForm.setValue({ format: "zip" }); } this.organizations$ = combineLatest({ @@ -256,6 +261,10 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { ); } + get zipPasswordProtected() { + return this.exportForm.get("passwordProtected").value; + } + protected async doExport() { try { const data = await this.getExportData(); @@ -313,7 +322,7 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { private async verifyUser(): Promise { let confirmDescription = "exportWarningDesc"; - if (this.isFileEncryptedExport) { + if (this.isFileEncryptedExport || (this.format === "zip" && this.zipPasswordProtected)) { confirmDescription = "fileEncryptedExportWarningDesc"; } else if (this.isAccountEncryptedExport) { confirmDescription = "encExportKeyWarningDesc"; @@ -407,13 +416,14 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { if ( (this.encryptedFormat && this.fileEncryptionType == EncryptedExportType.FileEncrypted) || - this.format === "zip" + (this.format === "zip" && this.zipPasswordProtected) ) { this.exportForm.controls.filePassword.enable(); this.exportForm.controls.confirmFilePassword.enable(); } else { this.exportForm.controls.filePassword.disable(); this.exportForm.controls.confirmFilePassword.disable(); + this.exportForm.setValue({ filePassword: "", confirmFilePassword: "" }); } } From 5257eb2315d60f50fef0d64bbf5087d124004646 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 9 Sep 2024 12:49:58 +0200 Subject: [PATCH 3/9] Fix tests --- .../trial-initiation.component.spec.ts | 5 +++ .../vault-banners.component.spec.ts | 4 +++ apps/web/src/locales/en/messages.json | 36 +++++++++---------- ...vidual-vault-export.service.abstraction.ts | 7 ++-- .../individual-vault-export.service.spec.ts | 31 ++++++++++++---- .../individual-vault-export.service.ts | 29 +++++++++------ .../vault-export.service.abstraction.ts | 2 +- .../src/services/vault-export.service.spec.ts | 18 ++++++++-- .../src/services/vault-export.service.ts | 2 +- 9 files changed, 91 insertions(+), 43 deletions(-) diff --git a/apps/web/src/app/auth/trial-initiation/trial-initiation.component.spec.ts b/apps/web/src/app/auth/trial-initiation/trial-initiation.component.spec.ts index a7916ae946d9..fa7e8904af36 100644 --- a/apps/web/src/app/auth/trial-initiation/trial-initiation.component.spec.ts +++ b/apps/web/src/app/auth/trial-initiation/trial-initiation.component.spec.ts @@ -1,3 +1,8 @@ +import { TransformStream, WritableStream, ReadableStream } from "node:stream/web"; +globalThis.TransformStream = TransformStream; +globalThis.WritableStream = WritableStream; +globalThis.ReadableStream = ReadableStream; + import { StepperSelectionEvent } from "@angular/cdk/stepper"; import { TitleCasePipe } from "@angular/common"; import { NO_ERRORS_SCHEMA } from "@angular/core"; diff --git a/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.spec.ts b/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.spec.ts index 5fdac63e9327..7e1b7dc4c51e 100644 --- a/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.spec.ts +++ b/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.spec.ts @@ -1,3 +1,7 @@ +// zip.js needs these globals in node +import { TransformStream } from "node:stream/web"; +globalThis.TransformStream = TransformStream; + import { ComponentFixture, TestBed } from "@angular/core/testing"; import { By } from "@angular/platform-browser"; import { RouterTestingModule } from "@angular/router/testing"; diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 3f1468f741dc..216dd2e1d5fb 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -942,8 +942,8 @@ "unexpectedError": { "message": "An unexpected error has occurred." }, - "expirationDateError" : { - "message":"Please select an expiration date that is in the future." + "expirationDateError": { + "message": "Please select an expiration date that is in the future." }, "emailAddress": { "message": "Email address" @@ -951,8 +951,8 @@ "yourVaultIsLockedV2": { "message": "Your vault is locked" }, - "uuid":{ - "message" : "UUID" + "uuid": { + "message": "UUID" }, "unlock": { "message": "Unlock" @@ -1188,10 +1188,10 @@ "copyUuid": { "message": "Copy UUID" }, - "errorRefreshingAccessToken":{ + "errorRefreshingAccessToken": { "message": "Access Token Refresh Error" }, - "errorRefreshingAccessTokenDesc":{ + "errorRefreshingAccessTokenDesc": { "message": "No refresh token or API keys found. Please try logging out and logging back in." }, "warning": { @@ -1258,7 +1258,7 @@ "message": "Encrypt exported zip archive" }, "zipPasswordProtectedOptionDescription": { - "message": "Set a file password for the zip archive to encrypt the exported vault items and attachments, and import it to any Bitwarden account using the password for decryption." + "message": "Set a file password to encrypt the zip export and import it to any Bitwarden account using the password for decryption." }, "exportTypeHeading": { "message": "Export type" @@ -4912,7 +4912,7 @@ "youNeedApprovalFromYourAdminToTrySecretsManager": { "message": "You need approval from your administrator to try Secrets Manager." }, - "smAccessRequestEmailSent" : { + "smAccessRequestEmailSent": { "message": "Access request for secrets manager email sent to admins." }, "requestAccessSMDefaultEmailContent": { @@ -4921,8 +4921,8 @@ "giveMembersAccess": { "message": "Give members access:" }, - "viewAndSelectTheMembers" : { - "message" :"view and select the members you want to give access to Secrets Manager." + "viewAndSelectTheMembers": { + "message": "view and select the members you want to give access to Secrets Manager." }, "openYourOrganizations": { "message": "Open your organization's" @@ -5843,10 +5843,10 @@ "selfHostedBaseUrlHint": { "message": "Specify the base URL of your on-premises hosted Bitwarden installation. Example: https://bitwarden.company.com" }, - "selfHostedCustomEnvHeader" :{ + "selfHostedCustomEnvHeader": { "message": "For advanced configuration, you can specify the base URL of each service independently." }, - "selfHostedEnvFormInvalid" :{ + "selfHostedEnvFormInvalid": { "message": "You must add either the base Server URL or at least one custom environment." }, "apiUrl": { @@ -7608,7 +7608,7 @@ } } }, - "verificationRequired" : { + "verificationRequired": { "message": "Verification required", "description": "Default title for the user verification dialog." }, @@ -8400,7 +8400,7 @@ "deleteProviderRecoverConfirmDesc": { "message": "You have requested to delete this Provider. Use the button below to confirm." }, - "deleteProviderWarning": { + "deleteProviderWarning": { "message": "Deleting your provider is permanent. It cannot be undone." }, "errorAssigningTargetCollection": { @@ -8413,7 +8413,7 @@ "message": "Integrations & SDKs", "description": "The title for the section that deals with integrations and SDKs." }, - "integrations":{ + "integrations": { "message": "Integrations" }, "integrationsDesc": { @@ -8484,7 +8484,7 @@ }, "createdNewClient": { "message": "Successfully created new client" - }, + }, "noAccess": { "message": "No access" }, @@ -8699,11 +8699,11 @@ "placeholders": { "value": { "content": "$1", - "example":"increments of 100,000" + "example": "increments of 100,000" } } }, - "providerReinstate":{ + "providerReinstate": { "message": " Contact Customer Support to reinstate your subscription." }, "secretPeopleDescription": { diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.abstraction.ts b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.abstraction.ts index aed486378fa2..8d05dc3c7375 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.abstraction.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.abstraction.ts @@ -1,6 +1,9 @@ import { ExportFormat } from "./vault-export.service.abstraction"; export abstract class IndividualVaultExportServiceAbstraction { - getExport: (format: ExportFormat) => Promise; - getPasswordProtectedExport: (format: ExportFormat, password: string) => Promise; + getExport: (format: ExportFormat) => Promise; + getPasswordProtectedExport: ( + format: ExportFormat, + password: string, + ) => Promise; } diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts index 657ca08d7c5b..7cc551f44eee 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts @@ -1,3 +1,9 @@ +// zip.js needs these globals in node +import { TransformStream, WritableStream, ReadableStream } from "node:stream/web"; +globalThis.TransformStream = TransformStream; +globalThis.WritableStream = WritableStream; +globalThis.ReadableStream = ReadableStream; + import { mock, MockProxy } from "jest-mock-extended"; import * as JSZip from "jszip"; @@ -215,8 +221,9 @@ describe("VaultExportService", () => { it("contains data.json", async () => { cipherService.getAllDecrypted.mockResolvedValue([]); folderService.getAllDecryptedFromState.mockResolvedValue([]); - const exportZip = (await exportService.getExport("zip")) as Blob; - const zip = await JSZip.loadAsync(exportZip); + const exportZip = (await exportService.getExport("zip")) as Uint8Array; + const zipBlob = new Blob([exportZip], { type: "application/zip" }); + const zip = await JSZip.loadAsync(zipBlob); const data = await zip.file("data.json")?.async("string"); expect(data).toBeDefined(); }); @@ -240,8 +247,9 @@ describe("VaultExportService", () => { ) as any; global.Request = jest.fn(() => {}) as any; - const exportZip = (await exportService.getExport("zip")) as Blob; - const zip = await JSZip.loadAsync(exportZip); + const exportZip = (await exportService.getExport("zip")) as Uint8Array; + const zipBlob = new Blob([exportZip], { type: "application/zip" }); + const zip = await JSZip.loadAsync(zipBlob); const attachment = await zip.file("attachments/mock-id/mock-file-name")?.async("blob"); expect(attachment).toBeDefined(); }); @@ -266,7 +274,10 @@ describe("VaultExportService", () => { jest.spyOn(Utils, "fromBufferToB64").mockReturnValue(salt); cipherService.getAllDecrypted.mockResolvedValue(UserCipherViews.slice(0, 1)); - exportString = await exportService.getPasswordProtectedExport(password); + exportString = (await exportService.getPasswordProtectedExport( + "encrypted_json", + password, + )) as string; exportObject = JSON.parse(exportString); }); @@ -292,7 +303,10 @@ describe("VaultExportService", () => { it("has a mac property", async () => { cryptoService.encrypt.mockResolvedValue(mac); - exportString = await exportService.getPasswordProtectedExport(password); + exportString = (await exportService.getPasswordProtectedExport( + "encrypted_json", + password, + )) as string; exportObject = JSON.parse(exportString); expect(exportObject.encKeyValidation_DO_NOT_EDIT).toEqual(mac.encryptedString); @@ -300,7 +314,10 @@ describe("VaultExportService", () => { it("has data property", async () => { cryptoService.encrypt.mockResolvedValue(data); - exportString = await exportService.getPasswordProtectedExport(password); + exportString = (await exportService.getPasswordProtectedExport( + "encrypted_json", + password, + )) as string; exportObject = JSON.parse(exportString); expect(exportObject.data).toEqual(data.encryptedString); diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts index 952251b672ff..4ece819adfed 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts @@ -1,4 +1,4 @@ -import { BlobWriter, ZipWriter, TextReader, Uint8ArrayReader } from "@zip.js/zip.js"; +import { Uint8ArrayWriter, ZipWriter, Uint8ArrayReader } from "@zip.js/zip.js"; import * as papa from "papaparse"; import { PinServiceAbstraction } from "@bitwarden/auth/common"; @@ -41,16 +41,19 @@ export class IndividualVaultExportService super(pinService, cryptoService, cryptoFunctionService, kdfConfigService); } - async getExport(format: ExportFormat = "csv"): Promise { + async getExport(format: ExportFormat = "csv"): Promise { if (format === "encrypted_json") { return this.getEncryptedExport(); } else if (format === "zip") { return this.getExportZip(null); } - return this.getExportZip("abc"); + return this.getDecryptedExport(format); } - async getPasswordProtectedExport(format: ExportFormat, password: string): Promise { + async getPasswordProtectedExport( + format: ExportFormat, + password: string, + ): Promise { if (format == "encrypted_json") { const clearText = (await this.getExport("json")) as string; return await this.buildPasswordExport(clearText, password); @@ -61,12 +64,13 @@ export class IndividualVaultExportService } } - async getExportZip(password?: string): Promise { - const blobWriter = new BlobWriter("application/zip"); - const zipWriter = new ZipWriter(blobWriter, { bufferedWrite: true, password }); + async getExportZip(password?: string): Promise { + const blobWriter = new Uint8ArrayWriter(); + const zipWriter = new ZipWriter(blobWriter, { bufferedWrite: false, password }); const dataJson = await this.getDecryptedExport("json"); - await zipWriter.add("data.json", new TextReader(dataJson)); + const dataJsonUint8Array = Utils.fromByteStringToArray(dataJson); + await zipWriter.add("data.json", new Uint8ArrayReader(dataJsonUint8Array)); // attachments for (const cipher of await this.cipherService.getAllDecrypted()) { @@ -82,14 +86,17 @@ export class IndividualVaultExportService ? attachment.key : await this.cryptoService.getOrgKey(cipher.organizationId); const decBuf = await this.cryptoService.decryptFromBytes(encBuf, key); - await zipWriter.add(`${cipher.id}/${attachment.fileName}`, new Uint8ArrayReader(decBuf)); + await zipWriter.add( + `attachments/${cipher.id}/${attachment.fileName}`, + new Uint8ArrayReader(decBuf), + ); } } await zipWriter.close(); - const zipFileBlob = await blobWriter.getData(); + const zipFileArray = await blobWriter.getData(); - return zipFileBlob; + return zipFileArray; } private async getDecryptedExport(format: "json" | "csv"): Promise { diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.abstraction.ts b/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.abstraction.ts index 30e098e46b14..36337502cd6f 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.abstraction.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.abstraction.ts @@ -2,7 +2,7 @@ export const EXPORT_FORMATS = ["csv", "json", "encrypted_json", "zip"] as const; export type ExportFormat = (typeof EXPORT_FORMATS)[number]; export abstract class VaultExportServiceAbstraction { - getExport: (format: ExportFormat, password: string) => Promise; + getExport: (format: ExportFormat, password: string) => Promise; getOrganizationExport: ( organizationId: string, format: ExportFormat, diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.spec.ts b/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.spec.ts index df8c61a36721..041edf08aef7 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.spec.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.spec.ts @@ -1,3 +1,6 @@ +import { TransformStream } from "node:stream/web"; +globalThis.TransformStream = TransformStream; + import { mock, MockProxy } from "jest-mock-extended"; import { PinServiceAbstraction } from "@bitwarden/auth/common"; @@ -225,7 +228,10 @@ describe("VaultExportService", () => { jest.spyOn(Utils, "fromBufferToB64").mockReturnValue(salt); cipherService.getAllDecrypted.mockResolvedValue(UserCipherViews.slice(0, 1)); - exportString = await exportService.getPasswordProtectedExport(password); + exportString = (await exportService.getPasswordProtectedExport( + "encrypted_json", + password, + )) as string; exportObject = JSON.parse(exportString); }); @@ -251,7 +257,10 @@ describe("VaultExportService", () => { it("has a mac property", async () => { cryptoService.encrypt.mockResolvedValue(mac); - exportString = await exportService.getPasswordProtectedExport(password); + exportString = (await exportService.getPasswordProtectedExport( + "encrypted_json", + password, + )) as string; exportObject = JSON.parse(exportString); expect(exportObject.encKeyValidation_DO_NOT_EDIT).toEqual(mac.encryptedString); @@ -259,7 +268,10 @@ describe("VaultExportService", () => { it("has data property", async () => { cryptoService.encrypt.mockResolvedValue(data); - exportString = await exportService.getPasswordProtectedExport(password); + exportString = (await exportService.getPasswordProtectedExport( + "encrypted_json", + password, + )) as string; exportObject = JSON.parse(exportString); expect(exportObject.data).toEqual(data.encryptedString); diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.ts b/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.ts index 7da81a299fbd..b65f24f46686 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.ts @@ -11,7 +11,7 @@ export class VaultExportService implements VaultExportServiceAbstraction { private organizationVaultExportService: OrganizationVaultExportServiceAbstraction, ) {} - async getExport(format: ExportFormat = "csv", password: string): Promise { + async getExport(format: ExportFormat = "csv", password: string): Promise { if (!Utils.isNullOrWhitespace(password)) { if (format == "csv") { throw new Error("CSV does not support password protected export"); From 307afff7a55be97523d2ad4806f3c91cf0905dfe Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 9 Sep 2024 12:58:04 +0200 Subject: [PATCH 4/9] Import cleanup --- .../trial-initiation.component.spec.ts | 5 ----- .../vault-banners/vault-banners.component.spec.ts | 4 ---- .../services/individual-vault-export.service.spec.ts | 6 ------ .../src/services/individual-vault-export.service.ts | 10 ++++++++++ 4 files changed, 10 insertions(+), 15 deletions(-) diff --git a/apps/web/src/app/auth/trial-initiation/trial-initiation.component.spec.ts b/apps/web/src/app/auth/trial-initiation/trial-initiation.component.spec.ts index fa7e8904af36..a7916ae946d9 100644 --- a/apps/web/src/app/auth/trial-initiation/trial-initiation.component.spec.ts +++ b/apps/web/src/app/auth/trial-initiation/trial-initiation.component.spec.ts @@ -1,8 +1,3 @@ -import { TransformStream, WritableStream, ReadableStream } from "node:stream/web"; -globalThis.TransformStream = TransformStream; -globalThis.WritableStream = WritableStream; -globalThis.ReadableStream = ReadableStream; - import { StepperSelectionEvent } from "@angular/cdk/stepper"; import { TitleCasePipe } from "@angular/common"; import { NO_ERRORS_SCHEMA } from "@angular/core"; diff --git a/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.spec.ts b/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.spec.ts index 7e1b7dc4c51e..5fdac63e9327 100644 --- a/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.spec.ts +++ b/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.spec.ts @@ -1,7 +1,3 @@ -// zip.js needs these globals in node -import { TransformStream } from "node:stream/web"; -globalThis.TransformStream = TransformStream; - import { ComponentFixture, TestBed } from "@angular/core/testing"; import { By } from "@angular/platform-browser"; import { RouterTestingModule } from "@angular/router/testing"; diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts index 7cc551f44eee..f368cf9154ab 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts @@ -1,9 +1,3 @@ -// zip.js needs these globals in node -import { TransformStream, WritableStream, ReadableStream } from "node:stream/web"; -globalThis.TransformStream = TransformStream; -globalThis.WritableStream = WritableStream; -globalThis.ReadableStream = ReadableStream; - import { mock, MockProxy } from "jest-mock-extended"; import * as JSZip from "jszip"; diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts index 4ece819adfed..88976f2ea3ef 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts @@ -1,3 +1,13 @@ +// node requires setting these globally to make zip.js work +if (typeof global !== "undefined") { + // eslint-disable-next-line @typescript-eslint/no-var-requires + globalThis.TransformStream = require("node:stream/web").TransformStream; + // eslint-disable-next-line @typescript-eslint/no-var-requires + globalThis.WritableStream = require("node:stream/web").WritableStream; + // eslint-disable-next-line @typescript-eslint/no-var-requires + globalThis.ReadableStream = require("node:stream/web").ReadableStream; +} + import { Uint8ArrayWriter, ZipWriter, Uint8ArrayReader } from "@zip.js/zip.js"; import * as papa from "papaparse"; From 3fb53896657f2d01b372a539c285f5b2a7c2d467 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 9 Sep 2024 13:03:36 +0200 Subject: [PATCH 5/9] Switch back to blob return type --- .../individual-vault-export.service.abstraction.ts | 7 ++----- .../services/individual-vault-export.service.spec.ts | 10 ++++------ .../src/services/individual-vault-export.service.ts | 11 ++++------- .../src/services/vault-export.service.abstraction.ts | 2 +- .../src/services/vault-export.service.ts | 2 +- 5 files changed, 12 insertions(+), 20 deletions(-) diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.abstraction.ts b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.abstraction.ts index 8d05dc3c7375..aed486378fa2 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.abstraction.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.abstraction.ts @@ -1,9 +1,6 @@ import { ExportFormat } from "./vault-export.service.abstraction"; export abstract class IndividualVaultExportServiceAbstraction { - getExport: (format: ExportFormat) => Promise; - getPasswordProtectedExport: ( - format: ExportFormat, - password: string, - ) => Promise; + getExport: (format: ExportFormat) => Promise; + getPasswordProtectedExport: (format: ExportFormat, password: string) => Promise; } diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts index f368cf9154ab..3c7ad782964c 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts @@ -215,9 +215,8 @@ describe("VaultExportService", () => { it("contains data.json", async () => { cipherService.getAllDecrypted.mockResolvedValue([]); folderService.getAllDecryptedFromState.mockResolvedValue([]); - const exportZip = (await exportService.getExport("zip")) as Uint8Array; - const zipBlob = new Blob([exportZip], { type: "application/zip" }); - const zip = await JSZip.loadAsync(zipBlob); + const exportZip = (await exportService.getExport("zip")) as Blob; + const zip = await JSZip.loadAsync(exportZip); const data = await zip.file("data.json")?.async("string"); expect(data).toBeDefined(); }); @@ -241,9 +240,8 @@ describe("VaultExportService", () => { ) as any; global.Request = jest.fn(() => {}) as any; - const exportZip = (await exportService.getExport("zip")) as Uint8Array; - const zipBlob = new Blob([exportZip], { type: "application/zip" }); - const zip = await JSZip.loadAsync(zipBlob); + const exportZip = (await exportService.getExport("zip")) as Blob; + const zip = await JSZip.loadAsync(exportZip); const attachment = await zip.file("attachments/mock-id/mock-file-name")?.async("blob"); expect(attachment).toBeDefined(); }); diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts index 88976f2ea3ef..233b497436c7 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts @@ -51,7 +51,7 @@ export class IndividualVaultExportService super(pinService, cryptoService, cryptoFunctionService, kdfConfigService); } - async getExport(format: ExportFormat = "csv"): Promise { + async getExport(format: ExportFormat = "csv"): Promise { if (format === "encrypted_json") { return this.getEncryptedExport(); } else if (format === "zip") { @@ -60,10 +60,7 @@ export class IndividualVaultExportService return this.getDecryptedExport(format); } - async getPasswordProtectedExport( - format: ExportFormat, - password: string, - ): Promise { + async getPasswordProtectedExport(format: ExportFormat, password: string): Promise { if (format == "encrypted_json") { const clearText = (await this.getExport("json")) as string; return await this.buildPasswordExport(clearText, password); @@ -74,7 +71,7 @@ export class IndividualVaultExportService } } - async getExportZip(password?: string): Promise { + async getExportZip(password?: string): Promise { const blobWriter = new Uint8ArrayWriter(); const zipWriter = new ZipWriter(blobWriter, { bufferedWrite: false, password }); @@ -106,7 +103,7 @@ export class IndividualVaultExportService await zipWriter.close(); const zipFileArray = await blobWriter.getData(); - return zipFileArray; + return new Blob([zipFileArray], { type: "application/zip" }); } private async getDecryptedExport(format: "json" | "csv"): Promise { diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.abstraction.ts b/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.abstraction.ts index 36337502cd6f..30e098e46b14 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.abstraction.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.abstraction.ts @@ -2,7 +2,7 @@ export const EXPORT_FORMATS = ["csv", "json", "encrypted_json", "zip"] as const; export type ExportFormat = (typeof EXPORT_FORMATS)[number]; export abstract class VaultExportServiceAbstraction { - getExport: (format: ExportFormat, password: string) => Promise; + getExport: (format: ExportFormat, password: string) => Promise; getOrganizationExport: ( organizationId: string, format: ExportFormat, diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.ts b/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.ts index b65f24f46686..7da81a299fbd 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.ts @@ -11,7 +11,7 @@ export class VaultExportService implements VaultExportServiceAbstraction { private organizationVaultExportService: OrganizationVaultExportServiceAbstraction, ) {} - async getExport(format: ExportFormat = "csv", password: string): Promise { + async getExport(format: ExportFormat = "csv", password: string): Promise { if (!Utils.isNullOrWhitespace(password)) { if (format == "csv") { throw new Error("CSV does not support password protected export"); From 8a453a45e4c2627e47f4d62982e01abf03b0137c Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 9 Sep 2024 13:12:40 +0200 Subject: [PATCH 6/9] Fix build on web --- .../src/services/individual-vault-export.service.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts index 233b497436c7..3be0142c6aa0 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts @@ -1,11 +1,11 @@ // node requires setting these globally to make zip.js work if (typeof global !== "undefined") { // eslint-disable-next-line @typescript-eslint/no-var-requires - globalThis.TransformStream = require("node:stream/web").TransformStream; + globalThis.TransformStream = eval("require")("node:stream/web").TransformStream; // eslint-disable-next-line @typescript-eslint/no-var-requires - globalThis.WritableStream = require("node:stream/web").WritableStream; + globalThis.WritableStream = eval("require")("node:stream/web").WritableStream; // eslint-disable-next-line @typescript-eslint/no-var-requires - globalThis.ReadableStream = require("node:stream/web").ReadableStream; + globalThis.ReadableStream = eval("require")("node:stream/web").ReadableStream; } import { Uint8ArrayWriter, ZipWriter, Uint8ArrayReader } from "@zip.js/zip.js"; From 01a1b8fe30122d42f4e51e1dbd78ac9541d38593 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 9 Sep 2024 13:46:59 +0200 Subject: [PATCH 7/9] Move require to jest setup --- .eslintignore | 1 + jest.config.js | 3 +++ .../src/services/individual-vault-export.service.ts | 10 ---------- scripts/setupTests.ts | 3 +++ 4 files changed, 7 insertions(+), 10 deletions(-) create mode 100644 scripts/setupTests.ts diff --git a/.eslintignore b/.eslintignore index 68d426174ac3..527b7a8d022b 100644 --- a/.eslintignore +++ b/.eslintignore @@ -27,3 +27,4 @@ libs/components/tailwind.config.base.js libs/components/tailwind.config.js scripts/*.js +scripts/setupTests.ts diff --git a/jest.config.js b/jest.config.js index 6526237261fa..8d29cd53e975 100644 --- a/jest.config.js +++ b/jest.config.js @@ -46,4 +46,7 @@ module.exports = { // https://github.com/facebook/jest/issues/9430#issuecomment-1149882002 // Also anecdotally improves performance when run locally maxWorkers: 3, + + // zip.js requires setting streams on the global object + setupFilesAfterEnv: ["/scripts/setupTests.ts"], }; diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts index 3be0142c6aa0..e48b1623861a 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts @@ -1,13 +1,3 @@ -// node requires setting these globally to make zip.js work -if (typeof global !== "undefined") { - // eslint-disable-next-line @typescript-eslint/no-var-requires - globalThis.TransformStream = eval("require")("node:stream/web").TransformStream; - // eslint-disable-next-line @typescript-eslint/no-var-requires - globalThis.WritableStream = eval("require")("node:stream/web").WritableStream; - // eslint-disable-next-line @typescript-eslint/no-var-requires - globalThis.ReadableStream = eval("require")("node:stream/web").ReadableStream; -} - import { Uint8ArrayWriter, ZipWriter, Uint8ArrayReader } from "@zip.js/zip.js"; import * as papa from "papaparse"; diff --git a/scripts/setupTests.ts b/scripts/setupTests.ts new file mode 100644 index 000000000000..ab5d0382b0c2 --- /dev/null +++ b/scripts/setupTests.ts @@ -0,0 +1,3 @@ +globalThis.TransformStream = require("node:stream/web").TransformStream; +globalThis.WritableStream = require("node:stream/web").WritableStream; +globalThis.ReadableStream = require("node:stream/web").ReadableStream; From 8e2f7b37eeae56a3ad1cd632798f7b100d99f6d5 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 9 Sep 2024 13:48:29 +0200 Subject: [PATCH 8/9] Fix linting --- .eslintignore | 2 +- jest.config.js | 2 +- scripts/{setupTests.ts => setup-tests.ts} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename scripts/{setupTests.ts => setup-tests.ts} (100%) diff --git a/.eslintignore b/.eslintignore index 527b7a8d022b..2934031e6f46 100644 --- a/.eslintignore +++ b/.eslintignore @@ -27,4 +27,4 @@ libs/components/tailwind.config.base.js libs/components/tailwind.config.js scripts/*.js -scripts/setupTests.ts +scripts/setup-tests.ts diff --git a/jest.config.js b/jest.config.js index 8d29cd53e975..da79c76097f1 100644 --- a/jest.config.js +++ b/jest.config.js @@ -48,5 +48,5 @@ module.exports = { maxWorkers: 3, // zip.js requires setting streams on the global object - setupFilesAfterEnv: ["/scripts/setupTests.ts"], + setupFilesAfterEnv: ["/scripts/setup-tests.ts"], }; diff --git a/scripts/setupTests.ts b/scripts/setup-tests.ts similarity index 100% rename from scripts/setupTests.ts rename to scripts/setup-tests.ts From 7bc6dae71da277b8c8f21f2431dbcc0af5491eb5 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 9 Sep 2024 14:14:39 +0200 Subject: [PATCH 9/9] Fix linting --- .eslintignore | 1 - apps/browser/test.setup.ts | 5 +++++ apps/desktop/test.setup.ts | 5 +++++ apps/web/test.setup.ts | 5 +++++ jest.config.js | 3 --- .../export/vault-export/vault-export-core/jest.config.js | 1 + .../export/vault-export/vault-export-core/test.setup.ts | 4 ++++ scripts/setup-tests.ts | 3 --- 8 files changed, 20 insertions(+), 7 deletions(-) create mode 100644 libs/tools/export/vault-export/vault-export-core/test.setup.ts delete mode 100644 scripts/setup-tests.ts diff --git a/.eslintignore b/.eslintignore index 2934031e6f46..68d426174ac3 100644 --- a/.eslintignore +++ b/.eslintignore @@ -27,4 +27,3 @@ libs/components/tailwind.config.base.js libs/components/tailwind.config.js scripts/*.js -scripts/setup-tests.ts diff --git a/apps/browser/test.setup.ts b/apps/browser/test.setup.ts index 7f10b4075f53..6227f12ccf28 100644 --- a/apps/browser/test.setup.ts +++ b/apps/browser/test.setup.ts @@ -1,4 +1,9 @@ import "jest-preset-angular/setup-jest"; +import { TransformStream, WritableStream, ReadableStream } from "node:stream/web"; +(globalThis as any).TransformStream = TransformStream; +(globalThis as any).WritableStream = WritableStream; +(globalThis as any).ReadableStream = ReadableStream; + import { addCustomMatchers } from "@bitwarden/common/spec"; addCustomMatchers(); diff --git a/apps/desktop/test.setup.ts b/apps/desktop/test.setup.ts index 224262ec83e7..64debc8b7dfa 100644 --- a/apps/desktop/test.setup.ts +++ b/apps/desktop/test.setup.ts @@ -1,5 +1,10 @@ import "jest-preset-angular/setup-jest"; +import { TransformStream, WritableStream, ReadableStream } from "node:stream/web"; +(globalThis as any).TransformStream = TransformStream; +(globalThis as any).WritableStream = WritableStream; +(globalThis as any).ReadableStream = ReadableStream; + Object.defineProperty(window, "CSS", { value: null }); Object.defineProperty(window, "getComputedStyle", { value: () => { diff --git a/apps/web/test.setup.ts b/apps/web/test.setup.ts index 224262ec83e7..64debc8b7dfa 100644 --- a/apps/web/test.setup.ts +++ b/apps/web/test.setup.ts @@ -1,5 +1,10 @@ import "jest-preset-angular/setup-jest"; +import { TransformStream, WritableStream, ReadableStream } from "node:stream/web"; +(globalThis as any).TransformStream = TransformStream; +(globalThis as any).WritableStream = WritableStream; +(globalThis as any).ReadableStream = ReadableStream; + Object.defineProperty(window, "CSS", { value: null }); Object.defineProperty(window, "getComputedStyle", { value: () => { diff --git a/jest.config.js b/jest.config.js index da79c76097f1..6526237261fa 100644 --- a/jest.config.js +++ b/jest.config.js @@ -46,7 +46,4 @@ module.exports = { // https://github.com/facebook/jest/issues/9430#issuecomment-1149882002 // Also anecdotally improves performance when run locally maxWorkers: 3, - - // zip.js requires setting streams on the global object - setupFilesAfterEnv: ["/scripts/setup-tests.ts"], }; diff --git a/libs/tools/export/vault-export/vault-export-core/jest.config.js b/libs/tools/export/vault-export/vault-export-core/jest.config.js index 955b8e7763c4..8224f4cea8b3 100644 --- a/libs/tools/export/vault-export/vault-export-core/jest.config.js +++ b/libs/tools/export/vault-export/vault-export-core/jest.config.js @@ -7,6 +7,7 @@ module.exports = { testMatch: ["**/+(*.)+(spec).+(ts)"], preset: "ts-jest", testEnvironment: "jsdom", + setupFilesAfterEnv: ["/test.setup.ts"], moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, { prefix: "/../../../", }), diff --git a/libs/tools/export/vault-export/vault-export-core/test.setup.ts b/libs/tools/export/vault-export/vault-export-core/test.setup.ts new file mode 100644 index 000000000000..dac2d03e1ac4 --- /dev/null +++ b/libs/tools/export/vault-export/vault-export-core/test.setup.ts @@ -0,0 +1,4 @@ +import { TransformStream, WritableStream, ReadableStream } from "node:stream/web"; +(globalThis as any).TransformStream = TransformStream; +(globalThis as any).WritableStream = WritableStream; +(globalThis as any).ReadableStream = ReadableStream; diff --git a/scripts/setup-tests.ts b/scripts/setup-tests.ts deleted file mode 100644 index ab5d0382b0c2..000000000000 --- a/scripts/setup-tests.ts +++ /dev/null @@ -1,3 +0,0 @@ -globalThis.TransformStream = require("node:stream/web").TransformStream; -globalThis.WritableStream = require("node:stream/web").WritableStream; -globalThis.ReadableStream = require("node:stream/web").ReadableStream;