pax_global_header 0000666 0000000 0000000 00000000064 14434507621 0014520 g ustar 00root root 0000000 0000000 52 comment=9a0549477baaebb9fb03991b7dc123b06d8c2ff4
golang-github-jeremija-gosubmit-0.2.7/ 0000775 0000000 0000000 00000000000 14434507621 0017670 5 ustar 00root root 0000000 0000000 golang-github-jeremija-gosubmit-0.2.7/.gitignore 0000664 0000000 0000000 00000000320 14434507621 0021653 0 ustar 00root root 0000000 0000000 ### ./Go.gitignore ###
# Binaries for programs and plugins
*.exe
*.dll
*.so
*.dylib
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
golang-github-jeremija-gosubmit-0.2.7/.travis.yml 0000664 0000000 0000000 00000000077 14434507621 0022005 0 ustar 00root root 0000000 0000000 language: go
go:
- "1.13"
script:
- go test ./... -cover
golang-github-jeremija-gosubmit-0.2.7/LICENSE 0000664 0000000 0000000 00000002035 14434507621 0020675 0 ustar 00root root 0000000 0000000 Copyright 2022 Jerko Steiner
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
golang-github-jeremija-gosubmit-0.2.7/Makefile 0000664 0000000 0000000 00000000137 14434507621 0021331 0 ustar 00root root 0000000 0000000 coverage:
go test ./... -coverprofile=coverage.out
report:
go tool cover -html=coverage.out
golang-github-jeremija-gosubmit-0.2.7/README.md 0000664 0000000 0000000 00000005066 14434507621 0021156 0 ustar 00root root 0000000 0000000 # gosubmit
[](https://travis-ci.com/jeremija/gosubmit)
# Description
Docs are available here: https://godoc.org/github.com/jeremija/gosubmit
Helps filling out plain html forms during testing. Will automatically take the
existing values from the form so there is no need to manually set things like
csrf tokens. Alerts about missing required fields, or when pattern validation
does not match. See [example_test.go](example_test.go) for a full example.
```golang
package gosubmit_test
import (
// TODO import app
. "github.com/jeremija/gosubmit"
"net/http"
"net/http/httptest"
)
func TestLogin(t *testing.T) {
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/auth/login", nil)
app.ServeHTTP(w, r)
r, err := ParseResponse(w.Result(), r.URL).FirstForm().NewTestRequest(
Set("username", "user"),
Set("password", "password"),
)
if err != nil {
t.Fatalf("Error filling form: %s", err)
}
w := httptest.NewRecorder()
app.ServeHTTP(w, r)
if code := w.Result().StatusCode; code != http.StatusOK {
t.Errorf("Expected status ok but got %d", code)
}
}
```
Autofilling of all required input fields is supported:
```golang
r, err := ParseResponse(w.Result(), r.URL).FirstForm().NewTestRequest(
Autofill(),
)
```
Elements that include a pattern attribute for validation will not be autofilled
and have to be filled in manually. For example:
```golang
r, err := ParseResponse(w.Result(), r.URL).FirstForm().NewTestRequest(
Autofill(),
Set("validatedURL", "https://www.example.com"),
)
```
# Testing Helpers
To avoid checking for error in tests manually when creating a new test request
, the value of `t *testing.T` can be provided:
```golang
r := ParseResponse(w.Result(), r.URL).FirstForm().Testing(t).NewTestRequest(
Autofill(),
Set("validatedURL", "https://www.example.com"),
)
```
In case of any errors, the `t.Fatalf()` function will be called. `t.Helper()`
is used appropriately to ensure line numbers reported by `go test` are correct.
# Supported Elements
- `input[type=checkbox]`
- `input[type=date]`
- `input[type=email]`
- `input[type=hidden]`
- `input[type=number]`
- `input[type=radio]`
- `input[type=text]`
- `input[type=url]`
- `textarea`
- `select`
- `select[multiple]`
- `button[type=submit]` with name and value
- `input[type=submit]` with name and value
If an input element is not on this list, it will default to text input.
# Who Is Using `gosubmit`?
- [rondomoon](https://rondomoon.com)
- [rondoBB](https://bb.rondo.dev)
- _Your app here - send a PR!_
# License
MIT
golang-github-jeremija-gosubmit-0.2.7/example_test.go 0000664 0000000 0000000 00000004005 14434507621 0022710 0 ustar 00root root 0000000 0000000 package gosubmit_test
import (
"net/http"
"net/http/httptest"
"regexp"
"testing"
. "github.com/jeremija/gosubmit"
)
func Serve(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
username := r.FormValue("username")
password := r.FormValue("password")
csrf := r.FormValue("csrf")
if csrf == "1234" && username == "user" && password == "pass" {
w.Write([]byte("Welcome, " + username))
return
}
w.WriteHeader(http.StatusForbidden)
default:
w.Write([]byte(`
`))
}
}
var mux *http.ServeMux
func init() {
mux = http.NewServeMux()
mux.HandleFunc("/auth/login", Serve)
}
func TestLogin(t *testing.T) {
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/auth/login", nil)
mux.ServeHTTP(w, r)
form := ParseResponse(w.Result(), r.URL).FirstForm()
for _, test := range []struct {
code int
pass string
}{
{http.StatusForbidden, "invalid-password"},
{http.StatusOK, "pass"},
} {
t.Run("password_"+test.pass, func(t *testing.T) {
w := httptest.NewRecorder()
r, err := form.NewTestRequest(
Set("username", "user"),
Set("password", test.pass),
)
if err != nil {
t.Fatalf("Error filling in form: %s", err)
}
mux.ServeHTTP(w, r)
if code := w.Result().StatusCode; code != test.code {
t.Fatalf("Expected status code %d, but got %d", test.code, code)
}
})
}
}
func TestFill_invalid(t *testing.T) {
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/auth/login", nil)
mux.ServeHTTP(w, r)
form := ParseResponse(w.Result(), r.URL).FirstForm()
_, err := form.NewTestRequest(
Set("invalid-field", "user"),
)
re := regexp.MustCompile("Cannot find input name='invalid-field'")
if err == nil || !re.MatchString(err.Error()) {
t.Errorf("Expected an error to match %s but got %s", re, err)
}
}
golang-github-jeremija-gosubmit-0.2.7/fill.go 0000664 0000000 0000000 00000021256 14434507621 0021153 0 ustar 00root root 0000000 0000000 package gosubmit
import (
"bytes"
"context"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/http/httptest"
"net/url"
)
type Option func(f *filler) error
type multipartFile struct {
Contents []byte
Name string
}
type filler struct {
context context.Context
form Form
values url.Values
url string
method string
clicked bool
multipart map[string][]multipartFile
required map[string]struct{}
isMultipart bool
}
// Creates a new form filler. It is preferred to use Form.Fill() instead.
func newFiller(form Form, opts []Option) (f *filler, err error) {
values := make(url.Values)
f = &filler{
form: form,
values: values,
required: make(map[string]struct{}),
multipart: make(map[string][]multipartFile),
isMultipart: form.ContentType == ContentTypeMultipart,
}
f.prefill(form.Inputs)
err = f.apply(opts)
return
}
func (f *filler) apply(opts []Option) (err error) {
for _, opt := range opts {
err = opt(f)
if err != nil {
return
}
}
return
}
func (f *filler) prefill(inputs Inputs) {
for name, input := range inputs {
if input.Required() {
f.required[name] = struct{}{}
}
if input.Multipart() {
continue
}
for _, value := range input.Values() {
f.values.Add(name, value)
}
}
}
func (f *filler) createRequest(test bool, method string, url string, body io.Reader) (r *http.Request, err error) {
defer func() {
p := recover()
if p != nil {
err = fmt.Errorf("Caught panic when creating request: %s", p)
}
return
}()
if test {
r = httptest.NewRequest(method, url, body)
return
}
ctx := f.context
if ctx == nil {
ctx = context.Background()
}
r, err = http.NewRequestWithContext(ctx, method, url, body)
return
}
func (f *filler) NewTestRequest() (*http.Request, error) {
return f.prepareRequest(true)
}
func (f *filler) NewRequest() (*http.Request, error) {
return f.prepareRequest(false)
}
// Builds a form depeding on the enctype and creates a new test request.
func (f *filler) prepareRequest(test bool) (r *http.Request, err error) {
form := f.form
switch form.Method {
case http.MethodPost:
if !f.isMultipart {
body, err := f.BuildPost()
if err != nil {
return nil, err
}
r, err = f.createRequest(test, "POST", form.URL, bytes.NewReader(body))
if err != nil {
err = fmt.Errorf("Error creating post request: %w", err)
return nil, err
}
r.Header.Add("Content-Type", form.ContentType)
} else {
boundary, body, err := f.BuildMultipart()
if err != nil {
return nil, err
}
r, err = f.createRequest(test, "POST", form.URL, bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("Error creating multipart request: %w", err)
}
r.Header.Add("Content-Type",
fmt.Sprintf("%s; boundary=%s", ContentTypeMultipart, boundary))
}
default:
query, err := f.BuildGet()
if err != nil {
return nil, err
}
url := fmt.Sprintf("%s?%s", form.URL, query)
r, err = f.createRequest(test, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("Error creating get request: %w", err)
}
}
return
}
// Builds form body for a multipart request
func (f *filler) BuildMultipart() (boundary string, data []byte, err error) {
if err = f.validateForm(); err != nil {
return "", nil, err
}
var body bytes.Buffer
writer := multipart.NewWriter(&body)
boundary = writer.Boundary()
defer func() {
e := writer.Close()
if e != nil && err == nil {
err = fmt.Errorf("Error closing multipart writer: %s", e)
}
data = body.Bytes()
}()
for field, files := range f.multipart {
for _, file := range files {
w, e := writer.CreateFormFile(field, file.Name)
if e != nil {
err = fmt.Errorf("Error creating multipart for field '%s': %w", field, e)
return
}
_, err = w.Write(file.Contents)
if err != nil {
err = fmt.Errorf("Error writing multipart data for field '%s': %w", field, err)
return
}
}
}
for field, values := range f.values {
for _, value := range values {
err := writer.WriteField(field, value)
if err != nil {
err = fmt.Errorf("Error writing multipart string for field '%s': %w", field, err)
}
}
}
return
}
func WithContext(ctx context.Context) Option {
return func(f *filler) error {
f.context = ctx
return nil
}
}
// // Adds value to all empty required fields.
// func (f *filler) AutoFill(defaultValue string) {
// for requiredField, _ := range f.required {
// value := f.values.Get(requiredField)
// if value != "" {
// continue
// }
// f.Set(requiredField, fmt.Sprintf("%s-%s", requiredField, defaultValue))
// }
// }
// Validates the form (for a plain form request). No need to call this method
// directly if BuildForm or NewTestRequest are used.
func (f *filler) validateForm() error {
for requiredField, _ := range f.required {
hasTextValue := f.values.Get(requiredField) != ""
hasByteValue := false
if f.isMultipart {
_, hasByteValue = f.multipart[requiredField]
}
if !hasTextValue && !hasByteValue {
return fmt.Errorf("Required field '%s' has no value", requiredField)
}
}
return nil
}
// Build values for form submission
func (f *filler) BuildGet() (params string, err error) {
err = f.validateForm()
params = f.values.Encode()
return params, err
}
// Build form body for post request
func (f *filler) BuildPost() (body []byte, err error) {
err = f.validateForm()
body = []byte(f.values.Encode())
return
}
func AutoFill() Option {
return func(f *filler) error {
for requiredField, _ := range f.required {
value := f.values.Get(requiredField)
input := f.form.Inputs[requiredField]
if value == "" {
add := false
for _, value := range input.AutoFill() {
var opt Option
if input.Type() == InputTypeFile {
opt = AddFile(requiredField, "auto-filename", []byte(value))
} else {
opt = setOrAdd(requiredField, value, add)
}
if err := opt(f); err != nil {
return err
}
add = true
}
}
}
return nil
}
}
// Adds the submit buttons name=value combination to the form submission.
// Useful when there are two or more buttons on a form and their values
// make a difference on how the server's going to process the form data.
func Click(buttonValue string) Option {
return func(f *filler) error {
if f.clicked == true {
return fmt.Errorf("Already clicked on one button")
}
ok := false
var b Button
for _, button := range f.form.Buttons {
if button.Value == buttonValue {
ok = true
b = button
break
}
}
if !ok {
return fmt.Errorf("Cannot find button with value: '%s'", buttonValue)
}
f.clicked = true
f.values.Set(b.Name, b.Value)
return nil
}
}
// Deletes a field from the form. Useful to remove preselected values
func Reset(name string) Option {
return func(f *filler) error {
f.values.Del(name)
delete(f.multipart, name)
return nil
}
}
// Adds a name=value pair to the form. If there is an empty value it will
// be replaced, otherwise a second value will be added, but only if the
// element supports multiple values, like checkboxes or