mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 04:34:06 -08:00
feat(core): Add internal API for test definitions (no-changelog) (#11591)
Co-authored-by: Tomi Turtiainen <10324676+tomi@users.noreply.github.com>
This commit is contained in:
parent
c08d23c00f
commit
e875bc5592
|
@ -20,7 +20,7 @@ import { Settings } from './settings';
|
||||||
import { SharedCredentials } from './shared-credentials';
|
import { SharedCredentials } from './shared-credentials';
|
||||||
import { SharedWorkflow } from './shared-workflow';
|
import { SharedWorkflow } from './shared-workflow';
|
||||||
import { TagEntity } from './tag-entity';
|
import { TagEntity } from './tag-entity';
|
||||||
import { TestDefinition } from './test-definition';
|
import { TestDefinition } from './test-definition.ee';
|
||||||
import { User } from './user';
|
import { User } from './user';
|
||||||
import { Variables } from './variables';
|
import { Variables } from './variables';
|
||||||
import { WebhookEntity } from './webhook-entity';
|
import { WebhookEntity } from './webhook-entity';
|
||||||
|
|
|
@ -4,7 +4,6 @@ import {
|
||||||
Generated,
|
Generated,
|
||||||
Index,
|
Index,
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
OneToOne,
|
|
||||||
PrimaryColumn,
|
PrimaryColumn,
|
||||||
RelationId,
|
RelationId,
|
||||||
} from '@n8n/typeorm';
|
} from '@n8n/typeorm';
|
||||||
|
@ -31,7 +30,9 @@ export class TestDefinition extends WithTimestamps {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
||||||
@Column({ length: 255 })
|
@Column({ length: 255 })
|
||||||
@Length(1, 255, { message: 'Test name must be $constraint1 to $constraint2 characters long.' })
|
@Length(1, 255, {
|
||||||
|
message: 'Test definition name must be $constraint1 to $constraint2 characters long.',
|
||||||
|
})
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -56,6 +57,9 @@ export class TestDefinition extends WithTimestamps {
|
||||||
* Relation to the annotation tag associated with the test
|
* Relation to the annotation tag associated with the test
|
||||||
* This tag will be used to select the test cases to run from previous executions
|
* This tag will be used to select the test cases to run from previous executions
|
||||||
*/
|
*/
|
||||||
@OneToOne('AnnotationTagEntity', 'test')
|
@ManyToOne('AnnotationTagEntity', 'test')
|
||||||
annotationTag: AnnotationTagEntity;
|
annotationTag: AnnotationTagEntity;
|
||||||
|
|
||||||
|
@RelationId((test: TestDefinition) => test.annotationTag)
|
||||||
|
annotationTagId: string;
|
||||||
}
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
import type { FindManyOptions, FindOptionsWhere } from '@n8n/typeorm';
|
||||||
|
import { DataSource, In, Repository } from '@n8n/typeorm';
|
||||||
|
import { Service } from 'typedi';
|
||||||
|
|
||||||
|
import { TestDefinition } from '@/databases/entities/test-definition.ee';
|
||||||
|
import type { ListQuery } from '@/requests';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class TestDefinitionRepository extends Repository<TestDefinition> {
|
||||||
|
constructor(dataSource: DataSource) {
|
||||||
|
super(TestDefinition, dataSource.manager);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMany(accessibleWorkflowIds: string[], options?: ListQuery.Options) {
|
||||||
|
if (accessibleWorkflowIds.length === 0) return { tests: [], count: 0 };
|
||||||
|
|
||||||
|
const where: FindOptionsWhere<TestDefinition> = {
|
||||||
|
...options?.filter,
|
||||||
|
workflow: {
|
||||||
|
id: In(accessibleWorkflowIds),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const findManyOptions: FindManyOptions<TestDefinition> = {
|
||||||
|
where,
|
||||||
|
relations: ['annotationTag'],
|
||||||
|
order: { createdAt: 'DESC' },
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options?.take) {
|
||||||
|
findManyOptions.skip = options.skip;
|
||||||
|
findManyOptions.take = options.take;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [testDefinitions, count] = await this.findAndCount(findManyOptions);
|
||||||
|
|
||||||
|
return { testDefinitions, count };
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOne(id: number, accessibleWorkflowIds: string[]) {
|
||||||
|
return await this.findOne({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
workflow: {
|
||||||
|
id: In(accessibleWorkflowIds),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
relations: ['annotationTag'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteById(id: number, accessibleWorkflowIds: string[]) {
|
||||||
|
return await this.delete({
|
||||||
|
id,
|
||||||
|
workflow: {
|
||||||
|
id: In(accessibleWorkflowIds),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
17
packages/cli/src/evaluation/test-definition.schema.ts
Normal file
17
packages/cli/src/evaluation/test-definition.schema.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const testDefinitionCreateRequestBodySchema = z
|
||||||
|
.object({
|
||||||
|
name: z.string().min(1).max(255),
|
||||||
|
workflowId: z.string().min(1),
|
||||||
|
evaluationWorkflowId: z.string().min(1).optional(),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
export const testDefinitionPatchRequestBodySchema = z
|
||||||
|
.object({
|
||||||
|
name: z.string().min(1).max(255).optional(),
|
||||||
|
evaluationWorkflowId: z.string().min(1).optional(),
|
||||||
|
annotationTagId: z.string().min(1).optional(),
|
||||||
|
})
|
||||||
|
.strict();
|
124
packages/cli/src/evaluation/test-definition.service.ee.ts
Normal file
124
packages/cli/src/evaluation/test-definition.service.ee.ts
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
import { Service } from 'typedi';
|
||||||
|
|
||||||
|
import type { TestDefinition } from '@/databases/entities/test-definition.ee';
|
||||||
|
import { AnnotationTagRepository } from '@/databases/repositories/annotation-tag.repository.ee';
|
||||||
|
import { TestDefinitionRepository } from '@/databases/repositories/test-definition.repository.ee';
|
||||||
|
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||||
|
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
||||||
|
import { validateEntity } from '@/generic-helpers';
|
||||||
|
import type { ListQuery } from '@/requests';
|
||||||
|
|
||||||
|
type TestDefinitionLike = Omit<
|
||||||
|
Partial<TestDefinition>,
|
||||||
|
'workflow' | 'evaluationWorkflow' | 'annotationTag'
|
||||||
|
> & {
|
||||||
|
workflow?: { id: string };
|
||||||
|
evaluationWorkflow?: { id: string };
|
||||||
|
annotationTag?: { id: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class TestDefinitionService {
|
||||||
|
constructor(
|
||||||
|
private testDefinitionRepository: TestDefinitionRepository,
|
||||||
|
private annotationTagRepository: AnnotationTagRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private toEntityLike(attrs: {
|
||||||
|
name?: string;
|
||||||
|
workflowId?: string;
|
||||||
|
evaluationWorkflowId?: string;
|
||||||
|
annotationTagId?: string;
|
||||||
|
id?: number;
|
||||||
|
}) {
|
||||||
|
const entity: TestDefinitionLike = {};
|
||||||
|
|
||||||
|
if (attrs.id) {
|
||||||
|
entity.id = attrs.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attrs.name) {
|
||||||
|
entity.name = attrs.name?.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attrs.workflowId) {
|
||||||
|
entity.workflow = {
|
||||||
|
id: attrs.workflowId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attrs.evaluationWorkflowId) {
|
||||||
|
entity.evaluationWorkflow = {
|
||||||
|
id: attrs.evaluationWorkflowId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attrs.annotationTagId) {
|
||||||
|
entity.annotationTag = {
|
||||||
|
id: attrs.annotationTagId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
toEntity(attrs: {
|
||||||
|
name?: string;
|
||||||
|
workflowId?: string;
|
||||||
|
evaluationWorkflowId?: string;
|
||||||
|
annotationTagId?: string;
|
||||||
|
id?: number;
|
||||||
|
}) {
|
||||||
|
const entity = this.toEntityLike(attrs);
|
||||||
|
return this.testDefinitionRepository.create(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(id: number, accessibleWorkflowIds: string[]) {
|
||||||
|
return await this.testDefinitionRepository.getOne(id, accessibleWorkflowIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
async save(test: TestDefinition) {
|
||||||
|
await validateEntity(test);
|
||||||
|
|
||||||
|
return await this.testDefinitionRepository.save(test);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: number, attrs: TestDefinitionLike) {
|
||||||
|
if (attrs.name) {
|
||||||
|
const updatedTest = this.toEntity(attrs);
|
||||||
|
await validateEntity(updatedTest);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the annotation tag exists
|
||||||
|
if (attrs.annotationTagId) {
|
||||||
|
const annotationTagExists = await this.annotationTagRepository.exists({
|
||||||
|
where: {
|
||||||
|
id: attrs.annotationTagId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!annotationTagExists) {
|
||||||
|
throw new BadRequestError('Annotation tag not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the test definition
|
||||||
|
const queryResult = await this.testDefinitionRepository.update(id, this.toEntityLike(attrs));
|
||||||
|
|
||||||
|
if (queryResult.affected === 0) {
|
||||||
|
throw new NotFoundError('Test definition not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: number, accessibleWorkflowIds: string[]) {
|
||||||
|
const deleteResult = await this.testDefinitionRepository.deleteById(id, accessibleWorkflowIds);
|
||||||
|
|
||||||
|
if (deleteResult.affected === 0) {
|
||||||
|
throw new NotFoundError('Test definition not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMany(options: ListQuery.Options, accessibleWorkflowIds: string[] = []) {
|
||||||
|
return await this.testDefinitionRepository.getMany(accessibleWorkflowIds, options);
|
||||||
|
}
|
||||||
|
}
|
138
packages/cli/src/evaluation/test-definitions.controller.ee.ts
Normal file
138
packages/cli/src/evaluation/test-definitions.controller.ee.ts
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
import express from 'express';
|
||||||
|
import assert from 'node:assert';
|
||||||
|
|
||||||
|
import { Get, Post, Patch, RestController, Delete } from '@/decorators';
|
||||||
|
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||||
|
import { ForbiddenError } from '@/errors/response-errors/forbidden.error';
|
||||||
|
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
||||||
|
import {
|
||||||
|
testDefinitionCreateRequestBodySchema,
|
||||||
|
testDefinitionPatchRequestBodySchema,
|
||||||
|
} from '@/evaluation/test-definition.schema';
|
||||||
|
import { listQueryMiddleware } from '@/middlewares';
|
||||||
|
import { getSharedWorkflowIds } from '@/public-api/v1/handlers/workflows/workflows.service';
|
||||||
|
import { isPositiveInteger } from '@/utils';
|
||||||
|
|
||||||
|
import { TestDefinitionService } from './test-definition.service.ee';
|
||||||
|
import { TestDefinitionsRequest } from './test-definitions.types.ee';
|
||||||
|
|
||||||
|
@RestController('/evaluation/test-definitions')
|
||||||
|
export class TestDefinitionsController {
|
||||||
|
private validateId(id: string) {
|
||||||
|
if (!isPositiveInteger(id)) {
|
||||||
|
throw new BadRequestError('Test ID is not a number');
|
||||||
|
}
|
||||||
|
|
||||||
|
return Number(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(private readonly testDefinitionService: TestDefinitionService) {}
|
||||||
|
|
||||||
|
@Get('/', { middlewares: listQueryMiddleware })
|
||||||
|
async getMany(req: TestDefinitionsRequest.GetMany) {
|
||||||
|
const userAccessibleWorkflowIds = await getSharedWorkflowIds(req.user, ['workflow:read']);
|
||||||
|
|
||||||
|
return await this.testDefinitionService.getMany(
|
||||||
|
req.listQueryOptions,
|
||||||
|
userAccessibleWorkflowIds,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/:id')
|
||||||
|
async getOne(req: TestDefinitionsRequest.GetOne) {
|
||||||
|
const testDefinitionId = this.validateId(req.params.id);
|
||||||
|
|
||||||
|
const userAccessibleWorkflowIds = await getSharedWorkflowIds(req.user, ['workflow:read']);
|
||||||
|
|
||||||
|
const testDefinition = await this.testDefinitionService.findOne(
|
||||||
|
testDefinitionId,
|
||||||
|
userAccessibleWorkflowIds,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!testDefinition) throw new NotFoundError('Test definition not found');
|
||||||
|
|
||||||
|
return testDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/')
|
||||||
|
async create(req: TestDefinitionsRequest.Create, res: express.Response) {
|
||||||
|
const bodyParseResult = testDefinitionCreateRequestBodySchema.safeParse(req.body);
|
||||||
|
if (!bodyParseResult.success) {
|
||||||
|
res.status(400).json({ errors: bodyParseResult.error.errors });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userAccessibleWorkflowIds = await getSharedWorkflowIds(req.user, ['workflow:read']);
|
||||||
|
|
||||||
|
if (!userAccessibleWorkflowIds.includes(req.body.workflowId)) {
|
||||||
|
throw new ForbiddenError('User does not have access to the workflow');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
req.body.evaluationWorkflowId &&
|
||||||
|
!userAccessibleWorkflowIds.includes(req.body.evaluationWorkflowId)
|
||||||
|
) {
|
||||||
|
throw new ForbiddenError('User does not have access to the evaluation workflow');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.testDefinitionService.save(
|
||||||
|
this.testDefinitionService.toEntity(bodyParseResult.data),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('/:id')
|
||||||
|
async delete(req: TestDefinitionsRequest.Delete) {
|
||||||
|
const testDefinitionId = this.validateId(req.params.id);
|
||||||
|
|
||||||
|
const userAccessibleWorkflowIds = await getSharedWorkflowIds(req.user, ['workflow:read']);
|
||||||
|
|
||||||
|
if (userAccessibleWorkflowIds.length === 0)
|
||||||
|
throw new ForbiddenError('User does not have access to any workflows');
|
||||||
|
|
||||||
|
await this.testDefinitionService.delete(testDefinitionId, userAccessibleWorkflowIds);
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch('/:id')
|
||||||
|
async patch(req: TestDefinitionsRequest.Patch, res: express.Response) {
|
||||||
|
const testDefinitionId = this.validateId(req.params.id);
|
||||||
|
|
||||||
|
const bodyParseResult = testDefinitionPatchRequestBodySchema.safeParse(req.body);
|
||||||
|
if (!bodyParseResult.success) {
|
||||||
|
res.status(400).json({ errors: bodyParseResult.error.errors });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userAccessibleWorkflowIds = await getSharedWorkflowIds(req.user, ['workflow:read']);
|
||||||
|
|
||||||
|
// Fail fast if no workflows are accessible
|
||||||
|
if (userAccessibleWorkflowIds.length === 0)
|
||||||
|
throw new ForbiddenError('User does not have access to any workflows');
|
||||||
|
|
||||||
|
const existingTest = await this.testDefinitionService.findOne(
|
||||||
|
testDefinitionId,
|
||||||
|
userAccessibleWorkflowIds,
|
||||||
|
);
|
||||||
|
if (!existingTest) throw new NotFoundError('Test definition not found');
|
||||||
|
|
||||||
|
if (
|
||||||
|
req.body.evaluationWorkflowId &&
|
||||||
|
!userAccessibleWorkflowIds.includes(req.body.evaluationWorkflowId)
|
||||||
|
) {
|
||||||
|
throw new ForbiddenError('User does not have access to the evaluation workflow');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.testDefinitionService.update(testDefinitionId, req.body);
|
||||||
|
|
||||||
|
// Respond with the updated test definition
|
||||||
|
const testDefinition = await this.testDefinitionService.findOne(
|
||||||
|
testDefinitionId,
|
||||||
|
userAccessibleWorkflowIds,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert(testDefinition, 'Test definition not found');
|
||||||
|
|
||||||
|
return testDefinition;
|
||||||
|
}
|
||||||
|
}
|
33
packages/cli/src/evaluation/test-definitions.types.ee.ts
Normal file
33
packages/cli/src/evaluation/test-definitions.types.ee.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import type { AuthenticatedRequest, ListQuery } from '@/requests';
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// /test-definitions
|
||||||
|
// ----------------------------------
|
||||||
|
|
||||||
|
export declare namespace TestDefinitionsRequest {
|
||||||
|
namespace RouteParams {
|
||||||
|
type TestId = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetOne = AuthenticatedRequest<RouteParams.TestId>;
|
||||||
|
|
||||||
|
type GetMany = AuthenticatedRequest<{}, {}, {}, ListQuery.Params & { includeScopes?: string }> & {
|
||||||
|
listQueryOptions: ListQuery.Options;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Create = AuthenticatedRequest<
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
{ name: string; workflowId: string; evaluationWorkflowId?: string }
|
||||||
|
>;
|
||||||
|
|
||||||
|
type Patch = AuthenticatedRequest<
|
||||||
|
RouteParams.TestId,
|
||||||
|
{},
|
||||||
|
{ name?: string; evaluationWorkflowId?: string; annotationTagId?: string }
|
||||||
|
>;
|
||||||
|
|
||||||
|
type Delete = AuthenticatedRequest<RouteParams.TestId>;
|
||||||
|
}
|
|
@ -3,7 +3,7 @@ import { validate } from 'class-validator';
|
||||||
import type { AnnotationTagEntity } from '@/databases/entities/annotation-tag-entity.ee';
|
import type { AnnotationTagEntity } from '@/databases/entities/annotation-tag-entity.ee';
|
||||||
import type { CredentialsEntity } from '@/databases/entities/credentials-entity';
|
import type { CredentialsEntity } from '@/databases/entities/credentials-entity';
|
||||||
import type { TagEntity } from '@/databases/entities/tag-entity';
|
import type { TagEntity } from '@/databases/entities/tag-entity';
|
||||||
import type { TestDefinition } from '@/databases/entities/test-definition';
|
import type { TestDefinition } from '@/databases/entities/test-definition.ee';
|
||||||
import type { User } from '@/databases/entities/user';
|
import type { User } from '@/databases/entities/user';
|
||||||
import type { WorkflowEntity } from '@/databases/entities/workflow-entity';
|
import type { WorkflowEntity } from '@/databases/entities/workflow-entity';
|
||||||
|
|
||||||
|
|
|
@ -63,6 +63,7 @@ import '@/events/events.controller';
|
||||||
import '@/executions/executions.controller';
|
import '@/executions/executions.controller';
|
||||||
import '@/external-secrets/external-secrets.controller.ee';
|
import '@/external-secrets/external-secrets.controller.ee';
|
||||||
import '@/license/license.controller';
|
import '@/license/license.controller';
|
||||||
|
import '@/evaluation/test-definitions.controller.ee';
|
||||||
import '@/workflows/workflow-history/workflow-history.controller.ee';
|
import '@/workflows/workflow-history/workflow-history.controller.ee';
|
||||||
import '@/workflows/workflows.controller';
|
import '@/workflows/workflows.controller';
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,361 @@
|
||||||
|
import { Container } from 'typedi';
|
||||||
|
|
||||||
|
import type { AnnotationTagEntity } from '@/databases/entities/annotation-tag-entity.ee';
|
||||||
|
import type { User } from '@/databases/entities/user';
|
||||||
|
import type { WorkflowEntity } from '@/databases/entities/workflow-entity';
|
||||||
|
import { TestDefinitionRepository } from '@/databases/repositories/test-definition.repository.ee';
|
||||||
|
import { createAnnotationTags } from '@test-integration/db/executions';
|
||||||
|
|
||||||
|
import { createUserShell } from './../shared/db/users';
|
||||||
|
import { createWorkflow } from './../shared/db/workflows';
|
||||||
|
import * as testDb from './../shared/test-db';
|
||||||
|
import type { SuperAgentTest } from './../shared/types';
|
||||||
|
import * as utils from './../shared/utils/';
|
||||||
|
|
||||||
|
let authOwnerAgent: SuperAgentTest;
|
||||||
|
let workflowUnderTest: WorkflowEntity;
|
||||||
|
let evaluationWorkflow: WorkflowEntity;
|
||||||
|
let otherWorkflow: WorkflowEntity;
|
||||||
|
let ownerShell: User;
|
||||||
|
let annotationTag: AnnotationTagEntity;
|
||||||
|
const testServer = utils.setupTestServer({ endpointGroups: ['evaluation'] });
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
ownerShell = await createUserShell('global:owner');
|
||||||
|
authOwnerAgent = testServer.authAgentFor(ownerShell);
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await testDb.truncate(['TestDefinition', 'Workflow', 'AnnotationTag']);
|
||||||
|
|
||||||
|
workflowUnderTest = await createWorkflow({ name: 'workflow-under-test' }, ownerShell);
|
||||||
|
evaluationWorkflow = await createWorkflow({ name: 'evaluation-workflow' }, ownerShell);
|
||||||
|
otherWorkflow = await createWorkflow({ name: 'other-workflow' });
|
||||||
|
annotationTag = (await createAnnotationTags(['test-tag']))[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /evaluation/test-definitions', () => {
|
||||||
|
test('should retrieve empty test definitions list', async () => {
|
||||||
|
const resp = await authOwnerAgent.get('/evaluation/test-definitions');
|
||||||
|
expect(resp.statusCode).toBe(200);
|
||||||
|
expect(resp.body.data.count).toBe(0);
|
||||||
|
expect(resp.body.data.testDefinitions).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should retrieve test definitions list', async () => {
|
||||||
|
const newTest = Container.get(TestDefinitionRepository).create({
|
||||||
|
name: 'test',
|
||||||
|
workflow: { id: workflowUnderTest.id },
|
||||||
|
});
|
||||||
|
await Container.get(TestDefinitionRepository).save(newTest);
|
||||||
|
|
||||||
|
const resp = await authOwnerAgent.get('/evaluation/test-definitions');
|
||||||
|
|
||||||
|
expect(resp.statusCode).toBe(200);
|
||||||
|
expect(resp.body.data).toEqual({
|
||||||
|
count: 1,
|
||||||
|
testDefinitions: [
|
||||||
|
expect.objectContaining({
|
||||||
|
name: 'test',
|
||||||
|
workflowId: workflowUnderTest.id,
|
||||||
|
evaluationWorkflowId: null,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should retrieve test definitions list with pagination', async () => {
|
||||||
|
// Add a bunch of test definitions
|
||||||
|
const testDefinitions = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < 15; i++) {
|
||||||
|
const newTest = Container.get(TestDefinitionRepository).create({
|
||||||
|
name: `test-${i}`,
|
||||||
|
workflow: { id: workflowUnderTest.id },
|
||||||
|
});
|
||||||
|
testDefinitions.push(newTest);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Container.get(TestDefinitionRepository).save(testDefinitions);
|
||||||
|
|
||||||
|
// Fetch the first page
|
||||||
|
let resp = await authOwnerAgent.get('/evaluation/test-definitions?take=10');
|
||||||
|
expect(resp.statusCode).toBe(200);
|
||||||
|
expect(resp.body.data.count).toBe(15);
|
||||||
|
expect(resp.body.data.testDefinitions).toHaveLength(10);
|
||||||
|
|
||||||
|
// Fetch the second page
|
||||||
|
resp = await authOwnerAgent.get('/evaluation/test-definitions?take=10&skip=10');
|
||||||
|
expect(resp.statusCode).toBe(200);
|
||||||
|
expect(resp.body.data.count).toBe(15);
|
||||||
|
expect(resp.body.data.testDefinitions).toHaveLength(5);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /evaluation/test-definitions/:id', () => {
|
||||||
|
test('should retrieve test definition', async () => {
|
||||||
|
const newTest = Container.get(TestDefinitionRepository).create({
|
||||||
|
name: 'test',
|
||||||
|
workflow: { id: workflowUnderTest.id },
|
||||||
|
});
|
||||||
|
await Container.get(TestDefinitionRepository).save(newTest);
|
||||||
|
|
||||||
|
const resp = await authOwnerAgent.get(`/evaluation/test-definitions/${newTest.id}`);
|
||||||
|
|
||||||
|
expect(resp.statusCode).toBe(200);
|
||||||
|
expect(resp.body.data.name).toBe('test');
|
||||||
|
expect(resp.body.data.workflowId).toBe(workflowUnderTest.id);
|
||||||
|
expect(resp.body.data.evaluationWorkflowId).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 404 for non-existent test definition', async () => {
|
||||||
|
const resp = await authOwnerAgent.get('/evaluation/test-definitions/123');
|
||||||
|
|
||||||
|
expect(resp.statusCode).toBe(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should retrieve test definition with evaluation workflow', async () => {
|
||||||
|
const newTest = Container.get(TestDefinitionRepository).create({
|
||||||
|
name: 'test',
|
||||||
|
workflow: { id: workflowUnderTest.id },
|
||||||
|
evaluationWorkflow: { id: evaluationWorkflow.id },
|
||||||
|
});
|
||||||
|
await Container.get(TestDefinitionRepository).save(newTest);
|
||||||
|
|
||||||
|
const resp = await authOwnerAgent.get(`/evaluation/test-definitions/${newTest.id}`);
|
||||||
|
|
||||||
|
expect(resp.statusCode).toBe(200);
|
||||||
|
expect(resp.body.data.name).toBe('test');
|
||||||
|
expect(resp.body.data.workflowId).toBe(workflowUnderTest.id);
|
||||||
|
expect(resp.body.data.evaluationWorkflowId).toBe(evaluationWorkflow.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not retrieve test definition if user does not have access to workflow under test', async () => {
|
||||||
|
const newTest = Container.get(TestDefinitionRepository).create({
|
||||||
|
name: 'test',
|
||||||
|
workflow: { id: otherWorkflow.id },
|
||||||
|
});
|
||||||
|
await Container.get(TestDefinitionRepository).save(newTest);
|
||||||
|
|
||||||
|
const resp = await authOwnerAgent.get(`/evaluation/test-definitions/${newTest.id}`);
|
||||||
|
|
||||||
|
expect(resp.statusCode).toBe(404);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /evaluation/test-definitions', () => {
|
||||||
|
test('should create test definition', async () => {
|
||||||
|
const resp = await authOwnerAgent.post('/evaluation/test-definitions').send({
|
||||||
|
name: 'test',
|
||||||
|
workflowId: workflowUnderTest.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resp.statusCode).toBe(200);
|
||||||
|
expect(resp.body.data.name).toBe('test');
|
||||||
|
expect(resp.body.data.workflowId).toBe(workflowUnderTest.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should create test definition with evaluation workflow', async () => {
|
||||||
|
const resp = await authOwnerAgent.post('/evaluation/test-definitions').send({
|
||||||
|
name: 'test',
|
||||||
|
workflowId: workflowUnderTest.id,
|
||||||
|
evaluationWorkflowId: evaluationWorkflow.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resp.statusCode).toBe(200);
|
||||||
|
expect(resp.body.data.name).toBe('test');
|
||||||
|
expect(resp.body.data.workflowId).toBe(workflowUnderTest.id);
|
||||||
|
expect(resp.body.data.evaluationWorkflowId).toBe(evaluationWorkflow.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return error if name is empty', async () => {
|
||||||
|
const resp = await authOwnerAgent.post('/evaluation/test-definitions').send({
|
||||||
|
name: '',
|
||||||
|
workflowId: workflowUnderTest.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resp.statusCode).toBe(400);
|
||||||
|
expect(resp.body.errors).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
code: 'too_small',
|
||||||
|
path: ['name'],
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return error if user has no access to the workflow', async () => {
|
||||||
|
const resp = await authOwnerAgent.post('/evaluation/test-definitions').send({
|
||||||
|
name: 'test',
|
||||||
|
workflowId: otherWorkflow.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resp.statusCode).toBe(403);
|
||||||
|
expect(resp.body.message).toBe('User does not have access to the workflow');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return error if user has no access to the evaluation workflow', async () => {
|
||||||
|
const resp = await authOwnerAgent.post('/evaluation/test-definitions').send({
|
||||||
|
name: 'test',
|
||||||
|
workflowId: workflowUnderTest.id,
|
||||||
|
evaluationWorkflowId: otherWorkflow.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resp.statusCode).toBe(403);
|
||||||
|
expect(resp.body.message).toBe('User does not have access to the evaluation workflow');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PATCH /evaluation/test-definitions/:id', () => {
|
||||||
|
test('should update test definition', async () => {
|
||||||
|
const newTest = Container.get(TestDefinitionRepository).create({
|
||||||
|
name: 'test',
|
||||||
|
workflow: { id: workflowUnderTest.id },
|
||||||
|
});
|
||||||
|
await Container.get(TestDefinitionRepository).save(newTest);
|
||||||
|
|
||||||
|
const resp = await authOwnerAgent.patch(`/evaluation/test-definitions/${newTest.id}`).send({
|
||||||
|
name: 'updated-test',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resp.statusCode).toBe(200);
|
||||||
|
expect(resp.body.data.name).toBe('updated-test');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 404 if user has no access to the workflow', async () => {
|
||||||
|
const newTest = Container.get(TestDefinitionRepository).create({
|
||||||
|
name: 'test',
|
||||||
|
workflow: { id: otherWorkflow.id },
|
||||||
|
});
|
||||||
|
await Container.get(TestDefinitionRepository).save(newTest);
|
||||||
|
|
||||||
|
const resp = await authOwnerAgent.patch(`/evaluation/test-definitions/${newTest.id}`).send({
|
||||||
|
name: 'updated-test',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resp.statusCode).toBe(404);
|
||||||
|
expect(resp.body.message).toBe('Test definition not found');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should update test definition with evaluation workflow', async () => {
|
||||||
|
const newTest = Container.get(TestDefinitionRepository).create({
|
||||||
|
name: 'test',
|
||||||
|
workflow: { id: workflowUnderTest.id },
|
||||||
|
});
|
||||||
|
await Container.get(TestDefinitionRepository).save(newTest);
|
||||||
|
|
||||||
|
const resp = await authOwnerAgent.patch(`/evaluation/test-definitions/${newTest.id}`).send({
|
||||||
|
name: 'updated-test',
|
||||||
|
evaluationWorkflowId: evaluationWorkflow.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resp.statusCode).toBe(200);
|
||||||
|
expect(resp.body.data.name).toBe('updated-test');
|
||||||
|
expect(resp.body.data.evaluationWorkflowId).toBe(evaluationWorkflow.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return error if user has no access to the evaluation workflow', async () => {
|
||||||
|
const newTest = Container.get(TestDefinitionRepository).create({
|
||||||
|
name: 'test',
|
||||||
|
workflow: { id: workflowUnderTest.id },
|
||||||
|
});
|
||||||
|
await Container.get(TestDefinitionRepository).save(newTest);
|
||||||
|
|
||||||
|
const resp = await authOwnerAgent.patch(`/evaluation/test-definitions/${newTest.id}`).send({
|
||||||
|
name: 'updated-test',
|
||||||
|
evaluationWorkflowId: otherWorkflow.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resp.statusCode).toBe(403);
|
||||||
|
expect(resp.body.message).toBe('User does not have access to the evaluation workflow');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should disallow workflowId', async () => {
|
||||||
|
const newTest = Container.get(TestDefinitionRepository).create({
|
||||||
|
name: 'test',
|
||||||
|
workflow: { id: workflowUnderTest.id },
|
||||||
|
});
|
||||||
|
await Container.get(TestDefinitionRepository).save(newTest);
|
||||||
|
|
||||||
|
const resp = await authOwnerAgent.patch(`/evaluation/test-definitions/${newTest.id}`).send({
|
||||||
|
name: 'updated-test',
|
||||||
|
workflowId: otherWorkflow.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resp.statusCode).toBe(400);
|
||||||
|
expect(resp.body.errors).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
code: 'unrecognized_keys',
|
||||||
|
keys: ['workflowId'],
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should update annotationTagId', async () => {
|
||||||
|
const newTest = Container.get(TestDefinitionRepository).create({
|
||||||
|
name: 'test',
|
||||||
|
workflow: { id: workflowUnderTest.id },
|
||||||
|
});
|
||||||
|
await Container.get(TestDefinitionRepository).save(newTest);
|
||||||
|
|
||||||
|
const resp = await authOwnerAgent.patch(`/evaluation/test-definitions/${newTest.id}`).send({
|
||||||
|
annotationTagId: annotationTag.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resp.statusCode).toBe(200);
|
||||||
|
expect(resp.body.data.annotationTag.id).toBe(annotationTag.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return error if annotationTagId is invalid', async () => {
|
||||||
|
const newTest = Container.get(TestDefinitionRepository).create({
|
||||||
|
name: 'test',
|
||||||
|
workflow: { id: workflowUnderTest.id },
|
||||||
|
});
|
||||||
|
await Container.get(TestDefinitionRepository).save(newTest);
|
||||||
|
|
||||||
|
const resp = await authOwnerAgent.patch(`/evaluation/test-definitions/${newTest.id}`).send({
|
||||||
|
annotationTagId: '123',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resp.statusCode).toBe(400);
|
||||||
|
expect(resp.body.message).toBe('Annotation tag not found');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('DELETE /evaluation/test-definitions/:id', () => {
|
||||||
|
test('should delete test definition', async () => {
|
||||||
|
const newTest = Container.get(TestDefinitionRepository).create({
|
||||||
|
name: 'test',
|
||||||
|
workflow: { id: workflowUnderTest.id },
|
||||||
|
});
|
||||||
|
await Container.get(TestDefinitionRepository).save(newTest);
|
||||||
|
|
||||||
|
const resp = await authOwnerAgent.delete(`/evaluation/test-definitions/${newTest.id}`);
|
||||||
|
|
||||||
|
expect(resp.statusCode).toBe(200);
|
||||||
|
expect(resp.body.data.success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 404 if test definition does not exist', async () => {
|
||||||
|
const resp = await authOwnerAgent.delete('/evaluation/test-definitions/123');
|
||||||
|
|
||||||
|
expect(resp.statusCode).toBe(404);
|
||||||
|
expect(resp.body.message).toBe('Test definition not found');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 404 if user has no access to the workflow', async () => {
|
||||||
|
const newTest = Container.get(TestDefinitionRepository).create({
|
||||||
|
name: 'test',
|
||||||
|
workflow: { id: otherWorkflow.id },
|
||||||
|
});
|
||||||
|
await Container.get(TestDefinitionRepository).save(newTest);
|
||||||
|
|
||||||
|
const resp = await authOwnerAgent.delete(`/evaluation/test-definitions/${newTest.id}`);
|
||||||
|
|
||||||
|
expect(resp.statusCode).toBe(404);
|
||||||
|
expect(resp.body.message).toBe('Test definition not found');
|
||||||
|
});
|
||||||
|
});
|
|
@ -74,6 +74,7 @@ const repositories = [
|
||||||
'SharedCredentials',
|
'SharedCredentials',
|
||||||
'SharedWorkflow',
|
'SharedWorkflow',
|
||||||
'Tag',
|
'Tag',
|
||||||
|
'TestDefinition',
|
||||||
'User',
|
'User',
|
||||||
'Variables',
|
'Variables',
|
||||||
'Webhook',
|
'Webhook',
|
||||||
|
|
|
@ -41,7 +41,8 @@ type EndpointGroup =
|
||||||
| 'project'
|
| 'project'
|
||||||
| 'role'
|
| 'role'
|
||||||
| 'dynamic-node-parameters'
|
| 'dynamic-node-parameters'
|
||||||
| 'apiKeys';
|
| 'apiKeys'
|
||||||
|
| 'evaluation';
|
||||||
|
|
||||||
export interface SetupProps {
|
export interface SetupProps {
|
||||||
endpointGroups?: EndpointGroup[];
|
endpointGroups?: EndpointGroup[];
|
||||||
|
|
|
@ -277,6 +277,10 @@ export const setupTestServer = ({
|
||||||
case 'apiKeys':
|
case 'apiKeys':
|
||||||
await import('@/controllers/api-keys.controller');
|
await import('@/controllers/api-keys.controller');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'evaluation':
|
||||||
|
await import('@/evaluation/test-definitions.controller.ee');
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue