package usecase_test // T219 — M6 invite integration (spec §9.16 IV-01~IV-13) import ( "context" "testing" "apps/backend/internal/module/member/domain" "apps/backend/internal/module/member/usecase" "github.com/stretchr/testify/require" ) func TestIV_01_GetMyNetwork(t *testing.T) { store, svc := newM1Svc() m, _ := seedMember(t, store, svc, "iv01@test.local") net, err := svc.GetMyNetwork(context.Background(), m.UID) require.NoError(t, err) require.Equal(t, m.UID, net.Me.UID) require.NotEmpty(t, net.Me.InviteCode) require.Nil(t, net.Me.ParentUID) require.Empty(t, net.Downlines) } func TestIV_02_ApplyLegalCode(t *testing.T) { store, svc := newM1Svc() inv, _ := seedMember(t, store, svc, "iv02-inv@test.local") me, _ := seedMember(t, store, svc, "iv02-me@test.local") net, err := svc.ApplyInviteCode(context.Background(), me.UID, inv.InviteCode) require.NoError(t, err) require.NotNil(t, net.Me.ParentUID) require.Equal(t, inv.UID, *net.Me.ParentUID) require.NotNil(t, net.Upline) require.Equal(t, inv.UID, net.Upline.UID) } func TestIV_03_AlreadyBound(t *testing.T) { store, svc := newM1Svc() a, _ := seedMember(t, store, svc, "iv03a@test.local") b, _ := seedMember(t, store, svc, "iv03b@test.local") c, _ := seedMember(t, store, svc, "iv03c@test.local") _, err := svc.ApplyInviteCode(context.Background(), b.UID, a.InviteCode) require.NoError(t, err) _, err = svc.ApplyInviteCode(context.Background(), b.UID, c.InviteCode) require.ErrorIs(t, err, domain.ErrInviteAlreadyBound) } func TestIV_04_ApplySelfCode(t *testing.T) { store, svc := newM1Svc() me, _ := seedMember(t, store, svc, "iv04@test.local") _, err := svc.ApplyInviteCode(context.Background(), me.UID, me.InviteCode) require.ErrorIs(t, err, domain.ErrInviteCodeSelf) } func TestIV_05_ApplyUnknownCode(t *testing.T) { store, svc := newM1Svc() me, _ := seedMember(t, store, svc, "iv05@test.local") _, err := svc.ApplyInviteCode(context.Background(), me.UID, "NO-SUCH-CODE") require.ErrorIs(t, err, domain.ErrInviteCodeNotFound) } func TestIV_06_ApplyEmptyCode(t *testing.T) { store, svc := newM1Svc() me, _ := seedMember(t, store, svc, "iv06@test.local") _, err := svc.ApplyInviteCode(context.Background(), me.UID, " ") require.ErrorIs(t, err, domain.ErrInviteCodeRequired) } func TestIV_07_DownlinesCount(t *testing.T) { store, svc := newM1Svc() inv, _ := seedMember(t, store, svc, "iv07-inv@test.local") d1, _ := seedMember(t, store, svc, "iv07-d1@test.local") d2, _ := seedMember(t, store, svc, "iv07-d2@test.local") _, err := svc.ApplyInviteCode(context.Background(), d1.UID, inv.InviteCode) require.NoError(t, err) _, err = svc.ApplyInviteCode(context.Background(), d2.UID, inv.InviteCode) require.NoError(t, err) net, err := svc.GetMyNetwork(context.Background(), inv.UID) require.NoError(t, err) require.Len(t, net.Downlines, 2) require.Equal(t, 2, net.TotalDownlineCount) require.Equal(t, 2, net.Me.DownlineCount) } func TestIV_08_GetTree(t *testing.T) { store, svc := newM1Svc() root, _ := seedMember(t, store, svc, "iv08-root@test.local") child, _ := seedMember(t, store, svc, "iv08-c@test.local") _, err := svc.ApplyInviteCode(context.Background(), child.UID, root.InviteCode) require.NoError(t, err) forest, err := svc.GetInviteTree(context.Background()) require.NoError(t, err) require.NotEmpty(t, forest) // find root var found bool var walk func([]usecase.InviteTreeNode) walk = func(nodes []usecase.InviteTreeNode) { for _, n := range nodes { if n.UID == root.UID { found = true require.NotEmpty(t, n.Children) } walk(n.Children) } } walk(forest) require.True(t, found) } func TestIV_09_MoveLegal(t *testing.T) { store, svc := newM1Svc() admin, _ := seedAdmin(t, store, svc, "iv09-admin@test.local") a, _ := seedMember(t, store, svc, "iv09-a@test.local") b, _ := seedMember(t, store, svc, "iv09-b@test.local") // b under a _, err := svc.ApplyInviteCode(context.Background(), b.UID, a.InviteCode) require.NoError(t, err) // move b to root brief, err := svc.MoveInviteMember(context.Background(), admin.UID, b.UID, nil) require.NoError(t, err) require.Nil(t, brief.ParentUID) } func TestIV_10_NoCycle(t *testing.T) { store, svc := newM1Svc() admin, _ := seedAdmin(t, store, svc, "iv10-admin@test.local") a, _ := seedMember(t, store, svc, "iv10-a@test.local") b, _ := seedMember(t, store, svc, "iv10-b@test.local") _, err := svc.ApplyInviteCode(context.Background(), b.UID, a.InviteCode) require.NoError(t, err) // move a under b → cycle _, err = svc.MoveInviteMember(context.Background(), admin.UID, a.UID, &b.UID) require.ErrorIs(t, err, domain.ErrInviteCycle) } func TestIV_11_MoveToSelf(t *testing.T) { store, svc := newM1Svc() admin, _ := seedAdmin(t, store, svc, "iv11-admin@test.local") a, _ := seedMember(t, store, svc, "iv11-a@test.local") _, err := svc.MoveInviteMember(context.Background(), admin.UID, a.UID, &a.UID) require.ErrorIs(t, err, domain.ErrInviteSelfParent) } func TestIV_12_MemberCannotMove(t *testing.T) { store, svc := newM1Svc() mem, _ := seedMember(t, store, svc, "iv12-mem@test.local") a, _ := seedMember(t, store, svc, "iv12-a@test.local") b, _ := seedMember(t, store, svc, "iv12-b@test.local") _, err := svc.MoveInviteMember(context.Background(), mem.UID, a.UID, &b.UID) require.ErrorIs(t, err, domain.ErrInviteNeedAdmin) } func TestIV_13_ListParentCandidatesExcludeSubtree(t *testing.T) { store, svc := newM1Svc() a, _ := seedMember(t, store, svc, "iv13-a@test.local") b, _ := seedMember(t, store, svc, "iv13-b@test.local") c, _ := seedMember(t, store, svc, "iv13-c@test.local") _, err := svc.ApplyInviteCode(context.Background(), b.UID, a.InviteCode) require.NoError(t, err) _, err = svc.ApplyInviteCode(context.Background(), c.UID, b.InviteCode) require.NoError(t, err) // exclude subtree of a → no a,b,c cands, err := svc.ListParentCandidates(context.Background(), a.UID) require.NoError(t, err) for _, x := range cands { require.NotEqual(t, a.UID, x.UID) require.NotEqual(t, b.UID, x.UID) require.NotEqual(t, c.UID, x.UID) } }