mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 04:47:29 -08:00
feat(core): Support create, read, update, delete projects in Public API (#10269)
This commit is contained in:
parent
dc8c94d036
commit
489ce10063
|
@ -0,0 +1,65 @@
|
||||||
|
import { globalScope, isLicensed, validCursor } from '../../shared/middlewares/global.middleware';
|
||||||
|
import type { Response } from 'express';
|
||||||
|
import type { ProjectRequest } from '@/requests';
|
||||||
|
import type { PaginatedRequest } from '@/PublicApi/types';
|
||||||
|
import Container from 'typedi';
|
||||||
|
import { ProjectController } from '@/controllers/project.controller';
|
||||||
|
import { ProjectRepository } from '@/databases/repositories/project.repository';
|
||||||
|
import { encodeNextCursor } from '../../shared/services/pagination.service';
|
||||||
|
|
||||||
|
type Create = ProjectRequest.Create;
|
||||||
|
type Update = ProjectRequest.Update;
|
||||||
|
type Delete = ProjectRequest.Delete;
|
||||||
|
type GetAll = PaginatedRequest;
|
||||||
|
|
||||||
|
export = {
|
||||||
|
createProject: [
|
||||||
|
isLicensed('feat:projectRole:admin'),
|
||||||
|
globalScope('project:create'),
|
||||||
|
async (req: Create, res: Response) => {
|
||||||
|
const project = await Container.get(ProjectController).createProject(req);
|
||||||
|
|
||||||
|
return res.status(201).json(project);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
updateProject: [
|
||||||
|
isLicensed('feat:projectRole:admin'),
|
||||||
|
globalScope('project:update'),
|
||||||
|
async (req: Update, res: Response) => {
|
||||||
|
await Container.get(ProjectController).updateProject(req);
|
||||||
|
|
||||||
|
return res.status(204).send();
|
||||||
|
},
|
||||||
|
],
|
||||||
|
deleteProject: [
|
||||||
|
isLicensed('feat:projectRole:admin'),
|
||||||
|
globalScope('project:delete'),
|
||||||
|
async (req: Delete, res: Response) => {
|
||||||
|
await Container.get(ProjectController).deleteProject(req);
|
||||||
|
|
||||||
|
return res.status(204).send();
|
||||||
|
},
|
||||||
|
],
|
||||||
|
getProjects: [
|
||||||
|
isLicensed('feat:projectRole:admin'),
|
||||||
|
globalScope('project:list'),
|
||||||
|
validCursor,
|
||||||
|
async (req: GetAll, res: Response) => {
|
||||||
|
const { offset = 0, limit = 100 } = req.query;
|
||||||
|
|
||||||
|
const [projects, count] = await Container.get(ProjectRepository).findAndCount({
|
||||||
|
skip: offset,
|
||||||
|
take: limit,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
data: projects,
|
||||||
|
nextCursor: encodeNextCursor({
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
numberOfTotalRecords: count,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
|
@ -0,0 +1,43 @@
|
||||||
|
delete:
|
||||||
|
x-eov-operation-id: deleteProject
|
||||||
|
x-eov-operation-handler: v1/handlers/projects/projects.handler
|
||||||
|
tags:
|
||||||
|
- Projects
|
||||||
|
summary: Delete a project
|
||||||
|
description: Delete a project from your instance.
|
||||||
|
parameters:
|
||||||
|
- $ref: '../schemas/parameters/projectId.yml'
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
description: Operation successful.
|
||||||
|
'401':
|
||||||
|
$ref: '../../../../shared/spec/responses/unauthorized.yml'
|
||||||
|
'403':
|
||||||
|
$ref: '../../../../shared/spec/responses/forbidden.yml'
|
||||||
|
'404':
|
||||||
|
$ref: '../../../../shared/spec/responses/notFound.yml'
|
||||||
|
put:
|
||||||
|
x-eov-operation-id: updateProject
|
||||||
|
x-eov-operation-handler: v1/handlers/projects/projects.handler
|
||||||
|
tags:
|
||||||
|
- Project
|
||||||
|
summary: Update a project
|
||||||
|
description: Update a project.
|
||||||
|
requestBody:
|
||||||
|
description: Updated project object.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../schemas/project.yml'
|
||||||
|
required: true
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
description: Operation successful.
|
||||||
|
'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'
|
|
@ -0,0 +1,40 @@
|
||||||
|
post:
|
||||||
|
x-eov-operation-id: createProject
|
||||||
|
x-eov-operation-handler: v1/handlers/projects/projects.handler
|
||||||
|
tags:
|
||||||
|
- Projects
|
||||||
|
summary: Create a project
|
||||||
|
description: Create a project in your instance.
|
||||||
|
requestBody:
|
||||||
|
description: Payload for project to create.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../schemas/project.yml'
|
||||||
|
required: true
|
||||||
|
responses:
|
||||||
|
'201':
|
||||||
|
description: Operation successful.
|
||||||
|
'400':
|
||||||
|
$ref: '../../../../shared/spec/responses/badRequest.yml'
|
||||||
|
'401':
|
||||||
|
$ref: '../../../../shared/spec/responses/unauthorized.yml'
|
||||||
|
get:
|
||||||
|
x-eov-operation-id: getProjects
|
||||||
|
x-eov-operation-handler: v1/handlers/projects/projects.handler
|
||||||
|
tags:
|
||||||
|
- Projects
|
||||||
|
summary: Retrieve projects
|
||||||
|
description: Retrieve projects from your instance.
|
||||||
|
parameters:
|
||||||
|
- $ref: '../../../../shared/spec/parameters/limit.yml'
|
||||||
|
- $ref: '../../../../shared/spec/parameters/cursor.yml'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Operation successful.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../schemas/projectList.yml'
|
||||||
|
'401':
|
||||||
|
$ref: '../../../../shared/spec/responses/unauthorized.yml'
|
|
@ -0,0 +1,6 @@
|
||||||
|
name: projectId
|
||||||
|
in: path
|
||||||
|
description: The ID of the project.
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
|
@ -0,0 +1,13 @@
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
|
@ -0,0 +1,11 @@
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: './project.yml'
|
||||||
|
nextCursor:
|
||||||
|
type: string
|
||||||
|
description: Paginate through projects by setting the cursor parameter to a nextCursor attribute returned by a previous request. Default value fetches the first "page" of the collection.
|
||||||
|
nullable: true
|
||||||
|
example: MTIzZTQ1NjctZTg5Yi0xMmQzLWE0NTYtNDI2NjE0MTc0MDA
|
|
@ -32,6 +32,8 @@ tags:
|
||||||
description: Operations about source control
|
description: Operations about source control
|
||||||
- name: Variables
|
- name: Variables
|
||||||
description: Operations about variables
|
description: Operations about variables
|
||||||
|
- name: Projects
|
||||||
|
description: Operations about projects
|
||||||
|
|
||||||
paths:
|
paths:
|
||||||
/audit:
|
/audit:
|
||||||
|
@ -72,6 +74,10 @@ paths:
|
||||||
$ref: './handlers/variables/spec/paths/variables.yml'
|
$ref: './handlers/variables/spec/paths/variables.yml'
|
||||||
/variables/{id}:
|
/variables/{id}:
|
||||||
$ref: './handlers/variables/spec/paths/variables.id.yml'
|
$ref: './handlers/variables/spec/paths/variables.id.yml'
|
||||||
|
/projects:
|
||||||
|
$ref: './handlers/projects/spec/paths/projects.yml'
|
||||||
|
/projects/{projectId}:
|
||||||
|
$ref: './handlers/projects/spec/paths/projects.projectId.yml'
|
||||||
components:
|
components:
|
||||||
schemas:
|
schemas:
|
||||||
$ref: './shared/spec/schemas/_index.yml'
|
$ref: './shared/spec/schemas/_index.yml'
|
||||||
|
|
401
packages/cli/test/integration/publicApi/projects.test.ts
Normal file
401
packages/cli/test/integration/publicApi/projects.test.ts
Normal file
|
@ -0,0 +1,401 @@
|
||||||
|
import { setupTestServer } from '@test-integration/utils';
|
||||||
|
import { createMember, createOwner } from '@test-integration/db/users';
|
||||||
|
import * as testDb from '../shared/testDb';
|
||||||
|
import { FeatureNotLicensedError } from '@/errors/feature-not-licensed.error';
|
||||||
|
import { createTeamProject, getProjectByNameOrFail } from '@test-integration/db/projects';
|
||||||
|
import { mockInstance } from '@test/mocking';
|
||||||
|
import { Telemetry } from '@/telemetry';
|
||||||
|
|
||||||
|
describe('Projects in Public API', () => {
|
||||||
|
const testServer = setupTestServer({ endpointGroups: ['publicApi'] });
|
||||||
|
mockInstance(Telemetry);
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await testDb.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await testDb.truncate(['Project', 'User']);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /projects', () => {
|
||||||
|
it('if licensed, should return all projects with pagination', async () => {
|
||||||
|
/**
|
||||||
|
* Arrange
|
||||||
|
*/
|
||||||
|
testServer.license.setQuota('quota:maxTeamProjects', -1);
|
||||||
|
testServer.license.enable('feat:projectRole:admin');
|
||||||
|
const owner = await createOwner({ withApiKey: true });
|
||||||
|
const projects = await Promise.all([
|
||||||
|
createTeamProject(),
|
||||||
|
createTeamProject(),
|
||||||
|
createTeamProject(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Act
|
||||||
|
*/
|
||||||
|
const response = await testServer.publicApiAgentFor(owner).get('/projects');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert
|
||||||
|
*/
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body).toHaveProperty('data');
|
||||||
|
expect(response.body).toHaveProperty('nextCursor');
|
||||||
|
expect(Array.isArray(response.body.data)).toBe(true);
|
||||||
|
expect(response.body.data.length).toBe(projects.length + 1); // +1 for the owner's personal project
|
||||||
|
|
||||||
|
projects.forEach(({ id, name }) => {
|
||||||
|
expect(response.body.data).toContainEqual(expect.objectContaining({ id, name }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('if not authenticated, should reject', async () => {
|
||||||
|
/**
|
||||||
|
* Arrange
|
||||||
|
*/
|
||||||
|
const owner = await createOwner({ withApiKey: false });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Act
|
||||||
|
*/
|
||||||
|
const response = await testServer.publicApiAgentFor(owner).get('/projects');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert
|
||||||
|
*/
|
||||||
|
expect(response.status).toBe(401);
|
||||||
|
expect(response.body).toHaveProperty('message', "'X-N8N-API-KEY' header required");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('if not licensed, should reject', async () => {
|
||||||
|
/**
|
||||||
|
* Arrange
|
||||||
|
*/
|
||||||
|
const owner = await createOwner({ withApiKey: true });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Act
|
||||||
|
*/
|
||||||
|
const response = await testServer.publicApiAgentFor(owner).get('/projects');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert
|
||||||
|
*/
|
||||||
|
expect(response.status).toBe(403);
|
||||||
|
expect(response.body).toHaveProperty(
|
||||||
|
'message',
|
||||||
|
new FeatureNotLicensedError('feat:projectRole:admin').message,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('if missing scope, should reject', async () => {
|
||||||
|
/**
|
||||||
|
* Arrange
|
||||||
|
*/
|
||||||
|
testServer.license.setQuota('quota:maxTeamProjects', -1);
|
||||||
|
testServer.license.enable('feat:projectRole:admin');
|
||||||
|
const owner = await createMember({ withApiKey: true });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Act
|
||||||
|
*/
|
||||||
|
const response = await testServer.publicApiAgentFor(owner).get('/projects');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert
|
||||||
|
*/
|
||||||
|
expect(response.status).toBe(403);
|
||||||
|
expect(response.body).toHaveProperty('message', 'Forbidden');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /projects', () => {
|
||||||
|
it('if licensed, should create a new project', async () => {
|
||||||
|
/**
|
||||||
|
* Arrange
|
||||||
|
*/
|
||||||
|
testServer.license.setQuota('quota:maxTeamProjects', -1);
|
||||||
|
testServer.license.enable('feat:projectRole:admin');
|
||||||
|
const owner = await createOwner({ withApiKey: true });
|
||||||
|
const projectPayload = { name: 'some-project' };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Act
|
||||||
|
*/
|
||||||
|
const response = await testServer
|
||||||
|
.publicApiAgentFor(owner)
|
||||||
|
.post('/projects')
|
||||||
|
.send(projectPayload);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert
|
||||||
|
*/
|
||||||
|
expect(response.status).toBe(201);
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
name: 'some-project',
|
||||||
|
type: 'team',
|
||||||
|
id: expect.any(String),
|
||||||
|
createdAt: expect.any(String),
|
||||||
|
updatedAt: expect.any(String),
|
||||||
|
role: 'project:admin',
|
||||||
|
scopes: expect.any(Array),
|
||||||
|
});
|
||||||
|
await expect(getProjectByNameOrFail(projectPayload.name)).resolves.not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('if not authenticated, should reject', async () => {
|
||||||
|
/**
|
||||||
|
* Arrange
|
||||||
|
*/
|
||||||
|
const owner = await createOwner({ withApiKey: false });
|
||||||
|
const projectPayload = { name: 'some-project' };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Act
|
||||||
|
*/
|
||||||
|
const response = await testServer
|
||||||
|
.publicApiAgentFor(owner)
|
||||||
|
.post('/projects')
|
||||||
|
.send(projectPayload);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert
|
||||||
|
*/
|
||||||
|
expect(response.status).toBe(401);
|
||||||
|
expect(response.body).toHaveProperty('message', "'X-N8N-API-KEY' header required");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('if not licensed, should reject', async () => {
|
||||||
|
/**
|
||||||
|
* Arrange
|
||||||
|
*/
|
||||||
|
const owner = await createOwner({ withApiKey: true });
|
||||||
|
const projectPayload = { name: 'some-project' };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Act
|
||||||
|
*/
|
||||||
|
const response = await testServer
|
||||||
|
.publicApiAgentFor(owner)
|
||||||
|
.post('/projects')
|
||||||
|
.send(projectPayload);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert
|
||||||
|
*/
|
||||||
|
expect(response.status).toBe(403);
|
||||||
|
expect(response.body).toHaveProperty(
|
||||||
|
'message',
|
||||||
|
new FeatureNotLicensedError('feat:projectRole:admin').message,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('if missing scope, should reject', async () => {
|
||||||
|
/**
|
||||||
|
* Arrange
|
||||||
|
*/
|
||||||
|
testServer.license.setQuota('quota:maxTeamProjects', -1);
|
||||||
|
testServer.license.enable('feat:projectRole:admin');
|
||||||
|
const member = await createMember({ withApiKey: true });
|
||||||
|
const projectPayload = { name: 'some-project' };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Act
|
||||||
|
*/
|
||||||
|
const response = await testServer
|
||||||
|
.publicApiAgentFor(member)
|
||||||
|
.post('/projects')
|
||||||
|
.send(projectPayload);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert
|
||||||
|
*/
|
||||||
|
expect(response.status).toBe(403);
|
||||||
|
expect(response.body).toHaveProperty('message', 'Forbidden');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('DELETE /projects/:id', () => {
|
||||||
|
it('if licensed, should delete a project', async () => {
|
||||||
|
/**
|
||||||
|
* Arrange
|
||||||
|
*/
|
||||||
|
testServer.license.setQuota('quota:maxTeamProjects', -1);
|
||||||
|
testServer.license.enable('feat:projectRole:admin');
|
||||||
|
const owner = await createOwner({ withApiKey: true });
|
||||||
|
const project = await createTeamProject();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Act
|
||||||
|
*/
|
||||||
|
const response = await testServer.publicApiAgentFor(owner).delete(`/projects/${project.id}`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert
|
||||||
|
*/
|
||||||
|
expect(response.status).toBe(204);
|
||||||
|
await expect(getProjectByNameOrFail(project.id)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('if not authenticated, should reject', async () => {
|
||||||
|
/**
|
||||||
|
* Arrange
|
||||||
|
*/
|
||||||
|
const owner = await createOwner({ withApiKey: false });
|
||||||
|
const project = await createTeamProject();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Act
|
||||||
|
*/
|
||||||
|
const response = await testServer.publicApiAgentFor(owner).delete(`/projects/${project.id}`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert
|
||||||
|
*/
|
||||||
|
expect(response.status).toBe(401);
|
||||||
|
expect(response.body).toHaveProperty('message', "'X-N8N-API-KEY' header required");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('if not licensed, should reject', async () => {
|
||||||
|
/**
|
||||||
|
* Arrange
|
||||||
|
*/
|
||||||
|
const owner = await createOwner({ withApiKey: true });
|
||||||
|
const project = await createTeamProject();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Act
|
||||||
|
*/
|
||||||
|
const response = await testServer.publicApiAgentFor(owner).delete(`/projects/${project.id}`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert
|
||||||
|
*/
|
||||||
|
expect(response.status).toBe(403);
|
||||||
|
expect(response.body).toHaveProperty(
|
||||||
|
'message',
|
||||||
|
new FeatureNotLicensedError('feat:projectRole:admin').message,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('if missing scope, should reject', async () => {
|
||||||
|
/**
|
||||||
|
* Arrange
|
||||||
|
*/
|
||||||
|
testServer.license.setQuota('quota:maxTeamProjects', -1);
|
||||||
|
testServer.license.enable('feat:projectRole:admin');
|
||||||
|
const member = await createMember({ withApiKey: true });
|
||||||
|
const project = await createTeamProject();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Act
|
||||||
|
*/
|
||||||
|
const response = await testServer.publicApiAgentFor(member).delete(`/projects/${project.id}`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert
|
||||||
|
*/
|
||||||
|
expect(response.status).toBe(403);
|
||||||
|
expect(response.body).toHaveProperty('message', 'Forbidden');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PUT /projects/:id', () => {
|
||||||
|
it('if licensed, should update a project', async () => {
|
||||||
|
/**
|
||||||
|
* Arrange
|
||||||
|
*/
|
||||||
|
testServer.license.setQuota('quota:maxTeamProjects', -1);
|
||||||
|
testServer.license.enable('feat:projectRole:admin');
|
||||||
|
const owner = await createOwner({ withApiKey: true });
|
||||||
|
const project = await createTeamProject('old-name');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Act
|
||||||
|
*/
|
||||||
|
const response = await testServer
|
||||||
|
.publicApiAgentFor(owner)
|
||||||
|
.put(`/projects/${project.id}`)
|
||||||
|
.send({ name: 'new-name' });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert
|
||||||
|
*/
|
||||||
|
expect(response.status).toBe(204);
|
||||||
|
await expect(getProjectByNameOrFail('new-name')).resolves.not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('if not authenticated, should reject', async () => {
|
||||||
|
/**
|
||||||
|
* Arrange
|
||||||
|
*/
|
||||||
|
const owner = await createOwner({ withApiKey: false });
|
||||||
|
const project = await createTeamProject();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Act
|
||||||
|
*/
|
||||||
|
const response = await testServer
|
||||||
|
.publicApiAgentFor(owner)
|
||||||
|
.put(`/projects/${project.id}`)
|
||||||
|
.send({ name: 'new-name' });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert
|
||||||
|
*/
|
||||||
|
expect(response.status).toBe(401);
|
||||||
|
expect(response.body).toHaveProperty('message', "'X-N8N-API-KEY' header required");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('if not licensed, should reject', async () => {
|
||||||
|
/**
|
||||||
|
* Arrange
|
||||||
|
*/
|
||||||
|
const owner = await createOwner({ withApiKey: true });
|
||||||
|
const project = await createTeamProject();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Act
|
||||||
|
*/
|
||||||
|
const response = await testServer
|
||||||
|
.publicApiAgentFor(owner)
|
||||||
|
.put(`/projects/${project.id}`)
|
||||||
|
.send({ name: 'new-name' });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert
|
||||||
|
*/
|
||||||
|
expect(response.status).toBe(403);
|
||||||
|
expect(response.body).toHaveProperty(
|
||||||
|
'message',
|
||||||
|
new FeatureNotLicensedError('feat:projectRole:admin').message,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('if missing scope, should reject', async () => {
|
||||||
|
/**
|
||||||
|
* Arrange
|
||||||
|
*/
|
||||||
|
testServer.license.setQuota('quota:maxTeamProjects', -1);
|
||||||
|
testServer.license.enable('feat:projectRole:admin');
|
||||||
|
const member = await createMember({ withApiKey: true });
|
||||||
|
const project = await createTeamProject();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Act
|
||||||
|
*/
|
||||||
|
const response = await testServer
|
||||||
|
.publicApiAgentFor(member)
|
||||||
|
.put(`/projects/${project.id}`)
|
||||||
|
.send({ name: 'new-name' });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert
|
||||||
|
*/
|
||||||
|
expect(response.status).toBe(403);
|
||||||
|
expect(response.body).toHaveProperty('message', 'Forbidden');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -34,6 +34,10 @@ export const linkUserToProject = async (user: User, project: Project, role: Proj
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export async function getProjectByNameOrFail(name: string) {
|
||||||
|
return await Container.get(ProjectRepository).findOneOrFail({ where: { name } });
|
||||||
|
}
|
||||||
|
|
||||||
export const getPersonalProject = async (user: User): Promise<Project> => {
|
export const getPersonalProject = async (user: User): Promise<Project> => {
|
||||||
return await Container.get(ProjectRepository).findOneOrFail({
|
return await Container.get(ProjectRepository).findOneOrFail({
|
||||||
where: {
|
where: {
|
||||||
|
|
|
@ -86,7 +86,11 @@ export async function createOwner({ withApiKey } = { withApiKey: false }) {
|
||||||
return await createUser({ role: 'global:owner' });
|
return await createUser({ role: 'global:owner' });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createMember() {
|
export async function createMember({ withApiKey } = { withApiKey: false }) {
|
||||||
|
if (withApiKey) {
|
||||||
|
return await addApiKey(await createUser({ role: 'global:member' }));
|
||||||
|
}
|
||||||
|
|
||||||
return await createUser({ role: 'global:member' });
|
return await createUser({ role: 'global:member' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue