Skip to content

Commit bde74f1

Browse files
committed
feat: custom status response/test improvement
1 parent a43d675 commit bde74f1

10 files changed

Lines changed: 193 additions & 44 deletions

File tree

.github/workflows/ci.yml

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,17 +36,17 @@ jobs:
3636
- name: Build
3737
run: task build
3838

39-
- name: Lint with golangci-lint
40-
run: golangci-lint run ./...
39+
- name: Lint
40+
run: task lint
4141

42-
# - name: Test with go
43-
# run: go test ./... -json > TestResults-${{ matrix.go-version }}.json
42+
- name: Test cov
43+
run: task test-cov-html
4444

45-
# - name: Upload test results
46-
# uses: actions/upload-artifact@v4
47-
# with:
48-
# name: go-results-${{ matrix.go-version }}
49-
# path: TestResults-${{ matrix.go-version }}.json
45+
- name: Upload test results
46+
uses: actions/upload-artifact@v4
47+
with:
48+
name: go-results-${{ matrix.go-version }}
49+
path: cov/coverage.html
5050

5151
build-and-push-image:
5252
runs-on: ubuntu-latest

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ tmp/
1616
# Output of the go coverage tool, specifically when used with LiteIDE
1717
*.out
1818

19+
# go cover output
20+
*.coverprofile
21+
*.cov
22+
cov/
23+
1924
# Dependency directories (remove the comment below to include it)
2025
# vendor/
2126

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.3.0] - 2025-01-24
9+
10+
### Added
11+
12+
Custom status code response
13+
14+
### Fixed
15+
16+
Unit tests
17+
18+
819
## [0.2.0] - 2025-01-23
920

1021
### Added
@@ -13,6 +24,7 @@ Application handlers
1324
Application middlewares
1425
Utils added
1526

27+
1628
## [0.1.0] - 2025-01-22
1729

1830
Initial project release

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ The HTTP Echo server provides the following features:
2424
- **queries**: query parameters
2525
- **params**: path parameters
2626
- **body**: request body
27-
- Returns a custom status code for a request (provided via query) - **TBD**.
27+
- Returns a custom status code for a request (provided via query).
28+
- E.g. `http://127.0.0.1:3000/?status=404` returns a `404` response code.
2829
- Exports metrics in `prometheus` format for monitoring.
2930

3031
## Usage

Taskfile.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,26 @@ tasks:
55
desc: "Build the server binary"
66
cmd: "go build -o bin/http-echo ./cmd/server/main.go"
77

8+
lint:
9+
desc: "Lint the source code"
10+
cmd: "golangci-lint run ./..."
11+
12+
test:
13+
desc: "Run the unit tests"
14+
cmd: "go test ./..."
15+
16+
test-cov:
17+
desc: "Run the unit tests with coverage"
18+
cmd: "go test -coverprofile=coverage.out ./..."
19+
20+
test-cov-html:
21+
deps:
22+
- test-cov
23+
desc: "Generate the HTML coverage report"
24+
cmds:
25+
- "mkdir -p cov"
26+
- "go tool cover -html=coverage.out -o cov/coverage.html"
27+
828
docker-build:
929
desc: "Build the docker image"
1030
cmd: "docker build -t rellyson/http-echo ."

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.2.0
1+
0.3.0

internal/handlers/echo.go

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,14 @@ import (
55
"io"
66
"net/http"
77
"net/url"
8+
"strconv"
89
"strings"
910

1011
"github.com/rellyson/http-echo/pkg/netutils"
11-
"github.com/rellyson/http-echo/pkg/version"
1212
)
1313

1414
const (
15-
httpHeaderAppName = "X-App-Name"
16-
httpHeaderAppVersion = "X-App-Version"
17-
httpHeaderAppBuild = "X-App-Build"
15+
httpHeaderAppName = "X-App-Name"
1816
)
1917

2018
// EchoResponse is the response for the echo request.
@@ -45,8 +43,22 @@ func Echo(w http.ResponseWriter, r *http.Request) {
4543
panic(err)
4644
}
4745

