test(pgserver): wire-protocol roundtrip via net.Pipe

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>
This commit is contained in:
2026-05-16 22:13:40 +09:00
parent d98f5e1767
commit 708329785a
3 changed files with 139 additions and 1 deletions

7
go.mod
View File

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

13
go.sum
View File

@@ -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=

120
hbrtl/pgserver/wire_test.go Normal file
View File

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