@@ -19,9 +19,11 @@ import (
1919 "os"
2020 "strings"
2121 "testing"
22- "time"
22+ "time"
23+ "path/filepath"
2324
2425 "github.com/altinity/altinity-mcp/pkg/config"
26+ "github.com/altinity/altinity-mcp/pkg/clickhouse"
2527 "github.com/altinity/altinity-mcp/pkg/jwe_auth"
2628 altinitymcp "github.com/altinity/altinity-mcp/pkg/server"
2729 "github.com/stretchr/testify/require"
@@ -358,6 +360,174 @@ MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7d7Qj8fKjKjKjKjKjKjK
358360 })
359361}
360362
363+ // setupClickHouseContainerMain is a local helper for this package's tests
364+ func setupClickHouseContainerMain (t * testing.T ) * config.ClickHouseConfig {
365+ t .Helper ()
366+ ctx := context .Background ()
367+
368+ req := testcontainers.ContainerRequest {
369+ Image : "clickhouse/clickhouse-server:latest" ,
370+ ExposedPorts : []string {"8123/tcp" , "9000/tcp" },
371+ Env : map [string ]string {
372+ "CLICKHOUSE_SKIP_USER_SETUP" : "1" ,
373+ "CLICKHOUSE_DB" : "default" ,
374+ "CLICKHOUSE_USER" : "default" ,
375+ "CLICKHOUSE_PASSWORD" : "" ,
376+ "CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT" : "1" ,
377+ },
378+ WaitingFor : wait .ForHTTP ("/" ).WithPort ("8123/tcp" ).WithStartupTimeout (30 * time .Second ).WithPollInterval (2 * time .Second ),
379+ }
380+ chContainer , err := testcontainers .GenericContainer (ctx , testcontainers.GenericContainerRequest {ContainerRequest : req , Started : true })
381+ require .NoError (t , err )
382+
383+ t .Cleanup (func () {
384+ cleanupCtx , cancel := context .WithTimeout (context .Background (), 30 * time .Second )
385+ defer cancel ()
386+ _ = chContainer .Terminate (cleanupCtx )
387+ })
388+
389+ host , err := chContainer .Host (ctx )
390+ require .NoError (t , err )
391+ port , err := chContainer .MappedPort (ctx , "9000" )
392+ require .NoError (t , err )
393+
394+ cfg := & config.ClickHouseConfig {
395+ Host : host ,
396+ Port : port .Int (),
397+ Database : "default" ,
398+ Username : "default" ,
399+ Password : "" ,
400+ Protocol : config .TCPProtocol ,
401+ ReadOnly : false ,
402+ MaxExecutionTime : 60 ,
403+ Limit : 1000 ,
404+ }
405+
406+ // create base table
407+ client , err := clickhouse .NewClient (ctx , * cfg )
408+ require .NoError (t , err )
409+ defer func () { _ = client .Close () }()
410+ _ , _ = client .ExecuteQuery (ctx , "CREATE TABLE IF NOT EXISTS default.test (id UInt64, value String) ENGINE = Memory" )
411+ _ , _ = client .ExecuteQuery (ctx , "INSERT INTO default.test VALUES (1, 'one') ON CLUSTER default" )
412+ return cfg
413+ }
414+
415+ // Health handler tests
416+ func TestHealthHandler_Additions (t * testing.T ) {
417+ // JWE enabled -> should return 200 and auth=jwe_enabled
418+ t .Run ("jwe_enabled" , func (t * testing.T ) {
419+ app := & application {config : config.Config {Server : config.ServerConfig {JWE : config.JWEConfig {Enabled : true }}}}
420+ rr := httptest .NewRecorder ()
421+ req := httptest .NewRequest (http .MethodGet , "/health" , nil )
422+ app .healthHandler (rr , req )
423+ require .Equal (t , http .StatusOK , rr .Code )
424+ var body map [string ]interface {}
425+ require .NoError (t , json .Unmarshal (rr .Body .Bytes (), & body ))
426+ require .Equal (t , "jwe_enabled" , body ["auth" ])
427+ })
428+
429+ // JWE disabled with invalid CH -> 503
430+ t .Run ("clickhouse_unhealthy" , func (t * testing.T ) {
431+ app := & application {config : config.Config {Server : config.ServerConfig {JWE : config.JWEConfig {Enabled : false }}, ClickHouse : config.ClickHouseConfig {Host : "127.0.0.1" , Port : 9999 , Database : "default" , Username : "default" , Protocol : config .TCPProtocol }}}
432+ rr := httptest .NewRecorder ()
433+ req := httptest .NewRequest (http .MethodGet , "/health" , nil )
434+ app .healthHandler (rr , req )
435+ require .Equal (t , http .StatusServiceUnavailable , rr .Code )
436+ })
437+
438+ // JWE disabled with real CH -> 200
439+ t .Run ("clickhouse_healthy" , func (t * testing.T ) {
440+ // spin container
441+ ctx := context .Background ()
442+ cfg := setupClickHouseContainerMain (t )
443+ app := & application {config : config.Config {Server : config.ServerConfig {JWE : config.JWEConfig {Enabled : false }}, ClickHouse : * cfg }}
444+ rr := httptest .NewRecorder ()
445+ req := httptest .NewRequest (http .MethodGet , "/health" , nil )
446+ app .healthHandler (rr , req )
447+ require .Equal (t , http .StatusOK , rr .Code )
448+ var body map [string ]interface {}
449+ require .NoError (t , json .Unmarshal (rr .Body .Bytes (), & body ))
450+ require .Equal (t , "connected" , body ["clickhouse" ])
451+ _ = ctx
452+ })
453+
454+ // Method not allowed
455+ t .Run ("method_not_allowed" , func (t * testing.T ) {
456+ app := & application {config : config.Config {}}
457+ rr := httptest .NewRecorder ()
458+ req := httptest .NewRequest (http .MethodPost , "/health" , nil )
459+ app .healthHandler (rr , req )
460+ require .Equal (t , http .StatusMethodNotAllowed , rr .Code )
461+ })
462+ }
463+
464+ // testConnection tests
465+ func TestTestConnection_Additions (t * testing.T ) {
466+ t .Run ("success" , func (t * testing.T ) {
467+ cfg := setupClickHouseContainerMain (t )
468+ ctx , cancel := context .WithTimeout (context .Background (), 20 * time .Second )
469+ defer cancel ()
470+ err := testConnection (ctx , * cfg )
471+ require .NoError (t , err )
472+ })
473+
474+ t .Run ("failure" , func (t * testing.T ) {
475+ ctx , cancel := context .WithTimeout (context .Background (), 5 * time .Second )
476+ defer cancel ()
477+ bad := config.ClickHouseConfig {Host : "127.0.0.1" , Port : 9999 , Database : "default" , Username : "default" , Protocol : config .TCPProtocol }
478+ err := testConnection (ctx , bad )
479+ require .Error (t , err )
480+ })
481+ }
482+
483+ func TestNewApplication_ErrorPaths (t * testing.T ) {
484+ ctx := context .Background ()
485+ t .Run ("jwe_enabled_missing_key" , func (t * testing.T ) {
486+ cfg := config.Config {Server : config.ServerConfig {JWE : config.JWEConfig {Enabled : true }}}
487+ _ , err := newApplication (ctx , cfg , & mockCommand {flags : map [string ]interface {}{"config-reload-time" : 0 }, setFlags : map [string ]bool {"config-reload-time" : true }, stringMaps : map [string ]map [string ]string {}})
488+ require .Error (t , err )
489+ require .Contains (t , err .Error (), "JWE encryption is enabled" )
490+ })
491+
492+ t .Run ("clickhouse_ping_fail" , func (t * testing.T ) {
493+ cfg := config.Config {ClickHouse : config.ClickHouseConfig {Host : "127.0.0.1" , Port : 65000 , Database : "default" , Username : "default" , Protocol : config .TCPProtocol }}
494+ _ , err := newApplication (ctx , cfg , & mockCommand {flags : map [string ]interface {}{"config-reload-time" : 0 }, setFlags : map [string ]bool {"config-reload-time" : true }, stringMaps : map [string ]map [string ]string {}})
495+ require .Error (t , err )
496+ })
497+ }
498+
499+ func TestConfigReloadLoop_ErrorAndStop (t * testing.T ) {
500+ // Create temp invalid config file to trigger reload error
501+ dir := t .TempDir ()
502+ cfgPath := filepath .Join (dir , "config.yaml" )
503+ require .NoError (t , os .WriteFile (cfgPath , []byte ("invalid: : yaml" ), 0o600 ))
504+
505+ cfg := config.Config {ReloadTime : 1 }
506+ app := & application {config : cfg , configFile : cfgPath , stopConfigReload : make (chan struct {}), mcpServer : altinitymcp .NewClickHouseMCPServer (config.Config {}, "test" )}
507+
508+ ctx , cancel := context .WithCancel (context .Background ())
509+ defer cancel ()
510+ done := make (chan struct {})
511+ go func () { app .configReloadLoop (ctx , & mockCommand {flags : map [string ]interface {}{}, setFlags : map [string ]bool {}, stringMaps : map [string ]map [string ]string {}}); close (done ) }()
512+ time .Sleep (1500 * time .Millisecond )
513+ close (app .stopConfigReload )
514+ <- done
515+ }
516+
517+ // ClickHouse client Ping/DescribeTable extra coverage
518+ func TestClickHouseClient_PingAndDescribeTable (t * testing.T ) {
519+ cfg := setupClickHouseContainerMain (t )
520+ ctx := context .Background ()
521+ client , err := clickhouse .NewClient (ctx , * cfg )
522+ require .NoError (t , err )
523+ defer func () { require .NoError (t , client .Close ()) }()
524+
525+ require .NoError (t , client .Ping (ctx ))
526+ cols , err := client .DescribeTable (ctx , cfg .Database , "test" )
527+ require .NoError (t , err )
528+ require .NotEmpty (t , cols )
529+ }
530+
361531// TestHealthHandler tests the health check endpoint
362532func TestHealthHandler (t * testing.T ) {
363533 t .Run ("method_not_allowed" , func (t * testing.T ) {
0 commit comments