48-
setHeaders(w)
49-
w.WriteHeader(http.StatusOK)
46+
statusCode := http.StatusOK
47+
48+
// set custom status code if query parameter is set
49+
if r.URL.Query().Get("status") != "" {
50+
s, err := strconv.Atoi(r.URL.Query().Get("status"))
51+
52+
if err != nil {
53+
panic(err)
54+
} else {
55+
statusCode = s
56+
}
57+
}
58+
59+
w.Header().Set(httpHeaderAppName, "http-echo")
60+
w.Header().Set("Content-Type", "application/json")
61+
w.WriteHeader(statusCode)
5062

5163
response := &EchoResponse{
5264
HostInfo: HostInfoResponse{
@@ -66,20 +78,6 @@ func Echo(w http.ResponseWriter, r *http.Request) {
6678
}
6779
}
6880

69-
// setHeaders sets the headers for the response.
70-
func setHeaders(w http.ResponseWriter) {
71-
v, err := version.GetVersion()
72-
73-
if err != nil {
74-
panic(err)
75-
}
76-
77-
w.Header().Set("Content-Type", "application/json")
78-
w.Header().Set(httpHeaderAppName, "http-echo")
79-
w.Header().Set(httpHeaderAppVersion, v.Version)
80-
w.Header().Set(httpHeaderAppBuild, v.Build)
81-
}
82-
8381
// mapHeaders maps the headers to a map.
8482
func mapHeaders(h http.Header) map[string]string {
8583
headers := make(map[string]string)

internal/handlers/echo_test.go

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@ func TestEcho(t *testing.T) {
1717
method string
1818
path string
1919
headers map[string]string
20+
parameters []string
21+
queries map[string]string
2022
body interface{}
2123
expectedStatus int
2224
}{
2325
{
2426
name: "GET request with no parameters",
2527
method: "GET",
26-
path: "/echo",
28+
path: "/",
2729
expectedStatus: http.StatusOK,
2830
},
2931
{
@@ -39,11 +41,31 @@ func TestEcho(t *testing.T) {
3941
expectedStatus: http.StatusOK,
4042
},
4143
{
42-
name: "GET request with query parameters",
44+
name: "GET request with query parameters",
45+
method: "GET",
46+
path: "/echo?param1=value1&param2=value2",
47+
queries: map[string]string{
48+
"param1": "value1",
49+
"param2": "value2",
50+
},
51+
expectedStatus: http.StatusOK,
52+
},
53+
{
54+
name: "GET request with path parameters",
4355
method: "GET",
44-
path: "/echo?param1=value1¶m2=value2",
56+
path: "/echo/param1/param2",
57+
parameters: []string{"param1", "param2"},
4558
expectedStatus: http.StatusOK,
4659
},
60+
{
61+
name: "GET request with custom status code",
62+
method: "GET",
63+
path: "/?status=404",
64+
queries: map[string]string{
65+
"status": "404",
66+
},
67+
expectedStatus: http.StatusNotFound,
68+
},
4769
}
4870

4971
for _, tt := range tests {
@@ -68,16 +90,25 @@ func TestEcho(t *testing.T) {
6890
assert.Equal(t, tt.expectedStatus, resp.StatusCode)
6991
assert.Equal(t, "application/json", resp.Header.Get("Content-Type"))
7092
assert.NotEmpty(t, resp.Header.Get(httpHeaderAppName))
71-
assert.NotEmpty(t, resp.Header.Get(httpHeaderAppVersion))
72-
assert.NotEmpty(t, resp.Header.Get(httpHeaderAppBuild))
7393

7494
var response EchoResponse
7595
err := json.NewDecoder(resp.Body).Decode(&response)
7696
assert.NoError(t, err)
7797

7898
assert.NotEmpty(t, response.HostInfo.Hostname)
7999
assert.NotEmpty(t, response.HostInfo.IP)
80-
assert.NotEmpty(t, response.HttpInfo.Headers)
100+
101+
if tt.headers != nil {
102+
assert.NotEmpty(t, response.HttpInfo.Headers)
103+
}
104+
105+
if tt.parameters != nil {
106+
assert.NotEmpty(t, response.HttpInfo.Params)
107+
}
108+
109+
if tt.queries != nil {
110+
assert.NotEmpty(t, response.HttpInfo.Queries)
111+
}
81112

82113
if tt.body != nil {
83114
assert.NotNil(t, response.HttpInfo.Body)
@@ -103,7 +134,7 @@ func TestMapHeaders(t *testing.T) {
103134
}
104135

105136
func TestMapQuery(t *testing.T) {
106-
url := "http://example.com?param1=value1¶m2=value2"
137+
url := "http://example.com?param1=value1&param2=value2"
107138
req, _ := http.NewRequest("GET", url, nil)
108139

109140
result := mapQuery(req.URL.Query())
@@ -135,9 +166,9 @@ func TestMapBody(t *testing.T) {
135166
},
136167
},
137168
{
138-
name: "invalid JSON body",
139-
body: "invalid json",
140-
expected: nil,
169+
name: "text body",
170+
body: "valid text",
171+
expected: "valid text",
141172
},
142173
}
143174

pkg/middlewares/stack_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package middlewares
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"testing"
7+
)
8+
9+
func TestCreateStack(t *testing.T) {
10+
// Create test middleware that adds headers
11+
middleware1 := func(next http.Handler) http.Handler {
12+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
13+
w.Header().Add("X-Test-1", "value1")
14+
next.ServeHTTP(w, r)
15+
})
16+
}
17+
18+
middleware2 := func(next http.Handler) http.Handler {
19+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
20+
w.Header().Add("X-Test-2", "value2")
21+
next.ServeHTTP(w, r)
22+
})
23+
}
24+
25+
// Create final handler
26+
finalHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
27+
w.WriteHeader(http.StatusOK)
28+
})
29+
30+
// Test cases
31+
tests := []struct {
32+
name string
33+
middlewares []Middleware
34+
expectedHeader map[string]string
35+
}{
36+
{
37+
name: "Empty middleware stack",
38+
middlewares: []Middleware{},
39+
expectedHeader: map[string]string{},
40+
},
41+
{
42+
name: "Single middleware",
43+
middlewares: []Middleware{middleware1},
44+
expectedHeader: map[string]string{
45+
"X-Test-1": "value1",
46+
},
47+
},
48+
{
49+
name: "Multiple middlewares",
50+
middlewares: []Middleware{middleware1, middleware2},
51+
expectedHeader: map[string]string{
52+
"X-Test-1": "value1",
53+
"X-Test-2": "value2",
54+
},
55+
},
56+
}
57+
58+
for _, tt := range tests {
59+
t.Run(tt.name, func(t *testing.T) {
60+
// Create middleware stack
61+
stack := CreateStack(tt.middlewares...)
62+
handler := stack(finalHandler)
63+
64+
// Create test request
65+
req := httptest.NewRequest("GET", "/", nil)
66+
w := httptest.NewRecorder()
67+
68+
// Execute request
69+
handler.ServeHTTP(w, req)
70+
71+
// Check status code
72+
if w.Code != http.StatusOK {
73+
t.Errorf("expected status code %d, got %d", http.StatusOK, w.Code)
74+
}
75+
76+
// Verify headers
77+
for key, expectedValue := range tt.expectedHeader {
78+
if got := w.Header().Get(key); got != expectedValue {
79+
t.Errorf("expected header %s to be %s, got %s", key, expectedValue, got)
80+
}
81+
}
82+
})
83+
}
84+
}

pkg/netutils/host_test.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,11 @@ func TestIsPrivateIP(t *testing.T) {
4545
ip string
4646
expected bool
4747
}{
48-
{"loopback v4", "127.0.0.1", true},
4948
{"private class A", "10.0.0.1", true},
5049
{"private class B", "172.16.0.1", true},
5150
{"private class C", "192.168.0.1", true},
5251
{"public IP", "8.8.8.8", false},
53-
{"IPv6 loopback", "::1", true},
54-
{"IPv6 link-local", "fe80::1", true},
52+
{"public IP", "17.123.10.90", false},
5553
}
5654

5755
for _, tt := range tests {

0 commit comments

Comments
 (0)