// Copyright 2026 Arctel.net // SPDX-License-Identifier: Apache-2.0 package cloudflare import ( "context" "testing" db "github.com/Rain-kl/Wavelet/internal/infra/persistence" "github.com/Rain-kl/Wavelet/internal/model" "github.com/Rain-kl/Wavelet/internal/repository" ) func TestMoveMemberAndBatchOperations(t *testing.T) { ctx, member1ID := setupCloudflareLogicDB(t) var dispatchedTasks []string restoreDispatch := SetDispatchTaskForTest(func(ctx context.Context, taskType string, payload []byte, triggeredBy string) (string, error) { dispatchedTasks = append(dispatchedTasks, triggeredBy) return "mock-task-id", nil }) t.Cleanup(restoreDispatch) fake := &fakeClient{} restoreClient := SetClientFactoryForTest(func(string) Client { return fake }) t.Cleanup(restoreClient) member1, err := repository.GetCFPointingMemberByID(ctx, member1ID) if err != nil { t.Fatalf("GetCFPointingMemberByID() error = %v", err) } sourceGroupID := member1.GroupID // Create target group targetGroup := model.CFPointingGroup{ Name: "secondary", PrimaryNodeID: 1, ActiveNodeID: 1, DefaultProxied: true, Enabled: true, } if err := db.DB(ctx).Create(&targetGroup).Error; err != nil { t.Fatalf("Create(targetGroup) error = %v", err) } // 1. Target group equals source group -> errTargetGroupSame t.Run("MoveMember target equals source", func(t *testing.T) { _, err := MoveMember(ctx, sourceGroupID, member1.ID, sourceGroupID) if err == nil || err.Error() != errTargetGroupSame { t.Fatalf("MoveMember() error = %v, want %s", err, errTargetGroupSame) } }) // 2. Target group does not exist -> errTargetGroupInvalid t.Run("MoveMember target group invalid", func(t *testing.T) { _, err := MoveMember(ctx, sourceGroupID, member1.ID, 99999) if err == nil || err.Error() != errTargetGroupInvalid { t.Fatalf("MoveMember() error = %v, want %s", err, errTargetGroupInvalid) } }) // 3. Successfully move member to target group t.Run("MoveMember success", func(t *testing.T) { item, err := MoveMember(ctx, sourceGroupID, member1.ID, targetGroup.ID) if err != nil { t.Fatalf("MoveMember() error = %v", err) } if item == nil || item.GroupID != targetGroup.ID { t.Fatalf("MoveMember() returned item group ID = %v, want %d", item, targetGroup.ID) } updated, err := repository.GetCFPointingMemberByID(ctx, member1.ID) if err != nil { t.Fatalf("GetCFPointingMemberByID() error = %v", err) } if updated.GroupID != targetGroup.ID { t.Errorf("member GroupID = %d, want %d", updated.GroupID, targetGroup.ID) } if updated.SyncStatus != model.CFMemberSyncPending { t.Errorf("member SyncStatus = %s, want %s", updated.SyncStatus, model.CFMemberSyncPending) } if updated.LastError != "" { t.Errorf("member LastError = %q, want empty", updated.LastError) } }) // 4. Batch move multiple members t.Run("BatchMoveMembers", func(t *testing.T) { // Test empty members error emptyErr := BatchMoveMembers(ctx, sourceGroupID, MemberBatchMoveInput{MemberIDs: nil, TargetGroupID: targetGroup.ID}) if emptyErr == nil || emptyErr.Error() != errNoMembersSelected { t.Fatalf("BatchMoveMembers() empty error = %v, want %s", emptyErr, errNoMembersSelected) } // Test target equals source error sameErr := BatchMoveMembers(ctx, sourceGroupID, MemberBatchMoveInput{MemberIDs: []uint{1}, TargetGroupID: sourceGroupID}) if sameErr == nil || sameErr.Error() != errTargetGroupSame { t.Fatalf("BatchMoveMembers() same group error = %v, want %s", sameErr, errTargetGroupSame) } // Test invalid target group error invalidErr := BatchMoveMembers(ctx, sourceGroupID, MemberBatchMoveInput{MemberIDs: []uint{1}, TargetGroupID: 99999}) if invalidErr == nil || invalidErr.Error() != errTargetGroupInvalid { t.Fatalf("BatchMoveMembers() invalid group error = %v, want %s", invalidErr, errTargetGroupInvalid) } // Create 2 additional members in sourceGroup domain2 := model.ZoneDomain{ZoneID: 1, Domain: "test2.example.com"} domain3 := model.ZoneDomain{ZoneID: 1, Domain: "test3.example.com"} if err := db.DB(ctx).Create(&domain2).Error; err != nil { t.Fatalf("Create(domain2) error = %v", err) } if err := db.DB(ctx).Create(&domain3).Error; err != nil { t.Fatalf("Create(domain3) error = %v", err) } member2 := model.CFPointingMember{GroupID: sourceGroupID, ZoneDomainID: domain2.ID, Proxied: false, SyncStatus: model.CFMemberSyncOK} member3 := model.CFPointingMember{GroupID: sourceGroupID, ZoneDomainID: domain3.ID, Proxied: true, SyncStatus: model.CFMemberSyncOK} if err := db.DB(ctx).Create(&member2).Error; err != nil { t.Fatalf("Create(member2) error = %v", err) } if err := db.DB(ctx).Create(&member3).Error; err != nil { t.Fatalf("Create(member3) error = %v", err) } // Perform batch move if err := BatchMoveMembers(ctx, sourceGroupID, MemberBatchMoveInput{ MemberIDs: []uint{member2.ID, member3.ID}, TargetGroupID: targetGroup.ID, }); err != nil { t.Fatalf("BatchMoveMembers() error = %v", err) } // Verify updated in DB for _, mid := range []uint{member2.ID, member3.ID} { m, err := repository.GetCFPointingMemberByID(ctx, mid) if err != nil { t.Fatalf("GetCFPointingMemberByID(%d) error = %v", mid, err) } if m.GroupID != targetGroup.ID { t.Errorf("member %d GroupID = %d, want %d", mid, m.GroupID, targetGroup.ID) } if m.SyncStatus != model.CFMemberSyncPending { t.Errorf("member %d SyncStatus = %s, want %s", mid, m.SyncStatus, model.CFMemberSyncPending) } if m.LastError != "" { t.Errorf("member %d LastError = %q, want empty", mid, m.LastError) } } }) // 5. Batch remove members t.Run("BatchRemoveMembers", func(t *testing.T) { // Test empty members error emptyErr := BatchRemoveMembers(ctx, targetGroup.ID, MemberBatchRemoveInput{MemberIDs: nil}) if emptyErr == nil || emptyErr.Error() != errNoMembersSelected { t.Fatalf("BatchRemoveMembers() empty error = %v, want %s", emptyErr, errNoMembersSelected) } // Create members to remove domain4 := model.ZoneDomain{ZoneID: 1, Domain: "test4.example.com"} domain5 := model.ZoneDomain{ZoneID: 1, Domain: "test5.example.com"} if err := db.DB(ctx).Create(&domain4).Error; err != nil { t.Fatalf("Create(domain4) error = %v", err) } if err := db.DB(ctx).Create(&domain5).Error; err != nil { t.Fatalf("Create(domain5) error = %v", err) } member4 := model.CFPointingMember{GroupID: targetGroup.ID, ZoneDomainID: domain4.ID, Proxied: false, SyncStatus: model.CFMemberSyncOK} member5 := model.CFPointingMember{GroupID: targetGroup.ID, ZoneDomainID: domain5.ID, Proxied: true, SyncStatus: model.CFMemberSyncOK} if err := db.DB(ctx).Create(&member4).Error; err != nil { t.Fatalf("Create(member4) error = %v", err) } if err := db.DB(ctx).Create(&member5).Error; err != nil { t.Fatalf("Create(member5) error = %v", err) } // Perform batch remove if err := BatchRemoveMembers(ctx, targetGroup.ID, MemberBatchRemoveInput{ MemberIDs: []uint{member4.ID, member5.ID}, }); err != nil { t.Fatalf("BatchRemoveMembers() error = %v", err) } // Verify deleted from DB for _, mid := range []uint{member4.ID, member5.ID} { _, err := repository.GetCFPointingMemberByID(ctx, mid) if err == nil { t.Errorf("GetCFPointingMemberByID(%d) should be deleted, but found", mid) } } }) }