dynamic registration of routes, model, migrations and initializers.

This commit is contained in:
2026-04-01 14:40:50 +02:00
parent 4f5029e656
commit f82dc480f7
16 changed files with 383 additions and 240 deletions
-1
View File
@@ -10,5 +10,4 @@ func init() {
// Public routes (/api/public/version, /api/public/systeminfo).
registry.RegisterPublicRoutes(RegisterPublicRoutes)
//registry.RegisterMigration(MigrateStructure)
}
+2 -3
View File
@@ -5,10 +5,9 @@ import "bamort/registry"
// init self-registers the character module with the central registry.
// main.go blank-imports this package to trigger this function.
func init() {
// Protected API routes (/api/version, /api/systeminfo).
// Protected API routes (/api/characters/*).
registry.RegisterRoutes(RegisterRoutes)
// Public routes (/api/public/version, /api/public/systeminfo).
// Public routes.
registry.RegisterPublicRoutes(RegisterPublicRoutes)
//registry.RegisterMigration(MigrateStructure)
}
+23 -45
View File
@@ -1,19 +1,23 @@
package main
import (
_ "bamort/appsystem"
_ "bamort/character"
"bamort/config"
"bamort/database"
_ "bamort/equipment"
_ "bamort/gsmaster"
_ "bamort/importer"
"bamort/logger"
_ "bamort/maintenance"
"bamort/pdfrender"
_ "bamort/pdfrender"
"bamort/registry"
"bamort/router"
// Blank imports trigger each module's init(), which self-registers
// routes and migrations with the central registry.
_ "bamort/appsystem"
_ "bamort/character"
_ "bamort/equipment"
_ "bamort/gamesystem"
_ "bamort/gsmaster"
_ "bamort/importer"
_ "bamort/maintenance"
_ "bamort/models"
_ "bamort/pdfrender"
_ "bamort/transfer"
_ "bamort/user"
@@ -62,23 +66,18 @@ func main() {
database.ConnectDatabase()
logger.Info("Datenbankverbindung erfolgreich")
/*
// Populate initial misc lookup data
logger.Debug("Initialisiere Misc-Lookup-Daten...")
if err := gsmaster.PopulateMiscLookupData(); err != nil {
logger.Warn("Fehler beim Initialisieren der Misc-Lookup-Daten: %s", err.Error())
} else {
logger.Info("Misc-Lookup-Daten erfolgreich initialisiert")
}
*/
// Initialize PDF templates
logger.Debug("Initialisiere PDF-Templates...")
if err := pdfrender.InitializeTemplates("/app/default_templates", cfg.TemplatesDir); err != nil {
logger.Warn("Fehler beim Initialisieren der Templates: %s", err.Error())
} else {
logger.Info("PDF-Templates erfolgreich initialisiert")
// Run all registered migrations
logger.Debug("Führe Datenbankmigrationen aus...")
if err := registry.RunAllMigrations(database.DB); err != nil {
logger.Error("Fehler bei Datenbankmigrationen: %s", err.Error())
panic(err)
}
logger.Info("Datenbankmigrationen erfolgreich")
// Run all registered initializers (post-migration startup tasks)
logger.Debug("Führe Initialisierer aus...")
registry.RunAllInitializers(database.DB)
logger.Info("Initialisierer erfolgreich ausgeführt")
r := gin.Default()
router.SetupGin(r)
@@ -93,27 +92,6 @@ func main() {
logger.Info("API-Routen erfolgreich registriert")
/*
// Routes registrieren
logger.Debug("Registriere API-Routen...")
protected := router.BaseRouterGrp(r)
// Register your module routes
user.RegisterRoutes(protected)
gsmaster.RegisterRoutes(protected)
character.RegisterRoutes(protected)
equipment.RegisterRoutes(protected)
maintenance.RegisterRoutes(protected)
importer.RegisterRoutes(protected)
pdfrender.RegisterRoutes(protected)
transfer.RegisterRoutes(protected)
appsystem.RegisterRoutes(protected)
// Register public routes (no authentication)
pdfrender.RegisterPublicRoutes(r)
appsystem.RegisterPublicRoutes(r)
*/
logger.Info("API-Routen erfolgreich registriert")
// Server starten
serverAddress := cfg.GetServerAddress()
logger.Info("Server startet auf Adresse: %s", serverAddress)
+6 -4
View File
@@ -2,13 +2,15 @@ package database
import "bamort/registry"
// init self-registers the character module with the central registry.
// main.go blank-imports this package to trigger this function.
// init self-registers the database module with the central registry.
// main.go imports this package directly, which triggers this function.
func init() {
// Protected API routes (/api/version, /api/systeminfo).
// Protected API routes (/api/database/*).
registry.RegisterRoutes(RegisterRoutes)
// Public routes (/api/public/version, /api/public/systeminfo).
// Public routes.
registry.RegisterPublicRoutes(RegisterPublicRoutes)
// Database schema versioning migration.
registry.RegisterMigration(MigrateStructure)
}
+3 -4
View File
@@ -2,13 +2,12 @@ package equipment
import "bamort/registry"
// init self-registers the character module with the central registry.
// init self-registers the equipment module with the central registry.
// main.go blank-imports this package to trigger this function.
func init() {
// Protected API routes (/api/version, /api/systeminfo).
// Protected API routes (/api/equipment/*, /api/weapons/*).
registry.RegisterRoutes(RegisterRoutes)
// Public routes (/api/public/version, /api/public/systeminfo).
// Public routes.
registry.RegisterPublicRoutes(RegisterPublicRoutes)
//registry.RegisterMigration(MigrateStructure)
}
+5 -3
View File
@@ -2,13 +2,15 @@ package gamesystem
import "bamort/registry"
// init self-registers the character module with the central registry.
// init self-registers the gamesystem module with the central registry.
// main.go blank-imports this package to trigger this function.
func init() {
// Protected API routes (/api/version, /api/systeminfo).
// Protected API routes (/api/gamesystem/*).
registry.RegisterRoutes(RegisterRoutes)
// Public routes (/api/public/version, /api/public/systeminfo).
// Public routes.
registry.RegisterPublicRoutes(RegisterPublicRoutes)
// Database migration for GameSystem model.
registry.RegisterMigration(MigrateStructure)
}
+3 -4
View File
@@ -2,13 +2,12 @@ package gsmaster
import "bamort/registry"
// init self-registers the character module with the central registry.
// init self-registers the gsmaster module with the central registry.
// main.go blank-imports this package to trigger this function.
func init() {
// Protected API routes (/api/version, /api/systeminfo).
// Protected API routes (/api/gsmaster/*).
registry.RegisterRoutes(RegisterRoutes)
// Public routes (/api/public/version, /api/public/systeminfo).
// Public routes.
registry.RegisterPublicRoutes(RegisterPublicRoutes)
//registry.RegisterMigration(MigrateStructure)
}
+3 -3
View File
@@ -2,12 +2,12 @@ package importer
import "bamort/registry"
// init self-registers the character module with the central registry.
// init self-registers the importer module with the central registry.
// main.go blank-imports this package to trigger this function.
func init() {
// Protected API routes (/api/version, /api/systeminfo).
// Protected API routes (/api/importer/*).
registry.RegisterRoutes(RegisterRoutes)
// Public routes (/api/public/version, /api/public/systeminfo).
// Public routes.
registry.RegisterPublicRoutes(RegisterPublicRoutes)
}
+2 -3
View File
@@ -5,10 +5,9 @@ import "bamort/registry"
// init self-registers the maintenance module with the central registry.
// main.go blank-imports this package to trigger this function.
func init() {
// Protected API routes (/api/version, /api/systeminfo).
// Protected API routes (/api/maintenance/*).
registry.RegisterRoutes(RegisterRoutes)
// Public routes (/api/public/version, /api/public/systeminfo).
// Public routes.
registry.RegisterPublicRoutes(RegisterPublicRoutes)
//registry.RegisterMigration(MigrateStructure)
}
+2 -6
View File
@@ -2,13 +2,9 @@ package models
import "bamort/registry"
// init self-registers the character module with the central registry.
// init self-registers the models module with the central registry.
// main.go blank-imports this package to trigger this function.
func init() {
// Protected API routes (/api/version, /api/systeminfo).
//registry.RegisterRoutes(RegisterRoutes)
// Public routes (/api/public/version, /api/public/systeminfo).
//registry.RegisterPublicRoutes(RegisterPublicRoutes)
// Core domain model migrations.
registry.RegisterMigration(MigrateStructure)
}
+16 -5
View File
@@ -1,14 +1,25 @@
package pdfrender
import "bamort/registry"
import (
"bamort/config"
"bamort/logger"
"bamort/registry"
)
// init self-registers the character module with the central registry.
// init self-registers the pdfrender module with the central registry.
// main.go blank-imports this package to trigger this function.
func init() {
// Protected API routes (/api/version, /api/systeminfo).
// Protected API routes (/api/pdf/*).
registry.RegisterRoutes(RegisterRoutes)
// Public routes (/api/public/version, /api/public/systeminfo).
// Public routes (/api/pdf/file/*).
registry.RegisterPublicRoutes(RegisterPublicRoutes)
//registry.RegisterMigration(MigrateStructure)
cfg := config.Cfg
logger.Debug("Initialisiere PDF-Templates...")
if err := InitializeTemplates("/app/default_templates", cfg.TemplatesDir); err != nil {
logger.Warn("Fehler beim Initialisieren der Templates: %s", err.Error())
} else {
logger.Info("PDF-Templates erfolgreich initialisiert")
}
}
+32
View File
@@ -25,12 +25,18 @@ type BaseRouteFunc func(r *gin.Engine)
// AuthMiddlewareProvider returns a gin.HandlerFunc used for JWT authentication.
type AuthMiddlewareProvider func() gin.HandlerFunc
// InitializerFunc is called once after DB is connected and all migrations have run.
// Use it to load persisted settings into in-memory configuration.
type InitializerFunc func(db *gorm.DB)
var (
routeFuncs []RouteFunc
publicRouteFuncs []PublicRouteFunc
baseRouteFuncs []BaseRouteFunc
migrateFuncs []MigrateFunc
initializerFuncs []InitializerFunc
authProvider AuthMiddlewareProvider
modelInstances []interface{}
)
// RegisterRoutes adds a module's protected route registrar to the registry.
@@ -54,6 +60,24 @@ func RegisterMigration(fn MigrateFunc) {
migrateFuncs = append(migrateFuncs, fn)
}
// RegisterInitializer adds a startup function that is called once after all
// migrations have run. Use it to load persisted settings into config.
func RegisterInitializer(fn InitializerFunc) {
initializerFuncs = append(initializerFuncs, fn)
}
// RegisterModel adds a GORM model instance to the central model registry.
// Each module should register its model instances (e.g. MyModel{}) to allow
// cross-database operations such as test snapshot creation.
func RegisterModel(model interface{}) {
modelInstances = append(modelInstances, model)
}
// GetModels returns all registered GORM model instances.
func GetModels() []interface{} {
return modelInstances
}
// SetAuthMiddleware sets the authentication middleware provider.
// Only one module (user) should call this.
func SetAuthMiddleware(fn AuthMiddlewareProvider) {
@@ -99,3 +123,11 @@ func RunAllMigrations(db *gorm.DB) error {
}
return nil
}
// RunAllInitializers calls every registered initializer with the connected DB.
// Invoke this once after RunAllMigrations.
func RunAllInitializers(db *gorm.DB) {
for _, fn := range initializerFuncs {
fn(db)
}
}
+241 -159
View File
@@ -1,38 +1,40 @@
package registry
import (
"errors"
"net/http"
"net/http/httptest"
"testing"
"errors"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
// resetRegistry clears all registered functions so each test starts with a
// clean slate. Only callable from within the registry package (_test.go).
func resetRegistry() {
routeFuncs = nil
publicRouteFuncs = nil
baseRouteFuncs = nil
migrateFuncs = nil
authProvider = nil
routeFuncs = nil
publicRouteFuncs = nil
baseRouteFuncs = nil
migrateFuncs = nil
initializerFuncs = nil
authProvider = nil
modelInstances = nil
}
func newTestEngine() *gin.Engine {
gin.SetMode(gin.TestMode)
return gin.New()
gin.SetMode(gin.TestMode)
return gin.New()
}
func newTestDB(t *testing.T) *gorm.DB {
t.Helper()
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)
return db
t.Helper()
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)
return db
}
// ---------------------------------------------------------------------------
@@ -40,39 +42,39 @@ return db
// ---------------------------------------------------------------------------
func TestRegisterRoutes_SingleFunc_IsCalled(t *testing.T) {
resetRegistry()
resetRegistry()
called := false
RegisterRoutes(func(r *gin.RouterGroup) {
called = true
r.GET("/ping", func(c *gin.Context) { c.Status(http.StatusOK) })
})
called := false
RegisterRoutes(func(r *gin.RouterGroup) {
called = true
r.GET("/ping", func(c *gin.Context) { c.Status(http.StatusOK) })
})
engine := newTestEngine()
RunAllRoutes(engine.Group("/api"))
engine := newTestEngine()
RunAllRoutes(engine.Group("/api"))
assert.True(t, called)
assert.True(t, called)
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/api/ping", nil)
engine.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/api/ping", nil)
engine.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
}
func TestRegisterRoutes_MultipleFuncs_AllCalled(t *testing.T) {
resetRegistry()
resetRegistry()
count := 0
for i := 0; i < 3; i++ {
RegisterRoutes(func(r *gin.RouterGroup) { count++ })
}
RunAllRoutes(newTestEngine().Group("/api"))
assert.Equal(t, 3, count)
count := 0
for i := 0; i < 3; i++ {
RegisterRoutes(func(r *gin.RouterGroup) { count++ })
}
RunAllRoutes(newTestEngine().Group("/api"))
assert.Equal(t, 3, count)
}
func TestRunAllRoutes_NoFuncs_DoesNotPanic(t *testing.T) {
resetRegistry()
assert.NotPanics(t, func() { RunAllRoutes(newTestEngine().Group("/api")) })
resetRegistry()
assert.NotPanics(t, func() { RunAllRoutes(newTestEngine().Group("/api")) })
}
// ---------------------------------------------------------------------------
@@ -80,28 +82,28 @@ assert.NotPanics(t, func() { RunAllRoutes(newTestEngine().Group("/api")) })
// ---------------------------------------------------------------------------
func TestRegisterPublicRoutes_SingleFunc_IsCalled(t *testing.T) {
resetRegistry()
resetRegistry()
called := false
RegisterPublicRoutes(func(r *gin.Engine) {
called = true
r.GET("/health", func(c *gin.Context) { c.Status(http.StatusOK) })
})
called := false
RegisterPublicRoutes(func(r *gin.Engine) {
called = true
r.GET("/health", func(c *gin.Context) { c.Status(http.StatusOK) })
})
engine := newTestEngine()
RunAllPublicRoutes(engine)
engine := newTestEngine()
RunAllPublicRoutes(engine)
assert.True(t, called)
assert.True(t, called)
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/health", nil)
engine.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/health", nil)
engine.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
}
func TestRunAllPublicRoutes_NoFuncs_DoesNotPanic(t *testing.T) {
resetRegistry()
assert.NotPanics(t, func() { RunAllPublicRoutes(newTestEngine()) })
resetRegistry()
assert.NotPanics(t, func() { RunAllPublicRoutes(newTestEngine()) })
}
// ---------------------------------------------------------------------------
@@ -109,28 +111,28 @@ assert.NotPanics(t, func() { RunAllPublicRoutes(newTestEngine()) })
// ---------------------------------------------------------------------------
func TestRegisterBaseRoutes_SingleFunc_IsCalled(t *testing.T) {
resetRegistry()
resetRegistry()
called := false
RegisterBaseRoutes(func(r *gin.Engine) {
called = true
r.POST("/login", func(c *gin.Context) { c.Status(http.StatusOK) })
})
called := false
RegisterBaseRoutes(func(r *gin.Engine) {
called = true
r.POST("/login", func(c *gin.Context) { c.Status(http.StatusOK) })
})
engine := newTestEngine()
RunAllBaseRoutes(engine)
engine := newTestEngine()
RunAllBaseRoutes(engine)
assert.True(t, called)
assert.True(t, called)
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodPost, "/login", nil)
engine.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodPost, "/login", nil)
engine.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
}
func TestRunAllBaseRoutes_NoFuncs_DoesNotPanic(t *testing.T) {
resetRegistry()
assert.NotPanics(t, func() { RunAllBaseRoutes(newTestEngine()) })
resetRegistry()
assert.NotPanics(t, func() { RunAllBaseRoutes(newTestEngine()) })
}
// ---------------------------------------------------------------------------
@@ -138,64 +140,64 @@ assert.NotPanics(t, func() { RunAllBaseRoutes(newTestEngine()) })
// ---------------------------------------------------------------------------
func TestGetAuthMiddleware_NoProvider_ReturnsPassThrough(t *testing.T) {
resetRegistry()
resetRegistry()
mw := GetAuthMiddleware()
require.NotNil(t, mw)
mw := GetAuthMiddleware()
require.NotNil(t, mw)
engine := newTestEngine()
engine.Use(mw)
engine.GET("/test", func(c *gin.Context) { c.Status(http.StatusOK) })
engine := newTestEngine()
engine.Use(mw)
engine.GET("/test", func(c *gin.Context) { c.Status(http.StatusOK) })
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/test", nil)
engine.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code, "pass-through should not block requests")
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/test", nil)
engine.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code, "pass-through should not block requests")
}
func TestSetAuthMiddleware_ProviderIsUsed(t *testing.T) {
resetRegistry()
resetRegistry()
providerCalled := false
SetAuthMiddleware(func() gin.HandlerFunc {
providerCalled = true
return func(c *gin.Context) {
c.Header("X-Auth", "ok")
c.Next()
}
})
providerCalled := false
SetAuthMiddleware(func() gin.HandlerFunc {
providerCalled = true
return func(c *gin.Context) {
c.Header("X-Auth", "ok")
c.Next()
}
})
mw := GetAuthMiddleware()
assert.True(t, providerCalled)
mw := GetAuthMiddleware()
assert.True(t, providerCalled)
engine := newTestEngine()
engine.Use(mw)
engine.GET("/secure", func(c *gin.Context) { c.Status(http.StatusOK) })
engine := newTestEngine()
engine.Use(mw)
engine.GET("/secure", func(c *gin.Context) { c.Status(http.StatusOK) })
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/secure", nil)
engine.ServeHTTP(w, req)
assert.Equal(t, "ok", w.Header().Get("X-Auth"))
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/secure", nil)
engine.ServeHTTP(w, req)
assert.Equal(t, "ok", w.Header().Get("X-Auth"))
}
func TestSetAuthMiddleware_OverwritesPreviousProvider(t *testing.T) {
resetRegistry()
resetRegistry()
SetAuthMiddleware(func() gin.HandlerFunc {
return func(c *gin.Context) { c.Header("X-Version", "first"); c.Next() }
})
SetAuthMiddleware(func() gin.HandlerFunc {
return func(c *gin.Context) { c.Header("X-Version", "second"); c.Next() }
})
SetAuthMiddleware(func() gin.HandlerFunc {
return func(c *gin.Context) { c.Header("X-Version", "first"); c.Next() }
})
SetAuthMiddleware(func() gin.HandlerFunc {
return func(c *gin.Context) { c.Header("X-Version", "second"); c.Next() }
})
engine := newTestEngine()
engine.Use(GetAuthMiddleware())
engine.GET("/v", func(c *gin.Context) { c.Status(http.StatusOK) })
engine := newTestEngine()
engine.Use(GetAuthMiddleware())
engine.GET("/v", func(c *gin.Context) { c.Status(http.StatusOK) })
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/v", nil)
engine.ServeHTTP(w, req)
assert.Equal(t, "second", w.Header().Get("X-Version"), "last registration wins")
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/v", nil)
engine.ServeHTTP(w, req)
assert.Equal(t, "second", w.Header().Get("X-Version"), "last registration wins")
}
// ---------------------------------------------------------------------------
@@ -203,75 +205,155 @@ assert.Equal(t, "second", w.Header().Get("X-Version"), "last registration wins")
// ---------------------------------------------------------------------------
func TestRunAllMigrations_NoFuncs_ReturnsNil(t *testing.T) {
resetRegistry()
assert.NoError(t, RunAllMigrations(newTestDB(t)))
resetRegistry()
assert.NoError(t, RunAllMigrations(newTestDB(t)))
}
func TestRunAllMigrations_SingleFunc_IsCalled(t *testing.T) {
resetRegistry()
resetRegistry()
called := false
RegisterMigration(func(d ...*gorm.DB) error {
called = true
return nil
})
called := false
RegisterMigration(func(d ...*gorm.DB) error {
called = true
return nil
})
require.NoError(t, RunAllMigrations(newTestDB(t)))
assert.True(t, called)
require.NoError(t, RunAllMigrations(newTestDB(t)))
assert.True(t, called)
}
func TestRunAllMigrations_ReceivesCorrectDB(t *testing.T) {
resetRegistry()
db := newTestDB(t)
resetRegistry()
db := newTestDB(t)
var received *gorm.DB
RegisterMigration(func(d ...*gorm.DB) error {
if len(d) > 0 {
received = d[0]
}
return nil
})
var received *gorm.DB
RegisterMigration(func(d ...*gorm.DB) error {
if len(d) > 0 {
received = d[0]
}
return nil
})
require.NoError(t, RunAllMigrations(db))
assert.Same(t, db, received, "RunAllMigrations should pass the provided DB instance")
require.NoError(t, RunAllMigrations(db))
assert.Same(t, db, received, "RunAllMigrations should pass the provided DB instance")
}
func TestRunAllMigrations_MultipleFuncs_AllCalled(t *testing.T) {
resetRegistry()
resetRegistry()
count := 0
for i := 0; i < 3; i++ {
RegisterMigration(func(d ...*gorm.DB) error { count++; return nil })
}
require.NoError(t, RunAllMigrations(newTestDB(t)))
assert.Equal(t, 3, count)
count := 0
for i := 0; i < 3; i++ {
RegisterMigration(func(d ...*gorm.DB) error { count++; return nil })
}
require.NoError(t, RunAllMigrations(newTestDB(t)))
assert.Equal(t, 3, count)
}
func TestRunAllMigrations_FirstErrorAbortsChain(t *testing.T) {
resetRegistry()
resetRegistry()
secondCalled := false
RegisterMigration(func(d ...*gorm.DB) error { return errors.New("migration failed") })
RegisterMigration(func(d ...*gorm.DB) error { secondCalled = true; return nil })
secondCalled := false
RegisterMigration(func(d ...*gorm.DB) error { return errors.New("migration failed") })
RegisterMigration(func(d ...*gorm.DB) error { secondCalled = true; return nil })
err := RunAllMigrations(newTestDB(t))
assert.Error(t, err)
assert.False(t, secondCalled, "subsequent migrations must not run after an error")
err := RunAllMigrations(newTestDB(t))
assert.Error(t, err)
assert.False(t, secondCalled, "subsequent migrations must not run after an error")
}
func TestRunAllMigrations_AutoMigratesModel(t *testing.T) {
resetRegistry()
db := newTestDB(t)
resetRegistry()
db := newTestDB(t)
type SampleModel struct {
gorm.Model
Name string
type SampleModel struct {
gorm.Model
Name string
}
RegisterMigration(func(d ...*gorm.DB) error {
return d[0].AutoMigrate(&SampleModel{})
})
require.NoError(t, RunAllMigrations(db))
assert.True(t, db.Migrator().HasTable(&SampleModel{}), "table should exist after migration")
}
RegisterMigration(func(d ...*gorm.DB) error {
return d[0].AutoMigrate(&SampleModel{})
})
// ---------------------------------------------------------------------------
// RegisterInitializer / RunAllInitializers
// ---------------------------------------------------------------------------
require.NoError(t, RunAllMigrations(db))
assert.True(t, db.Migrator().HasTable(&SampleModel{}), "table should exist after migration")
func TestRunAllInitializers_NoFuncs_DoesNotPanic(t *testing.T) {
resetRegistry()
assert.NotPanics(t, func() { RunAllInitializers(newTestDB(t)) })
}
func TestRegisterInitializer_SingleFunc_IsCalled(t *testing.T) {
resetRegistry()
called := false
RegisterInitializer(func(db *gorm.DB) {
called = true
})
RunAllInitializers(newTestDB(t))
assert.True(t, called)
}
func TestRunAllInitializers_ReceivesCorrectDB(t *testing.T) {
resetRegistry()
db := newTestDB(t)
var received *gorm.DB
RegisterInitializer(func(d *gorm.DB) {
received = d
})
RunAllInitializers(db)
assert.Same(t, db, received, "RunAllInitializers should pass the provided DB instance")
}
func TestRunAllInitializers_MultipleFuncs_AllCalled(t *testing.T) {
resetRegistry()
count := 0
for i := 0; i < 3; i++ {
RegisterInitializer(func(db *gorm.DB) { count++ })
}
RunAllInitializers(newTestDB(t))
assert.Equal(t, 3, count)
}
// ---------------------------------------------------------------------------
// RegisterModel / GetModels
// ---------------------------------------------------------------------------
func TestGetModels_NoModels_ReturnsNil(t *testing.T) {
resetRegistry()
assert.Nil(t, GetModels())
}
func TestRegisterModel_SingleModel_IsReturned(t *testing.T) {
resetRegistry()
type Foo struct{ Name string }
RegisterModel(Foo{})
models := GetModels()
require.Len(t, models, 1)
assert.IsType(t, Foo{}, models[0])
}
func TestRegisterModel_MultipleModels_AllReturned(t *testing.T) {
resetRegistry()
type A struct{ X int }
type B struct{ Y string }
RegisterModel(A{})
RegisterModel(B{})
models := GetModels()
require.Len(t, models, 2)
assert.IsType(t, A{}, models[0])
assert.IsType(t, B{}, models[1])
}
+10
View File
@@ -0,0 +1,10 @@
package transfer
import "bamort/registry"
// init self-registers the transfer module with the central registry.
// main.go blank-imports this package to trigger this function.
func init() {
// Protected API routes (/api/transfer/*).
registry.RegisterRoutes(RegisterRoutes)
}
+22
View File
@@ -0,0 +1,22 @@
package user
import "bamort/registry"
// init self-registers the user module with the central registry.
// main.go blank-imports this package to trigger this function.
func init() {
// Auth middleware for JWT authentication.
registry.SetAuthMiddleware(AuthMiddleware)
// Base routes (login, register, password-reset) — no auth required.
registry.RegisterBaseRoutes(RegisterBaseRoutes)
// Protected API routes (/api/user/*, /api/users/*).
registry.RegisterRoutes(RegisterRoutes)
// Database migration for User model.
registry.RegisterMigration(MigrateStructure)
// Register GORM model for cross-DB operations.
registry.RegisterModel(User{})
}
+13
View File
@@ -4,6 +4,19 @@ import (
"github.com/gin-gonic/gin"
)
// RegisterBaseRoutes registers unauthenticated routes (login, register, password-reset).
func RegisterBaseRoutes(r *gin.Engine) {
r.POST("/login", LoginUser)
r.POST("/register", RegisterUser)
pwReset := r.Group("/password-reset")
{
pwReset.POST("/request", RequestPasswordReset)
pwReset.GET("/validate/:token", ValidateResetToken)
pwReset.POST("/reset", ResetPassword)
}
}
// RegisterRoutes registers user-related routes
func RegisterRoutes(r *gin.RouterGroup) {
userGroup := r.Group("/user")