1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-28 23:33:27 +00:00

Move delete/restore functions to cipher-sdk.service.ts when using SDK flag

This commit is contained in:
Nik Gilmore
2026-01-21 16:07:59 -08:00
parent 5cc039c7d9
commit cc2b39fc0c
5 changed files with 611 additions and 330 deletions

View File

@@ -1,4 +1,4 @@
import { UserId } from "@bitwarden/common/types/guid";
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
/**
@@ -34,4 +34,76 @@ export abstract class CipherSdkService {
originalCipherView?: CipherView,
orgAdmin?: boolean,
): Promise<CipherView | undefined>;
/**
* Deletes a cipher on the server using the SDK.
*
* @param id The cipher ID to delete
* @param userId The user ID to use for SDK client
* @param asAdmin Whether this is an organization admin operation
* @returns A promise that resolves when the cipher is deleted
*/
abstract deleteWithServer(id: string, userId: UserId, asAdmin?: boolean): Promise<void>;
/**
* Deletes multiple ciphers on the server using the SDK.
*
* @param ids The cipher IDs to delete
* @param userId The user ID to use for SDK client
* @param asAdmin Whether this is an organization admin operation
* @param orgId The organization ID (required when asAdmin is true)
* @returns A promise that resolves when the ciphers are deleted
*/
abstract deleteManyWithServer(
ids: string[],
userId: UserId,
asAdmin?: boolean,
orgId?: OrganizationId,
): Promise<void>;
/**
* Soft deletes a cipher on the server using the SDK.
*
* @param id The cipher ID to soft delete
* @param userId The user ID to use for SDK client
* @param asAdmin Whether this is an organization admin operation
* @returns A promise that resolves when the cipher is soft deleted
*/
abstract softDeleteWithServer(id: string, userId: UserId, asAdmin?: boolean): Promise<void>;
/**
* Soft deletes multiple ciphers on the server using the SDK.
*
* @param ids The cipher IDs to soft delete
* @param userId The user ID to use for SDK client
* @param asAdmin Whether this is an organization admin operation
* @param orgId The organization ID (required when asAdmin is true)
* @returns A promise that resolves when the ciphers are soft deleted
*/
abstract softDeleteManyWithServer(
ids: string[],
userId: UserId,
asAdmin?: boolean,
orgId?: OrganizationId,
): Promise<void>;
/**
* Restores a soft-deleted cipher on the server using the SDK.
*
* @param id The cipher ID to restore
* @param userId The user ID to use for SDK client
* @param asAdmin Whether this is an organization admin operation
* @returns A promise that resolves when the cipher is restored
*/
abstract restoreWithServer(id: string, userId: UserId, asAdmin?: boolean): Promise<void>;
/**
* Restores multiple soft-deleted ciphers on the server using the SDK.
*
* @param ids The cipher IDs to restore
* @param userId The user ID to use for SDK client
* @param orgId The organization ID (determines whether to use admin API)
* @returns A promise that resolves when the ciphers are restored
*/
abstract restoreManyWithServer(ids: string[], userId: UserId, orgId?: string): Promise<void>;
}

View File

