/* * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ import { ConfigModule, ConfigService } from '@nestjs/config'; import { NestExpressApplication } from '@nestjs/platform-express'; import { Test, TestingModule, TestingModuleBuilder } from '@nestjs/testing'; import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm'; import { RouterModule, Routes } from 'nest-router'; import { Connection, createConnection } from 'typeorm'; import { PrivateApiModule } from '../src/api/private/private-api.module'; import { PublicApiModule } from '../src/api/public/public-api.module'; import { AuthTokenWithSecretDto } from '../src/auth/auth-token.dto'; import { AuthModule } from '../src/auth/auth.module'; import { AuthService } from '../src/auth/auth.service'; import { MockAuthGuard } from '../src/auth/mock-auth.guard'; import { TokenAuthGuard } from '../src/auth/token.strategy'; import { AuthorsModule } from '../src/authors/authors.module'; import { AuthConfig } from '../src/config/auth.config'; import { DatabaseConfig } from '../src/config/database.config'; import appConfigMock from '../src/config/mock/app.config.mock'; import authConfigMock from '../src/config/mock/auth.config.mock'; import customizationConfigMock from '../src/config/mock/customization.config.mock'; import databaseConfigMock from '../src/config/mock/database.config.mock'; import externalServicesConfigMock from '../src/config/mock/external-services.config.mock'; import mediaConfigMock from '../src/config/mock/media.config.mock'; import noteConfigMock from '../src/config/mock/note.config.mock'; import { ErrorExceptionMapping } from '../src/errors/error-mapping'; import { FrontendConfigModule } from '../src/frontend-config/frontend-config.module'; import { GroupsModule } from '../src/groups/groups.module'; import { GroupsService } from '../src/groups/groups.service'; import { HistoryModule } from '../src/history/history.module'; import { HistoryService } from '../src/history/history.service'; import { IdentityModule } from '../src/identity/identity.module'; import { IdentityService } from '../src/identity/identity.service'; import { ConsoleLoggerService } from '../src/logger/console-logger.service'; import { LoggerModule } from '../src/logger/logger.module'; import { MediaModule } from '../src/media/media.module'; import { MediaService } from '../src/media/media.service'; import { MonitoringModule } from '../src/monitoring/monitoring.module'; import { AliasService } from '../src/notes/alias.service'; import { Note } from '../src/notes/note.entity'; import { NotesModule } from '../src/notes/notes.module'; import { NotesService } from '../src/notes/notes.service'; import { PermissionsModule } from '../src/permissions/permissions.module'; import { RevisionsModule } from '../src/revisions/revisions.module'; import { User } from '../src/users/user.entity'; import { UsersModule } from '../src/users/users.module'; import { UsersService } from '../src/users/users.service'; import { setupSessionMiddleware } from '../src/utils/session'; import { setupValidationPipe } from '../src/utils/setup-pipes'; export class TestSetup { moduleRef: TestingModule; app: NestExpressApplication; userService: UsersService; groupService: GroupsService; configService: ConfigService; identityService: IdentityService; notesService: NotesService; mediaService: MediaService; historyService: HistoryService; aliasService: AliasService; authService: AuthService; users: User[] = []; authTokens: AuthTokenWithSecretDto[] = []; anonymousNotes: Note[] = []; ownedNotes: Note[] = []; /** * Cleans up remnants from a test run from the database */ public async cleanup() { const appConnection = this.app.get(Connection); const connectionOptions = appConnection.options; if (!connectionOptions.database) { throw new Error('Database name not set in connection options'); } if (connectionOptions.type === 'sqlite') { // Bail out early, as SQLite runs from memory anyway return; } if (appConnection.isConnected) { await appConnection.close(); } switch (connectionOptions.type) { case 'postgres': case 'mariadb': { const connection = await createConnection({ type: connectionOptions.type, username: 'hedgedoc', password: 'hedgedoc', }); await connection.query(`DROP DATABASE ${connectionOptions.database}`); await connection.close(); } } } } /** * Builder class for TestSetup * Should be instantiated with the create() method * The useable TestSetup is genereated using build() */ export class TestSetupBuilder { // list of functions that should be executed before or after builing the TestingModule private setupPreCompile: (() => Promise)[] = []; private setupPostCompile: (() => Promise)[] = []; private testingModuleBuilder: TestingModuleBuilder; private testSetup = new TestSetup(); private testId: string; /** * Prepares a test database * @param dbName The name of the database to use * @private */ private static async setupTestDB(dbName: string) { const dbType = process.env.HEDGEDOC_TEST_DB_TYPE; if (!dbType || dbType === 'sqlite') { return; } if (!['postgres', 'mariadb'].includes(dbType)) { throw new Error('Unknown database type in HEDGEDOC_TEST_DB_TYPE'); } const connection = await createConnection({ type: dbType as 'postgres' | 'mariadb', username: dbType === 'mariadb' ? 'root' : 'hedgedoc', password: 'hedgedoc', }); await connection.query(`CREATE DATABASE ${dbName}`); if (dbType === 'mariadb') { await connection.query( `GRANT ALL PRIVILEGES ON ${dbName}.* TO 'hedgedoc'@'%'`, ); } await connection.close(); } private static getTestDBConf(dbName: string): TypeOrmModuleOptions { switch (process.env.HEDGEDOC_TEST_DB_TYPE || 'sqlite') { case 'sqlite': return { type: 'sqlite', database: ':memory:', autoLoadEntities: true, synchronize: true, dropSchema: true, }; case 'postgres': case 'mariadb': return { type: process.env.HEDGEDOC_TEST_DB_TYPE as 'postgres' | 'mariadb', database: dbName, username: 'hedgedoc', password: 'hedgedoc', autoLoadEntities: true, synchronize: true, dropSchema: true, }; default: throw new Error('Unknown database type in HEDGEDOC_TEST_DB_TYPE'); } } /** * Creates a new instance of TestSetupBuilder */ public static create(): TestSetupBuilder { const testSetupBuilder = new TestSetupBuilder(); testSetupBuilder.testId = 'hedgedoc_test_' + Math.random().toString(36).substring(2, 15); const routes: Routes = [ { path: '/api/v2', module: PublicApiModule, }, { path: '/api/private', module: PrivateApiModule, }, ]; testSetupBuilder.testingModuleBuilder = Test.createTestingModule({ imports: [ RouterModule.forRoutes(routes), TypeOrmModule.forRoot( TestSetupBuilder.getTestDBConf(testSetupBuilder.testId), ), ConfigModule.forRoot({ isGlobal: true, load: [ appConfigMock, databaseConfigMock, noteConfigMock, authConfigMock, mediaConfigMock, customizationConfigMock, externalServicesConfigMock, ], }), NotesModule, UsersModule, RevisionsModule, AuthorsModule, PublicApiModule, PrivateApiModule, HistoryModule, MonitoringModule, PermissionsModule, GroupsModule, LoggerModule, MediaModule, AuthModule, FrontendConfigModule, IdentityModule, ], providers: [ { provide: 'APP_FILTER', useClass: ErrorExceptionMapping, }, ], }); return testSetupBuilder; } /** * Builds the final TestSetup from the configured builder */ public async build(): Promise { await TestSetupBuilder.setupTestDB(this.testId); for (const setupFunction of this.setupPreCompile) { await setupFunction(); } this.testSetup.moduleRef = await this.testingModuleBuilder.compile(); this.testSetup.userService = this.testSetup.moduleRef.get(UsersService); this.testSetup.groupService = this.testSetup.moduleRef.get(GroupsService); this.testSetup.configService = this.testSetup.moduleRef.get(ConfigService); this.testSetup.identityService = this.testSetup.moduleRef.get(IdentityService); this.testSetup.notesService = this.testSetup.moduleRef.get(NotesService); this.testSetup.mediaService = this.testSetup.moduleRef.get(MediaService); this.testSetup.historyService = this.testSetup.moduleRef.get(HistoryService); this.testSetup.aliasService = this.testSetup.moduleRef.get(AliasService); this.testSetup.authService = this.testSetup.moduleRef.get(AuthService); this.testSetup.app = this.testSetup.moduleRef.createNestApplication(); setupSessionMiddleware( this.testSetup.app, this.testSetup.configService.get('authConfig'), this.testSetup.configService.get('databaseConfig'), ); this.testSetup.app.useGlobalPipes( setupValidationPipe( await this.testSetup.app.resolve(ConsoleLoggerService), ), ); for (const setupFunction of this.setupPostCompile) { await setupFunction(); } return this.testSetup; } /** * Enable mock authentication for the public API */ public withMockAuth() { this.setupPreCompile.push(async () => { this.testingModuleBuilder .overrideGuard(TokenAuthGuard) .useClass(MockAuthGuard); return await Promise.resolve(); }); return this; } /** * Generate a few users, identities and auth tokens for testing */ public withUsers() { this.setupPostCompile.push(async () => { // Create users this.testSetup.users.push( await this.testSetup.userService.createUser('testuser1', 'Test User 1'), ); this.testSetup.users.push( await this.testSetup.userService.createUser('testuser2', 'Test User 2'), ); this.testSetup.users.push( await this.testSetup.userService.createUser('testuser3', 'Test User 3'), ); // Create identities for login await this.testSetup.identityService.createLocalIdentity( this.testSetup.users[0], 'testuser1', ); await this.testSetup.identityService.createLocalIdentity( this.testSetup.users[1], 'testuser2', ); await this.testSetup.identityService.createLocalIdentity( this.testSetup.users[2], 'testuser3', ); // create auth tokens this.testSetup.authTokens = await Promise.all( this.testSetup.users.map(async (user) => { return await this.testSetup.authService.createTokenForUser( user, 'test', new Date().getTime() + 60 * 60 * 1000, ); }), ); // create notes with owner this.testSetup.ownedNotes.push( await this.testSetup.notesService.createNote( 'Test Note 1', this.testSetup.users[0], 'testAlias1', ), ); this.testSetup.ownedNotes.push( await this.testSetup.notesService.createNote( 'Test Note 2', this.testSetup.users[1], 'testAlias2', ), ); this.testSetup.ownedNotes.push( await this.testSetup.notesService.createNote( 'Test Note 3', this.testSetup.users[2], 'testAlias3', ), ); }); return this; } /** * Generate a few anonymousNotes for testing */ public withNotes(): TestSetupBuilder { this.setupPostCompile.push(async () => { this.testSetup.anonymousNotes.push( await this.testSetup.notesService.createNote( 'Anonymous Note 1', null, 'anonAlias1', ), ); this.testSetup.anonymousNotes.push( await this.testSetup.notesService.createNote( 'Anonymous Note 2', null, 'anonAlias2', ), ); this.testSetup.anonymousNotes.push( await this.testSetup.notesService.createNote( 'Anonymous Note 3', null, 'anonAlias3', ), ); }); return this; } }