1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-06 11:43:51 +00:00

implement unit tests

This commit is contained in:
✨ Audrey ✨
2025-07-18 12:02:20 -04:00
parent 9186d9a4ed
commit b011ca2b06
6 changed files with 621 additions and 13 deletions

View File

@@ -0,0 +1,175 @@
import { TestBed, fakeAsync, tick } from "@angular/core/testing";
import { Router, UrlTree } from "@angular/router";
import { mock, MockProxy } from "jest-mock-extended";
import { firstValueFrom, NEVER } from "rxjs";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { StateProvider } from "@bitwarden/common/platform/state";
import { mockAccountServiceWith, FakeStateProvider } from "@bitwarden/common/spec";
import { SemanticLogger } from "@bitwarden/common/tools/log";
import { SystemServiceProvider } from "@bitwarden/common/tools/providers";
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
import { UserId } from "@bitwarden/common/types/guid";
import { SYSTEM_SERVICE_PROVIDER } from "@bitwarden/generator-components";
import { DefaultSendAccessService } from "./default-send-access-service";
import { SEND_RESPONSE_KEY, SEND_CONTEXT_KEY } from "./send-access-memory";
describe("DefaultSendAccessService", () => {
let service: DefaultSendAccessService;
let stateProvider: FakeStateProvider;
let sendApiService: MockProxy<SendApiService>;
let router: MockProxy<Router>;
let logger: MockProxy<SemanticLogger>;
let systemServiceProvider: MockProxy<SystemServiceProvider>;
beforeEach(() => {
const accountService = mockAccountServiceWith("user-id" as UserId);
stateProvider = new FakeStateProvider(accountService);
sendApiService = mock<SendApiService>();
router = mock<Router>();
logger = mock<SemanticLogger>();
systemServiceProvider = mock<SystemServiceProvider>();
systemServiceProvider.log.mockReturnValue(logger);
TestBed.configureTestingModule({
providers: [
DefaultSendAccessService,
{ provide: StateProvider, useValue: stateProvider },
{ provide: SendApiService, useValue: sendApiService },
{ provide: Router, useValue: router },
{ provide: SYSTEM_SERVICE_PROVIDER, useValue: systemServiceProvider },
],
});
service = TestBed.inject(DefaultSendAccessService);
});
describe("constructor", () => {
it("creates logger with type 'SendAccessAuthenticationService' when initialized", () => {
expect(systemServiceProvider.log).toHaveBeenCalledWith({
type: "SendAccessAuthenticationService",
});
});
});
describe("redirect$", () => {
const sendId = "test-send-id";
it("returns content page UrlTree and logs info when API returns success", async () => {
const expectedUrlTree = { toString: () => "/send/content/test-send-id" } as UrlTree;
sendApiService.postSendAccess.mockResolvedValue({} as any);
router.createUrlTree.mockReturnValue(expectedUrlTree);
const result = await firstValueFrom(service.redirect$(sendId));
expect(result).toBe(expectedUrlTree);
expect(logger.info).toHaveBeenCalledWith(
"public send detected; redirecting to send access with token.",
);
});
describe("given error responses", () => {
it("returns password flow UrlTree and logs debug when 401 received", async () => {
const expectedUrlTree = { toString: () => "/send/test-send-id" } as UrlTree;
const errorResponse = new ErrorResponse([], 401);
sendApiService.postSendAccess.mockRejectedValue(errorResponse);
router.createUrlTree.mockReturnValue(expectedUrlTree);
const result = await firstValueFrom(service.redirect$(sendId));
expect(result).toBe(expectedUrlTree);
expect(logger.debug).toHaveBeenCalledWith(errorResponse, "redirecting to password flow");
});
it("returns 404 page UrlTree and logs debug when 404 received", async () => {
const expectedUrlTree = { toString: () => "/404.html" } as UrlTree;
const errorResponse = new ErrorResponse([], 404);
sendApiService.postSendAccess.mockRejectedValue(errorResponse);
router.parseUrl.mockReturnValue(expectedUrlTree);
const result = await firstValueFrom(service.redirect$(sendId));
expect(result).toBe(expectedUrlTree);
expect(logger.debug).toHaveBeenCalledWith(errorResponse, "redirecting to unavailable page");
});
it("logs warning and throws error when 500 received", async () => {
const errorResponse = new ErrorResponse([], 500);
sendApiService.postSendAccess.mockRejectedValue(errorResponse);
await expect(firstValueFrom(service.redirect$(sendId))).rejects.toBe(errorResponse);
expect(logger.warn).toHaveBeenCalledWith(
errorResponse,
"received unexpected error response",
);
});
it("throws error when unexpected error code received", async () => {
const errorResponse = new ErrorResponse([], 403);
sendApiService.postSendAccess.mockRejectedValue(errorResponse);
await expect(firstValueFrom(service.redirect$(sendId))).rejects.toBe(errorResponse);
expect(logger.warn).toHaveBeenCalledWith(
errorResponse,
"received unexpected error response",
);
});
});
it("throws error when non-ErrorResponse error occurs", async () => {
const regularError = new Error("Network error");
sendApiService.postSendAccess.mockRejectedValue(regularError);
await expect(firstValueFrom(service.redirect$(sendId))).rejects.toThrow("Network error");
expect(logger.warn).not.toHaveBeenCalled();
});
it("emits timeout error when API response exceeds 10 seconds", fakeAsync(() => {
// Mock API to never resolve (simulating a hung request)
sendApiService.postSendAccess.mockReturnValue(firstValueFrom(NEVER));
const result$ = service.redirect$(sendId);
let error: any;
result$.subscribe({
error: (err: unknown) => (error = err),
});
// Advance time past 10 seconds
tick(10001);
expect(error).toBeDefined();
expect(error.name).toBe("TimeoutError");
}));
});
describe("setContext", () => {
it("updates global state with send context when called with sendId and key", async () => {
const sendId = "test-send-id";
const key = "test-key";
await service.setContext(sendId, key);
const context = await firstValueFrom(stateProvider.getGlobal(SEND_CONTEXT_KEY).state$);
expect(context).toEqual({ id: sendId, key });
});
});
describe("clear", () => {
it("sets both SEND_RESPONSE_KEY and SEND_CONTEXT_KEY to null when called", async () => {
// Set initial values
await stateProvider.getGlobal(SEND_RESPONSE_KEY).update(() => ({ some: "response" }) as any);
await stateProvider.getGlobal(SEND_CONTEXT_KEY).update(() => ({ id: "test", key: "test" }));
await service.clear();
const response = await firstValueFrom(stateProvider.getGlobal(SEND_RESPONSE_KEY).state$);
const context = await firstValueFrom(stateProvider.getGlobal(SEND_CONTEXT_KEY).state$);
expect(response).toBeNull();
expect(context).toBeNull();
});
});
});

