Skip to content

Commit e25c074

Browse files
committed
feat(tests,config): add tests, response delay, and logging fixes
Add comprehensive for configuration parsing and port precedence. New main_test.go covers missing config file handling, invalid PORT env values, using env port when no flag is provided, flag overriding env, config+flag precedence, and decoding unknown fields. Helpers are added to write temporary config files. Introduce a Delay field on Response (time.Duration) and apply the delay when writing responses. Extract JSON content type into a constant (JsonContentType) and use it when setting the header. Simplify responsesWriter by removing the logger parameter and use slog.Error for error reporting when writing response bodies. Misc: update test logger helper signature to accept testing.TB and adjust imports to include time. These changes enable testing of port/config behavior and add configurable response delays.
1 parent 9b1997b commit e25c074

File tree

9 files changed

+382
-81
lines changed

9 files changed

+382
-81
lines changed

.golangci.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,9 @@ linters:
77
- asasalint
88
- asciicheck
99
- bodyclose
10-
- contextcheck
1110
- copyloopvar
1211
- dupl
1312
- durationcheck
14-
- err113
1513
- errcheck
1614
- errorlint
1715
- exhaustive

config.go

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@ import (
77
"net/http"
88
"os"
99
"text/template"
10+
"time"
1011
)
1112

13+
const JsonContentType = "application/json"
14+
1215
type Config struct {
1316
RequestIDHeader string `json:"request_id_header,omitempty" yaml:"request_id_header,omitempty"`
1417
Routes []Route `json:"routes" yaml:"routes"`
@@ -21,15 +24,16 @@ type Route struct {
2124
}
2225

2326
type Response struct {
24-
Headers http.Header `json:"headers,omitempty" yaml:"headers,omitempty"`
25-
Repeat *int `json:"repeat,omitempty" yaml:"repeat,omitempty"`
26-
Body string `json:"body,omitempty" yaml:"body,omitempty"`
27-
File string `json:"file,omitempty" yaml:"file,omitempty"`
28-
Code int `json:"code,omitempty" yaml:"code,omitempty"`
29-
IsJSON bool `json:"is_json,omitempty" yaml:"is_json,omitempty"`
27+
Headers http.Header `json:"headers,omitempty" yaml:"headers,omitempty"`
28+
Repeat *int `json:"repeat,omitempty" yaml:"repeat,omitempty"`
29+
Body string `json:"body,omitempty" yaml:"body,omitempty"`
30+
File string `json:"file,omitempty" yaml:"file,omitempty"`
31+
Code int `json:"code,omitempty" yaml:"code,omitempty"`
32+
IsJSON bool `json:"is_json,omitempty" yaml:"is_json,omitempty"`
33+
Delay time.Duration `json:"delay,omitempty" yaml:"delay,omitempty"`
3034
}
3135

32-
func responsesWriter(responses []Response, log *slog.Logger) http.HandlerFunc {
36+
func responsesWriter(responses []Response) http.HandlerFunc {
3337
var i int
3438
return func(writer http.ResponseWriter, request *http.Request) {
3539
for {
@@ -69,14 +73,17 @@ func responsesWriter(responses []Response, log *slog.Logger) http.HandlerFunc {
6973
}
7074
if response.IsJSON {
7175
if writer.Header().Get("Content-Type") == "" {
72-
writer.Header().Set("Content-Type", "application/json")
76+
writer.Header().Set("Content-Type", JsonContentType)
7377
}
7478
}
79+
if response.Delay > 0 {
80+
time.Sleep(response.Delay)
81+
}
7582
writer.WriteHeader(response.Code)
7683

7784
if len(data) > 0 {
7885
if _, err := writer.Write(data); err != nil {
79-
log.ErrorContext(request.Context(), "sending response failed", slog.String("error", err.Error()))
86+
slog.Error("Sending response failed", slog.String("error", err.Error()))
8087
}
8188
}
8289
return

config.schema.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@
6363
"repeat": {
6464
"description": "the number of repeats. Infinity if no set. Zero to skip. Or an exact number of repeats.",
6565
"type": "integer"
66+
},
67+
"delay": {
68+
"description": "The delay before sending the response",
69+
"default": "0ms",
70+
"type": "string"
6671
}
6772
}
6873
},

example_config.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,7 @@ routes:
4444
repeat: 1
4545
- code: 404
4646
body: user "{{.PathValue "id"}}" not found
47+
- pattern: GET /delay/1min
48+
responses:
49+
- code: 200
50+
delay: 1m

integration_test.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net"
7+
"net/http"
8+
"strconv"
9+
"testing"
10+
"time"
11+
)
12+
13+
// TestRun_ServerIntegration starts the real HTTP server, exercises endpoints, then shuts it down.
14+
func TestRun_ServerIntegration(t *testing.T) {
15+
t.Parallel()
16+
17+
// Pick a free port
18+
lc := net.ListenConfig{}
19+
ln, err := lc.Listen(t.Context(), "tcp", ":0")
20+
if err != nil {
21+
t.Fatalf("listen :0: %v", err)
22+
}
23+
port := ln.Addr().(*net.TCPAddr).Port
24+
if err := ln.Close(); err != nil {
25+
t.Fatalf("close listener: %v", err)
26+
}
27+
28+
configContent := `routes:
29+
- pattern: /hello
30+
responses:
31+
- code: 200
32+
body: Hello
33+
- pattern: /json
34+
responses:
35+
- code: 201
36+
body: '{"ok":true}'
37+
is_json: true
38+
`
39+
cfgPath := writeConfig(t, configContent)
40+
ctx, cancel := context.WithCancel(t.Context())
41+
42+
done := make(chan struct{})
43+
go func() {
44+
defer close(done)
45+
err := run(ctx, []string{"-c", cfgPath, "-p", strconv.Itoa(port)})
46+
if err != nil {
47+
t.Errorf("run: %v", err)
48+
}
49+
}()
50+
51+
client := &http.Client{Timeout: 2 * time.Second}
52+
base := fmt.Sprintf("http://localhost:%d", port)
53+
54+
deadline := time.Now().Add(5 * time.Second)
55+
for {
56+
if time.Now().After(deadline) {
57+
t.Fatalf("server did not start in time on %s", base)
58+
}
59+
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, base+"/hello", http.NoBody)
60+
if err != nil {
61+
t.Fatalf("new request: %v", err)
62+
}
63+
resp, err := client.Do(req)
64+
if err != nil {
65+
time.Sleep(50 * time.Millisecond)
66+
continue
67+
}
68+
_ = resp.Body.Close()
69+
break
70+
}
71+
72+
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, base+"/hello", http.NoBody)
73+
if err != nil {
74+
t.Fatalf("new request: %v", err)
75+
}
76+
resp, err := client.Do(req)
77+
if err != nil {
78+
t.Fatalf("GET /hello: %v", err)
79+
}
80+
if resp.StatusCode != http.StatusOK {
81+
t.Fatalf("/hello expected 200 got %d", resp.StatusCode)
82+
}
83+
_ = resp.Body.Close()
84+
85+
req, err = http.NewRequestWithContext(t.Context(), http.MethodGet, base+"/json", http.NoBody)
86+
if err != nil {
87+
t.Fatalf("new request: %v", err)
88+
}
89+
resp, err = client.Do(req)
90+
if err != nil {
91+
t.Fatalf("GET /json: %v", err)
92+
}
93+
if resp.StatusCode != http.StatusCreated {
94+
t.Fatalf("/json expected 201 got %d", resp.StatusCode)
95+
}
96+
if ct := resp.Header.Get("Content-Type"); ct != JsonContentType {
97+
t.Fatalf("expected application/json got %q", ct)
98+
}
99+
_ = resp.Body.Close()
100+
101+
req, err = http.NewRequestWithContext(t.Context(), http.MethodGet, base+"/", http.NoBody)
102+
if err != nil {
103+
t.Fatalf("new request: %v", err)
104+
}
105+
resp, err = client.Do(req)
106+
if err != nil {
107+
t.Fatalf("GET /: %v", err)
108+
}
109+
if resp.StatusCode != http.StatusNotFound {
110+
t.Fatalf("/ expected 404 got %d", resp.StatusCode)
111+
}
112+
_ = resp.Body.Close()
113+
114+
cancel()
115+
116+
select {
117+
case <-done:
118+
case <-time.After(5 * time.Second):
119+
t.Fatalf("server did not exit in time")
120+
}
121+
}

