Пошук уроків, статей та іншого контенту
Перевірите логіку guards, доступ до маршрутів і поведінку за різних даних автентифікації.
Guard відповідає на запитання: чи може поточний запит продовжити виконання.
Під час тестування варто перевірити:
доступ із коректними даними автентифікації;
відмову без заголовка автентифікації;
відмову з некоректним токеном;
застосування guard до потрібного маршруту;
HTTP-відповідь маршруту, якщо guard відхиляє запит.
У NestJS зручно розділяти такі перевірки на два рівні:
Модульні тести — перевіряють логіку canActivate ізольовано.
E2E-тести — перевіряють реальний HTTP-запит до маршруту разом із guard.
Створимо guard, який дозволяє доступ лише за наявності токена test-token.
// src/auth/auth.guard.ts
import {
CanActivate,
ExecutionContext,
Injectable,
} from '@nestjs/common';
import { Request } from 'express';
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest<Request>();
const authorization = request.headers.authorization;
return authorization === 'Bearer test-token';
}
}Guard повертає:
true, якщо запит може продовжити виконання;
false, якщо доступ потрібно заборонити.
Якщо guard повертає false, NestJS за замовчуванням відповідає статусом 403 Forbidden.
Застосуємо guard до приватного маршруту:
// src/private/private.controller.ts
import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard } from '../auth/auth.guard';
@Controller('private')
@UseGuards(AuthGuard)
export class PrivateController {
@Get()
getPrivateData() {
return {
message: 'Доступ дозволено',
};
}
}Тепер guard захищає всі методи PrivateController.
Модульний тест не запускає HTTP-сервер. Ми безпосередньо викликаємо canActivate і передаємо йому підроблений ExecutionContext.
Guard отримує запит через такий ланцюжок викликів:
context.switchToHttp().getRequest()Тому в тесті потрібно створити об'єкт із відповідною структурою.
// src/auth/auth.guard.spec.ts
import { ExecutionContext } from '@nestjs/common';
import { AuthGuard } from './auth.guard';
describe('AuthGuard', () => {
let guard: AuthGuard;
beforeEach(() => {
guard = new AuthGuard();
});
function createContext(authorization?: string): ExecutionContext {
const request = {
headers: {
authorization,
},
};
return {
switchToHttp: () => ({
getRequest: () => request,
}),
} as ExecutionContext;
}
it('дозволяє доступ із коректним токеном', () => {
const context = createContext('Bearer test-token');
expect(guard.canActivate(context)).toBe(true);
});
it('забороняє доступ без заголовка Authorization', () => {
const context = createContext();
expect(guard.canActivate(context)).toBe(false);
});
it('забороняє доступ із некоректним токеном', () => {
const context = createContext('Bearer wrong-token');
expect(guard.canActivate(context)).toBe(false);
});
it('забороняє доступ із неправильним форматом заголовка', () => {
const context = createContext('test-token');
expect(guard.canActivate(context)).toBe(false);
});
});Запустити цей тест можна командою:
npm run test -- auth.guard.spec.tsНедостатньо перевірити лише позитивний сценарій. Зокрема, потрібно переконатися, що guard не пропускає:
відсутній заголовок;
порожній заголовок;
випадковий токен;
токен без префікса Bearer;
токен із помилкою в одному символі.
Кожен із цих випадків може з'явитися через помилку клієнта або спробу отримати несанкціонований доступ.
TestingModuleЯкщо guard має залежності, наприклад сервіс для перевірки токена, його варто створювати через TestingModule. Це дає змогу підмінити залежності mock-об'єктами.
Наприклад, guard використовує сервіс AuthService:
// src/auth/auth.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class AuthService {
isValidToken(token: string): boolean {
return token === 'test-token';
}
}// src/auth/auth.guard.ts
import {
CanActivate,
ExecutionContext,
Injectable,
} from '@nestjs/common';
import { Request } from 'express';
import { AuthService } from './auth.service';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private readonly authService: AuthService) {}
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest<Request>();
const authorization = request.headers.authorization;
if (!authorization?.startsWith('Bearer ')) {
return false;
}
const token = authorization.slice('Bearer '.length);
return this.authService.isValidToken(token);
}
}У тесті справжній AuthService можна замінити mock-реалізацією:
// src/auth/auth.guard.spec.ts
import { ExecutionContext } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { AuthGuard } from './auth.guard';
import { AuthService } from './auth.service';
describe('AuthGuard', () => {
let guard: AuthGuard;
let authService: { isValidToken: jest.Mock };
beforeEach(async () => {
authService = {
isValidToken: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
AuthGuard,
{
provide: AuthService,
useValue: authService,
},
],
}).compile();
guard = module.get<AuthGuard>(AuthGuard);
});
function createContext(authorization?: string): ExecutionContext {
const request = {
headers: {
authorization,
},
};
return {
switchToHttp: () => ({
getRequest: () => request,
}),
} as ExecutionContext;
}
it('перевіряє токен через AuthService', () => {
authService.isValidToken.mockReturnValue(true);
const context = createContext('Bearer valid-token');
expect(guard.canActivate(context)).toBe(true);
expect(authService.isValidToken).toHaveBeenCalledWith('valid-token');
});
it('забороняє доступ, якщо AuthService відхиляє токен', () => {
authService.isValidToken.mockReturnValue(false);
const context = createContext('Bearer invalid-token');
expect(guard.canActivate(context)).toBe(false);
expect(authService.isValidToken).toHaveBeenCalledWith('invalid-token');
});
it('не викликає AuthService без Bearer-токена', () => {
const context = createContext('invalid-token');
expect(guard.canActivate(context)).toBe(false);
expect(authService.isValidToken).not.toHaveBeenCalled();
});
});Такий підхід перевіряє саме поведінку guard. Тест не залежить від реалізації AuthService, бази даних або зовнішнього сервісу автентифікації.
Модульний тест відповідає на запитання «чи правильно працює canActivate?». Але він не перевіряє, чи guard справді підключений до потрібного маршруту.
Для цього потрібен E2E-тест.
// src/app.module.ts
import { Module } from '@nestjs/common';
import { AuthGuard } from './auth/auth.guard';
import { AuthService } from './auth/auth.service';
import { PrivateController } from './private/private.controller';
@Module({
controllers: [PrivateController],
providers: [AuthGuard, AuthService],
})
export class AppModule {}Якщо AuthGuard використовується через @UseGuards(AuthGuard), він має бути доступним у списку providers відповідного модуля.
// test/private.e2e-spec.ts
import { INestApplication } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
describe('PrivateController (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleFixture: TestingModule =
await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
afterAll(async () => {
await app.close();
});
it('повертає приватні дані з коректним токеном', () => {
return request(app.getHttpServer())
.get('/private')
.set('Authorization', 'Bearer test-token')
.expect(200)
.expect({
message: 'Доступ дозволено',
});
});
it('забороняє доступ без токена', () => {
return request(app.getHttpServer())
.get('/private')
.expect(403);
});
it('забороняє доступ із некоректним токеном', () => {
return request(app.getHttpServer())
.get('/private')
.set('Authorization', 'Bearer wrong-token')
.expect(403);
});
});У цьому тесті перевіряється повний ланцюжок:
створюється NestJS-застосунок;
надсилається HTTP-запит;
контролер знаходить маршрут /private;
запускається AuthGuard;
перевіряється заголовок Authorization;
перевіряється статус і тіло відповіді.
Запуск E2E-тестів залежить від конфігурації проєкту. У типовому NestJS-проєкті використовується команда:
npm run test:e2eAuthorizationУ HTTP-запитах назва заголовка зазвичай передається як Authorization. Express нормалізує назви заголовків, тому в коді доступ до нього виконується через:
request.headers.authorizationПрефікс Bearer потрібно перевіряти окремо:
if (!authorization?.startsWith('Bearer ')) {
return false;
}Після цього з рядка можна вилучити сам токен:
const token = authorization.slice('Bearer '.length);Для guard важливо перевірити не тільки значення токена, а й формат усього заголовка:
Bearer test-tokenТакі значення мають бути відхилені:
test-token
Basic test-token
Bearer
Bearer
Bearer wrong-tokenДля кожного захищеного маршруту бажано мати щонайменше такі сценарії:
запит із дійсними даними — очікується 200;
запит без автентифікації — очікується 403;
запит із недійсною автентифікацією — очікується 403;
запит до правильного маршруту перевіряється разом із його відповіддю.
Якщо guard застосовано лише до одного методу, E2E-тести також мають перевірити, що інші маршрути не отримали захист випадково.
Тест із правильним токеном не доводить, що доступ справді захищено. Додайте тести для відсутнього та неправильного токена.
ExecutionContextGuard очікує виклики:
context.switchToHttp().getRequest()Якщо mock не містить switchToHttp або getRequest, тест завершиться помилкою ще до перевірки логіки guard.
Якщо guard використовує сервіс автентифікації, перевіряйте також:
чи був сервіс викликаний;
із яким токеном його викликали;
чи не викликається він, коли формат заголовка неправильний.
401 і 403Guard, який повертає false, за замовчуванням призводить до відповіді 403 Forbidden.
Статус 401 Unauthorized зазвичай означає, що для запиту не надано коректні облікові дані, але NestJS автоматично не змінює false на 401. Якщо застосунок має повертати саме 401, це потрібно реалізувати окремо, наприклад через викидання відповідного винятку.
app.close()E2E-тест повинен закривати застосунок у afterAll. Інакше Jest може не завершити процес через відкриті HTTP-ресурси.
Модульний тест може показати, що canActivate працює правильно, але не підтвердити, що guard застосовано до потрібного контролера. Для цього потрібен E2E-тест.
Модульні тести перевіряють логіку canActivate без запуску сервера.
ExecutionContext у тесті потрібно замокувати так, щоб він повертав HTTP-запит.
Перевіряйте правильний, відсутній і неправильний токени.
Залежності guard підміняйте через TestingModule і mock-об'єкти.
E2E-тести перевіряють, що guard реально захищає маршрут.
Для guard, який повертає false, типовою відповіддю NestJS є 403 Forbidden.