|
| 1 | +/* eslint-disable @typescript-eslint/no-unused-vars */ |
| 2 | +import { faker } from '@faker-js/faker'; |
| 3 | +import { INestApplication, ValidationPipe } from '@nestjs/common'; |
| 4 | +import { Test } from '@nestjs/testing'; |
| 5 | +import test from 'ava'; |
| 6 | +import { ValidationError } from 'class-validator'; |
| 7 | +import cookieParser from 'cookie-parser'; |
| 8 | +import jwt from 'jsonwebtoken'; |
| 9 | +import request from 'supertest'; |
| 10 | +import { ApplicationModule } from '../../../src/app.module.js'; |
| 11 | +import { WinstonLogger } from '../../../src/entities/logging/winston-logger.js'; |
| 12 | +import { AllExceptionsFilter } from '../../../src/exceptions/all-exceptions.filter.js'; |
| 13 | +import { ValidationException } from '../../../src/exceptions/custom-exceptions/validation-exception.js'; |
| 14 | +import { Cacher } from '../../../src/helpers/cache/cacher.js'; |
| 15 | +import { appConfig } from '../../../src/shared/config/app-config.js'; |
| 16 | +import { DatabaseModule } from '../../../src/shared/database/database.module.js'; |
| 17 | +import { DatabaseService } from '../../../src/shared/database/database.service.js'; |
| 18 | +import { |
| 19 | + createInitialTestUser, |
| 20 | + registerUserAndReturnUserInfo, |
| 21 | +} from '../../utils/register-user-and-return-user-info.js'; |
| 22 | +import { setSaasEnvVariable } from '../../utils/set-saas-env-variable.js'; |
| 23 | +import { TestUtils } from '../../utils/test.utils.js'; |
| 24 | + |
| 25 | +let app: INestApplication; |
| 26 | +let _testUtils: TestUtils; |
| 27 | +let currentTest; |
| 28 | + |
| 29 | +// Microservice JWT (request_id claim) for the internal saas bridge controller. |
| 30 | +function microserviceAuthHeader(): string { |
| 31 | + const secret = appConfig.auth.microserviceJwtSecret as string; |
| 32 | + return `Bearer ${jwt.sign({ request_id: faker.string.uuid() }, secret, { expiresIn: '1h' })}`; |
| 33 | +} |
| 34 | + |
| 35 | +test.before(async () => { |
| 36 | + setSaasEnvVariable(); |
| 37 | + const moduleFixture = await Test.createTestingModule({ |
| 38 | + imports: [ApplicationModule, DatabaseModule], |
| 39 | + providers: [DatabaseService, TestUtils], |
| 40 | + }).compile(); |
| 41 | + app = moduleFixture.createNestApplication(); |
| 42 | + _testUtils = moduleFixture.get<TestUtils>(TestUtils); |
| 43 | + |
| 44 | + app.use(cookieParser()); |
| 45 | + app.useGlobalFilters(new AllExceptionsFilter(app.get(WinstonLogger))); |
| 46 | + app.useGlobalPipes( |
| 47 | + new ValidationPipe({ |
| 48 | + exceptionFactory(validationErrors: ValidationError[] = []) { |
| 49 | + return new ValidationException(validationErrors); |
| 50 | + }, |
| 51 | + }), |
| 52 | + ); |
| 53 | + await app.init(); |
| 54 | + await createInitialTestUser(app); |
| 55 | + app.getHttpServer().listen(0); |
| 56 | +}); |
| 57 | + |
| 58 | +test.after(async () => { |
| 59 | + try { |
| 60 | + await Cacher.clearAllCache(); |
| 61 | + await app.close(); |
| 62 | + } catch (e) { |
| 63 | + console.error('After tests error ' + e); |
| 64 | + } |
| 65 | +}); |
| 66 | + |
| 67 | +async function loginAndReturnCookieToken(): Promise<string> { |
| 68 | + const { email, password } = await registerUserAndReturnUserInfo(app); |
| 69 | + const loginUserResult = await request(app.getHttpServer()) |
| 70 | + .post('/user/login/') |
| 71 | + .send({ email, password }) |
| 72 | + .set('Content-Type', 'application/json') |
| 73 | + .set('Accept', 'application/json'); |
| 74 | + return loginUserResult.headers['set-cookie'][0].split(';')[0].replace('rocketadmin_cookie=', ''); |
| 75 | +} |
| 76 | + |
| 77 | +function logoutRequest(body: Record<string, unknown>, withAuth = true): request.Test { |
| 78 | + const req = request(app.getHttpServer()) |
| 79 | + .post('/saas/user/logout') |
| 80 | + .send(body) |
| 81 | + .set('Content-Type', 'application/json') |
| 82 | + .set('Accept', 'application/json'); |
| 83 | + return withAuth ? req.set('Authorization', microserviceAuthHeader()) : req; |
| 84 | +} |
| 85 | + |
| 86 | +currentTest = 'POST /saas/user/logout (internal, microservice JWT)'; |
| 87 | + |
| 88 | +test.serial(`${currentTest} blacklists the token so later validations fail`, async (t) => { |
| 89 | + const cookieToken = await loginAndReturnCookieToken(); |
| 90 | + |
| 91 | + // The token is valid before logout. |
| 92 | + const userBefore = await request(app.getHttpServer()) |
| 93 | + .get('/user') |
| 94 | + .set('Cookie', `rocketadmin_cookie=${cookieToken}`) |
| 95 | + .set('Accept', 'application/json'); |
| 96 | + t.is(userBefore.status, 200); |
| 97 | + |
| 98 | + const logoutResult = await logoutRequest({ token: cookieToken }); |
| 99 | + t.is(logoutResult.status, 201); |
| 100 | + t.deepEqual(JSON.parse(logoutResult.text), { success: true }); |
| 101 | + |
| 102 | + // The core's own auth middleware rejects the logged-out token. |
| 103 | + const userAfter = await request(app.getHttpServer()) |
| 104 | + .get('/user') |
| 105 | + .set('Cookie', `rocketadmin_cookie=${cookieToken}`) |
| 106 | + .set('Accept', 'application/json'); |
| 107 | + t.is(userAfter.status, 401); |
| 108 | + |
| 109 | + // The agents-microservice validation chain rejects it too. |
| 110 | + const validateResult = await request(app.getHttpServer()) |
| 111 | + .post('/internal/agents/auth/validate-user-token') |
| 112 | + .send({ token: cookieToken }) |
| 113 | + .set('Authorization', microserviceAuthHeader()) |
| 114 | + .set('Content-Type', 'application/json') |
| 115 | + .set('Accept', 'application/json'); |
| 116 | + t.is(validateResult.status, 401); |
| 117 | + |
| 118 | + // And the saas-microservice validation bridge (used by the saas AuthMiddleware). |
| 119 | + const saasValidateResult = await request(app.getHttpServer()) |
| 120 | + .post('/saas/user/validate-token') |
| 121 | + .send({ token: cookieToken }) |
| 122 | + .set('Authorization', microserviceAuthHeader()) |
| 123 | + .set('Content-Type', 'application/json') |
| 124 | + .set('Accept', 'application/json'); |
| 125 | + t.is(saasValidateResult.status, 401); |
| 126 | +}); |
| 127 | + |
| 128 | +test.serial(`${currentTest} rejects calls without a token in the body (400)`, async (t) => { |
| 129 | + const response = await logoutRequest({}); |
| 130 | + t.is(response.status, 400); |
| 131 | +}); |
| 132 | + |
| 133 | +test.serial(`${currentTest} rejects calls without the microservice JWT (401)`, async (t) => { |
| 134 | + const cookieToken = await loginAndReturnCookieToken(); |
| 135 | + const response = await logoutRequest({ token: cookieToken }, false); |
| 136 | + t.is(response.status, 401); |
| 137 | +}); |
| 138 | + |
| 139 | +currentTest = 'POST /saas/user/validate-token (internal, microservice JWT)'; |
| 140 | + |
| 141 | +test.serial(`${currentTest} returns the token claims (incl. companyId) for a live token`, async (t) => { |
| 142 | + const cookieToken = await loginAndReturnCookieToken(); |
| 143 | + |
| 144 | + const response = await request(app.getHttpServer()) |
| 145 | + .post('/saas/user/validate-token') |
| 146 | + .send({ token: cookieToken }) |
| 147 | + .set('Authorization', microserviceAuthHeader()) |
| 148 | + .set('Content-Type', 'application/json') |
| 149 | + .set('Accept', 'application/json'); |
| 150 | + t.is(response.status, 201); |
| 151 | + const ro = JSON.parse(response.text); |
| 152 | + const decoded = jwt.decode(cookieToken) as jwt.JwtPayload; |
| 153 | + t.is(ro.sub, decoded.id); |
| 154 | + t.is(ro.companyId, decoded.companyId); |
| 155 | + t.truthy(ro.companyId); |
| 156 | +}); |
| 157 | + |
| 158 | +test.serial(`${currentTest} rejects unauthenticated calls (401)`, async (t) => { |
| 159 | + const cookieToken = await loginAndReturnCookieToken(); |
| 160 | + |
| 161 | + const response = await request(app.getHttpServer()) |
| 162 | + .post('/saas/user/validate-token') |
| 163 | + .send({ token: cookieToken }) |
| 164 | + .set('Content-Type', 'application/json') |
| 165 | + .set('Accept', 'application/json'); |
| 166 | + t.is(response.status, 401); |
| 167 | +}); |
0 commit comments