Files
ihasmail/web/src/store/__tests__/compose-sign-out.test.ts
T
jcoffey-dev dfe885a921 Close the smaller gaps from the security review
Ask for the account password before minting an app password, and keep
sessions the proxy checks from writing the account's own registry objects,
so a session left open on someone else's machine cannot take a credential
away from it. The password is compared with what the session holds; Stalwart
is asked only when 2FA moved the session onto an app password.

Serve attachments and proxied images with no-store on a device that is not
the person's own. Give files from a winmail.dat only the types the server
would show inline. Strip direction controls from sender and attachment
names and from saved filenames.

On signing out, send what is inside its undo window, then close every
composer, so the next person to sign in does not find the last one's draft.

Group sessions by the account Stalwart names and its server, so "sign out
other sessions" also reaches a session opened as a bare or differently
cased username.
2026-09-16 08:47:23 -07:00

54 lines
2.0 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { useCompose } from "@/store/compose";
import { useSession } from "@/store/session";
/**
* A message being written belongs to the session it was written in. On a
* shared machine the next person to sign in -- after an idle sign-out, with no
* reload in between -- used to find the last one's composer still open.
*/
beforeEach(() => {
vi.useFakeTimers();
useSession.setState({ status: "authenticated" });
});
afterEach(() => {
useCompose.setState({ drafts: [], activeKey: null, pendingSends: {} });
vi.useRealTimers();
});
describe("signing out", () => {
it("closes every composer and stops sends that are still waiting", () => {
const run = vi.fn(async () => {});
const timer = window.setTimeout(() => void run(), 5000);
useCompose.setState({
drafts: [{ key: "d1", subject: "Half written" } as never],
activeKey: "d1",
pendingSends: { d2: { timer, toastId: 1, draft: { key: "d2" } as never, run } },
});
useSession.setState({ status: "anonymous" });
expect(useCompose.getState().drafts).toEqual([]);
expect(useCompose.getState().activeKey).toBeNull();
expect(useCompose.getState().pendingSends).toEqual({});
vi.advanceTimersByTime(10_000);
expect(run).not.toHaveBeenCalled();
});
it("leaves the composer alone while still signed in", () => {
useCompose.setState({ drafts: [{ key: "d1" } as never], activeKey: "d1" });
useSession.setState({ pushConnected: true });
expect(useCompose.getState().drafts).toHaveLength(1);
});
it("sends what is inside its undo window before the session goes", async () => {
const run = vi.fn(async () => {});
const timer = window.setTimeout(() => void run(), 5000);
useCompose.setState({ pendingSends: { d2: { timer, toastId: 1, draft: { key: "d2" } as never, run } } });
await useCompose.getState().flushPendingSends();
expect(run).toHaveBeenCalledTimes(1);
vi.advanceTimersByTime(10_000);
expect(run).toHaveBeenCalledTimes(1);
});
});