hlfw.ca

webbing

Download patch

ref: 0f6b28b1933ccf1de2ad1493d3f941b760c07cdd
parent: 3500f3abe40407f49e03ddd9cef4b35e09510060
parent: 1b4520b8cf130ca34181064c5876cee2479dd9ea
author: halfwit <michaelmisch1985@gmail.com>
date: Thu Aug 6 05:34:21 PDT 2020

Merge remote-tracking branch 'forms/master' into master

--- /dev/null
+++ b/.github/workflows/go.yml
@@ -1,0 +1,35 @@
+name: Testing
+
+on:
+  pull_request:
+    branches: [ master ]
+
+jobs:
+
+  build:
+    name: Build
+    runs-on: ubuntu-latest
+    steps:
+    - name: Configure git for private modules
+      env:
+        TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
+      run: git config --global url."https://halfwit:${TOKEN}@github.com".insteadOf "https://github.com"
+    - name: Set up Go 1.13
+      uses: actions/setup-go@v1
+      with:
+        go-version: 1.13
+      id: go
+
+    - name: Check out code into the Go module directory
+      uses: actions/checkout@v2
+
+    - name: Get dependencies
+      run: |
+        go get -v -t -d ./...
+        if [ -f Gopkg.toml ]; then
+            curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
+            dep ensure
+        fi
+
+    - name: Run Tests
+      run: go test -v ./...
--- /dev/null
+++ b/README.md
@@ -1,0 +1,10 @@
+# forms
+
+Contains form validation code
+
+ - Any code that has validation scopes which are poorly defined, especially dates, must be tested with sanity checks.
+   - Dates for symptoms cannot have an onset in the future
+   - Birthdates cannot be older than 117 years
+   - binary choices, such as true false, must test to show other values cannot be entered
+ 
+ - The util has helper functions for writing your unit tests. See existing tests for canonical usage
--- /dev/null
+++ b/doctor/application.go
@@ -1,0 +1,79 @@
+package forms
+
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/albrow/forms"
+	"github.com/olmaxmedical/plugins"
+	"github.com/olmaxmedical/router"
+	"golang.org/x/text/message"
+)
+
+func init() {
+	b := &router.Form{
+		Access:    router.GuestAuth,
+		Path:      "doctor/application",
+		Validator: application,
+		Redirect:  "/index.html",
+		After:     plugins.EmailForm | plugins.Countries | plugins.Services | plugins.FormToken,
+	}
+	router.AddPost(b)
+}
+
+func application(r *http.Request, p *message.Printer) []string {
+	var errors []string
+
+	data, err := forms.ParseMax(r, r.ContentLength)
+	if err != nil {
+		errors = append(errors, fmt.Sprintf("validation error %v", err))
+		return errors
+	}
+
+	val := data.Validator()
+	val.Require("gender").Message(p.Sprint("Please select a biological gender"))
+
+	if r.PostFormValue("gender") != "male" && r.PostFormValue("gender") != "female" {
+		val.AddError("gender", p.Sprint("Invalid selection for gender"))
+	}
+
+	val.RequireFile("cv").Message(p.Sprint("Empty or missing CV"))
+	val.AcceptFileExts("cv", "application/msword,applicationvnd.openxmlformats-officedocument.wordprocessingml.document,application/pdf").Message(p.Sprint("unsupported filetype for cv"))
+	val.RequireFile("diploma").Message(p.Sprint("Empty or missing Diploma/Board Certification"))
+	val.AcceptFileExts("diploma", "application/msword,applicationvnd.openxmlformats-officedocument.wordprocessingml.document,application/pdf").Message(p.Sprint("unsupported filetype for diploma"))
+
+	for i := 1; i < 12; i++ {
+		num := fmt.Sprintf("q%d", i)
+
+		sel, ok := r.Form[num]
+		if !ok {
+			val.AddError(num, p.Sprintf("No selection for question %d", i))
+			continue
+		}
+
+		if sel[0] == "Yes" || sel[0] == "yes" || sel[0] == "no" || sel[0] == "No" {
+			continue
+		}
+
+		val.AddError(num, p.Sprintf("Invalid selection for question %d", i))
+	}
+
+	val.Require("email").Message(p.Sprintf("Valid email required"))
+	val.MatchEmail("email").Message(p.Sprintf("Invalid email"))
+	val.Require("name").Message(p.Sprintf("Full name required"))
+	val.MinLength("name", 2).Message(p.Sprintf("Full name must be at least 2 characters"))
+
+	if r.PostFormValue("redFlag") != "on" {
+		val.AddError("redFlag", p.Sprint("Invalid selection for confirm element"))
+	}
+
+	if val.HasErrors() {
+		errors = append(errors, val.Messages()...)
+	}
+
+	r.Form["pagetitle"] = []string{"Application for doctor"}
+	r.Form["sendto"] = []string{"olmaxmedical@gmail.com"}
+	delete(r.Form, "redFlag")
+
+	return errors
+}
--- /dev/null
+++ b/doctor/application_test.go
@@ -1,0 +1,94 @@
+package forms
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+	"mime/multipart"
+	"net/http"
+	"testing"
+
+	"github.com/olmaxmedical/forms/util"
+	"golang.org/x/text/message"
+)
+
+func TestApplication(t *testing.T) {
+	fields := map[string]string{
+		"qs":      "no",
+		"gender":  "male",
+		"email":   "foo@bar.ca",
+		"name":    "Doctor Octopus",
+		"redFlag": "on",
+	}
+
+	if e := testApplication(t, fields); e != nil {
+		t.Error(e)
+	}
+
+	fields["gender"] = "pineapple"
+	if e := testApplication(t, fields); e == nil {
+		t.Error("invalid field accepted")
+	}
+
+	fields["gender"] = "male"
+	fields["email"] = "foo@bar@ca"
+	if e := testApplication(t, fields); e == nil {
+		t.Error("invalid field accepted")
+	}
+
+	fields["email"] = "foo@bar.ca"
+	fields["qs"] = "true"
+	if e := testApplication(t, fields); e == nil {
+		t.Error("invalid field accepted")
+	}
+}
+
+func testApplication(t *testing.T, fields map[string]string) error {
+	var reqBody bytes.Buffer
+
+	mpw := multipart.NewWriter(&reqBody)
+	files := map[string]string{
+		"cv":      "resume.pdf",
+		"diploma": "certificate.pdf",
+	}
+
+	for key, value := range files {
+		if e := util.WriteFile(mpw, key, "../resources/"+value); e != nil {
+			panic(e)
+		}
+	}
+
+	for key, value := range fields {
+		if key == "qs" {
+			for i := 0; i < 12; i++ {
+				key = fmt.Sprintf("q%d", i)
+				if e := mpw.WriteField(key, value); e != nil {
+					panic(e)
+				}
+			}
+			continue
+		}
+
+		if e := mpw.WriteField(key, value); e != nil {
+			panic(e)
+		}
+	}
+
+	request := util.BuildMultiRequest(mpw, &reqBody)
+	printer := message.NewPrinter(message.MatchLanguage("en"))
+	return runTest(request, printer)
+}
+
+func runTest(request *http.Request, printer *message.Printer) error {
+	for _, err := range application(request, printer) {
+		switch err {
+		case "unsupported filetype for cv":
+		case "unsupported filetype for diploma":
+		default:
+			return errors.New(err)
+		}
+
+	}
+
+	return nil
+}
--- /dev/null
+++ b/doctor/profile.go
@@ -1,0 +1,53 @@
+package forms
+
+import (
+	"net/http"
+	"time"
+
+	"github.com/albrow/forms"
+	"github.com/olmaxmedical/plugins"
+	"github.com/olmaxmedical/router"
+	"golang.org/x/text/message"
+)
+
+func init() {
+	b := &router.Form{
+		Access:    router.DoctorAuth,
+		Path:      "doctor/profile",
+		Validator: profile,
+		Redirect:  "/doctor/profile.html",
+		After:     plugins.FormToken | plugins.AddAppointment,
+	}
+	router.AddPost(b)
+}
+
+func profile(r *http.Request, p *message.Printer) []string {
+	var errors []string
+	data, err := forms.ParseMax(r, r.ContentLength)
+	if err != nil {
+		errors = append(errors, "Internal server error")
+		return errors
+	}
+	val := data.Validator()
+	val.Require("BTCperU").Message(p.Sprint("Please enter a rate (Bitcoin/15min)"))
+	bcu := data.GetFloat("BTCperU")
+	if 0.0 > bcu || bcu > 1.0 {
+		val.AddError("BTCperU", p.Sprint("BTC/15min rate out of range"))
+	}
+	val.Require("startDate").Message(p.Sprint("Start date required"))
+	_, err = time.Parse("2006-01-02T15:04:05", r.Form.Get("startDate"))
+	if err != nil {
+		val.AddError("startDate", p.Sprint("Invalid start-date entered"))
+	}
+
+	val.Require("endDate").Message(p.Sprint("End date required"))
+	_, err = time.Parse("2006-01-02T15:04:05", r.Form.Get("endDate"))
+	if err != nil {
+		val.AddError("endDate", p.Sprint("Invalid end-date entered"))
+	}
+
+	if val.HasErrors() {
+		errors = append(errors, val.Messages()...)
+	}
+	return errors
+}
--- /dev/null
+++ b/doctor/profile_test.go
@@ -1,0 +1,31 @@
+package forms
+
+import (
+	"net/url"
+	"testing"
+
+	"github.com/olmaxmedical/forms/util"
+)
+
+func TestProfile(t *testing.T) {
+	values := url.Values{}
+
+	values.Add("BTCperU", "0.1234")
+	values.Add("startDate", "2020-04-04T00:00:00")
+	values.Add("endDate", "2020-06-06T00:00:00")
+
+	if e := util.TestValues(values, profile); e != nil {
+		t.Error(e)
+	}
+
+	values.Set("BTCperU", "-1")
+	if e := util.TestValues(values, profile); e == nil {
+		t.Error("invalid BTC rate allowed")
+	}
+
+	values.Set("BTCperU", "0.1234")
+	values.Set("startDate", "1995-30-30T23:23:59")
+	if e := util.TestValues(values, profile); e == nil {
+		t.Error("invalid date allowed")
+	}
+}
--- /dev/null
+++ b/go.mod
@@ -1,0 +1,10 @@
+module github.com/olmaxmedical/forms
+
+go 1.14
+
+require (
+	github.com/albrow/forms v0.3.4-0.20170215231405-c4277021bca2
+	github.com/olmaxmedical/plugins v0.0.1
+	github.com/olmaxmedical/router v0.0.1
+	golang.org/x/text v0.3.2
+)
--- /dev/null
+++ b/go.sum
@@ -1,0 +1,34 @@
+github.com/albrow/forms v0.3.4-0.20170215231405-c4277021bca2 h1:SnKbJhjY6YOX6cC0JL19DzxJ4nMZoTFqRJ006tgpclw=
+github.com/albrow/forms v0.3.4-0.20170215231405-c4277021bca2/go.mod h1:jvrM3b0gPuIRiY1E/KmKfPk2XXDEKj7yFB+g9g0BItQ=
+github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
+github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/olmaxmedical/database v0.0.0/go.mod h1:BPYEBAP3GYeSeqg5hOaH7GC8+P/VnD2OuHYounbVjQs=
+github.com/olmaxmedical/database v0.0.1 h1:cuocVljXq7cPRS9HygFg2B/WSdzWAZEHrc5uMnN+A0A=
+github.com/olmaxmedical/database v0.0.1/go.mod h1:/5Tl6/p0jpvLpj4GaoFki3wRG/3b+ipNNhM5Dyi6Zf8=
+github.com/olmaxmedical/email v0.0.1 h1:bhOERmPiUmFJqC133s+FFXucSI3dNnfDKsboDYFEbkc=
+github.com/olmaxmedical/email v0.0.1/go.mod h1:bz6en9uc6h9fyu3MW2jTwYW19ZclQ22JkcIxsl3/epc=
+github.com/olmaxmedical/plugins v0.0.1 h1:fON9AAfH635gLKNbytrUdRoRCjhI0ZPP7VFzCVZDVWA=
+github.com/olmaxmedical/plugins v0.0.1/go.mod h1:bHkYv5oh6bk5y1jCZDO9Bk5IyYMPWgZDjMrEifxVgbU=
+github.com/olmaxmedical/router v0.0.1 h1:x3fZ9u00xwKxLvPj8Au0QhIxpmxRv+q2lVMdBmyRQjM=
+github.com/olmaxmedical/router v0.0.1/go.mod h1:28e377pByZCQMBAdERDBhX4wYeTsJgc6U4yOEiz7MsA=
+github.com/olmaxmedical/session v0.0.1 h1:2xdSjpEg89+ClRFLPp/gR3lNxl4JjPAUd2Hsds++FFs=
+github.com/olmaxmedical/session v0.0.1/go.mod h1:XOVyHL+cKa5t2fLDIJtFxwEzJOa3r1hUkwlL4aybdqA=
+github.com/pariz/gountries v0.0.0-20191029140926-233bc78cf5b5 h1:842t0ixg/A4my8/Q3oDNdHIsKYIx02NDlWVEhaiBToo=
+github.com/pariz/gountries v0.0.0-20191029140926-233bc78cf5b5/go.mod h1:U0ETmPPEsfd7CpUKNMYi68xIOL8Ww4jPZlaqNngcwqs=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/scorredoira/email v0.0.0-20191107070024-dc7b732c55da h1:hhmnjfzz7szp75AyXxn8tDfEA0oU4REQLmpuW6zNAOY=
+github.com/scorredoira/email v0.0.0-20191107070024-dc7b732c55da/go.mod h1:Q5ljvYIBpukMH+wgB8kcPV1i9NX8TqU++8GgBKq3pt0=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+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/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
--- /dev/null
+++ b/login.go
@@ -1,0 +1,39 @@
+package forms
+
+import (
+	"net/http"
+
+	"github.com/albrow/forms"
+	"github.com/olmaxmedical/plugins"
+	"github.com/olmaxmedical/router"
+	"golang.org/x/text/message"
+)
+
+func init() {
+	b := &router.Form{
+		Access:    router.GuestAuth,
+		Path:      "login",
+		Validator: login,
+		After:     plugins.ValidateLogin,
+		Redirect:  "/profile.html",
+	}
+	router.AddPost(b)
+}
+
+func login(r *http.Request, p *message.Printer) []string {
+	var errors []string
+	data, err := forms.Parse(r)
+	if err != nil {
+		errors = append(errors, p.Sprint("Internal server error"))
+		return errors
+	}
+	val := data.Validator()
+	val.Require("email").Message(p.Sprint("Username required"))
+	val.MatchEmail("email").Message(p.Sprint("User name must be a valid email"))
+	val.Require("pass").Message(p.Sprint("Password required"))
+	val.MinLength("pass", 8).Message(p.Sprint("Password must be at least 8 characters"))
+	if val.HasErrors() {
+		errors = append(errors, val.Messages()...)
+	}
+	return errors
+}
--- /dev/null
+++ b/newpassword.go
@@ -1,0 +1,42 @@
+package forms
+
+import (
+	"net/http"
+
+	"github.com/albrow/forms"
+	"github.com/olmaxmedical/plugins"
+	"github.com/olmaxmedical/router"
+	"golang.org/x/text/message"
+)
+
+func init() {
+	b := &router.Form{
+		Access:    router.GuestAuth,
+		Path:      "newpassword",
+		Validator: newPassword,
+		Redirect:  "/login.html",
+		After:     plugins.ResetPassword | plugins.FormToken,
+	}
+	router.AddPost(b)
+}
+
+func newPassword(r *http.Request, p *message.Printer) []string {
+	var errors []string
+	data, err := forms.Parse(r)
+	if err != nil {
+		errors = append(errors, "Internal server error")
+		return errors
+	}
+	val := data.Validator()
+	val.Require("password").Message(p.Sprintf("Password required"))
+	val.MinLength("password", 8).Message(p.Sprintf("Password must be at least 8 characters"))
+	val.Require("reenter").Message(p.Sprintf("Re-enter same password"))
+	val.MinLength("reenter", 8).Message(p.Sprintf("Password must be at least 8 characters"))
+	if val.HasErrors() {
+		errors = append(errors, val.Messages()...)
+	}
+	if data.Get("reenter") != data.Get("password") {
+		errors = append(errors, p.Sprint("Passwords do not match"))
+	}
+	return errors
+}
--- /dev/null
+++ b/patient/offer.go
@@ -1,0 +1,53 @@
+package forms
+
+import (
+	"net/http"
+	"time"
+
+	"github.com/albrow/forms"
+	"github.com/olmaxmedical/plugins"
+	"github.com/olmaxmedical/router"
+	"golang.org/x/text/message"
+)
+
+func init() {
+	b := &router.Form{
+		Access:    router.PatientAuth,
+		Path:      "patient/offer",
+		Validator: offer,
+		After:     plugins.Search | plugins.Services, //|plugins.Offer
+		Redirect:  "results.html",
+	}
+	router.AddPost(b)
+}
+
+func offer(r *http.Request, p *message.Printer) []string {
+	var errors []string
+	data, err := forms.ParseMax(r, r.ContentLength)
+	if err != nil {
+		errors = append(errors, p.Sprint("Internal server error"))
+		return errors
+	}
+	val := data.Validator()
+	val.Require("Amount").Message(p.Sprint("Please enter a target rate (Bitcoin/15min)"))
+	bcu := data.GetFloat("Amount")
+	if 0.0 > bcu || bcu > 1.0 {
+		val.AddError("Amount", p.Sprint("BTC/15min rate out of range"))
+	}
+	val.Require("startDate").Message(p.Sprint("Start date required"))
+	_, err = time.Parse("2006-01-02T15:04:05", r.Form.Get("startDate"))
+	if err != nil {
+		val.AddError("startDate", p.Sprint("Invalid start-date entered"))
+	}
+
+	val.Require("endDate").Message(p.Sprint("End date required"))
+	_, err = time.Parse("2006-01-02T15:04:05", r.Form.Get("endDate"))
+	if err != nil {
+		val.AddError("endDate", p.Sprint("Invalid end-date entered"))
+	}
+
+	if val.HasErrors() {
+		errors = append(errors, val.Messages()...)
+	}
+	return errors
+}
--- /dev/null
+++ b/patient/offer_test.go
@@ -1,0 +1,31 @@
+package forms
+
+import (
+	"net/url"
+	"testing"
+
+	"github.com/olmaxmedical/forms/util"
+)
+
+func TestOffer(t *testing.T) {
+	values := url.Values{}
+
+	values.Add("Amount", "0.1234")
+	values.Add("startDate", "2020-04-04T00:00:00")
+	values.Add("endDate", "2020-06-06T00:00:00")
+
+	if e := util.TestValues(values, offer); e != nil {
+		t.Error(e)
+	}
+
+	values.Set("Amount", "-1")
+	if e := util.TestValues(values, offer); e == nil {
+		t.Error("invalid BTC rate allowed")
+	}
+
+	values.Set("Amount", "0.1234")
+	values.Set("startDate", "1995-30-30T23:23:59")
+	if e := util.TestValues(values, offer); e == nil {
+		t.Error("invalid date allowed")
+	}
+}
--- /dev/null
+++ b/patient/profile.go
@@ -1,0 +1,35 @@
+package forms
+
+import (
+	"net/http"
+
+	"github.com/albrow/forms"
+	"github.com/olmaxmedical/router"
+	"golang.org/x/text/message"
+)
+
+func init() {
+	b := &router.Form{
+		Access:    router.PatientAuth,
+		Path:      "patient/profile",
+		Validator: profile,
+		After:     0,
+		Redirect:  "/patient/profile.html",
+	}
+	router.AddPost(b)
+}
+
+func profile(r *http.Request, p *message.Printer) []string {
+	var errors []string
+	data, err := forms.Parse(r)
+	if err != nil {
+		errors = append(errors, p.Sprint("Internal server error"))
+		return errors
+	}
+	val := data.Validator()
+	//
+	if val.HasErrors() {
+		errors = append(errors, val.Messages()...)
+	}
+	return errors
+}
--- /dev/null
+++ b/patient/symptoms.go
@@ -1,0 +1,94 @@
+package forms
+
+import (
+	"net/http"
+	"time"
+
+	"github.com/albrow/forms"
+	"github.com/olmaxmedical/plugins"
+	"github.com/olmaxmedical/router"
+	"golang.org/x/text/message"
+)
+
+func init() {
+	b := &router.Form{
+		Access:    router.PatientAuth,
+		Path:      "patient/symptoms",
+		Validator: symptoms,
+		After:     plugins.EmailForm,
+		Redirect:  "patient/profile.html",
+	}
+	router.AddPost(b)
+}
+
+func symptoms(r *http.Request, p *message.Printer) []string {
+	var errors []string
+
+	data, err := forms.Parse(r)
+	if err != nil {
+		errors = append(errors, p.Sprint("Internal server error"))
+		return errors
+	}
+
+	val := data.Validator()
+
+	// NOTE(halfwit) This is the current record oldest person
+	// Anything older than this is most definitely invalid
+	oldest := time.Date(1901, 1, 1, 0, 0, 0, 0, time.UTC)
+
+	// NOTE(halfwit) There's potential that symptoms started that day
+	// and the client is in a different time zone, use our tomorrow as a gate
+	youngest := time.Now().Add(time.Hour * 24)
+
+	val.Require("bday").Message(p.Sprint("Birth date required"))
+	if d, e := time.Parse("2006-01-02T15:04:05", r.Form.Get("bday")); e != nil || oldest.After(d) || youngest.Before(d) {
+		val.AddError("bday", p.Sprint("Invalid birth date"))
+	}
+
+	val.Require("onset").Message(p.Sprint("Please enter the date and time your symptoms started"))
+	if d, e := time.Parse("2006-01-02T15:04:05", r.Form.Get("onset")); e != nil || oldest.After(d) || youngest.Before(d) {
+		val.AddError("bday", p.Sprint("Invalid date"))
+	}
+
+	val.Require("gender").Message(p.Sprint("Please select a biological gender"))
+	if r.PostFormValue("gender") != "male" && r.PostFormValue("gender") != "female" {
+		val.AddError("gender", p.Sprint("Invalid selection for gender"))
+	}
+
+	val.GreaterOrEqual("duration", 0).Message(p.Sprint("Invalid value entered for how long symptoms have lasted"))
+	val.Require("reason").Message(p.Sprint("Please provide the reason for visit"))
+	val.Require("location").Message(p.Sprint("Please list the area the symptom(s) appear"))
+	val.Require("characteristic").Message(p.Sprint("Please provide a description of your symptoms"))
+	val.Require("aggreAlevi").Message(p.Sprint("Please note anything which improves/worsens your symptoms"))
+
+	for _, i := range []string{
+		"feversChills",
+		"wtGainLoss",
+		"vision",
+		"lung",
+		"heart",
+		"bowel",
+		"renal",
+		"musSkel",
+		"neuro",
+		"psych",
+	} {
+		sel, ok := r.Form[i]
+		if !ok {
+			val.AddError(i, p.Sprintf("No selection for %s", i))
+			continue
+		}
+
+		if sel[0] == "Yes" || sel[0] == "yes" || sel[0] == "no" || sel[0] == "No" {
+			continue
+		}
+
+		val.AddError(i, p.Sprintf("Invalid selection for %s", i))
+	}
+
+	r.Form["pagetitle"] = []string{"Client symptoms"}
+	if val.HasErrors() {
+		errors = append(errors, val.Messages()...)
+	}
+	return errors
+}
--- /dev/null
+++ b/patient/symptoms_test.go
@@ -1,0 +1,53 @@
+package forms
+
+import (
+	"net/url"
+	"testing"
+	"time"
+
+	"github.com/olmaxmedical/forms/util"
+)
+
+func TestSymptoms(t *testing.T) {
+	values := url.Values{}
+
+	values.Add("bday", "1990-01-01T01:01:01")
+	values.Add("onset", "2001-01-01T01:01:01")
+	values.Add("gender", "male")
+	values.Add("duration", "1")
+	values.Add("reason", "test")
+	values.Add("location", "test")
+	values.Add("characteristic", "test")
+	values.Add("aggreAlevi", "test")
+	for _, i := range []string{
+		"feversChills",
+		"wtGainLoss",
+		"vision",
+		"lung",
+		"heart",
+		"bowel",
+		"renal",
+		"musSkel",
+		"neuro",
+		"psych",
+	} {
+		values.Add(i, "yes")
+	}
+
+	if e := util.TestValues(values, symptoms); e != nil {
+		t.Error(e)
+	}
+
+	values.Set("bday", "1891-01-01T01:01:01")
+
+	if e := util.TestValues(values, symptoms); e == nil {
+		t.Error("forms parsing: invalid date accepted")
+	}
+
+	values.Set("bday", "1990-01-01T01:01:01")
+	values.Set("onset", time.Now().Add(time.Hour+48).String())
+
+	if e := util.TestValues(values, symptoms); e == nil {
+		t.Error("form parsing: invalid onset accepted")
+	}
+}
--- /dev/null
+++ b/resetpassword.go
@@ -1,0 +1,37 @@
+package forms
+
+import (
+	"net/http"
+
+	"github.com/albrow/forms"
+	"github.com/olmaxmedical/plugins"
+	"github.com/olmaxmedical/router"
+	"golang.org/x/text/message"
+)
+
+func init() {
+	b := &router.Form{
+		Access:    router.GuestAuth,
+		Path:      "resetpassword",
+		Validator: reset,
+		Redirect:  "/login.html",
+		After:     plugins.ResetPassword,
+	}
+	router.AddPost(b)
+}
+
+func reset(r *http.Request, p *message.Printer) []string {
+	var errors []string
+	data, err := forms.Parse(r)
+	if err != nil {
+		errors = append(errors, "Internal server error")
+		return errors
+	}
+	val := data.Validator()
+	val.Require("email").Message(p.Sprintf("Valid email required"))
+	val.MatchEmail("email").Message(p.Sprintf("Invalid email"))
+	if val.HasErrors() {
+		errors = append(errors, val.Messages()...)
+	}
+	return errors
+}
binary files /dev/null b/resources/certificate.pdf differ
binary files /dev/null b/resources/resume.pdf differ
--- /dev/null
+++ b/signup.go
@@ -1,0 +1,43 @@
+package forms
+
+import (
+	"net/http"
+
+	"github.com/albrow/forms"
+	"github.com/olmaxmedical/plugins"
+	"github.com/olmaxmedical/router"
+	"golang.org/x/text/message"
+)
+
+func init() {
+	b := &router.Form{
+		Access:    router.GuestAuth,
+		Path:      "signup",
+		Validator: signin,
+		Redirect:  "/login.html",
+		After:     plugins.SendSignup,
+	}
+	router.AddPost(b)
+}
+
+func signin(r *http.Request, p *message.Printer) []string {
+	var errors []string
+	data, err := forms.Parse(r)
+	if err != nil {
+		errors = append(errors, "Internal server error")
+		return errors
+	}
+	val := data.Validator()
+	val.Require("fname").Message(p.Sprintf("First name required"))
+	val.MinLength("fname", 2).Message(p.Sprintf("First name must be at least 2 characters"))
+	val.Require("lname").Message(p.Sprintf("Last name required"))
+	val.MinLength("lname", 2).Message(p.Sprintf("Last name must be at least 2 characters"))
+	val.Require("email").Message(p.Sprintf("Valid email required"))
+	val.MatchEmail("email").Message(p.Sprintf("Invalid email"))
+	val.Require("pass").Message(p.Sprintf("Password required"))
+	val.MinLength("pass", 8).Message(p.Sprintf("Password must be at least 8 characters"))
+	if val.HasErrors() {
+		errors = append(errors, val.Messages()...)
+	}
+	return errors
+}
--- /dev/null
+++ b/util/testing.go
@@ -1,0 +1,54 @@
+package util
+
+import (
+	"bytes"
+	"errors"
+	"io"
+	"mime/multipart"
+	"net/http"
+	"net/http/httptest"
+	"net/url"
+	"os"
+
+	"golang.org/x/text/message"
+)
+
+func BuildMultiRequest(mpw *multipart.Writer, buff *bytes.Buffer) *http.Request {
+	mpw.Close()
+
+	req := httptest.NewRequest("POST", "/", buff)
+	req.Header.Add("Content-Type", "multipart/form-data; boundary="+mpw.Boundary())
+
+	return req
+}
+
+func WriteFile(mpw *multipart.Writer, key, path string) error {
+	file, err := os.Open(path)
+	if err != nil {
+		return err
+	}
+
+	defer file.Close()
+
+	fw, err := mpw.CreateFormFile(key, path)
+	if err != nil {
+		return err
+	}
+
+	_, err = io.Copy(fw, file)
+	return err
+}
+
+func TestValues(values url.Values, fn func(*http.Request, *message.Printer) []string) error {
+	req := httptest.NewRequest("GET", "/", nil)
+
+	req.PostForm = values
+	req.Header.Set("Content-Type", "form-urlencoded")
+
+	printer := message.NewPrinter(message.MatchLanguage("en"))
+	for _, err := range fn(req, printer) {
+		return errors.New(err)
+	}
+
+	return nil
+}