Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions libs/vault/src/services/routed-vault-filter.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { TestBed } from "@angular/core/testing";
import { ActivatedRoute, convertToParamMap, ParamMap } from "@angular/router";
import { BehaviorSubject, firstValueFrom } from "rxjs";

import { CollectionId } from "@bitwarden/common/types/guid";

import { RoutedVaultFilterModel } from "../models/routed-vault-filter.model";

import { RoutedVaultFilterService } from "./routed-vault-filter.service";
import { Vfo1TerminologyService } from "./vfo1-terminology.service";

describe("RoutedVaultFilterService", () => {
function setup(flagEnabled: boolean, queryParams: Record<string, string> = {}) {
const paramMap = new BehaviorSubject<ParamMap>(convertToParamMap({}));
const queryParamMap = new BehaviorSubject<ParamMap>(convertToParamMap(queryParams));

TestBed.configureTestingModule({
providers: [
RoutedVaultFilterService,
{ provide: ActivatedRoute, useValue: { paramMap, queryParamMap } },
{ provide: Vfo1TerminologyService, useValue: { enabled: () => flagEnabled } },
],
});

return TestBed.inject(RoutedVaultFilterService);
}

describe("filter$ reader", () => {
it("reads the collection from the legacy collectionId query param", async () => {
const service = setup(false, { collectionId: "col-legacy" });

const filter = await firstValueFrom(service.filter$);

expect(filter.collectionId).toBe("col-legacy");
});

it("reads the collection from the new sharedFolderId query param", async () => {
const service = setup(true, { sharedFolderId: "col-new" });

const filter = await firstValueFrom(service.filter$);

expect(filter.collectionId).toBe("col-new");
});

it("prefers sharedFolderId over collectionId when both are present", async () => {
const service = setup(true, { sharedFolderId: "col-new", collectionId: "col-legacy" });

const filter = await firstValueFrom(service.filter$);

expect(filter.collectionId).toBe("col-new");
});

it("leaves the collection undefined when neither param is present", async () => {
const service = setup(false, {});

const filter = await firstValueFrom(service.filter$);

expect(filter.collectionId).toBeUndefined();
});
});

describe("createRoute writer", () => {
const filter: RoutedVaultFilterModel = { collectionId: "col-1" as CollectionId };

it("writes collectionId and clears sharedFolderId when the flag is off", () => {
const service = setup(false);

const [, extras] = service.createRoute(filter);

expect(extras?.queryParams).toMatchObject({
collectionId: "col-1",
sharedFolderId: null,
});
});

it("writes sharedFolderId and clears collectionId when the flag is on", () => {
const service = setup(true);

const [, extras] = service.createRoute(filter);

expect(extras?.queryParams).toMatchObject({
collectionId: null,
sharedFolderId: "col-1",
});
});

it("merges query params so unrelated params are preserved", () => {
const service = setup(true);

const [, extras] = service.createRoute(filter);

expect(extras?.queryParamsHandling).toBe("merge");
});
});
});
14 changes: 12 additions & 2 deletions libs/vault/src/services/routed-vault-filter.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
RoutedVaultFilterModel,
} from "../models/routed-vault-filter.model";

import { Vfo1TerminologyService } from "./vfo1-terminology.service";

/**
* Injection token for the base route path used in vault filter navigation.
*/
Expand All @@ -26,6 +28,7 @@ export const VAULT_FILTER_BASE_ROUTE = new SafeInjectionToken<string>("VaultFilt
export class RoutedVaultFilterService implements OnDestroy {
private onDestroy = new Subject<void>();
private baseRoute: string = inject(VAULT_FILTER_BASE_ROUTE, { optional: true }) ?? "";
private terminology = inject(Vfo1TerminologyService);

/**
* Filter values extracted from the URL.
Expand All @@ -40,7 +43,10 @@ export class RoutedVaultFilterService implements OnDestroy {
const type = isRoutedVaultFilterItemType(unsafeType) ? unsafeType : undefined;

return {
collectionId: (queryParams.get("collectionId") as CollectionId) ?? undefined,
// Accept both param names during the shared-folder terminology transition.
collectionId:
((queryParams.get("sharedFolderId") ??
queryParams.get("collectionId")) as CollectionId | null) ?? undefined,
folderId: queryParams.get("folderId") ?? undefined,
organizationId:
(params.get("organizationId") as OrganizationId) ??
Expand Down Expand Up @@ -72,9 +78,13 @@ export class RoutedVaultFilterService implements OnDestroy {
*/
createRoute(filter: RoutedVaultFilterModel): [commands: any[], extras?: NavigationExtras] {
const commands: string[] = this.baseRoute ? [this.baseRoute] : [];
// When the flag is on, write the new `sharedFolderId` param and clear the legacy
// `collectionId` (queryParamsHandling: "merge" removes any param set to null).
const useSharedFolderParam = this.terminology.enabled();
const extras: NavigationExtras = {
queryParams: {
collectionId: filter.collectionId ?? null,
collectionId: useSharedFolderParam ? null : (filter.collectionId ?? null),
sharedFolderId: useSharedFolderParam ? (filter.collectionId ?? null) : null,
folderId: filter.folderId ?? null,
organizationId:
filter.organizationIdParamType === "path" ? null : (filter.organizationId ?? null),
Expand Down
Loading