import { describe, expect, it, vi } from 'vitest'; import { LookerClient, type LookerSdkPort } from '../../../../../src/context/ingest/adapters/looker/client.js'; const clientSecretParam = 'client_secret'; // pragma: allowlist secret function params(): Record { return { base_url: 'https://example.looker.com', client_id: 'id', [clientSecretParam]: 'credential', // pragma: allowlist secret }; } function sdk(overrides: Partial = {}): LookerSdkPort { const port: LookerSdkPort = { me: vi.fn().mockResolvedValue({ id: '1', display_name: 'API User', email: 'api@example.com' }), search_dashboards: vi.fn().mockResolvedValue([{ id: '10' }]), dashboard: vi.fn().mockResolvedValue({ id: '10', title: 'Revenue Dashboard', description: 'Revenue concepts', folder_id: '20', user_id: '1', updated_at: '2026-04-30T00:00:00.000Z', dashboard_elements: [ { id: '99', title: 'ARR', look_id: null, query: { id: 'q1', model: 'b2b', view: 'sales_pipeline', fields: ['opportunities.arr', 'opportunities.stage'], filters: { 'opportunities.stage': 'open' }, sorts: ['opportunities.arr desc'], limit: '500', }, }, ], }), search_looks: vi.fn().mockResolvedValue([{ id: '30' }]), search_scheduled_plans: vi.fn().mockResolvedValue([]), look: vi.fn().mockResolvedValue({ id: '30', title: 'Open Pipeline ARR', description: 'ARR for open opportunities', folder_id: '20', user_id: '1', updated_at: '2026-04-30T00:00:00.000Z', query: { id: 'q2', model: 'b2b', view: 'sales_pipeline', fields: ['opportunities.arr'], filters: { 'opportunities.stage': 'open' }, }, }), all_folders: vi.fn().mockResolvedValue([{ id: '20', name: 'Executive', parent_id: null }]), all_users: vi.fn().mockResolvedValue([{ id: '1', display_name: 'API User', email: 'api@example.com' }]), all_groups: vi.fn().mockResolvedValue([{ id: '2', name: 'Finance' }]), all_connections: vi.fn().mockResolvedValue([ { name: 'b2b_sandbox_bq', host: 'warehouse.example.com', database: 'analytics', schema: 'public', dialect_name: 'bigquery_standard_sql', }, ]), all_lookml_models: vi .fn() .mockResolvedValue([ { name: 'b2b', label: 'B2B', explores: [{ name: 'sales_pipeline', label: 'Sales Pipeline' }] }, ]), lookml_model_explore: vi.fn().mockResolvedValue({ name: 'sales_pipeline', label: 'Sales Pipeline', description: 'Opportunity pipeline', sql_table_name: 'proj.dataset.opportunities AS opportunities', connection_name: 'b2b_sandbox_bq', view_name: 'opportunities', fields: { dimensions: [{ name: 'opportunities.stage', label: 'Stage', type: 'string', sql: '$' + '{TABLE}.stage' }], measures: [{ name: 'opportunities.arr', label: 'ARR', type: 'sum', sql: '$' + '{TABLE}.arr' }], }, joins: [ { name: 'accounts', type: 'left_outer', relationship: 'many_to_one', sql_table_name: 'proj.dataset.accounts', sql_on: '$' + '{opportunities.account_id} = $' + '{accounts.id}', from: null, }, ], }), run_inline_query: vi.fn().mockResolvedValue('[]'), logout: vi.fn().mockResolvedValue(undefined), ...overrides, }; return port; } describe('LookerClient', () => { it('validates credentials with me()', async () => { const client = new LookerClient(params(), { sdkFactory: () => sdk() }); await expect(client.testConnection()).resolves.toEqual({ success: true, metadata: { userId: '1', displayName: 'API User', email: 'api@example.com' }, }); }); it('does not warn to console when optional prioritization inputs fail by default', async () => { const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined); const fakeSdk = sdk({ search_dashboards: vi.fn().mockRejectedValue(new Error('dashboards unavailable')), search_looks: vi.fn().mockRejectedValue(new Error('looks unavailable')), }); const client = new LookerClient(params(), { sdkFactory: () => fakeSdk }); await expect(client.getSignals()).resolves.toMatchObject({ dashboardUsage: [], lookUsage: [], scheduledPlans: [], favorites: [], }); expect(warn).not.toHaveBeenCalled(); }); it('maps dashboards, looks, folders, models, explores, users, and groups to staged DTOs', async () => { const fakeSdk = sdk(); const client = new LookerClient(params(), { sdkFactory: () => fakeSdk }); await expect(client.listDashboards()).resolves.toEqual([{ id: '10', updatedAt: null }]); await expect(client.getDashboard('10')).resolves.toMatchObject({ lookerId: '10', title: 'Revenue Dashboard', tiles: [{ id: '99', query: { model: 'b2b', view: 'sales_pipeline' } }], }); await expect(client.listLooks()).resolves.toEqual([{ id: '30', updatedAt: null }]); await expect(client.getLook('30')).resolves.toMatchObject({ lookerId: '30', title: 'Open Pipeline ARR', query: { model: 'b2b', view: 'sales_pipeline' }, }); await expect(client.listFolders()).resolves.toEqual({ folders: [{ id: '20', name: 'Executive', parentId: null, path: ['Executive'] }], }); await expect(client.listLookmlModels()).resolves.toEqual({ models: [{ name: 'b2b', label: 'B2B', explores: [{ name: 'sales_pipeline', label: 'Sales Pipeline' }] }], }); await expect(client.listLookerConnections()).resolves.toEqual([ { name: 'b2b_sandbox_bq', host: 'warehouse.example.com', database: 'analytics', schema: 'public', dialect: 'bigquery_standard_sql', }, ]); await expect(client.getExplore('b2b', 'sales_pipeline')).resolves.toMatchObject({ modelName: 'b2b', exploreName: 'sales_pipeline', rawSqlTableName: 'proj.dataset.opportunities AS opportunities', connectionName: 'b2b_sandbox_bq', viewName: 'opportunities', fields: { dimensions: [{ name: 'opportunities.stage' }], measures: [{ name: 'opportunities.arr' }] }, joins: [ { name: 'accounts', rawSqlTableName: 'proj.dataset.accounts', sqlOn: '$' + '{opportunities.account_id} = $' + '{accounts.id}', from: null, targetTable: null, }, ], targetWarehouseConnectionId: null, targetTable: null, }); expect(fakeSdk.dashboard).toHaveBeenCalledWith( '10', 'id,title,description,folder_id,user_id,updated_at,dashboard_elements(id,title,look_id,query(id,model,view,fields,filters,sorts,limit,dynamic_fields))', ); expect(fakeSdk.look).toHaveBeenCalledWith( '30', 'id,title,description,folder_id,user_id,updated_at,query(id,model,view,fields,filters,sorts,limit,dynamic_fields)', ); expect(fakeSdk.lookml_model_explore).toHaveBeenCalledWith( 'b2b', 'sales_pipeline', 'name,label,description,sql_table_name,connection_name,view_name,fields,joins(name,type,relationship,sql_table_name,sql_on,from)', ); expect(fakeSdk.all_connections).toHaveBeenCalledWith('name,host,database,schema,dialect_name'); }); it('returns empty usage signals when system activity access fails', async () => { const client = new LookerClient(params(), { sdkFactory: () => sdk({ run_inline_query: vi.fn().mockRejectedValue(new Error('access denied')), search_dashboards: vi.fn().mockResolvedValue([{ id: '10', favorite_count: 4 }]), search_looks: vi.fn().mockResolvedValue([{ id: '30', favorite_count: 2 }]), search_scheduled_plans: vi.fn().mockResolvedValue([]), }), }); await expect(client.getSignals()).resolves.toEqual({ dashboardUsage: [], lookUsage: [], scheduledPlans: [], favorites: [ { contentId: '10', contentType: 'dashboard', favoriteCount: 4 }, { contentId: '30', contentType: 'look', favoriteCount: 2 }, ], }); }); it('paginates dashboard and Look searches', async () => { const dashboardPageOne = Array.from({ length: 500 }, (_, index) => ({ id: String(index + 1) })); const lookPageOne = Array.from({ length: 500 }, (_, index) => ({ id: String(index + 1001) })); const fakeSdk = sdk({ search_dashboards: vi .fn() .mockResolvedValueOnce(dashboardPageOne) .mockResolvedValueOnce([{ id: '501' }]), search_looks: vi .fn() .mockResolvedValueOnce(lookPageOne) .mockResolvedValueOnce([{ id: '1501' }]), }); const client = new LookerClient(params(), { sdkFactory: () => fakeSdk }); await expect(client.listDashboards()).resolves.toHaveLength(501); await expect(client.listLooks()).resolves.toHaveLength(501); expect(fakeSdk.search_dashboards).toHaveBeenNthCalledWith( 1, expect.objectContaining({ deleted: false, fields: 'id,updated_at', limit: 500, offset: 0, sorts: 'id', }), ); expect(fakeSdk.search_dashboards).toHaveBeenNthCalledWith( 2, expect.objectContaining({ limit: 500, offset: 500, }), ); expect(fakeSdk.search_looks).toHaveBeenNthCalledWith( 1, expect.objectContaining({ deleted: false, fields: 'id,updated_at', limit: 500, offset: 0, sorts: 'id', }), ); expect(fakeSdk.search_looks).toHaveBeenNthCalledWith( 2, expect.objectContaining({ limit: 500, offset: 500, }), ); }); it('returns updatedAt cursors from dashboard and Look listing rows', async () => { const fakeSdk = sdk({ search_dashboards: vi.fn().mockResolvedValue([{ id: '10', updated_at: '2026-04-30T12:00:00.000Z' }]), search_looks: vi.fn().mockResolvedValue([{ id: '30', updated_at: '2026-04-30T11:00:00.000Z' }]), }); const client = new LookerClient(params(), { sdkFactory: () => fakeSdk }); await expect(client.listDashboards()).resolves.toEqual([{ id: '10', updatedAt: '2026-04-30T12:00:00.000Z' }]); await expect(client.listLooks()).resolves.toEqual([{ id: '30', updatedAt: '2026-04-30T11:00:00.000Z' }]); }); it('logs out the SDK session during cleanup', async () => { const fakeSdk = sdk(); const client = new LookerClient(params(), { sdkFactory: () => fakeSdk }); await client.testConnection(); await client.cleanup(); expect(fakeSdk.logout).toHaveBeenCalledTimes(1); }); it('aggregates usage, scheduled-plan, and favorite signals', async () => { const runInlineQuery = vi .fn() .mockResolvedValueOnce( JSON.stringify([ { 'dashboard.id': '10', 'history.query_run_count': 3, 'history.created_date': '2026-04-30', 'user.id': 'user-1', }, { 'dashboard.id': '10', 'history.query_run_count': '2', 'history.created_date': '2026-04-29', 'user.id': 'user-2', }, ]), ) .mockResolvedValueOnce( JSON.stringify([ { 'look.id': '30', 'history.query_run_count': 7, 'history.created_date': '2026-04-28', 'user.id': 'user-1', }, ]), ); const fakeSdk = sdk({ run_inline_query: runInlineQuery, search_dashboards: vi.fn().mockResolvedValueOnce([{ id: '10', favorite_count: 4 }]), search_looks: vi.fn().mockResolvedValueOnce([{ id: '30', favorite_count: 2 }]), search_scheduled_plans: vi.fn().mockResolvedValueOnce([ { id: 'sp-dashboard', dashboard_id: '10', look_id: null, enabled: true, scheduled_plan_destination: [{ id: 'dest-1' }, { id: 'dest-2' }], }, { id: 'sp-look', dashboard_id: null, look_id: '30', enabled: true, scheduled_plan_destination: [{ id: 'dest-3' }], }, ]), }); const client = new LookerClient(params(), { sdkFactory: () => fakeSdk }); await expect(client.getSignals()).resolves.toEqual({ dashboardUsage: [ { contentId: '10', queryCount30d: 5, uniqueUsers30d: 2, lastRunAt: '2026-04-30', topUsers: ['user-1', 'user-2'], }, ], lookUsage: [ { contentId: '30', queryCount30d: 7, uniqueUsers30d: 1, lastRunAt: '2026-04-28', topUsers: ['user-1'], }, ], scheduledPlans: [ { contentId: '10', contentType: 'dashboard', isScheduled: true, scheduleCount: 1, recipientCount: 2, }, { contentId: '30', contentType: 'look', isScheduled: true, scheduleCount: 1, recipientCount: 1, }, ], favorites: [ { contentId: '10', contentType: 'dashboard', favoriteCount: 4 }, { contentId: '30', contentType: 'look', favoriteCount: 2 }, ], }); expect(runInlineQuery).toHaveBeenNthCalledWith( 1, expect.objectContaining({ result_format: 'json', body: expect.objectContaining({ model: 'system__activity', view: 'history', fields: ['dashboard.id', 'history.query_run_count', 'history.created_date', 'user.id'], }), }), ); expect(fakeSdk.search_scheduled_plans).toHaveBeenCalledWith( expect.objectContaining({ all_users: true, fields: 'id,dashboard_id,look_id,enabled,scheduled_plan_destination', limit: 500, offset: 0, sorts: 'id', }), ); }); it('retries a 429 response once using Retry-After seconds', async () => { const sleep = vi.fn().mockResolvedValue(undefined); const rateLimitError = new Error('rate limited'); Object.assign(rateLimitError, { statusCode: 429, headers: { 'retry-after': '2' } }); const fakeSdk = sdk({ search_dashboards: vi .fn() .mockRejectedValueOnce(rateLimitError) .mockResolvedValueOnce([{ id: '10' }]), }); const client = new LookerClient(params(), { sdkFactory: () => fakeSdk, sleep }); await expect(client.listDashboards()).resolves.toEqual([{ id: '10', updatedAt: null }]); expect(sleep).toHaveBeenCalledWith(2000); expect(fakeSdk.search_dashboards).toHaveBeenCalledTimes(2); }); it('does not retry non-429 errors', async () => { const sleep = vi.fn().mockResolvedValue(undefined); const error = new Error('broken dashboard'); Object.assign(error, { statusCode: 500 }); const fakeSdk = sdk({ dashboard: vi.fn().mockRejectedValue(error) }); const client = new LookerClient(params(), { sdkFactory: () => fakeSdk, sleep }); await expect(client.getDashboard('10')).rejects.toThrow('broken dashboard'); expect(sleep).not.toHaveBeenCalled(); expect(fakeSdk.dashboard).toHaveBeenCalledTimes(1); }); it('initializes the real @looker/sdk-node SDK with inline credentials without throwing', async () => { const client = new LookerClient(params()); const result = await client.testConnection(); // Without injected sdkFactory the real SDK is constructed via InlineLookerSettings. // This used to throw "Missing required configuration values like base_url" because // the parent NodeSettingsIniFile constructor validated config before the override // could supply credentials. Whatever happens now (auth/network failure against the // bogus example URL is fine) — what must NOT happen is a synchronous SDK-init throw. expect(result.success).toBe(false); expect(result.error).toBeDefined(); expect(result.error).not.toMatch(/Missing required configuration values/i); await client.cleanup(); }); it('strips trailing /api/4.0 from base_url so the SDK does not double-prefix it', async () => { const clientWithSuffix = new LookerClient({ base_url: 'https://example.looker.com/api/4.0', client_id: 'id', [clientSecretParam]: 'credential', // pragma: allowlist secret }); const result = await clientWithSuffix.testConnection(); expect(result.success).toBe(false); // If base_url is double-prefixed the SDK would hit /api/4.0/api/4.0/login. Either // the URL is correctly normalized (transport-level network failure) or we'd see a // 404/HTML response — either way the stack must not be a config-validation throw. expect(result.error).not.toMatch(/Missing required configuration values/i); await clientWithSuffix.cleanup(); }); });