Files
makelore/tests/unit/coding-attachments-routes.test.ts
brother7 5a275b93a7 feat: remove legacy OpenCode runtime
Cut product flows over to Coding/Pi and retain only the migration-owned v1 boundary. Promote supported native optional packages because electron-builder omitted pnpm transitive optional closure from the packaged ASAR.
2026-08-24 12:17:43 +08:00

312 lines
11 KiB
TypeScript

import { createServer, request as httpRequest } from 'node:http';
import { once } from 'node:events';
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { handleCodingAttachmentRoutes } from '../../electron/api/routes/coding-attachments';
import { getHostApiToken, startHostApiServer } from '../../electron/api/server';
import { CodingAttachmentStore } from '../../electron/coding-projects/attachment-store';
import type { HostApiContext } from '../../electron/api/context';
const roots: string[] = [];
afterEach(async () => {
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
});
async function startAttachmentServer(maxBytes = 16 * 1024 * 1024) {
const root = await mkdtemp(path.join(tmpdir(), 'makelore-coding-attachments-'));
roots.push(root);
const attachments = new CodingAttachmentStore(root, {
createId: () => 'attachment-1',
maxBytes,
});
const context = { codingProducts: { attachments } } as unknown as HostApiContext;
const server = createServer((request, response) => {
const url = new URL(request.url ?? '/', 'http://127.0.0.1');
void handleCodingAttachmentRoutes(request, response, url, context).then((handled) => {
if (!handled) {
response.statusCode = 404;
response.end();
}
});
});
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
const address = server.address();
if (!address || typeof address === 'string') throw new Error('Attachment test server failed');
return {
baseUrl: `http://127.0.0.1:${address.port}`,
close: async () => await new Promise<void>((resolve, reject) => {
server.closeAllConnections();
server.close((error) => error ? reject(error) : resolve());
}),
};
}
async function startAttachmentFailureServer() {
const attachments = {
put: vi.fn(async () => {
throw new Error('write failed');
}),
read: vi.fn(async () => {
throw new Error('read failed');
}),
};
const context = { codingProducts: { attachments } } as unknown as HostApiContext;
const server = createServer((request, response) => {
const url = new URL(request.url ?? '/', 'http://127.0.0.1');
void handleCodingAttachmentRoutes(request, response, url, context).then((handled) => {
if (!handled) {
response.statusCode = 404;
response.end();
}
});
});
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
const address = server.address();
if (!address || typeof address === 'string') throw new Error('Attachment test server failed');
return {
baseUrl: `http://127.0.0.1:${address.port}`,
close: async () => await new Promise<void>((resolve, reject) => {
server.closeAllConnections();
server.close((error) => error ? reject(error) : resolve());
}),
};
}
async function startAuthenticatedHostServer() {
const root = await mkdtemp(path.join(tmpdir(), 'makelore-coding-host-attachments-'));
roots.push(root);
const attachments = new CodingAttachmentStore(root, { createId: () => 'host-attachment-1' });
const context = {
codingProducts: { attachments },
} as unknown as HostApiContext;
const server = startHostApiServer(context, 0);
await once(server, 'listening');
const address = server.address();
if (!address || typeof address === 'string') throw new Error('Host API test server failed');
return {
baseUrl: `http://127.0.0.1:${address.port}`,
token: getHostApiToken(),
close: async () => await new Promise<void>((resolve, reject) => {
server.closeAllConnections();
server.close((error) => error ? reject(error) : resolve());
}),
};
}
async function postDeclaredLength(
url: string,
contentLength: number,
): Promise<{ status: number; body: Record<string, unknown> }> {
return await new Promise((resolve, reject) => {
const request = httpRequest(url, {
method: 'POST',
headers: {
'Content-Type': 'image/png',
'Content-Length': String(contentLength),
},
}, (response) => {
const chunks: Buffer[] = [];
response.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
response.on('end', () => resolve({
status: response.statusCode ?? 0,
body: JSON.parse(Buffer.concat(chunks).toString('utf8')) as Record<string, unknown>,
}));
});
request.on('error', reject);
request.end();
});
}
describe('coding attachment routes', () => {
it('accepts authenticated binary uploads through the production Host API gate', async () => {
const server = await startAuthenticatedHostServer();
try {
const bytes = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10, 1]);
const upload = await fetch(`${server.baseUrl}/api/coding/attachments`, {
method: 'POST',
headers: {
Authorization: `Bearer ${server.token}`,
'Content-Type': 'image/png',
},
body: bytes,
});
expect(upload.status).toBe(201);
await expect(upload.json()).resolves.toEqual({
attachmentId: 'host-attachment-1',
mime: 'image/png',
byteLength: bytes.byteLength,
});
const unrelatedMutation = await fetch(`${server.baseUrl}/api/coding/projects`, {
method: 'POST',
headers: {
Authorization: `Bearer ${server.token}`,
'Content-Type': 'image/png',
},
body: bytes,
});
expect(unrelatedMutation.status).toBe(415);
} finally {
await server.close();
}
});
it('round-trips bounded image bytes through attachment ids', async () => {
const server = await startAttachmentServer();
try {
const bytes = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10, 1, 2, 3]);
const upload = await fetch(`${server.baseUrl}/api/coding/attachments`, {
method: 'POST',
headers: { 'Content-Type': 'image/png' },
body: bytes,
});
expect(upload.status).toBe(201);
await expect(upload.json()).resolves.toEqual({
attachmentId: 'attachment-1',
mime: 'image/png',
byteLength: bytes.byteLength,
});
const preview = await fetch(
`${server.baseUrl}/api/coding/attachments/attachment-1/content`,
);
expect(preview.status).toBe(200);
expect(preview.headers.get('content-type')).toBe('image/png');
expect(preview.headers.get('cache-control')).toBe('private, no-store');
expect(Array.from(new Uint8Array(await preview.arrayBuffer()))).toEqual(Array.from(bytes));
} finally {
await server.close();
}
});
it('rejects unsupported media before writing and maps missing content to 404', async () => {
const server = await startAttachmentServer();
try {
const upload = await fetch(`${server.baseUrl}/api/coding/attachments`, {
method: 'POST',
headers: { 'Content-Type': 'text/plain' },
body: 'not an image',
});
expect(upload.status).toBe(400);
await expect(upload.json()).resolves.toMatchObject({
code: 'CODING_ATTACHMENT_INVALID',
});
const missing = await fetch(
`${server.baseUrl}/api/coding/attachments/missing/content`,
);
expect(missing.status).toBe(404);
await expect(missing.json()).resolves.toMatchObject({
code: 'CODING_ATTACHMENT_NOT_FOUND',
});
} finally {
await server.close();
}
});
it('rejects bytes that do not match the declared image MIME', async () => {
const server = await startAttachmentServer();
try {
const upload = await fetch(`${server.baseUrl}/api/coding/attachments`, {
method: 'POST',
headers: { 'Content-Type': 'image/png' },
body: 'not actually a png',
});
expect(upload.status).toBe(400);
await expect(upload.json()).resolves.toMatchObject({
code: 'CODING_ATTACHMENT_INVALID',
});
} finally {
await server.close();
}
});
it.each([
['image/jpeg', new Uint8Array([0xff, 0xd8, 0xff, 0x01])],
['image/gif', new TextEncoder().encode('GIF89a!')],
['image/webp', new TextEncoder().encode('RIFF\x04\x00\x00\x00WEBP')],
])('accepts the minimal %s image signature', async (mime, bytes) => {
const server = await startAttachmentServer();
try {
const upload = await fetch(`${server.baseUrl}/api/coding/attachments`, {
method: 'POST',
headers: { 'Content-Type': mime },
body: bytes,
});
expect(upload.status).toBe(201);
await expect(upload.json()).resolves.toMatchObject({ mime });
} finally {
await server.close();
}
});
it('rejects a declared body larger than the route limit', async () => {
const server = await startAttachmentServer();
try {
const response = await postDeclaredLength(
`${server.baseUrl}/api/coding/attachments`,
16 * 1024 * 1024 + 1,
);
expect(response.status).toBe(413);
expect(response.body).toMatchObject({
code: 'CODING_ATTACHMENT_INVALID',
});
} finally {
await server.close();
}
});
it('uses registered errors for unreadable content and upload storage failures', async () => {
const server = await startAttachmentFailureServer();
try {
const content = await fetch(
`${server.baseUrl}/api/coding/attachments/attachment-1/content`,
);
expect(content.status).toBe(404);
await expect(content.json()).resolves.toMatchObject({
code: 'CODING_ATTACHMENT_NOT_FOUND',
});
const upload = await fetch(`${server.baseUrl}/api/coding/attachments`, {
method: 'POST',
headers: { 'Content-Type': 'image/png' },
body: new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]),
});
expect(upload.status).toBe(500);
await expect(upload.json()).resolves.toMatchObject({
code: 'CODING_STORAGE_WRITE_FAILED',
error: '本地数据写入失败,请检查存储后重试。',
});
} finally {
await server.close();
}
});
it('maps empty bodies and invalid opaque ids to stable client errors', async () => {
const server = await startAttachmentServer();
try {
const empty = await fetch(`${server.baseUrl}/api/coding/attachments`, {
method: 'POST',
headers: { 'Content-Type': 'image/png' },
});
expect(empty.status).toBe(400);
await expect(empty.json()).resolves.toMatchObject({
code: 'CODING_ATTACHMENT_INVALID',
});
for (const id of ['bad%2Fid', '%ZZ']) {
const invalid = await fetch(`${server.baseUrl}/api/coding/attachments/${id}/content`);
expect(invalid.status).toBe(400);
await expect(invalid.json()).resolves.toMatchObject({
code: 'CODING_ATTACHMENT_INVALID',
});
}
} finally {
await server.close();
}
});
});