Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix project #2

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@

.env*
.idea/
133 changes: 129 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,134 @@
# Schwarz IT Code Review Repository
This project provides a simple coupon management service that allows clients to retrieve valid coupons and handle invalid coupon codes. The service exposes a REST API where you can POST coupon codes and get the results, which include valid coupons and any invalid codes. It also has an in-memory database to store created coupons. The service, once raised, will be up for up to one year.

This repository is meant to be used in the onboarding process of Go developers
applying to Schwarz IT.
## Project Structure

Code smells and security issues are made on purpose. All the code to be reviewed is
in the [review](review) directory.
```
coupon-service/
├── cmd/
│ └── coupon_service/
│ └── main.go # Main entry point for the coupon service
├── internal/
│ └── api/
│ ├── handler.go # HTTP handler and API logic
│ └── service.go # Service logic for coupon retrieval
├── entity/
│ └── coupon.go # Defines the Coupon and CouponRequest structs
├── Dockerfile # Dockerfile to build the service
├── go.mod # Go modules file
├── go.sum # Go sum file for dependency management
└── README.md # Project documentation
```

## API Endpoints

### POST `/coupons`

This endpoint accepts a JSON body containing an array of coupon codes and returns a response with the valid coupons and any codes that were not found.

#### Request Body

```json
{
"codes": ["valid1", "valid2", "invalid1"]
}
```

#### Response Body

```json
{
"data": {
"coupons": [
{
"code": "valid1",
"discount": 10,
"min_basket_value": 30
},
{
"code": "valid2",
"discount": 15,
"min_basket_value": 50
}
],
}
}
```

### Error Handling

If the request body is malformed or invalid, the API will respond with a `400 Bad Request` status.

## Running the Project

To run the project locally, ensure that Go is installed on your machine and the dependencies are resolved.

1. Clone the repository:
```bash
git clone https://github.com/SchwarzIT/go-code-review
cd go-code-review/review
```

2. Install dependencies:
```bash
go mod tidy
```

3. Run the service:
```bash
go run cmd/coupon_service/main.go
```

The service will be available at `http://localhost:8080`.

---

## Using Docker to Set Up the Environment

This project includes a `Dockerfile` to simplify building and running the service in a containerized environment. Below are the steps to build and run the service using Docker.

### Steps

1. **Build the Docker image**

First, build the Docker image from the `Dockerfile`. You can do this by running the following command in the root of the project:

```bash
docker build -t coupon-service .
```

This will use the `golang:1.18-alpine` image to build the Go application, and then copy the compiled binary to a minimal Alpine-based container.

2. **Run the Docker container**

Once the Docker image is built, you can run the container with the following command:

```bash
docker run -p 8080:8080 -e API_PORT=8080 -e API_HOST=0.0.0.0 -e SKIP_CPU_CHECK=1 coupon-service
```

This will start the application inside a container and map port `8080` on your local machine to port `8080` inside the container, so you can access the API at `http://localhost:8080`.

3. **Environment variables**

- **`API_PORT`**
Specifies the port the application will listen on. By default, the application listens on port `8080`.

- **`API_HOST`**
Defines the host or IP address where the application will be accessible. Set it to `0.0.0.0` to ensure it listens on all interfaces.

- **`SKIP_CPU_CHECK`**
If set to `1`, this variable skips any CPU-related checks during the application startup. This can be useful in specific environments where CPU checks are not necessary.

---

## Running Tests

To run tests locally, use the following command:

```bash
go test ./...
```

This will execute all the unit tests in the project.

1 change: 1 addition & 0 deletions review/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.env*
28 changes: 22 additions & 6 deletions review/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,14 +1,30 @@
# build stage
FROM golang:latest AS builder
FROM golang:1.18-alpine AS builder

RUN apk add git gcc libc-dev
RUN apk --no-cache add git gcc libc-dev

WORKDIR /go/src/coupon-service

COPY go.mod go.sum ./

RUN go mod tidy

COPY . .

WORKDIR /go/src/coupon-service/cmd/export
RUN go build -o main .
WORKDIR /go/src/coupon-service/cmd/coupon_service/

RUN go build -o coupon-service .

FROM alpine:3.20

RUN apk --no-cache add ca-certificates

WORKDIR /root/

COPY --from=builder /go/src/coupon-service/cmd/coupon_service .

ENTRYPOINT ["./coupon-service"]

ENTRYPOINT ./main
EXPOSE 8080

CMD ["./coupon-service"]

