UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

567 lines (479 loc) 16.7 kB
import { randomUUID } from "node:crypto"; import { Alepha, z } from "alepha"; import { $issuer, $permission, $secure, AlephaSecurity } from "alepha/security"; import { $action, ServerProvider } from "alepha/server"; import { describe, it } from "vitest"; import { LinkProvider, ServerLinksProvider } from "../index.ts"; describe("ServerLinksProvider", () => { describe("secured field in links", () => { it("should set secured=undefined for public actions (no $secure middleware)", async ({ expect, }) => { class App { publicAction = $action({ handler: () => "PUBLIC", }); } const alepha = Alepha.create().with(App).with(ServerLinksProvider); await alepha.start(); const links = alepha.inject(LinkProvider).getServerLinks(); const link = links.find((l) => l.name === "publicAction"); expect(link).toBeDefined(); expect(link?.secured).toBeUndefined(); }); it("should set secured=true for actions with $secure()", async ({ expect, }) => { class App { securedAction = $action({ use: [$secure()], handler: () => "SECURED", }); } const alepha = Alepha.create().with(App).with(ServerLinksProvider); await alepha.start(); const links = alepha.inject(LinkProvider).getServerLinks(); const link = links.find((l) => l.name === "securedAction"); expect(link).toBeDefined(); expect(link?.secured).toBe(true); }); it("should set secured to options object for realm-secured actions", async ({ expect, }) => { class App { realmAction = $action({ use: [$secure({ issuers: ["admin"] })], handler: () => "REALM", }); } const alepha = Alepha.create().with(App).with(ServerLinksProvider); await alepha.start(); const links = alepha.inject(LinkProvider).getServerLinks(); const link = links.find((l) => l.name === "realmAction"); expect(link).toBeDefined(); expect(link?.secured).toEqual({ issuers: ["admin"] }); }); }); describe("/_links endpoint with security", () => { it("should return public actions to unauthenticated users", async ({ expect, }) => { class App { publicAction = $action({ schema: { response: z.text() }, handler: () => "PUBLIC", }); issuer = $issuer({ secret: "test", roles: [{ name: "user", permissions: [{ name: "*" }] }], }); } const alepha = Alepha.create() .with(App) .with(ServerLinksProvider) .with(AlephaSecurity); await alepha.start(); const res = await fetch( `${alepha.inject(ServerProvider).hostname}/api/_links`, ); const data = await res.json(); expect(data.actions.publicAction).toBeDefined(); expect(data.actions.publicAction.path).toBe("/publicAction"); }); it("should NOT return secured actions to unauthenticated users", async ({ expect, }) => { class App { securedAction = $action({ use: [$secure()], schema: { response: z.text() }, handler: () => "SECURED", }); issuer = $issuer({ secret: "test", roles: [{ name: "user", permissions: [{ name: "*" }] }], }); } const alepha = Alepha.create() .with(App) .with(ServerLinksProvider) .with(AlephaSecurity); await alepha.start(); const res = await fetch( `${alepha.inject(ServerProvider).hostname}/api/_links`, ); const data = await res.json(); expect(data.actions.securedAction).toBeUndefined(); }); it("should return secured actions to authenticated users with permissions", async ({ expect, }) => { class App { securedAction = $action({ use: [$secure()], schema: { response: z.text() }, handler: () => "SECURED", }); issuer = $issuer({ secret: "test", roles: [{ name: "user", permissions: [{ name: "*" }] }], }); } const alepha = Alepha.create() .with(App) .with(ServerLinksProvider) .with(AlephaSecurity); await alepha.start(); // Use HttpClient to get a token automatically in test mode const app = alepha.inject(App); const { data } = await app.securedAction.fetch( {}, { user: { id: randomUUID(), roles: ["user"] }, }, ); expect(data).toBe("SECURED"); }); it("should return both public and secured actions when user is authenticated", async ({ expect, }) => { class App { publicAction = $action({ schema: { response: z.text() }, handler: () => "PUBLIC", }); securedAction = $action({ use: [$secure()], schema: { response: z.text() }, handler: () => "SECURED", }); issuer = $issuer({ secret: "test", roles: [{ name: "user", permissions: [{ name: "*" }] }], }); } const alepha = Alepha.create() .with(App) .with(ServerLinksProvider) .with(AlephaSecurity); await alepha.start(); const linksProvider = alepha.inject(ServerLinksProvider); const user = { id: randomUUID(), roles: ["user"] }; const registry = await linksProvider.getUserApiLinks({ user }); expect(registry.actions.publicAction).toBeDefined(); expect(registry.actions.securedAction).toBeDefined(); }); it("should filter secured actions based on explicit permissions", async ({ expect, }) => { class App { adminOnly = $action({ use: [$secure({ permissions: ["admin:manage"] })], schema: { response: z.text() }, handler: () => "ADMIN", }); userAction = $action({ use: [$secure({ permissions: ["user:read"] })], schema: { response: z.text() }, handler: () => "USER", }); issuer = $issuer({ secret: "test", roles: [ { name: "admin", permissions: [{ name: "*" }] }, { name: "user", permissions: [{ name: "user:*" }] }, ], }); } const alepha = Alepha.create() .with(App) .with(ServerLinksProvider) .with(AlephaSecurity); await alepha.start(); const linksProvider = alepha.inject(ServerLinksProvider); // User with "user" role should only see userAction const userRegistry = await linksProvider.getUserApiLinks({ user: { id: randomUUID(), roles: ["user"] }, }); expect(userRegistry.actions.userAction).toBeDefined(); expect(userRegistry.actions.adminOnly).toBeUndefined(); // User with "admin" role should see both const adminRegistry = await linksProvider.getUserApiLinks({ user: { id: randomUUID(), roles: ["admin"] }, }); expect(adminRegistry.actions.userAction).toBeDefined(); expect(adminRegistry.actions.adminOnly).toBeDefined(); }); it("should show auth-only $secure() actions to any authenticated user", async ({ expect, }) => { class App { authOnly = $action({ use: [$secure()], schema: { response: z.text() }, handler: () => "AUTH_ONLY", }); issuer = $issuer({ secret: "test", roles: [{ name: "limited", permissions: [] }], }); } const alepha = Alepha.create() .with(App) .with(ServerLinksProvider) .with(AlephaSecurity); await alepha.start(); const linksProvider = alepha.inject(ServerLinksProvider); // User with "limited" role (no permissions) should still see auth-only actions const registry = await linksProvider.getUserApiLinks({ user: { id: randomUUID(), roles: ["limited"] }, }); expect(registry.actions.authOnly).toBeDefined(); }); }); describe("mixed public and secured actions", () => { it("should correctly differentiate public from secured in server links", async ({ expect, }) => { class App { getUsers = $action({ path: "/users", schema: { response: z.array(z.text()) }, handler: () => ["user1", "user2"], }); createUser = $action({ path: "/users", use: [$secure()], schema: { body: z.object({ name: z.text() }), response: z.text(), }, handler: ({ body }) => body.name, }); deleteUser = $action({ method: "DELETE", path: "/users/:id", use: [$secure()], schema: { params: z.object({ id: z.text() }), response: z.void(), }, handler: () => {}, }); } const alepha = Alepha.create().with(App).with(ServerLinksProvider); await alepha.start(); const links = alepha.inject(LinkProvider).getServerLinks(); const getUsers = links.find((l) => l.name === "getUsers"); const createUser = links.find((l) => l.name === "createUser"); const deleteUser = links.find((l) => l.name === "deleteUser"); // getUsers is public (no $secure middleware) expect(getUsers?.secured).toBeUndefined(); // createUser and deleteUser are secured expect(createUser?.secured).toBe(true); expect(deleteUser?.secured).toBe(true); }); }); describe("permissions in registry", () => { it("should separate virtual permissions from actions", async ({ expect, }) => { class App { getUsers = $action({ path: "/users", schema: { response: z.array(z.text()) }, handler: () => [], }); teamManagement = $permission({ name: "TeamManagement", group: "team", }); issuer = $issuer({ secret: "test", roles: [ { name: "admin", permissions: [{ name: "*" }, { name: "team:TeamManagement" }], }, ], }); } const alepha = Alepha.create() .with(App) .with(ServerLinksProvider) .with(AlephaSecurity); await alepha.start(); const linksProvider = alepha.inject(ServerLinksProvider); const registry = await linksProvider.getUserApiLinks({ user: { id: randomUUID(), roles: ["admin"] }, }); // Virtual permissions should be in the permissions array, not in actions expect(registry.permissions).toContain("team:TeamManagement"); expect(registry.actions.TeamManagement).toBeUndefined(); // Real actions should be in the actions map expect(registry.actions.getUsers).toBeDefined(); }); }); describe("schemas endpoint", () => { it("should return schemas for requested actions", async ({ expect }) => { class App { ping = $action({ schema: { body: z.object({ message: z.text() }), response: z.object({ pong: z.boolean() }), }, handler: () => ({ pong: true }), }); } const alepha = Alepha.create().with(App).with(ServerLinksProvider); await alepha.start(); const res = await fetch( `${alepha.inject(ServerProvider).hostname}/api/_links/schemas`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ actions: ["ping"] }), }, ); const data = await res.json(); expect(data.ping).toBeDefined(); expect(data.ping.body).toBeDefined(); expect(data.ping.response).toBeDefined(); expect(JSON.parse(data.ping.body).type).toBe("object"); expect(JSON.parse(data.ping.response).type).toBe("object"); }); it("should return empty for unknown actions", async ({ expect }) => { class App { ping = $action({ schema: { response: z.text() }, handler: () => "pong", }); } const alepha = Alepha.create().with(App).with(ServerLinksProvider); await alepha.start(); const res = await fetch( `${alepha.inject(ServerProvider).hostname}/api/_links/schemas`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ actions: ["nonexistent"] }), }, ); const data = await res.json(); expect(data.nonexistent).toBeUndefined(); }); it("should filter based on user permissions", async ({ expect }) => { class App { publicAction = $action({ schema: { body: z.object({ name: z.text() }), response: z.text(), }, handler: () => "PUBLIC", }); securedAction = $action({ use: [$secure()], schema: { body: z.object({ secret: z.text() }), response: z.text(), }, handler: () => "SECURED", }); issuer = $issuer({ secret: "test", roles: [{ name: "user", permissions: [{ name: "*" }] }], }); } const alepha = Alepha.create() .with(App) .with(ServerLinksProvider) .with(AlephaSecurity); await alepha.start(); // Unauthenticated request — should only get public schema const res = await fetch( `${alepha.inject(ServerProvider).hostname}/api/_links/schemas`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ actions: ["publicAction", "securedAction"], }), }, ); const data = await res.json(); expect(data.publicAction).toBeDefined(); expect(data.securedAction).toBeUndefined(); }); }); describe("caching and ETag", () => { it("should return ETag header on first request", async ({ expect }) => { class App { ping = $action({ handler: () => "pong", }); } const alepha = Alepha.create().with(App).with(ServerLinksProvider); await alepha.start(); const res = await fetch( `${alepha.inject(ServerProvider).hostname}/api/_links`, ); expect(res.status).toBe(200); expect(res.headers.get("etag")).toBeDefined(); expect(res.headers.get("etag")).toMatch(/^"[a-f0-9]{32}"$/); }); it("should return 304 when If-None-Match matches ETag", async ({ expect, }) => { class App { ping = $action({ handler: () => "pong", }); } const alepha = Alepha.create().with(App).with(ServerLinksProvider); await alepha.start(); const hostname = alepha.inject(ServerProvider).hostname; // First request — get the ETag const res1 = await fetch(`${hostname}/api/_links`); const etag = res1.headers.get("etag")!; expect(etag).toBeDefined(); // Second request with If-None-Match — should get 304 const res2 = await fetch(`${hostname}/api/_links`, { headers: { "if-none-match": etag }, }); expect(res2.status).toBe(304); expect(res2.headers.get("etag")).toBe(etag); }); it("should return same ETag for same roles", async ({ expect }) => { class App { ping = $action({ handler: () => "pong", }); } const alepha = Alepha.create().with(App).with(ServerLinksProvider); await alepha.start(); const hostname = alepha.inject(ServerProvider).hostname; const res1 = await fetch(`${hostname}/api/_links`); const res2 = await fetch(`${hostname}/api/_links`); expect(res1.headers.get("etag")).toBe(res2.headers.get("etag")); }); it("should serve cached response on second request", async ({ expect }) => { class App { ping = $action({ schema: { response: z.text() }, handler: () => "pong", }); } const alepha = Alepha.create().with(App).with(ServerLinksProvider); await alepha.start(); const hostname = alepha.inject(ServerProvider).hostname; // First request populates cache const res1 = await fetch(`${hostname}/api/_links`); const data1 = await res1.json(); // Second request should use cache const res2 = await fetch(`${hostname}/api/_links`); const data2 = await res2.json(); expect(data1).toStrictEqual(data2); expect(data1.actions.ping).toBeDefined(); }); }); });