Skip to content
Open
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
17 changes: 13 additions & 4 deletions src/server/api/memory-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,10 +176,19 @@ export class MemoryApiRepository implements ApiRepository {
}
const now = Date.now();
const windowMilliseconds = windowSeconds * 1000;
const timestamps = this.counters.get(key) ?? [];
const filtered = [...timestamps, ...Array<number>(amount).fill(now)].filter(
(timestamp) => now - timestamp <= windowMilliseconds,
);
const timestamps = this.counters.get(key);
const filtered: number[] = [];

if (timestamps) {
for (let i = 0; i < timestamps.length; i++) {
if (now - timestamps[i] <= windowMilliseconds) {
filtered.push(timestamps[i]);
}
}
}
for (let i = 0; i < amount; i++) {
filtered.push(now);
}
this.counters.set(key, filtered);
return filtered.length;
}
Expand Down
62 changes: 27 additions & 35 deletions src/server/api/postage-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,41 +94,7 @@ export async function quotePostage(
: 15 * 60 * 1000;
const expiresAt = new Date(Date.now() + lifetimeMs).toISOString();

if (rule === "block") {
const amount = policy.minimumPostage;
const result = {
amount,
eligible: false,
reason: "sender_blocked" as const,
trusted: false,
issuedAt,
expiresAt,
digest: signQuote(input.recipient, input.sender, amount, issuedAt, expiresAt),
};

recordAuditEvent({
actor: input.sender,
action: "postage.quote",
targetType: "mailbox",
safeTargetReference: input.recipient,
result: "success",
requestId: context.requestId ?? "unknown",
});
return result;
}

const trusted = rule === "allow";
const amount = trusted ? "0" : policy.minimumPostage;

const result = {
amount,
eligible: true,
reason: trusted ? ("trusted_sender" as const) : ("mailbox_minimum" as const),
trusted,
issuedAt,
expiresAt,
digest: signQuote(input.recipient, input.sender, amount, issuedAt, expiresAt),
};
const result = buildQuoteResult(rule, policy, input, issuedAt, expiresAt);

recordAuditEvent({
actor: input.sender,
Expand All @@ -152,6 +118,32 @@ export async function quotePostage(
}
}

function buildQuoteResult(
rule: string,
policy: { minimumPostage: string },
input: { recipient: string; sender: string },
issuedAt: string,
expiresAt: string,
) {
const blocked = rule === "block";
const trusted = !blocked && rule === "allow";
const amount = blocked ? policy.minimumPostage : trusted ? "0" : policy.minimumPostage;

return {
amount,
eligible: !blocked,
reason: blocked
? ("sender_blocked" as const)
: trusted
? ("trusted_sender" as const)
: ("mailbox_minimum" as const),
trusted,
issuedAt,
expiresAt,
digest: signQuote(input.recipient, input.sender, amount, issuedAt, expiresAt),
};
}

export async function submitPostage(
context: ApiContext,
input: Omit<Postage, "createdAt" | "status">,
Expand Down
83 changes: 83 additions & 0 deletions tests/e2e/audit-log.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { test, expect, openDemoMailbox } from "./fixtures";

test.describe("audit log", () => {
test.beforeEach(async ({ page }) => {
await openDemoMailbox(page);
await page.getByRole("button", { name: "Settings" }).click();
await expect(page.getByRole("heading", { name: "Settings" })).toBeVisible();
await page.getByRole("tab", { name: "Audit log" }).click();
});

test("renders events with summary, actor, and timestamp", async ({ page }) => {
await expect(page.getByText("Session started")).toBeVisible();
await expect(page.getByText("Demo Operator")).toBeVisible();

Check failure on line 13 in tests/e2e/audit-log.spec.ts

View workflow job for this annotation

GitHub Actions / E2E Tests

[chromium] › tests/e2e/audit-log.spec.ts:11:3 › audit log › renders events with summary

1) [chromium] › tests/e2e/audit-log.spec.ts:11:3 › audit log › renders events with summary, actor, and timestamp Retry #2 ─────────────────────────────────────────────────────────────────────────────────────── Error: expect(locator).toBeVisible() failed Locator: getByText('Demo Operator') Expected: visible Error: strict mode violation: getByText('Demo Operator') resolved to 6 elements: 1) <span>Demo Operator</span> aka getByLabel('Security event: Session started').getByText('Demo Operator') 2) <span>Demo Operator</span> aka getByLabel('Policy event: Default policy').getByText('Demo Operator') 3) <span>Demo Operator</span> aka getByLabel('Policy event: Sender allowed').getByText('Demo Operator') 4) <span>Demo Operator</span> aka getByLabel('Policy event: Sender blocked').getByText('Demo Operator') 5) <span>Demo Operator</span> aka getByLabel('Policy event: Sender set to «').getByText('Demo Operator') 6) <span>Demo Operator</span> aka getByLabel('Security event: Session ended').getByText('Demo Operator') Call log: - Expect "toBeVisible" with timeout 10000ms - waiting for getByText('Demo Operator') 11 | test("renders events with summary, actor, and timestamp", async ({ page }) => { 12 | await expect(page.getByText("Session started")).toBeVisible(); > 13 | await expect(page.getByText("Demo Operator")).toBeVisible(); | ^ 14 | 15 | const events = page.getByRole("article"); 16 | const count = await events.count(); at /home/runner/work/stealth/stealth/tests/e2e/audit-log.spec.ts:13:51

Check failure on line 13 in tests/e2e/audit-log.spec.ts

View workflow job for this annotation

GitHub Actions / E2E Tests

[chromium] › tests/e2e/audit-log.spec.ts:11:3 › audit log › renders events with summary

1) [chromium] › tests/e2e/audit-log.spec.ts:11:3 › audit log › renders events with summary, actor, and timestamp Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── Error: expect(locator).toBeVisible() failed Locator: getByText('Demo Operator') Expected: visible Error: strict mode violation: getByText('Demo Operator') resolved to 6 elements: 1) <span>Demo Operator</span> aka getByLabel('Security event: Session started').getByText('Demo Operator') 2) <span>Demo Operator</span> aka getByLabel('Policy event: Default policy').getByText('Demo Operator') 3) <span>Demo Operator</span> aka getByLabel('Policy event: Sender allowed').getByText('Demo Operator') 4) <span>Demo Operator</span> aka getByLabel('Policy event: Sender blocked').getByText('Demo Operator') 5) <span>Demo Operator</span> aka getByLabel('Policy event: Sender set to «').getByText('Demo Operator') 6) <span>Demo Operator</span> aka getByLabel('Security event: Session ended').getByText('Demo Operator') Call log: - Expect "toBeVisible" with timeout 10000ms - waiting for getByText('Demo Operator') 11 | test("renders events with summary, actor, and timestamp", async ({ page }) => { 12 | await expect(page.getByText("Session started")).toBeVisible(); > 13 | await expect(page.getByText("Demo Operator")).toBeVisible(); | ^ 14 | 15 | const events = page.getByRole("article"); 16 | const count = await events.count(); at /home/runner/work/stealth/stealth/tests/e2e/audit-log.spec.ts:13:51

Check failure on line 13 in tests/e2e/audit-log.spec.ts

View workflow job for this annotation

GitHub Actions / E2E Tests

[chromium] › tests/e2e/audit-log.spec.ts:11:3 › audit log › renders events with summary

1) [chromium] › tests/e2e/audit-log.spec.ts:11:3 › audit log › renders events with summary, actor, and timestamp Error: expect(locator).toBeVisible() failed Locator: getByText('Demo Operator') Expected: visible Error: strict mode violation: getByText('Demo Operator') resolved to 6 elements: 1) <span>Demo Operator</span> aka getByLabel('Security event: Session started').getByText('Demo Operator') 2) <span>Demo Operator</span> aka getByLabel('Policy event: Default policy').getByText('Demo Operator') 3) <span>Demo Operator</span> aka getByLabel('Policy event: Sender allowed').getByText('Demo Operator') 4) <span>Demo Operator</span> aka getByLabel('Policy event: Sender blocked').getByText('Demo Operator') 5) <span>Demo Operator</span> aka getByLabel('Policy event: Sender set to «').getByText('Demo Operator') 6) <span>Demo Operator</span> aka getByLabel('Security event: Session ended').getByText('Demo Operator') Call log: - Expect "toBeVisible" with timeout 10000ms - waiting for getByText('Demo Operator') 11 | test("renders events with summary, actor, and timestamp", async ({ page }) => { 12 | await expect(page.getByText("Session started")).toBeVisible(); > 13 | await expect(page.getByText("Demo Operator")).toBeVisible(); | ^ 14 | 15 | const events = page.getByRole("article"); 16 | const count = await events.count(); at /home/runner/work/stealth/stealth/tests/e2e/audit-log.spec.ts:13:51

const events = page.getByRole("article");
const count = await events.count();
expect(count).toBeGreaterThan(0);

await expect(page.getByText(/Showing \d+ of 14 events/)).toBeVisible();
});

