test(core): Move unit tests closer to testable components (no-changelog) (#10287)
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
import { User } from '@db/entities/User';
|
||||
|
||||
describe('User Entity', () => {
|
||||
describe('JSON.stringify', () => {
|
||||
it('should not serialize sensitive data', () => {
|
||||
const user = Object.assign(new User(), {
|
||||
email: 'test@example.com',
|
||||
firstName: 'Don',
|
||||
lastName: 'Joe',
|
||||
password: '123456789',
|
||||
apiKey: '123',
|
||||
});
|
||||
expect(JSON.stringify(user)).toEqual(
|
||||
'{"email":"test@example.com","firstName":"Don","lastName":"Joe"}',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createPersonalProjectName', () => {
|
||||
test.each([
|
||||
['Nathan', 'Nathaniel', 'nathan@nathaniel.n8n', 'Nathan Nathaniel <nathan@nathaniel.n8n>'],
|
||||
[undefined, 'Nathaniel', 'nathan@nathaniel.n8n', '<nathan@nathaniel.n8n>'],
|
||||
['Nathan', undefined, 'nathan@nathaniel.n8n', '<nathan@nathaniel.n8n>'],
|
||||
[undefined, undefined, 'nathan@nathaniel.n8n', '<nathan@nathaniel.n8n>'],
|
||||
[undefined, undefined, undefined, 'Unnamed Project'],
|
||||
['Nathan', 'Nathaniel', undefined, 'Unnamed Project'],
|
||||
])(
|
||||
'given fistName: %s, lastName: %s and email: %s this gives the projectName: "%s"',
|
||||
async (firstName, lastName, email, projectName) => {
|
||||
const user = new User();
|
||||
Object.assign(user, { firstName, lastName, email });
|
||||
expect(user.createPersonalProjectName()).toBe(projectName);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
import Container from 'typedi';
|
||||
import { GlobalConfig } from '@n8n/config';
|
||||
import type { SelectQueryBuilder } from '@n8n/typeorm';
|
||||
import { Not, LessThanOrEqual } from '@n8n/typeorm';
|
||||
import { BinaryDataService } from 'n8n-core';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
|
||||
import { ExecutionEntity } from '@db/entities/ExecutionEntity';
|
||||
import { ExecutionRepository } from '@db/repositories/execution.repository';
|
||||
import { mockInstance, mockEntityManager } from '@test/mocking';
|
||||
|
||||
describe('ExecutionRepository', () => {
|
||||
const entityManager = mockEntityManager(ExecutionEntity);
|
||||
const globalConfig = mockInstance(GlobalConfig);
|
||||
const binaryDataService = mockInstance(BinaryDataService);
|
||||
const executionRepository = Container.get(ExecutionRepository);
|
||||
const mockDate = new Date('2023-12-28 12:34:56.789Z');
|
||||
|
||||
beforeAll(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.useFakeTimers().setSystemTime(mockDate);
|
||||
});
|
||||
|
||||
afterAll(() => jest.useRealTimers());
|
||||
|
||||
describe('getWaitingExecutions()', () => {
|
||||
test.each(['sqlite', 'postgresdb'] as const)(
|
||||
'on %s, should be called with expected args',
|
||||
async (dbType) => {
|
||||
globalConfig.database.type = dbType;
|
||||
entityManager.find.mockResolvedValueOnce([]);
|
||||
|
||||
await executionRepository.getWaitingExecutions();
|
||||
|
||||
expect(entityManager.find).toHaveBeenCalledWith(ExecutionEntity, {
|
||||
order: { waitTill: 'ASC' },
|
||||
select: ['id', 'waitTill'],
|
||||
where: {
|
||||
status: Not('crashed'),
|
||||
waitTill: LessThanOrEqual(
|
||||
dbType === 'sqlite'
|
||||
? '2023-12-28 12:36:06.789'
|
||||
: new Date('2023-12-28T12:36:06.789Z'),
|
||||
),
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('deleteExecutionsByFilter', () => {
|
||||
test('should delete binary data', async () => {
|
||||
const workflowId = nanoid();
|
||||
|
||||
jest.spyOn(executionRepository, 'createQueryBuilder').mockReturnValue(
|
||||
mock<SelectQueryBuilder<ExecutionEntity>>({
|
||||
select: jest.fn().mockReturnThis(),
|
||||
andWhere: jest.fn().mockReturnThis(),
|
||||
getMany: jest.fn().mockResolvedValue([{ id: '1', workflowId }]),
|
||||
}),
|
||||
);
|
||||
|
||||
await executionRepository.deleteExecutionsByFilter({ id: '1' }, ['1'], { ids: ['1'] });
|
||||
|
||||
expect(binaryDataService.deleteMany).toHaveBeenCalledWith([{ executionId: '1', workflowId }]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
import { Container } from 'typedi';
|
||||
import { In } from '@n8n/typeorm';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import { hasScope } from '@n8n/permissions';
|
||||
|
||||
import type { User } from '@db/entities/User';
|
||||
import type { CredentialsEntity } from '@db/entities/CredentialsEntity';
|
||||
import { SharedCredentials } from '@db/entities/SharedCredentials';
|
||||
import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository';
|
||||
import { GLOBAL_MEMBER_SCOPES, GLOBAL_OWNER_SCOPES } from '@/permissions/global-roles';
|
||||
import { mockEntityManager } from '@test/mocking';
|
||||
|
||||
describe('SharedCredentialsRepository', () => {
|
||||
const entityManager = mockEntityManager(SharedCredentials);
|
||||
const repository = Container.get(SharedCredentialsRepository);
|
||||
|
||||
describe('findCredentialForUser', () => {
|
||||
const credentialsId = 'cred_123';
|
||||
const sharedCredential = mock<SharedCredentials>();
|
||||
sharedCredential.credentials = mock<CredentialsEntity>({ id: credentialsId });
|
||||
const owner = mock<User>({
|
||||
isOwner: true,
|
||||
hasGlobalScope: (scope) =>
|
||||
hasScope(scope, {
|
||||
global: GLOBAL_OWNER_SCOPES,
|
||||
}),
|
||||
});
|
||||
const member = mock<User>({
|
||||
isOwner: false,
|
||||
id: 'test',
|
||||
hasGlobalScope: (scope) =>
|
||||
hasScope(scope, {
|
||||
global: GLOBAL_MEMBER_SCOPES,
|
||||
}),
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
test('should allow instance owner access to all credentials', async () => {
|
||||
entityManager.findOne.mockResolvedValueOnce(sharedCredential);
|
||||
const credential = await repository.findCredentialForUser(credentialsId, owner, [
|
||||
'credential:read',
|
||||
]);
|
||||
expect(entityManager.findOne).toHaveBeenCalledWith(SharedCredentials, {
|
||||
relations: { credentials: { shared: { project: { projectRelations: { user: true } } } } },
|
||||
where: { credentialsId },
|
||||
});
|
||||
expect(credential).toEqual(sharedCredential.credentials);
|
||||
});
|
||||
|
||||
test('should allow members', async () => {
|
||||
entityManager.findOne.mockResolvedValueOnce(sharedCredential);
|
||||
const credential = await repository.findCredentialForUser(credentialsId, member, [
|
||||
'credential:read',
|
||||
]);
|
||||
expect(entityManager.findOne).toHaveBeenCalledWith(SharedCredentials, {
|
||||
relations: { credentials: { shared: { project: { projectRelations: { user: true } } } } },
|
||||
where: {
|
||||
credentialsId,
|
||||
role: In(['credential:owner', 'credential:user']),
|
||||
project: {
|
||||
projectRelations: {
|
||||
role: In([
|
||||
'project:admin',
|
||||
'project:personalOwner',
|
||||
'project:editor',
|
||||
'project:viewer',
|
||||
]),
|
||||
userId: member.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(credential).toEqual(sharedCredential.credentials);
|
||||
});
|
||||
|
||||
test('should return null when no shared credential is found', async () => {
|
||||
entityManager.findOne.mockResolvedValueOnce(null);
|
||||
const credential = await repository.findCredentialForUser(credentialsId, member, [
|
||||
'credential:read',
|
||||
]);
|
||||
expect(entityManager.findOne).toHaveBeenCalledWith(SharedCredentials, {
|
||||
relations: { credentials: { shared: { project: { projectRelations: { user: true } } } } },
|
||||
where: {
|
||||
credentialsId,
|
||||
role: In(['credential:owner', 'credential:user']),
|
||||
project: {
|
||||
projectRelations: {
|
||||
role: In([
|
||||
'project:admin',
|
||||
'project:personalOwner',
|
||||
'project:editor',
|
||||
'project:viewer',
|
||||
]),
|
||||
userId: member.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(credential).toEqual(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Container } from 'typedi';
|
||||
import { type InsertResult, QueryFailedError } from '@n8n/typeorm';
|
||||
import { mock, mockClear } from 'jest-mock-extended';
|
||||
|
||||
import { StatisticsNames, WorkflowStatistics } from '@db/entities/WorkflowStatistics';
|
||||
import { WorkflowStatisticsRepository } from '@db/repositories/workflowStatistics.repository';
|
||||
|
||||
import { mockEntityManager } from '@test/mocking';
|
||||
|
||||
describe('insertWorkflowStatistics', () => {
|
||||
const entityManager = mockEntityManager(WorkflowStatistics);
|
||||
const workflowStatisticsRepository = Container.get(WorkflowStatisticsRepository);
|
||||
|
||||
beforeEach(() => {
|
||||
mockClear(entityManager.insert);
|
||||
});
|
||||
|
||||
it('Successfully inserts data when it is not yet present', async () => {
|
||||
entityManager.findOne.mockResolvedValueOnce(null);
|
||||
entityManager.insert.mockResolvedValueOnce(mock<InsertResult>());
|
||||
|
||||
const insertionResult = await workflowStatisticsRepository.insertWorkflowStatistics(
|
||||
StatisticsNames.dataLoaded,
|
||||
'workflowId',
|
||||
);
|
||||
|
||||
expect(insertionResult).toBe('insert');
|
||||
});
|
||||
|
||||
it('Does not insert when data is present', async () => {
|
||||
entityManager.findOne.mockResolvedValueOnce(mock<WorkflowStatistics>());
|
||||
const insertionResult = await workflowStatisticsRepository.insertWorkflowStatistics(
|
||||
StatisticsNames.dataLoaded,
|
||||
'workflowId',
|
||||
);
|
||||
|
||||
expect(insertionResult).toBe('alreadyExists');
|
||||
expect(entityManager.insert).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('throws an error when insertion fails', async () => {
|
||||
entityManager.findOne.mockResolvedValueOnce(null);
|
||||
entityManager.insert.mockImplementation(async () => {
|
||||
throw new QueryFailedError('Query', [], new Error('driver error'));
|
||||
});
|
||||
|
||||
const insertionResult = await workflowStatisticsRepository.insertWorkflowStatistics(
|
||||
StatisticsNames.dataLoaded,
|
||||
'workflowId',
|
||||
);
|
||||
|
||||
expect(insertionResult).toBe('failed');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import { NoXss } from '@db/utils/customValidators';
|
||||
import { validate } from 'class-validator';
|
||||
|
||||
describe('customValidators', () => {
|
||||
describe('NoXss', () => {
|
||||
class Person {
|
||||
@NoXss()
|
||||
name: string;
|
||||
}
|
||||
const person = new Person();
|
||||
|
||||
const invalidNames = ['http://google.com', '<script src/>', 'www.domain.tld'];
|
||||
|
||||
const validNames = [
|
||||
'Johann Strauß',
|
||||
'Вагиф Сәмәдоғлу',
|
||||
'René Magritte',
|
||||
'সুকুমার রায়',
|
||||
'མགོན་པོ་རྡོ་རྗེ།',
|
||||
'عبدالحليم حافظ',
|
||||
];
|
||||
|
||||
describe('Block XSS', () => {
|
||||
for (const name of invalidNames) {
|
||||
test(name, async () => {
|
||||
person.name = name;
|
||||
const validationErrors = await validate(person);
|
||||
expect(validationErrors[0].property).toEqual('name');
|
||||
expect(validationErrors[0].constraints).toEqual({ NoXss: 'Malicious name' });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('Allow Valid names', () => {
|
||||
for (const name of validNames) {
|
||||
test(name, async () => {
|
||||
person.name = name;
|
||||
expect(await validate(person)).toBeEmptyArray();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { IrreversibleMigration, ReversibleMigration } from '@/databases/types';
|
||||
import { wrapMigration } from '@/databases/utils/migrationHelpers';
|
||||
|
||||
describe('migrationHelpers.wrapMigration', () => {
|
||||
test('throws if passed a migration without up method', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
class TestMigration {}
|
||||
|
||||
//
|
||||
// ACT & ASSERT
|
||||
//
|
||||
expect(() => wrapMigration(TestMigration as never)).toThrow(
|
||||
'Migration "TestMigration" is missing the method `up`.',
|
||||
);
|
||||
});
|
||||
|
||||
test('wraps up method', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
class TestMigration implements IrreversibleMigration {
|
||||
async up() {}
|
||||
}
|
||||
const originalUp = jest.fn();
|
||||
TestMigration.prototype.up = originalUp;
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
wrapMigration(TestMigration);
|
||||
await new TestMigration().up();
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
expect(TestMigration.prototype.up).not.toBe(originalUp);
|
||||
expect(originalUp).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('wraps down method', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
class TestMigration implements ReversibleMigration {
|
||||
async up() {}
|
||||
|
||||
async down() {}
|
||||
}
|
||||
const originalDown = jest.fn();
|
||||
TestMigration.prototype.down = originalDown;
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
wrapMigration(TestMigration);
|
||||
await new TestMigration().down();
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
expect(TestMigration.prototype.down).not.toBe(originalDown);
|
||||
expect(originalDown).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user