#Test file working properly in the context of a larger project but not in packages monorepo?

21 messages · Page 1 of 1 (latest)

winged blade
#

I have a project in which I have an azure-app-configuration module which wraps @azure/app-configuration.
For this module (specifically for the service I created that this module provides) I created a .spec test file using @golevelup/ts-jest.
I a mocking ConfigService like this:

    const configServiceMock = createMock<ConfigService>({
      get: jest.fn().mockReturnValue('Endpoint=*;Id=*;Secret=*'),
    });

    const moduleRef = await Test.createTestingModule({
      providers: [
        {
          provide: ConfigService,
          useValue: configServiceMock,
        },
        AzureAppConfigurationService,
      ],
    }).compile();

This works fine in the context of a project I made, but now I'm trying to move this logic to, at the end, a nodejs package.
In the context of that however, I am getting a weird error:
Nest can't resolve dependencies of the AzureAppConfigurationService (?). Please make sure that the argument Function at index [0] is available in the RootTestModule context
Notifce it calls the argument "Function", as if it can't figure out it's a ConfigService mock under the hood.
But again, this works perfectly in the context of the original project, what gives?
I'm pretty sure I kept the same jest config between these projects...

winged blade
#

Anyone?

plucky canopy
#

Okay, so this test file doesn't work in the separate package, but worked in the original application?

winged blade
#

Pretty much

plucky canopy
#

So, first thought, what is the tsconfig in the package?

#

Does it have experimentalDecorators and emitDecoratorMetadata?

winged blade
#

Mmm yes

#

This is the config:

{ 
     "compilerOptions": { 
         "declaration": true, 
         "declarationMap": true, 
         "noImplicitAny": false, 
         "removeComments": false, 
         "noLib": false, 
         "module": "ES2022", 
         "target": "ES2022", 
         "sourceMap": true, 
         "moduleResolution": "node", 
         "experimentalDecorators": true, 
         "emitDecoratorMetadata": true, 
         "esModuleInterop": true, 
         "baseUrl": ".", 
         "outDir": "dist", 
         "paths": { 
             "@eitanmedical/*": [ 
                 "./*/src" 
             ] 
         } 
     }, 
     "exclude": [ 
         "node_modules", 
         "test", 
         "dist", 
         "**/*spec.ts" 
     ] 
 }
plucky canopy
#

Hmm, that seems right. Can you show the service code? That error makes it seem that the ConfigService isn't being properly reflected for some reason

