feat: 初始提交
This commit is contained in:
208
tests/server/bootstrap.test.ts
Normal file
208
tests/server/bootstrap.test.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
/* eslint-disable @typescript-eslint/no-empty-function, @typescript-eslint/require-await */
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import type { ResolvedConfig } from "../../src/server/config/types";
|
||||
import type { Logger } from "../../src/server/logger";
|
||||
import type { StartServerOptions } from "../../src/server/server";
|
||||
|
||||
import { bootstrap, type BootstrapDependencies } from "../../src/server/bootstrap";
|
||||
import { createMemoryLogger } from "../../src/server/logger";
|
||||
|
||||
function makeTempConfig(overrides: Partial<ResolvedConfig> = {}): ResolvedConfig {
|
||||
const base = join(tmpdir(), `bootstrap-test-${Date.now()}`);
|
||||
mkdirSync(base, { recursive: true });
|
||||
return {
|
||||
configDir: base,
|
||||
dataDir: join(base, "data"),
|
||||
host: "127.0.0.1",
|
||||
logging: {
|
||||
consoleLevel: "info",
|
||||
fileLevel: "info",
|
||||
filePath: join(base, "data", "logs", "test.log"),
|
||||
rotationFrequency: "daily",
|
||||
rotationMaxFiles: 14,
|
||||
rotationSizeBytes: 52428800,
|
||||
rotationSizeRaw: "50MB",
|
||||
},
|
||||
port: 0,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("bootstrap", () => {
|
||||
test("使用默认依赖启动", async () => {
|
||||
let started = false;
|
||||
let signalRegistered = false;
|
||||
let loggerPassedToServer: Logger | undefined;
|
||||
|
||||
const cfg = makeTempConfig();
|
||||
const mockLoadConfig = (async () => cfg) as unknown as BootstrapDependencies["loadConfig"];
|
||||
const mockOnSignal = (_signal: string, _handler: () => void) => {
|
||||
signalRegistered = true;
|
||||
};
|
||||
const mockStartServer = (options: StartServerOptions) => {
|
||||
loggerPassedToServer = options.logger;
|
||||
started = true;
|
||||
return {};
|
||||
};
|
||||
|
||||
const deps: BootstrapDependencies = {
|
||||
createLogger: async () => createMemoryLogger(),
|
||||
loadConfig: mockLoadConfig,
|
||||
onSignal: mockOnSignal,
|
||||
startServer: mockStartServer,
|
||||
};
|
||||
|
||||
await bootstrap({ configPath: join(cfg.configDir, "config.yaml"), mode: "production" }, deps);
|
||||
|
||||
expect(started).toBe(true);
|
||||
expect(signalRegistered).toBe(true);
|
||||
expect(loggerPassedToServer).toBeDefined();
|
||||
});
|
||||
|
||||
test("传递 version 给 startServer", async () => {
|
||||
let receivedVersion: string | undefined;
|
||||
let loggerCreated = false;
|
||||
|
||||
const cfg = makeTempConfig();
|
||||
const deps: BootstrapDependencies = {
|
||||
createLogger: async (_logConfig, _mode, version) => {
|
||||
loggerCreated = true;
|
||||
expect(version).toBe("1.2.3");
|
||||
return createMemoryLogger();
|
||||
},
|
||||
loadConfig: async () => cfg,
|
||||
onSignal: () => {},
|
||||
startServer: (options: StartServerOptions) => {
|
||||
receivedVersion = options.version;
|
||||
return {};
|
||||
},
|
||||
};
|
||||
|
||||
await bootstrap({ configPath: join(cfg.configDir, "config.yaml"), mode: "production", version: "1.2.3" }, deps);
|
||||
|
||||
expect(receivedVersion).toBe("1.2.3");
|
||||
expect(loggerCreated).toBe(true);
|
||||
});
|
||||
|
||||
test("logger 初始化失败时使用 fallback 并退出", async () => {
|
||||
let exitCode: number | undefined;
|
||||
|
||||
const cfg = makeTempConfig();
|
||||
const deps: BootstrapDependencies = {
|
||||
createLogger: async () => {
|
||||
throw new Error("pino import failed");
|
||||
},
|
||||
exit: (code: number) => {
|
||||
exitCode = code;
|
||||
throw new Error("exit called");
|
||||
},
|
||||
loadConfig: async () => cfg,
|
||||
startServer: () => {
|
||||
throw new Error("should not reach");
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
await bootstrap({ configPath: join(cfg.configDir, "config.yaml"), mode: "production" }, deps);
|
||||
} catch {
|
||||
// expected - exit threw
|
||||
}
|
||||
|
||||
expect(exitCode).toBe(1);
|
||||
});
|
||||
|
||||
test("启动失败时调用 logger.fatal 并 flush", async () => {
|
||||
let fatalCalled = false;
|
||||
let flushCalled = false;
|
||||
let exitCode: number | undefined;
|
||||
|
||||
const mockLogger = createMemoryLogger();
|
||||
const origFatal = mockLogger.fatal.bind(mockLogger);
|
||||
const origFlush = mockLogger.flush.bind(mockLogger);
|
||||
mockLogger.fatal = (objOrMsg, msg?) => {
|
||||
fatalCalled = true;
|
||||
origFatal(objOrMsg, msg);
|
||||
};
|
||||
mockLogger.flush = () => {
|
||||
flushCalled = true;
|
||||
origFlush();
|
||||
};
|
||||
|
||||
const cfg = makeTempConfig();
|
||||
const deps: BootstrapDependencies = {
|
||||
createLogger: async () => mockLogger,
|
||||
exit: (code: number) => {
|
||||
exitCode = code;
|
||||
throw new Error("exit called");
|
||||
},
|
||||
loadConfig: async () => cfg,
|
||||
startServer: () => {
|
||||
throw new Error("server start failed");
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
await bootstrap({ configPath: join(cfg.configDir, "config.yaml"), mode: "production" }, deps);
|
||||
} catch {
|
||||
// expected
|
||||
}
|
||||
|
||||
expect(fatalCalled).toBe(true);
|
||||
expect(flushCalled).toBe(true);
|
||||
expect(exitCode).toBe(1);
|
||||
});
|
||||
|
||||
test("数据目录创建后记录日志", async () => {
|
||||
const cfg = makeTempConfig();
|
||||
let infoDataDir: string | undefined;
|
||||
|
||||
const mockLogger = createMemoryLogger();
|
||||
const origInfo = mockLogger.info.bind(mockLogger);
|
||||
mockLogger.info = (objOrMsg, msg?) => {
|
||||
if (typeof objOrMsg === "object" && "dataDir" in objOrMsg) {
|
||||
infoDataDir = objOrMsg["dataDir"] as string;
|
||||
}
|
||||
origInfo(objOrMsg, msg);
|
||||
};
|
||||
|
||||
const deps: BootstrapDependencies = {
|
||||
createLogger: async () => mockLogger,
|
||||
loadConfig: async () => cfg,
|
||||
startServer: () => ({}),
|
||||
};
|
||||
|
||||
await bootstrap({ configPath: join(cfg.configDir, "config.yaml"), mode: "development" }, deps);
|
||||
|
||||
expect(infoDataDir).toBe(cfg.dataDir);
|
||||
});
|
||||
|
||||
test("shutdown 时 flush logger", async () => {
|
||||
let flushed = false;
|
||||
let shutdownHandler: (() => void) | undefined;
|
||||
|
||||
const mockLogger = createMemoryLogger();
|
||||
mockLogger.flush = () => {
|
||||
flushed = true;
|
||||
};
|
||||
|
||||
const cfg = makeTempConfig();
|
||||
const deps: BootstrapDependencies = {
|
||||
createLogger: async () => mockLogger,
|
||||
loadConfig: async () => cfg,
|
||||
onSignal: (_signal, handler) => {
|
||||
shutdownHandler = handler;
|
||||
},
|
||||
startServer: () => ({}),
|
||||
};
|
||||
|
||||
await bootstrap({ configPath: join(cfg.configDir, "config.yaml"), mode: "production" }, deps);
|
||||
|
||||
expect(shutdownHandler).toBeDefined();
|
||||
shutdownHandler!();
|
||||
expect(flushed).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user