Skip to content
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
4 changes: 2 additions & 2 deletions .agents/skills/tdd-unit-testing/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ description: Write unit tests in Go following Red-Green-Refactor TDD principles.

**Handlers**: Create handler struct with UseCase/Service field; return `http.HandlerFunc` from `Handler()` method; test via httptest
**Services**: Mock repositories; test business logic and error handling
**Repositories**: Test against real SQLite database (Bun ORM); use test fixtures to set up schema
**Repositories**: Test against real Postgres database (Bun ORM via shared testcontainer); use `NewIntegrationTestDB(t)` helper
**Integration tests**: Use fixtures; test plugin routes end-to-end

## Pattern
Expand All @@ -43,7 +43,7 @@ See [examples/](examples/) for Todos testing patterns:

- `test_helpers.go` - MockTodoUseCase and MockTodoService interfaces
- `handler_test.go` - Handler struct with UseCase, Handler() method, httptest patterns
- `repository_test.go` - Real SQLite database tests with test fixtures, CRUD operations
- `repository_test.go` - Real Postgres database tests with test fixtures, CRUD operations
- `todo_service_test.go` - Service tests with mocked repositories and table-driven tests
- `plugin_integration_test.go` - End-to-end route testing with fixtures

Expand Down
51 changes: 18 additions & 33 deletions .agents/skills/tdd-unit-testing/examples/repository_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,19 @@ package todos_test

import (
"context"
"database/sql"
"testing"

"github.com/stretchr/testify/assert"
"github.com/uptake/go-bun"
"github.com/uptake/go-bun/driver/sqliteshim"
"github.com/uptrace/bun"

"github.com/Authula/authula/internal/testdb"
)

// ================== TEST FIXTURE ==================

// setupTestDB creates an in-memory SQLite database with Bun ORM.
// setupTestDB creates a test Postgres database using the shared testcontainer.
func setupTestDB(t *testing.T) bun.IDB {
// Open in-memory SQLite
sqldb, err := sql.Open(sqliteshim.ShimName, "file::memory:?cache=shared")
assert.NoError(t, err)

// Create Bun database instance
db := bun.NewDB(sqldb)

t.Cleanup(func() {
db.Close()
})

// Register models for schema creation
db.RegisterModel((*Todo)(nil))

// Create todos table using Bun schema
ctx := context.Background()
_, err = db.NewCreateTable().Model((*Todo)(nil)).Exec(ctx)
assert.NoError(t, err)

return db
return testdb.NewIntegrationTestDB(t)
}

// Todo represents the table structure (Bun model).
Expand Down Expand Up @@ -82,7 +63,6 @@ func (r *TodoRepository) MarkComplete(ctx context.Context, id string) error {
}

func randomID() string {
// In real code, use UUID library
return "abc123"
}

