1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 07:43:35 +00:00

fix(EmergencyAccess): [Auth/PM-23860] - Restore contact removal functionality (#15666)

* PM-23860 - EmergencyAccessService - convert types from response model to actual constructed type to avoid structural typing issue at runtime

* PM-23860 - EmergencyAccessService tests - add tests to both methods to prevent this from being possible again.
This commit is contained in:
Jared Snider
2025-07-18 09:47:17 -04:00
committed by GitHub
parent f56e05404b
commit 93f7148289
3 changed files with 183 additions and 8 deletions

View File

@@ -5,6 +5,10 @@ import { KdfType } from "@bitwarden/key-management";
import { EmergencyAccessStatusType } from "../enums/emergency-access-status-type";
import { EmergencyAccessType } from "../enums/emergency-access-type";
import {
EmergencyAccessGranteeDetailsResponse,
EmergencyAccessGrantorDetailsResponse,
} from "../response/emergency-access.response";
export class GranteeEmergencyAccess {
id: string;
@@ -16,6 +20,24 @@ export class GranteeEmergencyAccess {
waitTimeDays: number;
creationDate: string;
avatarColor: string;
constructor(partial: Partial<GranteeEmergencyAccess> = {}) {
Object.assign(this, partial);
}
static fromResponse(response: EmergencyAccessGranteeDetailsResponse) {
return new GranteeEmergencyAccess({
id: response.id,
granteeId: response.granteeId,
name: response.name,
email: response.email,
type: response.type,
status: response.status,
waitTimeDays: response.waitTimeDays,
creationDate: response.creationDate,
avatarColor: response.avatarColor,
});
}
}
export class GrantorEmergencyAccess {
@@ -28,6 +50,24 @@ export class GrantorEmergencyAccess {
waitTimeDays: number;
creationDate: string;
avatarColor: string;
constructor(partial: Partial<GrantorEmergencyAccess> = {}) {
Object.assign(this, partial);
}
static fromResponse(response: EmergencyAccessGrantorDetailsResponse) {
return new GrantorEmergencyAccess({
id: response.id,
grantorId: response.grantorId,
name: response.name,
email: response.email,
type: response.type,
status: response.status,
waitTimeDays: response.waitTimeDays,
creationDate: response.creationDate,
avatarColor: response.avatarColor,
});
}
}
export class TakeoverTypeEmergencyAccess {

View File

@@ -22,9 +22,11 @@ import { KdfType, KeyService } from "@bitwarden/key-management";
import { EmergencyAccessStatusType } from "../enums/emergency-access-status-type";
import { EmergencyAccessType } from "../enums/emergency-access-type";
import { GranteeEmergencyAccess, GrantorEmergencyAccess } from "../models/emergency-access";
import { EmergencyAccessPasswordRequest } from "../request/emergency-access-password.request";
import {
EmergencyAccessGranteeDetailsResponse,
EmergencyAccessGrantorDetailsResponse,
EmergencyAccessTakeoverResponse,
} from "../response/emergency-access.response";
@@ -242,11 +244,19 @@ describe("EmergencyAccessService", () => {
const mockEmergencyAccess = {
data: [
createMockEmergencyAccess("0", "EA 0", EmergencyAccessStatusType.Invited),
createMockEmergencyAccess("1", "EA 1", EmergencyAccessStatusType.Accepted),
createMockEmergencyAccess("2", "EA 2", EmergencyAccessStatusType.Confirmed),
createMockEmergencyAccess("3", "EA 3", EmergencyAccessStatusType.RecoveryInitiated),
createMockEmergencyAccess("4", "EA 4", EmergencyAccessStatusType.RecoveryApproved),
createMockEmergencyAccessGranteeDetails("0", "EA 0", EmergencyAccessStatusType.Invited),
createMockEmergencyAccessGranteeDetails("1", "EA 1", EmergencyAccessStatusType.Accepted),
createMockEmergencyAccessGranteeDetails("2", "EA 2", EmergencyAccessStatusType.Confirmed),
createMockEmergencyAccessGranteeDetails(
"3",
"EA 3",
EmergencyAccessStatusType.RecoveryInitiated,
),
createMockEmergencyAccessGranteeDetails(
"4",
"EA 4",
EmergencyAccessStatusType.RecoveryApproved,
),
],
} as ListResponse<EmergencyAccessGranteeDetailsResponse>;
@@ -295,9 +305,113 @@ describe("EmergencyAccessService", () => {
).rejects.toThrow("New user key is required for rotation.");
});
});
describe("getEmergencyAccessTrusted", () => {
it("should return an empty array if no emergency access is granted", async () => {
emergencyAccessApiService.getEmergencyAccessTrusted.mockResolvedValue({
data: [],
} as ListResponse<EmergencyAccessGranteeDetailsResponse>);
const result = await emergencyAccessService.getEmergencyAccessTrusted();
expect(result).toEqual([]);
});
it("should return an empty array if the API returns an empty response", async () => {
emergencyAccessApiService.getEmergencyAccessTrusted.mockResolvedValue(
null as unknown as ListResponse<EmergencyAccessGranteeDetailsResponse>,
);
const result = await emergencyAccessService.getEmergencyAccessTrusted();
expect(result).toEqual([]);
});
it("should return a list of trusted emergency access contacts", async () => {
const mockEmergencyAccess = [
createMockEmergencyAccessGranteeDetails("1", "EA 1", EmergencyAccessStatusType.Invited),
createMockEmergencyAccessGranteeDetails("2", "EA 2", EmergencyAccessStatusType.Invited),
createMockEmergencyAccessGranteeDetails("3", "EA 3", EmergencyAccessStatusType.Accepted),
createMockEmergencyAccessGranteeDetails("4", "EA 4", EmergencyAccessStatusType.Confirmed),
createMockEmergencyAccessGranteeDetails(
"5",
"EA 5",
EmergencyAccessStatusType.RecoveryInitiated,
),
];
emergencyAccessApiService.getEmergencyAccessTrusted.mockResolvedValue({
data: mockEmergencyAccess,
} as ListResponse<EmergencyAccessGranteeDetailsResponse>);
const result = await emergencyAccessService.getEmergencyAccessTrusted();
expect(result).toHaveLength(mockEmergencyAccess.length);
result.forEach((access, index) => {
expect(access).toBeInstanceOf(GranteeEmergencyAccess);
expect(access.id).toBe(mockEmergencyAccess[index].id);
expect(access.name).toBe(mockEmergencyAccess[index].name);
expect(access.status).toBe(mockEmergencyAccess[index].status);
expect(access.type).toBe(mockEmergencyAccess[index].type);
});
});
});
describe("getEmergencyAccessGranted", () => {
it("should return an empty array if no emergency access is granted", async () => {
emergencyAccessApiService.getEmergencyAccessGranted.mockResolvedValue({
data: [],
} as ListResponse<EmergencyAccessGrantorDetailsResponse>);
const result = await emergencyAccessService.getEmergencyAccessGranted();
expect(result).toEqual([]);
});
it("should return an empty array if the API returns an empty response", async () => {
emergencyAccessApiService.getEmergencyAccessGranted.mockResolvedValue(
null as unknown as ListResponse<EmergencyAccessGrantorDetailsResponse>,
);
const result = await emergencyAccessService.getEmergencyAccessGranted();
expect(result).toEqual([]);
});
it("should return a list of granted emergency access contacts", async () => {
const mockEmergencyAccess = [
createMockEmergencyAccessGrantorDetails("1", "EA 1", EmergencyAccessStatusType.Invited),
createMockEmergencyAccessGrantorDetails("2", "EA 2", EmergencyAccessStatusType.Invited),
createMockEmergencyAccessGrantorDetails("3", "EA 3", EmergencyAccessStatusType.Accepted),
createMockEmergencyAccessGrantorDetails("4", "EA 4", EmergencyAccessStatusType.Confirmed),
createMockEmergencyAccessGrantorDetails(
"5",
"EA 5",
EmergencyAccessStatusType.RecoveryInitiated,
),
];
emergencyAccessApiService.getEmergencyAccessGranted.mockResolvedValue({
data: mockEmergencyAccess,
} as ListResponse<EmergencyAccessGrantorDetailsResponse>);
const result = await emergencyAccessService.getEmergencyAccessGranted();
expect(result).toHaveLength(mockEmergencyAccess.length);
result.forEach((access, index) => {
expect(access).toBeInstanceOf(GrantorEmergencyAccess);
expect(access.id).toBe(mockEmergencyAccess[index].id);
expect(access.name).toBe(mockEmergencyAccess[index].name);
expect(access.status).toBe(mockEmergencyAccess[index].status);
expect(access.type).toBe(mockEmergencyAccess[index].type);
});
});
});
});
function createMockEmergencyAccess(
function createMockEmergencyAccessGranteeDetails(
id: string,
name: string,
status: EmergencyAccessStatusType,
@@ -309,3 +423,16 @@ function createMockEmergencyAccess(
emergencyAccess.status = status;
return emergencyAccess;
}
function createMockEmergencyAccessGrantorDetails(
id: string,
name: string,
status: EmergencyAccessStatusType,
): EmergencyAccessGrantorDetailsResponse {
const emergencyAccess = new EmergencyAccessGrantorDetailsResponse({});
emergencyAccess.id = id;
emergencyAccess.name = name;
emergencyAccess.type = 0;
emergencyAccess.status = status;
return emergencyAccess;
}

View File

@@ -77,14 +77,22 @@ export class EmergencyAccessService
* Gets all emergency access that the user has been granted.
*/
async getEmergencyAccessTrusted(): Promise<GranteeEmergencyAccess[]> {
return (await this.emergencyAccessApiService.getEmergencyAccessTrusted()).data;
const listResponse = await this.emergencyAccessApiService.getEmergencyAccessTrusted();
if (!listResponse || listResponse.data.length === 0) {
return [];
}
return listResponse.data.map((response) => GranteeEmergencyAccess.fromResponse(response));
}
/**
* Gets all emergency access that the user has granted.
*/
async getEmergencyAccessGranted(): Promise<GrantorEmergencyAccess[]> {
return (await this.emergencyAccessApiService.getEmergencyAccessGranted()).data;
const listResponse = await this.emergencyAccessApiService.getEmergencyAccessGranted();
if (!listResponse || listResponse.data.length === 0) {
return [];
}
return listResponse.data.map((response) => GrantorEmergencyAccess.fromResponse(response));
}
/**