Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
496 changes: 492 additions & 4 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"test:ci-retry-failed": "jest --ci --onlyFailures --maxWorkers=3",
"test:watch": "jest --watch",
"test:quiet": "DOTENV_CONFIG_QUIET=true jest",
"test:e2e": "jest --config ./test/jest-e2e.json --runInBand --forceExit",
"sonar:analize": "docker compose -f sonar_docker-compose.yml up -d && sonar-scanner \\\n -Dsonar.projectKey=AltzoneLocal \\\n -Dsonar.sources=. \\\n -Dsonar.host.url=http://localhost:9000 \\\n -Dsonar.login=sqp_d0d523f9a2cf5e1e8f7587e0da315df0adc86568 \\\n && echo \"Remember to stop the sonar server with: \\n docker compose -f sonar_docker-compose.yml down\"\n",
"prepare": "husky",
"migrate:create": "migrate-mongo create",
Expand All @@ -35,6 +36,7 @@
"@nestjs/jwt": "11.0.2",
"@nestjs/mongoose": "11.0.3",
"@nestjs/platform-express": "11.1.27",
"@nestjs/platform-socket.io": "^11.2.3",
"@nestjs/platform-ws": "11.1.27",
"@nestjs/schedule": "6.1.3",
"@nestjs/swagger": "11.4.4",
Expand All @@ -56,6 +58,7 @@
"mongoose": "8.19.4",
"mqtt": "5.14.1",
"rxjs": "7.8.2",
"socket.io": "^4.8.3",
"webdav": "5.10.0",
"ws": "8.21.0"
},
Expand All @@ -65,10 +68,11 @@
"@nestjs/testing": "11.1.27",
"@types/cookie-parser": "1.4.10",
"@types/express": "5.0.5",
"@types/jest": "30.0.0",
"@types/jest": "^30.0.0",
"@types/lodash": "4.17.20",
"@types/multer": "2.0.0",
"@types/node": "24.10.1",
"@types/supertest": "^7.2.1",
"@types/ws": "8.18.1",
"eslint": "9.39.1",
"eslint-config-prettier": "10.1.8",
Expand All @@ -81,6 +85,7 @@
"nodemon": "3.1.11",
"prettier": "3.6.2",
"reflect-metadata": "0.2.2",
"supertest": "^7.2.2",
"ts-jest": "29.4.5",
"ts-node": "10.9.2",
"typescript": "5.9.3",
Expand Down
160 changes: 160 additions & 0 deletions test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# AltZone End-to-End (E2E) Testing

**Why this exists:** End-to-End (E2E) tests verify that the entire application stack, so controllers, services, database layers, and background connectors functions correctly from HTTP request down to response. These tests run against an active NestJS server context to prevent breaking API regressions before code reaches production.

E2E tests use [Jest] and [Supertest] to execute HTTP requests against a local test environment.

---

## Quick Start

Before running tests, ensure your local infrastructure dependencies (MongoDB, Redis, Mosquitto) are active via Docker.

```bash
# 1. Start docker
docker compose up -d

# 2. Execute the E2E test suite
npm run test:e2e

# 3. Add a new E2E test file in test/ (e.g., test/profile.e2e-spec.ts)

```

---

## Environment & Dependencies

E2E tests initialize the application using your root `.env` configuration. Ensure the following services are accessible locally:

| Dependency | Purpose | Port |
| --- | --- | --- |
| **MongoDB** | Database storage | `27017` |
| **Redis** | Caching & BullMQ job queues | `6379` |
| **Mosquitto** | MQTT messaging broker | `1883`, `9001` |

Key settings in `test/jest-e2e.json`:

| Setting | Value | Why it matters |
| --- | --- | --- |
| `moduleFileExtensions` | `["js", "json", "ts"]` | Supports TypeScript test resolution |
| `rootDir` | `.` | Context root relative to the test config |
| `testRegex` | `".e2e-spec.ts$"` | Discovers all E2E spec files inside `test/` |
| `transform` | `ts-jest` | Compiles TypeScript tests on the fly |

---

## CLI Reference

| Command | What it does | When to use it |
| --- | --- | --- |
| `npm run test:e2e` | Executes all `.e2e-spec.ts` files with `--runInBand` and `--forceExit` | Before committing or opening a PR |
| `npx jest --config ./test/jest-e2e.json test/app.e2e-spec.ts` | Runs a single spec file | When developing or debugging a specific spec |

> **Tip:** `--runInBand` ensures tests run sequentially in a single process, preventing state pollution across shared database collections. `--forceExit` forces Jest to exit cleanly after background microservices (like MQTT timers) finish execution.

---

## Writing an E2E Test

E2E spec files live in the `test/` folder and end with `.e2e-spec.ts`. Import `AppModule` using relative imports from the root (`../src/app.module`).

