Files
quyun-v2/backend/app/services/tenant_join_test.go
Rogee 39454458f1 feat: Implement public access for tenant content
- Add TenantOptionalAuth middleware to allow access to public content without requiring authentication.
- Introduce ListPublicPublished and PublicDetail methods in the content service to retrieve publicly accessible content.
- Create tenant_public HTTP routes for listing and showing public content, including preview and main asset retrieval.
- Enhance content tests to cover scenarios for public content access and permissions.
- Update specifications to reflect the new public content access features and rules.
2025-12-22 16:29:44 +08:00

370 lines
12 KiB
Go

package services
import (
"context"
"database/sql"
"errors"
"testing"
"time"
"quyun/v2/app/commands/testx"
"quyun/v2/app/errorx"
tenantdto "quyun/v2/app/http/tenant/dto"
tenant_join_dto "quyun/v2/app/http/tenant_join/dto"
"quyun/v2/database"
"quyun/v2/database/models"
"quyun/v2/pkg/consts"
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/suite"
_ "go.ipao.vip/atom"
"go.ipao.vip/atom/contracts"
"go.ipao.vip/gen/types"
"go.uber.org/dig"
)
type TenantJoinTestSuiteInjectParams struct {
dig.In
DB *sql.DB
Initials []contracts.Initial `group:"initials"` // nolint:structcheck
}
type TenantJoinTestSuite struct {
suite.Suite
TenantJoinTestSuiteInjectParams
}
func Test_TenantJoin(t *testing.T) {
providers := testx.Default().With(Provide)
testx.Serve(providers, t, func(p TenantJoinTestSuiteInjectParams) {
suite.Run(t, &TenantJoinTestSuite{TenantJoinTestSuiteInjectParams: p})
})
}
func (s *TenantJoinTestSuite) truncateAll(ctx context.Context) {
So(database.Truncate(ctx, s.DB,
models.TableNameTenantInvite,
models.TableNameTenantJoinRequest,
models.TableNameTenantUser,
), ShouldBeNil)
}
func (s *TenantJoinTestSuite) seedInvite(ctx context.Context, tenantID, creatorUserID int64, code string, status consts.TenantInviteStatus, maxUses, usedCount int32, expiresAt time.Time) *models.TenantInvite {
inv := &models.TenantInvite{
TenantID: tenantID,
UserID: creatorUserID,
Code: code,
Status: status,
MaxUses: maxUses,
UsedCount: usedCount,
ExpiresAt: expiresAt,
Remark: "seed",
CreatedAt: time.Now().UTC(),
UpdatedAt: time.Now().UTC(),
}
So(_db.WithContext(ctx).Create(inv).Error, ShouldBeNil)
return inv
}
func (s *TenantJoinTestSuite) seedJoinRequest(ctx context.Context, tenantID, userID int64, status consts.TenantJoinRequestStatus) *models.TenantJoinRequest {
req := &models.TenantJoinRequest{
TenantID: tenantID,
UserID: userID,
Status: status,
Reason: "seed",
DecidedAt: time.Time{},
DecidedReason: "",
CreatedAt: time.Now().UTC(),
UpdatedAt: time.Now().UTC(),
}
So(_db.WithContext(ctx).Omit("decided_at", "decided_operator_user_id").Create(req).Error, ShouldBeNil)
return req
}
func (s *TenantJoinTestSuite) Test_AdminCreateInvite() {
Convey("Tenant.AdminCreateInvite", s.T(), func() {
ctx := s.T().Context()
s.truncateAll(ctx)
tenantID := int64(1)
adminUserID := int64(10)
Convey("参数非法应返回参数错误", func() {
_, err := Tenant.AdminCreateInvite(ctx, 0, adminUserID, &tenantdto.AdminTenantInviteCreateForm{})
So(err, ShouldNotBeNil)
var appErr *errorx.AppError
So(errors.As(err, &appErr), ShouldBeTrue)
So(appErr.Code, ShouldEqual, errorx.CodeInvalidParameter)
_, err = Tenant.AdminCreateInvite(ctx, tenantID, 0, &tenantdto.AdminTenantInviteCreateForm{})
So(err, ShouldNotBeNil)
})
Convey("code 为空应自动生成并创建成功", func() {
out, err := Tenant.AdminCreateInvite(ctx, tenantID, adminUserID, &tenantdto.AdminTenantInviteCreateForm{
Code: "",
MaxUses: loToPtr(0),
Remark: "test",
})
So(err, ShouldBeNil)
So(out, ShouldNotBeNil)
So(out.ID, ShouldBeGreaterThan, 0)
So(out.Code, ShouldNotBeBlank)
So(out.Status, ShouldEqual, consts.TenantInviteStatusActive)
})
Convey("重复 code 应返回重复错误", func() {
_, err := Tenant.AdminCreateInvite(ctx, tenantID, adminUserID, &tenantdto.AdminTenantInviteCreateForm{
Code: "dup_code",
MaxUses: loToPtr(0),
})
So(err, ShouldBeNil)
_, err = Tenant.AdminCreateInvite(ctx, tenantID, adminUserID, &tenantdto.AdminTenantInviteCreateForm{
Code: "dup_code",
MaxUses: loToPtr(0),
})
So(err, ShouldNotBeNil)
var appErr *errorx.AppError
So(errors.As(err, &appErr), ShouldBeTrue)
So(appErr.Code, ShouldEqual, errorx.CodeRecordDuplicated)
})
})
}
func (s *TenantJoinTestSuite) Test_AdminDisableInvite() {
Convey("Tenant.AdminDisableInvite", s.T(), func() {
ctx := s.T().Context()
s.truncateAll(ctx)
tenantID := int64(1)
adminUserID := int64(10)
Convey("邀请码不存在应返回记录不存在", func() {
_, err := Tenant.AdminDisableInvite(ctx, tenantID, adminUserID, 999, "x")
So(err, ShouldNotBeNil)
var appErr *errorx.AppError
So(errors.As(err, &appErr), ShouldBeTrue)
So(appErr.Code, ShouldEqual, errorx.CodeRecordNotFound)
})
Convey("禁用应成功且幂等", func() {
inv := s.seedInvite(ctx, tenantID, adminUserID, "c1", consts.TenantInviteStatusActive, 0, 0, time.Time{})
out, err := Tenant.AdminDisableInvite(ctx, tenantID, adminUserID, inv.ID, "reason")
So(err, ShouldBeNil)
So(out.Status, ShouldEqual, consts.TenantInviteStatusDisabled)
So(out.DisabledOperatorUserID, ShouldEqual, adminUserID)
So(out.DisabledAt.IsZero(), ShouldBeFalse)
out2, err := Tenant.AdminDisableInvite(ctx, tenantID, adminUserID, inv.ID, "reason2")
So(err, ShouldBeNil)
So(out2.Status, ShouldEqual, consts.TenantInviteStatusDisabled)
})
})
}
func (s *TenantJoinTestSuite) Test_AdminInvitePage() {
Convey("Tenant.AdminInvitePage", s.T(), func() {
ctx := s.T().Context()
s.truncateAll(ctx)
tenantID := int64(1)
adminUserID := int64(10)
s.seedInvite(ctx, tenantID, adminUserID, "aaa", consts.TenantInviteStatusActive, 0, 0, time.Time{})
s.seedInvite(ctx, tenantID, adminUserID, "bbb", consts.TenantInviteStatusDisabled, 0, 0, time.Time{})
s.seedInvite(ctx, tenantID, adminUserID, "ccc", consts.TenantInviteStatusActive, 0, 0, time.Time{})
Convey("按 status 过滤应只返回匹配项", func() {
st := consts.TenantInviteStatusActive
pager, err := Tenant.AdminInvitePage(ctx, tenantID, &tenantdto.AdminTenantInviteListFilter{
Status: &st,
})
So(err, ShouldBeNil)
So(pager.Total, ShouldEqual, 2)
})
Convey("按 code 模糊过滤应生效", func() {
code := "bb"
pager, err := Tenant.AdminInvitePage(ctx, tenantID, &tenantdto.AdminTenantInviteListFilter{
Code: &code,
})
So(err, ShouldBeNil)
So(pager.Total, ShouldEqual, 1)
})
})
}
func (s *TenantJoinTestSuite) Test_JoinByInvite() {
Convey("Tenant.JoinByInvite", s.T(), func() {
ctx := s.T().Context()
s.truncateAll(ctx)
tenantID := int64(1)
adminUserID := int64(10)
userID := int64(20)
Convey("邀请码不存在应返回记录不存在", func() {
_, err := Tenant.JoinByInvite(ctx, tenantID, userID, "not_exist")
So(err, ShouldNotBeNil)
var appErr *errorx.AppError
So(errors.As(err, &appErr), ShouldBeTrue)
So(appErr.Code, ShouldEqual, errorx.CodeRecordNotFound)
})
Convey("成功加入应创建成员并消耗邀请码次数", func() {
inv := s.seedInvite(ctx, tenantID, adminUserID, "code1", consts.TenantInviteStatusActive, 1, 0, time.Now().UTC().Add(10*time.Minute))
tu, err := Tenant.JoinByInvite(ctx, tenantID, userID, "code1")
So(err, ShouldBeNil)
So(tu, ShouldNotBeNil)
So(tu.TenantID, ShouldEqual, tenantID)
So(tu.UserID, ShouldEqual, userID)
var inv2 models.TenantInvite
So(_db.WithContext(ctx).Where("id = ?", inv.ID).First(&inv2).Error, ShouldBeNil)
So(inv2.UsedCount, ShouldEqual, 1)
So(inv2.Status, ShouldEqual, consts.TenantInviteStatusExpired)
Convey("重复加入应幂等且不再消耗次数", func() {
_, err := Tenant.JoinByInvite(ctx, tenantID, userID, "code1")
So(err, ShouldBeNil)
var inv3 models.TenantInvite
So(_db.WithContext(ctx).Where("id = ?", inv.ID).First(&inv3).Error, ShouldBeNil)
So(inv3.UsedCount, ShouldEqual, 1)
})
})
})
}
func (s *TenantJoinTestSuite) Test_CreateJoinRequest() {
Convey("Tenant.CreateJoinRequest", s.T(), func() {
ctx := s.T().Context()
s.truncateAll(ctx)
tenantID := int64(1)
userID := int64(20)
Convey("已是成员应返回前置条件失败", func() {
So(_db.WithContext(ctx).Create(&models.TenantUser{
TenantID: tenantID,
UserID: userID,
Role: types.NewArray([]consts.TenantUserRole{consts.TenantUserRoleMember}),
Status: consts.UserStatusVerified,
}).Error, ShouldBeNil)
_, err := Tenant.CreateJoinRequest(ctx, tenantID, userID, &tenant_join_dto.JoinRequestCreateForm{Reason: "x"})
So(err, ShouldNotBeNil)
var appErr *errorx.AppError
So(errors.As(err, &appErr), ShouldBeTrue)
So(appErr.Code, ShouldEqual, errorx.CodePreconditionFailed)
})
Convey("重复提交应返回同一个 pending 申请(幂等)", func() {
s.truncateAll(ctx)
out1, err := Tenant.CreateJoinRequest(ctx, tenantID, userID, &tenant_join_dto.JoinRequestCreateForm{Reason: "a"})
So(err, ShouldBeNil)
So(out1, ShouldNotBeNil)
out2, err := Tenant.CreateJoinRequest(ctx, tenantID, userID, &tenant_join_dto.JoinRequestCreateForm{Reason: "b"})
So(err, ShouldBeNil)
So(out2, ShouldNotBeNil)
So(out2.ID, ShouldEqual, out1.ID)
So(out2.Status, ShouldEqual, consts.TenantJoinRequestStatusPending)
})
})
}
func (s *TenantJoinTestSuite) Test_AdminJoinRequestPage() {
Convey("Tenant.AdminJoinRequestPage", s.T(), func() {
ctx := s.T().Context()
s.truncateAll(ctx)
tenantID := int64(1)
s.seedJoinRequest(ctx, tenantID, 11, consts.TenantJoinRequestStatusPending)
s.seedJoinRequest(ctx, tenantID, 22, consts.TenantJoinRequestStatusRejected)
s.seedJoinRequest(ctx, tenantID, 33, consts.TenantJoinRequestStatusPending)
Convey("按 status 过滤应生效", func() {
st := consts.TenantJoinRequestStatusPending
pager, err := Tenant.AdminJoinRequestPage(ctx, tenantID, &tenantdto.AdminTenantJoinRequestListFilter{Status: &st})
So(err, ShouldBeNil)
So(pager.Total, ShouldEqual, 2)
})
})
}
func (s *TenantJoinTestSuite) Test_AdminApproveJoinRequest() {
Convey("Tenant.AdminApproveJoinRequest", s.T(), func() {
ctx := s.T().Context()
s.truncateAll(ctx)
tenantID := int64(1)
adminUserID := int64(10)
userID := int64(20)
Convey("申请不存在应返回记录不存在", func() {
_, err := Tenant.AdminApproveJoinRequest(ctx, tenantID, adminUserID, 999, "x")
So(err, ShouldNotBeNil)
var appErr *errorx.AppError
So(errors.As(err, &appErr), ShouldBeTrue)
So(appErr.Code, ShouldEqual, errorx.CodeRecordNotFound)
})
Convey("通过 pending 申请应成功且幂等", func() {
req := s.seedJoinRequest(ctx, tenantID, userID, consts.TenantJoinRequestStatusPending)
out, err := Tenant.AdminApproveJoinRequest(ctx, tenantID, adminUserID, req.ID, "ok")
So(err, ShouldBeNil)
So(out.Status, ShouldEqual, consts.TenantJoinRequestStatusApproved)
So(out.DecidedOperatorUserID, ShouldEqual, adminUserID)
So(out.DecidedAt.IsZero(), ShouldBeFalse)
var tu models.TenantUser
So(_db.WithContext(ctx).Where("tenant_id = ? AND user_id = ?", tenantID, userID).First(&tu).Error, ShouldBeNil)
out2, err := Tenant.AdminApproveJoinRequest(ctx, tenantID, adminUserID, req.ID, "ok2")
So(err, ShouldBeNil)
So(out2.Status, ShouldEqual, consts.TenantJoinRequestStatusApproved)
})
})
}
func (s *TenantJoinTestSuite) Test_AdminRejectJoinRequest() {
Convey("Tenant.AdminRejectJoinRequest", s.T(), func() {
ctx := s.T().Context()
s.truncateAll(ctx)
tenantID := int64(1)
adminUserID := int64(10)
userID := int64(20)
Convey("拒绝 pending 申请应成功且幂等", func() {
req := s.seedJoinRequest(ctx, tenantID, userID, consts.TenantJoinRequestStatusPending)
out, err := Tenant.AdminRejectJoinRequest(ctx, tenantID, adminUserID, req.ID, "no")
So(err, ShouldBeNil)
So(out.Status, ShouldEqual, consts.TenantJoinRequestStatusRejected)
So(out.DecidedOperatorUserID, ShouldEqual, adminUserID)
out2, err := Tenant.AdminRejectJoinRequest(ctx, tenantID, adminUserID, req.ID, "no2")
So(err, ShouldBeNil)
So(out2.Status, ShouldEqual, consts.TenantJoinRequestStatusRejected)
})
})
}
func loToPtr[T any](v T) *T { return &v }