From b9dba880a298b3af7e5551338332094281286379 Mon Sep 17 00:00:00 2001 From: Hristo Partenov Date: Tue, 6 Jan 2026 11:05:10 +0200 Subject: [PATCH 1/5] make test fail --- pkg/log/logger_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/log/logger_test.go b/pkg/log/logger_test.go index db65f7d..172e3d7 100644 --- a/pkg/log/logger_test.go +++ b/pkg/log/logger_test.go @@ -36,7 +36,7 @@ func Test_getCorrelationID(t *testing.T) { req, _ := http.NewRequest("GET", "http://example.com", bytes.NewBufferString("")) assert.Empty(t, getCorrelationID(req)) req.Header.Set("X-Correlation-ID", "test") - assert.Equal(t, "test", getCorrelationID(req)) + assert.Equal(t, "test", getCorrelationID(nil)) } func Test_getRequestID(t *testing.T) { From 93b45b14f6b66dffb8dd13e0a6010ad9e172364b Mon Sep 17 00:00:00 2001 From: Hristo Partenov Date: Tue, 6 Jan 2026 11:11:02 +0200 Subject: [PATCH 2/5] fix makefile so that when a test fails ci also fails --- Makefile | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/Makefile b/Makefile index d13e217..8760fe4 100644 --- a/Makefile +++ b/Makefile @@ -3,21 +3,22 @@ VERSION ?= $(shell git describe --tags --always --dirty --match=v* 2> /dev/null PACKAGES := $(shell go list ./... | grep -v /vendor/) LDFLAGS := -ldflags "-X main.Version=${VERSION}" -# DSN is required only for DB-related targets (migrate/testdata), not for build/lint/etc. APP_DSN ?= -# Fail only when DB targets are invoked (not on every make command) +PID_FILE := './.pid' +FSWATCH_FILE := './fswatch.cfg' + .PHONY: require-app-dsn require-app-dsn: @test -n "$(APP_DSN)" || (echo "APP_DSN is required. Set APP_DSN env var."; exit 1) # IMPORTANT: use '=' (recursive expansion) so APP_DSN is evaluated at runtime -MIGRATE = docker run -v $(shell pwd)/migrations:/migrations --network host migrate/migrate:v4.10.0 \ +MIGRATE = docker run --rm \ + -v $(shell pwd)/migrations:/migrations \ + --network host \ + migrate/migrate:v4.19.1 \ -path=/migrations/ -database "$(APP_DSN)" -PID_FILE := './.pid' -FSWATCH_FILE := './fswatch.cfg' - .PHONY: default default: help @@ -27,15 +28,23 @@ help: ## help information about make commands @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' .PHONY: test -test: ## run unit tests - @echo "mode: count" > coverage-all.out - @$(foreach pkg,$(PACKAGES), \ - go test -p=1 -cover -covermode=count -coverprofile=coverage.out ${pkg}; \ - tail -n +2 coverage.out >> coverage-all.out;) +test: ## run unit tests (fails properly on test failure) and aggregate coverage + @set -euo pipefail; \ + echo "mode: count" > coverage-all.out; \ + for pkg in $(PACKAGES); do \ + echo "==> testing $$pkg"; \ + go test -p=1 -cover -covermode=count -coverprofile=coverage.out "$$pkg"; \ + tail -n +2 coverage.out >> coverage-all.out; \ + done; \ + rm -f coverage.out .PHONY: test-cover -test-cover: test ## run unit tests and show test coverage information - go tool cover -html=coverage-all.out +test-cover: test ## run unit tests and print coverage summary (CI-friendly) + @go tool cover -func=coverage-all.out | tail -n 1 + +.PHONY: cover-html +cover-html: test ## open HTML coverage report locally + @go tool cover -html=coverage-all.out .PHONY: run run: ## run the API server @@ -83,12 +92,12 @@ db-stop: ## stop the database server .PHONY: testdata testdata: require-app-dsn ## populate the database with test data - make migrate-reset + @$(MAKE) migrate-reset @echo "Populating test data..." @docker exec -it postgres psql "$(APP_DSN)" -f /testdata/testdata.sql .PHONY: lint -lint: ## run golint on all Go package +lint: ## run golint on all Go packages @golint $(PACKAGES) .PHONY: fmt From 6f303966defc09fcac2f8427184d23dfc685c8e5 Mon Sep 17 00:00:00 2001 From: Hristo Partenov Date: Tue, 6 Jan 2026 11:16:16 +0200 Subject: [PATCH 3/5] use bash --- Makefile | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 8760fe4..bccd935 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,13 @@ +# Use bash for all recipes (GitHub runners + many systems default /bin/sh = dash, no pipefail) +SHELL := /usr/bin/env bash +.SHELLFLAGS := -euo pipefail -c + MODULE = $(shell go list -m) VERSION ?= $(shell git describe --tags --always --dirty --match=v* 2> /dev/null || echo "1.0.0") PACKAGES := $(shell go list ./... | grep -v /vendor/) LDFLAGS := -ldflags "-X main.Version=${VERSION}" +# DSN is required only for DB-related targets (migrate/testdata), not for build/lint/etc. APP_DSN ?= PID_FILE := './.pid' @@ -25,18 +30,19 @@ default: help # generate help info from comments: thanks to https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html .PHONY: help help: ## help information about make commands - @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | \ + awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' .PHONY: test test: ## run unit tests (fails properly on test failure) and aggregate coverage - @set -euo pipefail; \ - echo "mode: count" > coverage-all.out; \ - for pkg in $(PACKAGES); do \ + @echo "mode: count" > coverage-all.out + @rm -f coverage.out + @for pkg in $(PACKAGES); do \ echo "==> testing $$pkg"; \ go test -p=1 -cover -covermode=count -coverprofile=coverage.out "$$pkg"; \ tail -n +2 coverage.out >> coverage-all.out; \ - done; \ - rm -f coverage.out + done + @rm -f coverage.out .PHONY: test-cover test-cover: test ## run unit tests and print coverage summary (CI-friendly) @@ -52,7 +58,7 @@ run: ## run the API server .PHONY: run-restart run-restart: ## restart the API server - @pkill -P `cat $(PID_FILE)` || true + @pkill -P "$$(cat $(PID_FILE) 2>/dev/null)" || true @printf '%*s\n' "80" '' | tr ' ' - @echo "Source file changed. Restarting server..." @go run ${LDFLAGS} cmd/server/main.go & echo $$! > $(PID_FILE) @@ -61,7 +67,8 @@ run-restart: ## restart the API server .PHONY: run-live run-live: ## run the API server with live reload support (requires fswatch) @go run ${LDFLAGS} cmd/server/main.go & echo $$! > $(PID_FILE) - @fswatch -x -o --event Created --event Updated --event Renamed -r internal pkg cmd config | xargs -n1 -I {} make run-restart + @fswatch -x -o --event Created --event Updated --event Renamed -r internal pkg cmd config | \ + xargs -n1 -I {} make run-restart .PHONY: build build: ## build the API server binary From 3cf472e407a8717a2710c9641360b23ad6791bb7 Mon Sep 17 00:00:00 2001 From: Hristo Partenov Date: Tue, 6 Jan 2026 11:36:26 +0200 Subject: [PATCH 4/5] make test not depend on config file --- internal/config/config.go | 29 +++++++++++++++++++---------- internal/test/db.go | 17 ++++++++++------- pkg/log/logger_test.go | 2 +- 3 files changed, 30 insertions(+), 18 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index 9f43558..a086759 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,6 +1,7 @@ package config import ( + "errors" "fmt" "github.com/go-ozzo/ozzo-validation/v4" "github.com/ico12319/devops-project/pkg/log" @@ -69,24 +70,32 @@ func Load(file string, logger log.Logger) (*Config, error) { JWTExpiration: defaultJWTExpirationHours, } - // load from YAML config file - bytes, err := readFileScoped(file) - if err != nil { - return nil, err - } - if err = yaml.Unmarshal(bytes, &c); err != nil { - return nil, err + // load from YAML config file (optional) + if file != "" { + bytes, err := readFileScoped(file) + if err != nil { + // If the file doesn't exist, ignore and rely on env. + if errors.Is(err, os.ErrNotExist) { + // continue + } else { + return nil, err + } + } else { + if err := yaml.Unmarshal(bytes, &c); err != nil { + return nil, err + } + } } // load from environment variables prefixed with "APP_" - if err = env.New("APP_", logger.Infof).Load(&c); err != nil { + if err := env.New("APP_", logger.Infof).Load(&c); err != nil { return nil, err } // validation - if err = c.Validate(); err != nil { + if err := c.Validate(); err != nil { return nil, err } - return &c, err + return &c, nil } diff --git a/internal/test/db.go b/internal/test/db.go index f6fd277..4d69575 100644 --- a/internal/test/db.go +++ b/internal/test/db.go @@ -13,23 +13,26 @@ import ( var db *dbcontext.DB -// DB returns the database connection for testing purpose. func DB(t *testing.T) *dbcontext.DB { + t.Helper() + if db != nil { return db } + logger, _ := log.NewForTest() - dir := getSourcePath() - cfg, err := config.Load(dir+"/../../config/local.yml", logger) + + // Load config from env only (file optional / empty) + cfg, err := config.Load("", logger) if err != nil { - t.Error(err) - t.FailNow() + t.Fatal(err) } + dbc, err := dbx.MustOpen("postgres", cfg.DSN) if err != nil { - t.Error(err) - t.FailNow() + t.Fatal(err) } + dbc.LogFunc = logger.Infof db = dbcontext.New(dbc) return db diff --git a/pkg/log/logger_test.go b/pkg/log/logger_test.go index 172e3d7..db65f7d 100644 --- a/pkg/log/logger_test.go +++ b/pkg/log/logger_test.go @@ -36,7 +36,7 @@ func Test_getCorrelationID(t *testing.T) { req, _ := http.NewRequest("GET", "http://example.com", bytes.NewBufferString("")) assert.Empty(t, getCorrelationID(req)) req.Header.Set("X-Correlation-ID", "test") - assert.Equal(t, "test", getCorrelationID(nil)) + assert.Equal(t, "test", getCorrelationID(req)) } func Test_getRequestID(t *testing.T) { From 6206e3ca7a26915b13c78020ec7f13d84ab936f8 Mon Sep 17 00:00:00 2001 From: Hristo Partenov Date: Tue, 6 Jan 2026 11:38:10 +0200 Subject: [PATCH 5/5] fix unused function --- internal/test/db.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/internal/test/db.go b/internal/test/db.go index 4d69575..94355c4 100644 --- a/internal/test/db.go +++ b/internal/test/db.go @@ -6,8 +6,6 @@ import ( "github.com/ico12319/devops-project/pkg/dbcontext" "github.com/ico12319/devops-project/pkg/log" _ "github.com/lib/pq" // initialize posgresql for test - "path" - "runtime" "testing" ) @@ -48,9 +46,3 @@ func ResetTables(t *testing.T, db *dbcontext.DB, tables ...string) { } } } - -// getSourcePath returns the directory containing the source code that is calling this function. -func getSourcePath() string { - _, filename, _, _ := runtime.Caller(1) - return path.Dir(filename) -}