#!/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 }); } }