company-target-dir-config.test.js
6.34 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
/**
* Unit Tests — PUT /api/company/:companyId 的 target_dir 配置入口(42 号文档 Phase 3)
*
* 39 号文档 P5 落地的是"配了 company.json.target_dir 就生效"这一半,"人怎么把这两个字段
* 填进去"这一半此前只能手工编辑 company.json。这条测试钉住新入口的校验分支——校验复用
* path-resolver.validateLinkedTarget(P5 已经抽出来的共享函数),跟 graph-engine.js 运行时
* 用的是同一份判据,不重新写一遍。
*
* 走真入口:把 router 挂到真 express 上 listen(0) 再 fetch,不 mock company-service。
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import fs from 'fs';
import os from 'os';
import path from 'path';
import express from 'express';
const COMPANIES_DIR = path.resolve(__dirname, '../../../data/companies');
describe('PUT /api/company/:companyId — target_dir 配置(42 号文档 Phase 3)', () => {
let server;
let base;
const createdCompanies = [];
const createdDirs = [];
beforeAll(async () => {
const app = express();
app.use(express.json());
app.use('/api/company', require('../../routes/company'));
await new Promise((resolve) => { server = app.listen(0, resolve); });
base = `http://127.0.0.1:${server.address().port}/api/company`;
});
afterAll(() => {
if (server) server.close();
for (const cid of createdCompanies) {
fs.rmSync(path.join(COMPANIES_DIR, cid), { recursive: true, force: true });
}
for (const d of createdDirs) {
try { fs.rmSync(d, { recursive: true, force: true }); } catch { /* ignore */ }
}
});
function makeCompany(companyId) {
const dir = path.join(COMPANIES_DIR, companyId);
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, 'company.json'), JSON.stringify({ id: companyId, name: companyId }));
createdCompanies.push(companyId);
return dir;
}
async function put(companyId, body) {
const r = await fetch(`${base}/${companyId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
return [r.status, await r.json()];
}
it('linked-existing:合法的已存在空目录,写入成功', async () => {
const cid = 'target-dir-cfg-existing';
makeCompany(cid);
const target = fs.mkdtempSync(path.join(os.tmpdir(), 'target-dir-cfg-existing-'));
createdDirs.push(target);
const [status, body] = await put(cid, { target_dir: target, target_mode: 'linked-existing' });
expect(status).toBe(200);
expect(body.company.target_dir).toBe(path.resolve(target));
expect(body.company.target_mode).toBe('linked-existing');
// 真的落盘了,不是只回了个内存对象
const onDisk = JSON.parse(fs.readFileSync(path.join(COMPANIES_DIR, cid, 'company.json'), 'utf8'));
expect(onDisk.target_dir).toBe(path.resolve(target));
});
it('linked-new:目录不存在时自动创建,写入成功', async () => {
const cid = 'target-dir-cfg-new';
makeCompany(cid);
const parent = fs.mkdtempSync(path.join(os.tmpdir(), 'target-dir-cfg-new-parent-'));
createdDirs.push(parent);
const target = path.join(parent, 'brand-new-project');
expect(fs.existsSync(target)).toBe(false);
const [status, body] = await put(cid, { target_dir: target, target_mode: 'linked-new' });
expect(status).toBe(200);
expect(body.company.target_mode).toBe('linked-new');
expect(fs.existsSync(target)).toBe(true); // validateLinkedTarget 的既有副作用:自动建目录
});
it('不传 target_mode 时默认按 linked-existing 处理(与 graph-engine.js 的默认口径一致)', async () => {
const cid = 'target-dir-cfg-default-mode';
makeCompany(cid);
const target = fs.mkdtempSync(path.join(os.tmpdir(), 'target-dir-cfg-default-'));
createdDirs.push(target);
const [status, body] = await put(cid, { target_dir: target });
expect(status).toBe(200);
expect(body.company.target_mode).toBe('linked-existing');
});
it('敏感路径命中黑名单 → 400,不写入', async () => {
const cid = 'target-dir-cfg-sensitive';
makeCompany(cid);
const sensitive = path.join(os.homedir(), '.ssh', 'target-dir-cfg-should-not-write');
const [status, body] = await put(cid, { target_dir: sensitive, target_mode: 'linked-existing' });
expect(status).toBe(400);
expect(body.error).toContain('sensitive_path');
const onDisk = JSON.parse(fs.readFileSync(path.join(COMPANIES_DIR, cid, 'company.json'), 'utf8'));
expect(onDisk.target_dir).toBeUndefined(); // 校验失败,company.json 没有被改动
});
it('linked-new 但目录已存在且非空 → 400,不写入', async () => {
const cid = 'target-dir-cfg-new-nonempty';
makeCompany(cid);
const target = fs.mkdtempSync(path.join(os.tmpdir(), 'target-dir-cfg-nonempty-'));
createdDirs.push(target);
fs.writeFileSync(path.join(target, 'already-here.txt'), 'x');
const [status, body] = await put(cid, { target_dir: target, target_mode: 'linked-new' });
expect(status).toBe(400);
expect(body.error).toContain('not empty');
});
it('linked-existing 但目录不存在 → 400,不写入', async () => {
const cid = 'target-dir-cfg-existing-missing';
makeCompany(cid);
const parent = fs.mkdtempSync(path.join(os.tmpdir(), 'target-dir-cfg-missing-parent-'));
createdDirs.push(parent);
const target = path.join(parent, 'does-not-exist');
const [status, body] = await put(cid, { target_dir: target, target_mode: 'linked-existing' });
expect(status).toBe(400);
expect(body.error).toContain('does not exist');
});
it('target_dir 传空字符串清掉已有配置', async () => {
const cid = 'target-dir-cfg-clear';
makeCompany(cid);
const target = fs.mkdtempSync(path.join(os.tmpdir(), 'target-dir-cfg-clear-'));
createdDirs.push(target);
const [s1, b1] = await put(cid, { target_dir: target, target_mode: 'linked-existing' });
expect(s1).toBe(200);
expect(b1.company.target_dir).toBeTruthy();
const [s2, b2] = await put(cid, { target_dir: '' });
expect(s2).toBe(200);
expect(b2.company.target_dir).toBeNull();
expect(b2.company.target_mode).toBeNull();
});
it('公司不存在 → 404', async () => {
const [status] = await put('target-dir-cfg-nope', { target_dir: '/tmp/whatever' });
expect(status).toBe(404);
});
});