Use `test/app.e2e-spec.ts` as your baseline structure.

### Anatomy of an E2E Test

```typescript
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import request from 'supertest';
import { AppModule } from '../src/app.module';

describe('Profile API (e2e)', () => {
let app: INestApplication;

beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();

app = moduleFixture.createNestApplication();

// Set global prefix if used in main.ts
// app.setGlobalPrefix('api');

await app.init();
});

afterAll(async () => {
if (app) {
await app.close();
// Allow background handles time to clear
await new Promise((resolve) => setTimeout(resolve, 500));
}
});

it('GET /metadata/game -> should return game metadata', async () => {
return request(app.getHttpServer())
.get('/metadata/game')
.expect(200);
});
});

```

---

## Common Patterns

### 1. Relative Imports from Root

Always import application modules relative to the `test/` directory using `../src/`:

```typescript
import { AppModule } from '../src/app.module';

```

### 2. Testing Authenticated Endpoints

For endpoints that require authentication tokens, log in during `beforeAll` or inside the test block and pass the authorization header:

```typescript
it('GET /profile/info -> should return user profile', async () => {
const loginRes = await request(app.getHttpServer())
.post('/auth/signIn')
.send({ username: 'testuser', password: 'password123' });

const token = loginRes.body.accessToken;

return request(app.getHttpServer())
.get('/profile/info')
.set('Authorization', `Bearer ${token}`)
.expect(200);
});

```

---

## Safety Checklist

Before committing an E2E test spec, verify:

* [ ] **Tests run sequentially.** Do not remove `--runInBand` from the execution script if tests write to a shared database. This can cause race conditions and other issues.
* [ ] **Teardown is clean.** Verify `afterAll` calls `await app.close()` to prevent orphaned processes or port binding lockups.
* [ ] **Docker services are active.** Confirm MongoDB, Redis, and Mosquitto containers are running prior to executing test suites.
* [ ] **Endpoints match Swagger spec.** Ensure tested routes and HTTP methods align with defined API contracts.

---

## Troubleshooting

| Symptom | Likely Cause | Fix |
| --- | --- | --- |
| `ECONNREFUSED 127.0.0.1:6379` | Redis server is not running | Start Redis container using `docker compose up -d` |
| `404 Not Found` on valid route | Route missing global prefix or wrong route path | Verify route in Swagger or check `app.setGlobalPrefix()` |
| `TypeError: Right-hand side of 'instanceof' is not callable` | MQTT client reconnecting post-teardown | Ensure `--forceExit` is present in `npm run test:e2e` |
| `Cannot find module '../src/app.module'` | Incorrect relative import path | Use `import { AppModule } from '../src/app.module'` |

---
7 changes: 7 additions & 0 deletions test/__mocks__/webdav.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = {
createClient: () => ({
getFileContents: jest.fn(),
putFileContents: jest.fn(),
getDirectoryContents: jest.fn(),
}),
};
30 changes: 30 additions & 0 deletions test/app.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import request from 'supertest';
import { AppModule } from '../src/app.module';

describe('AppController (e2e)', () => {
let app: INestApplication;

beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();

app = moduleFixture.createNestApplication();
await app.init();
});

afterAll(async () => {
if (app) {
await new Promise((resolve) => setTimeout(resolve, 500));
await app.close();
}
});

it('/GET metadata/game', () => {
return request(app.getHttpServer())
.get('/metadata/game')
.expect(200);
});
});
12 changes: 12 additions & 0 deletions test/jest-e2e.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"moduleNameMapper": {
"^webdav$": "<rootDir>/__mocks__/webdav.js"
}
}
10 changes: 10 additions & 0 deletions test/tsconfig.e2e.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"noEmit": true,
"types": ["jest", "node", "multer"]
},
"include": [
"**/*.ts"
]
}
16 changes: 11 additions & 5 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@
"emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */

/* Modules */
"module": "CommonJS", /* Specify what module code is generated. */
"rootDir": "./src", /* Specify the root folder within your source files. */
"module": "CommonJS", /* Specify what module code is generated. */ /* Specify the root folder within your source files. */
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
"baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
"resolveJsonModule": true, /* Enable importing .json files. */
"resolveJsonModule": true,
"ignoreDeprecations": "6.0",
"types": ["jest", "node", "multer"], /* Enable importing .json files. */

/* Emit */
"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
Expand All @@ -31,7 +32,12 @@
"noFallthroughCasesInSwitch": false, /* Enable error reporting for fallthrough cases in switch statements. */

/* Completeness */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
},
"include": ["src/**/*.ts"]
"include": [
"src/**/*",
"test/**/*",
"**/*.spec.ts",
"**/*.test.ts"
]
}
Loading