mirror of
				https://github.com/aykhans/dodo.git
				synced 2025-11-04 06:29:58 +00:00 
			
		
		
		
	Compare commits
	
		
			85 Commits
		
	
	
		
			3cd72855e5
			...
			dependabot
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					599a7139cf | ||
| 25d4762a3c | |||
| 361d423651 | |||
| ffa724fae7 | |||
| 7930be490d | |||
| e6c54e9cb2 | |||
| b32f567de7 | |||
| b6e85d9443 | |||
| 827e3535cd | |||
| 7ecf534d87 | |||
| 
						 | 
					17ad5fadb9 | ||
| 7fb59a7989 | |||
| 527909c882 | |||
| 4459675efa | |||
| 
						 | 
					604af355e6 | ||
| 7d4267c4c2 | |||
| 
						 | 
					845ab7296c | ||
| 49d004ff06 | |||
| 045deb6120 | |||
| 075ef26203 | |||
| 
						 | 
					946afbb2c3 | ||
| aacb33cfa5 | |||
| 4a7db48351 | |||
| b73087dce5 | |||
| 
						 | 
					20a46feab8 | ||
| 0adde6e04e | |||
| ca50de4e2f | |||
| c99e7c66d9 | |||
| 280e5f5c4e | |||
| 47dfad6046 | |||
| 5bb644d55f | |||
| 9152eefdc5 | |||
| a8cd253c63 | |||
| 9aaf2db74d | |||
| 5c3e254e1e | |||
| e5c681a22b | |||
| 79668e4ece | |||
| f248c2af96 | |||
| 924bd819ee | |||
| e567155eb1 | |||
| 23c74bdbb1 | |||
| addf92df91 | |||
| 6aeda3706b | |||
| dc1cd05714 | |||
| 2b9d0520b0 | |||
| bea2e7c040 | |||
| 
						 | 
					b52b336a52 | ||
| c927e31c49 | |||
| d8e6f532a8 | |||
| cf5cd23d97 | |||
| 
						 | 
					350ff4d66d | ||
| cb8898d20e | |||
| a552d1c9f9 | |||
| 35263f1dd6 | |||
| 930e173a6a | |||
| bea2a81afa | |||
| 53ed486b23 | |||
| 0b9c32a09d | |||
| 42d5617e3f | |||
| e80ae9ab24 | |||
| 
						 | 
					86a6f7814b | ||
| 09034b5f9e | |||
| f1ca2041c3 | |||
| f5a29a2657 | |||
| 439f66eb87 | |||
| 415d0130ce | |||
| abaa8e90b2 | |||
| 046ce74cd9 | |||
| 681cafc213 | |||
| 7e05cf4f6b | |||
| 934cd0ad33 | |||
| 69c4841a05 | |||
| 3cc165cbf4 | |||
| 59f40ad825 | |||
| a170588574 | |||
| 2a0ac390d8 | |||
| 11bb8b3fb0 | |||
| 1aadc3419a | |||
| b3af3f6ad5 | |||
| ed52fff363 | |||
| 985fc6200d | |||
| 1808865358 | |||
| 56342e49c6 | |||
| ec80569d5d | |||
| 459f7ee0dc | 
							
								
								
									
										4
									
								
								.github/workflows/golangci-lint.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/golangci-lint.yml
									
									
									
									
										vendored
									
									
								
							@@ -19,7 +19,7 @@ jobs:
 | 
			
		||||
        with:
 | 
			
		||||
          go-version: stable
 | 
			
		||||
      - name: golangci-lint
 | 
			
		||||
        uses: golangci/golangci-lint-action@v6
 | 
			
		||||
        uses: golangci/golangci-lint-action@v7
 | 
			
		||||
        with:
 | 
			
		||||
          version: v1.64
 | 
			
		||||
          version: v2.4.0
 | 
			
		||||
          args: --timeout=10m --config=.golangci.yml
 | 
			
		||||
 
 | 
			
		||||
@@ -1,16 +1,16 @@
 | 
			
		||||
version: "2"
 | 
			
		||||
 | 
			
		||||
run:
 | 
			
		||||
    go: "1.24"
 | 
			
		||||
    go: "1.25"
 | 
			
		||||
    concurrency: 8
 | 
			
		||||
    timeout: 10m
 | 
			
		||||
 | 
			
		||||
linters:
 | 
			
		||||
    disable-all: true
 | 
			
		||||
    default: none
 | 
			
		||||
    enable:
 | 
			
		||||
        - asasalint
 | 
			
		||||
        - asciicheck
 | 
			
		||||
        - errcheck
 | 
			
		||||
        - gofmt
 | 
			
		||||
        - goimports
 | 
			
		||||
        - gomodguard
 | 
			
		||||
        - goprintffuncname
 | 
			
		||||
        - govet
 | 
			
		||||
@@ -21,7 +21,13 @@ linters:
 | 
			
		||||
        - prealloc
 | 
			
		||||
        - reassign
 | 
			
		||||
        - staticcheck
 | 
			
		||||
        - typecheck
 | 
			
		||||
        - unconvert
 | 
			
		||||
        - unused
 | 
			
		||||
        - whitespace
 | 
			
		||||
 | 
			
		||||
    settings:
 | 
			
		||||
        staticcheck:
 | 
			
		||||
            checks:
 | 
			
		||||
                - "all"
 | 
			
		||||
                - "-S1002"
 | 
			
		||||
                - "-ST1000"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
FROM golang:1.24-alpine AS builder
 | 
			
		||||
FROM golang:1.25-alpine AS builder
 | 
			
		||||
 | 
			
		||||
WORKDIR /src
 | 
			
		||||
 | 
			
		||||
@@ -6,14 +6,12 @@ COPY go.mod go.sum ./
 | 
			
		||||
RUN go mod download
 | 
			
		||||
COPY . .
 | 
			
		||||
 | 
			
		||||
RUN go build -ldflags "-s -w" -o dodo
 | 
			
		||||
RUN echo "{}" > config.json
 | 
			
		||||
RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o dodo
 | 
			
		||||
 | 
			
		||||
FROM gcr.io/distroless/static-debian12:latest
 | 
			
		||||
 | 
			
		||||
WORKDIR /
 | 
			
		||||
 | 
			
		||||
COPY --from=builder /src/dodo /dodo
 | 
			
		||||
COPY --from=builder /src/config.json /config.json
 | 
			
		||||
 | 
			
		||||
ENTRYPOINT ["./dodo", "-f", "/config.json"]
 | 
			
		||||
ENTRYPOINT ["./dodo"]
 | 
			
		||||
							
								
								
									
										934
									
								
								EXAMPLES.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										934
									
								
								EXAMPLES.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,934 @@
 | 
			
		||||
# Dodo Usage Examples
 | 
			
		||||
 | 
			
		||||
This document provides comprehensive examples of using Dodo with various configuration combinations. Each example includes three methods: CLI usage, YAML configuration, and JSON configuration.
 | 
			
		||||
 | 
			
		||||
## Table of Contents
 | 
			
		||||
 | 
			
		||||
