Skip to content

Commit 0d02ea1

Browse files
committed
feat: add before_script before making http request
1 parent 3772032 commit 0d02ea1

File tree

4 files changed

+117
-10
lines changed

4 files changed

+117
-10
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ require (
229229
golang.org/x/crypto v0.11.0 // indirect
230230
golang.org/x/mod v0.12.0 // indirect
231231
golang.org/x/net v0.12.0 // indirect
232-
golang.org/x/sync v0.3.0 // indirect
232+
golang.org/x/sync v0.3.0
233233
golang.org/x/sys v0.10.0 // indirect
234234
golang.org/x/term v0.10.0 // indirect
235235
golang.org/x/text v0.11.0 // indirect

plugins/extractors/http/execute_script.go

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package http
22

33
import (
44
"context"
5+
"encoding/json"
56
"errors"
67
"fmt"
78
"reflect"
@@ -19,8 +20,7 @@ import (
1920
"google.golang.org/protobuf/proto"
2021
)
2122

22-
func (e *Extractor) executeScript(ctx context.Context, res interface{}, emit plugins.Emit) error {
23-
scriptCfg := e.config.Script
23+
func (e *Extractor) executeScript(ctx context.Context, res interface{}, scriptCfg Script, emit plugins.Emit) error {
2424
s, err := tengoutil.NewSecureScript(
2525
([]byte)(scriptCfg.Source), e.scriptGlobals(ctx, res, emit),
2626
)
@@ -40,12 +40,23 @@ func (e *Extractor) executeScript(ctx context.Context, res interface{}, emit plu
4040
return fmt.Errorf("run: %w", err)
4141
}
4242

43+
err = e.convertTengoObjToRequest(c.Get("request").Value())
44+
if err != nil {
45+
return err
46+
}
47+
4348
return nil
4449
}
4550

4651
func (e *Extractor) scriptGlobals(ctx context.Context, res interface{}, emit plugins.Emit) map[string]interface{} {
52+
req, err := e.convertRequestToTengoObj()
53+
if err != nil {
54+
e.logger.Error(err.Error())
55+
}
56+
4757
return map[string]interface{}{
4858
"recipe_scope": &tengo.String{Value: e.UrnScope},
59+
"request": req,
4960
"response": res,
5061
"new_asset": &tengo.UserFunction{
5162
Name: "new_asset",
@@ -68,6 +79,30 @@ func (e *Extractor) scriptGlobals(ctx context.Context, res interface{}, emit plu
6879
}
6980
}
7081

82+
func (e *Extractor) convertTengoObjToRequest(obj interface{}) error {
83+
r, err := json.Marshal(obj)
84+
if err != nil {
85+
return err
86+
}
87+
err = json.Unmarshal(r, &e.config.Request)
88+
if err != nil {
89+
return err
90+
}
91+
return nil
92+
}
93+
func (e *Extractor) convertRequestToTengoObj() (tengo.Object, error) {
94+
var res map[string]interface{}
95+
r, err := json.Marshal(e.config.Request)
96+
if err != nil {
97+
return nil, err
98+
}
99+
err = json.Unmarshal(r, &res)
100+
if err != nil {
101+
return nil, err
102+
}
103+
return tengo.FromInterface(res)
104+
}
105+
71106
func newAssetWrapper() tengo.CallableFunc {
72107
typeURLs := knownTypeURLs()
73108
return func(args ...tengo.Object) (tengo.Object, error) {

plugins/extractors/http/http_extractor.go

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,20 @@ func init() {
2929
//go:embed README.md
3030
var summary string
3131

32+
type Script struct {
33+
Engine string `mapstructure:"engine" validate:"required,oneof=tengo"`
34+
Source string `mapstructure:"source" validate:"required"`
35+
MaxAllocs int64 `mapstructure:"max_allocs" validate:"gt=100" default:"5000"`
36+
MaxConstObjects int `mapstructure:"max_const_objects" validate:"gt=10" default:"500"`
37+
}
38+
3239
// Config holds the set of configuration for the HTTP extractor.
3340
type Config struct {
3441
Request RequestConfig `mapstructure:"request"`
3542
SuccessCodes []int `mapstructure:"success_codes" validate:"dive,gte=200,lt=300" default:"[200]"`
3643
Concurrency int `mapstructure:"concurrency" validate:"gte=1,lte=100" default:"5"`
37-
Script struct {
38-
Engine string `mapstructure:"engine" validate:"required,oneof=tengo"`
39-
Source string `mapstructure:"source" validate:"required"`
40-
MaxAllocs int64 `mapstructure:"max_allocs" validate:"gt=100" default:"5000"`
41-
MaxConstObjects int `mapstructure:"max_const_objects" validate:"gt=10" default:"500"`
42-
} `mapstructure:"script"`
44+
Script Script `mapstructure:"script"`
45+
BeforeScript *Script `mapstructure:"before_script"`
4346
}
4447

4548
type RequestConfig struct {
@@ -122,12 +125,17 @@ func (e *Extractor) Init(ctx context.Context, config plugins.Config) error {
122125
// executes the script. The script has access to the response and can use the
123126
// same to 'emit' assets from within the script.
124127
func (e *Extractor) Extract(ctx context.Context, emit plugins.Emit) error {
128+
if e.config.BeforeScript != nil {
129+
if err := e.executeScript(ctx, nil, *e.config.BeforeScript, emit); err != nil {
130+
return fmt.Errorf("http extractor: execute script: %w", err)
131+
}
132+
}
125133
res, err := e.executeRequest(ctx, e.config.Request)
126134
if err != nil {
127135
return fmt.Errorf("http extractor: execute request: %w", err)
128136
}
129137

130-
if err := e.executeScript(ctx, res, emit); err != nil {
138+
if err := e.executeScript(ctx, res, e.config.Script, emit); err != nil {
131139
return fmt.Errorf("http extractor: execute script: %w", err)
132140
}
133141

plugins/extractors/http/http_extractor_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,66 @@ func TestExtract(t *testing.T) {
202202
testutils.Respond(t, w, http.StatusOK, `[]`)
203203
},
204204
},
205+
{
206+
name: "MatchRequestBeforeScript",
207+
rawCfg: map[string]interface{}{
208+
"before_script": map[string]interface{}{
209+
"engine": "tengo",
210+
"source": heredoc.Doc(`
211+
fmt := import("fmt")
212+
reqs := []
213+
reqs = append(reqs, {
214+
url: "{{serverURL}}/token",
215+
method: "GET",
216+
content_type: "application/json",
217+
accept: "application/json",
218+
timeout: "5s"
219+
})
220+
221+
responses := execute_request(reqs...)
222+
for r in responses {
223+
if is_error(r) {
224+
continue
225+
}
226+
request.Headers = {"Authorization": format("Bearer %s", r.body.data.token)}
227+
}
228+
`),
229+
"max_allocs": 5000,
230+
"max_const_objects": 500,
231+
},
232+
"request": map[string]interface{}{
233+
"url": "{{serverURL}}/api/v2/endpoint",
234+
"content_type": "application/json",
235+
"accept": "application/json",
236+
},
237+
"script": map[string]interface{}{
238+
"engine": "tengo",
239+
"source": "// do nothing",
240+
},
241+
},
242+
handler: func(t *testing.T, w http.ResponseWriter, r *http.Request) {
243+
switch r.URL.Path {
244+
case "/token":
245+
testutils.Respond(t, w, http.StatusOK,
246+
`{"header":{"process_time":0.130462645,"messages":[],"error_code":""},"data":{"token":"testToken123123","service_id":1,"expires_at":1711537963}}`,
247+
)
248+
case "/api/v2/endpoint":
249+
assert.Equal(t, r.Method, http.MethodGet)
250+
assert.Equal(t, r.URL.Path, "/api/v2/endpoint")
251+
assert.Equal(t, r.URL.RawQuery, "")
252+
h := r.Header
253+
assert.Equal(t, "", h.Get("Content-Type"))
254+
assert.Equal(t, "Bearer testToken123123", h.Get("Authorization"))
255+
assert.Equal(t, "application/json", h.Get("Accept"))
256+
data, err := io.ReadAll(r.Body)
257+
assert.NoError(t, err)
258+
assert.Empty(t, data)
259+
testutils.Respond(t, w, http.StatusOK, `[]`)
260+
default:
261+
t.Error("Unexpected HTTP call on", r.URL.Path)
262+
}
263+
},
264+
},
205265
{
206266
name: "MatchRequestAdvanced",
207267
rawCfg: map[string]interface{}{
@@ -890,4 +950,8 @@ func replaceServerURL(cfg map[string]interface{}, serverURL string) {
890950
reqCfg["url"] = strings.Replace(reqCfg["url"].(string), "{{serverURL}}", serverURL, 1)
891951
scriptCfg := cfg["script"].(map[string]interface{})
892952
scriptCfg["source"] = strings.Replace(scriptCfg["source"].(string), "{{serverURL}}", serverURL, -1)
953+
beforeScriptCfg, ok := cfg["before_script"].(map[string]interface{})
954+
if ok {
955+
beforeScriptCfg["source"] = strings.Replace(beforeScriptCfg["source"].(string), "{{serverURL}}", serverURL, -1)
956+
}
893957
}

0 commit comments

Comments
 (0)