Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions tests/all_tests.nim
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{.push warning[UnusedImport]: off.}
import
test_abandonment_e2e, test_advisory_lock, test_aggregate, test_async_backend,
test_auth, test_cancel_e2e, test_copy_race, test_dsn, test_e2e_arrays,
test_e2e_connection, test_e2e_convenience, test_e2e_copy, test_e2e_cursor,
test_e2e_listen, test_e2e_misc, test_e2e_pool, test_e2e_query, test_e2e_transaction,
test_e2e_types, test_fill_recvbuf, test_keepalive, test_largeobject,
test_largeobject_parse, test_listen_reconnect, test_network_failure,
test_auth, test_bytes, test_cache, test_cancel_e2e, test_conn_types, test_copy_race,
test_dsn, test_e2e_arrays, test_e2e_connection, test_e2e_convenience, test_e2e_copy,
test_e2e_cursor, test_e2e_listen, test_e2e_misc, test_e2e_pool, test_e2e_query,
test_e2e_transaction, test_e2e_types, test_errors, test_fill_recvbuf, test_keepalive,
test_largeobject, test_largeobject_parse, test_listen_reconnect, test_network_failure,
test_physical_replication, test_pool, test_protocol, test_protocol_fuzz,
test_replication, test_replication_keepalive, test_rowdata, test_saslprep,
test_session_attrs, test_sql, test_ssl, test_tls_error_paths, test_tracing,
Expand Down
106 changes: 106 additions & 0 deletions tests/test_bytes.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
## Dedicated unit tests for ``pg_bytes`` — big-endian helpers and bounded copies.
## Keeps the leaf encoding primitives covered without relying on protocol suites.

import std/unittest

import ../async_postgres/[pg_bytes, pg_errors]

