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