Adds an in-process startup-handshake test using net.Pipe so we
can pin the protocol envelope (StartupMessage → AuthenticationOk
→ ParameterStatus×N → BackendKeyData → ReadyForQuery) without
binding a real TCP port. Runs in <1ms; safe for CI.
The PRG-dispatch path (runSQL → FIVE_SQL → row encoding) is
already covered manually by spinning a `five run` of
`pg_server_start(":15432")` and connecting with pgx — that flow
verified post-MVP that a real PostgreSQL client receives
`{ONE (INT4), GREET (TEXT)}` + row `[1 hello]` for
`SELECT 1 AS one, 'hello' AS greet` over the wire. An automated
shell harness will land in Phase 7 with the psql integration
tests.
Also rolls go.mod / go.sum forward with the pgx v5 toolchain pulled
in by Phase 2's pgproto3 dependency. Module bump 1.21.13 → 1.25.0
matches what `go get github.com/jackc/pgx/v5/pgproto3` selected;
cross-builds for windows/linux/darwin all still succeed (verified
locally).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
121 lines
3.5 KiB
Go
121 lines
3.5 KiB
Go
// 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
|
||
}
|