Skip to content

Commit e823730

Browse files
authored
feat(mockery): use HTTP headers as variables (#3)
1 parent 719aeea commit e823730

File tree

5 files changed

+55
-10
lines changed

5 files changed

+55
-10
lines changed

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@ To run http-mockery you'll need to have a config file. Example is available [her
77
Config file needs to include an `endpoints` config if you want to respond with anything else than 404 Not Found. Default listening address is "0.0.0.0:8080", but this can be changed with `listen_ip` and `listen_port`.
88
Default config file is `config.json` in the same directory as the application, but it can also be defined with `HTTP_MOCKERY_CONFIG` env variable.
99

10-
Endpoint config needs to have atleast `uri`, `method` and `response_code` to operate normally. If you want the endpoint to return any JSON, you'll also need to provide the name of a `template` file, and `variables` configuration if the template includes anything to replace. Replacable variables are marked with < and >, e.g. `<replace_me>`. Matching variable must then be found (see examples). Value can be either provided in the config as `value` or as an environment variable where the env var name should be included in the variable config as `env_var`. Both `value` and `env_var` can be defined, but env_var always has precedence.
11-
10+
Endpoint config needs to have atleast `uri`, `method` and `response_code` to operate normally. If you want the endpoint to return any JSON, you'll also need to provide the name of a `template` file, and `variables` configuration if the template includes template tags. Replacable variables are marked with `<` and `>` tags, e.g. `<replace_me>`. Matching `env_var` and `value` variables must then be found (see examples), `header` variables are treated as optional.
11+
Following template tag value providers are supported (order by priority):
12+
- `env_var` uses environment variable's value to replace tag
13+
- `value`, replaces template tag with raw value
14+
- `header`, uses HTTP request header field value to replace template tag
15+
1216
Endpoint `type` is defaulted to `normal` but can also be set as `regexp`. It allows for standard regular expressions in the `uri` to match more specific use cases. Endpoints are checked in a given order and first matching endpoint (with correct `uri` and `response_code`) will be used.
1317

1418
Request and response bodies from requests can be logged with their relevant config options under `logging`, example [here](examples/config-example.json). Request & response content logging can also be toggled with env variables `HTTP_MOCKERY_REQUEST_CONTENTS` and `HTTP_MOCKERY_RESPONSE_CONTENTS` Endpoint-specific secrets are censored from logs.

examples/config-example.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
{
1515
"name": "item2",
1616
"env_var": "TEMPLATE_VALUE2"
17+
},
18+
{
19+
"name": "item3",
20+
"header": "X-Real-IP"
1721
}
1822
]
1923
},

