@@ -344,5 +344,169 @@ var _ = Describe("MigrationChain", func() {
344344 Expect (err ).To (HaveOccurred ())
345345 Expect (err .Error ()).To (ContainSubstring ("no migration path found" ))
346346 })
347+
348+ Context ("with multiple changes at the same version level" , func () {
349+ var multiChain * MigrationChain
350+ var userMigrationApplied , productMigrationApplied , orderMigrationApplied bool
351+
352+ // Define realistic schema types for testing
353+ type User struct {
354+ ID int
355+ FullName string
356+ Email string
357+ Phone string
358+ }
359+
360+ type Product struct {
361+ ID int
362+ Name string
363+ Price float64
364+ Description string
365+ Currency string
366+ }
367+
368+ type Order struct {
369+ ID int
370+ UserID int
371+ ProductID int
372+ Quantity int
373+ CreatedAt string
374+ }
375+
376+ BeforeEach (func () {
377+ userMigrationApplied = false
378+ productMigrationApplied = false
379+ orderMigrationApplied = false
380+
381+ // Create multiple changes all from v2 to v3 (like User, Product, Order migrations)
382+ // Simulate realistic backward migrations (v3 -> v2)
383+
384+ // User migration: v3 has "full_name" and "phone", v2 has "name" without phone
385+ userChange := NewVersionChange ("User v2->v3: Add phone, rename name to full_name" , v2 , v3 ,
386+ & AlterResponseInstruction {
387+ Schemas : []interface {}{User {}},
388+ Transformer : func (resp * ResponseInfo ) error {
389+ if bodyMap , ok := resp .Body .(map [string ]interface {}); ok {
390+ // Backward migration: v3 -> v2
391+ if fullName , exists := bodyMap ["full_name" ]; exists {
392+ bodyMap ["name" ] = fullName
393+ delete (bodyMap , "full_name" )
394+ }
395+ delete (bodyMap , "phone" ) // v2 doesn't have phone
396+ userMigrationApplied = true
397+ }
398+ return nil
399+ },
400+ },
401+ )
402+
403+ // Product migration: v3 has "description" and "currency", v2 has "desc" without currency
404+ productChange := NewVersionChange ("Product v2->v3: Add currency, rename desc to description" , v2 , v3 ,
405+ & AlterResponseInstruction {
406+ Schemas : []interface {}{Product {}},
407+ Transformer : func (resp * ResponseInfo ) error {
408+ if bodyMap , ok := resp .Body .(map [string ]interface {}); ok {
409+ // Backward migration: v3 -> v2
410+ if description , exists := bodyMap ["description" ]; exists {
411+ bodyMap ["desc" ] = description
412+ delete (bodyMap , "description" )
413+ }
414+ delete (bodyMap , "currency" ) // v2 doesn't have currency
415+ productMigrationApplied = true
416+ }
417+ return nil
418+ },
419+ },
420+ )
421+
422+ // Order migration: v3 has "created_at", v2 doesn't
423+ orderChange := NewVersionChange ("Order v2->v3: Add created_at timestamp" , v2 , v3 ,
424+ & AlterResponseInstruction {
425+ Schemas : []interface {}{Order {}},
426+ Transformer : func (resp * ResponseInfo ) error {
427+ if bodyMap , ok := resp .Body .(map [string ]interface {}); ok {
428+ // Backward migration: v3 -> v2
429+ delete (bodyMap , "created_at" ) // v2 doesn't have created_at
430+ orderMigrationApplied = true
431+ }
432+ return nil
433+ },
434+ },
435+ )
436+
437+ multiChain = NewMigrationChain ([]* VersionChange {userChange , productChange , orderChange })
438+ })
439+
440+ It ("should apply ALL changes with the same FromVersion when migrating backward" , func () {
441+ // Setup response data representing a v3 response with all fields
442+ c , _ := gin .CreateTestContext (httptest .NewRecorder ())
443+ responseData := map [string ]interface {}{
444+ // User fields (v3)
445+ "id" : 1 ,
446+ "full_name" : "Alice Johnson" ,
447+ 448+ "phone" : "+1-555-0100" ,
449+ // Product fields (v3)
450+ "product_id" : 100 ,
451+ "name" : "Laptop" ,
452+ "price" : 999.99 ,
453+ "description" : "High-performance laptop" ,
454+ "currency" : "USD" ,
455+ // Order fields (v3)
456+ "order_id" : 1000 ,
457+ "user_id" : 1 ,
458+ "quantity" : 2 ,
459+ "created_at" : "2024-01-01T00:00:00Z" ,
460+ }
461+ responseInfo := NewResponseInfo (c , responseData )
462+
463+ // Migrate from v3 to v2 - should apply all three v2->v3 changes in reverse
464+ err := multiChain .MigrateResponse (ctx , responseInfo , v3 , v2 , nil , 0 )
465+ Expect (err ).NotTo (HaveOccurred ())
466+
467+ // Verify ALL three transformations were applied
468+ Expect (userMigrationApplied ).To (BeTrue (), "User migration should be applied" )
469+ Expect (productMigrationApplied ).To (BeTrue (), "Product migration should be applied" )
470+ Expect (orderMigrationApplied ).To (BeTrue (), "Order migration should be applied" )
471+
472+ // Verify the response body has all v2 transformations
473+ bodyMap := responseInfo .Body .(map [string ]interface {})
474+
475+ // User fields: should have "name" instead of "full_name", no "phone"
476+ Expect (bodyMap ["name" ]).To (Equal ("Alice Johnson" ), "full_name should be renamed to name" )
477+ Expect (bodyMap ).NotTo (HaveKey ("full_name" ), "full_name should not exist in v2" )
478+ Expect (bodyMap ).NotTo (HaveKey ("phone" ), "phone should not exist in v2" )
479+ Expect (
bodyMap [
"email" ]).
To (
Equal (
"[email protected] " ),
"email should remain" )
480+
481+ // Product fields: should have "desc" instead of "description", no "currency"
482+ Expect (bodyMap ["desc" ]).To (Equal ("High-performance laptop" ), "description should be renamed to desc" )
483+ Expect (bodyMap ).NotTo (HaveKey ("description" ), "description should not exist in v2" )
484+ Expect (bodyMap ).NotTo (HaveKey ("currency" ), "currency should not exist in v2" )
485+ Expect (bodyMap ["price" ]).To (Equal (999.99 ), "price should remain" )
486+
487+ // Order fields: should not have "created_at"
488+ Expect (bodyMap ).NotTo (HaveKey ("created_at" ), "created_at should not exist in v2" )
489+ Expect (bodyMap ["quantity" ]).To (Equal (2 ), "quantity should remain" )
490+ })
491+
492+ It ("should collect multiple changes at the same version level via GetMigrationPath" , func () {
493+ // GetMigrationPath should return all 3 changes when going from v3 to v2
494+ path := multiChain .GetMigrationPath (v3 , v2 )
495+ Expect (path ).To (HaveLen (3 ), "should include all changes from v2->v3" )
496+
497+ // Verify all changes are included by checking their descriptions
498+ descriptions := []string {}
499+ for _ , change := range path {
500+ descriptions = append (descriptions , change .Description ())
501+ }
502+
503+ Expect (descriptions ).To (ContainElement ("User v2->v3: Add phone, rename name to full_name" ),
504+ "User migration should be in the path" )
505+ Expect (descriptions ).To (ContainElement ("Product v2->v3: Add currency, rename desc to description" ),
506+ "Product migration should be in the path" )
507+ Expect (descriptions ).To (ContainElement ("Order v2->v3: Add created_at timestamp" ),
508+ "Order migration should be in the path" )
509+ })
510+ })
347511 })
348512})
0 commit comments