Skip to content

Commit

Permalink
add upload files support (#51)
Browse files Browse the repository at this point in the history
To upload file with test request you need to specify names and paths
 of files.
Don't forget specify Content-Type:
 > Content-Type: multipart/form-data

Example:

 - name: "upload-files"
   method: POST
   form:
       files:
         file1: "testdata/upload-files/file1.txt"
         file2: "testdata/upload-files/file2.log"
   headers:
     # case-sensitive, can be omitted
     Content-Type: multipart/form-data
   response:
     200: |
       {
         "status": "OK"
       }
  • Loading branch information
JustSkiv authored Jul 9, 2020
1 parent 005b332 commit 44040d9
Show file tree
Hide file tree
Showing 11 changed files with 331 additions and 13 deletions.
25 changes: 25 additions & 0 deletions README-ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,31 @@ password=private_password

env-файл, например, удобно использовать, когда нужно вынести из теста приватную информацию (пароли, ключи и т.п.)


### Загрузка файлов

В тестовом запросе можно загружать файлы. Для этого нужно указать тип запроса - POST и заголовок:

> Content-Type: multipart/form-data

Пример:

```yaml
- name: "upload-files"
method: POST
form:
files:
file1: "testdata/upload-files/file1.txt"
file2: "testdata/upload-files/file2.log"
headers:
Content-Type: multipart/form-data # case-sensitive, can be omitted
response:
200: |
{
"status": "OK"
}
```

### Фикстуры

Чтобы наполнить базу перед тестом, используются файлы с фикстурами.
Expand Down
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,30 @@ password=private_password

env-file can be convenient to hide sensitive information from a test (passwords, keys, etc.)

### Files uploading

You can upload files in test request. For this you must specify the type of request - POST and header:

> Content-Type: multipart/form-data

Example:

```yaml
- name: "upload-files"
method: POST
form:
files:
file1: "testdata/upload-files/file1.txt"
file2: "testdata/upload-files/file2.log"
headers:
Content-Type: multipart/form-data # case-sensitive, can be omitted
response:
200: |
{
"status": "OK"
}
```

### Fixtures

To seed the DB before the test, gonkey uses fixture files.
Expand Down
8 changes: 8 additions & 0 deletions models/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ type TestInterface interface {
BeforeScriptTimeout() int
Cookies() map[string]string
Headers() map[string]string
ContentType() string
GetForm() *Form
DbQueryString() string
DbResponseJson() []string
GetVariables() map[string]string
Expand All @@ -28,6 +30,7 @@ type TestInterface interface {
SetMethod(string)
SetPath(string)
SetRequest(string)
SetForm(form *Form)
SetResponses(map[int]string)
SetHeaders(map[string]string)

Expand All @@ -40,6 +43,11 @@ type TestInterface interface {
Clone() TestInterface
}

// TODO: add support for form fields
type Form struct {
Files map[string]string `json:"files" yaml:"files"`
}

type Summary struct {
Success bool
Failed int
Expand Down
150 changes: 137 additions & 13 deletions runner/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@ package runner
import (
"bytes"
"crypto/tls"
"fmt"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"

"github.com/lamoda/gonkey/models"
Expand All @@ -32,31 +36,151 @@ func newClient() (*http.Client, error) {
}, nil
}

func newRequest(host string, test models.TestInterface) (*http.Request, error) {
func newRequest(host string, test models.TestInterface) (req *http.Request, err error) {

if test.GetForm() != nil {
req, err = newMultipartRequest(host, test)
if err != nil {
return nil, err
}
} else {
req, err = newCommonRequest(host, test)
if err != nil {
return nil, err
}
}

for k, v := range test.Cookies() {
req.AddCookie(&http.Cookie{Name: k, Value: v})
}

return req, nil
}

func newMultipartRequest(host string, test models.TestInterface) (*http.Request, error) {

if test.ContentType() != "" && test.ContentType() != "multipart/form-data" {
return nil, fmt.Errorf(
"test has unexpected Content-Type: %s, expected: multipart/form-data",
test.ContentType(),
)
}

var b bytes.Buffer
w := multipart.NewWriter(&b)

params, err := url.ParseQuery(test.GetRequest())
if err != nil {
return nil, err
}

err = addFields(params, w)
if err != nil {
return nil, err
}

err = addFiles(test.GetForm().Files, w)
if err != nil {
return nil, err
}

_ = w.Close()

req, err := request(test, &b, host)
if err != nil {
return nil, err
}

// this is necessary, it will contain boundary
req.Header.Set("Content-Type", w.FormDataContentType())

return req, nil

}

func addFiles(files map[string]string, w *multipart.Writer) error {
for name, path := range files {

err := addFile(path, w, name)
if err != nil {
return err
}

}

return nil
}

func addFile(path string, w *multipart.Writer, name string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer func() { _ = f.Close() }()

fw, err := w.CreateFormFile(name, filepath.Base(f.Name()))
if err != nil {
return err
}

if _, err = io.Copy(fw, f); err != nil {
return err
}
return nil
}

func addFields(params url.Values, w *multipart.Writer) error {
for k, vv := range params {
for _, v := range vv {
fw, err := w.CreateFormField(k)
if err != nil {
return err
}

_, err = fw.Write([]byte(v))
if err != nil {
return err
}
}
}

return nil
}

func newCommonRequest(host string, test models.TestInterface) (*http.Request, error) {

body, err := test.ToJSON()
if err != nil {
return nil, err
}
request, err := http.NewRequest(

req, err := request(test, bytes.NewBuffer(body), host)
if err != nil {
return nil, err
}

if req.Header.Get("Content-Type") == "" {
req.Header.Set("Content-Type", "application/json")
}

return req, nil
}

func request(test models.TestInterface, b *bytes.Buffer, host string) (*http.Request, error) {

req, err := http.NewRequest(
strings.ToUpper(test.GetMethod()),
host+test.Path()+test.ToQuery(),
bytes.NewBuffer(body),
b,
)
if err != nil {
return nil, err
}
for k, v := range test.Headers() {
request.Header.Add(k, v)
}
for k, v := range test.Cookies() {
request.AddCookie(&http.Cookie{Name: k, Value: v})
}

if request.Header.Get("Content-Type") == "" {
request.Header.Set("Content-Type", "application/json")
for k, v := range test.Headers() {
req.Header.Add(k, v)
}

return request, nil
return req, nil
}

func actualRequestBody(req *http.Request) string {
Expand Down
66 changes: 66 additions & 0 deletions runner/runner_upload_file_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package runner

import (
"encoding/json"
"io/ioutil"
"net/http"
"net/http/httptest"
"path/filepath"
"testing"

"github.com/stretchr/testify/require"
)

func TestUploadFiles(t *testing.T) {
srv := testServerUpload(t)
defer srv.Close()

// TODO: refactor RunWithTesting() for testing negative scenario (when tests has expected errors)
RunWithTesting(t, &RunWithTestingParams{
Server: srv,
TestsDir: filepath.Join("testdata", "upload-files"),
})
}

type response struct {
Status string `json:"status"`
File1Name string `json:"file_1_name"`
File1Content string `json:"file_1_content"`
File2Name string `json:"file_2_name"`
File2Content string `json:"file_2_content"`
}

func testServerUpload(t *testing.T) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

resp := response{
Status: "OK",
}

resp.File1Name, resp.File1Content = formFile(t, r, "file1")
resp.File2Name, resp.File2Content = formFile(t, r, "file2")

respData, err := json.Marshal(resp)
require.NoError(t, err)

w.Header().Set("Content-Type", "application/json")

_, err = w.Write(respData)
require.NoError(t, err)

return
}))
}

func formFile(t *testing.T, r *http.Request, field string) (string, string) {

file, header, err := r.FormFile(field)
require.NoError(t, err)

defer func() { _ = file.Close() }()

contents, err := ioutil.ReadAll(file)
require.NoError(t, err)

return header.Filename, string(contents)
}
1 change: 1 addition & 0 deletions runner/testdata/upload-files/file1.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
file1_some_text
1 change: 1 addition & 0 deletions runner/testdata/upload-files/file2.log
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
file2_content
35 changes: 35 additions & 0 deletions runner/testdata/upload-files/upload-files.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
- name: "upload-files: with correct Content-Type"
method: POST
form:
files:
file1: "testdata/upload-files/file1.txt"
file2: "testdata/upload-files/file2.log"
headers:
Content-Type: multipart/form-data
response:
200: |
{
"status": "OK",
"file_1_name": "file1.txt",
"file_1_content": "file1_some_text",
"file_2_name": "file2.log",
"file_2_content": "file2_content"
}
- name: "upload-files: with empty Content-Type"
method: POST
form:
files:
file1: "testdata/upload-files/file1.txt"
file2: "testdata/upload-files/file2.log"
response:
200: |
{
"status": "OK",
"file_1_name": "file1.txt",
"file_1_content": "file1_some_text",
"file_2_name": "file2.log",
"file_2_content": "file2_content"
}
# TODO: test with incorrect Content-Type
Loading

0 comments on commit 44040d9

Please sign in to comment.