refactor: deduplicate code in history service unit test

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2022-07-10 17:22:29 +02:00 committed by David Mehren
parent 57365bb727
commit d9ef44766d
3 changed files with 82 additions and 161 deletions

View file

@ -1,6 +1,9 @@
# SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) # SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
# SPDX-License-Identifier: CC0-1.0 # SPDX-License-Identifier: CC0-1.0
ignore:
- "src/utils/test-utils"
codecov: codecov:
notify: notify:
# We currently have integration tests and E2E tests. Codecov should wait until both are done. # We currently have integration tests and E2E tests. Codecov should wait until both are done.

View file

@ -6,6 +6,7 @@
import { ConfigModule } from '@nestjs/config'; import { ConfigModule } from '@nestjs/config';
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { getDataSourceToken, getRepositoryToken } from '@nestjs/typeorm'; import { getDataSourceToken, getRepositoryToken } from '@nestjs/typeorm';
import { Mock } from 'ts-mockery';
import { DataSource, EntityManager, Repository } from 'typeorm'; import { DataSource, EntityManager, Repository } from 'typeorm';
import { AuthToken } from '../auth/auth-token.entity'; import { AuthToken } from '../auth/auth-token.entity';
@ -28,6 +29,7 @@ import { Revision } from '../revisions/revision.entity';
import { Session } from '../users/session.entity'; import { Session } from '../users/session.entity';
import { User } from '../users/user.entity'; import { User } from '../users/user.entity';
import { UsersModule } from '../users/users.module'; import { UsersModule } from '../users/users.module';
import { mockSelectQueryBuilderInRepo } from '../utils/test-utils/mockSelectQueryBuilder';
import { HistoryEntryImportDto } from './history-entry-import.dto'; import { HistoryEntryImportDto } from './history-entry-import.dto';
import { HistoryEntry } from './history-entry.entity'; import { HistoryEntry } from './history-entry.entity';
import { HistoryService } from './history.service'; import { HistoryService } from './history.service';
@ -35,18 +37,11 @@ import { HistoryService } from './history.service';
describe('HistoryService', () => { describe('HistoryService', () => {
let service: HistoryService; let service: HistoryService;
let historyRepo: Repository<HistoryEntry>; let historyRepo: Repository<HistoryEntry>;
let dataSource: DataSource;
let noteRepo: Repository<Note>; let noteRepo: Repository<Note>;
let mockedTransaction: jest.Mock<
type MockConnection = { Promise<void>,
transaction: () => void; [(entityManager: EntityManager) => Promise<void>]
}; >;
function mockConnection(): MockConnection {
return {
transaction: jest.fn(),
};
}
beforeEach(async () => { beforeEach(async () => {
noteRepo = new Repository<Note>( noteRepo = new Repository<Note>(
@ -64,7 +59,12 @@ describe('HistoryService', () => {
HistoryService, HistoryService,
{ {
provide: getDataSourceToken(), provide: getDataSourceToken(),
useFactory: mockConnection, useFactory: () => {
mockedTransaction = jest.fn();
return Mock.of<DataSource>({
transaction: mockedTransaction,
});
},
}, },
{ {
provide: getRepositoryToken(HistoryEntry), provide: getRepositoryToken(HistoryEntry),
@ -117,7 +117,6 @@ describe('HistoryService', () => {
historyRepo = module.get<Repository<HistoryEntry>>( historyRepo = module.get<Repository<HistoryEntry>>(
getRepositoryToken(HistoryEntry), getRepositoryToken(HistoryEntry),
); );
dataSource = module.get<DataSource>(DataSource);
noteRepo = module.get<Repository<Note>>(getRepositoryToken(Note)); noteRepo = module.get<Repository<Note>>(getRepositoryToken(Note));
}); });
@ -163,23 +162,11 @@ describe('HistoryService', () => {
Note.create(user, alias) as Note, Note.create(user, alias) as Note,
) as HistoryEntry; ) as HistoryEntry;
it('without an preexisting entry', async () => { it('without an preexisting entry', async () => {
const createQueryBuilder = { mockSelectQueryBuilderInRepo(historyRepo, null);
where: () => createQueryBuilder,
andWhere: () => createQueryBuilder,
leftJoinAndSelect: () => createQueryBuilder,
getOne: async () => {
return null;
},
};
jest
.spyOn(historyRepo, 'createQueryBuilder')
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
.mockImplementation(() => createQueryBuilder);
jest jest
.spyOn(historyRepo, 'save') .spyOn(historyRepo, 'save')
.mockImplementation( .mockImplementation(
async (entry: HistoryEntry): Promise<HistoryEntry> => entry, async (entry): Promise<HistoryEntry> => entry as HistoryEntry,
); );
const createHistoryEntry = await service.updateHistoryEntryTimestamp( const createHistoryEntry = await service.updateHistoryEntryTimestamp(
Note.create(user, alias) as Note, Note.create(user, alias) as Note,
@ -193,23 +180,11 @@ describe('HistoryService', () => {
}); });
it('with an preexisting entry', async () => { it('with an preexisting entry', async () => {
const createQueryBuilder = { mockSelectQueryBuilderInRepo(historyRepo, historyEntry);
where: () => createQueryBuilder,
andWhere: () => createQueryBuilder,
leftJoinAndSelect: () => createQueryBuilder,
getOne: async () => {
return historyEntry;
},
};
jest
.spyOn(historyRepo, 'createQueryBuilder')
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
.mockImplementation(() => createQueryBuilder);
jest jest
.spyOn(historyRepo, 'save') .spyOn(historyRepo, 'save')
.mockImplementation( .mockImplementation(
async (entry: HistoryEntry): Promise<HistoryEntry> => entry, async (entry): Promise<HistoryEntry> => entry as HistoryEntry,
); );
const createHistoryEntry = await service.updateHistoryEntryTimestamp( const createHistoryEntry = await service.updateHistoryEntryTimestamp(
Note.create(user, alias) as Note, Note.create(user, alias) as Note,
@ -232,39 +207,16 @@ describe('HistoryService', () => {
const alias = 'alias'; const alias = 'alias';
const note = Note.create(user, alias) as Note; const note = Note.create(user, alias) as Note;
beforeEach(() => { beforeEach(() => {
const createQueryBuilder = { mockSelectQueryBuilderInRepo(noteRepo, note);
leftJoinAndSelect: () => createQueryBuilder,
where: () => createQueryBuilder,
orWhere: () => createQueryBuilder,
setParameter: () => createQueryBuilder,
getOne: () => note,
};
jest
.spyOn(noteRepo, 'createQueryBuilder')
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
.mockImplementation(() => createQueryBuilder);
}); });
describe('works', () => { describe('works', () => {
it('with an entry', async () => { it('with an entry', async () => {
const historyEntry = HistoryEntry.create(user, note) as HistoryEntry; const historyEntry = HistoryEntry.create(user, note) as HistoryEntry;
const createQueryBuilder = { mockSelectQueryBuilderInRepo(historyRepo, historyEntry);
where: () => createQueryBuilder,
andWhere: () => createQueryBuilder,
leftJoinAndSelect: () => createQueryBuilder,
getOne: async () => {
return historyEntry;
},
};
jest
.spyOn(historyRepo, 'createQueryBuilder')
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
.mockImplementation(() => createQueryBuilder);
jest jest
.spyOn(historyRepo, 'save') .spyOn(historyRepo, 'save')
.mockImplementation( .mockImplementation(
async (entry: HistoryEntry): Promise<HistoryEntry> => entry, async (entry): Promise<HistoryEntry> => entry as HistoryEntry,
); );
const updatedHistoryEntry = await service.updateHistoryEntry( const updatedHistoryEntry = await service.updateHistoryEntry(
note, note,
@ -281,19 +233,7 @@ describe('HistoryService', () => {
}); });
it('without an entry', async () => { it('without an entry', async () => {
const createQueryBuilder = { mockSelectQueryBuilderInRepo(historyRepo, null);
where: () => createQueryBuilder,
andWhere: () => createQueryBuilder,
leftJoinAndSelect: () => createQueryBuilder,
getOne: async () => {
return null;
},
};
jest
.spyOn(historyRepo, 'createQueryBuilder')
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
.mockImplementation(() => createQueryBuilder);
await expect( await expect(
service.updateHistoryEntry(note, user, { service.updateHistoryEntry(note, user, {
pinStatus: true, pinStatus: true,
@ -359,31 +299,8 @@ describe('HistoryService', () => {
const alias = 'alias'; const alias = 'alias';
const note = Note.create(user, alias) as Note; const note = Note.create(user, alias) as Note;
const historyEntry = HistoryEntry.create(user, note) as HistoryEntry; const historyEntry = HistoryEntry.create(user, note) as HistoryEntry;
const historyQueryBuilder = { mockSelectQueryBuilderInRepo(historyRepo, historyEntry);
where: () => historyQueryBuilder, mockSelectQueryBuilderInRepo(noteRepo, note);
andWhere: () => historyQueryBuilder,
leftJoinAndSelect: () => historyQueryBuilder,
getOne: async () => {
return historyEntry;
},
};
jest
.spyOn(historyRepo, 'createQueryBuilder')
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
.mockImplementation(() => historyQueryBuilder);
const noteQueryBuilder = {
leftJoinAndSelect: () => noteQueryBuilder,
where: () => noteQueryBuilder,
orWhere: () => noteQueryBuilder,
setParameter: () => noteQueryBuilder,
getOne: () => note,
};
jest
.spyOn(noteRepo, 'createQueryBuilder')
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
.mockImplementation(() => noteQueryBuilder);
jest jest
.spyOn(historyRepo, 'remove') .spyOn(historyRepo, 'remove')
.mockImplementation( .mockImplementation(
@ -400,31 +317,9 @@ describe('HistoryService', () => {
const alias = 'alias'; const alias = 'alias';
it('without an entry', async () => { it('without an entry', async () => {
const note = Note.create(user, alias) as Note; const note = Note.create(user, alias) as Note;
const createQueryBuilder = {
leftJoinAndSelect: () => createQueryBuilder, mockSelectQueryBuilderInRepo(historyRepo, null);
where: () => createQueryBuilder, mockSelectQueryBuilderInRepo(noteRepo, note);
orWhere: () => createQueryBuilder,
setParameter: () => createQueryBuilder,
getOne: () => note,
};
jest
.spyOn(noteRepo, 'createQueryBuilder')
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
.mockImplementation(() => createQueryBuilder);
const historyQueryBuilder = {
where: () => historyQueryBuilder,
andWhere: () => historyQueryBuilder,
leftJoinAndSelect: () => historyQueryBuilder,
getOne: async () => {
return null;
},
};
jest
.spyOn(historyRepo, 'createQueryBuilder')
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
.mockImplementation(() => historyQueryBuilder);
await expect(service.deleteHistoryEntry(note, user)).rejects.toThrow( await expect(service.deleteHistoryEntry(note, user)).rejects.toThrow(
NotInDBError, NotInDBError,
); );
@ -448,19 +343,9 @@ describe('HistoryService', () => {
pinStatus: historyEntryImport.pinStatus, pinStatus: historyEntryImport.pinStatus,
updatedAt: historyEntryImport.lastVisitedAt, updatedAt: historyEntryImport.lastVisitedAt,
}; };
const createQueryBuilder = {
leftJoinAndSelect: () => createQueryBuilder, const createQueryBuilder = mockSelectQueryBuilderInRepo(noteRepo, note);
where: () => createQueryBuilder, const mockedManager = Mock.of<EntityManager>({
orWhere: () => createQueryBuilder,
setParameter: () => createQueryBuilder,
getOne: () => note,
};
jest
.spyOn(noteRepo, 'createQueryBuilder')
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
.mockImplementation(() => createQueryBuilder);
const mockedManager = {
find: jest.fn().mockResolvedValueOnce([historyEntry]), find: jest.fn().mockResolvedValueOnce([historyEntry]),
createQueryBuilder: () => createQueryBuilder, createQueryBuilder: () => createQueryBuilder,
remove: jest remove: jest
@ -477,14 +362,8 @@ describe('HistoryService', () => {
expect(entry.pinStatus).toEqual(newlyCreatedHistoryEntry.pinStatus); expect(entry.pinStatus).toEqual(newlyCreatedHistoryEntry.pinStatus);
expect(entry.updatedAt).toEqual(newlyCreatedHistoryEntry.updatedAt); expect(entry.updatedAt).toEqual(newlyCreatedHistoryEntry.updatedAt);
}), }),
};
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
dataSource.transaction.mockImplementation((cb) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
cb(mockedManager);
}); });
mockedTransaction.mockImplementation((cb) => cb(mockedManager));
await service.setHistory(user, [historyEntryImport]); await service.setHistory(user, [historyEntryImport]);
}); });
}); });
@ -507,17 +386,8 @@ describe('HistoryService', () => {
); );
const historyEntry = HistoryEntry.create(user, note) as HistoryEntry; const historyEntry = HistoryEntry.create(user, note) as HistoryEntry;
historyEntry.pinStatus = true; historyEntry.pinStatus = true;
const createQueryBuilder = {
leftJoinAndSelect: () => createQueryBuilder, mockSelectQueryBuilderInRepo(noteRepo, note);
where: () => createQueryBuilder,
orWhere: () => createQueryBuilder,
getOne: () => note,
};
jest
.spyOn(noteRepo, 'createQueryBuilder')
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
.mockImplementation(() => createQueryBuilder);
const historyEntryDto = await service.toHistoryEntryDto(historyEntry); const historyEntryDto = await service.toHistoryEntryDto(historyEntry);
expect(historyEntryDto.pinStatus).toEqual(true); expect(historyEntryDto.pinStatus).toEqual(true);
expect(historyEntryDto.identifier).toEqual(alias); expect(historyEntryDto.identifier).toEqual(alias);

View file

@ -0,0 +1,48 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Mock } from 'ts-mockery';
import { Repository, SelectQueryBuilder } from 'typeorm';
/**
* Mocks a {@link SelectQueryBuilder} that returns a given entity.
*
* @param returnValue The entity to return
* @return The mocked query builder
*/
export function mockSelectQueryBuilder<T>(
returnValue: T | null,
): SelectQueryBuilder<T> {
const mockedQueryBuilder: SelectQueryBuilder<T> = Mock.of<
SelectQueryBuilder<T>
>({
where: () => mockedQueryBuilder,
andWhere: () => mockedQueryBuilder,
leftJoinAndSelect: () => mockedQueryBuilder,
getOne: () => Promise.resolve(returnValue),
orWhere: () => mockedQueryBuilder,
setParameter: () => mockedQueryBuilder,
});
return mockedQueryBuilder;
}
/**
* Mocks an {@link SelectQueryBuilder} and injects it into the given {@link Repository}.
*
* @param repository The repository whose query builder function should be mocked
* @param returnValue The value that should be found by the query builder
* @return The mocked query builder
* @see mockSelectQueryBuilder
*/
export function mockSelectQueryBuilderInRepo<T>(
repository: Repository<T>,
returnValue: T | null,
): SelectQueryBuilder<T> {
const selectQueryBuilder = mockSelectQueryBuilder(returnValue);
jest
.spyOn(repository, 'createQueryBuilder')
.mockImplementation(() => selectQueryBuilder);
return selectQueryBuilder;
}