@@ -28,10 +28,22 @@ describe("DefaultCipherSdkService", () => {
mockAdminSdk = {
create: jest.fn(),
edit: jest.fn(),
delete: jest.fn().mockResolvedValue(undefined),
delete_many: jest.fn().mockResolvedValue(undefined),
soft_delete: jest.fn().mockResolvedValue(undefined),
soft_delete_many: jest.fn().mockResolvedValue(undefined),
restore: jest.fn().mockResolvedValue(undefined),
restore_many: jest.fn().mockResolvedValue(undefined),
};
mockCiphersSdk = {
create: jest.fn(),
edit: jest.fn(),
delete: jest.fn().mockResolvedValue(undefined),
delete_many: jest.fn().mockResolvedValue(undefined),
soft_delete: jest.fn().mockResolvedValue(undefined),
soft_delete_many: jest.fn().mockResolvedValue(undefined),
restore: jest.fn().mockResolvedValue(undefined),
restore_many: jest.fn().mockResolvedValue(undefined),
admin: jest.fn().mockReturnValue(mockAdminSdk),
};
mockVaultSdk = {
@@ -210,7 +222,7 @@ describe("DefaultCipherSdkService", () => {
id: expect.anything(),
name: cipherView.name,
}),
new CipherView().toSdkCipherView(),
expect.anything(), // Empty CipherView - timestamps vary so we just verify it was called
);
expect(result).toBeInstanceOf(CipherView);
expect(result.name).toBe(cipherView.name);
@@ -243,4 +255,280 @@ describe("DefaultCipherSdkService", () => {
);
});
});
describe("deleteWithServer()", () => {
const testCipherId = "5ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b22" as CipherId;
it("should delete cipher using SDK when asAdmin is false", async () => {
await cipherSdkService.deleteWithServer(testCipherId, userId, false);
expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
expect(mockVaultSdk.ciphers).toHaveBeenCalled();
expect(mockCiphersSdk.delete).toHaveBeenCalledWith(testCipherId);
expect(mockCiphersSdk.admin).not.toHaveBeenCalled();
});
it("should delete cipher using SDK admin API when asAdmin is true", async () => {
await cipherSdkService.deleteWithServer(testCipherId, userId, true);
expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
expect(mockVaultSdk.ciphers).toHaveBeenCalled();
expect(mockCiphersSdk.admin).toHaveBeenCalled();
expect(mockAdminSdk.delete).toHaveBeenCalledWith(testCipherId);
});
it("should throw error and log when SDK client is not available", async () => {
sdkService.userClient$.mockReturnValue(of(null));
await expect(cipherSdkService.deleteWithServer(testCipherId, userId)).rejects.toThrow(
"SDK not available",
);
expect(logService.error).toHaveBeenCalledWith(
expect.stringContaining("Failed to delete cipher"),
);
});
it("should throw error and log when SDK throws an error", async () => {
mockCiphersSdk.delete.mockRejectedValue(new Error("SDK error"));
await expect(cipherSdkService.deleteWithServer(testCipherId, userId)).rejects.toThrow();
expect(logService.error).toHaveBeenCalledWith(
expect.stringContaining("Failed to delete cipher"),
);
});
});
describe("deleteManyWithServer()", () => {
const testCipherIds = [
"5ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b22" as CipherId,
"6ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b23" as CipherId,
];
it("should delete multiple ciphers using SDK when asAdmin is false", async () => {
await cipherSdkService.deleteManyWithServer(testCipherIds, userId, false);
expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
expect(mockVaultSdk.ciphers).toHaveBeenCalled();
expect(mockCiphersSdk.delete_many).toHaveBeenCalledWith(testCipherIds);
expect(mockCiphersSdk.admin).not.toHaveBeenCalled();
});
it("should delete multiple ciphers using SDK admin API when asAdmin is true", async () => {
await cipherSdkService.deleteManyWithServer(testCipherIds, userId, true, orgId);
expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
expect(mockVaultSdk.ciphers).toHaveBeenCalled();
expect(mockCiphersSdk.admin).toHaveBeenCalled();
expect(mockAdminSdk.delete_many).toHaveBeenCalledWith(testCipherIds, orgId);
});
it("should throw error when asAdmin is true but orgId is missing", async () => {
await expect(
cipherSdkService.deleteManyWithServer(testCipherIds, userId, true, undefined),
).rejects.toThrow("Organization ID is required for admin delete.");
});
it("should throw error and log when SDK client is not available", async () => {
sdkService.userClient$.mockReturnValue(of(null));
await expect(cipherSdkService.deleteManyWithServer(testCipherIds, userId)).rejects.toThrow(
"SDK not available",
);
expect(logService.error).toHaveBeenCalledWith(
expect.stringContaining("Failed to delete multiple ciphers"),
);
});
it("should throw error and log when SDK throws an error", async () => {
mockCiphersSdk.delete_many.mockRejectedValue(new Error("SDK error"));
await expect(cipherSdkService.deleteManyWithServer(testCipherIds, userId)).rejects.toThrow();
expect(logService.error).toHaveBeenCalledWith(
expect.stringContaining("Failed to delete multiple ciphers"),
);
});
});
describe("softDeleteWithServer()", () => {
const testCipherId = "5ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b22" as CipherId;
it("should soft delete cipher using SDK when asAdmin is false", async () => {
await cipherSdkService.softDeleteWithServer(testCipherId, userId, false);
expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
expect(mockVaultSdk.ciphers).toHaveBeenCalled();
expect(mockCiphersSdk.soft_delete).toHaveBeenCalledWith(testCipherId);
expect(mockCiphersSdk.admin).not.toHaveBeenCalled();
});
it("should soft delete cipher using SDK admin API when asAdmin is true", async () => {
await cipherSdkService.softDeleteWithServer(testCipherId, userId, true);
expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
expect(mockVaultSdk.ciphers).toHaveBeenCalled();
expect(mockCiphersSdk.admin).toHaveBeenCalled();
expect(mockAdminSdk.soft_delete).toHaveBeenCalledWith(testCipherId);
});
it("should throw error and log when SDK client is not available", async () => {
sdkService.userClient$.mockReturnValue(of(null));
await expect(cipherSdkService.softDeleteWithServer(testCipherId, userId)).rejects.toThrow(
"SDK not available",
);
expect(logService.error).toHaveBeenCalledWith(
expect.stringContaining("Failed to soft delete cipher"),
);
});
it("should throw error and log when SDK throws an error", async () => {
mockCiphersSdk.soft_delete.mockRejectedValue(new Error("SDK error"));
await expect(cipherSdkService.softDeleteWithServer(testCipherId, userId)).rejects.toThrow();
expect(logService.error).toHaveBeenCalledWith(
expect.stringContaining("Failed to soft delete cipher"),
);
});
});
describe("softDeleteManyWithServer()", () => {
const testCipherIds = [
"5ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b22" as CipherId,
"6ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b23" as CipherId,
];
it("should soft delete multiple ciphers using SDK when asAdmin is false", async () => {
await cipherSdkService.softDeleteManyWithServer(testCipherIds, userId, false);
expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
expect(mockVaultSdk.ciphers).toHaveBeenCalled();
expect(mockCiphersSdk.soft_delete_many).toHaveBeenCalledWith(testCipherIds);
expect(mockCiphersSdk.admin).not.toHaveBeenCalled();
});
it("should soft delete multiple ciphers using SDK admin API when asAdmin is true", async () => {
await cipherSdkService.softDeleteManyWithServer(testCipherIds, userId, true, orgId);
expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
expect(mockVaultSdk.ciphers).toHaveBeenCalled();
expect(mockCiphersSdk.admin).toHaveBeenCalled();
expect(mockAdminSdk.soft_delete_many).toHaveBeenCalledWith(testCipherIds, orgId);
});
it("should throw error when asAdmin is true but orgId is missing", async () => {
await expect(
cipherSdkService.softDeleteManyWithServer(testCipherIds, userId, true, undefined),
).rejects.toThrow("Organization ID is required for admin soft delete.");
});
it("should throw error and log when SDK client is not available", async () => {
sdkService.userClient$.mockReturnValue(of(null));
await expect(
cipherSdkService.softDeleteManyWithServer(testCipherIds, userId),
).rejects.toThrow("SDK not available");
expect(logService.error).toHaveBeenCalledWith(
expect.stringContaining("Failed to soft delete multiple ciphers"),
);
});
it("should throw error and log when SDK throws an error", async () => {
mockCiphersSdk.soft_delete_many.mockRejectedValue(new Error("SDK error"));
await expect(
cipherSdkService.softDeleteManyWithServer(testCipherIds, userId),
).rejects.toThrow();
expect(logService.error).toHaveBeenCalledWith(
expect.stringContaining("Failed to soft delete multiple ciphers"),
);
});
});
describe("restoreWithServer()", () => {
const testCipherId = "5ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b22" as CipherId;
it("should restore cipher using SDK when asAdmin is false", async () => {
await cipherSdkService.restoreWithServer(testCipherId, userId, false);
expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
expect(mockVaultSdk.ciphers).toHaveBeenCalled();
expect(mockCiphersSdk.restore).toHaveBeenCalledWith(testCipherId);
expect(mockCiphersSdk.admin).not.toHaveBeenCalled();
});
it("should restore cipher using SDK admin API when asAdmin is true", async () => {
await cipherSdkService.restoreWithServer(testCipherId, userId, true);
expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
expect(mockVaultSdk.ciphers).toHaveBeenCalled();
expect(mockCiphersSdk.admin).toHaveBeenCalled();
expect(mockAdminSdk.restore).toHaveBeenCalledWith(testCipherId);
});
it("should throw error and log when SDK client is not available", async () => {
sdkService.userClient$.mockReturnValue(of(null));
await expect(cipherSdkService.restoreWithServer(testCipherId, userId)).rejects.toThrow(
"SDK not available",
);
expect(logService.error).toHaveBeenCalledWith(
expect.stringContaining("Failed to restore cipher"),
);
});
it("should throw error and log when SDK throws an error", async () => {
mockCiphersSdk.restore.mockRejectedValue(new Error("SDK error"));
await expect(cipherSdkService.restoreWithServer(testCipherId, userId)).rejects.toThrow();
expect(logService.error).toHaveBeenCalledWith(
expect.stringContaining("Failed to restore cipher"),
);
});
});
describe("restoreManyWithServer()", () => {
const testCipherIds = [
"5ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b22" as CipherId,
"6ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b23" as CipherId,
];
it("should restore multiple ciphers using SDK when orgId is not provided", async () => {
await cipherSdkService.restoreManyWithServer(testCipherIds, userId);
expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
expect(mockVaultSdk.ciphers).toHaveBeenCalled();
expect(mockCiphersSdk.restore_many).toHaveBeenCalledWith(testCipherIds);
expect(mockCiphersSdk.admin).not.toHaveBeenCalled();
});
it("should restore multiple ciphers using SDK admin API when orgId is provided", async () => {
const orgIdString = orgId as string;
await cipherSdkService.restoreManyWithServer(testCipherIds, userId, orgIdString);
expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
expect(mockVaultSdk.ciphers).toHaveBeenCalled();
expect(mockCiphersSdk.admin).toHaveBeenCalled();
expect(mockAdminSdk.restore_many).toHaveBeenCalledWith(testCipherIds, orgIdString);
});
it("should throw error and log when SDK client is not available", async () => {
sdkService.userClient$.mockReturnValue(of(null));
await expect(cipherSdkService.restoreManyWithServer(testCipherIds, userId)).rejects.toThrow(
"SDK not available",
);
expect(logService.error).toHaveBeenCalledWith(
expect.stringContaining("Failed to restore multiple ciphers"),
);
});
it("should throw error and log when SDK throws an error", async () => {
mockCiphersSdk.restore_many.mockRejectedValue(new Error("SDK error"));
await expect(cipherSdkService.restoreManyWithServer(testCipherIds, userId)).rejects.toThrow();
expect(logService.error).toHaveBeenCalledWith(
expect.stringContaining("Failed to restore multiple ciphers"),
);
});
});
});

