Files
jkooe 7ddd344d7f chore: 初始化仓库并落地 T-1 批次1(安全地基与仓库卫生)
- 新增 .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%
2026-10-26 00:10:01 +08:00

118 lines
4.6 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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);
});