Skip to content

Commit 82f623b

Browse files
SVilgelmCopilot
andauthored
test: add unit tests for responses writer, templ, logger (#104)
* test: add unit tests for responses writer, templ, logger Add comprehensive tests for the HTTP response writer, template execution, and structured logging middleware. Key changes: - Add TestResponsesWriter_FileAndJSONHeader to verify serving file contents, status code, JSON content-type, and custom headers. - Add TestResponsesWriter_RepeatLogic to assert Repeat behavior keeps a response for N calls before advancing to the next response. - Add TestResponsesWriter_NotFoundWhenExhausted to ensure a 404 is returned when no responses remain. - Add TestResponsesWriter_FileReadError to check 500 on missing file and that the error message includes the filename. - Add TestResponsesWriter_JSONDoesNotOverrideExistingContentType to confirm IsJSON does not overwrite an explicitly set Content-Type. - Add TestExecuteTemplate_Success and TestExecuteTemplate_ParseError to validate template execution and parse error handling. - Add TestStructuredLogger_BasicFields and helpers to validate JSON structured logging output and basic middleware behavior. Also add testLogger helper for building a JSON logger writing to a buffer. Tests use httptest, temporary files, and explicit header assertions to make behavior deterministic. Update responses_writer_test.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Update responses_writer_test.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update responses_writer_test.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 3b98e1a commit 82f623b

File tree

1 file changed

+206
-0
lines changed

1 file changed

+206
-0
lines changed

responses_writer_test.go

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"io"
6+
"log/slog"
7+
"net/http"
8+
"net/http/httptest"
9+
"os"
10+
"path/filepath"
11+
"strings"
12+
"testing"
13+
)
14+
15+
// helper to build logger writing into a buffer
16+
func testLogger(buf io.Writer) *slog.Logger {
17+
return slog.New(slog.NewJSONHandler(buf, nil))
18+
}
19+
20+
func TestResponsesWriter_FileAndJSONHeader(t *testing.T) {
21+
t.Parallel()
22+
23+
dir := t.TempDir()
24+
fname := filepath.Join(dir, "data.json")
25+
content := `{"msg":"ok"}`
26+
if err := os.WriteFile(fname, []byte(content), 0o600); err != nil {
27+
t.Fatalf("write temp file: %v", err)
28+
}
29+
30+
resp := Response{File: fname, Code: 201, IsJSON: true, Headers: http.Header{"X-Test": {"yes"}}}
31+
rec := httptest.NewRecorder()
32+
rw := responsesWriter([]Response{resp}, testLogger(io.Discard))
33+
r := httptest.NewRequest(http.MethodGet, "/", http.NoBody)
34+
rw(rec, r)
35+
36+
if rec.Code != 201 {
37+
t.Fatalf("expected status 201, got %d", rec.Code)
38+
}
39+
if got := rec.Header().Get("Content-Type"); got != "application/json" {
40+
t.Fatalf("expected application/json, got %q", got)
41+
}
42+
if got := rec.Header().Get("X-Test"); got != "yes" {
43+
t.Fatalf("expected custom header, got %q", got)
44+
}
45+
if body := strings.TrimSpace(rec.Body.String()); body != content {
46+
t.Fatalf("unexpected body %q", body)
47+
}
48+
}
49+
50+
func TestResponsesWriter_RepeatLogic(t *testing.T) {
51+
t.Parallel()
52+
53+
repeat := 2
54+
responses := []Response{{Body: "first", Code: 200, Repeat: &repeat}, {Body: "second", Code: 202}}
55+
rw := responsesWriter(responses, testLogger(io.Discard))
56+
57+
// first call
58+
rec1 := httptest.NewRecorder()
59+
rw(rec1, httptest.NewRequest(http.MethodGet, "/", http.NoBody))
60+
if rec1.Code != 200 || strings.TrimSpace(rec1.Body.String()) != "first" {
61+
t.Fatalf("first call wrong: %d %q", rec1.Code, rec1.Body.String())
62+
}
63+
// second call should still be first response
64+
rec2 := httptest.NewRecorder()
65+
rw(rec2, httptest.NewRequest(http.MethodGet, "/", http.NoBody))
66+
if rec2.Code != 200 || strings.TrimSpace(rec2.Body.String()) != "first" {
67+
t.Fatalf("second call wrong: %d %q", rec2.Code, rec2.Body.String())
68+
}
69+
// third call should switch to second response (repeat exhausted)
70+
rec3 := httptest.NewRecorder()
71+
rw(rec3, httptest.NewRequest(http.MethodGet, "/", http.NoBody))
72+
if rec3.Code != 202 || strings.TrimSpace(rec3.Body.String()) != "second" {
73+
t.Fatalf("third call wrong: %d %q", rec3.Code, rec3.Body.String())
74+
}
75+
}
76+
77+
func TestResponsesWriter_NotFoundWhenExhausted(t *testing.T) {
78+
t.Parallel()
79+
80+
rw := responsesWriter([]Response{}, testLogger(io.Discard))
81+
rec := httptest.NewRecorder()
82+
rw(rec, httptest.NewRequest(http.MethodGet, "/", http.NoBody))
83+
if rec.Code != http.StatusNotFound {
84+
t.Fatalf("expected 404, got %d", rec.Code)
85+
}
86+
}
87+
88+
func TestResponsesWriter_FileReadError(t *testing.T) {
89+
t.Parallel()
90+
91+
responses := []Response{{File: "no_such_file", Code: 200}}
92+
rec := httptest.NewRecorder()
93+
rw := responsesWriter(responses, testLogger(io.Discard))
94+
rw(rec, httptest.NewRequest(http.MethodGet, "/", http.NoBody))
95+
if rec.Code != http.StatusInternalServerError {
96+
t.Fatalf("expected 500, got %d", rec.Code)
97+
}
98+
if !strings.Contains(rec.Body.String(), "no_such_file") {
99+
t.Fatalf("expected error message, got %q", rec.Body.String())
100+
}
101+
}
102+
103+
func TestResponsesWriter_JSONDoesNotOverrideExistingContentType(t *testing.T) {
104+
t.Parallel()
105+
106+
responses := []Response{{Body: "{}", Code: 200, IsJSON: true, Headers: http.Header{"Content-Type": {"text/plain"}}}}
107+
rec := httptest.NewRecorder()
108+
rw := responsesWriter(responses, testLogger(io.Discard))
109+
rw(rec, httptest.NewRequest(http.MethodGet, "/", http.NoBody))
110+
if got := rec.Header().Get("Content-Type"); got != "text/plain" {
111+
t.Fatalf("expected text/plain kept, got %q", got)
112+
}
113+
}
114+
115+
func TestExecuteTemplate_Success(t *testing.T) {
116+
t.Parallel()
117+
118+
req := httptest.NewRequest(http.MethodPost, "/foo/bar?x=1", http.NoBody)
119+
got, err := executeTemplate("{{.Method}} {{.URL.Path}}", req)
120+
if err != nil {
121+
t.Fatalf("unexpected error: %v", err)
122+
}
123+
if string(got) != "POST /foo/bar" {
124+
t.Fatalf("unexpected template result %q", string(got))
125+
}
126+
}
127+
128+
func TestExecuteTemplate_ParseError(t *testing.T) {
129+
t.Parallel()
130+
131+
_, err := executeTemplate("{{", httptest.NewRequest(http.MethodGet, "/", http.NoBody))
132+
if err == nil {
133+
t.Fatalf("expected parse error")
134+
}
135+
}
136+
137+
func TestStructuredLogger_BasicFields(t *testing.T) {
138+
t.Parallel()
139+
140+
var buf strings.Builder
141+
logger := testLogger(&buf)
142+
143+
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
144+
w.Header().Set("X-Handled", "1")
145+
w.WriteHeader(http.StatusNoContent)
146+
})
147+
148+
h := StructuredLogger(logger, "X-Request-ID", next)
149+
rec := httptest.NewRecorder()
150+
// Use relative URL so that when we manually set Host, the constructed URI is correct.
151+
// If we used an absolute URL, Host would be duplicated in the URI
152+
// (e.g., "http://example.com/test?q=1").
153+
req := httptest.NewRequest(http.MethodGet, "/test?q=1", http.NoBody)
154+
req.Host = "example.com"
155+
req.Header.Set("X-Request-ID", "abc-123")
156+
h(rec, req)
157+
158+
if rec.Code != 204 {
159+
t.Fatalf("expected 204, got %d", rec.Code)
160+
}
161+
162+
// parse last JSON line
163+
lines := strings.Split(strings.TrimSpace(buf.String()), "\n")
164+
last := lines[len(lines)-1]
165+
var obj map[string]any
166+
if err := json.Unmarshal([]byte(last), &obj); err != nil {
167+
t.Fatalf("unmarshal log: %v: %s", err, last)
168+
}
169+
if obj["resp_status"].(float64) != 204 {
170+
t.Fatalf("expected status 204 in log, got %v", obj["resp_status"])
171+
}
172+
if obj["resp_byte_length"].(float64) != 0 {
173+
t.Fatalf("expected byte length 0, got %v", obj["resp_byte_length"])
174+
}
175+
if obj["request_id"] != "abc-123" {
176+
t.Fatalf("expected request id, got %v", obj["request_id"])
177+
}
178+
if obj["uri"].(string) != "http://example.com/test?q=1" {
179+
t.Fatalf("unexpected uri %v", obj["uri"])
180+
}
181+
}
182+
183+
func TestResponsesWriter_JSONBodySetsContentType(t *testing.T) {
184+
t.Parallel()
185+
186+
responses := []Response{{Body: "{\"k\":1}", Code: 200, IsJSON: true}}
187+
rec := httptest.NewRecorder()
188+
rw := responsesWriter(responses, testLogger(io.Discard))
189+
rw(rec, httptest.NewRequest(http.MethodGet, "/", http.NoBody))
190+
if ct := rec.Header().Get("Content-Type"); ct != "application/json" {
191+
t.Fatalf("expected application/json, got %q", ct)
192+
}
193+
}
194+
195+
func TestResponsesWriter_RepeatZeroSkips(t *testing.T) {
196+
t.Parallel()
197+
198+
zero := 0
199+
responses := []Response{{Body: "skip", Code: 200, Repeat: &zero}, {Body: "use", Code: 201}}
200+
rec := httptest.NewRecorder()
201+
rw := responsesWriter(responses, testLogger(io.Discard))
202+
rw(rec, httptest.NewRequest(http.MethodGet, "/", http.NoBody))
203+
if rec.Code != 201 || strings.TrimSpace(rec.Body.String()) != "use" {
204+
t.Fatalf("expected second response, got %d %q", rec.Code, rec.Body.String())
205+
}
206+
}

0 commit comments

Comments
 (0)