Merge pull request #16369 from nestjs/build/esm-migration

build: migrate from cjs to esm
This commit is contained in:
Kamil Mysliwiec
2026-02-15 16:52:54 +01:00
committed by GitHub
1531 changed files with 23930 additions and 17058 deletions

View File

@@ -1,12 +1,6 @@
version: 2.1
parameters:
check-legacy-node-version:
type: boolean
default: false
legacy-node-version:
type: string
default: '18.20'
maintenance-node-version:
type: string
default: '20.18'
@@ -15,7 +9,7 @@ parameters:
default: '22.11'
current-node-version:
type: string
default: '23.3'
default: '24.1'
aliases:
- &restore-cache
@@ -62,61 +56,36 @@ jobs:
docker:
- image: cimg/node:<< parameters.node-version >>
steps:
- checkout
- *restore-cache
- *install-deps
- *build-packages
- when:
condition:
and:
- equal:
[
'<< parameters.node-version >>',
'<< pipeline.parameters.legacy-node-version >>',
]
- not: << pipeline.parameters.check-legacy-node-version >>
equal:
[
'<< parameters.node-version >>',
'<< pipeline.parameters.maintenance-node-version >>',
]
steps:
- run:
name: Skip
command: |
echo Skipping
name: Test (coverage)
command: npm run test:cov
- run:
name: Collect coverage
command: npm run coverage
- store_artifacts:
path: coverage
- when:
condition:
or:
- not:
equal:
[
'<< parameters.node-version >>',
'<< pipeline.parameters.legacy-node-version >>',
]
- << pipeline.parameters.check-legacy-node-version >>
not:
equal:
[
'<< parameters.node-version >>',
'<< pipeline.parameters.maintenance-node-version >>',
]
steps:
- checkout
- *restore-cache
- *install-deps
- *build-packages
- when:
condition:
equal:
[
'<< parameters.node-version >>',
'<< pipeline.parameters.maintenance-node-version >>',
]
steps:
- run:
name: Test (coverage)
command: npm run test:cov
- run:
name: Collect coverage
command: npm run coverage
- store_artifacts:
path: coverage
- when:
condition:
not:
equal:
[
'<< parameters.node-version >>',
'<< pipeline.parameters.maintenance-node-version >>',
]
steps:
- *run-unit-tests
- *run-unit-tests
lint:
working_directory: ~/nest
@@ -183,7 +152,6 @@ jobs:
name: Build all samples
command: npm run build:samples
workflows:
build-and-test:
jobs:
@@ -195,7 +163,6 @@ workflows:
parameters:
node-version:
[
'<< pipeline.parameters.legacy-node-version >>',
'<< pipeline.parameters.maintenance-node-version >>',
'<< pipeline.parameters.active-node-version >>',
'<< pipeline.parameters.current-node-version >>',
@@ -209,4 +176,3 @@ workflows:
- samples:
requires:
- build

View File

@@ -116,7 +116,6 @@ We cannot accept code without this.
1. In GitHub, send a pull request to `nestjs:master`.
- If we suggest changes then:
- Make the required updates.
- Re-run the Nest test suites to ensure tests are still passing.
- Rebase your branch and force push to your GitHub repository (this will update your Pull Request):
@@ -159,7 +158,7 @@ from the main (upstream) repository:
## <a name="development"></a> Development Setup
You will need [Node.js](https://nodejs.org) version >= 10.13.0 (except for v13).
You will need [Node.js](https://nodejs.org) version >= 20.
1. After cloning the repo, run:
@@ -322,8 +321,10 @@ changes to be accepted, the CLA must be signed. It's a quick process, we promise
<!-- [coc]: https://github.com/angular/code-of-conduct/blob/master/CODE_OF_CONDUCT.md -->
[commit-message-format]: https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit#
<!-- [individual-cla]: http://code.google.com/legal/individual-cla-v1.0.html -->
<!-- [corporate-cla]: http://code.google.com/legal/corporate-cla-v1.0.html -->
[dev-doc]: https://github.com/nestjs/nest/blob/master/docs/DEVELOPER.md
[github]: https://github.com/nestjs/nest
[stackoverflow]: https://stackoverflow.com/questions/tagged/nestjs

View File

@@ -1,16 +0,0 @@
'use strict';
/**
* Load the TypeScript compiler, then load the TypeScript gulpfile which simply loads all
* the tasks. The tasks are really inside tools/gulp/tasks.
*/
const path = require('path');
const projectDir = __dirname;
const tsconfigPath = path.join(projectDir, 'tools/gulp/tsconfig.json');
require('ts-node').register({
project: tsconfigPath
});
require('./tools/gulp/gulpfile');

13
gulpfile.mjs Normal file
View File

@@ -0,0 +1,13 @@
/**
* Load the TypeScript compiler, then load the TypeScript gulpfile which simply loads all
* the tasks. The tasks are really inside tools/gulp/tasks.
*/
import { register } from 'node:module';
import { pathToFileURL } from 'node:url';
register('ts-node/esm', pathToFileURL('./'), {
data: { project: './tools/gulp/tsconfig.json' },
});
await import('./tools/gulp/gulpfile.ts');

View File

@@ -1,7 +0,0 @@
export const mochaHooks = (): Mocha.RootHookObject => {
return {
async beforeAll(this: Mocha.Context) {
await import('reflect-metadata');
},
};
};

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@nestjs/common';
import { FooService } from './foo.service';
import { FooService } from './foo.service.js';
@Injectable()
export class BarService {

View File

@@ -1,17 +1,11 @@
import { Test } from '@nestjs/testing';
import * as chai from 'chai';
import * as chaiAsPromised from 'chai-as-promised';
import * as sinon from 'sinon';
import { BarService } from '../src/bar.service';
import { FooService } from '../src/foo.service';
chai.use(chaiAsPromised);
const { expect } = chai;
import { BarService } from '../src/bar.service.js';
import { FooService } from '../src/foo.service.js';
describe('Auto-Mocking Bar Deps', () => {
let service: BarService;
let fooService: FooService;
const stub = sinon.stub();
const stub = vi.fn();
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
providers: [BarService],
@@ -23,12 +17,12 @@ describe('Auto-Mocking Bar Deps', () => {
});
it('should be defined', () => {
expect(service).not.to.be.undefined;
expect(fooService).not.to.be.undefined;
expect(service).not.toBeUndefined();
expect(fooService).not.toBeUndefined();
});
it('should call bar.bar', () => {
service.bar();
expect(stub.called);
expect(stub).toHaveBeenCalled();
});
});
@@ -39,23 +33,25 @@ describe('Auto-Mocking with token in factory', () => {
})
.useMocker(token => {
if (token === FooService) {
return { foo: sinon.stub };
return { foo: vi.fn() };
}
})
.compile();
const service = moduleRef.get(BarService);
const fooServ = moduleRef.get<{ foo: sinon.SinonStub }>(FooService as any);
const fooServ = moduleRef.get<{ foo: ReturnType<typeof vi.fn> }>(
FooService as any,
);
service.bar();
expect(fooServ.foo.called);
expect(fooServ.foo).toHaveBeenCalled();
});
it('cannot mock the dependencies', async () => {
const moduleRef = Test.createTestingModule({
providers: [BarService],
}).useMocker(token => {
if (token === FooService.name + 'something that fails the token') {
return { foo: sinon.stub };
return { foo: vi.fn() };
}
}).compile;
expect(moduleRef()).to.eventually.throw();
await expect(moduleRef()).rejects.toThrow();
});
});

View File

