diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..e8796d6 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,54 @@ +name: Build + +on: + workflow_dispatch: + push: + branches: + - main + - beta + - dev + - master + +env: + IMAGE_NAME: ghcr.io/${{ github.repository }} + IMAGE_TAG: ${{ github.sha }} + +jobs: + build: + name: Build + runs-on: ubuntu-latest + + permissions: + contents: read + packages: write + + outputs: + BRANCH: ${{ steps.branch.outputs.BRANCH }} + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Set branch + id: branch + run: | + echo "::set-output name=BRANCH::${GITHUB_REF#refs/heads/}" + + - name: Log in to the Container Registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and Push Docker Image + uses: docker/build-push-action@v2 + with: + context: . + push: true + tags: ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }},${{ env.IMAGE_NAME }}:latest + cache-from: type=registry,ref=${{ env.IMAGE_NAME }}:buildcache + cache-to: type=registry,ref=${{ env.IMAGE_NAME }}:buildcache,mode=max \ No newline at end of file diff --git a/.github/workflows/run-unit-test.yaml b/.github/workflows/run-unit-test.yaml new file mode 100644 index 0000000..d14323e --- /dev/null +++ b/.github/workflows/run-unit-test.yaml @@ -0,0 +1,38 @@ +name: 'Pull request/Push: Run unit test' + +on: + pull_request: + branches: + - dev + - master + - main + - beta + push: + branches: + - dev + - master + - main + - beta + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.21 + + - name: Download dependencies + run: go mod download + + - name: Vet + run: | + go vet ./... + + - name: Test + run: | + go test -v -coverpkg ./src/app/... -coverprofile coverage.out -covermode count ./src/app/... + go tool cover -func="./coverage.out" diff --git a/.github/workflows/test-build.yaml b/.github/workflows/test-build.yaml new file mode 100644 index 0000000..448e84c --- /dev/null +++ b/.github/workflows/test-build.yaml @@ -0,0 +1,35 @@ +name: 'Pull request/Push: Build test' + +on: + pull_request: + branches: + - dev + - master + - main + - beta + push: + branches: + - dev + - master + - main + - beta + +jobs: + docker-build: + name: 'Docker build and create an image' + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + - name: Build Docker image + id: docker_build + uses: docker/build-push-action@v2 + with: + context: . + push: false + tags: test \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index cb8a396..1301125 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,8 +36,8 @@ COPY --from=base /app/server ./ # Set ENV to production ENV GO_ENV production -# Expose port 3001 -EXPOSE 3001 +# Expose port 3003 +EXPOSE 3003 # Run the application CMD ["./server"] \ No newline at end of file diff --git a/Makefile b/Makefile index 1dd22b2..bc834cc 100644 --- a/Makefile +++ b/Makefile @@ -3,8 +3,8 @@ proto: publish: cat ./token.txt | docker login --username isd-team-sgcu --password-stdin ghcr.io - docker build . -t ghcr.io/isd-sgcu/johnjud-gateway - docker push ghcr.io/isd-sgcu/johnjud-gateway + docker build . -t ghcr.io/isd-sgcu/johnjud-backend + docker push ghcr.io/isd-sgcu/johnjud-backend test: go vet ./... diff --git a/README.md b/README.md index ad50fd8..f813ad7 100644 --- a/README.md +++ b/README.md @@ -1 +1,43 @@ -# johnjud-backend +# Johnjud-backend + +Johnjud-backend handles data management and business logic for the Johnjud project. + +### What is Johnjud? +Johnjud is a pet adoption web application of the [CUVET For Animal Welfare Club](https://www.facebook.com/CUVETforAnimalWelfareClub) + +## Stack + +- golang +- gRPC +- postgresql + +## Getting Started + +### Prerequisites + +- golang 1.21 or [later](https://go.dev) +- docker +- makefile + +### Installation + +1. Clone this repo +2. Copy every `config.example.yaml` in `config` and paste it in the same directory with `.example` removed from its name. + +3. Run `go mod download` to download all the dependencies. + +### Running +1. Run `docker-compose up -d` +2. Run `make server` or `go run ./src/.` + +### Testing +1. Run `make test` or `go test -v -coverpkg ./... -coverprofile coverage.out -covermode count ./...` + +## Other microservices/repositories of Johnjud +- [Johnjud-gateway](https://github.com/isd-sgcu/johnjud-gateway): Routing and request handling +- [Johnjud-auth](https://github.com/isd-sgcu/johnjud-auth): Authentication and authorization +- [Johnjud-backend](https://github.com/isd-sgcu/johnjud-backend): Main business logic +- [Johnjud-file](https://github.com/isd-sgcu/johnjud-file): File management service +- [Johnjud-proto](https://github.com/isd-sgcu/johnjud-proto): Protobuf files generator +- [Johnjud-go-proto](https://github.com/isd-sgcu/johnjud-go-proto): Generated protobuf files for golang +- [Johnjud-frontend](https://github.com/isd-sgcu/johnjud-frontend): Frontend web application diff --git a/config/config.example.yaml b/config/config.example.yaml index a413180..34bc114 100644 --- a/config/config.example.yaml +++ b/config/config.example.yaml @@ -8,3 +8,6 @@ database: name: johnjud_db username: root password: root + +service: + file: localhost:3004 \ No newline at end of file diff --git a/config/file/config.example.yaml b/config/file/config.example.yaml new file mode 100644 index 0000000..f1ecd10 --- /dev/null +++ b/config/file/config.example.yaml @@ -0,0 +1,14 @@ +app: + port: 3004 + debug: true + +database: + host: local-db + port: 5432 + name: johnjud_db + username: root + password: root + +s3: + bucket_name: + region: \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 7c2ad2e..707a72e 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,6 +1,17 @@ version: "3.9" services: + local-file: + image: ghcr.io/isd-sgcu/johnjud-file + container_name: file + depends_on: + - local-db + restart: unless-stopped + volumes: + - ./config/file:/app/config + ports: + - "3004:3004" + local-db: image: postgres:15.1-alpine3.17 container_name: johnjud-local-db @@ -12,7 +23,7 @@ services: volumes: - postgres:/var/lib/postgresql/data ports: - - "5433:5432" + - "5432:5432" volumes: postgres: \ No newline at end of file diff --git a/go.mod b/go.mod index 4baa5b2..ade5ec4 100644 --- a/go.mod +++ b/go.mod @@ -5,29 +5,41 @@ go 1.21.3 toolchain go1.21.5 require ( + github.com/bxcodec/faker/v3 v3.8.1 github.com/google/uuid v1.5.0 + github.com/isd-sgcu/johnjud-go-proto v0.0.9 github.com/pkg/errors v0.9.1 + github.com/rs/zerolog v1.31.0 github.com/spf13/viper v1.18.2 + github.com/stretchr/testify v1.8.4 + google.golang.org/grpc v1.60.1 gorm.io/driver/postgres v1.5.4 gorm.io/gorm v1.25.5 ) require ( - github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/golang/protobuf v1.5.3 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/stretchr/objx v0.5.0 // indirect + golang.org/x/net v0.19.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect + google.golang.org/protobuf v1.32.0 // indirect +) + +require ( + github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect - github.com/isd-sgcu/johnjud-go-proto v0.0.8 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgx/v5 v5.4.3 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/magiconair/properties v1.8.7 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect - github.com/rs/zerolog v1.31.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect @@ -39,12 +51,8 @@ require ( go.uber.org/multierr v1.9.0 // indirect golang.org/x/crypto v0.16.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect - golang.org/x/net v0.19.0 // indirect golang.org/x/sys v0.15.0 // indirect golang.org/x/text v0.14.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect - google.golang.org/grpc v1.60.1 // indirect - google.golang.org/protobuf v1.31.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 7e3bceb..fc7670b 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/bxcodec/faker/v3 v3.8.1 h1:qO/Xq19V6uHt2xujwpaetgKhraGCapqY2CRWGD/SqcM= +github.com/bxcodec/faker/v3 v3.8.1/go.mod h1:DdSDccxF5msjFo5aO4vrobRQ8nIApg8kq3QWPEQD6+o= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -18,8 +20,8 @@ github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/isd-sgcu/johnjud-go-proto v0.0.8 h1:nIQBZgK2OFVrLjVtpeDgwows8poA7LhsIVE4hlbBC1o= -github.com/isd-sgcu/johnjud-go-proto v0.0.8/go.mod h1:HP0w9gC30b5WNnqeFBM9JJZud+pvyikz0+pGFSI/Wjw= +github.com/isd-sgcu/johnjud-go-proto v0.0.9 h1:cFfZ2JSpW0jg94Iv5zHQJGnoekj0eCQe42SJaTpnp3c= +github.com/isd-sgcu/johnjud-go-proto v0.0.9/go.mod h1:1OK6aiCgtXQiLhxp0r6iLEejYIRpckWQZDrCZ9Trbo4= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= @@ -71,6 +73,7 @@ github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -98,15 +101,14 @@ golang.org/x/sys v0.15.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= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f h1:ultW7fxlIvee4HYrtnaRPon9HpEgFk5zYpmfMgtKB5I= google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f/go.mod h1:L9KNLi232K1/xB6f7AlSX692koaRnKaWSR0stBki0Yc= google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU= google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/src/app/model/adopt/adopt.model.go b/src/app/model/adopt/adopt.model.go deleted file mode 100644 index 24a8a86..0000000 --- a/src/app/model/adopt/adopt.model.go +++ /dev/null @@ -1,16 +0,0 @@ -package adopt - -import ( - "github.com/google/uuid" - "github.com/isd-sgcu/johnjud-backend/src/app/model" - "github.com/isd-sgcu/johnjud-backend/src/app/model/pet" - "github.com/isd-sgcu/johnjud-backend/src/app/model/user" -) - -type Adopt struct { - model.Base - PetID *uuid.UUID `json:"pet_id" gorm:"index:idx_name,unique"` - Pet *pet.Pet `json:"pet" gorm:"foreignKey:PetID;constraint:OnUpdate:CASCADE;OnDelete:SET NULL;"` - UserID *uuid.UUID `json:"user_id" gorm:"index:idx_name,unique"` - User *user.User `json:"user" gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE;OnDelete:SET NULL;"` -} diff --git a/src/app/model/common.model.go b/src/app/model/common.model.go index 682db1c..1ab9cd8 100644 --- a/src/app/model/common.model.go +++ b/src/app/model/common.model.go @@ -8,10 +8,10 @@ import ( ) type Base struct { - ID uuid.UUID `json:"id" gorm:"primaryKey;size:191"` - CreatedAt time.Time `json:"created_at" gorm:"type:datetime;autoCreateTime:nano"` - UpdatedAt time.Time `json:"updated_at" gorm:"type:datetime;autoUpdateTime:nano"` - DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index;type:datetime"` + ID uuid.UUID `json:"id" gorm:"primary_key"` + CreatedAt time.Time `json:"created_at" gorm:"type:timestamp;autoCreateTime:nano"` + UpdatedAt time.Time `json:"updated_at" gorm:"type:timestamp;autoUpdateTime:nano"` + DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index;type:timestamp"` } func (m *Base) BeforeCreate(_ *gorm.DB) error { diff --git a/src/app/model/pet/pet.model.go b/src/app/model/pet/pet.model.go index d6b916a..3c88a37 100644 --- a/src/app/model/pet/pet.model.go +++ b/src/app/model/pet/pet.model.go @@ -22,4 +22,5 @@ type Pet struct { Background string `json:"background" gorm:"tinytext"` Address string `json:"address" gorm:"tinytext"` Contact string `json:"contact" gorm:"tinytext"` + AdoptBy string `json:"adopt_by" gorm:"tinytext"` } diff --git a/src/app/model/user/user.model.go b/src/app/model/user/user.model.go index e93dae0..332db06 100644 --- a/src/app/model/user/user.model.go +++ b/src/app/model/user/user.model.go @@ -4,9 +4,9 @@ import "github.com/isd-sgcu/johnjud-backend/src/app/model" type User struct { model.Base - Email string `json:"email" gorm:"type:tinytext"` - Password string `json:"password" gorm:"type:tinytext"` - Firstname string `json:"firstname" gorm:"type:tinytext"` - Lastname string `json:"lastname" gorm:"type:tinytext"` - Role string `json:"role" gorm:"type:tinytext"` + Email string `json:"email" gorm:"tinytext"` + Password string `json:"password" gorm:"tinytext"` + Firstname string `json:"firstname" gorm:"tinytext"` + Lastname string `json:"lastname" gorm:"tinytext"` + Role string `json:"role" gorm:"tinytext"` } diff --git a/src/app/repository/adopt/adopt.repository.go b/src/app/repository/adopt/adopt.repository.go deleted file mode 100644 index 2acc99b..0000000 --- a/src/app/repository/adopt/adopt.repository.go +++ /dev/null @@ -1,26 +0,0 @@ -package adopt - -import ( - "github.com/isd-sgcu/johnjud-backend/src/app/model/adopt" - "gorm.io/gorm" -) - -type Repository struct { - db *gorm.DB -} - -func NewRepository(db *gorm.DB) *Repository { - return &Repository{db: db} -} - -func (r *Repository) FindAll(result *[]*adopt.Adopt) error { - return r.db.Model(&adopt.Adopt{}).Find(result).Error -} - -func (r *Repository) Create(in *adopt.Adopt) error { - return r.db.Create(&in).Error -} - -func (r *Repository) Delete(id string) error { - return r.db.Where("id = ?", id).Delete(&adopt.Adopt{}).Error -} diff --git a/src/app/service/image/image.service.go b/src/app/service/image/image.service.go new file mode 100644 index 0000000..0945ec5 --- /dev/null +++ b/src/app/service/image/image.service.go @@ -0,0 +1,34 @@ +package image + +import ( + "context" + "time" + + proto "github.com/isd-sgcu/johnjud-go-proto/johnjud/file/image/v1" + "github.com/rs/zerolog/log" +) + +type Service struct { + client proto.ImageServiceClient +} + +func NewService(client proto.ImageServiceClient) *Service { + return &Service{client: client} +} + +func (s *Service) FindByPetId(petId string) ([]*proto.Image, error) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + res, err := s.client.FindByPetId(ctx, &proto.FindImageByPetIdRequest{PetId: petId}) + if err != nil { + log.Error(). + Err(err). + Str("service", "image"). + Str("module", "find by petId"). + Msg("Error while connecting to service") + return nil, err + } + return res.Images, nil + +} diff --git a/src/app/service/image/image.service_test.go b/src/app/service/image/image.service_test.go new file mode 100644 index 0000000..3effd29 --- /dev/null +++ b/src/app/service/image/image.service_test.go @@ -0,0 +1,68 @@ +package image + +import ( + "testing" + + "github.com/bxcodec/faker/v3" + mock "github.com/isd-sgcu/johnjud-backend/src/mocks/image" + proto "github.com/isd-sgcu/johnjud-go-proto/johnjud/file/image/v1" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type ImageServiceTest struct { + suite.Suite + petId string + images []*proto.Image +} + +func TestImageService(t *testing.T) { + suite.Run(t, new(ImageServiceTest)) +} + +func (t *ImageServiceTest) SetupTest() { + t.petId = faker.UUIDDigit() + t.images = []*proto.Image{ + { + Id: faker.UUIDDigit(), + PetId: t.petId, + ImageUrl: faker.URL(), + }, + { + Id: faker.UUIDDigit(), + PetId: t.petId, + ImageUrl: faker.URL(), + }, + } +} + +func (t *ImageServiceTest) TestFindByPetIdSuccess() { + want := t.images + + c := mock.ClientMock{} + c.On("FindByPetId", &proto.FindImageByPetIdRequest{PetId: t.petId}). + Return(&proto.FindImageByPetIdResponse{Images: t.images}, nil) + + srv := NewService(&c) + actual, err := srv.FindByPetId(t.petId) + + assert.Nil(t.T(), err) + assert.Equal(t.T(), want, actual) +} + +func (t *ImageServiceTest) TestFindByPetIdError() { + c := mock.ClientMock{} + c.On("FindByPetId", &proto.FindImageByPetIdRequest{PetId: t.petId}). + Return(&proto.FindImageByPetIdResponse{Images: t.images}, status.Error(codes.Unavailable, "Connection Timeout")) + + srv := NewService(&c) + actual, err := srv.FindByPetId(t.petId) + + st, ok := status.FromError(err) + assert.True(t.T(), ok) + assert.Nil(t.T(), actual) + assert.Equal(t.T(), codes.Unavailable, st.Code()) +} diff --git a/src/app/service/like/like.service.go b/src/app/service/like/like.service.go index 064558b..1d111fc 100644 --- a/src/app/service/like/like.service.go +++ b/src/app/service/like/like.service.go @@ -15,6 +15,7 @@ import ( ) type Service struct { + proto.UnimplementedLikeServiceServer repository IRepository } diff --git a/src/app/service/pet/pet.service.go b/src/app/service/pet/pet.service.go new file mode 100644 index 0000000..552f81b --- /dev/null +++ b/src/app/service/pet/pet.service.go @@ -0,0 +1,267 @@ +package pet + +import ( + "context" + "errors" + + "time" + + "github.com/google/uuid" + "github.com/isd-sgcu/johnjud-backend/src/app/model" + "github.com/isd-sgcu/johnjud-backend/src/app/model/pet" + petConst "github.com/isd-sgcu/johnjud-backend/src/constant/pet" + proto "github.com/isd-sgcu/johnjud-go-proto/johnjud/backend/pet/v1" + image_proto "github.com/isd-sgcu/johnjud-go-proto/johnjud/file/image/v1" + "github.com/rs/zerolog/log" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "gorm.io/gorm" +) + +type Service struct { + proto.UnimplementedPetServiceServer + repository IRepository + imageService ImageService +} + +type IRepository interface { + FindAll(result *[]*pet.Pet) error + FindOne(id string, result *pet.Pet) error + Create(in *pet.Pet) error + Update(id string, result *pet.Pet) error + Delete(id string) error +} + +type ImageService interface { + FindByPetId(petId string) ([]*image_proto.Image, error) +} + +func NewService(repository IRepository, imageService ImageService) *Service { + return &Service{repository: repository, imageService: imageService} +} + +func (s *Service) Delete(ctx context.Context, req *proto.DeletePetRequest) (*proto.DeletePetResponse, error) { + err := s.repository.Delete(req.Id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, status.Error(codes.NotFound, "pet not found") + } + return nil, status.Error(codes.Internal, "internal error") + } + return &proto.DeletePetResponse{Success: true}, nil +} + +func (s *Service) Update(_ context.Context, req *proto.UpdatePetRequest) (res *proto.UpdatePetResponse, err error) { + raw, err := DtoToRaw(req.Pet) + if err != nil { + return nil, status.Error(codes.Internal, "error converting dto to raw") + } + + err = s.repository.Update(req.Pet.Id, raw) + if err != nil { + return nil, status.Error(codes.NotFound, "pet not found") + } + + images, err := s.imageService.FindByPetId(req.Pet.Id) + if err != nil { + return nil, status.Error(codes.Internal, "error querying image service") + } + imageUrls := ExtractImageUrls(images) + + return &proto.UpdatePetResponse{Pet: RawToDto(raw, imageUrls)}, nil +} + +func (s *Service) ChangeView(_ context.Context, req *proto.ChangeViewPetRequest) (res *proto.ChangeViewPetResponse, err error) { + petData, err := s.FindOne(context.Background(), &proto.FindOnePetRequest{Id: req.Id}) + if err != nil { + return nil, status.Error(codes.NotFound, "pet not found") + } + pet, err := DtoToRaw(petData.Pet) + if err != nil { + return nil, status.Error(codes.Internal, "error converting dto to raw") + } + pet.IsVisible = req.Visible + + err = s.repository.Update(req.Id, pet) + if err != nil { + return nil, status.Error(codes.NotFound, "pet not found") + } + + return &proto.ChangeViewPetResponse{Success: true}, nil +} + +func (s *Service) FindAll(_ context.Context, req *proto.FindAllPetRequest) (res *proto.FindAllPetResponse, err error) { + var pets []*pet.Pet + var imageUrlsList [][]string + + err = s.repository.FindAll(&pets) + if err != nil { + log.Error().Err(err).Str("service", "event").Str("module", "find all").Msg("Error while querying all events") + return nil, status.Error(codes.Unavailable, "Internal error") + } + + for _, pet := range pets { + images, err := s.imageService.FindByPetId(pet.ID.String()) + if err != nil { + return nil, status.Error(codes.Internal, "error querying image service") + } + imageUrls := ExtractImageUrls(images) + imageUrlsList = append(imageUrlsList, imageUrls) + } + + petWithImageUrls, err := RawToDtoList(&pets, imageUrlsList) + if err != nil { + return nil, status.Error(codes.Internal, "error converting raw to dto list") + } + + return &proto.FindAllPetResponse{Pets: petWithImageUrls}, nil +} + +func (s Service) FindOne(_ context.Context, req *proto.FindOnePetRequest) (res *proto.FindOnePetResponse, err error) { + var pet pet.Pet + + err = s.repository.FindOne(req.Id, &pet) + if err != nil { + log.Error().Err(err). + Str("service", "pet").Str("module", "find one").Str("id", req.Id).Msg("Not found") + return nil, status.Error(codes.NotFound, err.Error()) + } + + images, err := s.imageService.FindByPetId(req.Id) + if err != nil { + return nil, status.Error(codes.Internal, "error querying image service") + } + imageUrls := ExtractImageUrls(images) + + return &proto.FindOnePetResponse{Pet: RawToDto(&pet, imageUrls)}, err +} + +func (s *Service) Create(_ context.Context, req *proto.CreatePetRequest) (res *proto.CreatePetResponse, err error) { + raw, err := DtoToRaw(req.Pet) + if err != nil { + return nil, status.Error(codes.Internal, "error converting dto to raw: "+err.Error()) + } + + imgUrls := []string{} + + err = s.repository.Create(raw) + if err != nil { + return nil, status.Error(codes.Internal, "failed to create pet") + } + + return &proto.CreatePetResponse{Pet: RawToDto(raw, imgUrls)}, nil +} + +func (s *Service) AdoptPet(ctx context.Context, req *proto.AdoptPetRequest) (res *proto.AdoptPetResponse, err error) { + dtoPet, err := s.FindOne(context.Background(), &proto.FindOnePetRequest{Id: req.PetId}) + if err != nil { + return nil, status.Error(codes.NotFound, "pet not found") + } + pet, err := DtoToRaw(dtoPet.Pet) + if err != nil { + return nil, status.Error(codes.Internal, "error converting dto to raw") + } + pet.AdoptBy = req.UserId + + err = s.repository.Update(req.PetId, pet) + if err != nil { + return nil, status.Error(codes.NotFound, "pet not found") + } + + return &proto.AdoptPetResponse{Success: true}, nil +} + +func RawToDtoList(in *[]*pet.Pet, imageUrls [][]string) ([]*proto.Pet, error) { + var result []*proto.Pet + if len(*in) != len(imageUrls) { + return nil, errors.New("length of in and imageUrls have to be the same") + } + + for i, e := range *in { + result = append(result, RawToDto(e, imageUrls[i])) + } + return result, nil +} + +func RawToDto(in *pet.Pet, imgUrl []string) *proto.Pet { + return &proto.Pet{ + Id: in.ID.String(), + Type: in.Type, + Species: in.Species, + Name: in.Name, + Birthdate: in.Birthdate, + Gender: proto.Gender(in.Gender), + Habit: in.Habit, + Caption: in.Caption, + Status: proto.PetStatus(in.Status), + ImageUrls: imgUrl, + IsSterile: in.IsSterile, + IsVaccinated: in.IsVaccinated, + IsVisible: in.IsVisible, + IsClubPet: in.IsClubPet, + Background: in.Background, + Address: in.Address, + Contact: in.Contact, + AdoptBy: in.AdoptBy, + } +} + +func DtoToRaw(in *proto.Pet) (res *pet.Pet, err error) { + var id uuid.UUID + var gender petConst.Gender + var status petConst.Status + + if in.Id != "" { + id, err = uuid.Parse(in.Id) + if err != nil { + return nil, err + } + } + + switch in.Gender { + case 0: + gender = petConst.MALE + case 1: + gender = petConst.FEMALE + } + + switch in.Status { + case 0: + status = petConst.ADOPTED + case 1: + status = petConst.FINDHOME + } + + return &pet.Pet{ + Base: model.Base{ + ID: id, + CreatedAt: time.Time{}, + UpdatedAt: time.Time{}, + DeletedAt: gorm.DeletedAt{}, + }, + Type: in.Type, + Species: in.Species, + Name: in.Name, + Birthdate: in.Birthdate, + Gender: gender, + Habit: in.Habit, + Caption: in.Caption, + Status: status, + IsSterile: in.IsSterile, + IsVaccinated: in.IsVaccinated, + IsVisible: in.IsVisible, + IsClubPet: in.IsClubPet, + Background: in.Background, + Address: in.Address, + Contact: in.Contact, + AdoptBy: in.AdoptBy, + }, nil +} + +func ExtractImageUrls(in []*image_proto.Image) []string { + var result []string + for _, e := range in { + result = append(result, e.ImageUrl) + } + return result +} diff --git a/src/app/service/pet/pet.service_test.go b/src/app/service/pet/pet.service_test.go new file mode 100644 index 0000000..04d36b9 --- /dev/null +++ b/src/app/service/pet/pet.service_test.go @@ -0,0 +1,610 @@ +package pet + +import ( + "context" + "errors" + "math/rand" + "testing" + "time" + + "github.com/bxcodec/faker/v3" + "github.com/google/uuid" + img_mock "github.com/isd-sgcu/johnjud-backend/src/mocks/image" + mock "github.com/isd-sgcu/johnjud-backend/src/mocks/pet" + "gorm.io/gorm" + + "github.com/isd-sgcu/johnjud-backend/src/app/model" + "github.com/isd-sgcu/johnjud-backend/src/app/model/pet" + proto "github.com/isd-sgcu/johnjud-go-proto/johnjud/backend/pet/v1" + img_proto "github.com/isd-sgcu/johnjud-go-proto/johnjud/file/image/v1" + + petConst "github.com/isd-sgcu/johnjud-backend/src/constant/pet" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +type PetServiceTest struct { + suite.Suite + Pet *pet.Pet + UpdatePet *pet.Pet + ChangeViewPet *pet.Pet + Pets []*pet.Pet + PetDto *proto.Pet + CreatePetReqMock *proto.CreatePetRequest + UpdatePetReqMock *proto.UpdatePetRequest + ChangeViewPetReqMock *proto.ChangeViewPetRequest + Images []*img_proto.Image + ImageUrls []string + ImagesList [][]*img_proto.Image + ImageUrlsList [][]string + ChangeAdoptBy *pet.Pet + AdoptByReq *proto.AdoptPetRequest +} + +func TestPetService(t *testing.T) { + suite.Run(t, new(PetServiceTest)) +} + +func (t *PetServiceTest) SetupTest() { + var pets []*pet.Pet + for i := 0; i <= 3; i++ { + pet := &pet.Pet{ + Base: model.Base{ + ID: uuid.New(), + CreatedAt: time.Time{}, + UpdatedAt: time.Time{}, + DeletedAt: gorm.DeletedAt{}, + }, + Type: faker.Word(), + Species: faker.Word(), + Name: faker.Name(), + Birthdate: faker.Word(), + Gender: petConst.Gender(rand.Intn(1) + 1), + Habit: faker.Paragraph(), + Caption: faker.Paragraph(), + Status: petConst.Status(rand.Intn(1) + 1), + IsSterile: true, + IsVaccinated: true, + IsVisible: true, + IsClubPet: true, + Background: faker.Paragraph(), + Address: faker.Paragraph(), + Contact: faker.Paragraph(), + AdoptBy: "", + } + var images []*img_proto.Image + var imageUrls []string + for i := 0; i < 3; i++ { + url := faker.URL() + images = append(images, &img_proto.Image{ + Id: faker.UUIDDigit(), + PetId: pet.ID.String(), + ImageUrl: url, + }) + imageUrls = append(imageUrls, url) + } + t.ImagesList = append(t.ImagesList, images) + t.ImageUrlsList = append(t.ImageUrlsList, imageUrls) + pets = append(pets, pet) + } + + t.Pets = pets + t.Pet = pets[0] + + for _, images := range t.ImagesList { + for _, image := range images { + t.ImageUrls = append(t.ImageUrls, image.ImageUrl) + } + } + + t.Images = t.ImagesList[0] + t.ImageUrls = t.ImageUrlsList[0] + + t.PetDto = &proto.Pet{ + Id: t.Pet.ID.String(), + Type: t.Pet.Type, + Species: t.Pet.Species, + Name: t.Pet.Name, + Birthdate: t.Pet.Birthdate, + Gender: proto.Gender(t.Pet.Gender), + Habit: t.Pet.Habit, + Caption: t.Pet.Caption, + Status: proto.PetStatus(t.Pet.Status), + IsSterile: t.Pet.IsSterile, + IsVaccinated: t.Pet.IsVaccinated, + IsVisible: t.Pet.IsVisible, + IsClubPet: t.Pet.IsClubPet, + Background: t.Pet.Background, + Address: t.Pet.Address, + Contact: t.Pet.Contact, + ImageUrls: t.ImageUrls, + } + + t.UpdatePet = &pet.Pet{ + Base: model.Base{ + ID: t.Pet.Base.ID, + CreatedAt: t.Pet.Base.CreatedAt, + UpdatedAt: t.Pet.Base.UpdatedAt, + DeletedAt: t.Pet.Base.DeletedAt, + }, + Type: t.Pet.Type, + Species: t.Pet.Species, + Name: t.Pet.Name, + Birthdate: t.Pet.Birthdate, + Gender: t.Pet.Gender, + Habit: t.Pet.Habit, + Caption: t.Pet.Caption, + Status: t.Pet.Status, + IsSterile: t.Pet.IsSterile, + IsVaccinated: t.Pet.IsVaccinated, + IsVisible: t.Pet.IsVisible, + IsClubPet: t.Pet.IsClubPet, + Background: t.Pet.Background, + Address: t.Pet.Address, + Contact: t.Pet.Contact, + } + + t.ChangeViewPet = &pet.Pet{ + Base: model.Base{ + ID: t.Pet.Base.ID, + CreatedAt: t.Pet.Base.CreatedAt, + UpdatedAt: t.Pet.Base.UpdatedAt, + DeletedAt: t.Pet.Base.DeletedAt, + }, + Type: t.Pet.Type, + Species: t.Pet.Species, + Name: t.Pet.Name, + Birthdate: t.Pet.Birthdate, + Gender: t.Pet.Gender, + Habit: t.Pet.Habit, + Caption: t.Pet.Caption, + Status: t.Pet.Status, + IsSterile: t.Pet.IsSterile, + IsVaccinated: t.Pet.IsVaccinated, + IsVisible: false, + IsClubPet: t.Pet.IsClubPet, + Background: t.Pet.Background, + Address: t.Pet.Address, + Contact: t.Pet.Contact, + } + + t.CreatePetReqMock = &proto.CreatePetRequest{ + Pet: &proto.Pet{ + Type: t.Pet.Type, + Species: t.Pet.Species, + Name: t.Pet.Name, + Birthdate: t.Pet.Birthdate, + Gender: proto.Gender(t.Pet.Gender), + Habit: t.Pet.Habit, + Caption: t.Pet.Caption, + Status: proto.PetStatus(t.Pet.Status), + ImageUrls: t.ImageUrls, + IsSterile: t.Pet.IsSterile, + IsVaccinated: t.Pet.IsVaccinated, + IsVisible: t.Pet.IsVaccinated, + IsClubPet: t.Pet.IsClubPet, + Background: t.Pet.Background, + Address: t.Pet.Address, + Contact: t.Pet.Contact, + }, + } + + t.UpdatePetReqMock = &proto.UpdatePetRequest{ + Pet: &proto.Pet{ + Id: t.Pet.ID.String(), + Type: t.Pet.Type, + Species: t.Pet.Species, + Name: t.Pet.Name, + Birthdate: t.Pet.Birthdate, + Gender: proto.Gender(t.Pet.Gender), + Habit: t.Pet.Habit, + Caption: t.Pet.Caption, + Status: proto.PetStatus(t.Pet.Status), + ImageUrls: t.ImageUrls, + IsSterile: t.Pet.IsSterile, + IsVaccinated: t.Pet.IsVaccinated, + IsVisible: t.Pet.IsVisible, + IsClubPet: t.Pet.IsClubPet, + Background: t.Pet.Background, + Address: t.Pet.Address, + Contact: t.Pet.Contact, + }, + } + + t.ChangeViewPetReqMock = &proto.ChangeViewPetRequest{ + Id: t.Pet.ID.String(), + Visible: false, + } + + t.ChangeAdoptBy = &pet.Pet{ + Base: model.Base{ + ID: t.Pet.Base.ID, + CreatedAt: t.Pet.Base.CreatedAt, + UpdatedAt: t.Pet.Base.UpdatedAt, + DeletedAt: t.Pet.Base.DeletedAt, + }, + Type: t.Pet.Type, + Species: t.Pet.Species, + Name: t.Pet.Name, + Birthdate: t.Pet.Birthdate, + Gender: t.Pet.Gender, + Habit: t.Pet.Habit, + Caption: t.Pet.Caption, + Status: t.Pet.Status, + IsSterile: t.Pet.IsSterile, + IsVaccinated: t.Pet.IsVaccinated, + IsVisible: t.Pet.IsVisible, + IsClubPet: t.Pet.IsClubPet, + Background: t.Pet.Background, + Address: t.Pet.Address, + Contact: t.Pet.Contact, + AdoptBy: faker.UUIDDigit(), + } + + t.AdoptByReq = &proto.AdoptPetRequest{ + PetId: t.ChangeAdoptBy.ID.String(), + UserId: t.ChangeAdoptBy.AdoptBy, + } + +} +func (t *PetServiceTest) TestDeleteSuccess() { + want := &proto.DeletePetResponse{Success: true} + + repo := new(mock.RepositoryMock) + repo.On("Delete", t.Pet.ID.String()).Return(nil) + imgSrv := new(img_mock.ServiceMock) + + srv := NewService(repo, imgSrv) + actual, err := srv.Delete(context.Background(), &proto.DeletePetRequest{Id: t.Pet.ID.String()}) + + assert.Nil(t.T(), err) + assert.Equal(t.T(), want, actual) + repo.AssertExpectations(t.T()) +} + +func (t *PetServiceTest) TestDeleteNotFound() { + repo := new(mock.RepositoryMock) + repo.On("Delete", t.Pet.ID.String()).Return(gorm.ErrRecordNotFound) + imgSrv := new(img_mock.ServiceMock) + + srv := NewService(repo, imgSrv) + _, err := srv.Delete(context.Background(), &proto.DeletePetRequest{Id: t.Pet.ID.String()}) + + st, ok := status.FromError(err) + assert.True(t.T(), ok) + assert.Equal(t.T(), codes.NotFound, st.Code()) + repo.AssertExpectations(t.T()) +} + +func (t *PetServiceTest) TestDeleteWithDatabaseError() { + repo := new(mock.RepositoryMock) + repo.On("Delete", t.Pet.ID.String()).Return(errors.New("internal server error")) + imgSrv := new(img_mock.ServiceMock) + + srv := NewService(repo, imgSrv) + _, err := srv.Delete(context.Background(), &proto.DeletePetRequest{Id: t.Pet.ID.String()}) + + st, ok := status.FromError(err) + assert.True(t.T(), ok) + assert.Equal(t.T(), codes.Internal, st.Code()) + repo.AssertExpectations(t.T()) +} + +func (t *PetServiceTest) TestDeleteWithUnexpectedError() { + repo := new(mock.RepositoryMock) + repo.On("Delete", t.Pet.ID.String()).Return(errors.New("unexpected error")) + imgSrv := new(img_mock.ServiceMock) + + srv := NewService(repo, imgSrv) + _, err := srv.Delete(context.Background(), &proto.DeletePetRequest{Id: t.Pet.ID.String()}) + + assert.Error(t.T(), err) + repo.AssertExpectations(t.T()) +} + +func (t *PetServiceTest) TestFindOneSuccess() { + want := &proto.FindOnePetResponse{Pet: t.PetDto} + + repo := &mock.RepositoryMock{} + repo.On("FindOne", t.Pet.ID.String(), &pet.Pet{}).Return(t.Pet, nil) + imgSrv := new(img_mock.ServiceMock) + imgSrv.On("FindByPetId", t.Pet.ID.String()).Return(t.Images, nil) + + srv := NewService(repo, imgSrv) + actual, err := srv.FindOne(context.Background(), &proto.FindOnePetRequest{Id: t.Pet.ID.String()}) + + assert.Nil(t.T(), err) + assert.Equal(t.T(), want, actual) +} + +func (t *PetServiceTest) TestFindAllSuccess() { + + want := &proto.FindAllPetResponse{Pets: t.createPetsDto(t.Pets, t.ImageUrlsList)} + + var petsIn []*pet.Pet + + repo := &mock.RepositoryMock{} + repo.On("FindAll", petsIn).Return(&t.Pets, nil) + + imgSrv := new(img_mock.ServiceMock) + for i, pet := range t.Pets { + imgSrv.On("FindByPetId", pet.ID.String()).Return(t.ImagesList[i], nil) + } + + srv := NewService(repo, imgSrv) + + actual, err := srv.FindAll(context.Background(), &proto.FindAllPetRequest{}) + + assert.Nil(t.T(), err) + assert.Equal(t.T(), want, actual) +} + +func (t *PetServiceTest) TestFindOneNotFound() { + repo := &mock.RepositoryMock{} + repo.On("FindOne", t.Pet.ID.String(), &pet.Pet{}).Return(nil, errors.New("Not found pet")) + imgSrv := new(img_mock.ServiceMock) + imgSrv.On("FindByPetId", t.Pet.ID.String()).Return(nil, nil) + + srv := NewService(repo, imgSrv) + actual, err := srv.FindOne(context.Background(), &proto.FindOnePetRequest{Id: t.Pet.ID.String()}) + + st, ok := status.FromError(err) + + assert.True(t.T(), ok) + assert.Nil(t.T(), actual) + assert.Equal(t.T(), codes.NotFound, st.Code()) +} + +func createPets() []*pet.Pet { + var result []*pet.Pet + + for i := 0; i < rand.Intn(4)+1; i++ { + r := &pet.Pet{ + Base: model.Base{ + ID: uuid.New(), + CreatedAt: time.Time{}, + UpdatedAt: time.Time{}, + DeletedAt: gorm.DeletedAt{}, + }, + Type: faker.Word(), + Species: faker.Word(), + Name: faker.Name(), + Birthdate: faker.Word(), + Gender: petConst.Gender(rand.Intn(1) + 1), + Habit: faker.Paragraph(), + Caption: faker.Paragraph(), + Status: petConst.Status(rand.Intn(1) + 1), + IsSterile: true, + IsVaccinated: true, + IsVisible: true, + IsClubPet: true, + Background: faker.Paragraph(), + Address: faker.Paragraph(), + Contact: faker.Paragraph(), + } + result = append(result, r) + } + + return result +} + +func (t *PetServiceTest) createPetsDto(in []*pet.Pet, imageUrlsList [][]string) []*proto.Pet { + var result []*proto.Pet + + for i, p := range in { + r := &proto.Pet{ + Id: p.ID.String(), + Type: p.Type, + Species: p.Species, + Name: p.Name, + Birthdate: p.Birthdate, + Gender: proto.Gender(p.Gender), + Habit: p.Habit, + Caption: p.Caption, + Status: proto.PetStatus(p.Status), + ImageUrls: imageUrlsList[i], + IsSterile: p.IsSterile, + IsVaccinated: p.IsVaccinated, + IsVisible: p.IsVisible, + IsClubPet: p.IsClubPet, + Background: p.Background, + Address: p.Address, + Contact: p.Contact, + } + + result = append(result, r) + } + + return result +} + +func (t *PetServiceTest) TestCreateSuccess() { + want := &proto.CreatePetResponse{Pet: t.PetDto} + want.Pet.ImageUrls = []string{} // when pet is first created, it has no images + + repo := &mock.RepositoryMock{} + + in := &pet.Pet{ + Type: t.Pet.Type, + Species: t.Pet.Species, + Name: t.Pet.Name, + Birthdate: t.Pet.Birthdate, + Gender: t.Pet.Gender, + Habit: t.Pet.Habit, + Caption: t.Pet.Caption, + Status: t.Pet.Status, + IsSterile: t.Pet.IsSterile, + IsVaccinated: t.Pet.IsVaccinated, + IsVisible: t.Pet.IsVisible, + IsClubPet: t.Pet.IsClubPet, + Background: t.Pet.Background, + Address: t.Pet.Address, + Contact: t.Pet.Contact, + } + + repo.On("Create", in).Return(t.Pet, nil) + imgSrv := new(img_mock.ServiceMock) + + srv := NewService(repo, imgSrv) + + actual, err := srv.Create(context.Background(), t.CreatePetReqMock) + + assert.Nil(t.T(), err) + assert.Equal(t.T(), want, actual) +} + +func (t *PetServiceTest) TestCreateInternalErr() { + repo := &mock.RepositoryMock{} + + in := &pet.Pet{ + Type: t.Pet.Type, + Species: t.Pet.Species, + Name: t.Pet.Name, + Birthdate: t.Pet.Birthdate, + Gender: t.Pet.Gender, + Habit: t.Pet.Habit, + Caption: t.Pet.Caption, + Status: t.Pet.Status, + IsSterile: t.Pet.IsSterile, + IsVaccinated: t.Pet.IsVaccinated, + IsVisible: t.Pet.IsVisible, + IsClubPet: t.Pet.IsClubPet, + Background: t.Pet.Background, + Address: t.Pet.Address, + Contact: t.Pet.Contact, + } + + repo.On("Create", in).Return(nil, errors.New("something wrong")) + imgSrv := new(img_mock.ServiceMock) + + srv := NewService(repo, imgSrv) + + actual, err := srv.Create(context.Background(), t.CreatePetReqMock) + + st, ok := status.FromError(err) + + assert.True(t.T(), ok) + assert.Nil(t.T(), actual) + assert.Equal(t.T(), codes.Internal, st.Code()) +} + +func (t *PetServiceTest) TestUpdateSuccess() { + want := &proto.UpdatePetResponse{Pet: t.PetDto} + + repo := &mock.RepositoryMock{} + repo.On("Update", t.Pet.ID.String(), t.UpdatePet).Return(t.Pet, nil) + imgSrv := new(img_mock.ServiceMock) + imgSrv.On("FindByPetId", t.Pet.ID.String()).Return(t.Images, nil) + + srv := NewService(repo, imgSrv) + actual, err := srv.Update(context.Background(), t.UpdatePetReqMock) + + assert.Nil(t.T(), err) + assert.Equal(t.T(), want, actual) +} + +func (t *PetServiceTest) TestUpdateNotFound() { + repo := &mock.RepositoryMock{} + repo.On("Update", t.Pet.ID.String(), t.UpdatePet).Return(nil, errors.New("Not found pet")) + imgSrv := new(img_mock.ServiceMock) + imgSrv.On("FindByPetId", t.Pet.ID.String()).Return(t.Images, nil) + + srv := NewService(repo, imgSrv) + actual, err := srv.Update(context.Background(), t.UpdatePetReqMock) + + st, ok := status.FromError(err) + + assert.True(t.T(), ok) + assert.Nil(t.T(), actual) + assert.Equal(t.T(), codes.NotFound, st.Code()) +} + +func (t *PetServiceTest) TestChangeViewSuccess() { + want := &proto.ChangeViewPetResponse{Success: true} + + repo := &mock.RepositoryMock{} + repo.On("FindOne", t.Pet.ID.String(), &pet.Pet{}).Return(t.Pet, nil) + repo.On("Update", t.Pet.ID.String(), t.ChangeViewPet).Return(t.ChangeViewPet, nil) + imgSrv := new(img_mock.ServiceMock) + imgSrv.On("FindByPetId", t.Pet.ID.String()).Return(t.Images, nil) + + srv := NewService(repo, imgSrv) + actual, err := srv.ChangeView(context.Background(), t.ChangeViewPetReqMock) + + assert.Nil(t.T(), err) + assert.Equal(t.T(), want, actual) +} + +func (t *PetServiceTest) TestChangeViewNotFound() { + repo := &mock.RepositoryMock{} + repo.On("FindOne", t.Pet.ID.String(), &pet.Pet{}).Return(nil, errors.New("Not found pet")) + repo.On("Update", t.Pet.ID.String(), t.UpdatePet).Return(nil, errors.New("Not found pet")) + imgSrv := new(img_mock.ServiceMock) + + srv := NewService(repo, imgSrv) + actual, err := srv.ChangeView(context.Background(), t.ChangeViewPetReqMock) + + st, ok := status.FromError(err) + + assert.True(t.T(), ok) + assert.Nil(t.T(), actual) + assert.Equal(t.T(), codes.NotFound, st.Code()) +} + +func (t *PetServiceTest) TestAdoptBySuccess() { + want := &proto.AdoptPetResponse{Success: true} + repo := &mock.RepositoryMock{} + + repo.On("FindOne", t.AdoptByReq.PetId, &pet.Pet{}).Return(t.Pet, nil) + repo.On("Update", t.AdoptByReq.PetId, t.ChangeAdoptBy).Return(t.ChangeAdoptBy, nil) + + imgSrv := new(img_mock.ServiceMock) + imgSrv.On("FindByPetId", t.Pet.ID.String()).Return(t.Images, nil) + + srv := NewService(repo, imgSrv) + + actual, err := srv.AdoptPet(context.Background(), t.AdoptByReq) + + assert.Nil(t.T(), err) + assert.Equal(t.T(), want, actual) +} + +func (t *PetServiceTest) TestAdoptByPetNotFound() { + wantError := status.Error(codes.NotFound, "pet not found") + repo := &mock.RepositoryMock{} + + repo.On("FindOne", t.AdoptByReq.PetId, &pet.Pet{}).Return(nil, wantError) + + imgSrv := new(img_mock.ServiceMock) + srv := NewService(repo, imgSrv) + + actual, err := srv.AdoptPet(context.Background(), t.AdoptByReq) + + assert.NotNil(t.T(), err) + assert.Equal(t.T(), wantError, err) + assert.Nil(t.T(), actual) + + repo.AssertNotCalled(t.T(), "Update", t.AdoptByReq.PetId, t.ChangeAdoptBy) +} + +func (t *PetServiceTest) TestAdoptByUpdateError() { + wantError := status.Error(codes.NotFound, "pet not found") + repo := &mock.RepositoryMock{} + + repo.On("FindOne", t.AdoptByReq.PetId, &pet.Pet{}).Return(t.Pet, nil) + repo.On("Update", t.AdoptByReq.PetId, t.ChangeAdoptBy).Return(nil, errors.New("update error")) + + imgSrv := new(img_mock.ServiceMock) + imgSrv.On("FindByPetId", t.Pet.ID.String()).Return(nil, errors.New("pet not found")) + + srv := NewService(repo, imgSrv) + + actual, err := srv.AdoptPet(context.Background(), t.AdoptByReq) + + assert.NotNil(t.T(), err) + assert.Equal(t.T(), wantError, err) + assert.Nil(t.T(), actual) +} diff --git a/src/config/config.go b/src/config/config.go index c526550..fc6876a 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -19,9 +19,14 @@ type App struct { Debug bool `mapstructure:"debug"` } +type Service struct { + File string `mapstructure:"file"` +} + type Config struct { App App `mapstructure:"app"` Database Database `mapstructure:"database"` + Service Service `mapstructure:"service"` } func LoadConfig() (config *Config, err error) { diff --git a/src/constant/pet/pet.constant.go b/src/constant/pet/pet.constant.go index 34f4dc4..577bed6 100644 --- a/src/constant/pet/pet.constant.go +++ b/src/constant/pet/pet.constant.go @@ -3,13 +3,13 @@ package pet type Gender int const ( - MALE = 1 - FEMALE = 2 + MALE = 0 + FEMALE = 1 ) type Status int const ( - ADOPTED = 1 - FINDHOME = 2 + ADOPTED = 0 + FINDHOME = 1 ) diff --git a/src/database/postgresql.connection.go b/src/database/postgresql.connection.go index 098b96b..8369137 100644 --- a/src/database/postgresql.connection.go +++ b/src/database/postgresql.connection.go @@ -4,6 +4,9 @@ import ( "fmt" "strconv" + "github.com/isd-sgcu/johnjud-backend/src/app/model/like" + "github.com/isd-sgcu/johnjud-backend/src/app/model/pet" + "github.com/isd-sgcu/johnjud-backend/src/app/model/user" "github.com/isd-sgcu/johnjud-backend/src/config" "gorm.io/driver/postgres" "gorm.io/gorm" @@ -24,5 +27,10 @@ func InitPostgresDatabase(conf *config.Database, isDebug bool) (db *gorm.DB, err return nil, err } + err = db.AutoMigrate(&user.User{}, &like.Like{}, &pet.Pet{}) + if err != nil { + return nil, err + } + return } diff --git a/src/main.go b/src/main.go index 70b6c9a..e23b1b4 100644 --- a/src/main.go +++ b/src/main.go @@ -1,5 +1,165 @@ package main +import ( + "context" + "fmt" + "net" + "os" + "os/signal" + "sync" + "syscall" + "time" + + likeRepo "github.com/isd-sgcu/johnjud-backend/src/app/repository/like" + petRepo "github.com/isd-sgcu/johnjud-backend/src/app/repository/pet" + imageSrv "github.com/isd-sgcu/johnjud-backend/src/app/service/image" + likeSrv "github.com/isd-sgcu/johnjud-backend/src/app/service/like" + petSrv "github.com/isd-sgcu/johnjud-backend/src/app/service/pet" + "github.com/isd-sgcu/johnjud-backend/src/config" + "github.com/isd-sgcu/johnjud-backend/src/database" + likePb "github.com/isd-sgcu/johnjud-go-proto/johnjud/backend/like/v1" + petPb "github.com/isd-sgcu/johnjud-go-proto/johnjud/backend/pet/v1" + imagePb "github.com/isd-sgcu/johnjud-go-proto/johnjud/file/image/v1" + "github.com/rs/zerolog/log" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/health" + "google.golang.org/grpc/health/grpc_health_v1" + "google.golang.org/grpc/reflection" +) + +type operation func(ctx context.Context) error + +func gracefulShutdown(ctx context.Context, timeout time.Duration, ops map[string]operation) <-chan struct{} { + wait := make(chan struct{}) + go func() { + s := make(chan os.Signal, 1) + + signal.Notify(s, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) + sig := <-s + + log.Info(). + Str("service", "graceful shutdown"). + Msgf("got signal \"%v\" shutting down service", sig) + + timeoutFunc := time.AfterFunc(timeout, func() { + log.Error(). + Str("service", "graceful shutdown"). + Msgf("timeout %v ms has been elapsed, force exit", timeout.Milliseconds()) + os.Exit(0) + }) + + defer timeoutFunc.Stop() + + var wg sync.WaitGroup + + for key, op := range ops { + wg.Add(1) + innerOp := op + innerKey := key + go func() { + defer wg.Done() + + log.Info(). + Str("service", "graceful shutdown"). + Msgf("cleaning up: %v", innerKey) + if err := innerOp(ctx); err != nil { + log.Error(). + Str("service", "graceful shutdown"). + Err(err). + Msgf("%v: clean up failed: %v", innerKey, err.Error()) + return + } + + log.Info(). + Str("service", "graceful shutdown"). + Msgf("%v was shutdown gracefully", innerKey) + }() + } + + wg.Wait() + close(wait) + }() + + return wait +} + func main() { - // Code + conf, err := config.LoadConfig() + if err != nil { + log.Fatal(). + Err(err). + Str("service", "backend"). + Msg("Failed to load config") + } + + db, err := database.InitPostgresDatabase(&conf.Database, conf.App.Debug) + if err != nil { + log.Fatal(). + Err(err). + Str("service", "backend"). + Msg("Failed to init postgres connection") + } + + fileConn, err := grpc.Dial(conf.Service.File, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + log.Fatal(). + Err(err). + Str("service", "johnjud-file"). + Msg("Cannot connect to service") + } + + lis, err := net.Listen("tcp", fmt.Sprintf(":%v", conf.App.Port)) + if err != nil { + log.Fatal(). + Err(err). + Str("service", "backend"). + Msg("Failed to start service") + } + + grpcServer := grpc.NewServer() + + likeRepo := likeRepo.NewRepository(db) + likeService := likeSrv.NewService(likeRepo) + + imageClient := imagePb.NewImageServiceClient(fileConn) + imageService := imageSrv.NewService(imageClient) + petRepo := petRepo.NewRepository(db) + petService := petSrv.NewService(petRepo, imageService) + + grpc_health_v1.RegisterHealthServer(grpcServer, health.NewServer()) + likePb.RegisterLikeServiceServer(grpcServer, likeService) + petPb.RegisterPetServiceServer(grpcServer, petService) + + reflection.Register(grpcServer) + go func() { + log.Info(). + Str("service", "backend"). + Msgf("JohnJud backend starting at port %v", conf.App.Port) + + if err := grpcServer.Serve(lis); err != nil { + log.Fatal(). + Err(err). + Str("service", "backend"). + Msg("Failed to start service") + } + }() + + wait := gracefulShutdown(context.Background(), 2*time.Second, map[string]operation{ + "server": func(ctx context.Context) error { + grpcServer.GracefulStop() + return nil + }, + }) + + <-wait + + grpcServer.GracefulStop() + log.Info(). + Str("service", "backend"). + Msg("Closing the listener") + lis.Close() + log.Info(). + Str("service", "backend"). + Msg("End the program") } diff --git a/src/mocks/image/image.mock.go b/src/mocks/image/image.mock.go new file mode 100644 index 0000000..19aba0d --- /dev/null +++ b/src/mocks/image/image.mock.go @@ -0,0 +1,57 @@ +package image + +import ( + "context" + + proto "github.com/isd-sgcu/johnjud-go-proto/johnjud/file/image/v1" + "github.com/stretchr/testify/mock" + "google.golang.org/grpc" +) + +type ClientMock struct { + mock.Mock +} + +func (c *ClientMock) Upload(_ context.Context, in *proto.UploadImageRequest, _ ...grpc.CallOption) (res *proto.UploadImageResponse, err error) { + args := c.Called(in) + + if args.Get(0) != nil { + res = args.Get(0).(*proto.UploadImageResponse) + } + + return res, args.Error(1) +} + +func (c *ClientMock) FindByPetId(_ context.Context, in *proto.FindImageByPetIdRequest, _ ...grpc.CallOption) (res *proto.FindImageByPetIdResponse, err error) { + args := c.Called(in) + + if args.Get(0) != nil { + res = args.Get(0).(*proto.FindImageByPetIdResponse) + } + + return res, args.Error(1) +} + +func (c *ClientMock) Delete(_ context.Context, in *proto.DeleteImageRequest, _ ...grpc.CallOption) (res *proto.DeleteImageResponse, err error) { + args := c.Called(in) + + if args.Get(0) != nil { + res = args.Get(0).(*proto.DeleteImageResponse) + } + + return res, args.Error(1) +} + +type ServiceMock struct { + mock.Mock +} + +func (c *ServiceMock) FindByPetId(petId string) (res []*proto.Image, err error) { + args := c.Called(petId) + + if args.Get(0) != nil { + res = args.Get(0).([]*proto.Image) + } + + return res, args.Error(1) +} diff --git a/src/mocks/pet/pet.mock.go b/src/mocks/pet/pet.mock.go new file mode 100644 index 0000000..d1f5d87 --- /dev/null +++ b/src/mocks/pet/pet.mock.go @@ -0,0 +1,55 @@ +package pet + +import ( + "github.com/isd-sgcu/johnjud-backend/src/app/model/pet" + "github.com/stretchr/testify/mock" +) + +type RepositoryMock struct { + mock.Mock +} + +func (r *RepositoryMock) FindOne(id string, result *pet.Pet) error { + args := r.Called(id, result) + + if args.Get(0) != nil { + *result = *args.Get(0).(*pet.Pet) + } + + return args.Error(1) +} + +func (r *RepositoryMock) Create(in *pet.Pet) error { + args := r.Called(in) + + if args.Get(0) != nil { + *in = *args.Get(0).(*pet.Pet) + } + + return args.Error(1) +} + +func (r *RepositoryMock) FindAll(result *[]*pet.Pet) error { + args := r.Called(*result) + + if args.Get(0) != nil { + *result = *args.Get(0).(*[]*pet.Pet) + } + + return args.Error(1) +} + +func (r *RepositoryMock) Update(id string, result *pet.Pet) error { + args := r.Called(id, result) + + if args.Get(0) != nil { + *result = *args.Get(0).(*pet.Pet) + } + + return args.Error(1) +} + +func (r *RepositoryMock) Delete(id string) error { + args := r.Called(id) + return args.Error(0) +}