Skip to content

Commit beba68a

Browse files
test(coverage): drive server/handlers to ≥95% coverage (#20)
- server: 54.8% → 98.9%. Adds dedicated-backend dispatch tests for every RPC (Provision/Deprovision/StorageBytes/Regrade) across postgres, redis, mongo, and queue; pool-hit / pool-error / pool-miss branches via a new PoolClaimer interface seam; callBackendVoid breaker-open path; end-to-end gRPC round-trip via bufconn; New() constructor branches for k8s + MinIO + dedicated DSN configs; teamIDFromContext via metadata; PostgresBackend / Breakers accessors. - handlers: 94.1% → 100%. Adds long-error-message truncation path on platformDBCheck and a whitebox test for the defensive nil-pool branch. - Minor refactor in server.go: typed *pool.Manager field becomes a PoolClaimer interface so tests can inject a fake claimer without standing up a real *pgxpool.Pool. Production behaviour preserved by normalising nil *pool.Manager to a nil interface in NewWithBackends. Joint coverage now well above the 95% gate on both packages. Co-authored-by: Manas Srivastava <[email protected]> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 9c63384 commit beba68a

4 files changed

Lines changed: 1697 additions & 2 deletions

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package handlers
2+
3+
// readyz_internal_test.go — whitebox tests for the defensive nil-pool branch
4+
// in platformDBCheck. NewReadyzHandler only registers the platform_db check
5+
// when h.pool != nil, so the `if h.pool == nil` branch inside the closure is
6+
// dead under the public surface — but the guard is kept as belt-and-braces
7+
// in case a future refactor wires the check unconditionally. A whitebox test
8+
// pins the contract: "registered but pool nil" maps to failed +
9+
// pgxpool_not_configured.
10+
11+
import (
12+
"context"
13+
"testing"
14+
15+
"instant.dev/common/readiness"
16+
)
17+
18+
func TestPlatformDBCheck_NilPool_DefensiveBranch(t *testing.T) {
19+
h := &ReadyzHandler{} // pool intentionally nil
20+
check := h.platformDBCheck()
21+
res := check(context.Background())
22+
if res.Status != readiness.StatusFailed {
23+
t.Errorf("nil-pool branch: status = %q, want failed", res.Status)
24+
}
25+
if res.LastError != "pgxpool_not_configured" {
26+
t.Errorf("nil-pool branch: LastError = %q, want pgxpool_not_configured", res.LastError)
27+
}
28+
}

internal/handlers/readyz_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,39 @@ func TestReadyz_OpenCircuit_Degrades(t *testing.T) {
155155
}
156156
}
157157

