Skip to content

Commit dad1448

Browse files
authored
chore: update deps and fix linter issues (#54)
* chore: update deps and fix linter issues * test: add unit tests to improve coverage * fix: replace deprecated reflect.Ptr with reflect.Pointer * test: skip signal handling tests on Windows
1 parent fa3ad84 commit dad1448

9 files changed

Lines changed: 572 additions & 78 deletions

File tree

cmd/xsql/command_unit_test.go

Lines changed: 271 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ import (
66
"encoding/json"
77
"os"
88
"path/filepath"
9+
"runtime"
10+
"syscall"
911
"testing"
12+
"time"
1013

1114
"github.com/modelcontextprotocol/go-sdk/mcp"
1215

@@ -134,7 +137,7 @@ func TestRunQuery_MissingDB(t *testing.T) {
134137

135138
var out bytes.Buffer
136139
w := output.New(&out, &bytes.Buffer{})
137-
err := runQuery(nil, []string{"select 1"}, &QueryFlags{}, &w)
140+
err := runQuery([]string{"select 1"}, &QueryFlags{}, &w)
138141
if err == nil {
139142
t.Fatal("expected error for missing db type")
140143
}
@@ -149,7 +152,7 @@ func TestRunQuery_UnsupportedDriver(t *testing.T) {
149152

150153
var out bytes.Buffer
151154
w := output.New(&out, &bytes.Buffer{})
152-
err := runQuery(nil, []string{"select 1"}, &QueryFlags{}, &w)
155+
err := runQuery([]string{"select 1"}, &QueryFlags{}, &w)
153156
if err == nil {
154157
t.Fatal("expected error for unsupported driver")
155158
}
@@ -168,7 +171,7 @@ func TestRunQuery_PlaintextPasswordNotAllowed(t *testing.T) {
168171

169172
var out bytes.Buffer
170173
w := output.New(&out, &bytes.Buffer{})
171-
err := runQuery(nil, []string{"select 1"}, &QueryFlags{}, &w)
174+
err := runQuery([]string{"select 1"}, &QueryFlags{}, &w)
172175
if err == nil {
173176
t.Fatal("expected error for plaintext password not allowed")
174177
}
@@ -183,7 +186,7 @@ func TestRunSchemaDump_UnsupportedDriver(t *testing.T) {
183186

184187
var out bytes.Buffer
185188
w := output.New(&out, &bytes.Buffer{})
186-
err := runSchemaDump(nil, nil, &SchemaFlags{}, &w)
189+
err := runSchemaDump(&SchemaFlags{}, &w)
187190
if err == nil {
188191
t.Fatal("expected error for unsupported driver")
189192
}
@@ -202,7 +205,7 @@ func TestRunSchemaDump_PlaintextPasswordNotAllowed(t *testing.T) {
202205

203206
var out bytes.Buffer
204207
w := output.New(&out, &bytes.Buffer{})
205-
err := runSchemaDump(nil, nil, &SchemaFlags{}, &w)
208+
err := runSchemaDump(&SchemaFlags{}, &w)
206209
if err == nil {
207210
t.Fatal("expected error for plaintext password not allowed")
208211
}
@@ -217,7 +220,7 @@ func TestRunQuery_InvalidFormat(t *testing.T) {
217220

218221
var out bytes.Buffer
219222
w := output.New(&out, &bytes.Buffer{})
220-
err := runQuery(nil, []string{"select 1"}, &QueryFlags{}, &w)
223+
err := runQuery([]string{"select 1"}, &QueryFlags{}, &w)
221224
if err == nil {
222225
t.Fatal("expected error for invalid format")
223226
}
@@ -232,7 +235,7 @@ func TestRunSchemaDump_MissingDB(t *testing.T) {
232235

233236
var out bytes.Buffer
234237
w := output.New(&out, &bytes.Buffer{})
235-
err := runSchemaDump(nil, nil, &SchemaFlags{}, &w)
238+
err := runSchemaDump(&SchemaFlags{}, &w)
236239
if err == nil {
237240
t.Fatal("expected error for missing db type")
238241
}
@@ -247,7 +250,7 @@ func TestRunSchemaDump_InvalidFormat(t *testing.T) {
247250

248251
var out bytes.Buffer
249252
w := output.New(&out, &bytes.Buffer{})
250-
err := runSchemaDump(nil, nil, &SchemaFlags{}, &w)
253+
err := runSchemaDump(&SchemaFlags{}, &w)
251254
if err == nil {
252255
t.Fatal("expected error for invalid format")
253256
}
@@ -348,7 +351,7 @@ func TestRunProxy_SSHConnectError(t *testing.T) {
348351
}
349352

350353
func TestResolveSSH_NoConfig(t *testing.T) {
351-
client, err := app.ResolveSSH(nil, config.Profile{}, false, false)
354+
client, err := app.ResolveSSH(context.TODO(), config.Profile{}, false, false)
352355
if err != nil {
353356
t.Fatalf("unexpected error: %v", err)
354357
}
@@ -909,3 +912,262 @@ func TestFirstNonEmpty(t *testing.T) {
909912
func configProfile(dbType string) config.Profile {
910913
return config.Profile{DB: dbType}
911914
}
915+
916+
func TestHandlePortConflict_NonTTY(t *testing.T) {
917+
_, err := handlePortConflict(3306, "127.0.0.1")
918+
if err == nil {
919+
t.Fatal("expected error for non-TTY port conflict")
920+
}
921+
if err.Code != errors.CodePortInUse {
922+
t.Fatalf("expected CodePortInUse, got %s", err.Code)
923+
}
924+
}
925+
926+
func TestModeForWebCommand(t *testing.T) {
927+
if got := modeForWebCommand(true); got != "web" {
928+
t.Fatalf("expected 'web', got %q", got)
929+
}
930+
if got := modeForWebCommand(false); got != "serve" {
931+
t.Fatalf("expected 'serve', got %q", got)
932+
}
933+
}
934+
935+
func TestResolveWebOptions_InvalidAddr(t *testing.T) {
936+
_, xe := resolveWebOptions(&webCommandOptions{
937+
addr: "not-a-valid-addr",
938+
addrSet: true,
939+
}, config.File{})
940+
if xe == nil {
941+
t.Fatal("expected error for invalid addr")
942+
}
943+
if xe.Code != errors.CodeCfgInvalid {
944+
t.Fatalf("expected CodeCfgInvalid, got %s", xe.Code)
945+
}
946+
}
947+
948+
func TestResolveWebOptions_EnvVars(t *testing.T) {
949+
t.Setenv("XSQL_WEB_HTTP_AUTH_TOKEN", "env-token")
950+
resolved, xe := resolveWebOptions(&webCommandOptions{
951+
addr: "0.0.0.0:9999",
952+
addrSet: true,
953+
}, config.File{})
954+
if xe != nil {
955+
t.Fatalf("unexpected error: %v", xe)
956+
}
957+
if resolved.authToken != "env-token" {
958+
t.Fatalf("expected env-token, got %s", resolved.authToken)
959+
}
960+
if !resolved.authRequired {
961+
t.Fatal("expected authRequired=true")
962+
}
963+
}
964+
965+
func TestResolveMCPServerOptions_HttpAddrEnv(t *testing.T) {
966+
t.Setenv("XSQL_MCP_TRANSPORT", "streamable_http")
967+
t.Setenv("XSQL_MCP_HTTP_AUTH_TOKEN", "token")
968+
t.Setenv("XSQL_MCP_HTTP_ADDR", "127.0.0.1:5555")
969+
cfg := config.File{
970+
Profiles: map[string]config.Profile{},
971+
SSHProxies: map[string]config.SSHProxy{},
972+
}
973+
resolved, xe := resolveMCPServerOptions(&mcpServerOptions{}, cfg)
974+
if xe != nil {
975+
t.Fatalf("unexpected error: %v", xe)
976+
}
977+
if resolved.httpAddr != "127.0.0.1:5555" {
978+
t.Fatalf("expected 127.0.0.1:5555, got %s", resolved.httpAddr)
979+
}
980+
}
981+
982+
func TestRunMCPServer_InvalidConfigPath(t *testing.T) {
983+
GlobalConfig.ConfigStr = "/nonexistent/path/config.yaml"
984+
err := runMCPServer(&mcpServerOptions{})
985+
if err == nil {
986+
t.Fatal("expected error for nonexistent config")
987+
}
988+
}
989+
990+
func TestRunMCPServer_StreamableHTTPStarts(t *testing.T) {
991+
if runtime.GOOS == "windows" {
992+
t.Skip("skipping signal handling test on Windows")
993+
}
994+
configPath := filepath.Join(t.TempDir(), "xsql.yaml")
995+
if err := os.WriteFile(configPath, []byte("profiles: {}\nmcp:\n transport: streamable_http\n http:\n addr: 127.0.0.1:0\n auth_token: test-token\n allow_plaintext_token: true\n"), 0644); err != nil {
996+
t.Fatalf("failed to write config: %v", err)
997+
}
998+
999+
GlobalConfig.ConfigStr = configPath
1000+
1001+
// This will start the HTTP server and then we need to stop it
1002+
// We'll use a goroutine to run it and cancel after a short time
1003+
done := make(chan error, 1)
1004+
go func() {
1005+
done <- runMCPServer(&mcpServerOptions{})
1006+
}()
1007+
1008+
// Give the server time to start
1009+
time.Sleep(100 * time.Millisecond)
1010+
1011+
// The server should be running, send SIGINT to stop it
1012+
p, _ := os.FindProcess(os.Getpid())
1013+
_ = p.Signal(syscall.SIGINT)
1014+
1015+
select {
1016+
case err := <-done:
1017+
if err != nil {
1018+
t.Fatalf("unexpected error: %v", err)
1019+
}
1020+
case <-time.After(5 * time.Second):
1021+
t.Fatal("timeout waiting for server to stop")
1022+
}
1023+
}
1024+
1025+
func TestNewServeCommand(t *testing.T) {
1026+
var out bytes.Buffer
1027+
w := output.New(&out, &bytes.Buffer{})
1028+
cmd := NewServeCommand(&w)
1029+
if cmd.Use != "serve" {
1030+
t.Fatalf("expected 'serve', got %s", cmd.Use)
1031+
}
1032+
}
1033+
1034+
func TestNewWebCommand(t *testing.T) {
1035+
var out bytes.Buffer
1036+
w := output.New(&out, &bytes.Buffer{})
1037+
cmd := NewWebCommand(&w)
1038+
if cmd.Use != "web" {
1039+
t.Fatalf("expected 'web', got %s", cmd.Use)
1040+
}
1041+
}
1042+
1043+
func TestNewMCPCommand(t *testing.T) {
1044+
cmd := NewMCPCommand()
1045+
if cmd.Use != "mcp" {
1046+
t.Fatalf("expected 'mcp', got %s", cmd.Use)
1047+
}
1048+
}
1049+
1050+
func TestNewProfileCommand(t *testing.T) {
1051+
var out bytes.Buffer
1052+
w := output.New(&out, &bytes.Buffer{})
1053+
cmd := NewProfileCommand(&w)
1054+
if cmd.Use != "profile" {
1055+
t.Fatalf("expected 'profile', got %s", cmd.Use)
1056+
}
1057+
}
1058+
1059+
func TestNewSchemaCommand(t *testing.T) {
1060+
var out bytes.Buffer
1061+
w := output.New(&out, &bytes.Buffer{})
1062+
cmd := NewSchemaCommand(&w)
1063+
if cmd.Use != "schema" {
1064+
t.Fatalf("expected 'schema', got %s", cmd.Use)
1065+
}
1066+
}
1067+
1068+
func TestResolveWebOptions_NilOpts(t *testing.T) {
1069+
resolved, xe := resolveWebOptions(nil, config.File{})
1070+
if xe != nil {
1071+
t.Fatalf("unexpected error: %v", xe)
1072+
}
1073+
if resolved.addr != "127.0.0.1:8788" {
1074+
t.Fatalf("expected default addr, got %s", resolved.addr)
1075+
}
1076+
}
1077+
1078+
func TestResolveWebOptions_ConfigAddr(t *testing.T) {
1079+
resolved, xe := resolveWebOptions(&webCommandOptions{}, config.File{
1080+
Web: config.WebConfig{
1081+
HTTP: config.WebHTTPConfig{
1082+
Addr: "127.0.0.1:9999",
1083+
},
1084+
},
1085+
})
1086+
if xe != nil {
1087+
t.Fatalf("unexpected error: %v", xe)
1088+
}
1089+
if resolved.addr != "127.0.0.1:9999" {
1090+
t.Fatalf("expected 127.0.0.1:9999, got %s", resolved.addr)
1091+
}
1092+
}
1093+
1094+
func TestResolveWebOptions_NonLoopbackRequiresToken(t *testing.T) {
1095+
_, xe := resolveWebOptions(&webCommandOptions{
1096+
addr: "10.0.0.1:8788",
1097+
addrSet: true,
1098+
}, config.File{})
1099+
if xe == nil {
1100+
t.Fatal("expected error for non-loopback without token")
1101+
}
1102+
if xe.Code != errors.CodeCfgInvalid {
1103+
t.Fatalf("expected CodeCfgInvalid, got %s", xe.Code)
1104+
}
1105+
}
1106+
1107+
func TestRunProxy_InvalidFormat(t *testing.T) {
1108+
prev := GlobalConfig
1109+
GlobalConfig = &Config{ProfileStr: "dev", FormatStr: "invalid"}
1110+
t.Cleanup(func() { GlobalConfig = prev })
1111+
1112+
GlobalConfig.Resolved.Profile = config.Profile{DB: "mysql", SSHConfig: &config.SSHProxy{Host: "h", Port: 22, User: "u"}}
1113+
1114+
var out bytes.Buffer
1115+
w := output.New(&out, &bytes.Buffer{})
1116+
err := runProxy(nil, &ProxyFlags{}, &w)
1117+
if err == nil {
1118+
t.Fatal("expected error for invalid format")
1119+
}
1120+
}
1121+
1122+
func TestRunProxy_PlaintextNotAllowed(t *testing.T) {
1123+
prev := GlobalConfig
1124+
GlobalConfig = &Config{ProfileStr: "dev", FormatStr: "json"}
1125+
t.Cleanup(func() { GlobalConfig = prev })
1126+
1127+
GlobalConfig.Resolved.Profile = config.Profile{
1128+
DB: "mysql",
1129+
Password: "plain",
1130+
AllowPlaintext: false,
1131+
SSHConfig: &config.SSHProxy{Host: "h", Port: 22, User: "u"},
1132+
}
1133+
1134+
var out bytes.Buffer
1135+
w := output.New(&out, &bytes.Buffer{})
1136+
err := runProxy(nil, &ProxyFlags{}, &w)
1137+
if err == nil {
1138+
t.Fatal("expected error for plaintext not allowed")
1139+
}
1140+
}
1141+
1142+
func TestResolveProxyPort_AllPaths(t *testing.T) {
1143+
// Test with config port and no CLI flag
1144+
cmd := NewProxyCommand(nil)
1145+
port, fromConfig := resolveProxyPort(cmd, &ProxyFlags{}, 5555)
1146+
if port != 5555 || !fromConfig {
1147+
t.Errorf("expected port=5555, fromConfig=true, got port=%d, fromConfig=%v", port, fromConfig)
1148+
}
1149+
1150+
// Test with zero config port
1151+
port, fromConfig = resolveProxyPort(cmd, &ProxyFlags{}, 0)
1152+
if port != 0 || fromConfig {
1153+
t.Errorf("expected port=0, fromConfig=false, got port=%d, fromConfig=%v", port, fromConfig)
1154+
}
1155+
}
1156+
1157+
func TestNewQueryCommand(t *testing.T) {
1158+
var out bytes.Buffer
1159+
w := output.New(&out, &bytes.Buffer{})
1160+
cmd := NewQueryCommand(&w)
1161+
if cmd.Use != "query [SQL]" {
1162+
t.Fatalf("expected 'query [SQL]', got %s", cmd.Use)
1163+
}
1164+
}
1165+
1166+
func TestNewProxyCommand(t *testing.T) {
1167+
var out bytes.Buffer
1168+
w := output.New(&out, &bytes.Buffer{})
1169+
cmd := NewProxyCommand(&w)
1170+
if cmd.Use != "proxy [flags]" {
1171+
t.Fatalf("expected 'proxy [flags]', got %s", cmd.Use)
1172+
}
1173+
}

cmd/xsql/query.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func NewQueryCommand(w *output.Writer) *cobra.Command {
3131
Args: cobra.ExactArgs(1),
3232
RunE: func(cmd *cobra.Command, args []string) error {
3333
flags.QueryTimeoutSet = cmd.Flags().Changed("query-timeout")
34-
return runQuery(cmd, args, flags, w)
34+
return runQuery(args, flags, w)
3535
},
3636
}
3737

@@ -44,7 +44,7 @@ func NewQueryCommand(w *output.Writer) *cobra.Command {
4444
}
4545

4646
// runQuery executes a SQL query
47-
func runQuery(cmd *cobra.Command, args []string, flags *QueryFlags, w *output.Writer) error {
47+
func runQuery(args []string, flags *QueryFlags, w *output.Writer) error {
4848
sql := args[0]
4949
format, err := parseOutputFormat(GlobalConfig.FormatStr)
5050
if err != nil {

cmd/xsql/root.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ func NewRootCommand() *cobra.Command {
5050
CLIFormatSet: formatSet,
5151
EnvProfile: os.Getenv("XSQL_PROFILE"),
5252
EnvFormat: os.Getenv("XSQL_FORMAT"),
53-
WorkDir: "",
54-
HomeDir: "",
53+
WorkDir: os.Getenv("XSQL_WORKDIR"),
54+
HomeDir: os.Getenv("XSQL_HOMEDIR"),
5555
})
5656
if xe != nil {
5757
return xe

0 commit comments

Comments
 (0)