Binary file removed review/cmd/coupon_service/main
Binary file not shown.
28 changes: 19 additions & 9 deletions review/cmd/coupon_service/main.go
Original file line number Diff line number Diff line change
@@ -1,25 +1,35 @@
package main

import (
"fmt"
"time"

"coupon_service/internal/api"
"coupon_service/internal/common"
"coupon_service/internal/config"
"coupon_service/internal/repository/memdb"
memdb "coupon_service/internal/repository"
"coupon_service/internal/service"
"fmt"
"time"
)

var (
cfg = config.New()
repo = memdb.New()
)

func init() {
common.ValidateCPUs()
}

func main() {
svc := service.New(repo)
本 := api.New(cfg.API, svc)
本.Start()
fmt.Println("Starting Coupon service server")
<-time.After(1 * time.Hour * 24 * 365)
fmt.Println("Coupon service server alive for a year, closing")
本.Close()
api := api.New(cfg.API, svc)
go api.Start()
fmt.Printf("Starting Coupon service server on port: %d\n", cfg.API.Port)

duration := 1 * time.Hour * 24 * 365
expirationDate := time.Now().Add(duration)
fmt.Printf("Coupon service server alive until: %s\n", expirationDate.Format("2006-01-02"))
<-time.After(duration)

api.Close()
}
6 changes: 4 additions & 2 deletions review/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ require (
github.com/brumhard/alligotor v0.3.3
github.com/gin-gonic/gin v1.7.6
github.com/google/uuid v1.3.0
github.com/urfave/negroni v1.0.0
github.com/stretchr/testify v1.7.1
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
Expand All @@ -22,8 +23,9 @@ require (
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/testify v1.7.1 // indirect
github.com/stretchr/objx v0.1.0 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 // indirect
Expand Down
1 change: 1 addition & 0 deletions review/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUA
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
Expand Down
26 changes: 19 additions & 7 deletions review/internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,21 @@ package api

import (
"context"
"coupon_service/internal/service/entity"
"fmt"
"log"
"net/http"
"time"

"github.com/gin-gonic/gin"

"coupon_service/internal/entity"
)

type Service interface {
ApplyCoupon(entity.Basket, string) (*entity.Basket, error)
CreateCoupon(int, string, int) any
GetCoupons([]string) ([]entity.Coupon, error)
ApplyCoupon(*entity.Basket, string) error
CreateCoupon(int, string, int) error
GetCoupons([]string) []entity.Coupon
GetCoupon(string) (*entity.Coupon, error)
}

type Config struct {
Expand All @@ -29,6 +31,8 @@ type API struct {
CFG Config
}

// New creates a new instance of the API, initializes the router, sets up routes, and configures the server.
// It accepts a configuration struct and a service that implements the Service interface.
func New[T Service](cfg Config, svc T) API {
gin.SetMode(gin.ReleaseMode)
r := new(gin.Engine)
Expand All @@ -39,15 +43,16 @@ func New[T Service](cfg Config, svc T) API {
MUX: r,
CFG: cfg,
svc: svc,
}.withServer()
}.withRoutes().withServer()
}

// withServer configures the HTTP server for the API, binding it to the host and port specified in the configuration.
// It starts the server in a separate goroutine and returns the API instance.
func (a API) withServer() API {

ch := make(chan API)
go func() {
a.srv = &http.Server{
Addr: fmt.Sprintf(":%d", a.CFG.Port),
Addr: fmt.Sprintf("%s:%d", a.CFG.Host, a.CFG.Port),
Handler: a.MUX,
}
ch <- a
Expand All @@ -56,6 +61,8 @@ func (a API) withServer() API {
return <-ch
}

// withRoutes defines the API routes for handling requests and returns the API instance.
// It includes endpoints for applying, creating, and retrieving coupons.
func (a API) withRoutes() API {
apiGroup := a.MUX.Group("/api")
apiGroup.POST("/apply", a.Apply)
Expand All @@ -64,12 +71,17 @@ func (a API) withRoutes() API {
return a
}

// Start begins serving HTTP requests on the specified address.
// It listens on the server's configured host and port, logging any critical errors if the server fails to start.
func (a API) Start() {
log.Println(a.srv.Addr)
if err := a.srv.ListenAndServe(); err != nil {
log.Fatal(err)
}
}

// Close gracefully shuts down the HTTP server within a 5-second timeout period.
// This allows ongoing requests to complete before the server stops.
func (a API) Close() {
<-time.After(5 * time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
Expand Down
Loading