-
Notifications
You must be signed in to change notification settings - Fork 29
Expand file tree
/
Copy pathmain_test.go
More file actions
168 lines (146 loc) · 5.78 KB
/
Copy pathmain_test.go
File metadata and controls
168 lines (146 loc) · 5.78 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
package main
import (
"bytes"
"encoding/json"
"errors"
"net"
"os"
"os/exec"
"runtime"
"strings"
"syscall"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/dependabot/proxy/internal/config"
)
// helperProcessEnv, when set to "1" in the environment, tells TestMain to run
// the real main() instead of the test suite. This lets tests re-exec the
// already-compiled test binary as a standalone proxy process and observe its
// real exit code - something that can't be done in-process once main() calls
// log.Fatal/os.Exit. This is the same "helper process" technique the Go
// standard library uses to test os.Exit/signal behavior (see os/exec and
// os/signal tests).
const helperProcessEnv = "PROXY_HELPER_PROCESS"
func TestMain(m *testing.M) {
if os.Getenv(helperProcessEnv) == "1" {
main()
return
}
os.Exit(m.Run())
}
// runHelperProcess builds an *exec.Cmd that re-executes this test binary as a
// standalone proxy process (via the TestMain hook above), passing args as
// command-line flags and stdin as its stdin.
func runHelperProcess(t *testing.T, args []string, stdin string) *exec.Cmd {
t.Helper()
cmd := exec.CommandContext(t.Context(), os.Args[0], args...) //nolint:gosec // args are test-controlled, not user input
cmd.Env = append(os.Environ(), helperProcessEnv+"=1")
cmd.Stdin = strings.NewReader(stdin)
return cmd
}
// minimalConfigJSON returns a valid proxy config (with a working MITM CA)
// serialized as JSON, suitable for feeding via stdin so the helper process
// can get all the way to server.ListenAndServe().
func minimalConfigJSON(t *testing.T) string {
t.Helper()
cfg := config.Config{CA: testCA()}
b, err := json.Marshal(cfg)
require.NoError(t, err)
return string(b)
}
// freeAddr returns a "host:port" address that is free at the time of the
// call by briefly binding to port 0 and releasing it.
func freeAddr(t *testing.T) string {
t.Helper()
var listenConfig net.ListenConfig
l, err := listenConfig.Listen(t.Context(), "tcp", "127.0.0.1:0")
require.NoError(t, err)
addr := l.Addr().String()
require.NoError(t, l.Close())
return addr
}
// exitCodeFromWaitErr extracts the process exit code from the error returned
// by exec.Cmd.Wait()/Run(). A nil error means exit code 0.
func exitCodeFromWaitErr(t *testing.T, err error) int {
t.Helper()
if err == nil {
return 0
}
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
return exitErr.ExitCode()
}
t.Fatalf("helper process did not exit normally: %v", err)
return -1
}
// TestListenAndServe_AddressInUse_ExitsNonZero reproduces the reported
// defect: the proxy previously logged the bind error and exited 0, causing
// integrations that gate on exit code (e.g. github/codeql-action) to
// silently proceed as if the proxy were listening.
func TestListenAndServe_AddressInUse_ExitsNonZero(t *testing.T) {
var listenConfig net.ListenConfig
l, err := listenConfig.Listen(t.Context(), "tcp", "127.0.0.1:0")
require.NoError(t, err)
defer func() {
require.NoError(t, l.Close())
}()
addr := l.Addr().String()
cmd := runHelperProcess(t, []string{"-addr=" + addr, "-config=-"}, minimalConfigJSON(t))
var stderr bytes.Buffer
cmd.Stderr = &stderr
runErr := cmd.Run()
code := exitCodeFromWaitErr(t, runErr)
assert.NotEqual(t, 0, code, "expected non-zero exit code when the listen address is already in use, got 0 (output: %s)", stderr.String())
// The OS-level error text differs by platform (e.g. "address already in
// use" on Unix vs. "Only one usage of each socket address..." on
// Windows), but the "listen tcp <addr>: bind:" prefix is generated by
// Go's net package itself and is stable across platforms.
assert.Contains(t, stderr.String(), "listen tcp "+addr+": bind:")
}
// TestGracefulShutdown_ExitsZero is a regression guard: a clean shutdown via
// SIGTERM (the normal operational path) must still exit 0 after the fix.
func TestGracefulShutdown_ExitsZero(t *testing.T) {
addr := freeAddr(t)
cmd := runHelperProcess(t, []string{"-addr=" + addr, "-config=-"}, minimalConfigJSON(t))
var stderr bytes.Buffer
cmd.Stderr = &stderr
require.NoError(t, cmd.Start())
// Note: stderr is not safe to read here - it's being written to
// concurrently by the still-running subprocess, so the failure message
// below intentionally omits its contents (only safe to read after
// cmd.Wait() below).
dialer := net.Dialer{Timeout: 100 * time.Millisecond}
require.Eventually(t, func() bool {
conn, dialErr := dialer.DialContext(t.Context(), "tcp", addr)
if dialErr != nil {
return false
}
_ = conn.Close()
return true
}, 5*time.Second, 50*time.Millisecond, "proxy did not start listening in time")
if runtime.GOOS == "windows" {
// os.Process.Signal(syscall.SIGTERM) is not supported on Windows: it
// returns os.ErrProcessDone/EWINDOWS instead of delivering a graceful
// shutdown signal, so the assertion below would fail there even
// though the exit-code fix itself is correct. Terminate the helper
// process directly instead of leaking it, and skip the
// signal-based assertion on this platform.
_ = cmd.Process.Kill()
_ = cmd.Wait()
t.Skip("SIGTERM cannot be delivered via os.Process.Signal on Windows; skipping graceful-shutdown assertion")
}
require.NoError(t, cmd.Process.Signal(syscall.SIGTERM))
waitErr := cmd.Wait()
code := exitCodeFromWaitErr(t, waitErr)
assert.Equal(t, 0, code, "expected graceful shutdown to exit 0 (output: %s)", stderr.String())
}
func TestInvalidConfigPath_ExitsNonZero(t *testing.T) {
cmd := runHelperProcess(t, []string{"-config=/nonexistent/path/definitely-missing.json"}, "")
var stderr bytes.Buffer
cmd.Stderr = &stderr
runErr := cmd.Run()
code := exitCodeFromWaitErr(t, runErr)
assert.NotEqual(t, 0, code, "expected non-zero exit code for an invalid config path, got 0 (output: %s)", stderr.String())
}