added validation when adding a book
This commit is contained in:
20
api_test.go
20
api_test.go
@@ -63,6 +63,26 @@ func TestPostBookHandler_WrongRating(t *testing.T) {
|
|||||||
testPostBookHandler(t, bookJson, 400)
|
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) {
|
func testPostBookHandler(t *testing.T, bookJson string, expectedCode int) {
|
||||||
router := testSetup()
|
router := testSetup()
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|||||||
@@ -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 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', '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', '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);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref, reactive, computed } from 'vue'
|
||||||
import { postBook } from './api.js'
|
import { postBook } from './api.js'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
|
||||||
@@ -9,10 +9,36 @@
|
|||||||
title: "",
|
title: "",
|
||||||
author: ""
|
author: ""
|
||||||
});
|
});
|
||||||
|
const errors = ref(null)
|
||||||
|
const titleError = computed(() => {
|
||||||
|
return extractErrorFromField("Title");
|
||||||
|
})
|
||||||
|
const authorError = computed(() => {
|
||||||
|
return extractErrorFromField("Author");
|
||||||
|
})
|
||||||
|
|
||||||
|
function extractErrorFromField(fieldName) {
|
||||||
|
if (errors.value === null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const titleErr = errors.value.find((e) => e["field"] === fieldName);
|
||||||
|
if (typeof titleErr !== 'undefined') {
|
||||||
|
return titleErr.error
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function onSubmit(e) {
|
function onSubmit(e) {
|
||||||
postBook(book);
|
postBook(book)
|
||||||
|
.then((res) => {
|
||||||
|
if (res.ok) {
|
||||||
router.push('/');
|
router.push('/');
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
res.json().then((json) => (errors.value = json));
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -21,14 +47,17 @@
|
|||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Title</label>
|
<label class="label">Title</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input class="input" type="text" v-model="book.title" placeholder="Title">
|
<input :class="'input ' + (titleError ? 'is-danger' : '')" type="text" maxlength="300"
|
||||||
|
required v-model="book.title" placeholder="Title">
|
||||||
</div>
|
</div>
|
||||||
|
<p v-if="titleError" class="help is-danger">{{titleError}}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Author</label>
|
<label class="label">Author</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input class="input" type="text" v-model="book.author" placeholder="Author">
|
<input :class="'input ' + (authorError ? 'is-danger' : '')" type="text" maxlength="100" v-model="book.author" placeholder="Author">
|
||||||
</div>
|
</div>
|
||||||
|
<p v-if="authorError" class="help is-danger">{{authorError}}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ import { ref } from 'vue'
|
|||||||
const baseUrl = "http://localhost:8080"
|
const baseUrl = "http://localhost:8080"
|
||||||
|
|
||||||
function useFetch(url) {
|
function useFetch(url) {
|
||||||
const data = ref(null)
|
const data = ref(null);
|
||||||
const error = ref(null)
|
const error = ref(null);
|
||||||
|
|
||||||
fetch(url)
|
fetch(url)
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((json) => (data.value = json))
|
.then((json) => (data.value = json))
|
||||||
.catch((err) => (error.value = err))
|
.catch((err) => (error.value = err));
|
||||||
|
|
||||||
return { data, error }
|
return { data, error }
|
||||||
}
|
}
|
||||||
@@ -19,11 +19,11 @@ export function getBooks() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function postBook(book) {
|
export function postBook(book) {
|
||||||
fetch(baseUrl + '/book', {
|
return fetch(baseUrl + '/book', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify(book.value)
|
body: JSON.stringify(book.value)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
type bookPostCreate struct {
|
type bookPostCreate struct {
|
||||||
Title string `json:"title" binding:"required"`
|
Title string `json:"title" binding:"required,max=300"`
|
||||||
Author string `json:"author"`
|
Author string `json:"author" binding:"max=100"`
|
||||||
Rating int `json:"rating" binding:"min=0,max=10"`
|
Rating int `json:"rating" binding:"min=0,max=10"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.artlef.fr/PersonalLibraryManager/internal/model"
|
"git.artlef.fr/PersonalLibraryManager/internal/model"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,7 +20,12 @@ func PostBookHandler(c *gin.Context, db *gorm.DB) {
|
|||||||
var book bookPostCreate
|
var book bookPostCreate
|
||||||
err := c.ShouldBindJSON(&book)
|
err := c.ShouldBindJSON(&book)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
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()})
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
bookDb := book.toBook()
|
bookDb := book.toBook()
|
||||||
|
|||||||
35
internal/api/validator.go
Normal file
35
internal/api/validator.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user