1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-09 13:10:17 +00:00

add/update unit tests to import.service.ts and metadata.rs

This commit is contained in:
John Harrington
2025-09-21 18:22:00 -07:00
parent 493d3385f0
commit 45649ae174
3 changed files with 269 additions and 151 deletions

View File

@@ -51,3 +51,104 @@ pub fn get_supported_importers() -> HashMap<String, ImporterMetadata> {
map
}
/*
Tests are cfg-gated based upon OS, and must be compiled/run on each OS for full coverage
*/
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
fn map_keys(map: &HashMap<String, ImporterMetadata>) -> HashSet<String> {
map.keys().cloned().collect()
}
fn get_loaders(map: &HashMap<String, ImporterMetadata>, id: &str) -> HashSet<&'static str> {
map.get(id)
.map(|m| m.loaders.iter().copied().collect::<HashSet<_>>())
.unwrap_or_default()
}
#[test]
fn returns_all_known_importers() {
let map = get_supported_importers();
let expected: HashSet<String> = HashSet::from([
"chromecsv".to_string(),
"chromiumcsv".to_string(),
"bravecsv".to_string(),
"operacsv".to_string(),
"vivaldicsv".to_string(),
"edgecsv".to_string(),
]);
assert_eq!(map.len(), expected.len());
assert_eq!(map_keys(&map), expected);
for (key, meta) in map.iter() {
assert_eq!(&meta.id, key);
assert_eq!(meta.instructions, "chromium");
assert!(meta.loaders.iter().any(|l| *l == "file"));
}
}
#[cfg(target_os = "macos")]
#[test]
fn macos_specific_loaders_match_const_array() {
let map = get_supported_importers();
let ids = [
"chromecsv",
"chromiumcsv",
"bravecsv",
"operacsv",
"vivaldicsv",
"edgecsv",
];
for id in ids {
let loaders = get_loaders(&map, id);
assert!(loaders.contains("file"));
assert!(loaders.contains("chromium"), "missing chromium for {id}");
}
}
#[cfg(target_os = "linux")]
#[test]
fn linux_specific_loaders_match_const_array() {
let map = get_supported_importers();
let with_chromium = ["chromecsv", "chromiumcsv", "bravecsv", "operacsv"];
let without_chromium = ["vivaldicsv", "edgecsv"];
for id in with_chromium {
let loaders = get_loaders(&map, id);
assert!(loaders.contains("file"));
assert!(loaders.contains("chromium"), "missing chromium for {id}");
}
for id in without_chromium {
let loaders = get_loaders(&map, id);
assert!(loaders.contains("file"));
assert!(!loaders.contains("chromium"), "unexpected chromium support for {id}");
}
}
#[cfg(target_os = "windows")]
#[test]
fn windows_specific_loaders_match_const_array() {
let map = get_supported_importers();
let with_chromium = ["chromiumcsv", "edgecsv", "operacsv", "vivaldicsv"];
let without_chromium = ["chromecsv", "bravecsv"];
for id in with_chromium {
let loaders = get_loaders(&map, id);
assert!(loaders.contains("file"));
assert!(loaders.contains("chromium"), "missing chromium for {id}");
}
for id in without_chromium {
let loaders = get_loaders(&map, id);
assert!(loaders.contains("file"));
assert!(!loaders.contains("chromium"), "unexpected chromium support for {id}");
}
}
}

View File

