Skip to content

Commit

Permalink
Introduce Gin web framework in codebase
Browse files Browse the repository at this point in the history
  • Loading branch information
hthunberg committed Oct 27, 2023
1 parent 2ff1923 commit ed6a996
Show file tree
Hide file tree
Showing 26 changed files with 1,386 additions and 130 deletions.
48 changes: 15 additions & 33 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ GO_ARCH := GOARCH=amd64
all: sqlc fmt lint test integrationtest gosec build

.PHONY: all-tools
all-tools: lint-install sqlc-install gosec-install goimports-install
all-tools: lint-install sqlc-install gosec-install goimports-install mockgen-install

.PHONY: fmt
fmt:
Expand All @@ -29,31 +29,14 @@ run: build
./bin/bank

.PHONY: test
test: fmt
test: fmt mock
$(GO_ARCH) $(CGO_FLAGS) \
go test -race -v -cover -short ./...

#########################
# Docker wormhole pattern
# Testcontainers will automatically detect if it's inside a container and instead of "localhost" will use the default gateway's IP.
#
# However, additional configuration is required if you use volume mapping. The following points need to be considered:
# - The docker socket must be available via a volume mount
# - The 'local' source code directory must be volume mounted at the same path inside the container that Testcontainers runs in,
# so that Testcontainers is able to set up the correct volume mounts for the containers it spawns.
#
# https://docs.docker.com/desktop/extensions-sdk/guides/use-docker-socket-from-backend/
#
# docker run -it --rm -v $PWD:$PWD -w $PWD -v /var/run/docker.sock:/var/run/docker.sock bank-integration-test
# docker run -it --rm -v $PWD:$PWD -w $PWD -v /var/run/docker.sock.raw:/var/run/docker.sock bank-integration-test
#########################
.PHONY: integrationtest-docker-run
integrationtest-docker-run: integrationtest-docker-build
docker run -it --rm --name bank-integration-test \
-v $(PWD):$(PWD) \
-w $(PWD) \
-v /var/run/docker.sock.raw:/var/run/docker.sock \
bank-integration-test
# Server requires a running PostgreSQL `make -C build up`
.PHONY: server
server:
go run cmd/bank/main.go

.PHONY: integrationtest
integrationtest: fmt
Expand All @@ -68,6 +51,15 @@ lint-install:
lint: fmt
revive -config=revivie-lint.toml ./...

.PHONY: mockgen-install
mockgen-install:
go install go.uber.org/mock/mockgen@latest

# Generate mocks
.PHONY: mock
mock: mockgen-install
mockgen -source internal/app/bank/bank.go -destination internal/app/bank/mock/bank.go -package mockdb

# We use https://docs.sqlc.dev/en/stable/index.html for database queries and mapping. This library
# has support for PostgreSQL, MySQL and SQLite, no other DBs supported.
.PHONY: sqlc-install
Expand Down Expand Up @@ -106,13 +98,3 @@ docker-build:
-t "bank" \
.

# Build a container to be used purely for running integration tests, docker in docker with testcontainers.
# Start tests by 'make integrationtest-docker-run'
.PHONY: integrationtest-docker-build
integrationtest-docker-build:
DOCKER_DEFAULT_PLATFORM=linux/amd64 \
DOCKER_BUILDKIT=1 \
docker build \
-f build/docker/integrationTest.Dockerfile \
-t "bank-integration-test" \
.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Learn everything about backend web development: Golang, Postgres, Redis, Gin, gR
* testcontainers-go, https://github.com/testcontainers/testcontainers-go
* sqlc, https://sqlc.dev
* PostgreSQL, https://www.postgresql.org
* Gin web framework, https://github.com/gin-gonic/gin

## VSCode setup

Expand Down
20 changes: 20 additions & 0 deletions build/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,24 @@ Create next migration path
$ make migrate-db-create-next-migration-path
/migrations/20230907110931_bank-db-migration.up.sql
/migrations/20230907110931_bank-db-migration.down.sql
~~~

## Test