View File

@@ -1,31 +1,30 @@
import { Injectable } from "@angular/core";
import { Injectable, Inject } from "@angular/core";
import { Router, UrlTree } from "@angular/router";
import { map, of, from, catchError, timeout } from "rxjs";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { StateProvider } from "@bitwarden/common/platform/state";
import { SemanticLogger } from "@bitwarden/common/tools/log";
import { SystemServiceProvider } from "@bitwarden/common/tools/providers.js";
import { SystemServiceProvider } from "@bitwarden/common/tools/providers";
import { SendAccessRequest } from "@bitwarden/common/tools/send/models/request/send-access.request";
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
import { SYSTEM_SERVICE_PROVIDER } from "@bitwarden/generator-components";
import { SEND_RESPONSE_KEY, SEND_CONTEXT_KEY } from "./send-access-memory";
import { SendAccessService } from "./send-access-service.abstraction";
import { isErrorResponse } from "./util";
const TEN_SECONDS = 10_000;
@Injectable({ providedIn: "root" })
export class SendAccessService {
export class DefaultSendAccessService implements SendAccessService {
private readonly logger: SemanticLogger;
constructor(
private readonly state: StateProvider,
private readonly api: SendApiService,
private readonly router: Router,
system: SystemServiceProvider,
private configuration = {
tryAccessTimeoutMs: TEN_SECONDS,
},
@Inject(SYSTEM_SERVICE_PROVIDER) system: SystemServiceProvider,
) {
this.logger = system.log({ type: "SendAccessAuthenticationService" });
}
@@ -36,7 +35,7 @@ export class SendAccessService {
const response$ = from(this.api.postSendAccess(sendId, new SendAccessRequest()));
const redirect$ = response$.pipe(
timeout({ first: this.configuration.tryAccessTimeoutMs }),
timeout({ first: TEN_SECONDS }),
map((_response) => {
this.logger.info("public send detected; redirecting to send access with token.");
const url = this.toViewRedirect(sendId);
@@ -87,7 +86,7 @@ export class SendAccessService {
}
async setContext(sendId: string, key: string) {
return this.state.getGlobal(SEND_CONTEXT_KEY).update(() => ({ id: sendId, key }));
await this.state.getGlobal(SEND_CONTEXT_KEY).update(() => ({ id: sendId, key }));
}
async clear(): Promise<void> {

View File

@@ -8,7 +8,6 @@ import { RouteDataProperties } from "../../../core";
import { SendAccessExplainerComponent } from "./send-access-explainer.component";
import { SendAccessPasswordComponent } from "./send-access-password.component";
import { trySendAccess } from "./try-send-access.guard";
import { ViewContentComponent } from "./view-content.component";
/** Routes to reach send access screens */
export const SendAccessRoutes: Routes = [
@@ -49,8 +48,7 @@ export const SendAccessRoutes: Routes = [
} satisfies RouteDataProperties & AnonLayoutWrapperData,
children: [
{
path: "send/password/:sendId/:key",
component: ViewContentComponent,
path: "send/password/:sendId",
},
{
path: "",

View File

@@ -0,0 +1,10 @@
import { UrlTree } from "@angular/router";
import { Observable } from "rxjs";
export abstract class SendAccessService {
abstract redirect$: (sendId: string) => Observable<UrlTree>;
abstract setContext: (sendId: string, key: string) => Promise<void>;
abstract clear: () => Promise<void>;
}

View File

@@ -0,0 +1,426 @@
import { TestBed } from "@angular/core/testing";
import { ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree } from "@angular/router";
import { firstValueFrom, Observable, of } from "rxjs";
import { SemanticLogger } from "@bitwarden/common/tools/log";
import { SystemServiceProvider } from "@bitwarden/common/tools/providers";
import { SYSTEM_SERVICE_PROVIDER } from "@bitwarden/generator-components";
import { SendAccessService } from "./send-access-service.abstraction";
import { trySendAccess } from "./try-send-access.guard";
function createMockRoute(params: Record<string, any>): ActivatedRouteSnapshot {
return { params } as ActivatedRouteSnapshot;
}
function createMockLogger(): SemanticLogger {
return {
warn: jest.fn(),
panic: jest.fn().mockImplementation(() => {
throw new Error("Logger panic called");
}),
} as any as SemanticLogger;
}
function createMockSystemServiceProvider(): SystemServiceProvider {
return {
log: jest.fn().mockReturnValue(createMockLogger()),
} as any as SystemServiceProvider;
}
function createMockSendAccessService() {
return {
setContext: jest.fn().mockResolvedValue(undefined),
redirect$: jest.fn().mockReturnValue(of({} as UrlTree)),
clear: jest.fn().mockResolvedValue(undefined),
};
}
describe("trySendAccess", () => {
let mockSendAccessService: ReturnType<typeof createMockSendAccessService>;
let mockSystemServiceProvider: SystemServiceProvider;
let mockRouterState: RouterStateSnapshot;
beforeEach(() => {
mockSendAccessService = createMockSendAccessService();
mockSystemServiceProvider = createMockSystemServiceProvider();
mockRouterState = {} as RouterStateSnapshot;
TestBed.configureTestingModule({
providers: [
{ provide: SendAccessService, useValue: mockSendAccessService },
{ provide: SYSTEM_SERVICE_PROVIDER, useValue: mockSystemServiceProvider },
],
});
});
afterEach(() => {
jest.clearAllMocks();
});
describe("canActivate", () => {
describe("given valid route parameters", () => {
it("extracts sendId and key from route params when both are valid strings", async () => {
const sendId = "test-send-id";
const key = "test-key";
const mockRoute = createMockRoute({ sendId, key });
const expectedUrlTree = { toString: () => "/test-url" } as UrlTree;
mockSendAccessService.redirect$.mockReturnValue(of(expectedUrlTree));
// need to cast the result because `CanActivateFn` performs type erasure
const result$ = TestBed.runInInjectionContext(() =>
trySendAccess(mockRoute, mockRouterState),
) as unknown as Observable<UrlTree>;
expect(mockSendAccessService.setContext).toHaveBeenCalledWith(sendId, key);
expect(mockSendAccessService.setContext).toHaveBeenCalledTimes(1);
await expect(firstValueFrom(result$)).resolves.toEqual(expectedUrlTree);
});
it("does not throw validation errors when sendId and key are valid strings", async () => {
const sendId = "valid-send-id";
const key = "valid-key";
const mockRoute = createMockRoute({ sendId, key });
const expectedUrlTree = { toString: () => "/test-url" } as UrlTree;
mockSendAccessService.redirect$.mockReturnValue(of(expectedUrlTree));
// Should not throw any errors during guard execution
let guardResult: Observable<UrlTree> | undefined;
expect(() => {
guardResult = TestBed.runInInjectionContext(() =>
trySendAccess(mockRoute, mockRouterState),
) as unknown as Observable<UrlTree>;
}).not.toThrow();
// Verify the observable can be subscribed to without errors
expect(guardResult).toBeDefined();
await expect(firstValueFrom(guardResult!)).resolves.toEqual(expectedUrlTree);
// Logger methods should not be called for warnings or panics
const mockLogger = (mockSystemServiceProvider.log as jest.Mock).mock.results[0].value;
expect(mockLogger.warn).not.toHaveBeenCalled();
expect(mockLogger.panic).not.toHaveBeenCalled();
});
});
describe("given invalid route parameters", () => {
describe("given invalid sendId", () => {
it.each([
["undefined", undefined],
["null", null],
])(
"logs warning with correct message when sendId is %s",
async (description, sendIdValue) => {
const key = "valid-key";
const mockRoute = createMockRoute(
sendIdValue === undefined ? { key } : { sendId: sendIdValue, key },
);
const mockLogger = createMockLogger();
(mockSystemServiceProvider.log as jest.Mock).mockReturnValue(mockLogger);
await expect(async () => {
const result$ = TestBed.runInInjectionContext(() =>
trySendAccess(mockRoute, mockRouterState),
) as unknown as Observable<UrlTree>;
await firstValueFrom(result$);
}).rejects.toThrow("Logger panic called");
expect(mockSystemServiceProvider.log).toHaveBeenCalledWith({
function: "trySendAccess",
});
expect(mockLogger.warn).toHaveBeenCalledWith(
"sendId missing from the route parameters; redirecting to 404",
);
},
);
it.each([
["number", 123],
["object", {}],
["boolean", true],
])("logs panic with expected/actual type info when sendId is %s", async (type, value) => {
const key = "valid-key";
const mockRoute = createMockRoute({ sendId: value, key });
const mockLogger = createMockLogger();
(mockSystemServiceProvider.log as jest.Mock).mockReturnValue(mockLogger);
await expect(async () => {
const result$ = TestBed.runInInjectionContext(() =>
trySendAccess(mockRoute, mockRouterState),
) as unknown as Observable<UrlTree>;
await firstValueFrom(result$);
}).rejects.toThrow("Logger panic called");
expect(mockSystemServiceProvider.log).toHaveBeenCalledWith({ function: "trySendAccess" });
expect(mockLogger.panic).toHaveBeenCalledWith(
{ expected: "string", actual: type },
"sendId has invalid type",
);
});
it("throws when sendId is not a string", async () => {
const key = "valid-key";
const invalidSendIdValues = [123, {}, true, null, undefined];
for (const invalidSendId of invalidSendIdValues) {
const mockRoute = createMockRoute(
invalidSendId === undefined ? { key } : { sendId: invalidSendId, key },
);
const mockLogger = createMockLogger();
(mockSystemServiceProvider.log as jest.Mock).mockReturnValue(mockLogger);
await expect(async () => {
const result$ = TestBed.runInInjectionContext(() =>
trySendAccess(mockRoute, mockRouterState),
) as unknown as Observable<UrlTree>;
await firstValueFrom(result$);
}).rejects.toThrow("Logger panic called");
}
});
});
describe("given invalid key", () => {
it.each([
["undefined", undefined],
["null", null],
])("logs panic with correct message when key is %s", async (description, keyValue) => {
const sendId = "valid-send-id";
const mockRoute = createMockRoute(
keyValue === undefined ? { sendId } : { sendId, key: keyValue },
);
const mockLogger = createMockLogger();
(mockSystemServiceProvider.log as jest.Mock).mockReturnValue(mockLogger);
await expect(async () => {
const result$ = TestBed.runInInjectionContext(() =>
trySendAccess(mockRoute, mockRouterState),
) as unknown as Observable<UrlTree>;
await firstValueFrom(result$);
}).rejects.toThrow("Logger panic called");
expect(mockSystemServiceProvider.log).toHaveBeenCalledWith({ function: "trySendAccess" });
expect(mockLogger.panic).toHaveBeenCalledWith("key missing from the route parameters");
});
it.each([
["number", 123],
["object", {}],
["boolean", true],
])("logs panic with expected/actual type info when key is %s", async (type, value) => {
const sendId = "valid-send-id";
const mockRoute = createMockRoute({ sendId, key: value });
const mockLogger = createMockLogger();
(mockSystemServiceProvider.log as jest.Mock).mockReturnValue(mockLogger);
await expect(async () => {
const result$ = TestBed.runInInjectionContext(() =>
trySendAccess(mockRoute, mockRouterState),
) as unknown as Observable<UrlTree>;
await firstValueFrom(result$);
}).rejects.toThrow("Logger panic called");
expect(mockSystemServiceProvider.log).toHaveBeenCalledWith({ function: "trySendAccess" });
expect(mockLogger.panic).toHaveBeenCalledWith(
{ expected: "string", actual: type },
"key has invalid type",
);
});
it("throws when key is not a string", async () => {
const sendId = "valid-send-id";
const invalidKeyValues = [123, {}, true, null, undefined];
for (const invalidKey of invalidKeyValues) {
const mockRoute = createMockRoute(
invalidKey === undefined ? { sendId } : { sendId, key: invalidKey },
);
const mockLogger = createMockLogger();
(mockSystemServiceProvider.log as jest.Mock).mockReturnValue(mockLogger);
await expect(async () => {
const result$ = TestBed.runInInjectionContext(() =>
trySendAccess(mockRoute, mockRouterState),
) as unknown as Observable<UrlTree>;
await firstValueFrom(result$);
}).rejects.toThrow("Logger panic called");
}
});
});
});
describe("given service interactions", () => {
it("calls setContext with extracted sendId and key when parameters are valid", async () => {
const sendId = "test-send-id";
const key = "test-key";
const mockRoute = createMockRoute({ sendId, key });
const expectedUrlTree = { toString: () => "/test-url" } as UrlTree;
mockSendAccessService.redirect$.mockReturnValue(of(expectedUrlTree));
const result$ = TestBed.runInInjectionContext(() =>
trySendAccess(mockRoute, mockRouterState),
) as unknown as Observable<UrlTree>;
await firstValueFrom(result$);
expect(mockSendAccessService.setContext).toHaveBeenCalledWith(sendId, key);
expect(mockSendAccessService.setContext).toHaveBeenCalledTimes(1);
});
it("calls redirect$ with extracted sendId when setContext completes", async () => {
const sendId = "test-send-id";
const key = "test-key";
const mockRoute = createMockRoute({ sendId, key });
const expectedUrlTree = { toString: () => "/test-url" } as UrlTree;
mockSendAccessService.redirect$.mockReturnValue(of(expectedUrlTree));
const result$ = TestBed.runInInjectionContext(() =>
trySendAccess(mockRoute, mockRouterState),
) as unknown as Observable<UrlTree>;
await firstValueFrom(result$);
expect(mockSendAccessService.redirect$).toHaveBeenCalledWith(sendId);
expect(mockSendAccessService.redirect$).toHaveBeenCalledTimes(1);
});
});
describe("given observable behavior", () => {
it("returns redirect$ emissions when setContext completes successfully", async () => {
const sendId = "test-send-id";
const key = "test-key";
const mockRoute = createMockRoute({ sendId, key });
const expectedUrlTree = { toString: () => "/test-url" } as UrlTree;
mockSendAccessService.redirect$.mockReturnValue(of(expectedUrlTree));
const result$ = TestBed.runInInjectionContext(() =>
trySendAccess(mockRoute, mockRouterState),
) as unknown as Observable<UrlTree>;
const actualResult = await firstValueFrom(result$);
expect(actualResult).toEqual(expectedUrlTree);
expect(mockSendAccessService.redirect$).toHaveBeenCalledWith(sendId);
});
it("does not emit setContext values when using ignoreElements", async () => {
const sendId = "test-send-id";
const key = "test-key";
const mockRoute = createMockRoute({ sendId, key });
const expectedUrlTree = { toString: () => "/test-url" } as UrlTree;
const setContextValue = "should-not-be-emitted";
// Mock setContext to return a value
mockSendAccessService.setContext.mockResolvedValue(setContextValue);
mockSendAccessService.redirect$.mockReturnValue(of(expectedUrlTree));
const result$ = TestBed.runInInjectionContext(() =>
trySendAccess(mockRoute, mockRouterState),
) as unknown as Observable<UrlTree>;
const actualResult = await firstValueFrom(result$);
// Should only emit the redirect$ value, not the setContext value
expect(actualResult).toEqual(expectedUrlTree);
expect(actualResult).not.toEqual(setContextValue);
});
it("ensures setContext completes before redirect$ executes (sequencing)", async () => {
const sendId = "test-send-id";
const key = "test-key";
const mockRoute = createMockRoute({ sendId, key });
const expectedUrlTree = { toString: () => "/test-url" } as UrlTree;
let setContextResolved = false;
// Mock setContext to track when it resolves
mockSendAccessService.setContext.mockImplementation(async () => {
await new Promise((resolve) => setTimeout(resolve, 10)); // Small delay
setContextResolved = true;
});
// Mock redirect$ to return a delayed observable and check if setContext resolved
mockSendAccessService.redirect$.mockImplementation((id) => {
return new Observable((subscriber) => {
// Check if setContext has resolved when redirect$ subscription starts
setTimeout(() => {
expect(setContextResolved).toBe(true);
subscriber.next(expectedUrlTree);
subscriber.complete();
}, 0);
});
});
const result$ = TestBed.runInInjectionContext(() =>
trySendAccess(mockRoute, mockRouterState),
) as unknown as Observable<UrlTree>;
await firstValueFrom(result$);
});
});
describe("given error scenarios", () => {
it("does not call redirect$ when setContext rejects", async () => {
const sendId = "test-send-id";
const key = "test-key";
const mockRoute = createMockRoute({ sendId, key });
const setContextError = new Error("setContext failed");
// Reset mocks to ensure clean state
jest.clearAllMocks();
// Mock setContext to reject
mockSendAccessService.setContext.mockRejectedValue(setContextError);
// Create a mock observable that we can spy on subscription
const mockRedirectObservable = of({} as UrlTree);
const subscribeSpy = jest.spyOn(mockRedirectObservable, "subscribe");
mockSendAccessService.redirect$.mockReturnValue(mockRedirectObservable);
const result$ = TestBed.runInInjectionContext(() =>
trySendAccess(mockRoute, mockRouterState),
) as unknown as Observable<UrlTree>;
// Expect the observable to reject when setContext fails
await expect(firstValueFrom(result$)).rejects.toThrow("setContext failed");
// The redirect$ method will be called (since it's called synchronously)
expect(mockSendAccessService.redirect$).toHaveBeenCalledWith(sendId);
// But the returned observable should not be subscribed to due to the error
// Note: This test verifies the error propagation behavior
expect(subscribeSpy).not.toHaveBeenCalled();
});
it("propagates error to guard return value when redirect$ throws", async () => {
const sendId = "test-send-id";
const key = "test-key";
const mockRoute = createMockRoute({ sendId, key });
const redirectError = new Error("redirect$ failed");
// Reset mocks to ensure clean state
jest.clearAllMocks();
// Mock setContext to succeed and redirect$ to throw
mockSendAccessService.setContext.mockResolvedValue(undefined);
mockSendAccessService.redirect$.mockReturnValue(
new Observable((subscriber) => {
subscriber.error(redirectError);
}),
);
const result$ = TestBed.runInInjectionContext(() =>
trySendAccess(mockRoute, mockRouterState),
) as unknown as Observable<UrlTree>;
// Expect the observable to propagate the redirect$ error
await expect(firstValueFrom(result$)).rejects.toThrow("redirect$ failed");
// Verify that setContext was called (should succeed)
expect(mockSendAccessService.setContext).toHaveBeenCalledWith(sendId, key);
// Verify that redirect$ was called (but it throws)
expect(mockSendAccessService.redirect$).toHaveBeenCalledWith(sendId);
});
});
});
});

View File

@@ -5,7 +5,7 @@ import { from, ignoreElements, concat } from "rxjs";
import { SystemServiceProvider } from "@bitwarden/common/tools/providers";
import { SYSTEM_SERVICE_PROVIDER } from "@bitwarden/generator-components";
import { SendAccessService } from "./send-access.service";
import { SendAccessService } from "./send-access-service.abstraction";
export const trySendAccess: CanActivateFn = (
route: ActivatedRouteSnapshot,