Files
inbuxa-webmail/web/src/store/__tests__/ldif-likeness.test.ts
T

151 lines
7.3 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { CAP, client } from "@/jmap/client";
import { useContacts } from "@/store/contacts";
import type { ContactCard, JmapSession } from "@/jmap/types";
/*
* Counting look-alikes on an LDIF import, without acting on them.
*
* Mozilla's schema has no UID, so the import invents one and a re-import
* duplicates everything. Whether to guess an identity from a name and an
* address is still open on #223 -- and the harm that was actually reported was
* confusion rather than duplication: somebody imports a file twice and cannot
* tell what happened. So this counts and says so, and imports every card
* regardless. Reporting is not matching.
*/
interface SetArgs { create?: Record<string, Record<string, unknown>> }
function server(existing: Array<Partial<ContactCard> & { id: string }>) {
const sets: SetArgs[] = [];
const fetchMock = vi.fn(async (_url: string, init: RequestInit) => {
const body = JSON.parse(init.body as string) as { methodCalls: [string, Record<string, unknown>, string][] };
const methodResponses = body.methodCalls.map(([name, args, id]) => {
if (name === "ContactCard/query") {
const position = (args.position as number) ?? 0;
return [name, { accountId: "a1", queryState: "1", canCalculateChanges: false, position, ids: position ? [] : existing.map((c) => c.id), total: existing.length }, id];
}
if (name === "ContactCard/get") {
const want = new Set((args.ids as string[]) ?? []);
return [name, { accountId: "a1", state: "1", list: existing.filter((c) => want.has(c.id)), notFound: [] }, id];
}
if (name === "ContactCard/set") {
sets.push({ create: args.create as Record<string, Record<string, unknown>> });
return [name, {
accountId: "a1", oldState: "1", newState: "2",
created: Object.fromEntries(Object.keys((args.create ?? {}) as object).map((k) => [k, { id: `new-${k}` }])),
notCreated: {},
}, id];
}
return [name, { accountId: "a1", state: "1", list: [], notFound: [], ids: [], total: 0, queryState: "q", position: 0, canCalculateChanges: false }, id];
});
return { ok: true, status: 200, json: async () => ({ methodResponses, sessionState: "1" }) } as Response;
});
vi.stubGlobal("fetch", fetchMock);
return sets;
}
/** A card already in the book, with the two fields likeness is read from. */
const card = (id: string, full: string, ...emails: string[]) => ({
id, uid: `uid-${id}`, addressBookIds: { book1: true },
name: { full },
emails: Object.fromEntries(emails.map((address, i) => [`e${i}`, { address }])),
}) as unknown as Partial<ContactCard> & { id: string };
const entry = (cn: string, mail: string) =>
`dn: cn=${cn}\ngivenName: ${cn.split(" ")[0]}\nsn: ${cn.split(" ").slice(-1)[0]}\ncn: ${cn}\nmail: ${mail}\n`;
beforeEach(() => {
client.session = {
capabilities: { [CAP.core]: { maxObjectsInGet: 500, maxObjectsInSet: 500 }, [CAP.contacts]: {} },
accounts: {}, primaryAccounts: {}, state: "s1",
} as unknown as JmapSession;
useContacts.setState({ accountId: "a1", available: true, books: {}, cards: {} as Record<string, ContactCard> });
});
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
describe("telling somebody what an LDIF re-import duplicated", () => {
it("counts an entry that matches an existing card on name and address", async () => {
server([card("c1", "Jane Doe", "[email protected]")]);
const r = await useContacts.getState().importLdif(entry("Jane Doe", "[email protected]"), "book1");
expect(r).toEqual({ created: 1, updated: 0, alike: 1 });
});
it("imports it anyway, which is the whole point of counting rather than matching", async () => {
const sets = server([card("c1", "Jane Doe", "[email protected]")]);
await useContacts.getState().importLdif(entry("Jane Doe", "[email protected]"), "book1");
expect(Object.keys(sets[0]!.create!)).toHaveLength(1);
});
it("does not count a name match with a different address", async () => {
// Two people who share a name are two people. This is exactly the guess
// the counting refuses to make on anyone's behalf.
server([card("c1", "Jane Doe", "[email protected]")]);
const r = await useContacts.getState().importLdif(entry("Jane Doe", "[email protected]"), "book1");
expect(r.alike).toBe(0);
});
it("does not count an address match under a different name", async () => {
server([card("c1", "Someone Else", "[email protected]")]);
const r = await useContacts.getState().importLdif(entry("Jane Doe", "[email protected]"), "book1");
expect(r.alike).toBe(0);
});
it("recognizes a match on a second address", async () => {
server([card("c1", "Jane Doe", "[email protected]", "[email protected]")]);
const r = await useContacts.getState().importLdif(entry("Jane Doe", "[email protected]"), "book1");
expect(r.alike).toBe(1);
});
it("ignores case and spacing, which an export and a hand-typed card differ in", async () => {
server([card("c1", " JANE DOE ", "[email protected]")]);
const r = await useContacts.getState().importLdif(entry("Jane Doe", "[email protected]"), "book1");
expect(r.alike).toBe(1);
});
it("counts each entry once however many of its addresses match", async () => {
server([card("c1", "Jane Doe", "[email protected]", "[email protected]")]);
const two = `dn: cn=Jane Doe\ncn: Jane Doe\nmail: [email protected]\nmozillaSecondEmail: [email protected]\n`;
const r = await useContacts.getState().importLdif(two, "book1");
expect(r.alike).toBe(1);
});
it("looks only at the book being imported into", async () => {
const elsewhere = { ...card("c1", "Jane Doe", "[email protected]"), addressBookIds: { book2: true } };
server([elsewhere]);
const r = await useContacts.getState().importLdif(entry("Jane Doe", "[email protected]"), "book1");
expect(r.alike).toBe(0);
});
it("counts nothing against an empty book", async () => {
server([]);
const r = await useContacts.getState().importLdif(entry("Jane Doe", "[email protected]"), "book1");
expect(r).toEqual({ created: 1, updated: 0, alike: 0 });
});
it("does not count the file against itself", async () => {
// Two people in one file are two new cards, neither a duplicate of
// something that was already here. The scan is read before anything lands.
server([]);
const two = entry("Jane Doe", "[email protected]") + "\n" + entry("Alan Turing", "[email protected]");
const r = await useContacts.getState().importLdif(two, "book1");
expect(r).toEqual({ created: 2, updated: 0, alike: 0 });
});
it("makes one card of two entries in a file that share a dn", async () => {
// A directory cannot hold two entries under one name, so a file that does
// is malformed -- and must not produce two cards sharing an identity,
// which is the duplication this all exists to prevent. The later wins.
const sets = server([]);
const twice = entry("Jane Doe", "[email protected]") + "\n" + entry("Jane Doe", "[email protected]");
const r = await useContacts.getState().importLdif(twice, "book1");
expect(r).toEqual({ created: 1, updated: 0, alike: 0 });
const only = Object.values(sets[0]!.create!)[0]!;
expect(Object.values(only.emails as Record<string, { address: string }>)[0]!.address).toBe("[email protected]");
});
});