package app import ( "context" "errors" "log/slog" "os" "testing" "github.com/stretchr/testify/assert" ) func TestNewLifecycle(t *testing.T) { assert := assert.New(t) // Create a blank Lifecycle instance lc := NewLifecycle() assert.NotNil(lc, "expected Lifecycle instance to be created") assert.Equal(slog.Default(), lc.Logger(), "expected default logger to be available") // Create a blank Lifecycle instance with a custom logger handler := slog.NewTextHandler(os.Stderr, nil) logger := slog.New(handler) lc = NewLifecycle().WithOpts(LifecycleOpts{ Logger: logger, }) assert.NotNil(lc) assert.Equal(logger, lc.Logger(), "expected logger to match") 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 := NewLifecycle(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: [module1]", func() { NewLifecycle(mod1, mod1) }, "expected panic on duplicate module names") } func TestLifecycleToFromContext(t *testing.T) { assert := assert.New(t) // Create a Lifecycle instance lc := NewLifecycle() // Add the Lifecycle to the context ctx := LifecycleToContext(context.Background(), lc) // Retrieve the Lifecycle from the context retrievedLC := LifecycleFromContext(ctx) assert.NotNil(retrievedLC, "expected Lifecycle to be retrieved from context") assert.Equal(lc, retrievedLC, "expected retrieved Lifecycle to match original") // Test with nil Lifecycle nilCtx := LifecycleToContext(context.Background(), nil) retrievedNilLC := LifecycleFromContext(nilCtx) assert.Nil(retrievedNilLC, "expected nil Lifecycle to return nil from context") } 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_WithLogger(t *testing.T) { assert := assert.New(t) lc := NewLifecycle() logger := slog.New(slog.NewTextHandler(os.Stderr, nil)) lc.WithLogger(logger) assert.Equal(logger, lc.Logger(), "expected Lifecycle to have the provided logger") assert.PanicsWithValue("logger cannot be nil", func() { lc.WithLogger(nil) }, "expected panic when setting nil logger") } 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 that custom logger is used logger := slog.New(slog.NewTextHandler(os.Stderr, nil)) lc.opts.Logger = logger assert.Equal(logger, lc.Logger()) // Test with nil Lifecycle assert.PanicsWithValue("lifecycle is nil, cannot get logger", func() { var nilLC *Lifecycle nilLC.Logger() }, "expected panic when calling Logger on nil Lifecycle") } 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 { mod.loaded = true // Mark as loaded 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.Truef(ok, "expected module %s to be marked as torn down", mod) assert.Falsef(mod.loaded, "expected module %s to be unloaded after teardown", mod) } } } 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) { assert := assert.New(t) lc := NewLifecycle() // Test that duplicate modules are allowed mod1 := NewModule("module1", ModuleOpts{}) mod2 := NewModule("module1", ModuleOpts{}) err := lc.Require(mod1, mod2) assert.NoError(err, "expected Require to succeed with duplicate modules") assert.Len(lc.modules, 1, "expected only one instance of the module to be added") assert.Equal(lc.Logger(), mod1.Logger(), "expected module logger to match lifecycle logger") } func TestLifecycle_RequireUnique(t *testing.T) { assert := assert.New(t) lc := NewLifecycle() mod := NewModule("module1", ModuleOpts{}) err := lc.RequireUnique(mod, mod) assert.Error(err, "expected RequireUnique to fail on duplicate module") } func TestLifecycle_RequireL(t *testing.T) { assert := assert.New(t) // Create a logger for the test cases logger := slog.New(slog.NewTextHandler(os.Stderr, nil)) lc := NewLifecycle() // Test that duplicate modules are allowed with RequireL mod1 := NewModule("module1", ModuleOpts{}) mod2 := NewModule("module1", ModuleOpts{}) err := lc.RequireL(logger, mod1, mod2) assert.NoError(err, "expected RequireL to succeed with duplicate modules") assert.Len(lc.modules, 1, "expected only one instance of the module to be added") assert.Equal(logger, mod1.Logger(), "expected module logger to match provided logger") // Test with nil logger err = lc.RequireL(nil, mod1) assert.Error(err, "expected RequireL to fail with nil logger") } func TestLifecycle_RequireUniqueL(t *testing.T) { assert := assert.New(t) // Create a logger for the test cases logger := slog.New(slog.NewTextHandler(os.Stderr, nil)) lc := NewLifecycle() // Test that duplicate modules are not allowed with RequireUniqueL mod1 := NewModule("module1", ModuleOpts{}) mod2 := NewModule("module1", ModuleOpts{}) err := lc.RequireUniqueL(logger, mod1, mod2) assert.Error(err, "expected RequireUniqueL to fail on duplicate module") assert.Contains(err.Error(), "cannot require again", "expected error message to match") // Test with nil logger err = lc.RequireUniqueL(nil, mod1) assert.Error(err, "expected RequireUniqueL to fail with nil logger") } func TestLifecycle_require(t *testing.T) { cases := []struct { name string logger *slog.Logger unique bool modules []*Module expectedErr string length int }{ { name: "no modules", expectedErr: "no modules to require", }, { name: "nil module", modules: []*Module{ NewModule("", ModuleOpts{}), nil, }, expectedErr: "module 1 is nil", }, { name: "basic modules", modules: []*Module{ NewModule("module1", ModuleOpts{}), NewModule("module2", ModuleOpts{}), }, }, { name: "module with setup error", modules: []*Module{ NewModule("errorModule", ModuleOpts{ Setup: func(m *Module) error { return errors.New("setup failed") }, }), }, expectedErr: "error setting up required module", }, { name: "duplicate module names", modules: []*Module{ NewModule("duplicateModule", ModuleOpts{}), NewModule("duplicateModule", ModuleOpts{}), }, length: 1, // Expect only one instance to be added }, { name: "duplicate module names with unique", unique: true, modules: []*Module{ NewModule("duplicateModule", ModuleOpts{}), NewModule("duplicateModule", ModuleOpts{}), }, expectedErr: "cannot require again", }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert := assert.New(t) lc := NewLifecycle() err := lc.require(tc.logger, tc.unique, tc.modules...) if tc.expectedErr == "" { assert.NoError(err, "expected require to succeed") expectedLength := tc.length if expectedLength == 0 { expectedLength = len(tc.modules) } assert.Equal(expectedLength, len(lc.modules), "expected all modules to be added to lifecycle") for _, mod := range tc.modules { if mod != nil { _, ok := lc.setupTracker[mod.name] assert.Truef(ok, "expected module %s to be marked as set up", mod) assert.Truef(mod.loaded, "expected module %s to be loaded", mod) assert.Equalf(lc, mod.lifecycle, "expected module %s lifecycle to match", mod) if tc.logger != nil { assert.Equalf(tc.logger, mod.Logger(), "expected module %s logger to match", mod) } else { assert.Equalf(lc.Logger(), mod.Logger(), "expected module %s logger to match lifecycle logger", mod) } } } } else { assert.Error(err, "expected require to fail") assert.Contains(err.Error(), tc.expectedErr, "expected error message to match") } }) } } func TestLifecycle_setupSingle(t *testing.T) { // Create a logger for the test cases logger := slog.New(slog.NewTextHandler(os.Stderr, nil)) 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: "dependency setup failed", }, { 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 = logger } lc := tc.lc if lc == nil { lc = NewLifecycle(tc.modules...) } err := lc.setupSingle(l, tc.targetModule) if tc.expectedErr == "" { assert.NoError(err, "expected no error from setupSingle") } else { assert.Contains(err.Error(), tc.expectedErr, "expected error message to match") } if tc.targetModule != nil { assert.Equal(l, tc.targetModule.logger, "expected module logger 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") }) } }