test("filters by category", async ({ page }) => {
await page.getByRole("button", { name: "Billing", exact: true }).click();
await expect(page.getByText("Postage attached for incoming message")).toBeVisible();
await expect(page.getByText("Postage settled for msg_4f2a")).toBeVisible();
await expect(page.getByText("Session started")).not.toBeVisible();
});

test("searches by summary text", async ({ page }) => {
await page
.getByPlaceholder("Search summaries, kinds, senders, or message IDs…")
.fill("bounced");
await expect(page.getByText("Message bounced")).toBeVisible();
await expect(page.getByText("Session started")).not.toBeVisible();
});

test("searches by message ID", async ({ page }) => {
await page
.getByPlaceholder("Search summaries, kinds, senders, or message IDs…")
.fill("msg_4f2a");
await expect(page.getByText("msg_4f2a")).toBeVisible();

Check failure on line 41 in tests/e2e/audit-log.spec.ts

View workflow job for this annotation

GitHub Actions / E2E Tests

[chromium] › tests/e2e/audit-log.spec.ts:37:3 › audit log › searches by message ID

2) [chromium] › tests/e2e/audit-log.spec.ts:37:3 › audit log › searches by message ID ──────────── Retry #2 ─────────────────────────────────────────────────────────────────────────────────────── Error: expect(locator).toBeVisible() failed Locator: getByText('msg_4f2a') Expected: visible Error: strict mode violation: getByText('msg_4f2a') resolved to 6 elements: 1) <span class="font-mono text-[10px] opacity-70">msg_4f2a</span> aka getByLabel('Billing event: Postage attached for incoming message').getByText('msg_4f2a') 2) <span class="font-mono text-[10px] opacity-70">msg_4f2a</span> aka getByLabel('Delivery event: Message').getByText('msg_4f2a') 3) <span class="truncate text-foreground">Delivery receipt issued for msg_4f2a</span> aka getByText('Delivery receipt issued for') 4) <span class="font-mono text-[10px] opacity-70">msg_4f2a</span> aka getByLabel('Delivery event: Delivery').getByText('msg_4f2a', { exact: true }) 5) <span class="truncate text-foreground">Postage settled for msg_4f2a</span> aka getByText('Postage settled for msg_4f2a') 6) <span class="font-mono text-[10px] opacity-70">msg_4f2a</span> aka getByLabel('Billing event: Postage settled for msg_4f2a').getByText('msg_4f2a', { exact: true }) Call log: - Expect "toBeVisible" with timeout 10000ms - waiting for getByText('msg_4f2a') 39 | .getByPlaceholder("Search summaries, kinds, senders, or message IDs…") 40 | .fill("msg_4f2a"); > 41 | await expect(page.getByText("msg_4f2a")).toBeVisible(); | ^ 42 | }); 43 | 44 | test("clears search and restores all events", async ({ page }) => { at /home/runner/work/stealth/stealth/tests/e2e/audit-log.spec.ts:41:46

Check failure on line 41 in tests/e2e/audit-log.spec.ts

View workflow job for this annotation

GitHub Actions / E2E Tests

[chromium] › tests/e2e/audit-log.spec.ts:37:3 › audit log › searches by message ID

2) [chromium] › tests/e2e/audit-log.spec.ts:37:3 › audit log › searches by message ID ──────────── Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── Error: expect(locator).toBeVisible() failed Locator: getByText('msg_4f2a') Expected: visible Error: strict mode violation: getByText('msg_4f2a') resolved to 6 elements: 1) <span class="font-mono text-[10px] opacity-70">msg_4f2a</span> aka getByLabel('Billing event: Postage attached for incoming message').getByText('msg_4f2a') 2) <span class="font-mono text-[10px] opacity-70">msg_4f2a</span> aka getByLabel('Delivery event: Message').getByText('msg_4f2a') 3) <span class="truncate text-foreground">Delivery receipt issued for msg_4f2a</span> aka getByText('Delivery receipt issued for') 4) <span class="font-mono text-[10px] opacity-70">msg_4f2a</span> aka getByLabel('Delivery event: Delivery').getByText('msg_4f2a', { exact: true }) 5) <span class="truncate text-foreground">Postage settled for msg_4f2a</span> aka getByText('Postage settled for msg_4f2a') 6) <span class="font-mono text-[10px] opacity-70">msg_4f2a</span> aka getByLabel('Billing event: Postage settled for msg_4f2a').getByText('msg_4f2a', { exact: true }) Call log: - Expect "toBeVisible" with timeout 10000ms - waiting for getByText('msg_4f2a') 39 | .getByPlaceholder("Search summaries, kinds, senders, or message IDs…") 40 | .fill("msg_4f2a"); > 41 | await expect(page.getByText("msg_4f2a")).toBeVisible(); | ^ 42 | }); 43 | 44 | test("clears search and restores all events", async ({ page }) => { at /home/runner/work/stealth/stealth/tests/e2e/audit-log.spec.ts:41:46

Check failure on line 41 in tests/e2e/audit-log.spec.ts

View workflow job for this annotation

GitHub Actions / E2E Tests

[chromium] › tests/e2e/audit-log.spec.ts:37:3 › audit log › searches by message ID

2) [chromium] › tests/e2e/audit-log.spec.ts:37:3 › audit log › searches by message ID ──────────── Error: expect(locator).toBeVisible() failed Locator: getByText('msg_4f2a') Expected: visible Error: strict mode violation: getByText('msg_4f2a') resolved to 6 elements: 1) <span class="font-mono text-[10px] opacity-70">msg_4f2a</span> aka getByLabel('Billing event: Postage attached for incoming message').getByText('msg_4f2a') 2) <span class="font-mono text-[10px] opacity-70">msg_4f2a</span> aka getByLabel('Delivery event: Message').getByText('msg_4f2a') 3) <span class="truncate text-foreground">Delivery receipt issued for msg_4f2a</span> aka getByText('Delivery receipt issued for') 4) <span class="font-mono text-[10px] opacity-70">msg_4f2a</span> aka getByLabel('Delivery event: Delivery').getByText('msg_4f2a', { exact: true }) 5) <span class="truncate text-foreground">Postage settled for msg_4f2a</span> aka getByText('Postage settled for msg_4f2a') 6) <span class="font-mono text-[10px] opacity-70">msg_4f2a</span> aka getByLabel('Billing event: Postage settled for msg_4f2a').getByText('msg_4f2a', { exact: true }) Call log: - Expect "toBeVisible" with timeout 10000ms - waiting for getByText('msg_4f2a') 39 | .getByPlaceholder("Search summaries, kinds, senders, or message IDs…") 40 | .fill("msg_4f2a"); > 41 | await expect(page.getByText("msg_4f2a")).toBeVisible(); | ^ 42 | }); 43 | 44 | test("clears search and restores all events", async ({ page }) => { at /home/runner/work/stealth/stealth/tests/e2e/audit-log.spec.ts:41:46
});

