Files
dockhand-upstream/tests/secret-provider-onepassword.test.ts
T

96 lines
3.7 KiB
TypeScript

// Unit tests for the 1Password service-account provider's log/client hygiene (#1436).
// Mocks @1password/sdk (process-global via bun mock.module) and drives the REAL
// serviceAccountProvider. No other unit test imports service-account.ts, so this
// mock does not leak into another suite.
import { describe, test, expect, mock, beforeEach } from 'bun:test';
let createClientCalls = 0;
let resolveAllCalls = 0;
mock.module('@1password/sdk', () => ({
createClient: async () => {
createClientCalls++;
return {
secrets: {
resolveAll: async (refs: string[]) => {
resolveAllCalls++;
const individualResponses: Record<string, any> = {};
for (const r of refs) {
// op://vault/item/field is valid; anything with < 3 segments is malformed.
const segs = r.replace('op://', '').split('/');
individualResponses[r] =
segs.length < 3
? { error: { message: 'invalid format', type: 'BadRef' } }
: { content: { secret: `secret-for-${r}` } };
}
return { individualResponses };
}
},
environments: {
getVariables: async () => ({ variables: [{ name: 'BULK_A', value: 'a' }] })
}
};
}
}));
const { serviceAccountProvider } = await import('../src/lib/server/secretproviders/service-account.ts');
const config = { token: 'ops_faketoken_for_test' } as any;
describe('1Password service-account provider (#1436 hygiene)', () => {
beforeEach(() => {
createClientCalls = 0;
resolveAllCalls = 0;
});
test('collapses malformed refs into a single warning, not one per ref', async () => {
const warnings: string[] = [];
const origWarn = console.warn;
console.warn = (...args: unknown[]) => warnings.push(args.join(' '));
try {
const refs = [
'op://Docker_Production/LEAFWIKI_PASSWORD', // malformed (no field)
'op://Docker_Production/LEAFWIKI_JWT_TOKEN', // malformed
'op://Docker_Production/EMILSE_MAIN_USER', // malformed
'op://Docker_Production/item/password' // valid
];
const resolved = await serviceAccountProvider.resolveSecretReferences(config, refs, '[1Password]');
// Exactly one summary warning for the 3 bad refs, not three.
expect(warnings.length).toBe(1);
expect(warnings[0]).toContain('Skipped 3 of 4');
expect(warnings[0]).toContain('op://<vault>/<item>/<field>');
// The valid ref still resolves.
expect(resolved.get('op://Docker_Production/item/password')).toBe(
'secret-for-op://Docker_Production/item/password'
);
expect(resolved.size).toBe(1);
} finally {
console.warn = origWarn;
}
});
test('does not warn when every ref resolves', async () => {
const warnings: string[] = [];
const origWarn = console.warn;
console.warn = (...args: unknown[]) => warnings.push(args.join(' '));
try {
const refs = ['op://v/item/password', 'op://v/item/token'];
const resolved = await serviceAccountProvider.resolveSecretReferences(config, refs, '[1Password]');
expect(warnings.length).toBe(0);
expect(resolved.size).toBe(2);
} finally {
console.warn = origWarn;
}
});
test('reuses one SDK client across bulk + inline in a deploy (single handshake)', async () => {
// A token unique to this test so the module-level client cache starts cold here.
const freshConfig = { token: 'ops_fresh_token_for_shared_client_test' } as any;
// Simulate what resolveProviderEnvVars does per deploy: one bulk, then one inline.
await serviceAccountProvider.resolveBulk(freshConfig, 'Docker_Production');
await serviceAccountProvider.resolveSecretReferences(freshConfig, ['op://v/item/password'], '[1Password]');
// Both operations run, but createClient is cached per token -> one handshake.
expect(createClientCalls).toBe(1);
expect(resolveAllCalls).toBe(1);
});
});