Files
n8n/packages/cli/test/integration/external-secrets/secret-providers-connections.api.test.ts
alighasami 3d5eaf9445
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
first commit
2026-03-17 16:22:57 +03:30

950 lines
29 KiB
TypeScript

import { LicenseState } from '@n8n/backend-common';
import { createTeamProject, mockInstance, testDb } from '@n8n/backend-test-utils';
import type { Project } from '@n8n/db';
import {
ProjectRepository,
ProjectSecretsProviderAccessRepository,
SecretsProviderConnectionRepository,
} from '@n8n/db';
import { Container } from '@n8n/di';
import { mock } from 'jest-mock-extended';
import { Cipher } from 'n8n-core';
import { ExternalSecretsConfig } from '@/modules/external-secrets.ee/external-secrets.config';
import { ExternalSecretsProviders } from '@/modules/external-secrets.ee/external-secrets-providers.ee';
import { MockProviders, createDummyProvider } from '../../shared/external-secrets/utils';
import { createAdmin, createMember, createOwner } from '../shared/db/users';
import type { SuperAgentTest } from '../shared/types';
import * as utils from '../shared/utils';
const mockProvidersInstance = new MockProviders();
// Register mock providers for types used in tests
mockProvidersInstance.setProviders({
awsSecretsManager: createDummyProvider({
name: 'awsSecretsManager',
properties: [
{
name: 'region',
displayName: 'Region',
type: 'string',
default: '',
},
{
name: 'accessKeyId',
displayName: 'Access Key ID',
type: 'string',
default: '',
typeOptions: {
password: true,
},
},
{
name: 'secretAccessKey',
displayName: 'Secret Access Key',
type: 'string',
default: '',
typeOptions: {
password: true,
},
},
{
name: 'sessionToken',
displayName: 'Session Token',
type: 'string',
default: '',
typeOptions: {
password: true,
},
},
],
}),
gcpSecretsManager: createDummyProvider({ name: 'gcpSecretsManager' }),
vault: createDummyProvider({ name: 'vault' }),
});
mockInstance(ExternalSecretsProviders, mockProvidersInstance);
const licenseMock = mock<LicenseState>();
licenseMock.isLicensed.mockReturnValue(true);
Container.set(LicenseState, licenseMock);
mockInstance(ExternalSecretsConfig, {
externalSecretsForProjects: true,
});
describe('Secret Providers Connections API', () => {
const testServer = utils.setupTestServer({
endpointGroups: ['externalSecrets'],
enabledFeatures: ['feat:externalSecrets'],
modules: ['external-secrets'],
});
let ownerAgent: SuperAgentTest;
let memberAgent: SuperAgentTest;
let adminAgent: SuperAgentTest;
let agents: Record<string, SuperAgentTest>;
let teamProject1: Project;
let teamProject2: Project;
const FORBIDDEN_MESSAGE = 'User is missing a scope required to perform this action';
beforeAll(async () => {
const owner = await createOwner();
ownerAgent = testServer.authAgentFor(owner);
const [member, admin] = await Promise.all([createMember(), createAdmin()]);
memberAgent = testServer.authAgentFor(member);
adminAgent = testServer.authAgentFor(admin);
agents = {
owner: ownerAgent,
admin: adminAgent,
member: memberAgent,
};
teamProject1 = await createTeamProject('Engineering');
teamProject2 = await createTeamProject('Marketing');
});
beforeEach(async () => {
await testDb.truncate(['SecretsProviderConnection', 'ProjectSecretsProviderAccess']);
});
describe('Create connection', () => {
test('should create a global connection by default with encrypted settings', async () => {
const payload = {
providerKey: 'awsProd',
type: 'awsSecretsManager',
projectIds: [],
settings: {
region: 'us-east-1',
accessKeyId: 'AKIAIOSFODNN7EXAMPLE',
secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
},
};
const response = await ownerAgent
.post('/secret-providers/connections')
.send(payload)
.expect(200);
expect(response.body.data).toEqual({
id: expect.any(String),
name: 'awsProd',
type: 'awsSecretsManager',
secretsCount: 2,
state: 'connected',
secrets: [{ name: 'test1' }, { name: 'test2' }],
projects: [],
settings: expect.any(Object),
createdAt: expect.any(String),
updatedAt: expect.any(String),
});
// Validate encrypted items in the DB
const savedConnection = await Container.get(
SecretsProviderConnectionRepository,
).findOneByOrFail({ providerKey: 'awsProd' });
expect(savedConnection.encryptedSettings).toBeTruthy();
expect(savedConnection.encryptedSettings).not.toContain('us-east-1');
expect(savedConnection.encryptedSettings).not.toContain('AKIAIOSFODNN7EXAMPLE');
// Check we can decrypt them
const cipher = Container.get(Cipher);
const decryptedSettings = JSON.parse(cipher.decrypt(savedConnection.encryptedSettings));
expect(decryptedSettings).toEqual(payload.settings);
});
test('should create a connection and assign its projects', async () => {
const payload = {
providerKey: 'awsShared',
type: 'awsSecretsManager',
projectIds: [teamProject1.id, teamProject2.id],
settings: {
region: 'us-west-2',
accessKeyId: 'AKIAIOSFODNN7EXAMPLE',
secretAccessKey: 'secret123',
},
};
const response = await ownerAgent
.post('/secret-providers/connections')
.send(payload)
.expect(200);
expect(response.body.data.projects).toHaveLength(2);
expect(response.body.data.projects).toEqual(
expect.arrayContaining([
{ id: teamProject1.id, name: 'Engineering' },
{ id: teamProject2.id, name: 'Marketing' },
]),
);
const getResponse = await ownerAgent
.get('/secret-providers/connections/awsShared')
.expect(200);
expect(getResponse.body.data.projects).toHaveLength(2);
});
test('should reject duplicate provider key', async () => {
const payload = {
providerKey: 'duplicateTest',
type: 'awsSecretsManager',
projectIds: [],
settings: { region: 'us-east-1' },
};
await ownerAgent.post('/secret-providers/connections').send(payload).expect(200);
const response = await ownerAgent
.post('/secret-providers/connections')
.send(payload)
.expect(400);
expect(response.body.message).toBe('There is already an entry with this name');
});
});
describe('Update connection', () => {
test('should update connection type', async () => {
await ownerAgent
.post('/secret-providers/connections')
.send({
providerKey: 'updateTypeTest',
type: 'awsSecretsManager',
projectIds: [teamProject1.id],
settings: { region: 'us-east-1' },
})
.expect(200);
const response = await ownerAgent
.patch('/secret-providers/connections/updateTypeTest')
.send({
type: 'gcpSecretsManager',
settings: { projectId: 'my-project' },
})
.expect(200);
expect(response.body.data.type).toBe('gcpSecretsManager');
expect(response.body.data.projects).toHaveLength(1);
const getResponse = await ownerAgent
.get('/secret-providers/connections/updateTypeTest')
.expect(200);
expect(getResponse.body.data.type).toBe('gcpSecretsManager');
});
test('should completely replace settings (not merge)', async () => {
await ownerAgent
.post('/secret-providers/connections')
.send({
providerKey: 'replaceSettingsTest',
type: 'awsSecretsManager',
projectIds: [],
settings: {
region: 'us-east-1',
accessKeyId: 'OLD_KEY',
secretAccessKey: 'OLD_SECRET',
},
})
.expect(200);
await ownerAgent
.patch('/secret-providers/connections/replaceSettingsTest')
.send({
settings: {
region: 'eu-west-1',
secretAccessKey: 'NEW_SECRET',
},
})
.expect(200);
const updated = await Container.get(SecretsProviderConnectionRepository).findOneByOrFail({
providerKey: 'replaceSettingsTest',
});
const cipher = Container.get(Cipher);
const decryptedSettings = JSON.parse(cipher.decrypt(updated.encryptedSettings));
expect(decryptedSettings).toEqual({
region: 'eu-west-1',
secretAccessKey: 'NEW_SECRET',
});
expect(decryptedSettings).not.toHaveProperty('accessKeyId');
});
test('should update project access', async () => {
await ownerAgent
.post('/secret-providers/connections')
.send({
providerKey: 'updateProjectsTest',
type: 'awsSecretsManager',
projectIds: [teamProject1.id],
settings: { region: 'us-east-1' },
})
.expect(200);
const response = await ownerAgent
.patch('/secret-providers/connections/updateProjectsTest')
.send({ projectIds: [teamProject2.id] })
.expect(200);
expect(response.body.data.projects).toEqual([{ id: teamProject2.id, name: 'Marketing' }]);
const getResponse = await ownerAgent
.get('/secret-providers/connections/updateProjectsTest')
.expect(200);
expect(getResponse.body.data.projects).toHaveLength(1);
expect(getResponse.body.data.projects[0].id).toBe(teamProject2.id);
});
test('should convert to global by passing empty projectIds', async () => {
await ownerAgent
.post('/secret-providers/connections')
.send({
providerKey: 'toGlobalTest',
type: 'awsSecretsManager',
projectIds: [teamProject1.id, teamProject2.id],
settings: { region: 'us-east-1' },
})
.expect(200);
const response = await ownerAgent
.patch('/secret-providers/connections/toGlobalTest')
.send({ projectIds: [] })
.expect(200);
expect(response.body.data.projects).toEqual([]);
const getResponse = await ownerAgent
.get('/secret-providers/connections/toGlobalTest')
.expect(200);
expect(getResponse.body.data.projects).toEqual([]);
});
test('should fail for non-existent connection', async () => {
const response = await ownerAgent
.patch('/secret-providers/connections/non-existent')
.send({ type: 'gcpSecretsManager' })
.expect(404);
expect(response.body.message).toBe('Connection with key "non-existent" not found');
});
});
describe('Delete connection', () => {
test('should delete connection and cascade project access', async () => {
await ownerAgent
.post('/secret-providers/connections')
.send({
providerKey: 'deleteTest',
type: 'awsSecretsManager',
projectIds: [teamProject1.id, teamProject2.id],
settings: { region: 'us-east-1' },
})
.expect(200);
// Save the connection ID for verification after deletion
const savedConnection = await Container.get(
SecretsProviderConnectionRepository,
).findOneByOrFail({ providerKey: 'deleteTest' });
const connectionId = savedConnection.id;
await ownerAgent.delete('/secret-providers/connections/deleteTest').expect(204);
// Verify deletion via GET
await ownerAgent.get('/secret-providers/connections/deleteTest').expect(404);
// Verify cascade delete
const remainingAccess = await Container.get(ProjectSecretsProviderAccessRepository).find({
where: { secretsProviderConnectionId: connectionId },
});
expect(remainingAccess).toHaveLength(0);
});
test('should fail for non-existent connection', async () => {
const response = await ownerAgent
.delete('/secret-providers/connections/non-existent')
.expect(404);
expect(response.body.message).toBe('Connection with key "non-existent" not found');
});
});
describe('List connections', () => {
test('should list all connections with eager-loaded projects', async () => {
await ownerAgent
.post('/secret-providers/connections')
.send({
providerKey: 'listTest1',
type: 'awsSecretsManager',
projectIds: [teamProject1.id],
settings: { region: 'us-east-1' },
})
.expect(200);
await ownerAgent
.post('/secret-providers/connections')
.send({
providerKey: 'listTest2',
type: 'gcpSecretsManager',
projectIds: [teamProject2.id],
settings: { projectId: 'my-project' },
})
.expect(200);
await ownerAgent
.post('/secret-providers/connections')
.send({
providerKey: 'listTest3',
type: 'vault',
projectIds: [],
settings: { vaultUrl: 'https://vault.example.com' },
})
.expect(200);
const response = await ownerAgent.get('/secret-providers/connections').expect(200);
expect(response.body.data).toHaveLength(3);
for (const connection of response.body.data) {
expect(connection).toMatchObject({
id: expect.any(String),
name: expect.any(String),
type: expect.any(String),
state: expect.any(String),
projects: expect.any(Array),
createdAt: expect.any(String),
updatedAt: expect.any(String),
});
// LIST endpoint should NOT include settings (lightweight response)
expect(connection).not.toHaveProperty('settings');
}
const connection1 = response.body.data.find((c: { name: string }) => c.name === 'listTest1');
expect(connection1.projects).toHaveLength(1);
const connection3 = response.body.data.find((c: { name: string }) => c.name === 'listTest3');
expect(connection3.projects).toEqual([]);
});
test('should return empty array when no connections exist', async () => {
const response = await ownerAgent.get('/secret-providers/connections').expect(200);
expect(response.body.data).toEqual([]);
});
});
describe('Get single connection', () => {
test('should return full connection details', async () => {
await ownerAgent
.post('/secret-providers/connections')
.send({
providerKey: 'getTest',
type: 'awsSecretsManager',
projectIds: [teamProject1.id, teamProject2.id],
settings: {
region: 'us-east-1',
accessKeyId: 'AKIAIOSFODNN7EXAMPLE',
},
})
.expect(200);
const response = await ownerAgent.get('/secret-providers/connections/getTest').expect(200);
expect(response.body.data).toMatchObject({
id: expect.any(String),
name: 'getTest',
type: 'awsSecretsManager',
state: expect.any(String),
createdAt: expect.any(String),
updatedAt: expect.any(String),
});
expect(response.body.data.projects).toHaveLength(2);
});
test('should fail for non-existent connection', async () => {
const response = await ownerAgent
.get('/secret-providers/connections/does-not-exist')
.expect(404);
expect(response.body.message).toBe('Connection with key "does-not-exist" not found');
});
});
describe('Security', () => {
test('should return redacted settings without exposing sensitive data', async () => {
const sensitiveSettings = {
region: 'us-east-1',
accessKeyId: 'AKIAIOSFODNN7EXAMPLE',
secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
sessionToken: 'very-secret-session-token',
};
// Create connection with sensitive settings
const createResponse = await ownerAgent
.post('/secret-providers/connections')
.send({
providerKey: 'securityTest',
type: 'awsSecretsManager',
projectIds: [teamProject1.id],
settings: sensitiveSettings,
})
.expect(200);
// Detail endpoint (POST) should include settings field
expect(createResponse.body.data).toHaveProperty('settings');
expect(createResponse.body.data.settings).toEqual(expect.any(Object));
// Password fields should be redacted (contain BLANK_VALUE marker)
const responseSettings = createResponse.body.data.settings;
const responseString = JSON.stringify(responseSettings);
expect(responseString).toContain('__n8n_BLANK_VALUE_');
// Actual secret values should NOT appear in response
expect(responseString).not.toContain('AKIAIOSFODNN7EXAMPLE');
expect(responseString).not.toContain('wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY');
expect(responseString).not.toContain('very-secret-session-token');
// GET detail endpoint should also include redacted settings
const getResponse = await ownerAgent
.get('/secret-providers/connections/securityTest')
.expect(200);
expect(getResponse.body.data).toHaveProperty('settings');
const getResponseString = JSON.stringify(getResponse.body.data.settings);
expect(getResponseString).toContain('__n8n_BLANK_VALUE_');
expect(getResponseString).not.toContain('AKIAIOSFODNN7EXAMPLE');
expect(getResponseString).not.toContain('very-secret-session-token');
// LIST endpoint should NOT include settings
const listResponse = await ownerAgent.get('/secret-providers/connections').expect(200);
const securityTestConnection = listResponse.body.data.find(
(conn: { name: string }) => conn.name === 'securityTest',
);
expect(securityTestConnection).toBeDefined();
expect(securityTestConnection).not.toHaveProperty('settings');
// Verify entire list response doesn't contain secrets
const listResponseString = JSON.stringify(listResponse.body.data);
expect(listResponseString).not.toContain('AKIAIOSFODNN7EXAMPLE');
expect(listResponseString).not.toContain('very-secret-session-token');
// DELETE endpoint should return 204 with no body
await ownerAgent.delete('/secret-providers/connections/securityTest').expect(204);
});
});
describe('Access Control', () => {
async function createTestConnection(providerKey: string, projectIds: string[] = []) {
await ownerAgent
.post('/secret-providers/connections')
.send({
providerKey,
type: 'awsSecretsManager',
projectIds,
settings: { region: 'us-east-1' },
})
.expect(200);
}
describe('Create connection', () => {
test.each([
{ role: 'owner', allowed: true },
{ role: 'admin', allowed: true },
{ role: 'member', allowed: false },
])('should allow=$allowed for $role to create connection', async ({ role, allowed }) => {
const providerKey = `createTest${role.charAt(0).toUpperCase() + role.slice(1)}`;
const response = await agents[role]
.post('/secret-providers/connections')
.send({
providerKey,
type: 'awsSecretsManager',
projectIds: [],
settings: { region: 'us-east-1' },
})
.expect(allowed ? 200 : 403);
if (allowed) {
expect(response.body.data.name).toBe(providerKey);
} else {
expect(response.body.message).toBe(FORBIDDEN_MESSAGE);
}
});
});
describe('read connections', () => {
beforeEach(async () => {
await createTestConnection('readTestConnection', [teamProject1.id]);
});
describe('list all connections', () => {
test.each([
{ role: 'owner', allowed: true },
{ role: 'admin', allowed: true },
{ role: 'member', allowed: false },
])('should allow=$allowed for $role to list connections', async ({ role, allowed }) => {
const response = await agents[role]
.get('/secret-providers/connections')
.expect(allowed ? 200 : 403);
if (allowed) {
expect(Array.isArray(response.body.data)).toBe(true);
const connection = response.body.data.find(
(c: { name: string }) => c.name === 'readTestConnection',
);
expect(connection).toBeDefined();
} else {
expect(response.body.message).toBe(FORBIDDEN_MESSAGE);
}
});
});
describe('get single connection', () => {
test.each([
{ role: 'owner', allowed: true },
{ role: 'admin', allowed: true },
{ role: 'member', allowed: false },
])(
'should allow=$allowed for $role to get single connection',
async ({ role, allowed }) => {
const response = await agents[role]
.get('/secret-providers/connections/readTestConnection')
.expect(allowed ? 200 : 403);
if (allowed) {
expect(response.body.data.name).toBe('readTestConnection');
} else {
expect(response.body.message).toBe(FORBIDDEN_MESSAGE);
}
},
);
});
});
describe('update connections', () => {
beforeEach(async () => {
await createTestConnection('updateAuthTest');
});
test.each([
{ role: 'owner', allowed: true },
{ role: 'admin', allowed: true },
{ role: 'member', allowed: false },
])('should allow=$allowed for $role to update connection', async ({ role, allowed }) => {
const response = await agents[role]
.patch('/secret-providers/connections/updateAuthTest')
.send({ projectIds: [teamProject1.id] })
.expect(allowed ? 200 : 403);
if (allowed) {
expect(response.body.data.projects).toHaveLength(1);
} else {
expect(response.body.message).toBe(FORBIDDEN_MESSAGE);
}
});
});
describe('delete connections', () => {
beforeEach(async () => {
await Promise.all(
['owner', 'admin', 'member'].map(
async (role) =>
await createTestConnection(
`deleteAuth${role.charAt(0).toUpperCase() + role.slice(1)}Test`,
),
),
);
});
test.each([
{ role: 'owner', allowed: true },
{ role: 'admin', allowed: true },
{ role: 'member', allowed: false },
])('should allow=$allowed for $role to delete connection', async ({ role, allowed }) => {
const providerKey = `deleteAuth${role.charAt(0).toUpperCase() + role.slice(1)}Test`;
const response = await agents[role]
.delete(`/secret-providers/connections/${providerKey}`)
.expect(allowed ? 204 : 403);
if (allowed) {
await ownerAgent.get(`/secret-providers/connections/${providerKey}`).expect(404);
} else {
expect(response.body.message).toBe(FORBIDDEN_MESSAGE);
await ownerAgent.get(`/secret-providers/connections/${providerKey}`).expect(200);
}
});
});
});
describe('Relationships with Project entity', () => {
test('multiple providers can be associated with the same project', async () => {
await ownerAgent
.post('/secret-providers/connections')
.send({
providerKey: 'provider1',
type: 'awsSecretsManager',
projectIds: [teamProject1.id],
settings: { region: 'us-east-1' },
})
.expect(200);
await ownerAgent
.post('/secret-providers/connections')
.send({
providerKey: 'provider2',
type: 'gcpSecretsManager',
projectIds: [teamProject1.id],
settings: { projectId: 'my-project' },
})
.expect(200);
const response = await ownerAgent.get('/secret-providers/connections').expect(200);
const provider1 = response.body.data.find((c: { name: string }) => c.name === 'provider1');
const provider2 = response.body.data.find((c: { name: string }) => c.name === 'provider2');
expect(provider1).toBeDefined();
expect(provider2).toBeDefined();
expect(provider1.projects[0].id).toBe(teamProject1.id);
expect(provider2.projects[0].id).toBe(teamProject1.id);
// Database validation
const accessForProject = await Container.get(ProjectSecretsProviderAccessRepository).find({
where: { projectId: teamProject1.id },
});
expect(accessForProject).toHaveLength(2);
});
test('should clean up project access when project is deleted', async () => {
const tempProject = await createTeamProject('Temporary');
await ownerAgent
.post('/secret-providers/connections')
.send({
providerKey: 'orphanTest',
type: 'awsSecretsManager',
projectIds: [tempProject.id],
settings: { region: 'us-east-1' },
})
.expect(200);
await Container.get(ProjectRepository).delete(tempProject.id);
// Verify no orphaned access entries
const orphaned = await Container.get(ProjectSecretsProviderAccessRepository).find({
where: { projectId: tempProject.id },
});
expect(orphaned).toHaveLength(0);
// Connection should still exist but with empty projects
const response = await ownerAgent.get('/secret-providers/connections/orphanTest').expect(200);
expect(response.body.data.projects).toEqual([]);
});
});
describe('Reload connection secrets', () => {
beforeAll(async () => {
const { DummyProvider } = await import('../../shared/external-secrets/utils');
mockProvidersInstance.setProviders({ awsSecretsManager: DummyProvider });
});
afterEach(async () => {
const { DummyProvider } = await import('../../shared/external-secrets/utils');
mockProvidersInstance.setProviders({ awsSecretsManager: DummyProvider });
});
test('should successfully reload connection secrets', async () => {
const { ExternalSecretsManager } = await import(
'@/modules/external-secrets.ee/external-secrets-manager.ee'
);
await ownerAgent
.post('/secret-providers/connections')
.send({
providerKey: 'reloadSuccess',
type: 'awsSecretsManager',
projectIds: [],
settings: { username: 'user', password: 'pass' },
})
.expect(200);
const manager = Container.get(ExternalSecretsManager);
await manager.reloadAllProviders();
const provider = manager.getProvider('reloadSuccess');
expect(provider).toBeDefined();
expect(provider?.state).toBe('connected');
const updateSpy = jest.spyOn(provider!, 'update');
const reloadResponse = await ownerAgent
.post('/secret-providers/connections/reloadSuccess/reload')
.expect(200);
expect(reloadResponse.body.data).toEqual({ success: true });
expect(updateSpy).toHaveBeenCalled();
});
test('should return 404 for non-existent connection', async () => {
const response = await ownerAgent
.post('/secret-providers/connections/non-existent/reload')
.expect(404);
expect(response.body.message).toBe('Connection with key "non-existent" not found');
});
describe('access control', () => {
test.each([
{ role: 'owner', allowed: true },
{ role: 'admin', allowed: true },
{ role: 'member', allowed: false },
])('should allow=$allowed for $role to reload connection', async ({ role, allowed }) => {
const { ExternalSecretsManager } = await import(
'@/modules/external-secrets.ee/external-secrets-manager.ee'
);
const providerKey = `reloadAccess${role.charAt(0).toUpperCase() + role.slice(1)}`;
await ownerAgent
.post('/secret-providers/connections')
.send({
providerKey,
type: 'awsSecretsManager',
projectIds: [],
settings: { region: 'us-east-1' },
})
.expect(200);
await Container.get(ExternalSecretsManager).reloadAllProviders();
const response = await agents[role]
.post(`/secret-providers/connections/${providerKey}/reload`)
.expect(allowed ? 200 : 403);
if (allowed) {
expect(response.body.data).toEqual({ success: true });
} else {
expect(response.body.message).toBe(FORBIDDEN_MESSAGE);
}
});
});
});
describe('Test connection', () => {
beforeAll(async () => {
const { DummyProvider } = await import('../../shared/external-secrets/utils');
mockProvidersInstance.setProviders({ awsSecretsManager: DummyProvider });
});
afterEach(async () => {
const { DummyProvider } = await import('../../shared/external-secrets/utils');
mockProvidersInstance.setProviders({ awsSecretsManager: DummyProvider });
});
test('should successfully test connection with valid settings', async () => {
await ownerAgent
.post('/secret-providers/connections')
.send({
providerKey: 'testSuccess',
type: 'awsSecretsManager',
projectIds: [],
settings: { username: 'user', password: 'pass' },
})
.expect(200);
const testResponse = await ownerAgent
.post('/secret-providers/connections/testSuccess/test')
.expect(200);
expect(testResponse.body.data).toMatchObject({ success: true });
});
test('should return failure when provider test fails', async () => {
const { TestFailProvider } = await import('../../shared/external-secrets/utils');
mockProvidersInstance.setProviders({ awsSecretsManager: TestFailProvider });
await ownerAgent
.post('/secret-providers/connections')
.send({
providerKey: 'testFail',
type: 'awsSecretsManager',
projectIds: [],
settings: { region: 'us-east-1' },
})
.expect(200);
const testResponse = await ownerAgent
.post('/secret-providers/connections/testFail/test')
.expect(200);
expect(testResponse.body.data).toMatchObject({ success: false });
});
test('should return error when connection fails', async () => {
const { FailedProvider } = await import('../../shared/external-secrets/utils');
mockProvidersInstance.setProviders({ awsSecretsManager: FailedProvider });
await ownerAgent
.post('/secret-providers/connections')
.send({
providerKey: 'testConnectionFail',
type: 'awsSecretsManager',
projectIds: [],
settings: { region: 'us-east-1' },
})
.expect(200);
const testResponse = await ownerAgent
.post('/secret-providers/connections/testConnectionFail/test')
.expect(200);
expect(testResponse.body.data.success).toBe(false);
expect(testResponse.body.data.error).toBeDefined();
});
test('should return 404 for non-existent connection', async () => {
const response = await ownerAgent
.post('/secret-providers/connections/non-existent/test')
.expect(404);
expect(response.body.message).toBe('Connection with key "non-existent" not found');
});
describe('access control', () => {
test.each([
{ role: 'owner', allowed: true },
{ role: 'admin', allowed: true },
{ role: 'member', allowed: false },
])('should allow=$allowed for $role to test connection', async ({ role, allowed }) => {
const providerKey = `testAccess${role.charAt(0).toUpperCase() + role.slice(1)}`;
await ownerAgent
.post('/secret-providers/connections')
.send({
providerKey,
type: 'awsSecretsManager',
projectIds: [],
settings: { region: 'us-east-1' },
})
.expect(200);
const response = await agents[role]
.post(`/secret-providers/connections/${providerKey}/test`)
.expect(allowed ? 200 : 403);
if (allowed) {
expect(response.body.data).toHaveProperty('success');
} else {
expect(response.body.message).toBe(FORBIDDEN_MESSAGE);
}
});
});
});
});