Files
five/hbrtl/pgserver/wire_test.go
CharlesKWON 708329785a 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>
2026-05-16 22:13:40 +09:00

121 lines
3.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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
}