merge static with dynamic data. Update tests

This commit is contained in:
Guillaume Jacquart
2025-12-05 14:59:04 +01:00
parent 764886efbb
commit 2f692e9b71
3 changed files with 91 additions and 19 deletions

View File

@@ -53,6 +53,7 @@ describe('DynamicCredentialService', () => {
const createMockResolver = (
shouldSucceed = true,
shouldThrowDataNotFound = false,
customData?: ICredentialDataDecryptedObject,
): jest.Mocked<ICredentialResolver> => ({
metadata: {
name: 'stub-resolver-1.0',
@@ -65,7 +66,7 @@ describe('DynamicCredentialService', () => {
if (!shouldSucceed) {
throw new Error('Resolution failed');
}
return { token: 'dynamic-token', apiKey: 'dynamic-key' };
return customData ?? { token: 'dynamic-token', apiKey: 'dynamic-key' };
}),
setSecret: jest.fn(),
validateOptions: jest.fn(),
@@ -160,7 +161,7 @@ describe('DynamicCredentialService', () => {
const result = await service.resolveIfNeeded(credentialsEntity, staticData, undefined);
expect(result).toBe(staticData);
expect(mockLogger.warn).toHaveBeenCalledWith(
expect(mockLogger.debug).toHaveBeenCalledWith(
'Resolver not found, falling back to static credentials',
expect.objectContaining({
credentialId: 'cred-123',
@@ -181,7 +182,7 @@ describe('DynamicCredentialService', () => {
const result = await service.resolveIfNeeded(credentialsEntity, staticData, undefined);
expect(result).toBe(staticData);
expect(mockLogger.warn).toHaveBeenCalled();
expect(mockLogger.debug).toHaveBeenCalled();
});
it('execution context is missing and fallback is allowed', async () => {
@@ -197,7 +198,7 @@ describe('DynamicCredentialService', () => {
const result = await service.resolveIfNeeded(credentialsEntity, staticData, undefined);
expect(result).toBe(staticData);
expect(mockLogger.warn).toHaveBeenCalledWith(
expect(mockLogger.debug).toHaveBeenCalledWith(
'No execution context available, falling back to static credentials',
expect.objectContaining({
credentialId: 'cred-123',
@@ -252,7 +253,7 @@ describe('DynamicCredentialService', () => {
);
expect(result).toBe(staticData);
expect(mockLogger.warn).toHaveBeenCalledWith(
expect(mockLogger.debug).toHaveBeenCalledWith(
'Dynamic credential resolution failed, falling back to static',
expect.objectContaining({
credentialId: 'cred-123',
@@ -282,7 +283,7 @@ describe('DynamicCredentialService', () => {
);
expect(result).toBe(staticData);
expect(mockLogger.warn).toHaveBeenCalledWith(
expect(mockLogger.debug).toHaveBeenCalledWith(
'Dynamic credential resolution failed, falling back to static',
expect.objectContaining({
isDataNotFound: true,
@@ -382,7 +383,7 @@ describe('DynamicCredentialService', () => {
service.resolveIfNeeded(credentialsEntity, staticData, executionContext),
).rejects.toThrow('Failed to resolve dynamic credentials for "Test Credential"');
expect(mockLogger.error).toHaveBeenCalledWith(
expect(mockLogger.debug).toHaveBeenCalledWith(
'Dynamic credential resolution failed without fallback',
expect.any(Object),
);
@@ -390,26 +391,46 @@ describe('DynamicCredentialService', () => {
});
describe('should successfully resolve dynamic credentials when', () => {
it('all conditions are met and resolver returns data', async () => {
it('all conditions are met and merges static with dynamic data', async () => {
const credentialsEntity = createMockCredentialsEntity();
const resolverEntity = createMockResolverEntity();
const mockResolver = createMockResolver();
const executionContext = createMockExecutionContext('encrypted-credentials');
const credentialContext = createMockCredentialContext();
// Static data includes OAuth client config and old token
const staticOAuthData: ICredentialDataDecryptedObject = {
clientId: 'static-client-id',
clientSecret: 'static-client-secret',
token: 'static-token', // Will be overridden
apiKey: 'static-key', // Will be overridden
};
// Dynamic data includes new tokens (overrides token) and new fields
const dynamicData: ICredentialDataDecryptedObject = {
token: 'dynamic-token',
apiKey: 'dynamic-key',
refreshToken: 'dynamic-refresh-token',
};
const mockResolver = createMockResolver(true, false, dynamicData);
mockResolverRepository.findOneBy.mockResolvedValue(resolverEntity);
mockResolverRegistry.getResolverByName.mockReturnValue(mockResolver);
mockCipher.decrypt.mockReturnValue(JSON.stringify(credentialContext));
const result = await service.resolveIfNeeded(
credentialsEntity,
staticData,
staticOAuthData,
executionContext,
);
// Verify merge: static fields preserved, dynamic fields added/overridden
expect(result).toEqual({
token: 'dynamic-token',
apiKey: 'dynamic-key',
clientId: 'static-client-id', // From static (preserved)
clientSecret: 'static-client-secret', // From static (preserved)
token: 'dynamic-token', // From dynamic (overridden)
apiKey: 'dynamic-key', // From dynamic (overridden)
refreshToken: 'dynamic-refresh-token', // From dynamic (new field)
});
expect(mockResolver.getSecret).toHaveBeenCalledWith('cred-123', credentialContext, {
prefix: 'test',
@@ -525,7 +546,7 @@ describe('DynamicCredentialService', () => {
);
expect(result).toBe(staticData);
expect(mockLogger.warn).toHaveBeenCalled();
expect(mockLogger.debug).toHaveBeenCalled();
});
it('empty resolver config', async () => {
@@ -644,7 +665,7 @@ describe('DynamicCredentialService', () => {
);
expect(result).toBe(staticData);
expect(mockLogger.warn).toHaveBeenCalledWith(
expect(mockLogger.debug).toHaveBeenCalledWith(
'Resolver not found, falling back to static credentials',
expect.objectContaining({
resolverId: 'workflow-resolver-789',

View File

@@ -94,7 +94,8 @@ export class DynamicCredentialService implements ICredentialResolutionProvider {
identity: credentialContext.identity,
});
return dynamicData;
// Adds and override static data with dynamically resolved data
return { ...staticData, ...dynamicData };
} catch (error) {
return this.handleResolutionError(credentialsEntity, staticData, error, resolverId);
}
@@ -132,7 +133,7 @@ export class DynamicCredentialService implements ICredentialResolutionProvider {
const isDataNotFound = error instanceof CredentialResolverDataNotFoundError;
if (credentialsEntity.resolvableAllowFallback) {
this.logger.warn('Dynamic credential resolution failed, falling back to static', {
this.logger.debug('Dynamic credential resolution failed, falling back to static', {
credentialId: credentialsEntity.id,
credentialName: credentialsEntity.name,
resolverId,
@@ -143,7 +144,7 @@ export class DynamicCredentialService implements ICredentialResolutionProvider {
return staticData;
}
this.logger.error('Dynamic credential resolution failed without fallback', {
this.logger.debug('Dynamic credential resolution failed without fallback', {
credentialId: credentialsEntity.id,
credentialName: credentialsEntity.name,
resolverId,
@@ -166,7 +167,7 @@ export class DynamicCredentialService implements ICredentialResolutionProvider {
resolverId: string,
): ICredentialDataDecryptedObject {
if (credentialsEntity.resolvableAllowFallback) {
this.logger.warn('Resolver not found, falling back to static credentials', {
this.logger.debug('Resolver not found, falling back to static credentials', {
credentialId: credentialsEntity.id,
credentialName: credentialsEntity.name,
resolverId,
@@ -188,7 +189,7 @@ export class DynamicCredentialService implements ICredentialResolutionProvider {
staticData: ICredentialDataDecryptedObject,
): ICredentialDataDecryptedObject {
if (credentialsEntity.resolvableAllowFallback) {
this.logger.warn('No execution context available, falling back to static credentials', {
this.logger.debug('No execution context available, falling back to static credentials', {
credentialId: credentialsEntity.id,
credentialName: credentialsEntity.name,
});

View File

@@ -195,6 +195,56 @@ describe('NodeExecutionContext', () => {
});
});
describe('_getCredentials', () => {
it('should set executionContext on additionalData before retrieving credentials', async () => {
const credentialDetails = { id: 'cred123', name: 'Test Credential' };
const testNode = mock<INode>({
type: 'n8n-nodes-base.httpRequest',
});
testNode.credentials = { testCredential: credentialDetails };
const runtimeData = {
version: 1 as const,
establishedAt: Date.now(),
source: 'manual' as const,
};
const testRunExecutionData = createRunExecutionData({
resultData: { runData: {} },
executionData: { runtimeData },
});
const mockCredentialsHelper = {
getDecrypted: jest.fn().mockResolvedValue({ token: 'test-token' }),
getCredentialsProperties: jest.fn(),
};
const mockAdditionalData = mock<IWorkflowExecuteAdditionalData>({
credentialsHelper: mockCredentialsHelper,
});
const contextWithCredentials = new TestContext(
workflow,
testNode,
mockAdditionalData,
mode,
testRunExecutionData,
);
await contextWithCredentials['_getCredentials']('testCredential');
expect(mockAdditionalData.executionContext).toEqual(runtimeData);
expect(mockCredentialsHelper.getDecrypted).toHaveBeenCalledWith(
mockAdditionalData,
credentialDetails,
'testCredential',
mode,
undefined,
false,
undefined,
);
});
});
describe('prepareOutputData', () => {
it('should return the input array wrapped in another array', async () => {
const outputData = [mock<INodeExecutionData>(), mock<INodeExecutionData>()];