129 lines
4.7 KiB
JavaScript
129 lines
4.7 KiB
JavaScript
// Run HTTP integration tests against a disposable MySQL database and API process.
|
|
require('dotenv').config({ quiet: true });
|
|
const mysql = require('mysql2/promise');
|
|
const { spawn, spawnSync } = require('node:child_process');
|
|
const { randomBytes } = require('node:crypto');
|
|
const net = require('node:net');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const pkg = require('../package.json');
|
|
async function main() {
|
|
const url = new URL(process.env.DATABASE_URL);
|
|
const name = 'wp_test_' + randomBytes(8).toString('hex');
|
|
const connection = await mysql.createConnection({
|
|
host: url.hostname,
|
|
port: Number(url.port || 3306),
|
|
user: decodeURIComponent(url.username),
|
|
password: decodeURIComponent(url.password),
|
|
});
|
|
let api;
|
|
try {
|
|
await connection.query('CREATE DATABASE `' + name + '`');
|
|
url.pathname = '/' + name;
|
|
const port = await new Promise((resolve, reject) => {
|
|
const server = net.createServer();
|
|
server.on('error', reject);
|
|
server.listen(0, '127.0.0.1', () => {
|
|
const port = server.address().port;
|
|
server.close(() => resolve(port));
|
|
});
|
|
});
|
|
const env = {
|
|
...process.env,
|
|
DATABASE_URL: url.toString(),
|
|
ADMIN_USERNAME: 'admin',
|
|
ADMIN_PASSWORD: 'admin',
|
|
PORT: String(port),
|
|
API_HOST: '127.0.0.1',
|
|
API_ALLOWED_HOSTS: '*',
|
|
WEB_ORIGIN: 'http://localhost:5173',
|
|
NETWORK_RATE_LIMIT_ENABLED: 'false',
|
|
MCP_PUBLIC_URL: 'http://127.0.0.1:' + port + '/mcp',
|
|
MCP_WEB_URL: 'http://localhost:5173',
|
|
NETWORK_ALLOW_HTTP: 'true',
|
|
TEST_API_URL: 'http://127.0.0.1:' + port + '/api',
|
|
};
|
|
const run = (args) => spawnSync(process.execPath, args, { env, encoding: 'utf8' });
|
|
// Historical 005-007 directories sort before the initial migration. Use the
|
|
// current schema only in this disposable database, then exercise our SQL.
|
|
const migration = run([
|
|
require.resolve('prisma/build/index.js'),
|
|
'db',
|
|
'push',
|
|
'--skip-generate',
|
|
]);
|
|
if (migration.status !== 0) {
|
|
const safe = (migration.stdout + migration.stderr)
|
|
.split(/\r?\n/)
|
|
.filter((line) => !/Datasource|mysql:\/\/|DATABASE_URL|Environment variables/.test(line))
|
|
.join('\n');
|
|
console.error(safe);
|
|
throw Error('Disposable database migration failed');
|
|
}
|
|
await connection.query('USE `' + name + '`');
|
|
// db push omits the historical column comments asserted by integration tests.
|
|
await connection.query(
|
|
"ALTER TABLE `User` MODIFY COLUMN `accountGroupOrder` JSON NULL COMMENT '账户分组显示顺序;空值表示沿用默认顺序'",
|
|
);
|
|
await connection.query(
|
|
'ALTER TABLE `User` DROP COLUMN `role`, DROP COLUMN `banned`, DROP COLUMN `mustChangePassword`',
|
|
);
|
|
await connection.query(
|
|
fs.readFileSync(
|
|
path.join(__dirname, '../prisma/migrations/20261005130000_admin_accounts/migration.sql'),
|
|
'utf8',
|
|
),
|
|
);
|
|
const build = run([require.resolve('typescript/bin/tsc')]);
|
|
if (build.status !== 0) {
|
|
console.log(build.stdout);
|
|
throw Error('API build failed');
|
|
}
|
|
api = spawn(process.execPath, ['dist/main.js'], { env, stdio: 'ignore' });
|
|
let ready = false;
|
|
for (let i = 0; i < 80; i++) {
|
|
try {
|
|
ready = (await fetch(env.TEST_API_URL + '/health')).ok;
|
|
} catch {}
|
|
if (ready) break;
|
|
if (api.exitCode !== null) throw Error('Disposable API startup failed');
|
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
}
|
|
if (!ready) throw Error('Disposable API startup timed out');
|
|
env.TEST_ISOLATED = 'true';
|
|
const requested = process.argv.slice(2);
|
|
const tests = requested.length
|
|
? requested
|
|
: [
|
|
...pkg.scripts['test:integration'].split(' ').filter((s) => s.endsWith('.test.ts')),
|
|
'test/admin-integration.test.ts',
|
|
'test/mcp.test.ts',
|
|
'test/oauth-duration.test.ts',
|
|
];
|
|
const result = spawnSync(
|
|
process.execPath,
|
|
[require.resolve('tsx/cli'), '--test', '--test-concurrency=1', ...tests],
|
|
{ env, stdio: 'inherit' },
|
|
);
|
|
process.exitCode = result.status || 0;
|
|
} finally {
|
|
if (api && api.exitCode === null) {
|
|
const exited = new Promise((resolve) => api.once('exit', resolve));
|
|
api.kill();
|
|
await exited;
|
|
}
|
|
await connection.query('DROP DATABASE `' + name + '`');
|
|
await connection.end();
|
|
console.log('Disposable test database and API cleaned up.');
|
|
}
|
|
}
|
|
main().catch((e) => {
|
|
console.error(
|
|
'Isolated test run failed: ' +
|
|
(/Disposable|API build/.test(e.message)
|
|
? e.message
|
|
: 'check test configuration or database access'),
|
|
);
|
|
process.exitCode = 1;
|
|
});
|