mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
refactor(core): Encapsulate logic to create new credential into its own method (#12361)
Some checks are pending
Test Master / install-and-build (push) Waiting to run
Test Master / Unit tests (18.x) (push) Blocked by required conditions
Test Master / Unit tests (20.x) (push) Blocked by required conditions
Test Master / Unit tests (22.4) (push) Blocked by required conditions
Test Master / Lint (push) Blocked by required conditions
Test Master / Notify Slack on failure (push) Blocked by required conditions
Some checks are pending
Test Master / install-and-build (push) Waiting to run
Test Master / Unit tests (18.x) (push) Blocked by required conditions
Test Master / Unit tests (20.x) (push) Blocked by required conditions
Test Master / Unit tests (22.4) (push) Blocked by required conditions
Test Master / Lint (push) Blocked by required conditions
Test Master / Notify Slack on failure (push) Blocked by required conditions
This commit is contained in:
parent
6891cefa6d
commit
66f8c9e249
19
packages/cli/src/__tests__/project.test-data.ts
Normal file
19
packages/cli/src/__tests__/project.test-data.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { nanoId, date, firstName, lastName, email } from 'minifaker';
|
||||||
|
import 'minifaker/locales/en';
|
||||||
|
|
||||||
|
import type { Project, ProjectType } from '@/databases/entities/project';
|
||||||
|
|
||||||
|
type RawProjectData = Pick<Project, 'name' | 'type' | 'createdAt' | 'updatedAt' | 'id'>;
|
||||||
|
|
||||||
|
const projectName = `${firstName()} ${lastName()} <${email}>`;
|
||||||
|
|
||||||
|
export const createRawProjectData = (payload: Partial<RawProjectData>): Project => {
|
||||||
|
return {
|
||||||
|
createdAt: date(),
|
||||||
|
updatedAt: date(),
|
||||||
|
id: nanoId.nanoid(),
|
||||||
|
name: projectName,
|
||||||
|
type: 'personal' as ProjectType,
|
||||||
|
...payload,
|
||||||
|
} as Project;
|
||||||
|
};
|
|
@ -0,0 +1,82 @@
|
||||||
|
import { mock } from 'jest-mock-extended';
|
||||||
|
|
||||||
|
import { createRawProjectData } from '@/__tests__/project.test-data';
|
||||||
|
import type { SharedCredentialsRepository } from '@/databases/repositories/shared-credentials.repository';
|
||||||
|
import type { EventService } from '@/events/event.service';
|
||||||
|
import type { AuthenticatedRequest } from '@/requests';
|
||||||
|
|
||||||
|
import { createdCredentialsWithScopes, createNewCredentialsPayload } from './credentials.test-data';
|
||||||
|
import { CredentialsController } from '../credentials.controller';
|
||||||
|
import type { CredentialsService } from '../credentials.service';
|
||||||
|
|
||||||
|
describe('CredentialsController', () => {
|
||||||
|
const eventService = mock<EventService>();
|
||||||
|
const credentialsService = mock<CredentialsService>();
|
||||||
|
const sharedCredentialsRepository = mock<SharedCredentialsRepository>();
|
||||||
|
|
||||||
|
const credentialsController = new CredentialsController(
|
||||||
|
mock(),
|
||||||
|
credentialsService,
|
||||||
|
mock(),
|
||||||
|
mock(),
|
||||||
|
mock(),
|
||||||
|
mock(),
|
||||||
|
mock(),
|
||||||
|
sharedCredentialsRepository,
|
||||||
|
mock(),
|
||||||
|
eventService,
|
||||||
|
);
|
||||||
|
|
||||||
|
let req: AuthenticatedRequest;
|
||||||
|
beforeAll(() => {
|
||||||
|
req = { user: { id: '123' } } as AuthenticatedRequest;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createCredentials', () => {
|
||||||
|
it('it should create new credentials and emit "credentials-created"', async () => {
|
||||||
|
// Arrange
|
||||||
|
|
||||||
|
const newCredentialsPayload = createNewCredentialsPayload();
|
||||||
|
|
||||||
|
req.body = newCredentialsPayload;
|
||||||
|
|
||||||
|
const { data, ...payloadWithoutData } = newCredentialsPayload;
|
||||||
|
|
||||||
|
const createdCredentials = createdCredentialsWithScopes(payloadWithoutData);
|
||||||
|
|
||||||
|
const projectOwningCredentialData = createRawProjectData({
|
||||||
|
id: newCredentialsPayload.projectId,
|
||||||
|
});
|
||||||
|
|
||||||
|
credentialsService.createCredential.mockResolvedValue(createdCredentials);
|
||||||
|
|
||||||
|
sharedCredentialsRepository.findCredentialOwningProject.mockResolvedValue(
|
||||||
|
projectOwningCredentialData,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
|
||||||
|
const newApiKey = await credentialsController.createCredentials(req);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
|
||||||
|
expect(credentialsService.createCredential).toHaveBeenCalledWith(
|
||||||
|
newCredentialsPayload,
|
||||||
|
req.user,
|
||||||
|
);
|
||||||
|
expect(sharedCredentialsRepository.findCredentialOwningProject).toHaveBeenCalledWith(
|
||||||
|
createdCredentials.id,
|
||||||
|
);
|
||||||
|
expect(eventService.emit).toHaveBeenCalledWith('credentials-created', {
|
||||||
|
user: expect.objectContaining({ id: req.user.id }),
|
||||||
|
credentialId: createdCredentials.id,
|
||||||
|
credentialType: createdCredentials.type,
|
||||||
|
projectId: projectOwningCredentialData.id,
|
||||||
|
projectType: projectOwningCredentialData.type,
|
||||||
|
publicApi: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(newApiKey).toEqual(createdCredentials);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,10 +1,16 @@
|
||||||
import { mock } from 'jest-mock-extended';
|
import { mock } from 'jest-mock-extended';
|
||||||
|
import { nanoId, date } from 'minifaker';
|
||||||
import { CREDENTIAL_EMPTY_VALUE, type ICredentialType } from 'n8n-workflow';
|
import { CREDENTIAL_EMPTY_VALUE, type ICredentialType } from 'n8n-workflow';
|
||||||
|
|
||||||
import { CREDENTIAL_BLANKING_VALUE } from '@/constants';
|
import { CREDENTIAL_BLANKING_VALUE } from '@/constants';
|
||||||
import type { CredentialTypes } from '@/credential-types';
|
import type { CredentialTypes } from '@/credential-types';
|
||||||
import { CredentialsService } from '@/credentials/credentials.service';
|
import { CredentialsService } from '@/credentials/credentials.service';
|
||||||
import type { CredentialsEntity } from '@/databases/entities/credentials-entity';
|
import type { CredentialsEntity } from '@/databases/entities/credentials-entity';
|
||||||
|
import type { AuthenticatedRequest } from '@/requests';
|
||||||
|
|
||||||
|
import { createNewCredentialsPayload, credentialScopes } from './credentials.test-data';
|
||||||
|
|
||||||
|
let req = { user: { id: '123' } } as AuthenticatedRequest;
|
||||||
|
|
||||||
describe('CredentialsService', () => {
|
describe('CredentialsService', () => {
|
||||||
const credType = mock<ICredentialType>({
|
const credType = mock<ICredentialType>({
|
||||||
|
@ -68,4 +74,67 @@ describe('CredentialsService', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('createCredential', () => {
|
||||||
|
it('it should create new credentials and return with scopes', async () => {
|
||||||
|
// Arrange
|
||||||
|
|
||||||
|
const encryptedData = 'encryptedData';
|
||||||
|
|
||||||
|
const newCredentialPayloadData = createNewCredentialsPayload();
|
||||||
|
|
||||||
|
const newCredential = mock<CredentialsEntity>({
|
||||||
|
name: newCredentialPayloadData.name,
|
||||||
|
data: JSON.stringify(newCredentialPayloadData.data),
|
||||||
|
type: newCredentialPayloadData.type,
|
||||||
|
});
|
||||||
|
|
||||||
|
const encryptedDataResponse = {
|
||||||
|
name: newCredentialPayloadData.name,
|
||||||
|
type: newCredentialPayloadData.type,
|
||||||
|
updatedAt: date(),
|
||||||
|
data: encryptedData,
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveCredentialsResponse = {
|
||||||
|
id: nanoId.nanoid(),
|
||||||
|
name: newCredentialPayloadData.name,
|
||||||
|
type: newCredentialPayloadData.type,
|
||||||
|
updatedAt: encryptedDataResponse.updatedAt,
|
||||||
|
createdAt: date(),
|
||||||
|
data: encryptedDataResponse.data,
|
||||||
|
isManaged: false,
|
||||||
|
shared: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
service.prepareCreateData = jest.fn().mockReturnValue(newCredential);
|
||||||
|
service.createEncryptedData = jest.fn().mockImplementation(() => encryptedDataResponse);
|
||||||
|
service.save = jest.fn().mockResolvedValue(saveCredentialsResponse);
|
||||||
|
service.getCredentialScopes = jest.fn().mockReturnValue(credentialScopes);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
|
||||||
|
const createdCredential = await service.createCredential(newCredentialPayloadData, req.user);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
|
||||||
|
expect(service.prepareCreateData).toHaveBeenCalledWith(newCredentialPayloadData);
|
||||||
|
expect(service.createEncryptedData).toHaveBeenCalledWith(null, newCredential);
|
||||||
|
expect(service.save).toHaveBeenCalledWith(
|
||||||
|
newCredential,
|
||||||
|
encryptedDataResponse,
|
||||||
|
req.user,
|
||||||
|
newCredentialPayloadData.projectId,
|
||||||
|
);
|
||||||
|
expect(service.getCredentialScopes).toHaveBeenCalledWith(
|
||||||
|
req.user,
|
||||||
|
saveCredentialsResponse.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(createdCredential).toEqual({
|
||||||
|
...saveCredentialsResponse,
|
||||||
|
scopes: credentialScopes,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
import type { Scope } from '@n8n/permissions';
|
||||||
|
import { nanoId, date } from 'minifaker';
|
||||||
|
import { randomString } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import type { CredentialRequest } from '@/requests';
|
||||||
|
|
||||||
|
type NewCredentialWithSCopes = {
|
||||||
|
scopes: Scope[];
|
||||||
|
name: string;
|
||||||
|
data: string;
|
||||||
|
type: string;
|
||||||
|
isManaged: boolean;
|
||||||
|
id: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
const name = 'new Credential';
|
||||||
|
const type = 'openAiApi';
|
||||||
|
const data = {
|
||||||
|
apiKey: 'apiKey',
|
||||||
|
url: 'url',
|
||||||
|
};
|
||||||
|
const projectId = nanoId.nanoid();
|
||||||
|
|
||||||
|
export const credentialScopes: Scope[] = [
|
||||||
|
'credential:create',
|
||||||
|
'credential:delete',
|
||||||
|
'credential:list',
|
||||||
|
'credential:move',
|
||||||
|
'credential:read',
|
||||||
|
'credential:share',
|
||||||
|
'credential:update',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const createNewCredentialsPayload = (
|
||||||
|
payload?: Partial<CredentialRequest.CredentialProperties>,
|
||||||
|
): CredentialRequest.CredentialProperties => {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
data,
|
||||||
|
projectId,
|
||||||
|
...payload,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createdCredentialsWithScopes = (
|
||||||
|
payload?: Partial<NewCredentialWithSCopes>,
|
||||||
|
): NewCredentialWithSCopes => {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
data: randomString(20),
|
||||||
|
id: nanoId.nanoid(),
|
||||||
|
createdAt: date(),
|
||||||
|
updatedAt: date(),
|
||||||
|
isManaged: false,
|
||||||
|
scopes: credentialScopes,
|
||||||
|
...payload,
|
||||||
|
};
|
||||||
|
};
|
|
@ -147,32 +147,22 @@ export class CredentialsController {
|
||||||
|
|
||||||
@Post('/')
|
@Post('/')
|
||||||
async createCredentials(req: CredentialRequest.Create) {
|
async createCredentials(req: CredentialRequest.Create) {
|
||||||
const newCredential = await this.credentialsService.prepareCreateData(req.body);
|
const newCredential = await this.credentialsService.createCredential(req.body, req.user);
|
||||||
|
|
||||||
const encryptedData = this.credentialsService.createEncryptedData(null, newCredential);
|
|
||||||
const { shared, ...credential } = await this.credentialsService.save(
|
|
||||||
newCredential,
|
|
||||||
encryptedData,
|
|
||||||
req.user,
|
|
||||||
req.body.projectId,
|
|
||||||
);
|
|
||||||
|
|
||||||
const project = await this.sharedCredentialsRepository.findCredentialOwningProject(
|
const project = await this.sharedCredentialsRepository.findCredentialOwningProject(
|
||||||
credential.id,
|
newCredential.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.eventService.emit('credentials-created', {
|
this.eventService.emit('credentials-created', {
|
||||||
user: req.user,
|
user: req.user,
|
||||||
credentialType: credential.type,
|
credentialType: newCredential.type,
|
||||||
credentialId: credential.id,
|
credentialId: newCredential.id,
|
||||||
publicApi: false,
|
publicApi: false,
|
||||||
projectId: project?.id,
|
projectId: project?.id,
|
||||||
projectType: project?.type,
|
projectType: project?.type,
|
||||||
});
|
});
|
||||||
|
|
||||||
const scopes = await this.credentialsService.getCredentialScopes(req.user, credential.id);
|
return newCredential;
|
||||||
|
|
||||||
return { ...credential, scopes };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch('/:credentialId')
|
@Patch('/:credentialId')
|
||||||
|
|
|
@ -602,4 +602,25 @@ export class CredentialsService {
|
||||||
mergedCredentials.data = decryptedData;
|
mergedCredentials.data = decryptedData;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new credential in user's account and return it along the scopes
|
||||||
|
* If a projectId is send, then it also binds the credential to that specific project
|
||||||
|
*/
|
||||||
|
async createCredential(credentialsData: CredentialRequest.CredentialProperties, user: User) {
|
||||||
|
const newCredential = await this.prepareCreateData(credentialsData);
|
||||||
|
|
||||||
|
const encryptedData = this.createEncryptedData(null, newCredential);
|
||||||
|
|
||||||
|
const { shared, ...credential } = await this.save(
|
||||||
|
newCredential,
|
||||||
|
encryptedData,
|
||||||
|
user,
|
||||||
|
credentialsData.projectId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const scopes = await this.getCredentialScopes(user, credential.id);
|
||||||
|
|
||||||
|
return { ...credential, scopes };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue