Merge branch 'main' into cp/216-fix-users-link

This commit is contained in:
Cameron Pak
2026-02-28 11:03:54 -06:00
260 changed files with 8084 additions and 3850 deletions

View File

@@ -108,7 +108,7 @@ describe("App tests", async () => {
expect(Array.from(app.plugins.keys())).toEqual(["test"]);
});
test.only("drivers", async () => {
test("drivers", async () => {
const called: string[] = [];
const app = new App(dummyConnection, undefined, {
drivers: {

View File

@@ -1,66 +1,76 @@
import { expect, describe, it, beforeAll, afterAll, mock } from "bun:test";
import * as adapter from "adapter";
import { disableConsoleLog, enableConsoleLog } from "core/utils";
import { disableConsoleLog, enableConsoleLog, omitKeys } from "core/utils";
import { adapterTestSuite } from "adapter/adapter-test-suite";
import { bunTestRunner } from "adapter/bun/test";
import { omitKeys } from "core/utils";
const stripConnection = <T extends Record<string, any>>(cfg: T) =>
omitKeys(cfg, ["connection"]);
beforeAll(disableConsoleLog);
afterAll(enableConsoleLog);
describe("adapter", () => {
it("makes config", async () => {
expect(omitKeys(await adapter.makeConfig({}), ["connection"])).toEqual({});
expect(
omitKeys(await adapter.makeConfig({}, { env: { TEST: "test" } }), ["connection"]),
).toEqual({});
describe("makeConfig", () => {
it("returns empty config for empty inputs", async () => {
const cases: Array<Parameters<typeof adapter.makeConfig>> = [
[{}],
[{}, { env: { TEST: "test" } }],
];
// merges everything returned from `app` with the config
expect(
omitKeys(
await adapter.makeConfig(
{ app: (a) => ({ config: { server: { cors: { origin: a.env.TEST } } } }) },
{ env: { TEST: "test" } },
),
["connection"],
),
).toEqual({
config: { server: { cors: { origin: "test" } } },
});
});
for (const args of cases) {
const cfg = await adapter.makeConfig(...(args as any));
expect(stripConnection(cfg)).toEqual({});
}
});
it("allows all properties in app function", async () => {
const called = mock(() => null);
const config = await adapter.makeConfig(
{
app: (env) => ({
connection: { url: "test" },
config: { server: { cors: { origin: "test" } } },
options: {
mode: "db",
},
onBuilt: () => {
called();
expect(env).toEqual({ foo: "bar" });
},
}),
},
{ foo: "bar" },
it("merges app output into config", async () => {
const cfg = await adapter.makeConfig(
{ app: (a) => ({ config: { server: { cors: { origin: a.env.TEST } } } }) },
{ env: { TEST: "test" } },
);
expect(config.connection).toEqual({ url: "test" });
expect(config.config).toEqual({ server: { cors: { origin: "test" } } });
expect(config.options).toEqual({ mode: "db" });
await config.onBuilt?.(null as any);
expect(called).toHaveBeenCalled();
});
adapterTestSuite(bunTestRunner, {
expect(stripConnection(cfg)).toEqual({
config: { server: { cors: { origin: "test" } } },
});
});
it("allows all properties in app() result", async () => {
const called = mock(() => null);
const cfg = await adapter.makeConfig(
{
app: (env) => ({
connection: { url: "test" },
config: { server: { cors: { origin: "test" } } },
options: { mode: "db" as const },
onBuilt: () => {
called();
expect(env).toEqual({ foo: "bar" });
},
}),
},
{ foo: "bar" },
);
expect(cfg.connection).toEqual({ url: "test" });
expect(cfg.config).toEqual({ server: { cors: { origin: "test" } } });
expect(cfg.options).toEqual({ mode: "db" });
await cfg.onBuilt?.({} as any);
expect(called).toHaveBeenCalledTimes(1);
});
});
describe("adapter test suites", () => {
adapterTestSuite(bunTestRunner, {
makeApp: adapter.createFrameworkApp,
label: "framework app",
});
});
adapterTestSuite(bunTestRunner, {
adapterTestSuite(bunTestRunner, {
makeApp: adapter.createRuntimeApp,
label: "runtime app",
});
});
});
});

View File

@@ -1,7 +1,7 @@
/// <reference types="@types/bun" />
import { describe, expect, it } from "bun:test";
import { describe, expect, it, mock } from "bun:test";
import { Hono } from "hono";
import { getFileFromContext, isFile, isReadableStream } from "core/utils";
import { getFileFromContext, isFile, isReadableStream, s, jsc } from "core/utils";
import { MediaApi } from "media/api/MediaApi";
import { assetsPath, assetsTmpPath } from "../helper";
@@ -15,7 +15,7 @@ const mockedBackend = new Hono()
.get("/file/:name", async (c) => {
const { name } = c.req.param();
const file = Bun.file(`${assetsPath}/${name}`);
return new Response(file, {
return new Response(new File([await file.bytes()], name, { type: file.type }), {
headers: {
"Content-Type": file.type,
"Content-Length": file.size.toString(),
@@ -67,7 +67,7 @@ describe("MediaApi", () => {
const res = await mockedBackend.request("/api/media/file/" + name);
await Bun.write(path, res);
const file = await Bun.file(path);
const file = Bun.file(path);
expect(file.size).toBeGreaterThan(0);
expect(file.type).toBe("image/png");
await file.delete();
@@ -98,14 +98,11 @@ describe("MediaApi", () => {
expect(isReadableStream(res.body)).toBe(true);
expect(isReadableStream(res.res.body)).toBe(true);
const blob = await res.res.blob();
// Response.blob() always returns Blob, not File - File metadata (name, lastModified) is lost
// Client code must manually construct File from Blob (see MediaApi.download() for reference)
const file = new File([blob], name, { type: blob.type });
expect(isFile(file)).toBe(true);
expect(file.size).toBeGreaterThan(0);
expect(file.type).toBe("image/png");
expect(file.name).toContain(name);
const blob = (await res.res.blob()) as File;
expect(isFile(blob)).toBe(true);
expect(blob.size).toBeGreaterThan(0);
expect(blob.type).toBe("image/png");
expect(blob.name).toContain(name);
});
it("getFileStream", async () => {
@@ -117,15 +114,11 @@ describe("MediaApi", () => {
const stream = await api.getFileStream(name);
expect(isReadableStream(stream)).toBe(true);
const blob = await new Response(stream).blob();
// Response.blob() always returns Blob, not File - File metadata (name, lastModified) is lost
// Client code must manually construct File from Blob (see MediaApi.download() for reference)
// Use originalRes.headers.get("Content-Type") to preserve MIME type from response
const file = new File([blob], name, { type: originalRes.headers.get("Content-Type") || blob.type });
expect(isFile(file)).toBe(true);
expect(file.size).toBeGreaterThan(0);
expect(file.type).toBe("image/png");
expect(file.name).toContain(name);
const blob = (await new Response(res).blob()) as File;
expect(isFile(blob)).toBe(true);
expect(blob.size).toBeGreaterThan(0);
expect(blob.type).toBe("image/png");
expect(blob.name).toContain(name);
});
it("should upload file in various ways", async () => {
@@ -162,15 +155,38 @@ describe("MediaApi", () => {
}
// upload via readable from bun
await matches(await api.upload(file.stream(), { filename: "readable.png" }), "readable.png");
await matches(api.upload(file.stream(), { filename: "readable.png" }), "readable.png");
// upload via readable from response
{
const response = (await mockedBackend.request(url)) as Response;
await matches(
await api.upload(response.body!, { filename: "readable.png" }),
"readable.png",
);
await matches(api.upload(response.body!, { filename: "readable.png" }), "readable.png");
}
});
it("should add overwrite query for entity upload", async (c) => {
const call = mock(() => null);
const hono = new Hono().post(
"/api/media/entity/:entity/:id/:field",
jsc("query", s.object({ overwrite: s.boolean().optional() })),
async (c) => {
const { overwrite } = c.req.valid("query");
expect(overwrite).toBe(true);
call();
return c.json({ ok: true });
},
);
const api = new MediaApi(
{
upload_fetcher: hono.request,
},
hono.request,
);
const file = Bun.file(`${assetsPath}/image.png`);
const res = await api.uploadToEntity("posts", 1, "cover", file as any, {
overwrite: true,
});
expect(res.ok).toBe(true);
expect(call).toHaveBeenCalled();
});
});

View File

@@ -1,8 +1,12 @@
import { describe, expect, mock, test } from "bun:test";
import { describe, expect, mock, test, beforeAll, afterAll } from "bun:test";
import { createApp as internalCreateApp, type CreateAppConfig } from "bknd";
import { getDummyConnection } from "../../__test__/helper";
import { ModuleManager } from "modules/ModuleManager";
import { em, entity, text } from "data/prototype";
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
beforeAll(disableConsoleLog);
afterAll(enableConsoleLog);
async function createApp(config: CreateAppConfig = {}) {
const app = internalCreateApp({

View File

@@ -1,7 +1,11 @@
import { AppEvents } from "App";
import { describe, test, expect, beforeAll, mock } from "bun:test";
import { describe, test, expect, beforeAll, mock, afterAll } from "bun:test";
import { type App, createApp, createMcpToolCaller } from "core/test/utils";
import type { McpServer } from "bknd/utils";
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
beforeAll(disableConsoleLog);
afterAll(enableConsoleLog);
/**
* - [x] system_config

View File

@@ -0,0 +1,42 @@
import { describe, expect, test } from "bun:test";
import { code, hybrid } from "modes";
describe("modes", () => {
describe("code", () => {
test("verify base configuration", async () => {
const c = code({}) as any;
const config = await c.app?.({} as any);
expect(Object.keys(config)).toEqual(["options"]);
expect(config.options.mode).toEqual("code");
expect(config.options.plugins).toEqual([]);
expect(config.options.manager.skipValidation).toEqual(false);
expect(config.options.manager.onModulesBuilt).toBeDefined();
});
test("keeps overrides", async () => {
const c = code({
connection: {
url: ":memory:",
},
}) as any;
const config = await c.app?.({} as any);
expect(config.connection.url).toEqual(":memory:");
});
});
describe("hybrid", () => {
test("fails if no reader is provided", () => {
// @ts-ignore
expect(hybrid({} as any).app?.({} as any)).rejects.toThrow(/reader/);
});
test("verify base configuration", async () => {
const c = hybrid({ reader: async () => ({}) }) as any;
const config = await c.app?.({} as any);
expect(Object.keys(config)).toEqual(["reader", "beforeBuild", "config", "options"]);
expect(config.options.mode).toEqual("db");
expect(config.options.plugins).toEqual([]);
expect(config.options.manager.skipValidation).toEqual(false);
expect(config.options.manager.onModulesBuilt).toBeDefined();
});
});
});

View File

@@ -76,7 +76,7 @@ describe("repros", async () => {
expect(app.em.entities.map((e) => e.name)).toEqual(["media", "test"]);
});
test.only("verify inversedBy", async () => {
test("verify inversedBy", async () => {
const schema = proto.em(
{
products: proto.entity("products", {

View File

@@ -1,8 +1,12 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test, beforeAll, afterAll } from "bun:test";
import { Guard, type GuardConfig } from "auth/authorize/Guard";
import { Permission } from "auth/authorize/Permission";
import { Role, type RoleSchema } from "auth/authorize/Role";
import { objectTransform, s } from "bknd/utils";
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
beforeAll(disableConsoleLog);
afterAll(enableConsoleLog);
function createGuard(
permissionNames: string[],

View File

@@ -7,8 +7,8 @@ import type { App, DB } from "bknd";
import type { CreateUserPayload } from "auth/AppAuth";
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
beforeAll(() => disableConsoleLog());
afterAll(() => enableConsoleLog());
beforeAll(disableConsoleLog);
afterAll(enableConsoleLog);
async function makeApp(config: Partial<CreateAppConfig["config"]> = {}) {
const app = createApp({

View File

@@ -0,0 +1,40 @@
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
import { createAuthTestApp } from "./shared";
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
import { em, entity, text } from "data/prototype";
beforeAll(disableConsoleLog);
afterAll(enableConsoleLog);
const schema = em(
{
posts: entity("posts", {
title: text(),
content: text(),
}),
comments: entity("comments", {
content: text(),
}),
},
({ relation }, { posts, comments }) => {
relation(posts).manyToOne(comments);
},
);
describe("DataController (auth)", () => {
test("reading schema.json", async () => {
const { request } = await createAuthTestApp(
{
permission: ["system.access.api", "data.entity.read", "system.schema.read"],
request: new Request("http://localhost/api/data/schema.json"),
},
{
config: { data: schema.toJSON() },
},
);
expect((await request.guest()).status).toBe(403);
expect((await request.member()).status).toBe(403);
expect((await request.authorized()).status).toBe(200);
expect((await request.admin()).status).toBe(200);
});
});

View File

@@ -1,20 +0,0 @@
import { describe, it, expect } from "bun:test";
import { SystemController } from "modules/server/SystemController";
import { createApp } from "core/test/utils";
import type { CreateAppConfig } from "App";
import { getPermissionRoutes } from "auth/middlewares/permission.middleware";
async function makeApp(config: Partial<CreateAppConfig> = {}) {
const app = createApp(config);
await app.build();
return app;
}
describe.skip("SystemController", () => {
it("...", async () => {
const app = await makeApp();
const controller = new SystemController(app);
const hono = controller.getController();
console.log(getPermissionRoutes(hono));
});
});

View File

@@ -0,0 +1,41 @@
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
import { createAuthTestApp } from "./shared";
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
beforeAll(disableConsoleLog);
afterAll(enableConsoleLog);
describe("SystemController (auth)", () => {
test("reading info", async () => {
const { request } = await createAuthTestApp({
permission: ["system.access.api", "system.info"],
request: new Request("http://localhost/api/system/info"),
});
expect((await request.guest()).status).toBe(403);
expect((await request.member()).status).toBe(403);
expect((await request.authorized()).status).toBe(200);
expect((await request.admin()).status).toBe(200);
});
test("reading permissions", async () => {
const { request } = await createAuthTestApp({
permission: ["system.access.api", "system.schema.read"],
request: new Request("http://localhost/api/system/permissions"),
});
expect((await request.guest()).status).toBe(403);
expect((await request.member()).status).toBe(403);
expect((await request.authorized()).status).toBe(200);
expect((await request.admin()).status).toBe(200);
});
test("access openapi", async () => {
const { request } = await createAuthTestApp({
permission: ["system.access.api", "system.openapi"],
request: new Request("http://localhost/api/system/openapi.json"),
});
expect((await request.guest()).status).toBe(403);
expect((await request.member()).status).toBe(403);
expect((await request.authorized()).status).toBe(200);
expect((await request.admin()).status).toBe(200);
});
});

View File

@@ -0,0 +1,171 @@
import { createApp } from "core/test/utils";
import type { CreateAppConfig } from "App";
import type { RoleSchema } from "auth/authorize/Role";
import { isPlainObject } from "core/utils";
export type AuthTestConfig = {
guest?: RoleSchema;
member?: RoleSchema;
authorized?: RoleSchema;
};
export async function createAuthTestApp(
testConfig: {
permission: AuthTestConfig | string | string[];
request: Request;
},
config: Partial<CreateAppConfig> = {},
) {
let member: RoleSchema | undefined;
let authorized: RoleSchema | undefined;
let guest: RoleSchema | undefined;
if (isPlainObject(testConfig.permission)) {
if (testConfig.permission.guest)
guest = {
...testConfig.permission.guest,
is_default: true,
};
if (testConfig.permission.member) member = testConfig.permission.member;
if (testConfig.permission.authorized) authorized = testConfig.permission.authorized;
} else {
member = {
permissions: [],
};
authorized = {
permissions: Array.isArray(testConfig.permission)
? testConfig.permission
: [testConfig.permission],
};
guest = {
permissions: [],
is_default: true,
};
}
console.log("authorized", authorized);
const app = createApp({
...config,
config: {
...config.config,
auth: {
...config.config?.auth,
enabled: true,
guard: {
enabled: true,
...config.config?.auth?.guard,
},
jwt: {
...config.config?.auth?.jwt,
secret: "secret",
},
roles: {
...config.config?.auth?.roles,
guest,
member,
authorized,
admin: {
implicit_allow: true,
},
},
},
},
});
await app.build();
const users = {
guest: null,
member: await app.createUser({
email: "member@test.com",
password: "12345678",
role: "member",
}),
authorized: await app.createUser({
email: "authorized@test.com",
password: "12345678",
role: "authorized",
}),
admin: await app.createUser({
email: "admin@test.com",
password: "12345678",
role: "admin",
}),
} as const;
const tokens = {} as Record<keyof typeof users, string>;
for (const [key, user] of Object.entries(users)) {
if (user) {
tokens[key as keyof typeof users] = await app.module.auth.authenticator.jwt(user);
}
}
async function makeRequest(user: keyof typeof users, input: string, init: RequestInit = {}) {
const headers = new Headers(init.headers ?? {});
if (user in tokens) {
headers.set("Authorization", `Bearer ${tokens[user as keyof typeof tokens]}`);
}
const res = await app.server.request(input, {
...init,
headers,
});
let data: any;
if (res.headers.get("Content-Type")?.startsWith("application/json")) {
data = await res.json();
} else if (res.headers.get("Content-Type")?.startsWith("text/")) {
data = await res.text();
}
return {
status: res.status,
ok: res.ok,
headers: Object.fromEntries(res.headers.entries()),
data,
};
}
const requestFn = new Proxy(
{},
{
get(_, prop: keyof typeof users) {
return async (input: string, init: RequestInit = {}) => {
return makeRequest(prop, input, init);
};
},
},
) as {
[K in keyof typeof users]: (
input: string,
init?: RequestInit,
) => Promise<{
status: number;
ok: boolean;
headers: Record<string, string>;
data: any;
}>;
};
const request = new Proxy(
{},
{
get(_, prop: keyof typeof users) {
return async () => {
return makeRequest(prop, testConfig.request.url, {
headers: testConfig.request.headers,
method: testConfig.request.method,
body: testConfig.request.body,
});
};
},
},
) as {
[K in keyof typeof users]: () => Promise<{
status: number;
ok: boolean;
headers: Record<string, string>;
data: any;
}>;
};
return { app, users, request, requestFn };
}

View File

@@ -0,0 +1,13 @@
import { PasswordStrategy } from "auth/authenticate/strategies/PasswordStrategy";
import { describe, expect, it } from "bun:test";
describe("PasswordStrategy", () => {
it("should enforce provided minimum length", async () => {
const strategy = new PasswordStrategy({ minLength: 8, hashing: "plain" });
expect(strategy.verify("password")({} as any)).rejects.toThrow();
expect(
strategy.verify("password1234")({ strategy_value: "password1234" } as any),
).resolves.toBeUndefined();
});
});

View File

@@ -0,0 +1,85 @@
import { describe, beforeAll, afterAll, test } from "bun:test";
import type { PostgresConnection } from "data/connection/postgres/PostgresConnection";
import { pg, postgresJs } from "bknd";
import { Pool } from "pg";
import postgres from "postgres";
import { disableConsoleLog, enableConsoleLog, $waitUntil } from "bknd/utils";
import { $ } from "bun";
import { connectionTestSuite } from "data/connection/connection-test-suite";
import { bunTestRunner } from "adapter/bun/test";
const credentials = {
host: "localhost",
port: 5430,
user: "postgres",
password: "postgres",
database: "bknd",
};
async function cleanDatabase(connection: InstanceType<typeof PostgresConnection>) {
const kysely = connection.kysely;
// drop all tables+indexes & create new schema
await kysely.schema.dropSchema("public").ifExists().cascade().execute();
await kysely.schema.dropIndex("public").ifExists().cascade().execute();
await kysely.schema.createSchema("public").execute();
}
async function isPostgresRunning() {
try {
// Try to actually connect to PostgreSQL
const conn = pg({ pool: new Pool(credentials) });
await conn.ping();
await conn.close();
return true;
} catch (e) {
return false;
}
}
describe("postgres", () => {
beforeAll(async () => {
if (!(await isPostgresRunning())) {
await $`docker run --rm --name bknd-test-postgres -d -e POSTGRES_PASSWORD=${credentials.password} -e POSTGRES_USER=${credentials.user} -e POSTGRES_DB=${credentials.database} -p ${credentials.port}:5432 postgres:17`;
await $waitUntil("Postgres is running", isPostgresRunning);
await new Promise((resolve) => setTimeout(resolve, 500));
}
disableConsoleLog();
});
afterAll(async () => {
if (await isPostgresRunning()) {
try {
await $`docker stop bknd-test-postgres`;
} catch (e) {}
}
enableConsoleLog();
});
describe.serial.each([
["pg", () => pg({ pool: new Pool(credentials) })],
["postgresjs", () => postgresJs({ postgres: postgres(credentials) })],
])("%s", (name, createConnection) => {
connectionTestSuite(
{
...bunTestRunner,
test: test.serial,
},
{
makeConnection: () => {
const connection = createConnection();
return {
connection,
dispose: async () => {
await cleanDatabase(connection);
await connection.close();
},
};
},
rawDialectDetails: [],
disableConsoleLog: false,
},
);
});
});

View File

@@ -124,6 +124,81 @@ describe("[Repository]", async () => {
.then((r) => [r.count, r.total]),
).resolves.toEqual([undefined, undefined]);
});
test("auto join", async () => {
const schema = $em(
{
posts: $entity("posts", {
title: $text(),
content: $text(),
}),
comments: $entity("comments", {
content: $text(),
}),
another: $entity("another", {
title: $text(),
}),
},
({ relation }, { posts, comments }) => {
relation(comments).manyToOne(posts);
},
);
const em = schema.proto.withConnection(getDummyConnection().dummyConnection);
await em.schema().sync({ force: true });
await em.mutator("posts").insertOne({ title: "post1", content: "content1" });
await em
.mutator("comments")
.insertMany([{ content: "comment1", posts_id: 1 }, { content: "comment2" }] as any);
const res = await em.repo("comments").findMany({
where: {
"posts.title": "post1",
},
});
expect(res.data as any).toEqual([
{
id: 1,
content: "comment1",
posts_id: 1,
},
]);
{
// manual join should still work
const res = await em.repo("comments").findMany({
join: ["posts"],
where: {
"posts.title": "post1",
},
});
expect(res.data as any).toEqual([
{
id: 1,
content: "comment1",
posts_id: 1,
},
]);
}
// inexistent should be detected and thrown
expect(
em.repo("comments").findMany({
where: {
"random.title": "post1",
},
}),
).rejects.toThrow(/Invalid where field/);
// existing alias, but not a relation should throw
expect(
em.repo("comments").findMany({
where: {
"another.title": "post1",
},
}),
).rejects.toThrow(/Invalid where field/);
});
});
describe("[data] Repository (Events)", async () => {

View File

@@ -59,7 +59,7 @@ describe("SqliteIntrospector", () => {
dataType: "INTEGER",
isNullable: false,
isAutoIncrementing: true,
hasDefaultValue: false,
hasDefaultValue: true,
comment: undefined,
},
{
@@ -89,7 +89,7 @@ describe("SqliteIntrospector", () => {
dataType: "INTEGER",
isNullable: false,
isAutoIncrementing: true,
hasDefaultValue: false,
hasDefaultValue: true,
comment: undefined,
},
{

View File

@@ -1,9 +1,15 @@
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
import { App, createApp, type AuthResponse } from "../../src";
import { auth } from "../../src/modules/middlewares";
import { randomString, secureRandomString, withDisabledConsole } from "../../src/core/utils";
import {
mergeObject,
randomString,
secureRandomString,
withDisabledConsole,
} from "../../src/core/utils";
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
import { getDummyConnection } from "../helper";
import type { AppAuthSchema } from "auth/auth-schema";
beforeAll(disableConsoleLog);
afterAll(enableConsoleLog);
@@ -62,12 +68,12 @@ const configs = {
},
};
function createAuthApp() {
function createAuthApp(config?: Partial<AppAuthSchema>) {
const { dummyConnection } = getDummyConnection();
const app = createApp({
connection: dummyConnection,
config: {
auth: configs.auth,
auth: mergeObject(configs.auth, config ?? {}),
},
});
@@ -132,6 +138,16 @@ const fns = <Mode extends "cookie" | "token" = "token">(app: App, mode?: Mode) =
return { res, data };
},
register: async (user: any): Promise<{ res: Response; data: AuthResponse }> => {
const res = (await app.server.request("/api/auth/password/register", {
method: "POST",
headers: headers(),
body: body(user),
})) as Response;
const data = mode === "cookie" ? getCookie(res, "auth") : await res.json();
return { res, data };
},
me: async (token?: string): Promise<Pick<AuthResponse, "user">> => {
const res = (await app.server.request("/api/auth/me", {
method: "GET",
@@ -245,4 +261,61 @@ describe("integration auth", () => {
expect(await $fns.me()).toEqual({ user: null as any });
}
});
it("should register users with default role", async () => {
const app = createAuthApp({ default_role_register: "guest" });
await app.build();
const $fns = fns(app);
// takes default role
expect(
await app
.createUser({
email: "test@bknd.io",
password: "12345678",
})
.then((r) => r.role),
).toBe("guest");
// throws error if role doesn't exist
expect(
app.createUser({
email: "test@bknd.io",
password: "12345678",
role: "doesnt exist",
}),
).rejects.toThrow();
// takes role if provided
expect(
await app
.createUser({
email: "test2@bknd.io",
password: "12345678",
role: "admin",
})
.then((r) => r.role),
).toBe("admin");
// registering with role is not allowed
expect(
await $fns
.register({
email: "test3@bknd.io",
password: "12345678",
role: "admin",
})
.then((r) => r.res.ok),
).toBe(false);
// takes default role
expect(
await $fns
.register({
email: "test3@bknd.io",
password: "12345678",
})
.then((r) => r.data.user.role),
).toBe("guest");
});
});

View File

@@ -8,9 +8,10 @@ import type { TAppMediaConfig } from "../../src/media/media-schema";
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
import { assetsPath, assetsTmpPath } from "../helper";
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
import * as proto from "data/prototype";
beforeAll(() => {
//disableConsoleLog();
disableConsoleLog();
registries.media.register("local", StorageLocalAdapter);
});
afterAll(enableConsoleLog);
@@ -128,4 +129,87 @@ describe("MediaController", () => {
expect(destFile.exists()).resolves.toBe(true);
await destFile.delete();
});
test("entity upload with max_items and overwrite", async () => {
const app = createApp({
config: {
media: mergeObject(
{
enabled: true,
adapter: {
type: "local",
config: {
path: assetsTmpPath,
},
},
},
{},
),
data: {
entities: {
posts: proto
.entity("posts", {
title: proto.text(),
cover: proto.medium(),
})
.toJSON(),
},
},
},
});
await app.build();
// create a post first
const createRes = await app.server.request("/api/data/entity/posts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: "Test Post" }),
});
expect(createRes.status).toBe(201);
const { data: post } = (await createRes.json()) as any;
const file = Bun.file(path);
const uploadedFiles: string[] = [];
// upload first file to entity (should succeed)
const res1 = await app.server.request(`/api/media/entity/posts/${post.id}/cover`, {
method: "POST",
body: file,
});
expect(res1.status).toBe(201);
const result1 = (await res1.json()) as any;
uploadedFiles.push(result1.name);
// upload second file without overwrite (should fail - max_items reached)
const res2 = await app.server.request(`/api/media/entity/posts/${post.id}/cover`, {
method: "POST",
body: file,
});
expect(res2.status).toBe(400);
const result2 = (await res2.json()) as any;
expect(result2.error).toContain("Max items");
// upload third file with overwrite=true (should succeed and delete old file)
const res3 = await app.server.request(
`/api/media/entity/posts/${post.id}/cover?overwrite=true`,
{
method: "POST",
body: file,
},
);
expect(res3.status).toBe(201);
const result3 = (await res3.json()) as any;
uploadedFiles.push(result3.name);
// verify old file was deleted from storage
const oldFile = Bun.file(assetsTmpPath + "/" + uploadedFiles[0]);
expect(await oldFile.exists()).toBe(false);
// verify new file exists
const newFile = Bun.file(assetsTmpPath + "/" + uploadedFiles[1]);
expect(await newFile.exists()).toBe(true);
// cleanup
await newFile.delete();
});
});

View File

@@ -10,12 +10,6 @@ beforeAll(disableConsoleLog);
afterAll(enableConsoleLog);
describe("AppAuth", () => {
test.skip("...", () => {
const auth = new AppAuth({});
console.log(auth.toJSON());
console.log(auth.config);
});
moduleTestSuite(AppAuth);
let ctx: ModuleBuildContext;
@@ -39,11 +33,9 @@ describe("AppAuth", () => {
await auth.build();
const oldConfig = auth.toJSON(true);
//console.log(oldConfig);
await auth.schema().patch("enabled", true);
await auth.build();
const newConfig = auth.toJSON(true);
//console.log(newConfig);
expect(newConfig.jwt.secret).not.toBe(oldConfig.jwt.secret);
});
@@ -69,7 +61,6 @@ describe("AppAuth", () => {
const app = new AuthController(auth).getController();
{
disableConsoleLog();
const res = await app.request("/password/register", {
method: "POST",
headers: {
@@ -80,7 +71,6 @@ describe("AppAuth", () => {
password: "12345678",
}),
});
enableConsoleLog();
expect(res.status).toBe(200);
const { data: users } = await ctx.em.repository("users").findMany();
@@ -119,7 +109,6 @@ describe("AppAuth", () => {
const app = new AuthController(auth).getController();
{
disableConsoleLog();
const res = await app.request("/password/register", {
method: "POST",
headers: {
@@ -130,7 +119,6 @@ describe("AppAuth", () => {
password: "12345678",
}),
});
enableConsoleLog();
expect(res.status).toBe(200);
const { data: users } = await ctx.em.repository("users").findMany();
@@ -235,4 +223,32 @@ describe("AppAuth", () => {
}
}
});
test("default role for registration must be a valid role", async () => {
const app = createApp({
config: {
auth: {
enabled: true,
jwt: {
secret: "123456",
},
allow_register: true,
roles: {
guest: {
is_default: true,
},
},
},
},
});
await app.build();
const auth = app.module.auth;
// doesn't allow invalid role
expect(auth.schema().patch("default_role_register", "admin")).rejects.toThrow();
// allows valid role
await auth.schema().patch("default_role_register", "guest");
expect(auth.toJSON().default_role_register).toBe("guest");
});
});

View File

@@ -1,10 +1,14 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test, beforeAll, afterAll } from "bun:test";
import { createApp } from "core/test/utils";
import { em, entity, text } from "data/prototype";
import { registries } from "modules/registries";
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
import { AppMedia } from "../../src/media/AppMedia";
import { moduleTestSuite } from "./module-test-suite";
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
beforeAll(disableConsoleLog);
afterAll(enableConsoleLog);
describe("AppMedia", () => {
test.skip("...", () => {

View File

@@ -1,7 +1,11 @@
import { it, expect, describe } from "bun:test";
import { it, expect, describe, beforeAll, afterAll } from "bun:test";
import { DbModuleManager } from "modules/db/DbModuleManager";
import { getDummyConnection } from "../helper";
import { TABLE_NAME } from "modules/db/migrations";
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
beforeAll(disableConsoleLog);
afterAll(enableConsoleLog);
describe("DbModuleManager", () => {
it("should extract secrets", async () => {

View File

@@ -11,7 +11,7 @@ import { s, stripMark } from "core/utils/schema";
import { Connection } from "data/connection/Connection";
import { entity, text } from "data/prototype";
beforeAll(disableConsoleLog);
beforeAll(() => disableConsoleLog());
afterAll(enableConsoleLog);
describe("ModuleManager", async () => {
@@ -82,7 +82,6 @@ describe("ModuleManager", async () => {
},
},
} as any;
//const { version, ...json } = mm.toJSON() as any;
const { dummyConnection } = getDummyConnection();
const db = dummyConnection.kysely;
@@ -97,10 +96,6 @@ describe("ModuleManager", async () => {
await mm2.build();
/* console.log({
json,
configs: mm2.configs(),
}); */
//expect(stripMark(json)).toEqual(stripMark(mm2.configs()));
expect(mm2.configs().data.entities?.test).toBeDefined();
expect(mm2.configs().data.entities?.test?.fields?.content).toBeDefined();
@@ -228,8 +223,6 @@ describe("ModuleManager", async () => {
const c = getDummyConnection();
const mm = new ModuleManager(c.dummyConnection);
await mm.build();
console.log("==".repeat(30));
console.log("");
const json = mm.configs();
const c2 = getDummyConnection();
@@ -275,7 +268,6 @@ describe("ModuleManager", async () => {
}
override async build() {
//console.log("building FailingModule", this.config);
if (this.config.value && this.config.value < 0) {
throw new Error("value must be positive, given: " + this.config.value);
}
@@ -296,9 +288,6 @@ describe("ModuleManager", async () => {
}
}
beforeEach(() => disableConsoleLog(["log", "warn", "error"]));
afterEach(enableConsoleLog);
test("it builds", async () => {
const { dummyConnection } = getDummyConnection();
const mm = new TestModuleManager(dummyConnection);