mirror of
https://github.com/sinanyuntu/trade-message-center.git
synced 2026-09-17 13:22:11 +08:00
395 lines
14 KiB
TypeScript
395 lines
14 KiB
TypeScript
// 验证服务端应用基础边界
|
|
|
|
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import { createApp } from "../src/app.ts";
|
|
import { createDatabase, type DatabaseConnection } from "../src/database/index.ts";
|
|
import { loadConfig } from "../src/config.ts";
|
|
import { createServerRuntime } from "../src/runtime.ts";
|
|
import type { OneTalkReadService } from "../src/onetalk/index.ts";
|
|
|
|
const testConfig = {
|
|
host: "127.0.0.1",
|
|
port: 3000,
|
|
databaseUrl: "postgres://test:test@localhost:5432/test",
|
|
environment: "non_development" as const,
|
|
chromeExtensionDownload: {
|
|
packageVersion: "1.2.3",
|
|
signing: {
|
|
bucket: "example-bucket",
|
|
endpoint: "https://oss.example.com",
|
|
accessKeyId: "test-access-key-id",
|
|
accessKeySecret: "test-only-secret-value",
|
|
},
|
|
},
|
|
};
|
|
|
|
const createDatabaseStub = (): {
|
|
connection: DatabaseConnection;
|
|
getCloseCount: () => number;
|
|
} => {
|
|
let closeCount = 0;
|
|
const connection: DatabaseConnection = {
|
|
db: {} as DatabaseConnection["db"],
|
|
close: async () => {
|
|
closeCount += 1;
|
|
},
|
|
};
|
|
|
|
return { connection, getCloseCount: () => closeCount };
|
|
};
|
|
|
|
test("rejects incomplete startup configuration without exposing values", () => {
|
|
assert.throws(() => loadConfig({ HOST: "127.0.0.1", PORT: "3000" }), /Missing DATABASE_URL/);
|
|
assert.throws(
|
|
() => loadConfig({ HOST: "127.0.0.1", PORT: "0", DATABASE_URL: "secret" }),
|
|
/Invalid PORT/,
|
|
);
|
|
});
|
|
|
|
test("composes the private summary app with one shared database close owner", async () => {
|
|
const database = createDatabaseStub();
|
|
const readService: OneTalkReadService = {
|
|
listConversations: async () => ({
|
|
status: "accepted",
|
|
conversations: [],
|
|
page: { hasMore: false, nextCursor: null },
|
|
}),
|
|
readConversation: async () => ({ status: "not_found" }),
|
|
readHistory: async () => ({
|
|
status: "accepted",
|
|
conversationId: "conversation-1",
|
|
messages: [],
|
|
page: { hasMore: false, nextCursor: null },
|
|
}),
|
|
};
|
|
const runtime = createServerRuntime(testConfig, {
|
|
database: database.connection,
|
|
readService,
|
|
});
|
|
|
|
try {
|
|
const response = await runtime.internalSummaryApp.inject({
|
|
method: "GET",
|
|
url: "/api/bright/onetalk/accounts/account-1/conversations/conversation-1/messages?fromSentAtMs=1&toSentAtMs=2",
|
|
});
|
|
assert.equal(response.statusCode, 200);
|
|
const body = response.json();
|
|
assert.deepEqual(body, {
|
|
conversationId: "conversation-1",
|
|
messages: [],
|
|
page: { hasMore: false, nextCursor: null },
|
|
});
|
|
assert.equal("scope" in body, false);
|
|
} finally {
|
|
await runtime.close();
|
|
}
|
|
assert.equal(database.getCloseCount(), 1);
|
|
});
|
|
|
|
test("requires strict production Mind authorization configuration", () => {
|
|
const base = {
|
|
HOST: "127.0.0.1",
|
|
PORT: "3000",
|
|
DATABASE_URL: "postgres://test:test@localhost:5432/test",
|
|
TMC_PACKAGE_VERSION: "1.2.3",
|
|
OSS_BUCKET: "example-bucket",
|
|
OSS_ENDPOINT: "https://oss.example.com",
|
|
OSS_ACCESS_KEY_ID: "test-access-key-id",
|
|
OSS_ACCESS_KEY_SECRET: "test-only-secret-value",
|
|
NODE_ENV: "production",
|
|
MIND_AUTH_BASE_URL: "https://mind.example.com",
|
|
MIND_PAGE_ORIGIN: "https://mind.example.com",
|
|
ONETALK_PLUGIN_ORIGINS: "chrome-extension://extension-id",
|
|
};
|
|
assert.equal(loadConfig(base).mindAuthorization?.timeoutMs, 3000);
|
|
assert.equal(loadConfig(base).chromeExtensionDownload?.packageVersion, "1.2.3");
|
|
assert.throws(
|
|
() => loadConfig({ ...base, TMC_PACKAGE_VERSION: "1.2" }),
|
|
/Invalid Chrome extension version/,
|
|
);
|
|
assert.equal(
|
|
loadConfig({
|
|
...base,
|
|
OSS_ACCESS_KEY_ID: undefined,
|
|
OSS_ACCESS_KEY_SECRET: undefined,
|
|
}).chromeExtensionDownload,
|
|
undefined,
|
|
);
|
|
assert.throws(
|
|
() => loadConfig({ ...base, ONETALK_PLUGIN_ORIGINS: "chrome-extension://" }),
|
|
/Invalid ONETALK_PLUGIN_ORIGINS/,
|
|
);
|
|
assert.throws(
|
|
() => loadConfig({ ...base, ONETALK_PLUGIN_ORIGINS: "chrome-extension://id/path" }),
|
|
/Invalid ONETALK_PLUGIN_ORIGINS/,
|
|
);
|
|
assert.throws(
|
|
() => loadConfig({ ...base, MIND_AUTH_BASE_URL: "http://mind.example.com" }),
|
|
/Invalid MIND_AUTH_BASE_URL/,
|
|
);
|
|
});
|
|
|
|
test("allows loopback HTTP Mind authorization only in development and test", () => {
|
|
const base = {
|
|
HOST: "127.0.0.1",
|
|
PORT: "3000",
|
|
DATABASE_URL: "postgres://test:test@localhost:5432/test",
|
|
TMC_PACKAGE_VERSION: "1.2.3",
|
|
OSS_BUCKET: "example-bucket",
|
|
OSS_ENDPOINT: "https://oss.example.com",
|
|
OSS_ACCESS_KEY_ID: "test-access-key-id",
|
|
OSS_ACCESS_KEY_SECRET: "test-only-secret-value",
|
|
MIND_AUTH_BASE_URL: "http://127.0.0.1:8787",
|
|
MIND_PAGE_ORIGIN: "http://127.0.0.1:3000",
|
|
ONETALK_PLUGIN_ORIGINS: "chrome-extension://extension-id",
|
|
};
|
|
for (const nodeEnvironment of ["development", "test"]) {
|
|
const config = loadConfig({ ...base, NODE_ENV: nodeEnvironment });
|
|
assert.equal(
|
|
config.environment,
|
|
nodeEnvironment === "development" ? "development" : "non_development",
|
|
);
|
|
assert.deepEqual(config.mindAuthorization, {
|
|
baseUrl: "http://127.0.0.1:8787",
|
|
mindPageOrigin: "http://127.0.0.1:3000",
|
|
pluginOrigins: ["chrome-extension://extension-id"],
|
|
timeoutMs: 3000,
|
|
});
|
|
}
|
|
for (const nodeEnvironment of ["production", "staging", undefined]) {
|
|
assert.throws(
|
|
() =>
|
|
loadConfig({ ...base, ...(nodeEnvironment ? { NODE_ENV: nodeEnvironment } : {}) }),
|
|
/Invalid MIND_AUTH_BASE_URL/,
|
|
);
|
|
}
|
|
assert.throws(
|
|
() =>
|
|
loadConfig({
|
|
...base,
|
|
NODE_ENV: "test",
|
|
MIND_AUTH_BASE_URL: "http://mind.example.com",
|
|
}),
|
|
/Invalid MIND_AUTH_BASE_URL/,
|
|
);
|
|
});
|
|
|
|
test("normalizes only NODE_ENV=development as the development environment", () => {
|
|
const requiredEnvironment = {
|
|
HOST: "127.0.0.1",
|
|
PORT: "3000",
|
|
DATABASE_URL: "postgres://test:test@localhost:5432/test",
|
|
TMC_PACKAGE_VERSION: "1.2.3",
|
|
OSS_BUCKET: "example-bucket",
|
|
OSS_ENDPOINT: "https://oss.example.com",
|
|
OSS_ACCESS_KEY_ID: "test-access-key-id",
|
|
OSS_ACCESS_KEY_SECRET: "test-only-secret-value",
|
|
MIND_AUTH_BASE_URL: "https://mind.example.com",
|
|
MIND_PAGE_ORIGIN: "https://mind.example.com",
|
|
ONETALK_PLUGIN_ORIGINS: "chrome-extension://extension-id",
|
|
};
|
|
|
|
assert.equal(
|
|
loadConfig({ ...requiredEnvironment, NODE_ENV: "development" }).environment,
|
|
"development",
|
|
);
|
|
assert.equal(
|
|
loadConfig({ ...requiredEnvironment, NODE_ENV: " development " }).environment,
|
|
"development",
|
|
);
|
|
assert.equal(
|
|
loadConfig({
|
|
...requiredEnvironment,
|
|
NODE_ENV: "development",
|
|
MIND_AUTH_BASE_URL: "http://127.0.0.1:8787",
|
|
}).mindAuthorization?.baseUrl,
|
|
"http://127.0.0.1:8787",
|
|
);
|
|
|
|
for (const value of [
|
|
undefined,
|
|
"",
|
|
" ",
|
|
"test",
|
|
"staging",
|
|
"production",
|
|
"Development",
|
|
"DEVELOPMENT",
|
|
"developmnt",
|
|
]) {
|
|
assert.equal(
|
|
loadConfig({ ...requiredEnvironment, NODE_ENV: value }).environment,
|
|
"non_development",
|
|
);
|
|
}
|
|
});
|
|
|
|
test("does not read the removed development authorization fixture fields", () => {
|
|
const config = loadConfig({
|
|
HOST: "127.0.0.1",
|
|
PORT: "3000",
|
|
DATABASE_URL: "postgres://test:test@localhost:5432/test",
|
|
TMC_PACKAGE_VERSION: "1.2.3",
|
|
OSS_BUCKET: "example-bucket",
|
|
OSS_ENDPOINT: "https://oss.example.com",
|
|
OSS_ACCESS_KEY_ID: "test-access-key-id",
|
|
OSS_ACCESS_KEY_SECRET: "test-only-secret-value",
|
|
NODE_ENV: "development",
|
|
MIND_AUTH_BASE_URL: "http://127.0.0.1:8787",
|
|
MIND_PAGE_ORIGIN: "http://127.0.0.1:3000",
|
|
ONETALK_PLUGIN_ORIGINS: "chrome-extension://extension-id",
|
|
ONETALK_DEV_MIND_USER_ID: "ignored",
|
|
ONETALK_DEV_WORKSPACE_ID: "ignored",
|
|
ONETALK_DEV_CHANNEL_ACCOUNT_ID: "ignored",
|
|
ONETALK_DEV_BINDING: "ignored",
|
|
ONETALK_DEV_AUTHORIZATION_VERSION: "ignored",
|
|
ONETALK_DEV_PERMISSIONS: "invalid",
|
|
});
|
|
assert.equal(config.mindAuthorization?.baseUrl, "http://127.0.0.1:8787");
|
|
});
|
|
|
|
test("rejects an empty database URL at the database boundary", () => {
|
|
assert.throws(() => createDatabase(""), /Missing DATABASE_URL/);
|
|
});
|
|
|
|
test("serves health and closes injected database resources", async () => {
|
|
const database = createDatabaseStub();
|
|
const app = createApp(testConfig, { database: database.connection });
|
|
|
|
await app.ready();
|
|
const response = await app.inject({ method: "GET", url: "/health" });
|
|
|
|
assert.equal(response.statusCode, 200);
|
|
assert.deepEqual(response.json(), { status: "ok" });
|
|
|
|
await app.close();
|
|
assert.equal(database.getCloseCount(), 1);
|
|
});
|
|
|
|
test("returns the configured whole-version Chrome extension download URL", async () => {
|
|
const database = createDatabaseStub();
|
|
const app = createApp(testConfig, { database: database.connection });
|
|
|
|
try {
|
|
await app.ready();
|
|
const response = await app.inject({
|
|
method: "GET",
|
|
url: "/api/downloads/chrome-extension",
|
|
});
|
|
const body = response.json() as { version: string; downloadUrl: string };
|
|
const downloadUrl = new URL(body.downloadUrl);
|
|
|
|
assert.equal(response.statusCode, 200);
|
|
assert.equal(body.version, "1.2.3");
|
|
assert.equal(
|
|
downloadUrl.pathname,
|
|
"/chrome-extension/1.2.3/trade-message-center-chrome-extension-1.2.3.zip",
|
|
);
|
|
assert.equal(downloadUrl.searchParams.get("OSSAccessKeyId"), "test-access-key-id");
|
|
assert.equal(body.downloadUrl.includes("test-only-secret-value"), false);
|
|
} finally {
|
|
await app.close();
|
|
}
|
|
});
|
|
|
|
test("returns a stable error when download signing is not configured", async () => {
|
|
const database = createDatabaseStub();
|
|
const app = createApp(
|
|
{ ...testConfig, chromeExtensionDownload: undefined },
|
|
{ database: database.connection },
|
|
);
|
|
|
|
try {
|
|
await app.ready();
|
|
const response = await app.inject({
|
|
method: "GET",
|
|
url: "/api/downloads/chrome-extension",
|
|
});
|
|
|
|
assert.equal(response.statusCode, 503);
|
|
assert.deepEqual(response.json(), {
|
|
error: { code: "extension_download_unavailable" },
|
|
});
|
|
} finally {
|
|
await app.close();
|
|
}
|
|
});
|
|
|
|
test("registers the websocket server without crashing the app", async () => {
|
|
const database = createDatabaseStub();
|
|
const app = createApp(testConfig, { database: database.connection });
|
|
|
|
await app.ready();
|
|
assert.ok(app.websocketServer);
|
|
assert.equal(app.hasRoute({ method: "GET", url: "/ws" }), true);
|
|
assert.equal(app.hasRoute({ method: "GET", url: "/ws/plugin" }), true);
|
|
assert.equal(app.hasRoute({ method: "GET", url: "/ws/mind" }), true);
|
|
await app.close();
|
|
});
|
|
|
|
test("does not print raw Mind authorization payloads from the development app", async () => {
|
|
const database = createDatabaseStub();
|
|
const logs: unknown[][] = [];
|
|
const originalInfo = console.info;
|
|
const originalFetch = globalThis.fetch;
|
|
console.info = (...args: unknown[]) => logs.push(args);
|
|
globalThis.fetch = async () =>
|
|
new Response(
|
|
JSON.stringify({
|
|
binding: "binding-1",
|
|
authorizationVersion: "version-1",
|
|
permissions: ["read"],
|
|
mindScope: {
|
|
mindUserId: "mind-user-1",
|
|
workspaceId: "workspace-1",
|
|
channelAccountId: "account-1",
|
|
},
|
|
}),
|
|
{ status: 200 },
|
|
);
|
|
const app = createApp(
|
|
{
|
|
...testConfig,
|
|
environment: "development",
|
|
mindAuthorization: {
|
|
baseUrl: "https://mind.example.com",
|
|
mindPageOrigin: "http://mind.localhost",
|
|
pluginOrigins: ["http://plugin.localhost"],
|
|
timeoutMs: 100,
|
|
},
|
|
},
|
|
{
|
|
database: database.connection,
|
|
readService: {
|
|
listConversations: async () => ({
|
|
status: "accepted",
|
|
conversations: [],
|
|
page: { hasMore: false, nextCursor: null },
|
|
}),
|
|
readConversation: async () => ({ status: "not_found" }),
|
|
readHistory: async () => ({ status: "not_found" }),
|
|
},
|
|
},
|
|
);
|
|
|
|
try {
|
|
const response = await app.inject({
|
|
method: "GET",
|
|
url: "/api/bright/onetalk/accounts/account-1/conversations",
|
|
headers: {
|
|
origin: "http://mind.localhost",
|
|
cookie: "mind_session=opaque",
|
|
},
|
|
});
|
|
assert.equal(response.statusCode, 200);
|
|
assert.equal(JSON.stringify(logs).includes("binding-1"), false);
|
|
assert.equal(JSON.stringify(logs).includes("mind-user-1"), false);
|
|
} finally {
|
|
await app.close();
|
|
console.info = originalInfo;
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|