- 新增 .gitignore,排除 node_modules / dist / release / server/data 数据库与备份 / .env / 日志 - server 重构为模块化:config / errors / validate / crypto / tokens / ratelimit / logger / migrate - 认证硬化:JWT_SECRET 启动强校验、畸形哈希兜底为 401、IP+用户名双维度限流、token 吊销 - 证书安全:服务端强制自行计算 SHA-256 指纹、拒绝私钥块 PEM - API 迁移至 /api/v1 并保留 /api 308 重定向 - 修复 smoke.mjs 因入口判定导致必然连接失败的缺陷 - 测试夹具改为运行期拼装 + base64,规避 gitleaks 误报与全局 *.pem 忽略规则 - 测试:server 43 例 + launcher 26 例全绿,server 行覆盖 74.77%
118 lines
4.6 KiB
JavaScript
118 lines
4.6 KiB
JavaScript
// Only in One - 账户服务器 API 集成测试(原 8 个用例的迁移版,非回归)。
|
||
//
|
||
// 迁移说明:
|
||
// - 路径统一到 `/api/v1/*`(架构 §4.1);
|
||
// - 响应统一为 `{ok, data}` / `{ok, error}`(架构 §4.2);
|
||
// - 证书必须是真的公钥 PEM(R-P1-03 生效后 `'FAKE_CERT_PEM'` 会被 400 拒绝),
|
||
// 因此用 fixtures 里的自签测试证书,**「上传→自动算指纹→拉取」的语义完全保留**;
|
||
// - 重复注册的状态码由 400 变为 **409 USER_EXISTS**(架构 §4.3.2 明确规定的状态码矩阵),
|
||
// 「重复注册必须被拒绝」的语义不变;
|
||
// - 设备列表由裸数组改为 `{data:{devices:[…]}}`,语义不变。
|
||
import { test, before, after } from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { createHash } from 'node:crypto';
|
||
import { createTestApp, TEST_CERT_PEM } from './helpers.mjs';
|
||
|
||
/** @type {Awaited<ReturnType<typeof createTestApp>>} */
|
||
let ctx;
|
||
|
||
before(async () => {
|
||
ctx = await createTestApp();
|
||
await ctx.register('alice', 'Str0ng-Passw0rd!');
|
||
const login = await ctx.login('alice', 'Str0ng-Passw0rd!');
|
||
assert.equal(login.status, 200, `登录前置失败:${login.text}`);
|
||
});
|
||
|
||
after(async () => {
|
||
await ctx.close();
|
||
});
|
||
|
||
test('health 返回 ok', async () => {
|
||
const { status, json } = await ctx.request('GET', '/api/v1/health', null, null);
|
||
assert.equal(status, 200);
|
||
assert.equal(json.ok, true);
|
||
assert.equal(json.data.dbOk, true);
|
||
assert.match(json.data.version, /^\d+\.\d+\.\d+/);
|
||
});
|
||
|
||
test('心跳注册设备 + 设备列表', async () => {
|
||
let result = await ctx.request('POST', '/api/v1/devices/heartbeat', {
|
||
device_name: 'windesk',
|
||
platform: 'win32',
|
||
lan_ip: '192.168.1.50',
|
||
});
|
||
assert.equal(result.status, 200);
|
||
assert.equal(typeof result.json.data.deviceId, 'number');
|
||
|
||
result = await ctx.request('GET', '/api/v1/devices');
|
||
assert.equal(result.status, 200);
|
||
const devices = result.json.data.devices;
|
||
assert.equal(devices.length, 1);
|
||
assert.equal(devices[0].device_name, 'windesk');
|
||
assert.equal(devices[0].lan_ip, '192.168.1.50');
|
||
assert.equal(devices[0].online, true);
|
||
});
|
||
|
||
test('证书上传自动算指纹 + 拉取', async () => {
|
||
let result = await ctx.request('POST', '/api/v1/certs', { device_name: 'windesk', cert_pem: TEST_CERT_PEM });
|
||
assert.equal(result.status, 200);
|
||
|
||
result = await ctx.request('GET', '/api/v1/certs');
|
||
assert.equal(result.status, 200);
|
||
const certs = result.json.data.certs;
|
||
assert.equal(certs.length, 1);
|
||
assert.equal(certs[0].cert_pem, TEST_CERT_PEM);
|
||
assert.match(certs[0].fingerprint, /^[0-9a-f]{64}$/);
|
||
// 指纹必须等于 sha256(cert_pem),即「服务端自行计算」
|
||
const expected = createHash('sha256').update(TEST_CERT_PEM, 'utf8').digest('hex');
|
||
assert.equal(certs[0].fingerprint, expected);
|
||
});
|
||
|
||
test('重复注册返回 409 USER_EXISTS', async () => {
|
||
const result = await ctx.request('POST', '/api/v1/auth/register', {
|
||
username: 'alice',
|
||
password: 'Str0ng-Passw0rd!',
|
||
});
|
||
assert.equal(result.status, 409);
|
||
assert.equal(result.json.ok, false);
|
||
assert.equal(result.json.error.code, 'USER_EXISTS');
|
||
});
|
||
|
||
test('错误密码返回 401', async () => {
|
||
const result = await ctx.request('POST', '/api/v1/auth/login', {
|
||
username: 'alice',
|
||
password: 'Wrong-Passw0rd!',
|
||
});
|
||
assert.equal(result.status, 401);
|
||
assert.equal(result.json.error.code, 'INVALID_CREDENTIALS');
|
||
});
|
||
|
||
test('无 token 访问设备列表返回 401', async () => {
|
||
const result = await ctx.request('GET', '/api/v1/devices', null, null);
|
||
assert.equal(result.status, 401);
|
||
assert.equal(result.json.error.code, 'UNAUTHORIZED');
|
||
});
|
||
|
||
test('心跳缺 device_name 返回 400', async () => {
|
||
const result = await ctx.request('POST', '/api/v1/devices/heartbeat', { platform: 'win32' });
|
||
assert.equal(result.status, 400);
|
||
assert.equal(result.json.error.code, 'VALIDATION_FAILED');
|
||
});
|
||
|
||
test('证书上传缺 cert_pem 返回 400', async () => {
|
||
const result = await ctx.request('POST', '/api/v1/certs', { device_name: 'windesk' });
|
||
assert.equal(result.status, 400);
|
||
assert.equal(result.json.error.code, 'VALIDATION_FAILED');
|
||
});
|
||
|
||
test('旧路径 /api/* 308 重定向到 /api/v1/*(A-02/§4.1)', async () => {
|
||
const result = await ctx.request('GET', '/api/devices', null, null, { redirect: 'manual' });
|
||
assert.equal(result.status, 308);
|
||
assert.equal(result.location, '/api/v1/devices');
|
||
|
||
// /api/health 双注册,不应重定向
|
||
const health = await ctx.request('GET', '/api/health', null, null, { redirect: 'manual' });
|
||
assert.equal(health.status, 200);
|
||
assert.equal(health.json.ok, true);
|
||
});
|