This commit is contained in:
commit
5ec99ce227
668 files changed
+164448
No files matched your search
@@ -0,0 +1,129 @@
|
||||
// 插件/客户端接入 SDK(`sdk/im-plugin-client`)的契约测试。
|
||||
//
|
||||
// 盯三件事(都是"插件侧会踩、且踩了很难查"的):
|
||||
// ① 凭据头与插件身份头的**注入**(`x-dsh-im-instance-token` / `x-dsh-im-plugin-id`);
|
||||
// ② 失败一律折成 `{ok:false,reason,detail}` —— 插件侧只需判 `ok`,⛔ 不必维护状态码表;
|
||||
// ③ 未配置时**立刻**回 `not-registered`,⛔ 不发请求、⛔ 不抛异常。
|
||||
//
|
||||
// 跑法:`npm test`。
|
||||
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { createServer } from 'node:http'
|
||||
|
||||
import { SDK_VERSION, createImClient } from '../sdk/im-plugin-client/index.mjs'
|
||||
|
||||
/** 起一个假平台:记下请求,按剧本回。 */
|
||||
async function withServer(handler, fn) {
|
||||
const seen = []
|
||||
const server = createServer((req, res) => {
|
||||
let body = ''
|
||||
req.on('data', (c) => (body += c))
|
||||
req.on('end', () => {
|
||||
const record = { method: req.method, url: req.url, headers: req.headers, body }
|
||||
seen.push(record)
|
||||
const out = handler(record)
|
||||
res.writeHead(out.status ?? 200, { 'content-type': 'application/json' })
|
||||
res.end(out.body === undefined ? '' : JSON.stringify(out.body))
|
||||
})
|
||||
})
|
||||
await new Promise((r) => server.listen(0, '127.0.0.1', r))
|
||||
const baseUrl = `http://127.0.0.1:${server.address().port}`
|
||||
try {
|
||||
return await fn(baseUrl, seen)
|
||||
} finally {
|
||||
await new Promise((r) => server.close(r))
|
||||
}
|
||||
}
|
||||
|
||||
test('im-plugin-client: 注入了实例凭据头与插件身份头', async () => {
|
||||
await withServer(
|
||||
() => ({ body: { ok: true, value: [{ note_id: 'n1' }] } }),
|
||||
async (baseUrl, seen) => {
|
||||
const im = createImClient({ baseUrl: baseUrl + '/', token: 'tok-1', pluginId: '@dsh-local/trpg-kit' })
|
||||
const r = await im.data.table('my_notes').find({ eq: { body: 'x' }, limit: 5, afterCursor: 'n0', order: 'desc' })
|
||||
assert.equal(r.ok, true)
|
||||
assert.deepEqual(r.value, [{ note_id: 'n1' }])
|
||||
const req = seen[0]
|
||||
assert.equal(req.method, 'GET')
|
||||
assert.equal(req.headers['x-dsh-im-instance-token'], 'tok-1')
|
||||
assert.equal(req.headers['x-dsh-im-plugin-id'], '@dsh-local/trpg-kit')
|
||||
assert.equal(req.url, '/api/im/data/my_notes?eq.body=x&after=n0&order=desc&limit=5')
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
test('im-plugin-client: 写与改与删走对方法与路径(表名不带 p_ 前缀)', async () => {
|
||||
await withServer(
|
||||
() => ({ body: { ok: true, value: {} } }),
|
||||
async (baseUrl, seen) => {
|
||||
const im = createImClient({ baseUrl, token: 't', pluginId: 'p' })
|
||||
const t = im.data.table('character_sheet')
|
||||
await t.insert({ pc_name: 'a', room_id: 'r1' })
|
||||
await t.update('a', { sheet: { hp: 3 } })
|
||||
await t.remove('a')
|
||||
await t.count({ eq: { room_id: 'r1' } })
|
||||
assert.deepEqual(seen.map((s) => `${s.method} ${s.url}`), [
|
||||
'POST /api/im/data/character_sheet',
|
||||
'PATCH /api/im/data/character_sheet/a',
|
||||
'DELETE /api/im/data/character_sheet/a',
|
||||
'GET /api/im/data/character_sheet/count?eq.room_id=r1',
|
||||
])
|
||||
assert.deepEqual(JSON.parse(seen[0].body), { pc_name: 'a', room_id: 'r1' })
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
test('im-plugin-client: 契约失败原样透传(200 + ok:false)', async () => {
|
||||
await withServer(
|
||||
() => ({ body: { ok: false, reason: 'quota-exceeded', detail: '超过单插件行数上限 200000' } }),
|
||||
async (baseUrl) => {
|
||||
const im = createImClient({ baseUrl, token: 't', pluginId: 'p' })
|
||||
const r = await im.data.table('my_notes').insert({ note_id: 'n' })
|
||||
assert.equal(r.ok, false)
|
||||
assert.equal(r.reason, 'quota-exceeded')
|
||||
assert.match(r.detail, /行数上限/)
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
test('im-plugin-client: 非 200(身份 / 就绪)也折成同一个 ok:false 形状', async () => {
|
||||
await withServer(
|
||||
(req) => (req.url.includes('status')
|
||||
? { status: 409, body: { error: 'plugin-data-unavailable', reason: 'datastore-not-ready', detail: '尚未建库' } }
|
||||
: { status: 401, body: { error: 'instance-token-rejected', reason: 'unknown' } }),
|
||||
async (baseUrl) => {
|
||||
const im = createImClient({ baseUrl, token: 'bad', pluginId: 'p' })
|
||||
const s = await im.data.status()
|
||||
assert.equal(s.ok, false)
|
||||
assert.equal(s.reason, 'disabled')
|
||||
assert.equal(s.httpStatus, 409)
|
||||
assert.match(s.detail, /尚未建库/)
|
||||
|
||||
const r = await im.rooms.list()
|
||||
assert.equal(r.reason, 'not-registered', '401 ⇒ not-registered(凭据不认)')
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
test('im-plugin-client: 未配置时立刻回 not-registered —— ⛔ 不发请求、⛔ 不抛', async () => {
|
||||
await withServer(
|
||||
() => {
|
||||
throw new Error('不应该发出请求')
|
||||
},
|
||||
async (baseUrl, seen) => {
|
||||
const im = createImClient({ baseUrl: '', token: 'super-secret-1', pluginId: 'p' })
|
||||
assert.equal(im.configured, false)
|
||||
const r = await im.data.table('my_notes').find()
|
||||
assert.equal(r.ok, false)
|
||||
assert.equal(r.reason, 'not-registered')
|
||||
assert.equal(seen.length, 0)
|
||||
assert.ok(!String(im).includes('super-secret-1'), '诊断串⛔ 不含 token 明文')
|
||||
assert.match(String(im), /token=已注入/, '只回"有没有配"')
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
test('im-plugin-client: 版本号可读(插件报障要带它)', () => {
|
||||
assert.match(SDK_VERSION, /^\d+\.\d+\.\d+$/)
|
||||
})
|
||||
Reference in new issue
Block a user