import { FeatureNotLicensedError } from '@/errors/feature-not-licensed.error'; import { Telemetry } from '@/telemetry'; import { mockInstance } from '@test/mocking'; import { createTeamProject, getProjectByNameOrFail } from '@test-integration/db/projects'; import { createMemberWithApiKey, createOwnerWithApiKey } from '@test-integration/db/users'; import { setupTestServer } from '@test-integration/utils'; import * as testDb from '../shared/test-db'; 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 createOwnerWithApiKey(); 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 () => { /** * Act */ const response = await testServer.publicApiAgentWithoutApiKey().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 createOwnerWithApiKey(); /** * 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 member = await createMemberWithApiKey(); /** * Act */ const response = await testServer.publicApiAgentFor(member).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 createOwnerWithApiKey(); 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 projectPayload = { name: 'some-project' }; /** * Act */ const response = await testServer .publicApiAgentWithoutApiKey() .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 createOwnerWithApiKey(); 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 createMemberWithApiKey(); 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 createOwnerWithApiKey(); 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 project = await createTeamProject(); /** * Act */ const response = await testServer .publicApiAgentWithoutApiKey() .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 createOwnerWithApiKey(); 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 owner = await createMemberWithApiKey(); 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', '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 createOwnerWithApiKey(); 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 project = await createTeamProject(); /** * Act */ const response = await testServer .publicApiAgentWithoutApiKey() .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 createOwnerWithApiKey(); 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 createMemberWithApiKey(); 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'); }); }); });