- 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.
370 lines
12 KiB
Go
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 }
|