package services import ( "database/sql" "errors" "testing" "time" "quyun/v2/app/commands/testx" "quyun/v2/app/errorx" super_dto "quyun/v2/app/http/super/v1/dto" "quyun/v2/app/requests" "quyun/v2/database" "quyun/v2/database/models" "quyun/v2/pkg/consts" "github.com/samber/lo" . "github.com/smartystreets/goconvey/convey" "github.com/stretchr/testify/suite" "go.ipao.vip/atom/contracts" "go.ipao.vip/gen/types" "go.uber.org/dig" ) type SuperTestSuiteInjectParams struct { dig.In DB *sql.DB Initials []contracts.Initial `group:"initials"` } type SuperTestSuite struct { suite.Suite SuperTestSuiteInjectParams } func Test_Super(t *testing.T) { providers := testx.Default().With(Provide) testx.Serve(providers, t, func(p SuperTestSuiteInjectParams) { suite.Run(t, &SuperTestSuite{SuperTestSuiteInjectParams: p}) }) } func (s *SuperTestSuite) Test_ListUsers() { Convey("ListUsers", s.T(), func() { ctx := s.T().Context() database.Truncate(ctx, s.DB, models.TableNameUser) u1 := &models.User{Username: "user1", Nickname: "Alice"} u2 := &models.User{Username: "user2", Nickname: "Bob"} models.UserQuery.WithContext(ctx).Create(u1, u2) Convey("should list users", func() { filter := &super_dto.UserListFilter{ Pagination: requests.Pagination{Page: 1, Limit: 10}, } res, err := Super.ListUsers(ctx, filter) So(err, ShouldBeNil) So(res.Total, ShouldEqual, 2) items := res.Items.([]super_dto.UserItem) So(items[0].Username, ShouldEqual, "user2") // Desc order }) Convey("should filter users", func() { filter := &super_dto.UserListFilter{ Pagination: requests.Pagination{Page: 1, Limit: 10}, Username: lo.ToPtr("Alice"), } res, err := Super.ListUsers(ctx, filter) So(err, ShouldBeNil) So(res.Total, ShouldEqual, 1) items := res.Items.([]super_dto.UserItem) So(items[0].Username, ShouldEqual, "user1") }) }) } func (s *SuperTestSuite) Test_LoginAndCheckToken() { Convey("Login and CheckToken", s.T(), func() { ctx := s.T().Context() database.Truncate(ctx, s.DB, models.TableNameUser) admin := &models.User{ Username: "super_admin", Password: "pass123", Roles: types.Array[consts.Role]{consts.RoleSuperAdmin}, Status: consts.UserStatusVerified, } normal := &models.User{ Username: "normal_user", Password: "pass123", Status: consts.UserStatusVerified, } models.UserQuery.WithContext(ctx).Create(admin, normal) Convey("should login as super admin", func() { res, err := Super.Login(ctx, &super_dto.LoginForm{ Username: admin.Username, Password: admin.Password, }) So(err, ShouldBeNil) So(res, ShouldNotBeNil) So(res.Token, ShouldNotBeBlank) So(res.User.ID, ShouldEqual, admin.ID) }) Convey("should reject non-super admin", func() { _, err := Super.Login(ctx, &super_dto.LoginForm{ Username: normal.Username, Password: normal.Password, }) So(err, ShouldNotBeNil) }) Convey("should refresh token", func() { loginRes, err := Super.Login(ctx, &super_dto.LoginForm{ Username: admin.Username, Password: admin.Password, }) So(err, ShouldBeNil) token := "Bearer " + loginRes.Token checkRes, err := Super.CheckToken(ctx, token) So(err, ShouldBeNil) So(checkRes, ShouldNotBeNil) So(checkRes.Token, ShouldNotBeBlank) So(checkRes.User.ID, ShouldEqual, admin.ID) }) }) } func (s *SuperTestSuite) Test_CreateTenant() { Convey("CreateTenant", s.T(), func() { ctx := s.T().Context() database.Truncate(ctx, s.DB, models.TableNameUser, models.TableNameTenant) u := &models.User{Username: "admin1"} models.UserQuery.WithContext(ctx).Create(u) Convey("should create tenant", func() { form := &super_dto.TenantCreateForm{ Name: "Super Tenant", Code: "st1", AdminUserID: u.ID, } err := Super.CreateTenant(ctx, form) So(err, ShouldBeNil) t, _ := models.TenantQuery.WithContext(ctx).Where(models.TenantQuery.Code.Eq("st1")).First() So(t, ShouldNotBeNil) So(t.Name, ShouldEqual, "Super Tenant") So(t.UserID, ShouldEqual, u.ID) So(t.Status, ShouldEqual, consts.TenantStatusVerified) }) }) } func (s *SuperTestSuite) Test_WithdrawalApproval() { Convey("Withdrawal Approval", s.T(), func() { ctx := s.T().Context() database.Truncate(ctx, s.DB, models.TableNameOrder, models.TableNameUser, models.TableNameTenantLedger) u := &models.User{Username: "user_w", Balance: 1000} // Initial 10.00 admin := &models.User{Username: "admin_w"} models.UserQuery.WithContext(ctx).Create(u, admin) // Create Withdrawal Order (Pending) o1 := &models.Order{ UserID: u.ID, Type: consts.OrderTypeWithdrawal, Status: consts.OrderStatusCreated, AmountPaid: 500, } models.OrderQuery.WithContext(ctx).Create(o1) Convey("should list withdrawals", func() { filter := &super_dto.SuperOrderListFilter{Pagination: requests.Pagination{Page: 1, Limit: 10}} res, err := Super.ListWithdrawals(ctx, filter) So(err, ShouldBeNil) So(res.Total, ShouldEqual, 1) }) Convey("should approve withdrawal", func() { err := Super.ApproveWithdrawal(ctx, admin.ID, o1.ID) So(err, ShouldBeNil) oReload, _ := models.OrderQuery.WithContext(ctx).Where(models.OrderQuery.ID.Eq(o1.ID)).First() So(oReload.Status, ShouldEqual, consts.OrderStatusPaid) }) Convey("should reject withdrawal and refund", func() { // Another order o2 := &models.Order{ UserID: u.ID, Type: consts.OrderTypeWithdrawal, Status: consts.OrderStatusCreated, AmountPaid: 200, } models.OrderQuery.WithContext(ctx).Create(o2) // Assuming user balance was deducted when o2 was created (logic in creator service) // But here we set balance manually to 1000. Let's assume it was 1200 before. // Current balance 1000. Refund 200 -> Expect 1200. err := Super.RejectWithdrawal(ctx, admin.ID, o2.ID, "Invalid account") So(err, ShouldBeNil) oReload, _ := models.OrderQuery.WithContext(ctx).Where(models.OrderQuery.ID.Eq(o2.ID)).First() So(oReload.Status, ShouldEqual, consts.OrderStatusFailed) uReload, _ := models.UserQuery.WithContext(ctx).Where(models.UserQuery.ID.Eq(u.ID)).First() So(uReload.Balance, ShouldEqual, 1200) // Check Ledger l, _ := models.TenantLedgerQuery.WithContext(ctx).Where(models.TenantLedgerQuery.OrderID.Eq(o2.ID)).First() So(l, ShouldNotBeNil) So(l.Type, ShouldEqual, consts.TenantLedgerTypeAdjustment) So(l.OperatorUserID, ShouldEqual, admin.ID) }) }) } func (s *SuperTestSuite) Test_CommentGovernance() { Convey("Comment Governance", s.T(), func() { ctx := s.T().Context() database.Truncate(ctx, s.DB, models.TableNameComment, models.TableNameContent, models.TableNameTenant, models.TableNameUser) owner := &models.User{Username: "owner_comment"} commenter := &models.User{Username: "commenter"} admin := &models.User{Username: "admin_comment"} models.UserQuery.WithContext(ctx).Create(owner, commenter, admin) tenant := &models.Tenant{UserID: owner.ID, Code: "t-comment", Name: "Comment Tenant", Status: consts.TenantStatusVerified} models.TenantQuery.WithContext(ctx).Create(tenant) content := &models.Content{ TenantID: tenant.ID, UserID: owner.ID, Title: "Comment Content", Description: "Desc", } models.ContentQuery.WithContext(ctx).Create(content) Convey("should list comments", func() { comment := &models.Comment{ TenantID: tenant.ID, UserID: commenter.ID, ContentID: content.ID, Content: "Nice work", } models.CommentQuery.WithContext(ctx).Create(comment) filter := &super_dto.SuperCommentListFilter{ Pagination: requests.Pagination{Page: 1, Limit: 10}, } res, err := Super.ListComments(ctx, filter) So(err, ShouldBeNil) So(res.Total, ShouldEqual, 1) items := res.Items.([]super_dto.SuperCommentItem) So(items[0].ContentTitle, ShouldEqual, "Comment Content") So(items[0].Username, ShouldEqual, commenter.Username) }) Convey("should delete comment", func() { comment := &models.Comment{ TenantID: tenant.ID, UserID: commenter.ID, ContentID: content.ID, Content: "Spam content", } models.CommentQuery.WithContext(ctx).Create(comment) err := Super.DeleteComment(ctx, admin.ID, comment.ID, &super_dto.SuperCommentDeleteForm{Reason: "spam"}) So(err, ShouldBeNil) deleted, err := models.CommentQuery.WithContext(ctx).Unscoped().Where(models.CommentQuery.ID.Eq(comment.ID)).First() So(err, ShouldBeNil) So(deleted.DeletedAt.Valid, ShouldBeTrue) filter := &super_dto.SuperCommentListFilter{ Pagination: requests.Pagination{Page: 1, Limit: 10}, } res, err := Super.ListComments(ctx, filter) So(err, ShouldBeNil) So(res.Total, ShouldEqual, 0) }) }) } func (s *SuperTestSuite) Test_ContentReportGovernance() { Convey("Content Report Governance", s.T(), func() { ctx := s.T().Context() database.Truncate(ctx, s.DB, models.TableNameContentReport, models.TableNameContent, models.TableNameTenant, models.TableNameUser) owner := &models.User{Username: "owner_report"} reporter := &models.User{Username: "reporter"} admin := &models.User{Username: "admin_report"} models.UserQuery.WithContext(ctx).Create(owner, reporter, admin) tenant := &models.Tenant{UserID: owner.ID, Code: "t-report", Name: "Report Tenant", Status: consts.TenantStatusVerified} models.TenantQuery.WithContext(ctx).Create(tenant) content := &models.Content{ TenantID: tenant.ID, UserID: owner.ID, Title: "Report Content", Description: "Report Desc", Status: consts.ContentStatusPublished, } models.ContentQuery.WithContext(ctx).Create(content) Convey("should list reports", func() { report := &models.ContentReport{ TenantID: tenant.ID, ContentID: content.ID, ReporterID: reporter.ID, Reason: "spam", Detail: "内容涉嫌违规", Status: "pending", } models.ContentReportQuery.WithContext(ctx).Create(report) filter := &super_dto.SuperContentReportListFilter{ Pagination: requests.Pagination{Page: 1, Limit: 10}, } res, err := Super.ListContentReports(ctx, filter) So(err, ShouldBeNil) So(res.Total, ShouldEqual, 1) items := res.Items.([]super_dto.SuperContentReportItem) So(items[0].ContentTitle, ShouldEqual, "Report Content") So(items[0].ReporterName, ShouldEqual, reporter.Username) So(items[0].Status, ShouldEqual, "pending") }) Convey("should process report and block content", func() { report := &models.ContentReport{ TenantID: tenant.ID, ContentID: content.ID, ReporterID: reporter.ID, Reason: "abuse", Detail: "严重违规", Status: "pending", } models.ContentReportQuery.WithContext(ctx).Create(report) err := Super.ProcessContentReport(ctx, admin.ID, report.ID, &super_dto.SuperContentReportProcessForm{ Action: "approve", ContentAction: "block", Reason: "违规属实", }) So(err, ShouldBeNil) reloaded, err := models.ContentReportQuery.WithContext(ctx).Where(models.ContentReportQuery.ID.Eq(report.ID)).First() So(err, ShouldBeNil) So(reloaded.Status, ShouldEqual, "approved") So(reloaded.HandledBy, ShouldEqual, admin.ID) So(reloaded.HandledAction, ShouldEqual, "block") So(reloaded.HandledReason, ShouldEqual, "违规属实") So(reloaded.HandledAt.IsZero(), ShouldBeFalse) contentReload, err := models.ContentQuery.WithContext(ctx).Where(models.ContentQuery.ID.Eq(content.ID)).First() So(err, ShouldBeNil) So(contentReload.Status, ShouldEqual, consts.ContentStatusBlocked) }) Convey("should reject report without content action", func() { report := &models.ContentReport{ TenantID: tenant.ID, ContentID: content.ID, ReporterID: reporter.ID, Reason: "other", Detail: "误报", Status: "pending", } models.ContentReportQuery.WithContext(ctx).Create(report) err := Super.ProcessContentReport(ctx, admin.ID, report.ID, &super_dto.SuperContentReportProcessForm{ Action: "reject", Reason: "证据不足", }) So(err, ShouldBeNil) reloaded, err := models.ContentReportQuery.WithContext(ctx).Where(models.ContentReportQuery.ID.Eq(report.ID)).First() So(err, ShouldBeNil) So(reloaded.Status, ShouldEqual, "rejected") So(reloaded.HandledBy, ShouldEqual, admin.ID) So(reloaded.HandledAction, ShouldEqual, "ignore") So(reloaded.HandledReason, ShouldEqual, "证据不足") contentReload, err := models.ContentQuery.WithContext(ctx).Where(models.ContentQuery.ID.Eq(content.ID)).First() So(err, ShouldBeNil) So(contentReload.Status, ShouldEqual, consts.ContentStatusPublished) }) }) } func (s *SuperTestSuite) Test_FinanceAnomalies() { Convey("Finance Anomalies", s.T(), func() { ctx := s.T().Context() database.Truncate(ctx, s.DB, models.TableNameOrder, models.TableNameTenant, models.TableNameUser) user := &models.User{Username: "finance_user", Balance: -100} models.UserQuery.WithContext(ctx).Create(user) tenant := &models.Tenant{UserID: user.ID, Code: "t-fin", Name: "Finance Tenant", Status: consts.TenantStatusVerified} models.TenantQuery.WithContext(ctx).Create(tenant) order := &models.Order{ TenantID: tenant.ID, UserID: user.ID, Type: consts.OrderTypeRecharge, Status: consts.OrderStatusPaid, AmountOriginal: 100, AmountDiscount: 0, AmountPaid: 100, IdempotencyKey: "anomaly-paid", } models.OrderQuery.WithContext(ctx).Create(order) Convey("should list balance anomalies", func() { filter := &super_dto.SuperBalanceAnomalyFilter{ Pagination: requests.Pagination{Page: 1, Limit: 10}, } res, err := Super.ListBalanceAnomalies(ctx, filter) So(err, ShouldBeNil) So(res.Total, ShouldEqual, 1) items := res.Items.([]super_dto.SuperBalanceAnomalyItem) So(items[0].UserID, ShouldEqual, user.ID) So(items[0].Issue, ShouldEqual, "negative_balance") }) Convey("should list order anomalies", func() { filter := &super_dto.SuperOrderAnomalyFilter{ Pagination: requests.Pagination{Page: 1, Limit: 10}, } res, err := Super.ListOrderAnomalies(ctx, filter) So(err, ShouldBeNil) So(res.Total, ShouldEqual, 1) items := res.Items.([]super_dto.SuperOrderAnomalyItem) So(items[0].OrderID, ShouldEqual, order.ID) So(items[0].Issue, ShouldEqual, "missing_paid_at") }) }) } func (s *SuperTestSuite) Test_TenantHealth() { Convey("TenantHealth", s.T(), func() { ctx := s.T().Context() database.Truncate( ctx, s.DB, models.TableNameUser, models.TableNameTenant, models.TableNameTenantUser, models.TableNameContent, models.TableNameOrder, ) owner1 := &models.User{Username: "health_owner_1"} owner2 := &models.User{Username: "health_owner_2"} models.UserQuery.WithContext(ctx).Create(owner1, owner2) tenant1 := &models.Tenant{ UserID: owner1.ID, Name: "Health Tenant 1", Code: "health1", Status: consts.TenantStatusVerified, } tenant2 := &models.Tenant{ UserID: owner2.ID, Name: "Health Tenant 2", Code: "health2", Status: consts.TenantStatusVerified, } models.TenantQuery.WithContext(ctx).Create(tenant1, tenant2) models.TenantUserQuery.WithContext(ctx).Create( &models.TenantUser{TenantID: tenant1.ID, UserID: owner1.ID}, &models.TenantUser{TenantID: tenant2.ID, UserID: owner2.ID}, ) models.ContentQuery.WithContext(ctx).Create( &models.Content{ TenantID: tenant1.ID, UserID: owner1.ID, Title: "Content H1", Status: consts.ContentStatusPublished, }, &models.Content{ TenantID: tenant2.ID, UserID: owner2.ID, Title: "Content H2", Status: consts.ContentStatusPublished, }, ) now := time.Now() models.OrderQuery.WithContext(ctx).Create( &models.Order{ TenantID: tenant1.ID, UserID: owner1.ID, Type: consts.OrderTypeContentPurchase, Status: consts.OrderStatusPaid, AmountPaid: 1000, PaidAt: now, }, &models.Order{ TenantID: tenant2.ID, UserID: owner2.ID, Type: consts.OrderTypeContentPurchase, Status: consts.OrderStatusPaid, AmountPaid: 1000, PaidAt: now, }, &models.Order{ TenantID: tenant2.ID, UserID: owner2.ID, Type: consts.OrderTypeContentPurchase, Status: consts.OrderStatusRefunded, AmountPaid: 1000, UpdatedAt: now, }, ) filter := &super_dto.TenantListFilter{ Pagination: requests.Pagination{Page: 1, Limit: 10}, } res, err := Super.TenantHealth(ctx, filter) So(err, ShouldBeNil) So(res.Total, ShouldEqual, 2) items := res.Items.([]super_dto.TenantHealthItem) itemMap := make(map[int64]super_dto.TenantHealthItem, len(items)) for _, item := range items { itemMap[item.TenantID] = item } So(itemMap[tenant1.ID].PaidOrders, ShouldEqual, 1) So(itemMap[tenant1.ID].RefundOrders, ShouldEqual, 0) So(itemMap[tenant1.ID].HealthLevel, ShouldEqual, "healthy") So(itemMap[tenant2.ID].PaidOrders, ShouldEqual, 1) So(itemMap[tenant2.ID].RefundOrders, ShouldEqual, 1) So(itemMap[tenant2.ID].HealthLevel, ShouldEqual, "risk") }) } func (s *SuperTestSuite) Test_ContentReview() { Convey("ContentReview", s.T(), func() { ctx := s.T().Context() database.Truncate(ctx, s.DB, models.TableNameUser, models.TableNameTenant, models.TableNameContent) admin := &models.User{Username: "review_admin"} owner := &models.User{Username: "review_owner"} models.UserQuery.WithContext(ctx).Create(admin, owner) tenant := &models.Tenant{ UserID: owner.ID, Name: "Review Tenant", Code: "review", Status: consts.TenantStatusVerified, } models.TenantQuery.WithContext(ctx).Create(tenant) content := &models.Content{ TenantID: tenant.ID, UserID: owner.ID, Title: "Review Content", Status: consts.ContentStatusReviewing, } models.ContentQuery.WithContext(ctx).Create(content) err := Super.ReviewContent(ctx, admin.ID, content.ID, &super_dto.SuperContentReviewForm{ Action: "approve", }) So(err, ShouldBeNil) reloaded, _ := models.ContentQuery.WithContext(ctx).Where(models.ContentQuery.ID.Eq(content.ID)).First() So(reloaded.Status, ShouldEqual, consts.ContentStatusPublished) So(reloaded.PublishedAt.IsZero(), ShouldBeFalse) content2 := &models.Content{ TenantID: tenant.ID, UserID: owner.ID, Title: "Review Content 2", Status: consts.ContentStatusReviewing, } models.ContentQuery.WithContext(ctx).Create(content2) err = Super.ReviewContent(ctx, admin.ID, content2.ID, &super_dto.SuperContentReviewForm{ Action: "reject", Reason: "Policy violation", }) So(err, ShouldBeNil) reloaded2, _ := models.ContentQuery.WithContext(ctx).Where(models.ContentQuery.ID.Eq(content2.ID)).First() So(reloaded2.Status, ShouldEqual, consts.ContentStatusBlocked) }) } func (s *SuperTestSuite) Test_OrderGovernance() { Convey("OrderGovernance", s.T(), func() { ctx := s.T().Context() database.Truncate(ctx, s.DB, models.TableNameOrder, models.TableNameAuditLog) newOrder := func() *models.Order { o := &models.Order{ TenantID: 1, UserID: 2, Type: consts.OrderTypeContentPurchase, Status: consts.OrderStatusPaid, AmountPaid: 100, } So(models.OrderQuery.WithContext(ctx).Create(o), ShouldBeNil) return o } operatorID := int64(9001) Convey("should require reason when flagging", func() { o := newOrder() err := Super.FlagOrder(ctx, operatorID, o.ID, &super_dto.SuperOrderFlagForm{ IsFlagged: true, }) So(err, ShouldNotBeNil) var appErr *errorx.AppError So(errors.As(err, &appErr), ShouldBeTrue) So(appErr.Code, ShouldEqual, errorx.ErrBadRequest.Code) }) Convey("should flag and unflag order", func() { o := newOrder() reason := "支付回调异常" So(Super.FlagOrder(ctx, operatorID, o.ID, &super_dto.SuperOrderFlagForm{ IsFlagged: true, Reason: reason, }), ShouldBeNil) reloaded, err := models.OrderQuery.WithContext(ctx).Where(models.OrderQuery.ID.Eq(o.ID)).First() So(err, ShouldBeNil) So(reloaded.IsFlagged, ShouldBeTrue) So(reloaded.FlagReason, ShouldEqual, reason) So(reloaded.FlaggedBy, ShouldEqual, operatorID) So(reloaded.FlaggedAt.IsZero(), ShouldBeFalse) So(Super.FlagOrder(ctx, operatorID, o.ID, &super_dto.SuperOrderFlagForm{ IsFlagged: false, }), ShouldBeNil) reloaded, err = models.OrderQuery.WithContext(ctx).Where(models.OrderQuery.ID.Eq(o.ID)).First() So(err, ShouldBeNil) So(reloaded.IsFlagged, ShouldBeFalse) So(reloaded.FlagReason, ShouldEqual, "") So(reloaded.FlaggedBy, ShouldEqual, int64(0)) So(reloaded.FlaggedAt.IsZero(), ShouldBeTrue) }) Convey("should reconcile and unreconcile order", func() { o := newOrder() note := "对账完成" So(Super.ReconcileOrder(ctx, operatorID, o.ID, &super_dto.SuperOrderReconcileForm{ IsReconciled: true, Note: note, }), ShouldBeNil) reloaded, err := models.OrderQuery.WithContext(ctx).Where(models.OrderQuery.ID.Eq(o.ID)).First() So(err, ShouldBeNil) So(reloaded.IsReconciled, ShouldBeTrue) So(reloaded.ReconcileNote, ShouldEqual, note) So(reloaded.ReconciledBy, ShouldEqual, operatorID) So(reloaded.ReconciledAt.IsZero(), ShouldBeFalse) So(Super.ReconcileOrder(ctx, operatorID, o.ID, &super_dto.SuperOrderReconcileForm{ IsReconciled: false, }), ShouldBeNil) reloaded, err = models.OrderQuery.WithContext(ctx).Where(models.OrderQuery.ID.Eq(o.ID)).First() So(err, ShouldBeNil) So(reloaded.IsReconciled, ShouldBeFalse) So(reloaded.ReconcileNote, ShouldEqual, "") So(reloaded.ReconciledBy, ShouldEqual, int64(0)) So(reloaded.ReconciledAt.IsZero(), ShouldBeTrue) }) }) }