1package app
2
3import (
4 "net/http"
5 "os"
6 "path/filepath"
7 "testing"
8 "time"
9
10 "github.com/bnema/zerowrap"
11 "github.com/stretchr/testify/assert"
12 "github.com/stretchr/testify/require"
13
14 "github.com/bnema/gordon/internal/domain"
15 "github.com/bnema/gordon/internal/usecase/traffic"
16)
17
18func writeTimeoutConfig(t *testing.T, body string) string {
19 t.Helper()
20 path := filepath.Join(t.TempDir(), "gordon.toml")
21 require.NoError(t, os.WriteFile(path, []byte(body), 0o600))
22 return path
23}
24
25func TestInitConfigHTTPTimeoutDefaults(t *testing.T) {
26 _, cfg, err := initConfig(writeTimeoutConfig(t, ""))
27 require.NoError(t, err)
28
29 registry, err := buildRegistryHTTPTimeouts(cfg)
30 require.NoError(t, err)
31 assert.Equal(t, domain.RegistryHTTPTimeouts{
32 ReadHeaderTimeout: 10 * time.Second,
33 ReadTimeout: 5 * time.Minute,
34 WriteTimeout: 5 * time.Minute,
35 IdleTimeout: 120 * time.Second,
36 }, registry)
37
38 entry, err := traffic.ParseHTTPOptions(cfg.Traffic.HTTP)
39 require.NoError(t, err)
40 assert.Equal(t, domain.HTTPOptions{
41 ReadHeaderTimeout: 5 * time.Second,
42 IdleTimeout: 30 * time.Second,
43 TransferIdleTimeout: 2 * time.Minute,
44 }, entry)
45}
46
47func TestInitConfigHTTPTimeoutFileOverride(t *testing.T) {
48 path := writeTimeoutConfig(t, `
49[server.http]
50read_header_timeout = "3s"
51read_timeout = "1m"
52write_timeout = "2m"
53idle_timeout = "45s"
54
55[traffic.http]
56read_header_timeout = "4s"
57idle_timeout = "15s"
58transfer_idle_timeout = "10m"
59`)
60 _, cfg, err := initConfig(path)
61 require.NoError(t, err)
62
63 registry, err := buildRegistryHTTPTimeouts(cfg)
64 require.NoError(t, err)
65 assert.Equal(t, 3*time.Second, registry.ReadHeaderTimeout)
66 assert.Equal(t, time.Minute, registry.ReadTimeout)
67 assert.Equal(t, 2*time.Minute, registry.WriteTimeout)
68 assert.Equal(t, 45*time.Second, registry.IdleTimeout)
69
70 entry, err := traffic.ParseHTTPOptions(cfg.Traffic.HTTP)
71 require.NoError(t, err)
72 assert.Equal(t, 4*time.Second, entry.ReadHeaderTimeout)
73 assert.Equal(t, 15*time.Second, entry.IdleTimeout)
74 assert.Equal(t, 10*time.Minute, entry.TransferIdleTimeout)
75}
76
77func TestInitConfigHTTPTimeoutEnvOverride(t *testing.T) {
78 t.Setenv("GORDON_TRAFFIC_HTTP_TRANSFER_IDLE_TIMEOUT", "7m")
79 t.Setenv("GORDON_TRAFFIC_HTTP_READ_HEADER_TIMEOUT", "6s")
80 t.Setenv("GORDON_TRAFFIC_HTTP_IDLE_TIMEOUT", "11s")
81 t.Setenv("GORDON_SERVER_HTTP_WRITE_TIMEOUT", "9m")
82 t.Setenv("GORDON_SERVER_HTTP_READ_TIMEOUT", "8m")
83 t.Setenv("GORDON_SERVER_HTTP_READ_HEADER_TIMEOUT", "12s")
84 t.Setenv("GORDON_SERVER_HTTP_IDLE_TIMEOUT", "50s")
85 t.Setenv("GORDON_TRAFFIC_TCP_DIAL_TIMEOUT", "3s")
86
87 path := writeTimeoutConfig(t, "[traffic.http]\ntransfer_idle_timeout = \"1m\"\n")
88 _, cfg, err := initConfig(path)
89 require.NoError(t, err)
90
91 entry, err := traffic.ParseHTTPOptions(cfg.Traffic.HTTP)
92 require.NoError(t, err)
93 assert.Equal(t, 7*time.Minute, entry.TransferIdleTimeout, "env wins over file")
94 assert.Equal(t, 6*time.Second, entry.ReadHeaderTimeout)
95 assert.Equal(t, 11*time.Second, entry.IdleTimeout)
96
97 registry, err := buildRegistryHTTPTimeouts(cfg)
98 require.NoError(t, err)
99 assert.Equal(t, 9*time.Minute, registry.WriteTimeout)
100 assert.Equal(t, 8*time.Minute, registry.ReadTimeout)
101 assert.Equal(t, 12*time.Second, registry.ReadHeaderTimeout)
102 assert.Equal(t, 50*time.Second, registry.IdleTimeout)
103
104 // Existing traffic.tcp keys are overridable too (they need a default).
105 assert.Equal(t, "3s", cfg.Traffic.TCP.DialTimeout)
106}
107
108func TestInitConfigRejectsInvalidHTTPTimeouts(t *testing.T) {
109 tests := []struct {
110 name string
111 body string
112 key string
113 }{
114 {"malformed registry", "[server.http]\nread_timeout = \"soon\"\n", "server.http.read_timeout"},
115 {"negative registry", "[server.http]\nwrite_timeout = \"-5s\"\n", "server.http.write_timeout"},
116 {"zero registry", "[server.http]\nidle_timeout = \"0s\"\n", "server.http.idle_timeout"},
117 {"malformed traffic", "[traffic.http]\nread_header_timeout = \"x\"\n", "traffic.http.read_header_timeout"},
118 {"negative traffic", "[traffic.http]\ntransfer_idle_timeout = \"-1m\"\n", "traffic.http.transfer_idle_timeout"},
119 {"zero traffic", "[traffic.http]\nidle_timeout = \"0\"\n", "traffic.http.idle_timeout"},
120 }
121 for _, tt := range tests {
122 t.Run(tt.name, func(t *testing.T) {
123 _, _, err := initConfig(writeTimeoutConfig(t, tt.body))
124 require.Error(t, err)
125 assert.ErrorContains(t, err, tt.key)
126 })
127 }
128}
129
130func TestInitConfigRejectsInvalidHTTPTimeoutFromEnv(t *testing.T) {
131 t.Setenv("GORDON_TRAFFIC_HTTP_TRANSFER_IDLE_TIMEOUT", "-3s")
132 _, _, err := initConfig(writeTimeoutConfig(t, ""))
133 require.ErrorContains(t, err, "traffic.http.transfer_idle_timeout")
134}
135
136func TestStartServerAppliesConfiguredTimeouts(t *testing.T) {
137 timeouts := domain.RegistryHTTPTimeouts{
138 ReadHeaderTimeout: 1 * time.Second,
139 ReadTimeout: 2 * time.Second,
140 WriteTimeout: 3 * time.Second,
141 IdleTimeout: 4 * time.Second,
142 }
143 errChan := make(chan error, 1)
144 srv, ready := startServer("127.0.0.1:0", http.NotFoundHandler(), "test", nil, timeouts, errChan, zerowrap.Default())
145 <-ready
146 defer func() { _ = srv.Close() }()
147
148 assert.Equal(t, timeouts.ReadHeaderTimeout, srv.ReadHeaderTimeout)
149 assert.Equal(t, timeouts.ReadTimeout, srv.ReadTimeout)
150 assert.Equal(t, timeouts.WriteTimeout, srv.WriteTimeout)
151 assert.Equal(t, timeouts.IdleTimeout, srv.IdleTimeout)
152}
153
154func TestStartServerFallsBackToDefaultTimeouts(t *testing.T) {
155 errChan := make(chan error, 1)
156 srv, ready := startServer("127.0.0.1:0", http.NotFoundHandler(), "test", nil, domain.RegistryHTTPTimeouts{}, errChan, zerowrap.Default())
157 <-ready
158 defer func() { _ = srv.Close() }()
159
160 assert.Equal(t, domain.DefaultRegistryHTTPTimeouts(), domain.RegistryHTTPTimeouts{
161 ReadHeaderTimeout: srv.ReadHeaderTimeout,
162 ReadTimeout: srv.ReadTimeout,
163 WriteTimeout: srv.WriteTimeout,
164 IdleTimeout: srv.IdleTimeout,
165 })
166}
167
168func TestStartRegistryServersRejectsInvalidTimeouts(t *testing.T) {
169 cfg := Config{}
170 cfg.Server.HTTP.ReadTimeout = "-1s"
171 _, _, _, _, err := startRegistryServers(cfg, http.NotFoundHandler(), make(chan error, 1), zerowrap.Default())
172 require.ErrorContains(t, err, "server.http.read_timeout")
173}