822 lines
28 KiB
Go
822 lines
28 KiB
Go
package host_test
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"hyapp/pkg/xerr"
|
|
hostdomain "hyapp/services/user-service/internal/domain/host"
|
|
userdomain "hyapp/services/user-service/internal/domain/user"
|
|
hostservice "hyapp/services/user-service/internal/service/host"
|
|
"hyapp/services/user-service/internal/testutil/mysqltest"
|
|
)
|
|
|
|
type sequenceIDGenerator struct {
|
|
next int64
|
|
}
|
|
|
|
func (g *sequenceIDGenerator) NewInt64() int64 {
|
|
id := g.next
|
|
g.next++
|
|
return id
|
|
}
|
|
|
|
func newHostService(repository *mysqltest.Repository, firstID int64, nowMs int64) *hostservice.Service {
|
|
return hostservice.New(repository.HostRepository(),
|
|
hostservice.WithIDGenerator(&sequenceIDGenerator{next: firstID}),
|
|
hostservice.WithClock(func() time.Time { return time.UnixMilli(nowMs) }),
|
|
)
|
|
}
|
|
|
|
func seedActiveUser(t *testing.T, repository *mysqltest.Repository, userID int64, regionID int64) {
|
|
t.Helper()
|
|
seedActiveUserInCountry(t, repository, userID, regionID, "CN", "")
|
|
}
|
|
|
|
func seedActiveUserInCountry(t *testing.T, repository *mysqltest.Repository, userID int64, regionID int64, country string, avatar string) {
|
|
t.Helper()
|
|
repository.PutUser(userdomain.User{
|
|
UserID: userID,
|
|
DefaultDisplayUserID: displayID(userID),
|
|
CurrentDisplayUserID: displayID(userID),
|
|
Country: country,
|
|
RegionID: regionID,
|
|
Avatar: avatar,
|
|
ProfileCompleted: true,
|
|
ProfileCompletedAtMs: 1,
|
|
OnboardingStatus: userdomain.OnboardingStatusCompleted,
|
|
Status: userdomain.StatusActive,
|
|
})
|
|
}
|
|
|
|
func displayID(userID int64) string {
|
|
return fmt.Sprintf("%06d", 900000+userID%100000)
|
|
}
|
|
|
|
func TestCheckBusinessCapabilityRequiresActiveAgencyOwner(t *testing.T) {
|
|
ctx := context.Background()
|
|
repository := mysqltest.NewRepository(t)
|
|
seedActiveUser(t, repository, 101, 10)
|
|
seedActiveUser(t, repository, 202, 10)
|
|
repository.PutAgency(hostdomain.Agency{
|
|
AgencyID: 301,
|
|
OwnerUserID: 101,
|
|
RegionID: 10,
|
|
Name: "Manager Agency",
|
|
Status: hostdomain.AgencyStatusActive,
|
|
})
|
|
svc := newHostService(repository, 1000, 2000)
|
|
|
|
allowed, reason, err := svc.CheckBusinessCapability(ctx, 101, hostservice.CapabilityManagerResourceGrant)
|
|
if err != nil {
|
|
t.Fatalf("CheckBusinessCapability for owner failed: %v", err)
|
|
}
|
|
if !allowed || reason != "" {
|
|
t.Fatalf("active agency owner should be allowed: allowed=%v reason=%q", allowed, reason)
|
|
}
|
|
|
|
allowed, reason, err = svc.CheckBusinessCapability(ctx, 202, hostservice.CapabilityManagerResourceGrant)
|
|
if err != nil {
|
|
t.Fatalf("CheckBusinessCapability for non-owner failed: %v", err)
|
|
}
|
|
if allowed || reason != "active_agency_owner_required" {
|
|
t.Fatalf("non-owner should be denied by stable reason: allowed=%v reason=%q", allowed, reason)
|
|
}
|
|
|
|
allowed, _, err = svc.CheckBusinessCapability(ctx, 101, "unknown_capability")
|
|
if !xerr.IsCode(err, xerr.InvalidArgument) || allowed {
|
|
t.Fatalf("unknown capability must fail closed: allowed=%v err=%v", allowed, err)
|
|
}
|
|
}
|
|
|
|
func TestApplyReviewApproveCreatesHostMembershipIdempotently(t *testing.T) {
|
|
ctx := context.Background()
|
|
repository := mysqltest.NewRepository(t)
|
|
seedActiveUser(t, repository, 101, 10)
|
|
seedActiveUser(t, repository, 202, 10)
|
|
repository.PutAgency(hostdomain.Agency{
|
|
AgencyID: 301,
|
|
OwnerUserID: 101,
|
|
RegionID: 10,
|
|
ParentBDUserID: 501,
|
|
Name: "Phase One Agency",
|
|
Status: hostdomain.AgencyStatusActive,
|
|
JoinEnabled: true,
|
|
CreatedAtMs: 100,
|
|
UpdatedAtMs: 100,
|
|
})
|
|
svc := newHostService(repository, 1000, 2000)
|
|
|
|
application, err := svc.ApplyToAgency(ctx, hostservice.ApplyToAgencyInput{
|
|
CommandID: "apply-202-301",
|
|
UserID: 202,
|
|
AgencyID: 301,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ApplyToAgency failed: %v", err)
|
|
}
|
|
if application.Status != hostdomain.ApplicationStatusPending || application.RegionID != 10 {
|
|
t.Fatalf("application mismatch: %+v", application)
|
|
}
|
|
|
|
retriedApplication, err := svc.ApplyToAgency(ctx, hostservice.ApplyToAgencyInput{
|
|
CommandID: "apply-202-301",
|
|
UserID: 202,
|
|
AgencyID: 301,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("retry ApplyToAgency failed: %v", err)
|
|
}
|
|
if retriedApplication.ApplicationID != application.ApplicationID {
|
|
t.Fatalf("apply retry must return same application: got %d want %d", retriedApplication.ApplicationID, application.ApplicationID)
|
|
}
|
|
|
|
result, err := svc.ReviewAgencyApplication(ctx, hostservice.ReviewAgencyApplicationInput{
|
|
CommandID: "review-approve-202-301",
|
|
ReviewerUserID: 101,
|
|
ApplicationID: application.ApplicationID,
|
|
Decision: hostdomain.ApplicationDecisionApprove,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ReviewAgencyApplication approve failed: %v", err)
|
|
}
|
|
if result.Application.Status != hostdomain.ApplicationStatusApproved {
|
|
t.Fatalf("approved application mismatch: %+v", result.Application)
|
|
}
|
|
if result.HostProfile.UserID != 202 || result.HostProfile.CurrentAgencyID != 301 || result.Membership.Status != hostdomain.MembershipStatusActive {
|
|
t.Fatalf("host membership facts mismatch: profile=%+v membership=%+v", result.HostProfile, result.Membership)
|
|
}
|
|
|
|
retriedReview, err := svc.ReviewAgencyApplication(ctx, hostservice.ReviewAgencyApplicationInput{
|
|
CommandID: "review-approve-202-301",
|
|
ReviewerUserID: 101,
|
|
ApplicationID: application.ApplicationID,
|
|
Decision: hostdomain.ApplicationDecisionApprove,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("retry ReviewAgencyApplication failed: %v", err)
|
|
}
|
|
if retriedReview.Membership.MembershipID != result.Membership.MembershipID {
|
|
t.Fatalf("review retry must return same membership: got %d want %d", retriedReview.Membership.MembershipID, result.Membership.MembershipID)
|
|
}
|
|
|
|
_, err = svc.ApplyToAgency(ctx, hostservice.ApplyToAgencyInput{
|
|
CommandID: "apply-second-active-member",
|
|
UserID: 202,
|
|
AgencyID: 301,
|
|
})
|
|
if got := xerr.CodeOf(err); got != xerr.Conflict {
|
|
t.Fatalf("active member must not create another application: got %s err=%v", got, err)
|
|
}
|
|
}
|
|
|
|
func TestAcceptAgencyInvitationCreatesOwnerHostAndReusesDetachedHost(t *testing.T) {
|
|
ctx := context.Background()
|
|
repository := mysqltest.NewRepository(t)
|
|
seedActiveUser(t, repository, 501, 10)
|
|
seedActiveUser(t, repository, 601, 10)
|
|
seedActiveUser(t, repository, 602, 10)
|
|
repository.PutBDProfile(hostdomain.BDProfile{
|
|
UserID: 501,
|
|
Role: hostdomain.BDRoleBD,
|
|
RegionID: 10,
|
|
Status: hostdomain.BDStatusActive,
|
|
})
|
|
svc := newHostService(repository, 2000, 3000)
|
|
|
|
invitation, err := svc.InviteAgency(ctx, hostservice.InviteAgencyInput{
|
|
CommandID: "invite-agency-601",
|
|
InviterUserID: 501,
|
|
TargetUserID: 601,
|
|
AgencyName: "Owner Agency",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("InviteAgency failed: %v", err)
|
|
}
|
|
if invitation.Status != hostdomain.InvitationStatusAccepted {
|
|
t.Fatalf("agency invitation should be accepted immediately: %+v", invitation)
|
|
}
|
|
summary, err := svc.GetUserRoleSummary(ctx, 601)
|
|
if err != nil {
|
|
t.Fatalf("GetUserRoleSummary failed: %v", err)
|
|
}
|
|
agency, err := svc.GetAgency(ctx, summary.AgencyID)
|
|
if err != nil {
|
|
t.Fatalf("GetAgency failed: %v", err)
|
|
}
|
|
hostProfile, err := svc.GetHostProfile(ctx, 601)
|
|
if err != nil {
|
|
t.Fatalf("GetHostProfile failed: %v", err)
|
|
}
|
|
if !summary.IsAgency || !summary.IsHost || agency.OwnerUserID != 601 || hostProfile.CurrentAgencyID != agency.AgencyID {
|
|
t.Fatalf("owner agency facts mismatch: summary=%+v agency=%+v host=%+v", summary, agency, hostProfile)
|
|
}
|
|
repository.PutHostProfile(hostdomain.HostProfile{
|
|
UserID: 602,
|
|
Status: hostdomain.HostStatusActive,
|
|
RegionID: 10,
|
|
Source: "test",
|
|
})
|
|
secondInvitation, err := svc.InviteAgency(ctx, hostservice.InviteAgencyInput{
|
|
CommandID: "invite-agency-602",
|
|
InviterUserID: 501,
|
|
TargetUserID: 602,
|
|
AgencyName: "Reused Host Agency",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("detached active host should become agency owner: %v", err)
|
|
}
|
|
secondSummary, err := svc.GetUserRoleSummary(ctx, 602)
|
|
if err != nil {
|
|
t.Fatalf("GetUserRoleSummary for reused host failed: %v", err)
|
|
}
|
|
secondHostProfile, err := svc.GetHostProfile(ctx, 602)
|
|
if err != nil {
|
|
t.Fatalf("GetHostProfile for reused host failed: %v", err)
|
|
}
|
|
if !secondSummary.IsAgency || !secondSummary.IsHost || secondHostProfile.CurrentAgencyID <= 0 || secondHostProfile.Source != "test" || secondInvitation.Status != hostdomain.InvitationStatusAccepted {
|
|
t.Fatalf("detached host reuse mismatch: summary=%+v host=%+v invitation=%+v", secondSummary, secondHostProfile, secondInvitation)
|
|
}
|
|
_, err = svc.InviteAgency(ctx, hostservice.InviteAgencyInput{
|
|
CommandID: "invite-agency-602-again",
|
|
InviterUserID: 501,
|
|
TargetUserID: 602,
|
|
AgencyName: "Blocked Agency",
|
|
})
|
|
if got := xerr.CodeOf(err); got != xerr.Conflict {
|
|
t.Fatalf("host with active membership must not become another agency owner: got %s err=%v", got, err)
|
|
}
|
|
}
|
|
|
|
func TestAcceptBDInvitationDoesNotCreateHostProfile(t *testing.T) {
|
|
ctx := context.Background()
|
|
repository := mysqltest.NewRepository(t)
|
|
seedActiveUser(t, repository, 701, 10)
|
|
seedActiveUser(t, repository, 702, 10)
|
|
repository.PutBDProfile(hostdomain.BDProfile{
|
|
UserID: 701,
|
|
Role: hostdomain.BDRoleLeader,
|
|
RegionID: 10,
|
|
Status: hostdomain.BDStatusActive,
|
|
})
|
|
svc := newHostService(repository, 3000, 4000)
|
|
|
|
invitation, err := svc.InviteBD(ctx, hostservice.InviteBDInput{
|
|
CommandID: "invite-bd-702",
|
|
InviterUserID: 701,
|
|
TargetUserID: 702,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("InviteBD failed: %v", err)
|
|
}
|
|
if invitation.Status != hostdomain.InvitationStatusAccepted {
|
|
t.Fatalf("bd invitation should be accepted immediately: %+v", invitation)
|
|
}
|
|
bdProfile, err := svc.GetBDProfile(ctx, 702)
|
|
if err != nil {
|
|
t.Fatalf("GetBDProfile failed: %v", err)
|
|
}
|
|
if bdProfile.UserID != 702 || bdProfile.Role != hostdomain.BDRoleBD || bdProfile.ParentLeaderUserID != 701 {
|
|
t.Fatalf("bd profile mismatch: %+v", bdProfile)
|
|
}
|
|
if _, err := svc.GetHostProfile(ctx, 702); xerr.CodeOf(err) != xerr.NotFound {
|
|
t.Fatalf("becoming bd must not create host profile: err=%v", err)
|
|
}
|
|
}
|
|
|
|
func TestAdminCreateBDLeaderAndBDIdempotently(t *testing.T) {
|
|
ctx := context.Background()
|
|
repository := mysqltest.NewRepository(t)
|
|
repository.PutRegion(userdomain.Region{RegionID: 20, RegionCode: "R20", Name: "Region 20"})
|
|
seedActiveUser(t, repository, 801, 20)
|
|
seedActiveUser(t, repository, 802, 20)
|
|
seedActiveUser(t, repository, 804, 20)
|
|
svc := newHostService(repository, 4000, 5000)
|
|
|
|
leader, err := svc.CreateBDLeader(ctx, hostservice.CreateBDLeaderInput{
|
|
CommandID: "admin-create-leader-801",
|
|
AdminUserID: 1,
|
|
TargetUserID: 801,
|
|
Reason: "seed leader",
|
|
RequestID: "req-leader-1",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("CreateBDLeader failed: %v", err)
|
|
}
|
|
if leader.Role != hostdomain.BDRoleLeader || leader.Status != hostdomain.BDStatusActive || leader.RegionID != 20 {
|
|
t.Fatalf("leader mismatch: %+v", leader)
|
|
}
|
|
updatedUser, err := repository.GetUser(ctx, 801)
|
|
if err != nil {
|
|
t.Fatalf("GetUser after CreateBDLeader failed: %v", err)
|
|
}
|
|
if updatedUser.RegionID != 20 {
|
|
t.Fatalf("CreateBDLeader must keep target user region as profile region: %+v", updatedUser)
|
|
}
|
|
retriedLeader, err := svc.CreateBDLeader(ctx, hostservice.CreateBDLeaderInput{
|
|
CommandID: "admin-create-leader-801",
|
|
AdminUserID: 1,
|
|
TargetUserID: 801,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("retry CreateBDLeader failed: %v", err)
|
|
}
|
|
if retriedLeader.UserID != leader.UserID {
|
|
t.Fatalf("leader retry must replay existing result: %+v", retriedLeader)
|
|
}
|
|
|
|
bd, err := svc.CreateBD(ctx, hostservice.CreateBDInput{
|
|
CommandID: "admin-create-bd-802",
|
|
AdminUserID: 1,
|
|
TargetUserID: 802,
|
|
ParentLeaderUserID: 801,
|
|
Reason: "seed bd",
|
|
RequestID: "req-bd-1",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("CreateBD failed: %v", err)
|
|
}
|
|
if bd.Role != hostdomain.BDRoleBD || bd.ParentLeaderUserID != 801 || bd.RegionID != 20 {
|
|
t.Fatalf("bd mismatch: %+v", bd)
|
|
}
|
|
|
|
independentBD, err := svc.CreateBD(ctx, hostservice.CreateBDInput{
|
|
CommandID: "admin-create-bd-804-independent",
|
|
AdminUserID: 1,
|
|
TargetUserID: 804,
|
|
Reason: "seed independent bd",
|
|
RequestID: "req-bd-independent-1",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("CreateBD without parent leader failed: %v", err)
|
|
}
|
|
if independentBD.Role != hostdomain.BDRoleBD || independentBD.ParentLeaderUserID != 0 || independentBD.RegionID != 20 {
|
|
t.Fatalf("independent bd mismatch: %+v", independentBD)
|
|
}
|
|
|
|
disabled, err := svc.SetBDStatus(ctx, hostservice.SetBDStatusInput{
|
|
CommandID: "admin-disable-bd-802",
|
|
AdminUserID: 1,
|
|
TargetUserID: 802,
|
|
Status: hostdomain.BDStatusDisabled,
|
|
Reason: "stop bd",
|
|
RequestID: "req-bd-stop-1",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("SetBDStatus disabled failed: %v", err)
|
|
}
|
|
if disabled.Status != hostdomain.BDStatusDisabled {
|
|
t.Fatalf("disabled bd mismatch: %+v", disabled)
|
|
}
|
|
}
|
|
|
|
func TestAdminCreateBDLeaderRejectsDisabledRegion(t *testing.T) {
|
|
ctx := context.Background()
|
|
repository := mysqltest.NewRepository(t)
|
|
repository.PutRegion(userdomain.Region{RegionID: 21, RegionCode: "R21", Name: "Region 21", Status: userdomain.RegionStatusDisabled})
|
|
seedActiveUser(t, repository, 803, 21)
|
|
svc := newHostService(repository, 4100, 5100)
|
|
|
|
_, err := svc.CreateBDLeader(ctx, hostservice.CreateBDLeaderInput{
|
|
CommandID: "admin-create-leader-disabled-region",
|
|
AdminUserID: 1,
|
|
TargetUserID: 803,
|
|
})
|
|
if got := xerr.CodeOf(err); got != xerr.RegionDisabled {
|
|
t.Fatalf("disabled region must be rejected: got %s err=%v", got, err)
|
|
}
|
|
}
|
|
|
|
func TestAdminCreateAgencyControlsAppSearch(t *testing.T) {
|
|
ctx := context.Background()
|
|
repository := mysqltest.NewRepository(t)
|
|
seedActiveUserInCountry(t, repository, 901, 30, "CN", "https://cdn.example/owner-901.png")
|
|
seedActiveUser(t, repository, 902, 30)
|
|
seedActiveUserInCountry(t, repository, 903, 30, "US", "")
|
|
repository.PutBDProfile(hostdomain.BDProfile{
|
|
UserID: 900,
|
|
Role: hostdomain.BDRoleLeader,
|
|
RegionID: 30,
|
|
Status: hostdomain.BDStatusActive,
|
|
})
|
|
svc := newHostService(repository, 5000, 6000)
|
|
|
|
created, err := svc.CreateAgency(ctx, hostservice.CreateAgencyInput{
|
|
CommandID: "admin-create-agency-901",
|
|
AdminUserID: 1,
|
|
OwnerUserID: 901,
|
|
ParentBDUserID: 900,
|
|
Name: "Admin Seed Agency",
|
|
JoinEnabled: true,
|
|
Reason: "seed agency",
|
|
RequestID: "req-agency-1",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("CreateAgency failed: %v", err)
|
|
}
|
|
if created.Agency.OwnerUserID != 901 || created.Agency.ParentBDUserID != 900 || !created.Agency.JoinEnabled {
|
|
t.Fatalf("agency mismatch: %+v", created.Agency)
|
|
}
|
|
if created.HostProfile.CurrentAgencyID != created.Agency.AgencyID || created.HostProfile.Source != hostdomain.HostSourceAdminCreateAgency {
|
|
t.Fatalf("owner host profile mismatch: %+v", created.HostProfile)
|
|
}
|
|
if created.Membership.MembershipType != hostdomain.MembershipTypeOwner || created.Membership.Status != hostdomain.MembershipStatusActive {
|
|
t.Fatalf("owner membership mismatch: %+v", created.Membership)
|
|
}
|
|
if _, err := svc.CreateAgency(ctx, hostservice.CreateAgencyInput{
|
|
CommandID: "admin-create-agency-903",
|
|
AdminUserID: 1,
|
|
OwnerUserID: 903,
|
|
ParentBDUserID: 900,
|
|
Name: "Other Country Agency",
|
|
JoinEnabled: true,
|
|
Reason: "seed agency",
|
|
RequestID: "req-agency-2",
|
|
}); err != nil {
|
|
t.Fatalf("CreateAgency in other country failed: %v", err)
|
|
}
|
|
agencies, err := svc.SearchAgencies(ctx, 902, "", 20)
|
|
if err != nil {
|
|
t.Fatalf("SearchAgencies list failed: %v", err)
|
|
}
|
|
if len(agencies) != 1 || agencies[0].AgencyID != created.Agency.AgencyID || agencies[0].OwnerAvatar != "https://cdn.example/owner-901.png" || agencies[0].ActiveHostCount != 1 {
|
|
t.Fatalf("created agency list should be scoped by country with display fields: %+v", agencies)
|
|
}
|
|
agencies, err = svc.SearchAgencies(ctx, 902, "Admin Seed", 20)
|
|
if err != nil {
|
|
t.Fatalf("SearchAgencies failed: %v", err)
|
|
}
|
|
if len(agencies) != 1 || agencies[0].AgencyID != created.Agency.AgencyID {
|
|
t.Fatalf("created agency should be searchable: %+v", agencies)
|
|
}
|
|
agencies, err = svc.SearchAgencies(ctx, 902, displayID(901), 20)
|
|
if err != nil {
|
|
t.Fatalf("SearchAgencies by owner display id failed: %v", err)
|
|
}
|
|
if len(agencies) != 1 || agencies[0].AgencyID != created.Agency.AgencyID {
|
|
t.Fatalf("created agency should be searchable by owner display id: %+v", agencies)
|
|
}
|
|
|
|
disabledJoin, err := svc.SetAgencyJoinEnabled(ctx, hostservice.SetAgencyJoinEnabledInput{
|
|
CommandID: "admin-disable-agency-join-901",
|
|
AdminUserID: 1,
|
|
AgencyID: created.Agency.AgencyID,
|
|
JoinEnabled: false,
|
|
Reason: "pause join",
|
|
RequestID: "req-agency-join-1",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("SetAgencyJoinEnabled failed: %v", err)
|
|
}
|
|
if disabledJoin.JoinEnabled {
|
|
t.Fatalf("agency join should be disabled: %+v", disabledJoin)
|
|
}
|
|
agencies, err = svc.SearchAgencies(ctx, 902, "Admin Seed", 20)
|
|
if err != nil {
|
|
t.Fatalf("SearchAgencies after join disabled failed: %v", err)
|
|
}
|
|
if len(agencies) != 0 {
|
|
t.Fatalf("join-disabled agency must not be searchable: %+v", agencies)
|
|
}
|
|
|
|
closed, err := svc.CloseAgency(ctx, hostservice.CloseAgencyInput{
|
|
CommandID: "admin-close-agency-901",
|
|
AdminUserID: 1,
|
|
AgencyID: created.Agency.AgencyID,
|
|
Reason: "close agency",
|
|
RequestID: "req-agency-close-1",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("CloseAgency failed: %v", err)
|
|
}
|
|
if closed.Status != hostdomain.AgencyStatusClosed || closed.JoinEnabled {
|
|
t.Fatalf("closed agency mismatch: %+v", closed)
|
|
}
|
|
|
|
deleted, err := svc.DeleteAgency(ctx, hostservice.DeleteAgencyInput{
|
|
CommandID: "admin-delete-agency-901",
|
|
AdminUserID: 1,
|
|
AgencyID: created.Agency.AgencyID,
|
|
Reason: "delete agency",
|
|
RequestID: "req-agency-delete-1",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("DeleteAgency failed: %v", err)
|
|
}
|
|
if deleted.Status != hostdomain.AgencyStatusDeleted || deleted.JoinEnabled {
|
|
t.Fatalf("deleted agency mismatch: %+v", deleted)
|
|
}
|
|
hostProfile, err := svc.GetHostProfile(ctx, 901)
|
|
if err != nil {
|
|
t.Fatalf("GetHostProfile after delete failed: %v", err)
|
|
}
|
|
if hostProfile.CurrentAgencyID != 0 || hostProfile.CurrentMembershipID != 0 || hostProfile.Status != hostdomain.HostStatusActive {
|
|
t.Fatalf("delete agency should detach but keep host profile: %+v", hostProfile)
|
|
}
|
|
recreated, err := svc.CreateAgency(ctx, hostservice.CreateAgencyInput{
|
|
CommandID: "admin-create-agency-901-after-delete",
|
|
AdminUserID: 1,
|
|
OwnerUserID: 901,
|
|
ParentBDUserID: 900,
|
|
Name: "Recreated Agency",
|
|
JoinEnabled: true,
|
|
Reason: "recreate agency",
|
|
RequestID: "req-agency-recreate-1",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("detached owner host should be reusable after delete: %v", err)
|
|
}
|
|
if recreated.HostProfile.CurrentAgencyID != recreated.Agency.AgencyID || recreated.HostProfile.FirstBecameHostAtMs != hostProfile.FirstBecameHostAtMs {
|
|
t.Fatalf("recreated agency should reuse host identity: host=%+v agency=%+v oldHost=%+v", recreated.HostProfile, recreated.Agency, hostProfile)
|
|
}
|
|
}
|
|
|
|
func TestAdminCreateAgencyWithoutParentBD(t *testing.T) {
|
|
ctx := context.Background()
|
|
repository := mysqltest.NewRepository(t)
|
|
seedActiveUser(t, repository, 904, 31)
|
|
svc := newHostService(repository, 5600, 6600)
|
|
|
|
created, err := svc.CreateAgency(ctx, hostservice.CreateAgencyInput{
|
|
CommandID: "admin-create-agency-904-independent",
|
|
AdminUserID: 1,
|
|
OwnerUserID: 904,
|
|
Name: "Independent Agency",
|
|
JoinEnabled: true,
|
|
Reason: "seed independent agency",
|
|
RequestID: "req-agency-independent-1",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("CreateAgency without parent bd failed: %v", err)
|
|
}
|
|
if created.Agency.OwnerUserID != 904 || created.Agency.ParentBDUserID != 0 || created.Agency.RegionID != 31 || !created.Agency.JoinEnabled {
|
|
t.Fatalf("independent agency mismatch: %+v", created.Agency)
|
|
}
|
|
if created.HostProfile.CurrentAgencyID != created.Agency.AgencyID || created.Membership.MembershipType != hostdomain.MembershipTypeOwner {
|
|
t.Fatalf("independent agency owner facts mismatch: host=%+v membership=%+v", created.HostProfile, created.Membership)
|
|
}
|
|
}
|
|
|
|
func TestAdminCreateAgencyReusesDetachedHost(t *testing.T) {
|
|
ctx := context.Background()
|
|
repository := mysqltest.NewRepository(t)
|
|
seedActiveUser(t, repository, 911, 40)
|
|
repository.PutBDProfile(hostdomain.BDProfile{
|
|
UserID: 910,
|
|
Role: hostdomain.BDRoleBD,
|
|
RegionID: 40,
|
|
Status: hostdomain.BDStatusActive,
|
|
})
|
|
repository.PutHostProfile(hostdomain.HostProfile{
|
|
UserID: 911,
|
|
Status: hostdomain.HostStatusActive,
|
|
RegionID: 40,
|
|
Source: "test",
|
|
})
|
|
svc := newHostService(repository, 6000, 7000)
|
|
|
|
created, err := svc.CreateAgency(ctx, hostservice.CreateAgencyInput{
|
|
CommandID: "admin-create-agency-existing-host",
|
|
AdminUserID: 1,
|
|
OwnerUserID: 911,
|
|
ParentBDUserID: 910,
|
|
Name: "Reused Host Agency",
|
|
JoinEnabled: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("detached host should become agency owner: %v", err)
|
|
}
|
|
if created.HostProfile.CurrentAgencyID != created.Agency.AgencyID || created.HostProfile.Source != "test" {
|
|
t.Fatalf("existing host identity should be reused without rewriting source: %+v", created.HostProfile)
|
|
}
|
|
_, err = svc.CreateAgency(ctx, hostservice.CreateAgencyInput{
|
|
CommandID: "admin-create-agency-existing-host-again",
|
|
AdminUserID: 1,
|
|
OwnerUserID: 911,
|
|
ParentBDUserID: 910,
|
|
Name: "Blocked Agency",
|
|
JoinEnabled: true,
|
|
})
|
|
if got := xerr.CodeOf(err); got != xerr.Conflict {
|
|
t.Fatalf("host with active membership must not become another agency owner: got %s err=%v", got, err)
|
|
}
|
|
}
|
|
|
|
func TestAdminCreateCoinSellerCanCoexistWithBDAndAgency(t *testing.T) {
|
|
ctx := context.Background()
|
|
repository := mysqltest.NewRepository(t)
|
|
seedActiveUser(t, repository, 921, 50)
|
|
repository.PutBDProfile(hostdomain.BDProfile{
|
|
UserID: 921,
|
|
Role: hostdomain.BDRoleLeader,
|
|
RegionID: 50,
|
|
Status: hostdomain.BDStatusActive,
|
|
})
|
|
repository.PutAgency(hostdomain.Agency{
|
|
AgencyID: 922,
|
|
OwnerUserID: 921,
|
|
RegionID: 50,
|
|
ParentBDUserID: 921,
|
|
Name: "Seller Agency",
|
|
Status: hostdomain.AgencyStatusActive,
|
|
JoinEnabled: true,
|
|
CreatedAtMs: 100,
|
|
UpdatedAtMs: 100,
|
|
})
|
|
svc := newHostService(repository, 7000, 8000)
|
|
|
|
profile, err := svc.CreateCoinSeller(ctx, hostservice.CreateCoinSellerInput{
|
|
CommandID: "admin-create-coin-seller-921",
|
|
AdminUserID: 1,
|
|
TargetUserID: 921,
|
|
Reason: "seed coin seller",
|
|
RequestID: "req-coin-seller-1",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("CreateCoinSeller failed: %v", err)
|
|
}
|
|
if profile.Status != hostdomain.CoinSellerStatusActive || profile.MerchantAssetType != hostdomain.CoinSellerMerchantAssetType {
|
|
t.Fatalf("coin seller profile mismatch: %+v", profile)
|
|
}
|
|
retried, err := svc.CreateCoinSeller(ctx, hostservice.CreateCoinSellerInput{
|
|
CommandID: "admin-create-coin-seller-921",
|
|
AdminUserID: 1,
|
|
TargetUserID: 921,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("retry CreateCoinSeller failed: %v", err)
|
|
}
|
|
if retried.UserID != profile.UserID {
|
|
t.Fatalf("coin seller retry must replay existing result: %+v", retried)
|
|
}
|
|
|
|
disabled, err := svc.SetCoinSellerStatus(ctx, hostservice.SetCoinSellerStatusInput{
|
|
CommandID: "admin-disable-coin-seller-921",
|
|
AdminUserID: 1,
|
|
TargetUserID: 921,
|
|
Status: hostdomain.CoinSellerStatusDisabled,
|
|
Reason: "pause seller",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("SetCoinSellerStatus failed: %v", err)
|
|
}
|
|
if disabled.Status != hostdomain.CoinSellerStatusDisabled {
|
|
t.Fatalf("disabled coin seller mismatch: %+v", disabled)
|
|
}
|
|
}
|
|
|
|
func TestListActiveCoinSellersInMyRegionFiltersByCurrentUserRegion(t *testing.T) {
|
|
ctx := context.Background()
|
|
repository := mysqltest.NewRepository(t)
|
|
repository.PutCountry(userdomain.Country{
|
|
CountryID: 86,
|
|
CountryCode: "CN",
|
|
CountryName: "China",
|
|
CountryDisplayName: "中国",
|
|
Enabled: true,
|
|
})
|
|
repository.PutRegion(userdomain.Region{
|
|
RegionID: 70,
|
|
RegionCode: "east-asia",
|
|
Name: "East Asia",
|
|
Status: userdomain.RegionStatusActive,
|
|
Countries: []string{"CN"},
|
|
})
|
|
repository.PutRegion(userdomain.Region{
|
|
RegionID: 71,
|
|
RegionCode: "west-asia",
|
|
Name: "West Asia",
|
|
Status: userdomain.RegionStatusActive,
|
|
Countries: []string{"CN"},
|
|
})
|
|
seedActiveUser(t, repository, 940, 70)
|
|
repository.PutUser(userdomain.User{
|
|
UserID: 941,
|
|
DefaultDisplayUserID: displayID(941),
|
|
CurrentDisplayUserID: displayID(941),
|
|
Username: "Seller One",
|
|
Avatar: "https://cdn.example/seller-one.png",
|
|
Country: "CN",
|
|
RegionID: 70,
|
|
ProfileCompleted: true,
|
|
ProfileCompletedAtMs: 1,
|
|
OnboardingStatus: userdomain.OnboardingStatusCompleted,
|
|
Status: userdomain.StatusActive,
|
|
})
|
|
repository.PutUser(userdomain.User{
|
|
UserID: 942,
|
|
DefaultDisplayUserID: displayID(942),
|
|
CurrentDisplayUserID: displayID(942),
|
|
Username: "Disabled Seller",
|
|
Country: "CN",
|
|
RegionID: 70,
|
|
ProfileCompleted: true,
|
|
ProfileCompletedAtMs: 1,
|
|
OnboardingStatus: userdomain.OnboardingStatusCompleted,
|
|
Status: userdomain.StatusActive,
|
|
})
|
|
repository.PutUser(userdomain.User{
|
|
UserID: 943,
|
|
DefaultDisplayUserID: displayID(943),
|
|
CurrentDisplayUserID: displayID(943),
|
|
Username: "Other Region Seller",
|
|
Country: "CN",
|
|
RegionID: 71,
|
|
ProfileCompleted: true,
|
|
ProfileCompletedAtMs: 1,
|
|
OnboardingStatus: userdomain.OnboardingStatusCompleted,
|
|
Status: userdomain.StatusActive,
|
|
})
|
|
svc := newHostService(repository, 9000, 10000)
|
|
for _, userID := range []int64{941, 942, 943} {
|
|
if _, err := svc.CreateCoinSeller(ctx, hostservice.CreateCoinSellerInput{
|
|
CommandID: fmt.Sprintf("admin-create-coin-seller-%d", userID),
|
|
AdminUserID: 1,
|
|
TargetUserID: userID,
|
|
}); err != nil {
|
|
t.Fatalf("CreateCoinSeller(%d) failed: %v", userID, err)
|
|
}
|
|
}
|
|
if _, err := svc.SetCoinSellerStatus(ctx, hostservice.SetCoinSellerStatusInput{
|
|
CommandID: "admin-disable-coin-seller-942",
|
|
AdminUserID: 1,
|
|
TargetUserID: 942,
|
|
Status: hostdomain.CoinSellerStatusDisabled,
|
|
}); err != nil {
|
|
t.Fatalf("SetCoinSellerStatus failed: %v", err)
|
|
}
|
|
|
|
items, err := svc.ListActiveCoinSellersInMyRegion(ctx, 940)
|
|
if err != nil {
|
|
t.Fatalf("ListActiveCoinSellersInMyRegion failed: %v", err)
|
|
}
|
|
if len(items) != 1 {
|
|
t.Fatalf("only same-region active sellers should be returned: %+v", items)
|
|
}
|
|
item := items[0]
|
|
if item.UserID != 941 || item.Username != "Seller One" || item.Avatar == "" || item.CountryID != 86 || item.CountryCode != "CN" || item.RegionID != 70 || item.RegionCode != "east-asia" || item.Status != hostdomain.CoinSellerStatusActive {
|
|
t.Fatalf("coin seller item mismatch: %+v", item)
|
|
}
|
|
}
|
|
|
|
func TestGetUserRoleSummaryAggregatesRolesAndPendingInvitations(t *testing.T) {
|
|
ctx := context.Background()
|
|
repository := mysqltest.NewRepository(t)
|
|
seedActiveUser(t, repository, 931, 60)
|
|
seedActiveUser(t, repository, 932, 60)
|
|
repository.PutHostProfile(hostdomain.HostProfile{
|
|
UserID: 931,
|
|
Status: hostdomain.HostStatusActive,
|
|
RegionID: 60,
|
|
CurrentAgencyID: 933,
|
|
Source: hostdomain.HostSourceAdminCreateAgency,
|
|
})
|
|
repository.PutAgency(hostdomain.Agency{
|
|
AgencyID: 933,
|
|
OwnerUserID: 931,
|
|
RegionID: 60,
|
|
ParentBDUserID: 931,
|
|
Status: hostdomain.AgencyStatusActive,
|
|
JoinEnabled: true,
|
|
})
|
|
repository.PutBDProfile(hostdomain.BDProfile{
|
|
UserID: 931,
|
|
Role: hostdomain.BDRoleLeader,
|
|
RegionID: 60,
|
|
Status: hostdomain.BDStatusActive,
|
|
})
|
|
svc := newHostService(repository, 8000, 9000)
|
|
if _, err := svc.CreateCoinSeller(ctx, hostservice.CreateCoinSellerInput{
|
|
CommandID: "admin-create-coin-seller-931",
|
|
AdminUserID: 1,
|
|
TargetUserID: 931,
|
|
Reason: "summary test",
|
|
}); err != nil {
|
|
t.Fatalf("CreateCoinSeller failed: %v", err)
|
|
}
|
|
if _, err := svc.InviteBD(ctx, hostservice.InviteBDInput{
|
|
CommandID: "invite-bd-932",
|
|
InviterUserID: 931,
|
|
TargetUserID: 932,
|
|
}); err != nil {
|
|
t.Fatalf("InviteBD failed: %v", err)
|
|
}
|
|
|
|
summary, err := svc.GetUserRoleSummary(ctx, 931)
|
|
if err != nil {
|
|
t.Fatalf("GetUserRoleSummary failed: %v", err)
|
|
}
|
|
if !summary.IsHost || !summary.IsAgency || !summary.IsManager || !summary.IsBD || !summary.IsBDLeader || !summary.IsCoinSeller || summary.AgencyID != 933 || summary.BDID != 931 {
|
|
t.Fatalf("role summary mismatch: %+v", summary)
|
|
}
|
|
directBD, err := svc.GetUserRoleSummary(ctx, 932)
|
|
if err != nil {
|
|
t.Fatalf("GetUserRoleSummary direct bd failed: %v", err)
|
|
}
|
|
if directBD.PendingRoleInvitations != 0 || directBD.IsHost || !directBD.IsBD || directBD.BDID != 932 {
|
|
t.Fatalf("direct bd role summary mismatch: %+v", directBD)
|
|
}
|
|
}
|