Skip to content

Commit d9c0c9c

Browse files
committed
Add better tests and improve CI pipeline
1 parent 7ee4ac7 commit d9c0c9c

File tree

16 files changed

+259
-72
lines changed

16 files changed

+259
-72
lines changed

.github/workflows/dockerbuild.yaml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,36 @@ jobs:
1111
name: build
1212
runs-on: ubuntu-latest
1313
timeout-minutes: 10
14+
services:
15+
postgres:
16+
image: postgres:18
17+
env:
18+
POSTGRES_PASSWORD: postgres
19+
options: >-
20+
--health-cmd "pg_isready -U postgres"
21+
--health-interval 5s
22+
--health-timeout 2s
23+
--health-retries 5
24+
ports:
25+
- 5432:5432
26+
1427
steps:
1528
- name: Checkout
1629
uses: actions/checkout@v6
1730

31+
- name: go vet
32+
run: go vet ./...
33+
34+
- name: golangci-lint
35+
uses: golangci/golangci-lint-action@v9
36+
with:
37+
version: v2.11.4
38+
39+
- name: Tests
40+
run: |-
41+
make migrate
42+
make test
43+
1844
- name: Set up Docker Buildx
1945
uses: docker/setup-buildx-action@v4
2046

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
# Binary
1+
# Build artifact
22
/api
33

4+
# Tools
5+
bin/
6+
47
# Environment
58
.env
69

.golangci.yaml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
version: "2"
2+
run:
3+
go: "1.26"
4+
linters:
5+
enable:
6+
- forbidigo
7+
- lll
8+
- prealloc
9+
- predeclared
10+
- staticcheck
11+
disable:
12+
- errcheck
13+
exclusions:
14+
generated: lax
15+
presets:
16+
- comments
17+
- common-false-positives
18+
- legacy
19+
- std-error-handling
20+
rules:
21+
- linters:
22+
- deadcode
23+
- gosimple (megacheck)
24+
- govet (vet, vetshadow)
25+
- ineffassign
26+
- lll
27+
- revive
28+
- staticcheck
29+
- staticcheck (megacheck)
30+
- structcheck
31+
- unused (megacheck)
32+
- varcheck
33+
path: mocks/
34+
formatters:
35+
enable:
36+
- gci
37+
- gofmt
38+
- gofumpt
39+
- goimports
40+
settings:
41+
gci:
42+
sections:
43+
- standard
44+
- default
45+
- localmodule

Makefile

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
.PHONY: all fmt lint test build
2+
3+
bin:
4+
mkdir bin
5+
bin/golangci-lint: bin
6+
GOBIN=$(PWD)/bin go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.4
7+
8+
fmt:
9+
go mod tidy
10+
bin/golangci-lint fmt
11+
12+
lint: bin/golangci-lint
13+
go mod tidy -diff
14+
go vet ./...
15+
bin/golangci-lint run
16+
bin/golangci-lint fmt -d
17+
18+
test:
19+
go test -timeout=30s -race ./...

README.md

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,25 @@ It is comprised of a cross-platform client written in Python which defers the ac
66

77
## Requirements
88

9-
- Go >= 1.24
9+
- Go >= 1.26
1010
- PostgreSQL
1111

1212
## Configuration
1313

1414
Copy `.env-example` to `.env` and configure:
1515

16-
| Key | Default | Description |
17-
| ----------------- | ----------------------- | ---------------------------------- |
18-
| ENV | production | Set to development for debug mode |
19-
| POSTGRES_USER | pyazo | PostgreSQL username |
20-
| POSTGRES_PASSWORD | | PostgreSQL password |
21-
| POSTGRES_DB | pyazo | Database name |
22-
| POSTGRES_HOST | localhost | Database host |
23-
| JWT_SECRET | | JWT signing secret |
24-
| BLOCK_REGISTER | true | Disable user registration |
25-
| IMAGES_PATH | /images | Image storage directory |
26-
| CORS_ORIGIN | https://app.pyazo.com | Allowed CORS origin |
27-
| PORT | 8000 | HTTP listen port |
16+
| Key | Default | Description |
17+
| ----------------- | --------------------- | --------------------------------- |
18+
| ENV | production | Set to development for debug mode |
19+
| POSTGRES_USER | pyazo | PostgreSQL username |
20+
| POSTGRES_PASSWORD | | PostgreSQL password |
21+
| POSTGRES_DB | pyazo | Database name |
22+
| POSTGRES_HOST | localhost | Database host |
23+
| JWT_SECRET | | JWT signing secret |
24+
| BLOCK_REGISTER | true | Disable user registration |
25+
| IMAGES_PATH | /images | Image storage directory |
26+
| CORS_ORIGIN | https://app.pyazo.com | Allowed CORS origin |
27+
| PORT | 8000 | HTTP listen port |
2828

