alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
1,263 lines (1,077 loc) • 37.5 kB
text/typescript
import { Alepha, type Middleware, OPTIONS, z } from "alepha";
import { $action } from "alepha/server";
import { describe, expect, it } from "vitest";
import { $swagger, ServerSwaggerProvider } from "../index.ts";
import type { SwaggerPrimitiveOptions } from "../primitives/$swagger.ts";
// -------------------------------------------------------------------------------------------------------------------------
// Helpers
// -------------------------------------------------------------------------------------------------------------------------
const fakeSecure = (): Middleware => {
const mw: Middleware = ((handler: any) => handler) as Middleware;
mw[OPTIONS] = { name: "$secure" };
return mw;
};
const doc = (app: Alepha, options: SwaggerPrimitiveOptions = {}) => {
return app.inject(ServerSwaggerProvider).generateSwaggerDoc(options);
};
// -------------------------------------------------------------------------------------------------------------------------
// Fixtures
// -------------------------------------------------------------------------------------------------------------------------
class App {
internal = $action({
hide: true,
schema: {
response: z.object({
message: z.text(),
}),
},
handler: async () => {
return {
message: "Hello world",
};
},
});
api = $action({
hide: true,
schema: {
response: z.object({
message: z.text(),
}),
},
handler: async () => {
return {
message: "Hello world",
};
},
});
text = $action({
schema: {
response: z.text(),
},
handler: async () => {
return "Hello world";
},
});
hello = $action({
path: "/hello/:name",
name: "hello",
description: "Hello world",
group: "app",
schema: {
params: z.object({
name: z.text(),
}),
query: z.object({
age: z.number().optional(),
}),
body: z.object({
name: z.text(),
}),
response: z
.object({
message: z.text(),
})
.meta({ title: "HelloResponse" })
.describe("Hello response"),
},
handler: async (req) => {
return {
message: `Hello ${req.body.name}`,
};
},
});
docs = $swagger({
info: {
title: "My API",
version: "1.0.0",
},
ui: false,
});
}
const alepha = Alepha.create().with(App);
// -------------------------------------------------------------------------------------------------------------------------
// Tests: existing spec (updated for 400 response)
// -------------------------------------------------------------------------------------------------------------------------
describe("$swagger", () => {
it("should generate OpenAPI spec from actions", () => {
const swagger = alepha.inject(ServerSwaggerProvider).json;
expect(swagger).toMatchObject({
openapi: "3.0.0",
info: {
title: "My API",
version: "1.0.0",
},
servers: expect.any(Array),
paths: {
"/api/hello/{name}": {
post: {
operationId: "hello",
parameters: [
{
in: "query",
name: "age",
required: false,
schema: {
type: "number",
},
},
{
in: "path",
name: "name",
required: true,
schema: {
maxLength: 255,
type: "string",
},
},
],
description: "Hello world",
tags: ["app"],
responses: {
"200": {
description: "OK",
content: {
"application/json": {
schema: {
$ref: "#/components/schemas/HelloResponse",
},
},
},
},
"400": {
description: "Bad Request",
},
},
requestBody: {
required: true,
content: {
"application/json": {
schema: {
additionalProperties: false,
type: "object",
properties: {
name: {
maxLength: 255,
type: "string",
},
},
required: ["name"],
},
},
},
},
},
},
"/api/text": {
get: {
operationId: "text",
responses: {
"200": {
content: {
"text/plain": {
schema: {
maxLength: 255,
type: "string",
},
},
},
description: "OK",
},
},
tags: ["App"],
},
},
},
components: {
schemas: {
HelloResponse: {
additionalProperties: false,
description: "Hello response",
properties: {
message: {
maxLength: 255,
type: "string",
},
},
required: ["message"],
title: "HelloResponse",
type: "object",
},
},
},
});
});
});
// -------------------------------------------------------------------------------------------------------------------------
// Tests: security detection
// -------------------------------------------------------------------------------------------------------------------------
describe("security", () => {
it("should add bearerAuth and 401 for secured actions", () => {
class SecuredApp {
secured = $action({
use: [fakeSecure()],
schema: {
response: z.object({ ok: z.boolean() }),
},
handler: async () => ({ ok: true }),
});
}
const app = Alepha.create().with(SecuredApp);
const json = doc(app);
const op = json.paths["/api/secured"].get;
expect(op.security).toEqual([{ bearerAuth: [] }]);
expect(op.responses["401"]).toEqual({ description: "Unauthorized" });
expect(json.components?.securitySchemes).toEqual({
bearerAuth: {
type: "http",
scheme: "bearer",
bearerFormat: "JWT",
description:
"Enter a JWT token or API key. Both are accepted as Bearer tokens.",
},
});
});
it("should not add security for unsecured actions", () => {
class UnsecuredApp {
open = $action({
schema: {
response: z.object({ ok: z.boolean() }),
},
handler: async () => ({ ok: true }),
});
}
const app = Alepha.create().with(UnsecuredApp);
const json = doc(app);
const op = json.paths["/api/open"].get;
expect(op.security).toBeUndefined();
expect(op.responses["401"]).toBeUndefined();
expect(json.components?.securitySchemes).toBeUndefined();
});
it("should add securitySchemes when at least one action is secured", () => {
class MixedApp {
open = $action({
schema: { response: z.text() },
handler: async () => "ok",
});
secured = $action({
use: [fakeSecure()],
schema: { response: z.text() },
handler: async () => "ok",
});
}
const app = Alepha.create().with(MixedApp);
const json = doc(app);
expect(json.paths["/api/open"].get.security).toBeUndefined();
expect(json.paths["/api/secured"].get.security).toEqual([
{ bearerAuth: [] },
]);
expect(json.components?.securitySchemes).toBeDefined();
});
});
// -------------------------------------------------------------------------------------------------------------------------
// Tests: query parameter required
// -------------------------------------------------------------------------------------------------------------------------
describe("query parameters", () => {
it("should mark required query params as required", () => {
class QueryApp {
search = $action({
schema: {
query: z.object({
q: z.text(),
page: z.number().optional(),
}),
response: z.object({ results: z.array(z.text()) }),
},
handler: async () => ({ results: [] }),
});
}
const app = Alepha.create().with(QueryApp);
const json = doc(app);
const params = json.paths["/api/search"].get.parameters;
const qParam = params.find((p: any) => p.name === "q");
const pageParam = params.find((p: any) => p.name === "page");
expect(qParam.required).toBe(true);
expect(pageParam.required).toBe(false);
});
it("should mark all query params as required when none are optional", () => {
class StrictQueryApp {
fetch = $action({
name: "fetchItems",
schema: {
query: z.object({
id: z.text(),
type: z.text(),
}),
response: z.object({ ok: z.boolean() }),
},
handler: async () => ({ ok: true }),
});
}
const app = Alepha.create().with(StrictQueryApp);
const json = doc(app);
const params = json.paths["/api/fetchItems"].get.parameters;
for (const p of params) {
expect(p.required).toBe(true);
}
});
});
// -------------------------------------------------------------------------------------------------------------------------
// Tests: error responses (400)
// -------------------------------------------------------------------------------------------------------------------------
describe("error responses", () => {
it("should add 400 for actions with body", () => {
class BodyApp {
create = $action({
schema: {
body: z.object({ name: z.text() }),
response: z.object({ id: z.text() }),
},
handler: async () => ({ id: "1" }),
});
}
const app = Alepha.create().with(BodyApp);
const json = doc(app);
const op = json.paths["/api/create"].post;
expect(op.responses["400"]).toEqual({ description: "Bad Request" });
});
it("should add 400 for actions with query params", () => {
class QueryApp {
search = $action({
schema: {
query: z.object({ q: z.text() }),
response: z.text(),
},
handler: async () => "ok",
});
}
const app = Alepha.create().with(QueryApp);
const json = doc(app);
const op = json.paths["/api/search"].get;
expect(op.responses["400"]).toEqual({ description: "Bad Request" });
});
it("should add 400 for actions with path params", () => {
class ParamsApp {
getById = $action({
path: "/items/:id",
schema: {
params: z.object({ id: z.text() }),
response: z.object({ name: z.text() }),
},
handler: async () => ({ name: "item" }),
});
}
const app = Alepha.create().with(ParamsApp);
const json = doc(app);
const op = json.paths["/api/items/{id}"].get;
expect(op.responses["400"]).toEqual({ description: "Bad Request" });
});
it("should not add 400 for actions without validation", () => {
class NoValidationApp {
ping = $action({
schema: {
response: z.text(),
},
handler: async () => "pong",
});
}
const app = Alepha.create().with(NoValidationApp);
const json = doc(app);
const op = json.paths["/api/ping"].get;
expect(op.responses["400"]).toBeUndefined();
});
it("should add both 400 and 401 for secured actions with body", () => {
class SecuredBodyApp {
create = $action({
use: [fakeSecure()],
schema: {
body: z.object({ name: z.text() }),
response: z.object({ id: z.text() }),
},
handler: async () => ({ id: "1" }),
});
}
const app = Alepha.create().with(SecuredBodyApp);
const json = doc(app);
const op = json.paths["/api/create"].post;
expect(op.responses["400"]).toEqual({ description: "Bad Request" });
expect(op.responses["401"]).toEqual({ description: "Unauthorized" });
});
});
// -------------------------------------------------------------------------------------------------------------------------
// Tests: response types
// -------------------------------------------------------------------------------------------------------------------------
describe("response types", () => {
it("should handle object response as application/json", () => {
class ObjApp {
get = $action({
name: "getObj",
schema: { response: z.object({ a: z.text() }) },
handler: async () => ({ a: "b" }),
});
}
const app = Alepha.create().with(ObjApp);
const json = doc(app);
const content = json.paths["/api/getObj"].get.responses["200"].content;
expect(content["application/json"]).toBeDefined();
});
it("should handle array response as application/json", () => {
class ArrayApp {
list = $action({
schema: { response: z.array(z.text()) },
handler: async () => ["a"],
});
}
const app = Alepha.create().with(ArrayApp);
const json = doc(app);
const content = json.paths["/api/list"].get.responses["200"].content;
expect(content["application/json"]).toBeDefined();
});
it("should handle string response as text/plain", () => {
class StrApp {
getText = $action({
schema: { response: z.text() },
handler: async () => "ok",
});
}
const app = Alepha.create().with(StrApp);
const json = doc(app);
const content = json.paths["/api/getText"].get.responses["200"].content;
expect(content["text/plain"]).toBeDefined();
});
it("should handle number response as application/json", () => {
class NumApp {
count = $action({
schema: { response: z.number() as any },
handler: async () => 42 as any,
});
}
const app = Alepha.create().with(NumApp);
const json = doc(app);
const resp = json.paths["/api/count"].get.responses["200"];
expect(resp.content["application/json"]).toBeDefined();
expect(resp.content["application/json"].schema.type).toBe("number");
});
it("should handle integer response as application/json", () => {
class IntApp {
countInt = $action({
schema: { response: z.integer() as any },
handler: async () => 42 as any,
});
}
const app = Alepha.create().with(IntApp);
const json = doc(app);
const resp = json.paths["/api/countInt"].get.responses["200"];
expect(resp.content["application/json"]).toBeDefined();
expect(resp.content["application/json"].schema.type).toBe("integer");
});
it("should handle boolean response as application/json", () => {
class BoolApp {
check = $action({
schema: { response: z.boolean() as any },
handler: async () => true as any,
});
}
const app = Alepha.create().with(BoolApp);
const json = doc(app);
const resp = json.paths["/api/check"].get.responses["200"];
expect(resp.content["application/json"]).toBeDefined();
expect(resp.content["application/json"].schema.type).toBe("boolean");
});
it("should handle file response as application/octet-stream", () => {
class FileApp {
download = $action({
schema: { response: z.file() as any },
handler: async () => new Blob([]) as any,
});
}
const app = Alepha.create().with(FileApp);
const json = doc(app);
const content = json.paths["/api/download"].get.responses["200"].content;
expect(content["application/octet-stream"]).toBeDefined();
});
it("should return 204 for actions without response schema", () => {
class NoResponseApp {
fire = $action({
schema: {},
handler: async () => {},
});
}
const app = Alepha.create().with(NoResponseApp);
const json = doc(app);
const responses = json.paths["/api/fire"].get.responses;
expect(responses["204"]).toBeDefined();
expect(responses["204"].content).toBeUndefined();
});
});
// -------------------------------------------------------------------------------------------------------------------------
// Tests: hide and excludeTags
// -------------------------------------------------------------------------------------------------------------------------
describe("hide and excludeTags", () => {
it("should exclude actions with hide: true", () => {
class HideApp {
visible = $action({
schema: { response: z.text() },
handler: async () => "ok",
});
hidden = $action({
hide: true,
schema: { response: z.text() },
handler: async () => "ok",
});
}
const app = Alepha.create().with(HideApp);
const json = doc(app);
expect(json.paths["/api/visible"]).toBeDefined();
expect(json.paths["/api/hidden"]).toBeUndefined();
});
it("should exclude actions matching excludeTags", () => {
class TagApp {
pub = $action({
group: "public",
schema: { response: z.text() },
handler: async () => "ok",
});
internal = $action({
group: "internal",
schema: { response: z.text() },
handler: async () => "ok",
});
}
const app = Alepha.create().with(TagApp);
const json = doc(app, { excludeTags: ["internal"] });
expect(json.paths["/api/pub"]).toBeDefined();
expect(json.paths["/api/internal"]).toBeUndefined();
});
});
// -------------------------------------------------------------------------------------------------------------------------
// Tests: rewrite
// -------------------------------------------------------------------------------------------------------------------------
describe("rewrite", () => {
it("should allow rewriting the OpenAPI document", () => {
class RewriteApp {
hello = $action({
schema: { response: z.text() },
handler: async () => "ok",
});
}
const app = Alepha.create().with(RewriteApp);
const json = doc(app, {
rewrite: (d) => {
d.info.title = "Rewritten Title";
d.info.description = "Added by rewrite";
},
});
expect(json.info.title).toBe("Rewritten Title");
expect(json.info.description).toBe("Added by rewrite");
});
});
// -------------------------------------------------------------------------------------------------------------------------
// Tests: multipart
// -------------------------------------------------------------------------------------------------------------------------
describe("multipart", () => {
it("should detect file upload as multipart/form-data", () => {
class UploadApp {
upload = $action({
schema: {
body: z.object({
file: z.file(),
name: z.text(),
}),
response: z.object({ ok: z.boolean() }),
},
handler: async () => ({ ok: true }),
});
}
const app = Alepha.create().with(UploadApp);
const json = doc(app);
const body = json.paths["/api/upload"].post.requestBody;
expect(body.content["multipart/form-data"]).toBeDefined();
expect(body.content["application/json"]).toBeUndefined();
});
it("should use application/json for body without files", () => {
class JsonBodyApp {
create = $action({
schema: {
body: z.object({ name: z.text() }),
response: z.object({ ok: z.boolean() }),
},
handler: async () => ({ ok: true }),
});
}
const app = Alepha.create().with(JsonBodyApp);
const json = doc(app);
const body = json.paths["/api/create"].post.requestBody;
expect(body.content["application/json"]).toBeDefined();
expect(body.content["multipart/form-data"]).toBeUndefined();
});
});
// -------------------------------------------------------------------------------------------------------------------------
// Tests: array body
// -------------------------------------------------------------------------------------------------------------------------
describe("array body", () => {
it("should handle array request body", () => {
class ArrayBodyApp {
batch = $action({
schema: {
body: z.array(z.object({ id: z.text() })),
response: z.object({ count: z.number() }),
},
handler: async () => ({ count: 0 }),
});
}
const app = Alepha.create().with(ArrayBodyApp);
const json = doc(app);
const body = json.paths["/api/batch"].post.requestBody;
expect(body.content["application/json"]).toBeDefined();
expect(body.content["application/json"].schema.type).toBe("array");
});
});
// -------------------------------------------------------------------------------------------------------------------------
// Tests: $ref schemas
// -------------------------------------------------------------------------------------------------------------------------
describe("$ref schemas", () => {
it("should extract titled schemas into components/schemas", () => {
class RefApp {
getItem = $action({
schema: {
response: z.object({ id: z.text() }).meta({ title: "ItemResponse" }),
},
handler: async () => ({ id: "1" }),
});
}
const app = Alepha.create().with(RefApp);
const json = doc(app);
const resp =
json.paths["/api/getItem"].get.responses["200"].content[
"application/json"
].schema;
expect(resp).toEqual({ $ref: "#/components/schemas/ItemResponse" });
expect(json.components?.schemas?.ItemResponse).toBeDefined();
expect(json.components?.schemas?.ItemResponse.type).toBe("object");
});
it("should inline untitled schemas", () => {
class InlineApp {
getInline = $action({
schema: {
response: z.object({ id: z.text() }),
},
handler: async () => ({ id: "1" }),
});
}
const app = Alepha.create().with(InlineApp);
const json = doc(app);
const resp =
json.paths["/api/getInline"].get.responses["200"].content[
"application/json"
].schema;
expect(resp.type).toBe("object");
expect(resp.$ref).toBeUndefined();
});
});
// -------------------------------------------------------------------------------------------------------------------------
// Tests: path params
// -------------------------------------------------------------------------------------------------------------------------
describe("path params", () => {
it("should convert :param to {param}", () => {
const swagger = alepha.inject(ServerSwaggerProvider);
expect(swagger.replacePathParams("/users/:id")).toBe("/users/{id}");
expect(swagger.replacePathParams("/users/:id/posts/:postId")).toBe(
"/users/{id}/posts/{postId}",
);
expect(swagger.replacePathParams("/static")).toBe("/static");
});
it("should extract param description from schema", () => {
class ParamDescApp {
getItem = $action({
path: "/items/:id",
schema: {
params: z.object({
id: z.text({ description: "The item identifier" }),
}),
response: z.text(),
},
handler: async () => "ok",
});
}
const app = Alepha.create().with(ParamDescApp);
const json = doc(app);
const param = json.paths["/api/items/{id}"].get.parameters[0];
expect(param.description).toBe("The item identifier");
expect(param.schema.description).toBeUndefined();
});
});
// -------------------------------------------------------------------------------------------------------------------------
// Tests: removePrivateFields
// -------------------------------------------------------------------------------------------------------------------------
describe("removePrivateFields", () => {
it("should remove excluded keys from nested objects", () => {
const swagger = alepha.inject(ServerSwaggerProvider);
const obj = {
a: 1,
"~options": { x: 1 },
nested: {
b: 2,
"~options": { y: 2 },
deep: {
c: 3,
secret: "hidden",
},
},
};
swagger.removePrivateFields(obj, ["~options", "secret"]);
expect(obj).toEqual({
a: 1,
nested: {
b: 2,
deep: {
c: 3,
},
},
});
});
it("should handle arrays", () => {
const swagger = alepha.inject(ServerSwaggerProvider);
const obj = {
items: [
{ id: 1, "~options": true },
{ id: 2, "~options": false },
],
};
swagger.removePrivateFields(obj, ["~options"]);
expect(obj).toEqual({
items: [{ id: 1 }, { id: 2 }],
});
});
it("should handle circular references without infinite loop", () => {
const swagger = alepha.inject(ServerSwaggerProvider);
const obj: any = { a: 1 };
obj.self = obj;
swagger.removePrivateFields(obj, ["a"]);
expect(obj.a).toBeUndefined();
expect(obj.self).toBe(obj);
});
});
// -------------------------------------------------------------------------------------------------------------------------
// Tests: isBodyMultipart
// -------------------------------------------------------------------------------------------------------------------------
describe("isBodyMultipart", () => {
it("should return true when body contains file type", () => {
const swagger = alepha.inject(ServerSwaggerProvider);
const schema = z.object({ file: z.file(), name: z.text() });
expect(swagger.isBodyMultipart(schema)).toBe(true);
});
it("should return false when body has no file type", () => {
const swagger = alepha.inject(ServerSwaggerProvider);
const schema = z.object({ name: z.text(), age: z.number() });
expect(swagger.isBodyMultipart(schema)).toBe(false);
});
});
// -------------------------------------------------------------------------------------------------------------------------
// Tests: disabled
// -------------------------------------------------------------------------------------------------------------------------
describe("disabled", () => {
it("should not generate spec when disabled", () => {
class DisabledApp {
hello = $action({
schema: { response: z.text() },
handler: async () => "ok",
});
docs = $swagger({ disabled: true, ui: false });
}
const app = Alepha.create().with(DisabledApp);
const json = app.inject(ServerSwaggerProvider).json;
expect(json).toBeUndefined();
});
});
// -------------------------------------------------------------------------------------------------------------------------
// Tests: default info
// -------------------------------------------------------------------------------------------------------------------------
describe("default info", () => {
it("should use default info when not provided", () => {
class DefaultApp {
hello = $action({
schema: { response: z.text() },
handler: async () => "ok",
});
}
const app = Alepha.create().with(DefaultApp);
const json = doc(app);
expect(json.info).toEqual({
title: "API Documentation",
version: "1.0.0",
});
});
});
// -------------------------------------------------------------------------------------------------------------------------
// Tests: tags
// -------------------------------------------------------------------------------------------------------------------------
describe("tags", () => {
it("should format colon-separated groups as slash-separated tags", () => {
class TagApp {
hello = $action({
group: "admin:users",
schema: { response: z.text() },
handler: async () => "ok",
});
}
const app = Alepha.create().with(TagApp);
const json = doc(app);
const op = json.paths["/api/hello"].get;
expect(op.tags).toEqual(["admin / users"]);
});
});
// -------------------------------------------------------------------------------------------------------------------------
// Tests: summary and description
// -------------------------------------------------------------------------------------------------------------------------
describe("summary and description", () => {
it("should include summary and description in operation", () => {
class SummaryApp {
hello = $action({
summary: "Say hello",
description: "Returns a greeting",
schema: { response: z.text() },
handler: async () => "ok",
});
}
const app = Alepha.create().with(SummaryApp);
const json = doc(app);
const op = json.paths["/api/hello"].get;
expect(op.summary).toBe("Say hello");
expect(op.description).toBe("Returns a greeting");
});
});
// -------------------------------------------------------------------------------------------------------------------------
// Tests: actions without schema are skipped
// -------------------------------------------------------------------------------------------------------------------------
describe("actions without schema", () => {
it("should skip actions without schema", () => {
class NoSchemaApp {
hello = $action({
handler: async () => "ok",
});
}
const app = Alepha.create().with(NoSchemaApp);
const json = doc(app);
expect(Object.keys(json.paths)).toHaveLength(0);
});
});
// -------------------------------------------------------------------------------------------------------------------------
// Tests: deprecated
// -------------------------------------------------------------------------------------------------------------------------
describe("deprecated", () => {
it("should mark action as deprecated when flag is set", () => {
class DeprecatedApp {
old = $action({
deprecated: true,
schema: { response: z.text() },
handler: async () => "ok",
});
}
const app = Alepha.create().with(DeprecatedApp);
const json = doc(app);
const op = json.paths["/api/old"].get;
expect(op.deprecated).toBe(true);
});
it("should not include deprecated field when not set", () => {
class NormalApp {
current = $action({
schema: { response: z.text() },
handler: async () => "ok",
});
}
const app = Alepha.create().with(NormalApp);
const json = doc(app);
const op = json.paths["/api/current"].get;
expect(op.deprecated).toBeUndefined();
});
it("should not include deprecated field when explicitly false", () => {
class NotDeprecatedApp {
active = $action({
deprecated: false,
schema: { response: z.text() },
handler: async () => "ok",
});
}
const app = Alepha.create().with(NotDeprecatedApp);
const json = doc(app);
const op = json.paths["/api/active"].get;
expect(op.deprecated).toBeUndefined();
});
});
// -------------------------------------------------------------------------------------------------------------------------
// Tests: servers
// -------------------------------------------------------------------------------------------------------------------------
describe("servers", () => {
it("should auto-populate servers from hostname", () => {
class ServerApp {
hello = $action({
schema: { response: z.text() },
handler: async () => "ok",
});
}
const app = Alepha.create().with(ServerApp);
const json = doc(app);
expect(json.servers).toBeDefined();
expect(json.servers).toHaveLength(1);
expect(json.servers![0].url).toBeDefined();
});
it("should use custom servers when provided", () => {
class CustomServerApp {
hello = $action({
schema: { response: z.text() },
handler: async () => "ok",
});
}
const app = Alepha.create().with(CustomServerApp);
const json = doc(app, {
servers: [
{ url: "https://api.example.com", description: "Production" },
{ url: "https://staging.example.com", description: "Staging" },
],
});
expect(json.servers).toEqual([
{ url: "https://api.example.com", description: "Production" },
{ url: "https://staging.example.com", description: "Staging" },
]);
});
});
// -------------------------------------------------------------------------------------------------------------------------
// Tests: response descriptions
// -------------------------------------------------------------------------------------------------------------------------
describe("response descriptions", () => {
it("should use 'OK' for 200 responses", () => {
class OkApp {
hello = $action({
schema: { response: z.text() },
handler: async () => "ok",
});
}
const app = Alepha.create().with(OkApp);
const json = doc(app);
expect(json.paths["/api/hello"].get.responses["200"].description).toBe(
"OK",
);
});
it("should use 'No Content' for 204 responses", () => {
class NoContentApp {
fire = $action({
schema: {},
handler: async () => {},
});
}
const app = Alepha.create().with(NoContentApp);
const json = doc(app);
expect(json.paths["/api/fire"].get.responses["204"].description).toBe(
"No Content",
);
});
it("should use 'Created' for 201 responses", () => {
class CreatedApp {
create = $action({
schema: {
body: z.object({ name: z.text() }),
response: { 201: z.object({ id: z.text() }) } as any,
},
handler: async () => ({ id: "1" }) as any,
});
}
const app = Alepha.create().with(CreatedApp);
const json = doc(app);
expect(json.paths["/api/create"].post.responses["201"].description).toBe(
"Created",
);
});
});
// -------------------------------------------------------------------------------------------------------------------------
// Tests: security scheme description (API key)
// -------------------------------------------------------------------------------------------------------------------------
describe("security scheme description", () => {
it("should include description about JWT and API key in bearerAuth", () => {
class AuthApp {
secured = $action({
use: [fakeSecure()],
schema: { response: z.text() },
handler: async () => "ok",
});
}
const app = Alepha.create().with(AuthApp);
const json = doc(app);
expect(json.components?.securitySchemes?.bearerAuth.description).toBe(
"Enter a JWT token or API key. Both are accepted as Bearer tokens.",
);
});
});
// -------------------------------------------------------------------------------------------------------------------------
// Tests: examples
// -------------------------------------------------------------------------------------------------------------------------
describe("examples", () => {
it("should extract examples from query param schema", () => {
class ExampleQueryApp {
search = $action({
schema: {
query: z.object({
q: z.text({ examples: ["hello world"] }),
}),
response: z.text(),
},
handler: async () => "ok",
});
}
const app = Alepha.create().with(ExampleQueryApp);
const json = doc(app);
const param = json.paths["/api/search"].get.parameters[0];
expect(param.example).toBe("hello world");
});
it("should extract default as example from query param schema", () => {
class DefaultQueryApp {
search = $action({
schema: {
query: z.object({
page: z.number().default(1),
}),
response: z.text(),
},
handler: async () => "ok",
});
}
const app = Alepha.create().with(DefaultQueryApp);
const json = doc(app);
const param = json.paths["/api/search"].get.parameters[0];
expect(param.example).toBe(1);
});
it("should extract examples from path param schema", () => {
class ExampleParamApp {
getUser = $action({
path: "/users/:id",
schema: {
params: z.object({
id: z.text({ examples: ["usr_abc123"] }),
}),
response: z.text(),
},
handler: async () => "ok",
});
}
const app = Alepha.create().with(ExampleParamApp);
const json = doc(app);
const param = json.paths["/api/users/{id}"].get.parameters[0];
expect(param.example).toBe("usr_abc123");
});
it("should not add example when none exists", () => {
class NoExampleApp {
search = $action({
schema: {
query: z.object({
q: z.text(),
}),
response: z.text(),
},
handler: async () => "ok",
});
}
const app = Alepha.create().with(NoExampleApp);
const json = doc(app);
const param = json.paths["/api/search"].get.parameters[0];
expect(param.example).toBeUndefined();
});
});