249 lines
8.8 KiB
TypeScript
249 lines
8.8 KiB
TypeScript
import { existsSync } from 'fs';
|
|
import { join } from 'path';
|
|
import { MODULE_METADATA } from '@nestjs/common/constants';
|
|
import { ConfigModule } from '@nestjs/config';
|
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
import { getMetadataArgsStorage } from 'typeorm';
|
|
import { AdminAuthGuardModule } from '../../../../src/modules/admin/identity/auth/admin-auth-guard.module';
|
|
import { DictModule } from '../../../../src/modules/admin/platform-config/dict/dict.module';
|
|
import { AppModule } from '../../../../src/app.module';
|
|
import { QqbotAccountController } from '../../../../src/modules/qqbot/core/contract/account/qqbot-account.controller';
|
|
import { QqbotCommandController } from '../../../../src/modules/qqbot/core/contract/command/qqbot-command.controller';
|
|
import { QqbotDashboardController } from '../../../../src/modules/qqbot/core/contract/dashboard/qqbot-dashboard.controller';
|
|
import { QqbotMessageController } from '../../../../src/modules/qqbot/core/contract/message/qqbot-message.controller';
|
|
import { QqbotPermissionController } from '../../../../src/modules/qqbot/core/contract/permission/qqbot-permission.controller';
|
|
import { QqbotRuleController } from '../../../../src/modules/qqbot/core/contract/rule/qqbot-rule.controller';
|
|
import { QqbotSendController } from '../../../../src/modules/qqbot/core/contract/send/qqbot-send.controller';
|
|
import {
|
|
QQBOT_CORE_CONTROLLERS,
|
|
QQBOT_CORE_ENTITIES,
|
|
QQBOT_CORE_EXPORTS,
|
|
QQBOT_CORE_PROVIDERS,
|
|
QqbotCoreModule,
|
|
} from '../../../../src/modules/qqbot/core/qqbot-core.module';
|
|
import {
|
|
collectControllerRoutes,
|
|
routeKey,
|
|
} from '../../../helpers/controller-route.helper';
|
|
import { readRefactorV3SqlSchema } from '../../../helpers/sql-schema.helper';
|
|
|
|
const getModuleMetadata = <T>(moduleClass: unknown, key: string): T[] => {
|
|
return Reflect.getMetadata(key, moduleClass) || [];
|
|
};
|
|
|
|
const getNames = (items: unknown[]) =>
|
|
items.map((item) => (item as { name?: string }).name || `${item}`);
|
|
|
|
type EntityClass = new (...args: never[]) => unknown;
|
|
|
|
const getEntityTableName = (entity: EntityClass) => {
|
|
return getMetadataArgsStorage().tables.find(
|
|
(table) => table.target === entity,
|
|
)?.name;
|
|
};
|
|
|
|
const getEntityColumnNames = (entity: EntityClass) => {
|
|
return getMetadataArgsStorage()
|
|
.columns.filter((column) => column.target === entity)
|
|
.map((column) => `${column.options.name || column.propertyName}`);
|
|
};
|
|
|
|
const getEntityNullableColumnNames = (entity: EntityClass) => {
|
|
return getMetadataArgsStorage()
|
|
.columns.filter((column) => column.target === entity)
|
|
.filter((column) => column.options.nullable === true)
|
|
.map((column) => `${column.options.name || column.propertyName}`);
|
|
};
|
|
|
|
const isOptionalSqlColumnForEntityInsert = (definition: string) => {
|
|
return (
|
|
!/\bNOT\s+NULL\b/i.test(definition) ||
|
|
/\bDEFAULT\b/i.test(definition) ||
|
|
/\bAUTO_INCREMENT\b/i.test(definition)
|
|
);
|
|
};
|
|
|
|
describe('QQBot core module contract', () => {
|
|
const schema = readRefactorV3SqlSchema();
|
|
|
|
it('keeps QQBot Admin and runtime routes compatible through the core boundary', () => {
|
|
const routes = collectControllerRoutes(QQBOT_CORE_CONTROLLERS);
|
|
|
|
expect(routes.map(routeKey)).toEqual(
|
|
expect.arrayContaining([
|
|
'GET /qqbot/account/list',
|
|
'GET /qqbot/account/enabled',
|
|
'POST /qqbot/account/bind/command',
|
|
'POST /qqbot/account/unbind/command',
|
|
'POST /qqbot/account/kick',
|
|
'GET /qqbot/command/list',
|
|
'POST /qqbot/command/test',
|
|
'GET /qqbot/conversation/list',
|
|
'GET /qqbot/message/list',
|
|
'GET /qqbot/permission/config',
|
|
'GET /qqbot/permission/allowlist',
|
|
'GET /qqbot/permission/blocklist',
|
|
'GET /qqbot/rule/list',
|
|
'GET /qqbot/send/log/list',
|
|
'POST /qqbot/send/private',
|
|
'POST /qqbot/send/group',
|
|
'GET /qqbot/dashboard/summary',
|
|
]),
|
|
);
|
|
expect(routes.map(routeKey)).not.toEqual(
|
|
expect.arrayContaining([
|
|
'POST /qqbot/account/scan/create',
|
|
'POST /qqbot/account/scan/refresh',
|
|
'GET /qqbot/account/scan/status',
|
|
'GET /qqbot/account/scan/events',
|
|
'POST /qqbot/account/scan/qrcode/refresh',
|
|
'POST /qqbot/account/scan/captcha/submit',
|
|
'POST /qqbot/account/scan/cancel',
|
|
]),
|
|
);
|
|
});
|
|
|
|
it('routes QQBot through the core module as the owning Nest boundary', () => {
|
|
const legacyWrapperName = ['Qqbot', 'Module'].join('');
|
|
const legacyWrapperPath = join(
|
|
process.cwd(),
|
|
'src',
|
|
'qqbot',
|
|
['qqbot', 'module.ts'].join('.'),
|
|
);
|
|
const appImports = getModuleMetadata(AppModule, MODULE_METADATA.IMPORTS);
|
|
|
|
expect(appImports).toEqual(
|
|
expect.arrayContaining([QqbotCoreModule]),
|
|
);
|
|
expect(getNames(appImports)).not.toContain(legacyWrapperName);
|
|
expect(existsSync(legacyWrapperPath)).toBe(false);
|
|
|
|
const coreImports = getModuleMetadata(
|
|
QqbotCoreModule,
|
|
MODULE_METADATA.IMPORTS,
|
|
);
|
|
expect(coreImports).toEqual(
|
|
expect.arrayContaining([ConfigModule, AdminAuthGuardModule, DictModule]),
|
|
);
|
|
expect(
|
|
coreImports.some(
|
|
(item) => (item as { module?: unknown }).module === TypeOrmModule,
|
|
),
|
|
).toBe(true);
|
|
expect(getNames(coreImports)).not.toContain(legacyWrapperName);
|
|
|
|
expect(
|
|
getModuleMetadata(QqbotCoreModule, MODULE_METADATA.CONTROLLERS),
|
|
).toEqual(expect.arrayContaining(QQBOT_CORE_CONTROLLERS));
|
|
expect(
|
|
getModuleMetadata(QqbotCoreModule, MODULE_METADATA.PROVIDERS),
|
|
).toEqual(expect.arrayContaining(QQBOT_CORE_PROVIDERS));
|
|
expect(getModuleMetadata(QqbotCoreModule, MODULE_METADATA.EXPORTS)).toEqual(
|
|
expect.arrayContaining(QQBOT_CORE_EXPORTS),
|
|
);
|
|
});
|
|
|
|
it('makes the legacy QQBot controllers, providers and entities explicit', () => {
|
|
expect(QQBOT_CORE_CONTROLLERS).toEqual(
|
|
expect.arrayContaining([
|
|
QqbotAccountController,
|
|
QqbotCommandController,
|
|
QqbotDashboardController,
|
|
QqbotMessageController,
|
|
QqbotPermissionController,
|
|
QqbotRuleController,
|
|
QqbotSendController,
|
|
]),
|
|
);
|
|
expect(getNames(QQBOT_CORE_PROVIDERS)).toEqual(
|
|
expect.arrayContaining([
|
|
'QqbotAccountService',
|
|
'QqbotBusService',
|
|
'QqbotCommandEngineService',
|
|
'QqbotCommandParserService',
|
|
'QqbotCommandService',
|
|
'QqbotConfigService',
|
|
'QqbotDashboardService',
|
|
'QqbotDedupeService',
|
|
'QqbotEventService',
|
|
'QqbotMessageService',
|
|
'QqbotPermissionService',
|
|
'QqbotRateLimitService',
|
|
'QqbotReplyTemplateService',
|
|
'QqbotReverseWsService',
|
|
'QqbotRuleEngineService',
|
|
'QqbotRuleService',
|
|
'QqbotSendService',
|
|
]),
|
|
);
|
|
expect(getNames(QQBOT_CORE_PROVIDERS)).not.toEqual(
|
|
expect.arrayContaining([
|
|
'NapcatDeviceIdentityService',
|
|
'NapcatLoginStateStoreService',
|
|
'QqbotNapcatContainerService',
|
|
'QqbotNapcatLoginService',
|
|
'QqbotNapcatWatchdogService',
|
|
]),
|
|
);
|
|
expect(getNames(QQBOT_CORE_ENTITIES)).toEqual(
|
|
expect.arrayContaining([
|
|
'QqbotAccount',
|
|
'QqbotAccountAbility',
|
|
'QqbotAllowlist',
|
|
'QqbotBlocklist',
|
|
'QqbotCommand',
|
|
'QqbotCommandLog',
|
|
'QqbotConfig',
|
|
'QqbotConversation',
|
|
'QqbotDedupe',
|
|
'QqbotMessage',
|
|
'QqbotRule',
|
|
'QqbotSendLog',
|
|
]),
|
|
);
|
|
expect(getNames(QQBOT_CORE_ENTITIES)).not.toEqual(
|
|
expect.arrayContaining([
|
|
'NapcatAccountBinding',
|
|
'NapcatContainer',
|
|
'NapcatDeviceIdentity',
|
|
'NapcatLoginChallengeEntity',
|
|
'NapcatLoginSession',
|
|
'NapcatRuntimeCleanup',
|
|
'QqbotAccountNapcat',
|
|
'QqbotNapcatContainer',
|
|
]),
|
|
);
|
|
});
|
|
|
|
it('keeps every registered QQBot core entity mapped to the refactor-v3 schema', () => {
|
|
for (const entity of QQBOT_CORE_ENTITIES) {
|
|
const tableName = getEntityTableName(entity);
|
|
const columns = getEntityColumnNames(entity);
|
|
const nullableColumns = getEntityNullableColumnNames(entity);
|
|
|
|
expect(tableName).toBeTruthy();
|
|
expect(schema.hasTable(tableName || '')).toBe(true);
|
|
schema.expectTableColumns(tableName || '', columns);
|
|
|
|
const sqlColumns = schema.getTableColumns(tableName || '');
|
|
const entityColumnNames = new Set(columns);
|
|
const requiredSqlOnlyColumns = sqlColumns
|
|
.filter((column) => !entityColumnNames.has(column.name))
|
|
.filter(
|
|
(column) => !isOptionalSqlColumnForEntityInsert(column.definition),
|
|
)
|
|
.map((column) => `${tableName}.${column.name}`);
|
|
|
|
expect(requiredSqlOnlyColumns).toEqual([]);
|
|
|
|
for (const nullableColumn of nullableColumns) {
|
|
const sqlColumn = sqlColumns.find(
|
|
(column) => column.name === nullableColumn,
|
|
);
|
|
expect(sqlColumn?.definition).not.toMatch(/\bNOT\s+NULL\b/i);
|
|
}
|
|
}
|
|
});
|
|
});
|