Files
dzc f55561c98d feat:上传到阿里云oss
feat:把以前的图片迁移到阿里云oss
2026-11-05 15:22:55 +08:00

310 lines
17 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { stringify } from 'yaml';
import { loadProjectConfig } from '../lib/project-config.mjs';
const defaultBase = () => ({ spring: { profiles: { active: 'dev' } } });
const datasource = overrides => ({
url: 'jdbc:mysql://db.example.test:3307/fixture_db?useSSL=false&serverTimezone=Asia%2FShanghai',
username: 'fixture-user', password: 'fixture-password', ...overrides,
});
const profileDocument = (overrides = {}) => ({
spring: { datasource: { dynamic: { datasource: { master: datasource(overrides) } } } },
server: { port: 48083 },
});
const documents = value => (Array.isArray(value) ? value : [value]).map(document => stringify(document)).join('---\n');
async function fixture(t, { base = defaultBase(), profiles = { dev: profileDocument() } } = {}) {
const root = await mkdtemp(join(tmpdir(), 'oss-project-config-test-'));
t.after(() => rm(root, { recursive: true, force: true }));
const resources = join(root, 'yudao-server/src/main/resources');
await mkdir(resources, { recursive: true });
await writeFile(join(resources, 'application.yaml'), typeof base === 'string' ? base : documents(base));
for (const [name, content] of Object.entries(profiles)) {
await writeFile(join(resources, 'application-' + name + '.yaml'), typeof content === 'string' ? content : documents(content));
}
return root;
}
test('default profile, master credentials and actual server port come from the project YAML', async t => {
const projectRoot = await fixture(t);
const result = await loadProjectConfig({ projectRoot, env: {} });
assert.equal(result.profile, 'dev');
assert.deepEqual(result.mysql, {
host: 'db.example.test', port: 3307, database: 'fixture_db',
user: 'fixture-user', password: 'fixture-password',
});
assert.equal(result.downloadBaseUrl, 'http://127.0.0.1:48083/admin-api/infra/file');
assert.deepEqual(result.sourceFiles, [
resolve(projectRoot, 'yudao-server/src/main/resources/application.yaml'),
resolve(projectRoot, 'yudao-server/src/main/resources/application-dev.yaml'),
]);
});
test('explicit profile overrides environment, which overrides the base active profile', async t => {
const projectRoot = await fixture(t, { profiles: {
dev: profileDocument({ url: 'jdbc:mysql://dev.example.test/dev_db' }),
local: profileDocument({ url: 'jdbc:mysql://local.example.test/local_db' }),
prod: profileDocument({ url: 'jdbc:mysql://prod.example.test/prod_db' }),
} });
const explicit = await loadProjectConfig({ projectRoot, profile: 'local', env: { SPRING_PROFILES_ACTIVE: 'prod' } });
const environment = await loadProjectConfig({ projectRoot, env: { SPRING_PROFILES_ACTIVE: 'prod' } });
const base = await loadProjectConfig({ projectRoot, env: {} });
assert.equal(explicit.mysql.database, 'local_db');
assert.equal(environment.mysql.database, 'prod_db');
assert.equal(base.mysql.database, 'dev_db');
});
test('single-profile selection resolves a base property or environment placeholder with a default', async t => {
const projectRoot = await fixture(t, {
base: { selection: { profile: '${APP_PROFILE:dev}' }, spring: { profiles: { active: '${selection.profile}' } } },
profiles: { dev: profileDocument(), local: profileDocument() },
});
assert.equal((await loadProjectConfig({ projectRoot, env: {} })).profile, 'dev');
assert.equal((await loadProjectConfig({ projectRoot, env: { APP_PROFILE: 'local' } })).profile, 'local');
});
test('explicit profile works even when the base active profile is absent or unusable', async t => {
const projectRoot = await fixture(t, { base: { spring: { profiles: { active: ['unused', 'group'] } } } });
const result = await loadProjectConfig({ projectRoot, profile: 'dev', env: {} });
assert.equal(result.profile, 'dev');
});
test('multiple, missing and path-traversal profiles fail with actionable selection errors', async t => {
for (const active of [['dev', 'prod'], 'dev,prod', 'dev prod', '', '../prod', null, undefined]) {
const projectRoot = await fixture(t, { base: { spring: { profiles: { active } } } });
await assert.rejects(loadProjectConfig({ projectRoot, env: {} }), /单个 profile.*--profile/);
}
const projectRoot = await fixture(t);
await assert.rejects(loadProjectConfig({ projectRoot, env: { SPRING_PROFILES_ACTIVE: 'dev,prod' } }), /单个 profile/);
await assert.rejects(loadProjectConfig({ projectRoot, profile: '../dev', env: {} }), /单个 profile/);
});
test('multiple unconditional documents merge recursively across base and selected profile', async t => {
const projectRoot = await fixture(t, {
base: [
{ spring: { profiles: { active: 'dev' }, datasource: { dynamic: { primary: 'main', datasource: { main: { username: 'base-user', password: 'base-password' } } } } } },
{ spring: { datasource: { dynamic: { datasource: { main: { url: 'jdbc:mysql://base.example.test/base_db' } } } } }, server: { port: 8081 } },
],
profiles: { dev: [
{ spring: { datasource: { dynamic: { datasource: { main: { url: 'jdbc:mysql://selected.example.test:3308/selected_db' } } } } } },
{ spring: { datasource: { dynamic: { datasource: { main: { password: 'selected-password' } } } } }, server: { port: 48083 } },
] },
});
const result = await loadProjectConfig({ projectRoot, env: {} });
assert.deepEqual(result.mysql, { host: 'selected.example.test', port: 3308, database: 'selected_db', user: 'base-user', password: 'selected-password' });
assert.equal(result.downloadBaseUrl, 'http://127.0.0.1:48083/admin-api/infra/file');
});
test('environment directly overrides master URL, username and password', async t => {
const projectRoot = await fixture(t);
const result = await loadProjectConfig({ projectRoot, env: {
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_URL: 'jdbc:mysql://override.example.test:3310/env_db',
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_USERNAME: 'env-user',
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_PASSWORD: ' env:pass#word ',
} });
assert.deepEqual(result.mysql, { host: 'override.example.test', port: 3310, database: 'env_db', user: 'env-user', password: ' env:pass#word ' });
});
test('dynamic primary environment override selects the named source instead of master or slave', async t => {
const document = profileDocument();
document.spring.datasource.dynamic.datasource.reporting = datasource({ url: 'jdbc:mysql://reporting.example.test/reporting_db' });
document.spring.datasource.dynamic.datasource.slave = datasource({ url: 'jdbc:mysql://slave.example.test/slave_db' });
const projectRoot = await fixture(t, { profiles: { dev: document } });
const result = await loadProjectConfig({ projectRoot, env: {
SPRING_DATASOURCE_DYNAMIC_PRIMARY: 'reporting',
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_REPORTING_PASSWORD: 'reporting-env-password',
} });
assert.equal(result.mysql.database, 'reporting_db');
assert.equal(result.mysql.password, 'reporting-env-password');
});
test('required fields support property references and nested environment defaults', async t => {
const document = profileDocument({
url: 'jdbc:mysql://${DB_HOST:${database.host}}:${DB_PORT:${database.port:3306}}/${database.name}',
username: '${DB_USER:${database.user}}',
password: '${DB_PASSWORD:${database.password}}',
});
document.database = { host: 'property.example.test', name: 'property_db', user: 'property-user', password: 'property-password' };
const projectRoot = await fixture(t, { profiles: { dev: document } });
const base = await loadProjectConfig({ projectRoot, env: {} });
assert.deepEqual(base.mysql, { host: 'property.example.test', port: 3306, database: 'property_db', user: 'property-user', password: 'property-password' });
const overridden = await loadProjectConfig({ projectRoot, env: { DB_HOST: 'env.example.test', DB_PORT: '3309', DB_PASSWORD: 'env-password' } });
assert.equal(overridden.mysql.host, 'env.example.test');
assert.equal(overridden.mysql.port, 3309);
assert.equal(overridden.mysql.password, 'env-password');
});
test('unrelated unresolved placeholders, service credentials and cycles are never resolved', async t => {
const document = profileDocument();
document.unrelated = { secret: '${MISSING_SERVICE_SECRET}', a: '${unrelated.b}', b: '${unrelated.a}', encrypted: 'ENC(unrelated-service-secret)' };
const projectRoot = await fixture(t, { profiles: { dev: document } });
assert.equal((await loadProjectConfig({ projectRoot, env: {} })).mysql.database, 'fixture_db');
});
test('HTTP context path, admin prefix and SSL settings determine the download URL', async t => {
const document = profileDocument();
document.server = { port: 8443, servlet: { contextPath: '/backend/' }, ssl: { enabled: true } };
document.yudao = { web: { adminApi: { prefix: '/management/' } } };
const projectRoot = await fixture(t, { profiles: { dev: document } });
const result = await loadProjectConfig({ projectRoot, env: {} });
assert.equal(result.downloadBaseUrl, 'https://127.0.0.1:8443/backend/management/infra/file');
const overridden = await loadProjectConfig({ projectRoot, env: {
SERVER_PORT: '9080', SERVER_SERVLET_CONTEXT_PATH: '/overridden',
YUDAO_WEB_ADMIN_API_PREFIX: '/admin', SERVER_SSL_ENABLED: 'false',
} });
assert.equal(overridden.downloadBaseUrl, 'http://127.0.0.1:9080/overridden/admin/infra/file');
});
test('Spring defaults apply to missing primary, HTTP port and API prefix', async t => {
const document = profileDocument();
delete document.server;
const projectRoot = await fixture(t, { profiles: { dev: document } });
assert.equal((await loadProjectConfig({ projectRoot, env: {} })).downloadBaseUrl, 'http://127.0.0.1:8080/admin-api/infra/file');
});
test('an explicit root context path does not produce duplicate slashes', async t => {
const document = profileDocument();
document.server.servlet = { 'context-path': '/' };
document.yudao = { web: { 'admin-api': { prefix: '/' } } };
const projectRoot = await fixture(t, { profiles: { dev: document } });
assert.equal((await loadProjectConfig({ projectRoot, env: {} })).downloadBaseUrl, 'http://127.0.0.1:48083/infra/file');
});
test('single-host MySQL URL supports omitted ports and IPv6 without changing database names', async t => {
for (const [url, expected] of [
['jdbc:mysql://localhost/test_db', { host: 'localhost', port: 3306, database: 'test_db' }],
['jdbc:mysql://[::1]:3309/test_db', { host: '::1', port: 3309, database: 'test_db' }],
['jdbc:mysql://db.example.test/test%20db', { host: 'db.example.test', port: 3306, database: 'test db' }],
]) {
const projectRoot = await fixture(t, { profiles: { dev: profileDocument({ url }) } });
const result = await loadProjectConfig({ projectRoot, env: {} });
assert.deepEqual({ host: result.mysql.host, port: result.mysql.port, database: result.mysql.database }, expected);
}
});
test('ambiguous, unsupported and incomplete JDBC URLs are rejected without echoing their values', async t => {
for (const url of [
'jdbc:mysql:loadbalance://first,second/db',
'jdbc:mysql://first,second/db',
'jdbc:mysql://first:3306,second:3306/db',
'jdbc:postgresql://secret-host/secret-database',
'jdbc:mysql://db.example.test',
'jdbc:mysql://db.example.test/a/b',
'jdbc:mysql://db.example.test:65536/db',
'jdbc:mysql://db.example.test:0/db',
'jdbc:mysql://user:sensitive-url-password@db.example.test/db',
'jdbc:mysql://db.example.test/a%2Fb',
]) {
const projectRoot = await fixture(t, { profiles: { dev: profileDocument({ url }) } });
await assert.rejects(loadProjectConfig({ projectRoot, env: {} }), error => {
assert.match(error.message, /MySQL|jdbc:mysql/);
assert.ok(!error.message.includes(url));
assert.ok(!error.message.includes('sensitive-url-password'));
return true;
});
}
});
test('missing or still-encrypted passwords are rejected, with direct environment override available', async t => {
for (const password of [null, undefined, '', 'ENC(sensitive-encrypted-password)']) {
const projectRoot = await fixture(t, { profiles: { dev: profileDocument({ password }) } });
await assert.rejects(loadProjectConfig({ projectRoot, env: {} }), error => {
assert.match(error.message, /缺少|缺失|ENC/);
assert.ok(!error.message.includes('sensitive-encrypted-password'));
return true;
});
assert.equal((await loadProjectConfig({ projectRoot, env: { SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_PASSWORD: 'provided-password' } })).mysql.password, 'provided-password');
}
});
test('required unresolved, malformed or cyclic placeholders stop safely without revealing secrets', async t => {
for (const password of [
'sensitive-prefix-${MISSING_PASSWORD}',
'sensitive-prefix-${MISSING_PASSWORD',
'${spring.datasource.dynamic.datasource.master.password}',
]) {
const projectRoot = await fixture(t, { profiles: { dev: profileDocument({ password }) } });
await assert.rejects(loadProjectConfig({ projectRoot, env: {} }), error => {
assert.match(error.message, /占位符/);
assert.ok(!error.message.includes('sensitive-prefix'));
return true;
});
}
});
test('numeric YAML scalar credentials retain bigint precision', async t => {
const document = profileDocument({ password: 900719925474099312345n });
const projectRoot = await fixture(t, { profiles: { dev: document } });
assert.equal((await loadProjectConfig({ projectRoot, env: {} })).mysql.password, '900719925474099312345');
});
test('config imports, included profiles and a selected profile group require explicit configuration', async t => {
for (const base of [
{ spring: { profiles: { active: 'dev' }, config: { import: 'optional:file:sensitive-location' } } },
{ spring: { profiles: { active: 'dev', include: ['common'] } } },
{ spring: { profiles: { active: 'dev', group: { dev: ['common', 'database'] } } } },
]) {
const projectRoot = await fixture(t, { base });
await assert.rejects(loadProjectConfig({ projectRoot, env: {} }), error => {
assert.match(error.message, /import|include|配置组/);
assert.ok(!error.message.includes('sensitive-location'));
return true;
});
}
const projectRoot = await fixture(t);
await assert.rejects(loadProjectConfig({ projectRoot, env: { SPRING_CONFIG_IMPORT: 'optional:file:sensitive-location' } }), /import/);
await assert.rejects(loadProjectConfig({ projectRoot, env: { SPRING_PROFILES_GROUP_DEV: 'common,database' } }), /配置组/);
});
test('an unrelated profile group does not affect an explicitly selected concrete profile', async t => {
const projectRoot = await fixture(t, { base: { spring: { profiles: { active: 'dev', group: { production: ['prod', 'shared'] } } } } });
assert.equal((await loadProjectConfig({ projectRoot, env: {} })).profile, 'dev');
});
test('conditional YAML documents and active-profile redefinition are rejected', async t => {
for (const content of [
[profileDocument(), { spring: { config: { activate: { 'on-profile': 'dev' } } }, server: { port: 8080 } }],
[profileDocument(), { spring: { profiles: 'dev' }, server: { port: 8080 } }],
[profileDocument(), { spring: { profiles: { active: 'prod' } } }],
]) {
const projectRoot = await fixture(t, { profiles: { dev: content } });
await assert.rejects(loadProjectConfig({ projectRoot, env: {} }), /条件|歧义/);
}
});
test('missing files and malformed YAML errors do not expose parser source or secrets', async t => {
const projectRoot = await fixture(t, { base: 'spring: [sensitive-password' });
await assert.rejects(loadProjectConfig({ projectRoot, env: {} }), error => {
assert.match(error.message, /YAML/);
assert.ok(!error.message.includes('sensitive-password'));
return true;
});
const validRoot = await fixture(t);
await assert.rejects(loadProjectConfig({ projectRoot: validRoot, profile: 'missing', env: {} }), /--profile/);
await assert.rejects(loadProjectConfig({ projectRoot: '', env: {} }), /项目根目录/);
});
test('duplicate keys inside one YAML document cannot silently choose credentials', async t => {
const projectRoot = await fixture(t, { base: 'spring:\n profiles:\n active: dev\nspring:\n password: sensitive-password\n' });
await assert.rejects(loadProjectConfig({ projectRoot, env: {} }), error => {
assert.match(error.message, /YAML/);
assert.ok(!error.message.includes('sensitive-password'));
return true;
});
});
test('invalid HTTP ports and route prefixes fail instead of generating unusable URLs', async t => {
const projectRoot = await fixture(t);
for (const env of [
{ SERVER_PORT: '0' }, { SERVER_PORT: '65536' }, { SERVER_PORT: 'not-a-port' },
{ SERVER_SERVLET_CONTEXT_PATH: 'missing-leading-slash' },
{ YUDAO_WEB_ADMIN_API_PREFIX: '/admin?query' },
{ SERVER_SERVLET_CONTEXT_PATH: '/a/../b' },
{ SERVER_SSL_ENABLED: 'unknown' },
]) await assert.rejects(loadProjectConfig({ projectRoot, env }), /HTTP/);
});