@@ -1,13 +1,15 @@
{
"compilerOptions": {
"types": ["vitest/globals"],
"module": "commonjs",
"declaration": false,
"noImplicitAny": false,
"removeComments": true,
"noLib": false,
"esModuleInterop": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "ES2021",
"target": "ES2023",
"sourceMap": true,
"allowJs": true,
"strictNullChecks": true,

View File

@@ -1,7 +1,7 @@
import { NestExpressApplication } from '@nestjs/platform-express';
import { Test } from '@nestjs/testing';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
import request from 'supertest';
import { AppModule } from '../src/app.module.js';
describe('Express Cors', () => {
let app: NestExpressApplication;
@@ -25,7 +25,7 @@ describe('Express Cors', () => {
];
describe('Dynamic config', () => {
describe('enableCors', () => {
before(async () => {
beforeAll(async () => {
const module = await Test.createTestingModule({
imports: [AppModule],
}).compile();
@@ -66,13 +66,13 @@ describe('Express Cors', () => {
.expect('content-length', '0');
});
after(async () => {
afterAll(async () => {
await app.close();
});
});
describe('Application Options', () => {
before(async () => {
beforeAll(async () => {
const module = await Test.createTestingModule({
imports: [AppModule],
}).compile();
@@ -114,14 +114,14 @@ describe('Express Cors', () => {
.expect('content-length', '0');
});
after(async () => {
afterAll(async () => {
await app.close();
});
});
});
describe('Static config', () => {
describe('enableCors', () => {
before(async () => {
beforeAll(async () => {
const module = await Test.createTestingModule({
imports: [AppModule],
}).compile();
@@ -141,14 +141,14 @@ describe('Express Cors', () => {
.expect('access-control-expose-headers', 'foo,bar')
.expect('content-length', '0');
});
});
after(async () => {
await app.close();
afterAll(async () => {
await app.close();
});
});
describe('Application Options', () => {
before(async () => {
beforeAll(async () => {
const module = await Test.createTestingModule({
imports: [AppModule],
}).compile();
@@ -169,7 +169,7 @@ describe('Express Cors', () => {
.expect('content-length', '0');
});
after(async () => {
afterAll(async () => {
await app.close();
});
});

View File

@@ -3,8 +3,8 @@ import {
NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { Test } from '@nestjs/testing';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
import request from 'supertest';
import { AppModule } from '../src/app.module.js';
describe.skip('Fastify Cors', () => {
let app: NestFastifyApplication;
@@ -28,7 +28,7 @@ describe.skip('Fastify Cors', () => {
];
describe('Dynamic config', () => {
describe('enableCors', () => {
before(async () => {
beforeAll(async () => {
const module = await Test.createTestingModule({
imports: [AppModule],
}).compile();
@@ -73,13 +73,13 @@ describe.skip('Fastify Cors', () => {
.expect('content-length', '0');
});
after(async () => {
afterAll(async () => {
await app.close();
});
});
describe('Application Options', () => {
before(async () => {
beforeAll(async () => {
const module = await Test.createTestingModule({
imports: [AppModule],
}).compile();
@@ -124,7 +124,7 @@ describe.skip('Fastify Cors', () => {
.expect('content-length', '0');
});
after(async () => {
afterAll(async () => {
await app.close();
});
});
@@ -132,7 +132,7 @@ describe.skip('Fastify Cors', () => {
describe('Static config', () => {
describe('enableCors', () => {
before(async () => {
beforeAll(async () => {
const module = await Test.createTestingModule({
imports: [AppModule],
}).compile();
@@ -154,14 +154,14 @@ describe.skip('Fastify Cors', () => {
.expect('access-control-expose-headers', 'foo,bar')
.expect('content-length', '0');
});
});
after(async () => {
await app.close();
afterAll(async () => {
await app.close();
});
});
describe('Application Options', () => {
before(async () => {
beforeAll(async () => {
const module = await Test.createTestingModule({
imports: [AppModule],
}).compile();
@@ -184,10 +184,10 @@ describe.skip('Fastify Cors', () => {
.expect('access-control-expose-headers', 'foo,bar')
.expect('content-length', '0');
});
});
after(async () => {
await app.close();
afterAll(async () => {
await app.close();
});
});
});
});

View File

@@ -1,5 +1,5 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppController } from './app.controller.js';
@Module({
controllers: [AppController],

View File

@@ -1,13 +1,15 @@
{
"compilerOptions": {
"types": ["vitest/globals"],
"module": "commonjs",
"declaration": false,
"noImplicitAny": false,
"removeComments": true,
"noLib": false,
"esModuleInterop": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "ES2021",
"target": "ES2023",
"sourceMap": true,
"allowJs": true,
"strictNullChecks": true,

View File

@@ -1,9 +1,8 @@
import { Test, TestingModule } from '@nestjs/testing';
import { DiscoveryService } from '@nestjs/core';
import { expect } from 'chai';
import { AppModule } from '../src/app.module';
import { WebhooksExplorer } from '../src/webhooks.explorer';
import { NonAppliedDecorator } from '../src/decorators/non-applied.decorator';
import { Test, TestingModule } from '@nestjs/testing';
import { AppModule } from '../src/app.module.js';
import { NonAppliedDecorator } from '../src/decorators/non-applied.decorator.js';
import { WebhooksExplorer } from '../src/webhooks.explorer.js';
describe('DiscoveryModule', () => {
let moduleRef: TestingModule;
@@ -14,10 +13,14 @@ describe('DiscoveryModule', () => {
}).compile();
});
afterEach(async () => {
await moduleRef.close();
});
it('should discover all providers & handlers with corresponding annotations', async () => {
const webhooksExplorer = moduleRef.get(WebhooksExplorer);
expect(webhooksExplorer.getWebhooks()).to.be.eql([
expect(webhooksExplorer.getWebhooks()).toEqual([
{
handlers: [
{
@@ -45,7 +48,7 @@ describe('DiscoveryModule', () => {
const providers = discoveryService.getProviders({
metadataKey: NonAppliedDecorator.KEY,
});
expect(providers).to.be.eql([]);
expect(providers).toEqual([]);
});
it('should return an empty array if no controllers were found for a given discoverable decorator', () => {
@@ -54,6 +57,6 @@ describe('DiscoveryModule', () => {
const controllers = discoveryService.getControllers({
metadataKey: NonAppliedDecorator.KEY,
});
expect(controllers).to.be.eql([]);
expect(controllers).toEqual([]);
});
});

View File

@@ -1,7 +1,7 @@
import { Module } from '@nestjs/common';
import { DiscoveryModule } from '@nestjs/core';
import { MyWebhookModule } from './my-webhook/my-webhook.module';
import { WebhooksExplorer } from './webhooks.explorer';
import { MyWebhookModule } from './my-webhook/my-webhook.module.js';
import { WebhooksExplorer } from './webhooks.explorer.js';
@Module({
imports: [MyWebhookModule, DiscoveryModule],

View File

@@ -1,4 +1,4 @@
import { Webhook, WebhookHandler } from '../decorators/webhook.decorators';
import { Webhook, WebhookHandler } from '../decorators/webhook.decorators.js';
@Webhook({ name: 'cleanup' })
export class CleanupWebhook {

View File

@@ -1,4 +1,4 @@
import { Webhook, WebhookHandler } from '../decorators/webhook.decorators';
import { Webhook, WebhookHandler } from '../decorators/webhook.decorators.js';
@Webhook({ name: 'flush' })
export class FlushWebhook {

View File

@@ -1,6 +1,6 @@
import { Module } from '@nestjs/common';
import { CleanupWebhook } from './cleanup.webhook';
import { FlushWebhook } from './flush.webhook';
import { CleanupWebhook } from './cleanup.webhook.js';
import { FlushWebhook } from './flush.webhook.js';
@Module({ providers: [CleanupWebhook, FlushWebhook] })
export class MyWebhookModule {}

View File

@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { DiscoveryService, MetadataScanner } from '@nestjs/core';
import { Webhook, WebhookHandler } from './decorators/webhook.decorators';
import { Webhook, WebhookHandler } from './decorators/webhook.decorators.js';
@Injectable()
export class WebhooksExplorer {

View File

@@ -1,13 +1,15 @@
{
"compilerOptions": {
"types": ["vitest/globals"],
"module": "commonjs",
"declaration": false,
"noImplicitAny": false,
"removeComments": true,
"noLib": false,
"esModuleInterop": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "ES2021",
"target": "ES2023",
"sourceMap": true,
"allowJs": true,
"strictNullChecks": true,

View File

@@ -1,7 +1,7 @@
import { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
import request from 'supertest';
import { AppModule } from '../src/app.module.js';
describe('GraphQL - Code-first', () => {
let app: INestApplication;

View File

@@ -1,7 +1,7 @@
import { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
import request from 'supertest';
import { AppModule } from '../src/app.module.js';
describe('GraphQL - Guards', () => {
let app: INestApplication;

View File

@@ -1,7 +1,7 @@
import { INestApplication, ValidationPipe } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
import request from 'supertest';
import { AppModule } from '../src/app.module.js';
describe('GraphQL Pipes', () => {
let app: INestApplication;

View File

@@ -2,7 +2,7 @@ import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { join } from 'path';
import { RecipesModule } from './recipes/recipes.module';
import { RecipesModule } from './recipes/recipes.module.js';
@Module({
imports: [

View File

@@ -1,6 +1,6 @@
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { AppModule } from './app.module.js';
async function bootstrap() {
const app = await NestFactory.create(AppModule);

View File

@@ -1,9 +1,9 @@
import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { UnauthorizedFilter } from '../common/filters/unauthorized.filter';
import { DateScalar } from '../common/scalars/date.scalar';
import { RecipesResolver } from './recipes.resolver';
import { RecipesService } from './recipes.service';
import { UnauthorizedFilter } from '../common/filters/unauthorized.filter.js';
import { DateScalar } from '../common/scalars/date.scalar.js';
import { RecipesResolver } from './recipes.resolver.js';
import { RecipesService } from './recipes.service.js';
@Module({
providers: [

View File

@@ -1,12 +1,12 @@
import { NotFoundException, UseGuards, UseInterceptors } from '@nestjs/common';
import { Args, Mutation, Query, Resolver, Subscription } from '@nestjs/graphql';
import { PubSub } from 'graphql-subscriptions';
import { AuthGuard } from '../common/guards/auth.guard';
import { DataInterceptor } from '../common/interceptors/data.interceptor';
import { NewRecipeInput } from './dto/new-recipe.input';
import { RecipesArgs } from './dto/recipes.args';
import { Recipe } from './models/recipe';
import { RecipesService } from './recipes.service';
import { AuthGuard } from '../common/guards/auth.guard.js';
import { DataInterceptor } from '../common/interceptors/data.interceptor.js';
import { NewRecipeInput } from './dto/new-recipe.input.js';
import { RecipesArgs } from './dto/recipes.args.js';
import { Recipe } from './models/recipe.js';
import { RecipesService } from './recipes.service.js';
const pubSub = new PubSub();

View File

@@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common';
import { NewRecipeInput } from './dto/new-recipe.input';
import { RecipesArgs } from './dto/recipes.args';
import { Recipe } from './models/recipe';
import { NewRecipeInput } from './dto/new-recipe.input.js';
import { RecipesArgs } from './dto/recipes.args.js';
import { Recipe } from './models/recipe.js';
@Injectable()
export class RecipesService {

View File

@@ -1,13 +1,15 @@
{
"compilerOptions": {
"types": ["vitest/globals"],
"module": "commonjs",
"declaration": false,
"noImplicitAny": false,
"removeComments": true,
"noLib": false,
"esModuleInterop": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "ES2021",
"target": "ES2023",
"sourceMap": true,
"allowJs": true,
"strictNullChecks": true,

View File

@@ -1,7 +1,7 @@
import { INestApplication } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import * as request from 'supertest';
import { AsyncClassApplicationModule } from '../src/async-options-class.module';
import request from 'supertest';
import { AsyncClassApplicationModule } from '../src/async-options-class.module.js';
describe('GraphQL (async class)', () => {
let app: INestApplication;

View File

@@ -1,7 +1,7 @@
import { INestApplication } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import * as request from 'supertest';
import { AsyncExistingApplicationModule } from '../src/async-options-existing.module';
import request from 'supertest';
import { AsyncExistingApplicationModule } from '../src/async-options-existing.module.js';
describe('GraphQL (async existing)', () => {
let app: INestApplication;

View File

@@ -1,7 +1,7 @@
import { INestApplication } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import * as request from 'supertest';
import { AsyncApplicationModule } from '../src/async-options.module';
import request from 'supertest';
import { AsyncApplicationModule } from '../src/async-options.module.js';
describe('GraphQL (async configuration)', () => {
let app: INestApplication;

View File

@@ -2,11 +2,10 @@ import { ApolloDriver } from '@nestjs/apollo';
import { INestApplication } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { Test } from '@nestjs/testing';
import { expect } from 'chai';
import { join } from 'path';
import * as request from 'supertest';
import { CatsRequestScopedService } from '../src/cats/cats-request-scoped.service';
import { CatsModule } from '../src/cats/cats.module';
import request from 'supertest';
import { CatsRequestScopedService } from '../src/cats/cats-request-scoped.service.js';
import { CatsModule } from '../src/cats/cats.module.js';
describe('GraphQL request scoped', () => {
let app: INestApplication;
@@ -17,7 +16,9 @@ describe('GraphQL request scoped', () => {
CatsModule.enableRequestScope(),
GraphQLModule.forRoot({
driver: ApolloDriver,
typePaths: [join(__dirname, '..', 'src', '**', '*.graphql')],
typePaths: [
join(import.meta.dirname, '..', 'src', '**', '*.graphql'),
],
}),
],
}).compile();
@@ -53,7 +54,7 @@ describe('GraphQL request scoped', () => {
});
it(`should create resolver for each incoming request`, () => {
expect(CatsRequestScopedService.COUNTER).to.be.eql(3);
expect(CatsRequestScopedService.COUNTER).toEqual(3);
});
afterEach(async () => {

View File

@@ -1,7 +1,7 @@
import { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
import request from 'supertest';
import { AppModule } from '../src/app.module.js';
describe('GraphQL', () => {
let app: INestApplication;

View File

@@ -2,7 +2,7 @@ import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { join } from 'path';
import { CatsModule } from './cats/cats.module';
import { CatsModule } from './cats/cats.module.js';
@Module({
imports: [
@@ -10,7 +10,7 @@ import { CatsModule } from './cats/cats.module';
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
includeStacktraceInErrorResponses: true,
typePaths: [join(__dirname, '**', '*.graphql')],
typePaths: [join(import.meta.dirname, '**', '*.graphql')],
}),
],
})

View File

@@ -2,12 +2,12 @@ import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { GqlOptionsFactory, GraphQLModule } from '@nestjs/graphql';
import { join } from 'path';
import { CatsModule } from './cats/cats.module';
import { CatsModule } from './cats/cats.module.js';
class ConfigService implements GqlOptionsFactory {
createGqlOptions(): ApolloDriverConfig {
return {
typePaths: [join(__dirname, '**', '*.graphql')],
typePaths: [join(import.meta.dirname, '**', '*.graphql')],
};
}
}

View File

@@ -1,9 +1,9 @@
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { CatsModule } from './cats/cats.module';
import { ConfigModule } from './config.module';
import { ConfigService } from './config.service';
import { CatsModule } from './cats/cats.module.js';
import { ConfigModule } from './config.module.js';
import { ConfigService } from './config.service.js';
@Module({
imports: [

View File

@@ -2,7 +2,7 @@ import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { join } from 'path';
import { CatsModule } from './cats/cats.module';
import { CatsModule } from './cats/cats.module.js';
@Module({
imports: [
@@ -10,7 +10,7 @@ import { CatsModule } from './cats/cats.module';
GraphQLModule.forRootAsync<ApolloDriverConfig>({
driver: ApolloDriver,
useFactory: async () => ({
typePaths: [join(__dirname, '**', '*.graphql')],
typePaths: [join(import.meta.dirname, '**', '*.graphql')],
}),
}),
],

View File

@@ -1,5 +1,5 @@
import { Injectable, Scope } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';
import { Cat } from './interfaces/cat.interface.js';
@Injectable({ scope: Scope.REQUEST })
export class CatsRequestScopedService {

View File

@@ -1,7 +1,7 @@
import { DynamicModule, Module, Scope } from '@nestjs/common';
import { CatsRequestScopedService } from './cats-request-scoped.service';
import { CatsResolvers } from './cats.resolvers';
import { CatsService } from './cats.service';
import { CatsRequestScopedService } from './cats-request-scoped.service.js';
import { CatsResolvers } from './cats.resolvers.js';
import { CatsService } from './cats.service.js';
@Module({
providers: [CatsService, CatsResolvers],

View File

@@ -1,9 +1,9 @@
import { ParseIntPipe, UseGuards } from '@nestjs/common';
import { Args, Mutation, Query, Resolver, Subscription } from '@nestjs/graphql';
import { PubSub } from 'graphql-subscriptions';
import { CatsGuard } from './cats.guard';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';
import { CatsGuard } from './cats.guard.js';
import { CatsService } from './cats.service.js';
import { Cat } from './interfaces/cat.interface.js';
const pubSub = new PubSub();

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';
import { Cat } from './interfaces/cat.interface.js';
@Injectable()
export class CatsService {

View File

@@ -1,5 +1,5 @@
import { Module } from '@nestjs/common';
import { ConfigService } from './config.service';
import { ConfigService } from './config.service.js';
@Module({
providers: [ConfigService],

View File

@@ -6,7 +6,7 @@ import { join } from 'path';
export class ConfigService implements GqlOptionsFactory {
createGqlOptions(): GqlModuleOptions {
return {
typePaths: [join(__dirname, '**', '*.graphql')],
typePaths: [join(import.meta.dirname, '**', '*.graphql')],
};
}
}

View File

@@ -1,5 +1,5 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { AppModule } from './app.module.js';
async function bootstrap() {
const app = await NestFactory.create(AppModule);

View File

@@ -1,13 +1,15 @@
{
"compilerOptions": {
"types": ["vitest/globals"],
"module": "commonjs",
"declaration": false,
"noImplicitAny": false,
"removeComments": true,
"noLib": false,
"esModuleInterop": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "ES2021",
"target": "ES2023",
"sourceMap": true,
"allowJs": true,
"strictNullChecks": true,

View File

@@ -4,10 +4,9 @@ import {
NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { Test } from '@nestjs/testing';
import { expect } from 'chai';
import { RawServerDefault } from 'fastify';
import * as request from 'supertest';
import { ErrorsController } from '../src/errors/errors.controller';
import request from 'supertest';
import { ErrorsController } from '../src/errors/errors.controller.js';
describe('Error messages', () => {
let server: RawServerDefault;
@@ -82,14 +81,12 @@ describe('Error messages', () => {
url: '/sync',
})
.then(({ payload, statusCode }) => {
expect(statusCode).to.equal(HttpStatus.BAD_REQUEST);
expect(payload).to.equal(
JSON.stringify({
statusCode: 400,
error: 'Bad Request',
message: 'Integration test',
}),
);
expect(statusCode).toBe(HttpStatus.BAD_REQUEST);
expect(JSON.parse(payload)).toEqual({
statusCode: 400,
error: 'Bad Request',
message: 'Integration test',
});
});
});
@@ -97,17 +94,15 @@ describe('Error messages', () => {
return app
.inject({
method: 'GET',
url: '/sync',
url: '/async',
})
.then(({ payload, statusCode }) => {
expect(statusCode).to.equal(HttpStatus.BAD_REQUEST);
expect(payload).to.equal(
JSON.stringify({
statusCode: 400,
error: 'Bad Request',
message: 'Integration test',
}),
);
expect(statusCode).toBe(HttpStatus.BAD_REQUEST);
expect(JSON.parse(payload)).toEqual({
statusCode: 400,
error: 'Bad Request',
message: 'Integration test',
});
});
});
@@ -118,13 +113,11 @@ describe('Error messages', () => {
url: '/unexpected-error',
})
.then(({ payload, statusCode }) => {
expect(statusCode).to.equal(HttpStatus.INTERNAL_SERVER_ERROR);
expect(payload).to.equal(
JSON.stringify({
statusCode: 500,
message: 'Internal server error',
}),
);
expect(statusCode).toBe(HttpStatus.INTERNAL_SERVER_ERROR);
expect(JSON.parse(payload)).toEqual({
statusCode: 500,
message: 'Internal server error',
});
});
});

View File

@@ -12,8 +12,8 @@ import {
NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { Test } from '@nestjs/testing';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
import request from 'supertest';
import { AppModule } from '../src/app.module.js';
const RETURN_VALUE = 'test';
const MIDDLEWARE_VALUE = 'middleware';

View File

@@ -8,8 +8,8 @@ import {
RequestMethod,
} from '@nestjs/common';
import { Test } from '@nestjs/testing';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
import request from 'supertest';
import { AppModule } from '../src/app.module.js';
const RETURN_VALUE = 'test';
const MIDDLEWARE_VALUE = 'middleware';

View File

@@ -1,10 +1,10 @@
import { INestApplication } from '@nestjs/common';
import { ExpressAdapter } from '@nestjs/platform-express';
import { Test } from '@nestjs/testing';
import * as express from 'express';
import * as request from 'supertest';
import express from 'express';
import request from 'supertest';
import { App } from 'supertest/types';
import { AppModule } from '../src/app.module';
import { AppModule } from '../src/app.module.js';
describe('Hello world (express instance)', () => {
let server: App;

View File

@@ -1,9 +1,9 @@
import { INestApplication } from '@nestjs/common';
import { ExpressAdapter } from '@nestjs/platform-express';
import { Test } from '@nestjs/testing';
import * as express from 'express';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
import express from 'express';
import request from 'supertest';
import { AppModule } from '../src/app.module.js';
describe('Hello world (express instance with multiple applications)', () => {
let server;

View File

@@ -3,8 +3,7 @@ import {
NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { Test } from '@nestjs/testing';
import { expect } from 'chai';
import { AppModule } from '../src/app.module';
import { AppModule } from '../src/app.module.js';
describe('Hello world (fastify adapter)', () => {
let app: NestFastifyApplication;
@@ -26,7 +25,10 @@ describe('Hello world (fastify adapter)', () => {
method: 'GET',
url: '/hello',
})
.then(({ payload }) => expect(payload).to.be.eql('Hello world!'));
.then(({ payload, statusCode }) => {
expect(statusCode).toBe(200);
expect(payload).toEqual('Hello world!');
});
});
it(`/GET (Promise/async)`, () => {
@@ -35,7 +37,10 @@ describe('Hello world (fastify adapter)', () => {
method: 'GET',
url: '/hello/async',
})
.then(({ payload }) => expect(payload).to.be.eql('Hello world!'));
.then(({ payload, statusCode }) => {
expect(statusCode).toBe(200);
expect(payload).toEqual('Hello world!');
});
});
it(`/GET (Observable stream)`, () => {
@@ -44,7 +49,10 @@ describe('Hello world (fastify adapter)', () => {
method: 'GET',
url: '/hello/stream',
})
.then(({ payload }) => expect(payload).to.be.eql('Hello world!'));
.then(({ payload, statusCode }) => {
expect(statusCode).toBe(200);
expect(payload).toEqual('Hello world!');
});
});
it(`/GET { host: ":tenant.example.com" } not matched`, () => {
@@ -54,7 +62,7 @@ describe('Hello world (fastify adapter)', () => {
url: '/host',
})
.then(({ payload }) => {
expect(JSON.parse(payload)).to.be.eql({
expect(JSON.parse(payload)).toEqual({
error: 'Internal Server Error',
message:
'HTTP adapter does not support filtering on host: ":tenant.example.com"',
@@ -70,7 +78,7 @@ describe('Hello world (fastify adapter)', () => {
url: '/host-array',
})
.then(({ payload }) => {
expect(JSON.parse(payload)).to.be.eql({
expect(JSON.parse(payload)).toEqual({
error: 'Internal Server Error',
message:
'HTTP adapter does not support filtering on hosts: [":tenant.example1.com", ":tenant.example2.com"]',
@@ -84,7 +92,10 @@ describe('Hello world (fastify adapter)', () => {
.inject()
.get('/hello')
.end()
.then(({ payload }) => expect(payload).to.be.eql('Hello world!'));
.then(({ payload, statusCode }) => {
expect(statusCode).toBe(200);
expect(payload).toEqual('Hello world!');
});
});
it('/HEAD should respond to with a 200', () => {
@@ -93,7 +104,7 @@ describe('Hello world (fastify adapter)', () => {
method: 'HEAD',
url: '/hello',
})
.then(({ statusCode }) => expect(statusCode).to.be.eq(200));
.then(({ statusCode }) => expect(statusCode).toBe(200));
});
afterEach(async () => {

View File

@@ -11,7 +11,6 @@ import {
NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { Test } from '@nestjs/testing';
import { expect } from 'chai';
describe('Middleware before init (FastifyAdapter)', () => {
let app: NestFastifyApplication;
@@ -81,11 +80,11 @@ describe('Middleware before init (FastifyAdapter)', () => {
url: '/test',
})
.then(({ statusCode, payload, headers }) => {
expect(statusCode).to.equal(200);
expect(JSON.parse(payload)).to.deep.equal({ data: 'test_data' });
expect(statusCode).toBe(200);
expect(JSON.parse(payload)).toEqual({ data: 'test_data' });
// Verify both module-level and global middleware were applied
expect(headers['x-middleware']).to.equal('applied');
expect(headers['x-global-middleware']).to.equal('applied');
expect(headers['x-middleware']).toBe('applied');
expect(headers['x-global-middleware']).toBe('applied');
});
});
@@ -123,8 +122,8 @@ describe('Middleware before init (FastifyAdapter)', () => {
url: '/test',
})
.then(({ statusCode, payload }) => {
expect(statusCode).to.equal(200);
expect(JSON.parse(payload)).to.deep.equal({ data: 'test_data' });
expect(statusCode).toBe(200);
expect(JSON.parse(payload)).toEqual({ data: 'test_data' });
});
});

View File

@@ -1,91 +1,4 @@
/* Temporarily disabled due to various regressions
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
import { Test } from '@nestjs/testing';
import { expect } from 'chai';
import { ApplicationModule } from '../src/app.module';
describe('Hello world (fastify adapter with multiple applications)', () => {
let adapter: FastifyAdapter;
let apps: NestFastifyApplication[];
beforeEach(async () => {
const module1 = await Test.createTestingModule({
imports: [ApplicationModule],
}).compile();
const module2 = await Test.createTestingModule({
imports: [ApplicationModule],
}).compile();
adapter = new FastifyAdapter();
apps = [
module1.createNestApplication<NestFastifyApplication>(adapter),
module2
.createNestApplication<NestFastifyApplication>(adapter, {
bodyParser: false,
})
.setGlobalPrefix('/app2'),
];
await Promise.all(apps.map(app => app.init()));
});
it(`/GET`, () => {
return adapter
.inject({
method: 'GET',
url: '/hello',
})
.then(({ payload }) => expect(payload).to.be.eql('Hello world!'));
});
it(`/GET (app2)`, () => {
return adapter
.inject({
method: 'GET',
url: '/app2/hello',
})
.then(({ payload }) => expect(payload).to.be.eql('Hello world!'));
});
it(`/GET (Promise/async)`, () => {
return adapter
.inject({
method: 'GET',
url: '/hello/async',
})
.then(({ payload }) => expect(payload).to.be.eql('Hello world!'));
});
it(`/GET (app2 Promise/async)`, () => {
return adapter
.inject({
method: 'GET',
url: '/app2/hello/async',
})
.then(({ payload }) => expect(payload).to.be.eql('Hello world!'));
});
it(`/GET (Observable stream)`, () => {
return adapter
.inject({
method: 'GET',
url: '/hello/stream',
})
.then(({ payload }) => expect(payload).to.be.eql('Hello world!'));
});
it(`/GET (app2 Observable stream)`, () => {
return adapter
.inject({
method: 'GET',
url: '/app2/hello/stream',
})
.then(({ payload }) => expect(payload).to.be.eql('Hello world!'));
});
afterEach(async () => {
await Promise.all(apps.map(app => app.close()));
await adapter.close();
});
});*/
// Temporarily disabled due to various regressions
describe.skip('Hello world (fastify adapter with multiple applications)', () => {
it('placeholder', () => {});
});

View File

@@ -1,25 +1,23 @@
import { ConsoleLogger, INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
import * as sinon from 'sinon';
import { expect } from 'chai';
import request from 'supertest';
import { AppModule } from '../src/app.module.js';
describe('ForceConsole Option', () => {
let app: INestApplication;
describe('When forceConsole is true', () => {
let consoleLogSpy: sinon.SinonSpy;
let consoleErrorSpy: sinon.SinonSpy;
let processStdoutSpy: sinon.SinonSpy;
let processStderrSpy: sinon.SinonSpy;
let consoleLogSpy: ReturnType<typeof vi.fn>;
let consoleErrorSpy: ReturnType<typeof vi.fn>;
let processStdoutSpy: ReturnType<typeof vi.fn>;
let processStderrSpy: ReturnType<typeof vi.fn>;
beforeEach(async () => {
// Spy on console and process methods
consoleLogSpy = sinon.spy(console, 'log');
consoleErrorSpy = sinon.spy(console, 'error');
processStdoutSpy = sinon.spy(process.stdout, 'write');
processStderrSpy = sinon.spy(process.stderr, 'write');
consoleLogSpy = vi.spyOn(console, 'log');
consoleErrorSpy = vi.spyOn(console, 'error');
processStdoutSpy = vi.spyOn(process.stdout, 'write');
processStderrSpy = vi.spyOn(process.stderr, 'write');
const moduleRef = await Test.createTestingModule({
imports: [AppModule],
@@ -34,10 +32,10 @@ describe('ForceConsole Option', () => {
});
afterEach(async () => {
consoleLogSpy.restore();
consoleErrorSpy.restore();
processStdoutSpy.restore();
processStderrSpy.restore();
consoleLogSpy.mockRestore();
consoleErrorSpy.mockRestore();
processStdoutSpy.mockRestore();
processStderrSpy.mockRestore();
await app.close();
});
@@ -46,14 +44,12 @@ describe('ForceConsole Option', () => {
logger.log('Test log message');
// Should use console.log when forceConsole is true
expect(consoleLogSpy.called).to.be.true;
expect(consoleLogSpy).toHaveBeenCalled();
// Verify console.log was called with the message
const consoleLogCalls = consoleLogSpy
.getCalls()
.filter(call =>
call.args.some(arg => String(arg).includes('Test log message')),
);
expect(consoleLogCalls.length).to.be.greaterThan(0);
const consoleLogCalls = consoleLogSpy.mock.calls.filter(args =>
args.some(arg => String(arg).includes('Test log message')),
);
expect(consoleLogCalls.length).toBeGreaterThan(0);
});
it('should use console.error instead of process.stderr.write', async () => {
@@ -61,14 +57,12 @@ describe('ForceConsole Option', () => {
logger.error('Test error message');
// Should use console.error when forceConsole is true
expect(consoleErrorSpy.called).to.be.true;
expect(consoleErrorSpy).toHaveBeenCalled();
// Verify console.error was called with the message
const consoleErrorCalls = consoleErrorSpy
.getCalls()
.filter(call =>
call.args.some(arg => String(arg).includes('Test error message')),
);
expect(consoleErrorCalls.length).to.be.greaterThan(0);
const consoleErrorCalls = consoleErrorSpy.mock.calls.filter(args =>
args.some(arg => String(arg).includes('Test error message')),
);
expect(consoleErrorCalls.length).toBeGreaterThan(0);
});
it('should handle GET request with forceConsole option enabled', () => {
@@ -77,17 +71,17 @@ describe('ForceConsole Option', () => {
});
describe('When forceConsole is false (default)', () => {
let consoleLogSpy: sinon.SinonSpy;
let consoleErrorSpy: sinon.SinonSpy;
let processStdoutSpy: sinon.SinonSpy;
let processStderrSpy: sinon.SinonSpy;
let consoleLogSpy: ReturnType<typeof vi.fn>;
let consoleErrorSpy: ReturnType<typeof vi.fn>;
let processStdoutSpy: ReturnType<typeof vi.fn>;
let processStderrSpy: ReturnType<typeof vi.fn>;
beforeEach(async () => {
// Spy on console and process methods
consoleLogSpy = sinon.spy(console, 'log');
consoleErrorSpy = sinon.spy(console, 'error');
processStdoutSpy = sinon.spy(process.stdout, 'write');
processStderrSpy = sinon.spy(process.stderr, 'write');
consoleLogSpy = vi.spyOn(console, 'log');
consoleErrorSpy = vi.spyOn(console, 'error');
processStdoutSpy = vi.spyOn(process.stdout, 'write');
processStderrSpy = vi.spyOn(process.stderr, 'write');
const moduleRef = await Test.createTestingModule({
imports: [AppModule],
@@ -102,10 +96,10 @@ describe('ForceConsole Option', () => {
});
afterEach(async () => {
consoleLogSpy.restore();
consoleErrorSpy.restore();
processStdoutSpy.restore();
processStderrSpy.restore();
consoleLogSpy.mockRestore();
consoleErrorSpy.mockRestore();
processStdoutSpy.mockRestore();
processStderrSpy.mockRestore();
await app.close();
});
@@ -113,31 +107,31 @@ describe('ForceConsole Option', () => {
const logger = new ConsoleLogger('TestContext');
// Reset spy to ensure clean state
consoleLogSpy.resetHistory();
consoleLogSpy.mockClear();
logger.log('Test log message');
// When forceConsole is false, should not call console.log
expect(consoleLogSpy.called).to.be.false;
expect(consoleLogSpy).not.toHaveBeenCalled();
});
it('should not directly call console.error when forceConsole is false', async () => {
const logger = new ConsoleLogger('TestContext');
// Reset spy to ensure clean state
consoleErrorSpy.resetHistory();
consoleErrorSpy.mockClear();
logger.error('Test error message');
// When forceConsole is false, should not call console.error
expect(consoleErrorSpy.called).to.be.false;
expect(consoleErrorSpy).not.toHaveBeenCalled();
});
});
describe('When forceConsole is set via NestFactory.create', () => {
it('should apply forceConsole to the default logger', async () => {
const consoleLogSpy = sinon.spy(console, 'log');
const processStdoutSpy = sinon.spy(process.stdout, 'write');
const consoleLogSpy = vi.spyOn(console, 'log');
const processStdoutSpy = vi.spyOn(process.stdout, 'write');
const moduleRef = await Test.createTestingModule({
imports: [AppModule],
@@ -153,10 +147,10 @@ describe('ForceConsole Option', () => {
const logger = new ConsoleLogger('AppContext', { forceConsole: true });
logger.log('Application started');
expect(consoleLogSpy.called).to.be.true;
expect(consoleLogSpy).toHaveBeenCalled();
consoleLogSpy.restore();
processStdoutSpy.restore();
consoleLogSpy.mockRestore();
processStdoutSpy.mockRestore();
await testApp.close();
});
});

View File

@@ -5,8 +5,8 @@ import {
} from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { Test } from '@nestjs/testing';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
import request from 'supertest';
import { AppModule } from '../src/app.module.js';
@Injectable()
export class AuthGuard {
@@ -33,10 +33,31 @@ function createTestModule(guard) {
describe('Guards', () => {
let app: INestApplication;
afterEach(async () => {
await app.close();
});
it(`should prevent access (unauthorized)`, async () => {
app = (await createTestModule(new AuthGuard())).createNestApplication();
await app.init();
return request(app.getHttpServer()).get('/hello').expect(401);
return request(app.getHttpServer())
.get('/hello')
.expect(401)
.expect(({ body }) => {
expect(body.message).toBe('Unauthorized');
expect(body.statusCode).toBe(401);
});
});
it(`should allow access when guard returns true`, async () => {
const allowGuard = { canActivate: () => true };
app = (await createTestModule(allowGuard)).createNestApplication();
await app.init();
return request(app.getHttpServer())
.get('/hello')
.expect(200)
.expect('Hello world!');
});
});

View File

@@ -1,7 +1,7 @@
import * as request from 'supertest';
import request from 'supertest';
import { Test } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import { AppModule } from '../src/app.module';
import { AppModule } from '../src/app.module.js';
describe('Hello world (default adapter)', () => {
let server;

View File

@@ -9,8 +9,8 @@ import { APP_INTERCEPTOR } from '@nestjs/core';
import { Test } from '@nestjs/testing';
import { of } from 'rxjs';
import { map } from 'rxjs/operators';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
import request from 'supertest';
import { AppModule } from '../src/app.module.js';
const RETURN_VALUE = 'test';

View File

@@ -1,7 +1,7 @@
import { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
import request from 'supertest';
import { AppModule } from '../src/app.module.js';
describe('Hello world (default adapter)', () => {
let server;

View File

@@ -9,8 +9,8 @@ import {
} from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { Response } from 'express';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
import request from 'supertest';
import { AppModule } from '../src/app.module.js';
const INCLUDED_VALUE = 'test_included';
const RETURN_VALUE = 'test';

View File

@@ -5,7 +5,7 @@ import {
Module,
} from '@nestjs/common';
import { Test } from '@nestjs/testing';
import * as request from 'supertest';
import request from 'supertest';
const RETURN_VALUE_A = 'test_A';
const RETURN_VALUE_B = 'test_B';

View File

@@ -15,10 +15,9 @@ import {
NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { Test } from '@nestjs/testing';
import { expect } from 'chai';
import { FastifyRequest } from 'fastify';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
import request from 'supertest';
import { AppModule } from '../src/app.module.js';
describe('Middleware (FastifyAdapter)', () => {
let app: NestFastifyApplication;
@@ -117,7 +116,7 @@ describe('Middleware (FastifyAdapter)', () => {
method: 'GET',
url: '/hello',
})
.then(({ payload }) => expect(payload).to.be.eql(RETURN_VALUE));
.then(({ payload }) => expect(payload).toEqual(RETURN_VALUE));
});
it(`forRoutes(TestController)`, () => {
@@ -126,7 +125,7 @@ describe('Middleware (FastifyAdapter)', () => {
method: 'GET',
url: '/test',
})
.then(({ payload }) => expect(payload).to.be.eql(SCOPED_VALUE));
.then(({ payload }) => expect(payload).toEqual(SCOPED_VALUE));
});
it(`query?test=${QUERY_VALUE} forRoutes(query)`, () => {
@@ -138,7 +137,7 @@ describe('Middleware (FastifyAdapter)', () => {
test: QUERY_VALUE,
},
})
.then(({ payload }) => expect(payload).to.be.eql(QUERY_VALUE));
.then(({ payload }) => expect(payload).toEqual(QUERY_VALUE));
});
it(`${QUERY_VALUE}?test=${QUERY_VALUE} forRoutes(${QUERY_VALUE})`, () => {
@@ -150,7 +149,7 @@ describe('Middleware (FastifyAdapter)', () => {
test: QUERY_VALUE,
},
})
.then(({ payload }) => expect(payload).to.be.eql(QUERY_VALUE));
.then(({ payload }) => expect(payload).toEqual(QUERY_VALUE));
});
it(`forRoutes(tests/*path)`, () => {
@@ -159,7 +158,7 @@ describe('Middleware (FastifyAdapter)', () => {
method: 'GET',
url: '/tests/wildcard_nested',
})
.then(({ payload }) => expect(payload).to.be.eql(WILDCARD_VALUE));
.then(({ payload }) => expect(payload).toEqual(WILDCARD_VALUE));
});
it(`forRoutes(express_style_wildcard/*)`, () => {
@@ -168,7 +167,7 @@ describe('Middleware (FastifyAdapter)', () => {
method: 'GET',
url: '/express_style_wildcard/wildcard_nested',
})
.then(({ payload }) => expect(payload).to.be.eql(WILDCARD_VALUE));
.then(({ payload }) => expect(payload).toEqual(WILDCARD_VALUE));
});
it(`forRoutes(legacy_style_wildcard/*)`, () => {
@@ -177,7 +176,7 @@ describe('Middleware (FastifyAdapter)', () => {
method: 'GET',
url: '/legacy_style_wildcard/wildcard_nested',
})
.then(({ payload }) => expect(payload).to.be.eql(WILDCARD_VALUE));
.then(({ payload }) => expect(payload).toEqual(WILDCARD_VALUE));
});
it(`forRoutes(req/url/)`, () => {
@@ -187,7 +186,7 @@ describe('Middleware (FastifyAdapter)', () => {
method: 'GET',
url: `/req/url${reqUrl}`,
})
.then(({ payload }) => expect(payload).to.be.eql(REQ_URL_VALUE));
.then(({ payload }) => expect(payload).toEqual(REQ_URL_VALUE));
});
it(`GET forRoutes(POST tests/included)`, () => {
@@ -196,7 +195,7 @@ describe('Middleware (FastifyAdapter)', () => {
method: 'GET',
url: '/tests/included',
})
.then(({ payload }) => expect(payload).to.be.eql(WILDCARD_VALUE));
.then(({ payload }) => expect(payload).toEqual(WILDCARD_VALUE));
});
it(`POST forRoutes(POST tests/included)`, () => {
@@ -205,7 +204,7 @@ describe('Middleware (FastifyAdapter)', () => {
method: 'POST',
url: '/tests/included',
})
.then(({ payload }) => expect(payload).to.be.eql(INCLUDED_VALUE));
.then(({ payload }) => expect(payload).toEqual(INCLUDED_VALUE));
});
it(`GET forRoutes(POST /tests/%69ncluded) - ensure middleware is executed correctly with encoded characters`, () => {
@@ -214,7 +213,7 @@ describe('Middleware (FastifyAdapter)', () => {
method: 'POST',
url: '/tests/%69ncluded', // 'i' character is encoded
})
.then(({ payload }) => expect(payload).to.be.eql(INCLUDED_VALUE));
.then(({ payload }) => expect(payload).toEqual(INCLUDED_VALUE));
});
afterEach(async () => {
@@ -329,7 +328,7 @@ describe('Middleware (FastifyAdapter)', () => {
url: '/a/b/c',
})
.then(({ payload }) => {
expect(payload).to.be.eql(
expect(payload).toEqual(
JSON.stringify({
success: true,
actual: 1,
@@ -346,7 +345,7 @@ describe('Middleware (FastifyAdapter)', () => {
url: '/a/b',
})
.then(({ payload }) =>
expect(payload).to.be.eql(
expect(payload).toEqual(
JSON.stringify({
success: true,
actual: 1,
@@ -363,7 +362,7 @@ describe('Middleware (FastifyAdapter)', () => {
url: '/a',
})
.then(({ payload }) =>
expect(payload).to.be.eql(
expect(payload).toEqual(
JSON.stringify({
success: true,
actual: 1,
@@ -380,7 +379,7 @@ describe('Middleware (FastifyAdapter)', () => {
url: '/similar',
})
.then(({ payload }) =>
expect(payload).to.be.eql(
expect(payload).toEqual(
JSON.stringify({
success: true,
actual: 1,
@@ -397,7 +396,7 @@ describe('Middleware (FastifyAdapter)', () => {
url: '/similar/test',
})
.then(({ payload }) =>
expect(payload).to.be.eql(
expect(payload).toEqual(
JSON.stringify({
success: true,
actual: 1,
@@ -414,7 +413,7 @@ describe('Middleware (FastifyAdapter)', () => {
url: '/similar/arbitrary',
})
.then(({ payload }) =>
expect(payload).to.be.eql(
expect(payload).toEqual(
JSON.stringify({
success: true,
actual: 1,
@@ -494,7 +493,7 @@ describe('Middleware (FastifyAdapter)', () => {
url: '/api/pong',
})
.then(({ payload }) =>
expect(payload).to.be.eql(
expect(payload).toEqual(
JSON.stringify({
success: true,
pong: 'pong',
@@ -513,7 +512,7 @@ describe('Middleware (FastifyAdapter)', () => {
url: '/api',
})
.then(({ payload }) =>
expect(payload).to.be.eql(
expect(payload).toEqual(
JSON.stringify({
success: true,
pong: 'pong',
@@ -531,7 +530,7 @@ describe('Middleware (FastifyAdapter)', () => {
url: '/pong',
})
.then(({ payload }) =>
expect(payload).to.be.eql(
expect(payload).toEqual(
JSON.stringify({
success: true,
pong: 'pong',

View File

@@ -7,10 +7,8 @@ import {
NestMiddleware,
Module,
} from '@nestjs/common';
import { Test } from '../../../packages/testing';
import * as request from 'supertest';
import { expect } from 'chai';
import { Test } from '../../../packages/testing.js';
import request from 'supertest';
/**
* Number of times that the middleware was executed.
*/

View File

@@ -6,14 +6,14 @@ import {
Module,
RequestMethod,
Version,
VERSION_NEUTRAL,
VersioningOptions,
VersioningType,
VERSION_NEUTRAL,
} from '@nestjs/common';
import { CustomVersioningOptions } from '@nestjs/common/interfaces';
import { CustomVersioningOptions } from '@nestjs/common/interfaces/index.js';
import { Test } from '@nestjs/testing';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
import request from 'supertest';
import { AppModule } from '../src/app.module.js';
const RETURN_VALUE = 'test';
const VERSIONED_VALUE = 'test_versioned';

View File

@@ -6,8 +6,8 @@ import {
Module,
} from '@nestjs/common';
import { Test } from '@nestjs/testing';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
import request from 'supertest';
import { AppModule } from '../src/app.module.js';
const RETURN_VALUE = 'test';
const SCOPED_VALUE = 'test_scoped';

View File

@@ -7,8 +7,8 @@ import {
} from '@nestjs/common';
import { RouterModule } from '@nestjs/core';
import { Test } from '@nestjs/testing';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
import request from 'supertest';
import { AppModule } from '../src/app.module.js';
const RETURN_VALUE = 'test';
const SCOPED_VALUE = 'test_scoped';

View File

@@ -1,7 +1,7 @@
import { Controller, Get, INestApplication, Module } from '@nestjs/common';
import { RouterModule, Routes } from '@nestjs/core';
import { Test } from '@nestjs/testing';
import * as request from 'supertest';
import request from 'supertest';
describe('RouterModule', () => {
let app: INestApplication;
@@ -66,7 +66,7 @@ describe('RouterModule', () => {
})
class AppModule {}
before(async () => {
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [MainModule, AppModule],
}).compile();

View File

@@ -0,0 +1,166 @@
import {
ArgumentMetadata,
Body,
Controller,
createParamDecorator,
ExecutionContext,
Get,
INestApplication,
Injectable,
Module,
Param,
PipeTransform,
Post,
Query,
} from '@nestjs/common';
import { Test } from '@nestjs/testing';
import type { StandardSchemaV1 } from '@standard-schema/spec';
import request from 'supertest';
const testSchema: StandardSchemaV1 = {
'~standard': {
version: 1,
vendor: 'test',
validate: (value: unknown) => ({ value }),
},
};
/**
* A pipe that captures the ArgumentMetadata it receives,
* so the test can verify that `schema` is propagated.
*/
@Injectable()
class SchemaCaptorPipe implements PipeTransform {
static lastMetadata: ArgumentMetadata | undefined;
transform(value: any, metadata: ArgumentMetadata) {
SchemaCaptorPipe.lastMetadata = metadata;
return value;
}
}
const CustomParam = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
return ctx.switchToHttp().getRequest().query;
},
);
@Controller('schema-test')
class SchemaTestController {
@Post('body')
bodyWithSchema(
@Body({ schema: testSchema, pipes: [SchemaCaptorPipe] }) body: any,
) {
return { received: body };
}
@Get('query')
queryWithSchema(
@Query({ schema: testSchema, pipes: [SchemaCaptorPipe] }) query: any,
) {
return { received: query };
}
@Get('param/:id')
paramWithSchema(
@Param('id', { schema: testSchema, pipes: [SchemaCaptorPipe] }) id: string,
) {
return { received: id };
}
@Get('custom')
customWithSchema(
@CustomParam({ schema: testSchema, pipes: [SchemaCaptorPipe] }) value: any,
) {
return { received: value };
}
@Post('body-property')
bodyPropertyWithSchema(
@Body('name', { schema: testSchema, pipes: [SchemaCaptorPipe] })
name: string,
) {
return { received: name };
}
}
@Module({
controllers: [SchemaTestController],
providers: [SchemaCaptorPipe],
})
class SchemaTestModule {}
describe('Schema propagation to pipes', () => {
let app: INestApplication;
let server: any;
beforeAll(async () => {
const module = await Test.createTestingModule({
imports: [SchemaTestModule],
}).compile();
app = module.createNestApplication();
server = app.getHttpServer();
await app.init();
});
beforeEach(() => {
SchemaCaptorPipe.lastMetadata = undefined;
});
afterAll(async () => {
await app.close();
});
it('should pass schema to pipe via @Body(options)', async () => {
await request(server)
.post('/schema-test/body')
.send({ name: 'test' })
.expect(201);
expect(SchemaCaptorPipe.lastMetadata).toBeDefined();
expect(SchemaCaptorPipe.lastMetadata!.schema).toBe(testSchema);
expect(SchemaCaptorPipe.lastMetadata!.type).toBe('body');
});
it('should pass schema to pipe via @Query(options)', async () => {
await request(server).get('/schema-test/query?user=john').expect(200);
expect(SchemaCaptorPipe.lastMetadata).toBeDefined();
expect(SchemaCaptorPipe.lastMetadata!.schema).toBe(testSchema);
expect(SchemaCaptorPipe.lastMetadata!.type).toBe('query');
});
it('should pass schema to pipe via @Param(property, options)', async () => {
await request(server)
.get('/schema-test/param/42')
.expect(200)
.expect({ received: '42' });
expect(SchemaCaptorPipe.lastMetadata).toBeDefined();
expect(SchemaCaptorPipe.lastMetadata!.schema).toBe(testSchema);
expect(SchemaCaptorPipe.lastMetadata!.type).toBe('param');
expect(SchemaCaptorPipe.lastMetadata!.data).toBe('id');
});
it('should pass schema to pipe via createParamDecorator(options)', async () => {
await request(server).get('/schema-test/custom?key=val').expect(200);
expect(SchemaCaptorPipe.lastMetadata).toBeDefined();
expect(SchemaCaptorPipe.lastMetadata!.schema).toBe(testSchema);
expect(SchemaCaptorPipe.lastMetadata!.type).toBe('custom');
});
it('should pass schema to pipe via @Body(property, options)', async () => {
await request(server)
.post('/schema-test/body-property')
.send({ name: 'Alice' })
.expect(201)
.expect({ received: 'Alice' });
expect(SchemaCaptorPipe.lastMetadata).toBeDefined();
expect(SchemaCaptorPipe.lastMetadata!.schema).toBe(testSchema);
expect(SchemaCaptorPipe.lastMetadata!.type).toBe('body');
expect(SchemaCaptorPipe.lastMetadata!.data).toBe('name');
});
});

View File

@@ -0,0 +1,316 @@
import {
Controller,
Get,
INestApplication,
Module,
SerializeOptions,
StandardSchemaSerializerInterceptor,
UseInterceptors,
} from '@nestjs/common';
import { APP_INTERCEPTOR, Reflector } from '@nestjs/core';
import { Test } from '@nestjs/testing';
import type { StandardSchemaV1 } from '@standard-schema/spec';
import request from 'supertest';
// ─── Test schemas ──────────────────────────────────────────────
/**
* Schema that strips out the `password` field (simulating a "safe user" DTO).
*/
const safeUserSchema: StandardSchemaV1 = {
'~standard': {
version: 1,
vendor: 'test',
validate: (value: unknown) => {
const { password, ...safe } = value as Record<string, unknown>;
return { value: safe };
},
},
};
/**
* Schema that adds a `serialized: true` flag (useful for asserting the schema ran).
*/
const flagSchema: StandardSchemaV1 = {
'~standard': {
version: 1,
vendor: 'test',
validate: (value: unknown) => ({
value: { ...(value as any), serialized: true },
}),
},
};
/**
* Schema that always fails — used for the error case.
*/
const failingSchema: StandardSchemaV1 = {
'~standard': {
version: 1,
vendor: 'test',
validate: () => ({
issues: [{ message: 'not allowed' }],
}),
},
};
/**
* Async schema — validates that the interceptor awaits promises.
*/
const asyncSchema: StandardSchemaV1 = {
'~standard': {
version: 1,
vendor: 'test',
validate: async (value: unknown) => ({
value: { ...(value as any), async: true },
}),
},
};
// ─── Controllers ───────────────────────────────────────────────
@Controller('serializer')
@UseInterceptors(StandardSchemaSerializerInterceptor)
class SerializerTestController {
@Get('user')
@SerializeOptions({ schema: safeUserSchema })
getUser() {
return { id: 1, name: 'Alice', password: 'secret123' };
}
@Get('users')
@SerializeOptions({ schema: safeUserSchema })
getUsers() {
return [
{ id: 1, name: 'Alice', password: 'pw1' },
{ id: 2, name: 'Bob', password: 'pw2' },
];
}
@Get('flagged')
@SerializeOptions({ schema: flagSchema })
getFlagged() {
return { id: 1 };
}
@Get('no-schema')
getNoSchema() {
return { id: 1, secret: 'visible' };
}
@Get('failing')
@SerializeOptions({ schema: failingSchema })
getFailing() {
return { id: 1 };
}
@Get('async')
@SerializeOptions({ schema: asyncSchema })
getAsync() {
return { id: 1 };
}
@Get('primitive')
@SerializeOptions({ schema: failingSchema })
getPrimitive() {
return 'plain string';
}
}
/**
* Controller-level schema applied via class decorator — all routes inherit it.
*/
@Controller('class-level')
@UseInterceptors(StandardSchemaSerializerInterceptor)
@SerializeOptions({ schema: safeUserSchema })
class ClassLevelSerializerController {
@Get('user')
getUser() {
return { id: 1, name: 'Carol', password: 'secret' };
}
@Get('override')
@SerializeOptions({ schema: flagSchema })
getOverride() {
return { id: 1, name: 'Carol', password: 'secret' };
}
}
/**
* Controller demonstrating global interceptor registration with a default schema.
*/
@Controller('global')
class GlobalSerializerController {
@Get('default')
getDefault() {
return { id: 1, name: 'Dave', password: 'global-secret' };
}
@Get('override')
@SerializeOptions({ schema: flagSchema })
getOverride() {
return { id: 1 };
}
}
@Module({
controllers: [SerializerTestController, ClassLevelSerializerController],
})
class SerializerTestModule {}
@Module({
controllers: [GlobalSerializerController],
providers: [
{
provide: APP_INTERCEPTOR,
useFactory: (reflector: Reflector) =>
new StandardSchemaSerializerInterceptor(reflector, {
schema: safeUserSchema,
}),
inject: [Reflector],
},
],
})
class GlobalSerializerTestModule {}
// ─── Tests ─────────────────────────────────────────────────────
describe('StandardSchemaSerializerInterceptor (integration)', () => {
let app: INestApplication;
afterEach(async () => {
await app.close();
});
describe('handler-level @SerializeOptions', () => {
beforeEach(async () => {
const module = await Test.createTestingModule({
imports: [SerializerTestModule],
}).compile();
app = module.createNestApplication();
await app.init();
});
it('should strip fields via schema on a single object', () => {
return request(app.getHttpServer())
.get('/serializer/user')
.expect(200)
.expect(({ body }) => {
expect(body).toEqual({ id: 1, name: 'Alice' });
expect(body).not.toHaveProperty('password');
});
});
it('should apply schema to each item in an array response', () => {
return request(app.getHttpServer())
.get('/serializer/users')
.expect(200)
.expect(({ body }) => {
expect(body).toEqual([
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
]);
body.forEach((item: any) =>
expect(item).not.toHaveProperty('password'),
);
});
});
it('should augment response through the schema', () => {
return request(app.getHttpServer())
.get('/serializer/flagged')
.expect(200)
.expect({ id: 1, serialized: true });
});
it('should return response unchanged when no schema is set', () => {
return request(app.getHttpServer())
.get('/serializer/no-schema')
.expect(200)
.expect({ id: 1, secret: 'visible' });
});
it('should return 500 when schema validation fails', () => {
return request(app.getHttpServer())
.get('/serializer/failing')
.expect(500);
});
it('should handle async schemas', () => {
return request(app.getHttpServer())
.get('/serializer/async')
.expect(200)
.expect({ id: 1, async: true });
});
it('should pass primitive values through even when a schema is set', () => {
return request(app.getHttpServer())
.get('/serializer/primitive')
.expect(200)
.expect(({ text }) => {
expect(text).toBe('plain string');
});
});
});
describe('class-level @SerializeOptions', () => {
beforeEach(async () => {
const module = await Test.createTestingModule({
imports: [SerializerTestModule],
}).compile();
app = module.createNestApplication();
await app.init();
});
it('should apply class-level schema to all routes', () => {
return request(app.getHttpServer())
.get('/class-level/user')
.expect(200)
.expect(({ body }) => {
expect(body).toEqual({ id: 1, name: 'Carol' });
expect(body).not.toHaveProperty('password');
});
});
it('should allow handler-level schema to override class-level', () => {
return request(app.getHttpServer())
.get('/class-level/override')
.expect(200)
.expect(({ body }) => {
// flagSchema adds `serialized: true` but does NOT strip password
expect(body).toHaveProperty('serialized', true);
expect(body).toHaveProperty('password', 'secret');
});
});
});
describe('global interceptor with default schema', () => {
beforeEach(async () => {
const module = await Test.createTestingModule({
imports: [GlobalSerializerTestModule],
}).compile();
app = module.createNestApplication();
await app.init();
});
it('should apply the default schema globally', () => {
return request(app.getHttpServer())
.get('/global/default')
.expect(200)
.expect(({ body }) => {
expect(body).toEqual({ id: 1, name: 'Dave' });
expect(body).not.toHaveProperty('password');
});
});
it('should let @SerializeOptions override the global default', () => {
return request(app.getHttpServer())
.get('/global/override')
.expect(200)
.expect({ id: 1, serialized: true });
});
});
});

View File

@@ -1,7 +1,7 @@
import { Module } from '@nestjs/common';
import { HelloModule } from './hello/hello.module';
import { HostArrayModule } from './host-array/host-array.module';
import { HostModule } from './host/host.module';
import { HelloModule } from './hello/hello.module.js';
import { HostArrayModule } from './host-array/host-array.module.js';
import { HostModule } from './host/host.module.js';
@Module({
imports: [HelloModule, HostModule, HostArrayModule],

View File

@@ -1,7 +1,7 @@
import { Controller, Get, Header, Param } from '@nestjs/common';
import { Observable, of } from 'rxjs';
import { HelloService } from './hello.service';
import { UserByIdPipe } from './users/user-by-id.pipe';
import { HelloService } from './hello.service.js';
import { UserByIdPipe } from './users/user-by-id.pipe.js';
@Controller('hello')
export class HelloController {

View File

@@ -1,7 +1,7 @@
import { Module } from '@nestjs/common';
import { HelloController } from './hello.controller';
import { HelloService } from './hello.service';
import { UsersService } from './users/users.service';
import { HelloController } from './hello.controller.js';
import { HelloService } from './hello.service.js';
import { UsersService } from './users/users.service.js';
@Module({
controllers: [HelloController],

View File

@@ -1,5 +1,5 @@
import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersService } from './users.service.js';
@Injectable()
export class UserByIdPipe implements PipeTransform<string> {

View File

@@ -1,7 +1,7 @@
import { Controller, Get, Header, HostParam, Param } from '@nestjs/common';
import { Observable, of } from 'rxjs';
import { HostArrayService } from './host-array.service';
import { UserByIdPipe } from './users/user-by-id.pipe';
import { HostArrayService } from './host-array.service.js';
import { UserByIdPipe } from './users/user-by-id.pipe.js';
@Controller({
path: 'host-array',

View File

@@ -1,7 +1,7 @@
import { Module } from '@nestjs/common';
import { HostArrayController } from './host-array.controller';
import { HostArrayService } from './host-array.service';
import { UsersService } from './users/users.service';
import { HostArrayController } from './host-array.controller.js';
import { HostArrayService } from './host-array.service.js';
import { UsersService } from './users/users.service.js';
@Module({
controllers: [HostArrayController],

View File

@@ -1,5 +1,5 @@
import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersService } from './users.service.js';
@Injectable()
export class UserByIdPipe implements PipeTransform<string> {

View File

@@ -1,7 +1,7 @@
import { Controller, Get, Header, HostParam, Param } from '@nestjs/common';
import { Observable, of } from 'rxjs';
import { HostService } from './host.service';
import { UserByIdPipe } from './users/user-by-id.pipe';
import { HostService } from './host.service.js';
import { UserByIdPipe } from './users/user-by-id.pipe.js';
@Controller({
path: 'host',

View File

@@ -1,7 +1,7 @@
import { Module } from '@nestjs/common';
import { HostController } from './host.controller';
import { HostService } from './host.service';
import { UsersService } from './users/users.service';
import { HostController } from './host.controller.js';
import { HostService } from './host.service.js';
import { UsersService } from './users/users.service.js';
@Module({
controllers: [HostController],

View File

@@ -1,5 +1,5 @@
import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersService } from './users.service.js';
@Injectable()
export class UserByIdPipe implements PipeTransform<string> {

View File

@@ -1,13 +1,15 @@
{
"compilerOptions": {
"types": ["vitest/globals"],
"module": "commonjs",
"declaration": false,
"noImplicitAny": false,
"removeComments": true,
"noLib": false,
"esModuleInterop": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "ES2021",
"target": "ES2023",
"sourceMap": true,
"allowJs": true,
"strictNullChecks": true,

View File

@@ -1,11 +1,8 @@
import { BeforeApplicationShutdown, Injectable, Module } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { expect } from 'chai';
import * as Sinon from 'sinon';
@Injectable()
class TestInjectable implements BeforeApplicationShutdown {
beforeApplicationShutdown = Sinon.spy();
beforeApplicationShutdown = vi.fn();
}
describe('BeforeApplicationShutdown', () => {
@@ -17,13 +14,13 @@ describe('BeforeApplicationShutdown', () => {
const app = module.createNestApplication();
await app.close();
const instance = module.get(TestInjectable);
expect(instance.beforeApplicationShutdown.called).to.be.true;
expect(instance.beforeApplicationShutdown).toHaveBeenCalled();
});
it('should sort modules by distance (topological sort) - DESC order', async () => {
@Injectable()
class BB implements BeforeApplicationShutdown {
beforeApplicationShutdown = Sinon.spy();
beforeApplicationShutdown = vi.fn();
}
@Module({
@@ -35,7 +32,7 @@ describe('BeforeApplicationShutdown', () => {
@Injectable()
class AA implements BeforeApplicationShutdown {
constructor(private bb: BB) {}
beforeApplicationShutdown = Sinon.spy();
beforeApplicationShutdown = vi.fn();
}
@Module({
imports: [B],
@@ -53,9 +50,8 @@ describe('BeforeApplicationShutdown', () => {
const aa = module.get(AA);
const bb = module.get(BB);
Sinon.assert.callOrder(
aa.beforeApplicationShutdown,
bb.beforeApplicationShutdown,
);
expect(
aa.beforeApplicationShutdown.mock.invocationCallOrder[0],
).toBeLessThan(bb.beforeApplicationShutdown.mock.invocationCallOrder[0]);
});
});

View File

@@ -1,71 +1,81 @@
import { expect } from 'chai';
import { spawnSync } from 'child_process';
import { join } from 'path';
const nodeCmd = process.execPath;
function spawnTsNode(...args: string[]) {
return spawnSync(nodeCmd, ['--import', 'jiti/register', ...args]);
}
describe('enableShutdownHooks', () => {
it('should call the correct hooks if any shutdown signal gets invoked', done => {
const result = spawnSync('ts-node', [
join(__dirname, '../src/enable-shutdown-hooks-main.ts'),
'SIGHUP',
]);
const calls = result.stdout
.toString()
.split('\n')
.map((call: string) => call.trim());
expect(calls[0]).to.equal('beforeApplicationShutdown SIGHUP');
expect(calls[1]).to.equal('onApplicationShutdown SIGHUP');
done();
}).timeout(10000);
it('should call the correct hooks if any shutdown signal gets invoked', () =>
new Promise<void>(done => {
const result = spawnTsNode(
join(import.meta.dirname, '../src/enable-shutdown-hooks-main.ts'),
'SIGHUP',
);
const calls = result.stdout
.toString()
.split('\n')
.map((call: string) => call.trim());
expect(calls[0]).toBe('beforeApplicationShutdown SIGHUP');
expect(calls[1]).toBe('onApplicationShutdown SIGHUP');
done();
}));
it('should call the correct hooks if a specific shutdown signal gets invoked', done => {
const result = spawnSync('ts-node', [
join(__dirname, '../src/enable-shutdown-hooks-main.ts'),
'SIGINT',
'SIGINT',
]);
const calls = result.stdout
.toString()
.split('\n')
.map((call: string) => call.trim());
expect(calls[0]).to.equal('beforeApplicationShutdown SIGINT');
expect(calls[1]).to.equal('onApplicationShutdown SIGINT');
done();
}).timeout(10000);
it('should call the correct hooks if a specific shutdown signal gets invoked', () =>
new Promise<void>(done => {
const result = spawnTsNode(
join(import.meta.dirname, '../src/enable-shutdown-hooks-main.ts'),
'SIGINT',
'SIGINT',
);
const calls = result.stdout
.toString()
.split('\n')
.map((call: string) => call.trim());
expect(calls[0]).toBe('beforeApplicationShutdown SIGINT');
expect(calls[1]).toBe('onApplicationShutdown SIGINT');
done();
}));
it('should ignore system signals which are not specified', done => {
const result = spawnSync('ts-node', [
join(__dirname, '../src/enable-shutdown-hooks-main.ts'),
'SIGINT',
'SIGHUP',
]);
expect(result.stdout.toString().trim()).to.be.eq('');
done();
}).timeout(10000);
it('should ignore system signals which are not specified', () =>
new Promise<void>(done => {
const result = spawnTsNode(
join(import.meta.dirname, '../src/enable-shutdown-hooks-main.ts'),
'SIGINT',
'SIGHUP',
);
expect(result.stdout.toString().trim()).toBe('');
done();
}));
it('should ignore system signals if "enableShutdownHooks" was not called', done => {
const result = spawnSync('ts-node', [
join(__dirname, '../src/enable-shutdown-hooks-main.ts'),
'SIGINT',
'NONE',
]);
expect(result.stdout.toString().trim()).to.be.eq('');
done();
}).timeout(10000);
it('should ignore system signals if "enableShutdownHooks" was not called', () =>
new Promise<void>(done => {
const result = spawnTsNode(
join(import.meta.dirname, '../src/enable-shutdown-hooks-main.ts'),
'SIGINT',
'NONE',
);
expect(result.stdout.toString().trim()).toBe('');
done();
}));
it('should call the correct hooks with useProcessExit option', done => {
const result = spawnSync('ts-node', [
join(__dirname, '../src/enable-shutdown-hooks-main.ts'),
'SIGHUP',
'SIGHUP',
'graceful',
]);
const calls = result.stdout
.toString()
.split('\n')
.map((call: string) => call.trim());
expect(calls[0]).to.equal('beforeApplicationShutdown SIGHUP');
expect(calls[1]).to.equal('onApplicationShutdown SIGHUP');
expect(result.status).to.equal(0);
done();
}).timeout(10000);
it('should call the correct hooks with useProcessExit option', () =>
new Promise<void>(done => {
const result = spawnTsNode(
join(import.meta.dirname, '../src/enable-shutdown-hooks-main.ts'),
'SIGHUP',
'SIGHUP',
'graceful',
);
const calls = result.stdout
.toString()
.split('\n')
.map((call: string) => call.trim());
expect(calls[0]).toBe('beforeApplicationShutdown SIGHUP');
expect(calls[1]).toBe('onApplicationShutdown SIGHUP');
expect(result.status).toBe(0);
done();
}));
});

View File

@@ -1,13 +1,12 @@
import { Test } from '@nestjs/testing';
import * as Sinon from 'sinon';
import {
BeforeApplicationShutdown,
Injectable,
OnApplicationBootstrap,
OnApplicationShutdown,
OnModuleDestroy,
OnModuleInit,
BeforeApplicationShutdown,
} from '@nestjs/common';
import { Test } from '@nestjs/testing';
@Injectable()
class TestInjectable
@@ -18,11 +17,11 @@ class TestInjectable
OnApplicationShutdown,
BeforeApplicationShutdown
{
onApplicationBootstrap = Sinon.spy();
beforeApplicationShutdown = Sinon.spy();
onApplicationShutdown = Sinon.spy();
onModuleDestroy = Sinon.spy();
onModuleInit = Sinon.spy();
onApplicationBootstrap = vi.fn();
beforeApplicationShutdown = vi.fn();
onApplicationShutdown = vi.fn();
onModuleDestroy = vi.fn();
onModuleInit = vi.fn();
}
describe('Lifecycle Hook Order', () => {
@@ -36,12 +35,17 @@ describe('Lifecycle Hook Order', () => {
await app.close();
const instance = module.get(TestInjectable);
Sinon.assert.callOrder(
const order = [
instance.onModuleInit,
instance.onApplicationBootstrap,
instance.onModuleDestroy,
instance.beforeApplicationShutdown,
instance.onApplicationShutdown,
);
];
for (let i = 0; i < order.length - 1; i++) {
expect(order[i].mock.invocationCallOrder[0]).toBeLessThan(
order[i + 1].mock.invocationCallOrder[0],
);
}
});
});

View File

@@ -1,11 +1,8 @@
import { Injectable, Module, OnApplicationBootstrap } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { expect } from 'chai';
import * as Sinon from 'sinon';
@Injectable()
class TestInjectable implements OnApplicationBootstrap {
onApplicationBootstrap = Sinon.spy();
onApplicationBootstrap = vi.fn();
}
describe('OnApplicationBootstrap', () => {
@@ -17,7 +14,8 @@ describe('OnApplicationBootstrap', () => {
const app = module.createNestApplication();
await app.init();
const instance = module.get(TestInjectable);
expect(instance.onApplicationBootstrap.called).to.be.true;
expect(instance.onApplicationBootstrap).toHaveBeenCalled();
await app.close();
});
it('should not throw an error when onApplicationBootstrap is null', async () => {
@@ -28,7 +26,8 @@ describe('OnApplicationBootstrap', () => {
}).compile();
const app = module.createNestApplication();
await app.init().then(obj => expect(obj).to.not.be.undefined);
await app.init().then(obj => expect(obj).not.toBeUndefined());
await app.close();
});
it('should not throw an error when onApplicationBootstrap is undefined', async () => {
@@ -39,7 +38,8 @@ describe('OnApplicationBootstrap', () => {
}).compile();
const app = module.createNestApplication();
await app.init().then(obj => expect(obj).to.not.be.undefined);
await app.init().then(obj => expect(obj).not.toBeUndefined());
await app.close();
});
it('should sort modules by distance (topological sort) - DESC order', async () => {
@@ -80,6 +80,7 @@ describe('OnApplicationBootstrap', () => {
await app.init();
const instance = module.get(AA);
expect(instance.field).to.equal('b-field_a-field');
expect(instance.field).toBe('b-field_a-field');
await app.close();
});
});

View File

@@ -1,11 +1,8 @@
import { Injectable, Module, OnApplicationShutdown } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { expect } from 'chai';
import * as Sinon from 'sinon';
@Injectable()
class TestInjectable implements OnApplicationShutdown {
onApplicationShutdown = Sinon.spy();
onApplicationShutdown = vi.fn();
}
describe('OnApplicationShutdown', () => {
@@ -17,13 +14,13 @@ describe('OnApplicationShutdown', () => {
const app = module.createNestApplication();
await app.close();
const instance = module.get(TestInjectable);
expect(instance.onApplicationShutdown.called).to.be.true;
expect(instance.onApplicationShutdown).toHaveBeenCalled();
});
it('should sort modules by distance (topological sort) - DESC order', async () => {
@Injectable()
class BB implements OnApplicationShutdown {
onApplicationShutdown = Sinon.spy();
onApplicationShutdown = vi.fn();
}
@Module({
@@ -35,7 +32,7 @@ describe('OnApplicationShutdown', () => {
@Injectable()
class AA implements OnApplicationShutdown {
constructor(private bb: BB) {}
onApplicationShutdown = Sinon.spy();
onApplicationShutdown = vi.fn();
}
@Module({
imports: [B],
@@ -53,6 +50,8 @@ describe('OnApplicationShutdown', () => {
const aa = module.get(AA);
const bb = module.get(BB);
Sinon.assert.callOrder(aa.onApplicationShutdown, bb.onApplicationShutdown);
expect(aa.onApplicationShutdown.mock.invocationCallOrder[0]).toBeLessThan(
bb.onApplicationShutdown.mock.invocationCallOrder[0],
);
});
});

View File

@@ -1,11 +1,8 @@
import { Injectable, Module, OnModuleDestroy } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { expect } from 'chai';
import * as Sinon from 'sinon';
@Injectable()
class TestInjectable implements OnModuleDestroy {
onModuleDestroy = Sinon.spy();
onModuleDestroy = vi.fn();
}
describe('OnModuleDestroy', () => {
@@ -17,7 +14,7 @@ describe('OnModuleDestroy', () => {
const app = module.createNestApplication();
await app.close();
const instance = module.get(TestInjectable);
expect(instance.onModuleDestroy.called).to.be.true;
expect(instance.onModuleDestroy).toHaveBeenCalled();
});
it('should not throw an error when onModuleDestroy is null', async () => {
@@ -26,7 +23,8 @@ describe('OnModuleDestroy', () => {
}).compile();
const app = module.createNestApplication();
await app.init().then(obj => expect(obj).to.not.be.undefined);
await app.init().then(obj => expect(obj).not.toBeUndefined());
await app.close();
});
it('should not throw an error when onModuleDestroy is undefined', async () => {
@@ -37,13 +35,14 @@ describe('OnModuleDestroy', () => {
}).compile();
const app = module.createNestApplication();
await app.init().then(obj => expect(obj).to.not.be.undefined);
await app.init().then(obj => expect(obj).not.toBeUndefined());
await app.close();
});
it('should sort modules by distance (topological sort) - DESC order', async () => {
@Injectable()
class BB implements OnModuleDestroy {
onModuleDestroy = Sinon.spy();
onModuleDestroy = vi.fn();
}
@Module({
@@ -55,7 +54,7 @@ describe('OnModuleDestroy', () => {
@Injectable()
class AA implements OnModuleDestroy {
constructor(private bb: BB) {}
onModuleDestroy = Sinon.spy();
onModuleDestroy = vi.fn();
}
@Module({
@@ -74,6 +73,8 @@ describe('OnModuleDestroy', () => {
const aa = module.get(AA);
const bb = module.get(BB);
Sinon.assert.callOrder(aa.onModuleDestroy, bb.onModuleDestroy);
expect(aa.onModuleDestroy.mock.invocationCallOrder[0]).toBeLessThan(
bb.onModuleDestroy.mock.invocationCallOrder[0],
);
});
});

View File

@@ -1,11 +1,8 @@
import { Injectable, Module, OnModuleInit } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { expect } from 'chai';
import * as Sinon from 'sinon';
@Injectable()
class TestInjectable implements OnModuleInit {
onModuleInit = Sinon.spy();
onModuleInit = vi.fn();
}
describe('OnModuleInit', () => {
@@ -17,7 +14,8 @@ describe('OnModuleInit', () => {
const app = module.createNestApplication();
await app.init();
const instance = module.get(TestInjectable);
expect(instance.onModuleInit.called).to.be.true;
expect(instance.onModuleInit).toHaveBeenCalled();
await app.close();
});
it('should not throw an error when onModuleInit is null', async () => {
@@ -26,7 +24,8 @@ describe('OnModuleInit', () => {
}).compile();
const app = module.createNestApplication();
await app.init().then(obj => expect(obj).to.not.be.undefined);
await app.init().then(obj => expect(obj).not.toBeUndefined());
await app.close();
});
it('should not throw an error when onModuleInit is undefined', async () => {
@@ -35,7 +34,8 @@ describe('OnModuleInit', () => {
}).compile();
const app = module.createNestApplication();
await app.init().then(obj => expect(obj).to.not.be.undefined);
await app.init().then(obj => expect(obj).not.toBeUndefined());
await app.close();
});
it('should sort modules by distance (topological sort) - DESC order', async () => {
@@ -109,6 +109,7 @@ describe('OnModuleInit', () => {
await app.init();
const instance = module.get(AA);
expect(instance.field).to.equal('c-field_b-field_a-field');
expect(instance.field).toBe('c-field_b-field_a-field');
await app.close();
});
});

View File

@@ -1,13 +1,15 @@
{
"compilerOptions": {
"types": ["vitest/globals"],
"module": "commonjs",
"declaration": false,
"noImplicitAny": false,
"removeComments": true,
"noLib": false,
"esModuleInterop": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "ES2021",
"target": "ES2023",
"sourceMap": true,
"allowJs": true,
"strictNullChecks": true,

View File

@@ -1,5 +1,4 @@
import { Test } from '@nestjs/testing';
import { expect } from 'chai';
import { Controller, Injectable, Module } from '@nestjs/common';
@@ -29,35 +28,23 @@ export class AppModule {}
describe('Circular custom providers', () => {
it('should throw an exception (useClass + regular provider)', async () => {
try {
const builder = Test.createTestingModule({
imports: [AppModule],
});
await builder.compile();
expect(true).to.be.eql(false);
} catch (err) {
expect(err.message).to.be.eql(
'A circular dependency has been detected inside "A". Please, make sure that each side of a bidirectional relationships are decorated with "forwardRef()". Note that circular relationships between custom providers (e.g., factories) are not supported since functions cannot be called more than once.',
);
}
const builder = Test.createTestingModule({
imports: [AppModule],
});
await expect(builder.compile()).rejects.toThrow(
'A circular dependency has been detected inside "A". Please, make sure that each side of a bidirectional relationships are decorated with "forwardRef()". Note that circular relationships between custom providers (e.g., factories) are not supported since functions cannot be called more than once.',
);
});
it('should throw an exception (2 factories)', async () => {
try {
const builder = Test.createTestingModule({
providers: [
{ provide: 'ABC', useFactory: () => ({}), inject: ['DEF'] },
{ provide: 'DEF', useFactory: () => ({}), inject: ['ABC'] },
],
});
await builder.compile();
expect(true).to.be.eql(false);
} catch (err) {
expect(err.message).to.be.eql(
'A circular dependency has been detected inside "ABC". Please, make sure that each side of a bidirectional relationships are decorated with "forwardRef()". Note that circular relationships between custom providers (e.g., factories) are not supported since functions cannot be called more than once.',
);
}
const builder = Test.createTestingModule({
providers: [
{ provide: 'ABC', useFactory: () => ({}), inject: ['DEF'] },
{ provide: 'DEF', useFactory: () => ({}), inject: ['ABC'] },
],
});
await expect(builder.compile()).rejects.toThrow(
'A circular dependency has been detected inside "ABC"',
);
});
});

View File

@@ -1,9 +1,8 @@
import { Test } from '@nestjs/testing';
import { expect } from 'chai';
import { CircularModule } from '../src/circular-modules/circular.module';
import { CircularService } from '../src/circular-modules/circular.service';
import { InputModule } from '../src/circular-modules/input.module';
import { InputService } from '../src/circular-modules/input.service';
import { CircularModule } from '../src/circular-modules/circular.module.js';
import { CircularService } from '../src/circular-modules/circular.service.js';
import { InputModule } from '../src/circular-modules/input.module.js';
import { InputService } from '../src/circular-modules/input.service.js';
describe('Circular dependency (modules)', () => {
it('should resolve circular dependency between providers', async () => {
@@ -14,7 +13,7 @@ describe('Circular dependency (modules)', () => {
const inputService = testingModule.get<InputService>(InputService);
const circularService = testingModule.get<CircularService>(CircularService);
expect(inputService.service).to.be.eql(circularService);
expect(circularService.service).to.be.eql(inputService);
expect(inputService.service).toEqual(circularService);
expect(circularService.service).toEqual(inputService);
});
});

View File

@@ -1,9 +1,8 @@
import { Test } from '@nestjs/testing';
import { expect } from 'chai';
import { CircularPropertiesModule } from '../src/circular-properties/circular-properties.module';
import { CircularService } from '../src/circular-properties/circular.service';
import { InputPropertiesModule } from '../src/circular-properties/input-properties.module';
import { InputService } from '../src/circular-properties/input.service';
import { CircularPropertiesModule } from '../src/circular-properties/circular-properties.module.js';
import { CircularService } from '../src/circular-properties/circular.service.js';
import { InputPropertiesModule } from '../src/circular-properties/input-properties.module.js';
import { InputService } from '../src/circular-properties/input.service.js';
describe('Circular properties dependency (modules)', () => {
it('should resolve circular dependency between providers', async () => {
@@ -14,7 +13,7 @@ describe('Circular properties dependency (modules)', () => {
const inputService = testingModule.get<InputService>(InputService);
const circularService = testingModule.get<CircularService>(CircularService);
expect(inputService.service).to.be.eql(circularService);
expect(circularService.service).to.be.eql(inputService);
expect(inputService.service).toEqual(circularService);
expect(circularService.service).toEqual(inputService);
});
});

View File

@@ -1,7 +1,6 @@
import { expect } from 'chai';
import { Test } from '@nestjs/testing';
import { CircularModule } from '../src/circular-structure-dynamic-module/circular.module';
import { InputService } from '../src/circular-structure-dynamic-module/input.service';
import { CircularModule } from '../src/circular-structure-dynamic-module/circular.module.js';
import { InputService } from '../src/circular-structure-dynamic-module/input.service.js';
describe('Circular structure for dynamic modules', () => {
it('should resolve circular structure with dynamic modules', async () => {
@@ -11,6 +10,6 @@ describe('Circular structure for dynamic modules', () => {
const testingModule = await builder.compile();
const inputService = testingModule.get<InputService>(InputService);
expect(inputService).to.be.instanceof(InputService);
expect(inputService).toBeInstanceOf(InputService);
});
});

View File

@@ -1,8 +1,7 @@
import { Test } from '@nestjs/testing';
import { expect } from 'chai';
import { CircularModule } from '../src/circular/circular.module';
import { CircularService } from '../src/circular/circular.service';
import { InputService } from '../src/circular/input.service';
import { CircularModule } from '../src/circular/circular.module.js';
import { CircularService } from '../src/circular/circular.service.js';
import { InputService } from '../src/circular/input.service.js';
describe('Circular dependency', () => {
it('should resolve circular dependency between providers', async () => {
@@ -13,7 +12,7 @@ describe('Circular dependency', () => {
const inputService = testingModule.get<InputService>(InputService);
const circularService = testingModule.get<CircularService>(CircularService);
expect(inputService.service).to.be.eql(circularService);
expect(circularService.service).to.be.eql(inputService);
expect(inputService.service).toEqual(circularService);
expect(circularService.service).toEqual(inputService);
});
});

View File

@@ -1,7 +1,6 @@
import { Test, TestingModule } from '@nestjs/testing';
import { expect } from 'chai';
import { CoreInjectablesModule } from '../src/core-injectables/core-injectables.module';
import { ApplicationConfig, ModuleRef } from '@nestjs/core';
import { Test, TestingModule } from '@nestjs/testing';
import { CoreInjectablesModule } from '../src/core-injectables/core-injectables.module.js';
describe('Core Injectables', () => {
let testingModule: TestingModule;
@@ -13,26 +12,30 @@ describe('Core Injectables', () => {
testingModule = await builder.compile();
});
afterEach(async () => {
await testingModule.close();
});
it('should provide ApplicationConfig as core injectable', () => {
const applicationConfig =
testingModule.get<ApplicationConfig>(ApplicationConfig);
applicationConfig.setGlobalPrefix('/api');
expect(applicationConfig).to.not.be.undefined;
expect(applicationConfig.getGlobalPrefix()).to.be.eq('/api');
expect(applicationConfig).not.toBeUndefined();
expect(applicationConfig.getGlobalPrefix()).toBe('/api');
});
it('should provide ModuleRef as core injectable', () => {
const moduleRef = testingModule.get<ModuleRef>(ModuleRef);
expect(moduleRef).to.not.be.undefined;
expect(moduleRef).not.toBeUndefined();
});
it('should provide the current Module as provider', () => {
const module = testingModule.get<CoreInjectablesModule>(
CoreInjectablesModule,
);
expect(module).to.not.be.undefined;
expect(module.constructor.name).to.be.eq('CoreInjectablesModule');
expect(module).not.toBeUndefined();
expect(module.constructor.name).toBe('CoreInjectablesModule');
});
});

View File

@@ -1,7 +1,6 @@
import { Test } from '@nestjs/testing';
import { expect } from 'chai';
import { DefaultsModule } from '../src/defaults/defaults.module';
import { DefaultsService } from '../src/defaults/defaults.service';
import { DefaultsModule } from '../src/defaults/defaults.module.js';
import { DefaultsService } from '../src/defaults/defaults.service.js';
describe('Injector', () => {
describe('when optional', () => {
@@ -10,7 +9,7 @@ describe('Injector', () => {
imports: [DefaultsModule],
});
const app = await builder.compile();
expect(app.get(DefaultsService).coreService.default).to.be.true;
expect(app.get(DefaultsService).coreService.default).toBe(true);
});
});
});

View File

@@ -1,36 +1,30 @@
import { RuntimeException } from '@nestjs/core/errors/exceptions/runtime.exception';
import { UnknownDependenciesException } from '@nestjs/core/errors/exceptions/unknown-dependencies.exception';
import { UnknownExportException } from '@nestjs/core/errors/exceptions/unknown-export.exception';
import { RuntimeException } from '@nestjs/core/errors/exceptions/runtime.exception.js';
import { UnknownDependenciesException } from '@nestjs/core/errors/exceptions/unknown-dependencies.exception.js';
import { UnknownExportException } from '@nestjs/core/errors/exceptions/unknown-export.exception.js';
import { Test } from '@nestjs/testing';
import { expect } from 'chai';
import * as chai from 'chai';
import * as chaiAsPromised from 'chai-as-promised';
import {
DYNAMIC_TOKEN,
DYNAMIC_VALUE,
NestDynamicModule,
} from '../src/dynamic/dynamic.module';
import { ExportsModule } from '../src/exports/exports.module';
import { InjectModule } from '../src/inject/inject.module';
import { InjectSameNameModule } from '../src/inject/inject-same-name.module';
} from '../src/dynamic/dynamic.module.js';
import { ExportsModule } from '../src/exports/exports.module.js';
import { InjectSameNameModule } from '../src/inject/inject-same-name.module.js';
import { InjectModule } from '../src/inject/inject.module.js';
import {
SelfInjectionProviderModule,
SelfInjectionProviderCustomTokenModule,
SelfInjectionForwardProviderModule,
} from '../src/self-injection/self-injection-provider.module';
chai.use(chaiAsPromised);
SelfInjectionProviderCustomTokenModule,
SelfInjectionProviderModule,
} from '../src/self-injection/self-injection-provider.module.js';
describe('Injector', () => {
describe('when "providers" and "exports" properties are inconsistent', () => {
it(`should fail with "UnknownExportException"`, async () => {
try {
const builder = Test.createTestingModule({
imports: [ExportsModule],
});
await builder.compile();
} catch (err) {
expect(err).to.be.instanceof(UnknownExportException);
}
const builder = Test.createTestingModule({
imports: [ExportsModule],
});
await expect(builder.compile()).rejects.toBeInstanceOf(
UnknownExportException,
);
});
});
@@ -40,20 +34,16 @@ describe('Injector', () => {
imports: [InjectSameNameModule],
});
await expect(builder.compile()).to.eventually.be.fulfilled;
await expect(builder.compile()).resolves.toBeDefined();
});
});
describe('when Nest cannot resolve dependencies', () => {
it(`should fail with "RuntimeException"`, async () => {
try {
const builder = Test.createTestingModule({
imports: [InjectModule],
});
await builder.compile();
} catch (err) {
expect(err).to.be.instanceof(RuntimeException);
}
const builder = Test.createTestingModule({
imports: [InjectModule],
});
await expect(builder.compile()).rejects.toBeInstanceOf(RuntimeException);
});
describe('due to self-injection providers', () => {
@@ -62,9 +52,7 @@ describe('Injector', () => {
imports: [SelfInjectionProviderModule],
});
await expect(
builder.compile(),
).to.eventually.be.rejected.and.be.an.instanceOf(
await expect(builder.compile()).rejects.toBeInstanceOf(
UnknownDependenciesException,
);
});
@@ -73,9 +61,7 @@ describe('Injector', () => {
imports: [SelfInjectionForwardProviderModule],
});
await expect(
builder.compile(),
).to.eventually.be.rejected.and.be.an.instanceOf(
await expect(builder.compile()).rejects.toBeInstanceOf(
UnknownDependenciesException,
);
});
@@ -84,9 +70,7 @@ describe('Injector', () => {
imports: [SelfInjectionProviderCustomTokenModule],
});
await expect(
builder.compile(),
).to.eventually.be.rejected.and.be.an.instanceOf(
await expect(builder.compile()).rejects.toBeInstanceOf(
UnknownDependenciesException,
);
});
@@ -99,7 +83,7 @@ describe('Injector', () => {
imports: [NestDynamicModule.byObject()],
});
const app = await builder.compile();
expect(app.get(DYNAMIC_TOKEN)).to.be.eql(DYNAMIC_VALUE);
expect(app.get(DYNAMIC_TOKEN)).toEqual(DYNAMIC_VALUE);
});
it(`should return provider via token (exported by token)`, async () => {
@@ -107,7 +91,7 @@ describe('Injector', () => {
imports: [NestDynamicModule.byName()],
});
const app = await builder.compile();
expect(app.get(DYNAMIC_TOKEN)).to.be.eql(DYNAMIC_VALUE);
expect(app.get(DYNAMIC_TOKEN)).toEqual(DYNAMIC_VALUE);
});
});
});

View File

@@ -1,10 +1,9 @@
import { Scope } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { Test, TestingModule } from '@nestjs/testing';
import { expect } from 'chai';
import { ScopedModule, STATIC_FACTORY } from '../src/scoped/scoped.module';
import { ScopedService } from '../src/scoped/scoped.service';
import { TransientService } from '../src/scoped/transient.service';
import { ScopedModule, STATIC_FACTORY } from '../src/scoped/scoped.module.js';
import { ScopedService } from '../src/scoped/scoped.service.js';
import { TransientService } from '../src/scoped/transient.service.js';
describe('Providers introspection', () => {
let testingModule: TestingModule;
@@ -17,18 +16,22 @@ describe('Providers introspection', () => {
moduleRef = testingModule.get(ModuleRef);
});
afterEach(async () => {
await testingModule.close();
});
it('should properly introspect a transient provider', async () => {
const introspectionResult = moduleRef.introspect(TransientService);
expect(introspectionResult.scope).to.be.equal(Scope.TRANSIENT);
expect(introspectionResult.scope).toBe(Scope.TRANSIENT);
});
it('should properly introspect a singleton provider', async () => {
const introspectionResult = moduleRef.introspect(STATIC_FACTORY);
expect(introspectionResult.scope).to.be.equal(Scope.DEFAULT);
expect(introspectionResult.scope).toBe(Scope.DEFAULT);
});
it('should properly introspect a request-scoped provider', async () => {
const introspectionResult = moduleRef.introspect(ScopedService);
expect(introspectionResult.scope).to.be.equal(Scope.REQUEST);
expect(introspectionResult.scope).toBe(Scope.REQUEST);
});
});

View File

@@ -1,6 +1,4 @@
import { Test } from '@nestjs/testing';
import { expect } from 'chai';
import * as sinon from 'sinon';
import { Global, Inject, Injectable, Module, Scope } from '@nestjs/common';
@Global()
@@ -64,11 +62,11 @@ export class Dependant {
) {}
public checkDependencies() {
expect(this.transientProvider).to.be.instanceOf(TransientProvider);
expect(this.foreignTransientProvider).to.be.instanceOf(
expect(this.transientProvider).toBeInstanceOf(TransientProvider);
expect(this.foreignTransientProvider).toBeInstanceOf(
ForeignTransientProvider,
);
expect(this.requestProvider).to.be.instanceOf(RequestProvider);
expect(this.requestProvider).toBeInstanceOf(RequestProvider);
}
}
@@ -149,9 +147,9 @@ describe('Many global modules', () => {
const moduleRef = await moduleBuilder.compile();
const dependant = await moduleRef.resolve(Dependant);
const checkDependenciesSpy = sinon.spy(dependant, 'checkDependencies');
const checkDependenciesSpy = vi.spyOn(dependant, 'checkDependencies');
dependant.checkDependencies();
expect(checkDependenciesSpy.called).to.be.true;
expect(checkDependenciesSpy).toHaveBeenCalled();
});
});

View File

@@ -1,6 +1,5 @@
import { Test } from '@nestjs/testing';
import { expect } from 'chai';
import { MultipleProvidersModule } from '../src/multiple-providers/multiple-providers.module';
import { MultipleProvidersModule } from '../src/multiple-providers/multiple-providers.module.js';
describe('Multiple providers under the same token ("each" feature)', () => {
describe('get()', () => {
@@ -20,7 +19,7 @@ describe('Multiple providers under the same token ("each" feature)', () => {
// @ts-expect-error: make sure "multiProviderInstances" is string[] not string
multiProviderInstances.charAt;
expect(multiProviderInstances).to.be.eql(['A', 'B', 'C']);
expect(multiProviderInstances).toEqual(['A', 'B', 'C']);
});
});
describe('resolve()', () => {
@@ -41,7 +40,7 @@ describe('Multiple providers under the same token ("each" feature)', () => {
// @ts-expect-error: make sure "multiProviderInstances" is string[] not string
multiProviderInstances.charAt;
expect(multiProviderInstances).to.be.eql(['A', 'B', 'C']);
expect(multiProviderInstances).toEqual(['A', 'B', 'C']);
});
it('should return an array of default-scoped providers', async () => {
@@ -61,7 +60,7 @@ describe('Multiple providers under the same token ("each" feature)', () => {
// @ts-expect-error: make sure "multiProviderInstances" is string[] not string
multiProviderInstances.charAt;
expect(multiProviderInstances).to.be.eql(['A', 'B', 'C']);
expect(multiProviderInstances).toEqual(['A', 'B', 'C']);
});
});
});

View File

@@ -1,7 +1,6 @@
import { Scope } from '@nestjs/common';
import { UnknownDependenciesException } from '@nestjs/core/errors/exceptions/unknown-dependencies.exception';
import { UnknownDependenciesException } from '@nestjs/core/errors/exceptions/unknown-dependencies.exception.js';
import { Test } from '@nestjs/testing';
import { expect } from 'chai';
describe('Optional factory provider deps', () => {
describe('when dependency is optional', () => {
@@ -20,7 +19,7 @@ describe('Optional factory provider deps', () => {
}).compile();
const factoryProvider = moduleRef.get('FACTORY');
expect(factoryProvider).to.equal('OPTIONAL_DEP_VALUE');
expect(factoryProvider).toBe('OPTIONAL_DEP_VALUE');
});
});
describe('otherwise', () => {
@@ -37,7 +36,7 @@ describe('Optional factory provider deps', () => {
}).compile();
const factoryProvider = moduleRef.get('FACTORY');
expect(factoryProvider).to.equal(defaultValue);
expect(factoryProvider).toBe(defaultValue);
});
it('"undefined" should be injected into the factory function (scoped provider)', async () => {
const MY_PROVIDER = 'MY_PROVIDER';
@@ -70,7 +69,7 @@ describe('Optional factory provider deps', () => {
],
}).compile();
expect(await module.resolve(MY_PROVIDER)).to.deep.equal({
expect(await module.resolve(MY_PROVIDER)).toEqual({
first: undefined,
second: 'second',
});
@@ -80,56 +79,44 @@ describe('Optional factory provider deps', () => {
describe('otherwise', () => {
describe('and dependency is not registered', () => {
it('should error out', async () => {
try {
const builder = Test.createTestingModule({
providers: [
{
provide: 'FACTORY',
useFactory: () => 'RETURNED_VALUE',
inject: ['MISSING_DEP'],
},
],
});
await builder.compile();
} catch (err) {
expect(err).to.be.instanceOf(UnknownDependenciesException);
}
const builder = Test.createTestingModule({
providers: [
{
provide: 'FACTORY',
useFactory: () => 'RETURNED_VALUE',
inject: ['MISSING_DEP'],
},
],
});
await expect(builder.compile()).rejects.toBeInstanceOf(
UnknownDependenciesException,
);
});
});
});
describe('and dependency is registered but it cannot be instantiated', () => {
it('should error out', async () => {
try {
const builder = Test.createTestingModule({
providers: [
{
provide: 'POSSIBLY_MISSING_DEP',
useFactory: () => null,
inject: ['MISSING_DEP'],
},
{
provide: 'FACTORY',
useFactory: () => 'RETURNED_VALUE',
inject: [{ token: 'POSSIBLY_MISSING_DEP', optional: false }],
},
],
});
await builder.compile();
} catch (err) {
expect(err).to.be.instanceOf(UnknownDependenciesException);
expect(err.message).to
.equal(`Nest can't resolve dependencies of the POSSIBLY_MISSING_DEP (?). Please make sure that the argument "MISSING_DEP" at index [0] is available in the RootTestModule context.
Potential solutions:
- Is RootTestModule a valid NestJS module?
- If "MISSING_DEP" is a provider, is it part of the current RootTestModule?
- If "MISSING_DEP" is exported from a separate @Module, is that module imported within RootTestModule?
@Module({
imports: [ /* the Module containing "MISSING_DEP" */ ]
})
For more common dependency resolution issues, see: https://docs.nestjs.com/faq/common-errors`);
}
const builder = Test.createTestingModule({
providers: [
{
provide: 'POSSIBLY_MISSING_DEP',
useFactory: () => null,
inject: ['MISSING_DEP'],
},
{
provide: 'FACTORY',
useFactory: () => 'RETURNED_VALUE',
inject: [{ token: 'POSSIBLY_MISSING_DEP', optional: false }],
},
],
});
await expect(builder.compile()).rejects.toSatisfy((err: any) => {
expect(err).toBeInstanceOf(UnknownDependenciesException);
expect(err.message).toContain(
`Nest can't resolve dependencies of the POSSIBLY_MISSING_DEP (?)`,
);
return true;
});
});
});
});

View File

@@ -1,9 +1,8 @@
import { UnknownDependenciesException } from '@nestjs/core/errors/exceptions/unknown-dependencies.exception.js';
import { Test } from '@nestjs/testing';
import { expect } from 'chai';
import { DependencyService } from '../src/properties/dependency.service';
import { PropertiesModule } from '../src/properties/properties.module';
import { PropertiesService } from '../src/properties/properties.service';
import { UnknownDependenciesException } from '@nestjs/core/errors/exceptions/unknown-dependencies.exception';
import { DependencyService } from '../src/properties/dependency.service.js';
import { PropertiesModule } from '../src/properties/properties.module.js';
import { PropertiesService } from '../src/properties/properties.service.js';
describe('Injector', () => {
it('should resolve property-based dependencies', async () => {
@@ -13,32 +12,25 @@ describe('Injector', () => {
const app = await builder.compile();
const dependency = app.get(DependencyService);
expect(app.get(PropertiesService).service).to.be.eql(dependency);
expect(app.get(PropertiesService).token).to.be.true;
expect(app.get(PropertiesService).symbolToken).to.be.true;
expect(app.get(PropertiesService).service).toEqual(dependency);
expect(app.get(PropertiesService).token).toBe(true);
expect(app.get(PropertiesService).symbolToken).toBe(true);
});
it('should throw UnknownDependenciesException when dependency is not met', async () => {
let exception;
try {
const builder = Test.createTestingModule({
providers: [
DependencyService,
PropertiesService,
{
provide: 'token',
useValue: true,
},
// symbol token is missing here
],
});
const app = await builder.compile();
app.get(DependencyService);
} catch (e) {
exception = e;
}
expect(exception).to.be.instanceOf(UnknownDependenciesException);
const builder = Test.createTestingModule({
providers: [
DependencyService,
PropertiesService,
{
provide: 'token',
useValue: true,
},
// symbol token is missing here
],
});
await expect(builder.compile()).rejects.toBeInstanceOf(
UnknownDependenciesException,
);
});
});

Some files were not shown because too many files have changed in this diff Show More