mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-27 23:07:12 +02:00
256 lines
7.5 KiB
TypeScript
256 lines
7.5 KiB
TypeScript
import { LicenseState } from '@n8n/backend-common';
|
|
import { mockInstance, getPersonalProject, testDb } from '@n8n/backend-test-utils';
|
|
import type { CredentialsEntity, User } from '@n8n/db';
|
|
import {
|
|
GLOBAL_OWNER_ROLE,
|
|
WorkflowRepository,
|
|
SharedWorkflowRepository,
|
|
WorkflowEntity,
|
|
} from '@n8n/db';
|
|
import { Container } from '@n8n/di';
|
|
import { mock } from 'jest-mock-extended';
|
|
import nock from 'nock';
|
|
import { v4 as uuid } from 'uuid';
|
|
import type { INode } from 'n8n-workflow';
|
|
|
|
import * as utils from '../shared/utils';
|
|
import { DynamicCredentialResolverService } from '@/modules/dynamic-credentials.ee/services/credential-resolver.service';
|
|
import { Telemetry } from '@/telemetry';
|
|
import { createCredentials } from '../shared/db/credentials';
|
|
import { DynamicCredentialsConfig } from '@/modules/dynamic-credentials.ee/dynamic-credentials.config';
|
|
|
|
import { createUser } from '../shared/db/users';
|
|
|
|
mockInstance(Telemetry);
|
|
|
|
const licenseMock = mock<LicenseState>();
|
|
licenseMock.isLicensed.mockReturnValue(true);
|
|
Container.set(LicenseState, licenseMock);
|
|
|
|
process.env.N8N_ENV_FEAT_DYNAMIC_CREDENTIALS = 'true';
|
|
|
|
const testServer = utils.setupTestServer({
|
|
endpointGroups: ['credentials'],
|
|
enabledFeatures: ['feat:externalSecrets'],
|
|
modules: ['dynamic-credentials'],
|
|
});
|
|
|
|
mockInstance(DynamicCredentialsConfig, {
|
|
corsOrigin: 'https://app.example.com',
|
|
corsAllowCredentials: false,
|
|
endpointAuthToken: 'static-test-token',
|
|
});
|
|
|
|
const setupWorkflow = async () => {
|
|
const owner = await createUser({ role: GLOBAL_OWNER_ROLE });
|
|
const resolverService = Container.get(DynamicCredentialResolverService);
|
|
|
|
const resolver = await resolverService.create({
|
|
name: 'Test Resolver',
|
|
type: 'credential-resolver.oauth2-1.0',
|
|
config: {
|
|
metadataUri: 'https://auth.example.com/.well-known/openid-configuration',
|
|
clientId: 'test-client-id',
|
|
clientSecret: 'test-client-secret',
|
|
validation: 'oauth2-introspection',
|
|
},
|
|
user: owner,
|
|
});
|
|
|
|
const personalProject = await getPersonalProject(owner);
|
|
|
|
const savedCredential = await createCredentials(
|
|
{
|
|
name: 'Test Dynamic Credential',
|
|
type: 'OAuth2',
|
|
data: '',
|
|
isResolvable: true,
|
|
resolverId: resolver.id,
|
|
},
|
|
personalProject,
|
|
);
|
|
|
|
const node: INode = {
|
|
id: uuid(),
|
|
name: 'Test Node',
|
|
type: 'n8n-nodes-base.httpRequest',
|
|
typeVersion: 1,
|
|
position: [0, 0],
|
|
parameters: {},
|
|
credentials: {
|
|
oAuth2Api: {
|
|
id: savedCredential.id,
|
|
name: savedCredential.name,
|
|
},
|
|
},
|
|
};
|
|
|
|
const workflow = new WorkflowEntity();
|
|
workflow.name = 'Test Workflow';
|
|
workflow.nodes = [node];
|
|
workflow.active = true;
|
|
workflow.versionId = uuid();
|
|
workflow.connections = {};
|
|
|
|
const workflowRepository = Container.get(WorkflowRepository);
|
|
const savedWorkflow = await workflowRepository.save(workflow);
|
|
|
|
await Container.get(SharedWorkflowRepository).save({
|
|
workflow: savedWorkflow,
|
|
user: owner,
|
|
project: personalProject,
|
|
role: 'workflow:owner',
|
|
});
|
|
|
|
return { savedWorkflow, savedCredential, owner };
|
|
};
|
|
|
|
describe('Workflow Status API', () => {
|
|
let savedWorkflow: WorkflowEntity;
|
|
let savedCredential: CredentialsEntity;
|
|
let owner: User;
|
|
|
|
beforeAll(async () => {
|
|
// Mock OAuth metadata endpoint for resolver validation
|
|
nock.cleanAll();
|
|
nock('https://auth.example.com')
|
|
.persist()
|
|
.get('/.well-known/openid-configuration')
|
|
.reply(200, {
|
|
issuer: 'https://auth.example.com',
|
|
introspection_endpoint: 'https://auth.example.com/oauth/introspect',
|
|
introspection_endpoint_auth_methods_supported: [
|
|
'client_secret_basic',
|
|
'client_secret_post',
|
|
],
|
|
});
|
|
|
|
// Mock OAuth introspection endpoint for identity validation
|
|
nock('https://auth.example.com')
|
|
.persist()
|
|
.post('/oauth/introspect')
|
|
.reply(200, {
|
|
active: true,
|
|
sub: 'user-123',
|
|
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
});
|
|
|
|
await testDb.truncate([
|
|
'User',
|
|
'SharedWorkflow',
|
|
'WorkflowEntity',
|
|
'CredentialsEntity',
|
|
'DynamicCredentialResolver',
|
|
]);
|
|
|
|
({ savedWorkflow, savedCredential, owner } = await setupWorkflow());
|
|
});
|
|
|
|
afterAll(async () => {
|
|
nock.cleanAll();
|
|
await testDb.terminate();
|
|
testServer.httpServer.close();
|
|
});
|
|
|
|
describe('GET /workflows/:workflowId/execution-status', () => {
|
|
describe('when a static auth token is provided', () => {
|
|
it('should return the execution status of a workflow', async () => {
|
|
const response = await testServer.authlessAgent
|
|
.get(`/workflows/${savedWorkflow.id}/execution-status`)
|
|
.set('Authorization', 'Bearer test-token')
|
|
.set('X-Authorization', 'Bearer static-test-token')
|
|
.expect(200);
|
|
|
|
expect(response.body.data).toMatchObject({
|
|
workflowId: savedWorkflow.id,
|
|
readyToExecute: expect.any(Boolean),
|
|
credentials: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
credentialId: savedCredential.id,
|
|
credentialName: savedCredential.name,
|
|
credentialType: savedCredential.type,
|
|
credentialStatus: expect.any(String),
|
|
}),
|
|
]),
|
|
});
|
|
});
|
|
|
|
it('should return 401 if the static auth token is invalid', async () => {
|
|
await testServer.authlessAgent
|
|
.get(`/workflows/${savedWorkflow.id}/execution-status`)
|
|
.set('Authorization', 'Bearer test-token')
|
|
.set('X-Authorization', 'Bearer invalid-token')
|
|
.expect(401);
|
|
});
|
|
|
|
it('should return 401 if the static auth token is missing', async () => {
|
|
await testServer.authlessAgent
|
|
.get(`/workflows/${savedWorkflow.id}/execution-status`)
|
|
.set('Authorization', 'Bearer test-token')
|
|
.expect(401);
|
|
});
|
|
|
|
it('should return 401 if the static auth token is empty', async () => {
|
|
await testServer.authlessAgent
|
|
.get(`/workflows/${savedWorkflow.id}/execution-status`)
|
|
.set('Authorization', 'Bearer test-token')
|
|
.set('X-Authorization', 'Bearer ')
|
|
.expect(401);
|
|
});
|
|
});
|
|
|
|
it('should return 401 if the authorization header is missing', async () => {
|
|
await testServer.authlessAgent
|
|
.get(`/workflows/${savedWorkflow.id}/execution-status`)
|
|
.set('X-Authorization', 'Bearer static-test-token')
|
|
.expect(401);
|
|
});
|
|
|
|
describe('when a user is authenticated via cookie', () => {
|
|
it('should allow access without static auth token', async () => {
|
|
const response = await testServer
|
|
.authAgentFor(owner)
|
|
.get(`/workflows/${savedWorkflow.id}/execution-status`)
|
|
.set('Authorization', 'Bearer test-token')
|
|
// Note: NO X-Authorization header provided
|
|
.expect(200);
|
|
|
|
expect(response.body.data).toMatchObject({
|
|
workflowId: savedWorkflow.id,
|
|
readyToExecute: expect.any(Boolean),
|
|
credentials: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
credentialId: savedCredential.id,
|
|
credentialName: savedCredential.name,
|
|
credentialType: savedCredential.type,
|
|
credentialStatus: expect.any(String),
|
|
}),
|
|
]),
|
|
});
|
|
});
|
|
|
|
it('should allow access even with invalid static token if cookie auth succeeds', async () => {
|
|
const response = await testServer
|
|
.authAgentFor(owner)
|
|
.get(`/workflows/${savedWorkflow.id}/execution-status`)
|
|
.set('Authorization', 'Bearer test-token')
|
|
.set('X-Authorization', 'Bearer invalid-static-token') // Invalid token
|
|
.expect(200);
|
|
|
|
expect(response.body.data).toMatchObject({
|
|
workflowId: savedWorkflow.id,
|
|
readyToExecute: expect.any(Boolean),
|
|
credentials: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
credentialId: savedCredential.id,
|
|
credentialName: savedCredential.name,
|
|
credentialType: savedCredential.type,
|
|
credentialStatus: expect.any(String),
|
|
}),
|
|
]),
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|