-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrun_test.go
More file actions
407 lines (363 loc) · 15.9 KB
/
run_test.go
File metadata and controls
407 lines (363 loc) · 15.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
package main
import (
"context"
"database/sql"
"errors"
"os"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/gofiber/fiber/v2"
"github.com/newrelic/go-agent/v3/newrelic"
"github.com/oschwald/maxminddb-golang"
"github.com/redis/go-redis/v9"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"instant.dev/internal/config"
"instant.dev/internal/email"
"instant.dev/internal/middleware"
"instant.dev/internal/plans"
"instant.dev/internal/provisioner"
"instant.dev/internal/router"
"instant.dev/internal/testhelpers"
)
// newRunSeams snapshots every package-level seam var and restores them on
// cleanup so a run() test can substitute stubs without leaking overrides into
// sibling tests in the same package.
func newRunSeams(t *testing.T) {
t.Helper()
pInit := initTracer
pPg := connectPostgres
pMig := runMigrations
pPool := startPoolStatsExporter
pRedis := connectRedis
pGeo := loadGeoLite2
pProv := newProvisionerClient
pRouter := newRouterWithHooks
pServe := serveFunc
t.Cleanup(func() {
initTracer = pInit
connectPostgres = pPg
runMigrations = pMig
startPoolStatsExporter = pPool
connectRedis = pRedis
loadGeoLite2 = pGeo
newProvisionerClient = pProv
newRouterWithHooks = pRouter
serveFunc = pServe
})
}
// setMinimalValidEnv sets exactly the env config.Load() needs to return
// without panicking, plus a no-op tracer endpoint. PLANS_PATH points at a
// missing file so loadPlansRegistry takes the dev-fallback branch
// (ENVIRONMENT=development) — no on-disk plans.yaml required.
func setMinimalValidEnv(t *testing.T) {
t.Helper()
t.Setenv("DATABASE_URL", "postgres://u:p@127.0.0.1:1/none?sslmode=disable")
t.Setenv("JWT_SECRET", "0123456789012345678901234567890123456789")
t.Setenv("AES_KEY", "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff")
t.Setenv("PLANS_PATH", t.TempDir()+"/missing-plans.yaml")
t.Setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "")
t.Setenv("NEW_RELIC_LICENSE_KEY", "")
t.Setenv("PROVISIONER_ADDR", "")
t.Setenv("ENVIRONMENT", "development")
}
// runState records boot-ordering observations made through the seams.
type runState struct {
tracerShutdownCalled atomic.Bool
migrationsCalled atomic.Bool
poolExporterCalled atomic.Bool
routerBuilt atomic.Bool
served atomic.Bool
}
// fakeDB returns a non-pinging *sql.DB handle. sql.Open never dials, so this
// is safe and fast — the model wiring only stores the handle at boot.
func fakeDB(t *testing.T) *sql.DB {
t.Helper()
dbh, err := sql.Open("postgres", "postgres://u:p@127.0.0.1:1/none?sslmode=disable")
require.NoError(t, err)
return dbh
}
// newClosableGeoDBs returns a GeoDBs with non-nil City/ASN readers. A
// zero-value maxminddb.Reader has hasMappedFile=false, so Close() is a safe
// no-op — enough to exercise run()'s geo-close defer branches without a real
// .mmdb fixture on disk.
func newClosableGeoDBs(t *testing.T) *middleware.GeoDBs {
t.Helper()
return &middleware.GeoDBs{City: &maxminddb.Reader{}, ASN: &maxminddb.Reader{}}
}
// wireHappyPathSeams points every external boundary at a non-networking fake
// so run() can boot, build the router, reach the serve seam, and tear down
// without a real Postgres / Redis / GeoIP volume / bound listener. The serve
// seam is left for the caller to set (clean drain vs error arm).
func wireHappyPathSeams(t *testing.T) *runState {
st := &runState{}
initTracer = func(string, string) func(context.Context) error {
return func(context.Context) error {
st.tracerShutdownCalled.Store(true)
return nil
}
}
connectPostgres = func(string) *sql.DB { return fakeDB(t) }
runMigrations = func(*sql.DB) error { st.migrationsCalled.Store(true); return nil }
startPoolStatsExporter = func(ctx context.Context, _ *sql.DB, _ string) {
st.poolExporterCalled.Store(true)
<-ctx.Done() // mirror prod: lives until the boot ctx cancels at teardown
}
connectRedis = func(string) *redis.Client {
return redis.NewClient(&redis.Options{Addr: "127.0.0.1:1"})
}
loadGeoLite2 = func(string) *middleware.GeoDBs { return nil }
newRouterWithHooks = func(_ *config.Config, _ *sql.DB, _ *redis.Client, _ *middleware.GeoDBs, _ *email.Client, _ *plans.Registry, _ *provisioner.Client, _ *newrelic.Application) (*fiber.App, router.ShutdownHooks) {
st.routerBuilt.Store(true)
return fiber.New(fiber.Config{DisableStartupMessage: true}), router.ShutdownHooks{}
}
return st
}
// TestRun_HappyPath_BootsReadyTeardown drives run() end-to-end with all
// external boundaries stubbed. The serve seam returns nil immediately to
// simulate a clean SIGTERM-triggered drain; run() must boot, build the
// router, run migrations, start the pool exporter, then unwind every defer
// and return nil.
func TestRun_HappyPath_BootsReadyTeardown(t *testing.T) {
newRunSeams(t)
setMinimalValidEnv(t)
st := wireHappyPathSeams(t)
serveFunc = func(*fiber.App, string, time.Duration, router.ShutdownHooks) error {
st.served.Store(true)
return nil // clean drain
}
err := run()
require.NoError(t, err, "clean serve return must yield a nil run() error")
assert.True(t, st.migrationsCalled.Load(), "migrations must run during boot")
assert.True(t, st.routerBuilt.Load(), "router must be built before serving")
assert.True(t, st.served.Load(), "serve seam must be reached")
// poolExporter runs in a goroutine; give the scheduler a beat, then the
// defers (poolStatsCancel) will have fired and the tracer shutdown ran.
assert.Eventually(t, st.tracerShutdownCalled.Load, time.Second, 10*time.Millisecond,
"deferred tracer shutdown must run on a clean return")
}
// TestRun_MigrationsFailReturnsError — a migration failure must surface as a
// non-nil run() error (main() turns it into os.Exit(1) → CrashLoopBackoff)
// rather than booting a server against an un-migrated schema.
func TestRun_MigrationsFailReturnsError(t *testing.T) {
newRunSeams(t)
setMinimalValidEnv(t)
st := wireHappyPathSeams(t)
runMigrations = func(*sql.DB) error { return errors.New("relation does not exist") }
serveFunc = func(*fiber.App, string, time.Duration, router.ShutdownHooks) error {
st.served.Store(true)
return nil
}
err := run()
require.Error(t, err, "migration failure must abort boot")
assert.Contains(t, err.Error(), "migrations")
assert.False(t, st.served.Load(), "serve must NOT be reached when migrations fail")
}
// TestRun_PlansLoadFailsInProductionReturnsError — when ENVIRONMENT=production
// and plans.yaml is missing, loadPlansRegistry returns an error and run() must
// abort before serving (fail-loud — never serve stale embedded limits in prod).
func TestRun_PlansLoadFailsInProductionReturnsError(t *testing.T) {
newRunSeams(t)
setMinimalValidEnv(t)
t.Setenv("ENVIRONMENT", "production")
st := wireHappyPathSeams(t)
serveFunc = func(*fiber.App, string, time.Duration, router.ShutdownHooks) error {
st.served.Store(true)
return nil
}
err := run()
require.Error(t, err, "missing plans.yaml in production must abort boot")
assert.Contains(t, err.Error(), "plans")
assert.False(t, st.served.Load(), "serve must NOT be reached when plans load fails in prod")
}
// TestRun_ProvisionerConnectFailsReturnsError — when PROVISIONER_ADDR is set
// but the gRPC client constructor errors, run() must abort before serving.
func TestRun_ProvisionerConnectFailsReturnsError(t *testing.T) {
newRunSeams(t)
setMinimalValidEnv(t)
t.Setenv("PROVISIONER_ADDR", "provisioner.invalid:50051")
st := wireHappyPathSeams(t)
newProvisionerClient = func(string, string) (*provisioner.Client, *grpc.ClientConn, error) {
return nil, nil, errors.New("dial failed")
}
serveFunc = func(*fiber.App, string, time.Duration, router.ShutdownHooks) error {
st.served.Store(true)
return nil
}
err := run()
require.Error(t, err, "provisioner connect failure must abort boot")
assert.Contains(t, err.Error(), "provisioner")
assert.False(t, st.served.Load())
}
// TestRun_ProvisionerConnectSucceedsServes — PROVISIONER_ADDR set and the
// client constructs cleanly: run() must reach the serve seam (the
// remote-provisioner branch), and a clean serve return yields nil.
func TestRun_ProvisionerConnectSucceedsServes(t *testing.T) {
newRunSeams(t)
setMinimalValidEnv(t)
t.Setenv("PROVISIONER_ADDR", "provisioner.invalid:50051")
st := wireHappyPathSeams(t)
newProvisionerClient = func(string, string) (*provisioner.Client, *grpc.ClientConn, error) {
// A nil-backed client + a real (lazy) ClientConn. grpc.NewClient does
// not dial until first RPC, so this never touches the network here.
conn, err := grpc.NewClient("passthrough:///provisioner.invalid:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()))
require.NoError(t, err)
return nil, conn, nil
}
serveFunc = func(*fiber.App, string, time.Duration, router.ShutdownHooks) error {
st.served.Store(true)
return nil
}
err := run()
require.NoError(t, err)
assert.True(t, st.served.Load(), "serve must be reached on the remote-provisioner happy path")
}
// TestRun_ServeErrorReturnsError — when the serve seam reports a fatal
// listener error (port bind failure, stuck-drain timeout), run() must
// surface it so main() exits non-zero.
func TestRun_ServeErrorReturnsError(t *testing.T) {
newRunSeams(t)
setMinimalValidEnv(t)
wireHappyPathSeams(t)
serveFunc = func(*fiber.App, string, time.Duration, router.ShutdownHooks) error {
return errors.New("listen tcp :8080: bind: address already in use")
}
err := run()
require.Error(t, err, "a fatal serve error must propagate out of run()")
assert.Contains(t, err.Error(), "serve")
}
// TestRun_TracerShutdownErrorIsLoggedNotFatal — the deferred tracer shutdown
// returning an error must NOT change run()'s return value (it is logged at
// ERROR and swallowed). A clean serve return stays nil even when the tracer's
// shutdown errors.
func TestRun_TracerShutdownErrorIsLoggedNotFatal(t *testing.T) {
newRunSeams(t)
setMinimalValidEnv(t)
wireHappyPathSeams(t)
initTracer = func(string, string) func(context.Context) error {
return func(context.Context) error { return errors.New("tp shutdown timeout") }
}
serveFunc = func(*fiber.App, string, time.Duration, router.ShutdownHooks) error { return nil }
err := run()
require.NoError(t, err, "tracer shutdown error must be swallowed, not propagated")
}
// TestInitNewRelic_ValidLicenseReturnsApp — a syntactically valid 40-char
// license must produce a non-nil *newrelic.Application (the success arm:
// NewApplication + the "newrelic.initialized" log). NEW_RELIC_APP_NAME, when
// unset, derives "instant-<service>".
func TestInitNewRelic_ValidLicenseReturnsApp(t *testing.T) {
t.Setenv("NEW_RELIC_LICENSE_KEY", strings.Repeat("a", 40))
t.Setenv("NEW_RELIC_APP_NAME", "")
app := initNewRelic("api")
require.NotNil(t, app, "a valid 40-char license must yield a non-nil NR app")
app.Shutdown(2 * 1_000_000_000)
}
// TestInitNewRelic_AppNameOverride — NEW_RELIC_APP_NAME, when set, overrides
// the derived "instant-<service>" name (covers the appName-set branch).
func TestInitNewRelic_AppNameOverride(t *testing.T) {
t.Setenv("NEW_RELIC_LICENSE_KEY", strings.Repeat("b", 40))
t.Setenv("NEW_RELIC_APP_NAME", "custom-app-name")
app := initNewRelic("api")
require.NotNil(t, app)
app.Shutdown(2 * 1_000_000_000)
}
// TestInitNewRelic_InvalidLicenseFailsOpen — a malformed (non-40-char,
// non-empty) license makes NewApplication error; initNewRelic must log and
// return nil rather than crash boot (the init_failed fail-open arm).
func TestInitNewRelic_InvalidLicenseFailsOpen(t *testing.T) {
t.Setenv("NEW_RELIC_LICENSE_KEY", "too-short-to-be-valid")
app := initNewRelic("api")
require.Nil(t, app, "a malformed license must fail open to nil, not panic")
}
// TestRun_WithNRAppAndGeoDBs_CoversTeardownDefers drives run() with a non-nil
// NR app (so the nrApp.Shutdown + SetNRApp branch runs) and a non-nil GeoDBs
// with closable City/ASN handles (so both geo-close defers run). Asserts a
// clean boot→serve→teardown with no panic on the extra defer paths.
func TestRun_WithNRAppAndGeoDBs_CoversTeardownDefers(t *testing.T) {
newRunSeams(t)
setMinimalValidEnv(t)
// Valid license → initNewRelic returns a non-nil app inside run().
t.Setenv("NEW_RELIC_LICENSE_KEY", strings.Repeat("c", 40))
st := wireHappyPathSeams(t)
// Non-nil GeoDBs with real (closable) maxmind readers from an embedded
// fixture would be heavy; instead supply a GeoDBs whose City/ASN are
// non-nil readers via the test-only opener. middleware.LoadGeoLite2 is
// seamed, so we return a GeoDBs the defers can Close() without panicking.
loadGeoLite2 = func(string) *middleware.GeoDBs { return newClosableGeoDBs(t) }
serveFunc = func(*fiber.App, string, time.Duration, router.ShutdownHooks) error {
st.served.Store(true)
return nil
}
err := run()
require.NoError(t, err)
assert.True(t, st.served.Load())
}
// TestEmitDeployAuditSelfReport_SuccessAgainstRealDB — against a real
// migrated platform DB, emitDeployAuditSelfReport must insert a row and take
// the success-log arm. Skips when TEST_DATABASE_URL is unset.
func TestEmitDeployAuditSelfReport_SuccessAgainstRealDB(t *testing.T) {
if os.Getenv("TEST_DATABASE_URL") == "" {
t.Skip("TEST_DATABASE_URL not set; skipping DB-backed self-report test")
}
dbh, clean := testhelpers.SetupTestDB(t)
defer clean()
_, _ = dbh.Exec(`DELETE FROM deploys_audit`)
t.Cleanup(func() { _, _ = dbh.Exec(`DELETE FROM deploys_audit`) })
// Must not panic and must write a row (success arm).
emitDeployAuditSelfReport(dbh)
var n int
require.NoError(t, dbh.QueryRow(`SELECT count(*) FROM deploys_audit WHERE service='api'`).Scan(&n))
assert.GreaterOrEqual(t, n, 1, "self-report success arm must insert at least one row")
}
// TestEmitDeployAuditSelfReport_DBErrorIsSwallowed — a non-pinging handle
// makes InsertSelfReport fail; emitDeployAuditSelfReport must log at WARN and
// return without panicking (observability, never a boot gate).
func TestEmitDeployAuditSelfReport_DBErrorIsSwallowed(t *testing.T) {
dbh, err := sql.Open("postgres", "postgres://u:p@127.0.0.1:1/none?sslmode=disable")
require.NoError(t, err)
defer dbh.Close()
assert.NotPanics(t, func() { emitDeployAuditSelfReport(dbh) },
"a DB error in the self-report must be swallowed, never panic boot")
}
// TestMain_DelegatesToRun is a compile-time + behaviour guard that main()
// is the thin os.Exit wrapper around run(). We can't call main() directly (it
// would os.Exit the test binary), but we assert run() is a free function with
// the documented error-returning contract that main() depends on.
func TestRun_IsErrorReturning(t *testing.T) {
// Documents the seam contract relied on by main(): run returns an error.
var fn func() error = run
require.NotNil(t, fn)
// envProduction sanity — run()'s plans branch keys off it.
require.True(t, strings.EqualFold(envProduction, "production"))
}
// TestMain_ExitsNonZeroOnRunError — main() must call os.Exit(1) when run()
// returns an error. We stub the runFunc and osExit seams so main() can be
// driven in-process (it normally os.Exit()s the test binary). Production
// wiring (runFunc==run, osExit==os.Exit) is untouched.
func TestMain_ExitsNonZeroOnRunError(t *testing.T) {
prevRun, prevExit := runFunc, osExit
t.Cleanup(func() { runFunc, osExit = prevRun, prevExit })
runFunc = func() error { return errors.New("boot failed") }
var gotCode int
var exited bool
osExit = func(code int) { gotCode = code; exited = true }
main()
require.True(t, exited, "main() must call osExit when run() returns an error")
require.Equal(t, 1, gotCode, "main() must exit with code 1 on a run() error")
}
// TestMain_NoExitOnCleanRun — when run() returns nil (clean SIGTERM drain),
// main() must NOT call os.Exit. Pins the happy-path wrapper.
func TestMain_NoExitOnCleanRun(t *testing.T) {
prevRun, prevExit := runFunc, osExit
t.Cleanup(func() { runFunc, osExit = prevRun, prevExit })
runFunc = func() error { return nil }
exited := false
osExit = func(int) { exited = true }
main()
require.False(t, exited, "main() must not exit when run() returns nil")
}