feat(core): Add tests for createImportStubCredential method in CredentialsService

This commit is contained in:
Sandra Zollner
2026-06-19 08:29:12 +02:00
parent 8e4b504489
commit f6806408fe
3 changed files with 94 additions and 6 deletions
@@ -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(),
];
@@ -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 */