A run on the production server with throwaway tenants, a role, lists and a domain, all removed, found three things the source reading had not: - Something in a tenant has to be on a domain in that tenant (a list in a tenant on an unassigned domain is invalidForeignKey), while something in no tenant may be on a tenant's domain. The account panel's tenant choice offered every tenant; it offers only the domain's now, and a new account starts in the tenant of the domain it is made on. The domain list reads memberTenantId for it. - A domain created in a tenant puts its DKIM keys there too, and they keep the tenant from being deleted. They are counted with the rest, so Delete is not offered while any remain. - Stalwart lets a domain leave a tenant while the tenant still has accounts on it, stranding them. The panel asks first and refuses while any are there. The refusal to delete a tenant that holds anything was confirmed, as were tenant create, quota pointers, logo and rename. The mock follows the domain rule, filters DKIM keys by tenant, and KNOWN-ISSUES records the run. The non-Enterprise notice is now just "Tenants are a Stalwart Enterprise feature." Two sentences were reworded and one plural added, in all nine catalogues, and the old sentences are gone.
50 lines
2.5 KiB
TypeScript
50 lines
2.5 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import { client } from "@/jmap/client";
|
|
import { countTenantMembers, drawableLogo, quotasPatch, setDomainTenant } from "@/lib/adminTenants";
|
|
|
|
describe("a tenant's limits", () => {
|
|
it("change one pointer each, leaving the quotas ihasmail does not offer alone", () => {
|
|
const before = { maxAccounts: 25, maxDomains: 2, maxOauthClients: 7 };
|
|
expect(quotasPatch(before, { maxAccounts: 30, maxDomains: null, maxGroups: 5, maxRoles: null })).toEqual({
|
|
"quotas/maxAccounts": 30,
|
|
"quotas/maxDomains": null,
|
|
"quotas/maxGroups": 5,
|
|
});
|
|
expect(quotasPatch(before, { maxAccounts: 25 })).toEqual({});
|
|
});
|
|
});
|
|
|
|
describe("what a tenant holds", () => {
|
|
it("is counted with a memberTenantId filter per kind, users and groups apart", async () => {
|
|
const call = vi.spyOn(client, "call").mockImplementation(async (method, args) => {
|
|
const f = (args as { filter: Record<string, unknown> }).filter;
|
|
if (method === "x:Role/query") throw new Error("forbidden");
|
|
return { total: method === "x:Account/query" && f["@type"] === "Group" ? 2 : 1 };
|
|
});
|
|
expect(await countTenantMembers("t1")).toEqual({ accounts: 1, groups: 2, lists: 1, domains: 1, dkimKeys: 1 });
|
|
expect(call).toHaveBeenCalledWith("x:Account/query", { filter: { "@type": "User", memberTenantId: "t1" }, limit: 0, calculateTotal: true });
|
|
expect(call).toHaveBeenCalledWith("x:Domain/query", { filter: { memberTenantId: "t1" }, limit: 0, calculateTotal: true });
|
|
call.mockRestore();
|
|
});
|
|
|
|
it("moves a domain in and out by its memberTenantId", async () => {
|
|
const call = vi.spyOn(client, "call").mockResolvedValue({ updated: { d4: null } });
|
|
await setDomainTenant("d4", "t1");
|
|
expect(call).toHaveBeenLastCalledWith("x:Domain/set", { update: { d4: { memberTenantId: "t1" } } });
|
|
await setDomainTenant("d4", null);
|
|
expect(call).toHaveBeenLastCalledWith("x:Domain/set", { update: { d4: { memberTenantId: null } } });
|
|
call.mockRestore();
|
|
});
|
|
});
|
|
|
|
describe("a tenant's logo", () => {
|
|
it("is drawn only from https or an image data URL", () => {
|
|
expect(drawableLogo("https://example.com/logo.png")).toBe("https://example.com/logo.png");
|
|
expect(drawableLogo("data:image/png;base64,AAAA")).toBe("data:image/png;base64,AAAA");
|
|
expect(drawableLogo("http://example.com/logo.png")).toBeNull();
|
|
expect(drawableLogo("javascript:alert(1)")).toBeNull();
|
|
expect(drawableLogo("data:text/html;base64,AAAA")).toBeNull();
|
|
expect(drawableLogo(null)).toBeNull();
|
|
});
|
|
});
|