mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 22:03:36 +00:00
[PM-17635] [PM-18601] Simplifying mocking and usage of the sdk (#14287)
* feat: add our own custom deep mocker * feat: use new mock service in totp tests * feat: implement userClient mocking * chore: move mock files * feat: replace existing manual sdkService mocking * chore: rename to 'client' * chore: improve docs * feat: refactor sdkService to never return undefined BitwardenClient
This commit is contained in:
@@ -53,15 +53,18 @@ export abstract class SdkService {
|
|||||||
* This client can be used for operations that require a user context, such as retrieving ciphers
|
* This client can be used for operations that require a user context, such as retrieving ciphers
|
||||||
* and operations involving crypto. It can also be used for operations that don't require a user context.
|
* and operations involving crypto. It can also be used for operations that don't require a user context.
|
||||||
*
|
*
|
||||||
|
* - If the user is not logged when the subscription is created, the observable will complete
|
||||||
|
* immediately with {@link UserNotLoggedInError}.
|
||||||
|
* - If the user is logged in, the observable will emit the client and complete whithout an error
|
||||||
|
* when the user logs out.
|
||||||
|
*
|
||||||
* **WARNING:** Do not use `firstValueFrom(userClient$)`! Any operations on the client must be done within the observable.
|
* **WARNING:** Do not use `firstValueFrom(userClient$)`! Any operations on the client must be done within the observable.
|
||||||
* The client will be destroyed when the observable is no longer subscribed to.
|
* The client will be destroyed when the observable is no longer subscribed to.
|
||||||
* Please let platform know if you need a client that is not destroyed when the observable is no longer subscribed to.
|
* Please let platform know if you need a client that is not destroyed when the observable is no longer subscribed to.
|
||||||
*
|
*
|
||||||
* @param userId The user id for which to retrieve the client
|
* @param userId The user id for which to retrieve the client
|
||||||
*
|
|
||||||
* @throws {UserNotLoggedInError} If the user is not logged in
|
|
||||||
*/
|
*/
|
||||||
abstract userClient$(userId: UserId): Observable<Rc<BitwardenClient> | undefined>;
|
abstract userClient$(userId: UserId): Observable<Rc<BitwardenClient>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method is used during/after an authentication procedure to set a new client for a specific user.
|
* This method is used during/after an authentication procedure to set a new client for a specific user.
|
||||||
|
|||||||
@@ -132,15 +132,13 @@ describe("DefaultSdkService", () => {
|
|||||||
);
|
);
|
||||||
keyService.userKey$.calledWith(userId).mockReturnValue(userKey$);
|
keyService.userKey$.calledWith(userId).mockReturnValue(userKey$);
|
||||||
|
|
||||||
const subject = new BehaviorSubject<Rc<BitwardenClient> | undefined>(undefined);
|
const userClientTracker = new ObservableTracker(service.userClient$(userId), false);
|
||||||
service.userClient$(userId).subscribe(subject);
|
await userClientTracker.pauseUntilReceived(1);
|
||||||
await new Promise(process.nextTick);
|
|
||||||
|
|
||||||
userKey$.next(undefined);
|
userKey$.next(undefined);
|
||||||
await new Promise(process.nextTick);
|
await userClientTracker.expectCompletion();
|
||||||
|
|
||||||
expect(mockClient.free).toHaveBeenCalledTimes(1);
|
expect(mockClient.free).toHaveBeenCalledTimes(1);
|
||||||
expect(subject.value).toBe(undefined);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export class DefaultSdkService implements SdkService {
|
|||||||
private userAgent: string | null = null,
|
private userAgent: string | null = null,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
userClient$(userId: UserId): Observable<Rc<BitwardenClient> | undefined> {
|
userClient$(userId: UserId): Observable<Rc<BitwardenClient>> {
|
||||||
return this.sdkClientOverrides.pipe(
|
return this.sdkClientOverrides.pipe(
|
||||||
takeWhile((clients) => clients[userId] !== UnsetClient, false),
|
takeWhile((clients) => clients[userId] !== UnsetClient, false),
|
||||||
map((clients) => {
|
map((clients) => {
|
||||||
@@ -88,6 +88,7 @@ export class DefaultSdkService implements SdkService {
|
|||||||
|
|
||||||
return this.internalClient$(userId);
|
return this.internalClient$(userId);
|
||||||
}),
|
}),
|
||||||
|
takeWhile((client) => client !== undefined, false),
|
||||||
throwIfEmpty(() => new UserNotLoggedInError(userId)),
|
throwIfEmpty(() => new UserNotLoggedInError(userId)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -112,7 +113,7 @@ export class DefaultSdkService implements SdkService {
|
|||||||
* @param userId The user id for which to create the client
|
* @param userId The user id for which to create the client
|
||||||
* @returns An observable that emits the client for the user
|
* @returns An observable that emits the client for the user
|
||||||
*/
|
*/
|
||||||
private internalClient$(userId: UserId): Observable<Rc<BitwardenClient> | undefined> {
|
private internalClient$(userId: UserId): Observable<Rc<BitwardenClient>> {
|
||||||
const cached = this.sdkClientCache.get(userId);
|
const cached = this.sdkClientCache.get(userId);
|
||||||
if (cached !== undefined) {
|
if (cached !== undefined) {
|
||||||
return cached;
|
return cached;
|
||||||
|
|||||||
58
libs/common/src/platform/spec/mock-deep.spec.ts
Normal file
58
libs/common/src/platform/spec/mock-deep.spec.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { mockDeep } from "./mock-deep";
|
||||||
|
|
||||||
|
class ToBeMocked {
|
||||||
|
property = "value";
|
||||||
|
|
||||||
|
method() {
|
||||||
|
return "method";
|
||||||
|
}
|
||||||
|
|
||||||
|
sub() {
|
||||||
|
return new SubToBeMocked();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SubToBeMocked {
|
||||||
|
subProperty = "subValue";
|
||||||
|
|
||||||
|
sub() {
|
||||||
|
return new SubSubToBeMocked();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SubSubToBeMocked {
|
||||||
|
subSubProperty = "subSubValue";
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("deepMock", () => {
|
||||||
|
it("can mock properties", () => {
|
||||||
|
const mock = mockDeep<ToBeMocked>();
|
||||||
|
mock.property.replaceProperty("mocked value");
|
||||||
|
expect(mock.property).toBe("mocked value");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can mock methods", () => {
|
||||||
|
const mock = mockDeep<ToBeMocked>();
|
||||||
|
mock.method.mockReturnValue("mocked method");
|
||||||
|
expect(mock.method()).toBe("mocked method");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can mock sub-properties", () => {
|
||||||
|
const mock = mockDeep<ToBeMocked>();
|
||||||
|
mock.sub.mockDeep().subProperty.replaceProperty("mocked sub value");
|
||||||
|
expect(mock.sub().subProperty).toBe("mocked sub value");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can mock sub-sub-properties", () => {
|
||||||
|
const mock = mockDeep<ToBeMocked>();
|
||||||
|
mock.sub.mockDeep().sub.mockDeep().subSubProperty.replaceProperty("mocked sub-sub value");
|
||||||
|
expect(mock.sub().sub().subSubProperty).toBe("mocked sub-sub value");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns the same mock object when calling mockDeep multiple times", () => {
|
||||||
|
const mock = mockDeep<ToBeMocked>();
|
||||||
|
const subMock1 = mock.sub.mockDeep();
|
||||||
|
const subMock2 = mock.sub.mockDeep();
|
||||||
|
expect(subMock1).toBe(subMock2);
|
||||||
|
});
|
||||||
|
});
|
||||||
271
libs/common/src/platform/spec/mock-deep.ts
Normal file
271
libs/common/src/platform/spec/mock-deep.ts
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
// This is a modification of the code found in https://github.com/marchaos/jest-mock-extended
|
||||||
|
// to better support deep mocking of objects.
|
||||||
|
|
||||||
|
// MIT License
|
||||||
|
|
||||||
|
// Copyright (c) 2019 Marc McIntyre
|
||||||
|
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
// of this software and associated documentation files (the "Software"), to deal
|
||||||
|
// in the Software without restriction, including without limitation the rights
|
||||||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
// copies of the Software, and to permit persons to whom the Software is
|
||||||
|
// furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
// The above copyright notice and this permission notice shall be included in all
|
||||||
|
// copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
// SOFTWARE.
|
||||||
|
|
||||||
|
import { jest } from "@jest/globals";
|
||||||
|
import { FunctionLike } from "jest-mock";
|
||||||
|
import { calledWithFn, MatchersOrLiterals } from "jest-mock-extended";
|
||||||
|
import { PartialDeep } from "type-fest";
|
||||||
|
|
||||||
|
type ProxiedProperty = string | number | symbol;
|
||||||
|
|
||||||
|
export interface GlobalConfig {
|
||||||
|
// ignoreProps is required when we don't want to return anything for a mock (for example, when mocking a promise).
|
||||||
|
ignoreProps?: ProxiedProperty[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_CONFIG: GlobalConfig = {
|
||||||
|
ignoreProps: ["then"],
|
||||||
|
};
|
||||||
|
|
||||||
|
let GLOBAL_CONFIG = DEFAULT_CONFIG;
|
||||||
|
|
||||||
|
export const JestMockExtended = {
|
||||||
|
DEFAULT_CONFIG,
|
||||||
|
configure: (config: GlobalConfig) => {
|
||||||
|
// Shallow merge so they can override anything they want.
|
||||||
|
GLOBAL_CONFIG = { ...DEFAULT_CONFIG, ...config };
|
||||||
|
},
|
||||||
|
resetConfig: () => {
|
||||||
|
GLOBAL_CONFIG = DEFAULT_CONFIG;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface CalledWithMock<T extends FunctionLike> extends jest.Mock<T> {
|
||||||
|
calledWith: (...args: [...MatchersOrLiterals<Parameters<T>>]) => jest.Mock<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MockDeepMock<R> {
|
||||||
|
mockDeep: () => DeepMockProxy<R>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReplaceProperty<T> {
|
||||||
|
/**
|
||||||
|
* mockDeep will by default return a jest.fn() for all properties,
|
||||||
|
* but this allows you to replace the property with a value.
|
||||||
|
* @param value The value to replace the property with.
|
||||||
|
*/
|
||||||
|
replaceProperty(value: T): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type _MockProxy<T> = {
|
||||||
|
[K in keyof T]: T[K] extends FunctionLike ? T[K] & CalledWithMock<T[K]> : T[K];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MockProxy<T> = _MockProxy<T> & T;
|
||||||
|
|
||||||
|
export type _DeepMockProxy<T> = {
|
||||||
|
// This supports deep mocks in the else branch
|
||||||
|
[K in keyof T]: T[K] extends (...args: infer A) => infer R
|
||||||
|
? T[K] & CalledWithMock<T[K]> & MockDeepMock<R>
|
||||||
|
: T[K] & ReplaceProperty<T[K]> & _DeepMockProxy<T[K]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// we intersect with T here instead of on the mapped type above to
|
||||||
|
// prevent immediate type resolution on a recursive type, this will
|
||||||
|
// help to improve performance for deeply nested recursive mocking
|
||||||
|
// at the same time, this intersection preserves private properties
|
||||||
|
export type DeepMockProxy<T> = _DeepMockProxy<T> & T;
|
||||||
|
|
||||||
|
export type _DeepMockProxyWithFuncPropSupport<T> = {
|
||||||
|
// This supports deep mocks in the else branch
|
||||||
|
[K in keyof T]: T[K] extends FunctionLike
|
||||||
|
? CalledWithMock<T[K]> & DeepMockProxy<T[K]>
|
||||||
|
: DeepMockProxy<T[K]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DeepMockProxyWithFuncPropSupport<T> = _DeepMockProxyWithFuncPropSupport<T> & T;
|
||||||
|
|
||||||
|
export interface MockOpts {
|
||||||
|
deep?: boolean;
|
||||||
|
fallbackMockImplementation?: (...args: any[]) => any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mockClear = (mock: MockProxy<any>) => {
|
||||||
|
for (const key of Object.keys(mock)) {
|
||||||
|
if (mock[key] === null || mock[key] === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mock[key]._isMockObject) {
|
||||||
|
mockClear(mock[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mock[key]._isMockFunction) {
|
||||||
|
mock[key].mockClear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a catch for if they pass in a jest.fn()
|
||||||
|
if (!mock._isMockObject) {
|
||||||
|
return mock.mockClear();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mockReset = (mock: MockProxy<any>) => {
|
||||||
|
for (const key of Object.keys(mock)) {
|
||||||
|
if (mock[key] === null || mock[key] === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mock[key]._isMockObject) {
|
||||||
|
mockReset(mock[key]);
|
||||||
|
}
|
||||||
|
if (mock[key]._isMockFunction) {
|
||||||
|
mock[key].mockReset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a catch for if they pass in a jest.fn()
|
||||||
|
// Worst case, we will create a jest.fn() (since this is a proxy)
|
||||||
|
// below in the get and call mockReset on it
|
||||||
|
if (!mock._isMockObject) {
|
||||||
|
return mock.mockReset();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export function mockDeep<T>(
|
||||||
|
opts: {
|
||||||
|
funcPropSupport?: true;
|
||||||
|
fallbackMockImplementation?: MockOpts["fallbackMockImplementation"];
|
||||||
|
},
|
||||||
|
mockImplementation?: PartialDeep<T>,
|
||||||
|
): DeepMockProxyWithFuncPropSupport<T>;
|
||||||
|
export function mockDeep<T>(mockImplementation?: PartialDeep<T>): DeepMockProxy<T>;
|
||||||
|
export function mockDeep(arg1: any, arg2?: any) {
|
||||||
|
const [opts, mockImplementation] =
|
||||||
|
typeof arg1 === "object" &&
|
||||||
|
(typeof arg1.fallbackMockImplementation === "function" || arg1.funcPropSupport === true)
|
||||||
|
? [arg1, arg2]
|
||||||
|
: [{}, arg1];
|
||||||
|
return mock(mockImplementation, {
|
||||||
|
deep: true,
|
||||||
|
fallbackMockImplementation: opts.fallbackMockImplementation,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const overrideMockImp = (obj: PartialDeep<any>, opts?: MockOpts) => {
|
||||||
|
const proxy = new Proxy<MockProxy<any>>(obj, handler(opts));
|
||||||
|
for (const name of Object.keys(obj)) {
|
||||||
|
if (typeof obj[name] === "object" && obj[name] !== null) {
|
||||||
|
proxy[name] = overrideMockImp(obj[name], opts);
|
||||||
|
} else {
|
||||||
|
proxy[name] = obj[name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return proxy;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handler = (opts?: MockOpts): ProxyHandler<any> => ({
|
||||||
|
ownKeys(target: MockProxy<any>) {
|
||||||
|
return Reflect.ownKeys(target);
|
||||||
|
},
|
||||||
|
|
||||||
|
set: (obj: MockProxy<any>, property: ProxiedProperty, value: any) => {
|
||||||
|
obj[property] = value;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
get: (obj: MockProxy<any>, property: ProxiedProperty) => {
|
||||||
|
const fn = calledWithFn({ fallbackMockImplementation: opts?.fallbackMockImplementation });
|
||||||
|
|
||||||
|
if (!(property in obj)) {
|
||||||
|
if (GLOBAL_CONFIG.ignoreProps?.includes(property)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
// Jest's internal equality checking does some wierd stuff to check for iterable equality
|
||||||
|
if (property === Symbol.iterator) {
|
||||||
|
return obj[property];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (property === "_deepMock") {
|
||||||
|
return obj[property];
|
||||||
|
}
|
||||||
|
// So this calls check here is totally not ideal - jest internally does a
|
||||||
|
// check to see if this is a spy - which we want to say no to, but blindly returning
|
||||||
|
// an proxy for calls results in the spy check returning true. This is another reason
|
||||||
|
// why deep is opt in.
|
||||||
|
if (opts?.deep && property !== "calls") {
|
||||||
|
obj[property] = new Proxy<MockProxy<any>>(fn, handler(opts));
|
||||||
|
obj[property].replaceProperty = <T extends typeof obj, K extends keyof T>(value: T[K]) => {
|
||||||
|
obj[property] = value;
|
||||||
|
};
|
||||||
|
obj[property].mockDeep = () => {
|
||||||
|
if (obj[property]._deepMock) {
|
||||||
|
return obj[property]._deepMock;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mock = mockDeep({
|
||||||
|
fallbackMockImplementation: opts?.fallbackMockImplementation,
|
||||||
|
});
|
||||||
|
(obj[property] as CalledWithMock<any>).mockReturnValue(mock);
|
||||||
|
obj[property]._deepMock = mock;
|
||||||
|
return mock;
|
||||||
|
};
|
||||||
|
obj[property]._isMockObject = true;
|
||||||
|
} else {
|
||||||
|
obj[property] = calledWithFn({
|
||||||
|
fallbackMockImplementation: opts?.fallbackMockImplementation,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error Hack by author of jest-mock-extended
|
||||||
|
if (obj instanceof Date && typeof obj[property] === "function") {
|
||||||
|
// @ts-expect-error Hack by author of jest-mock-extended
|
||||||
|
return obj[property].bind(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj[property];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const mock = <T, MockedReturn extends MockProxy<T> & T = MockProxy<T> & T>(
|
||||||
|
mockImplementation: PartialDeep<T> = {} as PartialDeep<T>,
|
||||||
|
opts?: MockOpts,
|
||||||
|
): MockedReturn => {
|
||||||
|
// @ts-expect-error private
|
||||||
|
mockImplementation!._isMockObject = true;
|
||||||
|
return overrideMockImp(mockImplementation, opts);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mockFn = <T extends FunctionLike>(): CalledWithMock<T> & T => {
|
||||||
|
// @ts-expect-error Hack by author of jest-mock-extended
|
||||||
|
return calledWithFn();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const stub = <T extends object>(): T => {
|
||||||
|
return new Proxy<T>({} as T, {
|
||||||
|
get: (obj, property: ProxiedProperty) => {
|
||||||
|
if (property in obj) {
|
||||||
|
// @ts-expect-error Hack by author of jest-mock-extended
|
||||||
|
return obj[property];
|
||||||
|
}
|
||||||
|
return jest.fn();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default mock;
|
||||||
81
libs/common/src/platform/spec/mock-sdk.service.ts
Normal file
81
libs/common/src/platform/spec/mock-sdk.service.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import {
|
||||||
|
BehaviorSubject,
|
||||||
|
distinctUntilChanged,
|
||||||
|
map,
|
||||||
|
Observable,
|
||||||
|
takeWhile,
|
||||||
|
throwIfEmpty,
|
||||||
|
} from "rxjs";
|
||||||
|
|
||||||
|
import { BitwardenClient } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
|
import { UserId } from "../../types/guid";
|
||||||
|
import { SdkService, UserNotLoggedInError } from "../abstractions/sdk/sdk.service";
|
||||||
|
import { Rc } from "../misc/reference-counting/rc";
|
||||||
|
|
||||||
|
import { DeepMockProxy, mockDeep } from "./mock-deep";
|
||||||
|
|
||||||
|
export class MockSdkService implements SdkService {
|
||||||
|
private userClients$ = new BehaviorSubject<{
|
||||||
|
[userId: UserId]: Rc<BitwardenClient> | undefined;
|
||||||
|
}>({});
|
||||||
|
|
||||||
|
private _client$ = new BehaviorSubject(mockDeep<BitwardenClient>());
|
||||||
|
client$ = this._client$.asObservable();
|
||||||
|
|
||||||
|
version$ = new BehaviorSubject("0.0.1-test").asObservable();
|
||||||
|
|
||||||
|
userClient$(userId: UserId): Observable<Rc<BitwardenClient>> {
|
||||||
|
return this.userClients$.pipe(
|
||||||
|
takeWhile((clients) => clients[userId] !== undefined, false),
|
||||||
|
map((clients) => clients[userId] as Rc<BitwardenClient>),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
throwIfEmpty(() => new UserNotLoggedInError(userId)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setClient(): void {
|
||||||
|
throw new Error("Not supported in mock service");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the non-user scoped client mock.
|
||||||
|
* This is what is returned by the `client$` observable.
|
||||||
|
*/
|
||||||
|
get client(): DeepMockProxy<BitwardenClient> {
|
||||||
|
return this._client$.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly simulate = {
|
||||||
|
/**
|
||||||
|
* Simulates a user login, and returns a user-scoped mock for the user.
|
||||||
|
* This will be return by the `userClient$` observable.
|
||||||
|
*
|
||||||
|
* @param userId The userId to simulate login for.
|
||||||
|
* @returns A user-scoped mock for the user.
|
||||||
|
*/
|
||||||
|
userLogin: (userId: UserId) => {
|
||||||
|
const client = mockDeep<BitwardenClient>();
|
||||||
|
this.userClients$.next({
|
||||||
|
...this.userClients$.getValue(),
|
||||||
|
[userId]: new Rc(client),
|
||||||
|
});
|
||||||
|
return client;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simulates a user logout, and disposes the user-scoped mock for the user.
|
||||||
|
* This will remove the user-scoped mock from the `userClient$` observable.
|
||||||
|
*
|
||||||
|
* @param userId The userId to simulate logout for.
|
||||||
|
*/
|
||||||
|
userLogout: (userId: UserId) => {
|
||||||
|
const clients = this.userClients$.value;
|
||||||
|
clients[userId]?.markForDisposal();
|
||||||
|
this.userClients$.next({
|
||||||
|
...clients,
|
||||||
|
[userId]: undefined,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,38 +1,27 @@
|
|||||||
import { mock } from "jest-mock-extended";
|
import { take } from "rxjs";
|
||||||
import { of, take } from "rxjs";
|
|
||||||
|
|
||||||
import { BitwardenClient, TotpResponse } from "@bitwarden/sdk-internal";
|
import { TotpResponse } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
import { SdkService } from "../../platform/abstractions/sdk/sdk.service";
|
import { MockSdkService } from "../../platform/spec/mock-sdk.service";
|
||||||
|
|
||||||
import { TotpService } from "./totp.service";
|
import { TotpService } from "./totp.service";
|
||||||
|
|
||||||
describe("TotpService", () => {
|
describe("TotpService", () => {
|
||||||
let totpService: TotpService;
|
let totpService!: TotpService;
|
||||||
let generateTotpMock: jest.Mock;
|
let sdkService!: MockSdkService;
|
||||||
|
|
||||||
const sdkService = mock<SdkService>();
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
generateTotpMock = jest
|
sdkService = new MockSdkService();
|
||||||
.fn()
|
sdkService.client.vault
|
||||||
.mockReturnValueOnce({
|
.mockDeep()
|
||||||
|
.totp.mockDeep()
|
||||||
|
.generate_totp.mockReturnValueOnce({
|
||||||
code: "123456",
|
code: "123456",
|
||||||
period: 30,
|
period: 30,
|
||||||
})
|
})
|
||||||
.mockReturnValueOnce({ code: "654321", period: 30 })
|
.mockReturnValueOnce({ code: "654321", period: 30 })
|
||||||
.mockReturnValueOnce({ code: "567892", period: 30 });
|
.mockReturnValueOnce({ code: "567892", period: 30 });
|
||||||
|
|
||||||
const mockBitwardenClient = {
|
|
||||||
vault: () => ({
|
|
||||||
totp: () => ({
|
|
||||||
generate_totp: generateTotpMock,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
sdkService.client$ = of(mockBitwardenClient as unknown as BitwardenClient);
|
|
||||||
|
|
||||||
totpService = new TotpService(sdkService);
|
totpService = new TotpService(sdkService);
|
||||||
|
|
||||||
// TOTP is time-based, so we need to mock the current time
|
// TOTP is time-based, so we need to mock the current time
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { mock, MockProxy } from "jest-mock-extended";
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
import { of } from "rxjs";
|
|
||||||
|
|
||||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
@@ -8,14 +7,13 @@ import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
|||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
import { MockSdkService } from "@bitwarden/common/platform/spec/mock-sdk.service";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||||
import { KeyService } from "@bitwarden/key-management";
|
import { KeyService } from "@bitwarden/key-management";
|
||||||
import { BitwardenClient } from "@bitwarden/sdk-internal";
|
|
||||||
|
|
||||||
import { BitwardenPasswordProtectedImporter } from "../importers/bitwarden/bitwarden-password-protected-importer";
|
import { BitwardenPasswordProtectedImporter } from "../importers/bitwarden/bitwarden-password-protected-importer";
|
||||||
import { Importer } from "../importers/importer";
|
import { Importer } from "../importers/importer";
|
||||||
@@ -35,7 +33,7 @@ describe("ImportService", () => {
|
|||||||
let encryptService: MockProxy<EncryptService>;
|
let encryptService: MockProxy<EncryptService>;
|
||||||
let pinService: MockProxy<PinServiceAbstraction>;
|
let pinService: MockProxy<PinServiceAbstraction>;
|
||||||
let accountService: MockProxy<AccountService>;
|
let accountService: MockProxy<AccountService>;
|
||||||
let sdkService: MockProxy<SdkService>;
|
let sdkService: MockSdkService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cipherService = mock<CipherService>();
|
cipherService = mock<CipherService>();
|
||||||
@@ -46,9 +44,7 @@ describe("ImportService", () => {
|
|||||||
keyService = mock<KeyService>();
|
keyService = mock<KeyService>();
|
||||||
encryptService = mock<EncryptService>();
|
encryptService = mock<EncryptService>();
|
||||||
pinService = mock<PinServiceAbstraction>();
|
pinService = mock<PinServiceAbstraction>();
|
||||||
const mockClient = mock<BitwardenClient>();
|
sdkService = new MockSdkService();
|
||||||
sdkService = mock<SdkService>();
|
|
||||||
sdkService.client$ = of(mockClient, mockClient, mockClient);
|
|
||||||
|
|
||||||
importService = new ImportService(
|
importService = new ImportService(
|
||||||
cipherService,
|
cipherService,
|
||||||
|
|||||||
@@ -5,17 +5,17 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
|||||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
|
||||||
import { EncryptedString } from "@bitwarden/common/platform/models/domain/enc-string";
|
import { EncryptedString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
|
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
|
||||||
|
import { MockSdkService } from "@bitwarden/common/platform/spec/mock-sdk.service";
|
||||||
import { makeStaticByteArray, mockEnc } from "@bitwarden/common/spec";
|
import { makeStaticByteArray, mockEnc } from "@bitwarden/common/spec";
|
||||||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
import { UserKey } from "@bitwarden/common/types/key";
|
import { UserKey } from "@bitwarden/common/types/key";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||||
import { BitwardenClient, VerifyAsymmetricKeysResponse } from "@bitwarden/sdk-internal";
|
import { VerifyAsymmetricKeysResponse } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
import { KeyService } from "../../abstractions/key.service";
|
import { KeyService } from "../../abstractions/key.service";
|
||||||
import { UserAsymmetricKeysRegenerationApiService } from "../abstractions/user-asymmetric-key-regeneration-api.service";
|
import { UserAsymmetricKeysRegenerationApiService } from "../abstractions/user-asymmetric-key-regeneration-api.service";
|
||||||
@@ -24,24 +24,17 @@ import { DefaultUserAsymmetricKeysRegenerationService } from "./default-user-asy
|
|||||||
|
|
||||||
function setupVerificationResponse(
|
function setupVerificationResponse(
|
||||||
mockVerificationResponse: VerifyAsymmetricKeysResponse,
|
mockVerificationResponse: VerifyAsymmetricKeysResponse,
|
||||||
sdkService: MockProxy<SdkService>,
|
sdkService: MockSdkService,
|
||||||
) {
|
) {
|
||||||
const mockKeyPairResponse = {
|
const mockKeyPairResponse = {
|
||||||
userPublicKey: "userPublicKey",
|
userPublicKey: "userPublicKey",
|
||||||
userKeyEncryptedPrivateKey: "userKeyEncryptedPrivateKey",
|
userKeyEncryptedPrivateKey: "userKeyEncryptedPrivateKey",
|
||||||
};
|
};
|
||||||
|
|
||||||
sdkService.client$ = of({
|
sdkService.client.crypto
|
||||||
crypto: () => ({
|
.mockDeep()
|
||||||
verify_asymmetric_keys: jest.fn().mockReturnValue(mockVerificationResponse),
|
.verify_asymmetric_keys.mockReturnValue(mockVerificationResponse);
|
||||||
make_key_pair: jest.fn().mockReturnValue(mockKeyPairResponse),
|
sdkService.client.crypto.mockDeep().make_key_pair.mockReturnValue(mockKeyPairResponse);
|
||||||
}),
|
|
||||||
free: jest.fn(),
|
|
||||||
echo: jest.fn(),
|
|
||||||
version: jest.fn(),
|
|
||||||
throw: jest.fn(),
|
|
||||||
catch: jest.fn(),
|
|
||||||
} as unknown as BitwardenClient);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupUserKeyValidation(
|
function setupUserKeyValidation(
|
||||||
@@ -74,7 +67,7 @@ describe("regenerateIfNeeded", () => {
|
|||||||
let cipherService: MockProxy<CipherService>;
|
let cipherService: MockProxy<CipherService>;
|
||||||
let userAsymmetricKeysRegenerationApiService: MockProxy<UserAsymmetricKeysRegenerationApiService>;
|
let userAsymmetricKeysRegenerationApiService: MockProxy<UserAsymmetricKeysRegenerationApiService>;
|
||||||
let logService: MockProxy<LogService>;
|
let logService: MockProxy<LogService>;
|
||||||
let sdkService: MockProxy<SdkService>;
|
let sdkService: MockSdkService;
|
||||||
let apiService: MockProxy<ApiService>;
|
let apiService: MockProxy<ApiService>;
|
||||||
let configService: MockProxy<ConfigService>;
|
let configService: MockProxy<ConfigService>;
|
||||||
let encryptService: MockProxy<EncryptService>;
|
let encryptService: MockProxy<EncryptService>;
|
||||||
@@ -84,7 +77,7 @@ describe("regenerateIfNeeded", () => {
|
|||||||
cipherService = mock<CipherService>();
|
cipherService = mock<CipherService>();
|
||||||
userAsymmetricKeysRegenerationApiService = mock<UserAsymmetricKeysRegenerationApiService>();
|
userAsymmetricKeysRegenerationApiService = mock<UserAsymmetricKeysRegenerationApiService>();
|
||||||
logService = mock<LogService>();
|
logService = mock<LogService>();
|
||||||
sdkService = mock<SdkService>();
|
sdkService = new MockSdkService();
|
||||||
apiService = mock<ApiService>();
|
apiService = mock<ApiService>();
|
||||||
configService = mock<ConfigService>();
|
configService = mock<ConfigService>();
|
||||||
encryptService = mock<EncryptService>();
|
encryptService = mock<EncryptService>();
|
||||||
|
|||||||
Reference in New Issue
Block a user