Example uses [httpie](https://httpie.io)

~~~json
$ http POST localhost:8080/users username=johndoe password=qwerty full_name=John [email protected]
HTTP/1.1 200 OK
Content-Length: 164
Content-Type: application/json; charset=utf-8
Date: Fri, 27 Oct 2023 18:41:58 GMT

{
"created_at": "2023-10-27T18:41:58.248839Z",
"email": "[email protected]",
"full_name": "John",
"password_changed_at": "0001-01-01T00:00:00Z",
"username": "johndoe"
}
~~~
2 changes: 2 additions & 0 deletions build/docker/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ services:
LOG_LEVEL: DEBUG
networks:
- backend
ports:
- 8080:8080
depends_on:
db:
condition: service_healthy
Expand Down
39 changes: 0 additions & 39 deletions build/docker/integrationTest.Dockerfile

This file was deleted.

15 changes: 15 additions & 0 deletions cmd/bank/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (

"github.com/golang-migrate/migrate/v4"
"github.com/hthunberg/course-golang-postgres-grpc-api/cmd"
"github.com/hthunberg/course-golang-postgres-grpc-api/internal/app/api"
"github.com/hthunberg/course-golang-postgres-grpc-api/internal/app/bank"
"github.com/hthunberg/course-golang-postgres-grpc-api/internal/app/util"
"github.com/jackc/pgx/v5/pgxpool"
"go.uber.org/zap"
Expand Down Expand Up @@ -53,6 +55,19 @@ func main() {

runDBMigration(cfg.MigrationURL, cfg.DBSource, logger)

// Set up the bank
bank := bank.NewBank(connPool)

// Set up the API server for the bank
server, err := api.NewServer(cfg, bank)
if err != nil {
logger.Fatal("initializing: api server", zap.Error(err))
}

if err := server.Start(cfg.HTTPServerAddress); err != nil {
logger.Fatal("initializing: start api server", zap.Error(err))
}

termChan := make(chan os.Signal, 1)
signal.Notify(termChan, syscall.SIGINT, syscall.SIGTERM)
select {
Expand Down
89 changes: 58 additions & 31 deletions dbtest/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"os"
"time"

"github.com/docker/go-connections/nat"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"

Expand All @@ -19,9 +20,11 @@ import (
)

const (
DbName = "test_db"
DbUser = "test_user"
DbPass = "test_password"
DBName = "test_db"
DBUser = "test_user"
DBPass = "test_password"
DBPort = "5432"
port = DBPort + "/tcp"
)

// TestDatabase represents
Expand All @@ -30,37 +33,49 @@ const (
// - handle to running test container
type TestDatabase struct {
DbInstance *pgxpool.Pool
DbAddress string
container testcontainers.Container
DBPort string
DBHost string
Container testcontainers.Container
}

func SetupTestDatabase() *TestDatabase {
func SetupTestDatabase(ctx context.Context, testDatabaseContainerRequest testcontainers.GenericContainerRequest) (*TestDatabase, error) {
// setup db container
ctx, cancel := context.WithTimeout(context.Background(), time.Second*60)
container, dbInstance, dbAddr, err := createContainer(ctx)
container, dbInstance, err := createContainer(ctx, testDatabaseContainerRequest)
if err != nil {
log.Fatal("failed to setup test", err)
return nil, fmt.Errorf("setup test db:create container: %v", err)
}

dbPort, err := container.MappedPort(ctx, nat.Port(port))
if err != nil {
return nil, fmt.Errorf("setup test bank:container port: %v", err)
}

dbHost, err := container.Host(ctx)
if err != nil {
return nil, fmt.Errorf("setup test db:container host: %v", err)
}

dbAddr := fmt.Sprintf("%s:%s", dbHost, dbPort.Port())

// migrate db schema
err = migrateDb(dbAddr)
if err != nil {
log.Fatal("failed to perform db migration", err)
return nil, fmt.Errorf("setup test bank:migrate db: %v", err)
}
cancel()

return &TestDatabase{
container: container,
Container: container,
DbInstance: dbInstance,
DbAddress: dbAddr,
}
DBPort: dbPort.Port(),
DBHost: dbHost,
}, nil
}

// TearDown tears down the running database container
func (tdb *TestDatabase) TearDown() {
tdb.DbInstance.Close()
// remove test container
_ = tdb.container.Terminate(context.Background())
_ = tdb.Container.Terminate(context.Background())
}

func (tdb *TestDatabase) Truncate() error {
Expand All @@ -77,40 +92,50 @@ func (tdb *TestDatabase) Truncate() error {
}
}

log.Println("database truncated: ", tdb.DbAddress)
dbAddr := fmt.Sprintf("%s:%s", tdb.DBHost, tdb.DBPort)

log.Println("database truncated: ", dbAddr)
return nil
}

func createContainer(ctx context.Context) (testcontainers.Container, *pgxpool.Pool, string, error) {
func TestDatabaseContainerRequest() testcontainers.GenericContainerRequest {
env := map[string]string{
"POSTGRES_PASSWORD": DbPass,
"POSTGRES_USER": DbUser,
"POSTGRES_DB": DbName,
"POSTGRES_PASSWORD": DBPass,
"POSTGRES_USER": DBUser,
"POSTGRES_DB": DBName,
}
port := "5432/tcp"

req := testcontainers.GenericContainerRequest{
ContainerRequest: testcontainers.ContainerRequest{
Image: "postgres:bullseye",
ExposedPorts: []string{port},
Env: env,
WaitingFor: wait.ForLog("database system is ready to accept connections"),
WaitingFor: wait.ForAll(
wait.ForLog("database system is ready to accept connections"),
wait.ForExposedPort().WithStartupTimeout(60*time.Second),
wait.ForListeningPort(nat.Port(port)).WithStartupTimeout(10*time.Second),
),
},
Started: true,
}

return req
}

func createContainer(ctx context.Context, req testcontainers.GenericContainerRequest) (testcontainers.Container, *pgxpool.Pool, error) {
container, err := testcontainers.GenericContainer(ctx, req)
if err != nil {
return container, nil, "", fmt.Errorf("failed to start container: %v", err)
return container, nil, fmt.Errorf("create container:failed to start container: %v", err)
}

p, err := container.MappedPort(ctx, "5432")
if err != nil {
return container, nil, "", fmt.Errorf("failed to get container external port: %v", err)
return container, nil, fmt.Errorf("create container:failed to get container external port: %v", err)
}

h, err := container.Host(ctx)
if err != nil {
return container, nil, "", fmt.Errorf("failed to get container host: %v", err)
return container, nil, fmt.Errorf("create container:failed to get container host: %v", err)
}

time.Sleep(time.Second)
Expand All @@ -119,24 +144,26 @@ func createContainer(ctx context.Context) (testcontainers.Container, *pgxpool.Po

log.Println("postgres container ready and running at: ", dbAddr)

db, err := pgxpool.New(ctx, fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=disable", DbUser, DbPass, dbAddr, DbName))
db, err := pgxpool.New(ctx, fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=disable", DBUser, DBPass, dbAddr, DBName))
if err != nil {
return container, db, dbAddr, fmt.Errorf("failed to establish database connection: %v", err)
return container, db, fmt.Errorf("create container:failed to establish database connection: %v", err)
}

return container, db, dbAddr, nil
return container, db, nil
}

func migrateDb(dbAddr string) error {
databaseURL := fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=disable", DbUser, DbPass, dbAddr, DbName)
databaseURL := fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=disable", DBUser, DBPass, dbAddr, DBName)

// file:./<path> to be relative to working directory
migrationsURL := os.Getenv("MIGRATION_URL")

if len(migrationsURL) < 1 {
return fmt.Errorf("missing env migration_url: %s", migrationsURL)
return fmt.Errorf("migrate db:missing env migration_url: %s", migrationsURL)
}

log.Printf("migrate db:running db migrations using db %s migrations %s", databaseURL, migrationsURL)

migration, err := migrate.New(migrationsURL, databaseURL)
if err != nil {
return err
Expand All @@ -145,7 +172,7 @@ func migrateDb(dbAddr string) error {

err = migration.Up()
if err != nil && !errors.Is(err, migrate.ErrNoChange) {
return err
return fmt.Errorf("migrate db:migrate up: %v", err)
}

log.Println("migration done")
Expand Down
Loading

0 comments on commit ed6a996

Please sign in to comment.