mirror of
https://github.com/n8n-io/n8n.git
synced 2026-06-19 07:36:52 +00:00
feat(core): Add tests for createImportStubCredential method in CredentialsService
This commit is contained in:
@@ -2325,6 +2325,92 @@ describe('CredentialsService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('createImportStubCredential', () => {
|
||||
const stubOpts = {
|
||||
name: 'Missing GitHub',
|
||||
type: 'githubApi',
|
||||
projectId: 'project-1',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
credentialsRepository.create.mockImplementation((data) => ({ ...data }) as CredentialsEntity);
|
||||
sharedCredentialsRepository.create.mockImplementation((data) => data as SharedCredentials);
|
||||
externalHooks.run.mockResolvedValue();
|
||||
projectService.getProjectWithScope.mockResolvedValue({ id: 'project-1' } as never);
|
||||
});
|
||||
|
||||
it('creates an empty stub credential without field validation', async () => {
|
||||
credentialsHelper.getCredentialsProperties.mockReturnValue([
|
||||
{
|
||||
displayName: 'Access Token',
|
||||
name: 'accessToken',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
displayOptions: {},
|
||||
},
|
||||
] as never);
|
||||
const checkCredentialDataSpy = jest.spyOn(service, 'checkCredentialData');
|
||||
|
||||
let credentialEntityInput: unknown;
|
||||
const savedEntities: unknown[] = [];
|
||||
credentialsRepository.create.mockImplementation((data) => {
|
||||
credentialEntityInput = data;
|
||||
return data as CredentialsEntity;
|
||||
});
|
||||
mockTransactionManager({
|
||||
credentialId: 'stub-cred-id',
|
||||
onSave: (entity) => {
|
||||
savedEntities.push(entity);
|
||||
},
|
||||
});
|
||||
|
||||
const result = await service.createImportStubCredential(stubOpts, ownerUser);
|
||||
|
||||
expect(checkCredentialDataSpy).not.toHaveBeenCalled();
|
||||
expect(credentialsHelper.getCredentialsProperties).not.toHaveBeenCalled();
|
||||
expect(credentialEntityInput).toMatchObject({
|
||||
name: 'Missing GitHub',
|
||||
type: 'githubApi',
|
||||
isManaged: false,
|
||||
isResolvable: false,
|
||||
});
|
||||
expect(savedEntities[0]).toMatchObject({
|
||||
isManaged: false,
|
||||
isResolvable: false,
|
||||
});
|
||||
expect(projectService.getProjectWithScope).toHaveBeenCalledWith(
|
||||
ownerUser,
|
||||
'project-1',
|
||||
['credential:create'],
|
||||
expect.anything(),
|
||||
);
|
||||
expect(result).toMatchObject({
|
||||
id: 'stub-cred-id',
|
||||
name: 'Missing GitHub',
|
||||
type: 'githubApi',
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects when user lacks credential:create on the target project', async () => {
|
||||
projectService.getProjectWithScope.mockResolvedValue(null);
|
||||
// @ts-expect-error - Mocking manager for testing
|
||||
credentialsRepository.manager = {
|
||||
transaction: jest.fn().mockImplementation(async (callback) => {
|
||||
const mockManager = {
|
||||
existsBy: jest.fn().mockResolvedValue(true),
|
||||
save: jest.fn(),
|
||||
};
|
||||
return await callback(mockManager);
|
||||
}),
|
||||
};
|
||||
|
||||
await expect(service.createImportStubCredential(stubOpts, memberUser)).rejects.toThrow(
|
||||
"You don't have the permissions to save the credential in this project.",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createManagedCredential', () => {
|
||||
const credentialData = {
|
||||
name: 'Managed Credential',
|
||||
|
||||
@@ -4,7 +4,7 @@ import { UnexpectedError } from 'n8n-workflow';
|
||||
import { CredentialsService } from '@/credentials/credentials.service';
|
||||
|
||||
import { CredentialMatcherFactory } from './credential-matcher-factory';
|
||||
import { credentialBlockingFailures } from './credential-missing-mode';
|
||||
import { credentialBlockingFailures, canStubNotFoundFailure } from './credential-missing-mode';
|
||||
import type {
|
||||
CredentialApplyResult,
|
||||
CredentialBindingRequest,
|
||||
@@ -92,14 +92,14 @@ export class CredentialImporter {
|
||||
}
|
||||
}
|
||||
|
||||
/** First `not_found` failure per source id that has no explicit binding target. */
|
||||
/** First stubbable `not_found` failure per source id. */
|
||||
function stubbableCredentialFailures(
|
||||
failures: CredentialResolutionFailure[],
|
||||
): CredentialResolutionFailure[] {
|
||||
return [
|
||||
...new Map(
|
||||
failures
|
||||
.filter((failure) => failure.kind === 'not_found' && failure.targetId === undefined)
|
||||
.filter((failure) => canStubNotFoundFailure(failure))
|
||||
.map((failure) => [failure.sourceId, failure] as const),
|
||||
).values(),
|
||||
];
|
||||
|
||||
+5
-3
@@ -2,6 +2,10 @@ import type { CredentialResolution, CredentialResolutionFailure } from './creden
|
||||
import type { CredentialMissingMode } from '../../n8n-packages.types';
|
||||
import type { PackageCredentialRequirement } from '../../spec/requirements.schema';
|
||||
|
||||
export function canStubNotFoundFailure(failure: CredentialResolutionFailure): boolean {
|
||||
return failure.kind === 'not_found' && failure.targetId === undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Classifies which unresolved credential references block the import, per missing-mode
|
||||
* policy. Read-only — never writes.
|
||||
@@ -13,9 +17,7 @@ const BLOCKING_FAILURES: Record<
|
||||
> = {
|
||||
'must-preexist': (resolution) => resolution.failures,
|
||||
'create-stub': (resolution) =>
|
||||
resolution.failures.filter(
|
||||
(failure) => failure.kind !== 'not_found' || failure.targetId !== undefined,
|
||||
),
|
||||
resolution.failures.filter((failure) => !canStubNotFoundFailure(failure)),
|
||||
};
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
|
||||
|
||||
Reference in New Issue
Block a user