mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
refactor(core): Extract Worfklow import request payload into a DTO (#12441)
This commit is contained in:
parent
b1940268e6
commit
1d3cb9f5ac
|
@ -35,3 +35,5 @@ export { CommunityRegisteredRequestDto } from './license/community-registered-re
|
|||
export { VariableListRequestDto } from './variables/variables-list-request.dto';
|
||||
export { CredentialsGetOneRequestQuery } from './credentials/credentials-get-one-request.dto';
|
||||
export { CredentialsGetManyRequestQuery } from './credentials/credentials-get-many-request.dto';
|
||||
|
||||
export { ImportWorkflowFromUrlDto } from './workflows/import-workflow-from-url.dto';
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
import { ImportWorkflowFromUrlDto } from '../import-workflow-from-url.dto';
|
||||
|
||||
describe('ImportWorkflowFromUrlDto', () => {
|
||||
describe('Valid requests', () => {
|
||||
test('should validate $name', () => {
|
||||
const result = ImportWorkflowFromUrlDto.safeParse({
|
||||
url: 'https://example.com/workflow.json',
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Invalid requests', () => {
|
||||
test.each([
|
||||
{
|
||||
name: 'invalid URL (not ending with .json)',
|
||||
url: 'https://example.com/workflow',
|
||||
expectedErrorPath: ['url'],
|
||||
},
|
||||
{
|
||||
name: 'invalid URL (missing protocol)',
|
||||
url: 'example.com/workflow.json',
|
||||
expectedErrorPath: ['url'],
|
||||
},
|
||||
{
|
||||
name: 'invalid URL (not a URL)',
|
||||
url: 'not-a-url',
|
||||
expectedErrorPath: ['url'],
|
||||
},
|
||||
{
|
||||
name: 'missing URL',
|
||||
url: undefined,
|
||||
expectedErrorPath: ['url'],
|
||||
},
|
||||
{
|
||||
name: 'null URL',
|
||||
url: null,
|
||||
expectedErrorPath: ['url'],
|
||||
},
|
||||
{
|
||||
name: 'invalid URL (ends with .json but not a valid URL)',
|
||||
url: 'not-a-url.json',
|
||||
expectedErrorPath: ['url'],
|
||||
},
|
||||
{
|
||||
name: 'valid URL with query parameters',
|
||||
url: 'https://example.com/workflow.json?param=value',
|
||||
},
|
||||
{
|
||||
name: 'valid URL with fragments',
|
||||
url: 'https://example.com/workflow.json#section',
|
||||
},
|
||||
])('should fail validation for $name', ({ url, expectedErrorPath }) => {
|
||||
const result = ImportWorkflowFromUrlDto.safeParse({ url });
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
|
||||
if (expectedErrorPath) {
|
||||
expect(result.error?.issues[0].path).toEqual(expectedErrorPath);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,6 @@
|
|||
import { z } from 'zod';
|
||||
import { Z } from 'zod-class';
|
||||
|
||||
export class ImportWorkflowFromUrlDto extends Z.class({
|
||||
url: z.string().url().endsWith('.json'),
|
||||
}) {}
|
|
@ -0,0 +1,72 @@
|
|||
import type { ImportWorkflowFromUrlDto } from '@n8n/api-types/src/dto/workflows/import-workflow-from-url.dto';
|
||||
import axios from 'axios';
|
||||
import type { Response } from 'express';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
|
||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
import type { AuthenticatedRequest } from '@/requests';
|
||||
|
||||
import { WorkflowsController } from '../workflows.controller';
|
||||
|
||||
jest.mock('axios');
|
||||
|
||||
describe('WorkflowsController', () => {
|
||||
const controller = Object.create(WorkflowsController.prototype);
|
||||
const axiosMock = axios.get as jest.Mock;
|
||||
const req = mock<AuthenticatedRequest>();
|
||||
const res = mock<Response>();
|
||||
|
||||
describe('getFromUrl', () => {
|
||||
describe('should return workflow data', () => {
|
||||
it('when the URL points to a valid JSON file', async () => {
|
||||
const mockWorkflowData = {
|
||||
nodes: [],
|
||||
connections: {},
|
||||
};
|
||||
|
||||
axiosMock.mockResolvedValue({ data: mockWorkflowData });
|
||||
|
||||
const query: ImportWorkflowFromUrlDto = { url: 'https://example.com/workflow.json' };
|
||||
const result = await controller.getFromUrl(req, res, query);
|
||||
|
||||
expect(result).toEqual(mockWorkflowData);
|
||||
expect(axiosMock).toHaveBeenCalledWith(query.url);
|
||||
});
|
||||
});
|
||||
|
||||
describe('should throw a BadRequestError', () => {
|
||||
const query: ImportWorkflowFromUrlDto = { url: 'https://example.com/invalid.json' };
|
||||
|
||||
it('when the URL does not point to a valid JSON file', async () => {
|
||||
axiosMock.mockRejectedValue(new Error('Network Error'));
|
||||
|
||||
await expect(controller.getFromUrl(req, res, query)).rejects.toThrow(BadRequestError);
|
||||
expect(axiosMock).toHaveBeenCalledWith(query.url);
|
||||
});
|
||||
|
||||
it('when the data is not a valid n8n workflow JSON', async () => {
|
||||
const invalidWorkflowData = {
|
||||
nodes: 'not an array',
|
||||
connections: 'not an object',
|
||||
};
|
||||
|
||||
axiosMock.mockResolvedValue({ data: invalidWorkflowData });
|
||||
|
||||
await expect(controller.getFromUrl(req, res, query)).rejects.toThrow(BadRequestError);
|
||||
expect(axiosMock).toHaveBeenCalledWith(query.url);
|
||||
});
|
||||
|
||||
it('when the data is missing required fields', async () => {
|
||||
const incompleteWorkflowData = {
|
||||
nodes: [],
|
||||
// Missing connections field
|
||||
};
|
||||
|
||||
axiosMock.mockResolvedValue({ data: incompleteWorkflowData });
|
||||
|
||||
await expect(controller.getFromUrl(req, res, query)).rejects.toThrow(BadRequestError);
|
||||
expect(axiosMock).toHaveBeenCalledWith(query.url);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -69,6 +69,4 @@ export declare namespace WorkflowRequest {
|
|||
{},
|
||||
{ destinationProjectId: string }
|
||||
>;
|
||||
|
||||
type FromUrl = AuthenticatedRequest<{}, {}, {}, { url?: string }>;
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { ImportWorkflowFromUrlDto } from '@n8n/api-types';
|
||||
import { GlobalConfig } from '@n8n/config';
|
||||
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
||||
import { In, type FindOptionsRelations } from '@n8n/typeorm';
|
||||
|
@ -18,7 +19,7 @@ import { SharedWorkflowRepository } from '@/databases/repositories/shared-workfl
|
|||
import { TagRepository } from '@/databases/repositories/tag.repository';
|
||||
import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
|
||||
import * as Db from '@/db';
|
||||
import { Delete, Get, Patch, Post, ProjectScope, Put, RestController } from '@/decorators';
|
||||
import { Delete, Get, Patch, Post, ProjectScope, Put, Query, RestController } from '@/decorators';
|
||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
import { ForbiddenError } from '@/errors/response-errors/forbidden.error';
|
||||
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
|
||||
|
@ -29,6 +30,7 @@ import { validateEntity } from '@/generic-helpers';
|
|||
import type { IWorkflowResponse } from '@/interfaces';
|
||||
import { License } from '@/license';
|
||||
import { listQueryMiddleware } from '@/middlewares';
|
||||
import { AuthenticatedRequest } from '@/requests';
|
||||
import * as ResponseHelper from '@/response-helper';
|
||||
import { NamingService } from '@/services/naming.service';
|
||||
import { ProjectService } from '@/services/project.service.ee';
|
||||
|
@ -215,18 +217,14 @@ export class WorkflowsController {
|
|||
}
|
||||
|
||||
@Get('/from-url')
|
||||
async getFromUrl(req: WorkflowRequest.FromUrl) {
|
||||
if (req.query.url === undefined) {
|
||||
throw new BadRequestError('The parameter "url" is missing!');
|
||||
}
|
||||
if (!/^http[s]?:\/\/.*\.json$/i.exec(req.query.url)) {
|
||||
throw new BadRequestError(
|
||||
'The parameter "url" is not valid! It does not seem to be a URL pointing to a n8n workflow JSON file.',
|
||||
);
|
||||
}
|
||||
async getFromUrl(
|
||||
_req: AuthenticatedRequest,
|
||||
_res: express.Response,
|
||||
@Query query: ImportWorkflowFromUrlDto,
|
||||
) {
|
||||
let workflowData: IWorkflowResponse | undefined;
|
||||
try {
|
||||
const { data } = await axios.get<IWorkflowResponse>(req.query.url);
|
||||
const { data } = await axios.get<IWorkflowResponse>(query.url);
|
||||
workflowData = data;
|
||||
} catch (error) {
|
||||
throw new BadRequestError('The URL does not point to valid JSON file!');
|
||||
|
|
Loading…
Reference in a new issue