2e14873f2d
Track application source and tests; exclude local env, user workspaces, and runtime data via .gitignore. Co-authored-by: Cursor <cursoragent@cursor.com>
221 lines
7.1 KiB
JavaScript
221 lines
7.1 KiB
JavaScript
#!/usr/bin/env node
|
|
import assert from 'node:assert/strict';
|
|
import { spawn } from 'node:child_process';
|
|
import fs from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import { createDbPool } from '../db.mjs';
|
|
import { loadH5Environment } from './load-env.mjs';
|
|
|
|
loadH5Environment(import.meta.dirname);
|
|
|
|
const portalPort = Number(process.env.MINDSPACE_E2E_PORT ?? 18090);
|
|
const baseUrl = process.env.MINDSPACE_E2E_URL ?? `http://127.0.0.1:${portalPort}`;
|
|
const suffix = `${Date.now()}${Math.floor(Math.random() * 1000)}`;
|
|
const storageRoot = path.join('/tmp', `mindspace-assets-${suffix}`);
|
|
const portal = process.env.MINDSPACE_E2E_URL
|
|
? null
|
|
: spawn(process.execPath, ['server.mjs'], {
|
|
cwd: path.join(import.meta.dirname, '..'),
|
|
env: {
|
|
...process.env,
|
|
H5_PORT: String(portalPort),
|
|
TKMIND_API_TARGET: 'http://127.0.0.1:9',
|
|
MINDSPACE_STORAGE_ROOT: storageRoot,
|
|
},
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
});
|
|
let portalLogs = '';
|
|
portal?.stdout.on('data', (chunk) => {
|
|
portalLogs += chunk.toString();
|
|
});
|
|
portal?.stderr.on('data', (chunk) => {
|
|
portalLogs += chunk.toString();
|
|
});
|
|
const users = [
|
|
{
|
|
username: `ms_owner_${suffix}`,
|
|
email: `ms-owner-${suffix}@example.test`,
|
|
password: 'MindSpace-E2E-Owner-2026',
|
|
},
|
|
{
|
|
username: `ms_other_${suffix}`,
|
|
email: `ms-other-${suffix}@example.test`,
|
|
password: 'MindSpace-E2E-Other-2026',
|
|
},
|
|
];
|
|
|
|
async function request(path, options = {}) {
|
|
const response = await fetch(`${baseUrl}${path}`, options);
|
|
const contentType = response.headers.get('content-type') ?? '';
|
|
const body = contentType.includes('application/json')
|
|
? await response.json()
|
|
: Buffer.from(await response.arrayBuffer());
|
|
return { response, body };
|
|
}
|
|
|
|
async function waitForPortal() {
|
|
if (!portal) return;
|
|
for (let attempt = 0; attempt < 60; attempt += 1) {
|
|
if (portal.exitCode != null) throw new Error(`Portal 提前退出\n${portalLogs}`);
|
|
try {
|
|
const response = await fetch(`${baseUrl}/auth/status`);
|
|
if (response.ok) return;
|
|
} catch {
|
|
// Portal is still starting.
|
|
}
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
}
|
|
throw new Error(`Portal 启动超时\n${portalLogs}`);
|
|
}
|
|
|
|
async function registerAndLogin(user) {
|
|
const registration = await request('/auth/register', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
username: user.username,
|
|
email: user.email,
|
|
password: user.password,
|
|
displayName: user.username,
|
|
}),
|
|
});
|
|
assert.equal(registration.response.status, 200, JSON.stringify(registration.body));
|
|
|
|
const login = await request('/auth/login', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ username: user.username, password: user.password }),
|
|
});
|
|
assert.equal(login.response.status, 200, JSON.stringify(login.body));
|
|
const cookie = login.response.headers.get('set-cookie')?.split(';', 1)[0];
|
|
assert.ok(cookie, '登录响应应设置会话 Cookie');
|
|
return { id: registration.body.user.id, cookie };
|
|
}
|
|
|
|
const pool = createDbPool();
|
|
let owner;
|
|
let other;
|
|
|
|
try {
|
|
await waitForPortal();
|
|
owner = await registerAndLogin(users[0]);
|
|
other = await registerAndLogin(users[1]);
|
|
|
|
const initialSpace = await request('/api/mindspace/v1/space', {
|
|
headers: { Cookie: owner.cookie },
|
|
});
|
|
assert.equal(initialSpace.response.status, 200, JSON.stringify(initialSpace.body));
|
|
const oaCategory = initialSpace.body.data.categories.find((category) => category.code === 'oa');
|
|
assert.ok(oaCategory, '默认空间应包含 OA 工作区');
|
|
|
|
const content = Buffer.from('MindSpace upload E2E\n', 'utf8');
|
|
const created = await request('/api/mindspace/v1/uploads', {
|
|
method: 'POST',
|
|
headers: {
|
|
Cookie: owner.cookie,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
category_id: oaCategory.id,
|
|
filename: 'e2e-notes.txt',
|
|
size_bytes: content.length,
|
|
declared_mime_type: 'text/plain',
|
|
}),
|
|
});
|
|
assert.equal(created.response.status, 201, JSON.stringify(created.body));
|
|
|
|
const contentUpload = await request(created.body.data.uploadUrl, {
|
|
method: 'PUT',
|
|
headers: {
|
|
Cookie: owner.cookie,
|
|
'Content-Type': 'application/octet-stream',
|
|
},
|
|
body: content,
|
|
});
|
|
assert.equal(contentUpload.response.status, 200, JSON.stringify(contentUpload.body));
|
|
|
|
const completed = await request(
|
|
`/api/mindspace/v1/uploads/${created.body.data.id}/complete`,
|
|
{
|
|
method: 'POST',
|
|
headers: {
|
|
Cookie: owner.cookie,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: '{}',
|
|
},
|
|
);
|
|
assert.equal(completed.response.status, 201, JSON.stringify(completed.body));
|
|
const assetId = completed.body.data.id;
|
|
|
|
const assets = await request('/api/mindspace/v1/assets?category_code=oa', {
|
|
headers: { Cookie: owner.cookie },
|
|
});
|
|
assert.equal(assets.response.status, 200, JSON.stringify(assets.body));
|
|
assert.equal(assets.body.data.length, 1);
|
|
assert.equal(assets.body.data[0].id, assetId);
|
|
|
|
const downloaded = await request(`/api/mindspace/v1/assets/${assetId}/download`, {
|
|
headers: { Cookie: owner.cookie },
|
|
});
|
|
assert.equal(downloaded.response.status, 200);
|
|
assert.deepEqual(downloaded.body, content);
|
|
|
|
const ownerSpaceAfterUpload = await request('/api/mindspace/v1/space', {
|
|
headers: { Cookie: owner.cookie },
|
|
});
|
|
assert.equal(ownerSpaceAfterUpload.body.data.quota.usedBytes, content.length);
|
|
assert.equal(ownerSpaceAfterUpload.body.data.quota.reservedBytes, 0);
|
|
assert.equal(
|
|
ownerSpaceAfterUpload.body.data.categories.find((category) => category.code === 'oa')
|
|
.itemCount,
|
|
1,
|
|
);
|
|
|
|
const foreignAssets = await request('/api/mindspace/v1/assets?category_code=oa', {
|
|
headers: { Cookie: other.cookie },
|
|
});
|
|
assert.equal(foreignAssets.response.status, 200);
|
|
assert.equal(foreignAssets.body.data.length, 0);
|
|
|
|
const foreignDownload = await request(`/api/mindspace/v1/assets/${assetId}/download`, {
|
|
headers: { Cookie: other.cookie },
|
|
});
|
|
assert.equal(foreignDownload.response.status, 404);
|
|
|
|
const foreignDelete = await request(`/api/mindspace/v1/assets/${assetId}`, {
|
|
method: 'DELETE',
|
|
headers: { Cookie: other.cookie },
|
|
});
|
|
assert.equal(foreignDelete.response.status, 404);
|
|
|
|
const deleted = await request(`/api/mindspace/v1/assets/${assetId}`, {
|
|
method: 'DELETE',
|
|
headers: { Cookie: owner.cookie },
|
|
});
|
|
assert.equal(deleted.response.status, 200, JSON.stringify(deleted.body));
|
|
|
|
const ownerSpaceAfterDelete = await request('/api/mindspace/v1/space', {
|
|
headers: { Cookie: owner.cookie },
|
|
});
|
|
assert.equal(ownerSpaceAfterDelete.body.data.quota.usedBytes, 0);
|
|
assert.equal(
|
|
ownerSpaceAfterDelete.body.data.categories.find((category) => category.code === 'oa')
|
|
.itemCount,
|
|
0,
|
|
);
|
|
|
|
console.log('MindSpace API E2E passed');
|
|
} finally {
|
|
await pool.query(`DELETE FROM h5_users WHERE username IN (?, ?)`, [
|
|
users[0].username,
|
|
users[1].username,
|
|
]);
|
|
await pool.end();
|
|
if (portal) {
|
|
portal.kill('SIGTERM');
|
|
await new Promise((resolve) => portal.once('exit', resolve));
|
|
await fs.rm(storageRoot, { recursive: true, force: true });
|
|
}
|
|
}
|