alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
567 lines (479 loc) • 16.7 kB
text/typescript
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();
});
});
});