Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions dsh-mneme/test/api-hardening.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { createStore } from "../src/store.js";
import { createService } from "../src/service.js";
import { createApi } from "../src/api.js";
import { createSettings } from "../src/settings.js";
import { FAKE_API_TOKEN } from "./helpers/fake-auth-values.js";

class FakeRes extends EventEmitter {
constructor() { super(); this.statusCode = 200; this.body = ""; }
Expand Down Expand Up @@ -51,7 +52,7 @@ function handler(routes, path) {
test("/export 在配置 apiToken 后必须带 Bearer", async () => {
// 审计 P0:这条路由长期没有鉴权分支,即使配了 token 也把全库(含
// archived/forgotten 全字段)交给任何能打到端口的人。
const { routes, service } = setup("secret-token");
const { routes, service } = setup(FAKE_API_TOKEN);
service.saveWithDedupe({ type: "preference", title: "甲", content: "内容甲" });
const route = handler(routes, "/api/dsh-mneme/export");

Expand All @@ -62,7 +63,7 @@ test("/export 在配置 apiToken 后必须带 Bearer", async () => {

res = new FakeRes();
const ok = req("/api/dsh-mneme/export?format=json");
ok.headers = { authorization: "Bearer secret-token" };
ok.headers = { authorization: `Bearer ${FAKE_API_TOKEN}` };
await route.handler(ok, res);
assert.equal(res.statusCode, 200, "带对 token → 200");
assert.equal(JSON.parse(res.body).count, 1);
Expand All @@ -71,7 +72,7 @@ test("/export 在配置 apiToken 后必须带 Bearer", async () => {
test("/external-api 的 GET 与 PUT 同门,未鉴权不铸造 token", async () => {
// 两个回归一起锁:①未鉴权 GET 能把 8790 的 Bearer 明文读走;②那次 GET 还会
// 顺手 randomBytes 铸造并持久化一个 token——没有凭证的写路径。
const { routes, settings } = setup("secret-token");
const { routes, settings } = setup(FAKE_API_TOKEN);
const route = handler(routes, "/api/dsh-mneme/external-api");

const res = new FakeRes();
Expand All @@ -80,7 +81,7 @@ test("/external-api 的 GET 与 PUT 同门,未鉴权不铸造 token", async ()
assert.equal(settings.getExternalApi()?.token, undefined, "未鉴权请求不得铸造 token");

const ok = req("/api/dsh-mneme/external-api");
ok.headers = { authorization: "Bearer secret-token" };
ok.headers = { authorization: `Bearer ${FAKE_API_TOKEN}` };
const res2 = new FakeRes();
await route.handler(ok, res2);
assert.equal(res2.statusCode, 200);
Expand Down Expand Up @@ -139,13 +140,13 @@ test("超限后又来 transport error,仍回 413 且不写库", async () => {
// readBody 的超限状态必须活过 error 分支:旧实现 error 时 resolve(""),
// 调用点只认 null,于是拿着空串继续走 → PUT /persona 把 "" 解析成 {},
// 覆盖既有设置。这条锁的就是「413 发出去了,但后面的写入还在跑」。
const { routes, settings } = setup("secret-token");
const { routes, settings } = setup(FAKE_API_TOKEN);
settings.setRules(["别丢"]);
const huge = Buffer.from("x".repeat(5 * 1024 * 1024));
const r = new EventEmitter();
r.url = "/api/dsh-mneme/rules";
r.method = "PUT";
r.headers = { authorization: "Bearer secret-token" };
r.headers = { authorization: `Bearer ${FAKE_API_TOKEN}` };
const res = new FakeRes();
const done = handler(routes, "/api/dsh-mneme/rules").handler(r, res);
process.nextTick(() => { r.emit("data", huge); r.emit("error", new Error("boom")); });
Expand All @@ -157,14 +158,14 @@ test("超限后又来 transport error,仍回 413 且不写库", async () => {
test("超大请求体不会清空画像/规则", async () => {
// 这是最要命的一条:readBody 旧实现超限回空串 → parseBody 得到 {} →
// setProfile("") / setRules([]) 把既有内容静默清掉。413 必须在碰 store 之前。
const { routes, settings } = setup("secret-token");
const { routes, settings } = setup(FAKE_API_TOKEN);
settings.setProfile("原有画像");
settings.setRules(["原有规则"]);

for (const [path, method] of [["/api/dsh-mneme/profile", "PUT"], ["/api/dsh-mneme/rules", "PUT"]]) {
const res = new FakeRes();
const req = oversizedPost(path, method);
req.headers = { authorization: "Bearer secret-token" };
req.headers = { authorization: `Bearer ${FAKE_API_TOKEN}` };
await handler(routes, path).handler(req, res);
assert.equal(res.statusCode, 413, path + " 超限应回 413");
}
Expand Down
9 changes: 5 additions & 4 deletions dsh-mneme/test/api-nudge-route.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { createStore } from "../src/store.js";
import { createService } from "../src/service.js";
import { createApi } from "../src/api.js";
import { createSettings } from "../src/settings.js";
import { FAKE_API_TOKEN } from "./helpers/fake-auth-values.js";

// /api/dsh-mneme/nudge 的接线锁。三件事分开守,因为它们混在一起时各自的失败长得一样:
// 1. policy 是 token-write —— GET 免 token(面板每刷一次就 GET 一次),POST 要 token(会写盘);
Expand Down Expand Up @@ -87,7 +88,7 @@ function setup({ apiToken = "", memoryDir = null } = {}) {
}

test("/nudge:policy 是 token-write —— GET 免 token,POST 要 token", async () => {
const { route } = setup({ apiToken: "secret-token" });
const { route } = setup({ apiToken: FAKE_API_TOKEN });

// GET 是预览(只读),与 /pipeline、/candidates 同级:不带 token 也要能读。
// 面板每次展开 M.O. 都会 GET 一次,要 token 等于让预览永远 401。
Expand All @@ -105,7 +106,7 @@ test("/nudge:policy 是 token-write —— GET 免 token,POST 要 token", as
// 带 token 才放行到业务判断(这里 project 是相对路径,应当由业务层回 400)。
const ok = new FakeRes();
const okDone = awaitEnd(ok);
route.handler(req("/api/dsh-mneme/nudge?project=relative", "POST", {}, { authorization: "Bearer secret-token" }), ok);
route.handler(req("/api/dsh-mneme/nudge?project=relative", "POST", {}, { authorization: `Bearer ${FAKE_API_TOKEN}` }), ok);
await okDone;
assert.equal(ok.statusCode, 400);
});
Expand Down Expand Up @@ -238,11 +239,11 @@ test("/nudge:PATCH/DELETE 一律 405,绝不落进「不是 GET/HEAD 就算
const prev = process.env.DSH_HOME;
process.env.DSH_HOME = dshHome;
try {
const { route } = setup({ apiToken: "secret-token", memoryDir });
const { route } = setup({ apiToken: FAKE_API_TOKEN, memoryDir });
for (const verb of ["PATCH", "DELETE"]) {
const res = new FakeRes();
// 带合法 token:证明拦住它的是方法白名单,不是鉴权。
await route.handler(req("/api/dsh-mneme/nudge?project=" + encodeURIComponent(projectRoot), verb, {}, { authorization: "Bearer secret-token" }), res);
await route.handler(req("/api/dsh-mneme/nudge?project=" + encodeURIComponent(projectRoot), verb, {}, { authorization: `Bearer ${FAKE_API_TOKEN}` }), res);
assert.equal(res.statusCode, 405, verb + " 必须回 405");
const doc = JSON.parse(res.body);
assert.equal(doc.error, "method-not-allowed");
Expand Down
59 changes: 30 additions & 29 deletions dsh-mneme/test/api-routes-gaps.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { createService } from "../src/service.js";
import { createApi } from "../src/api.js";
import { createSettings } from "../src/settings.js";
import { createMaintenance } from "../src/maintenance.js";
import { FAKE_API_TOKEN } from "./helpers/fake-auth-values.js";

const root = join(dirname(fileURLToPath(import.meta.url)), "..");

Expand Down Expand Up @@ -58,7 +59,7 @@ function handler(routes, path) {
// /api/dsh-mneme/delete
// ---------------------------------------------------------------------------
test("delete is auth-gated and rejects non-POST", async () => {
const { routes } = setup("secret-token");
const { routes } = setup(FAKE_API_TOKEN);
const route = handler(routes, "/api/dsh-mneme/delete");

// 未带 token → 401
Expand All @@ -70,27 +71,27 @@ test("delete is auth-gated and rejects non-POST", async () => {
// 带 token 但 GET → 404
res = new FakeRes();
const ok = req("/api/dsh-mneme/delete", "GET");
ok.headers = { authorization: "Bearer secret-token" };
ok.headers = { authorization: `Bearer ${FAKE_API_TOKEN}` };
await route.handler(ok, res);
assert.equal(res.statusCode, 404);
});

test("delete validates id and reports missing/unknown 404", async () => {
const { routes, service } = setup("secret-token");
const { routes, service } = setup(FAKE_API_TOKEN);
const route = handler(routes, "/api/dsh-mneme/delete");

// 空 id → 400
let res = new FakeRes();
let ok = req("/api/dsh-mneme/delete", "POST", { id: " " });
ok.headers = { authorization: "Bearer secret-token" };
ok.headers = { authorization: `Bearer ${FAKE_API_TOKEN}` };
await route.handler(ok, res);
assert.equal(res.statusCode, 400);
assert.equal(JSON.parse(res.body).error, "missing-id");

// 不存在的 id → 404(store.remove 静默,靠前置 getById 区分)
res = new FakeRes();
ok = req("/api/dsh-mneme/delete", "POST", { id: "no-such-memory" });
ok.headers = { authorization: "Bearer secret-token" };
ok.headers = { authorization: `Bearer ${FAKE_API_TOKEN}` };
await route.handler(ok, res);
assert.equal(res.statusCode, 404);
assert.equal(JSON.parse(res.body).error, "not-found");
Expand All @@ -100,15 +101,15 @@ test("delete validates id and reports missing/unknown 404", async () => {
});

test("delete removes an existing memory", async () => {
const { routes, service } = setup("secret-token");
const { routes, service } = setup(FAKE_API_TOKEN);
const route = handler(routes, "/api/dsh-mneme/delete");

const { memory } = service.saveWithDedupe({ type: "project", title: "要删的", content: "删掉我" });
assert.ok(service.getById(memory.id));

const res = new FakeRes();
const ok = req("/api/dsh-mneme/delete", "POST", { id: memory.id });
ok.headers = { authorization: "Bearer secret-token" };
ok.headers = { authorization: `Bearer ${FAKE_API_TOKEN}` };
await route.handler(ok, res);
assert.equal(res.statusCode, 200);
assert.deepEqual(JSON.parse(res.body), { ok: true });
Expand All @@ -129,7 +130,7 @@ test("entities directory is read-only and returns empty list", async () => {
});

test("entities lists seeded entities without auth", async () => {
const { routes, service } = setup("secret-token"); // 有 token 但读路由不校验
const { routes, service } = setup(FAKE_API_TOKEN); // 有 token 但读路由不校验
service.createEntity({ name: "SQLite", type: "technology" });
service.createEntity({ name: "记忆", type: "concept" });

Expand Down Expand Up @@ -230,7 +231,7 @@ test("external-api GET materializes and persists a token", async () => {
});

test("external-api PUT requires auth and validates port/host", async () => {
const { routes } = setup("secret-token");
const { routes } = setup(FAKE_API_TOKEN);
const route = handler(routes, "/api/dsh-mneme/external-api");

// 未带 token → 401
Expand All @@ -241,27 +242,27 @@ test("external-api PUT requires auth and validates port/host", async () => {
// 端口越界 → 400
res = new FakeRes();
let ok = req("/api/dsh-mneme/external-api", "PUT", { port: 70000 });
ok.headers = { authorization: "Bearer secret-token" };
ok.headers = { authorization: `Bearer ${FAKE_API_TOKEN}` };
await route.handler(ok, res);
assert.equal(res.statusCode, 400);
assert.equal(JSON.parse(res.body).error, "invalid-port");

// host 带协议 → 400
res = new FakeRes();
ok = req("/api/dsh-mneme/external-api", "PUT", { host: "https://example.com" });
ok.headers = { authorization: "Bearer secret-token" };
ok.headers = { authorization: `Bearer ${FAKE_API_TOKEN}` };
await route.handler(ok, res);
assert.equal(res.statusCode, 400);
assert.equal(JSON.parse(res.body).error, "invalid-host");
});

test("external-api PUT applies valid config and echoes it", async () => {
const { routes } = setup("secret-token");
const { routes } = setup(FAKE_API_TOKEN);
const route = handler(routes, "/api/dsh-mneme/external-api");

const res = new FakeRes();
const ok = req("/api/dsh-mneme/external-api", "PUT", { enabled: true, port: 8790, host: "127.0.0.1" });
ok.headers = { authorization: "Bearer secret-token" };
ok.headers = { authorization: `Bearer ${FAKE_API_TOKEN}` };
await route.handler(ok, res);
assert.equal(res.statusCode, 200);
const cfg = JSON.parse(res.body).config;
Expand Down Expand Up @@ -324,7 +325,7 @@ test("external-api GET with token=omit neither materializes nor returns a token"
// 路径、同一套去重键。历史缺口是面板只有看/改/删/导入,用户想手写一条必须开
// 终端或调外部 API——两条旁路都绕开了面板的去重提示。
test("create is auth-gated and rejects non-POST", async () => {
const { routes } = setup("secret-token");
const { routes } = setup(FAKE_API_TOKEN);
const route = handler(routes, "/api/dsh-mneme/create");
assert.ok(route, "route must be registered");

Expand All @@ -337,17 +338,17 @@ test("create is auth-gated and rejects non-POST", async () => {
// 带 token 但 GET → 404(不是 405:与既有写路由一致,避免探测路由存在性)
res = new FakeRes();
const ok = req("/api/dsh-mneme/create", "GET");
ok.headers = { authorization: "Bearer secret-token" };
ok.headers = { authorization: `Bearer ${FAKE_API_TOKEN}` };
await route.handler(ok, res);
assert.equal(res.statusCode, 404);
});

test("create validates type/title/content and refuses document rows", async () => {
const { routes, service } = setup("secret-token");
const { routes, service } = setup(FAKE_API_TOKEN);
const route = handler(routes, "/api/dsh-mneme/create");
const post = async (body) => {
const r = req("/api/dsh-mneme/create", "POST", body);
r.headers = { authorization: "Bearer secret-token" };
r.headers = { authorization: `Bearer ${FAKE_API_TOKEN}` };
const res = new FakeRes();
await route.handler(r, res);
return res;
Expand Down Expand Up @@ -384,7 +385,7 @@ test("create validates type/title/content and refuses document rows", async () =
});

test("create saves a memory and reports action=created with 201", async () => {
const { routes, service } = setup("secret-token");
const { routes, service } = setup(FAKE_API_TOKEN);
const route = handler(routes, "/api/dsh-mneme/create");

const r = req("/api/dsh-mneme/create", "POST", {
Expand All @@ -394,7 +395,7 @@ test("create saves a memory and reports action=created with 201", async () => {
importance: 4,
tags: ["panel"]
});
r.headers = { authorization: "Bearer secret-token" };
r.headers = { authorization: `Bearer ${FAKE_API_TOKEN}` };
const res = new FakeRes();
await route.handler(r, res);

Expand All @@ -411,12 +412,12 @@ test("create saves a memory and reports action=created with 201", async () => {
});

test("create merges into an existing row with the same (type, title) and says so", async () => {
const { routes, service } = setup("secret-token");
const { routes, service } = setup(FAKE_API_TOKEN);
const route = handler(routes, "/api/dsh-mneme/create");
service.saveWithDedupe({ type: "project", title: "同标题", content: "原来的正文" });

const r = req("/api/dsh-mneme/create", "POST", { type: "project", title: "同标题", content: "追加的正文" });
r.headers = { authorization: "Bearer secret-token" };
r.headers = { authorization: `Bearer ${FAKE_API_TOKEN}` };
const res = new FakeRes();
await route.handler(r, res);

Expand All @@ -434,13 +435,13 @@ test("create maps a denied write to 422 instead of leaking a 500", async () => {
// 恰好不带 _sessionKey,所以这条分支当前到不了。仍然要锁:一旦有人把会话身份接
// 进面板,落回 toApiList([null]) 会把「被拒」伪装成「服务器出错」——用户看到的
// 是「新建失败」,实际是准入拒绝,两者的处置完全不同。
const { routes, service } = setup("secret-token");
const { routes, service } = setup(FAKE_API_TOKEN);
const route = handler(routes, "/api/dsh-mneme/create");
const original = service.saveWithDedupe;
service.saveWithDedupe = () => ({ action: "denied", reason: "too-many-writes", memory: null });
try {
const r = req("/api/dsh-mneme/create", "POST", { type: "project", title: "t", content: "c" });
r.headers = { authorization: "Bearer secret-token" };
r.headers = { authorization: `Bearer ${FAKE_API_TOKEN}` };
const res = new FakeRes();
await route.handler(r, res);
assert.equal(res.statusCode, 422);
Expand All @@ -453,15 +454,15 @@ test("create maps a denied write to 422 instead of leaking a 500", async () => {
});

test("create returns 500 (not a leaked throw) when the store rejects the write", async () => {
const { routes, service } = setup("secret-token");
const { routes, service } = setup(FAKE_API_TOKEN);
const route = handler(routes, "/api/dsh-mneme/create");
// 绕过路由校验直达 saveWithDedupe 的守卫:document 行会 throw。这里用打桩
// 模拟「路由认为合法、存储层仍然拒绝」的那类情况(类型白名单漂移等)。
const original = service.saveWithDedupe;
service.saveWithDedupe = () => { throw new Error("invalid memory type: whatever"); };
try {
const r = req("/api/dsh-mneme/create", "POST", { type: "project", title: "t", content: "c" });
r.headers = { authorization: "Bearer secret-token" };
r.headers = { authorization: `Bearer ${FAKE_API_TOKEN}` };
const res = new FakeRes();
await route.handler(r, res);
assert.equal(res.statusCode, 500);
Expand All @@ -487,7 +488,7 @@ const RECLAIM_PATH = "/api/dsh-mneme/maintenance/reclaim";
* null(store.js:2134),而报告的口径就是「按 VACUUM 前后体积量」——用内存库测,
* 体积那几格永远测不到数字。
*/
function reclaimSetup(apiToken = "secret-token", { logger = null } = {}) {
function reclaimSetup(apiToken = FAKE_API_TOKEN, { logger = null } = {}) {
const dir = mkdtempSync(join(tmpdir(), "dsh-mneme-reclaim-api-"));
const store = createStore(join(dir, "memory.db"));
const service = createService({ store, mirror: null, config: {} });
Expand Down Expand Up @@ -565,7 +566,7 @@ function reqRaw(path, method, text, headers = {}) {
}

async function callReclaim(route, {
path = RECLAIM_PATH, method = "GET", body = null, raw = false, token = "secret-token"
path = RECLAIM_PATH, method = "GET", body = null, raw = false, token = FAKE_API_TOKEN
} = {}) {
const sendBody = body !== null || raw;
const r = sendBody
Expand Down Expand Up @@ -814,7 +815,7 @@ test("reclaim POST validates olderThanDays as a JSON number, not a string", asyn
test("reclaim without the maintenance dependency answers 503 instead of crashing", async () => {
// 既有 14 处 createApi(...) 调用全是位置传参、不传第 9 个。它们必须继续工作:
// 「没接线」要如实报 unavailable,而不是把 undefined.reclaim 落成崩溃。
const { routes } = setup("secret-token");
const { routes } = setup(FAKE_API_TOKEN);
const route = handler(routes, RECLAIM_PATH);
assert.ok(route, "路由注册与接线无关,少传依赖也要注册出来");

Expand Down Expand Up @@ -842,7 +843,7 @@ test("reclaim maps a throwing maintenance.reclaim to 500 instead of leaking", as
// 只剩一个「内部错误」,查不出是哪次回收炸的)。GET 那侧尤其要注意——外层 catch
// 也能兜成 500,所以只断言状态码抓不住内层 catch 的消失,必须看日志。
const warnings = [];
const bench = reclaimSetup("secret-token", { logger: { warn: (m) => warnings.push(String(m)) } });
const bench = reclaimSetup(FAKE_API_TOKEN, { logger: { warn: (m) => warnings.push(String(m)) } });
try {
const route = handler(bench.routes, RECLAIM_PATH);
const original = bench.maintenance.reclaim;
Expand Down
Loading
Loading