Skip to content

Commit

Permalink
Merge pull request #68 from studio-b12/dev-v1.3.0
Browse files Browse the repository at this point in the history
v1.3.0
  • Loading branch information
zekroTJA authored Sep 16, 2024
2 parents cd099e4 + f163d3a commit a664d28
Show file tree
Hide file tree
Showing 23 changed files with 501 additions and 71 deletions.
14 changes: 6 additions & 8 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,12 @@

# New Features

- Added a new request block `[FormData]`, which enables to specify `multipart/form-data` request payloads. [Here](https://studio-b12.github.io/goat/goatfile/requests/formdata.html) you can read the documentation. [#55]
- Added raw data identifier `$` to directly use raw data from a response as request body. See the example in the
documentation for more information. [#66, #67]

- Added a new request option [`followredirects`](https://studio-b12.github.io/goat/goatfile/requests/options.html#followredirects). This is `true` by default, but can be set to `false` if redirects should not be followed transparently. [#61]
- Added a new flag `--retry-failed` (or `-R` for short). When a batch execution fails, failed files are now saved to a
temporary file. After that, goat can be executed again with only the flag which will re-run only the failed files.

- Added a new script builtin [`assert_eq`](https://studio-b12.github.io/goat/scripting/builtins.html#assert_eq), where you can pass two values which are compared and output for better error clarification.
<!-- # Minor Changes and Bug Fixes -->

- Added a new script builtin [`jq`](https://studio-b12.github.io/goat/scripting/builtins.html#jq), to perform JQ commands on any object in script blocks.

# Minor Changes and Bug Fixes

- Fixed an issue when parsing file descriptors on Windows systems.
<!-- - Fixed an issue when parsing file descriptors on Windows systems. -->
70 changes: 65 additions & 5 deletions cmd/goat/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import (
"bufio"
"crypto/tls"
"fmt"
"github.com/davecgh/go-spew/spew"
"github.com/studio-b12/goat/pkg/errs"
"io/fs"
"net/http"
"os"
"path"
"path/filepath"
"runtime"
"strings"
Expand Down Expand Up @@ -41,10 +43,11 @@ type Args struct {
NoColor bool `arg:"--no-color,env:GOATARG_NOCOLOR" help:"Supress colored log output"`
Params []string `arg:"-p,--params,separate,env:GOATARG_PARAMS" help:"Params file location(s)"`
Profile []string `arg:"-P,--profile,separate,env:GOATARG_PROFILE" help:"Select a profile from your home config"`
ReducedErrors bool `arg:"--reduced-errors,-R,env:GOATARG_REDUCEDERRORS" help:"Hide template errors in teardown steps"`
ReducedErrors bool `arg:"-R,--reduced-errors,env:GOATARG_REDUCEDERRORS" help:"Hide template errors in teardown steps"`
Secure bool `arg:"--secure,env:GOATARG_SECURE" help:"Validate TLS certificates"`
Silent bool `arg:"-s,--silent,env:GOATARG_SILENT" help:"Disables all logging output"`
Skip []string `arg:"--skip,separate,env:GOATARG_SKIP" help:"Section(s) to be skipped during execution"`
RetryFailed bool `arg:"--retry-failed,env:GOATARG_RETRYFAILED" help:"Retry files which have failed in the previous run"`
}

func main() {
Expand Down Expand Up @@ -76,7 +79,24 @@ func main() {
return
}

if len(args.Goatfile) == 0 {
goatfiles := args.Goatfile

if args.RetryFailed {
failed, err := loadLastFailedFiles()
if err != nil {
log.Fatal().Err(err).Msg("Failed loading last failed files")
return
}
if len(failed) == 0 {
log.Fatal().Msg("No failed files have been recorded in previous runs")
return
}

goatfiles = failed
spew.Dump(goatfiles)
}

if len(goatfiles) == 0 {
argParser.Fail("Goatfile must be specified.")
return
}
Expand Down Expand Up @@ -125,7 +145,7 @@ func main() {

log.Debug().Msgf("Initial Params\n%s", state)

res, err := exec.Execute(args.Goatfile, state, !args.ReducedErrors)
res, err := exec.Execute(goatfiles, state, !args.ReducedErrors)
res.Log()
if err != nil {
if args.ReducedErrors {
Expand All @@ -134,7 +154,11 @@ func main() {

entry := log.Fatal().Err(err)

if batchErr, ok := err.(executor.BatchResultError); ok {
if batchErr, ok := errs.As[*executor.BatchResultError](err); ok {
if sErr := storeLastFailedFiles(batchErr.FailedFiles()); err != nil {
log.Error().Err(sErr).Msg("failed storing latest failed files")
}

coloredMessages := batchErr.ErrorMessages()
for i, p := range coloredMessages {
coloredMessages[i] = clr.Print(clr.Format(p, clr.ColorFGRed))
Expand Down Expand Up @@ -227,10 +251,46 @@ func filterTeardownParamErrors(err error) error {
newErrors = append(newErrors, e)
}
return newErrors
case executor.BatchResultError:
case *executor.BatchResultError:
tErr.Inner = filterTeardownParamErrors(tErr.Inner).(errs.Errors)
return tErr
default:
return err
}
}

const lastFailedRunFileName = "goat_last_failed_run"

func storeLastFailedFiles(paths []string) error {
failedRunPath := path.Join(os.TempDir(), lastFailedRunFileName)
f, err := os.Create(failedRunPath)
if err != nil {
return err
}
defer f.Close()

_, err = f.WriteString(strings.Join(paths, "\n"))
return err
}

func loadLastFailedFiles() (paths []string, err error) {
failedRunPath := path.Join(os.TempDir(), lastFailedRunFileName)
f, err := os.Open(failedRunPath)
if os.IsNotExist(err) {
return nil, nil
}
if err != nil {
return nil, err
}
defer f.Close()

scanner := bufio.NewScanner(f)
for scanner.Scan() {
if err = scanner.Err(); err != nil {
return nil, err
}
paths = append(paths, scanner.Text())
}

return paths, nil
}
31 changes: 30 additions & 1 deletion docs/book/src/goatfile/requests/body.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
> *BlockDelimiter* :
> `` ``` ``
## Example
## Examples

````toml
[Body]
Expand All @@ -32,6 +32,16 @@
```
````

````toml
[Body]
@path/to/some/file
````

````toml
[Body]
$someVar
````

## Explanation

Define the data to be transmitted with the request.
Expand All @@ -53,3 +63,22 @@ Example:
}
```
````

### File Descriptor
With the `@` prefix in front of a file path, it is possible to send a file as a request body.

Example:
````toml
[Body]
@path/to/some/file
````

### Raw Descriptor
With the `$` prefix in front of a variable name it is possible to pass raw byte arrays to the request body.
This can e.g. be used to send a `response.BodyRaw` to another endpoint.

Example:
````toml
[Body]
$someVar
````
9 changes: 9 additions & 0 deletions docs/book/src/goatfile/requests/formdata.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
someString = "some string"
someInt = 42
someFile = @files/goat.png:image/png
someRaw = $someVar:application/zip
```

## Explanation
Expand All @@ -23,6 +24,9 @@ The format of the contents of this block is [`TOML`](https://toml.io/).
The file's content type can be specified after the file descriptor, separated by a colon (`:`). Otherwise,
the content type will default to `application/octet-stream`.

The `$` sign can be used in `FormData` values to directly pass byte array variables. This can e.g. be used when a
previous `response.BodyRaw` should be send to another endpoint as a `FormData` request.

> The example from above results in the following body content.
> ```
> --e8b9253313450dbcf0d09df1a0f3ff36dd00e888339415a239ce167f279c
Expand All @@ -39,6 +43,11 @@ the content type will default to `application/octet-stream`.
>
> some string
> --e8b9253313450dbcf0d09df1a0f3ff36dd00e888339415a239ce167f279c--
> Content-Disposition: form-data; name="someRaw" ; filename="binary-data"
> Content-Type: application/zip
>
> <raw byte data>
> --e8b9253313450dbcf0d09df1a0f3ff36dd00e888339415a239ce167f279c--
> ```
Template parameters in parameter values will be substituted.
45 changes: 41 additions & 4 deletions e2e/cases/formdata/test.goat
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ someInt = 42
file = @test-file.txt:text/plain

[Script]
const [contentType, boundaryKV] = response.Body.headers["Content-Type"][0]
var [contentType, boundaryKV] = response.Body.headers["Content-Type"][0]
.split(';')
.map(v => v.trim());
assert(contentType, 'multipart/form-data', `Invalid Content-Type header: ${contentType}`);
const boundary = boundaryKV.split('=')[1];
const bodyText = response.Body.body_string.replaceAll('\r', '');
const bodyValues = bodyText
var boundary = boundaryKV.split('=')[1];
var bodyText = response.Body.body_string.replaceAll('\r', '');
var bodyValues = bodyText
.substr(0, bodyText.length - 3)
.split(`--${boundary}`)
.map(v => v.trim())
Expand All @@ -34,3 +34,40 @@ const bodyValues = bodyText
assert(v[1] === 'Content-Type: text/plain', `Invalid content type: ${v[1]}`);
assert(v[2] === 'This is a test file!', `Invalid value: ${v[2]}`);
}


---
// Paramter Substitution
POST {{.instance}}/

[PreScript]
var someOtherString = "some other string";
var someOtherInt = 43;

[FormData]
someOtherString = "{{.someOtherString}}"
someOtherInt = {{.someOtherInt}}

[Script]
var [contentType, boundaryKV] = response.Body.headers["Content-Type"][0]
.split(';')
.map(v => v.trim());
assert(contentType, 'multipart/form-data', `Invalid Content-Type header: ${contentType}`);
var boundary = boundaryKV.split('=')[1];
var bodyText = response.Body.body_string.replaceAll('\r', '');
var bodyValues = bodyText
.substr(0, bodyText.length - 3)
.split(`--${boundary}`)
.map(v => v.trim())
.filter(v => !!v)
.map(v => v.split('\n').map(l => l.trim()).filter(l => !!l));
{
const v = bodyValues.find(v => v[0].includes('name="someOtherString"'));
assert(v[0] === 'Content-Disposition: form-data; name="someOtherString"', `Invalid header: ${v[0]}`);
assert(v[1] === 'some other string', `[0] Invalid value: ${v[1]}`);
}
{
const v = bodyValues.find(v => v[0].includes('name="someOtherInt"'));
assert(v[0] === 'Content-Disposition: form-data; name="someOtherInt"', `Invalid header: ${v[0]}`);
assert(v[1] === '43', `Invalid value: ${v[1]}`);
}
7 changes: 7 additions & 0 deletions e2e/cases/rawdata-failing/body_fail.goat
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
POST {{.instance}}/

[PreScript]
var number = 123.5

[Body]
$number
7 changes: 7 additions & 0 deletions e2e/cases/rawdata-failing/formdata_fail.goat
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
POST {{.instance}}/

[PreScript]
var text = "test text"

[FormData]
file = $text
11 changes: 11 additions & 0 deletions e2e/cases/rawdata-failing/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
goat body_fail.goat && {
echo "should have failed but didn't"
exit 1
}

goat formdata_fail.goat && {
echo "should have failed but didn't"
exit 1
}

exit 0
33 changes: 33 additions & 0 deletions e2e/cases/rawdata/body.goat
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
POST {{.instance}}/

[PreScript]
var binaryBody = new Uint8Array(4);
binaryBody[0] = 0x67;
binaryBody[1] = 0x6f;
binaryBody[2] = 0x61;
binaryBody[3] = 0x74;

[Body]
$binaryBody

[Script]
assert_eq(response.Body.body_string, '\x67\x6f\x61\x74');

---
// With manually set Content-Type
POST {{.instance}}/

[PreScript]
var binaryBody = new Uint8Array(4);
binaryBody[0] = 0x67;
binaryBody[1] = 0x6f;
binaryBody[2] = 0x61;
binaryBody[3] = 0x74;

[Body]
$binaryBody:text/plain

[Script]
assert_eq(response.Body.body_string, '\x67\x6f\x61\x74');
var contentType = response.Body.headers["Content-Type"];
assert(contentType, 'text/plain', `Invalid Content-Type header: ${contentType}`);
Loading

0 comments on commit a664d28

Please sign in to comment.