nestjs / nest

A progressive Node.js framework for building efficient, scalable, and enterprise-grade server-side applications with TypeScript/JavaScript 🚀
https://nestjs.com
MIT License
67.33k stars 7.59k forks source link

Circular Dependency when using TestingModule outside of spec file #1821

Closed jeanfortheweb closed 5 years ago

jeanfortheweb commented 5 years ago

I'm submitting a...


[ ] Regression 
[x] Bug report
[ ] Feature request
[ ] Documentation issue or request
[ ] Support request => Please do not submit support request here, instead post your question on Stack Overflow.

Current behavior

When creating the TestingModule(Builder) and the overrides outside of the actual spec file, Nest reports a circular dependency.

BUT: When I move all the code from my helper file to the actual spec file, it's working fine. 100% same code.

The Error:

    A circular dependency has been detected. Please, make sure that each side of a bidirectional relationships are decorated with "forwardRef()".

      at NestContainer.addProvider (node_modules/@nestjs/core/injector/container.js:92:19)
      at DependenciesScanner.insertProvider (node_modules/@nestjs/core/scanner.js:149:35)
      at providers.forEach.provider (node_modules/@nestjs/core/scanner.js:72:18)
          at Array.forEach (<anonymous>)
      at DependenciesScanner.reflectProviders (node_modules/@nestjs/core/scanner.js:71:19)
      at DependenciesScanner.scanModulesForDependencies (node_modules/@nestjs/core/scanner.js:52:18)

Expected behavior

No circular dependency (which does not happen when running the actual app) when overriding or creating a TestingModule outside of the spec file.

Minimal reproduction of the problem with instructions

Just put the overrideProvider...and such calls in a different file outside of the spec file, like helpers.ts

What is the motivation / use case for changing the behavior?

Im creating a set of test helpers for common overrides across multiple e2e test spec files. These helpers setup common overrides for me.

Environment


Nest version: 6.0.0


For Tooling issues:
- Node version: 10.15.3  
- Platform:  Mac 

Others:

jeanfortheweb commented 5 years ago

Here is the code of my helpers file:

import { getRepositoryToken } from '@nestjs/typeorm';
import { TestingModuleBuilder } from '@nestjs/testing';
import { Permission, PermissionService, PermissionFlags, Identifiable } from '../src/permission';
import { AuthStrategy } from '../src/auth/auth.strategy';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-strategy';
import { User } from '../src/user';
import { Type } from '@nestjs/common';
import { DeepPartial, Repository } from 'typeorm';
import * as faker from 'faker';
import * as supertest from 'supertest';
import { NestApplication } from '@nestjs/core';

faker.seed(2103);

interface Helper<T = any> {
  init(builder: TestingModuleBuilder): TestingModuleBuilder;
  helpers: { [K in keyof T]: any };
}

export type RepositoryHelper<T extends string, E> = {
  [K in T]: {
    generate(data?: DeepPartial<E>): E;
  } & Repository<E>
};

export function withRepositoryHelper<N extends string, T extends Identifiable>(
  name: N,
  type: Type<T>,
  creator: () => DeepPartial<T>,
): Helper<RepositoryHelper<N, T>> {
  let data: T[] = [];

  const generate = (data: DeepPartial<T> = {}) => {
    return {
      ...creator(),
      ...data,
    };
  };

  const repository = {
    create: jest.fn((data: DeepPartial<T>) => {
      const entity = new type();

      for (const [property, value] of Object.entries(data)) {
        entity[property as keyof T] = value;
      }

      return entity;
    }),
    find: jest.fn().mockImplementation(() => data),
    findOne: jest.fn().mockImplementation(() => faker.random.arrayElement(data)),
    save: jest.fn(user => user),
  };

  data = Array(10)
    .fill(0)
    .map(() => repository.create(creator()));

  return {
    init(builder: TestingModuleBuilder) {
      return builder.overrideProvider(getRepositoryToken(type)).useValue(repository);
    },
    helpers: {
      [name]: {
        generate,
        ...repository,
      },
    },
  };
}

