From e97af356764927133c338253a67599dc5db27313 Mon Sep 17 00:00:00 2001 From: anthdm Date: Sun, 23 Jun 2024 08:40:24 +0200 Subject: [PATCH] Validate package tests --- Makefile | 2 + event/event_test.go | 20 +++--- go.sum | 12 ++++ validate/rules.go | 2 +- validate/validate.go | 24 +++++-- validate/validate_test.go | 138 ++++++++++++++++++++++++++++++++++++-- 6 files changed, 176 insertions(+), 22 deletions(-) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9c75bc2 --- /dev/null +++ b/Makefile @@ -0,0 +1,2 @@ +test: + @go test -v ./... diff --git a/event/event_test.go b/event/event_test.go index 1d14c75..70a7965 100644 --- a/event/event_test.go +++ b/event/event_test.go @@ -3,17 +3,14 @@ package event import ( "context" "reflect" - "sync" "testing" ) func TestEventSubscribeEmit(t *testing.T) { - var ( - expect = 1 - wg = sync.WaitGroup{} - ) - wg.Add(1) - Subscribe("foo.bar", func(_ context.Context, event any) { + expect := 1 + ctx, cancel := context.WithCancel(context.Background()) + Subscribe("foo.a", func(_ context.Context, event any) { + defer cancel() value, ok := event.(int) if !ok { t.Errorf("expected int got %v", reflect.TypeOf(event)) @@ -21,16 +18,15 @@ func TestEventSubscribeEmit(t *testing.T) { if value != 1 { t.Errorf("expected %d got %d", expect, value) } - wg.Done() }) - Emit("foo.bar", expect) - wg.Wait() + Emit("foo.a", expect) + <-ctx.Done() } func TestUnsubscribe(t *testing.T) { - sub := Subscribe("foo.bar", func(_ context.Context, _ any) {}) + sub := Subscribe("foo.b", func(_ context.Context, _ any) {}) Unsubscribe(sub) - if _, ok := stream.subs["foo.bar"]; ok { + if _, ok := stream.subs["foo.b"]; ok { t.Errorf("expected topic foo.bar to be deleted") } } diff --git a/go.sum b/go.sum index cc76961..1f527fe 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,20 @@ +github.com/a-h/templ v0.2.707 h1:T1Gkd2ugbRglZ9rYw/VBchWOSZVKmetDbBkm4YubM7U= github.com/a-h/templ v0.2.707/go.mod h1:5cqsugkq9IerRNucNsI4DEamdHPsoGMQy99DzydLhM8= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= +github.com/gorilla/sessions v1.3.0 h1:XYlkq7KcpOB2ZhHBPv5WpjMIxrQosiZanfoy1HLZFzg= github.com/gorilla/sessions v1.3.0/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= +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/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +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.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/validate/rules.go b/validate/rules.go index a0f4958..2053d16 100644 --- a/validate/rules.go +++ b/validate/rules.go @@ -10,7 +10,7 @@ import ( var ( emailRegex = regexp.MustCompile(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,4}$`) - urlRegex = regexp.MustCompile(`^(http(s)?://)?([\da-z\.-]+)\.([a-z\.]{2,6})([/\w \.-]*)*/?$`) + urlRegex = regexp.MustCompile(`^(https?:\/\/)?(www\.)?([a-zA-Z0-9\-]+\.)+[a-zA-Z]{2,}(\/[a-zA-Z0-9\-._~:\/?#\[\]@!$&'()*+,;=]*)?$`) ) // RuleSet holds the state of a single rule. diff --git a/validate/validate.go b/validate/validate.go index 9d1c073..01ac30d 100644 --- a/validate/validate.go +++ b/validate/validate.go @@ -39,7 +39,7 @@ func (e Errors) Has(field string) bool { // Schema represents a validation schema. type Schema map[string][]RuleSet -// Merge merges the two given schemas returning a new Schema. +// Merge merges the two given schemas, returning a new Schema. func Merge(schema, other Schema) Schema { newSchema := Schema{} maps.Copy(newSchema, schema) @@ -75,9 +75,14 @@ func Request(r *http.Request, data any, schema Schema) (Errors, bool) { func validate(data any, schema Schema, errors Errors) (Errors, bool) { ok := true for fieldName, ruleSets := range schema { - // Uppercase the field name so we never check un-exported fields - fieldName = string(unicode.ToUpper(rune(fieldName[0]))) + fieldName[1:] - fieldValue := getFieldValueByName(data, fieldName) + // Uppercase the field name so we never check un-exported fields. + // But we need to watch out for member fields that are uppercased by + // the user. For example (URL, ID, ...) + if !isUppercase(fieldName) { + fieldName = string(unicode.ToUpper(rune(fieldName[0]))) + fieldName[1:] + } + + fieldValue := getFieldAndTagByName(data, fieldName) for _, set := range ruleSets { set.FieldValue = fieldValue set.FieldName = fieldName @@ -98,7 +103,7 @@ func validate(data any, schema Schema, errors Errors) (Errors, bool) { return errors, ok } -func getFieldValueByName(v any, name string) any { +func getFieldAndTagByName(v any, name string) any { val := reflect.ValueOf(v) if val.Kind() == reflect.Ptr { val = val.Elem() @@ -168,3 +173,12 @@ func parseRequest(r *http.Request, v any) error { } return nil } + +func isUppercase(s string) bool { + for _, ch := range s { + if !unicode.IsUpper(rune(ch)) { + return false + } + } + return true +} diff --git a/validate/validate_test.go b/validate/validate_test.go index c8b9a1c..b29e12a 100644 --- a/validate/validate_test.go +++ b/validate/validate_test.go @@ -2,6 +2,9 @@ package validate import ( "fmt" + "net/http" + "net/url" + "strings" "testing" "time" @@ -28,6 +31,65 @@ var testSchema = Schema{ "username": Rules(Required), } +func TestValidateRequest(t *testing.T) { + var ( + email = "foo@bar.com" + password = "superHunter123@" + firstName = "Anthony" + website = "http://foo.com" + randomNumber = 123 + randomFloat = 9.999 + ) + formValues := url.Values{} + formValues.Set("email", email) + formValues.Set("password", password) + formValues.Set("firstName", firstName) + formValues.Set("url", website) + formValues.Set("brandom", fmt.Sprint(randomNumber)) + formValues.Set("arandom", fmt.Sprint(randomFloat)) + encodedValues := formValues.Encode() + + req, err := http.NewRequest("POST", "http://foo.com", strings.NewReader(encodedValues)) + assert.Nil(t, err) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + type SignupData struct { + Email string `form:"email"` + Password string `form:"password"` + FirstName string `form:"firstName"` + URL string `form:"url"` + ARandomRenamedNumber int `form:"brandom"` + ARandomRenamedFloat float64 `form:"arandom"` + } + + schema := Schema{ + "Email": Rules(Email), + "Password": Rules( + Required, + ContainsDigit, + ContainsUpper, + ContainsSpecial, + Min(7), + ), + "FirstName": Rules(Min(3), Max(50)), + "URL": Rules(URL), + "ARandomRenamedNumber": Rules(GT(100), LT(124)), + "ARandomRenamedFloat": Rules(GT(9.0), LT(10.1)), + } + + var data SignupData + errors, ok := Request(req, &data, schema) + assert.True(t, ok) + assert.Empty(t, errors) + + assert.Equal(t, data.Email, email) + assert.Equal(t, data.Password, password) + assert.Equal(t, data.FirstName, firstName) + assert.Equal(t, data.URL, website) + assert.Equal(t, data.ARandomRenamedNumber, randomNumber) + assert.Equal(t, data.ARandomRenamedFloat, randomFloat) +} + func TestTime(t *testing.T) { type Foo struct { CreatedAt time.Time @@ -48,21 +110,89 @@ func TestTime(t *testing.T) { func TestURL(t *testing.T) { type Foo struct { - URL string + URL string `v:"URL"` } foo := Foo{ URL: "not an url", } schema := Schema{ - "url": Rules(URL), + "URL": Rules(URL), } errors, ok := Validate(foo, schema) assert.False(t, ok) + assert.NotEmpty(t, errors) - foo.URL = "www.user.com" + validURLS := []string{ + "http://google.com", + "http://www.google.com", + "https://www.google.com", + "https://www.google.com", + "www.google.com", + "https://book.com/sales", + "app.book.com", + "app.book.com/signup", + } + + for _, url := range validURLS { + foo.URL = url + errors, ok = Validate(foo, schema) + assert.True(t, ok) + assert.Empty(t, errors) + } +} + +func TestContainsUpper(t *testing.T) { + type Foo struct { + Password string + } + foo := Foo{"hunter"} + schema := Schema{ + "Password": Rules(ContainsUpper), + } + errors, ok := Validate(foo, schema) + assert.False(t, ok) + assert.NotEmpty(t, errors) + + foo.Password = "Hunter" errors, ok = Validate(foo, schema) assert.True(t, ok) - fmt.Println(errors) + assert.Empty(t, errors) +} + +func TestContainsDigit(t *testing.T) { + type Foo struct { + Password string + } + foo := Foo{"hunter"} + schema := Schema{ + "Password": Rules(ContainsDigit), + } + errors, ok := Validate(foo, schema) + assert.False(t, ok) + assert.NotEmpty(t, errors) + + foo.Password = "Hunter1" + errors, ok = Validate(foo, schema) + assert.True(t, ok) + assert.Empty(t, errors) +} + +func TestContainsSpecial(t *testing.T) { + type Foo struct { + Password string + } + foo := Foo{"hunter"} + schema := Schema{ + "Password": Rules(ContainsSpecial), + } + errors, ok := Validate(foo, schema) + assert.False(t, ok) + assert.NotEmpty(t, errors) + + foo.Password = "Hunter@" + errors, ok = Validate(foo, schema) + assert.True(t, ok) + assert.Empty(t, errors) } func TestRuleIn(t *testing.T) {