Add Mailing lists to Administration
A mailing list is an address that passes mail on to everyone on it. To Stalwart it is its own object, x:MailingList, behind sysMailingList*, so it gets its own section under Directory after Groups: search, fifty to a page with each list's recipient count, and a panel to create, edit and delete one. Recipients are a property of the list, so unlike a group's members they save with the rest of the panel. What Save sends for them is only what was added and removed, one recipients/<address> pointer each -- the patch the live server accepted -- so a recipient added elsewhere while the panel was open is not taken out. They can be pasted several at a time, from a spreadsheet column, a comma-separated line or Name <address>; anything with an @ that is not an address stays in the box with a note. Past a dozen, a filter narrows them. That is all a list is in Stalwart -- no owners, moderation or posting rules -- so that is all the panel offers. The mock answers x:MailingList with two lists, the recipient set's live shape, and the refusals a wrong address, a clash with an account and a missing permission get. Twenty-five new strings and one plural, in all nine catalogues.
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import { act } from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { useSession } from "@/store/session";
|
||||
import type { JmapSession } from "@/jmap/types";
|
||||
import type { DirectoryList } from "@/lib/adminLists";
|
||||
import type { DirectoryContext } from "../directoryContext";
|
||||
|
||||
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
const api = vi.hoisted(() => ({ updateList: vi.fn(async () => {}) }));
|
||||
vi.mock("@/lib/adminLists", async (original) => ({ ...(await original<typeof import("@/lib/adminLists")>()), updateList: api.updateList }));
|
||||
|
||||
const { ListSheet } = await import("../ListSheet");
|
||||
|
||||
const list: DirectoryList = { id: "l1", name: "announce", domainId: "d1", emailAddress: "[email protected]", description: "Announcements", recipients: { "[email protected]": true, "[email protected]": true }, aliases: {} };
|
||||
const ctx: DirectoryContext = { domains: [{ id: "d1", name: "example.com" }], roles: null, groups: new Map(), self: { ids: new Set(), address: "[email protected]" } };
|
||||
const signIn = (permissions: string[]) =>
|
||||
useSession.setState({ session: { capabilities: {}, accounts: {}, primaryAccounts: {}, username: "[email protected]", ihasmail: { permissions } } as unknown as JmapSession });
|
||||
const button = (host: HTMLElement, label: string) => [...host.querySelectorAll("button")].find((b) => b.getAttribute("aria-label") === label || b.textContent?.trim() === label);
|
||||
const type = async (input: HTMLInputElement, value: string) => {
|
||||
await act(async () => {
|
||||
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")!.set!.call(input, value);
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
});
|
||||
};
|
||||
|
||||
describe("the mailing list sheet", () => {
|
||||
let host: HTMLDivElement;
|
||||
let root: Root;
|
||||
const render = async () => {
|
||||
await act(async () => {
|
||||
root.render(<ListSheet list={list} ctx={ctx} onClose={() => {}} onChanged={() => {}} onCreated={() => {}} onDeleted={() => {}} />);
|
||||
});
|
||||
};
|
||||
beforeEach(() => {
|
||||
host = document.createElement("div");
|
||||
document.body.appendChild(host);
|
||||
root = createRoot(host);
|
||||
api.updateList.mockClear();
|
||||
});
|
||||
afterEach(async () => {
|
||||
await act(async () => root.unmount());
|
||||
host.remove();
|
||||
});
|
||||
|
||||
it("saves only the recipients added and removed, and says what it could not read", async () => {
|
||||
signIn(["sysMailingListGet", "sysMailingListQuery", "sysMailingListUpdate"]);
|
||||
await render();
|
||||
await act(async () => button(host, "Remove [email protected]")!.click());
|
||||
await type(host.querySelector<HTMLInputElement>('input[aria-label="Add recipients"]')!, "Bob <[email protected]>, oops@");
|
||||
await act(async () => button(host, "Add")!.click());
|
||||
expect(host.querySelector(".admin-notice.warn")?.textContent).toContain("oops@");
|
||||
expect(host.querySelector<HTMLInputElement>('input[aria-label="Add recipients"]')!.value).toBe("oops@");
|
||||
await act(async () => button(host, "Save changes")!.click());
|
||||
expect(api.updateList).toHaveBeenCalledWith("l1", { "recipients/[email protected]": null, "recipients/[email protected]": true });
|
||||
});
|
||||
|
||||
it("offers nothing to change to a role that can only read, and no delete without the permission", async () => {
|
||||
signIn(["sysMailingListGet", "sysMailingListQuery"]);
|
||||
await render();
|
||||
expect(host.textContent).toContain("Your role lets you view mailing lists but not change them.");
|
||||
expect(button(host, "Remove [email protected]")).toBeUndefined();
|
||||
expect(host.querySelector('input[aria-label="Add recipients"]')).toBeNull();
|
||||
expect(button(host, "Delete list…")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user