package app import ( "errors" "log/slog" "testing" "github.com/stretchr/testify/assert" ) func TestNewLifecycle(t *testing.T) { assert := assert.New(t) lc := NewLifecycle() assert.NotNil(lc, "expected Lifecycle instance to be created") assert.Equal(slog.Default(), lc.logger, "expected default logger to be set") } func TestNewLifecycleL(t *testing.T) { assert := assert.New(t) // Create a blank Lifecycle instance with a custom logger logger := slog.Default() lc := NewLifecycleL(logger) assert.NotNil(lc) assert.Equal(logger, lc.logger, "expected logger to be set") assert.Len(lc.modules, 0, "expected modules to be empty") assert.Len(lc.setupTracker, 0, "expected setup tracker to be empty") assert.Len(lc.teardownTracker, 0, "expected teardown tracker to be empty") // Test with modules mod1 := NewModule("module1", ModuleOpts{}) mod2 := NewModule("module2", ModuleOpts{}) lcWithModules := NewLifecycleL(logger, mod1, mod2) assert.NotNil(lcWithModules, "expected Lifecycle with modules to be created") assert.Contains(lcWithModules.modules, mod1, "expected module1 to be included") assert.Contains(lcWithModules.modules, mod2, "expected module2 to be included") assert.Len(lcWithModules.modules, 2, "expected two modules to be present") // Test for duplicate module names assert.PanicsWithValue("duplicate module name: module1", func() { NewLifecycleL(logger, mod1, mod1) }, "expected panic on duplicate module names") } func TestLifecycle_WithOpts(t *testing.T) { assert := assert.New(t) lc := NewLifecycle() opts := LifecycleOpts{ DisableAutoload: true, } lc.WithOpts(opts) assert.Equal(opts, lc.opts, "expected Lifecycle to have the provided options") } func TestLifecycle_Logger(t *testing.T) { assert := assert.New(t) lc := NewLifecycle() assert.Equal(slog.Default(), lc.Logger(), "expected Logger to return a logger") // Test panic when logger is not initialized lc.logger = nil assert.PanicsWithValue("lifecycle logger is not initialized", func() { lc.Logger() }, "expected Logger to panic when logger is not set") } func TestLifecycle_Setup(t *testing.T) { cases := []struct { name string modules []*Module expectedErr string }{ { name: "empty lifecycle", modules: nil, }, { name: "single module with setup", modules: []*Module{ NewModule("module1", ModuleOpts{ Setup: func(m *Module) error { return nil }, }), }, }, { name: "multiple modules", modules: []*Module{ NewModule("module1", ModuleOpts{}), NewModule("module2", ModuleOpts{ Setup: func(m *Module) error { return nil }, }), }, }, { name: "module with setup error", modules: []*Module{ NewModule("module1", ModuleOpts{ Setup: func(m *Module) error { return errors.New("setup failed") }, }), }, expectedErr: "setup failed", }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert := assert.New(t) lc := NewLifecycle(tc.modules...) err := lc.Setup() if tc.expectedErr == "" { assert.NoError(err, "expected Setup to succeed") for _, mod := range tc.modules { if mod.setup != nil { _, ok := lc.setupTracker[mod.name] assert.True(ok, "expected module to be marked as set up") } } } else { assert.Error(err, "expected Setup to fail") assert.Contains(err.Error(), tc.expectedErr, "expected error message to match") } }) } } func TestLifecycle_Teardown(t *testing.T) { cases := []struct { name string modules []*Module expectedErr []string }{ { name: "empty lifecycle", modules: nil, }, { name: "single module with teardown", modules: []*Module{ NewModule("module1", ModuleOpts{ Teardown: func(m *Module) error { return nil }, }), }, }, { name: "multiple modules", modules: []*Module{ NewModule("module1", ModuleOpts{}), NewModule("module2", ModuleOpts{ Teardown: func(m *Module) error { return nil }, }), }, }, { name: "module with teardown error", modules: []*Module{ NewModule("module1", ModuleOpts{ Teardown: func(m *Module) error { return errors.New("teardown failed") }, }), }, expectedErr: []string{"teardown failed"}, }, { name: "multiple modules with teardown errors", modules: []*Module{ NewModule("module1", ModuleOpts{ Teardown: func(m *Module) error { return errors.New("module1 teardown failed") }, }), NewModule("module2", ModuleOpts{ Teardown: func(m *Module) error { return errors.New("module2 teardown failed") }, }), }, expectedErr: []string{"module1 teardown failed", "module2 teardown failed"}, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert := assert.New(t) lc := NewLifecycle(tc.modules...) // Fake setup for all modules for _, mod := range tc.modules { lc.setupTracker[mod.name] = 0 // Mark as set up } err := lc.Teardown() if len(tc.expectedErr) == 0 { assert.NoError(err, "expected Teardown to succeed") for _, mod := range tc.modules { if mod.teardown != nil { _, ok := lc.teardownTracker[mod.name] assert.True(ok, "expected module to be marked as torn down") } } } else { assert.Error(err, "expected Teardown to fail") for _, expected := range tc.expectedErr { assert.Contains(err.Error(), expected, "expected error message to match") } } }) } } func TestLifecycle_Require(t *testing.T) { lc := NewLifecycle() mod := NewModule("module1", ModuleOpts{ Setup: func(m *Module) error { return nil }, }) lc.Require(mod) assert.Equal(t, lc.logger, mod.logger, "expected module logger to match lifecycle logger") } func TestLifecycle_RequireUnique(t *testing.T) { lc := NewLifecycle() mod := NewModule("module1", ModuleOpts{}) err := lc.RequireUnique(mod) assert.NoError(t, err, "expected RequireL to succeed") assert.Equal(t, lc.logger, mod.logger, "expected module logger to match lifecycle logger") } func TestLifecycle_RequireL(t *testing.T) { cases := []struct { name string logger *slog.Logger modules []*Module existingModules []*Module expectedErr string }{ { name: "no modules", expectedErr: "no modules to require", }, { name: "nil module", modules: []*Module{nil}, expectedErr: "module is nil", }, { name: "basic module", modules: []*Module{ NewModule("testModule", ModuleOpts{ Setup: func(m *Module) error { if m.name != "testModule" { return errors.New("module name mismatch") } return nil }, }), }, }, { name: "several modules", modules: []*Module{ NewModule("module1", ModuleOpts{ Setup: func(m *Module) error { return nil }, }), NewModule("module2", ModuleOpts{ Setup: func(m *Module) error { return nil }, }), }, }, { name: "module with setup error", modules: []*Module{ NewModule("errorModule", ModuleOpts{ Setup: func(m *Module) error { return errors.New("setup failed") }, }), }, expectedErr: "setup failed", }, { name: "module with dependency", modules: []*Module{ NewModule("dependentModule", ModuleOpts{ Setup: func(m *Module) error { return nil }, Depends: []string{"dependencyModule"}, }), }, existingModules: []*Module{ NewModule("dependencyModule", ModuleOpts{ Setup: func(m *Module) error { return nil }, }), }, }, { name: "duplicate module names", modules: []*Module{ NewModule("duplicateModule", ModuleOpts{}), NewModule("duplicateModule", ModuleOpts{}), }, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert := assert.New(t) lc := NewLifecycle(tc.existingModules...) l := tc.logger if l == nil { l = lc.logger } err := lc.RequireL(tc.logger, tc.modules...) if tc.expectedErr == "" { assert.NoError(err, "expected RequireL to succeed") for _, mod := range tc.modules { if mod != nil { _, ok := lc.setupTracker[mod.name] assert.True(ok, "expected module to be marked as set up") assert.True(mod.loaded, "expected module to be loaded") assert.Equal(l, mod.logger, "expected module logger to match") } } } else { assert.Error(err, "expected RequireL to fail") assert.Contains(err.Error(), tc.expectedErr, "expected error message to match") } }) } } func TestLifecycle_RequireUniqueL(t *testing.T) { cases := []struct { name string modules []*Module existingModules []string expectedErr string }{ { name: "no existing modules", modules: []*Module{ NewModule("module1", ModuleOpts{}), }, }, { name: "module already set up", modules: []*Module{ NewModule("module1", ModuleOpts{}), NewModule("module2", ModuleOpts{}), }, existingModules: []string{"module1"}, expectedErr: "module 'module1' is already set up, cannot require it again", }, { name: "nil module", modules: []*Module{nil}, expectedErr: "module is nil", }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert := assert.New(t) lc := NewLifecycle() // Fake existing modules for _, modName := range tc.existingModules { lc.setupTracker[modName] = 0 // Mark as set up } err := lc.RequireUniqueL(lc.logger, tc.modules...) if tc.expectedErr == "" { assert.NoError(err, "expected RequireUniqueL to succeed") } else { assert.Error(err, "expected RequireUniqueL to fail") assert.Contains(err.Error(), tc.expectedErr, "expected error message to match") } }) } } func TestLifecycle_setupSingle(t *testing.T) { cases := []struct { name string lc *Lifecycle targetModule *Module modules []*Module expectedErr string logger *slog.Logger }{ { name: "nil module", targetModule: nil, expectedErr: "module is nil", }, { name: "basic module setup", targetModule: NewModule("testModule", ModuleOpts{ Setup: func(m *Module) error { if m.name != "testModule" { return errors.New("module name mismatch") } return nil }, }), }, { name: "module with dependencies", targetModule: NewModule("dependentModule", ModuleOpts{ Setup: func(m *Module) error { return nil }, Depends: []string{"dependencyModule"}, }), modules: []*Module{ NewModule("dependencyModule", ModuleOpts{ Setup: func(m *Module) error { return nil }, }), }, }, { name: "module with missing dependency", targetModule: NewModule("dependentModule", ModuleOpts{ Setup: func(m *Module) error { return nil }, Depends: []string{"missingDependency"}, }), expectedErr: "error getting dependency", }, { name: "module setup error", targetModule: NewModule("errorModule", ModuleOpts{ Setup: func(m *Module) error { return errors.New("setup failed") }, }), expectedErr: "setup failed", }, { name: "module dependency setup error", targetModule: NewModule("moduleWithoutError", ModuleOpts{ Setup: func(m *Module) error { return nil }, Depends: []string{"errorModule"}, }), modules: []*Module{ NewModule("errorModule", ModuleOpts{ Setup: func(m *Module) error { return errors.New("dependency setup failed") }, }), }, expectedErr: "error initializing 'errorModule'", }, { name: "dependency without autoload", lc: NewLifecycle().WithOpts(LifecycleOpts{ DisableAutoload: true, }), targetModule: NewModule("dependentModule", ModuleOpts{ Setup: func(m *Module) error { return nil }, Depends: []string{"dependencyModule"}, }), expectedErr: "not satisfied", }, { name: "module without setup function", targetModule: NewModule("noSetupModule", ModuleOpts{}), }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert := assert.New(t) l := tc.logger if l == nil { l = slog.Default() } lc := tc.lc if lc == nil { lc = NewLifecycle(tc.modules...) } err := lc.setupSingle(tc.targetModule, l) if tc.targetModule != nil { assert.Equal(l, tc.targetModule.logger, "expected module logger to match") } if tc.expectedErr == "" { assert.NoError(err, "expected no error from setupSingle") } else { assert.Contains(err.Error(), tc.expectedErr, "expected error message to match") } }) } } func TestLifecycle_teardownSingle(t *testing.T) { cases := []struct { name string targetModule *Module modules []string expectedErr string }{ { name: "nil module", expectedErr: "module is nil", }, { name: "basic module teardown", targetModule: NewModule("testModule", ModuleOpts{ Teardown: func(m *Module) error { if m.name != "testModule" { return errors.New("module name mismatch") } return nil }, }), modules: []string{"testModule"}, }, { name: "module not set up", targetModule: NewModule("notSetUpModule", ModuleOpts{ Teardown: func(m *Module) error { return nil }, }), expectedErr: "not set up, cannot tear down", }, { name: "module without teardown function", targetModule: NewModule("noTeardownModule", ModuleOpts{}), modules: []string{"noTeardownModule"}, }, { name: "module with teardown error", targetModule: NewModule("errorModule", ModuleOpts{ Teardown: func(m *Module) error { return errors.New("teardown failed") }, }), modules: []string{"errorModule"}, expectedErr: "error tearing down", }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert := assert.New(t) lc := NewLifecycle() // Fake setup for all modules var setupCount int for _, mod := range tc.modules { lc.setupTracker[mod] = setupCount setupCount++ } err := lc.teardownSingle(tc.targetModule) if tc.expectedErr == "" { assert.NoError(err, "expected no error from teardownSingle") } else { assert.Contains(err.Error(), tc.expectedErr, "expected error message to match") } }) } } func Test_mapToString(t *testing.T) { cases := []struct { name string input map[string]int expected string }{ { name: "empty map", input: map[string]int{}, expected: "[]", }, { name: "single entry", input: map[string]int{"key1": 0}, expected: "['key1']", }, { name: "multiple entries", input: map[string]int{"key1": 0, "key2": 3, "key3": 2}, expected: "['key1' 'key3' 'key2']", }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert := assert.New(t) result := mapToString(tc.input) assert.Equal(tc.expected, result, "expected mapToString to return correct string representation") }) } }