refactored adapters to run test suites (#126)

* refactored adapters to run test suites

* fix bun version for tests

* added missing adapter tests and refactored examples to use `bknd.config.ts` where applicable
This commit is contained in:
dswbx
2025-04-01 11:43:11 +02:00
committed by GitHub
parent 36e4224b33
commit 3f26c45dd9
55 changed files with 1130 additions and 647 deletions

View File

@@ -5,6 +5,15 @@ export * from "./node.adapter";
export { StorageLocalAdapter, type LocalAdapterConfig };
export { nodeTestRunner } from "./test";
let registered = false;
export function registerLocalMediaAdapter() {
registries.media.register("local", StorageLocalAdapter);
if (!registered) {
registries.media.register("local", StorageLocalAdapter);
registered = true;
}
return (config: Partial<LocalAdapterConfig> = {}) => {
const adapter = new StorageLocalAdapter(config);
return adapter.toJSON(true);
};
}

View File

@@ -0,0 +1,15 @@
import { describe, before, after } from "node:test";
import * as node from "./node.adapter";
import { adapterTestSuite } from "adapter/adapter-test-suite";
import { nodeTestRunner } from "adapter/node";
import { disableConsoleLog, enableConsoleLog } from "core/utils";
before(() => disableConsoleLog());
after(enableConsoleLog);
describe("node adapter", () => {
adapterTestSuite(nodeTestRunner, {
makeApp: node.createApp,
makeHandler: node.createHandler,
});
});

View File

@@ -0,0 +1,15 @@
import { afterAll, beforeAll, describe } from "bun:test";
import * as node from "./node.adapter";
import { adapterTestSuite } from "adapter/adapter-test-suite";
import { bunTestRunner } from "adapter/bun/test";
import { disableConsoleLog, enableConsoleLog } from "core/utils";
beforeAll(disableConsoleLog);
afterAll(enableConsoleLog);
describe("node adapter (bun)", () => {
adapterTestSuite(bunTestRunner, {
makeApp: node.createApp,
makeHandler: node.createHandler,
});
});

View File

@@ -2,11 +2,11 @@ import path from "node:path";
import { serve as honoServe } from "@hono/node-server";
import { serveStatic } from "@hono/node-server/serve-static";
import { registerLocalMediaAdapter } from "adapter/node/index";
import type { App } from "bknd";
import { type RuntimeBkndConfig, createRuntimeApp } from "bknd/adapter";
import { type RuntimeBkndConfig, createRuntimeApp, type RuntimeOptions } from "bknd/adapter";
import { config as $config } from "bknd/core";
export type NodeBkndConfig = RuntimeBkndConfig & {
type NodeEnv = NodeJS.ProcessEnv;
export type NodeBkndConfig<Env = NodeEnv> = RuntimeBkndConfig<Env> & {
port?: number;
hostname?: string;
listener?: Parameters<typeof honoServe>[1];
@@ -14,14 +14,11 @@ export type NodeBkndConfig = RuntimeBkndConfig & {
relativeDistPath?: string;
};
export function serve({
distPath,
relativeDistPath,
port = $config.server.default_port,
hostname,
listener,
...config
}: NodeBkndConfig = {}) {
export async function createApp<Env = NodeEnv>(
{ distPath, relativeDistPath, ...config }: NodeBkndConfig<Env> = {},
args: Env = {} as Env,
opts?: RuntimeOptions,
) {
const root = path.relative(
process.cwd(),
path.resolve(distPath ?? relativeDistPath ?? "./node_modules/bknd/dist", "static"),
@@ -30,23 +27,39 @@ export function serve({
console.warn("relativeDistPath is deprecated, please use distPath instead");
}
let app: App;
registerLocalMediaAdapter();
return await createRuntimeApp(
{
...config,
serveStatic: serveStatic({ root }),
},
// @ts-ignore
args ?? { env: process.env },
opts,
);
}
export function createHandler<Env = NodeEnv>(
config: NodeBkndConfig<Env> = {},
args: Env = {} as Env,
opts?: RuntimeOptions,
) {
return async (req: Request) => {
const app = await createApp(config, args ?? (process.env as Env), opts);
return app.fetch(req);
};
}
export function serve<Env = NodeEnv>(
{ port = $config.server.default_port, hostname, listener, ...config }: NodeBkndConfig<Env> = {},
args: Env = {} as Env,
opts?: RuntimeOptions,
) {
honoServe(
{
port,
hostname,
fetch: async (req: Request) => {
if (!app) {
registerLocalMediaAdapter();
app = await createRuntimeApp({
...config,
serveStatic: serveStatic({ root }),
});
}
return app.fetch(req);
},
fetch: createHandler(config, args, opts),
},
(connInfo) => {
console.log(`Server is running on http://localhost:${connInfo.port}`);

View File

@@ -3,6 +3,7 @@ import { StorageLocalAdapter } from "./StorageLocalAdapter";
// @ts-ignore
import { assetsPath, assetsTmpPath } from "../../../../__test__/helper";
import { adapterTestSuite } from "media/storage/adapters/adapter-test-suite";
import { bunTestRunner } from "adapter/bun/test";
describe("StorageLocalAdapter (bun)", async () => {
const adapter = new StorageLocalAdapter({
@@ -10,5 +11,5 @@ describe("StorageLocalAdapter (bun)", async () => {
});
const file = Bun.file(`${assetsPath}/image.png`);
await adapterTestSuite({ test, expect }, adapter, file);
await adapterTestSuite(bunTestRunner, adapter, file);
});

View File

@@ -7,14 +7,14 @@ export const localAdapterConfig = Type.Object(
{
path: Type.String({ default: "./" }),
},
{ title: "Local", description: "Local file system storage" },
{ title: "Local", description: "Local file system storage", additionalProperties: false },
);
export type LocalAdapterConfig = Static<typeof localAdapterConfig>;
export class StorageLocalAdapter extends StorageAdapter {
private config: LocalAdapterConfig;
constructor(config: any) {
constructor(config: Partial<LocalAdapterConfig> = {}) {
super();
this.config = parse(localAdapterConfig, config);
}

View File

@@ -2,6 +2,17 @@ import nodeAssert from "node:assert/strict";
import { test } from "node:test";
import type { Matcher, Test, TestFn, TestRunner } from "core/test";
// Track mock function calls
const mockCalls = new WeakMap<Function, number>();
function createMockFunction<T extends (...args: any[]) => any>(fn: T): T {
const mockFn = (...args: Parameters<T>) => {
const currentCalls = mockCalls.get(mockFn) || 0;
mockCalls.set(mockFn, currentCalls + 1);
return fn(...args);
};
return mockFn as T;
}
const nodeTestMatcher = <T = unknown>(actual: T, parentFailMsg?: string) =>
({
toEqual: (expected: T, failMsg = parentFailMsg) => {
@@ -23,6 +34,18 @@ const nodeTestMatcher = <T = unknown>(actual: T, parentFailMsg?: string) =>
const e = Array.isArray(expected) ? expected : [expected];
nodeAssert.ok(e.includes(actual), failMsg);
},
toHaveBeenCalled: (failMsg = parentFailMsg) => {
const calls = mockCalls.get(actual as Function) || 0;
nodeAssert.ok(calls > 0, failMsg || "Expected function to have been called at least once");
},
toHaveBeenCalledTimes: (expected: number, failMsg = parentFailMsg) => {
const calls = mockCalls.get(actual as Function) || 0;
nodeAssert.strictEqual(
calls,
expected,
failMsg || `Expected function to have been called ${expected} times`,
);
},
}) satisfies Matcher<T>;
const nodeTestResolverProxy = <T = unknown>(
@@ -63,6 +86,7 @@ nodeTest.skipIf = (condition: boolean): Test => {
export const nodeTestRunner: TestRunner = {
test: nodeTest,
mock: createMockFunction,
expect: <T = unknown>(actual?: T, failMsg?: string) => ({
...nodeTestMatcher(actual, failMsg),
resolves: nodeTestResolverProxy(actual as Promise<T>, {