diff --git a/dashboards/tracker-web/next.config.ts b/dashboards/tracker-web/next.config.ts index b64ddbe5..41463094 100644 --- a/dashboards/tracker-web/next.config.ts +++ b/dashboards/tracker-web/next.config.ts @@ -21,6 +21,13 @@ const securityHeaders = [ const nextConfig: NextConfig = { ...(process.env.VERCEL ? {} : { output: 'standalone' }), + transpilePackages: [ + '@bytelyst/api-client', + '@bytelyst/errors', + '@bytelyst/config', + '@bytelyst/react-auth', + '@bytelyst/telemetry-client', + ], async headers() { return [ { @@ -29,23 +36,6 @@ const nextConfig: NextConfig = { }, ]; }, - webpack: config => { - // Handle file: references for @bytelyst packages - // eslint-disable-next-line @typescript-eslint/no-var-requires - const path = require('path'); - config.resolve.alias = { - ...config.resolve.alias, - '@bytelyst/api-client': path.resolve( - __dirname, - '../../learning_ai_common_plat/packages/api-client/dist/index.js' - ), - '@bytelyst/errors': path.resolve( - __dirname, - '../../learning_ai_common_plat/packages/errors/dist/index.js' - ), - }; - return config; - }, }; export default nextConfig; diff --git a/packages/config/src/__tests__/keyvault.test.ts b/packages/config/src/__tests__/keyvault.test.ts index 441ab6b7..659f1936 100644 --- a/packages/config/src/__tests__/keyvault.test.ts +++ b/packages/config/src/__tests__/keyvault.test.ts @@ -6,6 +6,22 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { resolveKeyVaultSecrets, LYSNR_SECRETS } from '../keyvault.js'; import type { SecretMapping } from '../keyvault.js'; +// Mock Azure SDK dynamic imports to prevent test timeouts +const { mockGetSecret } = vi.hoisted(() => { + const mockGetSecret = vi.fn(); + return { mockGetSecret }; +}); + +vi.mock('@azure/identity', () => ({ + DefaultAzureCredential: vi.fn(), +})); + +vi.mock('@azure/keyvault-secrets', () => ({ + SecretClient: vi.fn().mockImplementation(() => ({ + getSecret: mockGetSecret, + })), +})); + describe('resolveKeyVaultSecrets', () => { const originalEnv = { ...process.env }; @@ -14,17 +30,16 @@ describe('resolveKeyVaultSecrets', () => { delete process.env.AZURE_KEYVAULT_URL; delete process.env.TEST_SECRET_A; delete process.env.TEST_SECRET_B; + mockGetSecret.mockReset(); }); afterEach(() => { process.env = { ...originalEnv }; - vi.restoreAllMocks(); + vi.clearAllMocks(); }); it('skips entirely when AZURE_KEYVAULT_URL is not set', async () => { - const secrets: SecretMapping[] = [ - { kvName: 'test-secret', envVar: 'TEST_SECRET_A' }, - ]; + const secrets: SecretMapping[] = [{ kvName: 'test-secret', envVar: 'TEST_SECRET_A' }]; await resolveKeyVaultSecrets(secrets); @@ -36,9 +51,7 @@ describe('resolveKeyVaultSecrets', () => { process.env.AZURE_KEYVAULT_URL = 'https://kv-test.vault.azure.net'; process.env.TEST_SECRET_A = 'already-set'; - const secrets: SecretMapping[] = [ - { kvName: 'test-secret-a', envVar: 'TEST_SECRET_A' }, - ]; + const secrets: SecretMapping[] = [{ kvName: 'test-secret-a', envVar: 'TEST_SECRET_A' }]; // Should not attempt KV call since all secrets are present await resolveKeyVaultSecrets(secrets); @@ -47,16 +60,14 @@ describe('resolveKeyVaultSecrets', () => { }); it('accepts custom vaultUrl via opts', async () => { - // With no AZURE_KEYVAULT_URL but custom vaultUrl, it should attempt resolution - // This will fail with import error in test env (no @azure/identity), which is expected - const secrets: SecretMapping[] = [ - { kvName: 'test-secret', envVar: 'TEST_SECRET_A' }, - ]; + mockGetSecret.mockResolvedValue({ value: 'resolved-value' }); - // Should not throw — gracefully handles missing @azure/identity - await expect( - resolveKeyVaultSecrets(secrets, { vaultUrl: 'https://kv-test.vault.azure.net' }) - ).resolves.not.toThrow(); + const secrets: SecretMapping[] = [{ kvName: 'test-secret', envVar: 'TEST_SECRET_A' }]; + + await resolveKeyVaultSecrets(secrets, { vaultUrl: 'https://kv-test.vault.azure.net' }); + + expect(process.env.TEST_SECRET_A).toBe('resolved-value'); + expect(mockGetSecret).toHaveBeenCalledWith('test-secret'); }); it('handles empty secrets array', async () => { @@ -65,45 +76,59 @@ describe('resolveKeyVaultSecrets', () => { await expect(resolveKeyVaultSecrets([])).resolves.not.toThrow(); }); - it('gracefully handles import failures (no @azure/identity installed)', async () => { + it('resolves multiple missing secrets from Key Vault', async () => { process.env.AZURE_KEYVAULT_URL = 'https://kv-test.vault.azure.net'; - - const secrets: SecretMapping[] = [ - { kvName: 'test-secret', envVar: 'TEST_SECRET_A' }, - ]; - - // In test env, @azure/identity likely isn't available - // resolveKeyVaultSecrets should catch and warn, not throw - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); - - await resolveKeyVaultSecrets(secrets); - - // Either warn was called (no Azure SDK) or the env var remains unset - // Both are acceptable — the function should not throw - expect(process.env.TEST_SECRET_A).toBeUndefined(); - - warnSpy.mockRestore(); - }); - - it('filters to only missing secrets', async () => { - process.env.AZURE_KEYVAULT_URL = 'https://kv-test.vault.azure.net'; - process.env.TEST_SECRET_A = 'present'; - // TEST_SECRET_B is missing + mockGetSecret + .mockResolvedValueOnce({ value: 'secret-a-val' }) + .mockResolvedValueOnce({ value: 'secret-b-val' }); const secrets: SecretMapping[] = [ { kvName: 'secret-a', envVar: 'TEST_SECRET_A' }, { kvName: 'secret-b', envVar: 'TEST_SECRET_B' }, ]; + await resolveKeyVaultSecrets(secrets); + + expect(process.env.TEST_SECRET_A).toBe('secret-a-val'); + expect(process.env.TEST_SECRET_B).toBe('secret-b-val'); + }); + + it('warns but does not throw when getSecret fails', async () => { + process.env.AZURE_KEYVAULT_URL = 'https://kv-test.vault.azure.net'; + mockGetSecret.mockRejectedValue(new Error('SecretNotFound')); + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const secrets: SecretMapping[] = [{ kvName: 'bad-secret', envVar: 'TEST_SECRET_A' }]; + await resolveKeyVaultSecrets(secrets); - // TEST_SECRET_A should remain unchanged - expect(process.env.TEST_SECRET_A).toBe('present'); + expect(process.env.TEST_SECRET_A).toBeUndefined(); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('1/1 secrets failed')); warnSpy.mockRestore(); }); + + it('filters to only missing secrets — skips already-present', async () => { + process.env.AZURE_KEYVAULT_URL = 'https://kv-test.vault.azure.net'; + process.env.TEST_SECRET_A = 'present'; + mockGetSecret.mockResolvedValue({ value: 'from-kv' }); + + const secrets: SecretMapping[] = [ + { kvName: 'secret-a', envVar: 'TEST_SECRET_A' }, + { kvName: 'secret-b', envVar: 'TEST_SECRET_B' }, + ]; + + await resolveKeyVaultSecrets(secrets); + + // TEST_SECRET_A should remain unchanged (already present) + expect(process.env.TEST_SECRET_A).toBe('present'); + // TEST_SECRET_B should be resolved from KV + expect(process.env.TEST_SECRET_B).toBe('from-kv'); + // getSecret should only be called for the missing secret + expect(mockGetSecret).toHaveBeenCalledTimes(1); + expect(mockGetSecret).toHaveBeenCalledWith('secret-b'); + }); }); describe('LYSNR_SECRETS', () => {