master
go 290 lines 7.64 KB
Raw
1 package common
2
3 import (
4 "bytes"
5 "encoding/json"
6 "fmt"
7 "maps"
8 "os"
9 "path/filepath"
10 "reflect"
11 "testing"
12 )
13
14 // TestCase represents a single migration test case
15 type TestCase struct {
16 Name string
17 InputConfig map[string]any
18 Assertions []ConfigAssertion
19 }
20
21 // ConfigAssertion represents an assertion about the migrated config
22 type ConfigAssertion struct {
23 Path string
24 Expected any
25 }
26
27 // RunMigrationTest runs a migration test with the given test case
28 func RunMigrationTest(t *testing.T, migration Migration, tc TestCase) {
29 t.Helper()
30
31 // Convert input to JSON
32 inputJSON, err := json.MarshalIndent(tc.InputConfig, "", " ")
33 if err != nil {
34 t.Fatalf("failed to marshal input config: %v", err)
35 }
36
37 // Run the migration's convert function
38 var output bytes.Buffer
39 if baseMig, ok := migration.(*BaseMigration); ok {
40 err = baseMig.Convert(bytes.NewReader(inputJSON), &output)
41 if err != nil {
42 t.Fatalf("migration failed: %v", err)
43 }
44 } else {
45 t.Skip("migration is not a BaseMigration")
46 }
47
48 // Parse output
49 var result map[string]any
50 err = json.Unmarshal(output.Bytes(), &result)
51 if err != nil {
52 t.Fatalf("failed to unmarshal output: %v", err)
53 }
54
55 // Run assertions
56 for _, assertion := range tc.Assertions {
57 AssertConfigField(t, result, assertion.Path, assertion.Expected)
58 }
59 }
60
61 // AssertConfigField asserts that a field in the config has the expected value
62 func AssertConfigField(t *testing.T, config map[string]any, path string, expected any) {
63 t.Helper()
64
65 actual, exists := GetField(config, path)
66 if expected == nil {
67 if exists {
68 t.Errorf("expected field %s to not exist, but it has value: %v", path, actual)
69 }
70 return
71 }
72
73 if !exists {
74 t.Errorf("expected field %s to exist with value %v, but it doesn't exist", path, expected)
75 return
76 }
77
78 // Handle different types of comparisons
79 switch exp := expected.(type) {
80 case []string:
81 actualSlice, ok := actual.([]any)
82 if !ok {
83 t.Errorf("field %s: expected []string, got %T", path, actual)
84 return
85 }
86 if len(exp) != len(actualSlice) {
87 t.Errorf("field %s: expected slice of length %d, got %d", path, len(exp), len(actualSlice))
88 return
89 }
90 for i, expVal := range exp {
91 if actualSlice[i] != expVal {
92 t.Errorf("field %s[%d]: expected %v, got %v", path, i, expVal, actualSlice[i])
93 }
94 }
95 case map[string]string:
96 actualMap, ok := actual.(map[string]any)
97 if !ok {
98 t.Errorf("field %s: expected map, got %T", path, actual)
99 return
100 }
101 for k, v := range exp {
102 if actualMap[k] != v {
103 t.Errorf("field %s[%s]: expected %v, got %v", path, k, v, actualMap[k])
104 }
105 }
106 default:
107 if actual != expected {
108 t.Errorf("field %s: expected %v, got %v", path, expected, actual)
109 }
110 }
111 }
112
113 // GenerateTestConfig creates a basic test config with the given fields
114 func GenerateTestConfig(fields map[string]any) map[string]any {
115 // Start with a minimal valid config
116 config := map[string]any{
117 "Identity": map[string]any{
118 "PeerID": "QmTest",
119 },
120 }
121
122 // Merge in the provided fields
123 maps.Copy(config, fields)
124
125 return config
126 }
127
128 // CreateTestRepo creates a temporary test repository with the given version and config
129 func CreateTestRepo(t *testing.T, version int, config map[string]any) string {
130 t.Helper()
131
132 tempDir := t.TempDir()
133
134 // Write version file
135 versionPath := filepath.Join(tempDir, "version")
136 err := os.WriteFile(versionPath, fmt.Appendf(nil, "%d", version), 0644)
137 if err != nil {
138 t.Fatalf("failed to write version file: %v", err)
139 }
140
141 // Write config file
142 configPath := filepath.Join(tempDir, "config")
143 configData, err := json.MarshalIndent(config, "", " ")
144 if err != nil {
145 t.Fatalf("failed to marshal config: %v", err)
146 }
147 err = os.WriteFile(configPath, configData, 0644)
148 if err != nil {
149 t.Fatalf("failed to write config file: %v", err)
150 }
151
152 return tempDir
153 }
154
155 // AssertMigrationSuccess runs a full migration and checks that it succeeds
156 func AssertMigrationSuccess(t *testing.T, migration Migration, fromVersion, toVersion int, inputConfig map[string]any) map[string]any {
157 t.Helper()
158
159 // Create test repo
160 repoPath := CreateTestRepo(t, fromVersion, inputConfig)
161
162 // Run migration
163 opts := Options{
164 Path: repoPath,
165 Verbose: false,
166 }
167
168 err := migration.Apply(opts)
169 if err != nil {
170 t.Fatalf("migration failed: %v", err)
171 }
172
173 // Check version was updated
174 versionBytes, err := os.ReadFile(filepath.Join(repoPath, "version"))
175 if err != nil {
176 t.Fatalf("failed to read version file: %v", err)
177 }
178 actualVersion := string(versionBytes)
179 if actualVersion != fmt.Sprintf("%d", toVersion) {
180 t.Errorf("expected version %d, got %s", toVersion, actualVersion)
181 }
182
183 // Read and return the migrated config
184 configBytes, err := os.ReadFile(filepath.Join(repoPath, "config"))
185 if err != nil {
186 t.Fatalf("failed to read config file: %v", err)
187 }
188
189 var result map[string]any
190 err = json.Unmarshal(configBytes, &result)
191 if err != nil {
192 t.Fatalf("failed to unmarshal config: %v", err)
193 }
194
195 return result
196 }
197
198 // AssertMigrationReversible checks that a migration can be reverted
199 func AssertMigrationReversible(t *testing.T, migration Migration, fromVersion, toVersion int, inputConfig map[string]any) {
200 t.Helper()
201
202 // Create test repo at target version
203 repoPath := CreateTestRepo(t, toVersion, inputConfig)
204
205 // Create backup file (simulating a previous migration)
206 backupPath := filepath.Join(repoPath, fmt.Sprintf("config.%d-to-%d.bak", fromVersion, toVersion))
207 originalConfig, err := json.MarshalIndent(inputConfig, "", " ")
208 if err != nil {
209 t.Fatalf("failed to marshal original config: %v", err)
210 }
211
212 if err := os.WriteFile(backupPath, originalConfig, 0644); err != nil {
213 t.Fatalf("failed to write backup file: %v", err)
214 }
215
216 // Run revert
217 if err := migration.Revert(Options{Path: repoPath}); err != nil {
218 t.Fatalf("revert failed: %v", err)
219 }
220
221 // Verify version was reverted
222 versionBytes, err := os.ReadFile(filepath.Join(repoPath, "version"))
223 if err != nil {
224 t.Fatalf("failed to read version file: %v", err)
225 }
226
227 if actualVersion := string(versionBytes); actualVersion != fmt.Sprintf("%d", fromVersion) {
228 t.Errorf("expected version %d after revert, got %s", fromVersion, actualVersion)
229 }
230
231 // Verify config was reverted
232 configBytes, err := os.ReadFile(filepath.Join(repoPath, "config"))
233 if err != nil {
234 t.Fatalf("failed to read reverted config file: %v", err)
235 }
236
237 var revertedConfig map[string]any
238 if err := json.Unmarshal(configBytes, &revertedConfig); err != nil {
239 t.Fatalf("failed to unmarshal reverted config: %v", err)
240 }
241
242 // Compare reverted config with original
243 compareConfigs(t, inputConfig, revertedConfig, "")
244 }
245
246 // compareConfigs recursively compares two config maps and reports differences
247 func compareConfigs(t *testing.T, expected, actual map[string]any, path string) {
248 t.Helper()
249
250 // Build current path helper
251 buildPath := func(key string) string {
252 if path == "" {
253 return key
254 }
255 return path + "." + key
256 }
257
258 // Check all expected fields exist and match
259 for key, expectedValue := range expected {
260 currentPath := buildPath(key)
261
262 actualValue, exists := actual[key]
263 if !exists {
264 t.Errorf("reverted config missing field %s", currentPath)
265 continue
266 }
267
268 switch exp := expectedValue.(type) {
269 case map[string]any:
270 act, ok := actualValue.(map[string]any)
271 if !ok {
272 t.Errorf("field %s: expected map, got %T", currentPath, actualValue)
273 continue
274 }
275 compareConfigs(t, exp, act, currentPath)
276 default:
277 if !reflect.DeepEqual(expectedValue, actualValue) {
278 t.Errorf("field %s: expected %v, got %v after revert",
279 currentPath, expectedValue, actualValue)
280 }
281 }
282 }
283
284 // Check for unexpected fields using maps.Keys (Go 1.23+)
285 for key := range actual {
286 if _, exists := expected[key]; !exists {
287 t.Errorf("reverted config has unexpected field %s", buildPath(key))
288 }
289 }
290 }