diff --git a/packages/cli/test/integration/credentials/credentials.ee.test.ts b/packages/cli/test/integration/credentials/credentials.api.ee.test.ts similarity index 100% rename from packages/cli/test/integration/credentials/credentials.ee.test.ts rename to packages/cli/test/integration/credentials/credentials.api.ee.test.ts diff --git a/packages/cli/test/integration/credentials.test.ts b/packages/cli/test/integration/credentials/credentials.api.test.ts similarity index 61% rename from packages/cli/test/integration/credentials.test.ts rename to packages/cli/test/integration/credentials/credentials.api.test.ts index c33c833515..ac046acbb7 100644 --- a/packages/cli/test/integration/credentials.test.ts +++ b/packages/cli/test/integration/credentials/credentials.api.test.ts @@ -1,65 +1,72 @@ -import { Container } from 'typedi'; -import type { SuperAgentTest } from 'supertest'; - -import type { Scope } from '@n8n/permissions'; -import config from '@/config'; import type { ListQuery } from '@/requests'; import type { User } from '@db/entities/User'; -import { CredentialsRepository } from '@db/repositories/credentials.repository'; -import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository'; -import { License } from '@/License'; - -import { randomCredentialPayload, randomName, randomString } from './shared/random'; -import * as testDb from './shared/testDb'; -import type { SaveCredentialFunction } from './shared/types'; -import * as utils from './shared/utils/'; +import config from '@/config'; +import * as testDb from '../shared/testDb'; +import { setupTestServer } from '../shared/utils'; import { - affixRoleToSaveCredential, + randomCredentialPayload as payload, + randomCredentialPayload, + randomName, + randomString, +} from '../shared/random'; +import { + saveCredential, shareCredentialWithProjects, shareCredentialWithUsers, -} from './shared/db/credentials'; -import { createManyUsers, createUser } from './shared/db/users'; -import { Credentials } from 'n8n-core'; +} from '../shared/db/credentials'; +import { createManyUsers, createMember, createOwner } from '../shared/db/users'; import { ProjectRepository } from '@/databases/repositories/project.repository'; +import Container from 'typedi'; import type { Project } from '@/databases/entities/Project'; +import { createTeamProject, linkUserToProject } from '../shared/db/projects'; +import type { SuperAgentTest } from 'supertest'; +import { Credentials } from 'n8n-core'; +import type { Scope } from '@sentry/node'; +import { CredentialsRepository } from '@/databases/repositories/credentials.repository'; +import { SharedCredentialsRepository } from '@/databases/repositories/sharedCredentials.repository'; import { ProjectService } from '@/services/project.service'; -import { createTeamProject, linkUserToProject } from './shared/db/projects'; -// mock that credentialsSharing is not enabled -jest.spyOn(License.prototype, 'isSharingEnabled').mockReturnValue(false); -const testServer = utils.setupTestServer({ endpointGroups: ['credentials'] }); +const { any } = expect; + +const testServer = setupTestServer({ endpointGroups: ['credentials'] }); let owner: User; -let ownerPersonalProject: Project; let member: User; -let memberPersonalProject: Project; let secondMember: User; + +let ownerPersonalProject: Project; +let memberPersonalProject: Project; + let authOwnerAgent: SuperAgentTest; let authMemberAgent: SuperAgentTest; -let saveCredential: SaveCredentialFunction; + let projectRepository: ProjectRepository; let sharedCredentialsRepository: SharedCredentialsRepository; let projectService: ProjectService; -beforeAll(async () => { - projectRepository = Container.get(ProjectRepository); - sharedCredentialsRepository = Container.get(SharedCredentialsRepository); - projectService = Container.get(ProjectService); - owner = await createUser({ role: 'global:owner' }); - ownerPersonalProject = await projectRepository.getPersonalProjectForUserOrFail(owner.id); - member = await createUser({ role: 'global:member' }); - memberPersonalProject = await projectRepository.getPersonalProjectForUserOrFail(member.id); - secondMember = await createUser({ role: 'global:member' }); +beforeEach(async () => { + await testDb.truncate(['SharedCredentials', 'Credentials']); - saveCredential = affixRoleToSaveCredential('credential:owner'); + owner = await createOwner(); + member = await createMember(); + secondMember = await createMember(); + + ownerPersonalProject = await Container.get(ProjectRepository).getPersonalProjectForUserOrFail( + owner.id, + ); + memberPersonalProject = await Container.get(ProjectRepository).getPersonalProjectForUserOrFail( + member.id, + ); authOwnerAgent = testServer.authAgentFor(owner); authMemberAgent = testServer.authAgentFor(member); + + projectRepository = Container.get(ProjectRepository); + sharedCredentialsRepository = Container.get(SharedCredentialsRepository); + projectService = Container.get(ProjectService); }); -beforeEach(async () => { - await testDb.truncate(['SharedCredentials', 'Credentials']); -}); +type GetAllResponse = { body: { data: ListQuery.Credentials.WithOwnedByAndSharedWith[] } }; // ---------------------------------------- // GET /credentials - fetch all credentials @@ -67,8 +74,8 @@ beforeEach(async () => { describe('GET /credentials', () => { test('should return all creds for owner', async () => { const [{ id: savedOwnerCredentialId }, { id: savedMemberCredentialId }] = await Promise.all([ - saveCredential(randomCredentialPayload(), { user: owner }), - saveCredential(randomCredentialPayload(), { user: member }), + saveCredential(randomCredentialPayload(), { user: owner, role: 'credential:owner' }), + saveCredential(randomCredentialPayload(), { user: member, role: 'credential:owner' }), ]); const response = await authOwnerAgent.get('/credentials'); @@ -90,8 +97,8 @@ describe('GET /credentials', () => { }); const [savedCredential1] = await Promise.all([ - saveCredential(randomCredentialPayload(), { user: member1 }), - saveCredential(randomCredentialPayload(), { user: member2 }), + saveCredential(randomCredentialPayload(), { user: member1, role: 'credential:owner' }), + saveCredential(randomCredentialPayload(), { user: member2, role: 'credential:owner' }), ]); const response = await testServer.authAgentFor(member1).get('/credentials'); @@ -115,8 +122,8 @@ describe('GET /credentials', () => { await linkUserToProject(member2, teamProject, 'project:editor'); const [savedCredential1, savedCredential2] = await Promise.all([ - saveCredential(randomCredentialPayload(), { project: teamProject }), - saveCredential(randomCredentialPayload(), { user: member2 }), + saveCredential(randomCredentialPayload(), { project: teamProject, role: 'credential:owner' }), + saveCredential(randomCredentialPayload(), { user: member2, role: 'credential:owner' }), ]); await shareCredentialWithProjects(savedCredential2, [teamProject]); @@ -204,6 +211,352 @@ describe('GET /credentials', () => { ); } }); + + describe('should return', () => { + test('all credentials for owner', async () => { + const { id: id1 } = await saveCredential(payload(), { + user: owner, + role: 'credential:owner', + }); + const { id: id2 } = await saveCredential(payload(), { + user: member, + role: 'credential:owner', + }); + + const response: GetAllResponse = await testServer + .authAgentFor(owner) + .get('/credentials') + .expect(200); + + expect(response.body.data).toHaveLength(2); + + response.body.data.forEach(validateCredentialWithNoData); + + const savedIds = [id1, id2].sort(); + const returnedIds = response.body.data.map((c) => c.id).sort(); + + expect(savedIds).toEqual(returnedIds); + }); + + test('only own credentials for member', async () => { + const firstMember = member; + const secondMember = await createMember(); + + const c1 = await saveCredential(payload(), { user: firstMember, role: 'credential:owner' }); + const c2 = await saveCredential(payload(), { user: secondMember, role: 'credential:owner' }); + + const response: GetAllResponse = await testServer + .authAgentFor(firstMember) + .get('/credentials') + .expect(200); + + expect(response.body.data).toHaveLength(1); + + const [firstMemberCred] = response.body.data; + + validateCredentialWithNoData(firstMemberCred); + expect(firstMemberCred.id).toBe(c1.id); + expect(firstMemberCred.id).not.toBe(c2.id); + }); + }); + + describe('filter', () => { + test('should filter credentials by field: name - full match', async () => { + const savedCred = await saveCredential(payload(), { user: owner, role: 'credential:owner' }); + + const response: GetAllResponse = await testServer + .authAgentFor(owner) + .get('/credentials') + .query(`filter={ "name": "${savedCred.name}" }`) + .expect(200); + + expect(response.body.data).toHaveLength(1); + + const [returnedCred] = response.body.data; + + expect(returnedCred.name).toBe(savedCred.name); + + const _response = await testServer + .authAgentFor(owner) + .get('/credentials') + .query('filter={ "name": "Non-Existing Credential" }') + .expect(200); + + expect(_response.body.data).toHaveLength(0); + }); + + test('should filter credentials by field: name - partial match', async () => { + const savedCred = await saveCredential(payload(), { user: owner, role: 'credential:owner' }); + + const partialName = savedCred.name.slice(3); + + const response: GetAllResponse = await testServer + .authAgentFor(owner) + .get('/credentials') + .query(`filter={ "name": "${partialName}" }`) + .expect(200); + + expect(response.body.data).toHaveLength(1); + + const [returnedCred] = response.body.data; + + expect(returnedCred.name).toBe(savedCred.name); + + const _response = await testServer + .authAgentFor(owner) + .get('/credentials') + .query('filter={ "name": "Non-Existing Credential" }') + .expect(200); + + expect(_response.body.data).toHaveLength(0); + }); + + test('should filter credentials by field: type - full match', async () => { + const savedCred = await saveCredential(payload(), { user: owner, role: 'credential:owner' }); + + const response: GetAllResponse = await testServer + .authAgentFor(owner) + .get('/credentials') + .query(`filter={ "type": "${savedCred.type}" }`) + .expect(200); + + expect(response.body.data).toHaveLength(1); + + const [returnedCred] = response.body.data; + + expect(returnedCred.type).toBe(savedCred.type); + + const _response = await testServer + .authAgentFor(owner) + .get('/credentials') + .query('filter={ "type": "Non-Existing Credential" }') + .expect(200); + + expect(_response.body.data).toHaveLength(0); + }); + + test('should filter credentials by field: type - partial match', async () => { + const savedCred = await saveCredential(payload(), { user: owner, role: 'credential:owner' }); + + const partialType = savedCred.type.slice(3); + + const response: GetAllResponse = await testServer + .authAgentFor(owner) + .get('/credentials') + .query(`filter={ "type": "${partialType}" }`) + .expect(200); + + expect(response.body.data).toHaveLength(1); + + const [returnedCred] = response.body.data; + + expect(returnedCred.type).toBe(savedCred.type); + + const _response = await testServer + .authAgentFor(owner) + .get('/credentials') + .query('filter={ "type": "Non-Existing Credential" }') + .expect(200); + + expect(_response.body.data).toHaveLength(0); + }); + + test('should filter credentials by projectId', async () => { + const credential = await saveCredential(payload(), { user: owner, role: 'credential:owner' }); + + const response1: GetAllResponse = await testServer + .authAgentFor(owner) + .get('/credentials') + .query(`filter={ "projectId": "${ownerPersonalProject.id}" }`) + .expect(200); + + expect(response1.body.data).toHaveLength(1); + expect(response1.body.data[0].id).toBe(credential.id); + + const response2 = await testServer + .authAgentFor(owner) + .get('/credentials') + .query('filter={ "projectId": "Non-Existing Project ID" }') + .expect(200); + + expect(response2.body.data).toHaveLength(0); + }); + + test('should return all credentials in a team project that member is part of', async () => { + const teamProjectWithMember = await createTeamProject('Team Project With member', owner); + void (await linkUserToProject(member, teamProjectWithMember, 'project:editor')); + await saveCredential(payload(), { + project: teamProjectWithMember, + role: 'credential:owner', + }); + await saveCredential(payload(), { + project: teamProjectWithMember, + role: 'credential:owner', + }); + const response: GetAllResponse = await testServer + .authAgentFor(member) + .get('/credentials') + .query(`filter={ "projectId": "${teamProjectWithMember.id}" }`) + .expect(200); + + expect(response.body.data).toHaveLength(2); + }); + + test('should return no credentials in a team project that member not is part of', async () => { + const teamProjectWithoutMember = await createTeamProject( + 'Team Project Without member', + owner, + ); + + await saveCredential(payload(), { + project: teamProjectWithoutMember, + role: 'credential:owner', + }); + + const response = await testServer + .authAgentFor(member) + .get('/credentials') + .query(`filter={ "projectId": "${teamProjectWithoutMember.id}" }`) + .expect(200); + + expect(response.body.data).toHaveLength(0); + }); + + test('should return only owned and explicitly shared credentials when filtering by any personal project id', async () => { + // Create credential owned by `owner` and share it to `member` + const ownerCredential = await saveCredential(payload(), { + user: owner, + role: 'credential:owner', + }); + await shareCredentialWithUsers(ownerCredential, [member]); + // Create credential owned by `member` + const memberCredential = await saveCredential(payload(), { + user: member, + role: 'credential:owner', + }); + + // Simulate editing a workflow owned by `owner` so request credentials to their personal project + const response: GetAllResponse = await testServer + .authAgentFor(member) + .get('/credentials') + .query(`filter={ "projectId": "${ownerPersonalProject.id}" }`) + .expect(200); + + expect(response.body.data).toHaveLength(2); + expect(response.body.data.map((credential) => credential.id)).toContain(ownerCredential.id); + expect(response.body.data.map((credential) => credential.id)).toContain(memberCredential.id); + }); + + test('should return all credentials to instance owners when working on their own personal project', async () => { + const ownerCredential = await saveCredential(payload(), { + user: owner, + role: 'credential:owner', + }); + const memberCredential = await saveCredential(payload(), { + user: member, + role: 'credential:owner', + }); + + const response: GetAllResponse = await testServer + .authAgentFor(owner) + .get('/credentials') + .query(`filter={ "projectId": "${ownerPersonalProject.id}" }&includeScopes=true`) + .expect(200); + + expect(response.body.data).toHaveLength(2); + expect(response.body.data.map((credential) => credential.id)).toContain(ownerCredential.id); + expect(response.body.data.map((credential) => credential.id)).toContain(memberCredential.id); + }); + }); + + describe('select', () => { + test('should select credential field: id', async () => { + await saveCredential(payload(), { user: owner, role: 'credential:owner' }); + await saveCredential(payload(), { user: owner, role: 'credential:owner' }); + + const response: GetAllResponse = await testServer + .authAgentFor(owner) + .get('/credentials') + .query('select=["id"]') + .expect(200); + + expect(response.body).toEqual({ + data: [{ id: any(String) }, { id: any(String) }], + }); + }); + + test('should select credential field: name', async () => { + await saveCredential(payload(), { user: owner, role: 'credential:owner' }); + await saveCredential(payload(), { user: owner, role: 'credential:owner' }); + + const response: GetAllResponse = await testServer + .authAgentFor(owner) + .get('/credentials') + .query('select=["name"]') + .expect(200); + + expect(response.body).toEqual({ + data: [{ name: any(String) }, { name: any(String) }], + }); + }); + + test('should select credential field: type', async () => { + await saveCredential(payload(), { user: owner, role: 'credential:owner' }); + await saveCredential(payload(), { user: owner, role: 'credential:owner' }); + + const response: GetAllResponse = await testServer + .authAgentFor(owner) + .get('/credentials') + .query('select=["type"]') + .expect(200); + + expect(response.body).toEqual({ + data: [{ type: any(String) }, { type: any(String) }], + }); + }); + }); + + describe('take', () => { + test('should return n credentials or less, without skip', async () => { + await saveCredential(payload(), { user: owner, role: 'credential:owner' }); + await saveCredential(payload(), { user: owner, role: 'credential:owner' }); + + const response = await testServer + .authAgentFor(owner) + .get('/credentials') + .query('take=2') + .expect(200); + + expect(response.body.data).toHaveLength(2); + + response.body.data.forEach(validateCredentialWithNoData); + + const _response = await testServer + .authAgentFor(owner) + .get('/credentials') + .query('take=1') + .expect(200); + + expect(_response.body.data).toHaveLength(1); + + _response.body.data.forEach(validateCredentialWithNoData); + }); + + test('should return n credentials or less, with skip', async () => { + await saveCredential(payload(), { user: owner, role: 'credential:owner' }); + await saveCredential(payload(), { user: owner, role: 'credential:owner' }); + + const response = await testServer + .authAgentFor(owner) + .get('/credentials') + .query('take=1&skip=1') + .expect(200); + + expect(response.body.data).toHaveLength(1); + + response.body.data.forEach(validateCredentialWithNoData); + }); + }); }); describe('POST /credentials', () => { @@ -322,38 +675,14 @@ describe('POST /credentials', () => { message: "You don't have the permissions to save the workflow in this project.", }); }); - - test('does not create the credential in a specific project if the user does not have the right role to do so', async () => { - // - // ARRANGE - // - const project = await projectRepository.save( - projectRepository.create({ - name: 'Team Project', - type: 'team', - }), - ); - await projectService.addUser(project.id, member.id, 'project:viewer'); - - // - // ACT - // - await authMemberAgent - .post('/credentials') - .send({ ...randomCredentialPayload(), projectId: project.id }) - // - // ASSERT - // - .expect(400, { - code: 400, - message: "You don't have the permissions to save the workflow in this project.", - }); - }); }); describe('DELETE /credentials/:id', () => { test('should delete owned cred for owner', async () => { - const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); + const savedCredential = await saveCredential(randomCredentialPayload(), { + user: owner, + role: 'credential:owner', + }); const response = await authOwnerAgent.delete(`/credentials/${savedCredential.id}`); @@ -372,7 +701,10 @@ describe('DELETE /credentials/:id', () => { }); test('should delete non-owned cred for owner', async () => { - const savedCredential = await saveCredential(randomCredentialPayload(), { user: member }); + const savedCredential = await saveCredential(randomCredentialPayload(), { + user: member, + role: 'credential:owner', + }); const response = await authOwnerAgent.delete(`/credentials/${savedCredential.id}`); @@ -391,7 +723,10 @@ describe('DELETE /credentials/:id', () => { }); test('should delete owned cred for member', async () => { - const savedCredential = await saveCredential(randomCredentialPayload(), { user: member }); + const savedCredential = await saveCredential(randomCredentialPayload(), { + user: member, + role: 'credential:owner', + }); const response = await authMemberAgent.delete(`/credentials/${savedCredential.id}`); @@ -410,7 +745,10 @@ describe('DELETE /credentials/:id', () => { }); test('should not delete non-owned cred for member', async () => { - const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); + const savedCredential = await saveCredential(randomCredentialPayload(), { + user: owner, + role: 'credential:owner', + }); const response = await authMemberAgent.delete(`/credentials/${savedCredential.id}`); @@ -428,7 +766,10 @@ describe('DELETE /credentials/:id', () => { }); test('should not delete non-owned but shared cred for member', async () => { - const savedCredential = await saveCredential(randomCredentialPayload(), { user: secondMember }); + const savedCredential = await saveCredential(randomCredentialPayload(), { + user: secondMember, + role: 'credential:owner', + }); await shareCredentialWithUsers(savedCredential, [member]); @@ -456,7 +797,10 @@ describe('DELETE /credentials/:id', () => { describe('PATCH /credentials/:id', () => { test('should update owned cred for owner', async () => { - const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); + const savedCredential = await saveCredential(randomCredentialPayload(), { + user: owner, + role: 'credential:owner', + }); const patchPayload = randomCredentialPayload(); const response = await authOwnerAgent @@ -498,7 +842,10 @@ describe('PATCH /credentials/:id', () => { }); test('should update non-owned cred for owner', async () => { - const savedCredential = await saveCredential(randomCredentialPayload(), { user: member }); + const savedCredential = await saveCredential(randomCredentialPayload(), { + user: member, + role: 'credential:owner', + }); const patchPayload = randomCredentialPayload(); const response = await authOwnerAgent @@ -530,7 +877,10 @@ describe('PATCH /credentials/:id', () => { }); test('should update owned cred for member', async () => { - const savedCredential = await saveCredential(randomCredentialPayload(), { user: member }); + const savedCredential = await saveCredential(randomCredentialPayload(), { + user: member, + role: 'credential:owner', + }); const patchPayload = randomCredentialPayload(); const response = await authMemberAgent @@ -561,7 +911,10 @@ describe('PATCH /credentials/:id', () => { }); test('should not update non-owned cred for member', async () => { - const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); + const savedCredential = await saveCredential(randomCredentialPayload(), { + user: owner, + role: 'credential:owner', + }); const patchPayload = randomCredentialPayload(); const response = await authMemberAgent @@ -578,7 +931,10 @@ describe('PATCH /credentials/:id', () => { }); test('should not update non-owned but shared cred for member', async () => { - const savedCredential = await saveCredential(randomCredentialPayload(), { user: secondMember }); + const savedCredential = await saveCredential(randomCredentialPayload(), { + user: secondMember, + role: 'credential:owner', + }); await shareCredentialWithUsers(savedCredential, [member]); const patchPayload = randomCredentialPayload(); @@ -596,7 +952,10 @@ describe('PATCH /credentials/:id', () => { }); test('should update non-owned but shared cred for instance owner', async () => { - const savedCredential = await saveCredential(randomCredentialPayload(), { user: secondMember }); + const savedCredential = await saveCredential(randomCredentialPayload(), { + user: secondMember, + role: 'credential:owner', + }); await shareCredentialWithUsers(savedCredential, [owner]); const patchPayload = randomCredentialPayload(); @@ -614,7 +973,10 @@ describe('PATCH /credentials/:id', () => { }); test('should fail with invalid inputs', async () => { - const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); + const savedCredential = await saveCredential(randomCredentialPayload(), { + user: owner, + role: 'credential:owner', + }); for (const invalidPayload of INVALID_PAYLOADS) { const response = await authOwnerAgent @@ -655,7 +1017,10 @@ describe('GET /credentials/new', () => { tempName = name + ' ' + (i + 1); expect(response.body.data.name).toBe(tempName); } - await saveCredential({ ...randomCredentialPayload(), name: tempName }, { user: owner }); + await saveCredential( + { ...randomCredentialPayload(), name: tempName }, + { user: owner, role: 'credential:owner' }, + ); } }); @@ -673,14 +1038,20 @@ describe('GET /credentials/new', () => { tempName = name + ' ' + (i + 1); expect(response.body.data.name).toBe(tempName); } - await saveCredential({ ...randomCredentialPayload(), name: tempName }, { user: owner }); + await saveCredential( + { ...randomCredentialPayload(), name: tempName }, + { user: owner, role: 'credential:owner' }, + ); } }); }); describe('GET /credentials/:id', () => { test('should retrieve owned cred for owner', async () => { - const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); + const savedCredential = await saveCredential(randomCredentialPayload(), { + user: owner, + role: 'credential:owner', + }); const firstResponse = await authOwnerAgent.get(`/credentials/${savedCredential.id}`); @@ -698,7 +1069,10 @@ describe('GET /credentials/:id', () => { }); test('should retrieve owned cred for member', async () => { - const savedCredential = await saveCredential(randomCredentialPayload(), { user: member }); + const savedCredential = await saveCredential(randomCredentialPayload(), { + user: member, + role: 'credential:owner', + }); const firstResponse = await authMemberAgent.get(`/credentials/${savedCredential.id}`); @@ -718,7 +1092,10 @@ describe('GET /credentials/:id', () => { }); test('should retrieve non-owned cred for owner', async () => { - const savedCredential = await saveCredential(randomCredentialPayload(), { user: member }); + const savedCredential = await saveCredential(randomCredentialPayload(), { + user: member, + role: 'credential:owner', + }); const response1 = await authOwnerAgent.get(`/credentials/${savedCredential.id}`); @@ -738,7 +1115,10 @@ describe('GET /credentials/:id', () => { }); test('should not retrieve non-owned cred for member', async () => { - const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); + const savedCredential = await saveCredential(randomCredentialPayload(), { + user: owner, + role: 'credential:owner', + }); const response = await authMemberAgent.get(`/credentials/${savedCredential.id}`); @@ -755,6 +1135,23 @@ describe('GET /credentials/:id', () => { }); }); +const INVALID_PAYLOADS = [ + { + type: randomName(), + data: { accessToken: randomString(6, 16) }, + }, + { + name: randomName(), + data: { accessToken: randomString(6, 16) }, + }, + { + name: randomName(), + type: randomName(), + }, + {}, + undefined, +]; + function validateMainCredentialData(credential: ListQuery.Credentials.WithOwnedByAndSharedWith) { const { name, type, sharedWithProjects, homeProject } = credential; @@ -774,19 +1171,8 @@ function validateMainCredentialData(credential: ListQuery.Credentials.WithOwnedB } } -const INVALID_PAYLOADS = [ - { - type: randomName(), - data: { accessToken: randomString(6, 16) }, - }, - { - name: randomName(), - data: { accessToken: randomString(6, 16) }, - }, - { - name: randomName(), - type: randomName(), - }, - {}, - undefined, -]; +function validateCredentialWithNoData(credential: ListQuery.Credentials.WithOwnedByAndSharedWith) { + validateMainCredentialData(credential); + + expect('data' in credential).toBe(false); +} diff --git a/packages/cli/test/integration/credentials/credentials.controller.test.ts b/packages/cli/test/integration/credentials/credentials.controller.test.ts deleted file mode 100644 index 7d0f9debe7..0000000000 --- a/packages/cli/test/integration/credentials/credentials.controller.test.ts +++ /dev/null @@ -1,401 +0,0 @@ -import type { ListQuery } from '@/requests'; -import type { User } from '@db/entities/User'; -import * as testDb from '../shared/testDb'; -import { setupTestServer } from '../shared/utils'; -import { randomCredentialPayload as payload } from '../shared/random'; -import { saveCredential, shareCredentialWithUsers } from '../shared/db/credentials'; -import { createMember, createOwner } from '../shared/db/users'; -import { ProjectRepository } from '@/databases/repositories/project.repository'; -import Container from 'typedi'; -import type { Project } from '@/databases/entities/Project'; -import { createTeamProject, linkUserToProject } from '../shared/db/projects'; - -const { any } = expect; - -const testServer = setupTestServer({ endpointGroups: ['credentials'] }); - -let owner: User; -let member: User; - -let ownerPersonalProject: Project; -let memberPersonalProject: Project; -beforeEach(async () => { - await testDb.truncate(['SharedCredentials', 'Credentials']); - - owner = await createOwner(); - member = await createMember(); - ownerPersonalProject = await Container.get(ProjectRepository).getPersonalProjectForUserOrFail( - owner.id, - ); - memberPersonalProject = await Container.get(ProjectRepository).getPersonalProjectForUserOrFail( - member.id, - ); -}); - -type GetAllResponse = { body: { data: ListQuery.Credentials.WithOwnedByAndSharedWith[] } }; - -describe('GET /credentials', () => { - describe('should return', () => { - test('all credentials for owner', async () => { - const { id: id1 } = await saveCredential(payload(), { - user: owner, - role: 'credential:owner', - }); - const { id: id2 } = await saveCredential(payload(), { - user: member, - role: 'credential:owner', - }); - - const response: GetAllResponse = await testServer - .authAgentFor(owner) - .get('/credentials') - .expect(200); - - expect(response.body.data).toHaveLength(2); - - response.body.data.forEach(validateCredential); - - const savedIds = [id1, id2].sort(); - const returnedIds = response.body.data.map((c) => c.id).sort(); - - expect(savedIds).toEqual(returnedIds); - }); - - test('only own credentials for member', async () => { - const firstMember = member; - const secondMember = await createMember(); - - const c1 = await saveCredential(payload(), { user: firstMember, role: 'credential:owner' }); - const c2 = await saveCredential(payload(), { user: secondMember, role: 'credential:owner' }); - - const response: GetAllResponse = await testServer - .authAgentFor(firstMember) - .get('/credentials') - .expect(200); - - expect(response.body.data).toHaveLength(1); - - const [firstMemberCred] = response.body.data; - - validateCredential(firstMemberCred); - expect(firstMemberCred.id).toBe(c1.id); - expect(firstMemberCred.id).not.toBe(c2.id); - }); - }); - - describe('filter', () => { - test('should filter credentials by field: name - full match', async () => { - const savedCred = await saveCredential(payload(), { user: owner, role: 'credential:owner' }); - - const response: GetAllResponse = await testServer - .authAgentFor(owner) - .get('/credentials') - .query(`filter={ "name": "${savedCred.name}" }`) - .expect(200); - - expect(response.body.data).toHaveLength(1); - - const [returnedCred] = response.body.data; - - expect(returnedCred.name).toBe(savedCred.name); - - const _response = await testServer - .authAgentFor(owner) - .get('/credentials') - .query('filter={ "name": "Non-Existing Credential" }') - .expect(200); - - expect(_response.body.data).toHaveLength(0); - }); - - test('should filter credentials by field: name - partial match', async () => { - const savedCred = await saveCredential(payload(), { user: owner, role: 'credential:owner' }); - - const partialName = savedCred.name.slice(3); - - const response: GetAllResponse = await testServer - .authAgentFor(owner) - .get('/credentials') - .query(`filter={ "name": "${partialName}" }`) - .expect(200); - - expect(response.body.data).toHaveLength(1); - - const [returnedCred] = response.body.data; - - expect(returnedCred.name).toBe(savedCred.name); - - const _response = await testServer - .authAgentFor(owner) - .get('/credentials') - .query('filter={ "name": "Non-Existing Credential" }') - .expect(200); - - expect(_response.body.data).toHaveLength(0); - }); - - test('should filter credentials by field: type - full match', async () => { - const savedCred = await saveCredential(payload(), { user: owner, role: 'credential:owner' }); - - const response: GetAllResponse = await testServer - .authAgentFor(owner) - .get('/credentials') - .query(`filter={ "type": "${savedCred.type}" }`) - .expect(200); - - expect(response.body.data).toHaveLength(1); - - const [returnedCred] = response.body.data; - - expect(returnedCred.type).toBe(savedCred.type); - - const _response = await testServer - .authAgentFor(owner) - .get('/credentials') - .query('filter={ "type": "Non-Existing Credential" }') - .expect(200); - - expect(_response.body.data).toHaveLength(0); - }); - - test('should filter credentials by field: type - partial match', async () => { - const savedCred = await saveCredential(payload(), { user: owner, role: 'credential:owner' }); - - const partialType = savedCred.type.slice(3); - - const response: GetAllResponse = await testServer - .authAgentFor(owner) - .get('/credentials') - .query(`filter={ "type": "${partialType}" }`) - .expect(200); - - expect(response.body.data).toHaveLength(1); - - const [returnedCred] = response.body.data; - - expect(returnedCred.type).toBe(savedCred.type); - - const _response = await testServer - .authAgentFor(owner) - .get('/credentials') - .query('filter={ "type": "Non-Existing Credential" }') - .expect(200); - - expect(_response.body.data).toHaveLength(0); - }); - - test('should filter credentials by projectId', async () => { - const credential = await saveCredential(payload(), { user: owner, role: 'credential:owner' }); - - const response1: GetAllResponse = await testServer - .authAgentFor(owner) - .get('/credentials') - .query(`filter={ "projectId": "${ownerPersonalProject.id}" }`) - .expect(200); - - expect(response1.body.data).toHaveLength(1); - expect(response1.body.data[0].id).toBe(credential.id); - - const response2 = await testServer - .authAgentFor(owner) - .get('/credentials') - .query('filter={ "projectId": "Non-Existing Project ID" }') - .expect(200); - - expect(response2.body.data).toHaveLength(0); - }); - - test('should return all credentials in a team project that member is part of', async () => { - const teamProjectWithMember = await createTeamProject('Team Project With member', owner); - void (await linkUserToProject(member, teamProjectWithMember, 'project:editor')); - await saveCredential(payload(), { - project: teamProjectWithMember, - role: 'credential:owner', - }); - await saveCredential(payload(), { - project: teamProjectWithMember, - role: 'credential:owner', - }); - const response: GetAllResponse = await testServer - .authAgentFor(member) - .get('/credentials') - .query(`filter={ "projectId": "${teamProjectWithMember.id}" }`) - .expect(200); - - expect(response.body.data).toHaveLength(2); - }); - - test('should return no credentials in a team project that member not is part of', async () => { - const teamProjectWithoutMember = await createTeamProject( - 'Team Project Without member', - owner, - ); - - await saveCredential(payload(), { - project: teamProjectWithoutMember, - role: 'credential:owner', - }); - - const response = await testServer - .authAgentFor(member) - .get('/credentials') - .query(`filter={ "projectId": "${teamProjectWithoutMember.id}" }`) - .expect(200); - - expect(response.body.data).toHaveLength(0); - }); - - test('should return only owned and explicitly shared credentials when filtering by any personal project id', async () => { - // Create credential owned by `owner` and share it to `member` - const ownerCredential = await saveCredential(payload(), { - user: owner, - role: 'credential:owner', - }); - await shareCredentialWithUsers(ownerCredential, [member]); - // Create credential owned by `member` - const memberCredential = await saveCredential(payload(), { - user: member, - role: 'credential:owner', - }); - - // Simulate editing a workflow owned by `owner` so request credentials to their personal project - const response: GetAllResponse = await testServer - .authAgentFor(member) - .get('/credentials') - .query(`filter={ "projectId": "${ownerPersonalProject.id}" }`) - .expect(200); - - expect(response.body.data).toHaveLength(2); - expect(response.body.data.map((credential) => credential.id)).toContain(ownerCredential.id); - expect(response.body.data.map((credential) => credential.id)).toContain(memberCredential.id); - }); - - test('should return all credentials to instance owners when working on their own personal project', async () => { - const ownerCredential = await saveCredential(payload(), { - user: owner, - role: 'credential:owner', - }); - const memberCredential = await saveCredential(payload(), { - user: member, - role: 'credential:owner', - }); - - const response: GetAllResponse = await testServer - .authAgentFor(owner) - .get('/credentials') - .query(`filter={ "projectId": "${ownerPersonalProject.id}" }&includeScopes=true`) - .expect(200); - - expect(response.body.data).toHaveLength(2); - expect(response.body.data.map((credential) => credential.id)).toContain(ownerCredential.id); - expect(response.body.data.map((credential) => credential.id)).toContain(memberCredential.id); - }); - }); - - describe('select', () => { - test('should select credential field: id', async () => { - await saveCredential(payload(), { user: owner, role: 'credential:owner' }); - await saveCredential(payload(), { user: owner, role: 'credential:owner' }); - - const response: GetAllResponse = await testServer - .authAgentFor(owner) - .get('/credentials') - .query('select=["id"]') - .expect(200); - - expect(response.body).toEqual({ - data: [{ id: any(String) }, { id: any(String) }], - }); - }); - - test('should select credential field: name', async () => { - await saveCredential(payload(), { user: owner, role: 'credential:owner' }); - await saveCredential(payload(), { user: owner, role: 'credential:owner' }); - - const response: GetAllResponse = await testServer - .authAgentFor(owner) - .get('/credentials') - .query('select=["name"]') - .expect(200); - - expect(response.body).toEqual({ - data: [{ name: any(String) }, { name: any(String) }], - }); - }); - - test('should select credential field: type', async () => { - await saveCredential(payload(), { user: owner, role: 'credential:owner' }); - await saveCredential(payload(), { user: owner, role: 'credential:owner' }); - - const response: GetAllResponse = await testServer - .authAgentFor(owner) - .get('/credentials') - .query('select=["type"]') - .expect(200); - - expect(response.body).toEqual({ - data: [{ type: any(String) }, { type: any(String) }], - }); - }); - }); - - describe('take', () => { - test('should return n credentials or less, without skip', async () => { - await saveCredential(payload(), { user: owner, role: 'credential:owner' }); - await saveCredential(payload(), { user: owner, role: 'credential:owner' }); - - const response = await testServer - .authAgentFor(owner) - .get('/credentials') - .query('take=2') - .expect(200); - - expect(response.body.data).toHaveLength(2); - - response.body.data.forEach(validateCredential); - - const _response = await testServer - .authAgentFor(owner) - .get('/credentials') - .query('take=1') - .expect(200); - - expect(_response.body.data).toHaveLength(1); - - _response.body.data.forEach(validateCredential); - }); - - test('should return n credentials or less, with skip', async () => { - await saveCredential(payload(), { user: owner, role: 'credential:owner' }); - await saveCredential(payload(), { user: owner, role: 'credential:owner' }); - - const response = await testServer - .authAgentFor(owner) - .get('/credentials') - .query('take=1&skip=1') - .expect(200); - - expect(response.body.data).toHaveLength(1); - - response.body.data.forEach(validateCredential); - }); - }); -}); - -function validateCredential(credential: ListQuery.Credentials.WithOwnedByAndSharedWith) { - const { name, type, sharedWithProjects, homeProject } = credential; - - expect(typeof name).toBe('string'); - expect(typeof type).toBe('string'); - expect('data' in credential).toBe(false); - - if (sharedWithProjects) expect(Array.isArray(sharedWithProjects)).toBe(true); - - if (homeProject) { - const { id, name, type } = homeProject; - - expect(typeof id).toBe('string'); - expect(typeof name).toBe('string'); - expect(type).toBe('personal'); - } -}