Compare commits

..

88 Commits

Author SHA1 Message Date
Kamil Mysliwiec
6b278afc2f Merge pull request #16409 from 976520/test/sample-28-sse
test(sample-28): add unit and e2e tests for sse
2026-02-22 12:27:33 +01:00
재욱
4cf1901ffb test(sample): add unit and e2e tests for sse 2026-02-22 15:52:12 +09:00
Kamil Mysliwiec
5b1a4ff4e1 Merge pull request #15075 from maxbronnikov10/refactor/http-adapter-error
refactor(core,express,fastify): HTTP adapter error mapping
2026-02-18 15:05:37 +01:00
Kamil Mysliwiec
3a17cb9358 Merge pull request #14753 from glebbash/fix/stricter-is-http-error-check
fix(core): make `isHttpError` check stricter
2026-02-17 17:19:23 +01:00
glebbash
4aef438088 fix: update tests 2026-02-17 12:18:53 +00:00
glebbash
4ec9445776 Merge branch 'fix/stricter-is-http-error-check' of https://github.com/glebbash/nest into fix/stricter-is-http-error-check 2026-02-17 11:58:25 +00:00
Gleb Bashkatov
0eb2340e64 fix: Handle FastifyError as before 2026-02-17 11:58:21 +00:00
Gleb Bashkatov
1c43374d92 fix: make isHttpError check stricter 2026-02-17 11:56:48 +00:00
Kamil Mysliwiec
5adeb427df Merge pull request #14832 from BrahimAbdelli/fix-shared-utils-edge-cases
test(common): improve test coverage for shared utils
2026-02-17 09:48:16 +01:00
Brahim Abdelli
9b88230da7 test(common): fix isempty issue in context 2026-02-16 23:37:49 +01:00
Brahim Abdelli
3aa97fc6d2 test(common): fix shared utils test 2026-02-16 23:08:50 +01:00
Brahim Abdelli
7b1a9d1828 test(common): Restoring isEmpty method with test coverage
I got an error when running npm run test:coverage.

The @nestjs/graphql package depends on the isEmpty function from @nestjs/common.

After renaming isEmpty to isEmptyArray, the @nestjs/graphql package can no longer find the isEmpty function, leading to the error.

To fix this issue, I restored the isEmpty function for the @nestjs/graphql package.
2026-02-16 23:02:25 +01:00
Brahim Abdelli
202995829a test(common): Fix isEmptyArray behavior and update tests 2026-02-16 23:02:24 +01:00
Brahim Abdelli
dc49342ee8 test(common): Fix isEmptyArray behavior and update tests
- Fix isEmptyArray to return false for non-array values and array-like objects

- Update tests to align with the new behavior

- Add comprehensive test coverage for edge cases
2026-02-16 23:02:23 +01:00
Brahim Abdelli
4fe18eb736 test(common): improve test coverage for shared utils
- Add tests for edge cases in shared utility functions

