From 2f0a9b51270c7011be221df186bd4c19ce36823c Mon Sep 17 00:00:00 2001 From: Arthur Lefebvre Date: Wed, 24 Sep 2025 17:06:39 +0200 Subject: [PATCH] added validation when adding a book --- api_test.go | 20 ++++++++++++++++++++ demodata.sql | 1 + front/src/AddBook.vue | 39 ++++++++++++++++++++++++++++++++++----- front/src/api.js | 10 +++++----- internal/api/dto.go | 4 ++-- internal/api/routes.go | 9 ++++++++- internal/api/validator.go | 35 +++++++++++++++++++++++++++++++++++ 7 files changed, 105 insertions(+), 13 deletions(-) create mode 100644 internal/api/validator.go diff --git a/api_test.go b/api_test.go index a69878d..6d99c0f 100644 --- a/api_test.go +++ b/api_test.go @@ -63,6 +63,26 @@ func TestPostBookHandler_WrongRating(t *testing.T) { testPostBookHandler(t, bookJson, 400) } +func TestPostBookHandler_TitleTooLong(t *testing.T) { + bookJson := + `{ + "title": "Noisy outlaws, unfriendly blobs, and some other things that aren't as scary, maybe, depending on how you feel about lost lands, stray cellphones, creatures from the sky, parents who disappear in Peru, a man named Lars Farf, and one other story we couldn't quite finish, so maybe you could help us out.", + "author": "Eli Horowitz", + "rating": 2 + }` + testPostBookHandler(t, bookJson, 400) +} + +func TestPostBookHandler_AuthorTooLong(t *testing.T) { + bookJson := + `{ + "title": "something", + "author": "Wolfeschlegelsteinhausenbergerdorffwelchevoralternwarengewissenhaftschaferswessenschafewarenwohlgepflegeundsorgfaltigkeitbeschutzenvonangreifendurchihrraubgierigfeindewelchevoralternzwolftausendjahresvorandieerscheinenvanderersteerdemenschderraumschiffgebrauchlichtalsseinursprungvonkraftgestartseinlangefahrthinzwischensternartigraumaufdersuchenachdiesternwelchegehabtbewohnbarplanetenkreisedrehensichundwohinderneurassevonverstandigmenschlichkeitkonntefortpflanzenundsicherfreuenanlebenslanglichfreudeundruhemitnichteinfurchtvorangreifenvonandererintelligentgeschopfsvonhinzwischensternartigraum", + "rating": 2 + }` + testPostBookHandler(t, bookJson, 400) +} + func testPostBookHandler(t *testing.T, bookJson string, expectedCode int) { router := testSetup() w := httptest.NewRecorder() diff --git a/demodata.sql b/demodata.sql index 50afdb5..4884646 100644 --- a/demodata.sql +++ b/demodata.sql @@ -25,3 +25,4 @@ INSERT INTO books(created_at, title, author, rating) VALUES ('NOW', 'Sa majesté INSERT INTO books(created_at, title, author, rating) VALUES ('NOW', 'Le Pavillon d''or','Yukio Mishima', 8); INSERT INTO books(created_at, title, author, rating) VALUES ('NOW', 'Le meurtre d''O-tsuya','Junichiro Tanizaki', 7); INSERT INTO books(created_at, title, author, rating) VALUES ('NOW', 'Dojoji et autres nouvelles','Yukio Mishima', 8); +INSERT INTO books(created_at, title, author, rating) VALUES ('NOW', 'Noisy outlaws, unfriendly blobs, and some other things that aren''t as scary, maybe, depending on how you feel about lost lands, stray cellphones, creatures from the sky, parents who disappear in Peru, a man named Lars Farf, and one other story we couldn''t quite finish, so maybe you could help us out','Wolfeschlegelsteinhausenbergerdorffwelchevoralternwarengewissenhaftschaferswessenschafewarenwohlgepf', 2); diff --git a/front/src/AddBook.vue b/front/src/AddBook.vue index 9609e92..a78c4f1 100644 --- a/front/src/AddBook.vue +++ b/front/src/AddBook.vue @@ -1,5 +1,5 @@ @@ -21,14 +47,17 @@
- +
+

{{titleError}}

- +
+

{{authorError}}

diff --git a/front/src/api.js b/front/src/api.js index ecb5bb8..79ffd7b 100644 --- a/front/src/api.js +++ b/front/src/api.js @@ -3,13 +3,13 @@ import { ref } from 'vue' const baseUrl = "http://localhost:8080" function useFetch(url) { - const data = ref(null) - const error = ref(null) + const data = ref(null); + const error = ref(null); fetch(url) .then((res) => res.json()) .then((json) => (data.value = json)) - .catch((err) => (error.value = err)) + .catch((err) => (error.value = err)); return { data, error } } @@ -19,11 +19,11 @@ export function getBooks() { } export function postBook(book) { - fetch(baseUrl + '/book', { + return fetch(baseUrl + '/book', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(book.value) - }); + }) } diff --git a/internal/api/dto.go b/internal/api/dto.go index dbb0a56..7bd917a 100644 --- a/internal/api/dto.go +++ b/internal/api/dto.go @@ -1,7 +1,7 @@ package api type bookPostCreate struct { - Title string `json:"title" binding:"required"` - Author string `json:"author"` + Title string `json:"title" binding:"required,max=300"` + Author string `json:"author" binding:"max=100"` Rating int `json:"rating" binding:"min=0,max=10"` } diff --git a/internal/api/routes.go b/internal/api/routes.go index ed17d2a..ad49b6d 100644 --- a/internal/api/routes.go +++ b/internal/api/routes.go @@ -1,10 +1,12 @@ package api import ( + "errors" "net/http" "git.artlef.fr/PersonalLibraryManager/internal/model" "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" "gorm.io/gorm" ) @@ -18,7 +20,12 @@ func PostBookHandler(c *gin.Context, db *gorm.DB) { var book bookPostCreate err := c.ShouldBindJSON(&book) if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + var ve validator.ValidationErrors + if errors.As(err, &ve) { + c.JSON(http.StatusBadRequest, getValidationErrors(&ve)) + } else { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + } return } bookDb := book.toBook() diff --git a/internal/api/validator.go b/internal/api/validator.go new file mode 100644 index 0000000..a63cf3c --- /dev/null +++ b/internal/api/validator.go @@ -0,0 +1,35 @@ +package api + +import ( + "fmt" + + "github.com/go-playground/validator/v10" +) + +type apiValidationError struct { + Field string `json:"field"` + Err string `json:"error"` +} + +func getValidationErrors(ve *validator.ValidationErrors) []apiValidationError { + errors := make([]apiValidationError, len(*ve)) + for i, fe := range *ve { + errors[i] = apiValidationError{ + Field: fe.Field(), + Err: computeValidationMessage(&fe), + } + } + return errors +} + +func computeValidationMessage(fe *validator.FieldError) string { + tag := (*fe).Tag() + switch tag { + case "required": + return fmt.Sprintf("%s is required.", (*fe).Field()) + case "max": + return fmt.Sprintf("%s is too long. It should be under %s characters.", (*fe).Field(), (*fe).Param()) + default: + return fmt.Sprintf("Validation failed for '%s' property.", tag) + } +}