mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-03 01:01:56 +08:00
## Summary Ports the Python `tenant_api` team/member management endpoints to Go, adding 4 endpoints under `/api/v1/tenants/:tenant_id/`: - `GET /tenants/:tenant_id/users` — list non-owner members with user details (owner only) - `POST /tenants/:tenant_id/users` — invite a user by email; creates invite-role join record (owner only) - `DELETE /tenants/:tenant_id/users` — remove a member by `user_id`; owner can remove anyone, members can remove themselves - `PATCH /tenants/:tenant_id` — accept a pending invitation, transitioning role `invite → normal` Closes #15294
This commit is contained in:
@@ -17,6 +17,8 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"ragflow/internal/entity"
|
||||
)
|
||||
|
||||
@@ -114,6 +116,38 @@ type TenantInfoByUserID struct {
|
||||
UpdateDate string `json:"update_date"`
|
||||
}
|
||||
|
||||
// TenantMemberItem holds user details for a tenant member listing.
|
||||
type TenantMemberItem struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
Role string `json:"role"`
|
||||
Status string `json:"status"`
|
||||
Nickname string `json:"nickname"`
|
||||
Email string `json:"email"`
|
||||
Avatar string `json:"avatar"`
|
||||
IsAuthenticated bool `json:"is_authenticated"`
|
||||
IsActive string `json:"is_active"`
|
||||
IsAnonymous bool `json:"is_anonymous"`
|
||||
IsSuperuser bool `json:"is_superuser"`
|
||||
UpdateDate string `json:"update_date"`
|
||||
}
|
||||
|
||||
// GetMembersByTenantID returns all non-owner members of a tenant with user details.
|
||||
// update_date is formatted as "2006-01-02T15:04:05" (no timezone) to match the Python API.
|
||||
func (dao *UserTenantDAO) GetMembersByTenantID(tenantID string) ([]*TenantMemberItem, error) {
|
||||
var results []*TenantMemberItem
|
||||
err := DB.Table("user_tenant").
|
||||
Select("user_tenant.id, user_tenant.user_id, user_tenant.role, user_tenant.status, "+
|
||||
"user.nickname, user.email, user.avatar, user.is_authenticated, "+
|
||||
"user.status AS is_active, user.is_anonymous, user.is_superuser, "+
|
||||
"DATE_FORMAT(user.update_date, '%Y-%m-%dT%H:%i:%s') AS update_date").
|
||||
Joins("JOIN user ON user_tenant.user_id = user.id").
|
||||
Where("user_tenant.tenant_id = ? AND user_tenant.status = ? AND user_tenant.role != ?",
|
||||
tenantID, "1", "owner").
|
||||
Scan(&results).Error
|
||||
return results, err
|
||||
}
|
||||
|
||||
// GetTenantsByUserID get tenants by user ID with user details
|
||||
func (dao *UserTenantDAO) GetTenantsByUserID(userID string) ([]*TenantInfoByUserID, error) {
|
||||
var results []*TenantInfoByUserID
|
||||
@@ -143,3 +177,25 @@ func (dao *UserTenantDAO) GetByUserIDAll(userID string) ([]*entity.UserTenant, e
|
||||
err := DB.Where("user_id = ?", userID).Find(&relations).Error
|
||||
return relations, err
|
||||
}
|
||||
|
||||
// DeleteByUserAndTenant hard-deletes the join record for a specific user+tenant pair.
|
||||
func (dao *UserTenantDAO) DeleteByUserAndTenant(userID, tenantID string) error {
|
||||
return DB.Unscoped().
|
||||
Where("user_id = ? AND tenant_id = ?", userID, tenantID).
|
||||
Delete(&entity.UserTenant{}).Error
|
||||
}
|
||||
|
||||
// UpdateRoleByUserAndTenant updates the role for a specific user+tenant pair.
|
||||
// Returns an error if no matching row was found.
|
||||
func (dao *UserTenantDAO) UpdateRoleByUserAndTenant(userID, tenantID, role string) error {
|
||||
result := DB.Model(&entity.UserTenant{}).
|
||||
Where("user_id = ? AND tenant_id = ? AND status = ?", userID, tenantID, "1").
|
||||
Update("role", role)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return fmt.Errorf("no active membership found for user %s in tenant %s", userID, tenantID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -542,3 +542,129 @@ func (h *TenantHandler) InsertMetadataFromFile(c *gin.Context) {
|
||||
"message": "success",
|
||||
})
|
||||
}
|
||||
|
||||
// ListTenantMembers lists all non-owner members of a tenant.
|
||||
// @Summary List tenant members
|
||||
// @Tags tenants
|
||||
// @Produce json
|
||||
// @Param tenant_id path string true "Tenant ID"
|
||||
// @Router /api/v1/tenants/{tenant_id}/users [get]
|
||||
func (h *TenantHandler) ListTenantMembers(c *gin.Context) {
|
||||
user, errorCode, errorMessage := GetUser(c)
|
||||
if errorCode != common.CodeSuccess {
|
||||
jsonError(c, errorCode, errorMessage)
|
||||
return
|
||||
}
|
||||
|
||||
tenantID := c.Param("tenant_id")
|
||||
if tenantID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": common.CodeBadRequest, "data": nil, "message": "tenant_id is required"})
|
||||
return
|
||||
}
|
||||
|
||||
members, code, err := h.tenantService.ListMembers(user.ID, tenantID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": code, "data": nil, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": common.CodeSuccess, "data": members, "message": "success"})
|
||||
}
|
||||
|
||||
// AddTenantMember invites a user (by email) to the tenant.
|
||||
// @Summary Invite a user to a tenant
|
||||
// @Tags tenants
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param tenant_id path string true "Tenant ID"
|
||||
// @Param request body service.AddMemberRequest true "Invite request"
|
||||
// @Router /api/v1/tenants/{tenant_id}/users [post]
|
||||
func (h *TenantHandler) AddTenantMember(c *gin.Context) {
|
||||
user, errorCode, errorMessage := GetUser(c)
|
||||
if errorCode != common.CodeSuccess {
|
||||
jsonError(c, errorCode, errorMessage)
|
||||
return
|
||||
}
|
||||
|
||||
tenantID := c.Param("tenant_id")
|
||||
if tenantID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": common.CodeBadRequest, "data": nil, "message": "tenant_id is required"})
|
||||
return
|
||||
}
|
||||
|
||||
var req service.AddMemberRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": common.CodeBadRequest, "data": nil, "message": "invalid request body: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
resp, code, err := h.tenantService.AddMember(user.ID, tenantID, &req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": code, "data": nil, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": common.CodeSuccess, "data": resp, "message": "success"})
|
||||
}
|
||||
|
||||
// RemoveTenantMember removes a user from the tenant.
|
||||
// @Summary Remove a user from a tenant
|
||||
// @Tags tenants
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param tenant_id path string true "Tenant ID"
|
||||
// @Param request body object true "Remove member request" SchemaExample({"user_id":"string"})
|
||||
// @Router /api/v1/tenants/{tenant_id}/users [delete]
|
||||
func (h *TenantHandler) RemoveTenantMember(c *gin.Context) {
|
||||
user, errorCode, errorMessage := GetUser(c)
|
||||
if errorCode != common.CodeSuccess {
|
||||
jsonError(c, errorCode, errorMessage)
|
||||
return
|
||||
}
|
||||
|
||||
tenantID := c.Param("tenant_id")
|
||||
if tenantID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": common.CodeBadRequest, "data": nil, "message": "tenant_id is required"})
|
||||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil || body.UserID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": common.CodeBadRequest, "data": nil, "message": "user_id is required"})
|
||||
return
|
||||
}
|
||||
|
||||
code, err := h.tenantService.RemoveMember(user.ID, tenantID, body.UserID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": code, "data": nil, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": common.CodeSuccess, "data": true, "message": "success"})
|
||||
}
|
||||
|
||||
// AcceptTenantInvite accepts a pending team invitation, transitioning role invite → normal.
|
||||
// @Summary Accept tenant invitation
|
||||
// @Tags tenants
|
||||
// @Produce json
|
||||
// @Param tenant_id path string true "Tenant ID"
|
||||
// @Router /api/v1/tenants/{tenant_id} [patch]
|
||||
func (h *TenantHandler) AcceptTenantInvite(c *gin.Context) {
|
||||
user, errorCode, errorMessage := GetUser(c)
|
||||
if errorCode != common.CodeSuccess {
|
||||
jsonError(c, errorCode, errorMessage)
|
||||
return
|
||||
}
|
||||
|
||||
tenantID := c.Param("tenant_id")
|
||||
if tenantID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": common.CodeBadRequest, "data": nil, "message": "tenant_id is required"})
|
||||
return
|
||||
}
|
||||
|
||||
code, err := h.tenantService.AcceptInvite(user.ID, tenantID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": code, "data": nil, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": common.CodeSuccess, "data": true, "message": "success"})
|
||||
}
|
||||
|
||||
@@ -160,6 +160,10 @@ func (r *Router) Setup(engine *gin.Engine) {
|
||||
tenants := v1.Group("/tenants")
|
||||
{
|
||||
tenants.GET("", r.tenantHandler.TenantList)
|
||||
tenants.PATCH("/:tenant_id", r.tenantHandler.AcceptTenantInvite)
|
||||
tenants.GET("/:tenant_id/users", r.tenantHandler.ListTenantMembers)
|
||||
tenants.POST("/:tenant_id/users", r.tenantHandler.AddTenantMember)
|
||||
tenants.DELETE("/:tenant_id/users", r.tenantHandler.RemoveTenantMember)
|
||||
}
|
||||
|
||||
v1.GET("/tenant/list", r.tenantHandler.TenantList)
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
type TenantService struct {
|
||||
tenantDAO *dao.TenantDAO
|
||||
userTenantDAO *dao.UserTenantDAO
|
||||
userDAO *dao.UserDAO
|
||||
modelProviderDAO *dao.TenantModelProviderDAO
|
||||
modelInstanceDAO *dao.TenantModelInstanceDAO
|
||||
modelDAO *dao.TenantModelDAO
|
||||
@@ -44,6 +45,7 @@ func NewTenantService() *TenantService {
|
||||
return &TenantService{
|
||||
tenantDAO: dao.NewTenantDAO(),
|
||||
userTenantDAO: dao.NewUserTenantDAO(),
|
||||
userDAO: dao.NewUserDAO(),
|
||||
modelProviderDAO: dao.NewTenantModelProviderDAO(),
|
||||
modelInstanceDAO: dao.NewTenantModelInstanceDAO(),
|
||||
modelDAO: dao.NewTenantModelDAO(),
|
||||
@@ -654,3 +656,157 @@ func (s *TenantService) SetTenantDefaultModels(userID, modelProvider, modelInsta
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Tenant member role constants.
|
||||
const (
|
||||
TenantRoleOwner = "owner"
|
||||
TenantRoleNormal = "normal"
|
||||
TenantRoleInvite = "invite"
|
||||
TenantRoleAdmin = "admin"
|
||||
)
|
||||
|
||||
// TenantMemberResponse is one entry in the member list response.
|
||||
type TenantMemberResponse struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
Role string `json:"role"`
|
||||
Status string `json:"status"`
|
||||
Nickname string `json:"nickname"`
|
||||
Email string `json:"email"`
|
||||
Avatar string `json:"avatar"`
|
||||
IsAuthenticated bool `json:"is_authenticated"`
|
||||
IsActive string `json:"is_active"`
|
||||
IsAnonymous bool `json:"is_anonymous"`
|
||||
IsSuperuser bool `json:"is_superuser"`
|
||||
UpdateDate string `json:"update_date"`
|
||||
DeltaSeconds float64 `json:"delta_seconds"`
|
||||
}
|
||||
|
||||
// ListMembers returns all non-owner members of tenantID.
|
||||
// Only the tenant owner (userID == tenantID) may call this.
|
||||
func (s *TenantService) ListMembers(userID, tenantID string) ([]*TenantMemberResponse, common.ErrorCode, error) {
|
||||
if userID != tenantID {
|
||||
return nil, common.CodeAuthenticationError, fmt.Errorf("no authorization")
|
||||
}
|
||||
rows, err := s.userTenantDAO.GetMembersByTenantID(tenantID)
|
||||
if err != nil {
|
||||
return nil, common.CodeServerError, err
|
||||
}
|
||||
result := make([]*TenantMemberResponse, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
delta, _ := common.DeltaSeconds(r.UpdateDate)
|
||||
result = append(result, &TenantMemberResponse{
|
||||
ID: r.ID,
|
||||
UserID: r.UserID,
|
||||
Role: r.Role,
|
||||
Status: r.Status,
|
||||
Nickname: r.Nickname,
|
||||
Email: r.Email,
|
||||
Avatar: r.Avatar,
|
||||
IsAuthenticated: r.IsAuthenticated,
|
||||
IsActive: r.IsActive,
|
||||
IsAnonymous: r.IsAnonymous,
|
||||
IsSuperuser: r.IsSuperuser,
|
||||
UpdateDate: r.UpdateDate,
|
||||
DeltaSeconds: delta,
|
||||
})
|
||||
}
|
||||
return result, common.CodeSuccess, nil
|
||||
}
|
||||
|
||||
// AddMemberRequest holds the invite payload.
|
||||
type AddMemberRequest struct {
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
// AddMemberResponse holds the new member's public data.
|
||||
type AddMemberResponse struct {
|
||||
ID string `json:"id"`
|
||||
Avatar string `json:"avatar"`
|
||||
Email string `json:"email"`
|
||||
Nickname string `json:"nickname"`
|
||||
}
|
||||
|
||||
// AddMember invites a user (by email) to the tenant.
|
||||
// Only the tenant owner (userID == tenantID) may call this.
|
||||
func (s *TenantService) AddMember(userID, tenantID string, req *AddMemberRequest) (*AddMemberResponse, common.ErrorCode, error) {
|
||||
if userID != tenantID {
|
||||
return nil, common.CodeAuthenticationError, fmt.Errorf("no authorization")
|
||||
}
|
||||
if req.Email == "" {
|
||||
return nil, common.CodeArgumentError, fmt.Errorf("email is required")
|
||||
}
|
||||
|
||||
invitee, err := s.userDAO.GetByEmail(req.Email)
|
||||
if err != nil {
|
||||
return nil, common.CodeDataError, fmt.Errorf("user not found")
|
||||
}
|
||||
|
||||
// Reject if already a member or has a pending invitation.
|
||||
existing, _ := s.userTenantDAO.FilterByUserIDAndTenantID(invitee.ID, tenantID)
|
||||
if existing != nil {
|
||||
switch existing.Role {
|
||||
case TenantRoleOwner:
|
||||
return nil, common.CodeDataError, fmt.Errorf("user is already the tenant owner")
|
||||
case TenantRoleNormal, TenantRoleAdmin:
|
||||
return nil, common.CodeDataError, fmt.Errorf("user is already a member")
|
||||
case TenantRoleInvite:
|
||||
return nil, common.CodeDataError, fmt.Errorf("user already has a pending invitation")
|
||||
}
|
||||
}
|
||||
|
||||
status := "1"
|
||||
ut := &entity.UserTenant{
|
||||
ID: common.GenerateUUID(),
|
||||
UserID: invitee.ID,
|
||||
TenantID: tenantID,
|
||||
Role: TenantRoleInvite,
|
||||
InvitedBy: userID,
|
||||
Status: &status,
|
||||
}
|
||||
if err := s.userTenantDAO.Create(ut); err != nil {
|
||||
return nil, common.CodeServerError, fmt.Errorf("failed to create invitation: %w", err)
|
||||
}
|
||||
|
||||
avatar := ""
|
||||
if invitee.Avatar != nil {
|
||||
avatar = *invitee.Avatar
|
||||
}
|
||||
return &AddMemberResponse{
|
||||
ID: invitee.ID,
|
||||
Avatar: avatar,
|
||||
Email: invitee.Email,
|
||||
Nickname: invitee.Nickname,
|
||||
}, common.CodeSuccess, nil
|
||||
}
|
||||
|
||||
// RemoveMember removes a user from the tenant.
|
||||
// Either the owner (userID == tenantID) or the member themselves (userID == targetUserID) may call this.
|
||||
// The tenant owner (targetUserID == tenantID) cannot be removed.
|
||||
func (s *TenantService) RemoveMember(userID, tenantID, targetUserID string) (common.ErrorCode, error) {
|
||||
if userID != tenantID && userID != targetUserID {
|
||||
return common.CodeAuthenticationError, fmt.Errorf("no authorization")
|
||||
}
|
||||
if targetUserID == tenantID {
|
||||
return common.CodeArgumentError, fmt.Errorf("cannot remove the tenant owner")
|
||||
}
|
||||
if err := s.userTenantDAO.DeleteByUserAndTenant(targetUserID, tenantID); err != nil {
|
||||
return common.CodeServerError, fmt.Errorf("failed to remove member: %w", err)
|
||||
}
|
||||
return common.CodeSuccess, nil
|
||||
}
|
||||
|
||||
// AcceptInvite transitions the calling user's role from "invite" → "normal" for the given tenant.
|
||||
func (s *TenantService) AcceptInvite(userID, tenantID string) (common.ErrorCode, error) {
|
||||
existing, err := s.userTenantDAO.FilterByUserIDAndTenantID(userID, tenantID)
|
||||
if err != nil || existing == nil {
|
||||
return common.CodeDataError, fmt.Errorf("no pending invitation found")
|
||||
}
|
||||
if existing.Role != TenantRoleInvite {
|
||||
return common.CodeArgumentError, fmt.Errorf("no pending invitation to accept")
|
||||
}
|
||||
if err := s.userTenantDAO.UpdateRoleByUserAndTenant(userID, tenantID, TenantRoleNormal); err != nil {
|
||||
return common.CodeServerError, fmt.Errorf("failed to accept invitation: %w", err)
|
||||
}
|
||||
return common.CodeSuccess, nil
|
||||
}
|
||||
|
||||
112
internal/service/tenant_test.go
Normal file
112
internal/service/tenant_test.go
Normal file
@@ -0,0 +1,112 @@
|
||||
//
|
||||
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"ragflow/internal/common"
|
||||
)
|
||||
|
||||
// TestListMembersAuthCheck verifies that a non-owner (userID != tenantID) gets
|
||||
// CodeAuthenticationError without hitting the database.
|
||||
func TestListMembersAuthCheck(t *testing.T) {
|
||||
s := &TenantService{}
|
||||
_, code, err := s.ListMembers("user-abc", "tenant-xyz")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for non-owner, got nil")
|
||||
}
|
||||
if code != common.CodeAuthenticationError {
|
||||
t.Errorf("expected CodeAuthenticationError, got %v", code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAddMemberAuthCheck verifies that a non-owner gets CodeAuthenticationError.
|
||||
func TestAddMemberAuthCheck(t *testing.T) {
|
||||
s := &TenantService{}
|
||||
_, code, err := s.AddMember("user-abc", "tenant-xyz", &AddMemberRequest{Email: "a@b.com"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for non-owner, got nil")
|
||||
}
|
||||
if code != common.CodeAuthenticationError {
|
||||
t.Errorf("expected CodeAuthenticationError, got %v", code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAddMemberEmailRequired verifies the email validation runs after the auth check.
|
||||
func TestAddMemberEmailRequired(t *testing.T) {
|
||||
// When userID == tenantID (owner) but no email, expect CodeArgumentError.
|
||||
s := &TenantService{}
|
||||
_, code, err := s.AddMember("owner-id", "owner-id", &AddMemberRequest{Email: ""})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for empty email, got nil")
|
||||
}
|
||||
if code != common.CodeArgumentError {
|
||||
t.Errorf("expected CodeArgumentError, got %v", code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRemoveMemberAuthCheck verifies that an unrelated user gets CodeAuthenticationError.
|
||||
func TestRemoveMemberAuthCheck(t *testing.T) {
|
||||
s := &TenantService{}
|
||||
code, err := s.RemoveMember("user-abc", "tenant-xyz", "user-def")
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if code != common.CodeAuthenticationError {
|
||||
t.Errorf("expected CodeAuthenticationError, got %v", code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRemoveMemberSelfAllowed verifies that a user removing themselves passes the auth check.
|
||||
// (It will fail at the DB layer in unit tests, but the auth check must pass first.)
|
||||
func TestRemoveMemberSelfAllowed(t *testing.T) {
|
||||
s := &TenantService{}
|
||||
// userID == targetUserID: auth check should pass, expect DB error (nil userTenantDAO).
|
||||
code, err := s.RemoveMember("user-abc", "tenant-xyz", "user-abc")
|
||||
if code == common.CodeAuthenticationError {
|
||||
t.Errorf("self-removal should pass auth check, got CodeAuthenticationError: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAcceptInviteAuthCheck verifies that AcceptInvite fails when no membership exists (nil DAO).
|
||||
func TestAcceptInviteAuthCheck(t *testing.T) {
|
||||
s := &TenantService{}
|
||||
// nil userTenantDAO: FilterByUserIDAndTenantID will panic/err, so we expect CodeDataError.
|
||||
code, err := s.AcceptInvite("user-abc", "tenant-xyz")
|
||||
if err == nil {
|
||||
t.Fatal("expected error when no membership exists, got nil")
|
||||
}
|
||||
if code != common.CodeDataError {
|
||||
t.Errorf("expected CodeDataError, got %v", code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTenantRoleConstants verifies the role string values match the Python enums.
|
||||
func TestTenantRoleConstants(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
TenantRoleOwner: "owner",
|
||||
TenantRoleNormal: "normal",
|
||||
TenantRoleInvite: "invite",
|
||||
TenantRoleAdmin: "admin",
|
||||
}
|
||||
for got, want := range cases {
|
||||
if got != want {
|
||||
t.Errorf("role constant = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user