Files
trade-message-center/apps/server/test/app.test.ts
T

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;
}
});