detached dfe0556

feat(usecase): add MigrateConfigUseCase for checking and applying config migration

commit
dfe055616e30c702f7576a8aad11bd2deb3e2b80
author
bnema · · 2025-12-27 18:47 UTC
parent
607f4ad
signature
GPG Unverified2B82207D370127A0 The 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+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close