Add URL path templating support with validation and documentation

This commit is contained in:
2026-01-11 19:05:58 +04:00
parent 46c6fa9912
commit 27bc8f2e96
6 changed files with 192 additions and 126 deletions

View File

@@ -74,18 +74,6 @@ Send 10,000 GET requests with 50 concurrent connections and a random User-Agent
sarin -U http://example.com -r 10_000 -c 50 -H "User-Agent: {{ fakeit_UserAgent }}" sarin -U http://example.com -r 10_000 -c 50 -H "User-Agent: {{ fakeit_UserAgent }}"
``` ```
Example output:
```
┌──────────┬───────┬──────────┬───────────┬─────────┬───────────┬──────────┬───────────┐
│ Response │ Count │ Min │ Max │ Average │ P90 │ P95 │ P99 │
├──────────┼───────┼──────────┼───────────┼─────────┼───────────┼──────────┼───────────┤
│ 200 │ 10000 │ 78.038ms │ 288.153ms │ 94.71ms │ 103.078ms │ 131.08ms │ 269.218ms │
├──────────┼───────┼──────────┼───────────┼─────────┼───────────┼──────────┼───────────┤
│ Total │ 10000 │ 78.038ms │ 288.153ms │ 94.71ms │ 103.078ms │ 131.08ms │ 269.218ms │
└──────────┴───────┴──────────┴───────────┴─────────┴───────────┴──────────┴───────────┘
```
Run a 5-minute duration-based test: Run a 5-minute duration-based test:
```sh ```sh
@@ -112,15 +100,15 @@ For detailed documentation on all configuration options (URL, method, timeout, c
## Templating ## Templating
Sarin supports Go templates in methods, bodies, headers, params, cookies, and values. Use the 320+ built-in functions to generate dynamic data for each request. Sarin supports Go templates in URL paths, methods, bodies, headers, params, cookies, and values. Use the 320+ built-in functions to generate dynamic data for each request.
**Example:** **Example:**
```sh ```sh
sarin -U http://example.com/users \ sarin -U "http://example.com/users/{{ fakeit_UUID }}" -r 1000 -c 10 \
-V "ID={{ fakeit_UUID }}" \ -V "RequestID={{ fakeit_UUID }}" \
-H "X-Request-ID: {{ .Values.ID }}" \ -H "X-Request-ID: {{ .Values.RequestID }}" \
-B '{"id": "{{ .Values.ID }}"}' -B '{"request_id": "{{ .Values.RequestID }}"}'
``` ```
For the complete templating guide and functions reference, see the **[Templating Guide](docs/templating.md)**. For the complete templating guide and functions reference, see the **[Templating Guide](docs/templating.md)**.

View File

@@ -76,7 +76,21 @@ config3.yaml > config2.yaml > config4.yaml > config1.yaml
## URL ## URL
Target URL. Must be HTTP or HTTPS. Target URL. Must be HTTP or HTTPS. The URL path supports [templating](templating.md), allowing dynamic path generation per request.
> **Note:** Templating is only supported in the URL path. Host and scheme must be static.
**Example with dynamic path:**
```yaml
url: http://example.com/users/{{ fakeit_UUID }}/profile
```
**CLI example with dynamic path:**
```sh
sarin -U "http://example.com/users/{{ fakeit_UUID }}" -r 1000 -c 10
```
## Method ## Method

View File

@@ -6,8 +6,8 @@ This guide provides practical examples for common Sarin use cases.
- [Basic Usage](#basic-usage) - [Basic Usage](#basic-usage)
- [Request-Based vs Duration-Based Tests](#request-based-vs-duration-based-tests) - [Request-Based vs Duration-Based Tests](#request-based-vs-duration-based-tests)
- [Dynamic Requests with Templating](#dynamic-requests-with-templating)
- [Headers, Cookies, and Parameters](#headers-cookies-and-parameters) - [Headers, Cookies, and Parameters](#headers-cookies-and-parameters)
- [Dynamic Requests with Templating](#dynamic-requests-with-templating)
- [Request Bodies](#request-bodies) - [Request Bodies](#request-bodies)
- [Using Proxies](#using-proxies) - [Using Proxies](#using-proxies)
- [Output Formats](#output-formats) - [Output Formats](#output-formats)
@@ -108,104 +108,6 @@ concurrency: 100
</details> </details>
## Dynamic Requests with Templating
Generate a random User-Agent for each request:
```sh
sarin -U http://example.com -r 1000 -c 10 \
-H "User-Agent: {{ fakeit_UserAgent }}"
```
<details>
<summary>YAML equivalent</summary>
```yaml
url: http://example.com
requests: 1000
concurrency: 10
headers:
User-Agent: "{{ fakeit_UserAgent }}"
```
</details>
Send requests with random user data:
```sh
sarin -U http://example.com/api/users -r 1000 -c 10 \
-M POST \
-H "Content-Type: application/json" \
-B '{"name": "{{ fakeit_Name }}", "email": "{{ fakeit_Email }}"}'
```
<details>
<summary>YAML equivalent</summary>
```yaml
url: http://example.com/api/users
requests: 1000
concurrency: 10
method: POST
headers:
Content-Type: application/json
body: '{"name": "{{ fakeit_Name }}", "email": "{{ fakeit_Email }}"}'
```
</details>
Use values to share generated data across headers and body:
```sh
sarin -U http://example.com/api/users -r 1000 -c 10 \
-M POST \
-V "ID={{ fakeit_UUID }}" \
-H "X-Request-ID: {{ .Values.ID }}" \
-B '{"id": "{{ .Values.ID }}", "name": "{{ fakeit_Name }}"}'
```
<details>
<summary>YAML equivalent</summary>
```yaml
url: http://example.com/api/users
requests: 1000
concurrency: 10
method: POST
values: "ID={{ fakeit_UUID }}"
headers:
X-Request-ID: "{{ .Values.ID }}"
body: '{"id": "{{ .Values.ID }}", "name": "{{ fakeit_Name }}"}'
```
</details>
Generate random IPs and timestamps:
```sh
sarin -U http://example.com/api/logs -r 500 -c 20 \
-M POST \
-H "Content-Type: application/json" \
-B '{"ip": "{{ fakeit_IPv4Address }}", "timestamp": "{{ fakeit_Date }}", "action": "{{ fakeit_HackerVerb }}"}'
```
<details>
<summary>YAML equivalent</summary>
```yaml
url: http://example.com/api/logs
requests: 500
concurrency: 20
method: POST
headers:
Content-Type: application/json
body: '{"ip": "{{ fakeit_IPv4Address }}", "timestamp": "{{ fakeit_Date }}", "action": "{{ fakeit_HackerVerb }}"}'
```
</details>
> For the complete list of 320+ template functions, see the **[Templating Guide](templating.md)**.
## Headers, Cookies, and Parameters ## Headers, Cookies, and Parameters
**Custom headers:** **Custom headers:**
@@ -323,6 +225,140 @@ cookies:
</details> </details>
## Dynamic Requests with Templating
**Dynamic URL paths:**
Test different resource endpoints with random IDs:
```sh
sarin -U "http://example.com/users/{{ fakeit_UUID }}/profile" -r 1000 -c 10
```
<details>
<summary>YAML equivalent</summary>
```yaml
url: http://example.com/users/{{ fakeit_UUID }}/profile
requests: 1000
concurrency: 10
```
</details>
Test with random numeric IDs:
```sh
sarin -U "http://example.com/products/{{ fakeit_Number 1 10000 }}" -r 1000 -c 10
```
<details>
<summary>YAML equivalent</summary>
```yaml
url: http://example.com/products/{{ fakeit_Number 1 10000 }}
requests: 1000
concurrency: 10
```
</details>
**Generate a random User-Agent for each request:**
```sh
sarin -U http://example.com -r 1000 -c 10 \
-H "User-Agent: {{ fakeit_UserAgent }}"
```
<details>
<summary>YAML equivalent</summary>
```yaml
url: http://example.com
requests: 1000
concurrency: 10
headers:
User-Agent: "{{ fakeit_UserAgent }}"
```
</details>
Send requests with random user data:
```sh
sarin -U http://example.com/api/users -r 1000 -c 10 \
-M POST \
-H "Content-Type: application/json" \
-B '{"name": "{{ fakeit_Name }}", "email": "{{ fakeit_Email }}"}'
```
<details>
<summary>YAML equivalent</summary>
```yaml
url: http://example.com/api/users
requests: 1000
concurrency: 10
method: POST
headers:
Content-Type: application/json
body: '{"name": "{{ fakeit_Name }}", "email": "{{ fakeit_Email }}"}'
```
</details>
Use values to share generated data across headers and body:
```sh
sarin -U http://example.com/api/users -r 1000 -c 10 \
-M POST \
-V "ID={{ fakeit_UUID }}" \
-H "X-Request-ID: {{ .Values.ID }}" \
-B '{"id": "{{ .Values.ID }}", "name": "{{ fakeit_Name }}"}'
```
<details>
<summary>YAML equivalent</summary>
```yaml
url: http://example.com/api/users
requests: 1000
concurrency: 10
method: POST
values: "ID={{ fakeit_UUID }}"
headers:
X-Request-ID: "{{ .Values.ID }}"
body: '{"id": "{{ .Values.ID }}", "name": "{{ fakeit_Name }}"}'
```
</details>
Generate random IPs and timestamps:
```sh
sarin -U http://example.com/api/logs -r 500 -c 20 \
-M POST \
-H "Content-Type: application/json" \
-B '{"ip": "{{ fakeit_IPv4Address }}", "timestamp": "{{ fakeit_Date }}", "action": "{{ fakeit_HackerVerb }}"}'
```
<details>
<summary>YAML equivalent</summary>
```yaml
url: http://example.com/api/logs
requests: 500
concurrency: 20
method: POST
headers:
Content-Type: application/json
body: '{"ip": "{{ fakeit_IPv4Address }}", "timestamp": "{{ fakeit_Date }}", "action": "{{ fakeit_HackerVerb }}"}'
```
</details>
> For the complete list of 320+ template functions, see the **[Templating Guide](templating.md)**.
## Request Bodies ## Request Bodies
**Simple JSON body:** **Simple JSON body:**

View File

@@ -1,6 +1,8 @@
# Templating # Templating
Sarin supports Go templates in methods, bodies, headers, params, cookies, and values. Sarin supports Go templates in URL paths, methods, bodies, headers, params, cookies, and values.
> **Note:** Templating in URL host and scheme is not supported. Only the path portion of the URL can contain templates.
## Table of Contents ## Table of Contents

View File

@@ -180,6 +180,15 @@ func validateTemplateValues(values []string, funcMap template.FuncMap) []types.F
return validationErrors return validationErrors
} }
func validateTemplateURLPath(urlPath string, funcMap template.FuncMap) []types.FieldValidationError {
if err := validateTemplateString(urlPath, funcMap); err != nil {
return []types.FieldValidationError{
types.NewFieldValidationError("URL.Path", urlPath, err),
}
}
return nil
}
func ValidateTemplates(config *Config) []types.FieldValidationError { func ValidateTemplates(config *Config) []types.FieldValidationError {
// Create template function map using the same functions as sarin package // Create template function map using the same functions as sarin package
randSource := sarin.NewDefaultRandSource() randSource := sarin.NewDefaultRandSource()
@@ -190,6 +199,11 @@ func ValidateTemplates(config *Config) []types.FieldValidationError {
var allErrors []types.FieldValidationError var allErrors []types.FieldValidationError
// Validate URL path
if config.URL != nil {
allErrors = append(allErrors, validateTemplateURLPath(config.URL.Path, funcMap)...)
}
// Validate methods // Validate methods
allErrors = append(allErrors, validateTemplateMethods(config.Methods, funcMap)...) allErrors = append(allErrors, validateTemplateMethods(config.Methods, funcMap)...)

View File

@@ -40,6 +40,7 @@ func NewRequestGenerator(
localRand := rand.New(randSource) localRand := rand.New(randSource)
templateFuncMap := NewDefaultTemplateFuncMap(randSource) templateFuncMap := NewDefaultTemplateFuncMap(randSource)
pathGenerator, isPathGeneratorDynamic := createTemplateFunc(requestURL.Path, templateFuncMap)
methodGenerator, isMethodGeneratorDynamic := NewMethodGeneratorFunc(localRand, methods, templateFuncMap) methodGenerator, isMethodGeneratorDynamic := NewMethodGeneratorFunc(localRand, methods, templateFuncMap)
paramsGenerator, isParamsGeneratorDynamic := NewParamsGeneratorFunc(localRand, params, templateFuncMap) paramsGenerator, isParamsGeneratorDynamic := NewParamsGeneratorFunc(localRand, params, templateFuncMap)
headersGenerator, isHeadersGeneratorDynamic := NewHeadersGeneratorFunc(localRand, headers, templateFuncMap) headersGenerator, isHeadersGeneratorDynamic := NewHeadersGeneratorFunc(localRand, headers, templateFuncMap)
@@ -51,35 +52,45 @@ func NewRequestGenerator(
valuesGenerator := NewValuesGeneratorFunc(values, templateFuncMap) valuesGenerator := NewValuesGeneratorFunc(values, templateFuncMap)
var (
data valuesData
path string
err error
)
return func(req *fasthttp.Request) error { return func(req *fasthttp.Request) error {
req.SetRequestURI(requestURL.Path)
req.Header.SetHost(requestURL.Host) req.Header.SetHost(requestURL.Host)
data, err := valuesGenerator() data, err = valuesGenerator()
if err != nil { if err != nil {
return err return err
} }
if err := methodGenerator(req, data); err != nil { path, err = pathGenerator(data)
if err != nil {
return err
}
req.SetRequestURI(path)
if err = methodGenerator(req, data); err != nil {
return err return err
} }
bodyTemplateFuncMapData.ClearFormDataContenType() bodyTemplateFuncMapData.ClearFormDataContenType()
if err := bodyGenerator(req, data); err != nil { if err = bodyGenerator(req, data); err != nil {
return err return err
} }
if err := headersGenerator(req, data); err != nil { if err = headersGenerator(req, data); err != nil {
return err return err
} }
if bodyTemplateFuncMapData.GetFormDataContenType() != "" { if bodyTemplateFuncMapData.GetFormDataContenType() != "" {
req.Header.Add("Content-Type", bodyTemplateFuncMapData.GetFormDataContenType()) req.Header.Add("Content-Type", bodyTemplateFuncMapData.GetFormDataContenType())
} }
if err := paramsGenerator(req, data); err != nil { if err = paramsGenerator(req, data); err != nil {
return err return err
} }
if err := cookiesGenerator(req, data); err != nil { if err = cookiesGenerator(req, data); err != nil {
return err return err
} }
@@ -87,7 +98,8 @@ func NewRequestGenerator(
req.URI().SetScheme("https") req.URI().SetScheme("https")
} }
return nil return nil
}, isMethodGeneratorDynamic || }, isPathGeneratorDynamic ||
isMethodGeneratorDynamic ||
isParamsGeneratorDynamic || isParamsGeneratorDynamic ||
isHeadersGeneratorDynamic || isHeadersGeneratorDynamic ||
isCookiesGeneratorDynamic || isCookiesGeneratorDynamic ||