Expand Down Expand Up @@ -110,8 +90,11 @@ func TestTodoRepository_Create(t *testing.T) {
t.Parallel()

db := setupTestDB(t)
repo := NewTodoRepository(db)
ctx := context.Background()
_, err := db.NewCreateTable().Model((*Todo)(nil)).Exec(ctx)
assert.NoError(t, err)

repo := NewTodoRepository(db)

todoID, err := repo.Create(ctx, tt.title)

Expand Down Expand Up @@ -173,8 +156,11 @@ func TestTodoRepository_MarkComplete(t *testing.T) {
t.Parallel()

db := setupTestDB(t)
repo := NewTodoRepository(db)
ctx := context.Background()
_, err := db.NewCreateTable().Model((*Todo)(nil)).Exec(ctx)
assert.NoError(t, err)

repo := NewTodoRepository(db)

todoID, err := repo.Create(ctx, tt.title)
assert.NoError(t, err)
Expand All @@ -189,7 +175,6 @@ func TestTodoRepository_MarkComplete(t *testing.T) {
}
}

// TestTodoRepository_TableDriven shows multiple operations in one test.
func TestTodoRepository_TableDriven(t *testing.T) {
t.Parallel()

Expand All @@ -211,7 +196,7 @@ func TestTodoRepository_TableDriven(t *testing.T) {
{
name: "empty title",
title: "",
wantErr: false, // DB allows empty, validation in service layer
wantErr: false,
},
}

Expand All @@ -220,20 +205,20 @@ func TestTodoRepository_TableDriven(t *testing.T) {
t.Parallel()

db := setupTestDB(t)
repo := NewTodoRepository(db)
ctx := context.Background()
_, err := db.NewCreateTable().Model((*Todo)(nil)).Exec(ctx)
assert.NoError(t, err)

repo := NewTodoRepository(db)

// Act
todoID, err := repo.Create(ctx, tt.title)

// Assert
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.NotEmpty(t, todoID)

// Verify retrieval
title, _, err := repo.GetByID(ctx, todoID)
assert.NoError(t, err)
assert.Equal(t, tt.title, title)
Expand Down
2 changes: 0 additions & 2 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ RUN apt update && apt install -y \
curl wget git zip unzip less zsh jq lsof ripgrep

ENV HOME="/root"
# Required for some cgo operations such as using certain database drivers like SQLite
ENV CGO_ENABLED=1

# --------------------------------------
# Git
Expand Down
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# -------------------
FROM golang:1.26.4-alpine AS builder

RUN apk add --no-cache git ca-certificates build-base
RUN apk add --no-cache git ca-certificates

WORKDIR /app

Expand All @@ -12,7 +12,7 @@ RUN go mod download

COPY . ./

RUN CGO_ENABLED=1 GOOS=linux go build \
RUN GOOS=linux go build \
-ldflags="-s -w" \
-o server ./cmd/main.go

Expand Down
13 changes: 5 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ APP_NAME=authula
BINARY_PATH=./tmp/$(APP_NAME)
MIGRATE_CONFIG?=./config.toml
MIGRATE_ARGS?=
MIGRATE_CMD=CGO_ENABLED=1 go run ./cmd/migrate
MIGRATE_CMD=go run ./cmd/migrate

.PHONY: help build build-exe run dev test clean install setup
.PHONY: test-coverage
Expand All @@ -29,20 +29,20 @@ build-exe: # Build the binary executable

run: # Run the application
@rm -f ./tmp/$(APP_NAME)
@CGO_ENABLED=1 go run ./cmd/main.go
@go run ./cmd/main.go

dev: # Run the application with live reloading using air
@rm -f ./tmp/$(APP_NAME)
@CGO_ENABLED=1 ./bin/air --build.cmd "go build -o ./tmp/$(APP_NAME) ./cmd/main.go" --build.entrypoint "./tmp/$(APP_NAME)"
@./bin/air --build.cmd "go build -o ./tmp/$(APP_NAME) ./cmd/main.go" --build.entrypoint "./tmp/$(APP_NAME)"

# Test commands
test: # Run all tests
@echo "Running tests..."
@CGO_ENABLED=1 go test -race -v ./...
@go test -race -v -p 1 ./...

test-coverage: # Run tests with coverage report
@echo "Running tests with coverage..."
@CGO_ENABLED=1 go test -race -v -coverprofile=coverage.out ./...
@go test -race -v -coverprofile=coverage.out ./...
@go tool cover -html=coverage.out -o coverage.html
@echo "Coverage report generated: coverage.html"
@go tool cover -func=coverage.out | grep total | awk '{print "Total coverage: " $$3}'
Expand Down Expand Up @@ -97,9 +97,6 @@ quick-check: format vet test # Run quick checks (format, vet, fast tests)

ci: clean install check # CI pipeline (clean, install, check)

# Integration testing
integration-test: docker-down docker-up docker-test # Run integration tests with Docker

# Migration commands
migrate-help: # Show migration CLI help
@$(MIGRATE_CMD) --help
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ Authula comes with a variety of plugins that provide essential authentication fe
- Email & Password: Email-based flows for registration, login, email verification, password reset & email change.
- OAuth providers
- TOTP: Authenticator app support, backup codes, trusted devices for two-factor authentication
- Multiple database backends (SQLite, PostgreSQL, MySQL)
- PostgreSQL database (primary)
- Secondary storage (In-memory, DB, Redis)
- Rate limiting
- CSRF protection
Expand Down
6 changes: 3 additions & 3 deletions auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func New(authConfig *AuthConfig) *Auth {

migrationManager := migrationmanager.NewManager(migrator)

if err := migrationManager.RunCore(context.Background(), authConfig.Config.Database.Provider); err != nil {
if err := migrationManager.RunCore(context.Background()); err != nil {
panic(fmt.Errorf("failed to run core migrations: %w", err))
}

Expand Down Expand Up @@ -160,14 +160,14 @@ func (auth *Auth) RunCoreMigrations(ctx context.Context) error {
if auth.migrationManager == nil {
return fmt.Errorf("migrator not initialized")
}
return auth.migrationManager.RunCore(ctx, auth.config.Database.Provider)
return auth.migrationManager.RunCore(ctx)
}

func (auth *Auth) DropCoreMigrations(ctx context.Context) error {
if auth.migrationManager == nil {
return fmt.Errorf("migrator not initialized")
}
return auth.migrationManager.DropCore(ctx, auth.config.Database.Provider)
return auth.migrationManager.DropCore(ctx)
}

// registerMiddleware registers all middleware from hooks and plugins
Expand Down
3 changes: 1 addition & 2 deletions bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,10 @@ func InitLogger(config *models.Config) models.Logger {
return internalbootstrap.InitLogger(internalbootstrap.LoggerOptions{Level: config.Logger.Level})
}

// InitDatabase creates a Bun DB connection based on provider
// InitDatabase creates a Bun DB connection
func InitDatabase(config *models.Config, logger models.Logger, logLevel string) (bun.IDB, error) {
return internalbootstrap.InitDatabase(
internalbootstrap.DatabaseOptions{
Provider: config.Database.Provider,
URL: config.Database.URL,
MaxOpenConns: config.Database.MaxOpenConns,
MaxIdleConns: config.Database.MaxIdleConns,
Expand Down
8 changes: 4 additions & 4 deletions cmd/migrate/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func runCore(action string) error {
switch action {
case "up":
runtime.logger.Info("running core migrations")
return runtime.manager.RunCore(ctx, runtime.config.Database.Provider)
return runtime.manager.RunCore(ctx)
case "down":
applied, err := runtime.manager.Migrator().ListApplied(ctx, "")
if err != nil {
Expand All @@ -91,7 +91,7 @@ func runCore(action string) error {
}

runtime.logger.Info("rolling back core migrations")
return runtime.manager.DropCore(ctx, runtime.config.Database.Provider)
return runtime.manager.DropCore(ctx)
default:
return fmt.Errorf("unknown core action: %s", action)
}
Expand Down Expand Up @@ -152,10 +152,10 @@ func runPlugins(action, only, except string, includeDisabled bool) error {
}

runtime.logger.Debug("running plugin migrations")
return runtime.manager.RunPlugins(ctx, runtime.config.Database.Provider, runtime.plugins, selector)
return runtime.manager.RunPlugins(ctx, runtime.plugins, selector)
case "down":
runtime.logger.Debug("rolling back plugin migrations")
return runtime.manager.DropPlugins(ctx, runtime.config.Database.Provider, runtime.plugins, selector)
return runtime.manager.DropPlugins(ctx, runtime.plugins, selector)
default:
return fmt.Errorf("unknown plugins action: %s", action)
}
Expand Down
11 changes: 0 additions & 11 deletions config/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,6 @@ func WithSecret(secret string) ConfigOption {

func WithDatabase(config models.DatabaseConfig) ConfigOption {
return func(c *models.Config) {
if config.Provider != "" {
c.Database.Provider = config.Provider
}
if envValue := os.Getenv(env.EnvDatabaseURL); envValue != "" {
c.Database.URL = envValue
} else if config.URL != "" {
Expand Down Expand Up @@ -248,9 +245,6 @@ func WithEventBus(config models.EventBusConfig) ConfigOption {
if config.GoChannel != nil {
c.EventBus.GoChannel = config.GoChannel
}
if config.SQLite != nil {
c.EventBus.SQLite = config.SQLite
}
if config.PostgreSQL != nil {
c.EventBus.PostgreSQL = config.PostgreSQL
}
Expand Down Expand Up @@ -307,11 +301,6 @@ func validateEventBusConfig(config *models.EventBusConfig) error {
return fmt.Errorf("gochannel provider selected but gochannel config is missing")
}

case "sqlite":
if config.SQLite == nil {
return fmt.Errorf("sqlite provider selected but sqlite config is missing")
}

case "postgres":
if config.PostgreSQL == nil {
return fmt.Errorf("postgres provider selected but postgres config is missing")
Expand Down
3 changes: 1 addition & 2 deletions events/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ type EventBusProvider string

const (
ProviderGoChannel EventBusProvider = "gochannel"
ProviderSQLite EventBusProvider = "sqlite"
ProviderPostgres EventBusProvider = "postgres"
ProviderRedis EventBusProvider = "redis"
ProviderKafka EventBusProvider = "kafka"
Expand All @@ -18,7 +17,7 @@ func (p EventBusProvider) String() string {

func (p EventBusProvider) Valid() bool {
switch p {
case ProviderGoChannel, ProviderSQLite, ProviderPostgres, ProviderRedis, ProviderKafka, ProviderNATS, ProviderRabbitMQ:
case ProviderGoChannel, ProviderPostgres, ProviderRedis, ProviderKafka, ProviderNATS, ProviderRabbitMQ:
return true
}
return false
Expand Down
Loading
Loading