mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-17 05:29:44 +02:00
chore: Migrate POST /api/v1/folders to @PublicApiController + Zod (#38619)
Co-authored-by: n8n-cat-bot[bot] <n8n-cat-bot[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Ali Elkhateeb <ali.elkhateeb@n8n.io>
This commit is contained in:
parent
a423a6b203
commit
098d1d365e
|
|
@ -1,4 +1,5 @@
|
|||
import {
|
||||
CreateFolderPublicDto,
|
||||
FolderDetailsPublicDto,
|
||||
FolderListPublicDto,
|
||||
folderPublicSchema,
|
||||
|
|
@ -201,3 +202,34 @@ describe('UpdatedFolderPublicDto', () => {
|
|||
expect(result.error?.errors[0].code).toBe('unrecognized_keys');
|
||||
});
|
||||
});
|
||||
|
||||
describe('CreateFolderPublicDto', () => {
|
||||
test('accepts a name on its own', () => {
|
||||
expect(CreateFolderPublicDto.safeParse({ name: 'My Folder' }).success).toBe(true);
|
||||
});
|
||||
|
||||
test('accepts a name and a parentFolderId', () => {
|
||||
const result = CreateFolderPublicDto.safeParse({ name: 'Child', parentFolderId: 'abc123' });
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data).toEqual({ name: 'Child', parentFolderId: 'abc123' });
|
||||
});
|
||||
|
||||
test.each([
|
||||
['a missing name', {}, ['name']],
|
||||
['an empty name', { name: '' }, ['name']],
|
||||
['a name with an illegal character', { name: 'a/b' }, ['name']],
|
||||
['a name over 128 characters', { name: 'a'.repeat(129) }, ['name']],
|
||||
[
|
||||
'a parentFolderId over 36 characters',
|
||||
{ name: 'a', parentFolderId: 'b'.repeat(37) },
|
||||
['parentFolderId'],
|
||||
],
|
||||
['an unknown key', { name: 'My Folder', tags: [] }, []],
|
||||
])('rejects %s', (_label, payload, path) => {
|
||||
const result = CreateFolderPublicDto.safeParse(payload);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error?.issues[0].path).toEqual(path);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
folderListQueryFieldDocs,
|
||||
folderProjectFieldDocs,
|
||||
folderProjectIconOpenApi,
|
||||
folderProjectIdParamDocs,
|
||||
updateFolderFieldDocs,
|
||||
} from './folder-public.openapi';
|
||||
import {
|
||||
|
|
@ -19,11 +20,18 @@ import {
|
|||
takeValidator,
|
||||
VALID_SORT_OPTIONS,
|
||||
} from './list-folder-query.dto';
|
||||
import { folderIdSchema, folderNameSchema } from '../../schemas/folder.schema';
|
||||
import {
|
||||
FOLDER_NAME_MAX_LENGTH,
|
||||
folderIdSchema,
|
||||
folderNameSchema,
|
||||
} from '../../schemas/folder.schema';
|
||||
import { nullableObjectGuardSchema } from '../../schemas/object-guard.schema';
|
||||
import { projectTypeSchema, type ProjectIcon } from '../../schemas/project.schema';
|
||||
import { Z } from '../../zod-class';
|
||||
|
||||
/** `personal` is accepted next to a real id, so this cannot reuse `projectIdParamSchema`. */
|
||||
export const folderProjectIdParamSchema = z.string().openapi(folderProjectIdParamDocs);
|
||||
|
||||
const folderProjectPublicSchema = z.object({
|
||||
id: z.string().openapi(folderProjectFieldDocs.id),
|
||||
name: z.string().openapi(folderProjectFieldDocs.name),
|
||||
|
|
@ -101,7 +109,10 @@ export class FolderDetailsPublicDto extends Z.class({
|
|||
|
||||
const updateFolderPublicSchema = z
|
||||
.object({
|
||||
name: folderNameSchema.optional().openapi(updateFolderFieldDocs.name),
|
||||
name: folderNameSchema.optional().openapi({
|
||||
...updateFolderFieldDocs.name,
|
||||
maxLength: FOLDER_NAME_MAX_LENGTH,
|
||||
}),
|
||||
parentFolderId: folderIdSchema.optional().openapi(updateFolderFieldDocs.parentFolderId),
|
||||
})
|
||||
.strict()
|
||||
|
|
@ -141,3 +152,17 @@ export class DeleteFolderQueryPublicDto extends Z.class(
|
|||
},
|
||||
{ strict: true },
|
||||
) {}
|
||||
|
||||
export class CreateFolderPublicDto extends Z.class(
|
||||
{
|
||||
name: folderNameSchema.openapi({
|
||||
...folderFieldDocs.name,
|
||||
maxLength: FOLDER_NAME_MAX_LENGTH,
|
||||
}),
|
||||
parentFolderId: folderIdSchema.optional().openapi(folderFieldDocs.parentFolderId),
|
||||
},
|
||||
{ strict: true },
|
||||
) {}
|
||||
|
||||
// Own class so create does not share UpdatedFolderPublicDto's OpenAPI component.
|
||||
export class CreatedFolderPublicDto extends Z.class(folderCorePublicShape, { strict: true }) {}
|
||||
|
|
|
|||
|
|
@ -77,3 +77,11 @@ export const deleteFolderQueryFieldDocs = {
|
|||
'Optional target folder ID to move workflows and sub-folders into before deleting.',
|
||||
},
|
||||
} as const satisfies Record<string, ZodOpenAPIMetadata>;
|
||||
|
||||
export const folderProjectIdParamDocs = {
|
||||
param: {
|
||||
description:
|
||||
"The ID of the project, or `personal` to create the folder in the calling user's own " +
|
||||
'personal project.',
|
||||
},
|
||||
} as const satisfies ZodOpenAPIMetadata;
|
||||
|
|
|
|||
|
|
@ -274,14 +274,17 @@ export { DeleteFolderDto } from './folders/delete-folder.dto';
|
|||
export { ListFolderQueryDto } from './folders/list-folder-query.dto';
|
||||
export { TransferFolderBodyDto } from './folders/transfer-folder.dto';
|
||||
export {
|
||||
CreateFolderPublicDto,
|
||||
CreatedFolderPublicDto,
|
||||
DeleteFolderQueryPublicDto,
|
||||
FolderDetailsPublicDto,
|
||||
FolderListPublicDto,
|
||||
FolderPublicDto,
|
||||
FolderDetailsPublicDto,
|
||||
ListFoldersQueryPublicDto,
|
||||
folderPublicSchema,
|
||||
UpdateFolderPublicDto,
|
||||
UpdatedFolderPublicDto,
|
||||
folderProjectIdParamSchema,
|
||||
folderPublicSchema,
|
||||
} from './folders/folder-public.dto';
|
||||
export type { FolderPublic } from './folders/folder-public.dto';
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { z } from 'zod';
|
|||
|
||||
const illegalCharacterRegex = /[[\]^\\/:*?"<>|]/;
|
||||
const dotsOnlyRegex = /^\.+$/;
|
||||
const FOLDER_NAME_MAX_LENGTH = 128;
|
||||
export const FOLDER_NAME_MAX_LENGTH = 128;
|
||||
|
||||
export const folderNameSchema = z
|
||||
.string()
|
||||
|
|
|
|||
|
|
@ -156,7 +156,6 @@ export default defineConfig(
|
|||
'./src/public-api/v1/handlers/data-tables/data-tables.handler.ts',
|
||||
'./src/public-api/v1/handlers/data-tables/data-tables.rows.handler.ts',
|
||||
'./src/public-api/v1/handlers/evaluations/evaluations.handler.ts',
|
||||
'./src/public-api/v1/handlers/folders/folders.handler.ts',
|
||||
'./src/public-api/v1/handlers/ldap/ldap.handler.ts',
|
||||
'./src/public-api/v1/handlers/log-streaming/log-streaming.handler.ts',
|
||||
'./src/public-api/v1/handlers/n8n-packages/n8n-packages.handler.ts',
|
||||
|
|
|
|||
|
|
@ -161,7 +161,6 @@ export default defineConfig({
|
|||
'./src/public-api/v1/handlers/data-tables/data-tables.rows.handler.ts',
|
||||
'./src/public-api/v1/handlers/discover/discover.handler.ts',
|
||||
'./src/public-api/v1/handlers/evaluations/evaluations.handler.ts',
|
||||
'./src/public-api/v1/handlers/folders/folders.handler.ts',
|
||||
'./src/public-api/v1/handlers/insights/insights.handler.ts',
|
||||
'./src/public-api/v1/handlers/ldap/ldap.handler.ts',
|
||||
'./src/public-api/v1/handlers/log-streaming/log-streaming.handler.ts',
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import {
|
||||
CreateFolderPublicDto,
|
||||
CreatedFolderPublicDto,
|
||||
DeleteFolderQueryPublicDto,
|
||||
FolderDetailsPublicDto,
|
||||
FolderListPublicDto,
|
||||
|
|
@ -7,6 +9,7 @@ import {
|
|||
UpdateFolderPublicDto,
|
||||
UpdatedFolderPublicDto,
|
||||
folderIdParamSchema,
|
||||
folderProjectIdParamSchema,
|
||||
projectIdParamSchema,
|
||||
} from '@n8n/api-types';
|
||||
import { LICENSE_FEATURES } from '@n8n/constants';
|
||||
|
|
@ -28,6 +31,7 @@ import {
|
|||
Licensed,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
PublicApiController,
|
||||
Query,
|
||||
} from '@n8n/decorators';
|
||||
|
|
@ -39,9 +43,12 @@ import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
|||
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
||||
import { assertProjectScope } from '@/public-api/v1/shared/services/utils.service';
|
||||
import { FolderService } from '@/services/folder.service';
|
||||
import { ProjectService } from '@/services/project.service.ee';
|
||||
|
||||
const tags = ['Folders'];
|
||||
|
||||
const PERSONAL_PROJECT_ALIAS = 'personal';
|
||||
|
||||
/**
|
||||
* The `select` query parameter decides which columns the query loads, so a field the caller did not
|
||||
* ask for is left `undefined` and drops out of the JSON body.
|
||||
|
|
@ -91,6 +98,16 @@ const toUpdatedFolderPublicDto = (folder: Folder): UpdatedFolderPublicDto => ({
|
|||
updatedAt: folder.updatedAt.toISOString(),
|
||||
});
|
||||
|
||||
type CreatedFolder = Omit<Folder, 'homeProject'>;
|
||||
|
||||
const toCreatedFolderPublicDto = (folder: CreatedFolder): CreatedFolderPublicDto => ({
|
||||
id: folder.id,
|
||||
name: folder.name,
|
||||
parentFolderId: folder.parentFolderId ?? folder.parentFolder?.id ?? null,
|
||||
createdAt: folder.createdAt.toISOString(),
|
||||
updatedAt: folder.updatedAt.toISOString(),
|
||||
});
|
||||
|
||||
const handleError = (error: unknown): never => {
|
||||
if (error instanceof FolderNotFoundError) {
|
||||
throw new NotFoundError(error.message);
|
||||
|
|
@ -104,7 +121,10 @@ const handleError = (error: unknown): never => {
|
|||
|
||||
@PublicApiController('/projects/:projectId/folders')
|
||||
export class FoldersPublicController {
|
||||
constructor(private readonly folderService: FolderService) {}
|
||||
constructor(
|
||||
private readonly folderService: FolderService,
|
||||
private readonly projectService: ProjectService,
|
||||
) {}
|
||||
|
||||
@Get('/')
|
||||
@Licensed(LICENSE_FEATURES.FOLDERS)
|
||||
|
|
@ -130,6 +150,34 @@ export class FoldersPublicController {
|
|||
return { count, data: data.map(toFolderPublicDto) };
|
||||
}
|
||||
|
||||
@Post('/')
|
||||
@Licensed(LICENSE_FEATURES.FOLDERS)
|
||||
@ApiKeyScope('folder:create')
|
||||
@ApiSummary('Create a folder')
|
||||
@ApiDescription('Create a folder within a project.')
|
||||
@ApiTags(tags)
|
||||
@ApiResponse(201, CreatedFolderPublicDto)
|
||||
@ApiErrorResponse(404)
|
||||
async createFolder(
|
||||
req: AuthenticatedRequest,
|
||||
_res: Response,
|
||||
@Param('projectId', folderProjectIdParamSchema) projectId: string,
|
||||
@Body body: CreateFolderPublicDto,
|
||||
): Promise<CreatedFolderPublicDto> {
|
||||
const resolvedProjectId = await this.resolveProjectId(req, projectId);
|
||||
|
||||
// A 404 for an unknown project, so `@ProjectScope` (which answers 403) cannot be used here.
|
||||
await assertProjectScope(req.user, resolvedProjectId, ['folder:create']);
|
||||
|
||||
try {
|
||||
return toCreatedFolderPublicDto(
|
||||
await this.folderService.createFolder(body, resolvedProjectId),
|
||||
);
|
||||
} catch (error) {
|
||||
return handleError(error);
|
||||
}
|
||||
}
|
||||
|
||||
@Get('/:folderId')
|
||||
@ApiKeyScope('folder:read')
|
||||
@Licensed(LICENSE_FEATURES.FOLDERS)
|
||||
|
|
@ -211,4 +259,15 @@ export class FoldersPublicController {
|
|||
return handleError(error);
|
||||
}
|
||||
}
|
||||
|
||||
private async resolveProjectId(req: AuthenticatedRequest, projectId: string): Promise<string> {
|
||||
if (projectId !== PERSONAL_PROJECT_ALIAS) return projectId;
|
||||
|
||||
const personalProject = await this.projectService.getPersonalProject(req.user);
|
||||
if (!personalProject) {
|
||||
throw new NotFoundError('Could not find a personal project for this user');
|
||||
}
|
||||
|
||||
return personalProject.id;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,67 +0,0 @@
|
|||
import { CreateFolderDto } from '@n8n/api-types';
|
||||
import type { AuthenticatedRequest } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import { UserError } from 'n8n-workflow';
|
||||
|
||||
import { FolderNotFoundError } from '@/errors/folder-not-found.error';
|
||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
||||
import { FolderService } from '@/services/folder.service';
|
||||
import { ProjectService } from '@/services/project.service.ee';
|
||||
|
||||
import type { PublicAPIEndpoint } from '../../shared/handler.types';
|
||||
import {
|
||||
apiKeyHasScopeWithGlobalScopeFallback,
|
||||
isLicensed,
|
||||
} from '../../shared/middlewares/global.middleware';
|
||||
import { assertProjectScope } from '../../shared/services/utils.service';
|
||||
|
||||
const PERSONAL_PROJECT_ALIAS = 'personal';
|
||||
|
||||
const handleError = (error: unknown) => {
|
||||
if (error instanceof FolderNotFoundError) {
|
||||
throw new NotFoundError(error.message);
|
||||
}
|
||||
if (error instanceof UserError) {
|
||||
throw new BadRequestError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
};
|
||||
|
||||
type FolderHandlers = {
|
||||
createFolder: PublicAPIEndpoint<AuthenticatedRequest<{ projectId: string }>>;
|
||||
};
|
||||
|
||||
const folderHandlers: FolderHandlers = {
|
||||
createFolder: [
|
||||
isLicensed('feat:folders'),
|
||||
apiKeyHasScopeWithGlobalScopeFallback({ scope: 'folder:create' }),
|
||||
async (req, res) => {
|
||||
let { projectId } = req.params;
|
||||
if (projectId === PERSONAL_PROJECT_ALIAS) {
|
||||
const personalProject = await Container.get(ProjectService).getPersonalProject(req.user);
|
||||
if (!personalProject) {
|
||||
throw new NotFoundError('Could not find a personal project for this user');
|
||||
}
|
||||
projectId = personalProject.id;
|
||||
}
|
||||
|
||||
await assertProjectScope(req.user, projectId, ['folder:create']);
|
||||
|
||||
const payload = CreateFolderDto.safeParse(req.body);
|
||||
if (payload.error) {
|
||||
throw new BadRequestError(payload.error.errors[0].message);
|
||||
}
|
||||
|
||||
try {
|
||||
const folder = await Container.get(FolderService).createFolder(payload.data, projectId);
|
||||
return res.status(201).json(folder);
|
||||
} catch (error) {
|
||||
return handleError(error);
|
||||
}
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export = folderHandlers;
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
post:
|
||||
x-eov-operation-id: createFolder
|
||||
x-required-scope: folder:create
|
||||
x-eov-operation-handler: v1/handlers/folders/folders.handler
|
||||
tags:
|
||||
- Folders
|
||||
summary: Create a folder
|
||||
description: Create a folder within a project.
|
||||
parameters:
|
||||
- name: projectId
|
||||
in: path
|
||||
description: >-
|
||||
The ID of the project, or `personal` to create the folder in the
|
||||
calling user's own personal project.
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
description: Payload for folder to create.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../schemas/folder.create.yml'
|
||||
required: true
|
||||
responses:
|
||||
'201':
|
||||
description: Operation successful.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../schemas/folder.yml'
|
||||
'400':
|
||||
$ref: '../../../../shared/spec/responses/badRequest.yml'
|
||||
'401':
|
||||
$ref: '../../../../shared/spec/responses/unauthorized.yml'
|
||||
'403':
|
||||
$ref: '../../../../shared/spec/responses/forbidden.yml'
|
||||
'404':
|
||||
$ref: '../../../../shared/spec/responses/notFound.yml'
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- name
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
example: My Folder
|
||||
parentFolderId:
|
||||
type: string
|
||||
example: abc123
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
type: object
|
||||
additionalProperties: false
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
readOnly: true
|
||||
name:
|
||||
type: string
|
||||
example: My Folder
|
||||
parentFolderId:
|
||||
type: string
|
||||
nullable: true
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
updatedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
operationId: createFolder
|
||||
tags:
|
||||
- Folders
|
||||
summary: Create a folder
|
||||
description: Create a folder within a project.
|
||||
x-required-scope: folder:create
|
||||
x-eov-operation-id: unreachable
|
||||
x-eov-operation-handler: v1/handlers/decorator-routed.handler
|
||||
x-decorator-routed: true
|
||||
parameters:
|
||||
- schema:
|
||||
type: string
|
||||
required: true
|
||||
description: The ID of the project, or `personal` to create the folder in the calling user's own personal project.
|
||||
name: projectId
|
||||
in: path
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
maxLength: 128
|
||||
example: My Folder
|
||||
parentFolderId:
|
||||
type: string
|
||||
maxLength: 36
|
||||
example: kRZ0ewrAEAhCkTBd
|
||||
required:
|
||||
- name
|
||||
additionalProperties: false
|
||||
responses:
|
||||
'201':
|
||||
description: Operation successful.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
readOnly: true
|
||||
example: zLXWgYyZrZ4Gn1Vk
|
||||
name:
|
||||
type: string
|
||||
example: My Folder
|
||||
parentFolderId:
|
||||
type: string
|
||||
nullable: true
|
||||
example: kRZ0ewrAEAhCkTBd
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
updatedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
- parentFolderId
|
||||
- createdAt
|
||||
- updatedAt
|
||||
additionalProperties: false
|
||||
'400':
|
||||
$ref: ../../../../shared/spec/responses/badRequest.yml
|
||||
'401':
|
||||
$ref: ../../../../shared/spec/responses/unauthorized.yml
|
||||
'403':
|
||||
$ref: ../../../../shared/spec/responses/forbidden.yml
|
||||
'404':
|
||||
$ref: ../../../../shared/spec/responses/notFound.yml
|
||||
'415':
|
||||
$ref: ../../../../shared/spec/responses/unsupportedMediaType.yml
|
||||
|
|
@ -30,6 +30,7 @@ requestBody:
|
|||
name:
|
||||
type: string
|
||||
example: Renamed Folder
|
||||
maxLength: 128
|
||||
parentFolderId:
|
||||
type: string
|
||||
description: ID of the parent folder to move this folder into. Set to "0" to move the folder to the project root.
|
||||
|
|
|
|||
|
|
@ -57,6 +57,8 @@ paths:
|
|||
/projects/{projectId}/folders:
|
||||
get:
|
||||
$ref: ./handlers/projects/spec/paths/listFolders.generated.yml
|
||||
post:
|
||||
$ref: ./handlers/projects/spec/paths/createFolder.generated.yml
|
||||
/projects/{projectId}/folders/{folderId}:
|
||||
get:
|
||||
$ref: ./handlers/projects/spec/paths/getFolder.generated.yml
|
||||
|
|
|
|||
|
|
@ -123,8 +123,6 @@ paths:
|
|||
$ref: './handlers/community-packages/spec/paths/community-packages.yml'
|
||||
/community-packages/{name}:
|
||||
$ref: './handlers/community-packages/spec/paths/community-packages.packageName.yml'
|
||||
/projects/{projectId}/folders:
|
||||
$ref: './handlers/folders/spec/paths/folders.yml'
|
||||
/n8n-packages/export:
|
||||
$ref: './handlers/n8n-packages/spec/paths/n8n-packages.export.yml'
|
||||
/n8n-packages/import:
|
||||
|
|
|
|||
|
|
@ -132,6 +132,30 @@ describe('POST /projects/:projectId/folders', () => {
|
|||
.send({});
|
||||
|
||||
expect(response.statusCode).toBe(400);
|
||||
expect(response.body).toEqual({
|
||||
message: "request/body must have required property 'name'",
|
||||
});
|
||||
});
|
||||
|
||||
test('should return 400 when folder name is empty', async () => {
|
||||
testServer.license.enable('feat:folders');
|
||||
|
||||
const response = await authOwnerAgent
|
||||
.post(`/projects/${ownerPersonalProject.id}/folders`)
|
||||
.send({ name: '' });
|
||||
|
||||
expect(response.statusCode).toBe(400);
|
||||
expect(response.body.message).toContain('Folder name cannot be empty');
|
||||
});
|
||||
|
||||
test('should return 400 when the body carries an unknown key', async () => {
|
||||
testServer.license.enable('feat:folders');
|
||||
|
||||
const response = await authOwnerAgent
|
||||
.post(`/projects/${ownerPersonalProject.id}/folders`)
|
||||
.send({ name: 'Folder', unknownKey: 'nope' });
|
||||
|
||||
expect(response.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
test('should return 404 when parentFolderId is invalid', async () => {
|
||||
|
|
@ -162,8 +186,13 @@ describe('POST /projects/:projectId/folders', () => {
|
|||
.send({ name: 'My Folder' });
|
||||
|
||||
expect(response.statusCode).toBe(201);
|
||||
expect(response.body).toHaveProperty('id');
|
||||
expect(response.body).toHaveProperty('name', 'My Folder');
|
||||
expect(response.body).toStrictEqual({
|
||||
id: expect.any(String),
|
||||
name: 'My Folder',
|
||||
parentFolderId: null,
|
||||
createdAt: expect.stringMatching(ISO_DATE_TIME),
|
||||
updatedAt: expect.stringMatching(ISO_DATE_TIME),
|
||||
});
|
||||
});
|
||||
|
||||
test('should create a folder with parentFolderId', async () => {
|
||||
|
|
@ -177,6 +206,7 @@ describe('POST /projects/:projectId/folders', () => {
|
|||
|
||||
expect(response.statusCode).toBe(201);
|
||||
expect(response.body).toHaveProperty('name', 'Child');
|
||||
expect(response.body).toHaveProperty('parentFolderId', parentFolder.id);
|
||||
});
|
||||
|
||||
test('should return 500 when createFolder throws an unexpected error', async () => {
|
||||
|
|
|
|||
|
|
@ -240,7 +240,6 @@
|
|||
"src/public-api/v1/handlers/data-tables/data-tables.rows.handler.ts",
|
||||
"src/public-api/v1/handlers/discover/discover.handler.ts",
|
||||
"src/public-api/v1/handlers/evaluations/evaluations.handler.ts",
|
||||
"src/public-api/v1/handlers/folders/folders.handler.ts",
|
||||
"src/public-api/v1/handlers/insights/insights.handler.ts",
|
||||
"src/public-api/v1/handlers/ldap/ldap.handler.ts",
|
||||
"src/public-api/v1/handlers/log-streaming/log-streaming.handler.ts",
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user