Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

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();
});
});