2929
## Development
3030

cmd/main.go

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,44 +2,42 @@ package main
22

33
import (
44
"context"
5-
"log"
5+
"log/slog"
6+
"os"
67

7-
"github.com/golang-migrate/migrate/v4"
8-
_ "github.com/golang-migrate/migrate/v4/database/postgres"
9-
"github.com/golang-migrate/migrate/v4/source/iofs"
108
"github.com/pyazo-screenshot/api/config"
119
"github.com/pyazo-screenshot/api/db"
1210
pyhttp "github.com/pyazo-screenshot/api/http"
13-
"github.com/pyazo-screenshot/api/migrations"
1411
)
1512

1613
func main() {
17-
cfg := config.Load()
18-
19-
runMigrations(cfg.DatabaseURL())
20-
21-
pool, err := db.NewPool(context.Background(), cfg.DatabaseURL())
14+
cfg, err := config.Load()
2215
if err != nil {
23-
log.Fatal("failed to connect to database: ", err)
16+
slog.Error("config", "error", err)
17+
os.Exit(1)
2418
}
25-
defer pool.Close()
2619

27-
s := pyhttp.NewServer(pool, cfg)
28-
log.Fatal(s.Router.Run(":" + cfg.Port))
29-
}
20+
if cfg.Env == "production" {
21+
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil)))
22+
}
3023

