1- import { describe , expect , test , beforeEach , beforeAll , afterAll } from "bun:test"
1+ import { describe , expect , test , beforeEach , afterEach , beforeAll , afterAll } from "bun:test"
22import * as Dispatcher from "../../src/altimate/native/dispatcher"
33
44// Disable telemetry via env var instead of mock.module
@@ -10,9 +10,10 @@ afterAll(() => { delete process.env.ALTIMATE_TELEMETRY_DISABLED })
1010// ---------------------------------------------------------------------------
1111
1212import * as Registry from "../../src/altimate/native/connections/registry"
13+ import { detectAuthMethod } from "../../src/altimate/native/connections/registry"
1314import * as CredentialStore from "../../src/altimate/native/connections/credential-store"
1415import { parseDbtProfiles } from "../../src/altimate/native/connections/dbt-profiles"
15- import { discoverContainers } from "../../src/altimate/native/connections/docker-discovery"
16+ import { discoverContainers , containerToConfig } from "../../src/altimate/native/connections/docker-discovery"
1617import { registerAll } from "../../src/altimate/native/connections/register"
1718
1819// ---------------------------------------------------------------------------
@@ -74,6 +75,132 @@ describe("ConnectionRegistry", () => {
7475 } )
7576} )
7677
78+ // ---------------------------------------------------------------------------
79+ // loadFromEnv — env-var-based connection config loading
80+ // ---------------------------------------------------------------------------
81+
82+ describe ( "loadFromEnv via Registry.load()" , ( ) => {
83+ const saved : Record < string , string | undefined > = { }
84+
85+ function setEnv ( key : string , value : string ) {
86+ saved [ key ] = process . env [ key ]
87+ process . env [ key ] = value
88+ }
89+
90+ beforeEach ( ( ) => {
91+ Registry . reset ( )
92+ } )
93+
94+ afterEach ( ( ) => {
95+ for ( const [ key , orig ] of Object . entries ( saved ) ) {
96+ if ( orig === undefined ) delete process . env [ key ]
97+ else process . env [ key ] = orig
98+ }
99+ for ( const key of Object . keys ( saved ) ) delete saved [ key ]
100+ } )
101+
102+ test ( "parses valid JSON from ALTIMATE_CODE_CONN_* env vars" , ( ) => {
103+ setEnv ( "ALTIMATE_CODE_CONN_MYDB" , JSON . stringify ( { type : "postgres" , host : "localhost" , port : 5432 } ) )
104+ Registry . load ( )
105+ const config = Registry . getConfig ( "mydb" )
106+ expect ( config ) . toBeDefined ( )
107+ expect ( config ?. type ) . toBe ( "postgres" )
108+ expect ( config ?. host ) . toBe ( "localhost" )
109+ } )
110+
111+ test ( "lowercases connection name from env var suffix" , ( ) => {
112+ setEnv ( "ALTIMATE_CODE_CONN_PROD_DB" , JSON . stringify ( { type : "snowflake" , account : "abc" } ) )
113+ Registry . load ( )
114+ expect ( Registry . getConfig ( "prod_db" ) ) . toBeDefined ( )
115+ expect ( Registry . getConfig ( "PROD_DB" ) ) . toBeUndefined ( )
116+ } )
117+
118+ test ( "ignores env var with invalid JSON" , ( ) => {
119+ setEnv ( "ALTIMATE_CODE_CONN_BAD" , "not-valid-json{" )
120+ Registry . load ( )
121+ expect ( Registry . getConfig ( "bad" ) ) . toBeUndefined ( )
122+ } )
123+
124+ test ( "ignores env var config without type field" , ( ) => {
125+ setEnv ( "ALTIMATE_CODE_CONN_NOTYPE" , JSON . stringify ( { host : "localhost" , port : 5432 } ) )
126+ Registry . load ( )
127+ expect ( Registry . getConfig ( "notype" ) ) . toBeUndefined ( )
128+ } )
129+
130+ test ( "ignores non-object JSON values (string, number, array)" , ( ) => {
131+ setEnv ( "ALTIMATE_CODE_CONN_STR" , JSON . stringify ( "just a string" ) )
132+ setEnv ( "ALTIMATE_CODE_CONN_NUM" , JSON . stringify ( 42 ) )
133+ setEnv ( "ALTIMATE_CODE_CONN_ARR" , JSON . stringify ( [ 1 , 2 , 3 ] ) )
134+ Registry . load ( )
135+ expect ( Registry . getConfig ( "str" ) ) . toBeUndefined ( )
136+ expect ( Registry . getConfig ( "num" ) ) . toBeUndefined ( )
137+ expect ( Registry . getConfig ( "arr" ) ) . toBeUndefined ( )
138+ } )
139+ } )
140+
141+ // ---------------------------------------------------------------------------
142+ // detectAuthMethod
143+ // ---------------------------------------------------------------------------
144+
145+ describe ( "detectAuthMethod" , ( ) => {
146+ test ( "returns 'connection_string' for config with connection_string" , ( ) => {
147+ expect ( detectAuthMethod ( { type : "postgres" , connection_string : "postgresql://..." } as any ) ) . toBe ( "connection_string" )
148+ } )
149+
150+ test ( "returns 'key_pair' for Snowflake private_key_path" , ( ) => {
151+ expect ( detectAuthMethod ( { type : "snowflake" , private_key_path : "/path/to/key.p8" } as any ) ) . toBe ( "key_pair" )
152+ } )
153+
154+ test ( "returns 'key_pair' for camelCase privateKeyPath" , ( ) => {
155+ expect ( detectAuthMethod ( { type : "snowflake" , privateKeyPath : "/path/to/key.p8" } as any ) ) . toBe ( "key_pair" )
156+ } )
157+
158+ test ( "returns 'sso' for Snowflake externalbrowser" , ( ) => {
159+ expect ( detectAuthMethod ( { type : "snowflake" , authenticator : "EXTERNALBROWSER" } as any ) ) . toBe ( "sso" )
160+ } )
161+
162+ test ( "returns 'sso' for Okta URL authenticator" , ( ) => {
163+ expect ( detectAuthMethod ( { type : "snowflake" , authenticator : "https://myorg.okta.com" } as any ) ) . toBe ( "sso" )
164+ } )
165+
166+ test ( "returns 'oauth' for OAuth authenticator" , ( ) => {
167+ expect ( detectAuthMethod ( { type : "snowflake" , authenticator : "OAUTH" } as any ) ) . toBe ( "oauth" )
168+ } )
169+
170+ test ( "returns 'token' for access_token" , ( ) => {
171+ expect ( detectAuthMethod ( { type : "databricks" , access_token : "dapi..." } as any ) ) . toBe ( "token" )
172+ } )
173+
174+ test ( "returns 'password' for config with password" , ( ) => {
175+ expect ( detectAuthMethod ( { type : "postgres" , password : "secret" } as any ) ) . toBe ( "password" )
176+ } )
177+
178+ test ( "returns 'file' for duckdb" , ( ) => {
179+ expect ( detectAuthMethod ( { type : "duckdb" , path : "/data/my.db" } as any ) ) . toBe ( "file" )
180+ } )
181+
182+ test ( "returns 'file' for sqlite" , ( ) => {
183+ expect ( detectAuthMethod ( { type : "sqlite" , path : "/data/my.sqlite" } as any ) ) . toBe ( "file" )
184+ } )
185+
186+ test ( "returns 'connection_string' for mongodb without password" , ( ) => {
187+ expect ( detectAuthMethod ( { type : "mongodb" } as any ) ) . toBe ( "connection_string" )
188+ } )
189+
190+ test ( "returns 'password' for mongo with password" , ( ) => {
191+ expect ( detectAuthMethod ( { type : "mongo" , password : "secret" } as any ) ) . toBe ( "password" )
192+ } )
193+
194+ test ( "returns 'unknown' for null/undefined" , ( ) => {
195+ expect ( detectAuthMethod ( null ) ) . toBe ( "unknown" )
196+ expect ( detectAuthMethod ( undefined ) ) . toBe ( "unknown" )
197+ } )
198+
199+ test ( "returns 'unknown' for empty config with no identifiable auth" , ( ) => {
200+ expect ( detectAuthMethod ( { type : "postgres" } as any ) ) . toBe ( "unknown" )
201+ } )
202+ } )
203+
77204// ---------------------------------------------------------------------------
78205// CredentialStore (keytar not available in test environment)
79206// ---------------------------------------------------------------------------
@@ -135,6 +262,36 @@ describe("CredentialStore", () => {
135262 expect ( sanitized . oauth_client_secret ) . toBeUndefined ( )
136263 expect ( sanitized . authenticator ) . toBe ( "oauth" )
137264 } )
265+
266+ test ( "saveConnection strips all sensitive fields from complex config" , async ( ) => {
267+ const config = {
268+ type : "snowflake" ,
269+ account : "abc123" ,
270+ user : "svc_user" ,
271+ password : "pw123" ,
272+ private_key : "-----BEGIN PRIVATE KEY-----" ,
273+ private_key_passphrase : "passphrase" ,
274+ token : "oauth-token" ,
275+ oauth_client_secret : "client-secret" ,
276+ ssh_password : "ssh-pw" ,
277+ connection_string : "mongodb://..." ,
278+ } as any
279+ const { sanitized, warnings } = await CredentialStore . saveConnection ( "complex" , config )
280+
281+ expect ( sanitized . password ) . toBeUndefined ( )
282+ expect ( sanitized . private_key ) . toBeUndefined ( )
283+ expect ( sanitized . private_key_passphrase ) . toBeUndefined ( )
284+ expect ( sanitized . token ) . toBeUndefined ( )
285+ expect ( sanitized . oauth_client_secret ) . toBeUndefined ( )
286+ expect ( sanitized . ssh_password ) . toBeUndefined ( )
287+ expect ( sanitized . connection_string ) . toBeUndefined ( )
288+
289+ expect ( sanitized . type ) . toBe ( "snowflake" )
290+ expect ( sanitized . account ) . toBe ( "abc123" )
291+ expect ( sanitized . user ) . toBe ( "svc_user" )
292+
293+ expect ( warnings ) . toHaveLength ( 7 )
294+ } )
138295} )
139296
140297// ---------------------------------------------------------------------------
@@ -261,6 +418,122 @@ snow:
261418 fs . rmSync ( tmpDir , { recursive : true } )
262419 }
263420 } )
421+
422+ // altimate_change start — tests for untested dbt profiles parser edge cases
423+ test ( "resolves env_var with default fallback when env var is missing" , async ( ) => {
424+ const fs = await import ( "fs" )
425+ const os = await import ( "os" )
426+ const path = await import ( "path" )
427+
428+ delete process . env . __TEST_DBT_MISSING_VAR_12345
429+
430+ const tmpDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "dbt-test-" ) )
431+ const profilesPath = path . join ( tmpDir , "profiles.yml" )
432+
433+ fs . writeFileSync (
434+ profilesPath ,
435+ `
436+ myproject:
437+ outputs:
438+ dev:
439+ type: postgres
440+ host: "{{ env_var('__TEST_DBT_MISSING_VAR_12345', 'localhost') }}"
441+ port: 5432
442+ user: "{{ env_var('__TEST_DBT_MISSING_USER_12345', 'default_user') }}"
443+ password: secret
444+ dbname: mydb
445+ ` ,
446+ )
447+
448+ try {
449+ const connections = await parseDbtProfiles ( profilesPath )
450+ expect ( connections ) . toHaveLength ( 1 )
451+ expect ( connections [ 0 ] . config . host ) . toBe ( "localhost" )
452+ expect ( connections [ 0 ] . config . user ) . toBe ( "default_user" )
453+ } finally {
454+ fs . rmSync ( tmpDir , { recursive : true } )
455+ }
456+ } )
457+
458+ test ( "skips 'config' top-level key (dbt global settings)" , async ( ) => {
459+ const fs = await import ( "fs" )
460+ const os = await import ( "os" )
461+ const path = await import ( "path" )
462+
463+ const tmpDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "dbt-test-" ) )
464+ const profilesPath = path . join ( tmpDir , "profiles.yml" )
465+
466+ fs . writeFileSync (
467+ profilesPath ,
468+ `
469+ config:
470+ send_anonymous_usage_stats: false
471+ use_colors: true
472+
473+ real_project:
474+ outputs:
475+ dev:
476+ type: postgres
477+ host: localhost
478+ dbname: analytics
479+ ` ,
480+ )
481+
482+ try {
483+ const connections = await parseDbtProfiles ( profilesPath )
484+ expect ( connections ) . toHaveLength ( 1 )
485+ expect ( connections [ 0 ] . name ) . toBe ( "real_project_dev" )
486+ expect ( connections . find ( ( c ) => c . name . startsWith ( "config" ) ) ) . toBeUndefined ( )
487+ } finally {
488+ fs . rmSync ( tmpDir , { recursive : true } )
489+ }
490+ } )
491+
492+ test ( "handles multiple profiles with multiple outputs" , async ( ) => {
493+ const fs = await import ( "fs" )
494+ const os = await import ( "os" )
495+ const path = await import ( "path" )
496+
497+ const tmpDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "dbt-test-" ) )
498+ const profilesPath = path . join ( tmpDir , "profiles.yml" )
499+
500+ fs . writeFileSync (
501+ profilesPath ,
502+ `
503+ warehouse_a:
504+ outputs:
505+ dev:
506+ type: postgres
507+ host: localhost
508+ dbname: dev_db
509+ prod:
510+ type: postgres
511+ host: prod.example.com
512+ dbname: prod_db
513+
514+ warehouse_b:
515+ outputs:
516+ staging:
517+ type: snowflake
518+ account: abc123
519+ user: admin
520+ password: pw
521+ database: STAGING
522+ warehouse: COMPUTE_WH
523+ schema: PUBLIC
524+ ` ,
525+ )
526+
527+ try {
528+ const connections = await parseDbtProfiles ( profilesPath )
529+ expect ( connections ) . toHaveLength ( 3 )
530+ const names = connections . map ( ( c ) => c . name ) . sort ( )
531+ expect ( names ) . toEqual ( [ "warehouse_a_dev" , "warehouse_a_prod" , "warehouse_b_staging" ] )
532+ } finally {
533+ fs . rmSync ( tmpDir , { recursive : true } )
534+ }
535+ } )
536+ // altimate_change end
264537} )
265538
266539// ---------------------------------------------------------------------------
@@ -272,6 +545,28 @@ describe("Docker discovery", () => {
272545 const containers = await discoverContainers ( )
273546 expect ( containers ) . toEqual ( [ ] )
274547 } )
548+
549+ test ( "containerToConfig omits undefined optional fields" , ( ) => {
550+ const container = {
551+ container_id : "def456" ,
552+ name : "mysql_dev" ,
553+ image : "mysql:8" ,
554+ db_type : "mysql" ,
555+ host : "127.0.0.1" ,
556+ port : 3306 ,
557+ user : undefined as string | undefined ,
558+ password : undefined as string | undefined ,
559+ database : undefined as string | undefined ,
560+ status : "running" ,
561+ }
562+ const config = containerToConfig ( container as any )
563+ expect ( config . type ) . toBe ( "mysql" )
564+ expect ( config . host ) . toBe ( "127.0.0.1" )
565+ expect ( config . port ) . toBe ( 3306 )
566+ expect ( config . user ) . toBeUndefined ( )
567+ expect ( config . password ) . toBeUndefined ( )
568+ expect ( config . database ) . toBeUndefined ( )
569+ } )
275570} )
276571
277572// ---------------------------------------------------------------------------
0 commit comments