alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
544 lines (443 loc) • 16.6 kB
text/typescript
import { Alepha } from "alepha";
import { DateTimeProvider } from "alepha/datetime";
import { $repository } from "alepha/orm";
import { AlephaOrmPostgres } from "alepha/orm/postgres";
import { describe, it } from "vitest";
import { AlephaApiAudits, AuditService, audits } from "../index.ts";
/**
* Direct repository access so retention tests can backdate `createdAt` (which
* the public `create` API does not expose).
*/
class Db {
audits = $repository(audits);
}
const setup = async () => {
const alepha = Alepha.create({
env: { LOG_LEVEL: "error" },
});
alepha.with(AlephaOrmPostgres);
alepha.with(AlephaApiAudits);
// Inject before start: the container locks once started, so test-only
// services (Db) must be registered first.
const db = alepha.inject(Db);
const time = alepha.inject(DateTimeProvider);
await alepha.start();
const auditService = alepha.inject(AuditService);
return {
alepha,
auditService,
db,
time,
};
};
describe("alepha/api/audits - AuditService", () => {
describe("create", () => {
it("should create a basic audit entry", async ({ expect }) => {
const { auditService } = await setup();
const entry = await auditService.create({
type: "test",
action: "create",
description: "Test audit entry",
});
expect(entry.id).toBeDefined();
expect(entry.type).toBe("test");
expect(entry.action).toBe("create");
expect(entry.description).toBe("Test audit entry");
expect(entry.severity).toBe("info");
expect(entry.success).toBe(true);
expect(entry.createdAt).toBeDefined();
});
it("should create audit entry with all fields", async ({ expect }) => {
const { auditService } = await setup();
const entry = await auditService.create({
type: "user",
action: "update",
severity: "warning",
userId: "550e8400-e29b-41d4-a716-446655440000",
userRealm: "admin",
userEmail: "admin@example.com",
resourceType: "user",
resourceId: "660e8400-e29b-41d4-a716-446655440001",
description: "Updated user profile",
metadata: { field: "email", oldValue: "old@test.com" },
ipAddress: "192.168.1.1",
userAgent: "Mozilla/5.0",
sessionId: "770e8400-e29b-41d4-a716-446655440002",
requestId: "req-123",
success: true,
});
expect(entry.type).toBe("user");
expect(entry.action).toBe("update");
expect(entry.severity).toBe("warning");
expect(entry.userId).toBe("550e8400-e29b-41d4-a716-446655440000");
expect(entry.userRealm).toBe("admin");
expect(entry.userEmail).toBe("admin@example.com");
expect(entry.resourceType).toBe("user");
expect(entry.resourceId).toBe("660e8400-e29b-41d4-a716-446655440001");
expect(entry.metadata).toEqual({
field: "email",
oldValue: "old@test.com",
});
expect(entry.ipAddress).toBe("192.168.1.1");
expect(entry.userAgent).toBe("Mozilla/5.0");
expect(entry.sessionId).toBe("770e8400-e29b-41d4-a716-446655440002");
expect(entry.requestId).toBe("req-123");
});
it("should create audit entry with failure status", async ({ expect }) => {
const { auditService } = await setup();
const entry = await auditService.create({
type: "auth",
action: "login",
success: false,
errorMessage: "Invalid credentials",
});
expect(entry.success).toBe(false);
expect(entry.errorMessage).toBe("Invalid credentials");
});
});
describe("record", () => {
it("should record audit event using convenience method", async ({
expect,
}) => {
const { auditService } = await setup();
const entry = await auditService.record("payment", "create", {
userId: "550e8400-e29b-41d4-a716-446655440000",
resourceType: "payment",
resourceId: "pay-123",
metadata: { amount: 99.99 },
});
expect(entry.type).toBe("payment");
expect(entry.action).toBe("create");
expect(entry.resourceType).toBe("payment");
expect(entry.resourceId).toBe("pay-123");
});
});
describe("severity from success", () => {
it("defaults a failed audit to warning severity", async ({ expect }) => {
const { auditService } = await setup();
const entry = await auditService.create({
type: "auth",
action: "login",
success: false,
errorMessage: "Invalid password",
});
expect(entry.success).toBe(false);
expect(entry.severity).toBe("warning");
});
it("defaults a successful audit to info severity", async ({ expect }) => {
const { auditService } = await setup();
const entry = await auditService.create({
type: "auth",
action: "login",
});
expect(entry.severity).toBe("info");
});
it("respects an explicit severity over the success default", async ({
expect,
}) => {
const { auditService } = await setup();
const entry = await auditService.create({
type: "security",
action: "sessions_invalidated",
severity: "warning",
});
expect(entry.severity).toBe("warning");
});
});
describe("find", () => {
it("should find audit entries with pagination", async ({ expect }) => {
const { auditService } = await setup();
// Create multiple entries
await auditService.create({ type: "test", action: "action1" });
await auditService.create({ type: "test", action: "action2" });
await auditService.create({ type: "test", action: "action3" });
const result = await auditService.find({ size: 2 });
expect(result.content.length).toBeLessThanOrEqual(2);
expect(result.page).toBeDefined();
});
it("should filter by type", async ({ expect }) => {
const { auditService } = await setup();
await auditService.create({ type: "auth", action: "login" });
await auditService.create({ type: "user", action: "create" });
const result = await auditService.find({ type: "auth" });
expect(result.content.every((e) => e.type === "auth")).toBe(true);
});
it("should filter by severity", async ({ expect }) => {
const { auditService } = await setup();
await auditService.create({
type: "test",
action: "info",
severity: "info",
});
await auditService.create({
type: "test",
action: "warning",
severity: "warning",
});
const result = await auditService.find({ severity: "warning" });
expect(result.content.every((e) => e.severity === "warning")).toBe(true);
});
it("should filter by userId", async ({ expect }) => {
const { auditService } = await setup();
const userId = "550e8400-e29b-41d4-a716-446655440000";
await auditService.create({ type: "test", action: "a", userId });
await auditService.create({ type: "test", action: "b" });
const result = await auditService.find({ userId });
expect(result.content.every((e) => e.userId === userId)).toBe(true);
});
it("should filter by success status", async ({ expect }) => {
const { auditService } = await setup();
await auditService.create({
type: "test",
action: "success",
success: true,
});
await auditService.create({
type: "test",
action: "failure",
success: false,
});
const failures = await auditService.find({ success: false });
expect(failures.content.every((e) => e.success === false)).toBe(true);
});
it("should sort by createdAt descending by default", async ({ expect }) => {
const { auditService } = await setup();
await auditService.create({ type: "test", action: "first" });
await new Promise((r) => setTimeout(r, 10));
await auditService.create({ type: "test", action: "second" });
const result = await auditService.find({ type: "test" });
if (result.content.length >= 2) {
const first = new Date(result.content[0].createdAt).getTime();
const second = new Date(result.content[1].createdAt).getTime();
expect(first).toBeGreaterThanOrEqual(second);
}
});
});
describe("findByUser", () => {
it("should find audit entries for a specific user", async ({ expect }) => {
const { auditService } = await setup();
const userId = "550e8400-e29b-41d4-a716-446655440000";
await auditService.create({ type: "auth", action: "login", userId });
await auditService.create({ type: "user", action: "update", userId });
const result = await auditService.findByUser(userId);
expect(result.content.every((e) => e.userId === userId)).toBe(true);
});
});
describe("findByResource", () => {
it("should find audit entries for a specific resource", async ({
expect,
}) => {
const { auditService } = await setup();
await auditService.create({
type: "user",
action: "create",
resourceType: "user",
resourceId: "user-123",
});
await auditService.create({
type: "user",
action: "update",
resourceType: "user",
resourceId: "user-123",
});
const result = await auditService.findByResource("user", "user-123");
expect(
result.content.every(
(e) => e.resourceType === "user" && e.resourceId === "user-123",
),
).toBe(true);
});
});
describe("getStats", () => {
it("should return audit statistics", async ({ expect }) => {
const { auditService } = await setup();
// Create diverse entries
await auditService.create({
type: "auth",
action: "login",
success: true,
});
await auditService.create({
type: "auth",
action: "login",
severity: "warning",
success: false,
});
await auditService.create({
type: "user",
action: "create",
success: true,
});
await auditService.create({
type: "system",
action: "error",
severity: "critical",
success: false,
});
const stats = await auditService.getStats();
expect(stats.total).toBeGreaterThanOrEqual(4);
expect(stats.byType).toBeDefined();
expect(stats.bySeverity).toBeDefined();
expect(stats.bySeverity.info).toBeGreaterThanOrEqual(0);
expect(stats.bySeverity.warning).toBeGreaterThanOrEqual(0);
expect(stats.bySeverity.critical).toBeGreaterThanOrEqual(0);
expect(stats.successRate).toBeDefined();
expect(stats.recentFailures).toBeDefined();
});
it("should calculate correct success rate", async ({ expect }) => {
const { auditService } = await setup();
// Create 3 successful and 1 failed
await auditService.create({
type: "stats",
action: "test1",
success: true,
});
await auditService.create({
type: "stats",
action: "test2",
success: true,
});
await auditService.create({
type: "stats",
action: "test3",
success: true,
});
await auditService.create({
type: "stats",
action: "test4",
success: false,
});
const stats = await auditService.getStats();
// Success rate should be around 75% for the entries we created
// (but there might be other entries from other tests)
expect(stats.successRate).toBeGreaterThan(0);
expect(stats.successRate).toBeLessThanOrEqual(1);
});
});
describe("registerType", () => {
it("should register audit types", async ({ expect }) => {
const { auditService } = await setup();
auditService.registerType({
type: "custom",
description: "Custom audit type",
actions: ["create", "update", "delete"],
});
const types = auditService.getRegisteredTypes();
const customType = types.find((t) => t.type === "custom");
expect(customType).toBeDefined();
expect(customType?.description).toBe("Custom audit type");
expect(customType?.actions).toEqual(["create", "update", "delete"]);
});
});
describe("getById", () => {
it("should retrieve audit entry by ID", async ({ expect }) => {
const { auditService } = await setup();
const created = await auditService.create({
type: "test",
action: "getById",
description: "Test getById",
});
const retrieved = await auditService.getById(String(created.id));
expect(retrieved.id).toBe(created.id);
expect(retrieved.type).toBe("test");
expect(retrieved.action).toBe("getById");
});
});
describe("deleteExpired", () => {
// Cutoffs are always computed from `now`, so backdating entries keeps the
// delete window in the past — concurrent test files' recent rows are safe.
const countByType = async (db: Db, type: string): Promise<number> =>
(await db.audits.findMany({ where: { type: { eq: type } } })).length;
it("deletes entries older than the default retention", async ({
expect,
}) => {
const { auditService, db, time } = await setup();
const type = "av-default";
await db.audits.deleteMany({ type: { eq: type } });
await db.audits.create({
type,
action: "old",
createdAt: time.now().subtract(200, "day").toISOString(),
});
await db.audits.create({
type,
action: "recent",
createdAt: time.nowISOString(),
});
const deleted = await auditService.deleteExpired(time.now().toDate(), 90);
expect(deleted).toBeGreaterThanOrEqual(1);
expect(await countByType(db, type)).toBe(1);
});
it("keeps entries within the default retention window", async ({
expect,
}) => {
const { auditService, db, time } = await setup();
const type = "av-within";
await db.audits.deleteMany({ type: { eq: type } });
await db.audits.create({
type,
action: "x",
createdAt: time.now().subtract(10, "day").toISOString(),
});
await auditService.deleteExpired(time.now().toDate(), 90);
expect(await countByType(db, type)).toBe(1);
});
it("applies a type's dedicated retention over the default", async ({
expect,
}) => {
const { auditService, db, time } = await setup();
auditService.registerType({
type: "av-short",
actions: ["x"],
retentionDays: 7,
});
await db.audits.deleteMany({ type: { eq: "av-short" } });
await db.audits.deleteMany({ type: { eq: "av-long" } });
// 30 days old: beyond the 7-day override, within the 90-day default.
await db.audits.create({
type: "av-short",
action: "x",
createdAt: time.now().subtract(30, "day").toISOString(),
});
await db.audits.create({
type: "av-long",
action: "x",
createdAt: time.now().subtract(30, "day").toISOString(),
});
await auditService.deleteExpired(time.now().toDate(), 90);
expect(await countByType(db, "av-short")).toBe(0);
expect(await countByType(db, "av-long")).toBe(1);
});
it("keeps a type forever when its retention is 0", async ({ expect }) => {
const { auditService, db, time } = await setup();
auditService.registerType({
type: "av-permanent",
actions: ["x"],
retentionDays: 0,
});
await db.audits.deleteMany({ type: { eq: "av-permanent" } });
await db.audits.create({
type: "av-permanent",
action: "x",
createdAt: time.now().subtract(5000, "day").toISOString(),
});
await auditService.deleteExpired(time.now().toDate(), 90);
expect(await countByType(db, "av-permanent")).toBe(1);
});
it("disables cleanup when the default retention is 0", async ({
expect,
}) => {
const { auditService, db, time } = await setup();
const type = "av-disabled";
await db.audits.deleteMany({ type: { eq: type } });
await db.audits.create({
type,
action: "x",
createdAt: time.now().subtract(5000, "day").toISOString(),
});
const deleted = await auditService.deleteExpired(time.now().toDate(), 0);
expect(deleted).toBe(0);
expect(await countByType(db, type)).toBe(1);
});
});
});