UNPKG

@toss/nestjs-aop

Version:

<!-- PROJECT LOGO --> <br /> <div align="center"> <a href="https://github.com/toss/nestjs-aop"> <img src="https://toss.tech/wp-content/uploads/2022/11/tech-article-nest-js-02.png" alt="Logo" height="200"> </a>

490 lines (434 loc) 12.4 kB
import 'reflect-metadata'; import { Controller, Get, Injectable, Module, Scope } from '@nestjs/common'; import { FastifyAdapter } from '@nestjs/platform-fastify'; import { Test } from '@nestjs/testing'; import { AopModule } from '../aop.module'; import { AopTesting, AopTestingDecorator } from './fixture/aop-testing.decorator'; import { AopTestingModule } from './fixture/aop-testing.module'; describe('AopModule', () => { it('Lazy decorator overwrites the original function', async () => { @Injectable() class FooService { @AopTesting({ callback: () => { return 2; }, }) foo() { return 1; } } @Module({ providers: [FooService], exports: [FooService], }) class FooModule {} const module = await Test.createTestingModule({ imports: [ AopModule, FooModule, AopTestingModule.registerAsync({ imports: [FooModule], inject: [FooService], useFactory: (fooService: FooService) => { return [fooService]; }, }), ], }).compile(); const app = module.createNestApplication(new FastifyAdapter()); await app.init(); const fooService = app.get(FooService); expect(fooService.foo()).toMatchInlineSnapshot(`2`); }); it('Prototype of the overwritten function must be the original function', async () => { // set original property to true const SetOriginalTrue = () => { return (_: any, __: string | symbol, descriptor: PropertyDescriptor) => { descriptor.value['original'] = true; }; }; @Injectable() class FooService { @AopTesting({ callback: ({ wrapParams, args }) => { return wrapParams.method(...args); }, }) @SetOriginalTrue() foo() { return 1; } } @Module({ providers: [FooService], exports: [FooService], }) class FooModule {} const module = await Test.createTestingModule({ imports: [ AopModule, FooModule, AopTestingModule.registerAsync({ imports: [FooModule], inject: [FooService], useFactory: (fooService: FooService) => { return [fooService]; }, }), ], }).compile(); const app = module.createNestApplication(new FastifyAdapter()); await app.init(); const fooService = app.get(FooService); // Verify that the 'fooService.foo' object has no properties and its 'original' property is true expect(Object.keys(fooService.foo)).toMatchInlineSnapshot(`[]`); expect((fooService.foo as any)['original']).toBe(true); // Get the prototype of the 'fooService.foo' object and verify that it only has an 'original' property const proto = Object.getPrototypeOf(fooService.foo); expect(Object.keys(proto)).toMatchInlineSnapshot(` [ "original", ] `); expect((proto as any)['original']).toBe(true); }); it('Decorator order must be guaranteed', async () => { @Injectable() class FooService { @AopTesting({ callback: ({ wrapParams, args }) => { return wrapParams.method(...args) + '2'; }, }) @AopTesting({ callback: ({ wrapParams, args }) => { return wrapParams.method(...args) + '1'; }, }) multipleDecorated() { return '0'; } } @Module({ providers: [FooService], exports: [FooService], }) class FooModule {} const module = await Test.createTestingModule({ imports: [ AopModule, FooModule, AopTestingModule.registerAsync({ imports: [FooModule], inject: [FooService], useFactory: (fooService: FooService) => { return [fooService]; }, }), ], }).compile(); const app = module.createNestApplication(new FastifyAdapter()); await app.init(); const fooService = app.get(FooService); expect(fooService.multipleDecorated()).toMatchInlineSnapshot(`"012"`); }); it('Wrap should be executed only once in default scope', async () => { let wrapped = 0; @Injectable({ scope: Scope.DEFAULT }) class FooService { @AopTesting({ wrapCallback: () => { wrapped++; }, }) decorated() { return '0'; } } @Module({ providers: [FooService], exports: [FooService], }) class FooModule {} const module = await Test.createTestingModule({ imports: [ AopModule, FooModule, AopTestingModule.registerAsync({ imports: [FooModule], inject: [FooService], useFactory: (fooService: FooService) => { return [fooService]; }, }), ], }).compile(); const app = module.createNestApplication(new FastifyAdapter()); await app.init(); const fooService = app.get(FooService); fooService.decorated(); fooService.decorated(); fooService.decorated(); expect(wrapped).toBe(1); }); /** * There are codes that using `function.name`. * Therefore the codes below are necessary. * * ex) @nestjs/swagger */ it('decorated function should have "name" property', async () => { @Injectable() class FooService { @AopTesting({ callback: ({ wrapParams, args }) => { return wrapParams.method(...args); }, }) foo() { return '0'; } } @Controller() class FooController { @AopTesting({ callback: ({ wrapParams, args }) => { return wrapParams.method(...args); }, }) @Get() getFoo() { return '0'; } } @Module({ controllers: [FooController], providers: [FooService], exports: [FooService], }) class FooModule {} const module = await Test.createTestingModule({ imports: [ AopModule, FooModule, AopTestingModule.registerAsync({ imports: [FooModule], inject: [FooService], useFactory: (fooService: FooService) => { return [fooService]; }, }), ], }).compile(); const app = module.createNestApplication(new FastifyAdapter()); await app.init(); const fooService = app.get(FooService); const fooController = app.get(FooController); expect(fooService.foo.name).toStrictEqual('foo'); expect(fooController.getFoo.name).toStrictEqual('getFoo'); }); describe('this of the decorated function must be this', () => { it('With AopModule', async () => { let _this: unknown = undefined; @Injectable() class FooService { @AopTesting({ callback: ({ wrapParams, args }) => { return wrapParams.method(...args); }, }) foo() { // eslint-disable-next-line @typescript-eslint/no-this-alias _this = this; return '0'; } } @Module({ providers: [FooService], exports: [FooService], }) class FooModule {} const module = await Test.createTestingModule({ imports: [ AopModule, FooModule, AopTestingModule.registerAsync({ imports: [FooModule], inject: [FooService], useFactory: (fooService: FooService) => { return [fooService]; }, }), ], }).compile(); const app = module.createNestApplication(new FastifyAdapter()); await app.init(); const fooService = app.get(FooService); fooService.foo(); expect(_this).toBe(fooService); }); it('Without AopModule', async () => { let _this: unknown = undefined; @Injectable() class FooService { @AopTesting({ callback: ({ wrapParams, args }) => { return wrapParams.method(...args); }, }) foo() { // eslint-disable-next-line @typescript-eslint/no-this-alias _this = this; return '0'; } } @Module({ providers: [FooService], exports: [FooService], }) class FooModule {} const module = await Test.createTestingModule({ imports: [ // AopModule, FooModule, AopTestingModule.registerAsync({ imports: [FooModule], inject: [FooService], useFactory: (fooService: FooService) => { return [fooService]; }, }), ], }).compile(); const app = module.createNestApplication(new FastifyAdapter()); await app.init(); const fooService = app.get(FooService); fooService.foo(); expect(_this).toBe(fooService); }); }); it('Each instance should have its dependencies applied correctly', async () => { @Injectable() class FooService { constructor(private readonly value: number) {} @AopTesting({ callback: ({ wrapParams, args }) => { return wrapParams.method(...args); }, }) getValue() { return this.value; } } @Module({ providers: [ { provide: 'DUPLICATE_1', useValue: new FooService(1), }, { provide: 'DUPLICATE_2', useValue: new FooService(2), }, { provide: 'DUPLICATE_3', useValue: new FooService(3), }, ], exports: ['DUPLICATE_1', 'DUPLICATE_2', 'DUPLICATE_3'], }) class FooModule {} const module = await Test.createTestingModule({ imports: [ AopModule, FooModule, AopTestingModule.registerAsync({ imports: [FooModule], inject: ['DUPLICATE_1'], useFactory: (fooService: FooService) => { return [fooService]; }, }), ], }).compile(); const app = module.createNestApplication(new FastifyAdapter()); await app.init(); const duplicateService1: FooService = app.get('DUPLICATE_1'); const duplicateService2: FooService = app.get('DUPLICATE_2'); const duplicateService3: FooService = await app.resolve('DUPLICATE_3'); expect(duplicateService1.getValue()).toMatchInlineSnapshot(`1`); expect(duplicateService2.getValue()).toMatchInlineSnapshot(`2`); expect(duplicateService3.getValue()).toMatchInlineSnapshot(`3`); }); it('AopDecorator created using useFactory should also be functional', async () => { @Injectable() class FooService { @AopTesting({ callback: () => { return 2; }, }) foo() { return 1; } } @Module({ providers: [FooService], exports: [FooService], }) class FooModule {} const module = await Test.createTestingModule({ imports: [AopModule, FooModule], providers: [ { provide: AopTestingDecorator, useFactory: () => { return new AopTestingDecorator(); }, }, ], }).compile(); const app = module.createNestApplication(new FastifyAdapter()); await app.init(); const fooService: FooService = app.get(FooService); expect(fooService.foo()).toMatchInlineSnapshot(`2`); }); it('lazy decorator should be initialized only once per decorator if instance is static', async () => { @Injectable() class FooService { @AopTesting({ callback: () => { return 2; }, }) foo() { return 1; } } @Module({ providers: [FooService], exports: [FooService], }) class FooModule {} let aopDecoratorInitialized = 0; const module = await Test.createTestingModule({ imports: [AopModule, FooModule], providers: [ { provide: AopTestingDecorator, useFactory: () => { aopDecoratorInitialized++; return new AopTestingDecorator(); }, }, ], }).compile(); const app = module.createNestApplication(new FastifyAdapter()); await app.init(); const fooService: FooService = app.get(FooService); fooService.foo(); fooService.foo(); expect(aopDecoratorInitialized).toMatchInlineSnapshot(`1`); }); });