- Fix issues with normalizePath and isEmpty
2026-02-16 23:02:22 +01:00
Kamil Mysliwiec
3ebb53bffb Merge pull request #15539 from mag123c/fix/graphql-federation-production-ready-samples
fix(sample): update gql federation samples to use production-ready
2026-02-16 17:49:22 +01:00
mag123c
e483cbe2ef fix(sample): update gql federation samples to use production-ready
Replace IntrospectAndCompose with static supergraph schema in both
code-first and schema-first federation samples. Add supergraph
generation scripts and e2e tests.
2026-02-16 23:03:14 +09:00
Kamil Mysliwiec
643162916f Merge pull request #15190 from nestjs/feat/nats-v3-migration
feat(microservices): nats v3 upgrade
2026-02-16 13:27:55 +01:00
Kamil Myśliwiec
a5c981c18b test: fix flaky test 2026-02-16 13:14:39 +01:00
Kamil Myśliwiec
dc33abecc1 style: address lint errors 2026-02-16 12:12:53 +01:00
Kamil Myśliwiec
9170ecb83f fix: update incorrect package import 2026-02-16 12:01:12 +01:00
Kamil Myśliwiec
43041eb779 chore: update deps 2026-02-16 11:57:47 +01:00
Kamil Myśliwiec
a1b4ee9678 chore: resolve conflicts 2026-02-16 11:55:52 +01:00
Kamil Mysliwiec
7ae88edf30 Merge pull request #15413 from y-nk/julien/argument-metadata-generic
feat(common): add generic for argument metadata
2026-02-16 11:14:08 +01:00
Kamil Mysliwiec
ec95499d4c Apply suggestion from @kamilmysliwiec 2026-02-16 11:13:24 +01:00
Kamil Mysliwiec
5a47244b7b Apply suggestion from @kamilmysliwiec 2026-02-16 11:12:49 +01:00
Kamil Mysliwiec
cd3cae26ec Merge pull request #14900 from nestjs/feat/call-hooks-by-hierarchy-level
feat(core): call hooks by components hierarchy level (order)
2026-02-16 11:12:17 +01:00
Kamil Myśliwiec
953569a0af fix: move hierarchy set after comp host resolution 2026-02-16 11:03:41 +01:00
Kamil Mysliwiec
84f92eb085 Merge pull request #16329 from at7211/feat/validation-pipe-error-format
feat(common): add error format option to validation pipe
2026-02-16 10:37:03 +01:00
Kamil Mysliwiec
5996b8c45a Merge pull request #16147 from miso-kyoungminkim/pipe-type-safety
refactor: update pipe `transform` signatures for improved type safety
2026-02-16 10:36:14 +01:00
Kamil Mysliwiec
6a50042f76 Merge pull request #16374 from snowykte0426/feature/websocket-disconnect-reason-v12
feat(websockets): add disconnect reason parameter
2026-02-16 10:35:12 +01:00
Kamil Mysliwiec
68d075cd90 Merge pull request #15787 from CodeVac513/fix/rabbitmq-prefix
refactor(microservices): fix the typo 'RQM_' to 'RMQ_'
2026-02-16 10:33:40 +01:00
Jay-Chou
cbdc24e91f feat(common): add error format option to validation pipe
Add a new `errorFormat` option to `ValidationPipeOptions` that allows
users to choose between two validation error formats:

- 'list' (default): Returns an array of error message strings with
  parent path prepended to messages (current behavior)
- 'grouped': Returns an object with property paths as keys and arrays
  of unmodified constraint messages as values

The 'grouped' format separates property paths from error messages,
which prevents custom validation messages from being modified with
parent path prefixes.

Closes #16268
2026-02-16 15:52:38 +08:00
codeVac513
d67a2ee555 refactor(microservices): update test to match rmq constant rename 2026-02-16 12:57:17 +09:00
CodeVac513
cec1113885 refactor(microservices): fix the typo 'RQM_' to 'RMQ_' 2026-02-16 12:36:19 +09:00
Julien Barbay
ba82ce7316 Merge branch 'nestjs:master' into julien/argument-metadata-generic 2026-02-16 09:43:29 +07:00
kyoungminkim
65297f651e refactor: update pipe transform signatures for improved type safety 2026-02-16 09:40:19 +09:00
maxbronnikov10
07603b53f2 refactor(core,express,fastify): HTTP adapter error mapping 2026-02-16 00:53:54 +03:00
snowykte0426
7828c68ab3 fix(microservices): Remove unnecessary exception filter priority changes
Remove the changes to exception filter handling in RPC exceptions handler
as the current behavior is the intended behavior according to maintainer
feedback.

Signed-off-by: snowykte0426 <snowykte0426@naver.com>
2026-02-16 05:00:40 +09:00
snowykte0426
722d27ad17 fix(websockets): Correct distinctUntilChanged for disconnect events
Fix the distinctUntilChanged operator in subscribeDisconnectEvent to properly
compare client objects when using the new { client, reason } format. The
previous implementation would not deduplicate correctly as it compared object
references instead of the actual client instances.

This ensures backward compatibility while properly handling both the old
format (just client) and new format ({ client, reason }) for disconnect events.

Signed-off-by: snowykte0426 <snowykte0426@naver.com>
2026-02-16 05:00:11 +09:00
snowykte0426
ad5b731cd0 feat(websockets): add disconnect reason parameter
This change enhances the WebSocket disconnect handling by providing
the disconnect reason as an optional second parameter to the
handleDisconnect method.

Changes:
- Add optional reason parameter to OnGatewayDisconnect interface
- Update NestGateway interface to support disconnect reason
- Modify WebSocketsController to capture and forward disconnect reason
- Enhance IoAdapter to extract reason from Socket.IO disconnect events
- Maintain full backward compatibility with existing implementations
- Add comprehensive unit and integration tests

The disconnect reason helps developers understand why clients disconnect,
enabling better error handling and debugging. Common reasons include
'client namespace disconnect', 'transport close', 'ping timeout', etc.

This change is fully backward compatible - existing code continues to
work without modification while new code can optionally access the
disconnect reason.

Closes #15437

Signed-off-by: snowykte0426 <snowykte0426@naver.com>
2026-02-16 04:59:58 +09:00
Kamil Myśliwiec
d37c2664b6 test: remove useless test file 2026-02-15 20:30:56 +01:00
Kamil Myśliwiec
9cf8699892 test: update integration tests to use vitest 2026-02-15 20:28:34 +01:00
Kamil Myśliwiec
d617106f5a chore: resolve merge conflicts 2026-02-15 20:25:59 +01:00
Kamil Myśliwiec
65615baafb chore: resolve merge conflicts 2026-02-15 20:23:58 +01:00
Kamil Mysliwiec
e15b3d79ca Merge pull request #16156 from manureja64/feat/express-graceful-shutdown
Feat/express graceful shutdown
2026-02-15 20:12:33 +01:00
Kamil Mysliwiec
254e5fe409 Merge pull request #15525 from lhj0621/feat/add-error-code
feat(common): allow passing errorCode in HttpExceptionOptions
2026-02-15 20:11:18 +01:00
Himanshu Gupta
d4e7540822 fix(testing): use vitest assertions in shutdown tests 2026-02-16 00:03:12 +05:30
Himanshu Gupta
decb89bf02 refactor(common,express): rename graceful shutdown option
Rename gracefulShutdown to return503OnClosing to align the HTTP
option name with Fastify's terminology. The previous name was too
vague and could imply broader behavior (connection draining, grace
periods) beyond what it actually does.
2026-02-15 23:27:34 +05:30
Himanshu Gupta
26953bab74 fix(common,core,express): activate shutdown flag before hooks
Move isShuttingDown flag activation from dispose() to a new
prepareClose() step that runs before callDestroyHook(). This prevents
new requests from being processed while providers are being destroyed
during shutdown.

