alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
187 lines (157 loc) • 5.19 kB
text/typescript
import { Alepha, z } from "alepha";
import { $action, ServerProvider } from "alepha/server";
import { describe, it } from "vitest";
import { LinkProvider, ServerLinksProvider } from "../index.ts";
class App {
ping = $action({
schema: {
response: z.object({
pong: z.boolean(),
}),
},
handler: () => {
return { pong: true };
},
});
}
describe("LinkProvider", () => {
it("should execute action through local handler", async ({ expect }) => {
const alepha = Alepha.create().with(App).with(ServerLinksProvider);
await alepha.start();
const app = alepha.inject(LinkProvider).client<App>();
expect(await app.ping()).toStrictEqual({ pong: true });
});
it("should expose links endpoint with new registry format", async ({
expect,
}) => {
const alepha = Alepha.create().with(App).with(ServerLinksProvider);
await alepha.start();
const res = await fetch(
`${alepha.inject(ServerProvider).hostname}/api/_links`,
);
const data = await res.json();
expect(data).toStrictEqual({
prefix: "/api",
actions: {
ping: {
path: "/ping",
},
},
});
});
it("should return actions as a map (O(1) lookup by name)", async ({
expect,
}) => {
const alepha = Alepha.create().with(App).with(ServerLinksProvider);
await alepha.start();
const res = await fetch(
`${alepha.inject(ServerProvider).hostname}/api/_links`,
);
const data = await res.json();
// actions is an object, not an array
expect(typeof data.actions).toBe("object");
expect(Array.isArray(data.actions)).toBe(false);
expect(data.actions.ping).toBeDefined();
expect(data.actions.ping.path).toBe("/ping");
});
it("should omit method for GET actions", async ({ expect }) => {
class GetApp {
getUsers = $action({
path: "/users",
schema: { response: z.array(z.text()) },
handler: () => ["user1"],
});
}
const alepha = Alepha.create().with(GetApp).with(ServerLinksProvider);
await alepha.start();
const res = await fetch(
`${alepha.inject(ServerProvider).hostname}/api/_links`,
);
const data = await res.json();
expect(data.actions.getUsers.method).toBeUndefined();
});
it("should include method for non-GET actions", async ({ expect }) => {
class PostApp {
createUser = $action({
method: "POST",
path: "/users",
schema: {
body: z.object({ name: z.text() }),
response: z.text(),
},
handler: ({ body }) => body.name,
});
}
const alepha = Alepha.create().with(PostApp).with(ServerLinksProvider);
await alepha.start();
const res = await fetch(
`${alepha.inject(ServerProvider).hostname}/api/_links`,
);
const data = await res.json();
expect(data.actions.createUser.method).toBe("POST");
});
it("should not include definitions or schema refs in registry", async ({
expect,
}) => {
class SchemaApp {
getUser = $action({
path: "/users/:id",
schema: { response: z.object({ id: z.integer(), name: z.text() }) },
handler: () => ({ id: 1, name: "John" }),
});
updateUser = $action({
method: "PUT",
path: "/users/:id",
schema: {
body: z.object({ name: z.text() }),
response: z.object({ id: z.integer(), name: z.text() }),
},
handler: () => ({ id: 1, name: "John" }),
});
}
const alepha = Alepha.create().with(SchemaApp).with(ServerLinksProvider);
await alepha.start();
const res = await fetch(
`${alepha.inject(ServerProvider).hostname}/api/_links`,
);
const data = await res.json();
expect(data.definitions).toBeUndefined();
expect(data.actions.getUser.body).toBeUndefined();
expect(data.actions.getUser.response).toBeUndefined();
expect(data.actions.updateUser.body).toBeUndefined();
expect(data.actions.updateUser.response).toBeUndefined();
});
it("should not include group or rawSchema in wire format", async ({
expect,
}) => {
const alepha = Alepha.create().with(App).with(ServerLinksProvider);
await alepha.start();
const res = await fetch(
`${alepha.inject(ServerProvider).hostname}/api/_links`,
);
const data = await res.json();
const action = data.actions.ping;
expect(action.group).toBeUndefined();
expect(action.rawSchema).toBeUndefined();
expect(action.name).toBeUndefined(); // name is the key, not a field
expect(action.requestBodyType).toBeUndefined();
});
it("should throw on duplicate local action names", async ({ expect }) => {
class DuplicateApp {
ping = $action({
handler: () => "first",
});
ping2 = $action({
name: "ping",
handler: () => "second",
});
}
const alepha = Alepha.create().with(DuplicateApp).with(ServerLinksProvider);
try {
await alepha.start();
expect.unreachable("should have thrown");
} catch (error: any) {
expect(error.cause?.message).toMatch(/Duplicate action name "ping"/);
}
});
});