jeffijoe/awilix

View on GitHub
src/__tests__/load-modules.test.ts

Summary

Maintainability
D
2 days
Test Coverage
import * as path from 'path'
import { loadModules, LoadModulesOptions } from '../load-modules'
import { createContainer } from '../container'
import { Lifetime } from '../lifetime'
import { InjectionMode } from '../injection-mode'
import { asFunction, RESOLVER, BuildResolver, asValue } from '../resolvers'

const lookupResultFor = (modules: any) =>
  Object.keys(modules).map((key) => ({
    name: key.replace('.js', ''),
    path: key,
    opts: null,
  }))

describe('loadModules', () => {
  it('registers loaded modules with the container using the name of the file', () => {
    const container = createContainer()

    class SomeClass {}

    const modules: any = {
      'nope.js': undefined,
      'standard.js': jest.fn(() => 42),
      'default.js': { default: jest.fn(() => 1337) },
      'someClass.js': SomeClass,
    }
    const moduleLookupResult = lookupResultFor(modules)
    const deps = {
      container,
      listModules: jest.fn(() => moduleLookupResult),
      require: jest.fn((path) => modules[path]),
    }

    const result = loadModules(deps, 'anything')
    expect(result).toEqual({ loadedModules: moduleLookupResult })
    expect(Object.keys(container.registrations).length).toBe(3)
    expect(container.resolve('standard')).toBe(42)
    expect(container.resolve('default')).toBe(1337)
    expect(container.resolve('someClass')).toBeInstanceOf(SomeClass)
  })

  it('registers loaded modules async when using native modules', async () => {
    const container = createContainer()

    class SomeClass {}

    const modules: any = {
      'nope.js': undefined,
      'standard.js': jest.fn(() => 42),
      'default.js': { default: jest.fn(() => 1337) },
      'someClass.js': SomeClass,
    }

    const moduleLookupResult = lookupResultFor(modules)
    const deps = {
      container,
      listModules: jest.fn(() => moduleLookupResult),
      require: jest.fn(async (p) => modules[path.parse(p).base]),
    }

    const result = await loadModules(deps, 'anything', { esModules: true })
    expect(result).toEqual({ loadedModules: moduleLookupResult })
    expect(Object.keys(container.registrations).length).toBe(3)
    expect(container.resolve('standard')).toBe(42)
    expect(container.resolve('default')).toBe(1337)
    expect(container.resolve('someClass')).toBeInstanceOf(SomeClass)
  })

  it('registers non-default export modules containing RESOLVER token with the container', () => {
    const container = createContainer()

    class SomeNonDefaultClass {
      static [RESOLVER] = {}
    }

    const modules: any = {
      'someIgnoredName.js': { SomeNonDefaultClass },
    }
    const moduleLookupResult = lookupResultFor(modules)
    const deps = {
      container,
      listModules: jest.fn(() => moduleLookupResult),
      require: jest.fn((path) => modules[path]),
    }

    const result = loadModules(deps, 'anything')
    expect(result).toEqual({ loadedModules: moduleLookupResult })
    expect(Object.keys(container.registrations).length).toBe(1)
    // Note the capital first letter because the export key name is used instead of the filename
    expect(container.resolve('SomeNonDefaultClass')).toBeInstanceOf(
      SomeNonDefaultClass,
    )
  })

  it('does not register non-default modules without a RESOLVER token', () => {
    const container = createContainer()

    class SomeClass {}

    const modules: any = {
      'nopeClass.js': { SomeClass },
    }
    const moduleLookupResult = lookupResultFor(modules)
    const deps = {
      container,
      listModules: jest.fn(() => moduleLookupResult),
      require: jest.fn((path) => modules[path]),
    }

    const result = loadModules(deps, 'anything')
    expect(result).toEqual({ loadedModules: moduleLookupResult })
    expect(Object.keys(container.registrations).length).toBe(0)
  })

  it('registers multiple loaded modules from one file with the container', () => {
    const container = createContainer()

    class SomeClass {}
    class SomeNonDefaultClass {
      static [RESOLVER] = {}
    }
    class SomeNamedNonDefaultClass {
      static [RESOLVER] = {
        name: 'nameOverride',
      }
    }

    const modules: any = {
      'mixedFile.js': {
        default: SomeClass,
        nonDefault: SomeNonDefaultClass,
        namedNonDefault: SomeNamedNonDefaultClass,
      },
    }
    const moduleLookupResult = lookupResultFor(modules)
    const deps = {
      container,
      listModules: jest.fn(() => moduleLookupResult),
      require: jest.fn((path) => modules[path]),
    }

    const result = loadModules(deps, 'anything')
    expect(result).toEqual({ loadedModules: moduleLookupResult })
    expect(Object.keys(container.registrations).length).toBe(3)
    expect(container.resolve('mixedFile')).toBeInstanceOf(SomeClass)
    expect(container.resolve('nonDefault')).toBeInstanceOf(SomeNonDefaultClass)
    expect(container.resolve('nameOverride')).toBeInstanceOf(
      SomeNamedNonDefaultClass,
    )
  })

  it('registers only the last module with a certain name with the container', () => {
    const container = createContainer()

    class SomeClass {}
    class SomeNonDefaultClass {
      static [RESOLVER] = {}
    }
    class SomeNamedNonDefaultClass {
      static [RESOLVER] = {
        name: 'nameOverride',
      }
    }

    const modules: any = {
      'mixedFileOne.js': {
        default: SomeClass,
        nameOverride: SomeNonDefaultClass,
        // this will override the above named export with its specified name
        namedNonDefault: SomeNamedNonDefaultClass,
      },
      'mixedFileTwo.js': {
        // this will override the default export from mixedFileOne
        mixedFileOne: SomeNonDefaultClass,
      },
    }

    const moduleLookupResult = lookupResultFor(modules)
    const deps = {
      container,
      listModules: jest.fn(() => moduleLookupResult),
      require: jest.fn((path) => modules[path]),
    }

    const result = loadModules(deps, 'anything')
    expect(result).toEqual({ loadedModules: moduleLookupResult })
    expect(Object.keys(container.registrations).length).toBe(2)
    expect(container.resolve('mixedFileOne')).toBeInstanceOf(
      SomeNonDefaultClass,
    )
    expect(container.resolve('nameOverride')).toBeInstanceOf(
      SomeNamedNonDefaultClass,
    )
  })

  it('uses built-in formatter when given a formatName as a string', () => {
    const container = createContainer()
    const modules: any = {
      'SomeClass.js': jest.fn(() => 42),
    }
    const moduleLookupResult = lookupResultFor(modules)
    const deps = {
      container,
      listModules: jest.fn(() => moduleLookupResult),
      require: jest.fn((path) => modules[path]),
    }
    const opts: LoadModulesOptions = {
      formatName: 'camelCase',
    }
    const result = loadModules(deps, 'anything', opts)
    expect(result).toEqual({ loadedModules: moduleLookupResult })
    const reg = container.registrations.someClass
    expect(reg).toBeTruthy()
  })

  it('uses the function passed in as formatName', () => {
    const container = createContainer()
    const modules: any = {
      'SomeClass.js': jest.fn(() => 42),
    }
    const moduleLookupResult = lookupResultFor(modules)
    const deps = {
      container,
      listModules: jest.fn(() => moduleLookupResult),
      require: jest.fn((path) => modules[path]),
    }
    const opts: LoadModulesOptions = {
      formatName: (name, descriptor) => {
        expect(descriptor.path).toBeTruthy()
        return name + 'IsGreat'
      },
    }
    const result = loadModules(deps, 'anything', opts)
    expect(result).toEqual({ loadedModules: moduleLookupResult })
    const reg = container.registrations.SomeClassIsGreat
    expect(reg).toBeTruthy()
  })

  it('does nothing with the name if the string formatName does not match a formatter', () => {
    const container = createContainer()
    const modules: any = {
      'SomeClass.js': jest.fn(() => 42),
    }
    const moduleLookupResult = lookupResultFor(modules)
    const deps = {
      container,
      listModules: jest.fn(() => moduleLookupResult),
      require: jest.fn((path) => modules[path]),
    }
    const opts: any = {
      formatName: 'unknownformatternope',
    }
    const result = loadModules(deps, 'anything', opts)
    expect(result).toEqual({ loadedModules: moduleLookupResult })
    const reg = container.registrations.SomeClass
    expect(reg).toBeTruthy()
  })

  it('defaults to transient lifetime if option is unreadable', () => {
    const container = createContainer()
    const modules: any = {
      'test.js': jest.fn(() => 42),
    }
    const moduleLookupResult = lookupResultFor(modules)
    const deps = {
      container,
      listModules: jest.fn(() => moduleLookupResult),
      require: jest.fn((path) => modules[path]),
    }
    const opts = {
      resolverOptions: {},
    }
    const result = loadModules(deps, 'anything', opts)
    expect(result).toEqual({ loadedModules: moduleLookupResult })
    const reg = container.registrations.test
    expect(reg).toBeTruthy()
  })

  it('supports passing in a register function', () => {
    const container = createContainer()
    const moduleSpy = jest.fn(() => () => 42)
    const modules: any = {
      'test.js': moduleSpy,
    }
    const moduleLookupResult = lookupResultFor(modules)
    const registerSpy = jest.fn(asFunction)
    const deps = {
      container,
      listModules: jest.fn(() => moduleLookupResult),
      require: jest.fn((path) => modules[path]),
    }
    const regOpts = {
      register: registerSpy,
      lifetime: Lifetime.SCOPED,
    }
    const opts = {
      resolverOptions: regOpts,
    }
    const result = loadModules(deps, 'anything', opts)
    expect(result).toEqual({ loadedModules: moduleLookupResult })
    const reg = container.registrations.test
    expect(reg).toBeTruthy()
    expect(registerSpy).toHaveBeenCalledWith(moduleSpy, regOpts)
  })

  it('supports array opts syntax with string (lifetime)', () => {
    const container = createContainer()
    const modules: any = {
      'test.js': jest.fn(() => 42),
      'test2.js': jest.fn(() => 42),
    }

    const deps = {
      container,
      listModules: jest.fn(() => [
        { name: 'test', path: 'test.js', opts: Lifetime.SCOPED },
        { name: 'test2', path: 'test2.js', opts: null },
      ]),
      require: jest.fn((path) => modules[path]),
    }

    loadModules(deps, 'anything', {
      resolverOptions: {
        lifetime: Lifetime.SINGLETON,
      },
    })

    expect(container.registrations.test.lifetime).toBe(Lifetime.SCOPED)
    expect(container.registrations.test2.lifetime).toBe(Lifetime.SINGLETON)
  })

  it('supports array opts syntax with object', () => {
    const container = createContainer()
    const modules: any = {
      'test.js': jest.fn(() => 42),
      'test2.js': jest.fn(() => 42),
    }

    const deps = {
      container,
      listModules: jest.fn(() => [
        { name: 'test', path: 'test.js', opts: { lifetime: Lifetime.SCOPED } },
        { name: 'test2', path: 'test2.js', opts: null },
      ]),
      require: jest.fn((path) => modules[path]),
    }

    loadModules(deps, 'anything', {
      resolverOptions: {
        lifetime: Lifetime.SINGLETON,
      },
    })

    expect(container.registrations.test.lifetime).toBe(Lifetime.SCOPED)
    expect(container.registrations.test2.lifetime).toBe(Lifetime.SINGLETON)
  })

  it('supports passing in a default injectionMode', () => {
    const container = createContainer()
    const modules: any = {
      'test.js': jest.fn(() => 42),
      'test2.js': jest.fn(() => 42),
    }

    const deps = {
      container,
      listModules: jest.fn(() => [
        {
          name: 'test',
          path: 'test.js',
          opts: { injectionMode: InjectionMode.PROXY },
        },
        { name: 'test2', path: 'test2.js', opts: null },
      ]),
      require: jest.fn((path) => modules[path]),
    }

    loadModules(deps, 'anything', {
      resolverOptions: {
        injectionMode: InjectionMode.CLASSIC,
      },
    })

    expect(
      (container.registrations.test as BuildResolver<any>).injectionMode,
    ).toBe(InjectionMode.PROXY)
    expect(
      (container.registrations.test2 as BuildResolver<any>).injectionMode,
    ).toBe(InjectionMode.CLASSIC)
  })

  describe('inline config via REGISTRATION symbol', () => {
    it('uses the inline config over anything else', () => {
      const container = createContainer()
      const test1Func = jest.fn(() => 42)
      ;(test1Func as any)[RESOLVER] = {
        injectionMode: InjectionMode.PROXY,
      }

      class Test2Class {}

      ;(Test2Class as any)[RESOLVER] = {
        lifetime: Lifetime.SCOPED,
      }

      class Test3Class {}

      ;(Test3Class as any)[RESOLVER] = {
        register: asValue,
      }

      const modules: any = {
        'test.js': test1Func,
        'test2.js': Test2Class,
        'test3.js': Test3Class,
      }

      const deps = {
        container,
        listModules: jest.fn(() => [
          { name: 'test', path: 'test.js', opts: null },
          { name: 'test2', path: 'test2.js', opts: null },
          { name: 'test3', path: 'test3.js', opts: null },
        ]),
        require: jest.fn((path) => modules[path]),
      }

      loadModules(deps, 'anything', {
        resolverOptions: {
          injectionMode: InjectionMode.CLASSIC,
        },
      })

      expect(container.registrations.test.lifetime).toBe(Lifetime.TRANSIENT)
      expect(
        (container.registrations.test as BuildResolver<any>).injectionMode,
      ).toBe(InjectionMode.PROXY)
      expect(container.registrations.test2.lifetime).toBe(Lifetime.SCOPED)
      expect(
        (container.registrations.test2 as BuildResolver<any>).injectionMode,
      ).toBe(InjectionMode.CLASSIC)
      expect(container.resolve('test3')).toBe(Test3Class)
    })

    it('allows setting a name to register as', () => {
      const container = createContainer()
      const test1Func = jest.fn(() => 42)
      ;(test1Func as any)[RESOLVER] = {
        name: 'awesome',
        lifetime: Lifetime.SINGLETON,
      }

      const test2Func = jest.fn(() => 42)
      const modules: any = {
        'test.js': test1Func,
        'test2.js': test2Func,
      }

      const deps = {
        container,
        listModules: jest.fn(() => [
          { name: 'test', path: 'test.js', opts: null },
          { name: 'test2', path: 'test2.js', opts: null },
        ]),
        require: jest.fn((path) => modules[path]),
      }

      loadModules(deps, 'anything', {
        formatName: () => 'formatNameCalled',
        resolverOptions: {
          lifetime: Lifetime.SCOPED,
        },
      })

      expect(container.registrations.awesome.lifetime).toBe(Lifetime.SINGLETON)
      expect(container.registrations.formatNameCalled.lifetime).toBe(
        Lifetime.SCOPED,
      )
    })
  })
})