mirror of
https://github.com/aykhans/dodo.git
synced 2025-07-01 16:07:49 +00:00
Compare commits
44 Commits
Author | SHA1 | Date | |
---|---|---|---|
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 |
4
.github/workflows/golangci-lint.yml
vendored
4
.github/workflows/golangci-lint.yml
vendored
@ -19,7 +19,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
go-version: stable
|
go-version: stable
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v6
|
uses: golangci/golangci-lint-action@v7
|
||||||
with:
|
with:
|
||||||
version: v1.64
|
version: v2.0.2
|
||||||
args: --timeout=10m --config=.golangci.yml
|
args: --timeout=10m --config=.golangci.yml
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
|
version: "2"
|
||||||
|
|
||||||
run:
|
run:
|
||||||
go: "1.24"
|
go: "1.24"
|
||||||
concurrency: 8
|
concurrency: 8
|
||||||
timeout: 10m
|
timeout: 10m
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
disable-all: true
|
default: none
|
||||||
enable:
|
enable:
|
||||||
- asasalint
|
- asasalint
|
||||||
- asciicheck
|
- asciicheck
|
||||||
- errcheck
|
- errcheck
|
||||||
- gofmt
|
|
||||||
- goimports
|
|
||||||
- gomodguard
|
- gomodguard
|
||||||
- goprintffuncname
|
- goprintffuncname
|
||||||
- govet
|
- govet
|
||||||
@ -21,7 +21,13 @@ linters:
|
|||||||
- prealloc
|
- prealloc
|
||||||
- reassign
|
- reassign
|
||||||
- staticcheck
|
- staticcheck
|
||||||
- typecheck
|
|
||||||
- unconvert
|
- unconvert
|
||||||
- unused
|
- unused
|
||||||
- whitespace
|
- whitespace
|
||||||
|
|
||||||
|
settings:
|
||||||
|
staticcheck:
|
||||||
|
checks:
|
||||||
|
- "all"
|
||||||
|
- "-S1002"
|
||||||
|
- "-ST1000"
|
||||||
|
@ -6,7 +6,7 @@ COPY go.mod go.sum ./
|
|||||||
RUN go mod download
|
RUN go mod download
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN go build -ldflags "-s -w" -o dodo
|
RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o dodo
|
||||||
|
|
||||||
FROM gcr.io/distroless/static-debian12:latest
|
FROM gcr.io/distroless/static-debian12:latest
|
||||||
|
|
||||||
|
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
|
|
238
README.md
238
README.md
@ -3,6 +3,26 @@
|
|||||||
<img width="30%" height="30%" src="https://ftp.aykhans.me/web/client/pubshares/VzPtSHS7yPQT7ngoZzZSNU/browse?path=%2Fdodo.png">
|
<img width="30%" height="30%" src="https://ftp.aykhans.me/web/client/pubshares/VzPtSHS7yPQT7ngoZzZSNU/browse?path=%2Fdodo.png">
|
||||||
</p>
|
</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
|
## Table of Contents
|
||||||
|
|
||||||
- [Installation](#installation)
|
- [Installation](#installation)
|
||||||
@ -12,10 +32,11 @@
|
|||||||
- [Usage](#usage)
|
- [Usage](#usage)
|
||||||
- [1. CLI Usage](#1-cli-usage)
|
- [1. CLI Usage](#1-cli-usage)
|
||||||
- [2. Config File Usage](#2-config-file-usage)
|
- [2. Config File Usage](#2-config-file-usage)
|
||||||
- [2.1 JSON Example](#21-json-example)
|
- [2.1 YAML/YML Example](#21-yamlyml-example)
|
||||||
- [2.2 YAML/YML Example](#22-yamlyml-example)
|
- [2.2 JSON Example](#22-json-example)
|
||||||
- [3. CLI & Config File Combination](#3-cli--config-file-combination)
|
- [3. CLI & Config File Combination](#3-cli--config-file-combination)
|
||||||
- [Config Parameters Reference](#config-parameters-reference)
|
- [Config Parameters Reference](#config-parameters-reference)
|
||||||
|
- [Template Functions](#template-functions)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@ -73,7 +94,84 @@ docker run --rm -i aykhans/dodo -u https://example.com -m GET -d 10 -r 1000 -o 1
|
|||||||
|
|
||||||
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:
|
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 JSON Example
|
#### 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
|
```jsonc
|
||||||
{
|
{
|
||||||
@ -84,6 +182,7 @@ Send 1000 GET requests to https://example.com with 10 parallel dodos (threads),
|
|||||||
"dodos": 10,
|
"dodos": 10,
|
||||||
"requests": 1000,
|
"requests": 1000,
|
||||||
"duration": "250s",
|
"duration": "250s",
|
||||||
|
"skip_verify": false,
|
||||||
|
|
||||||
"params": [
|
"params": [
|
||||||
// A random value will be selected from the list for first "key1" param on each request
|
// A random value will be selected from the list for first "key1" param on each request
|
||||||
@ -152,82 +251,6 @@ 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
|
docker run --rm -i aykhans/dodo -f https://example.com/config.json
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 2.2 YAML/YML Example
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
method: "GET"
|
|
||||||
url: "https://example.com"
|
|
||||||
yes: false
|
|
||||||
timeout: "800ms"
|
|
||||||
dodos: 10
|
|
||||||
requests: 1000
|
|
||||||
duration: "250s"
|
|
||||||
|
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. CLI & Config File Combination
|
### 3. CLI & Config File Combination
|
||||||
|
|
||||||
CLI arguments override config file values:
|
CLI arguments override config file values:
|
||||||
@ -242,13 +265,15 @@ With Docker:
|
|||||||
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
|
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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
You can find more usage examples in the [EXAMPLES.md](./EXAMPLES.md) file.
|
||||||
|
|
||||||
## Config Parameters Reference
|
## Config Parameters Reference
|
||||||
|
|
||||||
If `Headers`, `Params`, `Cookies`, `Body`, or `Proxy` fields have multiple values, each request will choose a random value from the list.
|
If `Headers`, `Params`, `Cookies`, `Body`, or `Proxy` fields have multiple values, each request will choose a random value from the list.
|
||||||
|
|
||||||
| Parameter | config file | CLI Flag | CLI Short Flag | Type | Description | Default |
|
| 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 | - |
|
| 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 |
|
| Yes | yes | -yes | -y | Boolean | Answer yes to all questions | false |
|
||||||
| URL | url | -url | -u | String | URL to send the request to | - |
|
| URL | url | -url | -u | String | URL to send the request to | - |
|
||||||
| Method | method | -method | -m | String | HTTP method | GET |
|
| Method | method | -method | -m | String | HTTP method | GET |
|
||||||
@ -261,3 +286,56 @@ If `Headers`, `Params`, `Cookies`, `Body`, or `Proxy` fields have multiple value
|
|||||||
| Cookies | cookies | -cookie | -c | [{String: String OR [String]}] | Request cookies | - |
|
| Cookies | cookies | -cookie | -c | [{String: String OR [String]}] | Request cookies | - |
|
||||||
| Body | body | -body | -b | String OR [String] | Request body or list of request bodies | - |
|
| 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 | - |
|
| 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
|
|
@ -6,6 +6,7 @@
|
|||||||
"dodos": 8,
|
"dodos": 8,
|
||||||
"requests": 1000,
|
"requests": 1000,
|
||||||
"duration": "10s",
|
"duration": "10s",
|
||||||
|
"skip_verify": false,
|
||||||
|
|
||||||
"params": [
|
"params": [
|
||||||
{ "key1": ["value1", "value2", "value3", "value4"] },
|
{ "key1": ["value1", "value2", "value3", "value4"] },
|
||||||
|
@ -5,6 +5,7 @@ timeout: "5s"
|
|||||||
dodos: 8
|
dodos: 8
|
||||||
requests: 1000
|
requests: 1000
|
||||||
duration: "10s"
|
duration: "10s"
|
||||||
|
skip_verify: false
|
||||||
|
|
||||||
params:
|
params:
|
||||||
- key1: ["value1", "value2", "value3", "value4"]
|
- key1: ["value1", "value2", "value3", "value4"]
|
||||||
|
@ -31,7 +31,7 @@ Usage with all flags:
|
|||||||
-p "param1=value1" -param "param2=value2" \
|
-p "param1=value1" -param "param2=value2" \
|
||||||
-c "cookie1=value1" -cookie "cookie2=value2" \
|
-c "cookie1=value1" -cookie "cookie2=value2" \
|
||||||
-x "http://proxy.example.com:8080" -proxy "socks5://proxy2.example.com:8080" \
|
-x "http://proxy.example.com:8080" -proxy "socks5://proxy2.example.com:8080" \
|
||||||
-y
|
-skip-verify -y
|
||||||
|
|
||||||
Flags:
|
Flags:
|
||||||
-h, -help help for dodo
|
-h, -help help for dodo
|
||||||
@ -48,7 +48,8 @@ Flags:
|
|||||||
-p, -param [string] Parameter for the request (e.g. "key1=value1")
|
-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")
|
-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) {
|
func (config *Config) ReadCLI() (types.ConfigFile, error) {
|
||||||
flag.Usage = func() {
|
flag.Usage = func() {
|
||||||
@ -58,6 +59,7 @@ func (config *Config) ReadCLI() (types.ConfigFile, error) {
|
|||||||
DefaultDodosCount,
|
DefaultDodosCount,
|
||||||
DefaultTimeout,
|
DefaultTimeout,
|
||||||
DefaultMethod,
|
DefaultMethod,
|
||||||
|
DefaultSkipVerify,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,6 +67,7 @@ func (config *Config) ReadCLI() (types.ConfigFile, error) {
|
|||||||
version = false
|
version = false
|
||||||
configFile = ""
|
configFile = ""
|
||||||
yes = false
|
yes = false
|
||||||
|
skipVerify = false
|
||||||
method = ""
|
method = ""
|
||||||
url types.RequestURL
|
url types.RequestURL
|
||||||
dodosCount = uint(0)
|
dodosCount = uint(0)
|
||||||
@ -83,6 +86,8 @@ func (config *Config) ReadCLI() (types.ConfigFile, error) {
|
|||||||
flag.BoolVar(&yes, "yes", false, "Answer yes to all questions")
|
flag.BoolVar(&yes, "yes", false, "Answer yes to all questions")
|
||||||
flag.BoolVar(&yes, "y", 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, "method", "", "HTTP Method")
|
||||||
flag.StringVar(&method, "m", "", "HTTP Method")
|
flag.StringVar(&method, "m", "", "HTTP Method")
|
||||||
|
|
||||||
@ -149,6 +154,8 @@ func (config *Config) ReadCLI() (types.ConfigFile, error) {
|
|||||||
config.Timeout = &types.Timeout{Duration: timeout}
|
config.Timeout = &types.Timeout{Duration: timeout}
|
||||||
case "yes", "y":
|
case "yes", "y":
|
||||||
config.Yes = utils.ToPtr(yes)
|
config.Yes = utils.ToPtr(yes)
|
||||||
|
case "skip-verify":
|
||||||
|
config.SkipVerify = utils.ToPtr(skipVerify)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
135
config/config.go
135
config/config.go
@ -1,12 +1,15 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/aykhans/dodo/types"
|
"github.com/aykhans/dodo/types"
|
||||||
@ -15,7 +18,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
VERSION string = "0.6.2"
|
VERSION string = "0.7.1"
|
||||||
DefaultUserAgent string = "Dodo/" + VERSION
|
DefaultUserAgent string = "Dodo/" + VERSION
|
||||||
DefaultMethod string = "GET"
|
DefaultMethod string = "GET"
|
||||||
DefaultTimeout time.Duration = time.Second * 10
|
DefaultTimeout time.Duration = time.Second * 10
|
||||||
@ -23,6 +26,7 @@ const (
|
|||||||
DefaultRequestCount uint = 0
|
DefaultRequestCount uint = 0
|
||||||
DefaultDuration time.Duration = 0
|
DefaultDuration time.Duration = 0
|
||||||
DefaultYes bool = false
|
DefaultYes bool = false
|
||||||
|
DefaultSkipVerify bool = false
|
||||||
)
|
)
|
||||||
|
|
||||||
var SupportedProxySchemes []string = []string{"http", "socks5", "socks5h"}
|
var SupportedProxySchemes []string = []string{"http", "socks5", "socks5h"}
|
||||||
@ -35,6 +39,7 @@ type RequestConfig struct {
|
|||||||
RequestCount uint
|
RequestCount uint
|
||||||
Duration time.Duration
|
Duration time.Duration
|
||||||
Yes bool
|
Yes bool
|
||||||
|
SkipVerify bool
|
||||||
Params types.Params
|
Params types.Params
|
||||||
Headers types.Headers
|
Headers types.Headers
|
||||||
Cookies types.Cookies
|
Cookies types.Cookies
|
||||||
@ -51,6 +56,7 @@ func NewRequestConfig(conf *Config) *RequestConfig {
|
|||||||
RequestCount: *conf.RequestCount,
|
RequestCount: *conf.RequestCount,
|
||||||
Duration: conf.Duration.Duration,
|
Duration: conf.Duration.Duration,
|
||||||
Yes: *conf.Yes,
|
Yes: *conf.Yes,
|
||||||
|
SkipVerify: *conf.SkipVerify,
|
||||||
Params: conf.Params,
|
Params: conf.Params,
|
||||||
Headers: conf.Headers,
|
Headers: conf.Headers,
|
||||||
Cookies: conf.Cookies,
|
Cookies: conf.Cookies,
|
||||||
@ -122,6 +128,8 @@ func (rc *RequestConfig) Print() {
|
|||||||
t.AppendRow(table.Row{"Proxy", rc.Proxies.String()})
|
t.AppendRow(table.Row{"Proxy", rc.Proxies.String()})
|
||||||
t.AppendSeparator()
|
t.AppendSeparator()
|
||||||
t.AppendRow(table.Row{"Body", rc.Body.String()})
|
t.AppendRow(table.Row{"Body", rc.Body.String()})
|
||||||
|
t.AppendSeparator()
|
||||||
|
t.AppendRow(table.Row{"Skip Verify", rc.SkipVerify})
|
||||||
|
|
||||||
t.Render()
|
t.Render()
|
||||||
}
|
}
|
||||||
@ -134,6 +142,7 @@ type Config struct {
|
|||||||
RequestCount *uint `json:"requests" yaml:"requests"`
|
RequestCount *uint `json:"requests" yaml:"requests"`
|
||||||
Duration *types.Duration `json:"duration" yaml:"duration"`
|
Duration *types.Duration `json:"duration" yaml:"duration"`
|
||||||
Yes *bool `json:"yes" yaml:"yes"`
|
Yes *bool `json:"yes" yaml:"yes"`
|
||||||
|
SkipVerify *bool `json:"skip_verify" yaml:"skip_verify"`
|
||||||
Params types.Params `json:"params" yaml:"params"`
|
Params types.Params `json:"params" yaml:"params"`
|
||||||
Headers types.Headers `json:"headers" yaml:"headers"`
|
Headers types.Headers `json:"headers" yaml:"headers"`
|
||||||
Cookies types.Cookies `json:"cookies" yaml:"cookies"`
|
Cookies types.Cookies `json:"cookies" yaml:"cookies"`
|
||||||
@ -145,20 +154,20 @@ func NewConfig() *Config {
|
|||||||
return &Config{}
|
return &Config{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) Validate() []error {
|
func (config *Config) Validate() []error {
|
||||||
var errs []error
|
var errs []error
|
||||||
if utils.IsNilOrZero(c.URL) {
|
if utils.IsNilOrZero(config.URL) {
|
||||||
errs = append(errs, errors.New("request URL is required"))
|
errs = append(errs, errors.New("request URL is required"))
|
||||||
} else {
|
} else {
|
||||||
if c.URL.Scheme == "" {
|
if config.URL.Scheme == "" {
|
||||||
c.URL.Scheme = "http"
|
config.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"))
|
errs = append(errs, errors.New("request URL scheme must be http or https"))
|
||||||
}
|
}
|
||||||
|
|
||||||
urlParams := types.Params{}
|
urlParams := types.Params{}
|
||||||
for key, values := range c.URL.Query() {
|
for key, values := range config.URL.Query() {
|
||||||
for _, value := range values {
|
for _, value := range values {
|
||||||
urlParams = append(urlParams, types.KeyValue[string, []string]{
|
urlParams = append(urlParams, types.KeyValue[string, []string]{
|
||||||
Key: key,
|
Key: key,
|
||||||
@ -166,24 +175,24 @@ func (c *Config) Validate() []error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.Params = append(urlParams, c.Params...)
|
config.Params = append(urlParams, config.Params...)
|
||||||
c.URL.RawQuery = ""
|
config.URL.RawQuery = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
if utils.IsNilOrZero(c.Method) {
|
if utils.IsNilOrZero(config.Method) {
|
||||||
errs = append(errs, errors.New("request method is required"))
|
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"))
|
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"))
|
errs = append(errs, errors.New("dodos count must be greater than 0"))
|
||||||
}
|
}
|
||||||
if utils.IsNilOrZero(c.Duration) && utils.IsNilOrZero(c.RequestCount) {
|
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"))
|
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() == "" {
|
if proxy.String() == "" {
|
||||||
errs = append(errs, fmt.Errorf("proxies[%d]: proxy cannot be empty", i))
|
errs = append(errs, fmt.Errorf("proxies[%d]: proxy cannot be empty", i))
|
||||||
} else if schema := proxy.Scheme; !slices.Contains(SupportedProxySchemes, schema) {
|
} else if schema := proxy.Scheme; !slices.Contains(SupportedProxySchemes, schema) {
|
||||||
@ -195,6 +204,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
|
return errs
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -220,6 +321,9 @@ func (config *Config) MergeConfig(newConfig *Config) {
|
|||||||
if newConfig.Yes != nil {
|
if newConfig.Yes != nil {
|
||||||
config.Yes = newConfig.Yes
|
config.Yes = newConfig.Yes
|
||||||
}
|
}
|
||||||
|
if newConfig.SkipVerify != nil {
|
||||||
|
config.SkipVerify = newConfig.SkipVerify
|
||||||
|
}
|
||||||
if len(newConfig.Params) != 0 {
|
if len(newConfig.Params) != 0 {
|
||||||
config.Params = newConfig.Params
|
config.Params = newConfig.Params
|
||||||
}
|
}
|
||||||
@ -256,5 +360,8 @@ func (config *Config) SetDefaults() {
|
|||||||
if config.Yes == nil {
|
if config.Yes == nil {
|
||||||
config.Yes = utils.ToPtr(DefaultYes)
|
config.Yes = utils.ToPtr(DefaultYes)
|
||||||
}
|
}
|
||||||
|
if config.SkipVerify == nil {
|
||||||
|
config.SkipVerify = utils.ToPtr(DefaultSkipVerify)
|
||||||
|
}
|
||||||
config.Headers.SetIfNotExists("User-Agent", DefaultUserAgent)
|
config.Headers.SetIfNotExists("User-Agent", DefaultUserAgent)
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ func (config *Config) ReadFile(filePath types.ConfigFile) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to fetch config file from %s", filePath)
|
return fmt.Errorf("failed to fetch config file from %s", filePath)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
data, err = io.ReadAll(io.Reader(resp.Body))
|
data, err = io.ReadAll(io.Reader(resp.Body))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -47,9 +47,10 @@ func (config *Config) ReadFile(filePath types.ConfigFile) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if fileExt == "json" {
|
switch fileExt {
|
||||||
|
case "json":
|
||||||
return parseJSONConfig(data, config)
|
return parseJSONConfig(data, config)
|
||||||
} else if fileExt == "yml" || fileExt == "yaml" {
|
case "yml", "yaml":
|
||||||
return parseYAMLConfig(data, config)
|
return parseYAMLConfig(data, config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
15
go.mod
15
go.mod
@ -1,21 +1,22 @@
|
|||||||
module github.com/aykhans/dodo
|
module github.com/aykhans/dodo
|
||||||
|
|
||||||
go 1.24.0
|
go 1.24.2
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/brianvoe/gofakeit/v7 v7.2.1
|
||||||
github.com/jedib0t/go-pretty/v6 v6.6.7
|
github.com/jedib0t/go-pretty/v6 v6.6.7
|
||||||
github.com/valyala/fasthttp v1.59.0
|
github.com/valyala/fasthttp v1.62.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||||
github.com/klauspost/compress v1.17.11 // indirect
|
github.com/klauspost/compress v1.18.0 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
golang.org/x/net v0.36.0 // indirect
|
golang.org/x/net v0.40.0 // indirect
|
||||||
golang.org/x/sys v0.30.0 // indirect
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
golang.org/x/term v0.29.0 // indirect
|
golang.org/x/term v0.32.0 // indirect
|
||||||
golang.org/x/text v0.22.0 // indirect
|
golang.org/x/text v0.25.0 // indirect
|
||||||
)
|
)
|
||||||
|
26
go.sum
26
go.sum
@ -1,11 +1,13 @@
|
|||||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
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.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||||
|
github.com/brianvoe/gofakeit/v7 v7.2.1 h1:AGojgaaCdgq4Adzrd2uWdbGNDyX6MWNhHdQBraNfOHI=
|
||||||
|
github.com/brianvoe/gofakeit/v7 v7.2.1/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
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/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 h1:m+LbHpm0aIAPLzLbMfn8dc3Ht8MW7lsSO4MPItz/Uuo=
|
||||||
github.com/jedib0t/go-pretty/v6 v6.6.7/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
|
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.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
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 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
@ -17,18 +19,18 @@ 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/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 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
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.62.0 h1:8dKRBX/y2rCzyc6903Zu1+3qN0H/d2MsxPPmVNamiH0=
|
||||||
github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU=
|
github.com/valyala/fasthttp v1.62.0/go.mod h1:FCINgr4GKdKqV8Q0xv8b+UxPV+H/O5nNFo3D+r54Htg=
|
||||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
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.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||||
golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
|
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
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/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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
5
main.go
5
main.go
@ -7,7 +7,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/aykhans/dodo/config"
|
"github.com/aykhans/dodo/config"
|
||||||
"github.com/aykhans/dodo/requests"
|
"github.com/aykhans/dodo/requests"
|
||||||
@ -50,10 +49,6 @@ func main() {
|
|||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
go listenForTermination(func() { cancel() })
|
go listenForTermination(func() { cancel() })
|
||||||
|
|
||||||
if requestConf.Duration > 0 {
|
|
||||||
time.AfterFunc(requestConf.Duration, func() { cancel() })
|
|
||||||
}
|
|
||||||
|
|
||||||
responses, err := requests.Run(ctx, requestConf)
|
responses, err := requests.Run(ctx, requestConf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == types.ErrInterrupt {
|
if err == types.ErrInterrupt {
|
||||||
|
@ -2,6 +2,7 @@ package requests
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/url"
|
"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.
|
// 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.
|
// It can either return clients with proxies or a single client without proxies.
|
||||||
func getClients(
|
func getClients(
|
||||||
ctx context.Context,
|
_ context.Context,
|
||||||
timeout time.Duration,
|
timeout time.Duration,
|
||||||
proxies []url.URL,
|
proxies []url.URL,
|
||||||
maxConns uint,
|
maxConns uint,
|
||||||
URL url.URL,
|
URL url.URL,
|
||||||
|
skipVerify bool,
|
||||||
) []*fasthttp.HostClient {
|
) []*fasthttp.HostClient {
|
||||||
isTLS := URL.Scheme == "https"
|
isTLS := URL.Scheme == "https"
|
||||||
|
|
||||||
@ -41,6 +43,9 @@ func getClients(
|
|||||||
clients = append(clients, &fasthttp.HostClient{
|
clients = append(clients, &fasthttp.HostClient{
|
||||||
MaxConns: int(maxConns),
|
MaxConns: int(maxConns),
|
||||||
IsTLS: isTLS,
|
IsTLS: isTLS,
|
||||||
|
TLSConfig: &tls.Config{
|
||||||
|
InsecureSkipVerify: skipVerify,
|
||||||
|
},
|
||||||
Addr: addr,
|
Addr: addr,
|
||||||
Dial: dialFunc,
|
Dial: dialFunc,
|
||||||
MaxIdleConnDuration: timeout,
|
MaxIdleConnDuration: timeout,
|
||||||
@ -56,6 +61,9 @@ func getClients(
|
|||||||
client := &fasthttp.HostClient{
|
client := &fasthttp.HostClient{
|
||||||
MaxConns: int(maxConns),
|
MaxConns: int(maxConns),
|
||||||
IsTLS: isTLS,
|
IsTLS: isTLS,
|
||||||
|
TLSConfig: &tls.Config{
|
||||||
|
InsecureSkipVerify: skipVerify,
|
||||||
|
},
|
||||||
Addr: URL.Host,
|
Addr: URL.Host,
|
||||||
MaxIdleConnDuration: timeout,
|
MaxIdleConnDuration: timeout,
|
||||||
MaxConnDuration: timeout,
|
MaxConnDuration: timeout,
|
||||||
@ -72,13 +80,19 @@ func getClients(
|
|||||||
func getDialFunc(proxy *url.URL, timeout time.Duration) (fasthttp.DialFunc, error) {
|
func getDialFunc(proxy *url.URL, timeout time.Duration) (fasthttp.DialFunc, error) {
|
||||||
var dialer fasthttp.DialFunc
|
var dialer fasthttp.DialFunc
|
||||||
|
|
||||||
if proxy.Scheme == "socks5" || proxy.Scheme == "socks5h" {
|
switch proxy.Scheme {
|
||||||
|
case "socks5", "socks5h":
|
||||||
dialer = fasthttpproxy.FasthttpSocksDialerDualStack(proxy.String())
|
dialer = fasthttpproxy.FasthttpSocksDialerDualStack(proxy.String())
|
||||||
} else if proxy.Scheme == "http" {
|
case "http":
|
||||||
dialer = fasthttpproxy.FasthttpHTTPDialerDualStackTimeout(proxy.String(), timeout)
|
dialer = fasthttpproxy.FasthttpHTTPDialerDualStackTimeout(proxy.String(), timeout)
|
||||||
} else {
|
default:
|
||||||
return nil, errors.New("unsupported proxy scheme")
|
return nil, errors.New("unsupported proxy scheme")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if dialer == nil {
|
||||||
|
return nil, errors.New("internal error: proxy dialer is nil")
|
||||||
|
}
|
||||||
|
|
||||||
return dialer, nil
|
return dialer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
package requests
|
package requests
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/aykhans/dodo/config"
|
"github.com/aykhans/dodo/config"
|
||||||
@ -21,6 +23,11 @@ type Request struct {
|
|||||||
getRequest RequestGeneratorFunc
|
getRequest RequestGeneratorFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type keyValueGenerator struct {
|
||||||
|
key func() string
|
||||||
|
value func() string
|
||||||
|
}
|
||||||
|
|
||||||
// Send sends the HTTP request using the fasthttp client with a specified timeout.
|
// 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.
|
// 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) {
|
func (r *Request) Send(ctx context.Context, timeout time.Duration) (*fasthttp.Response, error) {
|
||||||
@ -101,26 +108,28 @@ func getRequestGeneratorFunc(
|
|||||||
bodies []string,
|
bodies []string,
|
||||||
localRand *rand.Rand,
|
localRand *rand.Rand,
|
||||||
) RequestGeneratorFunc {
|
) 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)
|
getParams := getKeyValueGeneratorFunc(params, localRand)
|
||||||
getHeaders := getKeyValueGeneratorFunc(headers, localRand)
|
getHeaders := getKeyValueGeneratorFunc(headers, localRand)
|
||||||
getCookies := getKeyValueGeneratorFunc(cookies, localRand)
|
getCookies := getKeyValueGeneratorFunc(cookies, localRand)
|
||||||
|
getBody := getBodyValueFunc(bodies, utils.NewFuncMapGenerator(localRand), localRand)
|
||||||
|
|
||||||
return func() *fasthttp.Request {
|
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(
|
return newFasthttpRequest(
|
||||||
URL,
|
URL,
|
||||||
getParams(),
|
getParams(),
|
||||||
getHeaders(),
|
headers,
|
||||||
getCookies(),
|
getCookies(),
|
||||||
method,
|
method,
|
||||||
getBody(),
|
body,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -199,49 +208,134 @@ func getKeyValueGeneratorFunc[
|
|||||||
keyValueSlice []types.KeyValue[string, []string],
|
keyValueSlice []types.KeyValue[string, []string],
|
||||||
localRand *rand.Rand,
|
localRand *rand.Rand,
|
||||||
) func() T {
|
) func() T {
|
||||||
getKeyValueSlice := []map[string]func() string{}
|
keyValueGenerators := make([]keyValueGenerator, len(keyValueSlice))
|
||||||
isRandom := false
|
|
||||||
|
|
||||||
for _, kv := range keyValueSlice {
|
funcMap := *utils.NewFuncMapGenerator(localRand).GetFuncMap()
|
||||||
valuesLen := len(kv.Value)
|
|
||||||
|
|
||||||
getValueFunc := func() string { return "" }
|
for i, kv := range keyValueSlice {
|
||||||
if valuesLen == 1 {
|
keyValueGenerators[i] = keyValueGenerator{
|
||||||
getValueFunc = func() string { return kv.Value[0] }
|
key: getKeyFunc(kv.Key, funcMap),
|
||||||
} else if valuesLen > 1 {
|
value: getValueFunc(kv.Value, funcMap, localRand),
|
||||||
getValueFunc = utils.RandomValueCycle(kv.Value, localRand)
|
}
|
||||||
isRandom = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getKeyValueSlice = append(
|
|
||||||
getKeyValueSlice,
|
|
||||||
map[string]func() string{kv.Key: getValueFunc},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if isRandom {
|
|
||||||
return func() T {
|
return func() T {
|
||||||
keyValues := make(T, len(getKeyValueSlice))
|
keyValues := make(T, len(keyValueGenerators))
|
||||||
for i, keyValue := range getKeyValueSlice {
|
for i, keyValue := range keyValueGenerators {
|
||||||
for key, value := range keyValue {
|
|
||||||
keyValues[i] = types.KeyValue[string, string]{
|
keyValues[i] = types.KeyValue[string, string]{
|
||||||
Key: key,
|
Key: keyValue.key(),
|
||||||
Value: value(),
|
Value: keyValue.value(),
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return keyValues
|
return keyValues
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
keyValues := make(T, len(getKeyValueSlice))
|
|
||||||
for i, keyValue := range getKeyValueSlice {
|
// getKeyFunc creates a function that processes a key string through Go's template engine.
|
||||||
for key, value := range keyValue {
|
// It takes a key string and a template.FuncMap containing the available template functions.
|
||||||
keyValues[i] = types.KeyValue[string, string]{
|
//
|
||||||
Key: key,
|
// The returned function, when called, will execute the template with the given key and return
|
||||||
Value: value(),
|
// 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()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
return func() T { return keyValues }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,12 +20,19 @@ import (
|
|||||||
// - ctx: The context for managing request lifecycle and cancellation.
|
// - ctx: The context for managing request lifecycle and cancellation.
|
||||||
// - requestConfig: The configuration for the request, including timeout, proxies, and other settings.
|
// - requestConfig: The configuration for the request, including timeout, proxies, and other settings.
|
||||||
func Run(ctx context.Context, requestConfig *config.RequestConfig) (Responses, error) {
|
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(
|
clients := getClients(
|
||||||
ctx,
|
ctx,
|
||||||
requestConfig.Timeout,
|
requestConfig.Timeout,
|
||||||
requestConfig.Proxies,
|
requestConfig.Proxies,
|
||||||
requestConfig.GetMaxConns(fasthttp.DefaultMaxConnsPerHost),
|
requestConfig.GetMaxConns(fasthttp.DefaultMaxConnsPerHost),
|
||||||
requestConfig.URL,
|
requestConfig.URL,
|
||||||
|
requestConfig.SkipVerify,
|
||||||
)
|
)
|
||||||
if clients == nil {
|
if clients == nil {
|
||||||
return nil, types.ErrInterrupt
|
return nil, types.ErrInterrupt
|
||||||
@ -58,14 +65,14 @@ func releaseDodos(
|
|||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
streamWG sync.WaitGroup
|
streamWG sync.WaitGroup
|
||||||
requestCountPerDodo uint
|
requestCountPerDodo uint
|
||||||
dodosCount uint = requestConfig.GetValidDodosCountForRequests()
|
dodosCount = requestConfig.GetValidDodosCountForRequests()
|
||||||
responses = make([][]*Response, dodosCount)
|
responses = make([][]*Response, dodosCount)
|
||||||
increase = make(chan int64, requestConfig.RequestCount)
|
increase = make(chan int64, requestConfig.RequestCount)
|
||||||
)
|
)
|
||||||
|
|
||||||
wg.Add(int(dodosCount))
|
wg.Add(int(dodosCount))
|
||||||
streamWG.Add(1)
|
streamWG.Add(1)
|
||||||
streamCtx, streamCtxCancel := context.WithCancel(context.Background())
|
streamCtx, streamCtxCancel := context.WithCancel(ctx)
|
||||||
|
|
||||||
go streamProgress(streamCtx, &streamWG, requestConfig.RequestCount, "Dodos Working🔥", increase)
|
go streamProgress(streamCtx, &streamWG, requestConfig.RequestCount, "Dodos Working🔥", increase)
|
||||||
|
|
||||||
|
@ -13,12 +13,12 @@ type Body []string
|
|||||||
func (body Body) String() string {
|
func (body Body) String() string {
|
||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
if len(body) == 0 {
|
if len(body) == 0 {
|
||||||
return string(buffer.Bytes())
|
return buffer.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(body) == 1 {
|
if len(body) == 1 {
|
||||||
buffer.WriteString(body[0])
|
buffer.WriteString(body[0])
|
||||||
return string(buffer.Bytes())
|
return buffer.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer.WriteString(text.FgBlue.Sprint("Random") + "[\n")
|
buffer.WriteString(text.FgBlue.Sprint("Random") + "[\n")
|
||||||
@ -41,7 +41,7 @@ func (body Body) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buffer.WriteString("\n]")
|
buffer.WriteString("\n]")
|
||||||
return string(buffer.Bytes())
|
return buffer.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (body *Body) UnmarshalJSON(b []byte) error {
|
func (body *Body) UnmarshalJSON(b []byte) error {
|
||||||
@ -66,7 +66,7 @@ func (body *Body) UnmarshalJSON(b []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (body *Body) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
func (body *Body) UnmarshalYAML(unmarshal func(any) error) error {
|
||||||
var data any
|
var data any
|
||||||
if err := unmarshal(&data); err != nil {
|
if err := unmarshal(&data); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -14,7 +14,7 @@ type Cookies []KeyValue[string, []string]
|
|||||||
func (cookies Cookies) String() string {
|
func (cookies Cookies) String() string {
|
||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
if len(cookies) == 0 {
|
if len(cookies) == 0 {
|
||||||
return string(buffer.Bytes())
|
return buffer.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
indent := " "
|
indent := " "
|
||||||
@ -53,7 +53,7 @@ func (cookies Cookies) String() string {
|
|||||||
buffer.WriteString(",\n" + text.FgGreen.Sprintf("+%d cookies", remainingPairs))
|
buffer.WriteString(",\n" + text.FgGreen.Sprintf("+%d cookies", remainingPairs))
|
||||||
}
|
}
|
||||||
|
|
||||||
return string(buffer.Bytes())
|
return buffer.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cookies *Cookies) AppendByKey(key, value string) {
|
func (cookies *Cookies) AppendByKey(key, value string) {
|
||||||
@ -99,7 +99,7 @@ func (cookies *Cookies) UnmarshalJSON(b []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cookies *Cookies) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
func (cookies *Cookies) UnmarshalYAML(unmarshal func(any) error) error {
|
||||||
var raw []map[string]any
|
var raw []map[string]any
|
||||||
if err := unmarshal(&raw); err != nil {
|
if err := unmarshal(&raw); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -32,10 +32,10 @@ func (duration *Duration) UnmarshalJSON(b []byte) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (duration Duration) MarshalJSON() ([]byte, error) {
|
func (duration Duration) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(duration.Duration.String())
|
return json.Marshal(duration.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (duration *Duration) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
func (duration *Duration) UnmarshalYAML(unmarshal func(any) error) error {
|
||||||
var v any
|
var v any
|
||||||
if err := unmarshal(&v); err != nil {
|
if err := unmarshal(&v); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -14,7 +14,7 @@ type Headers []KeyValue[string, []string]
|
|||||||
func (headers Headers) String() string {
|
func (headers Headers) String() string {
|
||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
if len(headers) == 0 {
|
if len(headers) == 0 {
|
||||||
return string(buffer.Bytes())
|
return buffer.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
indent := " "
|
indent := " "
|
||||||
@ -53,7 +53,7 @@ func (headers Headers) String() string {
|
|||||||
buffer.WriteString(",\n" + text.FgGreen.Sprintf("+%d headers", remainingPairs))
|
buffer.WriteString(",\n" + text.FgGreen.Sprintf("+%d headers", remainingPairs))
|
||||||
}
|
}
|
||||||
|
|
||||||
return string(buffer.Bytes())
|
return buffer.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (headers *Headers) AppendByKey(key, value string) {
|
func (headers *Headers) AppendByKey(key, value string) {
|
||||||
@ -108,7 +108,7 @@ func (headers *Headers) UnmarshalJSON(b []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (headers *Headers) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
func (headers *Headers) UnmarshalYAML(unmarshal func(any) error) error {
|
||||||
var raw []map[string]any
|
var raw []map[string]any
|
||||||
if err := unmarshal(&raw); err != nil {
|
if err := unmarshal(&raw); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -14,7 +14,7 @@ type Params []KeyValue[string, []string]
|
|||||||
func (params Params) String() string {
|
func (params Params) String() string {
|
||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
if len(params) == 0 {
|
if len(params) == 0 {
|
||||||
return string(buffer.Bytes())
|
return buffer.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
indent := " "
|
indent := " "
|
||||||
@ -53,7 +53,7 @@ func (params Params) String() string {
|
|||||||
buffer.WriteString(",\n" + text.FgGreen.Sprintf("+%d params", remainingPairs))
|
buffer.WriteString(",\n" + text.FgGreen.Sprintf("+%d params", remainingPairs))
|
||||||
}
|
}
|
||||||
|
|
||||||
return string(buffer.Bytes())
|
return buffer.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (params *Params) AppendByKey(key, value string) {
|
func (params *Params) AppendByKey(key, value string) {
|
||||||
@ -99,7 +99,7 @@ func (params *Params) UnmarshalJSON(b []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (params *Params) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
func (params *Params) UnmarshalYAML(unmarshal func(any) error) error {
|
||||||
var raw []map[string]any
|
var raw []map[string]any
|
||||||
if err := unmarshal(&raw); err != nil {
|
if err := unmarshal(&raw); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -14,12 +14,12 @@ type Proxies []url.URL
|
|||||||
func (proxies Proxies) String() string {
|
func (proxies Proxies) String() string {
|
||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
if len(proxies) == 0 {
|
if len(proxies) == 0 {
|
||||||
return string(buffer.Bytes())
|
return buffer.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(proxies) == 1 {
|
if len(proxies) == 1 {
|
||||||
buffer.WriteString(proxies[0].String())
|
buffer.WriteString(proxies[0].String())
|
||||||
return string(buffer.Bytes())
|
return buffer.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer.WriteString(text.FgBlue.Sprint("Random") + "[\n")
|
buffer.WriteString(text.FgBlue.Sprint("Random") + "[\n")
|
||||||
@ -42,7 +42,7 @@ func (proxies Proxies) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buffer.WriteString("\n]")
|
buffer.WriteString("\n]")
|
||||||
return string(buffer.Bytes())
|
return buffer.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (proxies *Proxies) UnmarshalJSON(b []byte) error {
|
func (proxies *Proxies) UnmarshalJSON(b []byte) error {
|
||||||
@ -75,7 +75,7 @@ func (proxies *Proxies) UnmarshalJSON(b []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (proxies *Proxies) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
func (proxies *Proxies) UnmarshalYAML(unmarshal func(any) error) error {
|
||||||
var data any
|
var data any
|
||||||
if err := unmarshal(&data); err != nil {
|
if err := unmarshal(&data); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -18,14 +18,14 @@ func (requestURL *RequestURL) UnmarshalJSON(data []byte) error {
|
|||||||
|
|
||||||
parsedURL, err := url.Parse(urlStr)
|
parsedURL, err := url.Parse(urlStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("Request URL is invalid")
|
return errors.New("request URL is invalid")
|
||||||
}
|
}
|
||||||
|
|
||||||
requestURL.URL = *parsedURL
|
requestURL.URL = *parsedURL
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (requestURL *RequestURL) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
func (requestURL *RequestURL) UnmarshalYAML(unmarshal func(any) error) error {
|
||||||
var urlStr string
|
var urlStr string
|
||||||
if err := unmarshal(&urlStr); err != nil {
|
if err := unmarshal(&urlStr); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -33,7 +33,7 @@ func (requestURL *RequestURL) UnmarshalYAML(unmarshal func(interface{}) error) e
|
|||||||
|
|
||||||
parsedURL, err := url.Parse(urlStr)
|
parsedURL, err := url.Parse(urlStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("Request URL is invalid")
|
return errors.New("request URL is invalid")
|
||||||
}
|
}
|
||||||
|
|
||||||
requestURL.URL = *parsedURL
|
requestURL.URL = *parsedURL
|
||||||
|
@ -32,10 +32,10 @@ func (timeout *Timeout) UnmarshalJSON(b []byte) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (timeout Timeout) MarshalJSON() ([]byte, error) {
|
func (timeout Timeout) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(timeout.Duration.String())
|
return json.Marshal(timeout.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (timeout *Timeout) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
func (timeout *Timeout) UnmarshalYAML(unmarshal func(any) error) error {
|
||||||
var v any
|
var v any
|
||||||
if err := unmarshal(&v); err != nil {
|
if err := unmarshal(&v); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -6,9 +6,5 @@ func IsNilOrZero[T comparable](value *T) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var zero T
|
var zero T
|
||||||
if *value == zero {
|
return *value == zero
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
@ -10,32 +10,33 @@ func Flatten[T any](nested [][]*T) []*T {
|
|||||||
return flattened
|
return flattened
|
||||||
}
|
}
|
||||||
|
|
||||||
// RandomValueCycle returns a function that cycles through the provided slice of values
|
// RandomValueCycle returns a function that cycles through the provided values in a pseudo-random order.
|
||||||
// in a random order. Each call to the returned function will yield a value from the slice.
|
// Each value in the input slice will be returned before any value is repeated.
|
||||||
// The order of values is determined by the provided random number generator.
|
// 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.
|
||||||
// The returned function will cycle through the values in a random order until all values
|
// This function is not thread-safe and should not be called concurrently.
|
||||||
// have been returned at least once. After all values have been returned, the function will
|
func RandomValueCycle[T any](values []T, localRand *rand.Rand) func() T {
|
||||||
// reset and start cycling through the values in a random order again.
|
switch valuesLen := len(values); valuesLen {
|
||||||
// The returned function isn't thread-safe and should be used in a single-threaded context.
|
case 0:
|
||||||
func RandomValueCycle[Value any](values []Value, localRand *rand.Rand) func() Value {
|
var zero T
|
||||||
var (
|
return func() T { return zero }
|
||||||
clientsCount int = len(values)
|
case 1:
|
||||||
currentIndex int = localRand.Intn(clientsCount)
|
return func() T { return values[0] }
|
||||||
stopIndex int = currentIndex
|
default:
|
||||||
)
|
currentIndex := localRand.Intn(valuesLen)
|
||||||
|
stopIndex := currentIndex
|
||||||
return func() Value {
|
return func() T {
|
||||||
client := values[currentIndex]
|
value := values[currentIndex]
|
||||||
currentIndex++
|
currentIndex++
|
||||||
if currentIndex == clientsCount {
|
if currentIndex == valuesLen {
|
||||||
currentIndex = 0
|
currentIndex = 0
|
||||||
}
|
}
|
||||||
if currentIndex == stopIndex {
|
if currentIndex == stopIndex {
|
||||||
currentIndex = localRand.Intn(clientsCount)
|
currentIndex = localRand.Intn(valuesLen)
|
||||||
stopIndex = currentIndex
|
stopIndex = currentIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
return client
|
return value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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