pkg/mockery/mockery.go

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ type Endpoint struct {
5454
type Variable struct {
5555
Name string `json:"name"`
5656
EnvVar string `json:"env_var"`
57+
Header string `json:"header"`
5758
Value string `json:"value"`
5859
}
5960

@@ -105,7 +106,7 @@ func (s MockHandler) ValidateConfig() error {
105106
}
106107

107108
if endpoint.Template != "" {
108-
rendered, err := s.RenderTemplateResponse(endpoint)
109+
rendered, err := s.RenderTemplateResponse(endpoint, &http.Request{})
109110
if err != nil {
110111
return fmt.Errorf("unable to render template %s: %+v", endpoint.Template, err)
111112
}
@@ -119,7 +120,7 @@ func (s MockHandler) ValidateConfig() error {
119120
return nil
120121
}
121122

122-
func (s MockHandler) RenderTemplateResponse(e Endpoint) (string, error) {
123+
func (s MockHandler) RenderTemplateResponse(e Endpoint, r *http.Request) (string, error) {
123124
template, err := os.ReadFile(e.Template)
124125
if err != nil {
125126
return "", err
@@ -130,6 +131,7 @@ func (s MockHandler) RenderTemplateResponse(e Endpoint) (string, error) {
130131
return "", fmt.Errorf("unable to parse template %s: %+v", e.Template, err)
131132
}
132133

134+
header := buildHeaderVars(r)
133135
return t.ExecuteFuncStringWithErr(func(w io.Writer, tag string) (int, error) {
134136
for _, variable := range e.Variables {
135137
if tag == variable.Name {
@@ -140,10 +142,17 @@ func (s MockHandler) RenderTemplateResponse(e Endpoint) (string, error) {
140142
}
141143
return w.Write([]byte(envVar))
142144
}
143-
144145
if variable.Value != "" {
145146
return w.Write([]byte(variable.Value))
146147
}
148+
if variable.Header != "" {
149+
if h := header.Get(variable.Header); h != "" {
150+
return w.Write([]byte(h))
151+
}
152+
// Headers are dynamic and only available in runtime, so fail silently if header is not set.
153+
s.Log.Printf("HTTP header %s not found for template %s of endpoint %s", variable.Header, e.Template, e.Uri)
154+
return 0, nil
155+
}
147156
}
148157
}
149158

@@ -206,7 +215,7 @@ func (s MockHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
206215
return
207216
}
208217

209-
resp, err := s.RenderTemplateResponse(endpoint)
218+
resp, err := s.RenderTemplateResponse(endpoint, r)
210219
if err != nil {
211220
errorHandler(w, r, s.Log, fmt.Sprintf("Unknown error in parsing response for %s (%s)", endpoint.Uri, endpoint.Method), err, false)
212221
return
@@ -277,3 +286,15 @@ func errorHandler(w http.ResponseWriter, r *http.Request, l *log.Logger, msg str
277286
l.Printf("%s: %v\n", msg, err)
278287
logRequestAndRespond(l, r, w, http.StatusInternalServerError, "Unknown error has occurred, see logs\n", false, proxied)
279288
}
289+
290+
func buildHeaderVars(r *http.Request) http.Header {
291+
if r == nil {
292+
return http.Header{}
293+
}
294+
h := r.Header.Clone()
295+
if username, password, ok := r.BasicAuth(); ok {
296+
h.Set("authorization_username", username)
297+
h.Set("authorization_password", password)
298+
}
299+
return h
300+
}

pkg/mockery/mockery_test.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package mockery_test
22

33
import (
4+
"log"
45
"net/http"
56
"testing"
67

@@ -22,6 +23,10 @@ func testingConfig() mockery.Config {
2223
Name: "item1",
2324
Value: "test123",
2425
},
26+
{
27+
Name: "auth_username",
28+
Header: "authorization_username",
29+
},
2530
},
2631
},
2732
{
@@ -62,6 +67,7 @@ func TestConfigLoading(t *testing.T) {
6267
func TestConfigValidation(t *testing.T) {
6368
testMocker := mockery.MockHandler{
6469
Config: testingConfig(),
70+
Log: log.Default(),
6571
}
6672

6773
err := testMocker.ValidateConfig()
@@ -78,19 +84,23 @@ func TestConfigValidation(t *testing.T) {
7884
}
7985

8086
func TestTemplateRendering(t *testing.T) {
87+
const username = "user12345"
8188
testMocker := mockery.MockHandler{
8289
Config: testingConfig(),
90+
Log: log.Default(),
8391
}
8492

85-
rendered, err := testMocker.RenderTemplateResponse(testMocker.Config.Endpoints[0])
93+
rendered, err := testMocker.RenderTemplateResponse(testMocker.Config.Endpoints[0], basicAuthRequest(username, "bar"))
8694
assert.Equal(t, nil, err, "Template should be rendered correctly")
8795
assert.Contains(t, rendered, testMocker.Config.Endpoints[0].Variables[0].Value, "Template should contain rendered value from config")
96+
assert.Contains(t, rendered, username, "Template should contain rendered value from config")
8897
assert.True(t, mockery.IsJSON(rendered), "Rendered template should be valid JSON")
8998
}
9099

91100
func TestEndpointMatching(t *testing.T) {
92101
testMocker := mockery.MockHandler{
93102
Config: testingConfig(),
103+
Log: log.Default(),
94104
}
95105

96106
_, err := testMocker.MatchEndpoint(&http.Request{Method: http.MethodGet, RequestURI: "/example2"})
@@ -107,3 +117,9 @@ func TestEndpointMatching(t *testing.T) {
107117
assert.Equal(t, nil, err, "Endpoint should match")
108118
assert.Equal(t, testMocker.Config.Endpoints[2], endpoint, "Should be correctly matched endpoint")
109119
}
120+
121+
func basicAuthRequest(username, password string) *http.Request {
122+
r := http.Request{Header: http.Header{}}
123+
r.SetBasicAuth(username, password)
124+
return &r
125+
}

test/response-example.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"status": "success",
3-
"configured_var": "<item1>"
4-
}
5-
3+
"configured_var": "<item1>",
4+
"username": "<auth_username>"
5+
}

0 commit comments

Comments
 (0)