Skip to content
This repository was archived by the owner on Jul 30, 2026. It is now read-only.
Merged
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
40 changes: 40 additions & 0 deletions src/pages/yivi-popup/recipients.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { Recipient, PostGuard } from "@e4a/pg-js";
import type { SerializedRecipient } from "../../lib/types";
import { EMAIL_ATTRIBUTE_TYPE } from "../../lib/utils";

// Pulled out of yivi-popup.ts so the recipient-rebuild can be tested
// without depending on the DOM / popup runtime. The function is a pure
// translation of the wire format the background sends us into the
// typed Recipient builders pg-js exposes.

export type RecipientFactory = Pick<
PostGuard["recipient"],
"email" | "emailDomain"
>;

/**
* Reconstitute the `Recipient[]` argument for `pg.encrypt()` from the
* `SerializedRecipient[]` the background put on the wire. Custom
* attribute disclosures are layered onto the base via `extraAttribute`;
* the email attribute itself is the implicit identity and is not
* re-added.
*/
export function buildRecipients(
factory: RecipientFactory,
serialized: readonly SerializedRecipient[],
): Recipient[] {
return serialized.map((r) => {
const base =
r.type === "emailDomain"
? factory.emailDomain(r.email)
: factory.email(r.email);
if (r.policy) {
for (const attr of r.policy) {
if (attr.t !== EMAIL_ATTRIBUTE_TYPE) {
base.extraAttribute(attr.t, attr.v);
}
}
}
return base;
});
}
15 changes: 2 additions & 13 deletions src/pages/yivi-popup/yivi-popup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {
DecryptPopupData,
} from "../../lib/types";
import { EMAIL_ATTRIBUTE_TYPE } from "../../lib/utils";
import { buildRecipients } from "./recipients";

// console.log calls are stripped in release builds by esbuild's `pure` option

Expand Down Expand Up @@ -102,19 +103,7 @@ async function handleEncrypt(pg: PostGuard, data: EncryptPopupData, windowId: nu
const mimeData = fromBase64(data.mimeDataBase64);

// Rebuild typed recipients from serialized data
const recipients: Recipient[] = data.recipients.map((r) => {
const base = r.type === "emailDomain"
? pg.recipient.emailDomain(r.email)
: pg.recipient.email(r.email);
if (r.policy) {
for (const attr of r.policy) {
if (attr.t !== EMAIL_ATTRIBUTE_TYPE) {
base.extraAttribute(attr.t, attr.v);
}
}
}
return base;
});
const recipients: Recipient[] = buildRecipients(pg.recipient, data.recipients);

// Encrypt with element-based Yivi signing
const sealed = pg.encrypt({
Expand Down
117 changes: 92 additions & 25 deletions tests/crypto-popup.test.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,125 @@
import { describe, it, expect } from "vitest";

// These tests verify the crypto popup (yivi-popup) logic.
// They require browser API mocks and a PostGuard SDK mock.
import { describe, it, expect, beforeEach, vi } from "vitest";
import { buildRecipients } from "../src/pages/yivi-popup/recipients";
import type { SerializedRecipient } from "../src/lib/types";

interface FakeRecipient {
kind: "email" | "emailDomain";
email: string;
extras: Array<{ t: string; v: string }>;
extraAttribute(t: string, v: string): FakeRecipient;
}

function makeFactory() {
const make = (kind: "email" | "emailDomain") => (email: string) => {
const r: FakeRecipient = {
kind,
email,
extras: [],
extraAttribute(t, v) {
r.extras.push({ t, v });
return r;
},
};
return r;
};
return {
email: vi.fn(make("email")),
emailDomain: vi.fn(make("emailDomain")),
};
}

let factory: ReturnType<typeof makeFactory>;

beforeEach(() => {
factory = makeFactory();
});

describe("crypto popup — initialization", () => {
it.todo("should request init data using its own window ID");

it.todo("should show error when window ID cannot be resolved");

it.todo("should show error when no pending entry exists in background");

it.todo("should create PostGuard instance with config from background");
});

describe("crypto popup — encrypt", () => {
it.todo("should decode mimeDataBase64 before passing to SDK");

it.todo("should rebuild typed recipients from serialized data");

it.todo("should map customPolicy recipients with pg.recipient.withPolicy");

it.todo("should map emailDomain recipients with pg.recipient.emailDomain");

it.todo("should map plain email recipients with pg.recipient.email");
it("should rebuild typed recipients from serialized data", () => {
const input: SerializedRecipient[] = [
{ type: "email", email: "a@example.com" },
{ type: "email", email: "b@example.com" },
];
const out = buildRecipients(factory as any, input);
expect(out).toHaveLength(2);
expect(factory.email).toHaveBeenCalledTimes(2);
expect(factory.emailDomain).not.toHaveBeenCalled();
});

it("should map customPolicy recipients with extraAttribute calls", () => {
const input: SerializedRecipient[] = [
{
type: "email",
email: "alice@example.com",
policy: [
{ t: "pbdf.sidn-pbdf.email.email", v: "alice@example.com" },
{ t: "pbdf.gemeente.personalData.fullname", v: "Alice" },
{ t: "pbdf.pbdf.surfnet-2.id", v: "alice@uni.example" },
],
},
];
const out = buildRecipients(factory as any, input) as any as FakeRecipient[];
// The email attribute is the implicit identity and must NOT be
// re-added via extraAttribute.
expect(out[0].extras).toEqual([
{ t: "pbdf.gemeente.personalData.fullname", v: "Alice" },
{ t: "pbdf.pbdf.surfnet-2.id", v: "alice@uni.example" },
]);
});

it("should map emailDomain recipients with pg.recipient.emailDomain", () => {
const input: SerializedRecipient[] = [
{ type: "emailDomain", email: "@example.com" },
];
buildRecipients(factory as any, input);
expect(factory.emailDomain).toHaveBeenCalledWith("@example.com");
expect(factory.email).not.toHaveBeenCalled();
});

it("should map plain email recipients with pg.recipient.email", () => {
const input: SerializedRecipient[] = [
{ type: "email", email: "plain@example.com" },
];
buildRecipients(factory as any, input);
expect(factory.email).toHaveBeenCalledWith("plain@example.com");
expect(factory.emailDomain).not.toHaveBeenCalled();
});

it("should not call extraAttribute when no policy is set", () => {
const input: SerializedRecipient[] = [
{ type: "email", email: "a@example.com" },
];
const out = buildRecipients(factory as any, input) as any as FakeRecipient[];
expect(out[0].extras).toEqual([]);
});

it.todo("should pass element selector for Yivi QR rendering");

it.todo("should send encrypt result back to background with correct windowId");

it.todo("should include attachment size in result for size-gating");

it.todo("should auto-close popup after successful encryption");
});

describe("crypto popup — decrypt", () => {
it.todo("should decode ciphertextBase64 before passing to SDK");

it.todo("should pass recipient email to SDK for key selection");

it.todo("should pass element selector for Yivi QR rendering");

it.todo("should send decrypt result back to background with correct windowId");

it.todo("should include sender identity in result");

it.todo("should auto-close popup after successful decryption");
});

describe("crypto popup — error handling", () => {
it.todo("should send error message to background on encrypt failure");

it.todo("should send error message to background on decrypt failure");

it.todo("should display error in popup UI");

it.todo("should not auto-close on error");
});