test("clears search and restores all events", async ({ page }) => {
await page
.getByPlaceholder("Search summaries, kinds, senders, or message IDs…")
.fill("bounced");
await expect(page.getByText("Message bounced")).toBeVisible();

await page.getByRole("button", { name: "Clear search" }).click();
await expect(page.getByText("Session started")).toBeVisible();
});

test("shows empty state when no events match filters", async ({ page }) => {
await page
.getByPlaceholder("Search summaries, kinds, senders, or message IDs…")
.fill("zzzznotfound");
await expect(page.getByText("No events match these filters")).toBeVisible();
await expect(page.getByRole("button", { name: "Clear filters" })).toBeVisible();
});

test("clear filters button restores all events from no-match state", async ({ page }) => {
await page
.getByPlaceholder("Search summaries, kinds, senders, or message IDs…")
.fill("zzzznotfound");
await expect(page.getByText("No events match these filters")).toBeVisible();

await page.getByRole("button", { name: "Clear filters" }).click();
await expect(page.getByText("Session started")).toBeVisible();
});

test("copy and export buttons are enabled when events are visible", async ({ page }) => {
await expect(page.getByRole("button", { name: "Copy diagnostics" })).toBeEnabled();
await expect(page.getByRole("button", { name: /^Export JSON/ })).toBeEnabled();
});

