kt-template-online-api/test/modules/qqbot/napcat/napcat-module-ownership.spec.ts

189 lines
6.6 KiB
TypeScript

import { existsSync, readFileSync } 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 { AppModule } from '../../../../src/app.module';
import { QQBOT_ACCOUNT_NAPCAT_RUNTIME_PORT } from '../../../../src/modules/qqbot/core/application/account/qqbot-account-napcat-runtime.port';
import { QqbotCoreModule } from '../../../../src/modules/qqbot/core/qqbot-core.module';
import {
QQBOT_NAPCAT_CONTROLLERS,
QQBOT_NAPCAT_ENTITIES,
QQBOT_NAPCAT_EXPORTS,
QQBOT_NAPCAT_PROVIDERS,
QqbotNapcatModule,
} from '../../../../src/modules/qqbot/napcat/qqbot-napcat.module';
import { QqbotNapcatAccountRuntimeService } from '../../../../src/modules/qqbot/napcat/application/account-runtime/qqbot-napcat-account-runtime.service';
import { QqbotNapcatLoginController } from '../../../../src/modules/qqbot/napcat/contract/qqbot-napcat-login.controller';
import { QqbotNapcatContainerService } from '../../../../src/modules/qqbot/napcat/infrastructure/integration/container/qqbot-napcat-container.service';
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) => {
if (typeof item === 'symbol') return item.description || item.toString();
return (item as { name?: string }).name || `${item}`;
});
const unwrapForwardRef = (item: unknown) => {
const maybeForwardRef = item as { forwardRef?: () => unknown };
return typeof maybeForwardRef.forwardRef === 'function'
? maybeForwardRef.forwardRef()
: item;
};
type EntityClass = new (...args: never[]) => unknown;
const getEntityTableName = (entity: EntityClass) => {
return getMetadataArgsStorage().tables.find(
(table) => table.target === entity,
)?.name;
};
describe('QQBot NapCat module ownership', () => {
const schema = readRefactorV3SqlSchema();
it('owns scan, captcha, QR refresh and SSE routes under the NapCat boundary', () => {
const routes = collectControllerRoutes(QQBOT_NAPCAT_CONTROLLERS);
expect(QQBOT_NAPCAT_CONTROLLERS).toEqual(
expect.arrayContaining([QqbotNapcatLoginController]),
);
expect(routes.map(routeKey)).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('is imported as its own Nest module and keeps contract files in napcat/contract', () => {
const contractControllerPath = join(
process.cwd(),
'src',
'modules',
'qqbot',
'napcat',
'contract',
'qqbot-napcat-login.controller.ts',
);
const appImports = getModuleMetadata(AppModule, MODULE_METADATA.IMPORTS);
const napcatImports = getModuleMetadata(
QqbotNapcatModule,
MODULE_METADATA.IMPORTS,
).map(unwrapForwardRef);
expect(existsSync(contractControllerPath)).toBe(true);
expect(appImports).toEqual(expect.arrayContaining([QqbotNapcatModule]));
expect(napcatImports).toEqual(
expect.arrayContaining([
ConfigModule,
AdminAuthGuardModule,
QqbotCoreModule,
]),
);
expect(
napcatImports.some(
(item) => (item as { module?: unknown }).module === TypeOrmModule,
),
).toBe(true);
});
it('registers NapCat runtime providers and entities outside QQBot core', () => {
expect(getNames(QQBOT_NAPCAT_PROVIDERS)).toEqual(
expect.arrayContaining([
'NapcatDeviceIdentityService',
'NapcatLoginStateStoreService',
'QqbotNapcatAccountRuntimeService',
'QqbotNapcatContainerService',
'QqbotNapcatLoginService',
'QqbotNapcatWatchdogService',
]),
);
expect(getNames(QQBOT_NAPCAT_ENTITIES)).toEqual(
expect.arrayContaining([
'NapcatAccountBinding',
'NapcatContainer',
'NapcatDeviceIdentity',
'NapcatLoginChallengeEntity',
'NapcatLoginSession',
'NapcatRuntimeCleanup',
]),
);
expect(getNames(QQBOT_NAPCAT_ENTITIES)).not.toEqual(
expect.arrayContaining(['QqbotAccountNapcat', 'QqbotNapcatContainer']),
);
expect(getNames(QQBOT_NAPCAT_EXPORTS)).toEqual(
expect.arrayContaining([
'NapcatDeviceIdentityService',
'NapcatLoginStateStoreService',
'QQBOT_ACCOUNT_NAPCAT_RUNTIME_PORT',
'QqbotNapcatLoginService',
]),
);
expect(QQBOT_NAPCAT_EXPORTS).not.toContain(QqbotNapcatContainerService);
expect(QQBOT_NAPCAT_PROVIDERS).toEqual(
expect.arrayContaining([
expect.objectContaining({
provide: QQBOT_ACCOUNT_NAPCAT_RUNTIME_PORT,
useExisting: QqbotNapcatAccountRuntimeService,
}),
]),
);
});
it('keeps every registered NapCat entity mapped to the refactor-v3 schema', () => {
for (const entity of QQBOT_NAPCAT_ENTITIES) {
const tableName = getEntityTableName(entity);
expect(tableName).toBeTruthy();
expect(schema.hasTable(tableName || '')).toBe(true);
}
});
it('does not keep legacy qqbot_napcat tables as executable schema truth', () => {
const schemaSql = readFileSync(
join(process.cwd(), 'sql', 'refactor-v3', '00-full-schema.sql'),
'utf8',
);
const qqbotInitSql = readFileSync(
join(process.cwd(), 'sql', 'qqbot-init.sql'),
'utf8',
);
expect(schemaSql).not.toContain(
'CREATE TABLE IF NOT EXISTS qqbot_account_napcat',
);
expect(schemaSql).not.toContain(
'CREATE TABLE IF NOT EXISTS qqbot_napcat_container',
);
expect(qqbotInitSql).not.toContain('qqbot_account_napcat');
expect(qqbotInitSql).not.toContain('qqbot_napcat_container');
expect(qqbotInitSql).toContain(
'CREATE TABLE IF NOT EXISTS `napcat_account_binding`',
);
expect(qqbotInitSql).toContain(
'CREATE TABLE IF NOT EXISTS `napcat_container`',
);
expect(
getNames(QQBOT_NAPCAT_ENTITIES).filter((name) =>
name.startsWith('Qqbot'),
),
).toEqual([]);
});
});