31-
func runMigrations(databaseURL string) {
32-
source, err := iofs.New(migrations.FS, ".")
33-
if err != nil {
34-
log.Fatal("failed to read migrations: ", err)
24+
if err := db.RunMigrations(cfg.DatabaseURL()); err != nil {
25+
slog.Error("failed to run migrations", "error", err)
26+
os.Exit(1)
3527
}
3628

37-
m, err := migrate.NewWithSourceInstance("iofs", source, databaseURL)
29+
pool, err := db.NewPool(context.Background(), cfg.DatabaseURL())
3830
if err != nil {
39-
log.Fatal("failed to create migrate instance: ", err)
31+
slog.Error("failed to connect to database", "error", err)
32+
os.Exit(1)
4033
}
34+
defer pool.Close()
4135

42-
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
43-
log.Fatal("failed to run migrations: ", err)
36+
s := pyhttp.NewServer(pool, cfg)
37+
addr := ":" + cfg.Port
38+
slog.Info("server starting", "addr", addr)
39+
if err := s.Router.Run(addr); err != nil {
40+
slog.Error("server failed", "error", err)
41+
os.Exit(1)
4442
}
4543
}

compose.yaml

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,8 @@ services:
22
db:
33
image: postgres:18
44
environment:
5-
POSTGRES_USER: pyazo
6-
POSTGRES_PASSWORD: pyazo
7-
POSTGRES_DB: pyazo
5+
POSTGRES_USER: postgres
6+
POSTGRES_PASSWORD: postgres
7+
POSTGRES_DB: postgres
88
ports:
99
- 127.0.0.1:5432:5432
10-
volumes:
11-
- db:/var/lib/postgresql
12-
13-
volumes:
14-
db:

config/config.go

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,29 +19,42 @@ type Config struct {
1919
Port string
2020
}
2121

22-
func Load() *Config {
23-
return &Config{
24-
Env: getenv("ENV", "production"),
25-
PostgresUser: getenv("POSTGRES_USER", "pyazo"),
26-
PostgresPassword: getenv("POSTGRES_PASSWORD", ""),
27-
PostgresDB: getenv("POSTGRES_DB", "pyazo"),
28-
PostgresHost: getenv("POSTGRES_HOST", "localhost"),
29-
JWTSecret: getenv("JWT_SECRET", ""),
30-
BlockRegister: strings.ToLower(getenv("BLOCK_REGISTER", "true")) != "false",
31-
ImagesPath: getenv("IMAGES_PATH", "/images"),
32-
CORSOrigin: getenv("CORS_ORIGIN", "https://app.pyazo.com"),
33-
Port: getenv("PORT", "8000"),
22+
func Load() (*Config, error) {
23+
var missing []string
24+
cfg := &Config{
25+
Env: optional("ENV", "production"),
26+
PostgresUser: optional("POSTGRES_USER", "pyazo"),
27+
PostgresPassword: required("POSTGRES_PASSWORD", &missing),
28+
PostgresDB: optional("POSTGRES_DB", "pyazo"),
29+
PostgresHost: optional("POSTGRES_HOST", "localhost"),
30+
JWTSecret: required("JWT_SECRET", &missing),
31+
BlockRegister: strings.ToLower(optional("BLOCK_REGISTER", "true")) != "false",
32+
ImagesPath: optional("IMAGES_PATH", "/images"),
33+
CORSOrigin: optional("CORS_ORIGIN", "https://app.pyazo.com"),
34+
Port: optional("PORT", "8000"),
3435
}
36+
if len(missing) > 0 {
37+
return nil, fmt.Errorf("missing required env vars: %s", strings.Join(missing, ", "))
38+
}
39+
return cfg, nil
3540
}
3641

37-
func (c *Config) DatabaseURL() string {
38-
return fmt.Sprintf("postgres://%s:%s@%s:5432/%s?sslmode=disable",
39-
c.PostgresUser, c.PostgresPassword, c.PostgresHost, c.PostgresDB)
42+
func required(key string, missing *[]string) string {
43+
v := os.Getenv(key)
44+
if v == "" {
45+
*missing = append(*missing, key)
46+
}
47+
return v
4048
}
4149

42-
func getenv(key, fallback string) string {
50+
func optional(key, fallback string) string {
4351
if v := os.Getenv(key); v != "" {
4452
return v
4553
}
4654
return fallback
4755
}
56+
57+
func (c *Config) DatabaseURL() string {
58+
return fmt.Sprintf("postgres://%s:%s@%s:5432/%s?sslmode=disable",
59+
c.PostgresUser, c.PostgresPassword, c.PostgresHost, c.PostgresDB)
60+
}

db/db.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,15 @@ type Image struct {
2525
}
2626

2727
func NewPool(ctx context.Context, databaseURL string) (*pgxpool.Pool, error) {
28-
return pgxpool.New(ctx, databaseURL)
28+
pool, err := pgxpool.New(ctx, databaseURL)
29+
if err != nil {
30+
return nil, err
31+
}
32+
if err := pool.Ping(ctx); err != nil {
33+
pool.Close()
34+
return nil, err
35+
}
36+
return pool, nil
2937
}
3038

3139
func GetUserByUsername(ctx context.Context, pool *pgxpool.Pool, username string) (*User, error) {

db/migrate.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package db
2+
3+
import (
4+
"github.com/golang-migrate/migrate/v4"
5+
_ "github.com/golang-migrate/migrate/v4/database/postgres"
6+
"github.com/golang-migrate/migrate/v4/source/iofs"
7+
8+
"github.com/pyazo-screenshot/api/migrations"
9+
)
10+
11+
func RunMigrations(databaseURL string) error {
12+
source, err := iofs.New(migrations.FS, ".")
13+
if err != nil {
14+
return err
15+
}
16+
17+
m, err := migrate.NewWithSourceInstance("iofs", source, databaseURL)
18+
if err != nil {
19+
return err
20+
}
21+
22+
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
23+
return err
24+
}
25+
return nil
26+
}

0 commit comments

Comments
 (0)