suite "pg_bytes big-endian encode/decode":
test "encodes known big-endian byte order":
check toBE16(0x0102'i16) == [1'u8, 2'u8]
check toBE16(-1'i16) == [0xFF'u8, 0xFF'u8]
check toBE32(0x01020304'i32) == [1'u8, 2'u8, 3'u8, 4'u8]
check toBE32(-1'i32) == [0xFF'u8, 0xFF'u8, 0xFF'u8, 0xFF'u8]
check toBE64(0x1122334455667788'i64) ==
[0x11'u8, 0x22'u8, 0x33'u8, 0x44'u8, 0x55'u8, 0x66'u8, 0x77'u8, 0x88'u8]
check fromBE16([1'u8, 2'u8]) == 0x0102'i16
check fromBE32([1'u8, 2'u8, 3'u8, 4'u8]) == 0x01020304'i32
check fromBE64(
[0x11'u8, 0x22'u8, 0x33'u8, 0x44'u8, 0x55'u8, 0x66'u8, 0x77'u8, 0x88'u8]
) == 0x1122334455667788'i64

test "toBE16/fromBE16 roundtrip including negatives":
for v in [0'i16, 1'i16, -1'i16, high(int16), low(int16)]:
let enc = toBE16(v)
check fromBE16(enc) == v

test "toBE32/fromBE32 roundtrip including negatives":
for v in [0'i32, 1'i32, -1'i32, high(int32), low(int32)]:
let enc = toBE32(v)
check fromBE32(enc) == v

test "toBE64/fromBE64 roundtrip including negatives":
for v in [0'i64, 1'i64, -1'i64, high(int64), low(int64)]:
let enc = toBE64(v)
check fromBE64(enc) == v

test "writeBE* writes big-endian bytes at an offset":
var buf = newSeq[byte](16)
writeBE16(buf, 2, 0x0102'i16)
writeBE32(buf, 4, 0x01020304'i32)
writeBE64(buf, 8, 0x1122334455667788'i64)
check buf[2 ..< 4] == @[1'u8, 2'u8]
check buf[4 ..< 8] == @[1'u8, 2'u8, 3'u8, 4'u8]
check buf[8 ..< 16] ==
@[0x11'u8, 0x22'u8, 0x33'u8, 0x44'u8, 0x55'u8, 0x66'u8, 0x77'u8, 0x88'u8]

test "writeBE* matches toBE* at an offset":
var buf = newSeq[byte](16)
writeBE16(buf, 2, -42'i16)
writeBE32(buf, 4, 0x01020304'i32)
writeBE64(buf, 8, 0x1122334455667788'i64)
check fromBE16(buf, 2) == -42'i16
check fromBE32(buf, 4) == 0x01020304'i32
check fromBE64(buf, 8) == 0x1122334455667788'i64

test "decodeFloat32BE / decodeFloat64BE decode known byte order":
check decodeFloat32BE([0x3F'u8, 0xC0'u8, 0x00'u8, 0x00'u8]) == 1.5'f32
check decodeFloat64BE(
[0xC0'u8, 0x02'u8, 0x00'u8, 0x00'u8, 0x00'u8, 0x00'u8, 0x00'u8, 0x00'u8]
) == -2.25'f64

test "decodeFloat32BE / decodeFloat64BE match IEEE bit patterns":
let f32 = 1.5'f32
let f64 = -2.25'f64
check decodeFloat32BE(@(toBE32(cast[int32](cast[uint32](f32))))) == f32
check decodeFloat64BE(@(toBE64(cast[int64](cast[uint64](f64))))) == f64

suite "pg_bytes bounded copies":
test "writeBytesAt copies and rejects out-of-range slices":
var dst = newSeq[byte](4)
let src = @[1'u8, 2]
writeBytesAt(dst, 1, src)
check dst == @[0'u8, 1, 2, 0]
let empty: seq[byte] = @[]
writeBytesAt(dst, 0, empty) # empty is a no-op
check dst == @[0'u8, 1, 2, 0]
let tooLong = @[9'u8, 9]
expect PgProtocolError:
writeBytesAt(dst, 3, tooLong)
let one = @[1'u8]
expect PgProtocolError:
writeBytesAt(dst, -1, one)

test "appendBytes grows the destination":
var buf: seq[byte]
let empty: seq[byte] = @[]
appendBytes(buf, empty)
check buf.len == 0
let a = @[1'u8, 2, 3]
let b = @[4'u8]
appendBytes(buf, a)
appendBytes(buf, b)
check buf == @[1'u8, 2, 3, 4]

test "readString / readBytes copy and reject bad ranges":
let src = @[byte('a'), byte('b'), byte('c'), byte('d')]
check readString(src, 1, 2) == "bc"
check readBytes(src, 0, 4) == src
check readString(src, 0, 0) == ""
check readBytes(src, 2, 0) == newSeq[byte]()
expect PgProtocolError:
discard readString(src, 2, 3)
expect PgProtocolError:
discard readBytes(src, -1, 1)
expect PgProtocolError:
discard readString(src, 0, -1)
111 changes: 111 additions & 0 deletions tests/test_cache.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
## Dedicated unit tests for ``pg_connection/cache`` — LRU order, capacity-0
## disable, defensive eviction into ``pendingStmtCloses``, and Close staging.

import std/[unittest, tables, strutils, importutils]

import ../async_postgres/[async_backend, pg_protocol, pg_types]
import ../async_postgres/pg_connection/types {.all.}
import ../async_postgres/pg_connection/cache {.all.}

privateAccess(PgConnection)

proc mockConn(capacity: int = 2): PgConnection =
PgConnection(
recvBuf: @[],
state: csReady,
txStatus: tsIdle,
serverParams: initTable[string, string](),
createdAt: Moment.now(),
stmtCacheCapacity: capacity,
)

proc cached(name: string, fields: seq[FieldDescription] = @[]): CachedStmt =
CachedStmt(name: name, fields: fields)

suite "stmt cache LRU":
test "capacity 0 disables lookup and add":
let conn = mockConn(0)
conn.addStmtCache("SELECT 1", cached("_sc_1"))
check conn.stmtCache.len == 0
check conn.lookupStmtCache("SELECT 1").isNil
check not conn.stmtCachingEnabled

test "hit moves entry to MRU and miss returns nil":
let conn = mockConn(2)
conn.addStmtCache("a", cached("_sc_a"))
conn.addStmtCache("b", cached("_sc_b"))
check conn.lookupStmtCache("a").name == "_sc_a"
# After touching "a", "b" is LRU and is the next eviction victim.
conn.addStmtCache("c", cached("_sc_c"))
check conn.lookupStmtCache("b").isNil
check conn.lookupStmtCache("a").name == "_sc_a"
check conn.lookupStmtCache("c").name == "_sc_c"
check conn.pendingStmtCloses == @["_sc_b"]

test "defensive add eviction queues Close names":
let conn = mockConn(1)
conn.addStmtCache("old", cached("_sc_old"))
conn.addStmtCache("new", cached("_sc_new"))
check conn.stmtCache.len == 1
check conn.lookupStmtCache("new").name == "_sc_new"
check conn.pendingStmtCloses == @["_sc_old"]

test "clearStmtCache drops LRU, pending, and staged Closes":
let conn = mockConn(2)
conn.addStmtCache("a", cached("_sc_a"))
conn.pendingStmtCloses = @["_sc_x"]
conn.stagedStmtCloses = @["_sc_y"]
conn.clearStmtCache()
check conn.stmtCache.len == 0
check conn.lookupStmtCache("a").isNil
check conn.pendingStmtCloses.len == 0
check conn.stagedStmtCloses.len == 0

test "removeStmtCache drops one entry without touching others":
let conn = mockConn(2)
conn.addStmtCache("a", cached("_sc_a"))
conn.addStmtCache("b", cached("_sc_b"))
conn.removeStmtCache("a")
check conn.lookupStmtCache("a").isNil
check conn.lookupStmtCache("b").name == "_sc_b"

test "addStmtCache fills resultFormats from field OIDs":
let conn = mockConn(2)
let fields = @[
FieldDescription(name: "i", typeOid: OidInt4, formatCode: 0),
FieldDescription(name: "t", typeOid: OidText, formatCode: 0),
]
conn.addStmtCache("SELECT 1", cached("_sc_1", fields))
let entry = conn.lookupStmtCache("SELECT 1")
check entry.resultFormats == @[1'i16, 1'i16]
check entry.colOids == @[OidInt4, OidText]
check entry.colFmts == @[1'i16, 1'i16]

test "nextStmtName is unique and prefixed":
let conn = mockConn()
let a = conn.nextStmtName()
let b = conn.nextStmtName()
check a.startsWith(stmtNamePrefix)
check b.startsWith(stmtNamePrefix)
check a != b

test "stagePendingStmtCloses empties the queue into staged + buffer":
let conn = mockConn(1)
conn.pendingStmtCloses = @["_sc_1", "_sc_2"]
var buf: seq[byte]
conn.stagePendingStmtCloses(buf)
check conn.pendingStmtCloses.len == 0
check conn.stagedStmtCloses == @["_sc_1", "_sc_2"]
# Two Close('S') frames: type + int32 len + 'S' + cstring + NUL each.
check buf.len > 0
check buf[0] == byte('C')

test "beginSendBuf clears then stages owed Closes":
let conn = mockConn(1)
conn.sendBuf = @[1'u8, 2, 3]
conn.pendingStmtCloses = @["_sc_owed"]
conn.beginSendBuf()
check conn.sendBuf.len > 0
check conn.sendBuf[0] == byte('C')
check conn.stagedStmtCloses == @["_sc_owed"]
check conn.pendingStmtCloses.len == 0
79 changes: 79 additions & 0 deletions tests/test_conn_types.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
## Dedicated unit tests for pure helpers in ``pg_connection/types``.
## Covers default resolution, portal naming, dial/display host, and raw
## replication LSN bookkeeping — all without a live server or mock transport.

import std/[unittest, tables, importutils]

import ../async_postgres/[async_backend, pg_protocol, pg_auth]
import ../async_postgres/pg_connection/types {.all.}

privateAccess(PgConnection)

proc bareConn(config = ConnConfig()): PgConnection =
PgConnection(
recvBuf: @[],
state: csReady,
txStatus: tsIdle,
serverParams: initTable[string, string](),
createdAt: Moment.now(),
config: config,
)

suite "conn-types effective defaults":
test "effectiveMaxMessageSize resolves 0 to the protocol default":
let conn = bareConn()
check conn.config.maxMessageSize == 0
check conn.effectiveMaxMessageSize == DefaultMaxBackendMessageLen
conn.config.maxMessageSize = 4096
check conn.effectiveMaxMessageSize == 4096

test "effectiveMaxScramIterations resolves 0 to the auth default":
var cfg = ConnConfig()
check cfg.maxScramIterations == 0
check effectiveMaxScramIterations(cfg) == DefaultMaxScramIterations
cfg.maxScramIterations = 1000
check effectiveMaxScramIterations(cfg) == 1000

suite "conn-types portal and host helpers":
test "nextPortalName is monotonic under a shared prefix":
let conn = bareConn()
let a = conn.nextPortalName("p")
let b = conn.nextPortalName("p")
check a == "p1"
check b == "p2"

test "dialAddr prefers hostaddr; displayHost prefers host":
let both = HostEntry(host: "db.example", hostaddr: "10.0.0.1", port: 5432)
check dialAddr(both) == "10.0.0.1"
check displayHost(both) == "db.example"
let onlyHost = HostEntry(host: "db.example", port: 5432)
check dialAddr(onlyHost) == "db.example"
check displayHost(onlyHost) == "db.example"
let onlyAddr = HostEntry(hostaddr: "10.0.0.1", port: 5432)
check dialAddr(onlyAddr) == "10.0.0.1"
check displayHost(onlyAddr) == "10.0.0.1"

suite "conn-types replication LSN helpers":
test "init / update / confirm clamp and advance monotonically":
let conn = bareConn()
conn.initReplLsnTracking(100)
check conn.replConfirmedFlushLsn == 100
check conn.replMaxReceivedLsn == 100

check not conn.updateReplMaxReceivedLsn(50)
check conn.replMaxReceivedLsn == 100
check conn.updateReplMaxReceivedLsn(150)
check conn.replMaxReceivedLsn == 150

# Above max-received clamps to max and advances flush.
check conn.confirmReplFlushed(200)
check conn.replConfirmedFlushLsn == 150
# Below confirmed is a no-op; between confirmed and max advances.
check not conn.confirmReplFlushed(110)
check conn.replConfirmedFlushLsn == 150
conn.initReplLsnTracking(100)
discard conn.updateReplMaxReceivedLsn(150)
check conn.confirmReplFlushed(120)
check conn.replConfirmedFlushLsn == 120
check not conn.confirmReplFlushed(110)
check conn.replConfirmedFlushLsn == 120
32 changes: 32 additions & 0 deletions tests/test_dsn.nim
Original file line number Diff line number Diff line change
Expand Up @@ -794,6 +794,38 @@ suite "parseDsn":
check cfg.sslCert == dummyPem
check cfg.sslKey == dummyPem

test "error: empty sslcert PEM file rejected":
let certPath = writePemFile("")
let keyPath = writeKeyFile(dummyPem)
defer:
removeFile(certPath)
removeFile(keyPath)
var raised = false
try:
discard parseDsn(
"postgresql://host/db?sslmode=require&sslcert=" & certPath & "&sslkey=" & keyPath
)
except PgConfigError as e:
raised = true
check "file is empty" in e.msg
check raised

test "error: empty sslkey PEM file rejected":
let certPath = writePemFile(dummyPem)
let keyPath = writeKeyFile("")
defer:
removeFile(certPath)
removeFile(keyPath)
var raised = false
try:
discard parseDsn(
"postgresql://host/db?sslmode=require&sslcert=" & certPath & "&sslkey=" & keyPath
)
except PgConfigError as e:
raised = true
check "file is empty" in e.msg
check raised

test "error: sslcert with sslmode=disable rejected":
let certPath = writePemFile(dummyPem)
let keyPath = writeKeyFile(dummyPem)
Expand Down
Loading
Loading