1. [Basic HTTP Stress Testing](#1-basic-http-stress-testing)
 | 
			
		||||
2. [POST Request with Form Data](#2-post-request-with-form-data)
 | 
			
		||||
3. [API Testing with Authentication](#3-api-testing-with-authentication)
 | 
			
		||||
4. [Testing with Custom Headers and Cookies](#4-testing-with-custom-headers-and-cookies)
 | 
			
		||||
5. [Load Testing with Proxy Rotation](#5-load-testing-with-proxy-rotation)
 | 
			
		||||
6. [JSON API Testing with Dynamic Data](#6-json-api-testing-with-dynamic-data)
 | 
			
		||||
7. [File Upload Testing](#7-file-upload-testing)
 | 
			
		||||
8. [E-commerce Cart Testing](#8-e-commerce-cart-testing)
 | 
			
		||||
9. [GraphQL API Testing](#9-graphql-api-testing)
 | 
			
		||||
10. [WebSocket-style HTTP Testing](#10-websocket-style-http-testing)
 | 
			
		||||
11. [Multi-tenant Application Testing](#11-multi-tenant-application-testing)
 | 
			
		||||
12. [Rate Limiting Testing](#12-rate-limiting-testing)
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## 1. Basic HTTP Stress Testing
 | 
			
		||||
 | 
			
		||||
Test a simple website with basic GET requests to measure performance under load.
 | 
			
		||||
 | 
			
		||||
### CLI Usage
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
dodo -u https://httpbin.org/get \
 | 
			
		||||
     -m GET \
 | 
			
		||||
     -d 5 \
 | 
			
		||||
     -r 100 \
 | 
			
		||||
     -t 5s \
 | 
			
		||||
     -o 30s \
 | 
			
		||||
     --skip-verify=false \
 | 
			
		||||
     -y
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### YAML Configuration
 | 
			
		||||
 | 
			
		||||
```yaml
 | 
			
		||||
method: "GET"
 | 
			
		||||
url: "https://httpbin.org/get"
 | 
			
		||||
yes: true
 | 
			
		||||
timeout: "5s"
 | 
			
		||||
dodos: 5
 | 
			
		||||
requests: 100
 | 
			
		||||
duration: "30s"
 | 
			
		||||
skip_verify: false
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### JSON Configuration
 | 
			
		||||
 | 
			
		||||
```json
 | 
			
		||||
{
 | 
			
		||||
    "method": "GET",
 | 
			
		||||
    "url": "https://httpbin.org/get",
 | 
			
		||||
    "yes": true,
 | 
			
		||||
    "timeout": "5s",
 | 
			
		||||
    "dodos": 5,
 | 
			
		||||
    "requests": 100,
 | 
			
		||||
    "duration": "30s",
 | 
			
		||||
    "skip_verify": false
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## 2. POST Request with Form Data
 | 
			
		||||
 | 
			
		||||
Test form submission endpoints with randomized form data.
 | 
			
		||||
 | 
			
		||||
### CLI Usage
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
dodo -u https://httpbin.org/post \
 | 
			
		||||
     -m POST \
 | 
			
		||||
     -d 3 \
 | 
			
		||||
     -r 50 \
 | 
			
		||||
     -t 10s \
 | 
			
		||||
     --skip-verify=false \
 | 
			
		||||
     -H "Content-Type:application/x-www-form-urlencoded" \
 | 
			
		||||
     -b "username={{ fakeit_Username }}&password={{ fakeit_Password true true true true true 12 }}&email={{ fakeit_Email }}" \
 | 
			
		||||
     -b "username={{ fakeit_Username }}&password={{ fakeit_Password true true true true true 8 }}&email={{ fakeit_Email }}" \
 | 
			
		||||
     -y
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### YAML Configuration
 | 
			
		||||
 | 
			
		||||
```yaml
 | 
			
		||||
method: "POST"
 | 
			
		||||
url: "https://httpbin.org/post"
 | 
			
		||||
yes: true
 | 
			
		||||
timeout: "10s"
 | 
			
		||||
dodos: 3
 | 
			
		||||
requests: 50
 | 
			
		||||
skip_verify: false
 | 
			
		||||
 | 
			
		||||
headers:
 | 
			
		||||
    - Content-Type: "application/x-www-form-urlencoded"
 | 
			
		||||
 | 
			
		||||
body:
 | 
			
		||||
    - "username={{ fakeit_Username }}&password={{ fakeit_Password true true true true true 12 }}&email={{ fakeit_Email }}"
 | 
			
		||||
    - "username={{ fakeit_Username }}&password={{ fakeit_Password true true true true true 8 }}&email={{ fakeit_Email }}"
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### JSON Configuration
 | 
			
		||||
 | 
			
		||||
```json
 | 
			
		||||
{
 | 
			
		||||
    "method": "POST",
 | 
			
		||||
    "url": "https://httpbin.org/post",
 | 
			
		||||
    "yes": true,
 | 
			
		||||
    "timeout": "10s",
 | 
			
		||||
    "dodos": 3,
 | 
			
		||||
    "requests": 50,
 | 
			
		||||
    "skip_verify": false,
 | 
			
		||||
    "headers": [{ "Content-Type": "application/x-www-form-urlencoded" }],
 | 
			
		||||
    "body": [
 | 
			
		||||
        "username={{ fakeit_Username }}&password={{ fakeit_Password true true true true true 12 }}&email={{ fakeit_Email }}",
 | 
			
		||||
        "username={{ fakeit_Username }}&password={{ fakeit_Password true true true true true 8 }}&email={{ fakeit_Email }}"
 | 
			
		||||
    ]
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## 3. API Testing with Authentication
 | 
			
		||||
 | 
			
		||||
Test protected API endpoints with various authentication methods.
 | 
			
		||||
 | 
			
		||||
### CLI Usage
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
dodo -u https://httpbin.org/bearer \
 | 
			
		||||
     -m GET \
 | 
			
		||||
     -d 4 \
 | 
			
		||||
     -r 200 \
 | 
			
		||||
     -t 8s \
 | 
			
		||||
     --skip-verify=false \
 | 
			
		||||
     -H "Authorization:Bearer {{ fakeit_LetterN 32 }}" \
 | 
			
		||||
     -H "User-Agent:{{ fakeit_UserAgent }}" \
 | 
			
		||||
     -H "X-Request-ID:{{ fakeit_Int }}" \
 | 
			
		||||
     -H "Accept:application/json" \
 | 
			
		||||
     -p "api_version=v1" \
 | 
			
		||||
     -p "format=json" \
 | 
			
		||||
     -p "client_id=mobile" -p "client_id=web" -p "client_id=desktop" \
 | 
			
		||||
     -y
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### YAML Configuration
 | 
			
		||||
 | 
			
		||||
```yaml
 | 
			
		||||
method: "GET"
 | 
			
		||||
url: "https://httpbin.org/bearer"
 | 
			
		||||
yes: true
 | 
			
		||||
timeout: "8s"
 | 
			
		||||
dodos: 4
 | 
			
		||||
requests: 200
 | 
			
		||||
skip_verify: false
 | 
			
		||||
 | 
			
		||||
params:
 | 
			
		||||
    - api_version: "v1"
 | 
			
		||||
    - format: "json"
 | 
			
		||||
    - client_id: ["mobile", "web", "desktop"]
 | 
			
		||||
 | 
			
		||||
headers:
 | 
			
		||||
    - Authorization: "Bearer {{ fakeit_LetterN 32 }}"
 | 
			
		||||
    - User-Agent: "{{ fakeit_UserAgent }}"
 | 
			
		||||
    - X-Request-ID: "{{ fakeit_Int }}"
 | 
			
		||||
    - Accept: "application/json"
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### JSON Configuration
 | 
			
		||||
 | 
			
		||||
```json
 | 
			
		||||
{
 | 
			
		||||
    "method": "GET",
 | 
			
		||||
    "url": "https://httpbin.org/bearer",
 | 
			
		||||
    "yes": true,
 | 
			
		||||
    "timeout": "8s",
 | 
			
		||||
    "dodos": 4,
 | 
			
		||||
    "requests": 200,
 | 
			
		||||
    "skip_verify": false,
 | 
			
		||||
    "params": [
 | 
			
		||||
        { "api_version": "v1" },
 | 
			
		||||
        { "format": "json" },
 | 
			
		||||
        { "client_id": ["mobile", "web", "desktop"] }
 | 
			
		||||
    ],
 | 
			
		||||
    "headers": [
 | 
			
		||||
        { "Authorization": "Bearer {{ fakeit_LetterN 32 }}" },
 | 
			
		||||
        { "User-Agent": "{{ fakeit_UserAgent }}" },
 | 
			
		||||
        { "X-Request-ID": "{{ fakeit_Int }}" },
 | 
			
		||||
        { "Accept": "application/json" }
 | 
			
		||||
    ]
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## 4. Testing with Custom Headers and Cookies
 | 
			
		||||
 | 
			
		||||
Test applications that require specific headers and session cookies.
 | 
			
		||||
 | 
			
		||||
### CLI Usage
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
dodo -u https://httpbin.org/cookies \
 | 
			
		||||
     -m GET \
 | 
			
		||||
     -d 6 \
 | 
			
		||||
     -r 75 \
 | 
			
		||||
     -t 5s \
 | 
			
		||||
     --skip-verify=false \
 | 
			
		||||
     -H 'Accept-Language:{{ strings_Join "," (fakeit_LanguageAbbreviation) (fakeit_LanguageAbbreviation) (fakeit_LanguageAbbreviation) }}' \
 | 
			
		||||
     -H "X-Forwarded-For:{{ fakeit_IPv4Address }}" \
 | 
			
		||||
     -H "X-Real-IP:{{ fakeit_IPv4Address }}" \
 | 
			
		||||
     -H "Accept-Encoding:gzip" -H "Accept-Encoding:deflate" -H "Accept-Encoding:br" \
 | 
			
		||||
     -c "session_id={{ fakeit_UUID }}" \
 | 
			
		||||
     -c 'user_pref={{ fakeit_RandomString "a1" "b2" "c3" }}' \
 | 
			
		||||
     -c "theme=dark" -c "theme=light" -c "theme=auto" \
 | 
			
		||||
     -c "lang=en" -c "lang=es" -c "lang=fr" -c "lang=de" \
 | 
			
		||||
     -y
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### YAML Configuration
 | 
			
		||||
 | 
			
		||||
```yaml
 | 
			
		||||
method: "GET"
 | 
			
		||||
url: "https://httpbin.org/cookies"
 | 
			
		||||
yes: true
 | 
			
		||||
timeout: "5s"
 | 
			
		||||
dodos: 6
 | 
			
		||||
requests: 75
 | 
			
		||||
skip_verify: false
 | 
			
		||||
 | 
			
		||||
headers:
 | 
			
		||||
    - Accept-Language: '{{ strings_Join "," (fakeit_LanguageAbbreviation) (fakeit_LanguageAbbreviation) (fakeit_LanguageAbbreviation) }}'
 | 
			
		||||
    - X-Forwarded-For: "{{ fakeit_IPv4Address }}"
 | 
			
		||||
    - X-Real-IP: "{{ fakeit_IPv4Address }}"
 | 
			
		||||
    - Accept-Encoding: ["gzip", "deflate", "br"]
 | 
			
		||||
 | 
			
		||||
cookies:
 | 
			
		||||
    - session_id: "{{ fakeit_UUID }}"
 | 
			
		||||
    - user_pref: '{{ fakeit_RandomString "a1" "b2" "c3" }}'
 | 
			
		||||
    - theme: ["dark", "light", "auto"]
 | 
			
		||||
    - lang: ["en", "es", "fr", "de"]
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### JSON Configuration
 | 
			
		||||
 | 
			
		||||
```json
 | 
			
		||||
{
 | 
			
		||||
    "method": "GET",
 | 
			
		||||
    "url": "https://httpbin.org/cookies",
 | 
			
		||||
    "yes": true,
 | 
			
		||||
    "timeout": "5s",
 | 
			
		||||
    "dodos": 6,
 | 
			
		||||
    "requests": 75,
 | 
			
		||||
    "skip_verify": false,
 | 
			
		||||
    "headers": [
 | 
			
		||||
        {
 | 
			
		||||
            "Accept-Language": "{{ strings_Join \",\" (fakeit_LanguageAbbreviation) (fakeit_LanguageAbbreviation) (fakeit_LanguageAbbreviation) }}"
 | 
			
		||||
        },
 | 
			
		||||
        { "X-Forwarded-For": "{{ fakeit_IPv4Address }}" },
 | 
			
		||||
        { "X-Real-IP": "{{ fakeit_IPv4Address }}" },
 | 
			
		||||
        { "Accept-Encoding": ["gzip", "deflate", "br"] }
 | 
			
		||||
    ],
 | 
			
		||||
    "cookies": [
 | 
			
		||||
        { "session_id": "{{ fakeit_UUID }}" },
 | 
			
		||||
        { "user_pref": "{{ fakeit_RandomString \"a1\" \"b2\" \"c3\" }}" },
 | 
			
		||||
        { "theme": ["dark", "light", "auto"] },
 | 
			
		||||
        { "lang": ["en", "es", "fr", "de"] }
 | 
			
		||||
    ]
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## 5. Load Testing with Proxy Rotation
 | 
			
		||||
 | 
			
		||||
Test through multiple proxies for distributed load testing.
 | 
			
		||||
 | 
			
		||||
### CLI Usage
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
dodo -u https://httpbin.org/ip \
 | 
			
		||||
     -m GET \
 | 
			
		||||
     -d 8 \
 | 
			
		||||
     -r 300 \
 | 
			
		||||
     -t 15s \
 | 
			
		||||
     --skip-verify=false \
 | 
			
		||||
     -x "http://proxy1.example.com:8080" \
 | 
			
		||||
     -x "http://proxy2.example.com:8080" \
 | 
			
		||||
     -x "socks5://proxy3.example.com:1080" \
 | 
			
		||||
     -x "http://username:password@proxy4.example.com:8080" \
 | 
			
		||||
     -H "User-Agent:{{ fakeit_UserAgent }}" \
 | 
			
		||||
     -H "Accept:application/json" \
 | 
			
		||||
     -y
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### YAML Configuration
 | 
			
		||||
 | 
			
		||||
```yaml
 | 
			
		||||
method: "GET"
 | 
			
		||||
url: "https://httpbin.org/ip"
 | 
			
		||||
yes: true
 | 
			
		||||
timeout: "15s"
 | 
			
		||||
dodos: 8
 | 
			
		||||
requests: 300
 | 
			
		||||
skip_verify: false
 | 
			
		||||
 | 
			
		||||
proxy:
 | 
			
		||||
    - "http://proxy1.example.com:8080"
 | 
			
		||||
    - "http://proxy2.example.com:8080"
 | 
			
		||||
    - "socks5://proxy3.example.com:1080"
 | 
			
		||||
    - "http://username:password@proxy4.example.com:8080"
 | 
			
		||||
 | 
			
		||||
headers:
 | 
			
		||||
    - User-Agent: "{{ fakeit_UserAgent }}"
 | 
			
		||||
    - Accept: "application/json"
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### JSON Configuration
 | 
			
		||||
 | 
			
		||||
```json
 | 
			
		||||
{
 | 
			
		||||
    "method": "GET",
 | 
			
		||||
    "url": "https://httpbin.org/ip",
 | 
			
		||||
    "yes": true,
 | 
			
		||||
    "timeout": "15s",
 | 
			
		||||
    "dodos": 8,
 | 
			
		||||
    "requests": 300,
 | 
			
		||||
    "skip_verify": false,
 | 
			
		||||
    "proxy": [
 | 
			
		||||
        "http://proxy1.example.com:8080",
 | 
			
		||||
        "http://proxy2.example.com:8080",
 | 
			
		||||
        "socks5://proxy3.example.com:1080",
 | 
			
		||||
        "http://username:password@proxy4.example.com:8080"
 | 
			
		||||
    ],
 | 
			
		||||
    "headers": [
 | 
			
		||||
        { "User-Agent": "{{ fakeit_UserAgent }}" },
 | 
			
		||||
        { "Accept": "application/json" }
 | 
			
		||||
    ]
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## 6. JSON API Testing with Dynamic Data
 | 
			
		||||
 | 
			
		||||
Test REST APIs with realistic JSON payloads.
 | 
			
		||||
 | 
			
		||||
### CLI Usage
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
dodo -u https://httpbin.org/post \
 | 
			
		||||
     -m POST \
 | 
			
		||||
     -d 5 \
 | 
			
		||||
     -r 150 \
 | 
			
		||||
     -t 12s \
 | 
			
		||||
     --skip-verify=false \
 | 
			
		||||
     -H "Content-Type:application/json" \
 | 
			
		||||
     -H "Accept:application/json" \
 | 
			
		||||
     -H "X-API-Version:2023-10-01" \
 | 
			
		||||
     -b '{"user_id":{{ fakeit_Uint }},"name":"{{ fakeit_Name }}","email":"{{ fakeit_Email }}","created_at":"{{ fakeit_Date }}"}' \
 | 
			
		||||
     -b '{"product_id":{{ fakeit_Uint }},"name":"{{ fakeit_ProductName }}","price":{{ fakeit_Price 10 1000 }},"category":"{{ fakeit_ProductCategory }}"}' \
 | 
			
		||||
     -b '{"order_id":"{{ fakeit_UUID }}","items":[{"id":{{ fakeit_Uint }},"quantity":{{ fakeit_IntRange 1 10 }}}],"total":{{ fakeit_Price 50 500 }}}' \
 | 
			
		||||
     -y
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### YAML Configuration
 | 
			
		||||
 | 
			
		||||
```yaml
 | 
			
		||||
method: "POST"
 | 
			
		||||
url: "https://httpbin.org/post"
 | 
			
		||||
yes: true
 | 
			
		||||
timeout: "12s"
 | 
			
		||||
dodos: 5
 | 
			
		||||
requests: 150
 | 
			
		||||
skip_verify: false
 | 
			
		||||
 | 
			
		||||
headers:
 | 
			
		||||
    - Content-Type: "application/json"
 | 
			
		||||
    - Accept: "application/json"
 | 
			
		||||
    - X-API-Version: "2023-10-01"
 | 
			
		||||
 | 
			
		||||
body:
 | 
			
		||||
    - '{"user_id":{{ fakeit_Uint }},"name":"{{ fakeit_Name }}","email":"{{ fakeit_Email }}","created_at":"{{ fakeit_Date }}"}'
 | 
			
		||||
    - '{"product_id":{{ fakeit_Uint }},"name":"{{ fakeit_ProductName }}","price":{{ fakeit_Price 10 1000 }},"category":"{{ fakeit_ProductCategory }}"}'
 | 
			
		||||
    - '{"order_id":"{{ fakeit_UUID }}","items":[{"id":{{ fakeit_Uint }},"quantity":{{ fakeit_IntRange 1 10 }}}],"total":{{ fakeit_Price 50 500 }}}'
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### JSON Configuration
 | 
			
		||||
 | 
			
		||||
```json
 | 
			
		||||
{
 | 
			
		||||
    "method": "POST",
 | 
			
		||||
    "url": "https://httpbin.org/post",
 | 
			
		||||
    "yes": true,
 | 
			
		||||
    "timeout": "12s",
 | 
			
		||||
    "dodos": 5,
 | 
			
		||||
    "requests": 150,
 | 
			
		||||
    "skip_verify": false,
 | 
			
		||||
    "headers": [
 | 
			
		||||
        { "Content-Type": "application/json" },
 | 
			
		||||
        { "Accept": "application/json" },
 | 
			
		||||
        { "X-API-Version": "2023-10-01" }
 | 
			
		||||
    ],
 | 
			
		||||
    "body": [
 | 
			
		||||
        "{\"user_id\":{{ fakeit_Uint }},\"name\":\"{{ fakeit_Name }}\",\"email\":\"{{ fakeit_Email }}\",\"created_at\":\"{{ fakeit_Date }}\"}",
 | 
			
		||||
        "{\"product_id\":{{ fakeit_Uint }},\"name\":\"{{ fakeit_ProductName }}\",\"price\":{{ fakeit_Price 10 1000 }},\"category\":\"{{ fakeit_ProductCategory }}\"}",
 | 
			
		||||
        "{\"order_id\":\"{{ fakeit_UUID }}\",\"items\":[{\"id\":{{ fakeit_Uint }},\"quantity\":{{ fakeit_IntRange 1 10 }}}],\"total\":{{ fakeit_Price 50 500 }}}"
 | 
			
		||||
    ]
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## 7. File Upload Testing
 | 
			
		||||
 | 
			
		||||
Test file upload endpoints with multipart form data.
 | 
			
		||||
 | 
			
		||||
### CLI Usage
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
dodo -u https://httpbin.org/post \
 | 
			
		||||
     -m POST \
 | 
			
		||||
     -d 3 \
 | 
			
		||||
     -r 25 \
 | 
			
		||||
     -t 30s \
 | 
			
		||||
     --skip-verify=false \
 | 
			
		||||
     -H "X-Upload-Source:dodo-test" \
 | 
			
		||||
     -H "User-Agent:{{ fakeit_UserAgent }}" \
 | 
			
		||||
     -b '{{ body_FormData (dict_Str "filename" (fakeit_UUID) "content" (fakeit_Paragraph 3 5 10 " ")) }}' \
 | 
			
		||||
     -b '{{ body_FormData (dict_Str "file" (fakeit_UUID) "description" (fakeit_Sentence 10) "category" "image") }}' \
 | 
			
		||||
     -y
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### YAML Configuration
 | 
			
		||||
 | 
			
		||||
```yaml
 | 
			
		||||
method: "POST"
 | 
			
		||||
url: "https://httpbin.org/post"
 | 
			
		||||
yes: true
 | 
			
		||||
timeout: "30s"
 | 
			
		||||
dodos: 3
 | 
			
		||||
requests: 25
 | 
			
		||||
skip_verify: false
 | 
			
		||||
 | 
			
		||||
headers:
 | 
			
		||||
    - X-Upload-Source: "dodo-test"
 | 
			
		||||
    - User-Agent: "{{ fakeit_UserAgent }}"
 | 
			
		||||
 | 
			
		||||
body:
 | 
			
		||||
    - '{{ body_FormData (dict_Str "filename" (fakeit_UUID) "content" (fakeit_Paragraph 3 5 10 " ")) }}'
 | 
			
		||||
    - '{{ body_FormData (dict_Str "file" (fakeit_UUID) "description" (fakeit_Sentence 10) "category" "image") }}'
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### JSON Configuration
 | 
			
		||||
 | 
			
		||||
```json
 | 
			
		||||
{
 | 
			
		||||
    "method": "POST",
 | 
			
		||||
    "url": "https://httpbin.org/post",
 | 
			
		||||
    "yes": true,
 | 
			
		||||
    "timeout": "30s",
 | 
			
		||||
    "dodos": 3,
 | 
			
		||||
    "requests": 25,
 | 
			
		||||
    "skip_verify": false,
 | 
			
		||||
    "headers": [
 | 
			
		||||
        { "X-Upload-Source": "dodo-test" },
 | 
			
		||||
        { "User-Agent": "{{ fakeit_UserAgent }}" }
 | 
			
		||||
    ],
 | 
			
		||||
    "body": [
 | 
			
		||||
        "{{ body_FormData (dict_Str \"filename\" (fakeit_UUID) \"content\" (fakeit_Paragraph 3 5 10 \" \")) }}",
 | 
			
		||||
        "{{ body_FormData (dict_Str \"file\" (fakeit_UUID) \"description\" (fakeit_Sentence 10) \"category\" \"image\") }}"
 | 
			
		||||
    ]
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## 8. E-commerce Cart Testing
 | 
			
		||||
 | 
			
		||||
Test shopping cart operations with realistic product data.
 | 
			
		||||
 | 
			
		||||
### CLI Usage
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
dodo -u https://api.example-shop.com/cart \
 | 
			
		||||
     -m POST \
 | 
			
		||||
     -d 10 \
 | 
			
		||||
     -r 500 \
 | 
			
		||||
     -t 8s \
 | 
			
		||||
     --skip-verify=false \
 | 
			
		||||
     -H "Content-Type:application/json" \
 | 
			
		||||
     -H "Authorization:Bearer {{ fakeit_LetterN 32 }}" \
 | 
			
		||||
     -H "X-Client-Version:1.2.3" \
 | 
			
		||||
     -H "User-Agent:{{ fakeit_UserAgent }}" \
 | 
			
		||||
     -c "cart_session={{ fakeit_UUID }}" \
 | 
			
		||||
     -c "user_pref=guest" -c "user_pref=member" -c "user_pref=premium" \
 | 
			
		||||
     -c "region=US" -c "region=EU" -c "region=ASIA" \
 | 
			
		||||
     -p "currency=USD" -p "currency=EUR" -p "currency=GBP" \
 | 
			
		||||
     -p "locale=en-US" -p "locale=en-GB" -p "locale=de-DE" -p "locale=fr-FR" \
 | 
			
		||||
     -b '{"action":"add","product_id":"{{ fakeit_UUID }}","quantity":{{ fakeit_IntRange 1 5 }},"user_id":"{{ fakeit_UUID }}"}' \
 | 
			
		||||
     -b '{"action":"remove","product_id":"{{ fakeit_UUID }}","user_id":"{{ fakeit_UUID }}"}' \
 | 
			
		||||
     -b '{"action":"update","product_id":"{{ fakeit_UUID }}","quantity":{{ fakeit_IntRange 1 10 }},"user_id":"{{ fakeit_UUID }}"}' \
 | 
			
		||||
     -y
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### YAML Configuration
 | 
			
		||||
 | 
			
		||||
```yaml
 | 
			
		||||
method: "POST"
 | 
			
		||||
url: "https://api.example-shop.com/cart"
 | 
			
		||||
yes: true
 | 
			
		||||
timeout: "8s"
 | 
			
		||||
dodos: 10
 | 
			
		||||
requests: 500
 | 
			
		||||
skip_verify: false
 | 
			
		||||
 | 
			
		||||
headers:
 | 
			
		||||
    - Content-Type: "application/json"
 | 
			
		||||
    - Authorization: "Bearer {{ fakeit_LetterN 32 }}"
 | 
			
		||||
    - X-Client-Version: "1.2.3"
 | 
			
		||||
    - User-Agent: "{{ fakeit_UserAgent }}"
 | 
			
		||||
 | 
			
		||||
cookies:
 | 
			
		||||
    - cart_session: "{{ fakeit_UUID }}"
 | 
			
		||||
    - user_pref: ["guest", "member", "premium"]
 | 
			
		||||
    - region: ["US", "EU", "ASIA"]
 | 
			
		||||
 | 
			
		||||
params:
 | 
			
		||||
    - currency: ["USD", "EUR", "GBP"]
 | 
			
		||||
    - locale: ["en-US", "en-GB", "de-DE", "fr-FR"]
 | 
			
		||||
 | 
			
		||||
body:
 | 
			
		||||
    - '{"action":"add","product_id":"{{ fakeit_UUID }}","quantity":{{ fakeit_IntRange 1 5 }},"user_id":"{{ fakeit_UUID }}"}'
 | 
			
		||||
    - '{"action":"remove","product_id":"{{ fakeit_UUID }}","user_id":"{{ fakeit_UUID }}"}'
 | 
			
		||||
    - '{"action":"update","product_id":"{{ fakeit_UUID }}","quantity":{{ fakeit_IntRange 1 10 }},"user_id":"{{ fakeit_UUID }}"}'
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### JSON Configuration
 | 
			
		||||
 | 
			
		||||
```json
 | 
			
		||||
{
 | 
			
		||||
    "method": "POST",
 | 
			
		||||
    "url": "https://api.example-shop.com/cart",
 | 
			
		||||
    "yes": true,
 | 
			
		||||
    "timeout": "8s",
 | 
			
		||||
    "dodos": 10,
 | 
			
		||||
    "requests": 500,
 | 
			
		||||
    "skip_verify": false,
 | 
			
		||||
    "headers": [
 | 
			
		||||
        { "Content-Type": "application/json" },
 | 
			
		||||
        { "Authorization": "Bearer {{ fakeit_LetterN 32 }}" },
 | 
			
		||||
        { "X-Client-Version": "1.2.3" },
 | 
			
		||||
        { "User-Agent": "{{ fakeit_UserAgent }}" }
 | 
			
		||||
    ],
 | 
			
		||||
    "cookies": [
 | 
			
		||||
        { "cart_session": "{{ fakeit_UUID }}" },
 | 
			
		||||
        { "user_pref": ["guest", "member", "premium"] },
 | 
			
		||||
        { "region": ["US", "EU", "ASIA"] }
 | 
			
		||||
    ],
 | 
			
		||||
    "params": [
 | 
			
		||||
        { "currency": ["USD", "EUR", "GBP"] },
 | 
			
		||||
        { "locale": ["en-US", "en-GB", "de-DE", "fr-FR"] }
 | 
			
		||||
    ],
 | 
			
		||||
    "body": [
 | 
			
		||||
        "{\"action\":\"add\",\"product_id\":\"{{ fakeit_UUID }}\",\"quantity\":{{ fakeit_IntRange 1 5 }},\"user_id\":\"{{ fakeit_UUID }}\"}",
 | 
			
		||||
        "{\"action\":\"remove\",\"product_id\":\"{{ fakeit_UUID }}\",\"user_id\":\"{{ fakeit_UUID }}\"}",
 | 
			
		||||
        "{\"action\":\"update\",\"product_id\":\"{{ fakeit_UUID }}\",\"quantity\":{{ fakeit_IntRange 1 10 }},\"user_id\":\"{{ fakeit_UUID }}\"}"
 | 
			
		||||
    ]
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## 9. GraphQL API Testing
 | 
			
		||||
 | 
			
		||||
Test GraphQL endpoints with various queries and mutations.
 | 
			
		||||
 | 
			
		||||
### CLI Usage
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
dodo -u https://api.example.com/graphql \
 | 
			
		||||
     -m POST \
 | 
			
		||||
     -d 4 \
 | 
			
		||||
     -r 100 \
 | 
			
		||||
     -t 10s \
 | 
			
		||||
     --skip-verify=false \
 | 
			
		||||
     -H "Content-Type:application/json" \
 | 
			
		||||
     -H "Authorization:Bearer {{ fakeit_UUID }}" \
 | 
			
		||||
     -H "X-GraphQL-Client:dodo-test" \
 | 
			
		||||
     -b '{"query":"query GetUser($id: ID!) { user(id: $id) { id name email } }","variables":{"id":"{{ fakeit_UUID }}"}}' \
 | 
			
		||||
     -b '{"query":"query GetPosts($limit: Int) { posts(limit: $limit) { id title content } }","variables":{"limit":{{ fakeit_IntRange 5 20 }}}}' \
 | 
			
		||||
     -b '{"query":"mutation CreatePost($input: PostInput!) { createPost(input: $input) { id title } }","variables":{"input":{"title":"{{ fakeit_Sentence 5 }}","content":"{{ fakeit_Paragraph 2 3 5 " "}}","authorId":"{{ fakeit_UUID }}"}}}' \
 | 
			
		||||
     -y
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### YAML Configuration
 | 
			
		||||
 | 
			
		||||
```yaml
 | 
			
		||||
method: "POST"
 | 
			
		||||
url: "https://api.example.com/graphql"
 | 
			
		||||
yes: true
 | 
			
		||||
timeout: "10s"
 | 
			
		||||
dodos: 4
 | 
			
		||||
requests: 100
 | 
			
		||||
skip_verify: false
 | 
			
		||||
 | 
			
		||||
headers:
 | 
			
		||||
    - Content-Type: "application/json"
 | 
			
		||||
    - Authorization: "Bearer {{ fakeit_UUID }}"
 | 
			
		||||
    - X-GraphQL-Client: "dodo-test"
 | 
			
		||||
 | 
			
		||||
body:
 | 
			
		||||
    - '{"query":"query GetUser($id: ID!) { user(id: $id) { id name email } }","variables":{"id":"{{ fakeit_UUID }}"}}'
 | 
			
		||||
    - '{"query":"query GetPosts($limit: Int) { posts(limit: $limit) { id title content } }","variables":{"limit":{{ fakeit_IntRange 5 20 }}}}'
 | 
			
		||||
    - '{"query":"mutation CreatePost($input: PostInput!) { createPost(input: $input) { id title } }","variables":{"input":{"title":"{{ fakeit_Sentence 5 }}","content":"{{ fakeit_Paragraph 2 3 5 " "}}","authorId":"{{ fakeit_UUID }}"}}}'
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### JSON Configuration
 | 
			
		||||
 | 
			
		||||
```json
 | 
			
		||||
{
 | 
			
		||||
    "method": "POST",
 | 
			
		||||
    "url": "https://api.example.com/graphql",
 | 
			
		||||
    "yes": true,
 | 
			
		||||
    "timeout": "10s",
 | 
			
		||||
    "dodos": 4,
 | 
			
		||||
    "requests": 100,
 | 
			
		||||
    "skip_verify": false,
 | 
			
		||||
    "headers": [
 | 
			
		||||
        { "Content-Type": "application/json" },
 | 
			
		||||
        { "Authorization": "Bearer {{ fakeit_UUID }}" },
 | 
			
		||||
        { "X-GraphQL-Client": "dodo-test" }
 | 
			
		||||
    ],
 | 
			
		||||
    "body": [
 | 
			
		||||
        "{\"query\":\"query GetUser($id: ID!) { user(id: $id) { id name email } }\",\"variables\":{\"id\":\"{{ fakeit_UUID }}\"}}",
 | 
			
		||||
        "{\"query\":\"query GetPosts($limit: Int) { posts(limit: $limit) { id title content } }\",\"variables\":{\"limit\":{{ fakeit_IntRange 5 20 }}}}",
 | 
			
		||||
        "{\"query\":\"mutation CreatePost($input: PostInput!) { createPost(input: $input) { id title } }\",\"variables\":{\"input\":{\"title\":\"{{ fakeit_Sentence 5 }}\",\"content\":\"{{ fakeit_Paragraph 2 3 5 \\\" \\\"}}\",\"authorId\":\"{{ fakeit_UUID }}\"}}}"
 | 
			
		||||
    ]
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## 10. WebSocket-style HTTP Testing
 | 
			
		||||
 | 
			
		||||
Test real-time applications with WebSocket-like HTTP endpoints.
 | 
			
		||||
 | 
			
		||||
### CLI Usage
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
dodo -u https://api.realtime-app.com/events \
 | 
			
		||||
     -m POST \
 | 
			
		||||
     -d 15 \
 | 
			
		||||
     -r 1000 \
 | 
			
		||||
     -t 5s \
 | 
			
		||||
     -o 60s \
 | 
			
		||||
     --skip-verify=false \
 | 
			
		||||
     -H "Content-Type:application/json" \
 | 
			
		||||
     -H "X-Event-Type:{{ fakeit_LetterNN 4 12 }}" \
 | 
			
		||||
     -H "Connection:keep-alive" \
 | 
			
		||||
     -H "Cache-Control:no-cache" \
 | 
			
		||||
     -c "connection_id={{ fakeit_UUID }}" \
 | 
			
		||||
     -c "session_token={{ fakeit_UUID }}" \
 | 
			
		||||
     -p "channel=general" -p "channel=notifications" -p "channel=alerts" -p "channel=updates" \
 | 
			
		||||
     -p "version=v1" -p "version=v2" \
 | 
			
		||||
     -b '{"event":"{{ fakeit_Word }}","data":{"timestamp":"{{ fakeit_Date }}","user_id":"{{ fakeit_UUID }}","message":"{{ fakeit_Sentence 8 }}"}}' \
 | 
			
		||||
     -b '{"event":"ping","data":{"timestamp":"{{ fakeit_Date }}","client_id":"{{ fakeit_UUID }}"}}' \
 | 
			
		||||
     -b '{"event":"status_update","data":{"status":"{{ fakeit_Word }}","user_id":"{{ fakeit_UUID }}","timestamp":"{{ fakeit_Date }}"}}' \
 | 
			
		||||
     -y
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### YAML Configuration
 | 
			
		||||
 | 
			
		||||
```yaml
 | 
			
		||||
method: "POST"
 | 
			
		||||
url: "https://api.realtime-app.com/events"
 | 
			
		||||
yes: true
 | 
			
		||||
timeout: "5s"
 | 
			
		||||
dodos: 15
 | 
			
		||||
requests: 1000
 | 
			
		||||
duration: "60s"
 | 
			
		||||
skip_verify: false
 | 
			
		||||
 | 
			
		||||
headers:
 | 
			
		||||
    - Content-Type: "application/json"
 | 
			
		||||
    - X-Event-Type: "{{ fakeit_LetterNN 4 12 }}"
 | 
			
		||||
    - Connection: "keep-alive"
 | 
			
		||||
    - Cache-Control: "no-cache"
 | 
			
		||||
 | 
			
		||||
cookies:
 | 
			
		||||
    - connection_id: "{{ fakeit_UUID }}"
 | 
			
		||||
    - session_token: "{{ fakeit_UUID }}"
 | 
			
		||||
 | 
			
		||||
params:
 | 
			
		||||
    - channel: ["general", "notifications", "alerts", "updates"]
 | 
			
		||||
    - version: ["v1", "v2"]
 | 
			
		||||
 | 
			
		||||
body:
 | 
			
		||||
    - '{"event":"{{ fakeit_Word }}","data":{"timestamp":"{{ fakeit_Date }}","user_id":"{{ fakeit_UUID }}","message":"{{ fakeit_Sentence 8 }}"}}'
 | 
			
		||||
    - '{"event":"ping","data":{"timestamp":"{{ fakeit_Date }}","client_id":"{{ fakeit_UUID }}"}}'
 | 
			
		||||
    - '{"event":"status_update","data":{"status":"{{ fakeit_Word }}","user_id":"{{ fakeit_UUID }}","timestamp":"{{ fakeit_Date }}"}}'
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### JSON Configuration
 | 
			
		||||
 | 
			
		||||
```json
 | 
			
		||||
{
 | 
			
		||||
    "method": "POST",
 | 
			
		||||
    "url": "https://api.realtime-app.com/events",
 | 
			
		||||
    "yes": true,
 | 
			
		||||
    "timeout": "5s",
 | 
			
		||||
    "dodos": 15,
 | 
			
		||||
    "requests": 1000,
 | 
			
		||||
    "duration": "60s",
 | 
			
		||||
    "skip_verify": false,
 | 
			
		||||
    "headers": [
 | 
			
		||||
        { "Content-Type": "application/json" },
 | 
			
		||||
        { "X-Event-Type": "{{ fakeit_LetterNN 4 12 }}" },
 | 
			
		||||
        { "Connection": "keep-alive" },
 | 
			
		||||
        { "Cache-Control": "no-cache" }
 | 
			
		||||
    ],
 | 
			
		||||
    "cookies": [
 | 
			
		||||
        { "connection_id": "{{ fakeit_UUID }}" },
 | 
			
		||||
        { "session_token": "{{ fakeit_UUID }}" }
 | 
			
		||||
    ],
 | 
			
		||||
    "params": [
 | 
			
		||||
        { "channel": ["general", "notifications", "alerts", "updates"] },
 | 
			
		||||
        { "version": ["v1", "v2"] }
 | 
			
		||||
    ],
 | 
			
		||||
    "body": [
 | 
			
		||||
        "{\"event\":\"{{ fakeit_Word }}\",\"data\":{\"timestamp\":\"{{ fakeit_Date }}\",\"user_id\":\"{{ fakeit_UUID }}\",\"message\":\"{{ fakeit_Sentence 8 }}\"}}",
 | 
			
		||||
        "{\"event\":\"ping\",\"data\":{\"timestamp\":\"{{ fakeit_Date }}\",\"client_id\":\"{{ fakeit_UUID }}\"}}",
 | 
			
		||||
        "{\"event\":\"status_update\",\"data\":{\"status\":\"{{ fakeit_Word }}\",\"user_id\":\"{{ fakeit_UUID }}\",\"timestamp\":\"{{ fakeit_Date }}\"}}"
 | 
			
		||||
    ]
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## 11. Multi-tenant Application Testing
 | 
			
		||||
 | 
			
		||||
Test SaaS applications with tenant-specific configurations.
 | 
			
		||||
 | 
			
		||||
### CLI Usage
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
dodo -u https://app.saas-platform.com/api/data \
 | 
			
		||||
     -m GET \
 | 
			
		||||
     -d 12 \
 | 
			
		||||
     -r 600 \
 | 
			
		||||
     -t 15s \
 | 
			
		||||
     --skip-verify=false \
 | 
			
		||||
     -H "X-Tenant-ID:{{ fakeit_UUID }}" \
 | 
			
		||||
     -H "Authorization:Bearer {{ fakeit_LetterN 64 }}" \
 | 
			
		||||
     -H "X-Client-Type:web" -H "X-Client-Type:mobile" -H "X-Client-Type:api" \
 | 
			
		||||
     -H "Accept:application/json" \
 | 
			
		||||
     -c "tenant_session={{ fakeit_UUID }}" \
 | 
			
		||||
     -c "user_role=admin" -c "user_role=user" -c "user_role=viewer" \
 | 
			
		||||
     -c "subscription_tier=free" -c "subscription_tier=pro" -c "subscription_tier=enterprise" \
 | 
			
		||||
     -p "page={{ fakeit_IntRange 1 10 }}" \
 | 
			
		||||
     -p "limit={{ fakeit_IntRange 10 100 }}" \
 | 
			
		||||
     -p "sort=created_at" -p "sort=updated_at" -p "sort=name" \
 | 
			
		||||
     -p "order=asc" -p "order=desc" \
 | 
			
		||||
     -p "filter_by=active" -p "filter_by=inactive" -p "filter_by=pending" \
 | 
			
		||||
     -y
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### YAML Configuration
 | 
			
		||||
 | 
			
		||||
```yaml
 | 
			
		||||
method: "GET"
 | 
			
		||||
url: "https://app.saas-platform.com/api/data"
 | 
			
		||||
yes: true
 | 
			
		||||
timeout: "15s"
 | 
			
		||||
dodos: 12
 | 
			
		||||
requests: 600
 | 
			
		||||
skip_verify: false
 | 
			
		||||
 | 
			
		||||
headers:
 | 
			
		||||
    - X-Tenant-ID: "{{ fakeit_UUID }}"
 | 
			
		||||
    - Authorization: "Bearer {{ fakeit_LetterN 64 }}"
 | 
			
		||||
    - X-Client-Type: ["web", "mobile", "api"]
 | 
			
		||||
    - Accept: "application/json"
 | 
			
		||||
 | 
			
		||||
cookies:
 | 
			
		||||
    - tenant_session: "{{ fakeit_UUID }}"
 | 
			
		||||
    - user_role: ["admin", "user", "viewer"]
 | 
			
		||||
    - subscription_tier: ["free", "pro", "enterprise"]
 | 
			
		||||
 | 
			
		||||
params:
 | 
			
		||||
    - page: "{{ fakeit_IntRange 1 10 }}"
 | 
			
		||||
    - limit: "{{ fakeit_IntRange 10 100 }}"
 | 
			
		||||
    - sort: ["created_at", "updated_at", "name"]
 | 
			
		||||
    - order: ["asc", "desc"]
 | 
			
		||||
    - filter_by: ["active", "inactive", "pending"]
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### JSON Configuration
 | 
			
		||||
 | 
			
		||||
```json
 | 
			
		||||
{
 | 
			
		||||
    "method": "GET",
 | 
			
		||||
    "url": "https://app.saas-platform.com/api/data",
 | 
			
		||||
    "yes": true,
 | 
			
		||||
    "timeout": "15s",
 | 
			
		||||
    "dodos": 12,
 | 
			
		||||
    "requests": 600,
 | 
			
		||||
    "skip_verify": false,
 | 
			
		||||
    "headers": [
 | 
			
		||||
        { "X-Tenant-ID": "{{ fakeit_UUID }}" },
 | 
			
		||||
        { "Authorization": "Bearer {{ fakeit_LetterN 64 }}" },
 | 
			
		||||
        { "X-Client-Type": ["web", "mobile", "api"] },
 | 
			
		||||
        { "Accept": "application/json" }
 | 
			
		||||
    ],
 | 
			
		||||
    "cookies": [
 | 
			
		||||
        { "tenant_session": "{{ fakeit_UUID }}" },
 | 
			
		||||
        { "user_role": ["admin", "user", "viewer"] },
 | 
			
		||||
        { "subscription_tier": ["free", "pro", "enterprise"] }
 | 
			
		||||
    ],
 | 
			
		||||
    "params": [
 | 
			
		||||
        { "page": "{{ fakeit_IntRange 1 10 }}" },
 | 
			
		||||
        { "limit": "{{ fakeit_IntRange 10 100 }}" },
 | 
			
		||||
        { "sort": ["created_at", "updated_at", "name"] },
 | 
			
		||||
        { "order": ["asc", "desc"] },
 | 
			
		||||
        { "filter_by": ["active", "inactive", "pending"] }
 | 
			
		||||
    ]
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## 12. Rate Limiting Testing
 | 
			
		||||
 | 
			
		||||
Test API rate limits and throttling mechanisms.
 | 
			
		||||
 | 
			
		||||
### CLI Usage
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
dodo -u https://api.rate-limited.com/endpoint \
 | 
			
		||||
     -m GET \
 | 
			
		||||
     -d 20 \
 | 
			
		||||
     -r 2000 \
 | 
			
		||||
     -t 3s \
 | 
			
		||||
     -o 120s \
 | 
			
		||||
     --skip-verify=false \
 | 
			
		||||
     -H "X-API-Key:{{ fakeit_UUID }}" \
 | 
			
		||||
     -H "X-Client-ID:{{ fakeit_UUID }}" \
 | 
			
		||||
     -H "X-Rate-Limit-Test:true" \
 | 
			
		||||
     -H "User-Agent:{{ fakeit_UserAgent }}" \
 | 
			
		||||
     -c "rate_limit_bucket={{ fakeit_UUID }}" \
 | 
			
		||||
     -c "client_tier=tier1" -c "client_tier=tier2" -c "client_tier=tier3" \
 | 
			
		||||
     -p "burst_test=true" \
 | 
			
		||||
     -p "client_type=premium" -p "client_type=standard" -p "client_type=free" \
 | 
			
		||||
     -p "request_id={{ fakeit_UUID }}" \
 | 
			
		||||
     -y
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### YAML Configuration
 | 
			
		||||
 | 
			
		||||
```yaml
 | 
			
		||||
method: "GET"
 | 
			
		||||
url: "https://api.rate-limited.com/endpoint"
 | 
			
		||||
yes: true
 | 
			
		||||
timeout: "3s"
 | 
			
		||||
dodos: 20
 | 
			
		||||
requests: 2000
 | 
			
		||||
duration: "120s"
 | 
			
		||||
skip_verify: false
 | 
			
		||||
 | 
			
		||||
headers:
 | 
			
		||||
    - X-API-Key: "{{ fakeit_UUID }}"
 | 
			
		||||
    - X-Client-ID: "{{ fakeit_UUID }}"
 | 
			
		||||
    - X-Rate-Limit-Test: "true"
 | 
			
		||||
    - User-Agent: "{{ fakeit_UserAgent }}"
 | 
			
		||||
 | 
			
		||||
params:
 | 
			
		||||
    - burst_test: "true"
 | 
			
		||||
    - client_type: ["premium", "standard", "free"]
 | 
			
		||||
    - request_id: "{{ fakeit_UUID }}"
 | 
			
		||||
 | 
			
		||||
cookies:
 | 
			
		||||
    - rate_limit_bucket: "{{ fakeit_UUID }}"
 | 
			
		||||
    - client_tier: ["tier1", "tier2", "tier3"]
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### JSON Configuration
 | 
			
		||||
 | 
			
		||||
```json
 | 
			
		||||
{
 | 
			
		||||
    "method": "GET",
 | 
			
		||||
    "url": "https://api.rate-limited.com/endpoint",
 | 
			
		||||
    "yes": true,
 | 
			
		||||
    "timeout": "3s",
 | 
			
		||||
    "dodos": 20,
 | 
			
		||||
    "requests": 2000,
 | 
			
		||||
    "duration": "120s",
 | 
			
		||||
    "skip_verify": false,
 | 
			
		||||
    "headers": [
 | 
			
		||||
        { "X-API-Key": "{{ fakeit_UUID }}" },
 | 
			
		||||
        { "X-Client-ID": "{{ fakeit_UUID }}" },
 | 
			
		||||
        { "X-Rate-Limit-Test": "true" },
 | 
			
		||||
        { "User-Agent": "{{ fakeit_UserAgent }}" }
 | 
			
		||||
    ],
 | 
			
		||||
    "params": [
 | 
			
		||||
        { "burst_test": "true" },
 | 
			
		||||
        { "client_type": ["premium", "standard", "free"] },
 | 
			
		||||
        { "request_id": "{{ fakeit_UUID }}" }
 | 
			
		||||
    ],
 | 
			
		||||
    "cookies": [
 | 
			
		||||
        { "rate_limit_bucket": "{{ fakeit_UUID }}" },
 | 
			
		||||
        { "client_tier": ["tier1", "tier2", "tier3"] }
 | 
			
		||||
    ]
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Notes
 | 
			
		||||
 | 
			
		||||
- All examples use template functions for dynamic data generation
 | 
			
		||||
- Adjust `dodos`, `requests`, `duration`, and `timeout` values based on your testing requirements
 | 
			
		||||
- Use `skip_verify: true` for testing with self-signed certificates
 | 
			
		||||
- Set `yes: true` to skip confirmation prompts in automated testing
 | 
			
		||||
- Template functions like `{{ fakeit_* }}` generate random realistic data for each request
 | 
			
		||||
- Multiple values in arrays (e.g., `["value1", "value2"]`) will be randomly selected per request
 | 
			
		||||
- Use the `body_FormData` function for multipart form uploads
 | 
			
		||||
- Proxy configurations support HTTP, SOCKS5, and SOCKS5H protocols
 | 
			
		||||
 | 
			
		||||
For more template functions and advanced configuration options, refer to the main documentation and `utils/templates.go`.
 | 
			
		||||
							
								
								
									
										9
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										9
									
								
								Makefile
									
									
									
									
									
								
							@@ -1,9 +0,0 @@
 | 
			
		||||
lint:
 | 
			
		||||
	golangci-lint run
 | 
			
		||||
 | 
			
		||||
build:
 | 
			
		||||
	go build -ldflags "-s -w" -o "./dodo"
 | 
			
		||||
 | 
			
		||||
build-all:
 | 
			
		||||
	rm -rf ./binaries
 | 
			
		||||
	./build.sh
 | 
			
		||||
							
								
								
									
										268
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										268
									
								
								README.md
									
									
									
									
									
								
							@@ -1,80 +1,176 @@
 | 
			
		||||
<h1 align="center">Dodo - A Fast and Easy-to-Use HTTP Benchmarking Tool</h1>
 | 
			
		||||
<p align="center">
 | 
			
		||||
<img width="30%" height="30%" src="https://ftp.aykhans.me/web/client/pubshares/VzPtSHS7yPQT7ngoZzZSNU/browse?path=%2Fdodo.png">
 | 
			
		||||
</p>
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
<div align="center">
 | 
			
		||||
  <h4>
 | 
			
		||||
      <a href="./EXAMPLES.md">
 | 
			
		||||
          Examples
 | 
			
		||||
      </a>
 | 
			
		||||
      <span> | </span>
 | 
			
		||||
      <a href="#installation">
 | 
			
		||||
          Install
 | 
			
		||||
      </a>
 | 
			
		||||
      <span> | </span>
 | 
			
		||||
      <a href="https://hub.docker.com/r/aykhans/dodo">
 | 
			
		||||
          Docker
 | 
			
		||||
      </a>
 | 
			
		||||
  </h4>
 | 
			
		||||
  <br>
 | 
			
		||||
    <a href="https://coff.ee/aykhan">
 | 
			
		||||
        <img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 40px !important;width: 150px !important;">
 | 
			
		||||
    </a>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
## Table of Contents
 | 
			
		||||
 | 
			
		||||
- [Installation](#installation)
 | 
			
		||||
    - [Using Docker (Recommended)](#using-docker-recommended)
 | 
			
		||||
    - [Using Pre-built Binaries](#using-pre-built-binaries)
 | 
			
		||||
    - [Building from Source](#building-from-source)
 | 
			
		||||
- [Usage](#usage)
 | 
			
		||||
    - [1. CLI Usage](#1-cli-usage)
 | 
			
		||||
    - [2. Config File Usage](#2-config-file-usage)
 | 
			
		||||
        - [2.1 YAML/YML Example](#21-yamlyml-example)
 | 
			
		||||
        - [2.2 JSON Example](#22-json-example)
 | 
			
		||||
    - [3. CLI & Config File Combination](#3-cli--config-file-combination)
 | 
			
		||||
- [Config Parameters Reference](#config-parameters-reference)
 | 
			
		||||
- [Template Functions](#template-functions)
 | 
			
		||||
 | 
			
		||||
## Installation
 | 
			
		||||
 | 
			
		||||
### Using Docker (Recommended)
 | 
			
		||||
 | 
			
		||||
Pull the Dodo image from Docker Hub:
 | 
			
		||||
Pull the latest Dodo image from Docker Hub:
 | 
			
		||||
 | 
			
		||||
```sh
 | 
			
		||||
docker pull aykhans/dodo:latest
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
When using Dodo with Docker and a local config file, you must provide the config.json file as a volume to the Docker run command (not as the "-f config.json" argument):
 | 
			
		||||
To use Dodo with Docker and a local config file, mount the config file as a volume and pass it as an argument:
 | 
			
		||||
 | 
			
		||||
```sh
 | 
			
		||||
docker run -v /path/to/config.json:/config.json aykhans/dodo
 | 
			
		||||
docker run -v /path/to/config.json:/config.json aykhans/dodo -f /config.json
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
If you're using Dodo with Docker and providing a config file via URL, you don't need to set a volume:
 | 
			
		||||
If you're using a remote config file via URL, you don't need to mount a volume:
 | 
			
		||||
 | 
			
		||||
```sh
 | 
			
		||||
docker run aykhans/dodo -f https://raw.githubusercontent.com/aykhans/dodo/main/config.json
 | 
			
		||||
docker run aykhans/dodo -f https://raw.githubusercontent.com/aykhans/dodo/main/config.yaml
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Using Binary Files
 | 
			
		||||
### Using Pre-built Binaries
 | 
			
		||||
 | 
			
		||||
You can download pre-built binaries from the [releases](https://github.com/aykhans/dodo/releases) section.
 | 
			
		||||
Download the latest binaries from the [releases](https://github.com/aykhans/dodo/releases) section.
 | 
			
		||||
 | 
			
		||||
### Building from Source
 | 
			
		||||
 | 
			
		||||
To build Dodo from source, you need to have [Go 1.24+](https://golang.org/dl/) installed.
 | 
			
		||||
Follow these steps:
 | 
			
		||||
To build Dodo from source, ensure you have [Go 1.24+](https://golang.org/dl/) installed.
 | 
			
		||||
 | 
			
		||||
1. **Clone the repository:**
 | 
			
		||||
 | 
			
		||||
    ```sh
 | 
			
		||||
    git clone https://github.com/aykhans/dodo.git
 | 
			
		||||
    ```
 | 
			
		||||
 | 
			
		||||
2. **Navigate to the project directory:**
 | 
			
		||||
 | 
			
		||||
    ```sh
 | 
			
		||||
    cd dodo
 | 
			
		||||
    ```
 | 
			
		||||
 | 
			
		||||
3. **Build the project:**
 | 
			
		||||
 | 
			
		||||
    ```sh
 | 
			
		||||
    go build -ldflags "-s -w" -o dodo
 | 
			
		||||
    ```
 | 
			
		||||
 | 
			
		||||
This will generate an executable named `dodo` in the project directory.
 | 
			
		||||
```sh
 | 
			
		||||
go install -ldflags "-s -w" github.com/aykhans/dodo@latest
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Usage
 | 
			
		||||
 | 
			
		||||
You can use Dodo with CLI arguments, a JSON config file, or both. When using both, CLI arguments will override JSON config values if there's a conflict.
 | 
			
		||||
Dodo supports CLI arguments, configuration files (JSON/YAML), or a combination of both. If both are used, CLI arguments take precedence.
 | 
			
		||||
 | 
			
		||||
### 1. CLI
 | 
			
		||||
### 1. CLI Usage
 | 
			
		||||
 | 
			
		||||
Send 1000 GET requests to https://example.com with 10 parallel dodos (threads) and a timeout of 2 seconds:
 | 
			
		||||
Send 1000 GET requests to https://example.com with 10 parallel dodos (threads), each with a timeout of 2 seconds, within a maximum duration of 1 minute:
 | 
			
		||||
 | 
			
		||||
```sh
 | 
			
		||||
dodo -u https://example.com -m GET -d 10 -r 1000 -t 2s
 | 
			
		||||
dodo -u https://example.com -m GET -d 10 -r 1000 -o 1m -t 2s
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
With Docker:
 | 
			
		||||
 | 
			
		||||
```sh
 | 
			
		||||
docker run --rm -i aykhans/dodo -u https://example.com -m GET -d 10 -r 1000 -t 2s
 | 
			
		||||
docker run --rm -i aykhans/dodo -u https://example.com -m GET -d 10 -r 1000 -o 1m -t 2s
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 2. JSON Config File
 | 
			
		||||
### 2. Config File Usage
 | 
			
		||||
 | 
			
		||||
Send 1000 GET requests to https://example.com with 10 parallel dodos (threads) and a timeout of 800 milliseconds:
 | 
			
		||||
Send 1000 GET requests to https://example.com with 10 parallel dodos (threads), each with a timeout of 800 milliseconds, within a maximum duration of 250 seconds:
 | 
			
		||||
 | 
			
		||||
#### 2.1 YAML/YML Example
 | 
			
		||||
 | 
			
		||||
```yaml
 | 
			
		||||
method: "GET"
 | 
			
		||||
url: "https://example.com"
 | 
			
		||||
yes: false
 | 
			
		||||
timeout: "800ms"
 | 
			
		||||
dodos: 10
 | 
			
		||||
requests: 1000
 | 
			
		||||
duration: "250s"
 | 
			
		||||
skip_verify: false
 | 
			
		||||
 | 
			
		||||
params:
 | 
			
		||||
    # A random value will be selected from the list for first "key1" param on each request
 | 
			
		||||
    # And always "value" for second "key1" param on each request
 | 
			
		||||
    # e.g. "?key1=value2&key1=value"
 | 
			
		||||
    - key1: ["value1", "value2", "value3", "value4"]
 | 
			
		||||
    - key1: "value"
 | 
			
		||||
 | 
			
		||||
    # A random value will be selected from the list for param "key2" on each request
 | 
			
		||||
    # e.g. "?key2=value2"
 | 
			
		||||
    - key2: ["value1", "value2"]
 | 
			
		||||
 | 
			
		||||
headers:
 | 
			
		||||
    # A random value will be selected from the list for first "key1" header on each request
 | 
			
		||||
    # And always "value" for second "key1" header on each request
 | 
			
		||||
    # e.g. "key1: value3", "key1: value"
 | 
			
		||||
    - key1: ["value1", "value2", "value3", "value4"]
 | 
			
		||||
    - key1: "value"
 | 
			
		||||
 | 
			
		||||
    # A random value will be selected from the list for header "key2" on each request
 | 
			
		||||
    # e.g. "key2: value2"
 | 
			
		||||
    - key2: ["value1", "value2"]
 | 
			
		||||
 | 
			
		||||
cookies:
 | 
			
		||||
    # A random value will be selected from the list for first "key1" cookie on each request
 | 
			
		||||
    # And always "value" for second "key1" cookie on each request
 | 
			
		||||
    # e.g. "key1=value4; key1=value"
 | 
			
		||||
    - key1: ["value1", "value2", "value3", "value4"]
 | 
			
		||||
    - key1: "value"
 | 
			
		||||
 | 
			
		||||
    # A random value will be selected from the list for cookie "key2" on each request
 | 
			
		||||
    # e.g. "key2=value1"
 | 
			
		||||
    - key2: ["value1", "value2"]
 | 
			
		||||
 | 
			
		||||
body: "body-text"
 | 
			
		||||
# OR
 | 
			
		||||
# A random body value will be selected from the list for each request
 | 
			
		||||
body:
 | 
			
		||||
    - "body-text1"
 | 
			
		||||
    - "body-text2"
 | 
			
		||||
    - "body-text3"
 | 
			
		||||
 | 
			
		||||
proxy: "http://example.com:8080"
 | 
			
		||||
# OR
 | 
			
		||||
# A random proxy will be selected from the list for each request
 | 
			
		||||
proxy:
 | 
			
		||||
    - "http://example.com:8080"
 | 
			
		||||
    - "http://username:password@example.com:8080"
 | 
			
		||||
    - "socks5://example.com:8080"
 | 
			
		||||
    - "socks5h://example.com:8080"
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
```sh
 | 
			
		||||
dodo -f /path/config.yaml
 | 
			
		||||
# OR
 | 
			
		||||
dodo -f https://example.com/config.yaml
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
With Docker:
 | 
			
		||||
 | 
			
		||||
```sh
 | 
			
		||||
docker run --rm -i -v /path/to/config.yaml:/config.yaml aykhans/dodo -f /config.yaml
 | 
			
		||||
# OR
 | 
			
		||||
docker run --rm -i aykhans/dodo -f https://example.com/config.yaml
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
#### 2.2 JSON Example
 | 
			
		||||
 | 
			
		||||
```jsonc
 | 
			
		||||
{
 | 
			
		||||
@@ -84,6 +180,8 @@ Send 1000 GET requests to https://example.com with 10 parallel dodos (threads) a
 | 
			
		||||
    "timeout": "800ms",
 | 
			
		||||
    "dodos": 10,
 | 
			
		||||
    "requests": 1000,
 | 
			
		||||
    "duration": "250s",
 | 
			
		||||
    "skip_verify": false,
 | 
			
		||||
 | 
			
		||||
    "params": [
 | 
			
		||||
        // A random value will be selected from the list for first "key1" param on each request
 | 
			
		||||
@@ -152,35 +250,91 @@ docker run --rm -i -v /path/to/config.json:/config.json aykhans/dodo
 | 
			
		||||
docker run --rm -i aykhans/dodo -f https://example.com/config.json
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 3. Combined (CLI & JSON)
 | 
			
		||||
### 3. CLI & Config File Combination
 | 
			
		||||
 | 
			
		||||
Override the config file arguments with CLI arguments:
 | 
			
		||||
CLI arguments override config file values:
 | 
			
		||||
 | 
			
		||||
```sh
 | 
			
		||||
dodo -f /path/to/config.json -u https://example.com -m GET -d 10 -r 1000 -t 5s
 | 
			
		||||
dodo -f /path/to/config.yaml -u https://example.com -m GET -d 10 -r 1000 -o 1m -t 5s
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
With Docker:
 | 
			
		||||
 | 
			
		||||
```sh
 | 
			
		||||
docker run --rm -i -v /path/to/config.json:/config.json aykhans/dodo -u https://example.com -m GET -d 10 -r 1000 -t 5s
 | 
			
		||||
docker run --rm -i -v /path/to/config.json:/config.json aykhans/dodo -f /config.json -u https://example.com -m GET -d 10 -r 1000 -o 1m -t 5s
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## CLI and JSON Config Parameters
 | 
			
		||||
You can find more usage examples in the [EXAMPLES.md](./EXAMPLES.md) file.
 | 
			
		||||
 | 
			
		||||
## Config Parameters Reference
 | 
			
		||||
 | 
			
		||||
If `Headers`, `Params`, `Cookies`, `Body`, or `Proxy` fields have multiple values, each request will choose a random value from the list.
 | 
			
		||||
 | 
			
		||||
| Parameter       | JSON config file | CLI Flag     | CLI Short Flag | Type                           | Description                                                     | Default |
 | 
			
		||||
| --------------- | ---------------- | ------------ | -------------- | ------------------------------ | --------------------------------------------------------------- | ------- |
 | 
			
		||||
| Config file     | -                | -config-file | -f             | String                         | Path to local config file or http(s) URL of the config file     | -       |
 | 
			
		||||
| Yes             | yes              | -yes         | -y             | Boolean                        | Answer yes to all questions                                     | false   |
 | 
			
		||||
| URL             | url              | -url         | -u             | String                         | URL to send the request to                                      | -       |
 | 
			
		||||
| Method          | method           | -method      | -m             | String                         | HTTP method                                                     | GET     |
 | 
			
		||||
| Requests        | requests         | -requests    | -r             | UnsignedInteger                | Total number of requests to send                                | 1000    |
 | 
			
		||||
| Dodos (Threads) | dodos            | -dodos       | -d             | UnsignedInteger                | Number of dodos (threads) to send requests in parallel          | 1       |
 | 
			
		||||
| Timeout         | timeout          | -timeout     | -t             | Duration                       | Timeout for canceling each request                              | 10s     |
 | 
			
		||||
| Params          | params           | -param       | -p             | [{String: String OR [String]}] | Request parameters                                              | -       |
 | 
			
		||||
| Headers         | headers          | -header      | -H             | [{String: String OR [String]}] | Request headers                                                 | -       |
 | 
			
		||||
| Cookies         | cookies          | -cookie      | -c             | [{String: String OR [String]}] | Request cookies                                                 | -       |
 | 
			
		||||
| Body            | body             | -body        | -b             | String OR [String]             | Request body or list of request bodies                          | -       |
 | 
			
		||||
| Proxy           | proxies          | -proxy       | -x             | String OR [String]             | Proxy URL or list of proxy URLs                                 | -       |
 | 
			
		||||
| Parameter       | config file | CLI Flag     | CLI Short Flag | Type                           | Description                                                 | Default |
 | 
			
		||||
| --------------- | ----------- | ------------ | -------------- | ------------------------------ | ----------------------------------------------------------- | ------- |
 | 
			
		||||
| Config file     |             | -config-file | -f             | String                         | Path to local config file or http(s) URL of the config file | -       |
 | 
			
		||||
| Yes             | yes         | -yes         | -y             | Boolean                        | Answer yes to all questions                                 | false   |
 | 
			
		||||
| URL             | url         | -url         | -u             | String                         | URL to send the request to                                  | -       |
 | 
			
		||||
| Method          | method      | -method      | -m             | String                         | HTTP method                                                 | GET     |
 | 
			
		||||
| Dodos (Threads) | dodos       | -dodos       | -d             | UnsignedInteger                | Number of dodos (threads) to send requests in parallel      | 1       |
 | 
			
		||||
| Requests        | requests    | -requests    | -r             | UnsignedInteger                | Total number of requests to send                            | -       |
 | 
			
		||||
| Duration        | duration    | -duration    | -o             | Time                           | Maximum duration for the test                               | -       |
 | 
			
		||||
| Timeout         | timeout     | -timeout     | -t             | Time                           | Timeout for canceling each request                          | 10s     |
 | 
			
		||||
| Params          | params      | -param       | -p             | [{String: String OR [String]}] | Request parameters                                          | -       |
 | 
			
		||||
| Headers         | headers     | -header      | -H             | [{String: String OR [String]}] | Request headers                                             | -       |
 | 
			
		||||
| Cookies         | cookies     | -cookie      | -c             | [{String: String OR [String]}] | Request cookies                                             | -       |
 | 
			
		||||
| Body            | body        | -body        | -b             | String OR [String]             | Request body or list of request bodies                      | -       |
 | 
			
		||||
| Proxy           | proxies     | -proxy       | -x             | String OR [String]             | Proxy URL or list of proxy URLs                             | -       |
 | 
			
		||||
| Skip Verify     | skip_verify | -skip-verify |                | Boolean                        | Skip SSL/TLS certificate verification                       | false   |
 | 
			
		||||
 | 
			
		||||
## Template Functions
 | 
			
		||||
 | 
			
		||||
Dodo supports template functions in `Headers`, `Params`, `Cookies`, and `Body` fields. These functions allow you to generate dynamic values for each request.
 | 
			
		||||
 | 
			
		||||
You can use Go template syntax to include dynamic values in your requests. Here's how to use template functions:
 | 
			
		||||
 | 
			
		||||
In CLI config:
 | 
			
		||||
 | 
			
		||||
```sh
 | 
			
		||||
dodo -u https://example.com -r 1 \
 | 
			
		||||
    -header "User-Agent:{{ fakeit_UserAgent }}" \ # e.g. "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)"
 | 
			
		||||
    -param "username={{ strings_ToUpper fakeit_Username }}" \ # e.g. "username=JOHN BOB"
 | 
			
		||||
    -cookie "token={{ fakeit_Password true true true true true 10 }}" \ # e.g. token=1234567890abcdef1234567890abcdef
 | 
			
		||||
    -body '{"email":"{{ fakeit_Email }}", "password":"{{ fakeit_Password true true true true true 10 }}"}' # e.g. {"email":"john.doe@example.com", "password":"12rw4d-78d"}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
In YAML/YML config:
 | 
			
		||||
 | 
			
		||||
```yaml
 | 
			
		||||
headers:
 | 
			
		||||
    - User-Agent: "{{ fakeit_UserAgent }}" # e.g. "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)"
 | 
			
		||||
    - "Random-Header-{{fakeit_FirstName}}": "static_value" # e.g. "Random-Header-John: static_value"
 | 
			
		||||
 | 
			
		||||
cookies:
 | 
			
		||||
    - token: "Bearer {{ fakeit_UUID }}" # e.g. "token=Bearer 1234567890abcdef1234567890abcdef"
 | 
			
		||||
 | 
			
		||||
params:
 | 
			
		||||
    - id: "{{ fakeit_Uint }}" # e.g. "id=1234567890"
 | 
			
		||||
    - username: "{{ fakeit_Username }}" # e.g. "username=John Doe"
 | 
			
		||||
 | 
			
		||||
body:
 | 
			
		||||
    - '{ "username": "{{ fakeit_Username }}", "password": "{{ fakeit_Password }}" }' # e.g. { "username": "john.doe", "password": "password123" }
 | 
			
		||||
    - '{ "email": "{{ fakeit_Email }}", "phone": "{{ fakeit_Phone }}" }' # e.g. { "email": "john.doe@example.com", "phone": "1234567890" }
 | 
			
		||||
    - '{{ body_FormData (dict_Str "username" fakeit_Username "password" "secret123") }}' # Creates multipart form data for form submissions, automatically sets the appropriate Content-Type header.
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
In JSON config:
 | 
			
		||||
 | 
			
		||||
```jsonc
 | 
			
		||||
{
 | 
			
		||||
    "headers": [
 | 
			
		||||
        { "User-Agent": "{{ fakeit_UserAgent }}" }, // e.g. "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)"
 | 
			
		||||
    ],
 | 
			
		||||
    "body": [
 | 
			
		||||
        "{ \"username\": \"{{ strings_RemoveSpaces fakeit_Username }}\", \"password\": \"{{ fakeit_Password }}\" }", // e.g. { "username": "johndoe", "password": "password123" }
 | 
			
		||||
        "{{ body_FormData (dict_Str \"username\" fakeit_Username \"password\" \"12345\") }}", // Creates multipart form data for form submissions, automatically sets the appropriate Content-Type header.
 | 
			
		||||
    ],
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
For the full list of template functions over 200 functions, refer to the `NewFuncMap` function in `utils/templates.go`.
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										53
									
								
								Taskfile.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								Taskfile.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,53 @@
 | 
			
		||||
# https://taskfile.dev
 | 
			
		||||
 | 
			
		||||
version: "3"
 | 
			
		||||
 | 
			
		||||
vars:
 | 
			
		||||
    PLATFORMS:
 | 
			
		||||
        - os: darwin
 | 
			
		||||
          archs: [amd64, arm64]
 | 
			
		||||
        - os: freebsd
 | 
			
		||||
          archs: [386, amd64, arm]
 | 
			
		||||
        - os: linux
 | 
			
		||||
          archs: [386, amd64, arm, arm64]
 | 
			
		||||
        - os: netbsd
 | 
			
		||||
          archs: [386, amd64, arm]
 | 
			
		||||
        - os: openbsd
 | 
			
		||||
          archs: [386, amd64, arm, arm64]
 | 
			
		||||
        - os: windows
 | 
			
		||||
          archs: [386, amd64, arm64]
 | 
			
		||||
 | 
			
		||||
tasks:
 | 
			
		||||
    run: go run main.go
 | 
			
		||||
 | 
			
		||||
    ftl:
 | 
			
		||||
        cmds:
 | 
			
		||||
            - task: fmt
 | 
			
		||||
            - task: tidy
 | 
			
		||||
            - task: lint
 | 
			
		||||
 | 
			
		||||
    fmt: gofmt -w -d .
 | 
			
		||||
 | 
			
		||||
    tidy: go mod tidy
 | 
			
		||||
 | 
			
		||||
    lint: golangci-lint run
 | 
			
		||||
 | 
			
		||||
    build: CGO_ENABLED=0 go build -ldflags "-s -w" -o "dodo"
 | 
			
		||||
 | 
			
		||||
    build-all:
 | 
			
		||||
        silent: true
 | 
			
		||||
        cmds:
 | 
			
		||||
            - rm -rf binaries
 | 
			
		||||
            - |
 | 
			
		||||
                {{ $ext := "" }}
 | 
			
		||||
                {{- range $platform := .PLATFORMS }}
 | 
			
		||||
                    {{- if eq $platform.os "windows" }}
 | 
			
		||||
                        {{ $ext = ".exe" }}
 | 
			
		||||
                    {{- end }}
 | 
			
		||||
 | 
			
		||||
                    {{- range $arch := $platform.archs }}
 | 
			
		||||
                        echo "Building for {{$platform.os}}/{{$arch}}"
 | 
			
		||||
                        GOOS={{$platform.os}} GOARCH={{$arch}} go build -ldflags "-s -w" -o "./binaries/dodo-{{$platform.os}}-{{$arch}}{{$ext}}"
 | 
			
		||||
                    {{- end }}
 | 
			
		||||
                {{- end }}
 | 
			
		||||
            - echo -e "\033[32m*** Build completed ***\033[0m"
 | 
			
		||||
							
								
								
									
										32
									
								
								build.sh
									
									
									
									
									
								
							
							
						
						
									
										32
									
								
								build.sh
									
									
									
									
									
								
							@@ -1,32 +0,0 @@
 | 
			
		||||
#!/bin/bash
 | 
			
		||||
 | 
			
		||||
platforms=(
 | 
			
		||||
    "darwin,amd64"
 | 
			
		||||
    "darwin,arm64"
 | 
			
		||||
    "freebsd,386"
 | 
			
		||||
    "freebsd,amd64"
 | 
			
		||||
    "freebsd,arm"
 | 
			
		||||
    "linux,386"
 | 
			
		||||
    "linux,amd64"
 | 
			
		||||
    "linux,arm"
 | 
			
		||||
    "linux,arm64"
 | 
			
		||||
    "netbsd,386"
 | 
			
		||||
    "netbsd,amd64"
 | 
			
		||||
    "netbsd,arm"
 | 
			
		||||
    "openbsd,386"
 | 
			
		||||
    "openbsd,amd64"
 | 
			
		||||
    "openbsd,arm"
 | 
			
		||||
    "openbsd,arm64"
 | 
			
		||||
    "windows,386"
 | 
			
		||||
    "windows,amd64"
 | 
			
		||||
    "windows,arm64"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
for platform in "${platforms[@]}"; do
 | 
			
		||||
    IFS=',' read -r build_os build_arch <<< "$platform"
 | 
			
		||||
    ext=""
 | 
			
		||||
    if [ "$build_os" == "windows" ]; then
 | 
			
		||||
        ext=".exe"
 | 
			
		||||
    fi
 | 
			
		||||
    GOOS="$build_os" GOARCH="$build_arch" go build -ldflags "-s -w" -o "./binaries/dodo-$build_os-$build_arch$ext"
 | 
			
		||||
done
 | 
			
		||||
@@ -5,6 +5,8 @@
 | 
			
		||||
    "timeout": "5s",
 | 
			
		||||
    "dodos": 8,
 | 
			
		||||
    "requests": 1000,
 | 
			
		||||
    "duration": "10s",
 | 
			
		||||
    "skip_verify": false,
 | 
			
		||||
 | 
			
		||||
    "params": [
 | 
			
		||||
        { "key1": ["value1", "value2", "value3", "value4"] },
 | 
			
		||||
@@ -32,4 +34,4 @@
 | 
			
		||||
        "socks5://example.com:8080",
 | 
			
		||||
        "socks5h://example.com:8080"
 | 
			
		||||
    ]
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,11 @@
 | 
			
		||||
# YAML/YML config file option is not implemented yet.
 | 
			
		||||
# This file is a example for future implementation.
 | 
			
		||||
 | 
			
		||||
method: "GET"
 | 
			
		||||
url: "https://example.com"
 | 
			
		||||
yes: false
 | 
			
		||||
timeout: "5s"
 | 
			
		||||
dodos: 10
 | 
			
		||||
dodos: 8
 | 
			
		||||
requests: 1000
 | 
			
		||||
duration: "10s"
 | 
			
		||||
skip_verify: false
 | 
			
		||||
 | 
			
		||||
params:
 | 
			
		||||
    - key1: ["value1", "value2", "value3", "value4"]
 | 
			
		||||
@@ -25,6 +24,7 @@ cookies:
 | 
			
		||||
 | 
			
		||||
# body: "body-text"
 | 
			
		||||
# OR
 | 
			
		||||
# A random body value will be selected from the list for each request
 | 
			
		||||
body:
 | 
			
		||||
    - "body-text1"
 | 
			
		||||
    - "body-text2"
 | 
			
		||||
@@ -32,6 +32,7 @@ body:
 | 
			
		||||
 | 
			
		||||
# proxy: "http://example.com:8080"
 | 
			
		||||
# OR
 | 
			
		||||
# A random proxy will be selected from the list for each request
 | 
			
		||||
proxy:
 | 
			
		||||
    - "http://example.com:8080"
 | 
			
		||||
    - "http://username:password@example.com:8080"
 | 
			
		||||
 
 | 
			
		||||
@@ -16,8 +16,8 @@ const cliUsageText = `Usage:
 | 
			
		||||
 | 
			
		||||
Examples:
 | 
			
		||||
 | 
			
		||||
Simple usage only with URL:
 | 
			
		||||
  dodo -u https://example.com
 | 
			
		||||
Simple usage:
 | 
			
		||||
  dodo -u https://example.com -o 1m
 | 
			
		||||
 | 
			
		||||
Usage with config file:
 | 
			
		||||
  dodo -f /path/to/config/file/config.json
 | 
			
		||||
@@ -25,13 +25,13 @@ Usage with config file:
 | 
			
		||||
Usage with all flags:
 | 
			
		||||
  dodo -f /path/to/config/file/config.json \
 | 
			
		||||
    -u https://example.com -m POST \
 | 
			
		||||
    -d 10 -r 1000 -t 3s \
 | 
			
		||||
    -d 10 -r 1000 -o 3m -t 3s \
 | 
			
		||||
    -b "body1" -body "body2" \
 | 
			
		||||
    -H "header1:value1" -header "header2:value2" \
 | 
			
		||||
    -p "param1=value1" -param "param2=value2" \
 | 
			
		||||
    -c "cookie1=value1" -cookie "cookie2=value2" \
 | 
			
		||||
    -x "http://proxy.example.com:8080" -proxy "socks5://proxy2.example.com:8080" \
 | 
			
		||||
    -y
 | 
			
		||||
    -skip-verify -y
 | 
			
		||||
 | 
			
		||||
Flags:
 | 
			
		||||
  -h, -help                   help for dodo
 | 
			
		||||
@@ -39,15 +39,17 @@ Flags:
 | 
			
		||||
  -y, -yes          bool      Answer yes to all questions (default %v)
 | 
			
		||||
  -f, -config-file  string    Path to the local config file or http(s) URL of the config file
 | 
			
		||||
  -d, -dodos        uint      Number of dodos(threads) (default %d)
 | 
			
		||||
  -r, -requests     uint      Number of total requests (default %d)
 | 
			
		||||
  -t, -timeout      Duration  Timeout for each request (e.g. 400ms, 15s, 1m10s) (default %v)
 | 
			
		||||
  -r, -requests     uint      Number of total requests
 | 
			
		||||
  -o, -duration     Time      Maximum duration for the test (e.g. 30s, 1m, 5h)
 | 
			
		||||
  -t, -timeout      Time      Timeout for each request (e.g. 400ms, 15s, 1m10s) (default %v)
 | 
			
		||||
  -u, -url          string    URL for stress testing
 | 
			
		||||
  -m, -method       string    HTTP Method for the request (default %s)
 | 
			
		||||
  -b, -body         [string]  Body for the request (e.g. "body text")
 | 
			
		||||
  -p, -param        [string]  Parameter for the request (e.g. "key1=value1")
 | 
			
		||||
  -H, -header       [string]  Header for the request (e.g. "key1: value1")
 | 
			
		||||
  -H, -header       [string]  Header for the request (e.g. "key1:value1")
 | 
			
		||||
  -c, -cookie       [string]  Cookie for the request (e.g. "key1=value1")
 | 
			
		||||
  -x, -proxy        [string]  Proxy for the request (e.g. "http://proxy.example.com:8080")`
 | 
			
		||||
  -x, -proxy        [string]  Proxy for the request (e.g. "http://proxy.example.com:8080")
 | 
			
		||||
  -skip-verify      bool      Skip SSL/TLS certificate verification (default %v)`
 | 
			
		||||
 | 
			
		||||
func (config *Config) ReadCLI() (types.ConfigFile, error) {
 | 
			
		||||
	flag.Usage = func() {
 | 
			
		||||
@@ -55,9 +57,9 @@ func (config *Config) ReadCLI() (types.ConfigFile, error) {
 | 
			
		||||
			cliUsageText+"\n",
 | 
			
		||||
			DefaultYes,
 | 
			
		||||
			DefaultDodosCount,
 | 
			
		||||
			DefaultRequestCount,
 | 
			
		||||
			DefaultTimeout,
 | 
			
		||||
			DefaultMethod,
 | 
			
		||||
			DefaultSkipVerify,
 | 
			
		||||
		)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -65,11 +67,13 @@ func (config *Config) ReadCLI() (types.ConfigFile, error) {
 | 
			
		||||
		version      = false
 | 
			
		||||
		configFile   = ""
 | 
			
		||||
		yes          = false
 | 
			
		||||
		skipVerify   = false
 | 
			
		||||
		method       = ""
 | 
			
		||||
		url          types.RequestURL
 | 
			
		||||
		dodosCount   = uint(0)
 | 
			
		||||
		requestCount = uint(0)
 | 
			
		||||
		timeout      time.Duration
 | 
			
		||||
		duration     time.Duration
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	{
 | 
			
		||||
@@ -82,6 +86,8 @@ func (config *Config) ReadCLI() (types.ConfigFile, error) {
 | 
			
		||||
		flag.BoolVar(&yes, "yes", false, "Answer yes to all questions")
 | 
			
		||||
		flag.BoolVar(&yes, "y", false, "Answer yes to all questions")
 | 
			
		||||
 | 
			
		||||
		flag.BoolVar(&skipVerify, "skip-verify", false, "Skip SSL/TLS certificate verification")
 | 
			
		||||
 | 
			
		||||
		flag.StringVar(&method, "method", "", "HTTP Method")
 | 
			
		||||
		flag.StringVar(&method, "m", "", "HTTP Method")
 | 
			
		||||
 | 
			
		||||
@@ -94,6 +100,9 @@ func (config *Config) ReadCLI() (types.ConfigFile, error) {
 | 
			
		||||
		flag.UintVar(&requestCount, "requests", 0, "Number of total requests")
 | 
			
		||||
		flag.UintVar(&requestCount, "r", 0, "Number of total requests")
 | 
			
		||||
 | 
			
		||||
		flag.DurationVar(&duration, "duration", 0, "Maximum duration of the test")
 | 
			
		||||
		flag.DurationVar(&duration, "o", 0, "Maximum duration of the test")
 | 
			
		||||
 | 
			
		||||
		flag.DurationVar(&timeout, "timeout", 0, "Timeout for each request (e.g. 400ms, 15s, 1m10s)")
 | 
			
		||||
		flag.DurationVar(&timeout, "t", 0, "Timeout for each request (e.g. 400ms, 15s, 1m10s)")
 | 
			
		||||
 | 
			
		||||
@@ -139,10 +148,14 @@ func (config *Config) ReadCLI() (types.ConfigFile, error) {
 | 
			
		||||
			config.DodosCount = utils.ToPtr(dodosCount)
 | 
			
		||||
		case "requests", "r":
 | 
			
		||||
			config.RequestCount = utils.ToPtr(requestCount)
 | 
			
		||||
		case "duration", "o":
 | 
			
		||||
			config.Duration = &types.Duration{Duration: duration}
 | 
			
		||||
		case "timeout", "t":
 | 
			
		||||
			config.Timeout = &types.Timeout{Duration: timeout}
 | 
			
		||||
		case "yes", "y":
 | 
			
		||||
			config.Yes = utils.ToPtr(yes)
 | 
			
		||||
		case "skip-verify":
 | 
			
		||||
			config.SkipVerify = utils.ToPtr(skipVerify)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										208
									
								
								config/config.go
									
									
									
									
									
								
							
							
						
						
									
										208
									
								
								config/config.go
									
									
									
									
									
								
							@@ -1,12 +1,15 @@
 | 
			
		||||
package config
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"math/rand"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"os"
 | 
			
		||||
	"slices"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"text/template"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/aykhans/dodo/types"
 | 
			
		||||
@@ -15,29 +18,33 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	VERSION             string        = "0.6.0"
 | 
			
		||||
	VERSION             string        = "0.7.3"
 | 
			
		||||
	DefaultUserAgent    string        = "Dodo/" + VERSION
 | 
			
		||||
	DefaultMethod       string        = "GET"
 | 
			
		||||
	DefaultTimeout      time.Duration = time.Second * 10
 | 
			
		||||
	DefaultDodosCount   uint          = 1
 | 
			
		||||
	DefaultRequestCount uint          = 1
 | 
			
		||||
	DefaultRequestCount uint          = 0
 | 
			
		||||
	DefaultDuration     time.Duration = 0
 | 
			
		||||
	DefaultYes          bool          = false
 | 
			
		||||
	DefaultSkipVerify   bool          = false
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var SupportedProxySchemes []string = []string{"http", "socks5", "socks5h"}
 | 
			
		||||
 | 
			
		||||
type RequestConfig struct {
 | 
			
		||||
	Method       string        `json:"method"`
 | 
			
		||||
	URL          url.URL       `json:"url"`
 | 
			
		||||
	Timeout      time.Duration `json:"timeout"`
 | 
			
		||||
	DodosCount   uint          `json:"dodos"`
 | 
			
		||||
	RequestCount uint          `json:"requests"`
 | 
			
		||||
	Yes          bool          `json:"yes"`
 | 
			
		||||
	Params       types.Params  `json:"params"`
 | 
			
		||||
	Headers      types.Headers `json:"headers"`
 | 
			
		||||
	Cookies      types.Cookies `json:"cookies"`
 | 
			
		||||
	Body         types.Body    `json:"body"`
 | 
			
		||||
	Proxies      types.Proxies `json:"proxies"`
 | 
			
		||||
	Method       string
 | 
			
		||||
	URL          url.URL
 | 
			
		||||
	Timeout      time.Duration
 | 
			
		||||
	DodosCount   uint
 | 
			
		||||
	RequestCount uint
 | 
			
		||||
	Duration     time.Duration
 | 
			
		||||
	Yes          bool
 | 
			
		||||
	SkipVerify   bool
 | 
			
		||||
	Params       types.Params
 | 
			
		||||
	Headers      types.Headers
 | 
			
		||||
	Cookies      types.Cookies
 | 
			
		||||
	Body         types.Body
 | 
			
		||||
	Proxies      types.Proxies
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewRequestConfig(conf *Config) *RequestConfig {
 | 
			
		||||
@@ -47,7 +54,9 @@ func NewRequestConfig(conf *Config) *RequestConfig {
 | 
			
		||||
		Timeout:      conf.Timeout.Duration,
 | 
			
		||||
		DodosCount:   *conf.DodosCount,
 | 
			
		||||
		RequestCount: *conf.RequestCount,
 | 
			
		||||
		Duration:     conf.Duration.Duration,
 | 
			
		||||
		Yes:          *conf.Yes,
 | 
			
		||||
		SkipVerify:   *conf.SkipVerify,
 | 
			
		||||
		Params:       conf.Params,
 | 
			
		||||
		Headers:      conf.Headers,
 | 
			
		||||
		Cookies:      conf.Cookies,
 | 
			
		||||
@@ -57,6 +66,9 @@ func NewRequestConfig(conf *Config) *RequestConfig {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (rc *RequestConfig) GetValidDodosCountForRequests() uint {
 | 
			
		||||
	if rc.RequestCount == 0 {
 | 
			
		||||
		return rc.DodosCount
 | 
			
		||||
	}
 | 
			
		||||
	return min(rc.DodosCount, rc.RequestCount)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -95,7 +107,17 @@ func (rc *RequestConfig) Print() {
 | 
			
		||||
	t.AppendSeparator()
 | 
			
		||||
	t.AppendRow(table.Row{"Dodos", rc.DodosCount})
 | 
			
		||||
	t.AppendSeparator()
 | 
			
		||||
	t.AppendRow(table.Row{"Requests", rc.RequestCount})
 | 
			
		||||
	if rc.RequestCount > 0 {
 | 
			
		||||
		t.AppendRow(table.Row{"Requests", rc.RequestCount})
 | 
			
		||||
	} else {
 | 
			
		||||
		t.AppendRow(table.Row{"Requests"})
 | 
			
		||||
	}
 | 
			
		||||
	t.AppendSeparator()
 | 
			
		||||
	if rc.Duration > 0 {
 | 
			
		||||
		t.AppendRow(table.Row{"Duration", rc.Duration})
 | 
			
		||||
	} else {
 | 
			
		||||
		t.AppendRow(table.Row{"Duration"})
 | 
			
		||||
	}
 | 
			
		||||
	t.AppendSeparator()
 | 
			
		||||
	t.AppendRow(table.Row{"Params", rc.Params.String()})
 | 
			
		||||
	t.AppendSeparator()
 | 
			
		||||
@@ -106,42 +128,43 @@ func (rc *RequestConfig) Print() {
 | 
			
		||||
	t.AppendRow(table.Row{"Proxy", rc.Proxies.String()})
 | 
			
		||||
	t.AppendSeparator()
 | 
			
		||||
	t.AppendRow(table.Row{"Body", rc.Body.String()})
 | 
			
		||||
	t.AppendSeparator()
 | 
			
		||||
	t.AppendRow(table.Row{"Skip Verify", rc.SkipVerify})
 | 
			
		||||
 | 
			
		||||
	t.Render()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Config struct {
 | 
			
		||||
	Method       *string           `json:"method"`
 | 
			
		||||
	URL          *types.RequestURL `json:"url"`
 | 
			
		||||
	Timeout      *types.Timeout    `json:"timeout"`
 | 
			
		||||
	DodosCount   *uint             `json:"dodos"`
 | 
			
		||||
	RequestCount *uint             `json:"requests"`
 | 
			
		||||
	Yes          *bool             `json:"yes"`
 | 
			
		||||
	Params       types.Params      `json:"params"`
 | 
			
		||||
	Headers      types.Headers     `json:"headers"`
 | 
			
		||||
	Cookies      types.Cookies     `json:"cookies"`
 | 
			
		||||
	Body         types.Body        `json:"body"`
 | 
			
		||||
	Proxies      types.Proxies     `json:"proxy"`
 | 
			
		||||
	Method       *string           `json:"method" yaml:"method"`
 | 
			
		||||
	URL          *types.RequestURL `json:"url" yaml:"url"`
 | 
			
		||||
	Timeout      *types.Timeout    `json:"timeout" yaml:"timeout"`
 | 
			
		||||
	DodosCount   *uint             `json:"dodos" yaml:"dodos"`
 | 
			
		||||
	RequestCount *uint             `json:"requests" yaml:"requests"`
 | 
			
		||||
	Duration     *types.Duration   `json:"duration" yaml:"duration"`
 | 
			
		||||
	Yes          *bool             `json:"yes" yaml:"yes"`
 | 
			
		||||
	SkipVerify   *bool             `json:"skip_verify" yaml:"skip_verify"`
 | 
			
		||||
	Params       types.Params      `json:"params" yaml:"params"`
 | 
			
		||||
	Headers      types.Headers     `json:"headers" yaml:"headers"`
 | 
			
		||||
	Cookies      types.Cookies     `json:"cookies" yaml:"cookies"`
 | 
			
		||||
	Body         types.Body        `json:"body" yaml:"body"`
 | 
			
		||||
	Proxies      types.Proxies     `json:"proxy" yaml:"proxy"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewConfig() *Config {
 | 
			
		||||
	return &Config{}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Config) Validate() []error {
 | 
			
		||||
func (config *Config) Validate() []error {
 | 
			
		||||
	var errs []error
 | 
			
		||||
	if utils.IsNilOrZero(c.URL) {
 | 
			
		||||
	if utils.IsNilOrZero(config.URL) {
 | 
			
		||||
		errs = append(errs, errors.New("request URL is required"))
 | 
			
		||||
	} else {
 | 
			
		||||
		if c.URL.Scheme == "" {
 | 
			
		||||
			c.URL.Scheme = "http"
 | 
			
		||||
		}
 | 
			
		||||
		if c.URL.Scheme != "http" && c.URL.Scheme != "https" {
 | 
			
		||||
		if config.URL.Scheme != "http" && config.URL.Scheme != "https" {
 | 
			
		||||
			errs = append(errs, errors.New("request URL scheme must be http or https"))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		urlParams := types.Params{}
 | 
			
		||||
		for key, values := range c.URL.Query() {
 | 
			
		||||
		for key, values := range config.URL.Query() {
 | 
			
		||||
			for _, value := range values {
 | 
			
		||||
				urlParams = append(urlParams, types.KeyValue[string, []string]{
 | 
			
		||||
					Key:   key,
 | 
			
		||||
@@ -149,24 +172,24 @@ func (c *Config) Validate() []error {
 | 
			
		||||
				})
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		c.Params = append(urlParams, c.Params...)
 | 
			
		||||
		c.URL.RawQuery = ""
 | 
			
		||||
		config.Params = append(urlParams, config.Params...)
 | 
			
		||||
		config.URL.RawQuery = ""
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if utils.IsNilOrZero(c.Method) {
 | 
			
		||||
	if utils.IsNilOrZero(config.Method) {
 | 
			
		||||
		errs = append(errs, errors.New("request method is required"))
 | 
			
		||||
	}
 | 
			
		||||
	if utils.IsNilOrZero(c.Timeout) {
 | 
			
		||||
	if utils.IsNilOrZero(config.Timeout) {
 | 
			
		||||
		errs = append(errs, errors.New("request timeout must be greater than 0"))
 | 
			
		||||
	}
 | 
			
		||||
	if utils.IsNilOrZero(c.DodosCount) {
 | 
			
		||||
	if utils.IsNilOrZero(config.DodosCount) {
 | 
			
		||||
		errs = append(errs, errors.New("dodos count must be greater than 0"))
 | 
			
		||||
	}
 | 
			
		||||
	if utils.IsNilOrZero(c.RequestCount) {
 | 
			
		||||
		errs = append(errs, errors.New("request count must be greater than 0"))
 | 
			
		||||
	if utils.IsNilOrZero(config.Duration) && utils.IsNilOrZero(config.RequestCount) {
 | 
			
		||||
		errs = append(errs, errors.New("you should provide at least one of duration or request count"))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for i, proxy := range c.Proxies {
 | 
			
		||||
	for i, proxy := range config.Proxies {
 | 
			
		||||
		if proxy.String() == "" {
 | 
			
		||||
			errs = append(errs, fmt.Errorf("proxies[%d]: proxy cannot be empty", i))
 | 
			
		||||
		} else if schema := proxy.Scheme; !slices.Contains(SupportedProxySchemes, schema) {
 | 
			
		||||
@@ -178,6 +201,98 @@ func (c *Config) Validate() []error {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	funcMap := *utils.NewFuncMapGenerator(
 | 
			
		||||
		rand.New(
 | 
			
		||||
			rand.NewSource(
 | 
			
		||||
				time.Now().UnixNano(),
 | 
			
		||||
			),
 | 
			
		||||
		),
 | 
			
		||||
	).GetFuncMap()
 | 
			
		||||
 | 
			
		||||
	for _, header := range config.Headers {
 | 
			
		||||
		t, err := template.New("default").Funcs(funcMap).Parse(header.Key)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			errs = append(errs, fmt.Errorf("header key (%s) parse error: %v", header.Key, err))
 | 
			
		||||
		} else {
 | 
			
		||||
			var buf bytes.Buffer
 | 
			
		||||
			if err = t.Execute(&buf, nil); err != nil {
 | 
			
		||||
				errs = append(errs, fmt.Errorf("header key (%s) parse error: %v", header.Key, err))
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for _, value := range header.Value {
 | 
			
		||||
			t, err := template.New("default").Funcs(funcMap).Parse(value)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				errs = append(errs, fmt.Errorf("header value (%s) parse error: %v", value, err))
 | 
			
		||||
			} else {
 | 
			
		||||
				var buf bytes.Buffer
 | 
			
		||||
				if err = t.Execute(&buf, nil); err != nil {
 | 
			
		||||
					errs = append(errs, fmt.Errorf("header value (%s) parse error: %v", value, err))
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, cookie := range config.Cookies {
 | 
			
		||||
		t, err := template.New("default").Funcs(funcMap).Parse(cookie.Key)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			errs = append(errs, fmt.Errorf("cookie key (%s) parse error: %v", cookie.Key, err))
 | 
			
		||||
		} else {
 | 
			
		||||
			var buf bytes.Buffer
 | 
			
		||||
			if err = t.Execute(&buf, nil); err != nil {
 | 
			
		||||
				errs = append(errs, fmt.Errorf("cookie key (%s) parse error: %v", cookie.Key, err))
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for _, value := range cookie.Value {
 | 
			
		||||
			t, err := template.New("default").Funcs(funcMap).Parse(value)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				errs = append(errs, fmt.Errorf("cookie value (%s) parse error: %v", value, err))
 | 
			
		||||
			} else {
 | 
			
		||||
				var buf bytes.Buffer
 | 
			
		||||
				if err = t.Execute(&buf, nil); err != nil {
 | 
			
		||||
					errs = append(errs, fmt.Errorf("cookie value (%s) parse error: %v", value, err))
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, param := range config.Params {
 | 
			
		||||
		t, err := template.New("default").Funcs(funcMap).Parse(param.Key)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			errs = append(errs, fmt.Errorf("param key (%s) parse error: %v", param.Key, err))
 | 
			
		||||
		} else {
 | 
			
		||||
			var buf bytes.Buffer
 | 
			
		||||
			if err = t.Execute(&buf, nil); err != nil {
 | 
			
		||||
				errs = append(errs, fmt.Errorf("param key (%s) parse error: %v", param.Key, err))
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for _, value := range param.Value {
 | 
			
		||||
			t, err := template.New("default").Funcs(funcMap).Parse(value)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				errs = append(errs, fmt.Errorf("param value (%s) parse error: %v", value, err))
 | 
			
		||||
			} else {
 | 
			
		||||
				var buf bytes.Buffer
 | 
			
		||||
				if err = t.Execute(&buf, nil); err != nil {
 | 
			
		||||
					errs = append(errs, fmt.Errorf("param value (%s) parse error: %v", value, err))
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, body := range config.Body {
 | 
			
		||||
		t, err := template.New("default").Funcs(funcMap).Parse(body)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			errs = append(errs, fmt.Errorf("body (%s) parse error: %v", body, err))
 | 
			
		||||
		} else {
 | 
			
		||||
			var buf bytes.Buffer
 | 
			
		||||
			if err = t.Execute(&buf, nil); err != nil {
 | 
			
		||||
				errs = append(errs, fmt.Errorf("body (%s) parse error: %v", body, err))
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return errs
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -197,9 +312,15 @@ func (config *Config) MergeConfig(newConfig *Config) {
 | 
			
		||||
	if newConfig.RequestCount != nil {
 | 
			
		||||
		config.RequestCount = newConfig.RequestCount
 | 
			
		||||
	}
 | 
			
		||||
	if newConfig.Duration != nil {
 | 
			
		||||
		config.Duration = newConfig.Duration
 | 
			
		||||
	}
 | 
			
		||||
	if newConfig.Yes != nil {
 | 
			
		||||
		config.Yes = newConfig.Yes
 | 
			
		||||
	}
 | 
			
		||||
	if newConfig.SkipVerify != nil {
 | 
			
		||||
		config.SkipVerify = newConfig.SkipVerify
 | 
			
		||||
	}
 | 
			
		||||
	if len(newConfig.Params) != 0 {
 | 
			
		||||
		config.Params = newConfig.Params
 | 
			
		||||
	}
 | 
			
		||||
@@ -230,7 +351,14 @@ func (config *Config) SetDefaults() {
 | 
			
		||||
	if config.RequestCount == nil {
 | 
			
		||||
		config.RequestCount = utils.ToPtr(DefaultRequestCount)
 | 
			
		||||
	}
 | 
			
		||||
	if config.Duration == nil {
 | 
			
		||||
		config.Duration = &types.Duration{Duration: DefaultDuration}
 | 
			
		||||
	}
 | 
			
		||||
	if config.Yes == nil {
 | 
			
		||||
		config.Yes = utils.ToPtr(DefaultYes)
 | 
			
		||||
	}
 | 
			
		||||
	if config.SkipVerify == nil {
 | 
			
		||||
		config.SkipVerify = utils.ToPtr(DefaultSkipVerify)
 | 
			
		||||
	}
 | 
			
		||||
	config.Headers.SetIfNotExists("User-Agent", DefaultUserAgent)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -7,40 +7,55 @@ import (
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"os"
 | 
			
		||||
	"slices"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/aykhans/dodo/types"
 | 
			
		||||
	"gopkg.in/yaml.v3"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var supportedFileTypes = []string{"json", "yaml", "yml"}
 | 
			
		||||
 | 
			
		||||
func (config *Config) ReadFile(filePath types.ConfigFile) error {
 | 
			
		||||
	var (
 | 
			
		||||
		data []byte
 | 
			
		||||
		err  error
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	if filePath.LocationType() == types.FileLocationTypeRemoteHTTP {
 | 
			
		||||
		client := &http.Client{
 | 
			
		||||
			Timeout: 10 * time.Second,
 | 
			
		||||
	fileExt := filePath.Extension()
 | 
			
		||||
	if slices.Contains(supportedFileTypes, fileExt) {
 | 
			
		||||
		if filePath.LocationType() == types.FileLocationTypeRemoteHTTP {
 | 
			
		||||
			client := &http.Client{
 | 
			
		||||
				Timeout: 10 * time.Second,
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			resp, err := client.Get(filePath.String())
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return fmt.Errorf("failed to fetch config file from %s", filePath)
 | 
			
		||||
			}
 | 
			
		||||
			defer func() { _ = resp.Body.Close() }()
 | 
			
		||||
 | 
			
		||||
			data, err = io.ReadAll(io.Reader(resp.Body))
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return fmt.Errorf("failed to read config file from %s", filePath)
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			data, err = os.ReadFile(filePath.String())
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return errors.New("failed to read config file from " + filePath.String())
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		resp, err := client.Get(filePath.String())
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmt.Errorf("failed to fetch config file from %s", filePath)
 | 
			
		||||
		}
 | 
			
		||||
		defer resp.Body.Close()
 | 
			
		||||
 | 
			
		||||
		data, err = io.ReadAll(io.Reader(resp.Body))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmt.Errorf("failed to read config file from %s", filePath)
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		data, err = os.ReadFile(filePath.String())
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errors.New("failed to read config file from " + filePath.String())
 | 
			
		||||
		switch fileExt {
 | 
			
		||||
		case "json":
 | 
			
		||||
			return parseJSONConfig(data, config)
 | 
			
		||||
		case "yml", "yaml":
 | 
			
		||||
			return parseYAMLConfig(data, config)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return parseJSONConfig(data, config)
 | 
			
		||||
	return fmt.Errorf("unsupported config file type (supported types: %v)", strings.Join(supportedFileTypes, ", "))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func parseJSONConfig(data []byte, config *Config) error {
 | 
			
		||||
@@ -58,3 +73,12 @@ func parseJSONConfig(data []byte, config *Config) error {
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func parseYAMLConfig(data []byte, config *Config) error {
 | 
			
		||||
	err := yaml.Unmarshal(data, &config)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("YAML Config file: %s", err.Error())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										20
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								go.mod
									
									
									
									
									
								
							@@ -1,20 +1,22 @@
 | 
			
		||||
module github.com/aykhans/dodo
 | 
			
		||||
 | 
			
		||||
go 1.24.0
 | 
			
		||||
go 1.25
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	github.com/jedib0t/go-pretty/v6 v6.6.7
 | 
			
		||||
	github.com/valyala/fasthttp v1.59.0
 | 
			
		||||
	github.com/brianvoe/gofakeit/v7 v7.8.2
 | 
			
		||||
	github.com/jedib0t/go-pretty/v6 v6.6.8
 | 
			
		||||
	github.com/valyala/fasthttp v1.65.0
 | 
			
		||||
	gopkg.in/yaml.v3 v3.0.1
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	github.com/andybalholm/brotli v1.1.1 // indirect
 | 
			
		||||
	github.com/klauspost/compress v1.17.11 // indirect
 | 
			
		||||
	github.com/andybalholm/brotli v1.2.0 // indirect
 | 
			
		||||
	github.com/klauspost/compress v1.18.0 // indirect
 | 
			
		||||
	github.com/mattn/go-runewidth v0.0.16 // indirect
 | 
			
		||||
	github.com/rivo/uniseg v0.4.7 // indirect
 | 
			
		||||
	github.com/valyala/bytebufferpool v1.0.0 // indirect
 | 
			
		||||
	golang.org/x/net v0.36.0 // indirect
 | 
			
		||||
	golang.org/x/sys v0.30.0 // indirect
 | 
			
		||||
	golang.org/x/term v0.29.0 // indirect
 | 
			
		||||
	golang.org/x/text v0.22.0 // indirect
 | 
			
		||||
	golang.org/x/net v0.43.0 // indirect
 | 
			
		||||
	golang.org/x/sys v0.35.0 // indirect
 | 
			
		||||
	golang.org/x/term v0.34.0 // indirect
 | 
			
		||||
	golang.org/x/text v0.28.0 // indirect
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										36
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										36
									
								
								go.sum
									
									
									
									
									
								
							@@ -1,11 +1,13 @@
 | 
			
		||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
 | 
			
		||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
 | 
			
		||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
 | 
			
		||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
 | 
			
		||||
github.com/brianvoe/gofakeit/v7 v7.8.2 h1:FWxoSP4Ss9LWSvTOrWZHz7sIHcpZwLVw2xa/DhJABB4=
 | 
			
		||||
github.com/brianvoe/gofakeit/v7 v7.8.2/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA=
 | 
			
		||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 | 
			
		||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 | 
			
		||||
github.com/jedib0t/go-pretty/v6 v6.6.7 h1:m+LbHpm0aIAPLzLbMfn8dc3Ht8MW7lsSO4MPItz/Uuo=
 | 
			
		||||
github.com/jedib0t/go-pretty/v6 v6.6.7/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
 | 
			
		||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
 | 
			
		||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
 | 
			
		||||
github.com/jedib0t/go-pretty/v6 v6.6.8 h1:JnnzQeRz2bACBobIaa/r+nqjvws4yEhcmaZ4n1QzsEc=
 | 
			
		||||
github.com/jedib0t/go-pretty/v6 v6.6.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
 | 
			
		||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
 | 
			
		||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
 | 
			
		||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
 | 
			
		||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 | 
			
		||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 | 
			
		||||
@@ -17,17 +19,19 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
 | 
			
		||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 | 
			
		||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
 | 
			
		||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
 | 
			
		||||
github.com/valyala/fasthttp v1.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDpRI=
 | 
			
		||||
github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU=
 | 
			
		||||
github.com/valyala/fasthttp v1.65.0 h1:j/u3uzFEGFfRxw79iYzJN+TteTJwbYkru9uDp3d0Yf8=
 | 
			
		||||
github.com/valyala/fasthttp v1.65.0/go.mod h1:P/93/YkKPMsKSnATEeELUCkG8a7Y+k99uxNHVbKINr4=
 | 
			
		||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
 | 
			
		||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
 | 
			
		||||
golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
 | 
			
		||||
golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
 | 
			
		||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
 | 
			
		||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 | 
			
		||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
 | 
			
		||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
 | 
			
		||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
 | 
			
		||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
 | 
			
		||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
 | 
			
		||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
 | 
			
		||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
 | 
			
		||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 | 
			
		||||
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
 | 
			
		||||
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
 | 
			
		||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
 | 
			
		||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
 | 
			
		||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 | 
			
		||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 | 
			
		||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 | 
			
		||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ package requests
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"crypto/tls"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"math/rand"
 | 
			
		||||
	"net/url"
 | 
			
		||||
@@ -17,11 +18,12 @@ type ClientGeneratorFunc func() *fasthttp.HostClient
 | 
			
		||||
// getClients initializes and returns a slice of fasthttp.HostClient based on the provided parameters.
 | 
			
		||||
// It can either return clients with proxies or a single client without proxies.
 | 
			
		||||
func getClients(
 | 
			
		||||
	ctx context.Context,
 | 
			
		||||
	_ context.Context,
 | 
			
		||||
	timeout time.Duration,
 | 
			
		||||
	proxies []url.URL,
 | 
			
		||||
	maxConns uint,
 | 
			
		||||
	URL url.URL,
 | 
			
		||||
	skipVerify bool,
 | 
			
		||||
) []*fasthttp.HostClient {
 | 
			
		||||
	isTLS := URL.Scheme == "https"
 | 
			
		||||
 | 
			
		||||
@@ -39,8 +41,11 @@ func getClients(
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			clients = append(clients, &fasthttp.HostClient{
 | 
			
		||||
				MaxConns:            int(maxConns),
 | 
			
		||||
				IsTLS:               isTLS,
 | 
			
		||||
				MaxConns: int(maxConns),
 | 
			
		||||
				IsTLS:    isTLS,
 | 
			
		||||
				TLSConfig: &tls.Config{
 | 
			
		||||
					InsecureSkipVerify: skipVerify,
 | 
			
		||||
				},
 | 
			
		||||
				Addr:                addr,
 | 
			
		||||
				Dial:                dialFunc,
 | 
			
		||||
				MaxIdleConnDuration: timeout,
 | 
			
		||||
@@ -54,8 +59,11 @@ func getClients(
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	client := &fasthttp.HostClient{
 | 
			
		||||
		MaxConns:            int(maxConns),
 | 
			
		||||
		IsTLS:               isTLS,
 | 
			
		||||
		MaxConns: int(maxConns),
 | 
			
		||||
		IsTLS:    isTLS,
 | 
			
		||||
		TLSConfig: &tls.Config{
 | 
			
		||||
			InsecureSkipVerify: skipVerify,
 | 
			
		||||
		},
 | 
			
		||||
		Addr:                URL.Host,
 | 
			
		||||
		MaxIdleConnDuration: timeout,
 | 
			
		||||
		MaxConnDuration:     timeout,
 | 
			
		||||
@@ -72,13 +80,19 @@ func getClients(
 | 
			
		||||
func getDialFunc(proxy *url.URL, timeout time.Duration) (fasthttp.DialFunc, error) {
 | 
			
		||||
	var dialer fasthttp.DialFunc
 | 
			
		||||
 | 
			
		||||
	if proxy.Scheme == "socks5" || proxy.Scheme == "socks5h" {
 | 
			
		||||
	switch proxy.Scheme {
 | 
			
		||||
	case "socks5", "socks5h":
 | 
			
		||||
		dialer = fasthttpproxy.FasthttpSocksDialerDualStack(proxy.String())
 | 
			
		||||
	} else if proxy.Scheme == "http" {
 | 
			
		||||
	case "http":
 | 
			
		||||
		dialer = fasthttpproxy.FasthttpHTTPDialerDualStackTimeout(proxy.String(), timeout)
 | 
			
		||||
	} else {
 | 
			
		||||
	default:
 | 
			
		||||
		return nil, errors.New("unsupported proxy scheme")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if dialer == nil {
 | 
			
		||||
		return nil, errors.New("internal error: proxy dialer is nil")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return dialer, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ import (
 | 
			
		||||
func streamProgress(
 | 
			
		||||
	ctx context.Context,
 | 
			
		||||
	wg *sync.WaitGroup,
 | 
			
		||||
	total int64,
 | 
			
		||||
	total uint,
 | 
			
		||||
	message string,
 | 
			
		||||
	increase <-chan int64,
 | 
			
		||||
) {
 | 
			
		||||
@@ -27,21 +27,26 @@ func streamProgress(
 | 
			
		||||
	pw.SetStyle(progress.StyleBlocks)
 | 
			
		||||
	pw.SetTrackerLength(40)
 | 
			
		||||
	pw.SetUpdateFrequency(time.Millisecond * 250)
 | 
			
		||||
	if total == 0 {
 | 
			
		||||
		pw.Style().Visibility.Percentage = false
 | 
			
		||||
	}
 | 
			
		||||
	go pw.Render()
 | 
			
		||||
	dodosTracker := progress.Tracker{
 | 
			
		||||
		Message: message,
 | 
			
		||||
		Total:   total,
 | 
			
		||||
		Total:   int64(total),
 | 
			
		||||
	}
 | 
			
		||||
	pw.AppendTracker(&dodosTracker)
 | 
			
		||||
 | 
			
		||||
	for {
 | 
			
		||||
		select {
 | 
			
		||||
		case <-ctx.Done():
 | 
			
		||||
			if ctx.Err() != context.Canceled {
 | 
			
		||||
			if err := ctx.Err(); err == context.Canceled || err == context.DeadlineExceeded {
 | 
			
		||||
				dodosTracker.MarkAsDone()
 | 
			
		||||
			} else {
 | 
			
		||||
				dodosTracker.MarkAsErrored()
 | 
			
		||||
			}
 | 
			
		||||
			time.Sleep(time.Millisecond * 300)
 | 
			
		||||
			fmt.Printf("\r")
 | 
			
		||||
			time.Sleep(time.Millisecond * 500)
 | 
			
		||||
			pw.Stop()
 | 
			
		||||
			return
 | 
			
		||||
 | 
			
		||||
		case value := <-increase:
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,11 @@
 | 
			
		||||
package requests
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"context"
 | 
			
		||||
	"math/rand"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"text/template"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/aykhans/dodo/config"
 | 
			
		||||
@@ -21,6 +23,11 @@ type Request struct {
 | 
			
		||||
	getRequest RequestGeneratorFunc
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type keyValueGenerator struct {
 | 
			
		||||
	key   func() string
 | 
			
		||||
	value func() string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Send sends the HTTP request using the fasthttp client with a specified timeout.
 | 
			
		||||
// It returns the HTTP response or an error if the request fails or times out.
 | 
			
		||||
func (r *Request) Send(ctx context.Context, timeout time.Duration) (*fasthttp.Response, error) {
 | 
			
		||||
@@ -101,26 +108,28 @@ func getRequestGeneratorFunc(
 | 
			
		||||
	bodies []string,
 | 
			
		||||
	localRand *rand.Rand,
 | 
			
		||||
) RequestGeneratorFunc {
 | 
			
		||||
	bodiesLen := len(bodies)
 | 
			
		||||
	getBody := func() string { return "" }
 | 
			
		||||
	if bodiesLen == 1 {
 | 
			
		||||
		getBody = func() string { return bodies[0] }
 | 
			
		||||
	} else if bodiesLen > 1 {
 | 
			
		||||
		getBody = utils.RandomValueCycle(bodies, localRand)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	getParams := getKeyValueGeneratorFunc(params, localRand)
 | 
			
		||||
	getHeaders := getKeyValueGeneratorFunc(headers, localRand)
 | 
			
		||||
	getCookies := getKeyValueGeneratorFunc(cookies, localRand)
 | 
			
		||||
	getBody := getBodyValueFunc(bodies, utils.NewFuncMapGenerator(localRand), localRand)
 | 
			
		||||
 | 
			
		||||
	return func() *fasthttp.Request {
 | 
			
		||||
		body, contentType := getBody()
 | 
			
		||||
		headers := getHeaders()
 | 
			
		||||
		if contentType != "" {
 | 
			
		||||
			headers = append(headers, types.KeyValue[string, string]{
 | 
			
		||||
				Key:   "Content-Type",
 | 
			
		||||
				Value: contentType,
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return newFasthttpRequest(
 | 
			
		||||
			URL,
 | 
			
		||||
			getParams(),
 | 
			
		||||
			getHeaders(),
 | 
			
		||||
			headers,
 | 
			
		||||
			getCookies(),
 | 
			
		||||
			method,
 | 
			
		||||
			getBody(),
 | 
			
		||||
			body,
 | 
			
		||||
		)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -166,9 +175,6 @@ func setRequestHeaders(req *fasthttp.Request, headers []types.KeyValue[string, s
 | 
			
		||||
	for _, header := range headers {
 | 
			
		||||
		req.Header.Add(header.Key, header.Value)
 | 
			
		||||
	}
 | 
			
		||||
	if req.Header.UserAgent() == nil {
 | 
			
		||||
		req.Header.SetUserAgent(config.DefaultUserAgent)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// setRequestCookies adds the cookies of the given request with the provided key-value pairs.
 | 
			
		||||
@@ -202,49 +208,134 @@ func getKeyValueGeneratorFunc[
 | 
			
		||||
	keyValueSlice []types.KeyValue[string, []string],
 | 
			
		||||
	localRand *rand.Rand,
 | 
			
		||||
) func() T {
 | 
			
		||||
	getKeyValueSlice := []map[string]func() string{}
 | 
			
		||||
	isRandom := false
 | 
			
		||||
	keyValueGenerators := make([]keyValueGenerator, len(keyValueSlice))
 | 
			
		||||
 | 
			
		||||
	for _, kv := range keyValueSlice {
 | 
			
		||||
		valuesLen := len(kv.Value)
 | 
			
		||||
	funcMap := *utils.NewFuncMapGenerator(localRand).GetFuncMap()
 | 
			
		||||
 | 
			
		||||
		getValueFunc := func() string { return "" }
 | 
			
		||||
		if valuesLen == 1 {
 | 
			
		||||
			getValueFunc = func() string { return kv.Value[0] }
 | 
			
		||||
		} else if valuesLen > 1 {
 | 
			
		||||
			getValueFunc = utils.RandomValueCycle(kv.Value, localRand)
 | 
			
		||||
			isRandom = true
 | 
			
		||||
	for i, kv := range keyValueSlice {
 | 
			
		||||
		keyValueGenerators[i] = keyValueGenerator{
 | 
			
		||||
			key:   getKeyFunc(kv.Key, funcMap),
 | 
			
		||||
			value: getValueFunc(kv.Value, funcMap, localRand),
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		getKeyValueSlice = append(
 | 
			
		||||
			getKeyValueSlice,
 | 
			
		||||
			map[string]func() string{kv.Key: getValueFunc},
 | 
			
		||||
		)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if isRandom {
 | 
			
		||||
		return func() T {
 | 
			
		||||
			keyValues := make(T, len(getKeyValueSlice))
 | 
			
		||||
			for i, keyValue := range getKeyValueSlice {
 | 
			
		||||
				for key, value := range keyValue {
 | 
			
		||||
					keyValues[i] = types.KeyValue[string, string]{
 | 
			
		||||
						Key:   key,
 | 
			
		||||
						Value: value(),
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			return keyValues
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		keyValues := make(T, len(getKeyValueSlice))
 | 
			
		||||
		for i, keyValue := range getKeyValueSlice {
 | 
			
		||||
			for key, value := range keyValue {
 | 
			
		||||
				keyValues[i] = types.KeyValue[string, string]{
 | 
			
		||||
					Key:   key,
 | 
			
		||||
					Value: value(),
 | 
			
		||||
				}
 | 
			
		||||
	return func() T {
 | 
			
		||||
		keyValues := make(T, len(keyValueGenerators))
 | 
			
		||||
		for i, keyValue := range keyValueGenerators {
 | 
			
		||||
			keyValues[i] = types.KeyValue[string, string]{
 | 
			
		||||
				Key:   keyValue.key(),
 | 
			
		||||
				Value: keyValue.value(),
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return func() T { return keyValues }
 | 
			
		||||
		return keyValues
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// getKeyFunc creates a function that processes a key string through Go's template engine.
 | 
			
		||||
// It takes a key string and a template.FuncMap containing the available template functions.
 | 
			
		||||
//
 | 
			
		||||
// The returned function, when called, will execute the template with the given key and return
 | 
			
		||||
// the processed string result. If template parsing fails, the returned function will always
 | 
			
		||||
// return an empty string.
 | 
			
		||||
//
 | 
			
		||||
// This enables dynamic generation of keys that can include template directives and functions.
 | 
			
		||||
func getKeyFunc(key string, funcMap template.FuncMap) func() string {
 | 
			
		||||
	t, err := template.New("default").Funcs(funcMap).Parse(key)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return func() string { return "" }
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return func() string {
 | 
			
		||||
		var buf bytes.Buffer
 | 
			
		||||
		_ = t.Execute(&buf, nil)
 | 
			
		||||
		return buf.String()
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// getValueFunc creates a function that randomly selects and processes a value from a slice of strings
 | 
			
		||||
// through Go's template engine.
 | 
			
		||||
//
 | 
			
		||||
// Parameters:
 | 
			
		||||
//   - values: A slice of string templates that can contain template directives
 | 
			
		||||
//   - funcMap: A template.FuncMap containing all available template functions
 | 
			
		||||
//   - localRand: A random number generator for consistent randomization
 | 
			
		||||
//
 | 
			
		||||
// The returned function, when called, will:
 | 
			
		||||
//  1. Select a random template from the values slice
 | 
			
		||||
//  2. Execute the selected template
 | 
			
		||||
//  3. Return the processed string result
 | 
			
		||||
//
 | 
			
		||||
// If a selected template is nil (due to earlier parsing failure), the function will return an empty string.
 | 
			
		||||
// This enables dynamic generation of values with randomized selection from multiple templates.
 | 
			
		||||
func getValueFunc(
 | 
			
		||||
	values []string,
 | 
			
		||||
	funcMap template.FuncMap,
 | 
			
		||||
	localRand *rand.Rand,
 | 
			
		||||
) func() string {
 | 
			
		||||
	templates := make([]*template.Template, len(values))
 | 
			
		||||
 | 
			
		||||
	for i, value := range values {
 | 
			
		||||
		t, err := template.New("default").Funcs(funcMap).Parse(value)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			templates[i] = nil
 | 
			
		||||
		}
 | 
			
		||||
		templates[i] = t
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	randomTemplateFunc := utils.RandomValueCycle(templates, localRand)
 | 
			
		||||
 | 
			
		||||
	return func() string {
 | 
			
		||||
		if tmpl := randomTemplateFunc(); tmpl == nil {
 | 
			
		||||
			return ""
 | 
			
		||||
		} else {
 | 
			
		||||
			var buf bytes.Buffer
 | 
			
		||||
			_ = tmpl.Execute(&buf, nil)
 | 
			
		||||
			return buf.String()
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// getBodyValueFunc creates a function that randomly selects and processes a request body from a slice of templates.
 | 
			
		||||
// It returns a closure that generates both the body content and the appropriate Content-Type header value.
 | 
			
		||||
//
 | 
			
		||||
// Parameters:
 | 
			
		||||
//   - values: A slice of string templates that can contain template directives for request bodies
 | 
			
		||||
//   - funcMapGenerator: Provides template functions and content type information
 | 
			
		||||
//   - localRand: A random number generator for consistent randomization
 | 
			
		||||
//
 | 
			
		||||
// The returned function, when called, will:
 | 
			
		||||
//  1. Select a random body template from the values slice
 | 
			
		||||
//  2. Execute the selected template with available template functions
 | 
			
		||||
//  3. Return both the processed body string and the appropriate Content-Type header value
 | 
			
		||||
//
 | 
			
		||||
// If the selected template is nil (due to earlier parsing failure), the function will return
 | 
			
		||||
// empty strings for both the body and Content-Type.
 | 
			
		||||
//
 | 
			
		||||
// This enables dynamic generation of request bodies with proper content type headers.
 | 
			
		||||
func getBodyValueFunc(
 | 
			
		||||
	values []string,
 | 
			
		||||
	funcMapGenerator *utils.FuncMapGenerator,
 | 
			
		||||
	localRand *rand.Rand,
 | 
			
		||||
) func() (string, string) {
 | 
			
		||||
	templates := make([]*template.Template, len(values))
 | 
			
		||||
 | 
			
		||||
	for i, value := range values {
 | 
			
		||||
		t, err := template.New("default").Funcs(*funcMapGenerator.GetFuncMap()).Parse(value)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			templates[i] = nil
 | 
			
		||||
		}
 | 
			
		||||
		templates[i] = t
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	randomTemplateFunc := utils.RandomValueCycle(templates, localRand)
 | 
			
		||||
 | 
			
		||||
	return func() (string, string) {
 | 
			
		||||
		if tmpl := randomTemplateFunc(); tmpl == nil {
 | 
			
		||||
			return "", ""
 | 
			
		||||
		} else {
 | 
			
		||||
			var buf bytes.Buffer
 | 
			
		||||
			_ = tmpl.Execute(&buf, nil)
 | 
			
		||||
			return buf.String(), funcMapGenerator.GetBodyDataHeader()
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -14,47 +14,30 @@ type Response struct {
 | 
			
		||||
	Time     time.Duration
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Responses []*Response
 | 
			
		||||
type Responses []Response
 | 
			
		||||
 | 
			
		||||
// Print prints the responses in a tabular format, including information such as
 | 
			
		||||
// response count, minimum time, maximum time, average time, and latency percentiles.
 | 
			
		||||
func (responses Responses) Print() {
 | 
			
		||||
	total := struct {
 | 
			
		||||
		Count int
 | 
			
		||||
		Min   time.Duration
 | 
			
		||||
		Max   time.Duration
 | 
			
		||||
		Sum   time.Duration
 | 
			
		||||
		P90   time.Duration
 | 
			
		||||
		P95   time.Duration
 | 
			
		||||
		P99   time.Duration
 | 
			
		||||
	}{
 | 
			
		||||
		Count: len(responses),
 | 
			
		||||
		Min:   responses[0].Time,
 | 
			
		||||
		Max:   responses[0].Time,
 | 
			
		||||
	if len(responses) == 0 {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	mergedResponses := make(map[string]types.Durations)
 | 
			
		||||
	var allDurations types.Durations
 | 
			
		||||
 | 
			
		||||
	for _, response := range responses {
 | 
			
		||||
		if response.Time < total.Min {
 | 
			
		||||
			total.Min = response.Time
 | 
			
		||||
		}
 | 
			
		||||
		if response.Time > total.Max {
 | 
			
		||||
			total.Max = response.Time
 | 
			
		||||
		}
 | 
			
		||||
		total.Sum += response.Time
 | 
			
		||||
	mergedResponses := make(map[string]types.Durations)
 | 
			
		||||
 | 
			
		||||
	totalDurations := make(types.Durations, len(responses))
 | 
			
		||||
	var totalSum time.Duration
 | 
			
		||||
	totalCount := len(responses)
 | 
			
		||||
 | 
			
		||||
	for i, response := range responses {
 | 
			
		||||
		totalSum += response.Time
 | 
			
		||||
		totalDurations[i] = response.Time
 | 
			
		||||
 | 
			
		||||
		mergedResponses[response.Response] = append(
 | 
			
		||||
			mergedResponses[response.Response],
 | 
			
		||||
			response.Time,
 | 
			
		||||
		)
 | 
			
		||||
		allDurations = append(allDurations, response.Time)
 | 
			
		||||
	}
 | 
			
		||||
	allDurations.Sort()
 | 
			
		||||
	allDurationsLenAsFloat := float64(len(allDurations) - 1)
 | 
			
		||||
	total.P90 = allDurations[int(0.90*allDurationsLenAsFloat)]
 | 
			
		||||
	total.P95 = allDurations[int(0.95*allDurationsLenAsFloat)]
 | 
			
		||||
	total.P99 = allDurations[int(0.99*allDurationsLenAsFloat)]
 | 
			
		||||
 | 
			
		||||
	t := table.NewWriter()
 | 
			
		||||
	t.SetOutputMirror(os.Stdout)
 | 
			
		||||
@@ -93,15 +76,18 @@ func (responses Responses) Print() {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(mergedResponses) > 1 {
 | 
			
		||||
		totalDurations.Sort()
 | 
			
		||||
		allDurationsLenAsFloat := float64(len(totalDurations) - 1)
 | 
			
		||||
 | 
			
		||||
		t.AppendRow(table.Row{
 | 
			
		||||
			"Total",
 | 
			
		||||
			total.Count,
 | 
			
		||||
			utils.DurationRoundBy(total.Min, roundPrecision),
 | 
			
		||||
			utils.DurationRoundBy(total.Max, roundPrecision),
 | 
			
		||||
			utils.DurationRoundBy(total.Sum/time.Duration(total.Count), roundPrecision), // Average
 | 
			
		||||
			utils.DurationRoundBy(total.P90, roundPrecision),
 | 
			
		||||
			utils.DurationRoundBy(total.P95, roundPrecision),
 | 
			
		||||
			utils.DurationRoundBy(total.P99, roundPrecision),
 | 
			
		||||
			totalCount,
 | 
			
		||||
			utils.DurationRoundBy(totalDurations[0], roundPrecision),
 | 
			
		||||
			utils.DurationRoundBy(totalDurations[len(totalDurations)-1], roundPrecision),
 | 
			
		||||
			utils.DurationRoundBy(totalSum/time.Duration(totalCount), roundPrecision), // Average
 | 
			
		||||
			utils.DurationRoundBy(totalDurations[int(0.90*allDurationsLenAsFloat)], roundPrecision),
 | 
			
		||||
			utils.DurationRoundBy(totalDurations[int(0.95*allDurationsLenAsFloat)], roundPrecision),
 | 
			
		||||
			utils.DurationRoundBy(totalDurations[int(0.99*allDurationsLenAsFloat)], roundPrecision),
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
	t.Render()
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										121
									
								
								requests/run.go
									
									
									
									
									
								
							
							
						
						
									
										121
									
								
								requests/run.go
									
									
									
									
									
								
							@@ -20,12 +20,19 @@ import (
 | 
			
		||||
//   - ctx: The context for managing request lifecycle and cancellation.
 | 
			
		||||
//   - requestConfig: The configuration for the request, including timeout, proxies, and other settings.
 | 
			
		||||
func Run(ctx context.Context, requestConfig *config.RequestConfig) (Responses, error) {
 | 
			
		||||
	if requestConfig.Duration > 0 {
 | 
			
		||||
		var cancel context.CancelFunc
 | 
			
		||||
		ctx, cancel = context.WithTimeout(ctx, requestConfig.Duration)
 | 
			
		||||
		defer cancel()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	clients := getClients(
 | 
			
		||||
		ctx,
 | 
			
		||||
		requestConfig.Timeout,
 | 
			
		||||
		requestConfig.Proxies,
 | 
			
		||||
		requestConfig.GetMaxConns(fasthttp.DefaultMaxConnsPerHost),
 | 
			
		||||
		requestConfig.URL,
 | 
			
		||||
		requestConfig.SkipVerify,
 | 
			
		||||
	)
 | 
			
		||||
	if clients == nil {
 | 
			
		||||
		return nil, types.ErrInterrupt
 | 
			
		||||
@@ -58,52 +65,65 @@ func releaseDodos(
 | 
			
		||||
		wg                  sync.WaitGroup
 | 
			
		||||
		streamWG            sync.WaitGroup
 | 
			
		||||
		requestCountPerDodo uint
 | 
			
		||||
		dodosCount          uint = requestConfig.GetValidDodosCountForRequests()
 | 
			
		||||
		dodosCountInt       int  = int(dodosCount)
 | 
			
		||||
		responses                = make([][]*Response, dodosCount)
 | 
			
		||||
		increase                 = make(chan int64, requestConfig.RequestCount)
 | 
			
		||||
		dodosCount          = requestConfig.GetValidDodosCountForRequests()
 | 
			
		||||
		responses           = make([][]Response, dodosCount)
 | 
			
		||||
		increase            = make(chan int64, requestConfig.RequestCount)
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	wg.Add(dodosCountInt)
 | 
			
		||||
	wg.Add(int(dodosCount))
 | 
			
		||||
	streamWG.Add(1)
 | 
			
		||||
	streamCtx, streamCtxCancel := context.WithCancel(context.Background())
 | 
			
		||||
	streamCtx, streamCtxCancel := context.WithCancel(ctx)
 | 
			
		||||
 | 
			
		||||
	go streamProgress(streamCtx, &streamWG, int64(requestConfig.RequestCount), "Dodos Working🔥", increase)
 | 
			
		||||
	go streamProgress(streamCtx, &streamWG, requestConfig.RequestCount, "Dodos Working🔥", increase)
 | 
			
		||||
 | 
			
		||||
	for i := range dodosCount {
 | 
			
		||||
		if i+1 == dodosCount {
 | 
			
		||||
			requestCountPerDodo = requestConfig.RequestCount - (i * requestConfig.RequestCount / dodosCount)
 | 
			
		||||
		} else {
 | 
			
		||||
			requestCountPerDodo = ((i + 1) * requestConfig.RequestCount / dodosCount) -
 | 
			
		||||
				(i * requestConfig.RequestCount / dodosCount)
 | 
			
		||||
	if requestConfig.RequestCount == 0 {
 | 
			
		||||
		for i := range dodosCount {
 | 
			
		||||
			go sendRequest(
 | 
			
		||||
				ctx,
 | 
			
		||||
				newRequest(*requestConfig, clients, int64(i)),
 | 
			
		||||
				requestConfig.Timeout,
 | 
			
		||||
				&responses[i],
 | 
			
		||||
				increase,
 | 
			
		||||
				&wg,
 | 
			
		||||
			)
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		for i := range dodosCount {
 | 
			
		||||
			if i+1 == dodosCount {
 | 
			
		||||
				requestCountPerDodo = requestConfig.RequestCount - (i * requestConfig.RequestCount / dodosCount)
 | 
			
		||||
			} else {
 | 
			
		||||
				requestCountPerDodo = ((i + 1) * requestConfig.RequestCount / dodosCount) -
 | 
			
		||||
					(i * requestConfig.RequestCount / dodosCount)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		go sendRequest(
 | 
			
		||||
			ctx,
 | 
			
		||||
			newRequest(*requestConfig, clients, int64(i)),
 | 
			
		||||
			requestConfig.Timeout,
 | 
			
		||||
			requestCountPerDodo,
 | 
			
		||||
			&responses[i],
 | 
			
		||||
			increase,
 | 
			
		||||
			&wg,
 | 
			
		||||
		)
 | 
			
		||||
			go sendRequestByCount(
 | 
			
		||||
				ctx,
 | 
			
		||||
				newRequest(*requestConfig, clients, int64(i)),
 | 
			
		||||
				requestConfig.Timeout,
 | 
			
		||||
				requestCountPerDodo,
 | 
			
		||||
				&responses[i],
 | 
			
		||||
				increase,
 | 
			
		||||
				&wg,
 | 
			
		||||
			)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	wg.Wait()
 | 
			
		||||
	streamCtxCancel()
 | 
			
		||||
	streamWG.Wait()
 | 
			
		||||
	return utils.Flatten(responses)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// sendRequest sends a specified number of HTTP requests concurrently with a given timeout.
 | 
			
		||||
// sendRequestByCount sends a specified number of HTTP requests concurrently with a given timeout.
 | 
			
		||||
// It appends the responses to the provided responseData slice and sends the count of completed requests
 | 
			
		||||
// to the increase channel. The function terminates early if the context is canceled or if a custom
 | 
			
		||||
// interrupt error is encountered.
 | 
			
		||||
func sendRequest(
 | 
			
		||||
func sendRequestByCount(
 | 
			
		||||
	ctx context.Context,
 | 
			
		||||
	request *Request,
 | 
			
		||||
	timeout time.Duration,
 | 
			
		||||
	requestCount uint,
 | 
			
		||||
	responseData *[]*Response,
 | 
			
		||||
	responseData *[]Response,
 | 
			
		||||
	increase chan<- int64,
 | 
			
		||||
	wg *sync.WaitGroup,
 | 
			
		||||
) {
 | 
			
		||||
@@ -126,7 +146,7 @@ func sendRequest(
 | 
			
		||||
				if err == types.ErrInterrupt {
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
				*responseData = append(*responseData, &Response{
 | 
			
		||||
				*responseData = append(*responseData, Response{
 | 
			
		||||
					Response: err.Error(),
 | 
			
		||||
					Time:     completedTime,
 | 
			
		||||
				})
 | 
			
		||||
@@ -134,7 +154,54 @@ func sendRequest(
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			*responseData = append(*responseData, &Response{
 | 
			
		||||
			*responseData = append(*responseData, Response{
 | 
			
		||||
				Response: strconv.Itoa(response.StatusCode()),
 | 
			
		||||
				Time:     completedTime,
 | 
			
		||||
			})
 | 
			
		||||
			increase <- 1
 | 
			
		||||
		}()
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// sendRequest continuously sends HTTP requests until the context is canceled.
 | 
			
		||||
// It records the response status code or error message along with the response time,
 | 
			
		||||
// and signals each completed request through the increase channel.
 | 
			
		||||
func sendRequest(
 | 
			
		||||
	ctx context.Context,
 | 
			
		||||
	request *Request,
 | 
			
		||||
	timeout time.Duration,
 | 
			
		||||
	responseData *[]Response,
 | 
			
		||||
	increase chan<- int64,
 | 
			
		||||
	wg *sync.WaitGroup,
 | 
			
		||||
) {
 | 
			
		||||
	defer wg.Done()
 | 
			
		||||
 | 
			
		||||
	for {
 | 
			
		||||
		if ctx.Err() != nil {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		func() {
 | 
			
		||||
			startTime := time.Now()
 | 
			
		||||
			response, err := request.Send(ctx, timeout)
 | 
			
		||||
			completedTime := time.Since(startTime)
 | 
			
		||||
			if response != nil {
 | 
			
		||||
				defer fasthttp.ReleaseResponse(response)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				if err == types.ErrInterrupt {
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
				*responseData = append(*responseData, Response{
 | 
			
		||||
					Response: err.Error(),
 | 
			
		||||
					Time:     completedTime,
 | 
			
		||||
				})
 | 
			
		||||
				increase <- 1
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			*responseData = append(*responseData, Response{
 | 
			
		||||
				Response: strconv.Itoa(response.StatusCode()),
 | 
			
		||||
				Time:     completedTime,
 | 
			
		||||
			})
 | 
			
		||||
 
 | 
			
		||||
@@ -13,12 +13,12 @@ type Body []string
 | 
			
		||||
func (body Body) String() string {
 | 
			
		||||
	var buffer bytes.Buffer
 | 
			
		||||
	if len(body) == 0 {
 | 
			
		||||
		return string(buffer.Bytes())
 | 
			
		||||
		return buffer.String()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(body) == 1 {
 | 
			
		||||
		buffer.WriteString(body[0])
 | 
			
		||||
		return string(buffer.Bytes())
 | 
			
		||||
		return buffer.String()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	buffer.WriteString(text.FgBlue.Sprint("Random") + "[\n")
 | 
			
		||||
@@ -41,7 +41,7 @@ func (body Body) String() string {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	buffer.WriteString("\n]")
 | 
			
		||||
	return string(buffer.Bytes())
 | 
			
		||||
	return buffer.String()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (body *Body) UnmarshalJSON(b []byte) error {
 | 
			
		||||
@@ -66,6 +66,28 @@ func (body *Body) UnmarshalJSON(b []byte) error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (body *Body) UnmarshalYAML(unmarshal func(any) error) error {
 | 
			
		||||
	var data any
 | 
			
		||||
	if err := unmarshal(&data); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	switch v := data.(type) {
 | 
			
		||||
	case string:
 | 
			
		||||
		*body = []string{v}
 | 
			
		||||
	case []any:
 | 
			
		||||
		var slice []string
 | 
			
		||||
		for _, item := range v {
 | 
			
		||||
			slice = append(slice, fmt.Sprintf("%v", item))
 | 
			
		||||
		}
 | 
			
		||||
		*body = slice
 | 
			
		||||
	default:
 | 
			
		||||
		return fmt.Errorf("invalid type for Body: %T (should be string or []string)", v)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (body *Body) Set(value string) error {
 | 
			
		||||
	*body = append(*body, value)
 | 
			
		||||
	return nil
 | 
			
		||||
 
 | 
			
		||||
@@ -11,13 +11,22 @@ const (
 | 
			
		||||
 | 
			
		||||
type ConfigFile string
 | 
			
		||||
 | 
			
		||||
func (config ConfigFile) String() string {
 | 
			
		||||
	return string(config)
 | 
			
		||||
func (configFile ConfigFile) String() string {
 | 
			
		||||
	return string(configFile)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (config ConfigFile) LocationType() FileLocationType {
 | 
			
		||||
	if strings.HasPrefix(string(config), "http://") || strings.HasPrefix(string(config), "https://") {
 | 
			
		||||
func (configFile ConfigFile) LocationType() FileLocationType {
 | 
			
		||||
	if strings.HasPrefix(string(configFile), "http://") || strings.HasPrefix(string(configFile), "https://") {
 | 
			
		||||
		return FileLocationTypeRemoteHTTP
 | 
			
		||||
	}
 | 
			
		||||
	return FileLocationTypeLocal
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (configFile ConfigFile) Extension() string {
 | 
			
		||||
	i := strings.LastIndex(configFile.String(), ".")
 | 
			
		||||
	if i == -1 {
 | 
			
		||||
		return ""
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return configFile.String()[i+1:]
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,7 @@ type Cookies []KeyValue[string, []string]
 | 
			
		||||
func (cookies Cookies) String() string {
 | 
			
		||||
	var buffer bytes.Buffer
 | 
			
		||||
	if len(cookies) == 0 {
 | 
			
		||||
		return string(buffer.Bytes())
 | 
			
		||||
		return buffer.String()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	indent := "  "
 | 
			
		||||
@@ -53,7 +53,24 @@ func (cookies Cookies) String() string {
 | 
			
		||||
		buffer.WriteString(",\n" + text.FgGreen.Sprintf("+%d cookies", remainingPairs))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return string(buffer.Bytes())
 | 
			
		||||
	return buffer.String()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (cookies *Cookies) AppendByKey(key, value string) {
 | 
			
		||||
	if item := cookies.GetValue(key); item != nil {
 | 
			
		||||
		*item = append(*item, value)
 | 
			
		||||
	} else {
 | 
			
		||||
		*cookies = append(*cookies, KeyValue[string, []string]{Key: key, Value: []string{value}})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (cookies Cookies) GetValue(key string) *[]string {
 | 
			
		||||
	for i := range cookies {
 | 
			
		||||
		if cookies[i].Key == key {
 | 
			
		||||
			return &cookies[i].Value
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (cookies *Cookies) UnmarshalJSON(b []byte) error {
 | 
			
		||||
@@ -82,6 +99,31 @@ func (cookies *Cookies) UnmarshalJSON(b []byte) error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (cookies *Cookies) UnmarshalYAML(unmarshal func(any) error) error {
 | 
			
		||||
	var raw []map[string]any
 | 
			
		||||
	if err := unmarshal(&raw); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, param := range raw {
 | 
			
		||||
		for key, value := range param {
 | 
			
		||||
			switch parsed := value.(type) {
 | 
			
		||||
			case string:
 | 
			
		||||
				*cookies = append(*cookies, KeyValue[string, []string]{Key: key, Value: []string{parsed}})
 | 
			
		||||
			case []any:
 | 
			
		||||
				var values []string
 | 
			
		||||
				for _, v := range parsed {
 | 
			
		||||
					if str, ok := v.(string); ok {
 | 
			
		||||
						values = append(values, str)
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				*cookies = append(*cookies, KeyValue[string, []string]{Key: key, Value: values})
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (cookies *Cookies) Set(value string) error {
 | 
			
		||||
	parts := strings.SplitN(value, "=", 2)
 | 
			
		||||
	switch len(parts) {
 | 
			
		||||
@@ -95,20 +137,3 @@ func (cookies *Cookies) Set(value string) error {
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (cookies *Cookies) AppendByKey(key, value string) {
 | 
			
		||||
	if item := cookies.GetValue(key); item != nil {
 | 
			
		||||
		*item = append(*item, value)
 | 
			
		||||
	} else {
 | 
			
		||||
		*cookies = append(*cookies, KeyValue[string, []string]{Key: key, Value: []string{value}})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (cookies Cookies) GetValue(key string) *[]string {
 | 
			
		||||
	for i := range cookies {
 | 
			
		||||
		if cookies[i].Key == key {
 | 
			
		||||
			return &cookies[i].Value
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,31 +6,52 @@ import (
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Timeout struct {
 | 
			
		||||
type Duration struct {
 | 
			
		||||
	time.Duration
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (timeout *Timeout) UnmarshalJSON(b []byte) error {
 | 
			
		||||
func (duration *Duration) UnmarshalJSON(b []byte) error {
 | 
			
		||||
	var v any
 | 
			
		||||
	if err := json.Unmarshal(b, &v); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	switch value := v.(type) {
 | 
			
		||||
	case float64:
 | 
			
		||||
		timeout.Duration = time.Duration(value)
 | 
			
		||||
		duration.Duration = time.Duration(value)
 | 
			
		||||
		return nil
 | 
			
		||||
	case string:
 | 
			
		||||
		var err error
 | 
			
		||||
		timeout.Duration, err = time.ParseDuration(value)
 | 
			
		||||
		duration.Duration, err = time.ParseDuration(value)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errors.New("Timeout is invalid (e.g. 400ms, 1s, 5m, 1h)")
 | 
			
		||||
			return errors.New("Duration is invalid (e.g. 400ms, 1s, 5m, 1h)")
 | 
			
		||||
		}
 | 
			
		||||
		return nil
 | 
			
		||||
	default:
 | 
			
		||||
		return errors.New("Timeout is invalid (e.g. 400ms, 1s, 5m, 1h)")
 | 
			
		||||
		return errors.New("Duration is invalid (e.g. 400ms, 1s, 5m, 1h)")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (timeout Timeout) MarshalJSON() ([]byte, error) {
 | 
			
		||||
	return json.Marshal(timeout.Duration.String())
 | 
			
		||||
func (duration Duration) MarshalJSON() ([]byte, error) {
 | 
			
		||||
	return json.Marshal(duration.String())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (duration *Duration) UnmarshalYAML(unmarshal func(any) error) error {
 | 
			
		||||
	var v any
 | 
			
		||||
	if err := unmarshal(&v); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	switch value := v.(type) {
 | 
			
		||||
	case float64:
 | 
			
		||||
		duration.Duration = time.Duration(value)
 | 
			
		||||
		return nil
 | 
			
		||||
	case string:
 | 
			
		||||
		var err error
 | 
			
		||||
		duration.Duration, err = time.ParseDuration(value)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errors.New("Duration is invalid (e.g. 400ms, 1s, 5m, 1h)")
 | 
			
		||||
		}
 | 
			
		||||
		return nil
 | 
			
		||||
	default:
 | 
			
		||||
		return errors.New("Duration is invalid (e.g. 400ms, 1s, 5m, 1h)")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
package types
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"slices"
 | 
			
		||||
	"sort"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
@@ -14,9 +15,7 @@ func (d Durations) Sort(ascending ...bool) {
 | 
			
		||||
			return d[i] > d[j]
 | 
			
		||||
		})
 | 
			
		||||
	} else { // Otherwise, sort in ascending order
 | 
			
		||||
		sort.Slice(d, func(i, j int) bool {
 | 
			
		||||
			return d[i] < d[j]
 | 
			
		||||
		})
 | 
			
		||||
		slices.Sort(d)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,7 @@ type Headers []KeyValue[string, []string]
 | 
			
		||||
func (headers Headers) String() string {
 | 
			
		||||
	var buffer bytes.Buffer
 | 
			
		||||
	if len(headers) == 0 {
 | 
			
		||||
		return string(buffer.Bytes())
 | 
			
		||||
		return buffer.String()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	indent := "  "
 | 
			
		||||
@@ -53,7 +53,33 @@ func (headers Headers) String() string {
 | 
			
		||||
		buffer.WriteString(",\n" + text.FgGreen.Sprintf("+%d headers", remainingPairs))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return string(buffer.Bytes())
 | 
			
		||||
	return buffer.String()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (headers *Headers) AppendByKey(key, value string) {
 | 
			
		||||
	if item := headers.GetValue(key); item != nil {
 | 
			
		||||
		*item = append(*item, value)
 | 
			
		||||
	} else {
 | 
			
		||||
		*headers = append(*headers, KeyValue[string, []string]{Key: key, Value: []string{value}})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (headers Headers) GetValue(key string) *[]string {
 | 
			
		||||
	for i := range headers {
 | 
			
		||||
		if headers[i].Key == key {
 | 
			
		||||
			return &headers[i].Value
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (headers Headers) Has(key string) bool {
 | 
			
		||||
	for i := range headers {
 | 
			
		||||
		if headers[i].Key == key {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (headers *Headers) UnmarshalJSON(b []byte) error {
 | 
			
		||||
@@ -82,6 +108,31 @@ func (headers *Headers) UnmarshalJSON(b []byte) error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (headers *Headers) UnmarshalYAML(unmarshal func(any) error) error {
 | 
			
		||||
	var raw []map[string]any
 | 
			
		||||
	if err := unmarshal(&raw); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, param := range raw {
 | 
			
		||||
		for key, value := range param {
 | 
			
		||||
			switch parsed := value.(type) {
 | 
			
		||||
			case string:
 | 
			
		||||
				*headers = append(*headers, KeyValue[string, []string]{Key: key, Value: []string{parsed}})
 | 
			
		||||
			case []any:
 | 
			
		||||
				var values []string
 | 
			
		||||
				for _, v := range parsed {
 | 
			
		||||
					if str, ok := v.(string); ok {
 | 
			
		||||
						values = append(values, str)
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				*headers = append(*headers, KeyValue[string, []string]{Key: key, Value: values})
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (headers *Headers) Set(value string) error {
 | 
			
		||||
	parts := strings.SplitN(value, ":", 2)
 | 
			
		||||
	switch len(parts) {
 | 
			
		||||
@@ -96,19 +147,10 @@ func (headers *Headers) Set(value string) error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (headers *Headers) AppendByKey(key, value string) {
 | 
			
		||||
	if item := headers.GetValue(key); item != nil {
 | 
			
		||||
		*item = append(*item, value)
 | 
			
		||||
	} else {
 | 
			
		||||
		*headers = append(*headers, KeyValue[string, []string]{Key: key, Value: []string{value}})
 | 
			
		||||
func (headers *Headers) SetIfNotExists(key string, value string) bool {
 | 
			
		||||
	if headers.Has(key) {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (headers Headers) GetValue(key string) *[]string {
 | 
			
		||||
	for i := range headers {
 | 
			
		||||
		if headers[i].Key == key {
 | 
			
		||||
			return &headers[i].Value
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
	*headers = append(*headers, KeyValue[string, []string]{Key: key, Value: []string{value}})
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,7 @@ type Params []KeyValue[string, []string]
 | 
			
		||||
func (params Params) String() string {
 | 
			
		||||
	var buffer bytes.Buffer
 | 
			
		||||
	if len(params) == 0 {
 | 
			
		||||
		return string(buffer.Bytes())
 | 
			
		||||
		return buffer.String()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	indent := "  "
 | 
			
		||||
@@ -53,7 +53,24 @@ func (params Params) String() string {
 | 
			
		||||
		buffer.WriteString(",\n" + text.FgGreen.Sprintf("+%d params", remainingPairs))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return string(buffer.Bytes())
 | 
			
		||||
	return buffer.String()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (params *Params) AppendByKey(key, value string) {
 | 
			
		||||
	if item := params.GetValue(key); item != nil {
 | 
			
		||||
		*item = append(*item, value)
 | 
			
		||||
	} else {
 | 
			
		||||
		*params = append(*params, KeyValue[string, []string]{Key: key, Value: []string{value}})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (params Params) GetValue(key string) *[]string {
 | 
			
		||||
	for i := range params {
 | 
			
		||||
		if params[i].Key == key {
 | 
			
		||||
			return ¶ms[i].Value
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (params *Params) UnmarshalJSON(b []byte) error {
 | 
			
		||||
@@ -82,6 +99,31 @@ func (params *Params) UnmarshalJSON(b []byte) error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (params *Params) UnmarshalYAML(unmarshal func(any) error) error {
 | 
			
		||||
	var raw []map[string]any
 | 
			
		||||
	if err := unmarshal(&raw); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, param := range raw {
 | 
			
		||||
		for key, value := range param {
 | 
			
		||||
			switch parsed := value.(type) {
 | 
			
		||||
			case string:
 | 
			
		||||
				*params = append(*params, KeyValue[string, []string]{Key: key, Value: []string{parsed}})
 | 
			
		||||
			case []any:
 | 
			
		||||
				var values []string
 | 
			
		||||
				for _, v := range parsed {
 | 
			
		||||
					if str, ok := v.(string); ok {
 | 
			
		||||
						values = append(values, str)
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				*params = append(*params, KeyValue[string, []string]{Key: key, Value: values})
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (params *Params) Set(value string) error {
 | 
			
		||||
	parts := strings.SplitN(value, "=", 2)
 | 
			
		||||
	switch len(parts) {
 | 
			
		||||
@@ -95,20 +137,3 @@ func (params *Params) Set(value string) error {
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (params *Params) AppendByKey(key, value string) {
 | 
			
		||||
	if item := params.GetValue(key); item != nil {
 | 
			
		||||
		*item = append(*item, value)
 | 
			
		||||
	} else {
 | 
			
		||||
		*params = append(*params, KeyValue[string, []string]{Key: key, Value: []string{value}})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (params Params) GetValue(key string) *[]string {
 | 
			
		||||
	for i := range params {
 | 
			
		||||
		if params[i].Key == key {
 | 
			
		||||
			return ¶ms[i].Value
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -14,12 +14,12 @@ type Proxies []url.URL
 | 
			
		||||
func (proxies Proxies) String() string {
 | 
			
		||||
	var buffer bytes.Buffer
 | 
			
		||||
	if len(proxies) == 0 {
 | 
			
		||||
		return string(buffer.Bytes())
 | 
			
		||||
		return buffer.String()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(proxies) == 1 {
 | 
			
		||||
		buffer.WriteString(proxies[0].String())
 | 
			
		||||
		return string(buffer.Bytes())
 | 
			
		||||
		return buffer.String()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	buffer.WriteString(text.FgBlue.Sprint("Random") + "[\n")
 | 
			
		||||
@@ -42,7 +42,7 @@ func (proxies Proxies) String() string {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	buffer.WriteString("\n]")
 | 
			
		||||
	return string(buffer.Bytes())
 | 
			
		||||
	return buffer.String()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (proxies *Proxies) UnmarshalJSON(b []byte) error {
 | 
			
		||||
@@ -75,6 +75,36 @@ func (proxies *Proxies) UnmarshalJSON(b []byte) error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (proxies *Proxies) UnmarshalYAML(unmarshal func(any) error) error {
 | 
			
		||||
	var data any
 | 
			
		||||
	if err := unmarshal(&data); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	switch v := data.(type) {
 | 
			
		||||
	case string:
 | 
			
		||||
		parsed, err := url.Parse(v)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		*proxies = []url.URL{*parsed}
 | 
			
		||||
	case []any:
 | 
			
		||||
		var urls []url.URL
 | 
			
		||||
		for _, item := range v {
 | 
			
		||||
			url, err := url.Parse(item.(string))
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			urls = append(urls, *url)
 | 
			
		||||
		}
 | 
			
		||||
		*proxies = urls
 | 
			
		||||
	default:
 | 
			
		||||
		return fmt.Errorf("invalid type for Body: %T (should be URL or []URL)", v)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (proxies *Proxies) Set(value string) error {
 | 
			
		||||
	parsedURL, err := url.Parse(value)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,22 @@ func (requestURL *RequestURL) UnmarshalJSON(data []byte) error {
 | 
			
		||||
 | 
			
		||||
	parsedURL, err := url.Parse(urlStr)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errors.New("Request URL is invalid")
 | 
			
		||||
		return errors.New("request URL is invalid")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	requestURL.URL = *parsedURL
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (requestURL *RequestURL) UnmarshalYAML(unmarshal func(any) error) error {
 | 
			
		||||
	var urlStr string
 | 
			
		||||
	if err := unmarshal(&urlStr); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	parsedURL, err := url.Parse(urlStr)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errors.New("request URL is invalid")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	requestURL.URL = *parsedURL
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										57
									
								
								types/timeout.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								types/timeout.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,57 @@
 | 
			
		||||
package types
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Timeout struct {
 | 
			
		||||
	time.Duration
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (timeout *Timeout) UnmarshalJSON(b []byte) error {
 | 
			
		||||
	var v any
 | 
			
		||||
	if err := json.Unmarshal(b, &v); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	switch value := v.(type) {
 | 
			
		||||
	case float64:
 | 
			
		||||
		timeout.Duration = time.Duration(value)
 | 
			
		||||
		return nil
 | 
			
		||||
	case string:
 | 
			
		||||
		var err error
 | 
			
		||||
		timeout.Duration, err = time.ParseDuration(value)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errors.New("Timeout is invalid (e.g. 400ms, 1s, 5m, 1h)")
 | 
			
		||||
		}
 | 
			
		||||
		return nil
 | 
			
		||||
	default:
 | 
			
		||||
		return errors.New("Timeout is invalid (e.g. 400ms, 1s, 5m, 1h)")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (timeout Timeout) MarshalJSON() ([]byte, error) {
 | 
			
		||||
	return json.Marshal(timeout.String())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (timeout *Timeout) UnmarshalYAML(unmarshal func(any) error) error {
 | 
			
		||||
	var v any
 | 
			
		||||
	if err := unmarshal(&v); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	switch value := v.(type) {
 | 
			
		||||
	case float64:
 | 
			
		||||
		timeout.Duration = time.Duration(value)
 | 
			
		||||
		return nil
 | 
			
		||||
	case string:
 | 
			
		||||
		var err error
 | 
			
		||||
		timeout.Duration, err = time.ParseDuration(value)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errors.New("Timeout is invalid (e.g. 400ms, 1s, 5m, 1h)")
 | 
			
		||||
		}
 | 
			
		||||
		return nil
 | 
			
		||||
	default:
 | 
			
		||||
		return errors.New("Timeout is invalid (e.g. 400ms, 1s, 5m, 1h)")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -6,9 +6,5 @@ func IsNilOrZero[T comparable](value *T) bool {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var zero T
 | 
			
		||||
	if *value == zero {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return false
 | 
			
		||||
	return *value == zero
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,40 +2,41 @@ package utils
 | 
			
		||||
 | 
			
		||||
import "math/rand"
 | 
			
		||||
 | 
			
		||||
func Flatten[T any](nested [][]*T) []*T {
 | 
			
		||||
	flattened := make([]*T, 0)
 | 
			
		||||
func Flatten[T any](nested [][]T) []T {
 | 
			
		||||
	flattened := make([]T, 0)
 | 
			
		||||
	for _, n := range nested {
 | 
			
		||||
		flattened = append(flattened, n...)
 | 
			
		||||
	}
 | 
			
		||||
	return flattened
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RandomValueCycle returns a function that cycles through the provided slice of values
 | 
			
		||||
// in a random order. Each call to the returned function will yield a value from the slice.
 | 
			
		||||
// The order of values is determined by the provided random number generator.
 | 
			
		||||
//
 | 
			
		||||
// The returned function will cycle through the values in a random order until all values
 | 
			
		||||
// have been returned at least once. After all values have been returned, the function will
 | 
			
		||||
// reset and start cycling through the values in a random order again.
 | 
			
		||||
// The returned function isn't thread-safe and should be used in a single-threaded context.
 | 
			
		||||
func RandomValueCycle[Value any](values []Value, localRand *rand.Rand) func() Value {
 | 
			
		||||
	var (
 | 
			
		||||
		clientsCount int = len(values)
 | 
			
		||||
		currentIndex int = localRand.Intn(clientsCount)
 | 
			
		||||
		stopIndex    int = currentIndex
 | 
			
		||||
	)
 | 
			
		||||
// RandomValueCycle returns a function that cycles through the provided values in a pseudo-random order.
 | 
			
		||||
// Each value in the input slice will be returned before any value is repeated.
 | 
			
		||||
// If the input slice is empty, the returned function will always return the zero value of type T.
 | 
			
		||||
// If the input slice contains only one element, that element is always returned.
 | 
			
		||||
// This function is not thread-safe and should not be called concurrently.
 | 
			
		||||
func RandomValueCycle[T any](values []T, localRand *rand.Rand) func() T {
 | 
			
		||||
	switch valuesLen := len(values); valuesLen {
 | 
			
		||||
	case 0:
 | 
			
		||||
		var zero T
 | 
			
		||||
		return func() T { return zero }
 | 
			
		||||
	case 1:
 | 
			
		||||
		return func() T { return values[0] }
 | 
			
		||||
	default:
 | 
			
		||||
		currentIndex := localRand.Intn(valuesLen)
 | 
			
		||||
		stopIndex := currentIndex
 | 
			
		||||
		return func() T {
 | 
			
		||||
			value := values[currentIndex]
 | 
			
		||||
			currentIndex++
 | 
			
		||||
			if currentIndex == valuesLen {
 | 
			
		||||
				currentIndex = 0
 | 
			
		||||
			}
 | 
			
		||||
			if currentIndex == stopIndex {
 | 
			
		||||
				currentIndex = localRand.Intn(valuesLen)
 | 
			
		||||
				stopIndex = currentIndex
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
	return func() Value {
 | 
			
		||||
		client := values[currentIndex]
 | 
			
		||||
		currentIndex++
 | 
			
		||||
		if currentIndex == clientsCount {
 | 
			
		||||
			currentIndex = 0
 | 
			
		||||
			return value
 | 
			
		||||
		}
 | 
			
		||||
		if currentIndex == stopIndex {
 | 
			
		||||
			currentIndex = localRand.Intn(clientsCount)
 | 
			
		||||
			stopIndex = currentIndex
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return client
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										479
									
								
								utils/templates.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										479
									
								
								utils/templates.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,479 @@
 | 
			
		||||
package utils
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"math/rand"
 | 
			
		||||
	"mime/multipart"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"text/template"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/brianvoe/gofakeit/v7"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type FuncMapGenerator struct {
 | 
			
		||||
	bodyDataHeader string
 | 
			
		||||
	localFaker     *gofakeit.Faker
 | 
			
		||||
	funcMap        *template.FuncMap
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewFuncMapGenerator(localRand *rand.Rand) *FuncMapGenerator {
 | 
			
		||||
	f := &FuncMapGenerator{
 | 
			
		||||
		localFaker: gofakeit.NewFaker(localRand, false),
 | 
			
		||||
	}
 | 
			
		||||
	f.funcMap = f.newFuncMap()
 | 
			
		||||
 | 
			
		||||
	return f
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (g *FuncMapGenerator) GetBodyDataHeader() string {
 | 
			
		||||
	tempHeader := g.bodyDataHeader
 | 
			
		||||
	g.bodyDataHeader = ""
 | 
			
		||||
	return tempHeader
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (g *FuncMapGenerator) GetFuncMap() *template.FuncMap {
 | 
			
		||||
	return g.funcMap
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewFuncMap creates a template.FuncMap populated with string manipulation functions
 | 
			
		||||
// and data generation functions from gofakeit.
 | 
			
		||||
//
 | 
			
		||||
// It takes a random number generator that is used to initialize a localized faker
 | 
			
		||||
// instance, ensuring that random data generation is deterministic within a request context.
 | 
			
		||||
//
 | 
			
		||||
// All functions are prefixed to avoid naming conflicts:
 | 
			
		||||
//   - String functions: "strings_*"
 | 
			
		||||
//   - Dict functions: "dict_*"
 | 
			
		||||
//   - Body functions: "body_*"
 | 
			
		||||
//   - Data generation functions: "fakeit_*"
 | 
			
		||||
func (g *FuncMapGenerator) newFuncMap() *template.FuncMap {
 | 
			
		||||
	return &template.FuncMap{
 | 
			
		||||
		// Strings
 | 
			
		||||
		"strings_ToUpper":      strings.ToUpper,
 | 
			
		||||
		"strings_ToLower":      strings.ToLower,
 | 
			
		||||
		"strings_RemoveSpaces": func(s string) string { return strings.ReplaceAll(s, " ", "") },
 | 
			
		||||
		"strings_Replace":      strings.Replace,
 | 
			
		||||
		"strings_ToDate": func(dateString string) time.Time {
 | 
			
		||||
			date, err := time.Parse("2006-01-02", dateString)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return time.Now()
 | 
			
		||||
			}
 | 
			
		||||
			return date
 | 
			
		||||
		},
 | 
			
		||||
		"strings_First": func(s string, n int) string {
 | 
			
		||||
			if n >= len(s) {
 | 
			
		||||
				return s
 | 
			
		||||
			}
 | 
			
		||||
			return s[:n]
 | 
			
		||||
		},
 | 
			
		||||
		"strings_Last": func(s string, n int) string {
 | 
			
		||||
			if n >= len(s) {
 | 
			
		||||
				return s
 | 
			
		||||
			}
 | 
			
		||||
			return s[len(s)-n:]
 | 
			
		||||
		},
 | 
			
		||||
		"strings_Truncate": func(s string, n int) string {
 | 
			
		||||
			if n >= len(s) {
 | 
			
		||||
				return s
 | 
			
		||||
			}
 | 
			
		||||
			return s[:n] + "..."
 | 
			
		||||
		},
 | 
			
		||||
		"strings_TrimPrefix": strings.TrimPrefix,
 | 
			
		||||
		"strings_TrimSuffix": strings.TrimSuffix,
 | 
			
		||||
		"strings_Join": func(sep string, values ...string) string {
 | 
			
		||||
			return strings.Join(values, sep)
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		// Dict
 | 
			
		||||
		"dict_Str": func(values ...string) map[string]string {
 | 
			
		||||
			dict := make(map[string]string)
 | 
			
		||||
			for i := 0; i < len(values); i += 2 {
 | 
			
		||||
				if i+1 < len(values) {
 | 
			
		||||
					key := values[i]
 | 
			
		||||
					value := values[i+1]
 | 
			
		||||
					dict[key] = value
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			return dict
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		// Slice
 | 
			
		||||
		"slice_Str":  func(values ...string) []string { return values },
 | 
			
		||||
		"slice_Int":  func(values ...int) []int { return values },
 | 
			
		||||
		"slice_Uint": func(values ...uint) []uint { return values },
 | 
			
		||||
 | 
			
		||||
		// Body
 | 
			
		||||
		"body_FormData": func(kv map[string]string) string {
 | 
			
		||||
			var data bytes.Buffer
 | 
			
		||||
			writer := multipart.NewWriter(&data)
 | 
			
		||||
 | 
			
		||||
			for k, v := range kv {
 | 
			
		||||
				_ = writer.WriteField(k, v)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			_ = writer.Close()
 | 
			
		||||
			g.bodyDataHeader = writer.FormDataContentType()
 | 
			
		||||
 | 
			
		||||
			return data.String()
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		// FakeIt / Product
 | 
			
		||||
		"fakeit_ProductName":        g.localFaker.ProductName,
 | 
			
		||||
		"fakeit_ProductDescription": g.localFaker.ProductDescription,
 | 
			
		||||
		"fakeit_ProductCategory":    g.localFaker.ProductCategory,
 | 
			
		||||
		"fakeit_ProductFeature":     g.localFaker.ProductFeature,
 | 
			
		||||
		"fakeit_ProductMaterial":    g.localFaker.ProductMaterial,
 | 
			
		||||
		"fakeit_ProductUPC":         g.localFaker.ProductUPC,
 | 
			
		||||
		"fakeit_ProductAudience":    g.localFaker.ProductAudience,
 | 
			
		||||
		"fakeit_ProductDimension":   g.localFaker.ProductDimension,
 | 
			
		||||
		"fakeit_ProductUseCase":     g.localFaker.ProductUseCase,
 | 
			
		||||
		"fakeit_ProductBenefit":     g.localFaker.ProductBenefit,
 | 
			
		||||
		"fakeit_ProductSuffix":      g.localFaker.ProductSuffix,
 | 
			
		||||
 | 
			
		||||
		// FakeIt / Person
 | 
			
		||||
		"fakeit_Name":           g.localFaker.Name,
 | 
			
		||||
		"fakeit_NamePrefix":     g.localFaker.NamePrefix,
 | 
			
		||||
		"fakeit_NameSuffix":     g.localFaker.NameSuffix,
 | 
			
		||||
		"fakeit_FirstName":      g.localFaker.FirstName,
 | 
			
		||||
		"fakeit_MiddleName":     g.localFaker.MiddleName,
 | 
			
		||||
		"fakeit_LastName":       g.localFaker.LastName,
 | 
			
		||||
		"fakeit_Gender":         g.localFaker.Gender,
 | 
			
		||||
		"fakeit_SSN":            g.localFaker.SSN,
 | 
			
		||||
		"fakeit_Hobby":          g.localFaker.Hobby,
 | 
			
		||||
		"fakeit_Email":          g.localFaker.Email,
 | 
			
		||||
		"fakeit_Phone":          g.localFaker.Phone,
 | 
			
		||||
		"fakeit_PhoneFormatted": g.localFaker.PhoneFormatted,
 | 
			
		||||
 | 
			
		||||
		// FakeIt / Auth
 | 
			
		||||
		"fakeit_Username": g.localFaker.Username,
 | 
			
		||||
		"fakeit_Password": g.localFaker.Password,
 | 
			
		||||
 | 
			
		||||
		// FakeIt / Address
 | 
			
		||||
		"fakeit_City":         g.localFaker.City,
 | 
			
		||||
		"fakeit_Country":      g.localFaker.Country,
 | 
			
		||||
		"fakeit_CountryAbr":   g.localFaker.CountryAbr,
 | 
			
		||||
		"fakeit_State":        g.localFaker.State,
 | 
			
		||||
		"fakeit_StateAbr":     g.localFaker.StateAbr,
 | 
			
		||||
		"fakeit_Street":       g.localFaker.Street,
 | 
			
		||||
		"fakeit_StreetName":   g.localFaker.StreetName,
 | 
			
		||||
		"fakeit_StreetNumber": g.localFaker.StreetNumber,
 | 
			
		||||
		"fakeit_StreetPrefix": g.localFaker.StreetPrefix,
 | 
			
		||||
		"fakeit_StreetSuffix": g.localFaker.StreetSuffix,
 | 
			
		||||
		"fakeit_Zip":          g.localFaker.Zip,
 | 
			
		||||
		"fakeit_Latitude":     g.localFaker.Latitude,
 | 
			
		||||
		"fakeit_LatitudeInRange": func(min, max float64) float64 {
 | 
			
		||||
			value, err := g.localFaker.LatitudeInRange(min, max)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				var zero float64
 | 
			
		||||
				return zero
 | 
			
		||||
			}
 | 
			
		||||
			return value
 | 
			
		||||
		},
 | 
			
		||||
		"fakeit_Longitude": g.localFaker.Longitude,
 | 
			
		||||
		"fakeit_LongitudeInRange": func(min, max float64) float64 {
 | 
			
		||||
			value, err := g.localFaker.LongitudeInRange(min, max)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				var zero float64
 | 
			
		||||
				return zero
 | 
			
		||||
			}
 | 
			
		||||
			return value
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		// FakeIt / Game
 | 
			
		||||
		"fakeit_Gamertag": g.localFaker.Gamertag,
 | 
			
		||||
 | 
			
		||||
		// FakeIt / Beer
 | 
			
		||||
		"fakeit_BeerAlcohol": g.localFaker.BeerAlcohol,
 | 
			
		||||
		"fakeit_BeerBlg":     g.localFaker.BeerBlg,
 | 
			
		||||
		"fakeit_BeerHop":     g.localFaker.BeerHop,
 | 
			
		||||
		"fakeit_BeerIbu":     g.localFaker.BeerIbu,
 | 
			
		||||
		"fakeit_BeerMalt":    g.localFaker.BeerMalt,
 | 
			
		||||
		"fakeit_BeerName":    g.localFaker.BeerName,
 | 
			
		||||
		"fakeit_BeerStyle":   g.localFaker.BeerStyle,
 | 
			
		||||
		"fakeit_BeerYeast":   g.localFaker.BeerYeast,
 | 
			
		||||
 | 
			
		||||
		// FakeIt / Car
 | 
			
		||||
		"fakeit_CarMaker":            g.localFaker.CarMaker,
 | 
			
		||||
		"fakeit_CarModel":            g.localFaker.CarModel,
 | 
			
		||||
		"fakeit_CarType":             g.localFaker.CarType,
 | 
			
		||||
		"fakeit_CarFuelType":         g.localFaker.CarFuelType,
 | 
			
		||||
		"fakeit_CarTransmissionType": g.localFaker.CarTransmissionType,
 | 
			
		||||
 | 
			
		||||
		// FakeIt / Words
 | 
			
		||||
		"fakeit_Noun":                      g.localFaker.Noun,
 | 
			
		||||
		"fakeit_NounCommon":                g.localFaker.NounCommon,
 | 
			
		||||
		"fakeit_NounConcrete":              g.localFaker.NounConcrete,
 | 
			
		||||
		"fakeit_NounAbstract":              g.localFaker.NounAbstract,
 | 
			
		||||
		"fakeit_NounCollectivePeople":      g.localFaker.NounCollectivePeople,
 | 
			
		||||
		"fakeit_NounCollectiveAnimal":      g.localFaker.NounCollectiveAnimal,
 | 
			
		||||
		"fakeit_NounCollectiveThing":       g.localFaker.NounCollectiveThing,
 | 
			
		||||
		"fakeit_NounCountable":             g.localFaker.NounCountable,
 | 
			
		||||
		"fakeit_NounUncountable":           g.localFaker.NounUncountable,
 | 
			
		||||
		"fakeit_Verb":                      g.localFaker.Verb,
 | 
			
		||||
		"fakeit_VerbAction":                g.localFaker.VerbAction,
 | 
			
		||||
		"fakeit_VerbLinking":               g.localFaker.VerbLinking,
 | 
			
		||||
		"fakeit_VerbHelping":               g.localFaker.VerbHelping,
 | 
			
		||||
		"fakeit_Adverb":                    g.localFaker.Adverb,
 | 
			
		||||
		"fakeit_AdverbManner":              g.localFaker.AdverbManner,
 | 
			
		||||
		"fakeit_AdverbDegree":              g.localFaker.AdverbDegree,
 | 
			
		||||
		"fakeit_AdverbPlace":               g.localFaker.AdverbPlace,
 | 
			
		||||
		"fakeit_AdverbTimeDefinite":        g.localFaker.AdverbTimeDefinite,
 | 
			
		||||
		"fakeit_AdverbTimeIndefinite":      g.localFaker.AdverbTimeIndefinite,
 | 
			
		||||
		"fakeit_AdverbFrequencyDefinite":   g.localFaker.AdverbFrequencyDefinite,
 | 
			
		||||
		"fakeit_AdverbFrequencyIndefinite": g.localFaker.AdverbFrequencyIndefinite,
 | 
			
		||||
		"fakeit_Preposition":               g.localFaker.Preposition,
 | 
			
		||||
		"fakeit_PrepositionSimple":         g.localFaker.PrepositionSimple,
 | 
			
		||||
		"fakeit_PrepositionDouble":         g.localFaker.PrepositionDouble,
 | 
			
		||||
		"fakeit_PrepositionCompound":       g.localFaker.PrepositionCompound,
 | 
			
		||||
		"fakeit_Adjective":                 g.localFaker.Adjective,
 | 
			
		||||
		"fakeit_AdjectiveDescriptive":      g.localFaker.AdjectiveDescriptive,
 | 
			
		||||
		"fakeit_AdjectiveQuantitative":     g.localFaker.AdjectiveQuantitative,
 | 
			
		||||
		"fakeit_AdjectiveProper":           g.localFaker.AdjectiveProper,
 | 
			
		||||
		"fakeit_AdjectiveDemonstrative":    g.localFaker.AdjectiveDemonstrative,
 | 
			
		||||
		"fakeit_AdjectivePossessive":       g.localFaker.AdjectivePossessive,
 | 
			
		||||
		"fakeit_AdjectiveInterrogative":    g.localFaker.AdjectiveInterrogative,
 | 
			
		||||
		"fakeit_AdjectiveIndefinite":       g.localFaker.AdjectiveIndefinite,
 | 
			
		||||
		"fakeit_Pronoun":                   g.localFaker.Pronoun,
 | 
			
		||||
		"fakeit_PronounPersonal":           g.localFaker.PronounPersonal,
 | 
			
		||||
		"fakeit_PronounObject":             g.localFaker.PronounObject,
 | 
			
		||||
		"fakeit_PronounPossessive":         g.localFaker.PronounPossessive,
 | 
			
		||||
		"fakeit_PronounReflective":         g.localFaker.PronounReflective,
 | 
			
		||||
		"fakeit_PronounDemonstrative":      g.localFaker.PronounDemonstrative,
 | 
			
		||||
		"fakeit_PronounInterrogative":      g.localFaker.PronounInterrogative,
 | 
			
		||||
		"fakeit_PronounRelative":           g.localFaker.PronounRelative,
 | 
			
		||||
		"fakeit_Connective":                g.localFaker.Connective,
 | 
			
		||||
		"fakeit_ConnectiveTime":            g.localFaker.ConnectiveTime,
 | 
			
		||||
		"fakeit_ConnectiveComparative":     g.localFaker.ConnectiveComparative,
 | 
			
		||||
		"fakeit_ConnectiveComplaint":       g.localFaker.ConnectiveComplaint,
 | 
			
		||||
		"fakeit_ConnectiveListing":         g.localFaker.ConnectiveListing,
 | 
			
		||||
		"fakeit_ConnectiveCasual":          g.localFaker.ConnectiveCasual,
 | 
			
		||||
		"fakeit_ConnectiveExamplify":       g.localFaker.ConnectiveExamplify,
 | 
			
		||||
		"fakeit_Word":                      g.localFaker.Word,
 | 
			
		||||
		"fakeit_Sentence":                  g.localFaker.Sentence,
 | 
			
		||||
		"fakeit_Paragraph":                 g.localFaker.Paragraph,
 | 
			
		||||
		"fakeit_LoremIpsumWord":            g.localFaker.LoremIpsumWord,
 | 
			
		||||
		"fakeit_LoremIpsumSentence":        g.localFaker.LoremIpsumSentence,
 | 
			
		||||
		"fakeit_LoremIpsumParagraph":       g.localFaker.LoremIpsumParagraph,
 | 
			
		||||
		"fakeit_Question":                  g.localFaker.Question,
 | 
			
		||||
		"fakeit_Quote":                     g.localFaker.Quote,
 | 
			
		||||
		"fakeit_Phrase":                    g.localFaker.Phrase,
 | 
			
		||||
 | 
			
		||||
		// FakeIt / Foods
 | 
			
		||||
		"fakeit_Fruit":     g.localFaker.Fruit,
 | 
			
		||||
		"fakeit_Vegetable": g.localFaker.Vegetable,
 | 
			
		||||
		"fakeit_Breakfast": g.localFaker.Breakfast,
 | 
			
		||||
		"fakeit_Lunch":     g.localFaker.Lunch,
 | 
			
		||||
		"fakeit_Dinner":    g.localFaker.Dinner,
 | 
			
		||||
		"fakeit_Snack":     g.localFaker.Snack,
 | 
			
		||||
		"fakeit_Dessert":   g.localFaker.Dessert,
 | 
			
		||||
 | 
			
		||||
		// FakeIt / Misc
 | 
			
		||||
		"fakeit_Bool":      g.localFaker.Bool,
 | 
			
		||||
		"fakeit_UUID":      g.localFaker.UUID,
 | 
			
		||||
		"fakeit_FlipACoin": g.localFaker.FlipACoin,
 | 
			
		||||
 | 
			
		||||
		// FakeIt / Colors
 | 
			
		||||
		"fakeit_Color":      g.localFaker.Color,
 | 
			
		||||
		"fakeit_HexColor":   g.localFaker.HexColor,
 | 
			
		||||
		"fakeit_RGBColor":   g.localFaker.RGBColor,
 | 
			
		||||
		"fakeit_SafeColor":  g.localFaker.SafeColor,
 | 
			
		||||
		"fakeit_NiceColors": g.localFaker.NiceColors,
 | 
			
		||||
 | 
			
		||||
		// FakeIt / Internet
 | 
			
		||||
		"fakeit_URL":                  g.localFaker.URL,
 | 
			
		||||
		"fakeit_DomainName":           g.localFaker.DomainName,
 | 
			
		||||
		"fakeit_DomainSuffix":         g.localFaker.DomainSuffix,
 | 
			
		||||
		"fakeit_IPv4Address":          g.localFaker.IPv4Address,
 | 
			
		||||
		"fakeit_IPv6Address":          g.localFaker.IPv6Address,
 | 
			
		||||
		"fakeit_MacAddress":           g.localFaker.MacAddress,
 | 
			
		||||
		"fakeit_HTTPStatusCode":       g.localFaker.HTTPStatusCode,
 | 
			
		||||
		"fakeit_HTTPStatusCodeSimple": g.localFaker.HTTPStatusCodeSimple,
 | 
			
		||||
		"fakeit_LogLevel":             g.localFaker.LogLevel,
 | 
			
		||||
		"fakeit_HTTPMethod":           g.localFaker.HTTPMethod,
 | 
			
		||||
		"fakeit_HTTPVersion":          g.localFaker.HTTPVersion,
 | 
			
		||||
		"fakeit_UserAgent":            g.localFaker.UserAgent,
 | 
			
		||||
		"fakeit_ChromeUserAgent":      g.localFaker.ChromeUserAgent,
 | 
			
		||||
		"fakeit_FirefoxUserAgent":     g.localFaker.FirefoxUserAgent,
 | 
			
		||||
		"fakeit_OperaUserAgent":       g.localFaker.OperaUserAgent,
 | 
			
		||||
		"fakeit_SafariUserAgent":      g.localFaker.SafariUserAgent,
 | 
			
		||||
 | 
			
		||||
		// FakeIt / HTML
 | 
			
		||||
		"fakeit_InputName": g.localFaker.InputName,
 | 
			
		||||
 | 
			
		||||
		// FakeIt / Date/Time
 | 
			
		||||
		"fakeit_Date":           g.localFaker.Date,
 | 
			
		||||
		"fakeit_PastDate":       g.localFaker.PastDate,
 | 
			
		||||
		"fakeit_FutureDate":     g.localFaker.FutureDate,
 | 
			
		||||
		"fakeit_DateRange":      g.localFaker.DateRange,
 | 
			
		||||
		"fakeit_NanoSecond":     g.localFaker.NanoSecond,
 | 
			
		||||
		"fakeit_Second":         g.localFaker.Second,
 | 
			
		||||
		"fakeit_Minute":         g.localFaker.Minute,
 | 
			
		||||
		"fakeit_Hour":           g.localFaker.Hour,
 | 
			
		||||
		"fakeit_Month":          g.localFaker.Month,
 | 
			
		||||
		"fakeit_MonthString":    g.localFaker.MonthString,
 | 
			
		||||
		"fakeit_Day":            g.localFaker.Day,
 | 
			
		||||
		"fakeit_WeekDay":        g.localFaker.WeekDay,
 | 
			
		||||
		"fakeit_Year":           g.localFaker.Year,
 | 
			
		||||
		"fakeit_TimeZone":       g.localFaker.TimeZone,
 | 
			
		||||
		"fakeit_TimeZoneAbv":    g.localFaker.TimeZoneAbv,
 | 
			
		||||
		"fakeit_TimeZoneFull":   g.localFaker.TimeZoneFull,
 | 
			
		||||
		"fakeit_TimeZoneOffset": g.localFaker.TimeZoneOffset,
 | 
			
		||||
		"fakeit_TimeZoneRegion": g.localFaker.TimeZoneRegion,
 | 
			
		||||
 | 
			
		||||
		// FakeIt / Payment
 | 
			
		||||
		"fakeit_Price":             g.localFaker.Price,
 | 
			
		||||
		"fakeit_CreditCardCvv":     g.localFaker.CreditCardCvv,
 | 
			
		||||
		"fakeit_CreditCardExp":     g.localFaker.CreditCardExp,
 | 
			
		||||
		"fakeit_CreditCardNumber":  g.localFaker.CreditCardNumber,
 | 
			
		||||
		"fakeit_CreditCardType":    g.localFaker.CreditCardType,
 | 
			
		||||
		"fakeit_CurrencyLong":      g.localFaker.CurrencyLong,
 | 
			
		||||
		"fakeit_CurrencyShort":     g.localFaker.CurrencyShort,
 | 
			
		||||
		"fakeit_AchRouting":        g.localFaker.AchRouting,
 | 
			
		||||
		"fakeit_AchAccount":        g.localFaker.AchAccount,
 | 
			
		||||
		"fakeit_BitcoinAddress":    g.localFaker.BitcoinAddress,
 | 
			
		||||
		"fakeit_BitcoinPrivateKey": g.localFaker.BitcoinPrivateKey,
 | 
			
		||||
 | 
			
		||||
		// FakeIt / Finance
 | 
			
		||||
		"fakeit_Cusip": g.localFaker.Cusip,
 | 
			
		||||
		"fakeit_Isin":  g.localFaker.Isin,
 | 
			
		||||
 | 
			
		||||
		// FakeIt / Company
 | 
			
		||||
		"fakeit_BS":            g.localFaker.BS,
 | 
			
		||||
		"fakeit_Blurb":         g.localFaker.Blurb,
 | 
			
		||||
		"fakeit_BuzzWord":      g.localFaker.BuzzWord,
 | 
			
		||||
		"fakeit_Company":       g.localFaker.Company,
 | 
			
		||||
		"fakeit_CompanySuffix": g.localFaker.CompanySuffix,
 | 
			
		||||
		"fakeit_JobDescriptor": g.localFaker.JobDescriptor,
 | 
			
		||||
		"fakeit_JobLevel":      g.localFaker.JobLevel,
 | 
			
		||||
		"fakeit_JobTitle":      g.localFaker.JobTitle,
 | 
			
		||||
		"fakeit_Slogan":        g.localFaker.Slogan,
 | 
			
		||||
 | 
			
		||||
		// FakeIt / Hacker
 | 
			
		||||
		"fakeit_HackerAbbreviation": g.localFaker.HackerAbbreviation,
 | 
			
		||||
		"fakeit_HackerAdjective":    g.localFaker.HackerAdjective,
 | 
			
		||||
		"fakeit_HackerNoun":         g.localFaker.HackerNoun,
 | 
			
		||||
		"fakeit_HackerPhrase":       g.localFaker.HackerPhrase,
 | 
			
		||||
		"fakeit_HackerVerb":         g.localFaker.HackerVerb,
 | 
			
		||||
 | 
			
		||||
		// FakeIt / Hipster
 | 
			
		||||
		"fakeit_HipsterWord":      g.localFaker.HipsterWord,
 | 
			
		||||
		"fakeit_HipsterSentence":  g.localFaker.HipsterSentence,
 | 
			
		||||
		"fakeit_HipsterParagraph": g.localFaker.HipsterParagraph,
 | 
			
		||||
 | 
			
		||||
		// FakeIt / App
 | 
			
		||||
		"fakeit_AppName":    g.localFaker.AppName,
 | 
			
		||||
		"fakeit_AppVersion": g.localFaker.AppVersion,
 | 
			
		||||
		"fakeit_AppAuthor":  g.localFaker.AppAuthor,
 | 
			
		||||
 | 
			
		||||
		// FakeIt / Animal
 | 
			
		||||
		"fakeit_PetName":    g.localFaker.PetName,
 | 
			
		||||
		"fakeit_Animal":     g.localFaker.Animal,
 | 
			
		||||
		"fakeit_AnimalType": g.localFaker.AnimalType,
 | 
			
		||||
		"fakeit_FarmAnimal": g.localFaker.FarmAnimal,
 | 
			
		||||
		"fakeit_Cat":        g.localFaker.Cat,
 | 
			
		||||
		"fakeit_Dog":        g.localFaker.Dog,
 | 
			
		||||
		"fakeit_Bird":       g.localFaker.Bird,
 | 
			
		||||
 | 
			
		||||
		// FakeIt / Emoji
 | 
			
		||||
		"fakeit_Emoji":            g.localFaker.Emoji,
 | 
			
		||||
		"fakeit_EmojiDescription": g.localFaker.EmojiDescription,
 | 
			
		||||
		"fakeit_EmojiCategory":    g.localFaker.EmojiCategory,
 | 
			
		||||
		"fakeit_EmojiAlias":       g.localFaker.EmojiAlias,
 | 
			
		||||
		"fakeit_EmojiTag":         g.localFaker.EmojiTag,
 | 
			
		||||
 | 
			
		||||
		// FakeIt / Language
 | 
			
		||||
		"fakeit_Language":             g.localFaker.Language,
 | 
			
		||||
		"fakeit_LanguageAbbreviation": g.localFaker.LanguageAbbreviation,
 | 
			
		||||
		"fakeit_ProgrammingLanguage":  g.localFaker.ProgrammingLanguage,
 | 
			
		||||
 | 
			
		||||
		// FakeIt / Number
 | 
			
		||||
		"fakeit_Number":       g.localFaker.Number,
 | 
			
		||||
		"fakeit_Int":          g.localFaker.Int,
 | 
			
		||||
		"fakeit_IntN":         g.localFaker.IntN,
 | 
			
		||||
		"fakeit_IntRange":     g.localFaker.IntRange,
 | 
			
		||||
		"fakeit_RandomInt":    g.localFaker.RandomInt,
 | 
			
		||||
		"fakeit_Int8":         g.localFaker.Int8,
 | 
			
		||||
		"fakeit_Int16":        g.localFaker.Int16,
 | 
			
		||||
		"fakeit_Int32":        g.localFaker.Int32,
 | 
			
		||||
		"fakeit_Int64":        g.localFaker.Int64,
 | 
			
		||||
		"fakeit_Uint":         g.localFaker.Uint,
 | 
			
		||||
		"fakeit_UintN":        g.localFaker.UintN,
 | 
			
		||||
		"fakeit_UintRange":    g.localFaker.UintRange,
 | 
			
		||||
		"fakeit_RandomUint":   g.localFaker.RandomUint,
 | 
			
		||||
		"fakeit_Uint8":        g.localFaker.Uint8,
 | 
			
		||||
		"fakeit_Uint16":       g.localFaker.Uint16,
 | 
			
		||||
		"fakeit_Uint32":       g.localFaker.Uint32,
 | 
			
		||||
		"fakeit_Uint64":       g.localFaker.Uint64,
 | 
			
		||||
		"fakeit_Float32":      g.localFaker.Float32,
 | 
			
		||||
		"fakeit_Float32Range": g.localFaker.Float32Range,
 | 
			
		||||
		"fakeit_Float64":      g.localFaker.Float64,
 | 
			
		||||
		"fakeit_Float64Range": g.localFaker.Float64Range,
 | 
			
		||||
		"fakeit_HexUint":      g.localFaker.HexUint,
 | 
			
		||||
 | 
			
		||||
		// FakeIt / String
 | 
			
		||||
		"fakeit_Digit":   g.localFaker.Digit,
 | 
			
		||||
		"fakeit_DigitN":  g.localFaker.DigitN,
 | 
			
		||||
		"fakeit_Letter":  g.localFaker.Letter,
 | 
			
		||||
		"fakeit_LetterN": g.localFaker.LetterN,
 | 
			
		||||
		"fakeit_LetterNN": func(min, max uint) string {
 | 
			
		||||
			return g.localFaker.LetterN(g.localFaker.UintRange(min, max))
 | 
			
		||||
		},
 | 
			
		||||
		"fakeit_Lexify":   g.localFaker.Lexify,
 | 
			
		||||
		"fakeit_Numerify": g.localFaker.Numerify,
 | 
			
		||||
		"fakeit_RandomString": func(values ...string) string {
 | 
			
		||||
			return g.localFaker.RandomString(values)
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		// FakeIt / Celebrity
 | 
			
		||||
		"fakeit_CelebrityActor":    g.localFaker.CelebrityActor,
 | 
			
		||||
		"fakeit_CelebrityBusiness": g.localFaker.CelebrityBusiness,
 | 
			
		||||
		"fakeit_CelebritySport":    g.localFaker.CelebritySport,
 | 
			
		||||
 | 
			
		||||
		// FakeIt / Minecraft
 | 
			
		||||
		"fakeit_MinecraftOre":             g.localFaker.MinecraftOre,
 | 
			
		||||
		"fakeit_MinecraftWood":            g.localFaker.MinecraftWood,
 | 
			
		||||
		"fakeit_MinecraftArmorTier":       g.localFaker.MinecraftArmorTier,
 | 
			
		||||
		"fakeit_MinecraftArmorPart":       g.localFaker.MinecraftArmorPart,
 | 
			
		||||
		"fakeit_MinecraftWeapon":          g.localFaker.MinecraftWeapon,
 | 
			
		||||
		"fakeit_MinecraftTool":            g.localFaker.MinecraftTool,
 | 
			
		||||
		"fakeit_MinecraftDye":             g.localFaker.MinecraftDye,
 | 
			
		||||
		"fakeit_MinecraftFood":            g.localFaker.MinecraftFood,
 | 
			
		||||
		"fakeit_MinecraftAnimal":          g.localFaker.MinecraftAnimal,
 | 
			
		||||
		"fakeit_MinecraftVillagerJob":     g.localFaker.MinecraftVillagerJob,
 | 
			
		||||
		"fakeit_MinecraftVillagerStation": g.localFaker.MinecraftVillagerStation,
 | 
			
		||||
		"fakeit_MinecraftVillagerLevel":   g.localFaker.MinecraftVillagerLevel,
 | 
			
		||||
		"fakeit_MinecraftMobPassive":      g.localFaker.MinecraftMobPassive,
 | 
			
		||||
		"fakeit_MinecraftMobNeutral":      g.localFaker.MinecraftMobNeutral,
 | 
			
		||||
		"fakeit_MinecraftMobHostile":      g.localFaker.MinecraftMobHostile,
 | 
			
		||||
		"fakeit_MinecraftMobBoss":         g.localFaker.MinecraftMobBoss,
 | 
			
		||||
		"fakeit_MinecraftBiome":           g.localFaker.MinecraftBiome,
 | 
			
		||||
		"fakeit_MinecraftWeather":         g.localFaker.MinecraftWeather,
 | 
			
		||||
 | 
			
		||||
		// FakeIt / Book
 | 
			
		||||
		"fakeit_BookTitle":  g.localFaker.BookTitle,
 | 
			
		||||
		"fakeit_BookAuthor": g.localFaker.BookAuthor,
 | 
			
		||||
		"fakeit_BookGenre":  g.localFaker.BookGenre,
 | 
			
		||||
 | 
			
		||||
		// FakeIt / Movie
 | 
			
		||||
		"fakeit_MovieName":  g.localFaker.MovieName,
 | 
			
		||||
		"fakeit_MovieGenre": g.localFaker.MovieGenre,
 | 
			
		||||
 | 
			
		||||
		// FakeIt / Error
 | 
			
		||||
		"fakeit_Error":           g.localFaker.Error,
 | 
			
		||||
		"fakeit_ErrorDatabase":   g.localFaker.ErrorDatabase,
 | 
			
		||||
		"fakeit_ErrorGRPC":       g.localFaker.ErrorGRPC,
 | 
			
		||||
		"fakeit_ErrorHTTP":       g.localFaker.ErrorHTTP,
 | 
			
		||||
		"fakeit_ErrorHTTPClient": g.localFaker.ErrorHTTPClient,
 | 
			
		||||
		"fakeit_ErrorHTTPServer": g.localFaker.ErrorHTTPServer,
 | 
			
		||||
		"fakeit_ErrorRuntime":    g.localFaker.ErrorRuntime,
 | 
			
		||||
 | 
			
		||||
		// FakeIt / School
 | 
			
		||||
		"fakeit_School": g.localFaker.School,
 | 
			
		||||
 | 
			
		||||
		// FakeIt / Song
 | 
			
		||||
		"fakeit_SongName":   g.localFaker.SongName,
 | 
			
		||||
		"fakeit_SongArtist": g.localFaker.SongArtist,
 | 
			
		||||
		"fakeit_SongGenre":  g.localFaker.SongGenre,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user