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:
7
go.mod
7
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
|
||||
)
|
||||
|
||||
13
go.sum
13
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=
|
||||
|
||||
120
hbrtl/pgserver/wire_test.go
Normal file
120
hbrtl/pgserver/wire_test.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user