diff --git a/go.mod b/go.mod index 7b5f539..197f3e9 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,9 @@ module five go 1.25.0 -require github.com/jackc/pgx/v5 v5.9.2 // indirect +require ( + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.9.2 // indirect + golang.org/x/text v0.29.0 // indirect +) diff --git a/go.sum b/go.sum index 9f22e7a..8c1f5e4 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,15 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/hbrtl/pgserver/wire_test.go b/hbrtl/pgserver/wire_test.go new file mode 100644 index 0000000..51812ce --- /dev/null +++ b/hbrtl/pgserver/wire_test.go @@ -0,0 +1,120 @@ +// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com) +// All rights reserved. + +package pgserver + +import ( + "net" + "testing" + "time" + + "github.com/jackc/pgx/v5/pgproto3" +) + +// TestWireRoundtrip_Startup verifies the bare minimum protocol +// handshake without involving the PRG side: an in-memory client +// sends StartupMessage, the server replies AuthenticationOk + +// ParameterStatus + BackendKeyData + ReadyForQuery, the client +// confirms by reading exactly those messages in that order. +// +// Uses net.Pipe so no real TCP socket is bound — fast, robust on CI. +// The PRG-dispatch path (runSQL → FIVE_SQL) is covered separately by +// the psql integration test in _FiveSql2/test/pgserver/run.sh; this +// test pins ONLY the protocol envelope. +func TestWireRoundtrip_Startup(t *testing.T) { + clientConn, serverConn := net.Pipe() + defer clientConn.Close() + + // Server side — minimal stand-in for run() that exercises the + // startup steps without needing a VM (no Query loop here). + srvDone := make(chan struct{}) + go func() { + defer close(srvDone) + defer serverConn.Close() + + s := &session{conn: serverConn, txStatus: 'I', srv: &Server{cfg: Config{}}} + // Step 1 — SSL/Startup peek. negotiateTLS reads 8 bytes; + // we don't send SSLRequest so this just buffers the bytes + // for the StartupMessage parse. + if err := s.negotiateTLS(); err != nil { + t.Errorf("server negotiateTLS: %v", err) + return + } + s.be = pgproto3.NewBackend(s.conn, s.conn) + + msg, err := s.be.ReceiveStartupMessage() + if err != nil { + t.Errorf("ReceiveStartupMessage: %v", err) + return + } + if _, ok := msg.(*pgproto3.StartupMessage); !ok { + t.Errorf("expected StartupMessage, got %T", msg) + return + } + + // Trust auth → AuthenticationOk + s.send(&pgproto3.AuthenticationOk{}) + // Minimum ParameterStatus set + s.sendParameterStatus("server_version", "14.0 (FiveSql2)") + s.sendParameterStatus("client_encoding", "UTF8") + // BackendKeyData + s.send(&pgproto3.BackendKeyData{ProcessID: 1234, SecretKey: []byte{0, 0, 0, 0}}) + // ReadyForQuery — idle + s.sendReadyForQuery() + }() + + // Client side — drives the handshake via pgproto3.Frontend. + fe := pgproto3.NewFrontend(clientConn, clientConn) + fe.Send(&pgproto3.StartupMessage{ + ProtocolVersion: pgproto3.ProtocolVersionNumber, + Parameters: map[string]string{"user": "alice", "database": "alice"}, + }) + if err := fe.Flush(); err != nil { + t.Fatalf("client flush startup: %v", err) + } + + // Expect AuthenticationOk + clientConn.SetReadDeadline(time.Now().Add(2 * time.Second)) + msg, err := fe.Receive() + if err != nil { + t.Fatalf("client receive auth: %v", err) + } + if _, ok := msg.(*pgproto3.AuthenticationOk); !ok { + t.Fatalf("expected AuthenticationOk, got %T", msg) + } + + // ParameterStatus × 2 + for i := 0; i < 2; i++ { + msg, err = fe.Receive() + if err != nil { + t.Fatalf("client receive paramstatus %d: %v", i, err) + } + if _, ok := msg.(*pgproto3.ParameterStatus); !ok { + t.Fatalf("expected ParameterStatus, got %T", msg) + } + } + + // BackendKeyData + msg, err = fe.Receive() + if err != nil { + t.Fatalf("client receive bkd: %v", err) + } + if _, ok := msg.(*pgproto3.BackendKeyData); !ok { + t.Fatalf("expected BackendKeyData, got %T", msg) + } + + // ReadyForQuery + msg, err = fe.Receive() + if err != nil { + t.Fatalf("client receive rfq: %v", err) + } + rfq, ok := msg.(*pgproto3.ReadyForQuery) + if !ok { + t.Fatalf("expected ReadyForQuery, got %T", msg) + } + if rfq.TxStatus != 'I' { + t.Errorf("ReadyForQuery TxStatus = %c, want 'I' (idle)", rfq.TxStatus) + } + + <-srvDone +}