log.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"log/slog"
66
"net/http"
7+
"os"
78
)
89

910
type wrapper struct {
@@ -29,7 +30,7 @@ func (w *wrapper) Write(b []byte) (int, error) {
2930

3031
var _ http.ResponseWriter = &wrapper{}
3132

32-
func StructuredLogger(log *slog.Logger, reqIDHeader string, next http.HandlerFunc) http.HandlerFunc {
33+
func StructuredLogger(reqIDHeader string, next http.HandlerFunc) http.HandlerFunc {
3334
return func(writer http.ResponseWriter, request *http.Request) {
3435
wr := &wrapper{writer: writer}
3536
next.ServeHTTP(wr, request)
@@ -39,7 +40,7 @@ func StructuredLogger(log *slog.Logger, reqIDHeader string, next http.HandlerFun
3940
scheme = "https"
4041
}
4142

42-
log.LogAttrs(request.Context(), slog.LevelInfo, "request completed",
43+
slog.LogAttrs(request.Context(), slog.LevelInfo, "request completed",
4344
slog.String("http_scheme", scheme),
4445
slog.String("http_proto", request.Proto),
4546
slog.String("http_method", request.Method),
@@ -52,3 +53,11 @@ func StructuredLogger(log *slog.Logger, reqIDHeader string, next http.HandlerFun
5253
)
5354
}
5455
}
56+
57+
func InitLogger() {
58+
slog.SetLogLoggerLevel(slog.LevelDebug)
59+
logHandler := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{
60+
Level: slog.LevelDebug,
61+
})
62+
slog.SetDefault(slog.New(logHandler))
63+
}

0 commit comments

Comments
 (0)