11import assert from "node:assert/strict" ;
2+ import { once } from "node:events" ;
23import { mkdtemp , readFile , rm , writeFile } from "node:fs/promises" ;
4+ import { createServer } from "node:net" ;
35import os from "node:os" ;
46import path from "node:path" ;
57import test from "node:test" ;
@@ -11,6 +13,132 @@ import { systemFontInternals } from "../src/apps/system-fonts.ts";
1113
1214const projectRoot = path . resolve ( path . dirname ( fileURLToPath ( import . meta. url ) ) , "../.." ) ;
1315
16+ test (
17+ "installed DBX initiates a database connection through the plugin launcher" ,
18+ {
19+ skip : ! process . env . JMS_TEST_DBX_EXECUTABLE
20+ } ,
21+ async ( context ) => {
22+ const appData = await mkdtemp ( path . join ( os . tmpdir ( ) , "jms-dbx-native-" ) ) ;
23+ context . after ( ( ) => rm ( appData , { recursive : true , force : true } ) ) ;
24+ const server = createServer ( ( socket ) => socket . destroy ( ) ) ;
25+ context . after ( ( ) => server . close ( ) ) ;
26+ server . listen ( 0 , "127.0.0.1" ) ;
27+ await once ( server , "listening" ) ;
28+ const app = { getPath : ( ) => appData , isPackaged : false } ;
29+ const service = new ApplicationConfigService ( app , projectRoot ) ;
30+ await service . initialize ( ) ;
31+ await service . updateSelection ( {
32+ category : "databases" ,
33+ protocol : "mysql" ,
34+ name : "dbx" ,
35+ pluginId : undefined ,
36+ path : process . env . JMS_TEST_DBX_EXECUTABLE
37+ } ) ;
38+ const received = once ( server , "connection" , { signal : AbortSignal . timeout ( 8000 ) } ) ;
39+ const payload = {
40+ protocol : "mysql" ,
41+ client : "dbx" ,
42+ name : `Luna DBX connection probe ${ Date . now ( ) } ` ,
43+ endpoint : { host : "127.0.0.1" , port : ( server . address ( ) as { port : number } ) . port } ,
44+ token : { id : "luna-probe" , value : "test-only" } ,
45+ asset : { info : { db_name : "probe" } }
46+ } ;
47+ const launcher = new LocalApplicationLauncher ( app , projectRoot , service , null ) ;
48+ await Promise . all ( [ received , launcher . launch ( `jms2://${ Buffer . from ( JSON . stringify ( payload ) ) . toString ( "base64" ) } ` ) ] ) ;
49+ }
50+ ) ;
51+
52+ for ( const platform of [ "macos" , "linux" , "windows" ] ) {
53+ test ( `${ platform } defaults to DBX and launches encoded, one-time database connections` , async ( context ) => {
54+ const appData = await mkdtemp ( path . join ( os . tmpdir ( ) , "jms-dbx-" ) ) ;
55+ context . after ( ( ) => rm ( appData , { recursive : true , force : true } ) ) ;
56+ const app = { getPath : ( ) => appData , isPackaged : false } ;
57+ const service = new ApplicationConfigService ( app , projectRoot ) ;
58+ service . builtInDir = async ( ) => path . join ( projectRoot , "plugins" , platform ) ;
59+ await service . initialize ( ) ;
60+ const config = await service . getConfig ( ) ;
61+ const dbx = config . databases . find ( ( item ) => item . name === "dbx" ) ;
62+ assert . ok ( dbx ) ;
63+ assert . ok ( config . databases . some ( ( item ) => item . name === "dbeaver" ) ) ;
64+ assert . equal ( dbx . launch_type , "args" ) ;
65+ assert . equal ( dbx . executable_type , "user_path" ) ;
66+ const types = {
67+ mysql : "mysql" ,
68+ mariadb : "mysql" ,
69+ postgresql : "postgres" ,
70+ oracle : "oracle" ,
71+ sqlserver : "mssql" ,
72+ dameng : "dm" ,
73+ clickhouse : "clickhouse" ,
74+ mongodb : "mongodb" ,
75+ redis : "redis"
76+ } ;
77+ assert . deepEqual ( new Set ( dbx . protocol ) , new Set ( Object . keys ( types ) ) ) ;
78+ assert . deepEqual ( dbx . match_first , dbx . protocol ) ;
79+ const launcher = new LocalApplicationLauncher ( app , projectRoot , service , null ) ;
80+ for ( const [ protocol , type ] of Object . entries ( types ) ) {
81+ const payload = {
82+ protocol,
83+ name : "测试 DBX &连接" ,
84+ endpoint : { host : "2001:db8::1" , port : 5525 } ,
85+ token : { id : "token&+/*'\"\\中文" , value : " secret &one_time=false+?#%/*'\"\\中文 " , protocol } ,
86+ asset : { info : { db_name : protocol === "redis" ? "0" : "库 名&+/*'\"\\" } }
87+ } ;
88+ let launches = 0 ;
89+ launcher . launchExecutable = async ( application , argumentString ) => {
90+ assert . equal ( application . name , "dbx" ) ;
91+ const args = localAppLauncherInternals . splitArguments ( argumentString ) ;
92+ assert . equal ( args . length , 1 ) ;
93+ const url = new URL ( args [ 0 ] ) ;
94+ assert . equal ( url . protocol , "dbx:" ) ;
95+ assert . equal ( url . hostname , "connection" ) ;
96+ assert . equal ( url . pathname , "/new" ) ;
97+ assert . deepEqual ( Object . fromEntries ( url . searchParams ) , {
98+ name : "测试DBX&连接" ,
99+ type,
100+ host : payload . endpoint . host ,
101+ port : "5525" ,
102+ user : payload . token . id ,
103+ password : payload . token . value ,
104+ database : protocol === "oracle" ? payload . token . id : payload . asset . info . db_name ,
105+ one_time : "true" ,
106+ ...( protocol === "mongodb" ? { url_params : "authSource=admin&loadBalanced=true&retryWrites=false" } : { } )
107+ } ) ;
108+ launches ++ ;
109+ } ;
110+ for ( const client of [ undefined , "dbx" ] ) {
111+ await launcher . launch ( `jms2://${ Buffer . from ( JSON . stringify ( { ...payload , client } ) ) . toString ( "base64" ) } ` ) ;
112+ }
113+ if ( protocol === "mariadb" ) {
114+ await launcher . launch (
115+ `jms2://${ Buffer . from ( JSON . stringify ( { ...payload , protocol : "mysql" } ) ) . toString ( "base64" ) } `
116+ ) ;
117+ }
118+ assert . equal ( launches , protocol === "mariadb" ? 3 : 2 ) ;
119+ }
120+
121+ // Upgrades retain saved DBeaver paths, other defaults, and explicitly disabled protocols.
122+ const preferences = {
123+ version : 1 ,
124+ selections : {
125+ "databases:mysql" : `${ platform } .dbeaver` ,
126+ "databases:postgresql" : `${ platform } .${ platform === "windows" ? "navicat17" : "terminal-db" } ` ,
127+ "databases:redis" : ""
128+ } ,
129+ enabled_selections : { "databases:mysql" : [ `${ platform } .dbeaver` ] } ,
130+ plugins : { [ `${ platform } .dbeaver` ] : { path : process . execPath , enabled : true } }
131+ } ;
132+ await service . saveState ( preferences ) ;
133+ const state = await service . loadState ( ) ;
134+ for ( const [ key , value ] of Object . entries ( preferences . selections ) ) assert . equal ( state . selections [ key ] , value ) ;
135+ assert . deepEqual ( state . plugins , preferences . plugins ) ;
136+ assert . deepEqual ( state . enabled_selections , preferences . enabled_selections ) ;
137+ assert . equal ( ( await launcher . resolveApplication ( { protocol : "mysql" } ) ) . name , "dbeaver" ) ;
138+ await assert . rejects ( launcher . resolveApplication ( { protocol : "redis" } ) , / n o c o n f i g u r e d a p p l i c a t i o n / ) ;
139+ } ) ;
140+ }
141+
14142for ( const [ platform , protocol , expectedClient ] of [
15143 [ "macos" , "ssh" , "terminal" ] ,
16144 [ "linux" , "ssh" , "terminal" ] ,
0 commit comments