158+
// TestReadyz_LongErrorMessage_Truncated — when the pool ping fails with a
159+
// >80-char message, /readyz truncates it to 80 chars before surfacing it on
160+
// the platform_db check (avoids leaking long connection-string bytes through
161+
// the response body).
162+
func TestReadyz_LongErrorMessage_Truncated(t *testing.T) {
163+
// 120-char error message — must end up truncated to 80 in the response.
164+
longMsg := "connection refused: tcp dial 10.0.0.1:5432 failed because the server is offline and rebooting now"
165+
if len(longMsg) <= 80 {
166+
t.Fatalf("test setup: long message must be > 80 chars (got %d)", len(longMsg))
167+
}
168+
h := handlers.NewReadyzHandler(handlers.Config{
169+
PoolPinger: fakePool{err: errors.New(longMsg)},
170+
})
171+
rr := httptest.NewRecorder()
172+
req := httptest.NewRequest("GET", "/readyz", nil)
173+
h.Get(rr, req)
174+
if rr.Code != 503 {
175+
t.Fatalf("expected 503, got %d", rr.Code)
176+
}
177+
var got readiness.Response
178+
if err := json.Unmarshal(rr.Body.Bytes(), &got); err != nil {
179+
t.Fatalf("decode: %v", err)
180+
}
181+
for _, c := range got.Checks {
182+
if c.Name == "platform_db" {
183+
if len(c.LastError) > 80 {
184+
t.Errorf("platform_db.LastError = %q (len %d); want ≤80 chars",
185+
c.LastError, len(c.LastError))
186+
}
187+
}
188+
}
189+
}
190+
158191
// TestReadyz_ContentType_AndNoStore — the response is JSON and tagged
159192
// no-store so probes don't stale.
160193
func TestReadyz_ContentType_AndNoStore(t *testing.T) {

internal/server/server.go

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,16 @@ func teamIDFromContext(ctx context.Context) string {
5353
return vals[0]
5454
}
5555

56+
// PoolClaimer is the minimal Claim surface server.go uses from
57+
// *pool.Manager. Extracting it lets tests inject a fake claimer that
58+
// returns synthesised pool hits / errors without standing up a real
59+
// *pgxpool.Pool — *pool.Manager itself remains the production
60+
// implementation. Keep this interface byte-for-byte aligned with
61+
// (*pool.Manager).Claim so the concrete type continues to satisfy it.
62+
type PoolClaimer interface {
63+
Claim(ctx context.Context, resourceType string) (*pool.Item, error)
64+
}
65+
5666
// Server implements ProvisionerServiceServer.
5767
type Server struct {
5868
provisionerv1.UnimplementedProvisionerServiceServer
@@ -65,7 +75,7 @@ type Server struct {
6575
dedicatedRedisBackend redis.Backend // nil if not configured; used for Pro/Team tier
6676
dedicatedMongoBackend mongo.Backend // nil if not configured; used for Pro/Team tier
6777
dedicatedQueueBackend queue.Backend // nil if not configured; used for Pro/Team tier
68-
pool *pool.Manager // nil if pool is disabled
78+
pool PoolClaimer // nil if pool is disabled; production type is *pool.Manager
6979

7080
// breakers — per-backend in-process circuit breakers (audit P0-3).
7181
// Independent instance per backend so a Redis outage cannot trip the
@@ -168,6 +178,16 @@ func NewWithBackends(
168178
dedicatedQueue queue.Backend,
169179
poolMgr *pool.Manager,
170180
) *Server {
181+
// A typed nil *pool.Manager assigned to a PoolClaimer interface field
182+
// would make `s.pool != nil` evaluate true (the interface holds a
183+
// non-nil type descriptor) and the next Claim call would dereference a
184+
// nil receiver. Normalise the nil pointer to a nil interface here so
185+
// production callers that pass nil get the documented "pool disabled"
186+
// behaviour.
187+
var poolClaimer PoolClaimer
188+
if poolMgr != nil {
189+
poolClaimer = poolMgr
190+
}
171191
return &Server{
172192
postgresBackend: pgB,
173193
redisBackend: rB,
@@ -178,7 +198,7 @@ func NewWithBackends(
178198
dedicatedRedisBackend: dedicatedRedis,
179199
dedicatedMongoBackend: dedicatedMongo,
180200
dedicatedQueueBackend: dedicatedQueue,
181-
pool: poolMgr,
201+
pool: poolClaimer,
182202
// circuit.Default is the process-wide breaker set. Tests inject
183203
// their own via SetBreakers() (or NewServerForTest) so per-test
184204
// trips don't bleed into other tests.
@@ -194,6 +214,15 @@ func (s *Server) SetBreakers(b *circuit.Breakers) *Server {
194214
return s
195215
}
196216

217+
// SetPool injects an alternate pool claimer. Test-only entry point — production
218+
// code wires *pool.Manager via NewWithBackends and never calls this. Useful
219+
// for exercising the pool-hit / pool-error / pool-miss branches in
220+
// provisionPostgres/Redis/Mongo/Queue without standing up a real *pgxpool.Pool.
221+
func (s *Server) SetPool(p PoolClaimer) *Server {
222+
s.pool = p
223+
return s
224+
}
225+
197226
// breakerForPostgres returns the right breaker for a Postgres call: the
198227
// `postgres_k8s` breaker when the dedicated k8s backend will be used (Pro/
199228
// Team/Growth tier with a configured dedicated backend), and the

0 commit comments

Comments
 (0)