Single reference for getting the project running locally, executing the full
test suite, and resolving the most common failures. Supersedes the scattered
guidance in QUICK_START.md, TEST_EXECUTION.md, and
README_REPOSITORY_TESTS.md.
| Tool | Minimum version | Check |
|---|---|---|
| Go | 1.25 | go version |
| Git | any | git --version |
| Docker | 20+ (for integration tests) | docker info |
| PostgreSQL | optional (Docker handles it) | — |
Install Go from go.dev/dl. Docker is only required when running the integration test suite; unit tests have no external dependencies.
git clone https://github.com/YOUR_ORG/stellabill-backend.git
cd stellabill-backend
go mod downloadCreate a .env file in the project root — never commit it; it is already
in .gitignore.
# .env — local development only, do not commit
ENV=development
PORT=8080
# Required for the server to start (use placeholder values locally)
DATABASE_URL=postgres://postgres:postgres@localhost:5432/stellarbill?sslmode=disable
JWT_SECRET=Dev-Only-Secret-Change-In-Prod-1!
# Optional
ADMIN_TOKEN=dev-admin-token
AUDIT_HMAC_SECRET=stellarbill-dev-audit
AUDIT_LOG_PATH=audit.log
# Request ID trusted proxies (comma-separated CIDRs; empty = untrusted mode)
REQUEST_ID_TRUSTED_PROXIES=
# Rate limiting (disabled by default in dev)
RATE_LIMIT_ENABLED=falseExport them before running the server:
export $(grep -v '^#' .env | xargs)Security: Use your cloud provider's secrets manager in production. Never put real credentials in
.envor any file tracked by Git.
go run ./cmd/serverVerify it is up:
curl http://localhost:8080/api/health
# {"service":"stellarbill-backend","status":"ok",...}go test ./internal/... -count=1 -timeout 60sExpected: all packages pass. Coverage is enforced at ≥ 95% on internal/.
Generate a coverage report:
go test ./internal/... \
-covermode=atomic \
-coverpkg=./internal/... \
-coverprofile=coverage.out \
-count=1 \
-timeout 60s
go tool cover -html=coverage.out # opens browserIntegration tests spin up reusable Postgres, Redis, and mock webhook receiver
containers automatically via testcontainers-go. No Compose bootstrap or
fixed host ports are needed.
go test -tags integration -v -race -count=1 -timeout 5m ./tests/integration/...The suite creates one stack and shares it between tests (Reuse: true).
Docker assigns every host port, so parallel test processes do not contend for
5432, 6379, or 8080. Postgres and Redis credentials are generated for each
run. All mapped ports bind to 127.0.0.1, so the test services are not exposed
to the local network.
Ryuk removes leaked containers after interrupted runs and should remain
enabled on developer machines and CI. Only set
TESTCONTAINERS_RYUK_DISABLED=true in a trusted, isolated environment where
the Docker socket policy prevents Ryuk from running and an external cleanup
job is guaranteed. Disabling Ryuk on a shared Docker host can leave containers,
credentials, and consumed resources behind.
The images use explicit version tags. Review and update those versions
deliberately; do not replace them with latest. The mock webhook receiver is
for local test payloads only and must never receive production secrets or PII.
go test ./internal/... -race -count=1 -timeout 60s# Middleware
go test ./internal/middleware/... -v -count=1
# Worker
go test ./internal/worker/... -v -count=1
# Outbox
go test ./internal/outbox/... -v -count=1
# Config
go test ./internal/config/... -v -count=1
# Audit
go test ./internal/audit/... -v -count=1go build ./... # must compile cleanly
go vet ./... # no vet warnings
go fmt ./... # code is formatted
go test ./internal/... -count=1 -timeout 60s # all tests passgo run ./cmd/migrate upMigrations live in migrations/. See docs/migrations.md for conventions.
Symptom:
config error [MISSING_ENV_VAR]: required secret is missing (key=DATABASE_URL)
Fix: Export the required variables before running:
export DATABASE_URL=postgres://postgres:postgres@localhost:5432/stellarbill?sslmode=disable
export JWT_SECRET=Dev-Only-Secret-Change-In-Prod-1!
go run ./cmd/serverSymptom:
config error [WEAK_SECRET]: must be at least 12 characters and contain mixed
alphanumeric and special characters (key=JWT_SECRET)
Fix: Use a secret with uppercase, lowercase, digits, and a special character, minimum 12 characters:
export JWT_SECRET=Dev-Only-Secret-Change-In-Prod-1!Symptom:
dial tcp 127.0.0.1:5432: connect: connection refused
Fix options:
a) Start Postgres locally:
docker run -d \
--name stellarbill-pg \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_DB=stellarbill \
-p 5432:5432 \
postgres:16-alpineb) Or point DATABASE_URL at an existing instance.
c) The server runs without a real DB for most endpoints (mock repositories are used by default in dev). Only endpoints that require persistence will fail.
Symptom:
pq: relation "plans" already exists
Fix: Run the down migration first, then up:
go run ./cmd/migrate down
go run ./cmd/migrate upOr drop and recreate the database:
docker exec -it stellarbill-pg psql -U postgres -c "DROP DATABASE stellarbill;"
docker exec -it stellarbill-pg psql -U postgres -c "CREATE DATABASE stellarbill;"
go run ./cmd/migrate upSymptom:
Cannot connect to the Docker daemon at unix:///var/run/docker.sock
Fix: Start Docker Desktop (macOS/Windows) or the Docker daemon (Linux):
sudo systemctl start docker # LinuxThen re-run:
go test -tags integration -v -count=1 -timeout 120s ./integration/...Symptom:
context deadline exceeded waiting for container to be ready
Fix: Increase the timeout or check Docker resource limits:
go test -tags integration -v -count=1 -timeout 300s ./integration/...If Docker is resource-constrained, increase memory allocation in Docker Desktop settings (≥ 4 GB recommended).
Symptom: Tests that hit protected endpoints return 401 unexpectedly.
Cause: The Authorization header is missing or the bearer token does not
match JWT_SECRET.
Fix: Ensure the test sets the correct header:
req.Header.Set("Authorization", "Bearer "+jwtSecret)For integration tests, JWT_SECRET is set in TestMain via
os.Setenv("JWT_SECRET", ...). Check integration/main_test.go if the value
has drifted.
Symptom: Responses lack X-Request-ID or logs show a different ID than
the one sent.
Cause: The inbound X-Request-ID is only accepted from trusted sources.
If REQUEST_ID_TRUSTED_PROXIES is empty (the default), all inbound IDs are
discarded and a new one is generated.
Fix for local testing with curl:
# Without trusted proxies — server generates its own ID
curl -H "X-Request-ID: my-trace-id" http://localhost:8080/api/health
# Response X-Request-ID will be a generated ID, not "my-trace-id"
# To accept inbound IDs, add your IP to the allowlist
export REQUEST_ID_TRUSTED_PROXIES=127.0.0.1/32
go run ./cmd/server
curl -H "X-Request-ID: my-trace-id" http://localhost:8080/api/health
# Response X-Request-ID: my-trace-idSymptom: go test hangs and eventually times out with a goroutine dump
showing cleanupExpiredBuckets blocked on a ticker.
Cause: APIRateLimiter starts a background goroutine. Tests that create
one must call rl.Stop() to release it.
Fix: Always defer Stop() in tests:
rl := middleware.NewAPIRateLimiter(config)
defer rl.Stop()Symptom:
./requestid.go:8:2: RequestIDHeader redeclared in this block
Cause: internal/middleware/requestid.go (now deleted) conflicted with
internal/middleware/middleware.go. If you see this after a merge or rebase,
ensure internal/middleware/requestid.go does not exist:
ls internal/middleware/requestid.go # should not existIf it does, delete it — all request ID logic now lives in
internal/requestid/requestid.go.
Symptom:
FAIL stellarbill-backend/cmd/server [setup failed]
Cause: cmd/server/main.go is the process entry point and cannot be
instrumented as a unit-testable package. This is expected.
Fix: Run tests on internal/ only:
go test ./internal/... -count=1 -timeout 60sSymptom:
coverage: 87.3% of statements
Fix: Find uncovered lines:
go test ./internal/... \
-coverprofile=coverage.out \
-coverpkg=./internal/... \
-count=1
go tool cover -func=coverage.out | sort -t% -k1 -n | head -20Add tests for the uncovered paths, focusing on error branches and edge cases.
The GitHub Actions workflow (.github/workflows/ci.yml) runs on every push
and pull request:
| Step | Command |
|---|---|
| Build | go build ./... |
| Vet | go vet ./... |
| Unit tests + coverage | go test ./internal/... -covermode=atomic -coverpkg=./internal/... |
| Coverage threshold | ≥ 95% on internal/ |
Coverage artifacts are retained for 14 days.
- Never commit
.env, JWT secrets, database credentials, or API keys. REQUEST_ID_TRUSTED_PROXIESdefaults to empty (untrusted mode). Only add CIDRs you control (e.g., your load balancer's egress range).- The in-memory rate limiter is process-local. In multi-instance deployments, use a shared store (Redis) or rely on upstream rate limiting.
- CORS is configured as
*in development. Set an explicit origin in production via theCORS_ALLOW_ORIGINenvironment variable. - Audit log entries are HMAC-signed. Protect
AUDIT_HMAC_SECRETas you would any signing key.
| Document | Purpose |
|---|---|
README.md |
Project overview, API reference, contributing guide |
docs/migrations.md |
Migration conventions and production runbook |
docs/RATE_LIMITING.md |
Rate limiting configuration and security notes |
docs/outbox-pattern.md |
Outbox pattern design and operational notes |
docs/runbooks/outbox-operations.md |
Outbox operational runbook |
docs/security-notes.md |
Security analysis and threat model |
internal/worker/README.md |
Background worker documentation |