Skip to content

Commit 64cfec8

Browse files
committed
Add request body and transfer support to request action
1 parent 08912bf commit 64cfec8

File tree

10 files changed

+1059
-80
lines changed

10 files changed

+1059
-80
lines changed

TODO.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,6 @@ in the future.
4848

4949
#### Structure - Instances, Actions, Servers, Services
5050

51-
- extend request action to support file upload
52-
- it should chunked update and set option to set delay between chunks to be able to create server timeouts
53-
- it should also allow doing partial unfinished uploads
54-
- it is to support all requirements for testing https://github.com/php/php-src/pull/2180
55-
- test exceeding LimitRequestBody
5651
- add typed parameters substitution for integers
5752
- this is to be able to, for example, parameterize status code
5853
- alternatively, it might be easier to allow automatic string to int conversion
@@ -76,6 +71,10 @@ in the future.
7671
- support `protocols` field in bench action
7772
- extract the common logic
7873
- extend protocols to support http3 in bench and request action
74+
- extend request body to support multipart form data
75+
- it should support form fields
76+
- it should support files
77+
- look into extending bench to support request body (see if all request action options will be possible)
7978
- integrate better instance action identification
8079
- it should introduce name for each action and also pass parent name to nested actions in `parallel` or `not`
8180
- add execute action custom environment variables support
@@ -107,6 +106,7 @@ in the future.
107106
- consider more consistent naming differentiating that service port is public and server port is private
108107
- add support for ephemeral port allocation that should be the default if not ports specified
109108
- it should be also possible to overwrite port to ephemeral selection even if specified
109+
- Add Temp dir support to Dirs as it might be useful, for example, for nginx temp paths
110110
- consider adding support default Dirs so it is not required to specify in the config
111111
- could be either the actual enum name or another tag
112112
- add a special resource file structure that could be used for scripts but also for certs and keys

app/foundation.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"net/http"
2020
"os"
2121
"os/user"
22+
"time"
2223

2324
"github.com/google/uuid"
2425
"github.com/spf13/afero"
@@ -42,6 +43,7 @@ type Foundation interface {
4243
VegetaAttacker() VegetaAttacker
4344
VegetaMetrics() VegetaMetrics
4445
GenerateUuid() string
46+
Sleep(ctx context.Context, duration time.Duration) error
4547
}
4648

4749
type DefaultFoundation struct {
@@ -140,3 +142,19 @@ func (f *DefaultFoundation) VegetaAttacker() VegetaAttacker {
140142
func (f *DefaultFoundation) GenerateUuid() string {
141143
return uuid.New().String()
142144
}
145+
146+
func (f *DefaultFoundation) Sleep(ctx context.Context, duration time.Duration) error {
147+
if duration <= 0 {
148+
return nil
149+
}
150+
151+
timer := time.NewTimer(duration)
152+
defer timer.Stop()
153+
154+
select {
155+
case <-ctx.Done():
156+
return ctx.Err()
157+
case <-timer.C:
158+
return nil
159+
}
160+
}

conf/parser/parser_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1677,6 +1677,7 @@ func Test_ConfigParser_ParseStruct(t *testing.T) {
16771677
})
16781678
}
16791679
}
1680+
16801681
func Test_ConfigParser_ParseConfig(t *testing.T) {
16811682
tests := []struct {
16821683
name string
@@ -1934,6 +1935,29 @@ func Test_ConfigParser_ParseConfig(t *testing.T) {
19341935
},
19351936
},
19361937
},
1938+
map[string]interface{}{
1939+
"request": map[string]interface{}{
1940+
"service": "web_service",
1941+
"path": "/upload",
1942+
"method": "POST",
1943+
"body": map[string]interface{}{
1944+
"content": "test",
1945+
"transfer": map[string]interface{}{
1946+
"encoding": "chunked",
1947+
"content_length": 30,
1948+
"chunk_size": 10,
1949+
"chunk_delay": 5000,
1950+
},
1951+
},
1952+
},
1953+
},
1954+
map[string]interface{}{
1955+
"expect/web_service": map[string]interface{}{
1956+
"response": map[string]interface{}{
1957+
"status": 200,
1958+
},
1959+
},
1960+
},
19371961
},
19381962
},
19391963
},
@@ -2115,6 +2139,37 @@ func Test_ConfigParser_ParseConfig(t *testing.T) {
21152139
},
21162140
},
21172141
},
2142+
&types.RequestAction{
2143+
Service: "web_service",
2144+
Timeout: 0,
2145+
When: "on_success",
2146+
OnFailure: "fail",
2147+
Id: "last",
2148+
Scheme: "http",
2149+
Path: "/upload",
2150+
EncodePath: true,
2151+
Method: "POST",
2152+
Body: types.RequestBody{
2153+
Content: "test",
2154+
RenderTemplate: true,
2155+
Transfer: types.TransferConfig{
2156+
Encoding: "chunked",
2157+
ChunkSize: 10,
2158+
ChunkDelay: 5000,
2159+
ContentLength: 30,
2160+
},
2161+
},
2162+
},
2163+
&types.ResponseExpectationAction{
2164+
Service: "web_service",
2165+
Timeout: 0,
2166+
When: "on_success",
2167+
OnFailure: "fail",
2168+
Response: types.ResponseExpectation{
2169+
Request: "last",
2170+
Status: 200,
2171+
},
2172+
},
21182173
},
21192174
},
21202175
},

conf/types/action.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,19 @@ type TLSClientConfig struct {
113113
CACert string `wst:"ca_certificate"`
114114
}
115115

116+
type TransferConfig struct {
117+
Encoding string `wst:"encoding,enum=chunked|none,default=none"`
118+
ChunkSize int `wst:"chunk_size"`
119+
ChunkDelay int `wst:"chunk_delay"`
120+
ContentLength int `wst:"content_length"`
121+
}
122+
123+
type RequestBody struct {
124+
Content string `wst:"content"`
125+
RenderTemplate bool `wst:"render_template,default=true"`
126+
Transfer TransferConfig `wst:"transfer"`
127+
}
128+
116129
type RequestAction struct {
117130
Service string `wst:"service"`
118131
Timeout int `wst:"timeout"`
@@ -125,6 +138,7 @@ type RequestAction struct {
125138
EncodePath bool `wst:"encode_path,default=true"`
126139
Method string `wst:"method,enum=GET|HEAD|DELETE|POST|PUT|PATCH|PURGE,default=GET"`
127140
Headers Headers `wst:"headers"`
141+
Body RequestBody `wst:"body,string=Content"`
128142
TLS TLSClientConfig `wst:"tls"`
129143
}
130144

mocks/generated/app/mock_Foundation.go

Lines changed: 58 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Copyright 2025 Jakub Zelenka and The WST Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package request
16+
17+
import (
18+
"context"
19+
"io"
20+
"time"
21+
22+
"github.com/wstool/wst/app"
23+
)
24+
25+
// chunkControlledReader implements io.Reader with control over chunk sizes and delays.
26+
// Each Read() call returns at most chunkSize bytes, which forces the HTTP client to
27+
// make multiple Read() calls, effectively controlling the chunk sizes sent over the wire
28+
// when using chunked transfer encoding.
29+
type chunkControlledReader struct {
30+
ctx context.Context
31+
fnd app.Foundation
32+
data []byte
33+
chunkSize int
34+
chunkDelay time.Duration
35+
offset int
36+
readCount int
37+
}
38+
39+
func (r *chunkControlledReader) Read(p []byte) (n int, err error) {
40+
// Check context cancellation
41+
select {
42+
case <-r.ctx.Done():
43+
return 0, r.ctx.Err()
44+
default:
45+
}
46+
47+
// If all data has been read, return EOF (no delay before EOF)
48+
if r.offset >= len(r.data) {
49+
return 0, io.EOF
50+
}
51+
52+
// Add delay before each chunk (except the first one)
53+
if r.readCount > 0 && r.chunkDelay > 0 {
54+
if err := r.fnd.Sleep(r.ctx, r.chunkDelay); err != nil {
55+
return 0, err
56+
}
57+
}
58+
r.readCount++
59+
60+
// Determine how much to read in this chunk
61+
chunkSize := r.chunkSize
62+
if chunkSize <= 0 {
63+
// If chunk size not specified, use the full buffer provided by HTTP client
64+
chunkSize = len(p)
65+
}
66+
67+
// Calculate how much data to copy
68+
remaining := len(r.data) - r.offset
69+
toRead := chunkSize
70+
if toRead > remaining {
71+
toRead = remaining
72+
}
73+
if toRead > len(p) {
74+
// Never read more than the provided buffer can hold
75+
toRead = len(p)
76+
}
77+
78+
// Copy data to buffer
79+
n = copy(p, r.data[r.offset:r.offset+toRead])
80+
r.offset += n
81+
82+
return n, nil
83+
}

0 commit comments

Comments
 (0)