test("shows events count and total", async ({ page }) => {
await expect(page.getByText(/Showing \d+ of 14 events/)).toBeVisible();

await page.getByRole("button", { name: "Billing", exact: true }).click();
await expect(page.getByText(/Showing \d+ of 14 events for the current filters/)).toBeVisible();
});
});
147 changes: 146 additions & 1 deletion tests/unit/audit-log/useAuditLog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ import {
formatEventAsText,
hasActiveAuditFilter,
} from "@/features/audit-log/useAuditLog";
import type { AuditFilter } from "@/features/audit-log/types";
import { CATEGORY_FOR_KIND } from "@/features/audit-log/types";
import type { AuditEventKind, AuditFilter } from "@/features/audit-log/types";

// ---------------------------------------------------------------------------
// filterAuditEvents
// ---------------------------------------------------------------------------

describe("filterAuditEvents", () => {
it("returns all events when no filters are active (success path)", () => {
Expand Down Expand Up @@ -34,8 +39,45 @@ describe("filterAuditEvents", () => {
const filter: AuditFilter = { category: "security", search: "msg_4f2a" };
expect(filterAuditEvents(MOCK_AUDIT_EVENTS, filter)).toEqual([]);
});

it("performs case-insensitive search", () => {
const filter: AuditFilter = { category: "all", search: "SESSION" };
const events = filterAuditEvents(MOCK_AUDIT_EVENTS, filter);

expect(events.every((event) => event.kind.startsWith("session."))).toBe(true);
});

it("searches by senderDisplayName in context", () => {
const filter: AuditFilter = { category: "all", search: "Marcin Kowalski" };
const events = filterAuditEvents(MOCK_AUDIT_EVENTS, filter);

expect(events.length).toBeGreaterThan(0);
expect(
events.every((event) =>
event.context?.senderDisplayName?.toLowerCase().includes("marcin kowalski"),
),
).toBe(true);
});

it("intersects category and search filters", () => {
const filter: AuditFilter = { category: "billing", search: "msg_4f2a" };
const events = filterAuditEvents(MOCK_AUDIT_EVENTS, filter);

expect(events.length).toBeGreaterThan(0);
expect(events.every((event) => event.category === "billing")).toBe(true);
expect(events.every((event) => event.context?.messageId === "msg_4f2a")).toBe(true);
});

it("handles empty events array", () => {
const filter: AuditFilter = { category: "all", search: "" };
expect(filterAuditEvents([], filter)).toEqual([]);
});
});

// ---------------------------------------------------------------------------
// hasActiveAuditFilter
// ---------------------------------------------------------------------------

describe("hasActiveAuditFilter", () => {
it("treats the default filter as inactive", () => {
expect(hasActiveAuditFilter({ category: "all", search: "" })).toBe(false);
Expand All @@ -45,8 +87,16 @@ describe("hasActiveAuditFilter", () => {
expect(hasActiveAuditFilter({ category: "policy", search: "" })).toBe(true);
expect(hasActiveAuditFilter({ category: "all", search: "session" })).toBe(true);
});

it("treats whitespace-only search as inactive because the input is trimmed", () => {
expect(hasActiveAuditFilter({ category: "all", search: " " })).toBe(false);
});
});

// ---------------------------------------------------------------------------
// formatEventAsText
// ---------------------------------------------------------------------------

describe("formatEventAsText", () => {
it("formats a readable diagnostics line without message body content", () => {
const event = MOCK_AUDIT_EVENTS[0];
Expand All @@ -57,4 +107,99 @@ describe("formatEventAsText", () => {
expect(line).toContain(event.summary);
expect(line).not.toMatch(/body=/i);
});

it("includes context key-value pairs when context is present", () => {
const event = MOCK_AUDIT_EVENTS[2];
const line = formatEventAsText(event);

expect(line).toContain(event.ts);
expect(line).toContain(event.kind);
expect(line).toContain("policyValue=request");
});

it("does not include a context segment when context is absent", () => {
const event = MOCK_AUDIT_EVENTS[0];
const line = formatEventAsText(event);

expect(line).not.toMatch(/\| $/);
});

it("renders user actor with displayName", () => {
const event = MOCK_AUDIT_EVENTS[0];
const line = formatEventAsText(event);

expect(line).toContain("Demo Operator");
});

it("renders system actor", () => {
const event = MOCK_AUDIT_EVENTS[1];
const line = formatEventAsText(event);

expect(line).toContain("system");
});

it("renders relay actor with relayId", () => {
const event = MOCK_AUDIT_EVENTS[4];
const line = formatEventAsText(event);

expect(line).toContain("relay-us-east-1");
});

it("includes multiple context fields joined by spaces", () => {
const event = MOCK_AUDIT_EVENTS[4];
const line = formatEventAsText(event);

expect(line).toContain("messageId=msg_4f2a");
expect(line).toContain("senderDisplayName=Marcin Kowalski");
});
});

// ---------------------------------------------------------------------------
// CATEGORY_FOR_KIND mapping
// ---------------------------------------------------------------------------

describe("CATEGORY_FOR_KIND", () => {
it("maps every defined AuditEventKind to a category", () => {
const kinds: AuditEventKind[] = [
"policy.default_changed",
"policy.sender_allowed",
"policy.sender_blocked",
"policy.sender_verified",
"delivery.message_received",
"delivery.receipt_issued",
"delivery.message_bounced",
"session.started",
"session.ended",
"identity.resolved",
"identity.verification_failed",
"postage.attached",
"postage.settled",
"postage.refunded",
];

for (const kind of kinds) {
expect(CATEGORY_FOR_KIND[kind]).toBeDefined();
}
});

it("correctly classifies each kind into the expected category", () => {
expect(CATEGORY_FOR_KIND["policy.default_changed"]).toBe("policy");
expect(CATEGORY_FOR_KIND["policy.sender_allowed"]).toBe("policy");
expect(CATEGORY_FOR_KIND["policy.sender_blocked"]).toBe("policy");
expect(CATEGORY_FOR_KIND["policy.sender_verified"]).toBe("policy");
expect(CATEGORY_FOR_KIND["delivery.message_received"]).toBe("delivery");
expect(CATEGORY_FOR_KIND["delivery.receipt_issued"]).toBe("delivery");
expect(CATEGORY_FOR_KIND["delivery.message_bounced"]).toBe("delivery");
expect(CATEGORY_FOR_KIND["session.started"]).toBe("security");
expect(CATEGORY_FOR_KIND["session.ended"]).toBe("security");
expect(CATEGORY_FOR_KIND["identity.resolved"]).toBe("security");
expect(CATEGORY_FOR_KIND["identity.verification_failed"]).toBe("security");
expect(CATEGORY_FOR_KIND["postage.attached"]).toBe("billing");
expect(CATEGORY_FOR_KIND["postage.settled"]).toBe("billing");
expect(CATEGORY_FOR_KIND["postage.refunded"]).toBe("billing");
});

it("has exactly 14 entries matching the AuditEventKind union", () => {
expect(Object.keys(CATEGORY_FOR_KIND)).toHaveLength(14);
});
});
Loading