Skip to content

Commit 7b06cd6

Browse files
committed
unit test coverage
1 parent 5f8443c commit 7b06cd6

File tree

1 file changed

+345
-0
lines changed

1 file changed

+345
-0
lines changed
Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
import { ComponentFixture, TestBed } from "@angular/core/testing";
2+
import { FormControl } from "@angular/forms";
3+
import { mock, MockProxy } from "jest-mock-extended";
4+
import { of } from "rxjs";
5+
6+
import { ApiService } from "@bitwarden/common/abstractions/api.service";
7+
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
8+
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
9+
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
10+
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
11+
import {
12+
FakeAccountService,
13+
makeEncString,
14+
makeSymmetricCryptoKey,
15+
mockAccountServiceWith,
16+
} from "@bitwarden/common/spec";
17+
import { UserId } from "@bitwarden/common/types/guid";
18+
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
19+
import { DIALOG_DATA, DialogRef, ToastService } from "@bitwarden/components";
20+
import { KdfType, KeyService, PBKDF2KdfConfig, Argon2KdfConfig } from "@bitwarden/key-management";
21+
22+
import { SharedModule } from "../../shared";
23+
24+
import { ChangeKdfConfirmationComponent } from "./change-kdf-confirmation.component";
25+
26+
import SpyInstance = jest.SpyInstance;
27+
28+
describe("ChangeKdfConfirmationComponent", () => {
29+
let component: ChangeKdfConfirmationComponent;
30+
let fixture: ComponentFixture<ChangeKdfConfirmationComponent>;
31+
32+
// Mock Services
33+
let mockApiService: MockProxy<ApiService>;
34+
let mockI18nService: MockProxy<I18nService>;
35+
let mockKeyService: MockProxy<KeyService>;
36+
let mockMessagingService: MockProxy<MessagingService>;
37+
let mockToastService: MockProxy<ToastService>;
38+
let mockDialogRef: MockProxy<DialogRef<ChangeKdfConfirmationComponent>>;
39+
let mockConfigService: MockProxy<ConfigService>;
40+
let accountService: FakeAccountService;
41+
42+
const mockUserId = "user-id" as UserId;
43+
const mockEmail = "email";
44+
const mockMasterPassword = "master-password";
45+
const mockDialogData = jest.fn();
46+
47+
beforeEach(() => {
48+
mockApiService = mock<ApiService>();
49+
mockI18nService = mock<I18nService>();
50+
mockKeyService = mock<KeyService>();
51+
mockMessagingService = mock<MessagingService>();
52+
mockToastService = mock<ToastService>();
53+
mockDialogRef = mock<DialogRef<ChangeKdfConfirmationComponent>>();
54+
mockConfigService = mock<ConfigService>();
55+
accountService = mockAccountServiceWith(mockUserId, { email: mockEmail });
56+
57+
// Mock i18n service
58+
mockI18nService.t.mockImplementation((key: string) => {
59+
switch (key) {
60+
case "encKeySettingsChanged":
61+
return "Encryption key settings changed";
62+
case "logBackIn":
63+
return "Please log back in";
64+
default:
65+
return key;
66+
}
67+
});
68+
69+
// Mock config service feature flag
70+
mockConfigService.getFeatureFlag$.mockReturnValue(of(false));
71+
72+
mockDialogData.mockReturnValue({
73+
kdf: KdfType.PBKDF2_SHA256,
74+
kdfConfig: new PBKDF2KdfConfig(600_000),
75+
});
76+
77+
TestBed.configureTestingModule({
78+
declarations: [ChangeKdfConfirmationComponent],
79+
imports: [SharedModule],
80+
providers: [
81+
{ provide: ApiService, useValue: mockApiService },
82+
{ provide: I18nService, useValue: mockI18nService },
83+
{ provide: KeyService, useValue: mockKeyService },
84+
{ provide: MessagingService, useValue: mockMessagingService },
85+
{ provide: AccountService, useValue: accountService },
86+
{ provide: ToastService, useValue: mockToastService },
87+
{ provide: DialogRef, useValue: mockDialogRef },
88+
{ provide: ConfigService, useValue: mockConfigService },
89+
{
90+
provide: DIALOG_DATA,
91+
useFactory: mockDialogData,
92+
},
93+
],
94+
});
95+
});
96+
97+
describe("Component Initialization", () => {
98+
it("should create component with PBKDF2 config", () => {
99+
mockDialogData.mockReturnValue({
100+
kdf: KdfType.PBKDF2_SHA256,
101+
kdfConfig: new PBKDF2KdfConfig(600_001),
102+
});
103+
104+
const fixture = TestBed.createComponent(ChangeKdfConfirmationComponent);
105+
const component = fixture.componentInstance;
106+
107+
expect(component).toBeTruthy();
108+
expect(component.kdfConfig).toBeInstanceOf(PBKDF2KdfConfig);
109+
expect(component.kdfConfig.iterations).toBe(600_001);
110+
});
111+
112+
it("should create component with Argon2id config", () => {
113+
mockDialogData.mockReturnValue({
114+
kdf: KdfType.Argon2id,
115+
kdfConfig: new Argon2KdfConfig(4, 65, 5),
116+
});
117+
118+
const fixture = TestBed.createComponent(ChangeKdfConfirmationComponent);
119+
const component = fixture.componentInstance;
120+
121+
expect(component).toBeTruthy();
122+
expect(component.kdfConfig).toBeInstanceOf(Argon2KdfConfig);
123+
const kdfConfig = component.kdfConfig as Argon2KdfConfig;
124+
expect(kdfConfig.iterations).toBe(4);
125+
expect(kdfConfig.memory).toBe(65);
126+
expect(kdfConfig.parallelism).toBe(5);
127+
});
128+
129+
it("should initialize form with required master password field", () => {
130+
const fixture = TestBed.createComponent(ChangeKdfConfirmationComponent);
131+
const component = fixture.componentInstance;
132+
133+
expect(component.form.get("masterPassword")?.value).toEqual(null);
134+
expect(component.form.get("masterPassword")).toBeInstanceOf(FormControl);
135+
expect(component.form.get("masterPassword")?.hasError("required")).toBe(true);
136+
});
137+
});
138+
139+
describe("Form Validation", () => {
140+
beforeEach(() => {
141+
fixture = TestBed.createComponent(ChangeKdfConfirmationComponent);
142+
component = fixture.componentInstance;
143+
});
144+
145+
it("should be invalid when master password is empty", () => {
146+
component.form.get("masterPassword")?.setValue("");
147+
expect(component.form.invalid).toBe(true);
148+
});
149+
150+
it("should be valid when master password is provided", () => {
151+
component.form.get("masterPassword")?.setValue(mockMasterPassword);
152+
expect(component.form.valid).toBe(true);
153+
});
154+
});
155+
156+
describe("submit method", () => {
157+
let makeKeyAndSaveSpy: SpyInstance<Promise<void>>;
158+
beforeEach(() => {
159+
fixture = TestBed.createComponent(ChangeKdfConfirmationComponent);
160+
component = fixture.componentInstance;
161+
162+
makeKeyAndSaveSpy = jest.spyOn(component, "makeKeyAndSave");
163+
makeKeyAndSaveSpy.mockImplementation();
164+
});
165+
166+
describe("when form is invalid", () => {
167+
it("should return early without processing", async () => {
168+
// Arrange
169+
component.form.get("masterPassword")?.setValue("");
170+
expect(component.form.invalid).toBe(true);
171+
172+
// Act
173+
await component.submit();
174+
175+
// Assert
176+
expect(makeKeyAndSaveSpy).not.toHaveBeenCalled();
177+
});
178+
});
179+
180+
describe("when form is valid", () => {
181+
beforeEach(() => {
182+
component.form.get("masterPassword")?.setValue(mockMasterPassword);
183+
});
184+
185+
it("should set loading to true during submission", async () => {
186+
// Arrange
187+
let loadingDuringExecution = false;
188+
makeKeyAndSaveSpy.mockImplementation(async () => {
189+
loadingDuringExecution = component.loading;
190+
});
191+
192+
// Act
193+
await component.submit();
194+
195+
expect(loadingDuringExecution).toBe(true);
196+
expect(component.loading).toBe(false);
197+
});
198+
199+
it("should call makeKeyAndSaveAsync", async () => {
200+
// Act
201+
await component.submit();
202+
203+
// Assert
204+
expect(makeKeyAndSaveSpy).toHaveBeenCalledTimes(1);
205+
});
206+
207+
describe("when ForceUpdateKDFSettings feature flag is enabled", () => {
208+
it("should show success toast and close dialog", async () => {
209+
// Arrange - reset mocks and create component with feature flag enabled
210+
mockConfigService.getFeatureFlag$.mockReturnValue(of(true));
211+
212+
const fixture = TestBed.createComponent(ChangeKdfConfirmationComponent);
213+
const component = fixture.componentInstance;
214+
215+
makeKeyAndSaveSpy = jest.spyOn(component, "makeKeyAndSave");
216+
makeKeyAndSaveSpy.mockImplementation();
217+
218+
component.form.get("masterPassword")?.setValue(mockMasterPassword);
219+
220+
// Act
221+
await component.submit();
222+
223+
// Assert
224+
expect(mockToastService.showToast).toHaveBeenCalledWith({
225+
variant: "success",
226+
message: "Encryption key settings changed",
227+
});
228+
expect(mockDialogRef.close).toHaveBeenCalled();
229+
expect(mockMessagingService.send).not.toHaveBeenCalled();
230+
});
231+
});
232+
233+
describe("when ForceUpdateKDFSettings feature flag is disabled", () => {
234+
it("should show toast with logout message and send logout", async () => {
235+
// Act
236+
await component.submit();
237+
238+
// Assert
239+
expect(mockToastService.showToast).toHaveBeenCalledWith({
240+
variant: "success",
241+
title: "Encryption key settings changed",
242+
message: "Please log back in",
243+
});
244+
expect(mockMessagingService.send).toHaveBeenCalledWith("logout");
245+
expect(mockDialogRef.close).not.toHaveBeenCalled();
246+
});
247+
});
248+
});
249+
});
250+
251+
describe("makeKeyAndSaveAsync", () => {
252+
const kdfConfig = new PBKDF2KdfConfig();
253+
const validateKdfConfigForSetting = jest.spyOn(kdfConfig, "validateKdfConfigForSetting");
254+
const mockMasterKey = makeSymmetricCryptoKey(64) as MasterKey;
255+
const mockNewMasterKey = makeSymmetricCryptoKey(64) as MasterKey;
256+
const mockNewUserKey = makeSymmetricCryptoKey(64) as UserKey;
257+
const mockNewUserKeyEncrypted = makeEncString("user-key");
258+
const mockMasterPasswordHash = "master-password-hash";
259+
const mockNewMasterPasswordHash = "new-master-password-hash";
260+
261+
beforeEach(() => {
262+
fixture = TestBed.createComponent(ChangeKdfConfirmationComponent);
263+
component = fixture.componentInstance;
264+
265+
component.kdfConfig = kdfConfig;
266+
267+
component.form.get("masterPassword")?.setValue(mockMasterPassword);
268+
269+
mockKeyService.getOrDeriveMasterKey.mockResolvedValue(mockMasterKey);
270+
mockKeyService.hashMasterKey
271+
.mockResolvedValueOnce(mockMasterPasswordHash)
272+
.mockResolvedValueOnce(mockNewMasterPasswordHash);
273+
mockKeyService.makeMasterKey.mockResolvedValue(mockNewMasterKey);
274+
mockKeyService.encryptUserKeyWithMasterKey.mockResolvedValue([
275+
mockNewUserKey,
276+
mockNewUserKeyEncrypted,
277+
]);
278+
});
279+
280+
it("should throw error when no active account", async () => {
281+
accountService.activeAccount$ = of(null);
282+
283+
await expect(component.makeKeyAndSave()).rejects.toThrow("No active account found.");
284+
});
285+
286+
it("should throw error when KDF validation failed", async () => {
287+
validateKdfConfigForSetting.mockImplementation(() => {
288+
throw new Error("KDF config invalid");
289+
});
290+
component.kdfConfig = kdfConfig;
291+
292+
await expect(component.makeKeyAndSave()).rejects.toThrow("KDF config invalid");
293+
});
294+
295+
it.each([new PBKDF2KdfConfig(600_001), new Argon2KdfConfig(4, 65, 5)])(
296+
"should post KDF request to API when kdf = %s",
297+
async (kdfConfig) => {
298+
// Arrange
299+
component.kdfConfig = kdfConfig;
300+
const expectedRequest = {
301+
kdf: kdfConfig.kdfType,
302+
kdfIterations: kdfConfig.iterations,
303+
kdfMemory: kdfConfig instanceof Argon2KdfConfig ? kdfConfig.memory : undefined,
304+
kdfParallelism: kdfConfig instanceof Argon2KdfConfig ? kdfConfig.parallelism : undefined,
305+
masterPasswordHash: mockMasterPasswordHash,
306+
newMasterPasswordHash: mockNewMasterPasswordHash,
307+
key: mockNewUserKeyEncrypted.encryptedString,
308+
};
309+
if (kdfConfig instanceof PBKDF2KdfConfig) {
310+
delete expectedRequest.kdfMemory;
311+
delete expectedRequest.kdfParallelism;
312+
}
313+
314+
// Act
315+
await component.makeKeyAndSave();
316+
317+
// Assert
318+
expect(validateKdfConfigForSetting).toHaveBeenCalled();
319+
expect(mockKeyService.getOrDeriveMasterKey).toHaveBeenCalledWith(
320+
mockMasterPassword,
321+
mockUserId,
322+
);
323+
expect(mockKeyService.hashMasterKey).toHaveBeenNthCalledWith(
324+
1,
325+
mockMasterPassword,
326+
mockMasterKey,
327+
);
328+
expect(mockKeyService.makeMasterKey).toHaveBeenCalledWith(
329+
mockMasterPassword,
330+
mockEmail,
331+
kdfConfig,
332+
);
333+
expect(mockKeyService.hashMasterKey).toHaveBeenNthCalledWith(
334+
2,
335+
mockMasterPassword,
336+
mockNewMasterKey,
337+
);
338+
expect(mockKeyService.encryptUserKeyWithMasterKey).toHaveBeenCalledWith(mockNewMasterKey);
339+
expect(mockApiService.postAccountKdf).toHaveBeenCalledWith(
340+
expect.objectContaining(expectedRequest),
341+
);
342+
},
343+
);
344+
});
345+
});

0 commit comments

Comments
 (0)