mirror of
https://github.com/n8n-io/n8n.git
synced 2026-06-04 18:49:20 +02:00
fix(ai-builder): Tolerate id-less credential refs in eval mock execution (no-changelog) (#31428)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4188a7c4ed
commit
b20d740bd2
|
|
@ -415,11 +415,7 @@ describe('EvalMockedCredentialsHelper', () => {
|
|||
});
|
||||
|
||||
describe('getDecrypted — schema synthesis when id is null', () => {
|
||||
// Core's eval-mode bypass passes `{ id: null, name: type }` when a node
|
||||
// has no credentials configured at all. The inner helper throws
|
||||
// CredentialNotFoundError on a null id; the catch below schema-synthesizes
|
||||
// (and applies the URL rewrite) so vendor SDK traffic stays inside the
|
||||
// wire server instead of escaping to the real provider with 401.
|
||||
// `{ id: null }` short-circuits to schema synthesis without delegating to the inner helper.
|
||||
const propsSchema = [
|
||||
{
|
||||
name: 'apiKey',
|
||||
|
|
@ -441,7 +437,7 @@ describe('EvalMockedCredentialsHelper', () => {
|
|||
function makeSynthesizingInner(): ICredentialsHelper {
|
||||
return makeInner({
|
||||
getCredentialsProperties: jest.fn().mockReturnValue(propsSchema),
|
||||
// Inner throws on a null-id lookup → catch fires → schema synthesis.
|
||||
// Not reached for a null id (short-circuits first); left rejecting so a regression fails loudly.
|
||||
getDecrypted: jest.fn().mockRejectedValue(new CredentialNotFoundError('null', 'openAiApi')),
|
||||
});
|
||||
}
|
||||
|
|
@ -569,6 +565,65 @@ describe('EvalMockedCredentialsHelper', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('no-id credential references — regression for "Found credential with no ID."', () => {
|
||||
// Id-less refs (builder "set up later" placeholders, core's `{ id: null }`) make the real
|
||||
// inner throw UnexpectedError, not CredentialNotFoundError — so the helper must synthesize, not delegate.
|
||||
const propsSchema = [
|
||||
{
|
||||
name: 'apiKey',
|
||||
displayName: 'API Key',
|
||||
type: 'string' as const,
|
||||
default: '',
|
||||
typeOptions: { password: true },
|
||||
},
|
||||
];
|
||||
|
||||
it.each([
|
||||
['null id (fully-unconfigured bypass)', { id: null, name: 'telegramApi' }],
|
||||
['empty-string id (placeholder)', { id: '', name: 'Telegram cred' }],
|
||||
['missing id (placeholder)', { name: 'Telegram cred' }],
|
||||
])('synthesizes without delegating to inner — %s', async (_label, creds) => {
|
||||
const inner = makeInner({
|
||||
getCredentialsProperties: jest.fn().mockReturnValue(propsSchema),
|
||||
// Stands in for core's UnexpectedError on a falsy id — fails loudly if the short-circuit regresses.
|
||||
getDecrypted: jest.fn().mockRejectedValue(new Error('Found credential with no ID.')),
|
||||
});
|
||||
const helper = new EvalMockedCredentialsHelper(inner);
|
||||
|
||||
const result = await helper.getDecrypted(
|
||||
fakeAdditionalData,
|
||||
creds as INodeCredentialsDetails,
|
||||
'telegramApi',
|
||||
'manual',
|
||||
{ node: fakeNode } as IExecuteData,
|
||||
);
|
||||
|
||||
expect(result.__evalMockedCredential).toBe(true);
|
||||
expect(inner.getDecrypted).not.toHaveBeenCalled();
|
||||
expect(helper.mockedCredentials).toEqual([
|
||||
{ nodeName: 'Telegram', credentialType: 'telegramApi', credentialId: undefined },
|
||||
]);
|
||||
});
|
||||
|
||||
it('still delegates (and surfaces the throw) when an id IS present', async () => {
|
||||
// A present id whose lookup fails with a non-CredentialNotFoundError must still propagate.
|
||||
const inner = makeInner({
|
||||
getDecrypted: jest.fn().mockRejectedValue(new Error('database is down')),
|
||||
});
|
||||
const helper = new EvalMockedCredentialsHelper(inner);
|
||||
|
||||
await expect(
|
||||
helper.getDecrypted(
|
||||
fakeAdditionalData,
|
||||
{ id: 'real-id', name: 'Telegram cred' },
|
||||
'telegramApi',
|
||||
'manual',
|
||||
),
|
||||
).rejects.toThrow('database is down');
|
||||
expect(inner.getDecrypted).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('authenticate', () => {
|
||||
it('passes the request through unchanged for marker payloads', async () => {
|
||||
const inner = makeInner();
|
||||
|
|
|
|||
|
|
@ -104,6 +104,21 @@ export class EvalMockedCredentialsHelper extends ICredentialsHelper {
|
|||
raw?: boolean,
|
||||
expressionResolveValues?: ICredentialsExpressionResolveValues,
|
||||
): Promise<ICredentialDataDecryptedObject> {
|
||||
// Id-less refs make the inner helper throw UnexpectedError (not CredentialNotFoundError),
|
||||
// which the catch below won't handle — synthesize a mock here instead of delegating.
|
||||
if (!nodeCredentials.id) {
|
||||
this.mockedCredentials.push({
|
||||
nodeName: executeData?.node?.name ?? 'unknown',
|
||||
credentialType: type,
|
||||
credentialId: undefined,
|
||||
});
|
||||
const synthesized = {
|
||||
...buildEvalMockCredentials(this.inner.getCredentialsProperties(type)),
|
||||
[MOCK_MARKER]: true,
|
||||
} as ICredentialDataDecryptedObject;
|
||||
return this.applyServerUrlRewrite(synthesized, type, nodeCredentials, executeData);
|
||||
}
|
||||
|
||||
let credentials: ICredentialDataDecryptedObject;
|
||||
try {
|
||||
credentials = await this.inner.getDecrypted(
|
||||
|
|
@ -118,28 +133,13 @@ export class EvalMockedCredentialsHelper extends ICredentialsHelper {
|
|||
} catch (error) {
|
||||
if (!(error instanceof CredentialNotFoundError)) throw error;
|
||||
|
||||
// id present but absent from the DB — a bare marker stub is enough; URL rewrite still runs below.
|
||||
this.mockedCredentials.push({
|
||||
nodeName: executeData?.node?.name ?? 'unknown',
|
||||
credentialType: type,
|
||||
credentialId: nodeCredentials.id ?? undefined,
|
||||
});
|
||||
|
||||
// When called with no credential id (eval-mode bypass for nodes
|
||||
// with no credentials of any type configured), schema-synthesize
|
||||
// so the wire-server URL rewrite below has a real `url` field to
|
||||
// augment. Otherwise vendor SDK traffic would escape to the real
|
||||
// provider with placeholder values and 401 at the wire layer.
|
||||
// `buildEvalMockCredentials` is typed `Record<string, unknown>` —
|
||||
// schema defaults can be richer than `CredentialInformation`, but
|
||||
// at runtime emits only JSON-shaped values, which is what the
|
||||
// rewrite path consumes.
|
||||
credentials =
|
||||
nodeCredentials.id === null
|
||||
? ({
|
||||
...buildEvalMockCredentials(this.inner.getCredentialsProperties(type)),
|
||||
[MOCK_MARKER]: true,
|
||||
} as ICredentialDataDecryptedObject)
|
||||
: { [MOCK_MARKER]: true };
|
||||
credentials = { [MOCK_MARKER]: true };
|
||||
}
|
||||
|
||||
return this.applyServerUrlRewrite(credentials, type, nodeCredentials, executeData);
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user