import { mock } from 'jest-mock-extended'; import { CREDENTIAL_ERRORS, CredentialDataError, Credentials, type ErrorReporter } from 'n8n-core'; import { CREDENTIAL_EMPTY_VALUE, type ICredentialType } from 'n8n-workflow'; import { CREDENTIAL_BLANKING_VALUE } from '@/constants'; import type { CredentialTypes } from '@/credential-types'; import { CredentialsService } from '@/credentials/credentials.service'; import type { CredentialsEntity } from '@/databases/entities/credentials-entity'; import type { CredentialsRepository } from '@/databases/repositories/credentials.repository'; describe('CredentialsService', () => { const credType = mock({ extends: [], properties: [ { name: 'clientSecret', type: 'string', typeOptions: { password: true }, doNotInherit: false, }, { name: 'accessToken', type: 'string', typeOptions: { password: true }, doNotInherit: false, }, ], }); const errorReporter = mock(); const credentialTypes = mock(); const credentialsRepository = mock(); const service = new CredentialsService( credentialsRepository, mock(), mock(), mock(), errorReporter, mock(), mock(), credentialTypes, mock(), mock(), mock(), mock(), mock(), ); beforeEach(() => jest.resetAllMocks()); describe('redact', () => { it('should redact sensitive values', () => { const credential = mock({ id: '123', name: 'Test Credential', type: 'oauth2', }); const decryptedData = { clientId: 'abc123', clientSecret: 'sensitiveSecret', accessToken: '', oauthTokenData: 'super-secret', csrfSecret: 'super-secret', }; credentialTypes.getByName.calledWith(credential.type).mockReturnValueOnce(credType); const redactedData = service.redact(decryptedData, credential); expect(redactedData).toEqual({ clientId: 'abc123', clientSecret: CREDENTIAL_BLANKING_VALUE, accessToken: CREDENTIAL_EMPTY_VALUE, oauthTokenData: CREDENTIAL_BLANKING_VALUE, csrfSecret: CREDENTIAL_BLANKING_VALUE, }); }); }); describe('decrypt', () => { const data = { clientId: 'abc123', clientSecret: 'sensitiveSecret', accessToken: '', oauthTokenData: 'super-secret', csrfSecret: 'super-secret', }; const credentialEntity = mock({ id: '123', name: 'Test Credential', type: 'oauth2', }); const credentials = mock({ id: credentialEntity.id }); beforeEach(() => { credentialTypes.getByName.calledWith(credentialEntity.type).mockReturnValueOnce(credType); }); it('should redact sensitive values by default', () => { // ARRANGE jest.spyOn(Credentials.prototype, 'getData').mockReturnValueOnce(data); // ACT const redactedData = service.decrypt(credentialEntity); // ASSERT expect(redactedData).toEqual({ ...data, clientSecret: CREDENTIAL_BLANKING_VALUE, accessToken: CREDENTIAL_EMPTY_VALUE, oauthTokenData: CREDENTIAL_BLANKING_VALUE, csrfSecret: CREDENTIAL_BLANKING_VALUE, }); }); it('should return sensitive values if `includeRawData` is true', () => { // ARRANGE jest.spyOn(Credentials.prototype, 'getData').mockReturnValueOnce(data); // ACT const redactedData = service.decrypt(credentialEntity, true); // ASSERT expect(redactedData).toEqual(data); }); it('should return return an empty object if decryption fails', () => { // ARRANGE const decryptionError = new CredentialDataError( credentials, CREDENTIAL_ERRORS.DECRYPTION_FAILED, ); jest.spyOn(Credentials.prototype, 'getData').mockImplementation(() => { throw decryptionError; }); // ACT const redactedData = service.decrypt(credentialEntity, true); // ASSERT expect(redactedData).toEqual({}); expect(credentialTypes.getByName).not.toHaveBeenCalled(); expect(errorReporter.error).toHaveBeenCalledWith(decryptionError, { extra: { credentialId: credentialEntity.id }, level: 'error', tags: { credentialType: credentialEntity.type }, }); }); }); });