View File

@@ -1,8 +1,8 @@
import { firstValueFrom, switchMap, catchError } from "rxjs";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
import { UserId } from "@bitwarden/common/types/guid";
import { SdkService, asUuid } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { CipherView as SdkCipherView } from "@bitwarden/sdk-internal";
@@ -79,4 +79,185 @@ export class DefaultCipherSdkService implements CipherSdkService {
),
);
}
async deleteWithServer(id: string, userId: UserId, asAdmin = false): Promise<void> {
return await firstValueFrom(
this.sdkService.userClient$(userId).pipe(
switchMap(async (sdk) => {
if (!sdk) {
throw new Error("SDK not available");
}
using ref = sdk.take();
if (asAdmin) {
await ref.value.vault().ciphers().admin().delete(asUuid(id));
} else {
await ref.value.vault().ciphers().delete(asUuid(id));
}
}),
catchError((error: unknown) => {
this.logService.error(`Failed to delete cipher: ${error}`);
throw error;
}),
),
);
}
async deleteManyWithServer(
ids: string[],
userId: UserId,
asAdmin = false,
orgId?: OrganizationId,
): Promise<void> {
return await firstValueFrom(
this.sdkService.userClient$(userId).pipe(
switchMap(async (sdk) => {
if (!sdk) {
throw new Error("SDK not available");
}
using ref = sdk.take();
if (asAdmin) {
if (orgId == null) {
throw new Error("Organization ID is required for admin delete.");
}
await ref.value
.vault()
.ciphers()
.admin()
.delete_many(
ids.map((id) => asUuid(id)),
asUuid(orgId),
);
} else {
await ref.value
.vault()
.ciphers()
.delete_many(ids.map((id) => asUuid(id)));
}
}),
catchError((error: unknown) => {
this.logService.error(`Failed to delete multiple ciphers: ${error}`);
throw error;
}),
),
);
}
async softDeleteWithServer(id: string, userId: UserId, asAdmin = false): Promise<void> {
return await firstValueFrom(
this.sdkService.userClient$(userId).pipe(
switchMap(async (sdk) => {
if (!sdk) {
throw new Error("SDK not available");
}
using ref = sdk.take();
if (asAdmin) {
await ref.value.vault().ciphers().admin().soft_delete(asUuid(id));
} else {
await ref.value.vault().ciphers().soft_delete(asUuid(id));
}
}),
catchError((error: unknown) => {
this.logService.error(`Failed to soft delete cipher: ${error}`);
throw error;
}),
),
);
}
async softDeleteManyWithServer(
ids: string[],
userId: UserId,
asAdmin = false,
orgId?: OrganizationId,
): Promise<void> {
return await firstValueFrom(
this.sdkService.userClient$(userId).pipe(
switchMap(async (sdk) => {
if (!sdk) {
throw new Error("SDK not available");
}
using ref = sdk.take();
if (asAdmin) {
if (orgId == null) {
throw new Error("Organization ID is required for admin soft delete.");
}
await ref.value
.vault()
.ciphers()
.admin()
.soft_delete_many(
ids.map((id) => asUuid(id)),
asUuid(orgId),
);
} else {
await ref.value
.vault()
.ciphers()
.soft_delete_many(ids.map((id) => asUuid(id)));
}
}),
catchError((error: unknown) => {
this.logService.error(`Failed to soft delete multiple ciphers: ${error}`);
throw error;
}),
),
);
}
async restoreWithServer(id: string, userId: UserId, asAdmin = false): Promise<void> {
return await firstValueFrom(
this.sdkService.userClient$(userId).pipe(
switchMap(async (sdk) => {
if (!sdk) {
throw new Error("SDK not available");
}
using ref = sdk.take();
if (asAdmin) {
await ref.value.vault().ciphers().admin().restore(asUuid(id));
} else {
await ref.value.vault().ciphers().restore(asUuid(id));
}
}),
catchError((error: unknown) => {
this.logService.error(`Failed to restore cipher: ${error}`);
throw error;
}),
),
);
}
async restoreManyWithServer(ids: string[], userId: UserId, orgId?: string): Promise<void> {
return await firstValueFrom(
this.sdkService.userClient$(userId).pipe(
switchMap(async (sdk) => {
if (!sdk) {
throw new Error("SDK not available");
}
using ref = sdk.take();
// No longer using an asAdmin Param. Org Vault bulkRestore will assess if an item is unassigned or editable
// The Org Vault will pass those ids an array as well as the orgId when calling bulkRestore
if (orgId) {
await ref.value
.vault()
.ciphers()
.admin()
.restore_many(
ids.map((id) => asUuid(id)),
asUuid(orgId),
);
} else {
await ref.value
.vault()
.ciphers()
.restore_many(ids.map((id) => asUuid(id)));
}
}),
catchError((error: unknown) => {
this.logService.error(`Failed to restore multiple ciphers: ${error}`);
throw error;
}),
),
);
}
}

View File

@@ -1010,38 +1010,8 @@ describe("Cipher Service", () => {
});
describe("deleteWithServer()", () => {
let mockSdkClient: any;
let mockCiphersSdk: any;
let mockAdminSdk: any;
let mockVaultSdk: any;
const testCipherId = "5ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b22" as CipherId;
beforeEach(() => {
// Mock the SDK client chain for delete operations
mockAdminSdk = {
delete: jest.fn().mockResolvedValue(undefined),
};
mockCiphersSdk = {
delete: jest.fn().mockResolvedValue(undefined),
admin: jest.fn().mockReturnValue(mockAdminSdk),
};
mockVaultSdk = {
ciphers: jest.fn().mockReturnValue(mockCiphersSdk),
};
const mockSdkValue = {
vault: jest.fn().mockReturnValue(mockVaultSdk),
};
mockSdkClient = {
take: jest.fn().mockReturnValue({
value: mockSdkValue,
[Symbol.dispose]: jest.fn(),
}),
};
// Mock sdkService to return the mock client
sdkService.userClient$.mockReturnValue(of(mockSdkClient));
});
it("should call apiService.deleteCipher when feature flag is disabled", async () => {
configService.getFeatureFlag
.calledWith(FeatureFlag.PM27632_SdkCipherCrudOperations)
@@ -1052,7 +1022,6 @@ describe("Cipher Service", () => {
await cipherService.deleteWithServer(testCipherId, userId);
expect(apiSpy).toHaveBeenCalledWith(testCipherId);
expect(mockSdkClient.take).not.toHaveBeenCalled();
});
it("should call apiService.deleteCipherAdmin when feature flag is disabled and asAdmin is true", async () => {
@@ -1065,7 +1034,6 @@ describe("Cipher Service", () => {
await cipherService.deleteWithServer(testCipherId, userId, true);
expect(apiSpy).toHaveBeenCalledWith(testCipherId);
expect(mockSdkClient.take).not.toHaveBeenCalled();
});
it("should use SDK to delete cipher when feature flag is enabled", async () => {
@@ -1073,14 +1041,14 @@ describe("Cipher Service", () => {
.calledWith(FeatureFlag.PM27632_SdkCipherCrudOperations)
.mockResolvedValue(true);
const apiSpy = jest.spyOn(apiService, "deleteCipher");
const sdkServiceSpy = jest
.spyOn(cipherSdkService, "deleteWithServer")
.mockResolvedValue(undefined);
const clearCacheSpy = jest.spyOn(cipherService as any, "clearCache");
await cipherService.deleteWithServer(testCipherId, userId);
await cipherService.deleteWithServer(testCipherId, userId, false);
expect(mockSdkClient.take).toHaveBeenCalled();
expect(mockCiphersSdk.delete).toHaveBeenCalledWith(testCipherId);
expect(apiSpy).not.toHaveBeenCalled();
expect(sdkServiceSpy).toHaveBeenCalledWith(testCipherId, userId, false);
expect(clearCacheSpy).toHaveBeenCalledWith(userId);
});
@@ -1089,54 +1057,24 @@ describe("Cipher Service", () => {
.calledWith(FeatureFlag.PM27632_SdkCipherCrudOperations)
.mockResolvedValue(true);
const apiSpy = jest.spyOn(apiService, "deleteCipherAdmin");
const sdkServiceSpy = jest
.spyOn(cipherSdkService, "deleteWithServer")
.mockResolvedValue(undefined);
const clearCacheSpy = jest.spyOn(cipherService as any, "clearCache");
await cipherService.deleteWithServer(testCipherId, userId, true);
expect(mockSdkClient.take).toHaveBeenCalled();
expect(mockAdminSdk.delete).toHaveBeenCalledWith(testCipherId);
expect(apiSpy).not.toHaveBeenCalled();
expect(sdkServiceSpy).toHaveBeenCalledWith(testCipherId, userId, true);
expect(clearCacheSpy).toHaveBeenCalledWith(userId);
});
});
describe("deleteManyWithServer()", () => {
let mockSdkClient: any;
let mockCiphersSdk: any;
let mockAdminSdk: any;
let mockVaultSdk: any;
const testCipherIds = [
"5ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b22" as CipherId,
"6ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b23" as CipherId,
];
beforeEach(() => {
// Mock the SDK client chain for delete many operations
mockAdminSdk = {
delete_many: jest.fn().mockResolvedValue(undefined),
};
mockCiphersSdk = {
delete_many: jest.fn().mockResolvedValue(undefined),
admin: jest.fn().mockReturnValue(mockAdminSdk),
};
mockVaultSdk = {
ciphers: jest.fn().mockReturnValue(mockCiphersSdk),
};
const mockSdkValue = {
vault: jest.fn().mockReturnValue(mockVaultSdk),
};
mockSdkClient = {
take: jest.fn().mockReturnValue({
value: mockSdkValue,
[Symbol.dispose]: jest.fn(),
}),
};
// Mock sdkService to return the mock client
sdkService.userClient$.mockReturnValue(of(mockSdkClient));
});
it("should call apiService.deleteManyCiphers when feature flag is disabled", async () => {
configService.getFeatureFlag
.calledWith(FeatureFlag.PM27632_SdkCipherCrudOperations)
@@ -1147,7 +1085,6 @@ describe("Cipher Service", () => {
await cipherService.deleteManyWithServer(testCipherIds, userId);
expect(apiSpy).toHaveBeenCalled();
expect(mockSdkClient.take).not.toHaveBeenCalled();
});
it("should call apiService.deleteManyCiphersAdmin when feature flag is disabled and asAdmin is true", async () => {
@@ -1160,7 +1097,6 @@ describe("Cipher Service", () => {
await cipherService.deleteManyWithServer(testCipherIds, userId, true, orgId);
expect(apiSpy).toHaveBeenCalled();
expect(mockSdkClient.take).not.toHaveBeenCalled();
});
it("should use SDK to delete multiple ciphers when feature flag is enabled", async () => {
@@ -1168,14 +1104,14 @@ describe("Cipher Service", () => {
.calledWith(FeatureFlag.PM27632_SdkCipherCrudOperations)
.mockResolvedValue(true);
const apiSpy = jest.spyOn(apiService, "deleteManyCiphers");
const sdkServiceSpy = jest
.spyOn(cipherSdkService, "deleteManyWithServer")
.mockResolvedValue(undefined);
const clearCacheSpy = jest.spyOn(cipherService as any, "clearCache");
await cipherService.deleteManyWithServer(testCipherIds, userId);
await cipherService.deleteManyWithServer(testCipherIds, userId, false);
expect(mockSdkClient.take).toHaveBeenCalled();
expect(mockCiphersSdk.delete_many).toHaveBeenCalledWith(testCipherIds);
expect(apiSpy).not.toHaveBeenCalled();
expect(sdkServiceSpy).toHaveBeenCalledWith(testCipherIds, userId, false, undefined);
expect(clearCacheSpy).toHaveBeenCalledWith(userId);
});
@@ -1184,51 +1120,21 @@ describe("Cipher Service", () => {
.calledWith(FeatureFlag.PM27632_SdkCipherCrudOperations)
.mockResolvedValue(true);
const apiSpy = jest.spyOn(apiService, "deleteManyCiphersAdmin");
const sdkServiceSpy = jest
.spyOn(cipherSdkService, "deleteManyWithServer")
.mockResolvedValue(undefined);
const clearCacheSpy = jest.spyOn(cipherService as any, "clearCache");
await cipherService.deleteManyWithServer(testCipherIds, userId, true, orgId);
expect(mockSdkClient.take).toHaveBeenCalled();
expect(mockAdminSdk.delete_many).toHaveBeenCalledWith(testCipherIds, orgId);
expect(apiSpy).not.toHaveBeenCalled();
expect(sdkServiceSpy).toHaveBeenCalledWith(testCipherIds, userId, true, orgId);
expect(clearCacheSpy).toHaveBeenCalledWith(userId);
});
});
describe("softDeleteWithServer()", () => {
let mockSdkClient: any;
let mockCiphersSdk: any;
let mockAdminSdk: any;
let mockVaultSdk: any;
const testCipherId = "5ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b22" as CipherId;
beforeEach(() => {
// Mock the SDK client chain for soft delete operations
mockAdminSdk = {
soft_delete: jest.fn().mockResolvedValue(undefined),
};
mockCiphersSdk = {
soft_delete: jest.fn().mockResolvedValue(undefined),
admin: jest.fn().mockReturnValue(mockAdminSdk),
};
mockVaultSdk = {
ciphers: jest.fn().mockReturnValue(mockCiphersSdk),
};
const mockSdkValue = {
vault: jest.fn().mockReturnValue(mockVaultSdk),
};
mockSdkClient = {
take: jest.fn().mockReturnValue({
value: mockSdkValue,
[Symbol.dispose]: jest.fn(),
}),
};
// Mock sdkService to return the mock client
sdkService.userClient$.mockReturnValue(of(mockSdkClient));
});
it("should call apiService.putDeleteCipher when feature flag is disabled", async () => {
configService.getFeatureFlag
.calledWith(FeatureFlag.PM27632_SdkCipherCrudOperations)
@@ -1239,7 +1145,6 @@ describe("Cipher Service", () => {
await cipherService.softDeleteWithServer(testCipherId, userId);
expect(apiSpy).toHaveBeenCalledWith(testCipherId);
expect(mockSdkClient.take).not.toHaveBeenCalled();
});
it("should call apiService.putDeleteCipherAdmin when feature flag is disabled and asAdmin is true", async () => {
@@ -1252,7 +1157,6 @@ describe("Cipher Service", () => {
await cipherService.softDeleteWithServer(testCipherId, userId, true);
expect(apiSpy).toHaveBeenCalledWith(testCipherId);
expect(mockSdkClient.take).not.toHaveBeenCalled();
});
it("should use SDK to soft delete cipher when feature flag is enabled", async () => {
@@ -1260,14 +1164,14 @@ describe("Cipher Service", () => {
.calledWith(FeatureFlag.PM27632_SdkCipherCrudOperations)
.mockResolvedValue(true);
const apiSpy = jest.spyOn(apiService, "putDeleteCipher");
const sdkServiceSpy = jest
.spyOn(cipherSdkService, "softDeleteWithServer")
.mockResolvedValue(undefined);
const clearCacheSpy = jest.spyOn(cipherService as any, "clearCache");
await cipherService.softDeleteWithServer(testCipherId, userId);
await cipherService.softDeleteWithServer(testCipherId, userId, false);
expect(mockSdkClient.take).toHaveBeenCalled();
expect(mockCiphersSdk.soft_delete).toHaveBeenCalledWith(testCipherId);
expect(apiSpy).not.toHaveBeenCalled();
expect(sdkServiceSpy).toHaveBeenCalledWith(testCipherId, userId, false);
expect(clearCacheSpy).toHaveBeenCalledWith(userId);
});
@@ -1276,54 +1180,24 @@ describe("Cipher Service", () => {
.calledWith(FeatureFlag.PM27632_SdkCipherCrudOperations)
.mockResolvedValue(true);
const apiSpy = jest.spyOn(apiService, "putDeleteCipherAdmin");
const sdkServiceSpy = jest
.spyOn(cipherSdkService, "softDeleteWithServer")
.mockResolvedValue(undefined);
const clearCacheSpy = jest.spyOn(cipherService as any, "clearCache");
await cipherService.softDeleteWithServer(testCipherId, userId, true);
expect(mockSdkClient.take).toHaveBeenCalled();
expect(mockAdminSdk.soft_delete).toHaveBeenCalledWith(testCipherId);
expect(apiSpy).not.toHaveBeenCalled();
expect(sdkServiceSpy).toHaveBeenCalledWith(testCipherId, userId, true);
expect(clearCacheSpy).toHaveBeenCalledWith(userId);
});
});
describe("softDeleteManyWithServer()", () => {
let mockSdkClient: any;
let mockCiphersSdk: any;
let mockAdminSdk: any;
let mockVaultSdk: any;
const testCipherIds = [
"5ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b22" as CipherId,
"6ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b23" as CipherId,
];
beforeEach(() => {
// Mock the SDK client chain for soft delete many operations
mockAdminSdk = {
soft_delete_many: jest.fn().mockResolvedValue(undefined),
};
mockCiphersSdk = {
soft_delete_many: jest.fn().mockResolvedValue(undefined),
admin: jest.fn().mockReturnValue(mockAdminSdk),
};
mockVaultSdk = {
ciphers: jest.fn().mockReturnValue(mockCiphersSdk),
};
const mockSdkValue = {
vault: jest.fn().mockReturnValue(mockVaultSdk),
};
mockSdkClient = {
take: jest.fn().mockReturnValue({
value: mockSdkValue,
[Symbol.dispose]: jest.fn(),
}),
};
// Mock sdkService to return the mock client
sdkService.userClient$.mockReturnValue(of(mockSdkClient));
});
it("should call apiService.putDeleteManyCiphers when feature flag is disabled", async () => {
configService.getFeatureFlag
.calledWith(FeatureFlag.PM27632_SdkCipherCrudOperations)
@@ -1334,7 +1208,6 @@ describe("Cipher Service", () => {
await cipherService.softDeleteManyWithServer(testCipherIds, userId);
expect(apiSpy).toHaveBeenCalled();
expect(mockSdkClient.take).not.toHaveBeenCalled();
});
it("should call apiService.putDeleteManyCiphersAdmin when feature flag is disabled and asAdmin is true", async () => {
@@ -1349,7 +1222,6 @@ describe("Cipher Service", () => {
await cipherService.softDeleteManyWithServer(testCipherIds, userId, true, orgId);
expect(apiSpy).toHaveBeenCalled();
expect(mockSdkClient.take).not.toHaveBeenCalled();
});
it("should use SDK to soft delete multiple ciphers when feature flag is enabled", async () => {
@@ -1357,14 +1229,14 @@ describe("Cipher Service", () => {
.calledWith(FeatureFlag.PM27632_SdkCipherCrudOperations)
.mockResolvedValue(true);
const apiSpy = jest.spyOn(apiService, "putDeleteManyCiphers");
const sdkServiceSpy = jest
.spyOn(cipherSdkService, "softDeleteManyWithServer")
.mockResolvedValue(undefined);
const clearCacheSpy = jest.spyOn(cipherService as any, "clearCache");
await cipherService.softDeleteManyWithServer(testCipherIds, userId);
await cipherService.softDeleteManyWithServer(testCipherIds, userId, false);
expect(mockSdkClient.take).toHaveBeenCalled();
expect(mockCiphersSdk.soft_delete_many).toHaveBeenCalledWith(testCipherIds);
expect(apiSpy).not.toHaveBeenCalled();
expect(sdkServiceSpy).toHaveBeenCalledWith(testCipherIds, userId, false, undefined);
expect(clearCacheSpy).toHaveBeenCalledWith(userId);
});
@@ -1373,14 +1245,14 @@ describe("Cipher Service", () => {
.calledWith(FeatureFlag.PM27632_SdkCipherCrudOperations)
.mockResolvedValue(true);
const apiSpy = jest.spyOn(apiService, "putDeleteManyCiphersAdmin");
const sdkServiceSpy = jest
.spyOn(cipherSdkService, "softDeleteManyWithServer")
.mockResolvedValue(undefined);
const clearCacheSpy = jest.spyOn(cipherService as any, "clearCache");
await cipherService.softDeleteManyWithServer(testCipherIds, userId, true, orgId);
expect(mockSdkClient.take).toHaveBeenCalled();
expect(mockAdminSdk.soft_delete_many).toHaveBeenCalledWith(testCipherIds, orgId);
expect(apiSpy).not.toHaveBeenCalled();
expect(sdkServiceSpy).toHaveBeenCalledWith(testCipherIds, userId, true, orgId);
expect(clearCacheSpy).toHaveBeenCalledWith(userId);
});
});

View File

@@ -1394,7 +1394,7 @@ export class CipherService implements CipherServiceAbstraction {
FeatureFlag.PM27632_SdkCipherCrudOperations,
);
if (useSdk) {
return this.deleteWithServer_sdk(id, userId, asAdmin);
return this.deleteWithServerUsingSdk(id, userId, asAdmin);
}
if (asAdmin) {
@@ -1406,26 +1406,12 @@ export class CipherService implements CipherServiceAbstraction {
await this.delete(id, userId);
}
private async deleteWithServer_sdk(id: string, userId: UserId, asAdmin = false): Promise<any> {
await firstValueFrom(
this.sdkService.userClient$(userId).pipe(
switchMap(async (sdk) => {
if (!sdk) {
throw new Error("SDK not available");
}
using ref = sdk.take();
if (asAdmin) {
await ref.value.vault().ciphers().admin().delete(asUuid(id));
} else {
await ref.value.vault().ciphers().delete(asUuid(id));
}
}),
catchError((error: unknown) => {
this.logService.error(`Failed to delete cipher: ${error}`);
return EMPTY;
}),
),
);
private async deleteWithServerUsingSdk(
id: string,
userId: UserId,
asAdmin = false,
): Promise<any> {
await this.cipherSdkService.deleteWithServer(id, userId, asAdmin);
await this.clearCache(userId);
}
@@ -1439,7 +1425,7 @@ export class CipherService implements CipherServiceAbstraction {
FeatureFlag.PM27632_SdkCipherCrudOperations,
);
if (useSdk) {
return this.deleteManyWithServer_sdk(ids, userId, asAdmin, orgId);
return this.deleteManyWithServerUsingSdk(ids, userId, asAdmin, orgId);
}
const request = new CipherBulkDeleteRequest(ids);
@@ -1451,44 +1437,13 @@ export class CipherService implements CipherServiceAbstraction {
await this.delete(ids, userId);
}
private async deleteManyWithServer_sdk(
private async deleteManyWithServerUsingSdk(
ids: string[],
userId: UserId,
asAdmin = false,
orgId?: OrganizationId,
): Promise<any> {
await firstValueFrom(
this.sdkService.userClient$(userId).pipe(
switchMap(async (sdk) => {
if (!sdk) {
throw new Error("SDK not available");
}
using ref = sdk.take();
if (asAdmin) {
if (orgId == null) {
throw new Error("Organization ID is required for admin delete.");
}
await ref.value
.vault()
.ciphers()
.admin()
.delete_many(
ids.map((id) => asUuid(id)),
asUuid(orgId),
);
} else {
await ref.value
.vault()
.ciphers()
.delete_many(ids.map((id) => asUuid(id)));
}
}),
catchError((error: unknown) => {
this.logService.error(`Failed to delete multiple ciphers: ${error}`);
return EMPTY;
}),
),
);
await this.cipherSdkService.deleteManyWithServer(ids, userId, asAdmin, orgId);
await this.clearCache(userId);
}
@@ -1655,7 +1610,7 @@ export class CipherService implements CipherServiceAbstraction {
FeatureFlag.PM27632_SdkCipherCrudOperations,
);
if (useSdk) {
return this.softDeleteWithServer_sdk(id, userId, asAdmin);
return this.softDeleteWithServerUsingSdk(id, userId, asAdmin);
}
if (asAdmin) {
@@ -1667,26 +1622,12 @@ export class CipherService implements CipherServiceAbstraction {
await this.softDelete(id, userId);
}
async softDeleteWithServer_sdk(id: string, userId: UserId, asAdmin = false): Promise<any> {
await firstValueFrom(
this.sdkService.userClient$(userId).pipe(
switchMap(async (sdk) => {
if (!sdk) {
throw new Error("SDK not available");
}
using ref = sdk.take();
if (asAdmin) {
await ref.value.vault().ciphers().admin().soft_delete(asUuid(id));
} else {
await ref.value.vault().ciphers().soft_delete(asUuid(id));
}
}),
catchError((error: unknown) => {
this.logService.error(`Failed to soft delete cipher: ${error}`);
return EMPTY;
}),
),
);
private async softDeleteWithServerUsingSdk(
id: string,
userId: UserId,
asAdmin = false,
): Promise<any> {
await this.cipherSdkService.softDeleteWithServer(id, userId, asAdmin);
await this.clearCache(userId);
}
@@ -1700,7 +1641,7 @@ export class CipherService implements CipherServiceAbstraction {
FeatureFlag.PM27632_SdkCipherCrudOperations,
);
if (useSdk) {
return this.softDeleteManyWithServer_sdk(ids, userId, asAdmin, orgId);
return this.softDeleteManyWithServerUsingSdk(ids, userId, asAdmin, orgId);
}
const request = new CipherBulkDeleteRequest(ids);
@@ -1713,41 +1654,13 @@ export class CipherService implements CipherServiceAbstraction {
await this.softDelete(ids, userId);
}
async softDeleteManyWithServer_sdk(
private async softDeleteManyWithServerUsingSdk(
ids: string[],
userId: UserId,
asAdmin = false,
orgId?: OrganizationId,
): Promise<any> {
await firstValueFrom(
this.sdkService.userClient$(userId).pipe(
switchMap(async (sdk) => {
if (!sdk) {
throw new Error("SDK not available");
}
using ref = sdk.take();
if (asAdmin) {
await ref.value
.vault()
.ciphers()
.admin()
.soft_delete_many(
ids.map((id) => asUuid(id)),
asUuid(orgId),
);
} else {
await ref.value
.vault()
.ciphers()
.soft_delete_many(ids.map((id) => asUuid(id)));
}
}),
catchError((error: unknown) => {
this.logService.error(`Failed to soft delete multiple ciphers: ${error}`);
return EMPTY;
}),
),
);
await this.cipherSdkService.softDeleteManyWithServer(ids, userId, asAdmin, orgId);
await this.clearCache(userId);
}
@@ -1789,7 +1702,7 @@ export class CipherService implements CipherServiceAbstraction {
FeatureFlag.PM27632_SdkCipherCrudOperations,
);
if (useSdk) {
return await this.restoreWithServer_sdk(id, userId, asAdmin);
return await this.restoreWithServerUsingSdk(id, userId, asAdmin);
}
let response;
@@ -1802,26 +1715,12 @@ export class CipherService implements CipherServiceAbstraction {
await this.restore({ id: id, revisionDate: response.revisionDate }, userId);
}
private async restoreWithServer_sdk(id: string, userId: UserId, asAdmin = false): Promise<any> {
await firstValueFrom(
this.sdkService.userClient$(userId).pipe(
switchMap(async (sdk) => {
if (!sdk) {
throw new Error("SDK not available");
}
using ref = sdk.take();
if (asAdmin) {
await ref.value.vault().ciphers().admin().restore(asUuid(id));
} else {
await ref.value.vault().ciphers().restore(asUuid(id));
}
}),
catchError((error: unknown) => {
this.logService.error(`Failed to restore cipher: ${error}`);
return EMPTY;
}),
),
);
private async restoreWithServerUsingSdk(
id: string,
userId: UserId,
asAdmin = false,
): Promise<any> {
await this.cipherSdkService.restoreWithServer(id, userId, asAdmin);
await this.clearCache(userId);
}
@@ -1834,7 +1733,7 @@ export class CipherService implements CipherServiceAbstraction {
FeatureFlag.PM27632_SdkCipherCrudOperations,
);
if (useSdk) {
return await this.restoreManyWithServer_sdk(ids, userId, orgId);
return await this.restoreManyWithServerUsingSdk(ids, userId, orgId);
}
let response;
@@ -1854,43 +1753,12 @@ export class CipherService implements CipherServiceAbstraction {
await this.restore(restores, userId);
}
private async restoreManyWithServer_sdk(
private async restoreManyWithServerUsingSdk(
ids: string[],
userId: UserId,
orgId?: string,
): Promise<void> {
await firstValueFrom(
this.sdkService.userClient$(userId).pipe(
switchMap(async (sdk) => {
if (!sdk) {
throw new Error("SDK not available");
}
using ref = sdk.take();
// No longer using an asAdmin Param. Org Vault bulkRestore will assess if an item is unassigned or editable
// The Org Vault will pass those ids an array as well as the orgId when calling bulkRestore
if (orgId) {
await ref.value
.vault()
.ciphers()
.admin()
.restore_many(
ids.map((id) => asUuid(id)),
asUuid(orgId),
);
} else {
await ref.value
.vault()
.ciphers()
.restore_many(ids.map((id) => asUuid(id)));
}
}),
catchError((error: unknown) => {
this.logService.error(`Failed to restore multiple ciphers: ${error}`);
return EMPTY;
}),
),
);
await this.cipherSdkService.restoreManyWithServer(ids, userId, orgId);
await this.clearCache(userId);
}