// Copyright 2014 Martini Authors // Copyright 2014 The Macaron Authors // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. package binding import ( "fmt" "net/http" "net/http/httptest" "testing" chi "github.com/go-chi/chi/v5" "github.com/stretchr/testify/assert" ) var ( emptyStr = "" urlStr = "http://example.com/" alphaDashStr = "aB-12" idInt = 1 ) var validationTestCases = []validationTestCase{ { description: "No errors", data: BlogPost{ Id: 1, Post: Post{ Title: "Behold The Title!", Content: "And some content", }, Author: Person{ Name: "Matt Holt", }, }, expectedErrors: Errors{}, }, { description: "ID required", data: BlogPost{ Post: Post{ Title: "Behold The Title!", Content: "And some content", }, Author: Person{ Name: "Matt Holt", }, }, expectedErrors: Errors{ Error{ FieldNames: []string{"id"}, Classification: ERR_REQUIRED, Message: "Required", }, }, }, { description: "Embedded struct field required", data: BlogPost{ Id: 1, Post: Post{ Content: "Content given, but title is required", }, Author: Person{ Name: "Matt Holt", }, }, expectedErrors: Errors{ Error{ FieldNames: []string{"title"}, Classification: ERR_REQUIRED, Message: "Required", }, Error{ FieldNames: []string{"title"}, Classification: "LengthError", Message: "Life is too short", }, }, }, { description: "Nested struct field required", data: BlogPost{ Id: 1, Post: Post{ Title: "Behold The Title!", Content: "And some content", }, }, expectedErrors: Errors{ Error{ FieldNames: []string{"name"}, Classification: ERR_REQUIRED, Message: "Required", }, }, }, { description: "Required field missing in nested struct pointer", data: BlogPost{ Id: 1, Post: Post{ Title: "Behold The Title!", Content: "And some content", }, Author: Person{ Name: "Matt Holt", }, Coauthor: &Person{}, }, expectedErrors: Errors{ Error{ FieldNames: []string{"name"}, Classification: ERR_REQUIRED, Message: "Required", }, }, }, { description: "All required fields specified in nested struct pointer", data: BlogPost{ Id: 1, Post: Post{ Title: "Behold The Title!", Content: "And some content", }, Author: Person{ Name: "Matt Holt", }, Coauthor: &Person{ Name: "Jeremy Saenz", }, }, expectedErrors: Errors{}, }, { description: "Custom validation should put an error", data: BlogPost{ Id: 1, Post: Post{ Title: "Too short", Content: "And some content", }, Author: Person{ Name: "Matt Holt", }, }, expectedErrors: Errors{ Error{ FieldNames: []string{"title"}, Classification: "LengthError", Message: "Life is too short", }, }, }, { description: "List Validation", data: []BlogPost{ { Id: 1, Post: Post{ Title: "First Post", Content: "And some content", }, Author: Person{ Name: "Leeor Aharon", }, }, { Id: 2, Post: Post{ Title: "Second Post", Content: "And some content", }, Author: Person{ Name: "Leeor Aharon", }, }, }, expectedErrors: Errors{}, }, { description: "List Validation w/ Errors", data: []BlogPost{ { Id: 1, Post: Post{ Title: "First Post", Content: "And some content", }, Author: Person{ Name: "Leeor Aharon", }, }, { Id: 2, Post: Post{ Title: "Too Short", Content: "And some content", }, Author: Person{ Name: "Leeor Aharon", }, }, }, expectedErrors: Errors{ Error{ FieldNames: []string{"title"}, Classification: "LengthError", Message: "Life is too short", }, }, }, { description: "List of invalid custom validations", data: []SadForm{ { AlphaDash: ",", AlphaDashDot: ",", Size: "123", SizeSlice: []string{"1", "2", "3"}, MinSize: ",", MinSizeSlice: []string{",", ","}, MaxSize: ",,", MaxSizeSlice: []string{",", ","}, Range: 3, Email: ",", URL: ",", URLEmpty: "", InInvalid: "4", NotIn: "1", Include: "def", Exclude: "abc", }, }, expectedErrors: Errors{ Error{ FieldNames: []string{"AlphaDash"}, Classification: "AlphaDashError", Message: "AlphaDash", }, Error{ FieldNames: []string{"AlphaDashDot"}, Classification: "AlphaDashDot", Message: "AlphaDashDot", }, Error{ FieldNames: []string{"Size"}, Classification: "Size", Message: "Size", }, Error{ FieldNames: []string{"Size"}, Classification: "Size", Message: "Size", }, Error{ FieldNames: []string{"MinSize"}, Classification: "MinSize", Message: "MinSize", }, Error{ FieldNames: []string{"MinSize"}, Classification: "MinSize", Message: "MinSize", }, Error{ FieldNames: []string{"MaxSize"}, Classification: "MaxSize", Message: "MaxSize", }, Error{ FieldNames: []string{"MaxSize"}, Classification: "MaxSize", Message: "MaxSize", }, Error{ FieldNames: []string{"Range"}, Classification: "Range", Message: "Range", }, Error{ FieldNames: []string{"Email"}, Classification: "Email", Message: "Email", }, Error{ FieldNames: []string{"Url"}, Classification: "Url", Message: "Url", }, Error{ FieldNames: []string{"Default"}, Classification: "Default", Message: "Default", }, Error{ FieldNames: []string{"InInvalid"}, Classification: "In", Message: "In", }, Error{ FieldNames: []string{"NotIn"}, Classification: "NotIn", Message: "NotIn", }, Error{ FieldNames: []string{"Include"}, Classification: "Include", Message: "Include", }, Error{ FieldNames: []string{"Exclude"}, Classification: "Exclude", Message: "Exclude", }, }, }, { description: "List of valid custom validations", data: []SadForm{ { AlphaDash: "123-456", AlphaDashDot: "123.456", Size: "1", SizeSlice: []string{"1"}, MinSize: "12345", MinSizeSlice: []string{"1", "2", "3", "4", "5"}, MaxSize: "1", MaxSizeSlice: []string{"1"}, Range: 2, In: "1", InInvalid: "1", Email: "123@456.com", URL: "http://123.456", Include: "abc", }, }, }, { description: "slice of structs Validation", data: Group{ Name: "group1", People: []Person{ {Name: "anthony"}, {Name: "awoods"}, }, }, expectedErrors: Errors{}, }, { description: "slice of structs Validation failer", data: Group{ Name: "group1", People: []Person{ {Name: "anthony"}, {Name: ""}, }, }, expectedErrors: Errors{ Error{ FieldNames: []string{"name"}, Classification: ERR_REQUIRED, Message: "Required", }, }, }, { description: "email fail", data: struct { EmailValid string `binding:"Email"` EmailFail string `binding:"Email"` EmailFail2 string `binding:"Email"` EmailFail3 string `binding:"Email"` }{ EmailValid: "123@asd.com", EmailFail: "test 123@asd.com", EmailFail2: "123@asd.com test", EmailFail3: "test 123@asd.com test", }, expectedErrors: Errors{ Error{ FieldNames: []string{"EmailFail"}, Classification: ERR_EMAIL, Message: "Email", }, Error{ FieldNames: []string{"EmailFail2"}, Classification: ERR_EMAIL, Message: "Email", }, Error{ FieldNames: []string{"EmailFail3"}, Classification: ERR_EMAIL, Message: "Email", }, }, }, { description: "pointer form empty and nil", data: PointerForm{}, expectedErrors: Errors{}, }, { description: "pointer form empty", data: PointerForm{ URL: "", URLPointer: &emptyStr, AlphaDash: "", AlphaDashPointer: &emptyStr, }, expectedErrors: Errors{}, }, { description: "no errors with not required fields", data: []*struct { AlphaDash string `binding:"AlphaDash"` AlphaDashDot string `binding:"AlphaDashDot"` Size string `binding:"Size(1)"` SizeSlice []string `binding:"Size(1)"` MinSize string `binding:"MinSize(5)"` MinSizeSlice []string `binding:"MinSize(5)"` MaxSize string `binding:"MaxSize(1)"` MaxSizeSlice []string `binding:"MaxSize(1)"` Range int `binding:"Range(1,2)"` Email string `binding:"Email"` URL string `binding:"Url"` In string `binding:"Default(0);In(1,2,3)"` NotIn string `binding:"NotIn(1,2,3)"` }{ {}, }, expectedErrors: Errors{}, }, { description: "pointer form with valid data", data: PointerForm{ URL: urlStr, URLPointer: &urlStr, AlphaDash: alphaDashStr, AlphaDashPointer: &alphaDashStr, }, expectedErrors: Errors{}, }, { description: "pointer form with invalid data", data: PointerForm{ URL: alphaDashStr, URLPointer: &alphaDashStr, AlphaDash: urlStr, AlphaDashPointer: &urlStr, }, expectedErrors: Errors{ Error{ FieldNames: []string{"Url", "UrlPointer"}, Classification: "Url", Message: "Url", }, Error{ FieldNames: []string{"UrlPointer"}, Classification: "Url", Message: "Url", }, Error{ FieldNames: []string{"AlphaDash"}, Classification: "AlphaDash", Message: "AlphaDash", }, Error{ FieldNames: []string{"AlphaDashPointer"}, Classification: "AlphaDash", Message: "AlphaDash", }, }, }, { description: "errors with required fields", data: []*struct { AlphaDash string `binding:"Required;AlphaDash"` AlphaDashDot string `binding:"Required;AlphaDashDot"` Size string `binding:"Required;Size(1)"` SizeSlice []string `binding:"Required;Size(1)"` MinSize string `binding:"Required;MinSize(5)"` MinSizeSlice []string `binding:"Required;MinSize(5)"` MaxSize string `binding:"Required;MaxSize(1)"` MaxSizeSlice []string `binding:"Required;MaxSize(1)"` Range int `binding:"Required;Range(1,2)"` Email string `binding:"Required;Email"` URL string `binding:"Required;Url"` In string `binding:"Required;Default(0);In(1,2,3)"` NotIn string `binding:"Required;NotIn(1,2,3)"` }{ {}, }, expectedErrors: Errors{ Error{ FieldNames: []string{"AlphaDash"}, Classification: "Required", Message: "Required", }, Error{ FieldNames: []string{"AlphaDashDot"}, Classification: "Required", Message: "Required", }, Error{ FieldNames: []string{"Size"}, Classification: "Required", Message: "Required", }, Error{ FieldNames: []string{"SizeSlice"}, Classification: "Required", Message: "Required", }, Error{ FieldNames: []string{"MinSize"}, Classification: "Required", Message: "Required", }, Error{ FieldNames: []string{"MinSizeSlice"}, Classification: "Required", Message: "Required", }, Error{ FieldNames: []string{"MaxSize"}, Classification: "Required", Message: "Required", }, Error{ FieldNames: []string{"MaxSizeSlice"}, Classification: "Required", Message: "Required", }, Error{ FieldNames: []string{"Range"}, Classification: "Required", Message: "Required", }, Error{ FieldNames: []string{"Email"}, Classification: "Required", Message: "Required", }, Error{ FieldNames: []string{"Url"}, Classification: "Required", Message: "Required", }, Error{ FieldNames: []string{"In"}, Classification: "Required", Message: "Required", }, Error{ FieldNames: []string{"NotIn"}, Classification: "Required", Message: "Required", }, }, }, } func Test_Validation(t *testing.T) { for _, testCase := range validationTestCases { performValidationTest(t, testCase) } } func performValidationTest(t *testing.T, testCase validationTestCase) { httpRecorder := httptest.NewRecorder() m := chi.NewRouter() m.Post(testRoute, func(_ http.ResponseWriter, req *http.Request) { actual := Validate(req, testCase.data) assert.Equal(t, fmt.Sprintf("%+v", testCase.expectedErrors), fmt.Sprintf("%+v", actual), testCase.description) }) req, err := http.NewRequest("POST", testRoute, nil) if err != nil { panic(err) } m.ServeHTTP(httpRecorder, req) switch httpRecorder.Code { case http.StatusNotFound: panic("Routing is messed up in test fixture (got 404): check methods and paths") case http.StatusInternalServerError: panic("Something bad happened on '" + testCase.description + "'") } } type ( validationTestCase struct { description string data any expectedErrors Errors } )