Add beforeClose() to HttpServer interface and AbstractHttpAdapter so
adapters can be notified before the shutdown lifecycle begins.
2026-02-15 23:27:34 +05:30
Himanshu Gupta
f7e0495e49 fix(testing): increase timeouts for graceful shutdown tests 2026-02-15 23:27:34 +05:30
Himanshu Gupta
ea352f63eb chore(express): Remove debug logs from express adapter
Remove leftover debug logging statements to reduce console noise in the adapter.
2026-02-15 23:27:34 +05:30
Himanshu Gupta
a696ed4585 fix(testing): expose graceful-shutdown e2e specs
Override root tsconfig.json exclusion so TypeScript project service finds
e2e specs. Also set target, strict, and
esModuleInterop compiler options.
2026-02-15 23:27:34 +05:30
Himanshu Gupta
319437ac3f feat(common,express): add graceful shutdown option
Closes #11416
2026-02-15 23:27:34 +05:30
lhj0621
1ab6f0314b Merge branch 'v12.0.0' into feat/add-error-code 2026-02-16 02:25:10 +09:00
Kamil Myśliwiec
e8768e77dc sample: add zod validation, valibot serialization samples 2026-02-15 17:24:55 +01:00
Kamil Mysliwiec
01c5a95b7f Merge pull request #16369 from nestjs/build/esm-migration
build: migrate from cjs to esm
2026-02-15 16:52:54 +01:00
Kamil Myśliwiec
43c0eb67c1 test: fix outdated test 2026-02-15 16:52:29 +01:00
Kamil Myśliwiec
70323a132a chore: resolve merge conflicts 2026-02-15 16:45:03 +01:00
Kamil Mysliwiec
945ca4850d Merge pull request #16370 from nestjs/test/vitest-migration
refactor: migrate from mocha, chai, sinon to vitest
2026-02-15 16:43:08 +01:00
Kamil Mysliwiec
34f18293b9 Merge pull request #16371 from nestjs/sample/esm-migration
sample: migrate all samples to esm and vitest
2026-02-15 16:42:55 +01:00
Kamil Mysliwiec
b28012ebf9 Merge pull request #16372 from nestjs/refactor/minor-tweaks
refactor: minor codebase tweaks
2026-02-15 16:42:44 +01:00
Kamil Mysliwiec
a21fd1d83a Merge pull request #16373 from nestjs/feat/param-decorator-options
feat: standard schema validation pipe, and standard serializer, introduce internal export subpath
2026-02-15 16:42:30 +01:00
Kamil Myśliwiec
90a40d540e sample: turn off running sample tests for now 2026-02-15 16:03:05 +01:00
Kamil Myśliwiec
d2fd2e692e sample: fix file upload ts compilation error 2026-02-15 15:02:10 +01:00
Kamil Myśliwiec
b6cecd81c4 ci: ignore coverage script errors in ci 2026-02-15 14:30:14 +01:00
Kamil Myśliwiec
9d4c8656cd chore: exclude samples that use cli plugins for now 2026-02-15 14:29:12 +01:00
Kamil Myśliwiec
fb3ae67e25 chore: address lint errors, and fix ci test errors 2026-02-15 14:17:05 +01:00
Kamil Myśliwiec
543142d5ce fix: dont export plain literal object interface 2026-02-15 13:31:12 +01:00
Kamil Myśliwiec
09c81568f8 feat: introduce standard schema serializer 2026-02-15 13:07:08 +01:00
Kamil Myśliwiec
473810cc85 chore: introduce the internal export path 2026-02-13 17:40:00 +01:00
Kamil Myśliwiec
50f0f7286c feat(common): introduce param decorator options 2026-02-13 13:59:07 +01:00
Kamil Myśliwiec
6586c0d985 refactor: minor codebase tweaks 2026-02-13 13:58:25 +01:00
Kamil Myśliwiec
df1537f74e chore: add root dir to tsconfig 2026-02-12 21:03:38 +01:00
Kamil Myśliwiec
62908b4a8b sample: add await to bootstrap calls 2026-02-12 20:53:03 +01:00
Kamil Myśliwiec
1c046aeea7 sample: migrate samples to esm 2026-02-12 20:28:07 +01:00
Kamil Myśliwiec
288453fabb test: fix coverage collection, add more tests 2026-02-11 20:44:14 +01:00
Kamil Myśliwiec
6846cd7128 test: migrate to vitest 2026-02-11 15:04:51 +01:00
Kamil Myśliwiec
33f37d6270 refactor: minor tweaks 2026-02-09 16:05:02 +01:00
Kamil Myśliwiec
fa6d45da74 build: migrate from cjs to esm 2026-02-09 14:10:04 +01:00
lhj0621
fc405cb550 feat(common): allow passing errorCode in HttpExceptionOptions 2025-08-12 01:13:31 +09:00
Julien Barbay
ef0984f434 feat(common): add generic for argument metadata 2025-07-16 14:48:05 +07:00
Kamil Myśliwiec
51292e8320 chore: revert tsconfig changes 2025-05-27 10:47:26 +02:00
Kamil Myśliwiec
e0a9dfe37a feat(microservices): nats v3 migration 2025-05-27 10:38:15 +02:00
Kamil Myśliwiec
d1c9ec5b32 feat(core): call hooks by components hierarchy level 2025-04-03 10:01:17 +02:00
Gleb Bashkatov
5be47a0a76 chore: drop debugging leftover 2025-03-04 15:00:16 +00:00
Gleb Bashkatov
591251a42a fix: Handle FastifyError as before 2025-03-04 14:59:12 +00:00
Gleb Bashkatov
ccf416d159 fix: make isHttpError check stricter 2025-03-04 12:52:16 +00:00
1589 changed files with 33899 additions and 20590 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

