diff --git a/Makefile b/Makefile index b381585..77c6479 100644 --- a/Makefile +++ b/Makefile @@ -3,9 +3,16 @@ af = -f deploy/docker/compose-api-test.yaml COVER_FILE ?= coverage.out REPORT_FILE ?= coverage.html +# Run in docker + +init: ## Initialize environement variables + @if [ ! -f ./deploy/docker/.env ]; then \ + cp ./deploy/docker/.env.sample ./deploy/docker/.env; \ + echo "Adjust configuration in ./deploy/docker/.env"; \ + fi; build: ## Build docker containers docker compose $(cf) build -up: ## Start docker containers +up: init ## Start docker containers docker compose $(cf) up -d --remove-orphans down: ## Stop docker containers docker compose $(cf) down @@ -13,11 +20,24 @@ rebuild: ## Rebuild and start docker containers @make down @make build @make up -api-test: ## Build and start docker services and run API testing on them +restart: ## Restart docker containers + docker compose $(cf) restart + +# Hurl API testing in docker + +apitestbuild: ## Build containers for API testing docker compose $(af) build - docker compose $(af) -p mdtest up -d - docker run --rm -v .\test\:/test --net md-ship-public ghcr.io/orange-opensource/hurl:latest --test --color --variables-file=/test/api/docker-vars /test/api/customer.hurl - docker compose $(af) -p mdtest down +apitestup: ## Start containers for API testing + docker compose $(af) up -d --remove-orphans +apitestdown: ## Stop containers for API testing + docker compose $(af) down +apitestrun: ## Run Hurl testing scripts in docker container and in mutual network + docker run --rm -v ./test/:/test --net md-ship-public ghcr.io/orange-opensource/hurl:latest --test --color --variables-file=/test/api/docker-vars /test/api/customer.hurl +apitest: ## Build and start docker services and run API testing on them + @make apitestbuild + @make apitestup + @make apitestrun + @make apitestdown ## Local development @@ -58,7 +78,11 @@ hurl: ## Run hurl API testing on localhost installation hurl --variables-file=.\test\api\local-vars .\test\api\customer.hurl .PHONY: \ - api-test \ + apitest \ + apitestbuild \ + apitestdown \ + apitestrun \ + apitestup \ build \ check-coverage-threshold \ clean \ @@ -74,4 +98,4 @@ hurl: ## Run hurl API testing on localhost installation test \ test-with-coverage \ tools \ - up \ + up \ \ No newline at end of file diff --git a/README.md b/README.md index 9b46d98..36a6efe 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Before setting up the project, you need to have a Docker infrastructure installe First, start the ['Parcel Locker'](https://github.com/magicdelivery/parcel_locker) microservice, followed by the 'Shipping' microservice, as the latter interacts with the former through its network. ```sh +mkdir magicdelivery && cd magicdelivery git clone https://github.com/magicdelivery/parcel_locker.git cd parcel_locker make up @@ -39,4 +40,12 @@ Communication with the 'Parcel Locker' microservice is carried out via synchrono The [testify](https://github.com/stretchr/testify) library is used for unit testing the logic, allowing for the generation of mocks for out-of-process dependencies. -E2E testing of the API is implemented using the [hurl](https://hurl.dev/) command line tool. You can run the tests with the command `make api-test`, which will build and start the necessary containers and then execute the hurl test requests. +E2E testing of the API is implemented using the [hurl](https://hurl.dev/) command line tool. You can run the tests with the command `make testapi`, which will build and start the necessary containers and then execute the hurl test requests. + +## Contribution + +Since this is a learning project focused on microservices with Golang, I would be incredibly grateful for any advice or ideas for improvement! + +## Licence + +MIT diff --git a/deploy/docker/compose-api-test.yaml b/deploy/docker/compose-api-test.yaml index 7738498..cd8c0f0 100644 --- a/deploy/docker/compose-api-test.yaml +++ b/deploy/docker/compose-api-test.yaml @@ -21,7 +21,7 @@ services: extends: file: compose.yaml service: ship-redis-populator - entrypoint: ["/redis-populator/populate.sh", "/redis-populator/api-test-data.txt"] + entrypoint: ["sh", "/redis-populator/populate.sh", "/redis-populator/api-test-data.txt"] depends_on: - ship-redis-storage diff --git a/deploy/docker/compose.yaml b/deploy/docker/compose.yaml index 7ec3abb..5ef08a3 100644 --- a/deploy/docker/compose.yaml +++ b/deploy/docker/compose.yaml @@ -59,7 +59,7 @@ services: container_name: md-ship-redis-populator depends_on: - ship-redis-storage - entrypoint: ["/redis-populator/populate.sh", "/redis-populator/data.txt"] + entrypoint: ["bash", "/redis-populator/populate.sh", "/redis-populator/data.txt"] env_file: - ./.env image: redis diff --git a/internal/app/action/deleting.go b/internal/app/action/deleting.go index 29af0d4..20a0ede 100644 --- a/internal/app/action/deleting.go +++ b/internal/app/action/deleting.go @@ -9,17 +9,19 @@ import ( "shipping/internal/domain/repository" ) -func DeleteCustomer(ctx *gin.Context, deleter repository.CustomerDeleter, loader repository.CustomerLoader) { - id := ctx.Params.ByName("id") - if customer, _ := loader.LoadCustomerById(ctx, id); customer == nil { - ctx.JSON(http.StatusNotFound, gin.H{"id": id, "deleted": false, "message": fmt.Sprintf("Customer info not found by id: %s", id)}) - return - } +func DeleteCustomer(deleter repository.CustomerDeleter, loader repository.CustomerLoader) gin.HandlerFunc { + return func(ctx *gin.Context) { + id := ctx.Params.ByName("id") + if customer, _ := loader.LoadCustomerById(ctx, id); customer == nil { + ctx.JSON(http.StatusNotFound, gin.H{"id": id, "deleted": false, "message": fmt.Sprintf("Customer info not found by id: %s", id)}) + return + } - if err := deleter.DeleteCustomer(ctx, id); err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"id": id, "message": err.Error()}) - return - } + if err := deleter.DeleteCustomer(ctx, id); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"id": id, "message": err.Error()}) + return + } - ctx.JSON(http.StatusOK, gin.H{"id": id, "deleted": true, "message": "Customer info successfully deleted"}) + ctx.JSON(http.StatusOK, gin.H{"id": id, "deleted": true, "message": "Customer info successfully deleted"}) + } } diff --git a/internal/app/action/health.go b/internal/app/action/health.go index 2ae6c3e..390e1b4 100644 --- a/internal/app/action/health.go +++ b/internal/app/action/health.go @@ -6,8 +6,10 @@ import ( "github.com/gin-gonic/gin" ) -func Ping(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{ - "message": "pong", - }) +func Ping() gin.HandlerFunc { + return func(ctx *gin.Context) { + ctx.JSON(http.StatusOK, gin.H{ + "message": "pong", + }) + } } diff --git a/internal/app/action/loading.go b/internal/app/action/loading.go index 6cd3596..38676be 100644 --- a/internal/app/action/loading.go +++ b/internal/app/action/loading.go @@ -11,53 +11,58 @@ import ( "github.com/gin-gonic/gin" ) -func LoadAllCustomers(ctx *gin.Context, loader repository.CustomerLoader) { - if customers, err := loader.LoadAllCustomers(ctx); err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()}) - } else { - response := gin.H{"customers": customers} - ctx.JSON(http.StatusOK, response) +func LoadAllCustomers(loader repository.CustomerLoader) gin.HandlerFunc { + return func(ctx *gin.Context) { + if customers, err := loader.LoadAllCustomers(ctx); err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()}) + } else { + response := gin.H{"customers": customers} + ctx.JSON(http.StatusOK, response) + } } } -func LoadCustomerById(ctx *gin.Context, loader repository.CustomerLoader) { - id := ctx.Params.ByName("id") - customer, err := loader.LoadCustomerById(ctx, id) - if err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"id": id, "message": err.Error()}) - } else if customer == nil { - ctx.JSON(http.StatusNotFound, gin.H{"id": id, "message": fmt.Sprintf("Customer not found by id: %s", id)}) - } else { - ctx.JSON(http.StatusOK, gin.H{"customer": customer}) +func LoadCustomerById(loader repository.CustomerLoader) gin.HandlerFunc { + return func(ctx *gin.Context) { + id := ctx.Params.ByName("id") + customer, err := loader.LoadCustomerById(ctx, id) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"id": id, "message": err.Error()}) + } else if customer == nil { + ctx.JSON(http.StatusNotFound, gin.H{"id": id, "message": fmt.Sprintf("Customer not found by id: %s", id)}) + } else { + ctx.JSON(http.StatusOK, gin.H{"customer": customer}) + } } } func FindParcelLockersByCustomerId( - ctx *gin.Context, loader repository.CustomerLoader, plClient parcel_locker.ParcelLockerClient, -) { - id := ctx.Params.ByName("id") - distanceStr := ctx.DefaultQuery("distance", "10") - distance, err := strconv.ParseFloat(distanceStr, 64) - if err != nil { - ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid distance value"}) - // c.Abort() - return - } - - customer, err := loader.LoadCustomerById(ctx, id) - if err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"id": id, "message": err.Error()}) - } else if customer == nil { - ctx.JSON(http.StatusNotFound, gin.H{"id": id, "message": fmt.Sprintf("Customer not found by id: %s", id)}) - } else { - parcel_lockers, err := plClient.FindParcelLockersNear(ctx, customer, distance) +) gin.HandlerFunc { + return func(ctx *gin.Context) { + id := ctx.Params.ByName("id") + distanceStr := ctx.DefaultQuery("distance", "10") + distance, err := strconv.ParseFloat(distanceStr, 64) if err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"id": id, "message": err.Error()}) + ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid distance value"}) + // c.Abort() return } - ctx.JSON(http.StatusOK, gin.H{"parcel_lockers": parcel_lockers}) + customer, err := loader.LoadCustomerById(ctx, id) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"id": id, "message": err.Error()}) + } else if customer == nil { + ctx.JSON(http.StatusNotFound, gin.H{"id": id, "message": fmt.Sprintf("Customer not found by id: %s", id)}) + } else { + parcel_lockers, err := plClient.FindParcelLockersNear(ctx, customer, distance) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"id": id, "message": err.Error()}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"parcel_lockers": parcel_lockers}) + } } } diff --git a/internal/app/action/saving.go b/internal/app/action/saving.go index c9a17f1..8154d1c 100644 --- a/internal/app/action/saving.go +++ b/internal/app/action/saving.go @@ -9,18 +9,20 @@ import ( "github.com/gin-gonic/gin" ) -func SaveCustomer(ctx *gin.Context, saver repository.CustomerSaver) { - var customer model.Customer = model.Customer{} - if err := ctx.BindJSON(&customer); err != nil { - // ctx.BindJSON sets status code 400, thus the next code definition does not effect - ctx.JSON(http.StatusInternalServerError, map[string]any{"customer": customer, "created": false, "message": err.Error()}) - return - } +func SaveCustomer(saver repository.CustomerSaver) gin.HandlerFunc { + return func(ctx *gin.Context) { + var customer model.Customer = model.Customer{} + if err := ctx.BindJSON(&customer); err != nil { + // ctx.BindJSON sets status code 400, thus the next code definition does not effect + ctx.JSON(http.StatusInternalServerError, map[string]any{"customer": customer, "created": false, "message": err.Error()}) + return + } - if err := saver.SaveCustomer(ctx, customer); err != nil { - ctx.JSON(http.StatusInternalServerError, map[string]any{"customer": customer, "created": false, "message": err.Error()}) - return - } + if err := saver.SaveCustomer(ctx, customer); err != nil { + ctx.JSON(http.StatusInternalServerError, map[string]any{"customer": customer, "created": false, "message": err.Error()}) + return + } - ctx.JSON(http.StatusCreated, gin.H{"customer": customer, "created": true, "message": "Customer info created successfully"}) + ctx.JSON(http.StatusCreated, gin.H{"customer": customer, "created": true, "message": "Customer info created successfully"}) + } } diff --git a/internal/app/route/router.go b/internal/app/route/router.go index 02f495e..94dfffe 100644 --- a/internal/app/route/router.go +++ b/internal/app/route/router.go @@ -14,11 +14,11 @@ func SetupRouter(config *config.Config) *gin.Engine { loader, saver, deleter := storage.DefaultServices(&config.RedisStorage) plClient := parcel_locker.NewParcelLockerClient(config) - r.GET("/ping", func(c *gin.Context) { action.Ping(c) }) - r.GET("/customers", func(c *gin.Context) { action.LoadAllCustomers(c, loader) }) - r.GET("/customer/:id", func(c *gin.Context) { action.LoadCustomerById(c, loader) }) - r.GET("/customer-parcel-lockers/:id", func(c *gin.Context) { action.FindParcelLockersByCustomerId(c, loader, plClient) }) - r.POST("/customer", func(c *gin.Context) { action.SaveCustomer(c, saver) }) - r.DELETE("/customer/:id", func(c *gin.Context) { action.DeleteCustomer(c, deleter, loader) }) + r.GET("/ping", action.Ping()) + r.GET("/customers", action.LoadAllCustomers(loader)) + r.GET("/customer/:id", action.LoadCustomerById(loader)) + r.GET("/customer-parcel-lockers/:id", action.FindParcelLockersByCustomerId(loader, plClient)) + r.POST("/customer", action.SaveCustomer(saver)) + r.DELETE("/customer/:id", action.DeleteCustomer(deleter, loader)) return r }