export interface SecurityHelpers {
  security: {
    setAuthenticated(value: boolean): void;
    setGranted(flags: PermissionFlags): void;
  };
}

export function withSecurityHelper(user?: User): Helper<SecurityHelpers> {
  let authenticated: boolean = false;
  let granted: PermissionFlags = PermissionFlags.NONE;

  if (user === undefined) {
    user = new User();
    user.id = 1;
    user.createdAt = new Date();
    user.useruuid = '';
    user.email = 'foo@bar.com';
    user.password = 'xxx';
  }

  const authStrategy = class extends PassportStrategy(
    class extends Strategy {
      authenticate() {
        if (authenticated === false) {
          this.fail(400);
        } else {
          this.success(user);
        }
      }
    },
    'jwt',
  ) {
    validate(user: User) {
      return user;
    }
  };

  const permissionService = {
    granted: jest.fn(
      (securityIdentity: any, resourceIdentity: any, permission: PermissionFlags) => {
        return (granted & permission) === granted;
      },
    ),
  };

  return {
    init(builder) {
      return builder
        .overrideProvider(PermissionService)
        .useValue(permissionService)
        .overrideProvider(AuthStrategy)
        .useClass(authStrategy)
        .overrideProvider(getRepositoryToken(Permission))
        .useValue({});
    },
    helpers: {
      security: {
        setAuthenticated(value: boolean) {
          authenticated = value;
        },
        setGranted(flags: PermissionFlags) {
          granted = flags;
        },
      },
    },
  };
}

interface TestSuite<T> {
  init(builder: TestingModuleBuilder): Promise<void>;
  request(): supertest.SuperTest<supertest.Test>;
  helpers: T;
}

export function createTestSuite<H1>(): TestSuite<{}>;
export function createTestSuite<H1>(helper1: Helper<H1>): TestSuite<H1>;

export function createTestSuite<H1, H2>(
  helper1: Helper<H1>,
  helper2: Helper<H2>,
): TestSuite<H1 & H2>;

export function createTestSuite<H1, H2, H3>(
  helper1: Helper<H1>,
  helper2: Helper<H2>,
  helper3: Helper<H3>,
): TestSuite<H1 & H2 & H3>;

export function createTestSuite<H1, H2, H3, H4>(
  helper1: Helper<H1>,
  helper2: Helper<H2>,
  helper3: Helper<H3>,
  helper4: Helper<H4>,
): TestSuite<H1 & H2 & H3 & H4>;

export function createTestSuite<H1, H2, H3, H4, H5>(
  helper1: Helper<H1>,
  helper2: Helper<H2>,
  helper3: Helper<H3>,
  helper4: Helper<H4>,
  helper5: Helper<H5>,
): TestSuite<H1 & H2 & H3 & H4 & H5>;

export function createTestSuite<U>(...helpers: Helper[]) {
  let app: NestApplication;

  return {
    async init(builder: TestingModuleBuilder) {
      const testingModule = await helpers
        .reduce((builder, helper) => helper.init(builder), builder)
        .compile();

      app = testingModule.createNestApplication();
      await app.init();
    },
    request() {
      return supertest(app.getHttpServer());
    },
    helpers: helpers.reduce(
      (helpers, helper) => ({
        ...helpers,
        ...helper.helpers,
      }),
      {},
    ),
  };
}
jeanfortheweb commented 5 years ago

To clarify the current dependencies:

AuthModule <- TypeOrm PermissionModule <- TypeOrm UserModule <- AuthModule, PermissionModule, TypeOrm

There is actually no circular dependency at all. This is also why the actual app runs just fine.

kamilmysliwiec commented 5 years ago

Circular dependency error is being thrown when type passed to the addProvider is undefined. Since your issue appears only if you move your functions from one file to another one, I'd say that either of these: PermissionService or AuthStrategy is undefined. Frequently, barrel files lead to this issue (see here) so avoid them if you use any. Also, imports in your helper file (and its imported file) as well as relations between classes can also affect your code. Basically, you can add console.log in your init(builder) to check which one isn't defined in the runtime and afterwards, simply reorganise your code. It's the common TypeScript issue unfortunately.

lock[bot] commented 5 years ago

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.