@@ -0,0 +1,93 @@
import { NestFactory } from '@nestjs/core';
import { ExpressAdapter } from '@nestjs/platform-express';
import { INestApplication } from '@nestjs/common';
import * as http from 'http';
import { AppModule } from '../src/app.module';
describe('Graceful Shutdown (Express)', () => {
let app: INestApplication;
afterEach(async () => {
if (app) {
await app.close();
}
});
it('should allow in-flight requests to complete when return503OnClosing is enabled', async () => {
app = await NestFactory.create(AppModule, new ExpressAdapter() as any, {
return503OnClosing: true,
logger: false,
});
await app.listen(0);
const port = app.getHttpServer().address().port;
const requestPromise = new Promise<string>((resolve, reject) => {
http
.get(
`http://localhost:${port}/slow`,
{
// Explicitly close connection after response to speed up server shutdown
headers: { Connection: 'close' },
},
res => {
let data = '';
res.on('data', c => (data += c));
res.on('end', () => resolve(data));
},
)
.on('error', reject);
});
// Wait to ensure request is processing
await new Promise(r => setTimeout(r, 100));
const closePromise = app.close();
// The in-flight request should finish successfully
const response = await requestPromise;
expect(response).toBe('ok');
await closePromise;
}, 10000);
it('should return 503 for NEW queued requests on existing connections during shutdown', async () => {
app = await NestFactory.create(AppModule, new ExpressAdapter() as any, {
return503OnClosing: true,
logger: false,
});
await app.listen(0);
const port = app.getHttpServer().address().port;
// Force 1 socket to ensure queuing/reuse
const agent = new http.Agent({ keepAlive: true, maxSockets: 1 });
// 1. Send Request A (slow) - occupies the socket
const req1 = http.get(`http://localhost:${port}/slow`, { agent });
// 2. Wait so Request A is definitely "in flight"
await new Promise(r => setTimeout(r, 100));
// 3. Trigger Shutdown (don't await yet)
const closePromise = app.close();
// Allow the microtask for prepareClose() to flush (sets isShuttingDown)
await new Promise(r => setTimeout(r, 0));
// 4. Send Request B immediately using the same agent.
const statusPromise = new Promise<number>((resolve, reject) => {
const req = http.get(`http://localhost:${port}/slow`, { agent }, res => {
resolve(res.statusCode || 0);
});
req.on('error', reject);
});
// 5. Cleanup Request A
req1.on('error', () => {});
const status = await statusPromise;
expect(status).toBe(503);
await closePromise;
agent.destroy();
}, 10000);
});

View File

@@ -0,0 +1,11 @@
import { Controller, Get } from '@nestjs/common';
@Controller()
export class AppController {
@Get('slow')
async slow() {
// Simulate work
await new Promise(resolve => setTimeout(resolve, 500));
return 'ok';
}
}

View File

@@ -1,10 +1,7 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

View File

