diff --git a/internal/dto/userbookpostcreate.go b/internal/dto/userbookpostcreate.go new file mode 100644 index 0000000..76f76c9 --- /dev/null +++ b/internal/dto/userbookpostcreate.go @@ -0,0 +1,5 @@ +package dto + +type UserBookPostCreate struct { + BookID uint `json:"bookId" binding:"required"` +} diff --git a/internal/i18nresource/main.go b/internal/i18nresource/main.go index 4226659..7bebd25 100644 --- a/internal/i18nresource/main.go +++ b/internal/i18nresource/main.go @@ -20,7 +20,7 @@ func InitializeI18n() *i18n.Bundle { return bundle } -func GetTranslatedMessage(ac appcontext.AppContext, messageID string) string { +func GetTranslatedMessage(ac *appcontext.AppContext, messageID string) string { localizer := i18n.NewLocalizer(ac.I18n, ac.C.GetHeader("Accept-Language")) message, err := localizer.LocalizeMessage(&i18n.Message{ ID: messageID, diff --git a/internal/mapper/userbookpostcreate.go b/internal/mapper/userbookpostcreate.go new file mode 100644 index 0000000..670e8b2 --- /dev/null +++ b/internal/mapper/userbookpostcreate.go @@ -0,0 +1,13 @@ +package mapper + +import ( + "git.artlef.fr/PersonalLibraryManager/internal/dto" + "git.artlef.fr/PersonalLibraryManager/internal/model" +) + +func UserBookWsToDb(ub dto.UserBookPostCreate, user *model.User) model.UserBook { + return model.UserBook{ + UserID: user.ID, + BookID: ub.BookID, + } +} diff --git a/internal/myvalidator/myvalidator.go b/internal/myvalidator/myvalidator.go index 581ed18..7e1834f 100644 --- a/internal/myvalidator/myvalidator.go +++ b/internal/myvalidator/myvalidator.go @@ -9,23 +9,55 @@ import ( "git.artlef.fr/PersonalLibraryManager/internal/i18nresource" "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" + "gorm.io/gorm" ) +type HttpError struct { + StatusCode int + Err error +} + +func ValidateId(db *gorm.DB, id uint, value any) error { + record := map[string]any{} + result := db.Model(value).First(&record, id) + if result.Error == nil { + return nil + } + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return HttpError{ + StatusCode: http.StatusBadRequest, + Err: fmt.Errorf("Id %d could not be found for model %s", id, value), + } + } else { + return HttpError{ + StatusCode: http.StatusInternalServerError, + Err: result.Error, + } + } +} + +func ReturnErrorsAsJsonResponse(ac *appcontext.AppContext, err error) { + var httpError HttpError + var ve validator.ValidationErrors + if errors.As(err, &ve) { + ac.C.JSON(http.StatusBadRequest, gin.H{"error": getValidationErrors(ac, &ve)}) + } else if errors.As(err, &httpError) { + ac.C.JSON(httpError.StatusCode, gin.H{"error": httpError.Err}) + } else { + ac.C.JSON(http.StatusInternalServerError, gin.H{"error": err}) + } +} + +func (h HttpError) Error() string { + return fmt.Sprintf("%d: err %v", h.StatusCode, h.Err) +} + type apiValidationError struct { Field string `json:"field"` Err string `json:"error"` } -func ManageBindingError(ac appcontext.AppContext, err error) { - var ve validator.ValidationErrors - if errors.As(err, &ve) { - ac.C.JSON(http.StatusBadRequest, getValidationErrors(ac, &ve)) - } else { - ac.C.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - } -} - -func getValidationErrors(ac appcontext.AppContext, ve *validator.ValidationErrors) []apiValidationError { +func getValidationErrors(ac *appcontext.AppContext, ve *validator.ValidationErrors) []apiValidationError { errors := make([]apiValidationError, len(*ve)) for i, fe := range *ve { errors[i] = apiValidationError{ @@ -36,7 +68,7 @@ func getValidationErrors(ac appcontext.AppContext, ve *validator.ValidationError return errors } -func computeValidationMessage(ac appcontext.AppContext, fe *validator.FieldError) string { +func computeValidationMessage(ac *appcontext.AppContext, fe *validator.FieldError) string { tag := (*fe).Tag() switch tag { case "required": diff --git a/internal/routes/bookpostcreate.go b/internal/routes/bookpostcreate.go index 8d8f937..fcbf206 100644 --- a/internal/routes/bookpostcreate.go +++ b/internal/routes/bookpostcreate.go @@ -1,32 +1,29 @@ package routes import ( - "net/http" - "git.artlef.fr/PersonalLibraryManager/internal/appcontext" "git.artlef.fr/PersonalLibraryManager/internal/dto" "git.artlef.fr/PersonalLibraryManager/internal/mapper" "git.artlef.fr/PersonalLibraryManager/internal/model" "git.artlef.fr/PersonalLibraryManager/internal/myvalidator" - "github.com/gin-gonic/gin" ) func PostBookHandler(ac appcontext.AppContext) { var book dto.BookPostCreate err := ac.C.ShouldBindJSON(&book) if err != nil { - myvalidator.ManageBindingError(ac, err) + myvalidator.ReturnErrorsAsJsonResponse(&ac, err) return } user, fetchUserErr := ac.GetAuthenticatedUser() if fetchUserErr != nil { - ac.C.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + myvalidator.ReturnErrorsAsJsonResponse(&ac, err) return } bookDb := mapper.BookWsToDb(book, &user) err = ac.Db.Model(&model.Book{}).Save(&bookDb).Error if err != nil { - ac.C.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + myvalidator.ReturnErrorsAsJsonResponse(&ac, err) return } ac.C.String(200, "Success") diff --git a/internal/routes/bookuserget.go b/internal/routes/bookuserget.go index 82a27b1..53f6981 100644 --- a/internal/routes/bookuserget.go +++ b/internal/routes/bookuserget.go @@ -7,14 +7,14 @@ import ( "git.artlef.fr/PersonalLibraryManager/internal/dto" "git.artlef.fr/PersonalLibraryManager/internal/mapper" "git.artlef.fr/PersonalLibraryManager/internal/model" - "github.com/gin-gonic/gin" + "git.artlef.fr/PersonalLibraryManager/internal/myvalidator" ) func GetMyBooksHanderl(ac appcontext.AppContext) { var userbooks []model.UserBook user, err := ac.GetAuthenticatedUser() if err != nil { - ac.C.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + myvalidator.ReturnErrorsAsJsonResponse(&ac, err) return } ac.Db.Preload("Book").Where("user_id = ?", user.ID).Find(&userbooks) diff --git a/internal/routes/userbookpostcreate.go b/internal/routes/userbookpostcreate.go new file mode 100644 index 0000000..253e270 --- /dev/null +++ b/internal/routes/userbookpostcreate.go @@ -0,0 +1,37 @@ +package routes + +import ( + "net/http" + + "git.artlef.fr/PersonalLibraryManager/internal/appcontext" + "git.artlef.fr/PersonalLibraryManager/internal/dto" + "git.artlef.fr/PersonalLibraryManager/internal/mapper" + "git.artlef.fr/PersonalLibraryManager/internal/model" + "git.artlef.fr/PersonalLibraryManager/internal/myvalidator" +) + +func PostUserBookHandler(ac appcontext.AppContext) { + var userbook dto.UserBookPostCreate + err := ac.C.ShouldBindJSON(&userbook) + if err != nil { + myvalidator.ReturnErrorsAsJsonResponse(&ac, err) + return + } + err = myvalidator.ValidateId(ac.Db, userbook.BookID, &model.Book{}) + if err != nil { + myvalidator.ReturnErrorsAsJsonResponse(&ac, err) + return + } + user, fetchUserErr := ac.GetAuthenticatedUser() + if fetchUserErr != nil { + myvalidator.ReturnErrorsAsJsonResponse(&ac, err) + return + } + userbookDb := mapper.UserBookWsToDb(userbook, &user) + err = ac.Db.Save(&userbookDb).Error + if err != nil { + myvalidator.ReturnErrorsAsJsonResponse(&ac, err) + return + } + ac.C.String(http.StatusOK, "Success") +} diff --git a/internal/routes/userlogin.go b/internal/routes/userlogin.go index a90a069..4c724a8 100644 --- a/internal/routes/userlogin.go +++ b/internal/routes/userlogin.go @@ -19,13 +19,13 @@ func PostLoginHandler(ac appcontext.AppContext) { var user dto.UserLogin err := ac.C.ShouldBindJSON(&user) if err != nil { - myvalidator.ManageBindingError(ac, err) + myvalidator.ReturnErrorsAsJsonResponse(&ac, err) return } if !isUserAndPasswordOk(ac.Db, user.Username, user.Password) { - ac.C.JSON(http.StatusInternalServerError, - gin.H{"error": i18nresource.GetTranslatedMessage(ac, "InvalidCredentials")}) + ac.C.JSON(http.StatusUnauthorized, + gin.H{"error": i18nresource.GetTranslatedMessage(&ac, "InvalidCredentials")}) return } @@ -36,7 +36,7 @@ func PostLoginHandler(ac appcontext.AppContext) { gin.H{"error": fmt.Errorf("Error when generating JWT token: %w", err)}) return } - ac.C.JSON(200, gin.H{"message": i18nresource.GetTranslatedMessage(ac, "AuthenticationSuccess"), "token": jwtToken}) + ac.C.JSON(http.StatusOK, gin.H{"message": i18nresource.GetTranslatedMessage(&ac, "AuthenticationSuccess"), "token": jwtToken}) } func isUserAndPasswordOk(db *gorm.DB, username string, password string) bool { diff --git a/internal/routes/usersignup.go b/internal/routes/usersignup.go index 8926522..be3cd15 100644 --- a/internal/routes/usersignup.go +++ b/internal/routes/usersignup.go @@ -1,31 +1,28 @@ package routes import ( - "net/http" - "git.artlef.fr/PersonalLibraryManager/internal/appcontext" "git.artlef.fr/PersonalLibraryManager/internal/dto" "git.artlef.fr/PersonalLibraryManager/internal/mapper" "git.artlef.fr/PersonalLibraryManager/internal/model" "git.artlef.fr/PersonalLibraryManager/internal/myvalidator" - "github.com/gin-gonic/gin" ) func PostSignupHandler(ac appcontext.AppContext) { var user dto.UserSignup err := ac.C.ShouldBindJSON(&user) if err != nil { - myvalidator.ManageBindingError(ac, err) + myvalidator.ReturnErrorsAsJsonResponse(&ac, err) return } userDb, err := mapper.UserWsToDb(user) if err != nil { - ac.C.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + myvalidator.ReturnErrorsAsJsonResponse(&ac, err) return } err = ac.Db.Model(&model.User{}).Save(&userDb).Error if err != nil { - ac.C.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + myvalidator.ReturnErrorsAsJsonResponse(&ac, err) return } ac.C.String(200, "Success") diff --git a/internal/setup/setup.go b/internal/setup/setup.go index 34a1e69..7793329 100644 --- a/internal/setup/setup.go +++ b/internal/setup/setup.go @@ -32,6 +32,9 @@ func Setup(config *config.Config) *gin.Engine { r.POST("/book", func(c *gin.Context) { routes.PostBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle}) }) + r.POST("/userbook", func(c *gin.Context) { + routes.PostUserBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle}) + }) r.POST("/auth/signup", func(c *gin.Context) { routes.PostSignupHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle}) }) diff --git a/post_userbook_test.go b/post_userbook_test.go new file mode 100644 index 0000000..594503c --- /dev/null +++ b/post_userbook_test.go @@ -0,0 +1,43 @@ +package main + +import ( + "fmt" + "log" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "git.artlef.fr/PersonalLibraryManager/internal/testutils" + "github.com/stretchr/testify/assert" +) + +func TestPostUserBookHandler_Ok(t *testing.T) { + userBookJson := + `{ + "bookId": 6 + }` + testPostUserBookHandler(t, userBookJson, http.StatusOK) +} + +func TestPostUserBookHandler_IDDoesNotExist(t *testing.T) { + userBookJson := + `{ + "bookId": 46546 + }` + testPostUserBookHandler(t, userBookJson, http.StatusBadRequest) +} + +func testPostUserBookHandler(t *testing.T, userBookJson string, expectedCode int) { + router := testutils.TestSetup() + w := httptest.NewRecorder() + + token := testutils.ConnectDemo2User(router) + req, _ := http.NewRequest("POST", "/userbook", + strings.NewReader(string(userBookJson))) + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) + router.ServeHTTP(w, req) + log.Printf("%s\n", w.Body.String()) + assert.Equal(t, expectedCode, w.Code) + +}