package navicat import ( "context" "net/http/httptest" "strconv" "strings" "testing" "time" "mysql-tunnel/internal/tunnel" ) func testClient(t *testing.T, opts ...MockOption) (*Client, *httptest.Server) { t.Helper() srv := httptest.NewServer(NewMockHandler(opts...)) t.Cleanup(srv.Close) httpClient := tunnel.NewHTTPClient(tunnel.Options{Timeout: 5 * time.Second}) c := NewClient(httpClient, ClientOptions{ URL: srv.URL + "?token=secret", Target: Target{ Host: "mysql", Port: 3306, User: "root", Password: "remote-pass", Database: "app", Charset: "utf8mb4", }, }) return c, srv } func TestClientConnectAndSelect1(t *testing.T) { var cap CapturedRequest c, _ := testClient(t, WithToken("secret"), WithCapture(&cap)) ctx := context.Background() conn, err := c.Connect(ctx) if err != nil { t.Fatal(err) } if conn.Conn.ServerInfo != "8.0.36-mock" { t.Fatalf("%+v", conn.Conn) } if cap.Action != ActionConnect || cap.Host != "mysql" || cap.Login != "root" { t.Fatalf("connect capture %+v", cap) } if cap.Port != "3306" { t.Fatalf("port %s", cap.Port) } if !cap.HasPassword { t.Fatal("password field missing") } q, err := c.Query(ctx, "", "SELECT 1") if err != nil { t.Fatal(err) } res, ok := LastResultSet(q) if !ok || res.Rows[0][0].Text() != "1" { t.Fatalf("select 1: %+v", q) } if cap.Action != ActionQuery { t.Fatalf("query capture %+v", cap) } joined := strings.Join(cap.Queries, " | ") if !strings.Contains(joined, "SET NAMES utf8mb4") { t.Fatalf("expected SET NAMES in %q", joined) } if !strings.Contains(joined, "SELECT 1") { t.Fatalf("expected SELECT 1 in %q", joined) } } func TestClientTypesOverHTTP(t *testing.T) { c, _ := testClient(t, WithToken("secret")) ctx := context.Background() q, err := c.Query(ctx, "app", "SELECT NULL AS n") if err != nil { t.Fatal(err) } res, _ := LastResultSet(q) if !res.Rows[0][0].Null { t.Fatal("NULL") } q, err = c.Query(ctx, "", "SELECT bigint_val") if err != nil { t.Fatal(err) } res, _ = LastResultSet(q) got := res.Rows[0][0].Text() if got != "18446744073709551615" { t.Fatalf("bigint %q", got) } if _, err := strconv.ParseInt(got, 10, 64); err == nil { t.Fatal("test value should overflow int64") } q, err = c.Query(ctx, "", "SELECT dec_val") if err != nil { t.Fatal(err) } res, _ = LastResultSet(q) if res.Fields[0].Type != 246 || res.Rows[0][0].Text() != "1234567890.123456789" { t.Fatalf("decimal %+v", res) } q, err = c.Query(ctx, "", "SELECT zh_val") if err != nil { t.Fatal(err) } res, _ = LastResultSet(q) if res.Rows[0][0].Text() != "你好世界" { t.Fatalf("zh %q", res.Rows[0][0].Text()) } } func TestClientSQLError(t *testing.T) { c, _ := testClient(t, WithToken("secret")) q, err := c.Query(context.Background(), "", "SELECT SYNTAX") if err != nil { t.Fatal(err) } res, ok := LastResultSet(q) if !ok || !res.IsError() || res.Errno != 1064 { t.Fatalf("%+v", q) } } func TestClientDML(t *testing.T) { c, _ := testClient(t, WithToken("secret")) q, err := c.Query(context.Background(), "", "UPDATE product SET price=1") if err != nil { t.Fatal(err) } res := q.Results[len(q.Results)-1] if res.IsResultSet() || res.AffectedRows != 1 || res.InsertID != 42 { t.Fatalf("%+v", res) } } func TestClientBadTokenPOST(t *testing.T) { srv := httptest.NewServer(NewMockHandler(WithToken("right"))) t.Cleanup(srv.Close) httpClient := tunnel.NewHTTPClient(tunnel.Options{Timeout: 5 * time.Second}) c := NewClient(httpClient, ClientOptions{ URL: srv.URL + "?token=wrong", Target: Target{Host: "mysql", Port: 3306, User: "root"}, }) _, err := c.Connect(context.Background()) if err == nil { t.Fatal("expected token error") } ne, ok := err.(*Error) if !ok || ne.Errno != ErrnoInvalidToken { t.Fatalf("%v", err) } } func TestClientConnectDenied(t *testing.T) { c, _ := testClient(t, WithToken("secret"), WithConnectError(1045, "Access denied for user 'root'@'localhost'")) _, err := c.Connect(context.Background()) if err == nil { t.Fatal("expected deny") } if !strings.Contains(err.Error(), "1045") { t.Fatal(err) } if strings.Contains(err.Error(), "remote-pass") { t.Fatal("password leaked") } } func TestEncodeBase64Query(t *testing.T) { var cap CapturedRequest srv := httptest.NewServer(NewMockHandler(WithToken("secret"), WithCapture(&cap))) t.Cleanup(srv.Close) httpClient := tunnel.NewHTTPClient(tunnel.Options{Timeout: 5 * time.Second}) c := NewClient(httpClient, ClientOptions{ URL: srv.URL + "?token=secret", Target: Target{Host: "mysql", Port: 3306, User: "root", Charset: "utf8"}, EncodeBase64: true, }) _, err := c.Query(context.Background(), "", "SELECT 1") if err != nil { t.Fatal(err) } if cap.EncodeBase64 != "1" { t.Fatalf("%+v", cap) } if strings.Contains(strings.Join(cap.Queries, ""), "SELECT 1") { t.Fatalf("wire should be base64, got %v", cap.Queries) } }