company-target-dir-config.test.js 6.34 KB
/**
 * 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);
  });
});