@@ -1,7 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs";
import { BehaviorSubject, Subject, firstValueFrom } from "rxjs";
// 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
@@ -25,8 +23,8 @@ import { KeyService } from "@bitwarden/key-management";
import { BitwardenPasswordProtectedImporter } from "../importers/bitwarden/bitwarden-password-protected-importer";
import { Importer } from "../importers/importer";
// import { ImporterMetadata, Instructions, Loader } from "../metadata";
// import { ImportType } from "../models";
import type { ImporterMetadata } from "../metadata";
import { ImportType } from "../models";
import { ImportResult } from "../models/import-result";
import { ImportApiServiceAbstraction } from "./import-api.service.abstraction";
@@ -269,31 +267,87 @@ describe("ImportService", () => {
});
});
// TODO Move capability/metadata tests to Rust unit tests and remove this disable.
/* describe("metadata$", () => {
let featureFlagSubject: BehaviorSubject<boolean>;
let typeSubject: Subject<ImportType>;
let mockLogger: { debug: jest.Mock };
describe("metadata$", () => {
// Helper to build a native-like metadata map and re-import ImportService after mocking
async function createServiceWithNativeMap(
nativeMap: Partial<
Record<ImportType, { loaders: ("file" | "chromium")[]; instructions?: "chromium" }>
>,
) {
jest.resetModules();
beforeEach(() => {
featureFlagSubject = new BehaviorSubject(false);
typeSubject = new Subject<ImportType>();
mockLogger = { debug: jest.fn() };
jest.doMock("../metadata", () => {
const data = jest.requireActual("../metadata/data");
const availability = jest.requireActual("../metadata/availability");
const { Loader, Instructions } = data;
const Importers = Object.freeze(
Object.fromEntries(
Object.entries(nativeMap).map(([id, meta]) => {
const loaders = meta.loaders.map((l) => (Loader as any)[l]);
const instructions = meta.instructions
? (Instructions as any)[meta.instructions]
: undefined;
return [
id,
{
type: id,
loaders,
...(instructions ? { instructions } : {}),
},
];
}),
),
);
return {
Loader,
Instructions,
LoaderAvailability: availability.LoaderAvailability,
Importers,
};
});
jest.doMock("../util", () => {
const { Loader } = jest.requireActual("../metadata/data");
const { LoaderAvailability } = jest.requireActual("../metadata/availability");
const availableLoaders = (
type: ImportType,
client: import("@bitwarden/client-type").ClientType,
) => {
const entry = nativeMap[type];
if (!entry) {
return undefined;
}
const loaders = entry.loaders.map(
(l) => (Loader as any)[l],
) as import("../metadata/types").DataLoader[];
return loaders.filter((loader: import("../metadata/types").DataLoader) =>
LoaderAvailability[loader]?.includes(client),
);
};
return { availableLoaders };
});
const configService = mock<ConfigService>();
const featureFlagSubject = new BehaviorSubject<boolean>(false);
configService.getFeatureFlag$.mockReturnValue(featureFlagSubject);
const environment = mock<PlatformUtilsService>();
environment.getClientType.mockReturnValue(ClientType.Desktop);
const mockLogger = { debug: jest.fn() };
systemServiceProvider = mock<SystemServiceProvider>({
configService,
environment,
log: jest.fn().mockReturnValue(mockLogger),
});
// Recreate the service with the updated mocks for logging tests
importService = new ImportService(
let ImportServiceWithMocks: typeof ImportService;
await jest.isolateModulesAsync(async () => {
const svc = await import("./import.service");
ImportServiceWithMocks = svc.ImportService as typeof ImportService;
});
const service = new ImportServiceWithMocks(
cipherService,
folderService,
importApiService,
@@ -306,133 +360,164 @@ describe("ImportService", () => {
restrictedItemTypesService,
systemServiceProvider,
);
});
afterEach(() => {
featureFlagSubject.complete();
typeSubject.complete();
});
return { service, featureFlagSubject, mockLogger };
}
it("should emit metadata when type$ emits", async () => {
const testType: ImportType = "chromecsv";
const nativeMap: Partial<
Record<ImportType, { loaders: ("file" | "chromium")[]; instructions?: "chromium" }>
> = {
chromecsv: { loaders: ["file"], instructions: "chromium" },
};
const { service } = await createServiceWithNativeMap(nativeMap);
const metadataPromise = firstValueFrom(importService.metadata$(typeSubject));
typeSubject.next(testType);
const typeSubject = new Subject<ImportType>();
const promise = firstValueFrom(service.metadata$(typeSubject));
typeSubject.next("chromecsv");
const result: ImporterMetadata = await promise;
const result = await metadataPromise;
expect(result).toEqual({
type: testType,
loaders: expect.any(Array),
instructions: Instructions.chromium,
});
expect(result.type).toBe(testType);
expect(result).toEqual(
expect.objectContaining({
type: "chromecsv",
loaders: expect.any(Array),
}),
);
expect(result.instructions).toBeDefined();
expect(result.type).toBe("chromecsv");
});
it("should include all loaders when chromium feature flag is enabled", async () => {
const testType: ImportType = "bravecsv"; // bravecsv supports both file and chromium loaders
// bravecsv supports both file and chromium loaders
const nativeMap: Partial<
Record<ImportType, { loaders: ("file" | "chromium")[]; instructions?: "chromium" }>
> = {
bravecsv: { loaders: ["file", "chromium"], instructions: "chromium" },
};
const { service, featureFlagSubject } = await createServiceWithNativeMap(nativeMap);
featureFlagSubject.next(true);
const metadataPromise = firstValueFrom(importService.metadata$(typeSubject));
typeSubject.next(testType);
const typeSubject = new Subject<ImportType>();
const promise = firstValueFrom(service.metadata$(typeSubject));
typeSubject.next("bravecsv");
const result: ImporterMetadata = await promise;
const result = await metadataPromise;
expect(result.loaders).toContain(Loader.chromium);
expect(result.loaders).toContain(Loader.file);
expect(result.loaders).toContain("chromium");
expect(result.loaders).toContain("file");
});
it("should exclude chromium loader when feature flag is disabled", async () => {
const testType: ImportType = "bravecsv"; // bravecsv supports both file and chromium loaders
const nativeMap: Partial<
Record<ImportType, { loaders: ("file" | "chromium")[]; instructions?: "chromium" }>
> = {
bravecsv: { loaders: ["file", "chromium"], instructions: "chromium" },
};
const { service, featureFlagSubject } = await createServiceWithNativeMap(nativeMap);
featureFlagSubject.next(false);
const metadataPromise = firstValueFrom(importService.metadata$(typeSubject));
typeSubject.next(testType);
const typeSubject = new Subject<ImportType>();
const promise = firstValueFrom(service.metadata$(typeSubject));
typeSubject.next("bravecsv");
const result: ImporterMetadata = await promise;
const result = await metadataPromise;
expect(result.loaders).not.toContain(Loader.chromium);
expect(result.loaders).toContain(Loader.file);
expect(result.loaders).not.toContain("chromium");
expect(result.loaders).toContain("file");
});
it("should update when type$ changes", async () => {
const nativeMap: Partial<
Record<ImportType, { loaders: ("file" | "chromium")[]; instructions?: "chromium" }>
> = {
chromecsv: { loaders: ["file"], instructions: "chromium" },
bravecsv: { loaders: ["file", "chromium"], instructions: "chromium" },
};
const { service } = await createServiceWithNativeMap(nativeMap);
const emissions: ImporterMetadata[] = [];
const subscription = importService.metadata$(typeSubject).subscribe((metadata) => {
emissions.push(metadata);
});
const typeSubject = new Subject<ImportType>();
const subscription = service
.metadata$(typeSubject)
.subscribe((m: ImporterMetadata) => emissions.push(m));
typeSubject.next("chromecsv");
typeSubject.next("bravecsv");
// Wait for emissions
await new Promise((resolve) => setTimeout(resolve, 0));
await new Promise((r) => setTimeout(r, 0));
expect(emissions).toHaveLength(2);
expect(emissions[0].type).toBe("chromecsv");
expect(emissions[1].type).toBe("bravecsv");
subscription.unsubscribe();
});
it("should update when feature flag changes", async () => {
const testType: ImportType = "bravecsv"; // Use bravecsv which supports chromium loader
const nativeMap: Partial<
Record<ImportType, { loaders: ("file" | "chromium")[]; instructions?: "chromium" }>
> = {
bravecsv: { loaders: ["file", "chromium"], instructions: "chromium" },
};
const { service, featureFlagSubject } = await createServiceWithNativeMap(nativeMap);
const emissions: ImporterMetadata[] = [];
const typeSubject = new Subject<ImportType>();
const subscription = service
.metadata$(typeSubject)
.subscribe((m: ImporterMetadata) => emissions.push(m));
const subscription = importService.metadata$(typeSubject).subscribe((metadata) => {
emissions.push(metadata);
});
typeSubject.next(testType);
typeSubject.next("bravecsv");
featureFlagSubject.next(true);
// Wait for emissions
await new Promise((resolve) => setTimeout(resolve, 0));
await new Promise((r) => setTimeout(r, 0));
expect(emissions).toHaveLength(2);
expect(emissions[0].loaders).not.toContain(Loader.chromium);
expect(emissions[1].loaders).toContain(Loader.chromium);
expect(emissions[0].loaders).not.toContain("chromium");
expect(emissions[1].loaders).toContain("chromium");
subscription.unsubscribe();
});
it("should update when both type$ and feature flag change", async () => {
const nativeMap: Partial<
Record<ImportType, { loaders: ("file" | "chromium")[]; instructions?: "chromium" }>
> = {
chromecsv: { loaders: ["file"], instructions: "chromium" },
bravecsv: { loaders: ["file", "chromium"], instructions: "chromium" },
};
const { service, featureFlagSubject } = await createServiceWithNativeMap(nativeMap);
const emissions: ImporterMetadata[] = [];
const typeSubject = new Subject<ImportType>();
const subscription = service
.metadata$(typeSubject)
.subscribe((m: ImporterMetadata) => emissions.push(m));
const subscription = importService.metadata$(typeSubject).subscribe((metadata) => {
emissions.push(metadata);
});
// Initial emission
typeSubject.next("chromecsv");
// Change both at the same time
featureFlagSubject.next(true);
typeSubject.next("bravecsv");
// Wait for emissions
await new Promise((resolve) => setTimeout(resolve, 0));
await new Promise((r) => setTimeout(r, 0));
expect(emissions.length).toBeGreaterThanOrEqual(2);
const lastEmission = emissions[emissions.length - 1];
expect(lastEmission.type).toBe("bravecsv");
subscription.unsubscribe();
});
it("should log debug information with correct data", async () => {
const testType: ImportType = "chromecsv";
const nativeMap: Partial<
Record<ImportType, { loaders: ("file" | "chromium")[]; instructions?: "chromium" }>
> = {
chromecsv: { loaders: ["file"], instructions: "chromium" },
};
const { service, mockLogger } = await createServiceWithNativeMap(nativeMap);
const metadataPromise = firstValueFrom(importService.metadata$(typeSubject));
typeSubject.next(testType);
await metadataPromise;
const typeSubject = new Subject<ImportType>();
const promise = firstValueFrom(service.metadata$(typeSubject));
typeSubject.next("chromecsv");
await promise;
expect(mockLogger.debug).toHaveBeenCalledWith(
{ importType: testType, capabilities: expect.any(Object) },
{ importType: "chromecsv", capabilities: expect.any(Object) },
"capabilities updated",
);
});
}); */
});
});
function createCipher(options: Partial<CipherView> = {}) {

View File

@@ -1,68 +0,0 @@
// import { ClientType } from "@bitwarden/client-type";
// import { Loader } from "./metadata";
// import { availableLoaders } from "./util";
// TODO Recreate metadata capability tests in Rust (source of truth), then remove this disable.
/*
describe("availableLoaders", () => {
describe("given valid import types", () => {
it("returns available loaders when client supports all loaders", () => {
const result = availableLoaders("operacsv", ClientType.Desktop);
expect(result).toEqual([Loader.file, Loader.chromium]);
});
it("returns filtered loaders when client supports some loaders", () => {
const result = availableLoaders("operacsv", ClientType.Browser);
expect(result).toEqual([Loader.file]);
});
it("returns single loader for import types with one loader", () => {
const result = availableLoaders("chromecsv", ClientType.Desktop);
expect(result).toEqual([Loader.file]);
});
it("returns all supported loaders for multi-loader import types", () => {
const result = availableLoaders("bravecsv", ClientType.Desktop);
expect(result).toEqual([Loader.file, Loader.chromium]);
});
});
describe("given unknown import types", () => {
it("returns undefined when import type is not found in metadata", () => {
const result = availableLoaders("nonexistent" as any, ClientType.Desktop);
expect(result).toBeUndefined();
});
});
describe("given different client types", () => {
it("returns appropriate loaders for Browser client", () => {
const result = availableLoaders("operacsv", ClientType.Browser);
expect(result).toEqual([Loader.file]);
});
it("returns appropriate loaders for Web client", () => {
const result = availableLoaders("chromecsv", ClientType.Web);
expect(result).toEqual([Loader.file]);
});
it("returns appropriate loaders for CLI client", () => {
const result = availableLoaders("vivaldicsv", ClientType.Cli);
expect(result).toEqual([Loader.file]);
});
});
});
*/
it.skip("placeholder: metadata tests moved to Rust", () => {
// TODO(rust): Port availableLoaders tests to Rust
expect(true).toBe(true);
});