@@ -0,0 +1,30 @@
{
"compilerOptions": {
"types": ["vitest/globals"],
"module": "commonjs",
"declaration": false,
"noImplicitAny": false,
"removeComments": true,
"noLib": false,
"esModuleInterop": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "ES2023",
"sourceMap": true,
"allowJs": true,
"strictNullChecks": true,
"outDir": "./dist",
"paths": {
"@nestjs/common": ["../../packages/common"],
"@nestjs/common/*": ["../../packages/common/*"],
"@nestjs/core": ["../../packages/core"],
"@nestjs/core/*": ["../../packages/core/*"],
"@nestjs/platform-express": ["../../packages/platform-express"],
"@nestjs/platform-express/*": ["../../packages/platform-express/*"],
"@nestjs/testing": ["../../packages/testing"],
"@nestjs/testing/*": ["../../packages/testing/*"]
}
},
"include": ["src/**/*", "e2e/**/*"],
"exclude": ["node_modules"]
}

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',
@@ -612,158 +611,4 @@ describe('Middleware (FastifyAdapter)', () => {
await app.close();
});
});
describe('should respect fastify routing options', () => {
const MIDDLEWARE_RETURN_VALUE = 'middleware_return';
@Controller()
class TestController {
@Get('abc/def')
included() {
return 'whatnot';
}
}
@Module({
imports: [AppModule],
controllers: [TestController],
})
class TestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply((req, res, next) => res.end(MIDDLEWARE_RETURN_VALUE))
.forRoutes({ path: 'abc/def', method: RequestMethod.GET });
}
}
describe('[ignoreTrailingSlash] attribute', () => {
beforeEach(async () => {
app = (
await Test.createTestingModule({
imports: [TestModule],
}).compile()
).createNestApplication<NestFastifyApplication>(
new FastifyAdapter({
ignoreTrailingSlash: true,
// routerOptions: {
// ignoreTrailingSlash: true,
// },
}),
);
await app.init();
});
it(`GET forRoutes(GET /abc/def/)`, () => {
return app
.inject({
method: 'GET',
url: '/abc/def/', // trailing slash
})
.then(({ payload }) =>
expect(payload).to.be.eql(MIDDLEWARE_RETURN_VALUE),
);
});
afterEach(async () => {
await app.close();
});
});
describe('[ignoreDuplicateSlashes] attribute', () => {
beforeEach(async () => {
app = (
await Test.createTestingModule({
imports: [TestModule],
}).compile()
).createNestApplication<NestFastifyApplication>(
new FastifyAdapter({
routerOptions: {
ignoreDuplicateSlashes: true,
},
}),
);
await app.init();
});
it(`GET forRoutes(GET /abc//def)`, () => {
return app
.inject({
method: 'GET',
url: '/abc//def', // duplicate slashes
})
.then(({ payload }) =>
expect(payload).to.be.eql(MIDDLEWARE_RETURN_VALUE),
);
});
afterEach(async () => {
await app.close();
});
});
describe('[caseSensitive] attribute', () => {
beforeEach(async () => {
app = (
await Test.createTestingModule({
imports: [TestModule],
}).compile()
).createNestApplication<NestFastifyApplication>(
new FastifyAdapter({
routerOptions: {
caseSensitive: true,
},
}),
);
await app.init();
});
it(`GET forRoutes(GET /ABC/DEF)`, () => {
return app
.inject({
method: 'GET',
url: '/ABC/DEF', // different case
})
.then(({ payload }) =>
expect(payload).to.be.eql(MIDDLEWARE_RETURN_VALUE),
);
});
afterEach(async () => {
await app.close();
});
});
describe('[useSemicolonDelimiter] attribute', () => {
beforeEach(async () => {
app = (
await Test.createTestingModule({
imports: [TestModule],
}).compile()
).createNestApplication<NestFastifyApplication>(
new FastifyAdapter({
routerOptions: { useSemicolonDelimiter: true } as any,
}),
);
await app.init();
});
it(`GET forRoutes(GET /abc/def;foo=bar)`, () => {
return app
.inject({
method: 'GET',
url: '/abc/def;foo=bar', // semicolon delimiter
})
.then(({ payload }) =>
expect(payload).to.be.eql(MIDDLEWARE_RETURN_VALUE),
);
});
afterEach(async () => {
await app.close();
});
});
});
});

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,54 @@ 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]);
});
it('should sort components within a single module by injection hierarchy - ASC order', async () => {
@Injectable()
class A implements BeforeApplicationShutdown {
beforeApplicationShutdown = vi.fn();
}
@Injectable()
class AHost implements BeforeApplicationShutdown {
constructor(private a: A) {}
beforeApplicationShutdown = vi.fn();
}
@Injectable()
class Composition implements BeforeApplicationShutdown {
constructor(
private a: A,
private host: AHost,
) {}
beforeApplicationShutdown = vi.fn();
}
@Module({
providers: [AHost, A, Composition],
})
class AModule {}
const module = await Test.createTestingModule({
imports: [AModule],
}).compile();
const app = module.createNestApplication();
await app.init();
await app.close();
const child = module.get(A);
const parent = module.get(AHost);
const composition = module.get(Composition);
expect(composition.beforeApplicationShutdown).toHaveBeenCalledBefore(
parent.beforeApplicationShutdown,
);
expect(parent.beforeApplicationShutdown).toHaveBeenCalledBefore(
child.beforeApplicationShutdown,
);
});
});

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,53 @@ 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();
});
it('should sort components within a single module by injection hierarchy - DESC order', async () => {
@Injectable()
class A implements OnApplicationBootstrap {
onApplicationBootstrap = vi.fn();
}
@Injectable()
class AHost implements OnApplicationBootstrap {
constructor(private a: A) {}
onApplicationBootstrap = vi.fn();
}
@Injectable()
class Composition implements OnApplicationBootstrap {
constructor(
private a: A,
private host: AHost,
) {}
onApplicationBootstrap = vi.fn();
}
@Module({
providers: [AHost, A, Composition],
})
class AModule {}
const module = await Test.createTestingModule({
imports: [AModule],
}).compile();
const app = module.createNestApplication();
await app.init();
await app.close();
const child = module.get(A);
const parent = module.get(AHost);
const composition = module.get(Composition);
expect(child.onApplicationBootstrap).toHaveBeenCalledBefore(
parent.onApplicationBootstrap,
);
expect(parent.onApplicationBootstrap).toHaveBeenCalledBefore(
composition.onApplicationBootstrap,
);
});
});

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,54 @@ 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],
);
});
it('should sort components within a single module by injection hierarchy - ASC order', async () => {
@Injectable()
class A implements OnApplicationShutdown {
onApplicationShutdown = vi.fn();
}
@Injectable()
class AHost implements OnApplicationShutdown {
constructor(private a: A) {}
onApplicationShutdown = vi.fn();
}
@Injectable()
class Composition implements OnApplicationShutdown {
constructor(
private a: A,
private host: AHost,
) {}
onApplicationShutdown = vi.fn();
}
@Module({
providers: [AHost, A, Composition],
})
class AModule {}
const module = await Test.createTestingModule({
imports: [AModule],
}).compile();
const app = module.createNestApplication();
await app.init();
await app.close();
const child = module.get(A);
const parent = module.get(AHost);
const composition = module.get(Composition);
expect(composition.onApplicationShutdown).toHaveBeenCalledBefore(
parent.onApplicationShutdown,
);
expect(parent.onApplicationShutdown).toHaveBeenCalledBefore(
child.onApplicationShutdown,
);
});
});

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,53 @@ 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],
);
});
it('should sort components within a single module by injection hierarchy - ASC order', async () => {
@Injectable()
class A implements OnModuleDestroy {
onModuleDestroy = vi.fn();
}
@Injectable()
class AHost implements OnModuleDestroy {
constructor(private a: A) {}
onModuleDestroy = vi.fn();
}
@Injectable()
class Composition implements OnModuleDestroy {
constructor(
private a: A,
private host: AHost,
) {}
onModuleDestroy = vi.fn();
}
@Module({
providers: [AHost, A, Composition],
})
class AModule {}
const module = await Test.createTestingModule({
imports: [AModule],
}).compile();
const app = module.createNestApplication();
await app.init();
await app.close();
const child = module.get(A);
const parent = module.get(AHost);
const composition = module.get(Composition);
expect(composition.onModuleDestroy).toHaveBeenCalledBefore(
parent.onModuleDestroy,
);
expect(parent.onModuleDestroy).toHaveBeenCalledBefore(
child.onModuleDestroy,
);
});
});

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,50 @@ 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();
});
it('should sort components within a single module by injection hierarchy - DESC order', async () => {
@Injectable()
class A implements OnModuleInit {
onModuleInit = vi.fn();
}
@Injectable()
class AHost implements OnModuleInit {
constructor(private a: A) {}
onModuleInit = vi.fn();
}
@Injectable()
class Composition implements OnModuleInit {
constructor(
private a: A,
private host: AHost,
) {}
onModuleInit = vi.fn();
}
@Module({
providers: [AHost, A, Composition],
})
class AModule {}
const module = await Test.createTestingModule({
imports: [AModule],
}).compile();
const app = module.createNestApplication();
await app.init();
await app.close();
const child = module.get(A);
const parent = module.get(AHost);
const composition = module.get(Composition);
expect(child.onModuleInit).toHaveBeenCalledBefore(parent.onModuleInit);
expect(parent.onModuleInit).toHaveBeenCalledBefore(
composition.onModuleInit,
);
});
});

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);
});
});

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