Files
2026-09-21 16:46:55 +08:00

197 lines
4.9 KiB
Go

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)
}
}