426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
453 lines
13 KiB
TypeScript
453 lines
13 KiB
TypeScript
/** Unit tests for `resolveCredentials` matching required node credentials against stored mappings (deterministic, mocked store). */
|
|
import { describe, expect, mock, test } from 'bun:test';
|
|
import type {
|
|
CredentialProvider,
|
|
CredentialProviderResult,
|
|
WorkflowCredentialStoreApi,
|
|
WorkflowPluginConfig,
|
|
} from '../../src/types/index';
|
|
import { resolveCredentials } from '../../src/utils/credentialResolver';
|
|
import {
|
|
createGmailNode,
|
|
createSlackNode,
|
|
createTriggerNode,
|
|
createValidWorkflow,
|
|
} from '../fixtures/workflows';
|
|
|
|
interface CredentialApiClient {
|
|
createCredential(credential: {
|
|
name: string;
|
|
type: string;
|
|
data: Record<string, unknown>;
|
|
}): Promise<{ id: string }>;
|
|
}
|
|
|
|
function createMockCredStore(
|
|
overrides?: Partial<WorkflowCredentialStoreApi>
|
|
): WorkflowCredentialStoreApi {
|
|
return {
|
|
get: mock(() => Promise.resolve(null)),
|
|
set: mock(() => Promise.resolve()),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function createMockCredProvider(
|
|
resolveFn?: (userId: string, credType: string) => Promise<CredentialProviderResult>
|
|
): CredentialProvider {
|
|
return {
|
|
resolve: mock(resolveFn ?? (() => Promise.resolve(null))),
|
|
};
|
|
}
|
|
|
|
function createMockApiClient(overrides?: Partial<CredentialApiClient>): CredentialApiClient {
|
|
return {
|
|
createCredential: mock(() =>
|
|
Promise.resolve({
|
|
id: 'workflows-cred-123',
|
|
name: 'gmailOAuth2Api',
|
|
type: 'gmailOAuth2Api',
|
|
createdAt: '2025-01-01T00:00:00Z',
|
|
updatedAt: '2025-01-01T00:00:00Z',
|
|
})
|
|
),
|
|
...overrides,
|
|
} as CredentialApiClient;
|
|
}
|
|
|
|
const baseConfig: WorkflowPluginConfig = { apiKey: 'embedded', host: 'in-process' };
|
|
const testTagName = 'user:user-001';
|
|
|
|
// ============================================================================
|
|
// resolveCredentials
|
|
// ============================================================================
|
|
|
|
describe('resolveCredentials', () => {
|
|
test('returns unchanged workflow when no credentials needed', async () => {
|
|
const workflow = createValidWorkflow({
|
|
nodes: [createTriggerNode(), { ...createGmailNode(), credentials: undefined }],
|
|
});
|
|
|
|
const res = await resolveCredentials(
|
|
workflow,
|
|
'user-001',
|
|
baseConfig,
|
|
null,
|
|
null,
|
|
null,
|
|
testTagName
|
|
);
|
|
expect(res.missingConnections).toHaveLength(0);
|
|
expect(res.injectedCredentials.size).toBe(0);
|
|
});
|
|
|
|
test('reports missing when no config, no store, no provider', async () => {
|
|
const res = await resolveCredentials(
|
|
createValidWorkflow(),
|
|
'user-001',
|
|
baseConfig,
|
|
null,
|
|
null,
|
|
null,
|
|
testTagName
|
|
);
|
|
expect(res.missingConnections.length).toBeGreaterThan(0);
|
|
expect(res.missingConnections[0].credType).toBe('gmailOAuth2Api');
|
|
});
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Static config mode
|
|
// --------------------------------------------------------------------------
|
|
|
|
test('config mode: injects credential IDs from config', async () => {
|
|
const config: WorkflowPluginConfig = {
|
|
...baseConfig,
|
|
credentials: { gmailOAuth2Api: 'preconfigured-cred-id' },
|
|
};
|
|
|
|
const res = await resolveCredentials(
|
|
createValidWorkflow(),
|
|
'user-001',
|
|
config,
|
|
null,
|
|
null,
|
|
null,
|
|
testTagName
|
|
);
|
|
expect(res.injectedCredentials.get('gmailOAuth2Api')).toBe('preconfigured-cred-id');
|
|
const gmailNode = res.workflow.nodes.find((n) => n.name === 'Gmail');
|
|
expect(gmailNode?.credentials?.gmailOAuth2Api.id).toBe('preconfigured-cred-id');
|
|
});
|
|
|
|
test('config mode: fuzzy match without Api suffix', async () => {
|
|
const config: WorkflowPluginConfig = {
|
|
...baseConfig,
|
|
credentials: { gmailOAuth2: 'gmail-cred-from-config' },
|
|
};
|
|
|
|
const res = await resolveCredentials(
|
|
createValidWorkflow(),
|
|
'user-001',
|
|
config,
|
|
null,
|
|
null,
|
|
null,
|
|
testTagName
|
|
);
|
|
expect(res.injectedCredentials.get('gmailOAuth2Api')).toBe('gmail-cred-from-config');
|
|
expect(res.missingConnections).toHaveLength(0);
|
|
});
|
|
|
|
test('config mode: fuzzy match with Api suffix', async () => {
|
|
const workflow = createValidWorkflow({
|
|
nodes: [
|
|
createTriggerNode(),
|
|
{
|
|
...createGmailNode(),
|
|
credentials: { gmailOAuth2: { id: 'PLACEHOLDER', name: 'Gmail' } },
|
|
},
|
|
],
|
|
});
|
|
const config: WorkflowPluginConfig = {
|
|
...baseConfig,
|
|
credentials: { gmailOAuth2Api: 'gmail-cred-with-api' },
|
|
};
|
|
|
|
const res = await resolveCredentials(
|
|
workflow,
|
|
'user-001',
|
|
config,
|
|
null,
|
|
null,
|
|
null,
|
|
testTagName
|
|
);
|
|
expect(res.injectedCredentials.get('gmailOAuth2')).toBe('gmail-cred-with-api');
|
|
expect(res.missingConnections).toHaveLength(0);
|
|
});
|
|
|
|
test('config mode: handles multiple credential types', async () => {
|
|
const workflow = createValidWorkflow({
|
|
nodes: [createTriggerNode(), createGmailNode(), createSlackNode()],
|
|
});
|
|
const config: WorkflowPluginConfig = {
|
|
...baseConfig,
|
|
credentials: { gmailOAuth2Api: 'gmail-cred', slackApi: 'slack-cred' },
|
|
};
|
|
|
|
const res = await resolveCredentials(
|
|
workflow,
|
|
'user-001',
|
|
config,
|
|
null,
|
|
null,
|
|
null,
|
|
testTagName
|
|
);
|
|
expect(res.injectedCredentials.get('gmailOAuth2Api')).toBe('gmail-cred');
|
|
expect(res.injectedCredentials.get('slackApi')).toBe('slack-cred');
|
|
});
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Credential store DB mode
|
|
// --------------------------------------------------------------------------
|
|
|
|
test('db mode: resolves from credential store', async () => {
|
|
const credStore = createMockCredStore({
|
|
get: mock(() => Promise.resolve('cached-cred-id')),
|
|
});
|
|
|
|
const res = await resolveCredentials(
|
|
createValidWorkflow(),
|
|
'user-001',
|
|
baseConfig,
|
|
credStore,
|
|
null,
|
|
null,
|
|
testTagName
|
|
);
|
|
expect(res.injectedCredentials.get('gmailOAuth2Api')).toBe('cached-cred-id');
|
|
expect(res.missingConnections).toHaveLength(0);
|
|
});
|
|
|
|
test('db mode: takes priority over config', async () => {
|
|
const credStore = createMockCredStore({
|
|
get: mock(() => Promise.resolve('db-cred-id')),
|
|
});
|
|
const config: WorkflowPluginConfig = {
|
|
...baseConfig,
|
|
credentials: { gmailOAuth2Api: 'config-cred-id' },
|
|
};
|
|
|
|
const res = await resolveCredentials(
|
|
createValidWorkflow(),
|
|
'user-001',
|
|
config,
|
|
credStore,
|
|
null,
|
|
null,
|
|
testTagName
|
|
);
|
|
expect(res.injectedCredentials.get('gmailOAuth2Api')).toBe('db-cred-id');
|
|
});
|
|
|
|
// --------------------------------------------------------------------------
|
|
// External provider mode — credential_data
|
|
// --------------------------------------------------------------------------
|
|
|
|
test('provider mode: creates workflows credential from credential_data', async () => {
|
|
const oauthData = {
|
|
clientId: 'goog-client-id',
|
|
clientSecret: 'goog-secret',
|
|
oauthTokenData: { access_token: 'tok-123', token_type: 'Bearer' },
|
|
};
|
|
|
|
const provider = createMockCredProvider(async () => ({
|
|
status: 'credential_data' as const,
|
|
data: oauthData,
|
|
}));
|
|
|
|
const apiClient = createMockApiClient();
|
|
const credStore = createMockCredStore();
|
|
|
|
const res = await resolveCredentials(
|
|
createValidWorkflow(),
|
|
'user-001',
|
|
baseConfig,
|
|
credStore,
|
|
provider,
|
|
apiClient,
|
|
testTagName
|
|
);
|
|
|
|
expect(apiClient.createCredential).toHaveBeenCalledWith({
|
|
name: `gmailOAuth2Api_${testTagName}`,
|
|
type: 'gmailOAuth2Api',
|
|
data: oauthData,
|
|
});
|
|
expect(res.injectedCredentials.get('gmailOAuth2Api')).toBe('workflows-cred-123');
|
|
expect(res.missingConnections).toHaveLength(0);
|
|
});
|
|
|
|
test('provider mode: caches workflows credential ID after creation', async () => {
|
|
const provider = createMockCredProvider(async () => ({
|
|
status: 'credential_data' as const,
|
|
data: { access_token: 'tok' },
|
|
}));
|
|
|
|
const credStore = createMockCredStore();
|
|
const apiClient = createMockApiClient();
|
|
|
|
await resolveCredentials(
|
|
createValidWorkflow(),
|
|
'user-001',
|
|
baseConfig,
|
|
credStore,
|
|
provider,
|
|
apiClient,
|
|
testTagName
|
|
);
|
|
|
|
expect(credStore.set).toHaveBeenCalledWith('user-001', 'gmailOAuth2Api', 'workflows-cred-123');
|
|
});
|
|
|
|
test('provider mode: credential_data without apiClient reports missing', async () => {
|
|
const provider = createMockCredProvider(async () => ({
|
|
status: 'credential_data' as const,
|
|
data: { access_token: 'tok' },
|
|
}));
|
|
|
|
const res = await resolveCredentials(
|
|
createValidWorkflow(),
|
|
'user-001',
|
|
baseConfig,
|
|
null,
|
|
provider,
|
|
null,
|
|
testTagName
|
|
);
|
|
|
|
expect(res.missingConnections.length).toBeGreaterThan(0);
|
|
expect(res.missingConnections[0].credType).toBe('gmailOAuth2Api');
|
|
expect(res.injectedCredentials.size).toBe(0);
|
|
});
|
|
|
|
test('provider mode: credential_data with apiClient failure reports missing', async () => {
|
|
const provider = createMockCredProvider(async () => ({
|
|
status: 'credential_data' as const,
|
|
data: { access_token: 'tok' },
|
|
}));
|
|
|
|
const apiClient = createMockApiClient({
|
|
createCredential: mock(() => Promise.reject(new Error('workflow API down'))),
|
|
});
|
|
|
|
const res = await resolveCredentials(
|
|
createValidWorkflow(),
|
|
'user-001',
|
|
baseConfig,
|
|
null,
|
|
provider,
|
|
apiClient,
|
|
testTagName
|
|
);
|
|
|
|
expect(res.missingConnections.length).toBeGreaterThan(0);
|
|
expect(res.missingConnections[0].credType).toBe('gmailOAuth2Api');
|
|
});
|
|
|
|
// --------------------------------------------------------------------------
|
|
// External provider mode — needs_auth
|
|
// --------------------------------------------------------------------------
|
|
|
|
test('provider mode: returns authUrl when needs_auth', async () => {
|
|
const provider = createMockCredProvider(async () => ({
|
|
status: 'needs_auth' as const,
|
|
authUrl: 'https://auth.example.com/connect',
|
|
}));
|
|
|
|
const res = await resolveCredentials(
|
|
createValidWorkflow(),
|
|
'user-001',
|
|
baseConfig,
|
|
null,
|
|
provider,
|
|
null,
|
|
testTagName
|
|
);
|
|
expect(res.missingConnections.length).toBeGreaterThan(0);
|
|
expect(res.missingConnections[0].authUrl).toBe('https://auth.example.com/connect');
|
|
});
|
|
|
|
// --------------------------------------------------------------------------
|
|
// External provider mode — edge cases
|
|
// --------------------------------------------------------------------------
|
|
|
|
test('provider mode: falls back to missing on null result', async () => {
|
|
const provider = createMockCredProvider(async () => null);
|
|
|
|
const res = await resolveCredentials(
|
|
createValidWorkflow(),
|
|
'user-001',
|
|
baseConfig,
|
|
null,
|
|
provider,
|
|
null,
|
|
testTagName
|
|
);
|
|
expect(res.missingConnections.length).toBeGreaterThan(0);
|
|
expect(res.missingConnections[0].authUrl).toBeUndefined();
|
|
});
|
|
|
|
test('provider mode: falls back to missing on provider error', async () => {
|
|
const provider = createMockCredProvider(async () => {
|
|
throw new Error('Provider exploded');
|
|
});
|
|
|
|
const res = await resolveCredentials(
|
|
createValidWorkflow(),
|
|
'user-001',
|
|
baseConfig,
|
|
null,
|
|
provider,
|
|
null,
|
|
testTagName
|
|
);
|
|
expect(res.missingConnections.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Resolution priority
|
|
// --------------------------------------------------------------------------
|
|
|
|
test('priority: db > config > provider', async () => {
|
|
const credStore = createMockCredStore({
|
|
get: mock(() => Promise.resolve('db-wins')),
|
|
});
|
|
const config: WorkflowPluginConfig = {
|
|
...baseConfig,
|
|
credentials: { gmailOAuth2Api: 'config-loses' },
|
|
};
|
|
const provider = createMockCredProvider(async () => ({
|
|
status: 'credential_data' as const,
|
|
data: { access_token: 'provider-loses' },
|
|
}));
|
|
|
|
const res = await resolveCredentials(
|
|
createValidWorkflow(),
|
|
'user-001',
|
|
config,
|
|
credStore,
|
|
provider,
|
|
null,
|
|
testTagName
|
|
);
|
|
expect(res.injectedCredentials.get('gmailOAuth2Api')).toBe('db-wins');
|
|
expect(provider.resolve).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('priority: config > provider when db returns null', async () => {
|
|
const credStore = createMockCredStore();
|
|
const config: WorkflowPluginConfig = {
|
|
...baseConfig,
|
|
credentials: { gmailOAuth2Api: 'config-wins' },
|
|
};
|
|
const provider = createMockCredProvider(async () => ({
|
|
status: 'credential_data' as const,
|
|
data: { access_token: 'provider-loses' },
|
|
}));
|
|
|
|
const res = await resolveCredentials(
|
|
createValidWorkflow(),
|
|
'user-001',
|
|
config,
|
|
credStore,
|
|
provider,
|
|
null,
|
|
testTagName
|
|
);
|
|
expect(res.injectedCredentials.get('gmailOAuth2Api')).toBe('config-wins');
|
|
expect(provider.resolve).not.toHaveBeenCalled();
|
|
});
|
|
});
|