winged blade
#
describe(AzureAppConfigurationService.name, () => { 
   const testKey = 'testKey'; 
  
   let setting: GetConfigurationSettingResponse; 
  
   let service: AzureAppConfigurationService; 
   let configService: ConfigService; 
  
   beforeEach(async () => { 
     const configServiceMock = createMock<ConfigService>({ 
       get: jest.fn().mockReturnValue('Endpoint=*;Id=*;Secret=*'), 
     }); 
  
     const moduleRef = await Test.createTestingModule({ 
       providers: [ 
         { 
           provide: ConfigService, 
           useValue: configServiceMock, 
         }, 
         AzureAppConfigurationService, 
       ], 
     }).compile(); 
  
     setting = { 
       isReadOnly: false, 
       statusCode: 200, 
       _response: { 
         request: {} as WebResourceLike, 
         status: 200, 
         headers: createMock<HttpHeadersLike>(), 
         parsedHeaders: {}, 
         bodyAsText: '', 
       }, 
       key: '', 
       value: '', 
       contentType: '', 
     }; 
  
     service = moduleRef.get<AzureAppConfigurationService>(AzureAppConfigurationService); 
     configService = moduleRef.get<ConfigService>(ConfigService); 
   });
#

I'm using @golevelup/ts-jest btw

plucky canopy
#

Okay, but what about the actual service. AzureAppConfigurationService

winged blade
#
/* eslint-disable @typescript-eslint/naming-convention */ 
 import { Injectable } from '@nestjs/common'; 
 import { 
   AppConfigurationClient, 
   type GetConfigurationSettingResponse, 
 } from '@azure/app-configuration'; 
 import type { ConfigService } from '@nestjs/config'; 
 import { AZURE_APPCONFIG_ENDPOINT } from '../shared/constants.js'; 
 import { 
   isNullOrEmpty, 
   isNullOrUndefined, 
   ArgumentNullException, 
   UnknownArgumentException, 
 } from '@eitanmedical/common'; 
 import { SecretClient } from '@azure/keyvault-secrets'; 
 import { DefaultAzureCredential } from '@azure/identity'; 
  
 type Handler = (setting: GetConfigurationSettingResponse) => Promise<string>; 
  
 @Injectable() 
 export class AzureAppConfigurationService { 
   private readonly client: AppConfigurationClient; 
  
   private readonly handlers: Record<string, Handler> = { 
     '': this.handleValue, 
     'application/json': this.handleValue, 
     'text/plain': this.handleValue, 
     'application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8': 
       this.handleKeyVaultJsonRef, 
   }; 
  
   constructor(private readonly configService: ConfigService) { 
     const endpoint = configService.get<string>(AZURE_APPCONFIG_ENDPOINT); 
     const credential = new DefaultAzureCredential(); 
     if (isNullOrEmpty(endpoint)) { 
       throw new ArgumentNullException(`Configuration Key ${AZURE_APPCONFIG_ENDPOINT}`); 
     } 
     this.client = new AppConfigurationClient(endpoint, credential); 
   } 
#
  async getConfigurationSetting(key: string): Promise<string> { 
     const setting = await this.client.getConfigurationSetting({ 
       key, 
     }); 
     const contentType = setting.contentType ?? ''; 
     if (isNullOrUndefined(contentType)) { 
       throw new ArgumentNullException(`Azure App Configuration key[${key}] content type`); 
     } 
     if (!(contentType in this.handlers)) { 
       throw new UnknownArgumentException(`Azure App Configuration content type[${contentType}]`); 
     } 
     const value = await this.handlers[contentType](setting); 
     return value; 
   } 
  
   private async handleValue(setting: GetConfigurationSettingResponse): Promise<string> { 
     if (isNullOrEmpty(setting.value)) { 
       throw new ArgumentNullException(`Azure App Configuration key[${setting.key}] value`); 
     } 
     return setting.value; 
   } 
  
   private async handleKeyVaultJsonRef(setting: GetConfigurationSettingResponse): Promise<string> { 
     if (isNullOrEmpty(setting.value)) { 
       throw new ArgumentNullException(`Azure App Configuration key[${setting.key}] value`); 
     } 
     const { 
       uri, 
     }: { 
       uri: string; 
     } = JSON.parse(setting.value); 
     if (isNullOrEmpty(uri)) { 
       throw new ArgumentNullException( 
         `Azure App Configuration key[${setting.key}] JSON value[${setting.value}] uri`, 
       ); 
     } 
  
     const credential = new DefaultAzureCredential(); 
     const keyVaultUri = uri.substring(0, uri.indexOf('/secrets/')); 
     const secretName = uri.substring(uri.lastIndexOf('/') + 1); 
     const secretClient = new SecretClient(keyVaultUri, credential); 
     const secret = await secretClient.getSecret(secretName); 
     if (isNullOrEmpty(secret.value)) { 
       throw new ArgumentNullException(`Azure Key Vault key[${secretName}]`); 
     } 
  
     return secret.value; 
   } 
 }
plucky canopy
#

Ah! import type. Don't do that. mport { ConfigService } from '@nestjs/config'

#

import type makes Typescript not actually reflect the class type

winged blade
#

But does it explain why it works in the application context but not in my packages monorepo?

#

How does it work in my app?

plucky canopy
#

I'm gonna guess in the application it wasn't import type, but in the packages monorepo there was an eslint rule that added that on save that you didn't notice

winged blade
#

Huh you're right

#

I'll test it when I'm home