mirror of
https://github.com/hexastack/hexabot
synced 2025-06-26 18:27:28 +00:00
227 lines
6.5 KiB
TypeScript
227 lines
6.5 KiB
TypeScript
/*
|
|
* Copyright © 2025 Hexastack. All rights reserved.
|
|
*
|
|
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
|
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
|
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
|
*/
|
|
|
|
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
|
import { ModuleMetadata, Provider } from '@nestjs/common';
|
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
|
import { ModelDefinition, MongooseModule } from '@nestjs/mongoose';
|
|
import { Test, TestingModule } from '@nestjs/testing';
|
|
|
|
import { LoggerService } from '@/logger/logger.service';
|
|
|
|
import { LifecycleHookManager } from '../generics/lifecycle-hook-manager';
|
|
|
|
type TTypeOrToken = [
|
|
new (...args: any[]) => any,
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
// @ts-expect-error
|
|
...(new (...args: any[]) => any[]),
|
|
];
|
|
|
|
type TModel = ModelDefinition | `${string}Model`;
|
|
|
|
type ToUnionArray<T> = (NonNullable<T> extends (infer U)[] ? U : never)[];
|
|
|
|
type buildTestingMocksProps<
|
|
P extends ModuleMetadata['providers'] = ModuleMetadata['providers'],
|
|
C extends ModuleMetadata['controllers'] = ModuleMetadata['controllers'],
|
|
> = ModuleMetadata & {
|
|
models?: TModel[];
|
|
} & (
|
|
| {
|
|
providers: NonNullable<P>;
|
|
controllers: NonNullable<C>;
|
|
autoInjectFrom: ('providers' | 'controllers')[];
|
|
}
|
|
| {
|
|
providers: NonNullable<P>;
|
|
autoInjectFrom?: 'providers'[];
|
|
}
|
|
| {
|
|
controllers: NonNullable<C>;
|
|
autoInjectFrom?: 'controllers'[];
|
|
}
|
|
| {
|
|
providers?: never;
|
|
controllers?: never;
|
|
autoInjectFrom?: never;
|
|
}
|
|
);
|
|
|
|
const findInstances = async <T extends TTypeOrToken>(
|
|
type: keyof TestingModule,
|
|
module: TestingModule,
|
|
typesOrTokens: T,
|
|
): Promise<{ [K in keyof T]: InstanceType<T[K]> }> =>
|
|
Promise.all(
|
|
typesOrTokens.map((typeOrToken) =>
|
|
module[type.toString()]<InstanceType<typeof typeOrToken>>(typeOrToken),
|
|
),
|
|
);
|
|
|
|
const extractInstances =
|
|
(type: keyof TestingModule, module: TestingModule) =>
|
|
async <T extends TTypeOrToken>(types: T) =>
|
|
await findInstances(type, module, types);
|
|
|
|
const getParamTypes = (provider: Provider) =>
|
|
Reflect.getMetadata('design:paramtypes', provider) || [];
|
|
|
|
const getClassDependencies = (parentClass: Provider): Provider[] => {
|
|
const dependencies: Provider[] = [];
|
|
const seenClasses = new Set<Provider>();
|
|
const classQueue: Provider[] = [parentClass];
|
|
|
|
while (classQueue.length > 0) {
|
|
const currentClass = classQueue.pop()!;
|
|
|
|
if (seenClasses.has(currentClass)) {
|
|
continue;
|
|
}
|
|
|
|
seenClasses.add(currentClass);
|
|
|
|
if (currentClass) {
|
|
getParamTypes(currentClass).forEach((paramType: Provider) => {
|
|
if (paramType && !seenClasses.has(paramType)) {
|
|
classQueue.push(paramType);
|
|
dependencies.push(paramType);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
return dependencies;
|
|
};
|
|
|
|
const getModel = (name: string, suffix = ''): ModelDefinition => {
|
|
const modelName = name.replace(suffix, '');
|
|
const model = LifecycleHookManager.getModel(modelName);
|
|
|
|
if (!model) {
|
|
throw new Error(`Unable to find model for name '${modelName}!'`);
|
|
}
|
|
|
|
return model;
|
|
};
|
|
|
|
const getNestedModels = (
|
|
dynamicProviders: Provider[],
|
|
suffix = '',
|
|
): ModelDefinition[] =>
|
|
dynamicProviders.reduce((acc, dynamicProvider) => {
|
|
if ('name' in dynamicProvider && dynamicProvider.name.endsWith(suffix)) {
|
|
const model = getModel(dynamicProvider.name, suffix);
|
|
acc.push(model);
|
|
}
|
|
|
|
return acc;
|
|
}, [] as ModelDefinition[]);
|
|
|
|
const filterNestedDependencies = (dependency: Provider) =>
|
|
dependency.valueOf().toString().slice(0, 6) === 'class ';
|
|
|
|
const getNestedDependencies = (dynamicProviders: Provider[]): Provider[] => {
|
|
const nestedDependencies = new Set<Provider>();
|
|
|
|
dynamicProviders.filter(filterNestedDependencies).forEach((provider) => {
|
|
getClassDependencies(provider)
|
|
.filter(filterNestedDependencies)
|
|
.forEach((dependency) => {
|
|
if (
|
|
!dynamicProviders.includes(dependency) &&
|
|
!dynamicProviders.find(
|
|
(dynamicProvider) =>
|
|
'provide' in dynamicProvider &&
|
|
dynamicProvider.provide === dependency,
|
|
)
|
|
) {
|
|
nestedDependencies.add(dependency);
|
|
}
|
|
});
|
|
});
|
|
|
|
return [...nestedDependencies];
|
|
};
|
|
|
|
const canInjectModels = (imports: buildTestingMocksProps['imports']): boolean =>
|
|
(imports || []).some(
|
|
(dynamicModule) =>
|
|
'module' in dynamicModule &&
|
|
dynamicModule.module.name === 'MongooseModule',
|
|
);
|
|
|
|
const getModels = (models: TModel[]): ModelDefinition[] =>
|
|
models.map((model) =>
|
|
typeof model === 'string' ? getModel(model, 'Model') : model,
|
|
);
|
|
|
|
export const buildTestingMocks = async ({
|
|
models = [],
|
|
imports = [],
|
|
providers = [],
|
|
controllers = [],
|
|
autoInjectFrom,
|
|
...rest
|
|
}: buildTestingMocksProps) => {
|
|
const nestedProviders = new Set<Provider>();
|
|
const injectionFrom = autoInjectFrom as ToUnionArray<typeof autoInjectFrom>;
|
|
const canAutoInjectFromProviders = injectionFrom?.includes('providers');
|
|
const canAutoInjectFromControllers = injectionFrom?.includes('controllers');
|
|
|
|
if (canAutoInjectFromProviders) {
|
|
[...providers, ...getNestedDependencies(providers)].forEach((provider) =>
|
|
nestedProviders.add(provider),
|
|
);
|
|
}
|
|
|
|
if (canAutoInjectFromControllers) {
|
|
[...getNestedDependencies(controllers)].forEach((controller) =>
|
|
nestedProviders.add(controller),
|
|
);
|
|
}
|
|
|
|
const module = await Test.createTestingModule({
|
|
imports: [
|
|
...(canInjectModels(imports)
|
|
? [
|
|
MongooseModule.forFeature([
|
|
...getModels(models),
|
|
...(autoInjectFrom
|
|
? getNestedModels([...nestedProviders], 'Repository')
|
|
: []),
|
|
]),
|
|
]
|
|
: []),
|
|
...imports,
|
|
],
|
|
providers: [
|
|
LoggerService,
|
|
EventEmitter2,
|
|
{
|
|
provide: CACHE_MANAGER,
|
|
useValue: {
|
|
del: jest.fn(),
|
|
set: jest.fn(),
|
|
get: jest.fn(),
|
|
},
|
|
},
|
|
...(autoInjectFrom ? [...nestedProviders] : []),
|
|
...providers,
|
|
],
|
|
controllers,
|
|
...rest,
|
|
}).compile();
|
|
|
|
return {
|
|
module,
|
|
getMocks: extractInstances('get', module),
|
|
resolveMocks: extractInstances('resolve', module),
|
|
};
|
|
};
|