package services import ( "database/sql" "encoding/json" "errors" "testing" "time" "quyun/v2/app/commands/testx" "quyun/v2/app/errorx" super_dto "quyun/v2/app/http/super/v1/dto" v1_dto "quyun/v2/app/http/v1/dto" "quyun/v2/app/requests" "quyun/v2/database" "quyun/v2/database/fields" "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() { startAt := time.Now() form := &super_dto.TenantCreateForm{ Name: "Super Tenant", Code: "st1", AdminUserID: u.ID, Duration: 7, } 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) So(t.ExpiredAt.After(startAt), ShouldBeTrue) tu, _ := models.TenantUserQuery.WithContext(ctx). Where(models.TenantUserQuery.TenantID.Eq(t.ID), models.TenantUserQuery.UserID.Eq(u.ID)). First() So(tu, ShouldNotBeNil) So(tu.Status, ShouldEqual, consts.UserStatusVerified) }) }) } func (s *SuperTestSuite) Test_ListTenantsSortAggregates() { Convey("ListTenants sort by aggregates", s.T(), func() { ctx := s.T().Context() database.Truncate(ctx, s.DB, models.TableNameUser, models.TableNameTenant, models.TableNameTenantUser, models.TableNameOrder) owner1 := &models.User{Username: "tenant_sort_owner_1"} owner2 := &models.User{Username: "tenant_sort_owner_2"} owner3 := &models.User{Username: "tenant_sort_owner_3"} member := &models.User{Username: "tenant_sort_member"} models.UserQuery.WithContext(ctx).Create(owner1, owner2, owner3, member) tenant1 := &models.Tenant{UserID: owner1.ID, Code: "t-sort-1", Name: "Tenant Sort 1", Status: consts.TenantStatusVerified} tenant2 := &models.Tenant{UserID: owner2.ID, Code: "t-sort-2", Name: "Tenant Sort 2", Status: consts.TenantStatusVerified} tenant3 := &models.Tenant{UserID: owner3.ID, Code: "t-sort-3", Name: "Tenant Sort 3", Status: consts.TenantStatusVerified} models.TenantQuery.WithContext(ctx).Create(tenant1, tenant2, tenant3) models.TenantUserQuery.WithContext(ctx).Create( &models.TenantUser{TenantID: tenant1.ID, UserID: owner1.ID, Role: types.Array[consts.TenantUserRole]{consts.TenantUserRoleMember}}, &models.TenantUser{TenantID: tenant2.ID, UserID: owner2.ID, Role: types.Array[consts.TenantUserRole]{consts.TenantUserRoleMember}}, &models.TenantUser{TenantID: tenant2.ID, UserID: member.ID, Role: types.Array[consts.TenantUserRole]{consts.TenantUserRoleMember}}, &models.TenantUser{TenantID: tenant3.ID, UserID: owner3.ID, Role: types.Array[consts.TenantUserRole]{consts.TenantUserRoleMember}}, &models.TenantUser{TenantID: tenant3.ID, UserID: owner1.ID, Role: types.Array[consts.TenantUserRole]{consts.TenantUserRoleMember}}, &models.TenantUser{TenantID: tenant3.ID, UserID: member.ID, Role: types.Array[consts.TenantUserRole]{consts.TenantUserRoleMember}}, ) models.OrderQuery.WithContext(ctx).Create( &models.Order{ TenantID: tenant1.ID, UserID: owner1.ID, Type: consts.OrderTypeContentPurchase, Status: consts.OrderStatusPaid, AmountOriginal: 300, AmountDiscount: 0, AmountPaid: 300, IdempotencyKey: "tenant-sort-income-1", }, &models.Order{ TenantID: tenant2.ID, UserID: owner2.ID, Type: consts.OrderTypeContentPurchase, Status: consts.OrderStatusPaid, AmountOriginal: 100, AmountDiscount: 0, AmountPaid: 100, IdempotencyKey: "tenant-sort-income-2", }, &models.Order{ TenantID: tenant3.ID, UserID: owner3.ID, Type: consts.OrderTypeContentPurchase, Status: consts.OrderStatusPaid, AmountOriginal: 200, AmountDiscount: 0, AmountPaid: 200, IdempotencyKey: "tenant-sort-income-3", }, ) Convey("should sort by user_count asc", func() { filter := &super_dto.TenantListFilter{ Pagination: requests.Pagination{Page: 1, Limit: 10}, Asc: lo.ToPtr("user_count"), } res, err := Super.ListTenants(ctx, filter) So(err, ShouldBeNil) items := res.Items.([]super_dto.TenantItem) So(items[0].ID, ShouldEqual, tenant1.ID) So(items[1].ID, ShouldEqual, tenant2.ID) So(items[2].ID, ShouldEqual, tenant3.ID) }) Convey("should sort by user_count desc", func() { filter := &super_dto.TenantListFilter{ Pagination: requests.Pagination{Page: 1, Limit: 10}, Desc: lo.ToPtr("user_count"), } res, err := Super.ListTenants(ctx, filter) So(err, ShouldBeNil) items := res.Items.([]super_dto.TenantItem) So(items[0].ID, ShouldEqual, tenant3.ID) So(items[2].ID, ShouldEqual, tenant1.ID) }) Convey("should sort by income_amount_paid_sum asc", func() { filter := &super_dto.TenantListFilter{ Pagination: requests.Pagination{Page: 1, Limit: 10}, Asc: lo.ToPtr("income_amount_paid_sum"), } res, err := Super.ListTenants(ctx, filter) So(err, ShouldBeNil) items := res.Items.([]super_dto.TenantItem) So(items[0].ID, ShouldEqual, tenant2.ID) So(items[1].ID, ShouldEqual, tenant3.ID) So(items[2].ID, ShouldEqual, tenant1.ID) }) Convey("should sort by income_amount_paid_sum desc", func() { filter := &super_dto.TenantListFilter{ Pagination: requests.Pagination{Page: 1, Limit: 10}, Desc: lo.ToPtr("income_amount_paid_sum"), } res, err := Super.ListTenants(ctx, filter) So(err, ShouldBeNil) items := res.Items.([]super_dto.TenantItem) So(items[0].ID, ShouldEqual, tenant1.ID) So(items[2].ID, ShouldEqual, tenant2.ID) }) }) } 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_BatchProcessContentReports() { Convey("BatchProcessContentReports", s.T(), func() { ctx := s.T().Context() database.Truncate(ctx, s.DB, models.TableNameContentReport, models.TableNameContent, models.TableNameTenant, models.TableNameUser) owner := &models.User{Username: "batch_report_owner"} reporter := &models.User{Username: "batch_reporter"} admin := &models.User{Username: "batch_report_admin"} models.UserQuery.WithContext(ctx).Create(owner, reporter, admin) tenant := &models.Tenant{UserID: owner.ID, Code: "t-report-batch", Name: "Report Batch Tenant", Status: consts.TenantStatusVerified} models.TenantQuery.WithContext(ctx).Create(tenant) contentA := &models.Content{ TenantID: tenant.ID, UserID: owner.ID, Title: "Batch Report A", Status: consts.ContentStatusPublished, } contentB := &models.Content{ TenantID: tenant.ID, UserID: owner.ID, Title: "Batch Report B", Status: consts.ContentStatusPublished, } models.ContentQuery.WithContext(ctx).Create(contentA, contentB) Convey("should batch approve reports and unpublish contents", func() { reportA := &models.ContentReport{ TenantID: tenant.ID, ContentID: contentA.ID, ReporterID: reporter.ID, Reason: "spam", Detail: "批量举报A", Status: "pending", } reportB := &models.ContentReport{ TenantID: tenant.ID, ContentID: contentB.ID, ReporterID: reporter.ID, Reason: "abuse", Detail: "批量举报B", Status: "pending", } models.ContentReportQuery.WithContext(ctx).Create(reportA, reportB) err := Super.BatchProcessContentReports(ctx, admin.ID, &super_dto.SuperContentReportBatchProcessForm{ ReportIDs: []int64{reportA.ID, reportB.ID}, Action: "approve", ContentAction: "unpublish", Reason: "集中处理", }) So(err, ShouldBeNil) reloadedA, err := models.ContentReportQuery.WithContext(ctx).Where(models.ContentReportQuery.ID.Eq(reportA.ID)).First() So(err, ShouldBeNil) So(reloadedA.Status, ShouldEqual, "approved") So(reloadedA.HandledBy, ShouldEqual, admin.ID) So(reloadedA.HandledAction, ShouldEqual, "unpublish") So(reloadedA.HandledReason, ShouldEqual, "集中处理") So(reloadedA.HandledAt.IsZero(), ShouldBeFalse) reloadedB, err := models.ContentReportQuery.WithContext(ctx).Where(models.ContentReportQuery.ID.Eq(reportB.ID)).First() So(err, ShouldBeNil) So(reloadedB.Status, ShouldEqual, "approved") So(reloadedB.HandledAction, ShouldEqual, "unpublish") contentReloadA, err := models.ContentQuery.WithContext(ctx).Where(models.ContentQuery.ID.Eq(contentA.ID)).First() So(err, ShouldBeNil) So(contentReloadA.Status, ShouldEqual, consts.ContentStatusUnpublished) contentReloadB, err := models.ContentQuery.WithContext(ctx).Where(models.ContentQuery.ID.Eq(contentB.ID)).First() So(err, ShouldBeNil) So(contentReloadB.Status, ShouldEqual, consts.ContentStatusUnpublished) }) Convey("should reject when report already handled", func() { report := &models.ContentReport{ TenantID: tenant.ID, ContentID: contentA.ID, ReporterID: reporter.ID, Reason: "other", Detail: "已处理", Status: "approved", } models.ContentReportQuery.WithContext(ctx).Create(report) err := Super.BatchProcessContentReports(ctx, admin.ID, &super_dto.SuperContentReportBatchProcessForm{ ReportIDs: []int64{report.ID}, Action: "reject", Reason: "重复提交", }) So(err, ShouldNotBeNil) var appErr *errorx.AppError So(errors.As(err, &appErr), ShouldBeTrue) So(appErr.Code, ShouldEqual, errorx.ErrStatusConflict.Code) }) }) } 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_FinanceAnomalySorting() { Convey("Finance Anomaly Sorting", s.T(), func() { ctx := s.T().Context() database.Truncate(ctx, s.DB, models.TableNameOrder, models.TableNameTenant, models.TableNameUser) now := time.Now().Truncate(time.Second) userOld := &models.User{Username: "balance_old", Balance: -100, CreatedAt: now.Add(-2 * time.Hour)} userNew := &models.User{Username: "balance_new", Balance: -200, CreatedAt: now.Add(-1 * time.Hour)} models.UserQuery.WithContext(ctx).Create(userOld, userNew) orderOwner := &models.User{Username: "order_anomaly_owner"} models.UserQuery.WithContext(ctx).Create(orderOwner) tenant := &models.Tenant{UserID: orderOwner.ID, Code: "t-anomaly-sort", Name: "Anomaly Sort Tenant", Status: consts.TenantStatusVerified} models.TenantQuery.WithContext(ctx).Create(tenant) order1 := &models.Order{ TenantID: tenant.ID, UserID: orderOwner.ID, Type: consts.OrderTypeContentPurchase, Status: consts.OrderStatusPaid, AmountOriginal: 100, AmountDiscount: 0, AmountPaid: 100, IdempotencyKey: "anomaly-sort-1", } order2 := &models.Order{ TenantID: tenant.ID, UserID: orderOwner.ID, Type: consts.OrderTypeContentPurchase, Status: consts.OrderStatusPaid, AmountOriginal: 200, AmountDiscount: 0, AmountPaid: 200, IdempotencyKey: "anomaly-sort-2", } models.OrderQuery.WithContext(ctx).Create(order1, order2) Convey("should sort balance anomalies by created_at asc", func() { filter := &super_dto.SuperBalanceAnomalyFilter{ Pagination: requests.Pagination{Page: 1, Limit: 10}, Asc: lo.ToPtr("created_at"), } res, err := Super.ListBalanceAnomalies(ctx, filter) So(err, ShouldBeNil) items := res.Items.([]super_dto.SuperBalanceAnomalyItem) So(items[0].UserID, ShouldEqual, userOld.ID) So(items[1].UserID, ShouldEqual, userNew.ID) }) Convey("should sort balance anomalies by user_id desc", func() { filter := &super_dto.SuperBalanceAnomalyFilter{ Pagination: requests.Pagination{Page: 1, Limit: 10}, Desc: lo.ToPtr("user_id"), } res, err := Super.ListBalanceAnomalies(ctx, filter) So(err, ShouldBeNil) items := res.Items.([]super_dto.SuperBalanceAnomalyItem) So(items[0].UserID, ShouldEqual, userNew.ID) }) Convey("should sort order anomalies by order_id asc", func() { filter := &super_dto.SuperOrderAnomalyFilter{ Pagination: requests.Pagination{Page: 1, Limit: 10}, Asc: lo.ToPtr("order_id"), } res, err := Super.ListOrderAnomalies(ctx, filter) So(err, ShouldBeNil) items := res.Items.([]super_dto.SuperOrderAnomalyItem) So(items[0].OrderID, ShouldEqual, order1.ID) So(items[1].OrderID, ShouldEqual, order2.ID) }) Convey("should sort order anomalies by order_id desc", func() { filter := &super_dto.SuperOrderAnomalyFilter{ Pagination: requests.Pagination{Page: 1, Limit: 10}, Desc: lo.ToPtr("order_id"), } res, err := Super.ListOrderAnomalies(ctx, filter) So(err, ShouldBeNil) items := res.Items.([]super_dto.SuperOrderAnomalyItem) So(items[0].OrderID, ShouldEqual, order2.ID) }) }) } 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().Truncate(time.Second) 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_HealthOverview() { Convey("HealthOverview", s.T(), func() { ctx := s.T().Context() database.Truncate( ctx, s.DB, models.TableNameUser, models.TableNameTenant, models.TableNameTenantUser, models.TableNameContent, models.TableNameOrder, models.TableNameMediaAsset, ) owner := &models.User{Username: "health_overview_owner"} models.UserQuery.WithContext(ctx).Create(owner) tenant := &models.Tenant{ UserID: owner.ID, Name: "Health Overview", Code: "health_overview", Status: consts.TenantStatusVerified, } models.TenantQuery.WithContext(ctx).Create(tenant) models.TenantUserQuery.WithContext(ctx).Create(&models.TenantUser{ TenantID: tenant.ID, UserID: owner.ID, }) models.ContentQuery.WithContext(ctx).Create(&models.Content{ TenantID: tenant.ID, UserID: owner.ID, Title: "Health Content", Status: consts.ContentStatusPublished, }) now := time.Now() models.OrderQuery.WithContext(ctx).Create( &models.Order{ TenantID: tenant.ID, UserID: owner.ID, Type: consts.OrderTypeContentPurchase, Status: consts.OrderStatusPaid, AmountPaid: 1000, PaidAt: now, CreatedAt: now, }, &models.Order{ TenantID: tenant.ID, UserID: owner.ID, Type: consts.OrderTypeContentPurchase, Status: consts.OrderStatusFailed, AmountPaid: 1000, UpdatedAt: now, CreatedAt: now, }, ) models.MediaAssetQuery.WithContext(ctx).Create( &models.MediaAsset{ TenantID: tenant.ID, UserID: owner.ID, ObjectKey: "failed.mp4", Type: consts.MediaAssetTypeVideo, Status: consts.MediaAssetStatusFailed, CreatedAt: now, UpdatedAt: now, }, &models.MediaAsset{ TenantID: tenant.ID, UserID: owner.ID, ObjectKey: "ready.mp4", Type: consts.MediaAssetTypeVideo, Status: consts.MediaAssetStatusReady, CreatedAt: now, UpdatedAt: now, }, &models.MediaAsset{ TenantID: tenant.ID, UserID: owner.ID, ObjectKey: "processing.mp4", Type: consts.MediaAssetTypeVideo, Status: consts.MediaAssetStatusProcessing, CreatedAt: now.Add(-48 * time.Hour), UpdatedAt: now.Add(-48 * time.Hour), }, ) startAt := now.AddDate(0, 0, -7).Format(time.RFC3339) endAt := now.Add(time.Second).Format(time.RFC3339) filter := &super_dto.SuperHealthOverviewFilter{ TenantID: &tenant.ID, StartAt: &startAt, EndAt: &endAt, UploadStuckHours: lo.ToPtr(int64(24)), } res, err := Super.HealthOverview(ctx, filter) So(err, ShouldBeNil) So(res.TenantTotal, ShouldEqual, 1) So(res.OrderTotal, ShouldEqual, 2) So(res.OrderFailed, ShouldEqual, 1) So(res.UploadTotal, ShouldEqual, 3) So(res.UploadFailed, ShouldEqual, 1) So(res.UploadProcessingStuck, ShouldEqual, 1) }) } 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_BatchUpdateContentStatus() { Convey("BatchUpdateContentStatus", s.T(), func() { ctx := s.T().Context() database.Truncate(ctx, s.DB, models.TableNameUser, models.TableNameTenant, models.TableNameContent) admin := &models.User{Username: "batch_admin"} owner := &models.User{Username: "batch_owner"} models.UserQuery.WithContext(ctx).Create(admin, owner) tenant := &models.Tenant{ UserID: owner.ID, Name: "Batch Tenant", Code: "batch", Status: consts.TenantStatusVerified, } models.TenantQuery.WithContext(ctx).Create(tenant) content1 := &models.Content{ TenantID: tenant.ID, UserID: owner.ID, Title: "Batch Content 1", Status: consts.ContentStatusPublished, } content2 := &models.Content{ TenantID: tenant.ID, UserID: owner.ID, Title: "Batch Content 2", Status: consts.ContentStatusUnpublished, } models.ContentQuery.WithContext(ctx).Create(content1, content2) err := Super.BatchUpdateContentStatus(ctx, admin.ID, &super_dto.SuperContentBatchStatusForm{ ContentIDs: []int64{content1.ID, content2.ID}, Status: consts.ContentStatusBlocked, Reason: "违规处理", }) So(err, ShouldBeNil) reloaded1, _ := models.ContentQuery.WithContext(ctx).Where(models.ContentQuery.ID.Eq(content1.ID)).First() reloaded2, _ := models.ContentQuery.WithContext(ctx).Where(models.ContentQuery.ID.Eq(content2.ID)).First() So(reloaded1.Status, ShouldEqual, consts.ContentStatusBlocked) 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) }) }) } func (s *SuperTestSuite) Test_PayoutAccountReview() { Convey("PayoutAccountReview", s.T(), func() { ctx := s.T().Context() database.Truncate(ctx, s.DB, models.TableNamePayoutAccount, models.TableNameUser, models.TableNameTenant) admin := &models.User{Username: "payout_admin"} owner := &models.User{Username: "payout_owner"} models.UserQuery.WithContext(ctx).Create(admin, owner) tenant := &models.Tenant{ UserID: owner.ID, Name: "Payout Tenant", Code: "payout", Status: consts.TenantStatusVerified, } models.TenantQuery.WithContext(ctx).Create(tenant) account := &models.PayoutAccount{ TenantID: tenant.ID, UserID: owner.ID, Type: consts.PayoutAccountTypeBank, Name: "Bank", Account: "123", Realname: "Owner", Status: consts.PayoutAccountStatusPending, } models.PayoutAccountQuery.WithContext(ctx).Create(account) Convey("should approve payout account", func() { err := Super.ReviewPayoutAccount(ctx, admin.ID, account.ID, &super_dto.SuperPayoutAccountReviewForm{ Action: "approve", }) So(err, ShouldBeNil) reloaded, _ := models.PayoutAccountQuery.WithContext(ctx).Where(models.PayoutAccountQuery.ID.Eq(account.ID)).First() So(reloaded.Status, ShouldEqual, consts.PayoutAccountStatusApproved) So(reloaded.ReviewedBy, ShouldEqual, admin.ID) So(reloaded.ReviewedAt.IsZero(), ShouldBeFalse) }) Convey("should require reason when rejecting", func() { account2 := &models.PayoutAccount{ TenantID: tenant.ID, UserID: owner.ID, Type: consts.PayoutAccountTypeAlipay, Name: "Alipay", Account: "user@example.com", Realname: "Owner", Status: consts.PayoutAccountStatusPending, } models.PayoutAccountQuery.WithContext(ctx).Create(account2) err := Super.ReviewPayoutAccount(ctx, admin.ID, account2.ID, &super_dto.SuperPayoutAccountReviewForm{ Action: "reject", }) So(err, ShouldNotBeNil) var appErr *errorx.AppError So(errors.As(err, &appErr), ShouldBeTrue) So(appErr.Code, ShouldEqual, errorx.ErrBadRequest.Code) }) }) } func (s *SuperTestSuite) Test_UpdateUserProfile() { Convey("UpdateUserProfile", s.T(), func() { ctx := s.T().Context() database.Truncate(ctx, s.DB, models.TableNameUser) meta := types.JSON([]byte(`{"real_name":"Old Name","id_card":"ENC:1111"}`)) u := &models.User{ Username: "profile_user", Nickname: "Old", Avatar: "http://old-avatar", Gender: consts.GenderSecret, Bio: "old bio", Metas: meta, } models.UserQuery.WithContext(ctx).Create(u) Convey("should update profile fields and real-name meta", func() { form := &super_dto.SuperUserProfileUpdateForm{ Nickname: lo.ToPtr("New Nick"), Avatar: lo.ToPtr("http://new-avatar"), Gender: lo.ToPtr(consts.GenderMale), Bio: lo.ToPtr("new bio"), IsRealNameVerified: lo.ToPtr(true), RealName: lo.ToPtr("New Name"), IDCard: lo.ToPtr("123456789012345678"), } err := Super.UpdateUserProfile(ctx, 1001, u.ID, form) So(err, ShouldBeNil) updated, err := models.UserQuery.WithContext(ctx).Where(models.UserQuery.ID.Eq(u.ID)).First() So(err, ShouldBeNil) So(updated.Nickname, ShouldEqual, "New Nick") So(updated.Avatar, ShouldEqual, "http://new-avatar") So(updated.Gender, ShouldEqual, consts.GenderMale) So(updated.Bio, ShouldEqual, "new bio") So(updated.IsRealNameVerified, ShouldBeTrue) So(updated.VerifiedAt.IsZero(), ShouldBeFalse) metaMap := make(map[string]interface{}) So(json.Unmarshal(updated.Metas, &metaMap), ShouldBeNil) So(metaMap["real_name"], ShouldEqual, "New Name") So(metaMap["id_card"], ShouldEqual, "ENC:123456789012345678") }) }) } func (s *SuperTestSuite) Test_UpdateCreatorSettings() { Convey("UpdateCreatorSettings", s.T(), func() { ctx := s.T().Context() database.Truncate(ctx, s.DB, models.TableNameTenant, models.TableNameUser) owner := &models.User{Username: "settings_owner"} models.UserQuery.WithContext(ctx).Create(owner) tenant := &models.Tenant{ UserID: owner.ID, Name: "Old Tenant", Code: "creator-settings", Status: consts.TenantStatusVerified, Config: types.NewJSONType(fields.TenantConfig{}), } models.TenantQuery.WithContext(ctx).Create(tenant) form := &v1_dto.Settings{ Name: "New Tenant", Bio: "new bio", Avatar: "http://avatar", Cover: "http://cover", Description: "new description", } err := Super.UpdateCreatorSettings(ctx, 2001, tenant.ID, form) So(err, ShouldBeNil) updated, err := models.TenantQuery.WithContext(ctx).Where(models.TenantQuery.ID.Eq(tenant.ID)).First() So(err, ShouldBeNil) So(updated.Name, ShouldEqual, "New Tenant") cfg := updated.Config.Data() So(cfg.Bio, ShouldEqual, "new bio") So(cfg.Avatar, ShouldEqual, "http://avatar") So(cfg.Cover, ShouldEqual, "http://cover") So(cfg.Description, ShouldEqual, "new description") }) } func (s *SuperTestSuite) Test_PayoutAccountCreateUpdate() { Convey("PayoutAccountCreateUpdate", s.T(), func() { ctx := s.T().Context() database.Truncate(ctx, s.DB, models.TableNamePayoutAccount, models.TableNameUser, models.TableNameTenant) admin := &models.User{Username: "payout_admin_2"} owner := &models.User{Username: "payout_owner_2"} models.UserQuery.WithContext(ctx).Create(admin, owner) tenant := &models.Tenant{ UserID: owner.ID, Name: "Payout Tenant 2", Code: "payout-2", Status: consts.TenantStatusVerified, } models.TenantQuery.WithContext(ctx).Create(tenant) Convey("should create payout account", func() { err := Super.CreatePayoutAccount(ctx, admin.ID, tenant.ID, &super_dto.SuperPayoutAccountCreateForm{ UserID: owner.ID, Type: "bank", Name: "Bank", Account: "123", Realname: "Owner", }) So(err, ShouldBeNil) account, err := models.PayoutAccountQuery.WithContext(ctx). Where(models.PayoutAccountQuery.TenantID.Eq(tenant.ID), models.PayoutAccountQuery.UserID.Eq(owner.ID)). First() So(err, ShouldBeNil) So(account.Status, ShouldEqual, consts.PayoutAccountStatusPending) }) Convey("should reset status when updating approved account", func() { account := &models.PayoutAccount{ TenantID: tenant.ID, UserID: owner.ID, Type: consts.PayoutAccountTypeBank, Name: "Bank", Account: "111", Realname: "Owner", Status: consts.PayoutAccountStatusApproved, ReviewedBy: admin.ID, ReviewReason: "ok", ReviewedAt: time.Now(), } models.PayoutAccountQuery.WithContext(ctx).Create(account) err := Super.UpdatePayoutAccount(ctx, admin.ID, account.ID, &super_dto.SuperPayoutAccountUpdateForm{ Account: lo.ToPtr("222"), }) So(err, ShouldBeNil) updated, err := models.PayoutAccountQuery.WithContext(ctx).Where(models.PayoutAccountQuery.ID.Eq(account.ID)).First() So(err, ShouldBeNil) So(updated.Account, ShouldEqual, "222") So(updated.Status, ShouldEqual, consts.PayoutAccountStatusPending) So(updated.ReviewedBy, ShouldEqual, int64(0)) So(updated.ReviewReason, ShouldEqual, "") }) }) } func (s *SuperTestSuite) Test_UpdateNotificationTemplate() { Convey("UpdateNotificationTemplate", s.T(), func() { ctx := s.T().Context() database.Truncate(ctx, s.DB, models.TableNameNotificationTemplate, models.TableNameTenant, models.TableNameUser) owner := &models.User{Username: "tmpl_owner"} models.UserQuery.WithContext(ctx).Create(owner) tenant := &models.Tenant{ UserID: owner.ID, Name: "Template Tenant", Code: "tmpl", Status: consts.TenantStatusVerified, } models.TenantQuery.WithContext(ctx).Create(tenant) tmpl := &models.NotificationTemplate{ TenantID: tenant.ID, Name: "Old Template", Type: consts.NotificationTypeSystem, Title: "Old Title", Content: "Old Content", IsActive: true, } models.NotificationTemplateQuery.WithContext(ctx).Create(tmpl) item, err := Super.UpdateNotificationTemplate(ctx, 3001, tmpl.ID, &super_dto.SuperNotificationTemplateUpdateForm{ Name: lo.ToPtr("New Template"), Title: lo.ToPtr("New Title"), Content: lo.ToPtr("New Content"), IsActive: lo.ToPtr(false), }) So(err, ShouldBeNil) So(item.Name, ShouldEqual, "New Template") So(item.IsActive, ShouldBeFalse) updated, err := models.NotificationTemplateQuery.WithContext(ctx).Where(models.NotificationTemplateQuery.ID.Eq(tmpl.ID)).First() So(err, ShouldBeNil) So(updated.Title, ShouldEqual, "New Title") So(updated.Content, ShouldEqual, "New Content") So(updated.IsActive, ShouldBeFalse) }) }