feat(editor): Add option to disable credentials check in RLC (#17381)

This commit is contained in:
RomanDavydchuk
2025-07-17 12:02:34 +03:00
committed by GitHub
parent 436ec864d8
commit d466d9d373
4 changed files with 145 additions and 5 deletions

View File

@@ -103,3 +103,34 @@ export const TEST_NODE_SINGLE_MODE: INode = {
options: {},
},
};
export const TEST_PARAMETER_SKIP_CREDENTIALS_CHECK: INodeProperties = {
...TEST_PARAMETER_MULTI_MODE,
name: 'testParameterSkipCredentialsCheck',
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
typeOptions: {
searchListMethod: 'testSearch',
searchable: true,
skipCredentialsCheckInRLC: true,
},
},
],
};
export const TEST_NODE_NO_CREDENTIALS: INode = {
...TEST_NODE_MULTI_MODE,
name: 'Test Node - No Credentials',
parameters: {
authentication: undefined,
resource: 'test',
operation: 'get',
testParameterSkipCredentialsCheck: TEST_MODEL_VALUE,
id: '',
options: {},
},
credentials: undefined,
};

View File

@@ -9,9 +9,11 @@ import {
TEST_MODEL_VALUE,
TEST_NODE_MULTI_MODE,
TEST_NODE_SINGLE_MODE,
TEST_NODE_NO_CREDENTIALS,
TEST_PARAMETER_ADD_RESOURCE,
TEST_PARAMETER_MULTI_MODE,
TEST_PARAMETER_SINGLE_MODE,
TEST_PARAMETER_SKIP_CREDENTIALS_CHECK,
} from './ResourceLocator.test.constants';
vi.mock('vue-router', async () => {
@@ -197,6 +199,101 @@ describe('ResourceLocator', () => {
});
});
it('renders error when credentials are required and skipCredentialsCheckInRLC is false', async () => {
nodeTypesStore.getResourceLocatorResults.mockResolvedValue({
results: [
{
name: 'Test Resource',
value: 'test-resource',
url: 'https://test.com/test-resource',
},
],
paginationToken: null,
});
nodeTypesStore.getNodeType = vi.fn().mockReturnValue({
displayName: 'Test Node',
credentials: [
{
name: 'testAuth',
required: true,
},
],
});
const { getByTestId, queryByTestId } = renderComponent({
props: {
modelValue: TEST_MODEL_VALUE,
parameter: TEST_PARAMETER_MULTI_MODE,
path: `parameters.${TEST_PARAMETER_MULTI_MODE.name}`,
node: TEST_NODE_NO_CREDENTIALS,
displayTitle: 'Test Resource Locator',
expressionComputedValue: '',
isValueExpression: false,
},
});
expect(getByTestId(`resource-locator-${TEST_PARAMETER_MULTI_MODE.name}`)).toBeInTheDocument();
await userEvent.click(getByTestId('rlc-input'));
expect(getByTestId('rlc-error-container')).toBeInTheDocument();
expect(getByTestId('permission-error-link')).toBeInTheDocument();
expect(queryByTestId('rlc-item')).not.toBeInTheDocument();
});
it('renders list items when skipCredentialsCheckInRLC is true even without credentials', async () => {
const TEST_ITEMS = [
{ name: 'Test Resource', value: 'test-resource', url: 'https://test.com/test-resource' },
{
name: 'Test Resource 2',
value: 'test-resource-2',
url: 'https://test.com/test-resource-2',
},
];
nodeTypesStore.getResourceLocatorResults.mockResolvedValue({
results: TEST_ITEMS,
paginationToken: null,
});
nodeTypesStore.getNodeType = vi.fn().mockReturnValue({
displayName: 'Test Node',
credentials: [
{
name: 'testAuth',
required: true,
},
],
});
const { getByTestId, getByText, getAllByTestId, queryByTestId } = renderComponent({
props: {
modelValue: TEST_MODEL_VALUE,
parameter: TEST_PARAMETER_SKIP_CREDENTIALS_CHECK,
path: `parameters.${TEST_PARAMETER_SKIP_CREDENTIALS_CHECK.name}`,
node: TEST_NODE_NO_CREDENTIALS,
displayTitle: 'Test Resource Locator',
expressionComputedValue: '',
isValueExpression: false,
},
});
expect(
getByTestId(`resource-locator-${TEST_PARAMETER_SKIP_CREDENTIALS_CHECK.name}`),
).toBeInTheDocument();
await userEvent.click(getByTestId('rlc-input'));
await waitFor(() => {
expect(nodeTypesStore.getResourceLocatorResults).toHaveBeenCalled();
});
expect(getAllByTestId('rlc-item')).toHaveLength(TEST_ITEMS.length);
TEST_ITEMS.forEach((item) => {
expect(getByText(item.name)).toBeInTheDocument();
});
expect(queryByTestId('rlc-error-container')).not.toBeInTheDocument();
expect(queryByTestId('permission-error-link')).not.toBeInTheDocument();
});
// Testing error message deduplication
describe('ResourceLocator credentials error handling', () => {
it.each([

View File

@@ -186,8 +186,9 @@ const hasCredentialError = computed(() => {
);
});
const credentialsNotSet = computed(() => {
const credentialsRequiredAndNotSet = computed(() => {
if (!props.node) return false;
if (skipCredentialsCheckInRLC.value) return false;
const nodeType = nodeTypesStore.getNodeType(props.node.type);
if (nodeType) {
const usesCredentials = nodeType.credentials !== undefined && nodeType.credentials.length > 0;
@@ -315,6 +316,10 @@ const currentQueryError = computed(() => {
const isSearchable = computed(() => !!getPropertyArgument(currentMode.value, 'searchable'));
const skipCredentialsCheckInRLC = computed(
() => !!getPropertyArgument(currentMode.value, 'skipCredentialsCheckInRLC'),
);
const requiresSearchFilter = computed(
() => !!getPropertyArgument(currentMode.value, 'searchFilterRequired'),
);
@@ -666,7 +671,7 @@ async function loadResources() {
const paramsKey = currentRequestKey.value;
const cachedResponse = cachedResponses.value[paramsKey];
if (credentialsNotSet.value) {
if (credentialsRequiredAndNotSet.value) {
setResponse(paramsKey, { error: true });
return;
}
@@ -922,7 +927,7 @@ function removeOverride() {
<template #error>
<div :class="$style.errorContainer" data-test-id="rlc-error-container">
<n8n-text
v-if="credentialsNotSet || currentResponse.errorDetails"
v-if="credentialsRequiredAndNotSet || currentResponse.errorDetails"
color="text-dark"
align="center"
tag="div"
@@ -946,9 +951,12 @@ function removeOverride() {
{{ currentResponse.errorDetails.description }}
</N8nNotice>
</div>
<div v-if="hasCredentialError || credentialsNotSet" data-test-id="permission-error-link">
<div
v-if="hasCredentialError || credentialsRequiredAndNotSet"
data-test-id="permission-error-link"
>
<a
v-if="credentialsNotSet"
v-if="credentialsRequiredAndNotSet"
:class="$style['credential-link']"
@click="createNewCredential"
>