pax_global_header00006660000000000000000000000064144345076210014520gustar00rootroot0000000000000052 comment=9a0549477baaebb9fb03991b7dc123b06d8c2ff4 golang-github-jeremija-gosubmit-0.2.7/000077500000000000000000000000001443450762100176705ustar00rootroot00000000000000golang-github-jeremija-gosubmit-0.2.7/.gitignore000066400000000000000000000003201443450762100216530ustar00rootroot00000000000000### ./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.yml000066400000000000000000000000771443450762100220050ustar00rootroot00000000000000language: go go: - "1.13" script: - go test ./... -cover golang-github-jeremija-gosubmit-0.2.7/LICENSE000066400000000000000000000020351443450762100206750ustar00rootroot00000000000000Copyright 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/Makefile000066400000000000000000000001371443450762100213310ustar00rootroot00000000000000coverage: go test ./... -coverprofile=coverage.out report: go tool cover -html=coverage.out golang-github-jeremija-gosubmit-0.2.7/README.md000066400000000000000000000050661443450762100211560ustar00rootroot00000000000000# gosubmit [![Build Status](https://travis-ci.com/jeremija/gosubmit.svg?branch=master)](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.go000066400000000000000000000040051443450762100227100ustar00rootroot00000000000000package 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.go000066400000000000000000000212561443450762100211530ustar00rootroot00000000000000package 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 elements. Buttons []Button } // Returns true if field is required, false otherwise. func (f Form) IsRequired(name string) bool { input, ok := f.Inputs[name] return ok && input.Required() } func (f Form) newFiller(opts []Option) (filler *filler, err error) { if f.err != nil { err = f.err return } filler, err = newFiller(f, opts) return } // Fills the form and returns an error if there was an error. Useful for // testing. func (f Form) Validate(opts ...Option) error { _, err := f.newFiller(opts) return err } // Fills the form and returns a new request. If there was any error in the // parsing or if the form was filled incorrectly, it will return an error. func (f Form) NewRequest(opts ...Option) (*http.Request, error) { filler, err := f.newFiller(opts) if err != nil { return nil, err } return filler.NewRequest() } // Fills the form and returns a new test request. If there was any error in the // parsing or if the form was filled incorrectly, it will return an error. func (f Form) NewTestRequest(opts ...Option) (*http.Request, error) { filler, err := f.newFiller(opts) if err != nil { return nil, err } return filler.NewTestRequest() } // Fills the form and returns parameters for a multipart request. If there was // any error in the parsing or if the form was filled incorrectly, it will // return an error. func (f Form) MultipartParams(opts ...Option) (boundary string, data []byte, err error) { filler, err := f.newFiller(opts) if err != nil { return "", nil, err } return filler.BuildMultipart() } // Fills the form and returns query parameters for a GET request. func (f Form) GetParams(opts ...Option) (string, error) { filler, err := f.newFiller(opts) if err != nil { return "", err } return filler.BuildGet() } // Fills the form and returns body for a POST request. func (f Form) PostParams(opts ...Option) ([]byte, error) { filler, err := f.newFiller(opts) if err != nil { return nil, err } return filler.BuildPost() } // Returns a list of available input values for elements with options // (checkbox, radio or select). func (f Form) GetOptionsFor(name string) (options []string) { input, ok := f.Inputs[name] if !ok { return } return input.Options() } func (f Form) Testing(t test) TestingForm { return TestingForm{form: f, t: t} } golang-github-jeremija-gosubmit-0.2.7/form_test.go000066400000000000000000000054571443450762100222340ustar00rootroot00000000000000package gosubmit_test import ( "bytes" "fmt" "reflect" "testing" . "github.com/jeremija/gosubmit" ) type errReader struct { *bytes.Reader } func (r *errReader) Read(b []byte) (n int, err error) { return 0, fmt.Errorf("A test error") } func TestParse_error(t *testing.T) { r := &errReader{Reader: bytes.NewReader([]byte(""))} doc := Parse(r) if doc.Err() == nil { t.Error("Expected parsing error, but got nil") } } func TestParse_Find(t *testing.T) { r := bytes.NewReader([]byte("")) doc := Parse(r) if err := doc.Err(); err != nil { t.Fatalf("Unexpected Parse error: %s", err) } form := doc.FindForm("name", "test") expected := "No form with attributes name='test' found" if err := form.Err(); err == nil || err.Error() != expected { t.Fatalf("Expected no error '%s' but got %s", expected, err) } } func TestFindFormsByClass(t *testing.T) { r := bytes.NewReader([]byte(`
`)) doc := Parse(r) if err := doc.Err(); err != nil { t.Fatalf("Unexpected Parse error: %s", err) } forms := doc.FindFormsByClass("a") if size := len(forms); size != 1 { t.Fatalf("Expected to find one form with class a, but got: %d", size) } if !reflect.DeepEqual(forms.First(), forms.Last()) { t.Fatalf("Expected first and last to be same") } forms = doc.FindFormsByClass("b") if size := len(forms); size != 2 { t.Fatalf("Expected to find two forms with class b, but got: %d", size) } if reflect.DeepEqual(forms.First(), forms.Last()) { t.Fatalf("Expected forms to be different") } forms = doc.FindFormsByClass("c") if size := len(forms); size != 1 { t.Fatalf("Expected to find two forms with class c, but got: %d", size) } if !reflect.DeepEqual(forms.First(), forms.Last()) { t.Fatalf("Expected first and last to be same") } } func TestParse_GetOptionsFor(t *testing.T) { r := bytes.NewReader([]byte(`
`)) doc := Parse(r) if err := doc.Err(); err != nil { t.Fatalf("Unexpected Parse error: %s", err) } form := doc.Forms()[0] opts := form.GetOptionsFor("chk") if len(opts) != 2 || opts[0] != "one" || opts[1] != "two" { t.Errorf("Expected to find two options") } opts = form.GetOptionsFor("something-else") if len(opts) != 0 { t.Errorf("Expected to find no options") } } func TestFirstForm(t *testing.T) { var doc Document _, err := doc.FirstForm().NewTestRequest( Set("a", "b"), ) if err == nil || err.Error() != "No forms found" { t.Errorf("Expected an error 'No forms found', but got %s", err) } } golang-github-jeremija-gosubmit-0.2.7/forms/000077500000000000000000000000001443450762100210165ustar00rootroot00000000000000golang-github-jeremija-gosubmit-0.2.7/forms/big-empty.html000066400000000000000000000024711443450762100236050ustar00rootroot00000000000000
golang-github-jeremija-gosubmit-0.2.7/forms/big.html000066400000000000000000000022601443450762100224450ustar00rootroot00000000000000
golang-github-jeremija-gosubmit-0.2.7/forms/simple.html000066400000000000000000000004571443450762100232030ustar00rootroot00000000000000
golang-github-jeremija-gosubmit-0.2.7/go.mod000066400000000000000000000001521443450762100207740ustar00rootroot00000000000000module github.com/jeremija/gosubmit go 1.13 require golang.org/x/net v0.0.0-20200202094626-16171245cfb2 golang-github-jeremija-gosubmit-0.2.7/go.sum000066400000000000000000000007701443450762100210270ustar00rootroot00000000000000golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang-github-jeremija-gosubmit-0.2.7/html.go000066400000000000000000000167661443450762100212030ustar00rootroot00000000000000package gosubmit import ( "fmt" "io" "net/http" "net/url" "regexp" "strings" "golang.org/x/net/html" ) const ContentTypeForm = "application/x-www-form-urlencoded" const ContentTypeMultipart = "multipart/form-data" const ( ElementSelect = "select" ElementInput = "input" ElementButton = "button" ElementTextArea = "textarea" InputTypeText = "text" InputTypeFile = "file" InputTypeCheckbox = "checkbox" InputTypeRadio = "radio" InputTypeHidden = "hidden" InputTypeSubmit = "submit" InputTypeEmail = "email" InputTypeURL = "url" InputTypeDate = "date" InputTypeNumber = "number" ) // Parse all formsr in the HTML document and set the default URL if
attribute is missing func ParseWithURL(r io.Reader, defaultURL string) (doc Document) { doc = Parse(r) for index, form := range doc.forms { if form.URL == "" { form.URL = defaultURL doc.forms[index] = form } } return } // Parse all forms in the HTML document. func Parse(r io.Reader) (doc Document) { n, err := html.Parse(r) if err != nil { doc.setError(fmt.Errorf("Error parsing html: %w", err)) return } doc = findForms(n) return } func ParseResponse(r *http.Response, url *url.URL) Document { return ParseWithURL(r.Body, url.EscapedPath()) } var PatternEmail = regexp.MustCompile("[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,}$") var PatternURL = regexp.MustCompile("^https?://.+") func findForms(n *html.Node) (doc Document) { var recursivelyFindDocument func(n *html.Node) recursivelyFindDocument = func(n *html.Node) { if n.Type == html.ElementNode && n.Data == "form" { form := createForm(n) form.setError(doc.err) doc.forms = append(doc.forms, form) return } for c := n.FirstChild; c != nil; c = c.NextSibling { recursivelyFindDocument(c) } } recursivelyFindDocument(n) return doc } func getCheckbox(inputs Inputs, name string) (checkbox Checkbox, ok bool) { input, exists := inputs[name] ok = exists if !ok { return } checkbox, isCheckbox := input.(Checkbox) ok = isCheckbox return } func getRadio(inputs Inputs, name string) (radio Radio, ok bool) { input, exists := inputs[name] ok = exists if !ok { return } radio, isRadio := input.(Radio) ok = isRadio return } func getPattern(n *html.Node, defaultPattern *regexp.Regexp) *regexp.Regexp { p := getAttr(n, "pattern") if p == "" { return defaultPattern } if !strings.HasPrefix(p, "^") { p = "^" + p } if !strings.HasSuffix(p, "$") { p = p + "$" } return regexp.MustCompile(p) } func createForm(n *html.Node) (form Form) { inputs := Inputs{} var recursivelyFindInputs func(n *html.Node) recursivelyFindInputs = func(n *html.Node) { if n.Type != html.ElementNode { return } inputType := getAttr(n, "type") name := getAttr(n, "name") required := hasAttr(n, "required") switch n.Data { case "select": values, options, _ := findSelectOptions(n) inputs[name] = Select{ inputWithOptions: inputWithOptions{ anyInput: anyInput{ name: name, inputType: inputType, values: values, required: required, }, multiple: hasAttr(n, "multiple"), options: options, }, } case "input": value := getAttr(n, "value") anyInput := anyInput{ name: name, inputType: inputType, values: []string{value}, required: required, } switch inputType { case InputTypeCheckbox: i, ok := getCheckbox(inputs, name) if !ok { i = Checkbox{ inputWithOptions: inputWithOptions{ anyInput: anyInput, options: []string{}, multiple: true, }, } i.values = []string{} } if hasAttr(n, "checked") { i.values = append(i.values, value) } i.options = append(i.options, value) i.required = i.required || hasAttr(n, "required") inputs[name] = i case InputTypeFile: inputs[name] = FileInput{ anyInput: anyInput, } case InputTypeRadio: i, ok := getRadio(inputs, name) if !ok { i = Radio{ inputWithOptions: inputWithOptions{ anyInput: anyInput, options: []string{}, }, } i.values = []string{} } i.options = append(i.options, value) i.required = i.required || hasAttr(n, "required") if hasAttr(n, "checked") { i.values = append(i.values, value) } // need to reassing because map has plain struct (no pointers) inputs[name] = i case InputTypeHidden: inputs[name] = HiddenInput{ anyInput: anyInput, } case InputTypeSubmit: form.Buttons = append(form.Buttons, Button{ Name: name, Value: getAttr(n, "value"), }) case InputTypeEmail: textInput := createTextInput(anyInput, n) textInput.pattern = PatternEmail inputs[name] = EmailInput{ TextInput: textInput, } case InputTypeURL: textInput := createTextInput(anyInput, n) textInput.pattern = PatternURL inputs[name] = URLInput{ TextInput: textInput, } case InputTypeDate: inputs[name] = DateInput{ anyInput: anyInput, } case InputTypeNumber: inputs[name] = NumberInput{ anyInput: anyInput, min: atoi(getAttr(n, "min")), max: atoi(getAttr(n, "max")), } default: inputs[name] = createTextInput(anyInput, n) } case ElementTextArea: inputs[name] = TextInput{ anyInput: anyInput{ name: name, inputType: "textarea", values: []string{getText(n)}, }, minLength: atoi(getAttr(n, "minlength")), maxLength: atoi(getAttr(n, "maxlength")), } case ElementButton: if inputType == "submit" { form.Buttons = append(form.Buttons, Button{ Name: name, Value: getAttr(n, "value"), }) } default: for c := n.FirstChild; c != nil; c = c.NextSibling { recursivelyFindInputs(c) } } } recursivelyFindInputs(n) form.Inputs = inputs form.ContentType = getAttr(n, "enctype") if form.ContentType == "" { form.ContentType = ContentTypeForm } form.Method = strings.ToUpper(getAttr(n, "method")) if form.Method == "" { form.Method = http.MethodGet } form.ClassList = strings.Split(getAttr(n, "class"), " ") form.URL = getAttr(n, "action") form.Attr = n.Attr return } func getText(n *html.Node) string { var b strings.Builder var recursivelyGetText func(n *html.Node) recursivelyGetText = func(n *html.Node) { if n.Type == html.TextNode { b.WriteString(n.Data) return } for c := n.FirstChild; c != nil; c = c.NextSibling { recursivelyGetText(c) } } recursivelyGetText(n) return b.String() } func findSelectOptions(n *html.Node) (values []string, options []string, ok bool) { ok = true for c := n.FirstChild; c != nil; c = c.NextSibling { if c.Type == html.ElementNode && c.Data == "option" && !hasAttr(c, "disabled") { value := getAttr(c, "value") options = append(options, value) if hasAttr(c, "selected") { values = append(values, value) } } } return } func getAttr(n *html.Node, key string) (value string) { value, _ = getAttrOK(n, key) return } func hasAttr(n *html.Node, name string) bool { for _, attr := range n.Attr { if attr.Key == name { return true } } return false } func getAttrOK(n *html.Node, key string) (value string, ok bool) { ok = false for _, attr := range n.Attr { if attr.Key == key { ok = true value = attr.Val return } } return } func createTextInput(anyInput anyInput, n *html.Node) TextInput { return TextInput{ anyInput: anyInput, pattern: getPattern(n, nil), minLength: atoi(getAttr(n, "minlength")), maxLength: atoi(getAttr(n, "maxlength")), } } golang-github-jeremija-gosubmit-0.2.7/input.go000066400000000000000000000073321443450762100213630ustar00rootroot00000000000000package gosubmit import ( "fmt" "regexp" "strconv" "time" ) const ( AutoFillEmail = "test@example.com" AutoFillURL = "https://www.example.com" ISO8601Date = "2006-01-02" AutoFillDate = ISO8601Date ) var AutoFillFile = []byte{0xd, 0xe, 0xa, 0xd, 0xb, 0xe, 0xe, 0xf} type Input interface { Name() string Type() string Value() string Values() []string Options() []string Fill(val string) (value string, ok bool) Required() bool Multiple() bool Multipart() bool AutoFill() []string } type anyInput struct { name string inputType string values []string required bool } func (i anyInput) Name() string { return i.name } func (i anyInput) Type() string { return i.inputType } func (i anyInput) Value() string { if len(i.values) == 0 { return "" } return i.values[0] } func (i anyInput) AutoFill() (values []string) { return []string{fmt.Sprintf("%s-%s", i.Name(), "autofill")} } func (i anyInput) Values() []string { return i.values } func (i anyInput) Required() bool { return i.required } func (i anyInput) Multiple() bool { return false } func (i anyInput) Multipart() bool { return false } func (i anyInput) Options() (values []string) { return } type FileInput struct { anyInput } func (f FileInput) Fill(val string) (value string, ok bool) { return "", false } func (f FileInput) Multipart() bool { return true } func (f FileInput) AutoFill() (values []string) { values = append(values, string(AutoFillFile)) return } type TextInput struct { anyInput pattern *regexp.Regexp minLength int maxLength int } func (i TextInput) Fill(val string) (value string, ok bool) { length := len(val) ok = i.minLength == 0 && i.maxLength == 0 || length >= i.minLength && length <= i.maxLength value = val if i.pattern == nil { return } ok = i.pattern.MatchString(value) return } func (i TextInput) AutoFill() (value []string) { length := 10 if i.pattern != nil { return } if i.minLength > 0 { length = i.minLength return []string{randomString(length)} } return i.anyInput.AutoFill() } type HiddenInput struct { anyInput } func (i HiddenInput) Fill(val string) (value string, ok bool) { return i.Value(), false } type inputWithOptions struct { anyInput options []string multiple bool } func (i inputWithOptions) Options() []string { return i.options } func (i inputWithOptions) Multiple() bool { return i.multiple } func (i inputWithOptions) Fill(val string) (value string, ok bool) { ok = false for _, opt := range i.options { if opt == val { value = val ok = true } } return } func (i inputWithOptions) AutoFill() (values []string) { for _, opt := range i.options { values = append(values, opt) if !i.multiple { return } } return } type EmailInput struct { TextInput } func (i EmailInput) AutoFill() []string { return []string{AutoFillEmail} } type URLInput struct { TextInput } func (i URLInput) AutoFill() []string { return []string{AutoFillURL} } type NumberInput struct { anyInput min int max int } func (i NumberInput) AutoFill() []string { return []string{itoa(i.min)} } func (i NumberInput) Fill(val string) (value string, ok bool) { ok = false intValue, err := strconv.Atoi(val) if err != nil { return } ok = i.min == 0 && i.max == 0 || intValue >= i.min && intValue <= i.max value = itoa(intValue) return } type DateInput struct { anyInput } func (i DateInput) Fill(val string) (value string, ok bool) { value = val _, err := time.Parse(ISO8601Date, val) ok = err == nil return } func (i DateInput) AutoFill() []string { return []string{AutoFillDate} } type Checkbox struct { inputWithOptions } type Radio struct { inputWithOptions } type Select struct { inputWithOptions } type Button struct { Name string Value string } golang-github-jeremija-gosubmit-0.2.7/input_test.go000066400000000000000000000016721443450762100224230ustar00rootroot00000000000000package gosubmit import ( "testing" ) func TestFill(t *testing.T) { hi := HiddenInput{} _, ok := hi.Fill("test") if ok == true { t.Errorf("Should not be able to fill in a hidden input") } fi := FileInput{} _, ok = fi.Fill("test") if ok == true { t.Errorf("Should not be able to fill in a file input") } } func Test_anyinput(t *testing.T) { a := anyInput{ name: "test", inputType: "checkbox", values: []string{"a", "b"}, } if name := a.Name(); name != a.name { t.Errorf("a.Name() should return %s but got %s", a.name, name) } if inputType := a.Type(); inputType != a.inputType { t.Errorf("a.Type() should return %s but got %s", a.inputType, inputType) } if value := a.Value(); value != a.values[0] { t.Errorf("a.Value() should return first value %s but got %s", a.values[0], value) } if size := len(a.Options()); size > 0 { t.Errorf("a.Options() should always return 0 form this type but got %d", size) } } golang-github-jeremija-gosubmit-0.2.7/log.go000066400000000000000000000001471443450762100210020ustar00rootroot00000000000000package gosubmit import ( "log" "os" ) var logger = log.New(os.Stdout, "gosubmit ", log.LstdFlags) golang-github-jeremija-gosubmit-0.2.7/option.go000066400000000000000000000003411443450762100215250ustar00rootroot00000000000000package gosubmit // type Option func(f *Filler) error // func AddFile(fieldname string, filename string, contents []byte) Option { // return func(f *Filler) error { // f.AddFile(fieldname, filename, contents) // } // } golang-github-jeremija-gosubmit-0.2.7/testing.go000066400000000000000000000007141443450762100216760ustar00rootroot00000000000000package gosubmit import ( "net/http" ) type test interface { Fatalf(format string, values ...interface{}) Helper() } type TestingForm struct { form Form t test } func (f TestingForm) assertNoError(err error) { f.t.Helper() if err != nil { f.t.Fatalf("An error occurred: %s", err) } } func (f TestingForm) NewTestRequest(opts ...Option) *http.Request { f.t.Helper() r, err := f.form.NewTestRequest(opts...) f.assertNoError(err) return r } golang-github-jeremija-gosubmit-0.2.7/testing_test.go000066400000000000000000000025731443450762100227420ustar00rootroot00000000000000package gosubmit import ( "fmt" "regexp" "testing" ) type testMock struct { log []string failed bool helpers int } func (t *testMock) Fatalf(format string, values ...interface{}) { t.log = append(t.log, fmt.Sprintf(format, values...)) t.failed = true } func (t *testMock) Helper() { t.helpers++ } func TestTesting_ok(t *testing.T) { var f Form f.Inputs = Inputs{} input := TextInput{} input.name = "firstName" input.inputType = "text" f.Inputs[input.name] = input f.URL = "/test" mock := &testMock{} f.Testing(mock).NewTestRequest( Set("firstName", "John"), ) if mock.failed { t.Errorf("Should not fail") } if len(mock.log) > 0 { t.Errorf("Should not write to log") } } func TestTesting_fail(t *testing.T) { var f Form f.Inputs = Inputs{} input := TextInput{} input.name = "firstName" input.inputType = "text" f.Inputs[input.name] = input f.URL = "/test" mock := &testMock{} f.Testing(mock).NewTestRequest( Set("a", "John"), ) if !mock.failed { t.Errorf("Should have failed") } if len(mock.log) != 1 { t.Errorf("Should write a log entry") } if mock.helpers != 2 { t.Errorf("Should have marked 2 helpers, but got: %d", mock.helpers) } log := mock.log[0] re := regexp.MustCompile("An error occurred: Cannot find input name='a'") if !re.MatchString(log) { t.Errorf("Expected log entry to match '%s', but was '%s'", re, log) } } golang-github-jeremija-gosubmit-0.2.7/util.go000066400000000000000000000010621443450762100211730ustar00rootroot00000000000000package gosubmit import ( "math/rand" "strconv" "strings" "time" ) func atoi(str string) int { value, _ := strconv.Atoi(str) return value } func itoa(value int) string { return strconv.Itoa(value) } const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" var randomSource = rand.NewSource(time.Now().UnixNano()) func randomString(size int) string { lettersCount := len(letters) var b strings.Builder b.Grow(size) for i := 0; i < size; i++ { index := rand.Intn(lettersCount) b.WriteByte(letters[index]) } return b.String() }