feat(usecase): add MigrateConfigUseCase for checking and applying config migration
- commit
- dfe055616e30c702f7576a8aad11bd2deb3e2b80
- author
- bnema · · 2025-12-27 18:47 UTC
- parent
- 607f4ad
- signature
- GPG Unverified
2B82207D370127A0The key is unknown
2 files · +324 -0
A internal/application/usecase/migrate_config.go +118 -0
| @@ -0,0 +1,118 @@ | |||
| 1 | + | package usecase | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/dumber/internal/application/port" | |
| 7 | + | "github.com/bnema/dumber/internal/logging" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | // CheckConfigMigrationInput holds the input for checking config migration. | |
| 11 | + | type CheckConfigMigrationInput struct{} | |
| 12 | + | ||
| 13 | + | // CheckConfigMigrationOutput holds the result of the migration check. | |
| 14 | + | type CheckConfigMigrationOutput struct { | |
| 15 | + | // NeedsMigration is true if there are missing keys. | |
| 16 | + | NeedsMigration bool | |
| 17 | + | // MissingKeys contains info about each missing key. | |
| 18 | + | MissingKeys []port.KeyInfo | |
| 19 | + | // ConfigFile is the path to the config file. | |
| 20 | + | ConfigFile string | |
| 21 | + | } | |
| 22 | + | ||
| 23 | + | // MigrateConfigInput holds the input for migrating config. | |
| 24 | + | type MigrateConfigInput struct{} | |
| 25 | + | ||
| 26 | + | // MigrateConfigOutput holds the result of the migration. | |
| 27 | + | type MigrateConfigOutput struct { | |
| 28 | + | // AddedKeys contains the keys that were added. | |
| 29 | + | AddedKeys []string | |
| 30 | + | // ConfigFile is the path to the config file. | |
| 31 | + | ConfigFile string | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | // MigrateConfigUseCase handles config migration operations. | |
| 35 | + | type MigrateConfigUseCase struct { | |
| 36 | + | migrator port.ConfigMigrator | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | // NewMigrateConfigUseCase creates a new migrate config use case. | |
| 40 | + | func NewMigrateConfigUseCase(migrator port.ConfigMigrator) *MigrateConfigUseCase { | |
| 41 | + | return &MigrateConfigUseCase{ | |
| 42 | + | migrator: migrator, | |
| 43 | + | } | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | // Check checks if the user config is missing any default keys. | |
| 47 | + | func (uc *MigrateConfigUseCase) Check(ctx context.Context, _ CheckConfigMigrationInput) (*CheckConfigMigrationOutput, error) { | |
| 48 | + | log := logging.FromContext(ctx) | |
| 49 | + | ||
| 50 | + | result, err := uc.migrator.CheckMigration() | |
| 51 | + | if err != nil { | |
| 52 | + | log.Warn().Err(err).Msg("config migration check failed") | |
| 53 | + | return nil, err | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | // No migration needed | |
| 57 | + | if result == nil || len(result.MissingKeys) == 0 { | |
| 58 | + | log.Debug().Msg("config is up to date, no migration needed") | |
| 59 | + | return &CheckConfigMigrationOutput{ | |
| 60 | + | NeedsMigration: false, | |
| 61 | + | MissingKeys: nil, | |
| 62 | + | ConfigFile: "", | |
| 63 | + | }, nil | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | // Build key info for each missing key | |
| 67 | + | keyInfos := make([]port.KeyInfo, 0, len(result.MissingKeys)) | |
| 68 | + | for _, key := range result.MissingKeys { | |
| 69 | + | keyInfos = append(keyInfos, uc.migrator.GetKeyInfo(key)) | |
| 70 | + | } | |
| 71 | + | ||
| 72 | + | log.Debug(). | |
| 73 | + | Int("missing_keys", len(result.MissingKeys)). | |
| 74 | + | Str("config_file", result.ConfigFile). | |
| 75 | + | Msg("config migration check completed") | |
| 76 | + | ||
| 77 | + | return &CheckConfigMigrationOutput{ | |
| 78 | + | NeedsMigration: true, | |
| 79 | + | MissingKeys: keyInfos, | |
| 80 | + | ConfigFile: result.ConfigFile, | |
| 81 | + | }, nil | |
| 82 | + | } | |
| 83 | + | ||
| 84 | + | // Execute adds missing default keys to the user's config file. | |
| 85 | + | func (uc *MigrateConfigUseCase) Execute(ctx context.Context, _ MigrateConfigInput) (*MigrateConfigOutput, error) { | |
| 86 | + | log := logging.FromContext(ctx) | |
| 87 | + | ||
| 88 | + | // First check if migration is needed | |
| 89 | + | checkResult, err := uc.migrator.CheckMigration() | |
| 90 | + | if err != nil { | |
| 91 | + | return nil, err | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | if checkResult == nil || len(checkResult.MissingKeys) == 0 { | |
| 95 | + | log.Debug().Msg("no migration needed") | |
| 96 | + | return &MigrateConfigOutput{ | |
| 97 | + | AddedKeys: nil, | |
| 98 | + | ConfigFile: "", | |
| 99 | + | }, nil | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | // Perform migration | |
| 103 | + | addedKeys, err := uc.migrator.Migrate() | |
| 104 | + | if err != nil { | |
| 105 | + | log.Error().Err(err).Msg("config migration failed") | |
| 106 | + | return nil, err | |
| 107 | + | } | |
| 108 | + | ||
| 109 | + | log.Info(). | |
| 110 | + | Int("added_keys", len(addedKeys)). | |
| 111 | + | Str("config_file", checkResult.ConfigFile). | |
| 112 | + | Msg("config migration completed") | |
| 113 | + | ||
| 114 | + | return &MigrateConfigOutput{ | |
| 115 | + | AddedKeys: addedKeys, | |
| 116 | + | ConfigFile: checkResult.ConfigFile, | |
| 117 | + | }, nil | |
| 118 | + | } | |
| @@ -0,0 +1,118 @@ | |||||
| 1 | + | package usecase | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | ||||
| 6 | + | "github.com/bnema/dumber/internal/application/port" | |||
| 7 | + | "github.com/bnema/dumber/internal/logging" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | // CheckConfigMigrationInput holds the input for checking config migration. | |||
| 11 | + | type CheckConfigMigrationInput struct{} | |||
| 12 | + | ||||
| 13 | + | // CheckConfigMigrationOutput holds the result of the migration check. | |||
| 14 | + | type CheckConfigMigrationOutput struct { | |||
| 15 | + | // NeedsMigration is true if there are missing keys. | |||
| 16 | + | NeedsMigration bool | |||
| 17 | + | // MissingKeys contains info about each missing key. | |||
| 18 | + | MissingKeys []port.KeyInfo | |||
| 19 | + | // ConfigFile is the path to the config file. | |||
| 20 | + | ConfigFile string | |||
| 21 | + | } | |||
| 22 | + | ||||
| 23 | + | // MigrateConfigInput holds the input for migrating config. | |||
| 24 | + | type MigrateConfigInput struct{} | |||
| 25 | + | ||||
| 26 | + | // MigrateConfigOutput holds the result of the migration. | |||
| 27 | + | type MigrateConfigOutput struct { | |||
| 28 | + | // AddedKeys contains the keys that were added. | |||
| 29 | + | AddedKeys []string | |||
| 30 | + | // ConfigFile is the path to the config file. | |||
| 31 | + | ConfigFile string | |||
| 32 | + | } | |||
| 33 | + | ||||
| 34 | + | // MigrateConfigUseCase handles config migration operations. | |||
| 35 | + | type MigrateConfigUseCase struct { | |||
| 36 | + | migrator port.ConfigMigrator | |||
| 37 | + | } | |||
| 38 | + | ||||
| 39 | + | // NewMigrateConfigUseCase creates a new migrate config use case. | |||
| 40 | + | func NewMigrateConfigUseCase(migrator port.ConfigMigrator) *MigrateConfigUseCase { | |||
| 41 | + | return &MigrateConfigUseCase{ | |||
| 42 | + | migrator: migrator, | |||
| 43 | + | } | |||
| 44 | + | } | |||
| 45 | + | ||||
| 46 | + | // Check checks if the user config is missing any default keys. | |||
| 47 | + | func (uc *MigrateConfigUseCase) Check(ctx context.Context, _ CheckConfigMigrationInput) (*CheckConfigMigrationOutput, error) { | |||
| 48 | + | log := logging.FromContext(ctx) | |||
| 49 | + | ||||
| 50 | + | result, err := uc.migrator.CheckMigration() | |||
| 51 | + | if err != nil { | |||
| 52 | + | log.Warn().Err(err).Msg("config migration check failed") | |||
| 53 | + | return nil, err | |||
| 54 | + | } | |||
| 55 | + | ||||
| 56 | + | // No migration needed | |||
| 57 | + | if result == nil || len(result.MissingKeys) == 0 { | |||
| 58 | + | log.Debug().Msg("config is up to date, no migration needed") | |||
| 59 | + | return &CheckConfigMigrationOutput{ | |||
| 60 | + | NeedsMigration: false, | |||
| 61 | + | MissingKeys: nil, | |||
| 62 | + | ConfigFile: "", | |||
| 63 | + | }, nil | |||
| 64 | + | } | |||
| 65 | + | ||||
| 66 | + | // Build key info for each missing key | |||
| 67 | + | keyInfos := make([]port.KeyInfo, 0, len(result.MissingKeys)) | |||
| 68 | + | for _, key := range result.MissingKeys { | |||
| 69 | + | keyInfos = append(keyInfos, uc.migrator.GetKeyInfo(key)) | |||
| 70 | + | } | |||
| 71 | + | ||||
| 72 | + | log.Debug(). | |||
| 73 | + | Int("missing_keys", len(result.MissingKeys)). | |||
| 74 | + | Str("config_file", result.ConfigFile). | |||
| 75 | + | Msg("config migration check completed") | |||
| 76 | + | ||||
| 77 | + | return &CheckConfigMigrationOutput{ | |||
| 78 | + | NeedsMigration: true, | |||
| 79 | + | MissingKeys: keyInfos, | |||
| 80 | + | ConfigFile: result.ConfigFile, | |||
| 81 | + | }, nil | |||
| 82 | + | } | |||
| 83 | + | ||||
| 84 | + | // Execute adds missing default keys to the user's config file. | |||
| 85 | + | func (uc *MigrateConfigUseCase) Execute(ctx context.Context, _ MigrateConfigInput) (*MigrateConfigOutput, error) { | |||
| 86 | + | log := logging.FromContext(ctx) | |||
| 87 | + | ||||
| 88 | + | // First check if migration is needed | |||
| 89 | + | checkResult, err := uc.migrator.CheckMigration() | |||
| 90 | + | if err != nil { | |||
| 91 | + | return nil, err | |||
| 92 | + | } | |||
| 93 | + | ||||
| 94 | + | if checkResult == nil || len(checkResult.MissingKeys) == 0 { | |||
| 95 | + | log.Debug().Msg("no migration needed") | |||
| 96 | + | return &MigrateConfigOutput{ | |||
| 97 | + | AddedKeys: nil, | |||
| 98 | + | ConfigFile: "", | |||
| 99 | + | }, nil | |||
| 100 | + | } | |||
| 101 | + | ||||
| 102 | + | // Perform migration | |||
| 103 | + | addedKeys, err := uc.migrator.Migrate() | |||
| 104 | + | if err != nil { | |||
| 105 | + | log.Error().Err(err).Msg("config migration failed") | |||
| 106 | + | return nil, err | |||
| 107 | + | } | |||
| 108 | + | ||||
| 109 | + | log.Info(). | |||
| 110 | + | Int("added_keys", len(addedKeys)). | |||
| 111 | + | Str("config_file", checkResult.ConfigFile). | |||
| 112 | + | Msg("config migration completed") | |||
| 113 | + | ||||
| 114 | + | return &MigrateConfigOutput{ | |||
| 115 | + | AddedKeys: addedKeys, | |||
| 116 | + | ConfigFile: checkResult.ConfigFile, | |||
| 117 | + | }, nil | |||
| 118 | + | } | |||
A internal/application/usecase/migrate_config_test.go +206 -0
| @@ -0,0 +1,206 @@ | |||
| 1 | + | package usecase | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "errors" | |
| 6 | + | "testing" | |
| 7 | + | ||
| 8 | + | "github.com/stretchr/testify/assert" | |
| 9 | + | "github.com/stretchr/testify/mock" | |
| 10 | + | "github.com/stretchr/testify/require" | |
| 11 | + | ||
| 12 | + | "github.com/bnema/dumber/internal/application/port" | |
| 13 | + | "github.com/bnema/dumber/internal/application/port/mocks" | |
| 14 | + | ) | |
| 15 | + | ||
| 16 | + | func TestMigrateConfigUseCase_Check_NoMigrationNeeded(t *testing.T) { | |
| 17 | + | mockMigrator := mocks.NewMockConfigMigrator(t) | |
| 18 | + | mockMigrator.EXPECT().CheckMigration().Return(nil, nil) | |
| 19 | + | ||
| 20 | + | uc := NewMigrateConfigUseCase(mockMigrator) | |
| 21 | + | ctx := context.Background() | |
| 22 | + | ||
| 23 | + | result, err := uc.Check(ctx, CheckConfigMigrationInput{}) | |
| 24 | + | ||
| 25 | + | require.NoError(t, err) | |
| 26 | + | assert.False(t, result.NeedsMigration) | |
| 27 | + | assert.Empty(t, result.MissingKeys) | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | func TestMigrateConfigUseCase_Check_MigrationNeeded(t *testing.T) { | |
| 31 | + | mockMigrator := mocks.NewMockConfigMigrator(t) | |
| 32 | + | ||
| 33 | + | migrationResult := &port.MigrationResult{ | |
| 34 | + | MissingKeys: []string{"key1", "key2"}, | |
| 35 | + | ConfigFile: "/path/to/config.toml", | |
| 36 | + | } | |
| 37 | + | mockMigrator.EXPECT().CheckMigration().Return(migrationResult, nil) | |
| 38 | + | ||
| 39 | + | // Mock GetKeyInfo for each missing key | |
| 40 | + | mockMigrator.EXPECT().GetKeyInfo("key1").Return(port.KeyInfo{ | |
| 41 | + | Key: "key1", | |
| 42 | + | Type: "bool", | |
| 43 | + | DefaultValue: "true", | |
| 44 | + | }) | |
| 45 | + | mockMigrator.EXPECT().GetKeyInfo("key2").Return(port.KeyInfo{ | |
| 46 | + | Key: "key2", | |
| 47 | + | Type: "string", | |
| 48 | + | DefaultValue: `"default"`, | |
| 49 | + | }) | |
| 50 | + | ||
| 51 | + | uc := NewMigrateConfigUseCase(mockMigrator) | |
| 52 | + | ctx := context.Background() | |
| 53 | + | ||
| 54 | + | result, err := uc.Check(ctx, CheckConfigMigrationInput{}) | |
| 55 | + | ||
| 56 | + | require.NoError(t, err) | |
| 57 | + | assert.True(t, result.NeedsMigration) | |
| 58 | + | assert.Len(t, result.MissingKeys, 2) | |
| 59 | + | assert.Equal(t, "/path/to/config.toml", result.ConfigFile) | |
| 60 | + | ||
| 61 | + | // Verify key info | |
| 62 | + | assert.Equal(t, "key1", result.MissingKeys[0].Key) | |
| 63 | + | assert.Equal(t, "bool", result.MissingKeys[0].Type) | |
| 64 | + | assert.Equal(t, "key2", result.MissingKeys[1].Key) | |
| 65 | + | assert.Equal(t, "string", result.MissingKeys[1].Type) | |
| 66 | + | } | |
| 67 | + | ||
| 68 | + | func TestMigrateConfigUseCase_Check_Error(t *testing.T) { | |
| 69 | + | mockMigrator := mocks.NewMockConfigMigrator(t) | |
| 70 | + | expectedErr := errors.New("check failed") | |
| 71 | + | mockMigrator.EXPECT().CheckMigration().Return(nil, expectedErr) | |
| 72 | + | ||
| 73 | + | uc := NewMigrateConfigUseCase(mockMigrator) | |
| 74 | + | ctx := context.Background() | |
| 75 | + | ||
| 76 | + | result, err := uc.Check(ctx, CheckConfigMigrationInput{}) | |
| 77 | + | ||
| 78 | + | require.Error(t, err) | |
| 79 | + | assert.Nil(t, result) | |
| 80 | + | assert.Equal(t, expectedErr, err) | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | func TestMigrateConfigUseCase_Execute_NoMigrationNeeded(t *testing.T) { | |
| 84 | + | mockMigrator := mocks.NewMockConfigMigrator(t) | |
| 85 | + | mockMigrator.EXPECT().CheckMigration().Return(nil, nil) | |
| 86 | + | ||
| 87 | + | uc := NewMigrateConfigUseCase(mockMigrator) | |
| 88 | + | ctx := context.Background() | |
| 89 | + | ||
| 90 | + | result, err := uc.Execute(ctx, MigrateConfigInput{}) | |
| 91 | + | ||
| 92 | + | require.NoError(t, err) | |
| 93 | + | assert.Empty(t, result.AddedKeys) | |
| 94 | + | } | |
| 95 | + | ||
| 96 | + | func TestMigrateConfigUseCase_Execute_Success(t *testing.T) { | |
| 97 | + | mockMigrator := mocks.NewMockConfigMigrator(t) | |
| 98 | + | ||
| 99 | + | migrationResult := &port.MigrationResult{ | |
| 100 | + | MissingKeys: []string{"key1", "key2"}, | |
| 101 | + | ConfigFile: "/path/to/config.toml", | |
| 102 | + | } | |
| 103 | + | mockMigrator.EXPECT().CheckMigration().Return(migrationResult, nil) | |
| 104 | + | mockMigrator.EXPECT().Migrate().Return([]string{"key1", "key2"}, nil) | |
| 105 | + | ||
| 106 | + | uc := NewMigrateConfigUseCase(mockMigrator) | |
| 107 | + | ctx := context.Background() | |
| 108 | + | ||
| 109 | + | result, err := uc.Execute(ctx, MigrateConfigInput{}) | |
| 110 | + | ||
| 111 | + | require.NoError(t, err) | |
| 112 | + | assert.Len(t, result.AddedKeys, 2) | |
| 113 | + | assert.Equal(t, "/path/to/config.toml", result.ConfigFile) | |
| 114 | + | } | |
| 115 | + | ||
| 116 | + | func TestMigrateConfigUseCase_Execute_MigrateError(t *testing.T) { | |
| 117 | + | mockMigrator := mocks.NewMockConfigMigrator(t) | |
| 118 | + | ||
| 119 | + | migrationResult := &port.MigrationResult{ | |
| 120 | + | MissingKeys: []string{"key1"}, | |
| 121 | + | ConfigFile: "/path/to/config.toml", | |
| 122 | + | } | |
| 123 | + | mockMigrator.EXPECT().CheckMigration().Return(migrationResult, nil) | |
| 124 | + | ||
| 125 | + | expectedErr := errors.New("migrate failed") | |
| 126 | + | mockMigrator.EXPECT().Migrate().Return(nil, expectedErr) | |
| 127 | + | ||
| 128 | + | uc := NewMigrateConfigUseCase(mockMigrator) | |
| 129 | + | ctx := context.Background() | |
| 130 | + | ||
| 131 | + | result, err := uc.Execute(ctx, MigrateConfigInput{}) | |
| 132 | + | ||
| 133 | + | require.Error(t, err) | |
| 134 | + | assert.Nil(t, result) | |
| 135 | + | assert.Equal(t, expectedErr, err) | |
| 136 | + | } | |
| 137 | + | ||
| 138 | + | func TestMigrateConfigUseCase_Execute_CheckError(t *testing.T) { | |
| 139 | + | mockMigrator := mocks.NewMockConfigMigrator(t) | |
| 140 | + | ||
| 141 | + | expectedErr := errors.New("check failed") | |
| 142 | + | mockMigrator.EXPECT().CheckMigration().Return(nil, expectedErr) | |
| 143 | + | ||
| 144 | + | uc := NewMigrateConfigUseCase(mockMigrator) | |
| 145 | + | ctx := context.Background() | |
| 146 | + | ||
| 147 | + | result, err := uc.Execute(ctx, MigrateConfigInput{}) | |
| 148 | + | ||
| 149 | + | require.Error(t, err) | |
| 150 | + | assert.Nil(t, result) | |
| 151 | + | assert.Equal(t, expectedErr, err) | |
| 152 | + | } | |
| 153 | + | ||
| 154 | + | func TestMigrateConfigUseCase_Check_EmptyMissingKeys(t *testing.T) { | |
| 155 | + | mockMigrator := mocks.NewMockConfigMigrator(t) | |
| 156 | + | ||
| 157 | + | // Return a result with empty missing keys | |
| 158 | + | migrationResult := &port.MigrationResult{ | |
| 159 | + | MissingKeys: []string{}, | |
| 160 | + | ConfigFile: "/path/to/config.toml", | |
| 161 | + | } | |
| 162 | + | mockMigrator.EXPECT().CheckMigration().Return(migrationResult, nil) | |
| 163 | + | ||
| 164 | + | uc := NewMigrateConfigUseCase(mockMigrator) | |
| 165 | + | ctx := context.Background() | |
| 166 | + | ||
| 167 | + | result, err := uc.Check(ctx, CheckConfigMigrationInput{}) | |
| 168 | + | ||
| 169 | + | require.NoError(t, err) | |
| 170 | + | assert.False(t, result.NeedsMigration) | |
| 171 | + | assert.Empty(t, result.MissingKeys) | |
| 172 | + | } | |
| 173 | + | ||
| 174 | + | func TestNewMigrateConfigUseCase(t *testing.T) { | |
| 175 | + | mockMigrator := mocks.NewMockConfigMigrator(t) | |
| 176 | + | uc := NewMigrateConfigUseCase(mockMigrator) | |
| 177 | + | ||
| 178 | + | assert.NotNil(t, uc) | |
| 179 | + | assert.Equal(t, mockMigrator, uc.migrator) | |
| 180 | + | } | |
| 181 | + | ||
| 182 | + | // Ensure mock expectations are set up correctly | |
| 183 | + | func TestMigrateConfigUseCase_MockExpectations(t *testing.T) { | |
| 184 | + | mockMigrator := mocks.NewMockConfigMigrator(t) | |
| 185 | + | ||
| 186 | + | // Set up expectations with mock.Anything for flexibility | |
| 187 | + | mockMigrator.On("CheckMigration").Return(&port.MigrationResult{ | |
| 188 | + | MissingKeys: []string{"test.key"}, | |
| 189 | + | ConfigFile: "/test/config.toml", | |
| 190 | + | }, nil).Once() | |
| 191 | + | ||
| 192 | + | mockMigrator.On("GetKeyInfo", mock.AnythingOfType("string")).Return(port.KeyInfo{ | |
| 193 | + | Key: "test.key", | |
| 194 | + | Type: "string", | |
| 195 | + | DefaultValue: `"test"`, | |
| 196 | + | }).Once() | |
| 197 | + | ||
| 198 | + | uc := NewMigrateConfigUseCase(mockMigrator) | |
| 199 | + | ctx := context.Background() | |
| 200 | + | ||
| 201 | + | result, err := uc.Check(ctx, CheckConfigMigrationInput{}) | |
| 202 | + | ||
| 203 | + | require.NoError(t, err) | |
| 204 | + | assert.True(t, result.NeedsMigration) | |
| 205 | + | mockMigrator.AssertExpectations(t) | |
| 206 | + | } | |
| @@ -0,0 +1,206 @@ | |||||
| 1 | + | package usecase | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "errors" | |||
| 6 | + | "testing" | |||
| 7 | + | ||||
| 8 | + | "github.com/stretchr/testify/assert" | |||
| 9 | + | "github.com/stretchr/testify/mock" | |||
| 10 | + | "github.com/stretchr/testify/require" | |||
| 11 | + | ||||
| 12 | + | "github.com/bnema/dumber/internal/application/port" | |||
| 13 | + | "github.com/bnema/dumber/internal/application/port/mocks" | |||
| 14 | + | ) | |||
| 15 | + | ||||
| 16 | + | func TestMigrateConfigUseCase_Check_NoMigrationNeeded(t *testing.T) { | |||
| 17 | + | mockMigrator := mocks.NewMockConfigMigrator(t) | |||
| 18 | + | mockMigrator.EXPECT().CheckMigration().Return(nil, nil) | |||
| 19 | + | ||||
| 20 | + | uc := NewMigrateConfigUseCase(mockMigrator) | |||
| 21 | + | ctx := context.Background() | |||
| 22 | + | ||||
| 23 | + | result, err := uc.Check(ctx, CheckConfigMigrationInput{}) | |||
| 24 | + | ||||
| 25 | + | require.NoError(t, err) | |||
| 26 | + | assert.False(t, result.NeedsMigration) | |||
| 27 | + | assert.Empty(t, result.MissingKeys) | |||
| 28 | + | } | |||
| 29 | + | ||||
| 30 | + | func TestMigrateConfigUseCase_Check_MigrationNeeded(t *testing.T) { | |||
| 31 | + | mockMigrator := mocks.NewMockConfigMigrator(t) | |||
| 32 | + | ||||
| 33 | + | migrationResult := &port.MigrationResult{ | |||
| 34 | + | MissingKeys: []string{"key1", "key2"}, | |||
| 35 | + | ConfigFile: "/path/to/config.toml", | |||
| 36 | + | } | |||
| 37 | + | mockMigrator.EXPECT().CheckMigration().Return(migrationResult, nil) | |||
| 38 | + | ||||
| 39 | + | // Mock GetKeyInfo for each missing key | |||
| 40 | + | mockMigrator.EXPECT().GetKeyInfo("key1").Return(port.KeyInfo{ | |||
| 41 | + | Key: "key1", | |||
| 42 | + | Type: "bool", | |||
| 43 | + | DefaultValue: "true", | |||
| 44 | + | }) | |||
| 45 | + | mockMigrator.EXPECT().GetKeyInfo("key2").Return(port.KeyInfo{ | |||
| 46 | + | Key: "key2", | |||
| 47 | + | Type: "string", | |||
| 48 | + | DefaultValue: `"default"`, | |||
| 49 | + | }) | |||
| 50 | + | ||||
| 51 | + | uc := NewMigrateConfigUseCase(mockMigrator) | |||
| 52 | + | ctx := context.Background() | |||
| 53 | + | ||||
| 54 | + | result, err := uc.Check(ctx, CheckConfigMigrationInput{}) | |||
| 55 | + | ||||
| 56 | + | require.NoError(t, err) | |||
| 57 | + | assert.True(t, result.NeedsMigration) | |||
| 58 | + | assert.Len(t, result.MissingKeys, 2) | |||
| 59 | + | assert.Equal(t, "/path/to/config.toml", result.ConfigFile) | |||
| 60 | + | ||||
| 61 | + | // Verify key info | |||
| 62 | + | assert.Equal(t, "key1", result.MissingKeys[0].Key) | |||
| 63 | + | assert.Equal(t, "bool", result.MissingKeys[0].Type) | |||
| 64 | + | assert.Equal(t, "key2", result.MissingKeys[1].Key) | |||
| 65 | + | assert.Equal(t, "string", result.MissingKeys[1].Type) | |||
| 66 | + | } | |||
| 67 | + | ||||
| 68 | + | func TestMigrateConfigUseCase_Check_Error(t *testing.T) { | |||
| 69 | + | mockMigrator := mocks.NewMockConfigMigrator(t) | |||
| 70 | + | expectedErr := errors.New("check failed") | |||
| 71 | + | mockMigrator.EXPECT().CheckMigration().Return(nil, expectedErr) | |||
| 72 | + | ||||
| 73 | + | uc := NewMigrateConfigUseCase(mockMigrator) | |||
| 74 | + | ctx := context.Background() | |||
| 75 | + | ||||
| 76 | + | result, err := uc.Check(ctx, CheckConfigMigrationInput{}) | |||
| 77 | + | ||||
| 78 | + | require.Error(t, err) | |||
| 79 | + | assert.Nil(t, result) | |||
| 80 | + | assert.Equal(t, expectedErr, err) | |||
| 81 | + | } | |||
| 82 | + | ||||
| 83 | + | func TestMigrateConfigUseCase_Execute_NoMigrationNeeded(t *testing.T) { | |||
| 84 | + | mockMigrator := mocks.NewMockConfigMigrator(t) | |||
| 85 | + | mockMigrator.EXPECT().CheckMigration().Return(nil, nil) | |||
| 86 | + | ||||
| 87 | + | uc := NewMigrateConfigUseCase(mockMigrator) | |||
| 88 | + | ctx := context.Background() | |||
| 89 | + | ||||
| 90 | + | result, err := uc.Execute(ctx, MigrateConfigInput{}) | |||
| 91 | + | ||||
| 92 | + | require.NoError(t, err) | |||
| 93 | + | assert.Empty(t, result.AddedKeys) | |||
| 94 | + | } | |||
| 95 | + | ||||
| 96 | + | func TestMigrateConfigUseCase_Execute_Success(t *testing.T) { | |||
| 97 | + | mockMigrator := mocks.NewMockConfigMigrator(t) | |||
| 98 | + | ||||
| 99 | + | migrationResult := &port.MigrationResult{ | |||
| 100 | + | MissingKeys: []string{"key1", "key2"}, | |||
| 101 | + | ConfigFile: "/path/to/config.toml", | |||
| 102 | + | } | |||
| 103 | + | mockMigrator.EXPECT().CheckMigration().Return(migrationResult, nil) | |||
| 104 | + | mockMigrator.EXPECT().Migrate().Return([]string{"key1", "key2"}, nil) | |||
| 105 | + | ||||
| 106 | + | uc := NewMigrateConfigUseCase(mockMigrator) | |||
| 107 | + | ctx := context.Background() | |||
| 108 | + | ||||
| 109 | + | result, err := uc.Execute(ctx, MigrateConfigInput{}) | |||
| 110 | + | ||||
| 111 | + | require.NoError(t, err) | |||
| 112 | + | assert.Len(t, result.AddedKeys, 2) | |||
| 113 | + | assert.Equal(t, "/path/to/config.toml", result.ConfigFile) | |||
| 114 | + | } | |||
| 115 | + | ||||
| 116 | + | func TestMigrateConfigUseCase_Execute_MigrateError(t *testing.T) { | |||
| 117 | + | mockMigrator := mocks.NewMockConfigMigrator(t) | |||
| 118 | + | ||||
| 119 | + | migrationResult := &port.MigrationResult{ | |||
| 120 | + | MissingKeys: []string{"key1"}, | |||
| 121 | + | ConfigFile: "/path/to/config.toml", | |||
| 122 | + | } | |||
| 123 | + | mockMigrator.EXPECT().CheckMigration().Return(migrationResult, nil) | |||
| 124 | + | ||||
| 125 | + | expectedErr := errors.New("migrate failed") | |||
| 126 | + | mockMigrator.EXPECT().Migrate().Return(nil, expectedErr) | |||
| 127 | + | ||||
| 128 | + | uc := NewMigrateConfigUseCase(mockMigrator) | |||
| 129 | + | ctx := context.Background() | |||
| 130 | + | ||||
| 131 | + | result, err := uc.Execute(ctx, MigrateConfigInput{}) | |||
| 132 | + | ||||
| 133 | + | require.Error(t, err) | |||
| 134 | + | assert.Nil(t, result) | |||
| 135 | + | assert.Equal(t, expectedErr, err) | |||
| 136 | + | } | |||
| 137 | + | ||||
| 138 | + | func TestMigrateConfigUseCase_Execute_CheckError(t *testing.T) { | |||
| 139 | + | mockMigrator := mocks.NewMockConfigMigrator(t) | |||
| 140 | + | ||||
| 141 | + | expectedErr := errors.New("check failed") | |||
| 142 | + | mockMigrator.EXPECT().CheckMigration().Return(nil, expectedErr) | |||
| 143 | + | ||||
| 144 | + | uc := NewMigrateConfigUseCase(mockMigrator) | |||
| 145 | + | ctx := context.Background() | |||
| 146 | + | ||||
| 147 | + | result, err := uc.Execute(ctx, MigrateConfigInput{}) | |||
| 148 | + | ||||
| 149 | + | require.Error(t, err) | |||
| 150 | + | assert.Nil(t, result) | |||
| 151 | + | assert.Equal(t, expectedErr, err) | |||
| 152 | + | } | |||
| 153 | + | ||||
| 154 | + | func TestMigrateConfigUseCase_Check_EmptyMissingKeys(t *testing.T) { | |||
| 155 | + | mockMigrator := mocks.NewMockConfigMigrator(t) | |||
| 156 | + | ||||
| 157 | + | // Return a result with empty missing keys | |||
| 158 | + | migrationResult := &port.MigrationResult{ | |||
| 159 | + | MissingKeys: []string{}, | |||
| 160 | + | ConfigFile: "/path/to/config.toml", | |||
| 161 | + | } | |||
| 162 | + | mockMigrator.EXPECT().CheckMigration().Return(migrationResult, nil) | |||
| 163 | + | ||||
| 164 | + | uc := NewMigrateConfigUseCase(mockMigrator) | |||
| 165 | + | ctx := context.Background() | |||
| 166 | + | ||||
| 167 | + | result, err := uc.Check(ctx, CheckConfigMigrationInput{}) | |||
| 168 | + | ||||
| 169 | + | require.NoError(t, err) | |||
| 170 | + | assert.False(t, result.NeedsMigration) | |||
| 171 | + | assert.Empty(t, result.MissingKeys) | |||
| 172 | + | } | |||
| 173 | + | ||||
| 174 | + | func TestNewMigrateConfigUseCase(t *testing.T) { | |||
| 175 | + | mockMigrator := mocks.NewMockConfigMigrator(t) | |||
| 176 | + | uc := NewMigrateConfigUseCase(mockMigrator) | |||
| 177 | + | ||||
| 178 | + | assert.NotNil(t, uc) | |||
| 179 | + | assert.Equal(t, mockMigrator, uc.migrator) | |||
| 180 | + | } | |||
| 181 | + | ||||
| 182 | + | // Ensure mock expectations are set up correctly | |||
| 183 | + | func TestMigrateConfigUseCase_MockExpectations(t *testing.T) { | |||
| 184 | + | mockMigrator := mocks.NewMockConfigMigrator(t) | |||
| 185 | + | ||||
| 186 | + | // Set up expectations with mock.Anything for flexibility | |||
| 187 | + | mockMigrator.On("CheckMigration").Return(&port.MigrationResult{ | |||
| 188 | + | MissingKeys: []string{"test.key"}, | |||
| 189 | + | ConfigFile: "/test/config.toml", | |||
| 190 | + | }, nil).Once() | |||
| 191 | + | ||||
| 192 | + | mockMigrator.On("GetKeyInfo", mock.AnythingOfType("string")).Return(port.KeyInfo{ | |||
| 193 | + | Key: "test.key", | |||
| 194 | + | Type: "string", | |||
| 195 | + | DefaultValue: `"test"`, | |||
| 196 | + | }).Once() | |||
| 197 | + | ||||
| 198 | + | uc := NewMigrateConfigUseCase(mockMigrator) | |||
| 199 | + | ctx := context.Background() | |||
| 200 | + | ||||
| 201 | + | result, err := uc.Check(ctx, CheckConfigMigrationInput{}) | |||
| 202 | + | ||||
| 203 | + | require.NoError(t, err) | |||
| 204 | + | assert.True(t, result.NeedsMigration) | |||
| 205 | + | mockMigrator.AssertExpectations(t) | |||
| 206 | + | } | |||