alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
240 lines (190 loc) • 7.17 kB
text/typescript
import { $pipeline, Alepha } from "alepha";
import { DateTimeProvider } from "alepha/datetime";
import { describe, test } from "vitest";
import { $circuit } from "../primitives/$circuit.ts";
// -----------------------------------------------------------------------------------------------------------------
// $circuit — core behavior
// -----------------------------------------------------------------------------------------------------------------
describe("$circuit", () => {
test("calls pass through when circuit is closed", async ({ expect }) => {
const alepha = Alepha.create();
class TestService {
fn = $pipeline({
use: [$circuit({ threshold: 3, reset: [1, "seconds"] })],
handler: async (x: number) => x * 2,
});
}
const svc = alepha.inject(TestService);
expect(await svc.fn(5)).toBe(10);
});
test("failures below threshold keep circuit closed", async ({ expect }) => {
const alepha = Alepha.create();
let attempt = 0;
class TestService {
fn = $pipeline({
use: [$circuit({ threshold: 3, reset: [1, "seconds"] })],
handler: async () => {
attempt++;
if (attempt <= 2) throw new Error("Fail");
return "ok";
},
});
}
const svc = alepha.inject(TestService);
// 2 failures (below threshold of 3)
await expect(svc.fn()).rejects.toThrowError("Fail");
await expect(svc.fn()).rejects.toThrowError("Fail");
// Circuit still closed — next call goes through
expect(await svc.fn()).toBe("ok");
});
test("failures at threshold open the circuit", async ({ expect }) => {
const alepha = Alepha.create();
class TestService {
fn = $pipeline({
use: [$circuit({ threshold: 2, reset: [10, "seconds"] })],
handler: async () => {
throw new Error("Fail");
},
});
}
const svc = alepha.inject(TestService);
// 2 failures → opens circuit
await expect(svc.fn()).rejects.toThrowError("Fail");
await expect(svc.fn()).rejects.toThrowError("Fail");
// Circuit is open — handler not called, immediate rejection
await expect(svc.fn()).rejects.toThrowError("$circuit: circuit is open");
});
test("open circuit rejects without calling handler", async ({ expect }) => {
const alepha = Alepha.create();
let handlerCalls = 0;
class TestService {
fn = $pipeline({
use: [$circuit({ threshold: 1, reset: [10, "seconds"] })],
handler: async () => {
handlerCalls++;
throw new Error("Fail");
},
});
}
const svc = alepha.inject(TestService);
// 1 failure → opens circuit
await expect(svc.fn()).rejects.toThrowError("Fail");
expect(handlerCalls).toBe(1);
// Open circuit — handler never called
await expect(svc.fn()).rejects.toThrowError("$circuit: circuit is open");
expect(handlerCalls).toBe(1); // Still 1
});
test("circuit transitions to half-open after reset duration", async ({
expect,
}) => {
const alepha = Alepha.create();
const dt = alepha.inject(DateTimeProvider);
let attempt = 0;
class TestService {
fn = $pipeline({
use: [$circuit({ threshold: 1, reset: [50, "milliseconds"] })],
handler: async () => {
attempt++;
if (attempt === 1) throw new Error("Fail");
return "recovered";
},
});
}
const svc = alepha.inject(TestService);
// Trip the circuit
await expect(svc.fn()).rejects.toThrowError("Fail");
// Travel past reset window
dt.travel([60, "milliseconds"]);
// Half-open → probe succeeds → circuit closes
expect(await svc.fn()).toBe("recovered");
});
test("half-open failure re-opens the circuit", async ({ expect }) => {
const alepha = Alepha.create();
const dt = alepha.inject(DateTimeProvider);
class TestService {
fn = $pipeline({
use: [$circuit({ threshold: 1, reset: [50, "milliseconds"] })],
handler: async () => {
throw new Error("Still failing");
},
});
}
const svc = alepha.inject(TestService);
// Trip the circuit
await expect(svc.fn()).rejects.toThrowError("Still failing");
// Travel past reset window
dt.travel([60, "milliseconds"]);
// Half-open → probe fails → re-opens
await expect(svc.fn()).rejects.toThrowError("Still failing");
// Circuit is open again
await expect(svc.fn()).rejects.toThrowError("$circuit: circuit is open");
});
test("success resets failure counter", async ({ expect }) => {
const alepha = Alepha.create();
let attempt = 0;
class TestService {
fn = $pipeline({
use: [$circuit({ threshold: 3, reset: [1, "seconds"] })],
handler: async () => {
attempt++;
// Fail on 1, 2, succeed on 3, fail on 4, 5
if (attempt <= 2 || attempt >= 4) throw new Error("Fail");
return "ok";
},
});
}
const svc = alepha.inject(TestService);
// 2 failures
await expect(svc.fn()).rejects.toThrowError("Fail");
await expect(svc.fn()).rejects.toThrowError("Fail");
// Success resets counter
expect(await svc.fn()).toBe("ok");
// 2 more failures — still below threshold (counter was reset)
await expect(svc.fn()).rejects.toThrowError("Fail");
await expect(svc.fn()).rejects.toThrowError("Fail");
// Circuit still closed (2 < 3)
// Next failure will open it
await expect(svc.fn()).rejects.toThrowError("Fail");
await expect(svc.fn()).rejects.toThrowError("$circuit: circuit is open");
});
test("handler return value is preserved", async ({ expect }) => {
const alepha = Alepha.create();
class TestService {
fn = $pipeline({
use: [$circuit({ threshold: 3, reset: [1, "seconds"] })],
handler: async () => ({ data: [1, 2, 3] }),
});
}
const svc = alepha.inject(TestService);
expect(await svc.fn()).toEqual({ data: [1, 2, 3] });
});
test("handler arguments are passed through", async ({ expect }) => {
const alepha = Alepha.create();
class TestService {
fn = $pipeline({
use: [$circuit({ threshold: 3, reset: [1, "seconds"] })],
handler: async (a: number, b: number) => a + b,
});
}
const svc = alepha.inject(TestService);
expect(await svc.fn(3, 4)).toBe(7);
});
});
// -----------------------------------------------------------------------------------------------------------------
// $circuit — metadata
// -----------------------------------------------------------------------------------------------------------------
describe("$circuit metadata", () => {
test("has middleware metadata", ({ expect }) => {
const alepha = Alepha.create();
class TestService {
fn = $pipeline({
use: [$circuit({ threshold: 5, reset: [30, "seconds"] })],
handler: async () => "ok",
});
}
const svc = alepha.inject(TestService);
const meta = svc.fn.middlewares;
expect(meta).toHaveLength(1);
expect(meta[0].name).toBe("$circuit");
});
});