From 7841957aae6d0f2ca060ae5e337fece8c423b490 Mon Sep 17 00:00:00 2001 From: kuraishi <20122027@kaishi-pu.ac.jp> Date: Wed, 7 Feb 2024 15:45:41 +0900 Subject: [PATCH 01/20] Move .golangci.yml and update Dockerfile accordingly --- api/Dockerfile | 9 +++------ api/{ => src}/.golangci.yml | 0 docker-compose.yml | 3 +-- 3 files changed, 4 insertions(+), 8 deletions(-) rename api/{ => src}/.golangci.yml (100%) diff --git a/api/Dockerfile b/api/Dockerfile index 9fc979b..6844920 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -2,9 +2,8 @@ FROM golang:1.21.6-alpine AS base WORKDIR /app -COPY ./src /app/src +COPY ./src . -WORKDIR /app/src RUN apk add --no-cache git && \ go install -v golang.org/x/tools/cmd/goimports@latest && \ @@ -19,13 +18,11 @@ FROM base AS development WORKDIR /app -COPY ./.golangci.yml . - RUN wget -O- -nv https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b /usr/local/bin v1.55.2 # lint stage FROM development AS lint -WORKDIR /app/src +WORKDIR /app -CMD [ "golangci-lint","run","--config","/app/.golangci.yml" ] +CMD [ "golangci-lint","run","--config",".golangci.yml" ] diff --git a/api/.golangci.yml b/api/src/.golangci.yml similarity index 100% rename from api/.golangci.yml rename to api/src/.golangci.yml diff --git a/docker-compose.yml b/docker-compose.yml index 20d3cc7..74da114 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,8 +7,7 @@ services: dockerfile: Dockerfile target: development volumes: - - ./api/src:/app/src - - ./api/.golangci.yml:/app/.golangci.yml + - ./api/src:/app ports : - "8080:8080" env_file: From e743fefe8a3c0cfbde7866d9cf217110f155b356 Mon Sep 17 00:00:00 2001 From: kuraishi <20122027@kaishi-pu.ac.jp> Date: Wed, 7 Feb 2024 20:58:14 +0900 Subject: [PATCH 02/20] Implement User-related operations in api-server #9 --- api/.env.api.example | 1 + api/src/controller/api_controller.go | 51 +++++++++++++++++++++++++++ api/src/db/db.go | 2 +- api/src/main.go | 29 ++++++++------- api/src/model/entry.go | 6 ++++ api/src/repository/user_repository.go | 43 ++++++++++++++++++++++ api/src/router/router.go | 13 +++++++ api/src/usecase/user_usecase.go | 44 +++++++++++++++++++++++ cspell.json | 1 + 9 files changed, 177 insertions(+), 13 deletions(-) create mode 100644 api/src/controller/api_controller.go create mode 100644 api/src/repository/user_repository.go create mode 100644 api/src/router/router.go create mode 100644 api/src/usecase/user_usecase.go diff --git a/api/.env.api.example b/api/.env.api.example index b1758dd..2275cde 100644 --- a/api/.env.api.example +++ b/api/.env.api.example @@ -1,3 +1,4 @@ +TIMEZONE=Asia/Tokyo MYSQL_USER=admin MYSQL_PASSWORD=password MYSQL_HOST=mysql diff --git a/api/src/controller/api_controller.go b/api/src/controller/api_controller.go new file mode 100644 index 0000000..643b538 --- /dev/null +++ b/api/src/controller/api_controller.go @@ -0,0 +1,51 @@ +package controller + +import ( + "api/model" + "api/usecase" + "fmt" + "net/http" + "time" + + "github.com/labstack/echo" +) + +type IApiController interface { + RootController(c echo.Context) error +} + +type ApiController struct { + uu usecase.IUserUsecase + location *time.Location +} + +func NewApiController(uu usecase.IUserUsecase, location *time.Location) IApiController { + return &ApiController{uu, location} +} + +func (ac *ApiController) RootController(c echo.Context) error { + request := model.EntryRequest{} + if err := c.Bind(&request); err != nil { + fmt.Println(err.Error()) + return c.JSON(http.StatusBadRequest, err.Error()) + } + + // convert float64 to time.Time + seconds := int64(request.Timestamp) + nanoseconds := int64((request.Timestamp - float64(seconds)) * 1e9) + timestamp := time.Unix(seconds, nanoseconds).In(ac.location) + + user := model.User{ + StudentNumber: request.StudentNumber, + Name: request.Name, + CreatedAt: timestamp, + UpdatedAt: timestamp, + } + + if err := ac.uu.CreateOrUpdateUser(user); err != nil { + fmt.Println(err.Error()) + return c.JSON(http.StatusInternalServerError, err.Error()) + } + + return c.NoContent(http.StatusOK) +} diff --git a/api/src/db/db.go b/api/src/db/db.go index fdd0bbb..08d7498 100644 --- a/api/src/db/db.go +++ b/api/src/db/db.go @@ -11,7 +11,7 @@ import ( func ConnectDB() *gorm.DB { - url := fmt.Sprintf("%s:%s@tcp(%s)/%s", os.Getenv("MYSQL_USER"), os.Getenv("MYSQL_PASSWORD"), os.Getenv("MYSQL_HOST"), os.Getenv("MYSQL_DATABASE")) + url := fmt.Sprintf("%s:%s@tcp(%s)/%s?parseTime=true", os.Getenv("MYSQL_USER"), os.Getenv("MYSQL_PASSWORD"), os.Getenv("MYSQL_HOST"), os.Getenv("MYSQL_DATABASE")) db, err := gorm.Open(mysql.Open(url), &gorm.Config{}) if err != nil { diff --git a/api/src/main.go b/api/src/main.go index 32d6f68..1a4cd2a 100644 --- a/api/src/main.go +++ b/api/src/main.go @@ -1,21 +1,26 @@ package main import ( - "net/http" - - "github.com/labstack/echo" + "api/controller" + "api/db" + "api/repository" + "api/router" + "api/usecase" + "fmt" + "os" + "time" ) func main() { - e := echo.New() - - e.GET("/", hello) + location, err := time.LoadLocation(os.Getenv("TIMEZONE")) + if err != nil { + fmt.Println(err.Error()) + } + db := db.ConnectDB() + userRepository := repository.NewUserRepository(db) + userUsecase := usecase.NewUserUsecase(userRepository) + apiController := controller.NewApiController(userUsecase, location) + e := router.NewRouter(apiController) e.Logger.Fatal(e.Start(":8080")) } - -func hello(c echo.Context) error { - return c.JSON(http.StatusOK, map[string]string{ - "message": "Hello World", - }) -} diff --git a/api/src/model/entry.go b/api/src/model/entry.go index d3946d0..73bb504 100644 --- a/api/src/model/entry.go +++ b/api/src/model/entry.go @@ -8,3 +8,9 @@ type Entry struct { ExitTime time.Time `json:"exit_time"` StudentNumber uint `json:"student_number" gorm:"not null;foreignKey"` } + +type EntryRequest struct { + StudentNumber uint `json:"student_number"` + Name string `json:"name"` + Timestamp float64 `json:"timestamp"` +} diff --git a/api/src/repository/user_repository.go b/api/src/repository/user_repository.go new file mode 100644 index 0000000..2d319f7 --- /dev/null +++ b/api/src/repository/user_repository.go @@ -0,0 +1,43 @@ +package repository + +import ( + "api/model" + + "gorm.io/gorm" +) + +type IUserRepository interface { + CreateUser(user *model.User) error + UpdateUser(user *model.User) error + GetUserByStudentNumber(user *model.User,studentNumber uint) error +} + +type UserRepository struct { + db *gorm.DB +} + +func NewUserRepository(db *gorm.DB) *UserRepository { + return &UserRepository{db} +} + +func (ur *UserRepository) CreateUser(user *model.User) error { + if err := ur.db.Create(user).Error; err != nil { + return err + } + return nil +} + +func (ur *UserRepository) UpdateUser(user *model.User) error { + if err := ur.db.Save(user).Error; err != nil { + return err + } + return nil + +} + +func (ur *UserRepository) GetUserByStudentNumber(user *model.User,studentNumber uint) error { + if err := ur.db.Where("student_number=?", studentNumber).First(user).Error; err != nil { + return err + } + return nil +} diff --git a/api/src/router/router.go b/api/src/router/router.go new file mode 100644 index 0000000..d28f8c1 --- /dev/null +++ b/api/src/router/router.go @@ -0,0 +1,13 @@ +package router + +import ( + "api/controller" + + "github.com/labstack/echo" +) + +func NewRouter(uc controller.IApiController) *echo.Echo { + e := echo.New() + e.POST("/", uc.RootController) + return e +} diff --git a/api/src/usecase/user_usecase.go b/api/src/usecase/user_usecase.go new file mode 100644 index 0000000..a56372c --- /dev/null +++ b/api/src/usecase/user_usecase.go @@ -0,0 +1,44 @@ +package usecase + +import ( + "api/model" + "api/repository" + "errors" + + "gorm.io/gorm" +) + +type IUserUsecase interface { + CreateOrUpdateUser(user model.User) error +} + +type UserUsecase struct { + ur repository.IUserRepository +} + +func NewUserUsecase(ur repository.IUserRepository) IUserUsecase { + return &UserUsecase{ur: ur} +} + +func (uu *UserUsecase) CreateOrUpdateUser(user model.User) error { + DBUser := model.User{} + + if err := uu.ur.GetUserByStudentNumber(&DBUser, user.StudentNumber); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + if err := uu.ur.CreateUser(&user); err != nil { + return err + } + } else { + return err + } + } + + //Update user + if DBUser.Name != user.Name { + if err := uu.ur.UpdateUser(&user); err != nil { + return err + } + } + + return nil +} diff --git a/cspell.json b/cspell.json index e2bd3c9..e19de19 100644 --- a/cspell.json +++ b/cspell.json @@ -14,6 +14,7 @@ "words": [ "wontfix", "venv", + "usecase", "usbutils", "rdwr", "pyproject", From 7725e18af4982e22d166e30de908640a313402f8 Mon Sep 17 00:00:00 2001 From: Kuraishi Taiki <20122027@kaishi-pu.ac.jp> Date: Wed, 7 Feb 2024 21:10:41 +0900 Subject: [PATCH 03/20] Implement Entry-related operations in api-server #9 --- api/src/controller/api_controller.go | 35 +++++++++++++++--- api/src/main.go | 4 ++- api/src/model/entry.go | 8 ++--- api/src/repository/entry_repository.go | 50 ++++++++++++++++++++++++++ api/src/repository/user_repository.go | 2 +- api/src/usecase/entry_usecase.go | 41 +++++++++++++++++++++ api/src/usecase/user_usecase.go | 26 +++++++------- cspell.json | 1 + 8 files changed, 143 insertions(+), 24 deletions(-) create mode 100644 api/src/repository/entry_repository.go create mode 100644 api/src/usecase/entry_usecase.go diff --git a/api/src/controller/api_controller.go b/api/src/controller/api_controller.go index 643b538..bd3c596 100644 --- a/api/src/controller/api_controller.go +++ b/api/src/controller/api_controller.go @@ -16,15 +16,29 @@ type IApiController interface { type ApiController struct { uu usecase.IUserUsecase + eu usecase.IEntryUsecase location *time.Location } -func NewApiController(uu usecase.IUserUsecase, location *time.Location) IApiController { - return &ApiController{uu, location} +type Response struct { + UserMessage string `json:"user_message"` + EntryMessage string `json:"entry_message"` +} + + +type EntryRequest struct { + StudentNumber uint `json:"student_number"` + Name string `json:"name"` + Timestamp float64 `json:"timestamp"` +} + + +func NewApiController(uu usecase.IUserUsecase, eu usecase.IEntryUsecase, location *time.Location) IApiController { + return &ApiController{uu, eu, location} } func (ac *ApiController) RootController(c echo.Context) error { - request := model.EntryRequest{} + request := EntryRequest{} if err := c.Bind(&request); err != nil { fmt.Println(err.Error()) return c.JSON(http.StatusBadRequest, err.Error()) @@ -42,10 +56,21 @@ func (ac *ApiController) RootController(c echo.Context) error { UpdatedAt: timestamp, } - if err := ac.uu.CreateOrUpdateUser(user); err != nil { + userMessage, err := ac.uu.CreateOrUpdateUser(user) + if err != nil { fmt.Println(err.Error()) return c.JSON(http.StatusInternalServerError, err.Error()) } - return c.NoContent(http.StatusOK) + entryMessage, err := ac.eu.EntryOrExit(request.StudentNumber, timestamp) + if err != nil { + return c.JSON(http.StatusInternalServerError, err.Error()) + } + + response := Response{ + UserMessage: userMessage, + EntryMessage: entryMessage, + } + + return c.JSON(http.StatusOK, response) } diff --git a/api/src/main.go b/api/src/main.go index 1a4cd2a..e7b89d0 100644 --- a/api/src/main.go +++ b/api/src/main.go @@ -19,8 +19,10 @@ func main() { db := db.ConnectDB() userRepository := repository.NewUserRepository(db) + entryRepository := repository.NewEntryRepository(db) userUsecase := usecase.NewUserUsecase(userRepository) - apiController := controller.NewApiController(userUsecase, location) + entryUsecase := usecase.NewEntryUsecase(entryRepository) + apiController := controller.NewApiController(userUsecase, entryUsecase, location) e := router.NewRouter(apiController) e.Logger.Fatal(e.Start(":8080")) } diff --git a/api/src/model/entry.go b/api/src/model/entry.go index 73bb504..f1085d7 100644 --- a/api/src/model/entry.go +++ b/api/src/model/entry.go @@ -3,10 +3,10 @@ package model import "time" type Entry struct { - ID uint `json:"id" gorm:"primary_key"` - EntryTime time.Time `json:"entry_time"` - ExitTime time.Time `json:"exit_time"` - StudentNumber uint `json:"student_number" gorm:"not null;foreignKey"` + ID uint `json:"id" gorm:"primary_key"` + EntryTime time.Time `json:"entry_time" gorm:"not null"` + ExitTime *time.Time `json:"exit_time"` + StudentNumber uint `json:"student_number" gorm:"not null;foreignKey"` } type EntryRequest struct { diff --git a/api/src/repository/entry_repository.go b/api/src/repository/entry_repository.go new file mode 100644 index 0000000..7e8b756 --- /dev/null +++ b/api/src/repository/entry_repository.go @@ -0,0 +1,50 @@ +package repository + +import ( + "api/model" + "time" + + "gorm.io/gorm" +) + +type IEntryRepository interface { + Entry(studentNumber uint, entryTime time.Time) error + Exit(id uint, exitTime time.Time) error + FindUnexitedEntry(entry *model.Entry, studentNumber uint) error +} + +type EntryRepository struct { + db *gorm.DB +} + +func NewEntryRepository(db *gorm.DB) *EntryRepository { + return &EntryRepository{db} +} + +func (er *EntryRepository) Entry(studentNumber uint, entryTime time.Time) error { + newEntry := model.Entry{ + EntryTime: entryTime, + ExitTime: nil, + StudentNumber: studentNumber, + } + + if err := er.db.Create(&newEntry).Error; err != nil { + return err + } + + return nil +} + +func (er *EntryRepository) Exit(id uint, exitTime time.Time) error { + if err := er.db.Model(&model.Entry{}).Where("id=?", id).Update("exit_time", exitTime).Error; err != nil { + return err + } + return nil +} + +func (er *EntryRepository) FindUnexitedEntry(entry *model.Entry, studentNumber uint) error { + if err := er.db.Where("student_number=? AND exit_time IS NULL", studentNumber).FirstOrInit(&entry).Error; err != nil { + return err + } + return nil +} diff --git a/api/src/repository/user_repository.go b/api/src/repository/user_repository.go index 2d319f7..3baa286 100644 --- a/api/src/repository/user_repository.go +++ b/api/src/repository/user_repository.go @@ -36,7 +36,7 @@ func (ur *UserRepository) UpdateUser(user *model.User) error { } func (ur *UserRepository) GetUserByStudentNumber(user *model.User,studentNumber uint) error { - if err := ur.db.Where("student_number=?", studentNumber).First(user).Error; err != nil { + if err := ur.db.Where("student_number=?", studentNumber).FirstOrInit(user).Error; err != nil { return err } return nil diff --git a/api/src/usecase/entry_usecase.go b/api/src/usecase/entry_usecase.go new file mode 100644 index 0000000..610f666 --- /dev/null +++ b/api/src/usecase/entry_usecase.go @@ -0,0 +1,41 @@ +package usecase + +import ( + "api/model" + "api/repository" + "time" +) + +type IEntryUsecase interface { + EntryOrExit(studentNumber uint, timestamp time.Time) (string, error) +} + +type EntryUsecase struct { + er repository.IEntryRepository +} + +func NewEntryUsecase(er repository.IEntryRepository) IEntryUsecase { + return &EntryUsecase{er: er} +} + +func (eu *EntryUsecase) EntryOrExit(studentNumber uint, timestamp time.Time) (string, error) { + newEntry := model.Entry{} + if err := eu.er.FindUnexitedEntry(&newEntry, studentNumber); err != nil { + return "", err + } + + //entry + if newEntry.ID == 0 { + if err := eu.er.Entry(studentNumber, timestamp); err != nil { + return "", err + } + return "entry success", nil + } + + //exit + if err := eu.er.Exit(newEntry.ID, timestamp); err != nil { + return "", err + } + + return "exit success", nil +} diff --git a/api/src/usecase/user_usecase.go b/api/src/usecase/user_usecase.go index a56372c..17ae478 100644 --- a/api/src/usecase/user_usecase.go +++ b/api/src/usecase/user_usecase.go @@ -3,13 +3,10 @@ package usecase import ( "api/model" "api/repository" - "errors" - - "gorm.io/gorm" ) type IUserUsecase interface { - CreateOrUpdateUser(user model.User) error + CreateOrUpdateUser(user model.User) (string, error) } type UserUsecase struct { @@ -20,25 +17,28 @@ func NewUserUsecase(ur repository.IUserRepository) IUserUsecase { return &UserUsecase{ur: ur} } -func (uu *UserUsecase) CreateOrUpdateUser(user model.User) error { +func (uu *UserUsecase) CreateOrUpdateUser(user model.User) (string, error) { DBUser := model.User{} if err := uu.ur.GetUserByStudentNumber(&DBUser, user.StudentNumber); err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - if err := uu.ur.CreateUser(&user); err != nil { - return err - } - } else { - return err + return "", err + } + + //Create user + if DBUser.StudentNumber == 0 { + if err := uu.ur.CreateUser(&user); err != nil { + return "", err } + return "User created", nil } //Update user if DBUser.Name != user.Name { if err := uu.ur.UpdateUser(&user); err != nil { - return err + return "", err } + return "User updated", nil } - return nil + return "User already exists", nil } diff --git a/cspell.json b/cspell.json index e19de19..6afedc0 100644 --- a/cspell.json +++ b/cspell.json @@ -16,6 +16,7 @@ "venv", "usecase", "usbutils", + "Unexited", "rdwr", "pyproject", "pylint", From f225ba8be866b870b9496e1934e4eae97e5ef707 Mon Sep 17 00:00:00 2001 From: kuraishi <20122027@kaishi-pu.ac.jp> Date: Thu, 8 Feb 2024 12:39:39 +0900 Subject: [PATCH 04/20] Add user validator in api-server #9 --- api/src/controller/api_controller.go | 17 +++++----- api/src/go.mod | 1 + api/src/go.sum | 11 +++++-- api/src/main.go | 14 +++------ api/src/usecase/user_usecase.go | 11 +++++-- api/src/validator/user_validator.go | 46 ++++++++++++++++++++++++++++ cspell.json | 1 + 7 files changed, 77 insertions(+), 24 deletions(-) create mode 100644 api/src/validator/user_validator.go diff --git a/api/src/controller/api_controller.go b/api/src/controller/api_controller.go index bd3c596..a28c4b6 100644 --- a/api/src/controller/api_controller.go +++ b/api/src/controller/api_controller.go @@ -15,26 +15,23 @@ type IApiController interface { } type ApiController struct { - uu usecase.IUserUsecase - eu usecase.IEntryUsecase - location *time.Location + uu usecase.IUserUsecase + eu usecase.IEntryUsecase } type Response struct { - UserMessage string `json:"user_message"` - EntryMessage string `json:"entry_message"` + UserMessage string `json:"user_message"` + EntryMessage string `json:"entry_message"` } - type EntryRequest struct { StudentNumber uint `json:"student_number"` Name string `json:"name"` Timestamp float64 `json:"timestamp"` } - -func NewApiController(uu usecase.IUserUsecase, eu usecase.IEntryUsecase, location *time.Location) IApiController { - return &ApiController{uu, eu, location} +func NewApiController(uu usecase.IUserUsecase, eu usecase.IEntryUsecase) IApiController { + return &ApiController{uu, eu} } func (ac *ApiController) RootController(c echo.Context) error { @@ -47,7 +44,7 @@ func (ac *ApiController) RootController(c echo.Context) error { // convert float64 to time.Time seconds := int64(request.Timestamp) nanoseconds := int64((request.Timestamp - float64(seconds)) * 1e9) - timestamp := time.Unix(seconds, nanoseconds).In(ac.location) + timestamp := time.Unix(seconds, nanoseconds) user := model.User{ StudentNumber: request.StudentNumber, diff --git a/api/src/go.mod b/api/src/go.mod index 4ee9540..dc1a048 100644 --- a/api/src/go.mod +++ b/api/src/go.mod @@ -3,6 +3,7 @@ module api go 1.21.6 require ( + github.com/go-ozzo/ozzo-validation/v4 v4.3.0 github.com/labstack/echo v3.3.10+incompatible gorm.io/driver/mysql v1.5.2 gorm.io/gorm v1.25.6 diff --git a/api/src/go.sum b/api/src/go.sum index 2ab6065..be9b8d2 100644 --- a/api/src/go.sum +++ b/api/src/go.sum @@ -1,13 +1,16 @@ +github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 h1:zV3ejI06GQ59hwDQAvmK1qxOQGB3WuVTRoY0okPTAv0= +github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es= +github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew= github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= -github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg= github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= @@ -19,6 +22,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= @@ -35,6 +40,8 @@ golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs= diff --git a/api/src/main.go b/api/src/main.go index e7b89d0..97fdc67 100644 --- a/api/src/main.go +++ b/api/src/main.go @@ -6,23 +6,17 @@ import ( "api/repository" "api/router" "api/usecase" - "fmt" - "os" - "time" + "api/validator" ) func main() { - location, err := time.LoadLocation(os.Getenv("TIMEZONE")) - if err != nil { - fmt.Println(err.Error()) - } - db := db.ConnectDB() + userValidator := validator.NewUserValidator() userRepository := repository.NewUserRepository(db) entryRepository := repository.NewEntryRepository(db) - userUsecase := usecase.NewUserUsecase(userRepository) + userUsecase := usecase.NewUserUsecase(userRepository, userValidator) entryUsecase := usecase.NewEntryUsecase(entryRepository) - apiController := controller.NewApiController(userUsecase, entryUsecase, location) + apiController := controller.NewApiController(userUsecase, entryUsecase) e := router.NewRouter(apiController) e.Logger.Fatal(e.Start(":8080")) } diff --git a/api/src/usecase/user_usecase.go b/api/src/usecase/user_usecase.go index 17ae478..645467d 100644 --- a/api/src/usecase/user_usecase.go +++ b/api/src/usecase/user_usecase.go @@ -3,6 +3,7 @@ package usecase import ( "api/model" "api/repository" + "api/validator" ) type IUserUsecase interface { @@ -11,13 +12,19 @@ type IUserUsecase interface { type UserUsecase struct { ur repository.IUserRepository + uv validator.IUserValidator } -func NewUserUsecase(ur repository.IUserRepository) IUserUsecase { - return &UserUsecase{ur: ur} +func NewUserUsecase(ur repository.IUserRepository, uv validator.IUserValidator) IUserUsecase { + return &UserUsecase{ur: ur, uv: uv} } func (uu *UserUsecase) CreateOrUpdateUser(user model.User) (string, error) { + //Validate user + if eer := uu.uv.UserValidation(user); eer != nil { + return "", eer + } + DBUser := model.User{} if err := uu.ur.GetUserByStudentNumber(&DBUser, user.StudentNumber); err != nil { diff --git a/api/src/validator/user_validator.go b/api/src/validator/user_validator.go new file mode 100644 index 0000000..d48974e --- /dev/null +++ b/api/src/validator/user_validator.go @@ -0,0 +1,46 @@ +package validator + +import ( + "api/model" + "time" + + validation "github.com/go-ozzo/ozzo-validation/v4" +) + +type IUserValidator interface { + UserValidation(user model.User) error +} + +type UserValidator struct{} + +func NewUserValidator() IUserValidator { + return &UserValidator{} +} + +func (uv *UserValidator) UserValidation(user model.User) error { + return validation.ValidateStruct(&user, + validation.Field( + &user.StudentNumber, + validation.Required.Error("student number is required"), + validation.Min(uint(10000000)).Error("student number must be greater than 20121000"), + validation.Max(uint(39999999)).Error("student number must be less than 20130000"), + ), + validation.Field( + &user.Name, + validation.Required.Error("name is required"), + validation.RuneLength(2, 32).Error("name must be between 3 and 50 characters"), + ), + validation.Field( + &user.CreatedAt, + validation.Required.Error("created at is required"), + validation.Min(time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)).Error("created at must be after 2024"), + validation.Max(time.Now()).Error("created at must be before now"), + ), + validation.Field( + &user.UpdatedAt, + validation.Required.Error("updated at is required"), + validation.Min(time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)).Error("updated at must be after 2024"), + validation.Max(time.Now()).Error("updated at must be before now"), + ), + ) +} diff --git a/cspell.json b/cspell.json index 6afedc0..05224ea 100644 --- a/cspell.json +++ b/cspell.json @@ -20,6 +20,7 @@ "rdwr", "pyproject", "pylint", + "ozzo", "mypy", "labstack", "isort", From a636e44fa5e0029f8a63f4e7522d6074cc17f568 Mon Sep 17 00:00:00 2001 From: kuraishi <20122027@kaishi-pu.ac.jp> Date: Thu, 8 Feb 2024 14:20:25 +0900 Subject: [PATCH 05/20] Add entry validator in api-server #9 --- api/.env.api.example | 3 + api/src/main.go | 3 +- api/src/model/entry.go | 6 -- api/src/model/user.go | 4 +- api/src/repository/entry_repository.go | 23 ++--- api/src/repository/user_repository.go | 4 +- api/src/usecase/entry_usecase.go | 32 +++++-- api/src/validator/entry_validator.go | 112 +++++++++++++++++++++++++ 8 files changed, 155 insertions(+), 32 deletions(-) create mode 100644 api/src/validator/entry_validator.go diff --git a/api/.env.api.example b/api/.env.api.example index 2275cde..278bb44 100644 --- a/api/.env.api.example +++ b/api/.env.api.example @@ -1,4 +1,7 @@ TIMEZONE=Asia/Tokyo +TIME_VALIDATION_MIN=1704034800 +STUDENT_NUMBER_MIN=10000000 +STUDENT_NUMBER_MAX=39999999 MYSQL_USER=admin MYSQL_PASSWORD=password MYSQL_HOST=mysql diff --git a/api/src/main.go b/api/src/main.go index 97fdc67..92ec24b 100644 --- a/api/src/main.go +++ b/api/src/main.go @@ -11,11 +11,12 @@ import ( func main() { db := db.ConnectDB() + entryValidator := validator.NewEntryValidator() userValidator := validator.NewUserValidator() userRepository := repository.NewUserRepository(db) entryRepository := repository.NewEntryRepository(db) + entryUsecase := usecase.NewEntryUsecase(entryRepository, entryValidator) userUsecase := usecase.NewUserUsecase(userRepository, userValidator) - entryUsecase := usecase.NewEntryUsecase(entryRepository) apiController := controller.NewApiController(userUsecase, entryUsecase) e := router.NewRouter(apiController) e.Logger.Fatal(e.Start(":8080")) diff --git a/api/src/model/entry.go b/api/src/model/entry.go index f1085d7..6ef87a1 100644 --- a/api/src/model/entry.go +++ b/api/src/model/entry.go @@ -8,9 +8,3 @@ type Entry struct { ExitTime *time.Time `json:"exit_time"` StudentNumber uint `json:"student_number" gorm:"not null;foreignKey"` } - -type EntryRequest struct { - StudentNumber uint `json:"student_number"` - Name string `json:"name"` - Timestamp float64 `json:"timestamp"` -} diff --git a/api/src/model/user.go b/api/src/model/user.go index 9be0d15..38fa54a 100644 --- a/api/src/model/user.go +++ b/api/src/model/user.go @@ -1,8 +1,6 @@ package model -import ( - "time" -) +import "time" type User struct { StudentNumber uint `json:"student_number" gorm:"primary_key"` diff --git a/api/src/repository/entry_repository.go b/api/src/repository/entry_repository.go index 7e8b756..10803ae 100644 --- a/api/src/repository/entry_repository.go +++ b/api/src/repository/entry_repository.go @@ -2,15 +2,14 @@ package repository import ( "api/model" - "time" "gorm.io/gorm" ) type IEntryRepository interface { - Entry(studentNumber uint, entryTime time.Time) error - Exit(id uint, exitTime time.Time) error - FindUnexitedEntry(entry *model.Entry, studentNumber uint) error + CreateEntry(entry *model.Entry) error + UpdateEntry(entry *model.Entry) error + GetStudentNumberWithNullExitTime(entry *model.Entry, studentNumber uint) error } type EntryRepository struct { @@ -21,28 +20,22 @@ func NewEntryRepository(db *gorm.DB) *EntryRepository { return &EntryRepository{db} } -func (er *EntryRepository) Entry(studentNumber uint, entryTime time.Time) error { - newEntry := model.Entry{ - EntryTime: entryTime, - ExitTime: nil, - StudentNumber: studentNumber, - } - - if err := er.db.Create(&newEntry).Error; err != nil { +func (er *EntryRepository) CreateEntry(entry *model.Entry) error { + if err := er.db.Create(&entry).Error; err != nil { return err } return nil } -func (er *EntryRepository) Exit(id uint, exitTime time.Time) error { - if err := er.db.Model(&model.Entry{}).Where("id=?", id).Update("exit_time", exitTime).Error; err != nil { +func (er *EntryRepository) UpdateEntry(entry *model.Entry) error { + if err := er.db.Model(&model.Entry{}).Where("id=?", entry.ID).Update("exit_time", entry.ExitTime).Error; err != nil { return err } return nil } -func (er *EntryRepository) FindUnexitedEntry(entry *model.Entry, studentNumber uint) error { +func (er *EntryRepository) GetStudentNumberWithNullExitTime(entry *model.Entry, studentNumber uint) error { if err := er.db.Where("student_number=? AND exit_time IS NULL", studentNumber).FirstOrInit(&entry).Error; err != nil { return err } diff --git a/api/src/repository/user_repository.go b/api/src/repository/user_repository.go index 3baa286..61f3936 100644 --- a/api/src/repository/user_repository.go +++ b/api/src/repository/user_repository.go @@ -9,7 +9,7 @@ import ( type IUserRepository interface { CreateUser(user *model.User) error UpdateUser(user *model.User) error - GetUserByStudentNumber(user *model.User,studentNumber uint) error + GetUserByStudentNumber(user *model.User, studentNumber uint) error } type UserRepository struct { @@ -35,7 +35,7 @@ func (ur *UserRepository) UpdateUser(user *model.User) error { } -func (ur *UserRepository) GetUserByStudentNumber(user *model.User,studentNumber uint) error { +func (ur *UserRepository) GetUserByStudentNumber(user *model.User, studentNumber uint) error { if err := ur.db.Where("student_number=?", studentNumber).FirstOrInit(user).Error; err != nil { return err } diff --git a/api/src/usecase/entry_usecase.go b/api/src/usecase/entry_usecase.go index 610f666..ddba212 100644 --- a/api/src/usecase/entry_usecase.go +++ b/api/src/usecase/entry_usecase.go @@ -3,6 +3,7 @@ package usecase import ( "api/model" "api/repository" + "api/validator" "time" ) @@ -12,28 +13,49 @@ type IEntryUsecase interface { type EntryUsecase struct { er repository.IEntryRepository + ev validator.IEntryValidator } -func NewEntryUsecase(er repository.IEntryRepository) IEntryUsecase { - return &EntryUsecase{er: er} +func NewEntryUsecase(er repository.IEntryRepository, ev validator.IEntryValidator) IEntryUsecase { + return &EntryUsecase{er: er, ev: ev} } func (eu *EntryUsecase) EntryOrExit(studentNumber uint, timestamp time.Time) (string, error) { + if err := eu.ev.StudentNumberValidation(studentNumber); err != nil { + return "", err + } + newEntry := model.Entry{} - if err := eu.er.FindUnexitedEntry(&newEntry, studentNumber); err != nil { + if err := eu.er.GetStudentNumberWithNullExitTime(&newEntry, studentNumber); err != nil { return "", err } //entry if newEntry.ID == 0 { - if err := eu.er.Entry(studentNumber, timestamp); err != nil { + newEntry = model.Entry{ + EntryTime: timestamp, + ExitTime: nil, + StudentNumber: studentNumber, + } + + if err := eu.ev.EntryValidation(newEntry); err != nil { + return "", err + } + + if err := eu.er.CreateEntry(&newEntry); err != nil { return "", err } return "entry success", nil } //exit - if err := eu.er.Exit(newEntry.ID, timestamp); err != nil { + newEntry.ExitTime = ×tamp + + if err := eu.ev.EntryValidation(newEntry); err != nil { + return "", err + } + + if err := eu.er.UpdateEntry(&newEntry); err != nil { return "", err } diff --git a/api/src/validator/entry_validator.go b/api/src/validator/entry_validator.go new file mode 100644 index 0000000..f2920a0 --- /dev/null +++ b/api/src/validator/entry_validator.go @@ -0,0 +1,112 @@ +package validator + +import ( + "api/model" + "os" + "strconv" + "time" + + validation "github.com/go-ozzo/ozzo-validation/v4" +) + +type IEntryValidator interface { + StudentNumberValidation(studentNumber uint) error + EntryValidation(entry model.Entry) error +} + +type EntryValidator struct{} + +func NewEntryValidator() IEntryValidator { + return &EntryValidator{} +} + +func (ev *EntryValidator) StudentNumberValidation(studentNumber uint) error { + studentNumberMin, err := strconv.ParseUint(os.Getenv("STUDENT_NUMBER_MIN"), 10, 64) + if err != nil { + return err + } + + studentNumberMax, err := strconv.ParseUint(os.Getenv("STUDENT_NUMBER_MAX"), 10, 64) + if err != nil { + return err + } + + return validation.Validate(studentNumber, + validation.Required.Error("student number is required"), + validation.Min(studentNumberMin).Error("student number must be greater than 10000000"), + validation.Max(studentNumberMax).Error("student number must be less than 39999999"), + ) +} + +type TimeAfter struct { + required time.Time +} + +func (t TimeAfter) Validate(value interface{}) error { + if timeValue, ok := value.(*time.Time); ok { + if timeValue.Before(t.required) { + return validation.NewError("validation_min", "must be after "+t.required.String()) + } + } + return nil +} + +type TimeBefore struct { + required time.Time +} + +func (t TimeBefore) Validate(value interface{}) error { + if timeValue, ok := value.(*time.Time); ok { + if timeValue.After(t.required) { + return validation.NewError("validation_max", "must be before "+t.required.String()) + } + } + return nil +} + +func (ev *EntryValidator) EntryValidation(entry model.Entry) error { + studentNumberMin, err := strconv.ParseUint(os.Getenv("STUDENT_NUMBER_MIN"), 10, 64) + if err != nil { + return err + } + + studentNumberMax, err := strconv.ParseUint(os.Getenv("STUDENT_NUMBER_MAX"), 10, 64) + if err != nil { + return err + } + + TimeValidationMin, err := strconv.ParseInt(os.Getenv("TIME_VALIDATION_MIN"), 10, 64) + if err != nil { + return err + } + + err = validation.ValidateStruct(&entry, + validation.Field( + &entry.EntryTime, + validation.Required.Error("entry time is required"), + TimeAfter{required: time.Unix(TimeValidationMin, 0)}, + TimeBefore{required: time.Now()}, + ), + validation.Field( + &entry.StudentNumber, + validation.Required.Error("student number is required"), + validation.Min(studentNumberMin).Error("student number must be greater than 10000000"), + validation.Max(studentNumberMax).Error("student number must be less than 39999999"), + ), + ) + if err != nil { + return err + } + + if entry.ExitTime != nil { + err = validation.ValidateStruct(&entry, + validation.Field( + &entry.ExitTime, + TimeAfter{required: entry.EntryTime}, + TimeBefore{required: time.Now()}, + ), + ) + } + + return err +} From 8d80f3d4add47d3c5f0e48cb995d5fc0f6bc3596 Mon Sep 17 00:00:00 2001 From: kuraishi <20122027@kaishi-pu.ac.jp> Date: Fri, 9 Feb 2024 16:07:12 +0900 Subject: [PATCH 06/20] Add tests for entry_validator.go in spi-server #9 --- api/src/go.mod | 4 + api/src/go.sum | 1 + api/src/validator/entry_validator.go | 6 +- api/src/validator/entry_validator_test.go | 202 ++++++++++++++++++++++ cspell.json | 1 + 5 files changed, 211 insertions(+), 3 deletions(-) create mode 100644 api/src/validator/entry_validator_test.go diff --git a/api/src/go.mod b/api/src/go.mod index dc1a048..74a0e6f 100644 --- a/api/src/go.mod +++ b/api/src/go.mod @@ -5,21 +5,25 @@ go 1.21.6 require ( github.com/go-ozzo/ozzo-validation/v4 v4.3.0 github.com/labstack/echo v3.3.10+incompatible + github.com/stretchr/testify v1.8.4 gorm.io/driver/mysql v1.5.2 gorm.io/gorm v1.25.6 ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-sql-driver/mysql v1.7.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/labstack/gommon v0.4.2 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect golang.org/x/crypto v0.18.0 // indirect golang.org/x/net v0.10.0 // indirect golang.org/x/sys v0.16.0 // indirect golang.org/x/text v0.14.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/api/src/go.sum b/api/src/go.sum index be9b8d2..9eb7a6e 100644 --- a/api/src/go.sum +++ b/api/src/go.sum @@ -40,6 +40,7 @@ golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +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.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/api/src/validator/entry_validator.go b/api/src/validator/entry_validator.go index f2920a0..16b37df 100644 --- a/api/src/validator/entry_validator.go +++ b/api/src/validator/entry_validator.go @@ -84,8 +84,8 @@ func (ev *EntryValidator) EntryValidation(entry model.Entry) error { validation.Field( &entry.EntryTime, validation.Required.Error("entry time is required"), - TimeAfter{required: time.Unix(TimeValidationMin, 0)}, - TimeBefore{required: time.Now()}, + validation.Min(time.Unix(TimeValidationMin, 0)).Error("must be after "+time.Unix(TimeValidationMin, 0).String()), + validation.Max(time.Now()).Error("must be before "+time.Now().Round(time.Second).String()), ), validation.Field( &entry.StudentNumber, @@ -103,7 +103,7 @@ func (ev *EntryValidator) EntryValidation(entry model.Entry) error { validation.Field( &entry.ExitTime, TimeAfter{required: entry.EntryTime}, - TimeBefore{required: time.Now()}, + TimeBefore{required: time.Now().Round(time.Second)}, ), ) } diff --git a/api/src/validator/entry_validator_test.go b/api/src/validator/entry_validator_test.go new file mode 100644 index 0000000..5717b63 --- /dev/null +++ b/api/src/validator/entry_validator_test.go @@ -0,0 +1,202 @@ +package validator + +import ( + "api/model" + "os" + "strconv" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestEntryValidator_StudentNumberValidation(t *testing.T) { + StudentNumberMin, err := strconv.ParseUint(os.Getenv("STUDENT_NUMBER_MIN"), 10, 64) + assert.NoError(t, err) + + StudentNumberMax, err := strconv.ParseUint(os.Getenv("STUDENT_NUMBER_MAX"), 10, 64) + assert.NoError(t, err) + + ev := &EntryValidator{} + + // Test case 1 valid student number + err = ev.StudentNumberValidation(20122027) + assert.NoError(t, err) + + // Test case 2 invalid student number (not required) + err = ev.StudentNumberValidation(0) + exceptedErrorMessages := "student number is required" + assert.Equal(t, exceptedErrorMessages, err.Error()) + + // Test case 3 invalid student number (just below minimum value) + err = ev.StudentNumberValidation(uint(StudentNumberMin - 1)) + exceptedErrorMessages = "student number must be greater than 10000000" + assert.Equal(t, exceptedErrorMessages, err.Error()) + + // Test case 4 invalid student number (just above maximum value) + err = ev.StudentNumberValidation(uint(StudentNumberMax + 1)) + exceptedErrorMessages = "student number must be less than 39999999" + assert.Equal(t, exceptedErrorMessages, err.Error()) + + // Test case 5 valid student number (minimum allowed value) + err = ev.StudentNumberValidation(uint(StudentNumberMin)) + assert.NoError(t, err) + + // Test case 6 valid student number (maximum allowed value) + err = ev.StudentNumberValidation(uint(StudentNumberMax)) + assert.NoError(t, err) + +} + +func TestEntryValidator_EntryValidation(t *testing.T) { + TimeValidationMin, err := strconv.ParseInt(os.Getenv("TIME_VALIDATION_MIN"), 10, 64) + assert.NoError(t, err) + + ev := &EntryValidator{} + + // Test case 1 valid entry + entry := model.Entry{ + EntryTime: time.Now(), + ExitTime: nil, + StudentNumber: 20122027, + } + err = ev.EntryValidation(entry) + assert.NoError(t, err) + + //Test case 2 invalid EntryTime (not required ExitTime) + entry = model.Entry{ + ExitTime: nil, + StudentNumber: 20122027, + } + err = ev.EntryValidation(entry) + exceptedErrorMessages := "entry_time: entry time is required." + assert.Equal(t, exceptedErrorMessages, err.Error()) + + //Test case 3 invalid EntryTime (EntryTime with just below minimum value) + entry = model.Entry{ + EntryTime: time.Unix(TimeValidationMin-1, 0), + ExitTime: nil, + StudentNumber: 20122027, + } + err = ev.EntryValidation(entry) + exceptedErrorMessages = "entry_time: must be after " + time.Unix(TimeValidationMin, 0).String() + "." + assert.Equal(t, exceptedErrorMessages, err.Error()) + + //Test case 4 invalid EntryTime (EntryTime with just above maximum value) + entry = model.Entry{ + EntryTime: time.Now().Add(time.Second), + ExitTime: nil, + StudentNumber: 20122027, + } + err = ev.EntryValidation(entry) + exceptedErrorMessages = "entry_time: must be before " + time.Now().Round(time.Second).String() + "." + assert.Equal(t, exceptedErrorMessages, err.Error()) + + //Test case 5 invalid EntryTime (Entry with minimum allowed value) + entry = model.Entry{ + EntryTime: time.Unix(TimeValidationMin, 0), + ExitTime: nil, + StudentNumber: 20122027, + } + err = ev.EntryValidation(entry) + assert.NoError(t, err) + + //Test case 6 valid EntryTime (EntryTime with maximum allowed value) + entry = model.Entry{ + EntryTime: time.Now(), + ExitTime: nil, + StudentNumber: 20122027, + } + err = ev.EntryValidation(entry) + assert.NoError(t, err) + + //Test case 7 invalid StudentNumber (StudentNumber not required) + entry = model.Entry{ + EntryTime: time.Now(), + ExitTime: nil, + } + err = ev.EntryValidation(entry) + exceptedErrorMessages = "student_number: student number is required." + assert.Equal(t, exceptedErrorMessages, err.Error()) + + //Test case 8 invalid StudentNumber (StudentNumber with just below minimum value) + entry = model.Entry{ + EntryTime: time.Now(), + ExitTime: nil, + StudentNumber: 9999999, + } + err = ev.EntryValidation(entry) + exceptedErrorMessages = "student_number: student number must be greater than 10000000." + assert.Equal(t, exceptedErrorMessages, err.Error()) + + //Test case 9 invalid StudentNumber (StudentNumber with just above maximum value) + entry = model.Entry{ + EntryTime: time.Now(), + ExitTime: nil, + StudentNumber: 40000000, + } + err = ev.EntryValidation(entry) + exceptedErrorMessages = "student_number: student number must be less than 39999999." + assert.Equal(t, exceptedErrorMessages, err.Error()) + + //Test case 10 valid StudentNumber (Student with minimum allowed value) + entry = model.Entry{ + EntryTime: time.Now(), + ExitTime: nil, + StudentNumber: 10000000, + } + err = ev.EntryValidation(entry) + assert.NoError(t, err) + + //Test case 11 valid StudentNumber (Student with maximum allowed value) + entry = model.Entry{ + EntryTime: time.Now(), + ExitTime: nil, + StudentNumber: 39999999, + } + err = ev.EntryValidation(entry) + assert.NoError(t, err) + + //Test case 12 invalid ExitTime (ExitTime with just below minimum value) + exitTime := time.Now().Add(-time.Second) + entry = model.Entry{ + EntryTime: time.Now(), + ExitTime: &exitTime, + StudentNumber: 20122027, + } + err = ev.EntryValidation(entry) + exceptedErrorMessages = "exit_time: must be after " + entry.EntryTime.String() + "." + assert.Equal(t, exceptedErrorMessages, err.Error()) + + //Test case 13 valid ExitTime (ExitTime with just above maximum value) + exitTime = time.Now().Add(time.Second) + entry = model.Entry{ + EntryTime: time.Now(), + ExitTime: &exitTime, + StudentNumber: 20122027, + } + err = ev.EntryValidation(entry) + exceptedErrorMessages = "exit_time: must be before " + time.Now().Round(time.Second).String() + "." + assert.Equal(t, exceptedErrorMessages, err.Error()) + + //Test case 14 valid ExitTime (ExitTime with minimum allowed value) + exitTime = time.Unix(TimeValidationMin, 0) + entry = model.Entry{ + EntryTime: exitTime, + ExitTime: &exitTime, + StudentNumber: 20122027, + } + err = ev.EntryValidation(entry) + assert.NoError(t, err) + + //Test case 15 valid ExitTime (ExitTime with maximum allowed value) + exitTime = time.Now().Round(time.Second) + entry = model.Entry{ + EntryTime: time.Unix(TimeValidationMin, 0), + ExitTime: &exitTime, + StudentNumber: 20122027, + } + err = ev.EntryValidation(entry) + assert.NoError(t, err) + +} diff --git a/cspell.json b/cspell.json index 05224ea..1fa7ef2 100644 --- a/cspell.json +++ b/cspell.json @@ -17,6 +17,7 @@ "usecase", "usbutils", "Unexited", + "stretchr", "rdwr", "pyproject", "pylint", From 510910f5b661ba97f5bb4a5d19e45d608ae1c42a Mon Sep 17 00:00:00 2001 From: kuraishi <20122027@kaishi-pu.ac.jp> Date: Fri, 9 Feb 2024 17:37:07 +0900 Subject: [PATCH 07/20] Add tests for user_validator.go in spi-server #9 --- api/src/validator/entry_validator_test.go | 26 ++- api/src/validator/user_validator.go | 89 +++++--- api/src/validator/user_validator_test.go | 240 ++++++++++++++++++++++ 3 files changed, 319 insertions(+), 36 deletions(-) create mode 100644 api/src/validator/user_validator_test.go diff --git a/api/src/validator/entry_validator_test.go b/api/src/validator/entry_validator_test.go index 5717b63..4ca0191 100644 --- a/api/src/validator/entry_validator_test.go +++ b/api/src/validator/entry_validator_test.go @@ -17,10 +17,12 @@ func TestEntryValidator_StudentNumberValidation(t *testing.T) { StudentNumberMax, err := strconv.ParseUint(os.Getenv("STUDENT_NUMBER_MAX"), 10, 64) assert.NoError(t, err) + sampleStudentNumber := uint(20122027) + ev := &EntryValidator{} // Test case 1 valid student number - err = ev.StudentNumberValidation(20122027) + err = ev.StudentNumberValidation(sampleStudentNumber) assert.NoError(t, err) // Test case 2 invalid student number (not required) @@ -52,13 +54,15 @@ func TestEntryValidator_EntryValidation(t *testing.T) { TimeValidationMin, err := strconv.ParseInt(os.Getenv("TIME_VALIDATION_MIN"), 10, 64) assert.NoError(t, err) + sampleStudentNumber := uint(20122027) + ev := &EntryValidator{} // Test case 1 valid entry entry := model.Entry{ EntryTime: time.Now(), ExitTime: nil, - StudentNumber: 20122027, + StudentNumber: sampleStudentNumber, } err = ev.EntryValidation(entry) assert.NoError(t, err) @@ -66,7 +70,7 @@ func TestEntryValidator_EntryValidation(t *testing.T) { //Test case 2 invalid EntryTime (not required ExitTime) entry = model.Entry{ ExitTime: nil, - StudentNumber: 20122027, + StudentNumber: sampleStudentNumber, } err = ev.EntryValidation(entry) exceptedErrorMessages := "entry_time: entry time is required." @@ -76,7 +80,7 @@ func TestEntryValidator_EntryValidation(t *testing.T) { entry = model.Entry{ EntryTime: time.Unix(TimeValidationMin-1, 0), ExitTime: nil, - StudentNumber: 20122027, + StudentNumber: sampleStudentNumber, } err = ev.EntryValidation(entry) exceptedErrorMessages = "entry_time: must be after " + time.Unix(TimeValidationMin, 0).String() + "." @@ -86,7 +90,7 @@ func TestEntryValidator_EntryValidation(t *testing.T) { entry = model.Entry{ EntryTime: time.Now().Add(time.Second), ExitTime: nil, - StudentNumber: 20122027, + StudentNumber: sampleStudentNumber, } err = ev.EntryValidation(entry) exceptedErrorMessages = "entry_time: must be before " + time.Now().Round(time.Second).String() + "." @@ -96,7 +100,7 @@ func TestEntryValidator_EntryValidation(t *testing.T) { entry = model.Entry{ EntryTime: time.Unix(TimeValidationMin, 0), ExitTime: nil, - StudentNumber: 20122027, + StudentNumber: sampleStudentNumber, } err = ev.EntryValidation(entry) assert.NoError(t, err) @@ -105,7 +109,7 @@ func TestEntryValidator_EntryValidation(t *testing.T) { entry = model.Entry{ EntryTime: time.Now(), ExitTime: nil, - StudentNumber: 20122027, + StudentNumber: sampleStudentNumber, } err = ev.EntryValidation(entry) assert.NoError(t, err) @@ -162,7 +166,7 @@ func TestEntryValidator_EntryValidation(t *testing.T) { entry = model.Entry{ EntryTime: time.Now(), ExitTime: &exitTime, - StudentNumber: 20122027, + StudentNumber: sampleStudentNumber, } err = ev.EntryValidation(entry) exceptedErrorMessages = "exit_time: must be after " + entry.EntryTime.String() + "." @@ -173,7 +177,7 @@ func TestEntryValidator_EntryValidation(t *testing.T) { entry = model.Entry{ EntryTime: time.Now(), ExitTime: &exitTime, - StudentNumber: 20122027, + StudentNumber: sampleStudentNumber, } err = ev.EntryValidation(entry) exceptedErrorMessages = "exit_time: must be before " + time.Now().Round(time.Second).String() + "." @@ -184,7 +188,7 @@ func TestEntryValidator_EntryValidation(t *testing.T) { entry = model.Entry{ EntryTime: exitTime, ExitTime: &exitTime, - StudentNumber: 20122027, + StudentNumber: sampleStudentNumber, } err = ev.EntryValidation(entry) assert.NoError(t, err) @@ -194,7 +198,7 @@ func TestEntryValidator_EntryValidation(t *testing.T) { entry = model.Entry{ EntryTime: time.Unix(TimeValidationMin, 0), ExitTime: &exitTime, - StudentNumber: 20122027, + StudentNumber: sampleStudentNumber, } err = ev.EntryValidation(entry) assert.NoError(t, err) diff --git a/api/src/validator/user_validator.go b/api/src/validator/user_validator.go index d48974e..ccb6c4a 100644 --- a/api/src/validator/user_validator.go +++ b/api/src/validator/user_validator.go @@ -2,6 +2,8 @@ package validator import ( "api/model" + "os" + "strconv" "time" validation "github.com/go-ozzo/ozzo-validation/v4" @@ -17,30 +19,67 @@ func NewUserValidator() IUserValidator { return &UserValidator{} } + func (uv *UserValidator) UserValidation(user model.User) error { - return validation.ValidateStruct(&user, - validation.Field( - &user.StudentNumber, - validation.Required.Error("student number is required"), - validation.Min(uint(10000000)).Error("student number must be greater than 20121000"), - validation.Max(uint(39999999)).Error("student number must be less than 20130000"), - ), - validation.Field( - &user.Name, - validation.Required.Error("name is required"), - validation.RuneLength(2, 32).Error("name must be between 3 and 50 characters"), - ), - validation.Field( - &user.CreatedAt, - validation.Required.Error("created at is required"), - validation.Min(time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)).Error("created at must be after 2024"), - validation.Max(time.Now()).Error("created at must be before now"), - ), - validation.Field( - &user.UpdatedAt, - validation.Required.Error("updated at is required"), - validation.Min(time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)).Error("updated at must be after 2024"), - validation.Max(time.Now()).Error("updated at must be before now"), - ), - ) + studentNumberMin, err := strconv.ParseUint(os.Getenv("STUDENT_NUMBER_MIN"), 10, 64) + if err != nil { + return err + } + + studentNumberMax, err := strconv.ParseUint(os.Getenv("STUDENT_NUMBER_MAX"), 10, 64) + if err != nil { + return err + } + + nameMinLength, err := strconv.Atoi(os.Getenv("NAME_MIN_LENGTH")) + if err != nil { + return err + } + + nameMaxLength, err := strconv.Atoi(os.Getenv("NAME_MAX_LENGTH")) + if err != nil { + return err + } + + TimeValidationMin, err := strconv.ParseInt(os.Getenv("TIME_VALIDATION_MIN"), 10, 64) + if err != nil { + return err + } + + err = validation.Validate( + &user.StudentNumber, + validation.Required.Error("student number is required"), + validation.Min(studentNumberMin).Error("student number must be greater than 10000000"), + validation.Max(studentNumberMax).Error("student number must be less than 39999999"), + ) + if err != nil { + return err + } + + err = validation.Validate( + &user.Name, + validation.Required.Error("name is required"), + validation.RuneLength(nameMinLength, nameMaxLength).Error("name must be between 2 and 32 characters"), + ) + if err != nil { + return err + } + + err = validation.Validate( + &user.CreatedAt, + validation.Required.Error("created at is required"), + validation.Min(time.Unix(TimeValidationMin, 0)).Error("created at must be after " + time.Unix(TimeValidationMin, 0).String()), + validation.Max(time.Now()).Error("created at must be before "+time.Now().Round(time.Second).String()), + ) + if err != nil { + return err + } + + err = validation.Validate( + &user.UpdatedAt, + validation.Required.Error("updated at is required"), + validation.Min(user.CreatedAt).Error("updated at must be after " + time.Unix(TimeValidationMin, 0).String()), + validation.Max(time.Now()).Error("updated at must be before "+time.Now().Round(time.Second).String()), + ) + return err } diff --git a/api/src/validator/user_validator_test.go b/api/src/validator/user_validator_test.go new file mode 100644 index 0000000..25004ec --- /dev/null +++ b/api/src/validator/user_validator_test.go @@ -0,0 +1,240 @@ +package validator + +import ( + "api/model" + "os" + "strconv" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestUserValidator(t *testing.T) { + TimeValidationMin, err := strconv.ParseInt(os.Getenv("TIME_VALIDATION_MIN"), 10, 64) + assert.NoError(t, err) + + sampleStudentNumber := uint(20122027) + sampleName := "カイシ タロウ" + sampleTime := time.Now() + + uv := &UserValidator{} + + //Test case 1 valid user + user := model.User{ + StudentNumber: 20122027, + Name: sampleName, + CreatedAt: sampleTime, + UpdatedAt: sampleTime, + } + err = uv.UserValidation(user) + assert.NoError(t, err) + + //Test case 2 invalid StudentNumber (not required) + user = model.User{ + Name: sampleName, + CreatedAt: sampleTime, + UpdatedAt: sampleTime, + } + err = uv.UserValidation(user) + exceptedErrorMessages := "student number is required" + assert.Equal(t, exceptedErrorMessages, err.Error()) + + //Test case 3 invalid StudentNumber (just below minimum value) + user = model.User{ + StudentNumber: 9999999, + Name: sampleName, + CreatedAt: sampleTime, + UpdatedAt: sampleTime, + } + err = uv.UserValidation(user) + exceptedErrorMessages = "student number must be greater than 10000000" + assert.Equal(t, exceptedErrorMessages, err.Error()) + + //Test case 4 invalid StudentNumber (just above maximum value) + user = model.User{ + StudentNumber: 40000000, + Name: sampleName, + CreatedAt: sampleTime, + UpdatedAt: sampleTime, + } + err = uv.UserValidation(user) + exceptedErrorMessages = "student number must be less than 39999999" + assert.Equal(t, exceptedErrorMessages, err.Error()) + + //Test case 5 valid StudentNumber (minimum allowed value) + user = model.User{ + StudentNumber: 10000000, + Name: sampleName, + CreatedAt: sampleTime, + UpdatedAt: sampleTime, + } + err = uv.UserValidation(user) + assert.NoError(t, err) + + //Test case 6 valid StudentNumber (maximum allowed value) + user = model.User{ + StudentNumber: 39999999, + Name: sampleName, + CreatedAt: sampleTime, + UpdatedAt: sampleTime, + } + err = uv.UserValidation(user) + assert.NoError(t, err) + + //Test case 7 invalid Name (not required) + user = model.User{ + StudentNumber: sampleStudentNumber, + CreatedAt: sampleTime, + UpdatedAt: sampleTime, + } + err = uv.UserValidation(user) + exceptedErrorMessages = "name is required" + assert.Equal(t, exceptedErrorMessages, err.Error()) + + //Test case 8 invalid Name (just below minimum length) + user = model.User{ + StudentNumber: sampleStudentNumber, + Name: "カ", + CreatedAt: sampleTime, + UpdatedAt: sampleTime, + } + err = uv.UserValidation(user) + exceptedErrorMessages = "name must be between 2 and 32 characters" + assert.Equal(t, exceptedErrorMessages, err.Error()) + + //Test case 9 invalid Name (just above maximum length) + user = model.User{ + StudentNumber: sampleStudentNumber, + Name: "あああああああああああああああああああああああああああああああああ", //33 characters + CreatedAt: sampleTime, + UpdatedAt: sampleTime, + } + err = uv.UserValidation(user) + exceptedErrorMessages = "name must be between 2 and 32 characters" + assert.Equal(t, exceptedErrorMessages, err.Error()) + + //Test case 10 valid Name (minimum allowed length) + user = model.User{ + StudentNumber: sampleStudentNumber, + Name: "カイ", + CreatedAt: sampleTime, + UpdatedAt: sampleTime, + } + err = uv.UserValidation(user) + assert.NoError(t, err) + + //Test case 11 valid Name (maximum allowed length) + user = model.User{ + StudentNumber: sampleStudentNumber, + Name: "ああああああああああああああああああああああああああああああああ", //31 characters + CreatedAt: sampleTime, + UpdatedAt: sampleTime, + } + err = uv.UserValidation(user) + assert.NoError(t, err) + + //Test case 12 invalid CreatedAt (not required) + user = model.User{ + StudentNumber: sampleStudentNumber, + Name: sampleName, + UpdatedAt: sampleTime, + } + err = uv.UserValidation(user) + exceptedErrorMessages = "created at is required" + assert.Equal(t, exceptedErrorMessages, err.Error()) + + //Test case 13 invalid CreatedAt (just below minimum value) + user = model.User{ + StudentNumber: sampleStudentNumber, + Name: sampleName, + CreatedAt: time.Unix(TimeValidationMin -1 , 0), + UpdatedAt: sampleTime, + } + err = uv.UserValidation(user) + exceptedErrorMessages = "created at must be after " + time.Unix(TimeValidationMin, 0).String() + assert.Equal(t, exceptedErrorMessages, err.Error()) + + //Test case 14 invalid CreatedAt (just above maximum value) + user = model.User{ + StudentNumber: sampleStudentNumber, + Name: sampleName, + CreatedAt: time.Now().Add(time.Second), + UpdatedAt: time.Now().Add(time.Second), + } + err = uv.UserValidation(user) + exceptedErrorMessages = "created at must be before " + time.Now().Round(time.Second).String() + assert.Equal(t, exceptedErrorMessages, err.Error()) + + //Test case 15 valid CreatedAt (minimum allowed value) + user = model.User{ + StudentNumber: sampleStudentNumber, + Name: sampleName, + CreatedAt: time.Unix(TimeValidationMin, 0), + UpdatedAt: sampleTime, + } + err = uv.UserValidation(user) + assert.NoError(t, err) + + //Test case 16 valid CreatedAt (maximum allowed value) + user = model.User{ + StudentNumber: sampleStudentNumber, + Name: sampleName, + CreatedAt: time.Now().Add(-time.Second), + UpdatedAt: sampleTime, + } + err = uv.UserValidation(user) + assert.NoError(t, err) + + //Test case 17 invalid UpdatedAt (not required) + user = model.User{ + StudentNumber: sampleStudentNumber, + Name: sampleName, + CreatedAt: sampleTime, + } + err = uv.UserValidation(user) + exceptedErrorMessages = "updated at is required" + assert.Equal(t, exceptedErrorMessages, err.Error()) + + //Test case 18 invalid UpdatedAt (just below minimum value) + user = model.User{ + StudentNumber: sampleStudentNumber, + Name: sampleName, + CreatedAt: sampleTime, + UpdatedAt: time.Unix(TimeValidationMin -1 , 0), + } + err = uv.UserValidation(user) + exceptedErrorMessages = "updated at must be after " + time.Unix(TimeValidationMin, 0).String() + assert.Equal(t, exceptedErrorMessages, err.Error()) + + //Test case 19 invalid UpdatedAt (just above maximum value) + user = model.User{ + StudentNumber: sampleStudentNumber, + Name: sampleName, + CreatedAt: sampleTime, + UpdatedAt: time.Now().Add(time.Second), + } + err = uv.UserValidation(user) + exceptedErrorMessages = "updated at must be before " + time.Now().Round(time.Second).String() + assert.Equal(t, exceptedErrorMessages, err.Error()) + + //Test case 20 valid UpdatedAt (minimum allowed value) + user = model.User{ + StudentNumber: sampleStudentNumber, + Name: sampleName, + CreatedAt: time.Unix(TimeValidationMin, 0), + UpdatedAt: time.Unix(TimeValidationMin, 0), + } + err = uv.UserValidation(user) + assert.NoError(t, err) + + //Test case 21 valid UpdatedAt (maximum allowed value) + user = model.User{ + StudentNumber: sampleStudentNumber, + Name: sampleName, + CreatedAt: time.Now().Add(-time.Second), + UpdatedAt: time.Now().Add(-time.Second), + } + err = uv.UserValidation(user) + assert.NoError(t, err) +} From 71d2f6bb0d6fff2b079a4c67cb366b9d780c8d03 Mon Sep 17 00:00:00 2001 From: kuraishi <20122027@kaishi-pu.ac.jp> Date: Sat, 10 Feb 2024 12:47:38 +0900 Subject: [PATCH 08/20] Refactor validator tests to use interface --- api/src/validator/entry_validator_test.go | 11 ++- api/src/validator/user_validator.go | 107 +++++++++++----------- api/src/validator/user_validator_test.go | 59 ++++++------ 3 files changed, 90 insertions(+), 87 deletions(-) diff --git a/api/src/validator/entry_validator_test.go b/api/src/validator/entry_validator_test.go index 4ca0191..ac0884d 100644 --- a/api/src/validator/entry_validator_test.go +++ b/api/src/validator/entry_validator_test.go @@ -1,7 +1,8 @@ -package validator +package validator_test import ( "api/model" + "api/validator" "os" "strconv" "testing" @@ -11,6 +12,8 @@ import ( ) func TestEntryValidator_StudentNumberValidation(t *testing.T) { + ev := validator.IEntryValidator(validator.NewEntryValidator()) + StudentNumberMin, err := strconv.ParseUint(os.Getenv("STUDENT_NUMBER_MIN"), 10, 64) assert.NoError(t, err) @@ -19,8 +22,6 @@ func TestEntryValidator_StudentNumberValidation(t *testing.T) { sampleStudentNumber := uint(20122027) - ev := &EntryValidator{} - // Test case 1 valid student number err = ev.StudentNumberValidation(sampleStudentNumber) assert.NoError(t, err) @@ -51,13 +52,13 @@ func TestEntryValidator_StudentNumberValidation(t *testing.T) { } func TestEntryValidator_EntryValidation(t *testing.T) { + ev := validator.IEntryValidator(validator.NewEntryValidator()) + TimeValidationMin, err := strconv.ParseInt(os.Getenv("TIME_VALIDATION_MIN"), 10, 64) assert.NoError(t, err) sampleStudentNumber := uint(20122027) - ev := &EntryValidator{} - // Test case 1 valid entry entry := model.Entry{ EntryTime: time.Now(), diff --git a/api/src/validator/user_validator.go b/api/src/validator/user_validator.go index ccb6c4a..d8a9bb2 100644 --- a/api/src/validator/user_validator.go +++ b/api/src/validator/user_validator.go @@ -19,67 +19,66 @@ func NewUserValidator() IUserValidator { return &UserValidator{} } - func (uv *UserValidator) UserValidation(user model.User) error { - studentNumberMin, err := strconv.ParseUint(os.Getenv("STUDENT_NUMBER_MIN"), 10, 64) - if err != nil { - return err - } + studentNumberMin, err := strconv.ParseUint(os.Getenv("STUDENT_NUMBER_MIN"), 10, 64) + if err != nil { + return err + } - studentNumberMax, err := strconv.ParseUint(os.Getenv("STUDENT_NUMBER_MAX"), 10, 64) - if err != nil { - return err - } + studentNumberMax, err := strconv.ParseUint(os.Getenv("STUDENT_NUMBER_MAX"), 10, 64) + if err != nil { + return err + } - nameMinLength, err := strconv.Atoi(os.Getenv("NAME_MIN_LENGTH")) - if err != nil { - return err - } + nameMinLength, err := strconv.Atoi(os.Getenv("NAME_MIN_LENGTH")) + if err != nil { + return err + } - nameMaxLength, err := strconv.Atoi(os.Getenv("NAME_MAX_LENGTH")) - if err != nil { - return err - } + nameMaxLength, err := strconv.Atoi(os.Getenv("NAME_MAX_LENGTH")) + if err != nil { + return err + } - TimeValidationMin, err := strconv.ParseInt(os.Getenv("TIME_VALIDATION_MIN"), 10, 64) - if err != nil { - return err - } + TimeValidationMin, err := strconv.ParseInt(os.Getenv("TIME_VALIDATION_MIN"), 10, 64) + if err != nil { + return err + } - err = validation.Validate( - &user.StudentNumber, - validation.Required.Error("student number is required"), - validation.Min(studentNumberMin).Error("student number must be greater than 10000000"), - validation.Max(studentNumberMax).Error("student number must be less than 39999999"), - ) - if err != nil { - return err - } + err = validation.Validate( + &user.StudentNumber, + validation.Required.Error("student number is required"), + validation.Min(studentNumberMin).Error("student number must be greater than 10000000"), + validation.Max(studentNumberMax).Error("student number must be less than 39999999"), + ) + if err != nil { + return err + } - err = validation.Validate( - &user.Name, - validation.Required.Error("name is required"), - validation.RuneLength(nameMinLength, nameMaxLength).Error("name must be between 2 and 32 characters"), - ) - if err != nil { - return err - } + err = validation.Validate( + &user.Name, + validation.Required.Error("name is required"), + validation.RuneLength(nameMinLength, nameMaxLength).Error("name must be between 2 and 32 characters"), + ) + if err != nil { + return err + } - err = validation.Validate( - &user.CreatedAt, - validation.Required.Error("created at is required"), - validation.Min(time.Unix(TimeValidationMin, 0)).Error("created at must be after " + time.Unix(TimeValidationMin, 0).String()), - validation.Max(time.Now()).Error("created at must be before "+time.Now().Round(time.Second).String()), - ) - if err != nil { - return err - } + err = validation.Validate( + &user.CreatedAt, + validation.Required.Error("created at is required"), + validation.Min(time.Unix(TimeValidationMin, 0)).Error("created at must be after "+time.Unix(TimeValidationMin, 0).String()), + validation.Max(time.Now()).Error("created at must be before "+time.Now().Round(time.Second).String()), + ) + if err != nil { + return err + } - err = validation.Validate( - &user.UpdatedAt, - validation.Required.Error("updated at is required"), - validation.Min(user.CreatedAt).Error("updated at must be after " + time.Unix(TimeValidationMin, 0).String()), - validation.Max(time.Now()).Error("updated at must be before "+time.Now().Round(time.Second).String()), - ) - return err + err = validation.Validate( + &user.UpdatedAt, + validation.Required.Error("updated at is required"), + validation.Min(user.CreatedAt).Error("updated at must be after "+time.Unix(TimeValidationMin, 0).String()), + validation.Max(time.Now()).Error("updated at must be before "+time.Now().Round(time.Second).String()), + ) + return err } diff --git a/api/src/validator/user_validator_test.go b/api/src/validator/user_validator_test.go index 25004ec..3b32400 100644 --- a/api/src/validator/user_validator_test.go +++ b/api/src/validator/user_validator_test.go @@ -1,7 +1,8 @@ -package validator +package validator_test import ( "api/model" + "api/validator" "os" "strconv" "testing" @@ -10,7 +11,11 @@ import ( "github.com/stretchr/testify/assert" ) -func TestUserValidator(t *testing.T) { +type TestUserValidator struct { + uv validator.IUserValidator +} + +func (tuv *TestUserValidator) TestUserValidation(t *testing.T) { TimeValidationMin, err := strconv.ParseInt(os.Getenv("TIME_VALIDATION_MIN"), 10, 64) assert.NoError(t, err) @@ -18,16 +23,14 @@ func TestUserValidator(t *testing.T) { sampleName := "カイシ タロウ" sampleTime := time.Now() - uv := &UserValidator{} - //Test case 1 valid user user := model.User{ - StudentNumber: 20122027, + StudentNumber: sampleStudentNumber, Name: sampleName, CreatedAt: sampleTime, UpdatedAt: sampleTime, } - err = uv.UserValidation(user) + err = tuv.uv.UserValidation(user) assert.NoError(t, err) //Test case 2 invalid StudentNumber (not required) @@ -36,7 +39,7 @@ func TestUserValidator(t *testing.T) { CreatedAt: sampleTime, UpdatedAt: sampleTime, } - err = uv.UserValidation(user) + err = tuv.uv.UserValidation(user) exceptedErrorMessages := "student number is required" assert.Equal(t, exceptedErrorMessages, err.Error()) @@ -47,7 +50,7 @@ func TestUserValidator(t *testing.T) { CreatedAt: sampleTime, UpdatedAt: sampleTime, } - err = uv.UserValidation(user) + err = tuv.uv.UserValidation(user) exceptedErrorMessages = "student number must be greater than 10000000" assert.Equal(t, exceptedErrorMessages, err.Error()) @@ -58,7 +61,7 @@ func TestUserValidator(t *testing.T) { CreatedAt: sampleTime, UpdatedAt: sampleTime, } - err = uv.UserValidation(user) + err = tuv.uv.UserValidation(user) exceptedErrorMessages = "student number must be less than 39999999" assert.Equal(t, exceptedErrorMessages, err.Error()) @@ -69,7 +72,7 @@ func TestUserValidator(t *testing.T) { CreatedAt: sampleTime, UpdatedAt: sampleTime, } - err = uv.UserValidation(user) + err = tuv.uv.UserValidation(user) assert.NoError(t, err) //Test case 6 valid StudentNumber (maximum allowed value) @@ -79,7 +82,7 @@ func TestUserValidator(t *testing.T) { CreatedAt: sampleTime, UpdatedAt: sampleTime, } - err = uv.UserValidation(user) + err = tuv.uv.UserValidation(user) assert.NoError(t, err) //Test case 7 invalid Name (not required) @@ -88,7 +91,7 @@ func TestUserValidator(t *testing.T) { CreatedAt: sampleTime, UpdatedAt: sampleTime, } - err = uv.UserValidation(user) + err = tuv.uv.UserValidation(user) exceptedErrorMessages = "name is required" assert.Equal(t, exceptedErrorMessages, err.Error()) @@ -99,7 +102,7 @@ func TestUserValidator(t *testing.T) { CreatedAt: sampleTime, UpdatedAt: sampleTime, } - err = uv.UserValidation(user) + err = tuv.uv.UserValidation(user) exceptedErrorMessages = "name must be between 2 and 32 characters" assert.Equal(t, exceptedErrorMessages, err.Error()) @@ -110,7 +113,7 @@ func TestUserValidator(t *testing.T) { CreatedAt: sampleTime, UpdatedAt: sampleTime, } - err = uv.UserValidation(user) + err = tuv.uv.UserValidation(user) exceptedErrorMessages = "name must be between 2 and 32 characters" assert.Equal(t, exceptedErrorMessages, err.Error()) @@ -121,7 +124,7 @@ func TestUserValidator(t *testing.T) { CreatedAt: sampleTime, UpdatedAt: sampleTime, } - err = uv.UserValidation(user) + err = tuv.uv.UserValidation(user) assert.NoError(t, err) //Test case 11 valid Name (maximum allowed length) @@ -131,7 +134,7 @@ func TestUserValidator(t *testing.T) { CreatedAt: sampleTime, UpdatedAt: sampleTime, } - err = uv.UserValidation(user) + err = tuv.uv.UserValidation(user) assert.NoError(t, err) //Test case 12 invalid CreatedAt (not required) @@ -140,7 +143,7 @@ func TestUserValidator(t *testing.T) { Name: sampleName, UpdatedAt: sampleTime, } - err = uv.UserValidation(user) + err = tuv.uv.UserValidation(user) exceptedErrorMessages = "created at is required" assert.Equal(t, exceptedErrorMessages, err.Error()) @@ -148,10 +151,10 @@ func TestUserValidator(t *testing.T) { user = model.User{ StudentNumber: sampleStudentNumber, Name: sampleName, - CreatedAt: time.Unix(TimeValidationMin -1 , 0), + CreatedAt: time.Unix(TimeValidationMin-1, 0), UpdatedAt: sampleTime, } - err = uv.UserValidation(user) + err = tuv.uv.UserValidation(user) exceptedErrorMessages = "created at must be after " + time.Unix(TimeValidationMin, 0).String() assert.Equal(t, exceptedErrorMessages, err.Error()) @@ -162,7 +165,7 @@ func TestUserValidator(t *testing.T) { CreatedAt: time.Now().Add(time.Second), UpdatedAt: time.Now().Add(time.Second), } - err = uv.UserValidation(user) + err = tuv.uv.UserValidation(user) exceptedErrorMessages = "created at must be before " + time.Now().Round(time.Second).String() assert.Equal(t, exceptedErrorMessages, err.Error()) @@ -173,7 +176,7 @@ func TestUserValidator(t *testing.T) { CreatedAt: time.Unix(TimeValidationMin, 0), UpdatedAt: sampleTime, } - err = uv.UserValidation(user) + err = tuv.uv.UserValidation(user) assert.NoError(t, err) //Test case 16 valid CreatedAt (maximum allowed value) @@ -183,7 +186,7 @@ func TestUserValidator(t *testing.T) { CreatedAt: time.Now().Add(-time.Second), UpdatedAt: sampleTime, } - err = uv.UserValidation(user) + err = tuv.uv.UserValidation(user) assert.NoError(t, err) //Test case 17 invalid UpdatedAt (not required) @@ -192,7 +195,7 @@ func TestUserValidator(t *testing.T) { Name: sampleName, CreatedAt: sampleTime, } - err = uv.UserValidation(user) + err = tuv.uv.UserValidation(user) exceptedErrorMessages = "updated at is required" assert.Equal(t, exceptedErrorMessages, err.Error()) @@ -201,9 +204,9 @@ func TestUserValidator(t *testing.T) { StudentNumber: sampleStudentNumber, Name: sampleName, CreatedAt: sampleTime, - UpdatedAt: time.Unix(TimeValidationMin -1 , 0), + UpdatedAt: time.Unix(TimeValidationMin-1, 0), } - err = uv.UserValidation(user) + err = tuv.uv.UserValidation(user) exceptedErrorMessages = "updated at must be after " + time.Unix(TimeValidationMin, 0).String() assert.Equal(t, exceptedErrorMessages, err.Error()) @@ -214,7 +217,7 @@ func TestUserValidator(t *testing.T) { CreatedAt: sampleTime, UpdatedAt: time.Now().Add(time.Second), } - err = uv.UserValidation(user) + err = tuv.uv.UserValidation(user) exceptedErrorMessages = "updated at must be before " + time.Now().Round(time.Second).String() assert.Equal(t, exceptedErrorMessages, err.Error()) @@ -225,7 +228,7 @@ func TestUserValidator(t *testing.T) { CreatedAt: time.Unix(TimeValidationMin, 0), UpdatedAt: time.Unix(TimeValidationMin, 0), } - err = uv.UserValidation(user) + err = tuv.uv.UserValidation(user) assert.NoError(t, err) //Test case 21 valid UpdatedAt (maximum allowed value) @@ -235,6 +238,6 @@ func TestUserValidator(t *testing.T) { CreatedAt: time.Now().Add(-time.Second), UpdatedAt: time.Now().Add(-time.Second), } - err = uv.UserValidation(user) + err = tuv.uv.UserValidation(user) assert.NoError(t, err) } From 5b7b480e5058b18b43bb68dd8f2e8e8c8494c5af Mon Sep 17 00:00:00 2001 From: kuraishi <20122027@kaishi-pu.ac.jp> Date: Sat, 10 Feb 2024 16:37:07 +0900 Subject: [PATCH 09/20] Add tests for ewpository in spi-server --- api/src/go.mod | 1 + api/src/go.sum | 3 + api/src/repository/entry_repository_test.go | 118 ++++++++++++++++++++ api/src/repository/user_repository.go | 1 - api/src/repository/user_repository_test.go | 112 +++++++++++++++++++ cspell.json | 4 +- 6 files changed, 237 insertions(+), 2 deletions(-) create mode 100644 api/src/repository/entry_repository_test.go create mode 100644 api/src/repository/user_repository_test.go diff --git a/api/src/go.mod b/api/src/go.mod index 74a0e6f..e654c6a 100644 --- a/api/src/go.mod +++ b/api/src/go.mod @@ -3,6 +3,7 @@ module api go 1.21.6 require ( + github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/go-ozzo/ozzo-validation/v4 v4.3.0 github.com/labstack/echo v3.3.10+incompatible github.com/stretchr/testify v1.8.4 diff --git a/api/src/go.sum b/api/src/go.sum index 9eb7a6e..3b9ec7d 100644 --- a/api/src/go.sum +++ b/api/src/go.sum @@ -1,3 +1,5 @@ +github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= +github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 h1:zV3ejI06GQ59hwDQAvmK1qxOQGB3WuVTRoY0okPTAv0= github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -11,6 +13,7 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg= github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= diff --git a/api/src/repository/entry_repository_test.go b/api/src/repository/entry_repository_test.go new file mode 100644 index 0000000..21641f0 --- /dev/null +++ b/api/src/repository/entry_repository_test.go @@ -0,0 +1,118 @@ +package repository_test + +import ( + "api/model" + "api/repository" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +func NewDBMock() (*gorm.DB, sqlmock.Sqlmock, error) { + db, mock, err := sqlmock.New() + if err != nil { + return nil, nil, err + } + + gormDB, err := gorm.Open(mysql.Dialector{Config: &mysql.Config{DriverName: "mysql", Conn: db, SkipInitializeWithVersion: true}}, &gorm.Config{}) + if err != nil { + return nil, nil, err + } + + return gormDB, mock, nil +} + +func TestCreateEntry(t *testing.T) { + gormDB, mock, err := NewDBMock() + if err != nil { + t.Errorf(err.Error()) + } + + ter := repository.IEntryRepository(repository.NewEntryRepository(gormDB)) + + sampleTime := time.Unix(0, 0) + entry := model.Entry{ + EntryTime: sampleTime, + ExitTime: &sampleTime, + StudentNumber: uint(20122027), + } + + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `entries` \\(`entry_time`,`exit_time`,`student_number`\\) VALUES \\(\\?,\\?,\\?\\)"). + WithArgs(entry.EntryTime, entry.ExitTime, entry.StudentNumber). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + + if err = ter.CreateEntry(&entry); err != nil { + t.Errorf(err.Error()) + } + + if err = mock.ExpectationsWereMet(); err != nil { + t.Errorf(err.Error()) + } +} + +func TestUpdateEntry(t *testing.T) { + gormDB, mock, err := NewDBMock() + if err != nil { + t.Errorf(err.Error()) + } + + ter := repository.IEntryRepository(repository.NewEntryRepository(gormDB)) + + sampleTime := time.Unix(0, 0) + entry := model.Entry{ + ID: uint(1), + EntryTime: sampleTime, + ExitTime: &sampleTime, + StudentNumber: uint(20122027), + } + + mock.ExpectBegin() + mock.ExpectExec("UPDATE `entries` SET `exit_time`=\\? WHERE id=\\?"). + WithArgs(entry.EntryTime, entry.ID). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + + if err = ter.UpdateEntry(&entry); err != nil { + t.Errorf(err.Error()) + } + + if err = mock.ExpectationsWereMet(); err != nil { + t.Errorf(err.Error()) + } +} + +func TestGetStudentNumberWithNullExitTime(t *testing.T) { + gormDB, mock, err := NewDBMock() + if err != nil { + t.Error(err) + } + + ter := repository.IEntryRepository(repository.NewEntryRepository(gormDB)) + + sampleTime := time.Unix(0, 0) + entry := model.Entry{ + EntryTime: sampleTime, + ExitTime: &sampleTime, + StudentNumber: uint(20122027), + } + + rows := sqlmock.NewRows([]string{"id", "entry_time", "exit_time", "student_number"}). + AddRow(1, sampleTime, nil, entry.StudentNumber) + + mock.ExpectQuery("SELECT \\* FROM `entries` WHERE student_number=\\? AND exit_time IS NULL ORDER BY `entries`.`id` LIMIT 1"). + WithArgs(entry.StudentNumber). + WillReturnRows(rows) + + if err = ter.GetStudentNumberWithNullExitTime(&entry, entry.StudentNumber); err != nil { + t.Error(err) + } + + if err = mock.ExpectationsWereMet(); err != nil { + t.Error(err) + } +} diff --git a/api/src/repository/user_repository.go b/api/src/repository/user_repository.go index 61f3936..56efc86 100644 --- a/api/src/repository/user_repository.go +++ b/api/src/repository/user_repository.go @@ -32,7 +32,6 @@ func (ur *UserRepository) UpdateUser(user *model.User) error { return err } return nil - } func (ur *UserRepository) GetUserByStudentNumber(user *model.User, studentNumber uint) error { diff --git a/api/src/repository/user_repository_test.go b/api/src/repository/user_repository_test.go new file mode 100644 index 0000000..801501c --- /dev/null +++ b/api/src/repository/user_repository_test.go @@ -0,0 +1,112 @@ +package repository_test + +import ( + "api/model" + "api/repository" + "database/sql/driver" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" +) + +type AnyTime struct{} + +func (a AnyTime) Match(v driver.Value) bool { + _, ok := v.(time.Time) + return ok +} + +func TestCreateUser(t *testing.T) { + sampleStudentNumber := uint(20122027) + sampleName := "カイシ タロウ" + sampleTime := time.Unix(0, 0) + + gormDB, mock, err := NewDBMock() + if err != nil { + t.Errorf(err.Error()) + } + + tr := repository.IUserRepository(repository.NewUserRepository(gormDB)) + + sampleUser := model.User{ + StudentNumber: sampleStudentNumber, + Name: sampleName, + CreatedAt: sampleTime, + UpdatedAt: sampleTime, + } + + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `users` \\(`name`,`created_at`,`updated_at`,`student_number`\\) VALUES \\(\\?,\\?,\\?,\\?\\)"). + WithArgs(sampleName, sampleTime, sampleTime, sampleStudentNumber). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + + if err = tr.CreateUser(&sampleUser); err != nil { + t.Errorf(err.Error()) + } + + if err = mock.ExpectationsWereMet(); err != nil { + t.Errorf(err.Error()) + } +} + +func TestUpdateUser(t *testing.T) { + gormDB, mock, err := NewDBMock() + if err != nil { + t.Errorf(err.Error()) + } + + tr := repository.IUserRepository(repository.NewUserRepository(gormDB)) + + sampleUser := model.User{ + StudentNumber: uint(20122027), + Name: "カイシ タロウ", + CreatedAt: time.Unix(0, 0), + UpdatedAt: time.Unix(0, 0), + } + + mock.ExpectBegin() + mock.ExpectExec("UPDATE `users` SET `name`=\\?,`created_at`=\\?,`updated_at`=\\? WHERE `student_number` = \\?"). + WithArgs(sampleUser.Name, sampleUser.CreatedAt, AnyTime{}, sampleUser.StudentNumber). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + + if err = tr.UpdateUser(&sampleUser); err != nil { + t.Errorf(err.Error()) + } + + if err = mock.ExpectationsWereMet(); err != nil { + t.Errorf(err.Error()) + } +} + +func TestGetUserByStudentNumber(t *testing.T) { + gormDB, mock, err := NewDBMock() + if err != nil { + t.Errorf(err.Error()) + } + + tr := repository.IUserRepository(repository.NewUserRepository(gormDB)) + + // テストデータの作成 + sampleUser := &model.User{ + Name: "カイシ タロウ", + CreatedAt: time.Now().Round(time.Second), + UpdatedAt: time.Now().Round(time.Second), + StudentNumber: uint(20122027), + } + + // モックの期待値の設定 + rows := sqlmock.NewRows([]string{"name", "created_at", "updated_at", "student_number"}). + AddRow(sampleUser.Name, sampleUser.CreatedAt, sampleUser.UpdatedAt, sampleUser.StudentNumber) + mock.ExpectQuery("^SELECT \\* FROM `users` WHERE student_number=\\? ORDER BY `users`.`student_number` LIMIT 1$"). + WithArgs(sampleUser.StudentNumber).WillReturnRows(rows) + + responseUser := &model.User{} + err = tr.GetUserByStudentNumber(responseUser, sampleUser.StudentNumber) + + assert.NoError(t, err) + assert.Equal(t, sampleUser, responseUser) +} diff --git a/cspell.json b/cspell.json index 1fa7ef2..f6da166 100644 --- a/cspell.json +++ b/cspell.json @@ -18,6 +18,7 @@ "usbutils", "Unexited", "stretchr", + "sqlmock", "rdwr", "pyproject", "pylint", @@ -29,6 +30,7 @@ "gopls", "golangci", "goimports", - "dotenv" + "dotenv", + "Dialector" ] } From a3095bddd653fdacd0de6bd4147d7aa4048ba53d Mon Sep 17 00:00:00 2001 From: kuraishi <20122027@kaishi-pu.ac.jp> Date: Sat, 10 Feb 2024 20:41:34 +0900 Subject: [PATCH 10/20] Refactor validator tests into subtests in api-server #9 --- api/src/validator/entry_validator_test.go | 433 +++++++++++------- api/src/validator/user_validator_test.go | 530 +++++++++++++--------- 2 files changed, 561 insertions(+), 402 deletions(-) diff --git a/api/src/validator/entry_validator_test.go b/api/src/validator/entry_validator_test.go index ac0884d..a79a219 100644 --- a/api/src/validator/entry_validator_test.go +++ b/api/src/validator/entry_validator_test.go @@ -3,7 +3,6 @@ package validator_test import ( "api/model" "api/validator" - "os" "strconv" "testing" "time" @@ -12,196 +11,278 @@ import ( ) func TestEntryValidator_StudentNumberValidation(t *testing.T) { - ev := validator.IEntryValidator(validator.NewEntryValidator()) - - StudentNumberMin, err := strconv.ParseUint(os.Getenv("STUDENT_NUMBER_MIN"), 10, 64) - assert.NoError(t, err) - - StudentNumberMax, err := strconv.ParseUint(os.Getenv("STUDENT_NUMBER_MAX"), 10, 64) - assert.NoError(t, err) - + var err error + StudentNumberMin := 10000000 + StudentNumberMax := 39999999 sampleStudentNumber := uint(20122027) - // Test case 1 valid student number - err = ev.StudentNumberValidation(sampleStudentNumber) - assert.NoError(t, err) - - // Test case 2 invalid student number (not required) - err = ev.StudentNumberValidation(0) - exceptedErrorMessages := "student number is required" - assert.Equal(t, exceptedErrorMessages, err.Error()) - - // Test case 3 invalid student number (just below minimum value) - err = ev.StudentNumberValidation(uint(StudentNumberMin - 1)) - exceptedErrorMessages = "student number must be greater than 10000000" - assert.Equal(t, exceptedErrorMessages, err.Error()) - - // Test case 4 invalid student number (just above maximum value) - err = ev.StudentNumberValidation(uint(StudentNumberMax + 1)) - exceptedErrorMessages = "student number must be less than 39999999" - assert.Equal(t, exceptedErrorMessages, err.Error()) - - // Test case 5 valid student number (minimum allowed value) - err = ev.StudentNumberValidation(uint(StudentNumberMin)) - assert.NoError(t, err) + t.Setenv("STUDENT_NUMBER_MIN", strconv.Itoa(StudentNumberMin)) + t.Setenv("STUDENT_NUMBER_MAX", strconv.Itoa(StudentNumberMax)) - // Test case 6 valid student number (maximum allowed value) - err = ev.StudentNumberValidation(uint(StudentNumberMax)) - assert.NoError(t, err) + ev := validator.IEntryValidator(validator.NewEntryValidator()) + //Test case 1 正しいケース + t.Run("valid", func(t *testing.T) { + err = ev.StudentNumberValidation(sampleStudentNumber) + assert.NoError(t, err) + }) + + //Test case 2 STUDENT_NUMBER_MINが"無効 + t.Run("invalid_STUDENT_NUMBER_MIN", func(t *testing.T) { + t.Setenv("STUDENT_NUMBER_MIN", "") + err = ev.StudentNumberValidation(sampleStudentNumber) + assert.Equal(t, "strconv.ParseUint: parsing \"\": invalid syntax", err.Error()) + }) + + //Test case 3 STUDENT_NUMBER_MAXが無効 + t.Run("invalid_STUDENT_NUMBER_MAX", func(t *testing.T) { + t.Setenv("STUDENT_NUMBER_MAX", "") + err = ev.StudentNumberValidation(sampleStudentNumber) + assert.Equal(t, "strconv.ParseUint: parsing \"\": invalid syntax", err.Error()) + }) + + //Test case 4 StudentNumberが欠損している + t.Run("not_required", func(t *testing.T) { + err = ev.StudentNumberValidation(0) + exceptedErrorMessages := "student number is required" + assert.Equal(t, exceptedErrorMessages, err.Error()) + }) + + //Test case 5 StudentNumberが最小値よりも小さい + t.Run("below_minimum", func(t *testing.T) { + err = ev.StudentNumberValidation(uint(StudentNumberMin - 1)) + exceptedErrorMessages := "student number must be greater than 10000000" + assert.Equal(t, exceptedErrorMessages, err.Error()) + }) + + //Test case 6 StudentNumberが最大値よりも大きい + t.Run("above_maximum", func(t *testing.T) { + err = ev.StudentNumberValidation(uint(StudentNumberMax + 1)) + exceptedErrorMessages := "student number must be less than 39999999" + assert.Equal(t, exceptedErrorMessages, err.Error()) + }) + + //Test case 7 StudentNumberが最小値 + t.Run("minimum", func(t *testing.T) { + err = ev.StudentNumberValidation(uint(StudentNumberMin)) + assert.NoError(t, err) + }) + + //Test case 8 最大値のStudentNumber + t.Run("maximum", func(t *testing.T) { + err = ev.StudentNumberValidation(uint(StudentNumberMax)) + assert.NoError(t, err) + }) } func TestEntryValidator_EntryValidation(t *testing.T) { - ev := validator.IEntryValidator(validator.NewEntryValidator()) - - TimeValidationMin, err := strconv.ParseInt(os.Getenv("TIME_VALIDATION_MIN"), 10, 64) - assert.NoError(t, err) - + var err error + StudentNumberMin := 10000000 + StudentNumberMax := 39999999 + TimeValidationMin := int64(1704034800) sampleStudentNumber := uint(20122027) - // Test case 1 valid entry - entry := model.Entry{ - EntryTime: time.Now(), - ExitTime: nil, - StudentNumber: sampleStudentNumber, - } - err = ev.EntryValidation(entry) - assert.NoError(t, err) + t.Setenv("STUDENT_NUMBER_MIN", strconv.Itoa(StudentNumberMin)) + t.Setenv("STUDENT_NUMBER_MAX", strconv.Itoa(StudentNumberMax)) + t.Setenv("TIME_VALIDATION_MIN", strconv.FormatInt(TimeValidationMin, 10)) - //Test case 2 invalid EntryTime (not required ExitTime) - entry = model.Entry{ - ExitTime: nil, - StudentNumber: sampleStudentNumber, - } - err = ev.EntryValidation(entry) - exceptedErrorMessages := "entry_time: entry time is required." - assert.Equal(t, exceptedErrorMessages, err.Error()) - - //Test case 3 invalid EntryTime (EntryTime with just below minimum value) - entry = model.Entry{ - EntryTime: time.Unix(TimeValidationMin-1, 0), - ExitTime: nil, - StudentNumber: sampleStudentNumber, - } - err = ev.EntryValidation(entry) - exceptedErrorMessages = "entry_time: must be after " + time.Unix(TimeValidationMin, 0).String() + "." - assert.Equal(t, exceptedErrorMessages, err.Error()) - - //Test case 4 invalid EntryTime (EntryTime with just above maximum value) - entry = model.Entry{ - EntryTime: time.Now().Add(time.Second), - ExitTime: nil, - StudentNumber: sampleStudentNumber, - } - err = ev.EntryValidation(entry) - exceptedErrorMessages = "entry_time: must be before " + time.Now().Round(time.Second).String() + "." - assert.Equal(t, exceptedErrorMessages, err.Error()) - - //Test case 5 invalid EntryTime (Entry with minimum allowed value) - entry = model.Entry{ - EntryTime: time.Unix(TimeValidationMin, 0), - ExitTime: nil, - StudentNumber: sampleStudentNumber, - } - err = ev.EntryValidation(entry) - assert.NoError(t, err) - - //Test case 6 valid EntryTime (EntryTime with maximum allowed value) - entry = model.Entry{ - EntryTime: time.Now(), - ExitTime: nil, - StudentNumber: sampleStudentNumber, - } - err = ev.EntryValidation(entry) - assert.NoError(t, err) - - //Test case 7 invalid StudentNumber (StudentNumber not required) - entry = model.Entry{ - EntryTime: time.Now(), - ExitTime: nil, - } - err = ev.EntryValidation(entry) - exceptedErrorMessages = "student_number: student number is required." - assert.Equal(t, exceptedErrorMessages, err.Error()) - - //Test case 8 invalid StudentNumber (StudentNumber with just below minimum value) - entry = model.Entry{ - EntryTime: time.Now(), - ExitTime: nil, - StudentNumber: 9999999, - } - err = ev.EntryValidation(entry) - exceptedErrorMessages = "student_number: student number must be greater than 10000000." - assert.Equal(t, exceptedErrorMessages, err.Error()) - - //Test case 9 invalid StudentNumber (StudentNumber with just above maximum value) - entry = model.Entry{ - EntryTime: time.Now(), - ExitTime: nil, - StudentNumber: 40000000, - } - err = ev.EntryValidation(entry) - exceptedErrorMessages = "student_number: student number must be less than 39999999." - assert.Equal(t, exceptedErrorMessages, err.Error()) - - //Test case 10 valid StudentNumber (Student with minimum allowed value) - entry = model.Entry{ - EntryTime: time.Now(), - ExitTime: nil, - StudentNumber: 10000000, - } - err = ev.EntryValidation(entry) - assert.NoError(t, err) + ev := validator.IEntryValidator(validator.NewEntryValidator()) - //Test case 11 valid StudentNumber (Student with maximum allowed value) - entry = model.Entry{ + validEntry := model.Entry{ EntryTime: time.Now(), ExitTime: nil, - StudentNumber: 39999999, - } - err = ev.EntryValidation(entry) - assert.NoError(t, err) - - //Test case 12 invalid ExitTime (ExitTime with just below minimum value) - exitTime := time.Now().Add(-time.Second) - entry = model.Entry{ - EntryTime: time.Now(), - ExitTime: &exitTime, StudentNumber: sampleStudentNumber, } - err = ev.EntryValidation(entry) - exceptedErrorMessages = "exit_time: must be after " + entry.EntryTime.String() + "." - assert.Equal(t, exceptedErrorMessages, err.Error()) - //Test case 13 valid ExitTime (ExitTime with just above maximum value) - exitTime = time.Now().Add(time.Second) - entry = model.Entry{ - EntryTime: time.Now(), - ExitTime: &exitTime, - StudentNumber: sampleStudentNumber, - } - err = ev.EntryValidation(entry) - exceptedErrorMessages = "exit_time: must be before " + time.Now().Round(time.Second).String() + "." - assert.Equal(t, exceptedErrorMessages, err.Error()) - - //Test case 14 valid ExitTime (ExitTime with minimum allowed value) - exitTime = time.Unix(TimeValidationMin, 0) - entry = model.Entry{ - EntryTime: exitTime, - ExitTime: &exitTime, - StudentNumber: sampleStudentNumber, - } - err = ev.EntryValidation(entry) - assert.NoError(t, err) - - //Test case 15 valid ExitTime (ExitTime with maximum allowed value) - exitTime = time.Now().Round(time.Second) - entry = model.Entry{ - EntryTime: time.Unix(TimeValidationMin, 0), - ExitTime: &exitTime, - StudentNumber: sampleStudentNumber, - } - err = ev.EntryValidation(entry) - assert.NoError(t, err) + //Test case 1 正しいケース + t.Run("valid", func(t *testing.T) { + err = ev.EntryValidation(validEntry) + assert.NoError(t, err) + }) + + //Test case 2 STUDENT_NUMBER_MINが無効 + t.Run("invalid_STUDENT_NUMBER_MIN", func(t *testing.T) { + t.Setenv("STUDENT_NUMBER_MIN", "") + err = ev.EntryValidation(validEntry) + assert.Equal(t, "strconv.ParseUint: parsing \"\": invalid syntax", err.Error()) + }) + + //Test case 3 STUDENT_NUMBER_MAXが無効 + t.Run("invalid_STUDENT_NUMBER_MAX", func(t *testing.T) { + t.Setenv("STUDENT_NUMBER_MAX", "") + err = ev.EntryValidation(validEntry) + assert.Equal(t, "strconv.ParseUint: parsing \"\": invalid syntax", err.Error()) + }) + + //Test case 4 TIME_VALIDATION_MINが無効 + t.Run("invalid_TIME_VALIDATION_MIN", func(t *testing.T) { + t.Setenv("TIME_VALIDATION_MIN", "") + err = ev.EntryValidation(validEntry) + assert.Equal(t, "strconv.ParseInt: parsing \"\": invalid syntax", err.Error()) + }) + + //Test case 5 EntryTimeが欠損している + t.Run("not_required_EntryTime", func(t *testing.T) { + entry := model.Entry{ + ExitTime: nil, + StudentNumber: sampleStudentNumber, + } + err = ev.EntryValidation(entry) + exceptedErrorMessages := "entry_time: entry time is required." + assert.Equal(t, exceptedErrorMessages, err.Error()) + }) + + //Test case 6 EntryTimeが最小値よりも小さい + t.Run("below_minimum_EntryTime", func(t *testing.T) { + entry := model.Entry{ + EntryTime: time.Unix(TimeValidationMin-1, 0), + ExitTime: nil, + StudentNumber: sampleStudentNumber, + } + err = ev.EntryValidation(entry) + exceptedErrorMessages := "entry_time: must be after " + time.Unix(TimeValidationMin, 0).String() + "." + assert.Equal(t, exceptedErrorMessages, err.Error()) + }) + + //Test case 7 EntryTimeが最大値よりも大きい + t.Run("above_maximum_EntryTime", func(t *testing.T) { + entry := model.Entry{ + EntryTime: time.Now().Add(time.Second), + ExitTime: nil, + StudentNumber: sampleStudentNumber, + } + err = ev.EntryValidation(entry) + exceptedErrorMessages := "entry_time: must be before " + time.Now().Round(time.Second).String() + "." + assert.Equal(t, exceptedErrorMessages, err.Error()) + }) + + //Test case 8 EntryTimeが最小値 + t.Run("minimum_EntryTime", func(t *testing.T) { + entry := model.Entry{ + EntryTime: time.Unix(TimeValidationMin, 0), + ExitTime: nil, + StudentNumber: sampleStudentNumber, + } + err = ev.EntryValidation(entry) + assert.NoError(t, err) + }) + + //Test case 9 EntryTimeが最大値 + t.Run("maximum_EntryTime", func(t *testing.T) { + entry := model.Entry{ + EntryTime: time.Now(), + ExitTime: nil, + StudentNumber: sampleStudentNumber, + } + err = ev.EntryValidation(entry) + assert.NoError(t, err) + }) + + //Test case 10 StudentNumberが欠損している + t.Run("not_required_StudentNumber", func(t *testing.T) { + entry := model.Entry{ + EntryTime: time.Now(), + ExitTime: nil, + } + err = ev.EntryValidation(entry) + exceptedErrorMessages := "student_number: student number is required." + assert.Equal(t, exceptedErrorMessages, err.Error()) + }) + + //Test case 11 StudentNumberが最小値よりも小さい + t.Run("below_minimum_StudentNumber", func(t *testing.T) { + entry := model.Entry{ + EntryTime: time.Now(), + ExitTime: nil, + StudentNumber: 9999999, + } + err = ev.EntryValidation(entry) + exceptedErrorMessages := "student_number: student number must be greater than 10000000." + assert.Equal(t, exceptedErrorMessages, err.Error()) + }) + + //Test case 12 StudentNumberが最大値よりも大きい + t.Run("above_maximum_StudentNumber", func(t *testing.T) { + entry := model.Entry{ + EntryTime: time.Now(), + ExitTime: nil, + StudentNumber: 40000000, + } + err = ev.EntryValidation(entry) + exceptedErrorMessages := "student_number: student number must be less than 39999999." + assert.Equal(t, exceptedErrorMessages, err.Error()) + }) + + //Test case 13 StudentNumberが最小値 + t.Run("minimum_StudentNumber", func(t *testing.T) { + entry := model.Entry{ + EntryTime: time.Now(), + ExitTime: nil, + StudentNumber: 10000000, + } + err = ev.EntryValidation(entry) + assert.NoError(t, err) + }) + + //Test case 14 StudentNumberが最大値 + t.Run("maximum_StudentNumber", func(t *testing.T) { + entry := model.Entry{ + EntryTime: time.Now(), + ExitTime: nil, + StudentNumber: 39999999, + } + err = ev.EntryValidation(entry) + assert.NoError(t, err) + }) + + //Test case 15 ExitTimeが最小値よりも小さい + t.Run("below_minimum_ExitTime", func(t *testing.T) { + exitTime := time.Now().Add(-time.Second) + entry := model.Entry{ + EntryTime: time.Now(), + ExitTime: &exitTime, + StudentNumber: sampleStudentNumber, + } + err = ev.EntryValidation(entry) + exceptedErrorMessages := "exit_time: must be after " + entry.EntryTime.String() + "." + assert.Equal(t, exceptedErrorMessages, err.Error()) + }) + + //Test case 16 ExitTimeが最大値よりも大きい + t.Run("above_maximum_ExitTime", func(t *testing.T) { + exitTime := time.Now().Add(time.Second) + entry := model.Entry{ + EntryTime: time.Now(), + ExitTime: &exitTime, + StudentNumber: sampleStudentNumber, + } + err = ev.EntryValidation(entry) + exceptedErrorMessages := "exit_time: must be before " + time.Now().Round(time.Second).String() + "." + assert.Equal(t, exceptedErrorMessages, err.Error()) + }) + + //Test case 17 ExitTimeが最小値 + t.Run("minimum_ExitTime", func(t *testing.T) { + exitTime := time.Unix(TimeValidationMin, 0) + entry := model.Entry{ + EntryTime: exitTime, + ExitTime: &exitTime, + StudentNumber: sampleStudentNumber, + } + err = ev.EntryValidation(entry) + assert.NoError(t, err) + }) + + //Test case 18 ExitTimeが最大値 + t.Run("maximum_ExitTime", func(t *testing.T) { + exitTime := time.Now().Round(time.Second) + entry := model.Entry{ + EntryTime: time.Unix(TimeValidationMin, 0), + ExitTime: &exitTime, + StudentNumber: sampleStudentNumber, + } + err = ev.EntryValidation(entry) + assert.NoError(t, err) + }) } diff --git a/api/src/validator/user_validator_test.go b/api/src/validator/user_validator_test.go index 3b32400..d06286c 100644 --- a/api/src/validator/user_validator_test.go +++ b/api/src/validator/user_validator_test.go @@ -3,7 +3,6 @@ package validator_test import ( "api/model" "api/validator" - "os" "strconv" "testing" "time" @@ -11,233 +10,312 @@ import ( "github.com/stretchr/testify/assert" ) -type TestUserValidator struct { - uv validator.IUserValidator -} +func TestUserValidator_UserValidation(t *testing.T) { -func (tuv *TestUserValidator) TestUserValidation(t *testing.T) { - TimeValidationMin, err := strconv.ParseInt(os.Getenv("TIME_VALIDATION_MIN"), 10, 64) - assert.NoError(t, err) + var err error + TimeValidationMin := int64(1704034800) - sampleStudentNumber := uint(20122027) - sampleName := "カイシ タロウ" - sampleTime := time.Now() + t.Setenv("STUDENT_NUMBER_MIN", "10000000") + t.Setenv("STUDENT_NUMBER_MAX", "39999999") + t.Setenv("NAME_MIN_LENGTH", "2") + t.Setenv("NAME_MAX_LENGTH", "32") + t.Setenv("TIME_VALIDATION_MIN", strconv.FormatInt(TimeValidationMin, 10)) - //Test case 1 valid user - user := model.User{ - StudentNumber: sampleStudentNumber, - Name: sampleName, - CreatedAt: sampleTime, - UpdatedAt: sampleTime, - } - err = tuv.uv.UserValidation(user) - assert.NoError(t, err) - - //Test case 2 invalid StudentNumber (not required) - user = model.User{ - Name: sampleName, - CreatedAt: sampleTime, - UpdatedAt: sampleTime, - } - err = tuv.uv.UserValidation(user) - exceptedErrorMessages := "student number is required" - assert.Equal(t, exceptedErrorMessages, err.Error()) - - //Test case 3 invalid StudentNumber (just below minimum value) - user = model.User{ - StudentNumber: 9999999, - Name: sampleName, - CreatedAt: sampleTime, - UpdatedAt: sampleTime, - } - err = tuv.uv.UserValidation(user) - exceptedErrorMessages = "student number must be greater than 10000000" - assert.Equal(t, exceptedErrorMessages, err.Error()) - - //Test case 4 invalid StudentNumber (just above maximum value) - user = model.User{ - StudentNumber: 40000000, - Name: sampleName, - CreatedAt: sampleTime, - UpdatedAt: sampleTime, - } - err = tuv.uv.UserValidation(user) - exceptedErrorMessages = "student number must be less than 39999999" - assert.Equal(t, exceptedErrorMessages, err.Error()) - - //Test case 5 valid StudentNumber (minimum allowed value) - user = model.User{ - StudentNumber: 10000000, - Name: sampleName, - CreatedAt: sampleTime, - UpdatedAt: sampleTime, - } - err = tuv.uv.UserValidation(user) - assert.NoError(t, err) - - //Test case 6 valid StudentNumber (maximum allowed value) - user = model.User{ - StudentNumber: 39999999, - Name: sampleName, - CreatedAt: sampleTime, - UpdatedAt: sampleTime, - } - err = tuv.uv.UserValidation(user) - assert.NoError(t, err) - - //Test case 7 invalid Name (not required) - user = model.User{ - StudentNumber: sampleStudentNumber, - CreatedAt: sampleTime, - UpdatedAt: sampleTime, - } - err = tuv.uv.UserValidation(user) - exceptedErrorMessages = "name is required" - assert.Equal(t, exceptedErrorMessages, err.Error()) - - //Test case 8 invalid Name (just below minimum length) - user = model.User{ - StudentNumber: sampleStudentNumber, - Name: "カ", - CreatedAt: sampleTime, - UpdatedAt: sampleTime, - } - err = tuv.uv.UserValidation(user) - exceptedErrorMessages = "name must be between 2 and 32 characters" - assert.Equal(t, exceptedErrorMessages, err.Error()) - - //Test case 9 invalid Name (just above maximum length) - user = model.User{ - StudentNumber: sampleStudentNumber, - Name: "あああああああああああああああああああああああああああああああああ", //33 characters - CreatedAt: sampleTime, - UpdatedAt: sampleTime, - } - err = tuv.uv.UserValidation(user) - exceptedErrorMessages = "name must be between 2 and 32 characters" - assert.Equal(t, exceptedErrorMessages, err.Error()) - - //Test case 10 valid Name (minimum allowed length) - user = model.User{ - StudentNumber: sampleStudentNumber, - Name: "カイ", - CreatedAt: sampleTime, - UpdatedAt: sampleTime, - } - err = tuv.uv.UserValidation(user) - assert.NoError(t, err) - - //Test case 11 valid Name (maximum allowed length) - user = model.User{ - StudentNumber: sampleStudentNumber, - Name: "ああああああああああああああああああああああああああああああああ", //31 characters - CreatedAt: sampleTime, - UpdatedAt: sampleTime, - } - err = tuv.uv.UserValidation(user) - assert.NoError(t, err) - - //Test case 12 invalid CreatedAt (not required) - user = model.User{ - StudentNumber: sampleStudentNumber, - Name: sampleName, - UpdatedAt: sampleTime, + validUser := model.User{ + StudentNumber: uint(20122027), + Name: "カイシ タロウ", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), } - err = tuv.uv.UserValidation(user) - exceptedErrorMessages = "created at is required" - assert.Equal(t, exceptedErrorMessages, err.Error()) - - //Test case 13 invalid CreatedAt (just below minimum value) - user = model.User{ - StudentNumber: sampleStudentNumber, - Name: sampleName, - CreatedAt: time.Unix(TimeValidationMin-1, 0), - UpdatedAt: sampleTime, - } - err = tuv.uv.UserValidation(user) - exceptedErrorMessages = "created at must be after " + time.Unix(TimeValidationMin, 0).String() - assert.Equal(t, exceptedErrorMessages, err.Error()) - - //Test case 14 invalid CreatedAt (just above maximum value) - user = model.User{ - StudentNumber: sampleStudentNumber, - Name: sampleName, - CreatedAt: time.Now().Add(time.Second), - UpdatedAt: time.Now().Add(time.Second), - } - err = tuv.uv.UserValidation(user) - exceptedErrorMessages = "created at must be before " + time.Now().Round(time.Second).String() - assert.Equal(t, exceptedErrorMessages, err.Error()) - - //Test case 15 valid CreatedAt (minimum allowed value) - user = model.User{ - StudentNumber: sampleStudentNumber, - Name: sampleName, - CreatedAt: time.Unix(TimeValidationMin, 0), - UpdatedAt: sampleTime, - } - err = tuv.uv.UserValidation(user) - assert.NoError(t, err) - - //Test case 16 valid CreatedAt (maximum allowed value) - user = model.User{ - StudentNumber: sampleStudentNumber, - Name: sampleName, - CreatedAt: time.Now().Add(-time.Second), - UpdatedAt: sampleTime, - } - err = tuv.uv.UserValidation(user) - assert.NoError(t, err) - - //Test case 17 invalid UpdatedAt (not required) - user = model.User{ - StudentNumber: sampleStudentNumber, - Name: sampleName, - CreatedAt: sampleTime, - } - err = tuv.uv.UserValidation(user) - exceptedErrorMessages = "updated at is required" - assert.Equal(t, exceptedErrorMessages, err.Error()) - - //Test case 18 invalid UpdatedAt (just below minimum value) - user = model.User{ - StudentNumber: sampleStudentNumber, - Name: sampleName, - CreatedAt: sampleTime, - UpdatedAt: time.Unix(TimeValidationMin-1, 0), - } - err = tuv.uv.UserValidation(user) - exceptedErrorMessages = "updated at must be after " + time.Unix(TimeValidationMin, 0).String() - assert.Equal(t, exceptedErrorMessages, err.Error()) - - //Test case 19 invalid UpdatedAt (just above maximum value) - user = model.User{ - StudentNumber: sampleStudentNumber, - Name: sampleName, - CreatedAt: sampleTime, - UpdatedAt: time.Now().Add(time.Second), - } - err = tuv.uv.UserValidation(user) - exceptedErrorMessages = "updated at must be before " + time.Now().Round(time.Second).String() - assert.Equal(t, exceptedErrorMessages, err.Error()) - - //Test case 20 valid UpdatedAt (minimum allowed value) - user = model.User{ - StudentNumber: sampleStudentNumber, - Name: sampleName, - CreatedAt: time.Unix(TimeValidationMin, 0), - UpdatedAt: time.Unix(TimeValidationMin, 0), - } - err = tuv.uv.UserValidation(user) - assert.NoError(t, err) - - //Test case 21 valid UpdatedAt (maximum allowed value) - user = model.User{ - StudentNumber: sampleStudentNumber, - Name: sampleName, - CreatedAt: time.Now().Add(-time.Second), - UpdatedAt: time.Now().Add(-time.Second), - } - err = tuv.uv.UserValidation(user) - assert.NoError(t, err) + + uv := validator.IUserValidator(validator.NewUserValidator()) + + //Test case 1 正しいケース + t.Run("valid", func(t *testing.T) { + err = uv.UserValidation(validUser) + assert.NoError(t, err) + }) + + //Test case 2 STUDENT_NUMBER_MINの値が無効 + t.Run("invalid_STUDENT_NUMBER_MIN", func(t *testing.T) { + t.Setenv("STUDENT_NUMBER_MIN", "") + err = uv.UserValidation(validUser) + assert.Equal(t, "strconv.ParseUint: parsing \"\": invalid syntax", err.Error()) + }) + + //Test case 3 STUDENT_NUMBER_MAXの値が無効 + t.Run("invalid_STUDENT_NUMBER_MAX", func(t *testing.T) { + t.Setenv("STUDENT_NUMBER_MAX", "") + err = uv.UserValidation(validUser) + assert.Equal(t, "strconv.ParseUint: parsing \"\": invalid syntax", err.Error()) + }) + + //Test case 4 NAME_MIN_LENGTHの値が無効 + t.Run("invalid_NAME_MIN_LENGTH", func(t *testing.T) { + t.Setenv("NAME_MIN_LENGTH", "") + err = uv.UserValidation(validUser) + assert.Equal(t, "strconv.Atoi: parsing \"\": invalid syntax", err.Error()) + }) + + //Test case 5 NAME_MAX_LENGTHの値が無効 + t.Run("invalid_NAME_MAX_LENGTH", func(t *testing.T) { + t.Setenv("NAME_MAX_LENGTH", "") + err = uv.UserValidation(validUser) + assert.Equal(t, "strconv.Atoi: parsing \"\": invalid syntax", err.Error()) + }) + + //Test case 6 TIME_VALIDATION_MINの値が無効 + t.Run("invalid_TIME_VALIDATION_MIN", func(t *testing.T) { + t.Setenv("TIME_VALIDATION_MIN", "") + err = uv.UserValidation(validUser) + assert.Equal(t, "strconv.ParseInt: parsing \"\": invalid syntax", err.Error()) + }) + + //Test case 7 StudentNumberが欠損している + t.Run("not_required_StudentNumber", func(t *testing.T) { + user := model.User{ + Name: validUser.Name, + CreatedAt: validUser.CreatedAt, + UpdatedAt: validUser.UpdatedAt, + } + err = uv.UserValidation(user) + exceptedErrorMessages := "student number is required" + assert.Equal(t, exceptedErrorMessages, err.Error()) + }) + + //Test case 8 StudentNumberが最小値よりも小さい + t.Run("below_minimum_StudentNumber", func(t *testing.T) { + user := model.User{ + StudentNumber: 9999999, + Name: validUser.Name, + CreatedAt: validUser.CreatedAt, + UpdatedAt: validUser.UpdatedAt, + } + err = uv.UserValidation(user) + exceptedErrorMessages := "student number must be greater than 10000000" + assert.Equal(t, exceptedErrorMessages, err.Error()) + }) + + //Test case 9 StudentNumberが最大値よりも大きい + t.Run("above_maximum_StudentNumber", func(t *testing.T) { + user := model.User{ + StudentNumber: 40000000, + Name: validUser.Name, + CreatedAt: validUser.CreatedAt, + UpdatedAt: validUser.UpdatedAt, + } + err = uv.UserValidation(user) + exceptedErrorMessages := "student number must be less than 39999999" + assert.Equal(t, exceptedErrorMessages, err.Error()) + }) + + // Test case 10 StudentNumberが最小値 + t.Run("minimum_StudentNumber", func(t *testing.T) { + user := model.User{ + StudentNumber: 10000000, + Name: validUser.Name, + CreatedAt: validUser.CreatedAt, + UpdatedAt: validUser.UpdatedAt, + } + err = uv.UserValidation(user) + assert.NoError(t, err) + }) + + // Test case 11 StudentNumberが最大値 + t.Run("maximum_StudentNumber", func(t *testing.T) { + user := model.User{ + StudentNumber: 39999999, + Name: validUser.Name, + CreatedAt: validUser.CreatedAt, + UpdatedAt: validUser.UpdatedAt, + } + err = uv.UserValidation(user) + assert.NoError(t, err) + }) + + //Test case 12 Nameが欠損している + t.Run("not_required_Name", func(t *testing.T) { + user := model.User{ + StudentNumber: validUser.StudentNumber, + CreatedAt: validUser.CreatedAt, + UpdatedAt: validUser.UpdatedAt, + } + err = uv.UserValidation(user) + exceptedErrorMessages := "name is required" + assert.Equal(t, exceptedErrorMessages, err.Error()) + }) + + //Test case 13 Nameが最小値よりも小さい + t.Run("below_minimum_Name", func(t *testing.T) { + user := model.User{ + StudentNumber: validUser.StudentNumber, + Name: "カ", + CreatedAt: validUser.CreatedAt, + UpdatedAt: validUser.UpdatedAt, + } + err = uv.UserValidation(user) + exceptedErrorMessages := "name must be between 2 and 32 characters" + assert.Equal(t, exceptedErrorMessages, err.Error()) + }) + + //Test case 14 Nameが最大値よりも大きい + t.Run("above_maximum_Name", func(t *testing.T) { + user := model.User{ + StudentNumber: validUser.StudentNumber, + Name: "あああああああああああああああああああああああああああああああああ", //33 characters + CreatedAt: validUser.CreatedAt, + UpdatedAt: validUser.UpdatedAt, + } + err = uv.UserValidation(user) + exceptedErrorMessages := "name must be between 2 and 32 characters" + assert.Equal(t, exceptedErrorMessages, err.Error()) + }) + + //Test case 15 Nameが最小値 + t.Run("minimum_Name", func(t *testing.T) { + user := model.User{ + StudentNumber: validUser.StudentNumber, + Name: "カイ", + CreatedAt: validUser.CreatedAt, + UpdatedAt: validUser.UpdatedAt, + } + err = uv.UserValidation(user) + assert.NoError(t, err) + }) + + //Test case 16 Nameが最大値 + t.Run("maximum_Name", func(t *testing.T) { + user := model.User{ + StudentNumber: validUser.StudentNumber, + Name: "ああああああああああああああああああああああああああああああああ", //31 characters + CreatedAt: validUser.CreatedAt, + UpdatedAt: validUser.UpdatedAt, + } + err = uv.UserValidation(user) + assert.NoError(t, err) + }) + + //Test case 17 CreatedAtが欠損している + t.Run("not_required_CreatedAt", func(t *testing.T) { + user := model.User{ + StudentNumber: validUser.StudentNumber, + Name: validUser.Name, + UpdatedAt: validUser.UpdatedAt, + } + err = uv.UserValidation(user) + exceptedErrorMessages := "created at is required" + assert.Equal(t, exceptedErrorMessages, err.Error()) + }) + + //Test case 18 CreatedAtが最小値よりも小さい + t.Run("below_minimum_CreatedAt", func(t *testing.T) { + user := model.User{ + StudentNumber: validUser.StudentNumber, + Name: validUser.Name, + CreatedAt: time.Unix(TimeValidationMin-1, 0), + UpdatedAt: validUser.UpdatedAt, + } + err = uv.UserValidation(user) + exceptedErrorMessages := "created at must be after " + time.Unix(TimeValidationMin, 0).String() + assert.Equal(t, exceptedErrorMessages, err.Error()) + }) + + //Test case 19 CreatedAtが最大値よりも大きい + t.Run("above_maximum_CreatedAt", func(t *testing.T) { + user := model.User{ + StudentNumber: validUser.StudentNumber, + Name: validUser.Name, + CreatedAt: time.Now().Add(time.Second), + UpdatedAt: time.Now().Add(time.Second), + } + err = uv.UserValidation(user) + exceptedErrorMessages := "created at must be before " + time.Now().Round(time.Second).String() + assert.Equal(t, exceptedErrorMessages, err.Error()) + }) + + //Test case 20 CreatedAtが最小値 + t.Run("minimum_CreatedAt", func(t *testing.T) { + user := model.User{ + StudentNumber: validUser.StudentNumber, + Name: validUser.Name, + CreatedAt: time.Unix(TimeValidationMin, 0), + UpdatedAt: validUser.UpdatedAt, + } + err = uv.UserValidation(user) + assert.NoError(t, err) + }) + + //Test case 21 CreatedAtが最大値 + t.Run("maximum_CreatedAt", func(t *testing.T) { + user := model.User{ + StudentNumber: validUser.StudentNumber, + Name: validUser.Name, + CreatedAt: time.Now().Add(-time.Second), + UpdatedAt: validUser.UpdatedAt, + } + err = uv.UserValidation(user) + assert.NoError(t, err) + }) + + //Test case 22 UpdatedAtが欠損している + t.Run("not_required_UpdatedAt", func(t *testing.T) { + user := model.User{ + StudentNumber: validUser.StudentNumber, + Name: validUser.Name, + CreatedAt: validUser.CreatedAt, + } + err = uv.UserValidation(user) + exceptedErrorMessages := "updated at is required" + assert.Equal(t, exceptedErrorMessages, err.Error()) + }) + + //Test case 23 UpdatedAtが最小値よりも小さい + t.Run("below_minimum_UpdatedAt", func(t *testing.T) { + user := model.User{ + StudentNumber: validUser.StudentNumber, + Name: validUser.Name, + CreatedAt: validUser.CreatedAt, + UpdatedAt: time.Unix(TimeValidationMin-1, 0), + } + err = uv.UserValidation(user) + exceptedErrorMessages := "updated at must be after " + time.Unix(TimeValidationMin, 0).String() + assert.Equal(t, exceptedErrorMessages, err.Error()) + }) + + //Test case 24 UpdatedAtが最大値よりも大きい + t.Run("above_maximum_UpdatedAt", func(t *testing.T) { + user := model.User{ + StudentNumber: validUser.StudentNumber, + Name: validUser.Name, + CreatedAt: validUser.CreatedAt, + UpdatedAt: time.Now().Add(time.Second), + } + err = uv.UserValidation(user) + exceptedErrorMessages := "updated at must be before " + time.Now().Round(time.Second).String() + assert.Equal(t, exceptedErrorMessages, err.Error()) + }) + + //Test case 25 UpdatedAtが最小値 + t.Run("minimum_UpdatedAt", func(t *testing.T) { + user := model.User{ + StudentNumber: validUser.StudentNumber, + Name: validUser.Name, + CreatedAt: time.Unix(TimeValidationMin, 0), + UpdatedAt: time.Unix(TimeValidationMin, 0), + } + err = uv.UserValidation(user) + assert.NoError(t, err) + }) + + //Test case 26 UpdatedAtが最大値 + t.Run("maximum_UpdatedAt", func(t *testing.T) { + user := model.User{ + StudentNumber: validUser.StudentNumber, + Name: validUser.Name, + CreatedAt: time.Now().Add(-time.Second), + UpdatedAt: time.Now().Add(-time.Second), + } + err = uv.UserValidation(user) + assert.NoError(t, err) + }) } From cad6c8a9d3796781b7bbf9101c80f814f56ee9d5 Mon Sep 17 00:00:00 2001 From: kuraishi <20122027@kaishi-pu.ac.jp> Date: Sat, 10 Feb 2024 21:12:45 +0900 Subject: [PATCH 11/20] Add tests for ewpository in spi-server #9 --- api/src/db/db.go | 17 ++++----- api/src/db/db_test.go | 72 ++++++++++++++++++++++++++++++++++++++ api/src/main.go | 13 ++++++- api/src/migrate/migrate.go | 10 +++++- cspell.json | 1 + 5 files changed, 101 insertions(+), 12 deletions(-) create mode 100644 api/src/db/db_test.go diff --git a/api/src/db/db.go b/api/src/db/db.go index 08d7498..78b5262 100644 --- a/api/src/db/db.go +++ b/api/src/db/db.go @@ -1,24 +1,17 @@ package db import ( - "fmt" "log" - "os" "gorm.io/driver/mysql" "gorm.io/gorm" ) -func ConnectDB() *gorm.DB { - - url := fmt.Sprintf("%s:%s@tcp(%s)/%s?parseTime=true", os.Getenv("MYSQL_USER"), os.Getenv("MYSQL_PASSWORD"), os.Getenv("MYSQL_HOST"), os.Getenv("MYSQL_DATABASE")) - - db, err := gorm.Open(mysql.Open(url), &gorm.Config{}) +func ConnectDB(config mysql.Config) *gorm.DB { + db, err := gorm.Open(mysql.New(config), &gorm.Config{}) if err != nil { log.Fatalln(err) } - fmt.Printf("Connected") - return db } @@ -27,5 +20,9 @@ func CloseDB(db *gorm.DB) { if err != nil { log.Fatalln(err) } - dbSQL.Close() + + err = dbSQL.Close() + if err != nil { + log.Println(err) + } } diff --git a/api/src/db/db_test.go b/api/src/db/db_test.go new file mode 100644 index 0000000..1c65f06 --- /dev/null +++ b/api/src/db/db_test.go @@ -0,0 +1,72 @@ +package db_test + +import ( + "api/db" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +func NewDBMock() (*gorm.DB, sqlmock.Sqlmock, error) { + db, mock, err := sqlmock.New() + if err != nil { + return nil, nil, err + } + + gormDB, err := gorm.Open( + mysql.Dialector{ + Config: &mysql.Config{ + Conn: db, + SkipInitializeWithVersion: true, + }, + }, + &gorm.Config{}, + ) + if err != nil { + return nil, nil, err + } + + return gormDB, mock, nil +} + +func TestConnectDB(t *testing.T) { + mockdb, mock, err := sqlmock.New() + if err != nil { + t.Errorf(err.Error()) + } + + mock.ExpectQuery("SELECT VERSION()"). + WillReturnRows(sqlmock.NewRows([]string{"VERSION()"}). + AddRow("8.0.36")) + + mysqlConfig := mysql.Config{ + DriverName: "mysql", + Conn: mockdb, + } + + gormDB := db.ConnectDB(mysqlConfig) + + if gormDB == nil { + t.Errorf("failed to connect to database") + } + + if _, err := gormDB.DB(); err != nil { + t.Errorf(err.Error()) + } +} + +func TestCloseDB(t *testing.T) { + gormDB, mock, err := NewDBMock() + if err != nil { + t.Errorf(err.Error()) + } + + mock.ExpectClose() + db.CloseDB(gormDB) + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf(err.Error()) + } +} diff --git a/api/src/main.go b/api/src/main.go index 92ec24b..a33d39e 100644 --- a/api/src/main.go +++ b/api/src/main.go @@ -7,10 +7,21 @@ import ( "api/router" "api/usecase" "api/validator" + "fmt" + "os" + + "gorm.io/driver/mysql" ) func main() { - db := db.ConnectDB() + url := fmt.Sprintf("%s:%s@tcp(%s)/%s?parseTime=true", os.Getenv("MYSQL_USER"), os.Getenv("MYSQL_PASSWORD"), os.Getenv("MYSQL_HOST"), os.Getenv("MYSQL_DATABASE")) + mysqlConfig := mysql.Config{ + DriverName: "mysql", + DSN: url, + SkipInitializeWithVersion: true, + } + + db := db.ConnectDB(mysqlConfig) entryValidator := validator.NewEntryValidator() userValidator := validator.NewUserValidator() userRepository := repository.NewUserRepository(db) diff --git a/api/src/migrate/migrate.go b/api/src/migrate/migrate.go index 2da39ac..e368a82 100644 --- a/api/src/migrate/migrate.go +++ b/api/src/migrate/migrate.go @@ -4,10 +4,18 @@ import ( "api/db" "api/model" "fmt" + "os" + + "gorm.io/driver/mysql" ) func main() { - dbConn := db.ConnectDB() + mysqlConfig := mysql.Config{ + DriverName: "mysql", + DSN: fmt.Sprintf("%s:%s@tcp(%s)/%s?parseTime=true", os.Getenv("MYSQL_USER"), os.Getenv("MYSQL_PASSWORD"), os.Getenv("MYSQL_HOST"), os.Getenv("MYSQL_DATABASE")), + } + + dbConn := db.ConnectDB(mysqlConfig) defer fmt.Println("Successfully Migrated") defer db.CloseDB(dbConn) err := dbConn.AutoMigrate(&model.User{}, &model.Entry{}) diff --git a/cspell.json b/cspell.json index f6da166..1bcc3b9 100644 --- a/cspell.json +++ b/cspell.json @@ -24,6 +24,7 @@ "pylint", "ozzo", "mypy", + "mockdb", "labstack", "isort", "gorm", From 1d7fc052bac64eeba5b426d58fffb656c91a2248 Mon Sep 17 00:00:00 2001 From: kuraishi <20122027@kaishi-pu.ac.jp> Date: Sat, 10 Feb 2024 21:23:21 +0900 Subject: [PATCH 12/20] Refactor names of test functions in api-server repository #9 --- api/src/repository/entry_repository_test.go | 6 +++--- api/src/repository/user_repository_test.go | 8 +++----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/api/src/repository/entry_repository_test.go b/api/src/repository/entry_repository_test.go index 21641f0..446e935 100644 --- a/api/src/repository/entry_repository_test.go +++ b/api/src/repository/entry_repository_test.go @@ -25,7 +25,7 @@ func NewDBMock() (*gorm.DB, sqlmock.Sqlmock, error) { return gormDB, mock, nil } -func TestCreateEntry(t *testing.T) { +func TestEntryRepository_CreateEntry(t *testing.T) { gormDB, mock, err := NewDBMock() if err != nil { t.Errorf(err.Error()) @@ -55,7 +55,7 @@ func TestCreateEntry(t *testing.T) { } } -func TestUpdateEntry(t *testing.T) { +func TestEntryRepository_UpdateEntry(t *testing.T) { gormDB, mock, err := NewDBMock() if err != nil { t.Errorf(err.Error()) @@ -86,7 +86,7 @@ func TestUpdateEntry(t *testing.T) { } } -func TestGetStudentNumberWithNullExitTime(t *testing.T) { +func TestEntryRepository_GetStudentNumberWithNullExitTime(t *testing.T) { gormDB, mock, err := NewDBMock() if err != nil { t.Error(err) diff --git a/api/src/repository/user_repository_test.go b/api/src/repository/user_repository_test.go index 801501c..4f1090d 100644 --- a/api/src/repository/user_repository_test.go +++ b/api/src/repository/user_repository_test.go @@ -18,7 +18,7 @@ func (a AnyTime) Match(v driver.Value) bool { return ok } -func TestCreateUser(t *testing.T) { +func TestUserRepository_CreateUser(t *testing.T) { sampleStudentNumber := uint(20122027) sampleName := "カイシ タロウ" sampleTime := time.Unix(0, 0) @@ -52,7 +52,7 @@ func TestCreateUser(t *testing.T) { } } -func TestUpdateUser(t *testing.T) { +func TestUserRepository_UpdateUser(t *testing.T) { gormDB, mock, err := NewDBMock() if err != nil { t.Errorf(err.Error()) @@ -82,7 +82,7 @@ func TestUpdateUser(t *testing.T) { } } -func TestGetUserByStudentNumber(t *testing.T) { +func TestUserRepository_GetUserByStudentNumber(t *testing.T) { gormDB, mock, err := NewDBMock() if err != nil { t.Errorf(err.Error()) @@ -90,7 +90,6 @@ func TestGetUserByStudentNumber(t *testing.T) { tr := repository.IUserRepository(repository.NewUserRepository(gormDB)) - // テストデータの作成 sampleUser := &model.User{ Name: "カイシ タロウ", CreatedAt: time.Now().Round(time.Second), @@ -98,7 +97,6 @@ func TestGetUserByStudentNumber(t *testing.T) { StudentNumber: uint(20122027), } - // モックの期待値の設定 rows := sqlmock.NewRows([]string{"name", "created_at", "updated_at", "student_number"}). AddRow(sampleUser.Name, sampleUser.CreatedAt, sampleUser.UpdatedAt, sampleUser.StudentNumber) mock.ExpectQuery("^SELECT \\* FROM `users` WHERE student_number=\\? ORDER BY `users`.`student_number` LIMIT 1$"). From 07524c61168be375383beb2742c709aa44628ce8 Mon Sep 17 00:00:00 2001 From: kuraishi <20122027@kaishi-pu.ac.jp> Date: Sat, 10 Feb 2024 21:52:49 +0900 Subject: [PATCH 13/20] Add tests for usecase in spi-server #9 --- api/src/usecase/entry_usecase_test.go | 93 +++++++++++++++++++++ api/src/usecase/user_usecase_test.go | 112 ++++++++++++++++++++++++++ 2 files changed, 205 insertions(+) create mode 100644 api/src/usecase/entry_usecase_test.go create mode 100644 api/src/usecase/user_usecase_test.go diff --git a/api/src/usecase/entry_usecase_test.go b/api/src/usecase/entry_usecase_test.go new file mode 100644 index 0000000..f349ab9 --- /dev/null +++ b/api/src/usecase/entry_usecase_test.go @@ -0,0 +1,93 @@ +package usecase_test + +import ( + "api/model" + "api/repository" + "api/usecase" + "api/validator" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +func NewDBMock() (*gorm.DB, sqlmock.Sqlmock, error) { + db, mock, err := sqlmock.New() + if err != nil { + return nil, nil, err + } + + gormDB, err := gorm.Open(mysql.Dialector{Config: &mysql.Config{DriverName: "mysql", Conn: db, SkipInitializeWithVersion: true}}, &gorm.Config{}) + if err != nil { + return nil, nil, err + } + + return gormDB, mock, nil +} + +func TestEntryUsecase_EntryOrExit(t *testing.T) { + gormDB, mock, err := NewDBMock() + if err != nil { + t.Errorf(err.Error()) + } + + eu := usecase.IEntryUsecase( + usecase.NewEntryUsecase( + repository.IEntryRepository(repository.NewEntryRepository(gormDB)), + validator.IEntryValidator(validator.NewEntryValidator()), + ), + ) + + sampleStudentNumber := uint(20122027) + sampleTimestamp := time.Now().Add(-time.Second) + TimeValidationMin := int64(1704034800) + + t.Run("Entry", func(t *testing.T) { + //GetStudentNumberWithNullExitTime + initEntry := model.Entry{} + rows := sqlmock.NewRows([]string{"id", "entry_time", "exit_time", "student_number"}). + AddRow(initEntry.ID, initEntry.EntryTime, initEntry.ExitTime, initEntry.StudentNumber) + mock.ExpectQuery("SELECT \\* FROM `entries` WHERE student_number=\\? AND exit_time IS NULL ORDER BY `entries`.`id` LIMIT 1"). + WithArgs(sampleStudentNumber). + WillReturnRows(rows) + + //CreateEntry + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `entries` \\(`entry_time`,`exit_time`,`student_number`\\) VALUES \\(\\?,\\?,\\?\\)"). + WithArgs(sampleTimestamp, nil, sampleStudentNumber). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + + result, err := eu.EntryOrExit(sampleStudentNumber, sampleTimestamp) + assert.NoError(t, err) + assert.Equal(t, "entry success", result) + }) + + t.Run("Exit", func(t *testing.T) { + storedEntry := model.Entry{ + ID: 1, + EntryTime: time.Unix(TimeValidationMin, 0), + ExitTime: nil, + StudentNumber: sampleStudentNumber, + } + + rows := sqlmock.NewRows([]string{"id", "entry_time", "exit_time", "student_number"}). + AddRow(storedEntry.ID, storedEntry.EntryTime, storedEntry.ExitTime, storedEntry.StudentNumber) + mock.ExpectQuery("SELECT \\* FROM `entries` WHERE student_number=\\? AND exit_time IS NULL ORDER BY `entries`.`id` LIMIT 1"). + WithArgs(sampleStudentNumber). + WillReturnRows(rows) + + mock.ExpectBegin() + mock.ExpectExec("UPDATE `entries` SET `exit_time`=\\? WHERE id=\\?"). + WithArgs(sampleTimestamp, storedEntry.ID). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + + result, err := eu.EntryOrExit(sampleStudentNumber, sampleTimestamp) + assert.NoError(t, err) + assert.Equal(t, "exit success", result) + }) +} diff --git a/api/src/usecase/user_usecase_test.go b/api/src/usecase/user_usecase_test.go new file mode 100644 index 0000000..a25b2ef --- /dev/null +++ b/api/src/usecase/user_usecase_test.go @@ -0,0 +1,112 @@ +package usecase_test + +import ( + "api/model" + "api/repository" + "api/usecase" + "api/validator" + "database/sql/driver" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" +) + +type AnyTime struct{} + +func (a AnyTime) Match(v driver.Value) bool { + _, ok := v.(time.Time) + return ok +} + +func TestUserUsecase_CreateOrUpdateUser(t *testing.T) { + gormDB, mock, err := NewDBMock() + if err != nil { + t.Errorf(err.Error()) + } + + uu := usecase.IUserUsecase( + usecase.NewUserUsecase( + repository.IUserRepository(repository.NewUserRepository(gormDB)), + validator.IUserValidator(validator.NewUserValidator()), + ), + ) + + t.Run("CreateUser", func(t *testing.T) { + user := model.User{ + StudentNumber: 20122027, + Name: "カイシ タロウ", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + //GetUserByStudentNumber + storedUser := model.User{} + rows := sqlmock.NewRows([]string{"name", "created_at", "updated_at", "student_number"}). + AddRow(storedUser.Name, storedUser.CreatedAt, storedUser.UpdatedAt, storedUser.StudentNumber) + mock.ExpectQuery("^SELECT \\* FROM `users` WHERE student_number=\\? ORDER BY `users`.`student_number` LIMIT 1$"). + WithArgs(user.StudentNumber).WillReturnRows(rows) + + //CreateUser + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `users` \\(`name`,`created_at`,`updated_at`,`student_number`\\) VALUES \\(\\?,\\?,\\?,\\?\\)"). + WithArgs(user.Name, user.CreatedAt, user.UpdatedAt, user.StudentNumber). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + + result, err := uu.CreateOrUpdateUser(user) + assert.NoError(t, err) + assert.Equal(t, "User created", result) + }) + + t.Run("UpdateUser", func(t *testing.T) { + user := model.User{ + StudentNumber: 20122027, + Name: "カイシ タロウ", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + //GetUserByStudentNumber + storedUser := model.User{ + StudentNumber: 20122027, + Name: "ヨネヤマ タロウ", + CreatedAt: time.Now().Add(-time.Second), + UpdatedAt: time.Now().Add(-time.Second), + } + rows := sqlmock.NewRows([]string{"name", "created_at", "updated_at", "student_number"}). + AddRow(storedUser.Name, storedUser.CreatedAt, storedUser.UpdatedAt, storedUser.StudentNumber) + mock.ExpectQuery("^SELECT \\* FROM `users` WHERE student_number=\\? ORDER BY `users`.`student_number` LIMIT 1$"). + WithArgs(user.StudentNumber).WillReturnRows(rows) + + mock.ExpectBegin() + mock.ExpectExec("UPDATE `users` SET `name`=\\?,`created_at`=\\?,`updated_at`=\\? WHERE `student_number` = \\?"). + WithArgs(user.Name, user.CreatedAt, AnyTime{}, user.StudentNumber). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + + result, err := uu.CreateOrUpdateUser(user) + assert.NoError(t, err) + assert.Equal(t, "User updated", result) + }) + + t.Run("AlreadyExistsUser", func(t *testing.T) { + user := model.User{ + StudentNumber: 20122027, + Name: "カイシ タロウ", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + //GetUserByStudentNumber + rows := sqlmock.NewRows([]string{"name", "created_at", "updated_at", "student_number"}). + AddRow(user.Name, user.CreatedAt, user.UpdatedAt, user.StudentNumber) + mock.ExpectQuery("^SELECT \\* FROM `users` WHERE student_number=\\? ORDER BY `users`.`student_number` LIMIT 1$"). + WithArgs(user.StudentNumber).WillReturnRows(rows) + + result, err := uu.CreateOrUpdateUser(user) + assert.NoError(t, err) + assert.Equal(t, "User already exists", result) + }) +} From 2220d99be5eb0e57b381256c6d3e74682f86c0b3 Mon Sep 17 00:00:00 2001 From: kuraishi <20122027@kaishi-pu.ac.jp> Date: Sun, 11 Feb 2024 12:52:57 +0900 Subject: [PATCH 14/20] Add tests for controller in spi-server #9 --- api/src/controller/api_controller.go | 27 +- api/src/controller/api_controller_test.go | 299 ++++++++++++++++++++++ api/src/main.go | 4 +- api/src/router/router.go | 4 +- 4 files changed, 322 insertions(+), 12 deletions(-) create mode 100644 api/src/controller/api_controller_test.go diff --git a/api/src/controller/api_controller.go b/api/src/controller/api_controller.go index a28c4b6..48d6c2e 100644 --- a/api/src/controller/api_controller.go +++ b/api/src/controller/api_controller.go @@ -10,11 +10,11 @@ import ( "github.com/labstack/echo" ) -type IApiController interface { - RootController(c echo.Context) error +type IUserAndEntryController interface { + HandleUserAndEntry(c echo.Context) error } -type ApiController struct { +type UserAndEntryController struct { uu usecase.IUserUsecase eu usecase.IEntryUsecase } @@ -24,23 +24,34 @@ type Response struct { EntryMessage string `json:"entry_message"` } -type EntryRequest struct { +type Request struct { StudentNumber uint `json:"student_number"` Name string `json:"name"` Timestamp float64 `json:"timestamp"` } -func NewApiController(uu usecase.IUserUsecase, eu usecase.IEntryUsecase) IApiController { - return &ApiController{uu, eu} +func NewUserAndEntryController(uu usecase.IUserUsecase, eu usecase.IEntryUsecase) IUserAndEntryController { + return &UserAndEntryController{uu, eu} } -func (ac *ApiController) RootController(c echo.Context) error { - request := EntryRequest{} +func (ac *UserAndEntryController) HandleUserAndEntry(c echo.Context) error { + request := Request{} if err := c.Bind(&request); err != nil { fmt.Println(err.Error()) return c.JSON(http.StatusBadRequest, err.Error()) } + if request.StudentNumber == 0 { + fmt.Println("student number is required") + return c.JSON(http.StatusBadRequest, "student number is required") + } + if request.Name == "" { + return c.JSON(http.StatusBadRequest, "name is required") + } + if request.Timestamp == 0 { + return c.JSON(http.StatusBadRequest, "timestamp is required") + } + // convert float64 to time.Time seconds := int64(request.Timestamp) nanoseconds := int64((request.Timestamp - float64(seconds)) * 1e9) diff --git a/api/src/controller/api_controller_test.go b/api/src/controller/api_controller_test.go new file mode 100644 index 0000000..1846c2d --- /dev/null +++ b/api/src/controller/api_controller_test.go @@ -0,0 +1,299 @@ +package controller_test + +import ( + "api/controller" + "api/model" + "api/repository" + "api/usecase" + "api/validator" + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/labstack/echo" + "github.com/stretchr/testify/assert" + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +func NewDBMock() (*gorm.DB, sqlmock.Sqlmock, error) { + db, mock, err := sqlmock.New() + if err != nil { + return nil, nil, err + } + + gormDB, err := gorm.Open(mysql.Dialector{Config: &mysql.Config{DriverName: "mysql", Conn: db, SkipInitializeWithVersion: true}}, &gorm.Config{}) + if err != nil { + return nil, nil, err + } + + return gormDB, mock, nil +} + +func TestApiController_RootController(t *testing.T) { + + gormDB, mock, err := NewDBMock() + if err != nil { + t.Errorf(err.Error()) + } + + c := controller.IUserAndEntryController( + controller.NewUserAndEntryController( + usecase.IUserUsecase( + usecase.NewUserUsecase( + repository.IUserRepository( + repository.NewUserRepository(gormDB), + ), + validator.IUserValidator( + validator.NewUserValidator(), + ), + ), + ), + usecase.IEntryUsecase( + usecase.NewEntryUsecase( + repository.IEntryRepository( + repository.NewEntryRepository(gormDB), + ), + validator.IEntryValidator( + validator.NewEntryValidator(), + ), + ), + ), + ), + ) + + t.Run("valid", func(t *testing.T) { + + request := controller.Request{ + StudentNumber: 20122027, + Name: "カイシ タロウ", + Timestamp: float64(time.Now().UnixNano()) / 1e9, + } + + // convert float64 to time.Time + seconds := int64(request.Timestamp) + nanoseconds := int64((request.Timestamp - float64(seconds)) * 1e9) + requestTimestamp := time.Unix(seconds, nanoseconds) + + //GetUserByStudentNumber + storedUser := model.User{} + userRows := sqlmock.NewRows([]string{"name", "created_at", "updated_at", "student_number"}). + AddRow(storedUser.Name, storedUser.CreatedAt, storedUser.UpdatedAt, storedUser.StudentNumber) + mock.ExpectQuery("^SELECT \\* FROM `users` WHERE student_number=\\? ORDER BY `users`.`student_number` LIMIT 1$"). + WithArgs(request.StudentNumber).WillReturnRows(userRows) + + //CreateUser + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `users` \\(`name`,`created_at`,`updated_at`,`student_number`\\) VALUES \\(\\?,\\?,\\?,\\?\\)"). + WithArgs(request.Name, requestTimestamp, requestTimestamp, request.StudentNumber). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + + //GetStudentNumberWithNullExitTime + initEntry := model.Entry{} + entryRows := sqlmock.NewRows([]string{"id", "entry_time", "exit_time", "student_number"}). + AddRow(initEntry.ID, initEntry.EntryTime, initEntry.ExitTime, initEntry.StudentNumber) + mock.ExpectQuery("SELECT \\* FROM `entries` WHERE student_number=\\? AND exit_time IS NULL ORDER BY `entries`.`id` LIMIT 1"). + WithArgs(request.StudentNumber). + WillReturnRows(entryRows) + + //CreateEntry + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `entries` \\(`entry_time`,`exit_time`,`student_number`\\) VALUES \\(\\?,\\?,\\?\\)"). + WithArgs(requestTimestamp, nil, request.StudentNumber). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + + //request to json + requestJson, err := json.Marshal(request) + if err != nil { + t.Errorf(err.Error()) + } + + //create echo context + echoServer := echo.New() + req := httptest.NewRequest( + http.MethodPost, + "/", + strings.NewReader(string(requestJson)), + ) + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + echoContext := echoServer.NewContext(req, rec) + echoContext.SetPath("/") + + if err := c.HandleUserAndEntry(echoContext); err != nil { + t.Errorf(err.Error()) + } + + if rec.Code != http.StatusOK { + t.Errorf(err.Error()) + } + }) + + t.Run("empty_body", func(t *testing.T) { + //create echo context + echoServer := echo.New() + req := httptest.NewRequest( + http.MethodPost, + "/", + strings.NewReader(""), + ) + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + echoContext := echoServer.NewContext(req, rec) + echoContext.SetPath("/") + + err = c.HandleUserAndEntry(echoContext) + if err != nil { + t.Errorf(err.Error()) + } + + expectedStatus := http.StatusBadRequest + assert.Equal(t, expectedStatus, rec.Code) + + expectedBody := "\"code=400, message=Request body can't be empty\"\n" + assert.Equal(t, expectedBody, rec.Body.String()) + }) + + t.Run("invalid_json", func(t *testing.T) { + //create echo context + echoServer := echo.New() + req := httptest.NewRequest( + http.MethodPost, + "/", + strings.NewReader("{invalid json}"), + ) + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + echoContext := echoServer.NewContext(req, rec) + echoContext.SetPath("/") + + err = c.HandleUserAndEntry(echoContext) + if err != nil { + t.Errorf(err.Error()) + } + + expectedStatus := http.StatusBadRequest + assert.Equal(t, expectedStatus, rec.Code) + + expectedBody := "\"code=400, message=Syntax error: offset=2, error=invalid character 'i' looking for beginning of object key string\"\n" + assert.Equal(t, expectedBody, rec.Body.String()) + }) + + t.Run("not_required_studentNumber", func(t *testing.T) { + + request := controller.Request{ + Name: "カイシ タロウ", + Timestamp: float64(time.Now().UnixNano()) / 1e9, + } + + //request to json + requestJson, err := json.Marshal(request) + if err != nil { + t.Errorf(err.Error()) + } + + //create echo context + echoServer := echo.New() + req := httptest.NewRequest( + http.MethodPost, + "/", + strings.NewReader(string(requestJson)), + ) + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + echoContext := echoServer.NewContext(req, rec) + echoContext.SetPath("/") + + err = c.HandleUserAndEntry(echoContext) + if err != nil { + t.Errorf(err.Error()) + } + + expectedStatus := http.StatusBadRequest + assert.Equal(t, expectedStatus, rec.Code) + + expectedBody := "\"student number is required\"\n" + assert.Equal(t, expectedBody, rec.Body.String()) + + }) + + t.Run("not_required_name", func(t *testing.T) { + + request := controller.Request{ + StudentNumber: 20122027, + Timestamp: float64(time.Now().UnixNano()) / 1e9, + } + + //request to json + requestJson, err := json.Marshal(request) + if err != nil { + t.Errorf(err.Error()) + } + + //create echo context + echoServer := echo.New() + req := httptest.NewRequest( + http.MethodPost, + "/", + strings.NewReader(string(requestJson)), + ) + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + echoContext := echoServer.NewContext(req, rec) + echoContext.SetPath("/") + + err = c.HandleUserAndEntry(echoContext) + if err != nil { + t.Errorf(err.Error()) + } + + expectedStatus := http.StatusBadRequest + assert.Equal(t, expectedStatus, rec.Code) + + expectedBody := "\"name is required\"\n" + assert.Equal(t, expectedBody, rec.Body.String()) + }) + + t.Run("not_required_timestamp", func(t *testing.T) { + + request := controller.Request{ + StudentNumber: 20122027, + Name: "カイシ タロウ", + } + + //request to json + requestJson, err := json.Marshal(request) + if err != nil { + t.Errorf(err.Error()) + } + + //create echo context + echoServer := echo.New() + req := httptest.NewRequest( + http.MethodPost, + "/", + strings.NewReader(string(requestJson)), + ) + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + echoContext := echoServer.NewContext(req, rec) + echoContext.SetPath("/") + + err = c.HandleUserAndEntry(echoContext) + if err != nil { + t.Errorf(err.Error()) + } + + expectedStatus := http.StatusBadRequest + assert.Equal(t, expectedStatus, rec.Code) + + expectedBody := "\"timestamp is required\"\n" + assert.Equal(t, expectedBody, rec.Body.String()) + }) +} diff --git a/api/src/main.go b/api/src/main.go index a33d39e..d73c75b 100644 --- a/api/src/main.go +++ b/api/src/main.go @@ -28,7 +28,7 @@ func main() { entryRepository := repository.NewEntryRepository(db) entryUsecase := usecase.NewEntryUsecase(entryRepository, entryValidator) userUsecase := usecase.NewUserUsecase(userRepository, userValidator) - apiController := controller.NewApiController(userUsecase, entryUsecase) - e := router.NewRouter(apiController) + UserAndEntryController := controller.NewUserAndEntryController(userUsecase, entryUsecase) + e := router.NewRouter(UserAndEntryController) e.Logger.Fatal(e.Start(":8080")) } diff --git a/api/src/router/router.go b/api/src/router/router.go index d28f8c1..5dac12f 100644 --- a/api/src/router/router.go +++ b/api/src/router/router.go @@ -6,8 +6,8 @@ import ( "github.com/labstack/echo" ) -func NewRouter(uc controller.IApiController) *echo.Echo { +func NewRouter(c controller.IUserAndEntryController) *echo.Echo { e := echo.New() - e.POST("/", uc.RootController) + e.POST("/", c.HandleUserAndEntry) return e } From 1ed854f5bd9cdb52730c2f6f08eef5e898e461b4 Mon Sep 17 00:00:00 2001 From: kuraishi <20122027@kaishi-pu.ac.jp> Date: Sun, 11 Feb 2024 14:05:55 +0900 Subject: [PATCH 15/20] Add tests for model in spi-server #9 --- api/src/model/entry_test.go | 7 +++++++ api/src/model/user_test.go | 7 +++++++ cspell.json | 1 + 3 files changed, 15 insertions(+) create mode 100644 api/src/model/entry_test.go create mode 100644 api/src/model/user_test.go diff --git a/api/src/model/entry_test.go b/api/src/model/entry_test.go new file mode 100644 index 0000000..e24c534 --- /dev/null +++ b/api/src/model/entry_test.go @@ -0,0 +1,7 @@ +package model_test + +import "testing" + +func TestEntry_Nooop(t *testing.T) { + t.Log("Noop") +} diff --git a/api/src/model/user_test.go b/api/src/model/user_test.go new file mode 100644 index 0000000..41c499f --- /dev/null +++ b/api/src/model/user_test.go @@ -0,0 +1,7 @@ +package model_test + +import "testing" + +func TestUser_Nooop(t *testing.T) { + t.Log("Noop") +} diff --git a/cspell.json b/cspell.json index 1bcc3b9..dcff822 100644 --- a/cspell.json +++ b/cspell.json @@ -23,6 +23,7 @@ "pyproject", "pylint", "ozzo", + "Nooop", "mypy", "mockdb", "labstack", From 82ff54dca9c71b945c81c815e1b13bf6ad3030c7 Mon Sep 17 00:00:00 2001 From: kuraishi <20122027@kaishi-pu.ac.jp> Date: Sun, 11 Feb 2024 14:07:14 +0900 Subject: [PATCH 16/20] Add tests for migrate in spi-server #9 --- api/src/migrate/migrate_test.go | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 api/src/migrate/migrate_test.go diff --git a/api/src/migrate/migrate_test.go b/api/src/migrate/migrate_test.go new file mode 100644 index 0000000..014a09a --- /dev/null +++ b/api/src/migrate/migrate_test.go @@ -0,0 +1,7 @@ +package main_test + +import "testing" + +func TestMigrate_Nooop(t *testing.T) { + t.Log("Noop") +} From 1f4868414fba069ac1a31788f2f9800e3ad5317c Mon Sep 17 00:00:00 2001 From: kuraishi <20122027@kaishi-pu.ac.jp> Date: Sun, 11 Feb 2024 14:14:36 +0900 Subject: [PATCH 17/20] Add tests for router in spi-server #9 --- api/src/router/router_test.go | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 api/src/router/router_test.go diff --git a/api/src/router/router_test.go b/api/src/router/router_test.go new file mode 100644 index 0000000..67245a6 --- /dev/null +++ b/api/src/router/router_test.go @@ -0,0 +1,30 @@ +package router_test + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/labstack/echo" + "github.com/stretchr/testify/assert" + + "api/router" +) + +type mockUserAndEntryController struct{} + +func (m *mockUserAndEntryController) HandleUserAndEntry(c echo.Context) error { + return c.String(http.StatusOK, "Hello, World!") +} + +func TestNewRouter(t *testing.T) { + ctrl := &mockUserAndEntryController{} + router := router.NewRouter(ctrl) + + req := httptest.NewRequest(http.MethodPost, "/", nil) + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, "Hello, World!", rec.Body.String()) +} From 682e2c2939b4adf5bb78c59392054eee62c5eac7 Mon Sep 17 00:00:00 2001 From: kuraishi <20122027@kaishi-pu.ac.jp> Date: Sun, 11 Feb 2024 18:27:46 +0900 Subject: [PATCH 18/20] Add send_request_to_api function in nfc_reader #9 --- api/.env.api.example | 2 ++ nfc_reader/.env.nfc_reader.example | 3 ++- nfc_reader/requirements-dev.txt | 8 +++++++ nfc_reader/requirements.txt | 7 ++++++ nfc_reader/src/main.py | 34 ++++++++++++++++++++++++++++-- 5 files changed, 51 insertions(+), 3 deletions(-) diff --git a/api/.env.api.example b/api/.env.api.example index 278bb44..3e32096 100644 --- a/api/.env.api.example +++ b/api/.env.api.example @@ -2,6 +2,8 @@ TIMEZONE=Asia/Tokyo TIME_VALIDATION_MIN=1704034800 STUDENT_NUMBER_MIN=10000000 STUDENT_NUMBER_MAX=39999999 +NAME_MIN_LENGTH=2 +NAME_MAX_LENGTH=32 MYSQL_USER=admin MYSQL_PASSWORD=password MYSQL_HOST=mysql diff --git a/nfc_reader/.env.nfc_reader.example b/nfc_reader/.env.nfc_reader.example index 6e94dc6..988e811 100644 --- a/nfc_reader/.env.nfc_reader.example +++ b/nfc_reader/.env.nfc_reader.example @@ -1,4 +1,5 @@ NFC_SYSTEM_CODE=0xFE00 -NFC_SERVICE_CODE=0x1A8B NFC_STUDENT_NUM_BLOCK_CODE=0 +NFC_SERVICE_CODE=0x1A8B NFC_NAME_BLOCK_CODE=1 +API_URL=http://api:8080 diff --git a/nfc_reader/requirements-dev.txt b/nfc_reader/requirements-dev.txt index f7748ef..16c907c 100644 --- a/nfc_reader/requirements-dev.txt +++ b/nfc_reader/requirements-dev.txt @@ -1,7 +1,10 @@ astroid==3.0.2 black==24.1.1 +certifi==2024.2.2 +charset-normalizer==3.3.2 click==8.1.7 dill==0.3.8 +idna==3.6 isort==5.13.2 libusb1==3.1.0 mccabe==0.7.0 @@ -16,5 +19,10 @@ pyDes==2.0.1 pylint==3.0.3 pyserial==3.5 python-dotenv==1.0.1 +requests==2.31.0 +setuptools==69.0.3 tomlkit==0.12.3 +types-requests==2.31.0.20240125 typing_extensions==4.9.0 +urllib3==2.2.0 +wheel==0.42.0 diff --git a/nfc_reader/requirements.txt b/nfc_reader/requirements.txt index 8db34f8..27db39d 100644 --- a/nfc_reader/requirements.txt +++ b/nfc_reader/requirements.txt @@ -1,6 +1,13 @@ +certifi==2024.2.2 +charset-normalizer==3.3.2 +idna==3.6 libusb1==3.1.0 ndeflib==0.3.3 nfcpy==1.0.4 pyDes==2.0.1 pyserial==3.5 python-dotenv==1.0.1 +requests==2.31.0 +setuptools==69.0.3 +urllib3==2.2.0 +wheel==0.42.0 diff --git a/nfc_reader/src/main.py b/nfc_reader/src/main.py index b6fb4cd..fbc0fe1 100644 --- a/nfc_reader/src/main.py +++ b/nfc_reader/src/main.py @@ -3,9 +3,12 @@ """ import os +import threading +import time from dataclasses import dataclass import nfc +import requests from dotenv import load_dotenv @@ -56,7 +59,7 @@ def read_nfc_tag(tag: nfc.tag.Tag, config: Configuration) -> NfcTagInfo: student_num = tag.read_without_encryption([sc], [bc]) if isinstance(student_num, str): student_num = student_num.encode("shift_jis") - student_num = student_num.decode("shift_jis") + student_num = student_num.decode("shift_jis").strip("\x00").strip("\x001") print("student_number : " + str(student_num)) # name @@ -64,12 +67,33 @@ def read_nfc_tag(tag: nfc.tag.Tag, config: Configuration) -> NfcTagInfo: name = tag.read_without_encryption([sc], [bc]) if isinstance(name, str): name = name.encode("shift_jis") - name = name.decode("shift_jis") + name = name.decode("shift_jis").strip("\x00").strip("\x001") print("name : " + str(name)) return NfcTagInfo(idm, pmm, config.nfc_system_code, student_num, name) +def send_request_to_api(nfc_tag_info: NfcTagInfo, unix_timestamp: float) -> None: + """ + NFCタグの情報とUnixタイムスタンプをAPIに送信します。 + + Parameters: + nfc_tag_info (NfcTagInfo): NFCタグの情報を含むオブジェクト + unix_timestamp (float): Unixタイムスタンプ(1970年1月1日からの経過秒数) + """ + url = os.environ["API_URL"] + headers = {"Content-Type": "application/json"} + body = { + "student_number": int(nfc_tag_info.student_num), + "name": nfc_tag_info.name, + "timestamp": unix_timestamp, + } + + response = requests.post(url, json=body, headers=headers, timeout=5) + print(response.status_code) + print(response.text) + + def on_connect(tag: nfc.tag.Tag) -> bool: """ NFCタグが接続されたときに呼び出される関数。 @@ -90,6 +114,12 @@ def on_connect(tag: nfc.tag.Tag) -> bool: nfc_tag_info = read_nfc_tag(tag, configuration) print(nfc_tag_info) + current_unix_time = time.time() + + thread = threading.Thread( + target=send_request_to_api, args=(nfc_tag_info, current_unix_time) + ) + thread.start() return True From 396bd07e3aa74ee896a9722caf6ee66bce645770 Mon Sep 17 00:00:00 2001 From: kuraishi <20122027@kaishi-pu.ac.jp> Date: Tue, 13 Feb 2024 11:20:51 +0900 Subject: [PATCH 19/20] Add api-server tests to pre-commit and GitHub Actions --- .../Code_quality_and_docker_build.yml | 6 ++++ .github/workflows/Run_api_server_tests.yml | 33 +++++++++++++++++++ .pre-commit-config.yaml | 9 +++++ api/.env.api.example | 1 - api/Dockerfile | 7 ++++ mysql/.env.mysql.example | 1 + 6 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/Run_api_server_tests.yml diff --git a/.github/workflows/Code_quality_and_docker_build.yml b/.github/workflows/Code_quality_and_docker_build.yml index 6bc0236..9596528 100644 --- a/.github/workflows/Code_quality_and_docker_build.yml +++ b/.github/workflows/Code_quality_and_docker_build.yml @@ -136,6 +136,11 @@ jobs: - name: Create api/.env.api run: | touch api/.env.api + echo "TIME_VALIDATION_MIN=${{ secrets.TIME_VALIDATION_MIN }}" >> api/.env.api + echo "STUDENT_NUMBER_MIN=${{ secrets.STUDENT_NUMBER_MIN }}" >> api/.env.api + echo "STUDENT_NUMBER_MAX=${{ secrets.STUDENT_NUMBER_MAX }}" >> api/.env.api + echo "NAME_MIN_LENGTH=${{ secrets.NAME_MIN_LENGTH }}" >> api/.env.api + echo "NAME_MAX_LENGTH=${{ secrets.NAME_MAX_LENGTH }}" >> api/.env.api echo "MYSQL_USER=${{ secrets.MYSQL_USER }}" >> api/.env.api echo "MYSQL_PASSWORD=${{ secrets.MYSQL_PASSWORD }}" >> api/.env.api echo "MYSQL_HOST=${{ secrets.MYSQL_HOST }}" >> api/.env.api @@ -159,6 +164,7 @@ jobs: echo "NFC_SERVICE_CODE=${{ secrets.NFC_SERVICE_CODE }}" >> nfc_reader/.env.nfc_reader echo "NFC_STUDENT_NUM_BLOCK_CODE=${{ secrets.NFC_STUDENT_NUM_BLOCK_CODE }}" >> nfc_reader/.env.nfc_reader echo "NFC_NAME_BLOCK_CODE=${{ secrets.NFC_NAME_BLOCK_CODE }}" >> nfc_reader/.env.nfc_reader + echo "API_URL=${{ secrets.API_URL }}" >> nfc_reader/.env.nfc_reader - name: Build docker image run: sudo docker-compose build diff --git a/.github/workflows/Run_api_server_tests.yml b/.github/workflows/Run_api_server_tests.yml new file mode 100644 index 0000000..fb79ab3 --- /dev/null +++ b/.github/workflows/Run_api_server_tests.yml @@ -0,0 +1,33 @@ +name: Run api server tests + +on: + push: + branches: + - main + pull_request: + +jobs: + run-api-server-test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Create api/.env.api + run: | + touch api/.env.api + echo "TIME_VALIDATION_MIN=${{ secrets.TIME_VALIDATION_MIN }}" >> api/.env.api + echo "STUDENT_NUMBER_MIN=${{ secrets.STUDENT_NUMBER_MIN }}" >> api/.env.api + echo "STUDENT_NUMBER_MAX=${{ secrets.STUDENT_NUMBER_MAX }}" >> api/.env.api + echo "NAME_MIN_LENGTH=${{ secrets.NAME_MIN_LENGTH }}" >> api/.env.api + echo "NAME_MAX_LENGTH=${{ secrets.NAME_MAX_LENGTH }}" >> api/.env.api + echo "MYSQL_USER=${{ secrets.MYSQL_USER }}" >> api/.env.api + echo "MYSQL_PASSWORD=${{ secrets.MYSQL_PASSWORD }}" >> api/.env.api + echo "MYSQL_HOST=${{ secrets.MYSQL_HOST }}" >> api/.env.api + echo "MYSQL_DATABASE=${{ secrets.MYSQL_DATABASE }}" >> api/.env.api + + - name: Run Go tests + run: | + cd api/ && sudo docker build --target test -t nfc-entry-management-api-test:latest -f ./Dockerfile . + sudo docker run --env-file .env.api --rm nfc-entry-management-api-test:latest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2f58082..96df415 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -57,6 +57,15 @@ repos: language: system types: [python] + - repo: local + hooks: + - id: go-tests + name: Run Go tests + entry: sh -c 'cd api/ && docker build --target test -t nfc-entry-management-api-test:latest -f ./Dockerfile . && docker run --env-file .env.api --rm nfc-entry-management-api-test:latest' + language: system + pass_filenames: false + stages: [commit] + - repo: local hooks: - id: docker-compose-check diff --git a/api/.env.api.example b/api/.env.api.example index 3e32096..4d3bdd9 100644 --- a/api/.env.api.example +++ b/api/.env.api.example @@ -1,4 +1,3 @@ -TIMEZONE=Asia/Tokyo TIME_VALIDATION_MIN=1704034800 STUDENT_NUMBER_MIN=10000000 STUDENT_NUMBER_MAX=39999999 diff --git a/api/Dockerfile b/api/Dockerfile index 6844920..90a6faf 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -13,6 +13,13 @@ RUN go mod tidy ENV PATH="/app/bin:${PATH}" +# test stage +FROM base AS test + +WORKDIR /app + +CMD [ "go","test","-v","./..." ] + # development stage FROM base AS development diff --git a/mysql/.env.mysql.example b/mysql/.env.mysql.example index 35d0049..a161a3e 100644 --- a/mysql/.env.mysql.example +++ b/mysql/.env.mysql.example @@ -2,5 +2,6 @@ TZ=Asia/Tokyo MYSQL_USER=admin MYSQL_ROOT_PASSWORD=rootpassword MYSQL_PASSWORD=password +MYSQL_HOST=mysql MYSQL_DATABASE=nfc-entry-management-db LANG=ja_JP.UTF-8 From dd9a7ce8a8f07d513ea2b3a74a42dbcfbfbf5487 Mon Sep 17 00:00:00 2001 From: Kuraishi Taiki <20122027@kaishi-pu.ac.jp> Date: Tue, 13 Feb 2024 15:04:54 +0900 Subject: [PATCH 20/20] Add healthcheck dokcer-compose.yml #9 --- cspell.json | 3 +++ docker-compose.yml | 11 ++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/cspell.json b/cspell.json index dcff822..d6d298e 100644 --- a/cspell.json +++ b/cspell.json @@ -24,10 +24,13 @@ "pylint", "ozzo", "Nooop", + "mysqld", + "mysqladmin", "mypy", "mockdb", "labstack", "isort", + "healthcheck", "gorm", "gopls", "golangci", diff --git a/docker-compose.yml b/docker-compose.yml index 74da114..50361b5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,7 +12,10 @@ services: - "8080:8080" env_file: - ./api/.env.api - tty: true + depends_on: + mysql: + condition: service_healthy + command: ["go","run","main.go"] mysql: container_name: nfc-entry-management-mysql @@ -24,6 +27,12 @@ services: - "3306:3306" env_file: - ./mysql/.env.mysql + healthcheck: + test: ["CMD-SHELL","mysqladmin ping -h localhost -u root -p$$MYSQL_ROOT_PASSWORD | grep 'mysqld is alive'"] + interval: 10s + timeout: 3s + retries: 3 + start_period: 30s nfc_reader: container_name: nfc-entry-management-nfc-reader