Files
massive-cloud/server/analysisPlySample.test.ts

127 lines
4.3 KiB
TypeScript
Raw 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.
import express from 'express';
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import type { AddressInfo } from 'node:net';
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
const mocks = vi.hoisted(() => ({
authenticated: true,
resultTaskId: 7,
}));
vi.mock('./_core/auth.js', () => ({
authenticateRequest: vi.fn(async () => mocks.authenticated
? { id: 1, role: 'user', mustChangePassword: 0 }
: null),
}));
vi.mock('./db', () => ({
getTaskAnalysisResultById: vi.fn(async (id: number) => id === 11
? { id, taskId: mocks.resultTaskId }
: undefined),
listTaskPointcloudFiles: vi.fn(async () => []),
}));
const dataDir = mkdtempSync(join(tmpdir(), 'analysis-ply-route-'));
process.env.DATA_DIR = dataDir;
function writePly(relativePath: string, withColor = false): void {
const resultDir = join(dataDir, 'tasks', '7', 'analysis', '11');
const filePath = join(resultDir, relativePath);
mkdirSync(join(filePath, '..'), { recursive: true });
const properties = withColor
? ['x', 'y', 'z', 'red', 'green', 'blue']
: ['x', 'y', 'z'];
const header = Buffer.from([
'ply',
'format binary_little_endian 1.0',
'element vertex 2',
...properties.map((name) => `property float ${name}`),
'end_header',
'',
].join('\n'), 'ascii');
const values = withColor
? [[1, 2, 3, 255, 0, 128], [4, 5, 6, 0, 255, 64]]
: [[7, 8, 9], [10, 11, 12]];
const body = Buffer.alloc(values.length * properties.length * 4);
values.flat().forEach((value, index) => body.writeFloatLE(value, index * 4));
writeFileSync(filePath, Buffer.concat([header, body]));
}
describe('POST /api/files/sample/analysis', () => {
let origin: string;
let closeServer: () => Promise<void>;
beforeAll(async () => {
writePly('plain.ply');
writePly('nested/color.ply', true);
const { fileRouter } = await import('./fileRoutes');
const app = express();
app.use(express.json());
app.use('/api/files', fileRouter);
const server = app.listen(0);
await new Promise<void>((resolve) => server.once('listening', resolve));
origin = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
closeServer = () => new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
});
afterAll(async () => {
await closeServer();
rmSync(dataDir, { recursive: true, force: true });
});
beforeEach(() => {
mocks.authenticated = true;
mocks.resultTaskId = 7;
});
async function post(body: unknown) {
return fetch(`${origin}/api/files/sample/analysis`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
}
it('采样单个无色 PLY 并返回 16 字节步长', async () => {
const response = await post({ taskId: 7, resultId: 11, files: ['plain.ply'], totalPoints: 2 });
expect(response.status).toBe(200);
expect(response.headers.get('X-Point-Count')).toBe('2');
expect(response.headers.get('X-Has-Color')).toBe('0');
expect((await response.arrayBuffer()).byteLength).toBe(2 * 16);
});
it('采样多个 PLY,彩色响应使用 28 字节步长且不超预算', async () => {
const response = await post({
taskId: 7,
resultId: 11,
files: ['plain.ply', 'nested/color.ply'],
totalPoints: 3,
});
expect(response.status).toBe(200);
expect(response.headers.get('X-Point-Count')).toBe('3');
expect(response.headers.get('X-Has-Color')).toBe('1');
expect((await response.arrayBuffer()).byteLength).toBe(3 * 28);
});
it('拒绝未登录访问', async () => {
mocks.authenticated = false;
expect((await post({ taskId: 7, resultId: 11, files: ['plain.ply'] })).status).toBe(401);
});
it('拒绝错误的任务与结果组合', async () => {
mocks.resultTaskId = 8;
expect((await post({ taskId: 7, resultId: 11, files: ['plain.ply'] })).status).toBe(404);
});
it.each([
['路径穿越', '../plain.ply', 403],
['绝对路径', '/tmp/plain.ply', 400],
['非 PLY 文件', 'result.json', 400],
['缺失文件', 'missing.ply', 404],
])('拒绝%s', async (_label, file, status) => {
expect((await post({ taskId: 7, resultId: 11, files: [file] })).status).toBe(status);
});
});