Skip to content

Commit c9a405a

Browse files
committed
Add http handler tests and add http handler interface
1 parent 9887326 commit c9a405a

4 files changed

Lines changed: 138 additions & 12 deletions

File tree

go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,14 @@ require (
1111

1212
require (
1313
github.com/alicebob/miniredis/v2 v2.37.0 // indirect
14+
github.com/beorn7/perks v1.0.1 // indirect
1415
github.com/cespare/xxhash/v2 v2.2.0 // indirect
1516
github.com/davecgh/go-spew v1.1.1 // indirect
1617
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
1718
github.com/pmezard/go-difflib v1.0.0 // indirect
19+
github.com/prometheus/client_model v0.5.0 // indirect
20+
github.com/prometheus/common v0.48.0 // indirect
21+
github.com/prometheus/procfs v0.12.0 // indirect
1822
github.com/stretchr/testify v1.11.1 // indirect
1923
github.com/yuin/gopher-lua v1.1.1 // indirect
2024
golang.org/x/net v0.21.0 // indirect

go.sum

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
github.com/alicebob/miniredis/v2 v2.37.0 h1:RheObYW32G1aiJIj81XVt78ZHJpHonHLHW7OLIshq68=
22
github.com/alicebob/miniredis/v2 v2.37.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
3+
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
4+
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
35
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
46
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
57
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -10,6 +12,12 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
1012
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
1113
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
1214
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
15+
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
16+
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
17+
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
18+
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
19+
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
20+
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
1321
github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8=
1422
github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
1523
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=

internal/http/handler.go

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,42 @@
11
package http
22

33
import (
4+
"context"
45
"encoding/json"
5-
"net/http"
6+
stdhttp "net/http"
67

7-
"github.com/Pavan-Rana/rate-limiter/internal/limiter"
88
"github.com/prometheus/client_golang/prometheus/promhttp"
99
)
1010

11-
func NewRouter(lim *limiter.Limiter) http.Handler {
12-
mux := http.NewServeMux()
11+
type Limiter interface {
12+
AllowRequest(ctx context.Context, apiKey string) (bool, error)
13+
}
14+
15+
func NewRouter(lim Limiter) stdhttp.Handler {
16+
mux := stdhttp.NewServeMux()
1317
mux.HandleFunc("/check", checkHandler(lim))
1418
mux.Handle("/metrics", promhttp.Handler())
1519
return mux
1620
}
1721

18-
func checkHandler(lim *limiter.Limiter) http.HandlerFunc {
19-
return func(w http.ResponseWriter, r *http.Request) {
20-
if r.Method != http.MethodPost {
21-
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
22+
func checkHandler(lim Limiter) stdhttp.HandlerFunc {
23+
return func(w stdhttp.ResponseWriter, r *stdhttp.Request) {
24+
if r.Method != stdhttp.MethodPost {
25+
stdhttp.Error(w, "Method not allowed", stdhttp.StatusMethodNotAllowed)
2226
return
2327
}
2428

2529
apiKey := r.Header.Get("X-API-Key")
2630
if apiKey == "" {
27-
http.Error(w, "Missing X-API-Key", http.StatusBadRequest)
31+
stdhttp.Error(w, "Missing X-API-Key", stdhttp.StatusBadRequest)
2832
return
2933
}
3034

3135
allowed, _ := lim.AllowRequest(r.Context(), apiKey)
3236

33-
status := http.StatusOK
37+
status := stdhttp.StatusOK
3438
if !allowed {
35-
status = http.StatusTooManyRequests
39+
status = stdhttp.StatusTooManyRequests
3640
}
3741

3842
w.Header().Set("Content-Type", "application/json")

internal/http/handler_test.go

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,111 @@
1-
// internal/http/handler_test.go
1+
package http_test
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
stdhttp "net/http"
8+
"net/http/httptest"
9+
"testing"
10+
11+
handler "github.com/Pavan-Rana/rate-limiter/internal/http"
12+
)
13+
14+
// mockLimiter implements the Limiter interface for testing
15+
type mockLimiter struct {
16+
allow bool
17+
err error
18+
}
19+
20+
func (m *mockLimiter) AllowRequest(ctx context.Context, apiKey string) (bool, error) {
21+
return m.allow, m.err
22+
}
23+
24+
func TestCheckHandler(t *testing.T) {
25+
tests := []struct {
26+
name string
27+
method string
28+
apiKey string
29+
allow bool
30+
wantStatus int
31+
wantAllowed bool
32+
}{
33+
{
34+
name: "allowed",
35+
method: stdhttp.MethodPost,
36+
apiKey: "abc123",
37+
allow: true,
38+
wantStatus: stdhttp.StatusOK,
39+
wantAllowed: true,
40+
},
41+
{
42+
name: "rate limit exceeded",
43+
method: stdhttp.MethodPost,
44+
apiKey: "abc123",
45+
allow: false,
46+
wantStatus: stdhttp.StatusTooManyRequests,
47+
wantAllowed: false,
48+
},
49+
{
50+
name: "missing API key",
51+
method: stdhttp.MethodPost,
52+
apiKey: "",
53+
wantStatus: stdhttp.StatusBadRequest,
54+
},
55+
{
56+
name: "wrong method",
57+
method: stdhttp.MethodGet,
58+
apiKey: "abc123",
59+
wantStatus: stdhttp.StatusMethodNotAllowed,
60+
},
61+
}
62+
63+
for _, tt := range tests {
64+
t.Run(tt.name, func(t *testing.T) {
65+
mock := &mockLimiter{allow: tt.allow}
66+
router := handler.NewRouter(mock)
67+
68+
req := httptest.NewRequest(tt.method, "/check", bytes.NewReader([]byte{}))
69+
if tt.apiKey != "" {
70+
req.Header.Set("X-API-Key", tt.apiKey)
71+
}
72+
73+
rr := httptest.NewRecorder()
74+
router.ServeHTTP(rr, req)
75+
76+
if rr.Code != tt.wantStatus {
77+
t.Errorf("status code = %v, want %v", rr.Code, tt.wantStatus)
78+
}
79+
80+
// Only decode JSON for POST requests with API key
81+
if tt.method == stdhttp.MethodPost && tt.apiKey != "" {
82+
var body map[string]any
83+
if err := json.NewDecoder(rr.Body).Decode(&body); err != nil {
84+
t.Fatalf("failed to decode response: %v", err)
85+
}
86+
if body["allowed"] != tt.wantAllowed {
87+
t.Errorf("allowed = %v, want %v", body["allowed"], tt.wantAllowed)
88+
}
89+
if body["api_key"] != tt.apiKey {
90+
t.Errorf("api_key = %v, want %v", body["api_key"], tt.apiKey)
91+
}
92+
}
93+
})
94+
}
95+
}
96+
97+
func TestMetricsEndpoint(t *testing.T) {
98+
mock := &mockLimiter{allow: true}
99+
router := handler.NewRouter(mock)
100+
101+
req := httptest.NewRequest(stdhttp.MethodGet, "/metrics", nil)
102+
rr := httptest.NewRecorder()
103+
router.ServeHTTP(rr, req)
104+
105+
if rr.Code != stdhttp.StatusOK {
106+
t.Errorf("/metrics status code = %v, want %v", rr.Code, stdhttp.StatusOK)
107+
}
108+
if rr.Body.Len() == 0 {
109+
t.Error("/metrics returned empty body")
110+
}
111+
}

0 commit comments

Comments
 (0)