Skip to content

Commit 4a417dc

Browse files
authored
[+] add main integration tests (#753)
1 parent a8a8160 commit 4a417dc

File tree

3 files changed

+212
-39
lines changed

3 files changed

+212
-39
lines changed

internal/api/api.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type RestAPIServer struct {
2424
}
2525

2626
func Init(opts config.RestAPIOpts, logger log.LoggerIface) *RestAPIServer {
27+
mux := http.NewServeMux()
2728
s := &RestAPIServer{
2829
nil,
2930
logger,
@@ -32,12 +33,13 @@ func Init(opts config.RestAPIOpts, logger log.LoggerIface) *RestAPIServer {
3233
ReadTimeout: 10 * time.Second,
3334
WriteTimeout: 10 * time.Second,
3435
MaxHeaderBytes: 1 << 20,
36+
Handler: mux,
3537
},
3638
}
37-
http.HandleFunc("/liveness", s.livenessHandler)
38-
http.HandleFunc("/readiness", s.readinessHandler)
39-
http.HandleFunc("/startchain", s.chainHandler)
40-
http.HandleFunc("/stopchain", s.chainHandler)
39+
mux.HandleFunc("/liveness", s.livenessHandler)
40+
mux.HandleFunc("/readiness", s.readinessHandler)
41+
mux.HandleFunc("/startchain", s.chainHandler)
42+
mux.HandleFunc("/stopchain", s.chainHandler)
4143
if opts.Port != 0 {
4244
logger.WithField("port", opts.Port).Info("Starting REST API server...")
4345
go func() { logger.Error(s.ListenAndServe()) }()

main.go

Lines changed: 39 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -66,47 +66,21 @@ func printVersion() {
6666
`, version, dbapi, commit, date)
6767
}
6868

69-
func main() {
70-
cmdOpts, err := config.NewConfig(os.Stdout)
71-
if err != nil {
72-
if cmdOpts != nil && cmdOpts.VersionOnly() {
73-
printVersion()
74-
return
75-
}
76-
fmt.Println("Configuration error: ", err)
77-
exitCode = ExitCodeConfigError
78-
return
79-
}
80-
if cmdOpts.Version {
81-
printVersion()
82-
}
83-
84-
logger := log.Init(cmdOpts.Logging)
85-
ctx, cancel := context.WithCancel(context.Background())
86-
SetupCloseHandler(cancel)
87-
defer func() {
88-
cancel()
89-
if err := recover(); err != nil {
90-
exitCode = ExitCodeFatalError
91-
logger.WithField("callstack", string(debug.Stack())).Error(err)
92-
}
93-
os.Exit(exitCode)
94-
}()
95-
69+
// run contains the core application logic and returns an exit code.
70+
func run(ctx context.Context, cmdOpts *config.CmdOptions, logger log.LoggerHookerIface) int {
9671
apiserver := api.Init(cmdOpts.RESTApi, logger)
9772

73+
var err error
9874
if pge, err = pgengine.New(ctx, *cmdOpts, logger); err != nil {
9975
logger.WithError(err).Error("Connection failed")
100-
exitCode = ExitCodeDBEngineError
101-
return
76+
return ExitCodeDBEngineError
10277
}
10378
defer pge.Finalize()
10479

10580
if cmdOpts.Start.Upgrade {
10681
if err := pge.MigrateDb(ctx); err != nil {
10782
logger.WithError(err).Error("Upgrade failed")
108-
exitCode = ExitCodeUpgradeError
109-
return
83+
return ExitCodeUpgradeError
11084
}
11185
} else {
11286
if upgrade, err := pge.CheckNeedMigrateDb(ctx); upgrade || err != nil {
@@ -116,17 +90,47 @@ func main() {
11690
if err != nil {
11791
logger.WithError(err).Error("Migration check failed")
11892
}
119-
exitCode = ExitCodeUpgradeError
120-
return
93+
return ExitCodeUpgradeError
12194
}
12295
}
12396
if cmdOpts.Start.Init {
124-
return
97+
return ExitCodeOK
12598
}
12699
sch := scheduler.New(pge, logger)
127100
apiserver.APIHandler = sch
128101

129102
if sch.Run(ctx) == scheduler.ShutdownStatus {
130-
exitCode = ExitCodeShutdownCommand
103+
return ExitCodeShutdownCommand
104+
}
105+
return ExitCodeOK
106+
}
107+
108+
func main() {
109+
cmdOpts, err := config.NewConfig(os.Stdout)
110+
if err != nil {
111+
if cmdOpts != nil && cmdOpts.VersionOnly() {
112+
printVersion()
113+
return
114+
}
115+
fmt.Println("Configuration error: ", err)
116+
exitCode = ExitCodeConfigError
117+
return
118+
}
119+
if cmdOpts.Version {
120+
printVersion()
131121
}
122+
123+
logger := log.Init(cmdOpts.Logging)
124+
ctx, cancel := context.WithCancel(context.Background())
125+
SetupCloseHandler(cancel)
126+
defer func() {
127+
cancel()
128+
if err := recover(); err != nil {
129+
exitCode = ExitCodeFatalError
130+
logger.WithField("callstack", string(debug.Stack())).Error(err)
131+
}
132+
os.Exit(exitCode)
133+
}()
134+
135+
exitCode = run(ctx, cmdOpts, logger)
132136
}

main_test.go

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"io"
7+
"os"
8+
"runtime"
9+
"syscall"
10+
"testing"
11+
"time"
12+
13+
"github.com/cybertec-postgresql/pg_timetable/internal/config"
14+
"github.com/cybertec-postgresql/pg_timetable/internal/log"
15+
"github.com/stretchr/testify/assert"
16+
"github.com/stretchr/testify/require"
17+
"github.com/testcontainers/testcontainers-go"
18+
"github.com/testcontainers/testcontainers-go/modules/postgres"
19+
"github.com/testcontainers/testcontainers-go/wait"
20+
)
21+
22+
// newTestLogger returns a silent logger suitable for use in tests.
23+
func newTestLogger() log.LoggerHookerIface {
24+
return log.Init(config.LoggingOpts{LogLevel: "panic", LogDBLevel: "none"})
25+
}
26+
27+
// setupTestContainer starts a bare PostgreSQL container and returns the
28+
// connection string along with a cleanup function. Unlike the shared
29+
// testutils helper, it does NOT initialise the pg_timetable schema so that
30+
// run() can perform that step itself.
31+
func setupTestContainer(t *testing.T) (connStr string, cleanup func()) {
32+
t.Helper()
33+
ctx := context.Background()
34+
c, err := postgres.Run(
35+
ctx,
36+
"postgres:18-alpine",
37+
postgres.WithDatabase("timetable"),
38+
postgres.WithUsername("scheduler"),
39+
postgres.WithPassword("somestrong"),
40+
testcontainers.WithWaitStrategyAndDeadline(
41+
60*time.Second,
42+
wait.ForLog("database system is ready to accept connections").WithOccurrence(2),
43+
),
44+
)
45+
require.NoError(t, err, "Failed to start PostgreSQL container")
46+
cs, err := c.ConnectionString(ctx, "sslmode=disable")
47+
if err != nil {
48+
_ = c.Terminate(ctx)
49+
t.Fatalf("Failed to get connection string: %v", err)
50+
}
51+
return cs, func() { _ = c.Terminate(ctx) }
52+
}
53+
54+
// TestPrintVersion verifies that printVersion writes the expected fields to
55+
// stdout.
56+
func TestPrintVersion(t *testing.T) {
57+
r, w, err := os.Pipe()
58+
require.NoError(t, err)
59+
oldStdout := os.Stdout
60+
os.Stdout = w
61+
62+
printVersion()
63+
64+
w.Close()
65+
os.Stdout = oldStdout
66+
67+
var buf bytes.Buffer
68+
_, _ = io.Copy(&buf, r)
69+
out := buf.String()
70+
71+
assert.Contains(t, out, "pg_timetable:")
72+
assert.Contains(t, out, "Version:")
73+
assert.Contains(t, out, "DB Schema:")
74+
assert.Contains(t, out, "Git Commit:")
75+
assert.Contains(t, out, "Built:")
76+
}
77+
78+
// TestSetupCloseHandler verifies that sending SIGTERM causes the provided
79+
// cancel function to be called. Skipped on Windows where signal delivery to
80+
// the current process works differently.
81+
func TestSetupCloseHandler(t *testing.T) {
82+
if runtime.GOOS == "windows" {
83+
t.Skip("SIGTERM delivery to self is not supported on Windows")
84+
}
85+
86+
ctx, cancel := context.WithCancel(context.Background())
87+
done := make(chan struct{})
88+
SetupCloseHandler(func() {
89+
cancel()
90+
close(done)
91+
})
92+
93+
p, err := os.FindProcess(os.Getpid())
94+
require.NoError(t, err)
95+
require.NoError(t, p.Signal(syscall.SIGTERM))
96+
97+
select {
98+
case <-done:
99+
case <-time.After(3 * time.Second):
100+
t.Fatal("cancel was not called within 3 s of receiving SIGTERM")
101+
}
102+
assert.ErrorIs(t, ctx.Err(), context.Canceled)
103+
}
104+
105+
// TestRunDBConnectionFailure verifies that run returns ExitCodeDBEngineError
106+
// when the database is unreachable.
107+
func TestRunDBConnectionFailure(t *testing.T) {
108+
ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second)
109+
defer cancel()
110+
111+
cmdOpts := config.NewCmdOptions(
112+
"--clientname=test_conn_fail",
113+
// port 1 is almost universally refused immediately
114+
"--connstr=postgres://invalid:invalid@localhost:1/invalid?sslmode=disable",
115+
)
116+
code := run(ctx, cmdOpts, newTestLogger())
117+
assert.Equal(t, ExitCodeDBEngineError, code)
118+
}
119+
120+
// TestRunInitOnly verifies that run initialises the database schema and exits
121+
// cleanly when the --init flag is supplied.
122+
func TestRunInitOnly(t *testing.T) {
123+
connStr, cleanup := setupTestContainer(t)
124+
defer cleanup()
125+
126+
cmdOpts := config.NewCmdOptions(
127+
"--clientname=test_main_init",
128+
"--connstr="+connStr,
129+
"--init",
130+
)
131+
code := run(context.Background(), cmdOpts, newTestLogger())
132+
assert.Equal(t, ExitCodeOK, code)
133+
}
134+
135+
// TestRunUpgrade verifies that run performs a schema upgrade and exits cleanly
136+
// when the --upgrade flag is combined with --init.
137+
func TestRunUpgrade(t *testing.T) {
138+
connStr, cleanup := setupTestContainer(t)
139+
defer cleanup()
140+
141+
cmdOpts := config.NewCmdOptions(
142+
"--clientname=test_main_upgrade",
143+
"--connstr="+connStr,
144+
"--upgrade",
145+
"--init",
146+
)
147+
code := run(context.Background(), cmdOpts, newTestLogger())
148+
assert.Equal(t, ExitCodeOK, code)
149+
}
150+
151+
// TestRunContextCancellation verifies that run returns ExitCodeOK (not
152+
// ExitCodeShutdownCommand) when the context is cancelled while the scheduler
153+
// is running.
154+
func TestRunContextCancellation(t *testing.T) {
155+
connStr, cleanup := setupTestContainer(t)
156+
defer cleanup()
157+
158+
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
159+
defer cancel()
160+
161+
cmdOpts := config.NewCmdOptions(
162+
"--clientname=test_main_cancel",
163+
"--connstr="+connStr,
164+
)
165+
code := run(ctx, cmdOpts, newTestLogger())
166+
assert.Equal(t, ExitCodeOK, code)
167+
}

0 commit comments

Comments
 (0)