forked from baron/baron-sso
1481 lines
52 KiB
Go
1481 lines
52 KiB
Go
package handler
|
|
|
|
import (
|
|
"baron-sso-backend/internal/domain"
|
|
"baron-sso-backend/internal/service"
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/mock"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// MockTenantService is a mock for service.TenantService
|
|
type MockTenantService struct {
|
|
mock.Mock
|
|
}
|
|
|
|
func (m *MockTenantService) RegisterTenant(ctx context.Context, name, slug, tenantType, description string, domains []string, parentID *string, creatorID string) (*domain.Tenant, error) {
|
|
args := m.Called(ctx, name, slug, tenantType, description, domains, parentID, creatorID)
|
|
if args.Get(0) == nil {
|
|
return nil, args.Error(1)
|
|
}
|
|
return args.Get(0).(*domain.Tenant), args.Error(1)
|
|
}
|
|
|
|
func (m *MockTenantService) GetTenantByDomain(ctx context.Context, domainName string) (*domain.Tenant, error) {
|
|
args := m.Called(ctx, domainName)
|
|
if args.Get(0) == nil {
|
|
return nil, args.Error(1)
|
|
}
|
|
return args.Get(0).(*domain.Tenant), args.Error(1)
|
|
}
|
|
|
|
func (m *MockTenantService) ApproveTenant(ctx context.Context, tenantID string) error {
|
|
args := m.Called(ctx, tenantID)
|
|
return args.Error(0)
|
|
}
|
|
|
|
func (m *MockTenantService) RequestRegistration(ctx context.Context, name, slug, description, domainName, adminEmail string) (*domain.Tenant, error) {
|
|
args := m.Called(ctx, name, slug, description, domainName, adminEmail)
|
|
if args.Get(0) == nil {
|
|
return nil, args.Error(1)
|
|
}
|
|
return args.Get(0).(*domain.Tenant), args.Error(1)
|
|
}
|
|
|
|
func (m *MockTenantService) GetTenantBySlug(ctx context.Context, slug string) (*domain.Tenant, error) {
|
|
args := m.Called(ctx, slug)
|
|
if args.Get(0) == nil {
|
|
return nil, args.Error(1)
|
|
}
|
|
return args.Get(0).(*domain.Tenant), args.Error(1)
|
|
}
|
|
|
|
func (m *MockTenantService) GetTenant(ctx context.Context, id string) (*domain.Tenant, error) {
|
|
args := m.Called(ctx, id)
|
|
if args.Get(0) == nil {
|
|
return nil, args.Error(1)
|
|
}
|
|
return args.Get(0).(*domain.Tenant), args.Error(1)
|
|
}
|
|
|
|
func (m *MockTenantService) ListTenants(ctx context.Context, limit, offset int, parentID string) ([]domain.Tenant, int64, error) {
|
|
args := m.Called(ctx, limit, offset, parentID)
|
|
return args.Get(0).([]domain.Tenant), args.Get(1).(int64), args.Error(2)
|
|
}
|
|
|
|
func (m *MockTenantService) ListManageableTenants(ctx context.Context, userID string) ([]domain.Tenant, error) {
|
|
args := m.Called(ctx, userID)
|
|
if args.Get(0) == nil {
|
|
return nil, args.Error(1)
|
|
}
|
|
return args.Get(0).([]domain.Tenant), args.Error(1)
|
|
}
|
|
|
|
func (m *MockTenantService) IsDomainAllowed(ctx context.Context, domainName string) (bool, error) {
|
|
args := m.Called(ctx, domainName)
|
|
return args.Bool(0), args.Error(1)
|
|
}
|
|
|
|
func (m *MockTenantService) SetKetoService(keto service.KetoService) {
|
|
m.Called(keto)
|
|
}
|
|
|
|
func (m *MockTenantService) ProvisionTenantByDomain(ctx context.Context, domainName string) (*domain.Tenant, error) {
|
|
args := m.Called(ctx, domainName)
|
|
if args.Get(0) == nil {
|
|
return nil, args.Error(1)
|
|
}
|
|
return args.Get(0).(*domain.Tenant), args.Error(1)
|
|
}
|
|
|
|
type MockUserRepoForHandler struct {
|
|
mock.Mock
|
|
deletedIDs []string
|
|
}
|
|
|
|
func (m *MockUserRepoForHandler) DB() *gorm.DB {
|
|
return nil
|
|
}
|
|
|
|
func (m *MockUserRepoForHandler) Create(ctx context.Context, user *domain.User) error { return nil }
|
|
func (m *MockUserRepoForHandler) Update(ctx context.Context, user *domain.User) error { return nil }
|
|
func (m *MockUserRepoForHandler) Delete(ctx context.Context, id string) error {
|
|
m.deletedIDs = append(m.deletedIDs, id)
|
|
return nil
|
|
}
|
|
|
|
func (m *MockUserRepoForHandler) FindByEmail(ctx context.Context, email string) (*domain.User, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *MockUserRepoForHandler) FindByID(ctx context.Context, id string) (*domain.User, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *MockUserRepoForHandler) FindByIDs(ctx context.Context, ids []string) ([]domain.User, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *MockUserRepoForHandler) ListByTenant(ctx context.Context, tenantID string) ([]domain.User, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *MockUserRepoForHandler) List(ctx context.Context, offset, limit int, search string, tenantSlug string) ([]domain.User, int64, error) {
|
|
for _, call := range m.ExpectedCalls {
|
|
if call.Method == "List" {
|
|
args := m.Called(ctx, offset, limit, search, tenantSlug)
|
|
return args.Get(0).([]domain.User), args.Get(1).(int64), args.Error(2)
|
|
}
|
|
}
|
|
return nil, 0, nil
|
|
}
|
|
|
|
func (m *MockUserRepoForHandler) CountByTenant(ctx context.Context, tenantID string) (int64, error) {
|
|
return 0, nil
|
|
}
|
|
|
|
func (m *MockUserRepoForHandler) FindByTenantIDs(ctx context.Context, tenantIDs []string) ([]domain.User, error) {
|
|
args := m.Called(ctx, tenantIDs)
|
|
return args.Get(0).([]domain.User), args.Error(1)
|
|
}
|
|
|
|
func (m *MockUserRepoForHandler) CountByTenantIDs(ctx context.Context, tenantIDs []string) (map[string]int64, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *MockUserRepoForHandler) FindByCompanyCodes(ctx context.Context, codes []string) ([]domain.User, error) {
|
|
args := m.Called(ctx, codes)
|
|
return args.Get(0).([]domain.User), args.Error(1)
|
|
}
|
|
|
|
func (m *MockUserRepoForHandler) CountByCompanyCodes(ctx context.Context, codes []string) (map[string]int64, error) {
|
|
args := m.Called(ctx, codes)
|
|
if args.Get(0) == nil {
|
|
return nil, args.Error(1)
|
|
}
|
|
return args.Get(0).(map[string]int64), args.Error(1)
|
|
}
|
|
|
|
func (m *MockUserRepoForHandler) UpdateUserLoginIDs(ctx context.Context, userID string, loginIDs []domain.UserLoginID) error {
|
|
return nil
|
|
}
|
|
|
|
func (m *MockUserRepoForHandler) GetUserLoginIDs(ctx context.Context, userID string) ([]domain.UserLoginID, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *MockUserRepoForHandler) IsLoginIDTaken(ctx context.Context, loginID string) (bool, error) {
|
|
return false, nil
|
|
}
|
|
|
|
func (m *MockUserRepoForHandler) FindTenantIDByLoginID(ctx context.Context, loginID string) (string, error) {
|
|
return "", nil
|
|
}
|
|
|
|
type MockUserProjectionRepoForHandler struct {
|
|
mock.Mock
|
|
}
|
|
|
|
func (m *MockUserProjectionRepoForHandler) IsReady(ctx context.Context) (bool, error) {
|
|
args := m.Called(ctx)
|
|
return args.Bool(0), args.Error(1)
|
|
}
|
|
|
|
func (m *MockUserProjectionRepoForHandler) GetStatus(ctx context.Context) (domain.UserProjectionStatus, error) {
|
|
args := m.Called(ctx)
|
|
return args.Get(0).(domain.UserProjectionStatus), args.Error(1)
|
|
}
|
|
|
|
func (m *MockUserProjectionRepoForHandler) CountTenantMembers(ctx context.Context, tenants []domain.Tenant) (map[string]int64, error) {
|
|
args := m.Called(ctx, tenants)
|
|
if args.Get(0) == nil {
|
|
return nil, args.Error(1)
|
|
}
|
|
return args.Get(0).(map[string]int64), args.Error(1)
|
|
}
|
|
|
|
func (m *MockUserProjectionRepoForHandler) ReplaceAllFromKratos(ctx context.Context, users []domain.User) error {
|
|
args := m.Called(ctx, users)
|
|
return args.Error(0)
|
|
}
|
|
|
|
func (m *MockUserProjectionRepoForHandler) MarkFailed(ctx context.Context, syncErr error) error {
|
|
args := m.Called(ctx, syncErr)
|
|
return args.Error(0)
|
|
}
|
|
|
|
func toJSONString(t *testing.T, value any) string {
|
|
t.Helper()
|
|
raw, err := json.Marshal(value)
|
|
require.NoError(t, err)
|
|
return string(raw)
|
|
}
|
|
|
|
func TestTenantHandler_CreateTenant(t *testing.T) {
|
|
app := fiber.New()
|
|
mockSvc := new(MockTenantService)
|
|
// CreateTenant checks h.DB != nil
|
|
h := &TenantHandler{Service: mockSvc, DB: &gorm.DB{}}
|
|
|
|
app.Post("/tenants", h.CreateTenant)
|
|
|
|
input := map[string]any{
|
|
"name": "Test Tenant",
|
|
"slug": "test-tenant",
|
|
"domains": []string{"test.com"},
|
|
}
|
|
body, _ := json.Marshal(input)
|
|
|
|
mockSvc.On("RegisterTenant", mock.Anything, "Test Tenant", "test-tenant", domain.TenantTypeCompany, "", []string(nil), (*string)(nil), "").
|
|
Return(&domain.Tenant{ID: "t1", Name: "Test Tenant", Slug: "test-tenant"}, nil)
|
|
|
|
req := httptest.NewRequest("POST", "/tenants", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, _ := app.Test(req)
|
|
|
|
assert.Equal(t, http.StatusCreated, resp.StatusCode)
|
|
var got map[string]any
|
|
json.NewDecoder(resp.Body).Decode(&got)
|
|
assert.Equal(t, "t1", got["id"])
|
|
}
|
|
|
|
func TestTenantHandler_ListTenantsUsesReadyUserProjectionCountsWithoutKratos(t *testing.T) {
|
|
app := fiber.New()
|
|
mockSvc := new(MockTenantService)
|
|
mockProjection := new(MockUserProjectionRepoForHandler)
|
|
|
|
h := &TenantHandler{
|
|
Service: mockSvc,
|
|
UserProjectionRepo: mockProjection,
|
|
}
|
|
|
|
app.Use(func(c *fiber.Ctx) error {
|
|
c.Locals("user_profile", &domain.UserProfileResponse{
|
|
Role: "super_admin",
|
|
})
|
|
return c.Next()
|
|
})
|
|
app.Get("/tenants", h.ListTenants)
|
|
|
|
tenants := []domain.Tenant{
|
|
{ID: "00000000-0000-0000-0000-000000000001", Name: "Saman", Slug: "saman"},
|
|
}
|
|
mockSvc.On("ListTenants", mock.Anything, 10, 0, "").Return(tenants, int64(1), nil).Once()
|
|
mockProjection.On("IsReady", mock.Anything).Return(true, nil).Once()
|
|
mockProjection.On("CountTenantMembers", mock.Anything, tenants).
|
|
Return(map[string]int64{"00000000-0000-0000-0000-000000000001": 2}, nil).Once()
|
|
|
|
req := httptest.NewRequest("GET", "/tenants?limit=10&offset=0", nil)
|
|
resp, _ := app.Test(req)
|
|
|
|
require.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
var res tenantListResponse
|
|
json.NewDecoder(resp.Body).Decode(&res)
|
|
|
|
require.Len(t, res.Items, 1)
|
|
assert.Equal(t, int64(2), res.Items[0].MemberCount)
|
|
mockProjection.AssertExpectations(t)
|
|
}
|
|
|
|
func TestTenantHandler_ListTenantsRejectsStatsWhenUserProjectionIsNotReady(t *testing.T) {
|
|
app := fiber.New()
|
|
mockSvc := new(MockTenantService)
|
|
mockProjection := new(MockUserProjectionRepoForHandler)
|
|
|
|
h := &TenantHandler{
|
|
Service: mockSvc,
|
|
UserProjectionRepo: mockProjection,
|
|
}
|
|
|
|
app.Use(func(c *fiber.Ctx) error {
|
|
c.Locals("user_profile", &domain.UserProfileResponse{
|
|
Role: "super_admin",
|
|
})
|
|
return c.Next()
|
|
})
|
|
app.Get("/tenants", h.ListTenants)
|
|
|
|
tenants := []domain.Tenant{
|
|
{ID: "00000000-0000-0000-0000-000000000001", Name: "Saman", Slug: "saman"},
|
|
}
|
|
mockSvc.On("ListTenants", mock.Anything, 10, 0, "").Return(tenants, int64(1), nil).Once()
|
|
mockProjection.On("IsReady", mock.Anything).Return(false, nil).Once()
|
|
|
|
req := httptest.NewRequest("GET", "/tenants?limit=10&offset=0", nil)
|
|
resp, _ := app.Test(req)
|
|
|
|
assert.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
|
|
mockProjection.AssertNotCalled(t, "CountTenantMembers", mock.Anything, mock.Anything)
|
|
}
|
|
|
|
func TestTenantHandler_ListTenants(t *testing.T) {
|
|
app := fiber.New()
|
|
mockSvc := new(MockTenantService)
|
|
mockProjection := new(MockUserProjectionRepoForHandler)
|
|
|
|
h := &TenantHandler{
|
|
Service: mockSvc,
|
|
UserProjectionRepo: mockProjection,
|
|
}
|
|
|
|
app.Use(func(c *fiber.Ctx) error {
|
|
c.Locals("user_profile", &domain.UserProfileResponse{
|
|
Role: "super_admin",
|
|
})
|
|
return c.Next()
|
|
})
|
|
app.Get("/tenants", h.ListTenants)
|
|
tenants := []domain.Tenant{
|
|
{ID: "t1", Name: "Tenant A", Slug: "slug-a"},
|
|
{ID: "t2", Name: "Tenant B", Slug: "slug-b"},
|
|
}
|
|
|
|
// Mocking for the new allTenants check in ListTenants
|
|
mockSvc.On("ListTenants", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tenants, int64(2), nil).Maybe()
|
|
mockProjection.On("IsReady", mock.Anything).Return(true, nil).Once()
|
|
mockProjection.On("CountTenantMembers", mock.Anything, tenants).
|
|
Return(map[string]int64{"t1": 5, "t2": 10}, nil).Once()
|
|
|
|
req := httptest.NewRequest("GET", "/tenants?limit=10&offset=0", nil)
|
|
resp, _ := app.Test(req)
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
var res tenantListResponse
|
|
json.NewDecoder(resp.Body).Decode(&res)
|
|
|
|
assert.Equal(t, int64(2), res.Total)
|
|
assert.Len(t, res.Items, 2)
|
|
|
|
// Check if counts are mapped correctly
|
|
for _, item := range res.Items {
|
|
if item.Slug == "slug-a" {
|
|
assert.Equal(t, int64(5), item.MemberCount)
|
|
} else if item.Slug == "slug-b" {
|
|
assert.Equal(t, int64(10), item.MemberCount)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTenantHandler_ListTenantsReturnsNextCursorWhenMoreRowsExist(t *testing.T) {
|
|
app := fiber.New()
|
|
mockSvc := new(MockTenantService)
|
|
mockProjection := new(MockUserProjectionRepoForHandler)
|
|
|
|
h := &TenantHandler{
|
|
Service: mockSvc,
|
|
UserProjectionRepo: mockProjection,
|
|
}
|
|
|
|
app.Use(func(c *fiber.Ctx) error {
|
|
c.Locals("user_profile", &domain.UserProfileResponse{
|
|
Role: "super_admin",
|
|
})
|
|
return c.Next()
|
|
})
|
|
app.Get("/tenants", h.ListTenants)
|
|
|
|
createdAt := time.Date(2026, 5, 13, 8, 0, 0, 0, time.UTC)
|
|
tenants := []domain.Tenant{
|
|
{ID: "00000000-0000-0000-0000-000000000002", Name: "Tenant B", Slug: "slug-b", CreatedAt: createdAt},
|
|
{ID: "00000000-0000-0000-0000-000000000001", Name: "Tenant A", Slug: "slug-a", CreatedAt: createdAt.Add(-time.Minute)},
|
|
}
|
|
|
|
mockSvc.On("ListTenants", mock.Anything, 2, 0, "").Return(tenants, int64(3), nil).Once()
|
|
mockProjection.On("IsReady", mock.Anything).Return(true, nil).Once()
|
|
mockProjection.On("CountTenantMembers", mock.Anything, tenants).Return(map[string]int64{}, nil).Once()
|
|
|
|
req := httptest.NewRequest("GET", "/tenants?limit=2&offset=0", nil)
|
|
resp, _ := app.Test(req)
|
|
|
|
require.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
var res tenantListResponse
|
|
require.NoError(t, json.NewDecoder(resp.Body).Decode(&res))
|
|
require.Len(t, res.Items, 2)
|
|
require.NotEmpty(t, res.NextCursor)
|
|
}
|
|
|
|
func TestPageTenantsByCursorUsesStableCreatedAtAndIDOrder(t *testing.T) {
|
|
createdAt := time.Date(2026, 5, 13, 8, 0, 0, 0, time.UTC)
|
|
tenants := []domain.Tenant{
|
|
{ID: "00000000-0000-0000-0000-000000000001", Name: "Tenant A", Slug: "slug-a", CreatedAt: createdAt},
|
|
{ID: "00000000-0000-0000-0000-000000000003", Name: "Tenant C", Slug: "slug-c", CreatedAt: createdAt},
|
|
{ID: "00000000-0000-0000-0000-000000000002", Name: "Tenant B", Slug: "slug-b", CreatedAt: createdAt},
|
|
}
|
|
|
|
page, nextCursor, err := pageTenantsByCursor(tenants, 2, "")
|
|
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, nextCursor)
|
|
require.Equal(t, []string{
|
|
"00000000-0000-0000-0000-000000000003",
|
|
"00000000-0000-0000-0000-000000000002",
|
|
}, []string{page[0].ID, page[1].ID})
|
|
|
|
nextPage, _, err := pageTenantsByCursor(tenants, 2, nextCursor)
|
|
|
|
require.NoError(t, err)
|
|
require.Equal(t, []string{"00000000-0000-0000-0000-000000000001"}, []string{nextPage[0].ID})
|
|
}
|
|
|
|
func TestTenantHandler_ListTenantsHidesPrivateSubtreeForUnauthorizedUser(t *testing.T) {
|
|
app := fiber.New()
|
|
mockSvc := new(MockTenantService)
|
|
mockProjection := new(MockUserProjectionRepoForHandler)
|
|
|
|
h := &TenantHandler{
|
|
Service: mockSvc,
|
|
UserProjectionRepo: mockProjection,
|
|
}
|
|
|
|
parent := func(id string) *string { return &id }
|
|
tenants := []domain.Tenant{
|
|
{ID: "family", Type: domain.TenantTypeCompanyGroup, Name: "한맥가족", Slug: "hanmac-family"},
|
|
{ID: "company", Type: domain.TenantTypeCompany, ParentID: parent("family"), Name: "한맥", Slug: "hanmac"},
|
|
{ID: "public-team", Type: domain.TenantTypeUserGroup, ParentID: parent("company"), Name: "공개팀", Slug: "public-team"},
|
|
{ID: "private-team", Type: domain.TenantTypeUserGroup, ParentID: parent("company"), Name: "비공개팀", Slug: "private-team", Config: domain.JSONMap{"visibility": "private"}},
|
|
{ID: "private-child", Type: domain.TenantTypeUserGroup, ParentID: parent("private-team"), Name: "비공개하위", Slug: "private-child"},
|
|
}
|
|
|
|
app.Use(func(c *fiber.Ctx) error {
|
|
c.Locals("user_profile", &domain.UserProfileResponse{
|
|
ID: "user-1",
|
|
Role: domain.RoleTenantAdmin,
|
|
TenantID: parent("company"),
|
|
})
|
|
return c.Next()
|
|
})
|
|
app.Get("/tenants", h.ListTenants)
|
|
|
|
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "").Return(tenants, int64(len(tenants)), nil).Once()
|
|
mockProjection.On("IsReady", mock.Anything).Return(true, nil).Once()
|
|
mockProjection.On("CountTenantMembers", mock.Anything, mock.MatchedBy(func(got []domain.Tenant) bool {
|
|
return tenantSlugsMatch(got, "hanmac-family", "hanmac", "public-team")
|
|
})).Return(map[string]int64{}, nil).Once()
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/tenants?limit=100&offset=0", nil)
|
|
resp, err := app.Test(req)
|
|
require.NoError(t, err)
|
|
require.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
var res tenantListResponse
|
|
require.NoError(t, json.NewDecoder(resp.Body).Decode(&res))
|
|
require.Equal(t, int64(3), res.Total)
|
|
require.NotContains(t, toJSONString(t, res), "private-team")
|
|
require.NotContains(t, toJSONString(t, res), "private-child")
|
|
}
|
|
|
|
func TestTenantHandler_ListTenantsShowsPrivateSubtreeForManageableTenant(t *testing.T) {
|
|
app := fiber.New()
|
|
mockSvc := new(MockTenantService)
|
|
mockProjection := new(MockUserProjectionRepoForHandler)
|
|
|
|
h := &TenantHandler{
|
|
Service: mockSvc,
|
|
UserProjectionRepo: mockProjection,
|
|
}
|
|
|
|
parent := func(id string) *string { return &id }
|
|
tenants := []domain.Tenant{
|
|
{ID: "family", Type: domain.TenantTypeCompanyGroup, Name: "한맥가족", Slug: "hanmac-family"},
|
|
{ID: "company", Type: domain.TenantTypeCompany, ParentID: parent("family"), Name: "한맥", Slug: "hanmac"},
|
|
{ID: "private-team", Type: domain.TenantTypeUserGroup, ParentID: parent("company"), Name: "비공개팀", Slug: "private-team", Config: domain.JSONMap{"visibility": "private"}},
|
|
{ID: "private-child", Type: domain.TenantTypeUserGroup, ParentID: parent("private-team"), Name: "비공개하위", Slug: "private-child"},
|
|
}
|
|
|
|
app.Use(func(c *fiber.Ctx) error {
|
|
c.Locals("user_profile", &domain.UserProfileResponse{
|
|
ID: "user-1",
|
|
Role: domain.RoleTenantAdmin,
|
|
TenantID: parent("company"),
|
|
ManageableTenants: []domain.Tenant{
|
|
{ID: "private-team", Slug: "private-team"},
|
|
},
|
|
})
|
|
return c.Next()
|
|
})
|
|
app.Get("/tenants", h.ListTenants)
|
|
|
|
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "").Return(tenants, int64(len(tenants)), nil).Once()
|
|
mockProjection.On("IsReady", mock.Anything).Return(true, nil).Once()
|
|
mockProjection.On("CountTenantMembers", mock.Anything, mock.MatchedBy(func(got []domain.Tenant) bool {
|
|
return tenantSlugsMatch(got, "hanmac-family", "hanmac", "private-team", "private-child")
|
|
})).Return(map[string]int64{}, nil).Once()
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/tenants?limit=100&offset=0", nil)
|
|
resp, err := app.Test(req)
|
|
require.NoError(t, err)
|
|
require.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
var res tenantListResponse
|
|
require.NoError(t, json.NewDecoder(resp.Body).Decode(&res))
|
|
require.Equal(t, int64(4), res.Total)
|
|
require.Contains(t, toJSONString(t, res), "private-team")
|
|
require.Contains(t, toJSONString(t, res), "private-child")
|
|
}
|
|
|
|
func TestTenantHandler_FilterPrivateTenantsAllowsExplicitPrivatePermission(t *testing.T) {
|
|
parent := func(id string) *string { return &id }
|
|
tenants := []domain.Tenant{
|
|
{ID: "family", Type: domain.TenantTypeCompanyGroup, Name: "한맥가족", Slug: "hanmac-family"},
|
|
{ID: "company", Type: domain.TenantTypeCompany, ParentID: parent("family"), Name: "한맥", Slug: "hanmac"},
|
|
{ID: "private-team", Type: domain.TenantTypeUserGroup, ParentID: parent("company"), Name: "비공개팀", Slug: "private-team", Config: domain.JSONMap{"visibility": "private"}},
|
|
{ID: "private-child", Type: domain.TenantTypeUserGroup, ParentID: parent("private-team"), Name: "비공개하위", Slug: "private-child"},
|
|
}
|
|
mockKeto := new(devMockKetoService)
|
|
h := &TenantHandler{Keto: mockKeto}
|
|
|
|
mockKeto.On("CheckPermission", mock.Anything, "User:user-1", "Tenant", "private-team", "view_private").Return(true, nil).Once()
|
|
|
|
filtered, err := h.filterPrivateTenantsForProfile(context.Background(), tenants, &domain.UserProfileResponse{
|
|
ID: "user-1",
|
|
Role: domain.RoleTenantAdmin,
|
|
TenantID: parent("company"),
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
require.True(t, tenantSlugsMatch(filtered, "hanmac-family", "hanmac", "private-team", "private-child"))
|
|
mockKeto.AssertExpectations(t)
|
|
}
|
|
|
|
func tenantSlugsMatch(got []domain.Tenant, want ...string) bool {
|
|
if len(got) != len(want) {
|
|
return false
|
|
}
|
|
counts := make(map[string]int, len(want))
|
|
for _, slug := range want {
|
|
counts[slug]++
|
|
}
|
|
for _, tenant := range got {
|
|
counts[tenant.Slug]--
|
|
if counts[tenant.Slug] < 0 {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func TestTenantHandler_GetOrgContextJSONDefaultsToHanmacFamilyForApiKey(t *testing.T) {
|
|
app := fiber.New()
|
|
mockSvc := new(MockTenantService)
|
|
mockUsers := new(MockUserRepoForHandler)
|
|
h := &TenantHandler{Service: mockSvc, UserRepo: mockUsers}
|
|
|
|
app.Use(func(c *fiber.Ctx) error {
|
|
c.Locals("apiKeyName", "orgfront-ssot-client")
|
|
return c.Next()
|
|
})
|
|
app.Get("/org-context", h.GetOrgContext)
|
|
|
|
now := time.Date(2026, 5, 13, 12, 0, 0, 0, time.UTC)
|
|
parent := func(id string) *string { return &id }
|
|
tenants := []domain.Tenant{
|
|
{ID: "root-other", Type: domain.TenantTypeCompanyGroup, Name: "다른그룹", Slug: "other-family", Status: domain.TenantStatusActive, CreatedAt: now, UpdatedAt: now},
|
|
{ID: "group-hanmac-family", Type: domain.TenantTypeCompanyGroup, Name: "한맥가족", Slug: "hanmac-family", Status: domain.TenantStatusActive, CreatedAt: now, UpdatedAt: now},
|
|
{ID: "company-hanmac", Type: domain.TenantTypeCompany, ParentID: parent("group-hanmac-family"), Name: "한맥기술", Slug: "hanmac", Status: domain.TenantStatusActive, CreatedAt: now, UpdatedAt: now},
|
|
{ID: "dept-platform", Type: domain.TenantTypeUserGroup, ParentID: parent("company-hanmac"), Name: "플랫폼실", Slug: "platform", Status: domain.TenantStatusActive, Config: domain.JSONMap{"orgUnitType": "실"}, CreatedAt: now, UpdatedAt: now},
|
|
{ID: "team-sso", Type: domain.TenantTypeUserGroup, ParentID: parent("dept-platform"), Name: "SSO팀", Slug: "sso", Status: domain.TenantStatusActive, CreatedAt: now, UpdatedAt: now},
|
|
{ID: "private-team", Type: domain.TenantTypeUserGroup, ParentID: parent("company-hanmac"), Name: "비공개", Slug: "private-team", Status: domain.TenantStatusActive, Config: domain.JSONMap{"visibility": "private"}, CreatedAt: now, UpdatedAt: now},
|
|
}
|
|
usersByTenantID := []domain.User{
|
|
{
|
|
ID: "user-platform-lead",
|
|
Email: "lead@example.com",
|
|
Name: "플랫폼 리드",
|
|
Phone: "010-1111-2222",
|
|
Status: domain.UserStatusActive,
|
|
TenantID: parent("dept-platform"),
|
|
CompanyCode: "platform",
|
|
Grade: "책임",
|
|
Position: "실장",
|
|
JobTitle: "Backend Engineer",
|
|
Metadata: domain.JSONMap{
|
|
"additionalAppointments": []any{
|
|
map[string]any{
|
|
"tenantId": "dept-platform",
|
|
"isPrimary": true,
|
|
"isOwner": true,
|
|
"grade": "수석",
|
|
"position": "실장",
|
|
"jobTitle": "기술기획",
|
|
},
|
|
},
|
|
},
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
},
|
|
{
|
|
ID: "user-archived",
|
|
Email: "archived@example.com",
|
|
Name: "보관 사용자",
|
|
Status: domain.UserStatusArchived,
|
|
TenantID: parent("dept-platform"),
|
|
CompanyCode: "platform",
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
},
|
|
{
|
|
ID: "user-suspended",
|
|
Email: "suspended@example.com",
|
|
Name: "정지 사용자",
|
|
Status: domain.UserStatusSuspended,
|
|
TenantID: parent("dept-platform"),
|
|
CompanyCode: "platform",
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
},
|
|
{
|
|
ID: "user-temporary-leave",
|
|
Email: "temporary-leave@example.com",
|
|
Name: "단기휴무 사용자",
|
|
Status: domain.UserStatusTemporaryLeave,
|
|
TenantID: parent("dept-platform"),
|
|
CompanyCode: "platform",
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
},
|
|
{
|
|
ID: "user-preboarding",
|
|
Email: "preboarding@example.com",
|
|
Name: "입사대기 사용자",
|
|
Status: domain.UserStatusPreboarding,
|
|
TenantID: parent("dept-platform"),
|
|
CompanyCode: "platform",
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
},
|
|
{
|
|
ID: "user-baron-guest",
|
|
Email: "baron-guest@example.com",
|
|
Name: "Baron Guest",
|
|
Status: domain.UserStatusBaronGuest,
|
|
TenantID: parent("dept-platform"),
|
|
CompanyCode: "platform",
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
},
|
|
{
|
|
ID: "user-extended-leave",
|
|
Email: "extended-leave@example.com",
|
|
Name: "장기휴직 사용자",
|
|
Status: domain.UserStatusExtendedLeave,
|
|
TenantID: parent("dept-platform"),
|
|
CompanyCode: "platform",
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
},
|
|
}
|
|
usersBySlug := []domain.User{
|
|
{ID: "user-sso-member", Email: "member@example.com", Name: "SSO 구성원", Status: domain.UserStatusActive, CompanyCode: "sso", Grade: "선임", CreatedAt: now, UpdatedAt: now},
|
|
}
|
|
usersByList := []domain.User{
|
|
{
|
|
ID: "user-appointment-only",
|
|
Email: "appointment@example.com",
|
|
Name: "겸직 사용자",
|
|
Status: domain.UserStatusActive,
|
|
Metadata: domain.JSONMap{
|
|
"additionalAppointments": []any{
|
|
map[string]any{
|
|
"tenantSlug": "sso",
|
|
"isManager": true,
|
|
"position": "파트장",
|
|
},
|
|
},
|
|
},
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
},
|
|
}
|
|
|
|
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "").Return(tenants, int64(len(tenants)), nil)
|
|
mockUsers.On("FindByTenantIDs", mock.Anything, []string{"group-hanmac-family", "company-hanmac", "dept-platform", "team-sso"}).Return(usersByTenantID, nil)
|
|
mockUsers.On("FindByCompanyCodes", mock.Anything, []string{"hanmac-family", "hanmac", "platform", "sso"}).Return(usersBySlug, nil)
|
|
mockUsers.On("List", mock.Anything, 0, 10000, "", "").Return(usersByList, int64(len(usersByList)), nil)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/org-context", nil)
|
|
resp, err := app.Test(req)
|
|
require.NoError(t, err)
|
|
require.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
var got map[string]any
|
|
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
|
|
require.Equal(t, "baron.org-context.v1", got["schemaVersion"])
|
|
|
|
scope := got["scope"].(map[string]any)
|
|
require.Equal(t, "group-hanmac-family", scope["tenantId"])
|
|
require.Equal(t, "hanmac-family", scope["tenantSlug"])
|
|
|
|
tenantsPayload := got["tenants"].([]any)
|
|
require.Len(t, tenantsPayload, 4)
|
|
require.Equal(t, "group-hanmac-family", tenantsPayload[0].(map[string]any)["id"])
|
|
require.Equal(t, "company-hanmac", tenantsPayload[1].(map[string]any)["id"])
|
|
require.Equal(t, "dept-platform", tenantsPayload[2].(map[string]any)["id"])
|
|
require.Equal(t, "team-sso", tenantsPayload[3].(map[string]any)["id"])
|
|
|
|
require.NotContains(t, got, "users")
|
|
deptPlatform := tenantsPayload[2].(map[string]any)
|
|
platformMembers := deptPlatform["members"].([]any)
|
|
require.Len(t, platformMembers, 3)
|
|
firstUser := platformMembers[0].(map[string]any)
|
|
require.NotContains(t, firstUser, "id")
|
|
require.NotContains(t, firstUser, "phone")
|
|
require.NotContains(t, firstUser, "tenantIds")
|
|
require.NotContains(t, firstUser, "tenantSlugs")
|
|
require.NotContains(t, firstUser, "memberships")
|
|
require.NotContains(t, firstUser, "role")
|
|
require.NotContains(t, firstUser, "status")
|
|
require.NotContains(t, firstUser, "metadata")
|
|
require.NotContains(t, firstUser, "createdAt")
|
|
require.NotContains(t, firstUser, "updatedAt")
|
|
require.Equal(t, "lead@example.com", firstUser["email"])
|
|
require.Equal(t, "플랫폼 리드", firstUser["name"])
|
|
require.Equal(t, true, firstUser["isOwner"])
|
|
require.Equal(t, false, firstUser["isManager"])
|
|
require.Equal(t, true, firstUser["isPrimary"])
|
|
require.NotContains(t, firstUser, "isLeader")
|
|
require.Equal(t, "수석", firstUser["grade"])
|
|
require.Equal(t, "실장", firstUser["position"])
|
|
require.Equal(t, "기술기획", firstUser["jobTitle"])
|
|
teamSSO := tenantsPayload[3].(map[string]any)
|
|
ssoMembers := teamSSO["members"].([]any)
|
|
require.Len(t, ssoMembers, 1)
|
|
appointmentOnly := ssoMembers[0].(map[string]any)
|
|
require.Equal(t, "appointment@example.com", appointmentOnly["email"])
|
|
require.Equal(t, false, appointmentOnly["isOwner"])
|
|
require.Equal(t, true, appointmentOnly["isManager"])
|
|
require.NotContains(t, appointmentOnly, "isLeader")
|
|
|
|
tree := got["tree"].(map[string]any)
|
|
require.Equal(t, "group-hanmac-family", tree["id"])
|
|
require.NotContains(t, tree, "directUserIds")
|
|
require.Contains(t, tree, "members")
|
|
require.NotContains(t, toJSONString(t, got), "directUserIds")
|
|
require.NotContains(t, toJSONString(t, got), "private-team")
|
|
require.NotContains(t, toJSONString(t, got), "root-other")
|
|
require.NotContains(t, toJSONString(t, got), "archived@example.com")
|
|
require.Contains(t, toJSONString(t, got), "suspended@example.com")
|
|
require.Contains(t, toJSONString(t, got), "temporary-leave@example.com")
|
|
require.NotContains(t, toJSONString(t, got), "preboarding@example.com")
|
|
require.NotContains(t, toJSONString(t, got), "baron-guest@example.com")
|
|
require.NotContains(t, toJSONString(t, got), "extended-leave@example.com")
|
|
}
|
|
|
|
func TestTenantHandler_GetOrgContextJSONIncludesUserIDsOnlyWhenRequested(t *testing.T) {
|
|
app := fiber.New()
|
|
mockSvc := new(MockTenantService)
|
|
mockUsers := new(MockUserRepoForHandler)
|
|
h := &TenantHandler{Service: mockSvc, UserRepo: mockUsers}
|
|
|
|
app.Use(func(c *fiber.Ctx) error {
|
|
c.Locals("apiKeyName", "orgfront-ssot-client")
|
|
return c.Next()
|
|
})
|
|
app.Get("/org-context", h.GetOrgContext)
|
|
|
|
now := time.Date(2026, 5, 13, 12, 0, 0, 0, time.UTC)
|
|
parent := func(id string) *string { return &id }
|
|
tenants := []domain.Tenant{
|
|
{ID: "company-hanmac", Type: domain.TenantTypeCompany, Name: "한맥기술", Slug: "hanmac", Status: domain.TenantStatusActive, CreatedAt: now, UpdatedAt: now},
|
|
}
|
|
users := []domain.User{
|
|
{ID: "user-1", Email: "user@example.com", Name: "사용자", Phone: "010-1234-5678", Status: domain.UserStatusActive, TenantID: parent("company-hanmac"), CompanyCode: "hanmac", CreatedAt: now, UpdatedAt: now},
|
|
}
|
|
|
|
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "").Return(tenants, int64(len(tenants)), nil)
|
|
mockUsers.On("FindByTenantIDs", mock.Anything, []string{"company-hanmac"}).Return(users, nil)
|
|
mockUsers.On("FindByCompanyCodes", mock.Anything, []string{"hanmac"}).Return([]domain.User{}, nil)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/org-context?tenantSlug=hanmac&includeUserIds=true", nil)
|
|
resp, err := app.Test(req)
|
|
require.NoError(t, err)
|
|
require.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
var got map[string]any
|
|
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
|
|
require.NotContains(t, got, "users")
|
|
tenantsPayload := got["tenants"].([]any)
|
|
members := tenantsPayload[0].(map[string]any)["members"].([]any)
|
|
require.Len(t, members, 1)
|
|
member := members[0].(map[string]any)
|
|
require.Equal(t, "user-1", member["id"])
|
|
require.Equal(t, "010-1234-5678", member["phone"])
|
|
require.NotContains(t, member, "tenantIds")
|
|
require.NotContains(t, member, "tenantSlugs")
|
|
require.NotContains(t, member, "memberships")
|
|
tree := got["tree"].(map[string]any)
|
|
treeMembers := tree["members"].([]any)
|
|
require.Len(t, treeMembers, 1)
|
|
require.Equal(t, "user-1", treeMembers[0].(map[string]any)["id"])
|
|
require.Equal(t, "010-1234-5678", treeMembers[0].(map[string]any)["phone"])
|
|
require.NotContains(t, tree, "directUserIds")
|
|
}
|
|
|
|
func TestTenantHandler_GetOrgContextJSONScopesByTenantSlug(t *testing.T) {
|
|
app := fiber.New()
|
|
mockSvc := new(MockTenantService)
|
|
mockUsers := new(MockUserRepoForHandler)
|
|
h := &TenantHandler{Service: mockSvc, UserRepo: mockUsers}
|
|
|
|
app.Use(func(c *fiber.Ctx) error {
|
|
c.Locals("apiKeyName", "orgfront-ssot-client")
|
|
return c.Next()
|
|
})
|
|
app.Get("/org-context", h.GetOrgContext)
|
|
|
|
now := time.Date(2026, 5, 13, 12, 0, 0, 0, time.UTC)
|
|
parent := func(id string) *string { return &id }
|
|
tenants := []domain.Tenant{
|
|
{ID: "group-hanmac-family", Type: domain.TenantTypeCompanyGroup, Name: "한맥가족", Slug: "hanmac-family", Status: domain.TenantStatusActive, CreatedAt: now, UpdatedAt: now},
|
|
{ID: "company-hanmac", Type: domain.TenantTypeCompany, ParentID: parent("group-hanmac-family"), Name: "한맥기술", Slug: "hanmac", Status: domain.TenantStatusActive, CreatedAt: now, UpdatedAt: now},
|
|
{ID: "dept-platform", Type: domain.TenantTypeUserGroup, ParentID: parent("company-hanmac"), Name: "플랫폼실", Slug: "platform", Status: domain.TenantStatusActive, CreatedAt: now, UpdatedAt: now},
|
|
{ID: "company-other", Type: domain.TenantTypeCompany, ParentID: parent("group-hanmac-family"), Name: "다른회사", Slug: "other", Status: domain.TenantStatusActive, CreatedAt: now, UpdatedAt: now},
|
|
}
|
|
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "").Return(tenants, int64(len(tenants)), nil)
|
|
mockUsers.On("FindByTenantIDs", mock.Anything, []string{"company-hanmac", "dept-platform"}).Return([]domain.User{}, nil)
|
|
mockUsers.On("FindByCompanyCodes", mock.Anything, []string{"hanmac", "platform"}).Return([]domain.User{}, nil)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/org-context?tenantSlug=hanmac", nil)
|
|
resp, err := app.Test(req)
|
|
require.NoError(t, err)
|
|
require.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
var got map[string]any
|
|
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
|
|
scope := got["scope"].(map[string]any)
|
|
require.Equal(t, "company-hanmac", scope["tenantId"])
|
|
require.Equal(t, "hanmac", scope["tenantSlug"])
|
|
|
|
require.Contains(t, toJSONString(t, got), "dept-platform")
|
|
require.NotContains(t, toJSONString(t, got), "company-other")
|
|
}
|
|
|
|
func TestTenantHandler_GetOrgContextJSONRequiresApiKey(t *testing.T) {
|
|
app := fiber.New()
|
|
h := &TenantHandler{}
|
|
app.Get("/org-context", h.GetOrgContext)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/org-context", nil)
|
|
resp, err := app.Test(req)
|
|
require.NoError(t, err)
|
|
require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
|
}
|
|
|
|
func TestTenantHandler_ListTenantsReturnsServiceUnavailableWhenProjectionStatusFails(t *testing.T) {
|
|
app := fiber.New()
|
|
mockSvc := new(MockTenantService)
|
|
mockProjection := new(MockUserProjectionRepoForHandler)
|
|
|
|
h := &TenantHandler{
|
|
Service: mockSvc,
|
|
UserProjectionRepo: mockProjection,
|
|
}
|
|
|
|
app.Use(func(c *fiber.Ctx) error {
|
|
c.Locals("user_profile", &domain.UserProfileResponse{
|
|
Role: "super_admin",
|
|
})
|
|
return c.Next()
|
|
})
|
|
app.Get("/tenants", h.ListTenants)
|
|
|
|
tenants := []domain.Tenant{
|
|
{ID: "t1", Name: "Tenant A", Slug: "slug-a"},
|
|
}
|
|
mockSvc.On("ListTenants", mock.Anything, 10, 0, "").Return(tenants, int64(1), nil).Once()
|
|
mockProjection.On("IsReady", mock.Anything).Return(false, errors.New("projection state query failed")).Once()
|
|
|
|
req := httptest.NewRequest("GET", "/tenants?limit=10&offset=0", nil)
|
|
resp, _ := app.Test(req)
|
|
|
|
assert.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
|
|
mockProjection.AssertExpectations(t)
|
|
}
|
|
|
|
func TestTenantHandler_ListTenantsUsesProjectionCountsWhenAvailable(t *testing.T) {
|
|
app := fiber.New()
|
|
mockSvc := new(MockTenantService)
|
|
mockUserRepo := new(MockUserRepoForHandler)
|
|
mockProjection := new(MockUserProjectionRepoForHandler)
|
|
|
|
h := &TenantHandler{
|
|
Service: mockSvc,
|
|
UserRepo: mockUserRepo,
|
|
UserProjectionRepo: mockProjection,
|
|
}
|
|
|
|
app.Use(func(c *fiber.Ctx) error {
|
|
c.Locals("user_profile", &domain.UserProfileResponse{
|
|
Role: "super_admin",
|
|
})
|
|
return c.Next()
|
|
})
|
|
app.Get("/tenants", h.ListTenants)
|
|
|
|
tenants := []domain.Tenant{
|
|
{ID: "00000000-0000-0000-0000-000000000001", Name: "Saman", Slug: "saman"},
|
|
}
|
|
|
|
mockSvc.On("ListTenants", mock.Anything, 10, 0, "").Return(tenants, int64(1), nil).Once()
|
|
mockProjection.On("IsReady", mock.Anything).Return(true, nil).Once()
|
|
mockProjection.On("CountTenantMembers", mock.Anything, tenants).
|
|
Return(map[string]int64{"00000000-0000-0000-0000-000000000001": 2}, nil).Once()
|
|
|
|
mockUserRepo.On("CountByCompanyCodes", mock.Anything, []string{"saman"}).
|
|
Return(map[string]int64{"saman": 152}, nil).Maybe()
|
|
mockUserRepo.On("CountByTenantIDs", mock.Anything, []string{"00000000-0000-0000-0000-000000000001"}).
|
|
Return(map[string]int64{"00000000-0000-0000-0000-000000000001": 152}, nil).Maybe()
|
|
|
|
req := httptest.NewRequest("GET", "/tenants?limit=10&offset=0", nil)
|
|
resp, _ := app.Test(req)
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
var res tenantListResponse
|
|
json.NewDecoder(resp.Body).Decode(&res)
|
|
|
|
assert.Len(t, res.Items, 1)
|
|
assert.Equal(t, int64(2), res.Items[0].MemberCount)
|
|
mockProjection.AssertExpectations(t)
|
|
}
|
|
|
|
func TestTenantHandler_ExportTenantsCSV(t *testing.T) {
|
|
app := fiber.New()
|
|
mockSvc := new(MockTenantService)
|
|
h := &TenantHandler{Service: mockSvc}
|
|
|
|
app.Get("/tenants/export", h.ExportTenantsCSV)
|
|
|
|
parentID := "parent-1"
|
|
tenants := []domain.Tenant{
|
|
{
|
|
ID: "t1",
|
|
Name: "Tenant A",
|
|
Type: domain.TenantTypeCompany,
|
|
ParentID: &parentID,
|
|
Slug: "tenant-a",
|
|
Description: "Primary tenant",
|
|
Config: domain.JSONMap{
|
|
"visibility": "internal",
|
|
"orgUnitType": "센터",
|
|
},
|
|
Domains: []domain.TenantDomain{
|
|
{Domain: "tenant-a.example.com"},
|
|
{Domain: "login.tenant-a.example.com"},
|
|
},
|
|
},
|
|
}
|
|
|
|
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "").Return(tenants, int64(1), nil)
|
|
|
|
req := httptest.NewRequest("GET", "/tenants/export?includeIds=true", nil)
|
|
resp, _ := app.Test(req)
|
|
body, _ := io.ReadAll(resp.Body)
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
assert.Contains(t, resp.Header.Get("Content-Disposition"), "tenants.csv")
|
|
assert.Equal(t, "text/csv", strings.Split(resp.Header.Get("Content-Type"), ";")[0])
|
|
assert.Contains(t, string(body), "tenant_id,name,type,parent_tenant_id,parent_tenant_slug,slug,memo,email_domain,visibility,org_unit_type,worksmobile_sync")
|
|
assert.Contains(t, string(body), "t1,Tenant A,COMPANY,parent-1,,tenant-a,Primary tenant,tenant-a.example.com;login.tenant-a.example.com,internal,센터,yes")
|
|
}
|
|
|
|
func TestTenantHandler_ExportTenantsCSV_OmitsIDsAndUsesParentSlug(t *testing.T) {
|
|
app := fiber.New()
|
|
mockSvc := new(MockTenantService)
|
|
h := &TenantHandler{Service: mockSvc}
|
|
|
|
app.Get("/tenants/export", h.ExportTenantsCSV)
|
|
|
|
parentID := "parent-1"
|
|
tenants := []domain.Tenant{
|
|
{
|
|
ID: parentID,
|
|
Name: "Parent Tenant",
|
|
Type: domain.TenantTypeCompanyGroup,
|
|
Slug: "parent-tenant",
|
|
},
|
|
{
|
|
ID: "child-1",
|
|
Name: "Child Tenant",
|
|
Type: domain.TenantTypeUserGroup,
|
|
ParentID: &parentID,
|
|
Slug: "child-tenant",
|
|
},
|
|
}
|
|
|
|
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "").Return(tenants, int64(2), nil)
|
|
|
|
req := httptest.NewRequest("GET", "/tenants/export?includeIds=false", nil)
|
|
resp, _ := app.Test(req)
|
|
body, _ := io.ReadAll(resp.Body)
|
|
text := string(body)
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
assert.Contains(t, text, "name,type,parent_tenant_slug,slug,memo,email_domain,visibility,org_unit_type,worksmobile_sync")
|
|
assert.Contains(t, text, "Child Tenant,USER_GROUP,parent-tenant,child-tenant,,")
|
|
assert.NotContains(t, text, "tenant_id")
|
|
assert.NotContains(t, text, "parent_tenant_id")
|
|
assert.NotContains(t, text, "child-1")
|
|
mockSvc.AssertExpectations(t)
|
|
}
|
|
|
|
func TestTenantHandler_ExportTenantsCSV_OrdersByInputOrder(t *testing.T) {
|
|
app := fiber.New()
|
|
mockSvc := new(MockTenantService)
|
|
h := &TenantHandler{Service: mockSvc}
|
|
|
|
app.Get("/tenants/export", h.ExportTenantsCSV)
|
|
|
|
oldest := time.Date(2026, 1, 2, 9, 0, 0, 0, time.UTC)
|
|
middle := oldest.Add(time.Hour)
|
|
newest := oldest.Add(2 * time.Hour)
|
|
tenants := []domain.Tenant{
|
|
{ID: "newest", Name: "Newest Tenant", Type: domain.TenantTypeCompany, Slug: "newest", CreatedAt: newest},
|
|
{ID: "middle", Name: "Middle Tenant", Type: domain.TenantTypeCompany, Slug: "middle", CreatedAt: middle},
|
|
{ID: "oldest", Name: "Oldest Tenant", Type: domain.TenantTypeCompany, Slug: "oldest", CreatedAt: oldest},
|
|
}
|
|
|
|
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "").Return(tenants, int64(len(tenants)), nil)
|
|
|
|
req := httptest.NewRequest("GET", "/tenants/export?includeIds=true", nil)
|
|
resp, _ := app.Test(req)
|
|
body, _ := io.ReadAll(resp.Body)
|
|
lines := strings.Split(strings.TrimSpace(string(body)), "\n")
|
|
|
|
require.Equal(t, http.StatusOK, resp.StatusCode)
|
|
require.Len(t, lines, 4)
|
|
assert.Contains(t, lines[1], "oldest,Oldest Tenant")
|
|
assert.Contains(t, lines[2], "middle,Middle Tenant")
|
|
assert.Contains(t, lines[3], "newest,Newest Tenant")
|
|
mockSvc.AssertExpectations(t)
|
|
}
|
|
|
|
func TestTenantHandler_ExportTenantsCSV_FiltersDescendantsByParentIDWithIDs(t *testing.T) {
|
|
app := fiber.New()
|
|
mockSvc := new(MockTenantService)
|
|
h := &TenantHandler{Service: mockSvc}
|
|
|
|
app.Get("/tenants/export", h.ExportTenantsCSV)
|
|
|
|
parentID := "11111111-2222-4333-8444-555555555555"
|
|
childID := "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee"
|
|
grandchildID := "bbbbbbbb-cccc-4ddd-8eee-ffffffffffff"
|
|
unrelatedID := "cccccccc-dddd-4eee-8fff-111111111111"
|
|
tenants := []domain.Tenant{
|
|
{
|
|
ID: parentID,
|
|
Name: "Parent Org",
|
|
Type: domain.TenantTypeCompany,
|
|
Slug: "parent-org",
|
|
},
|
|
{
|
|
ID: childID,
|
|
Name: "Child Org",
|
|
Type: domain.TenantTypeOrganization,
|
|
ParentID: &parentID,
|
|
Slug: "child-org",
|
|
},
|
|
{
|
|
ID: grandchildID,
|
|
Name: "Leaf Team",
|
|
Type: domain.TenantTypeUserGroup,
|
|
ParentID: &childID,
|
|
Slug: "leaf-team",
|
|
},
|
|
{
|
|
ID: unrelatedID,
|
|
Name: "Unrelated Org",
|
|
Type: domain.TenantTypeOrganization,
|
|
Slug: "unrelated-org",
|
|
},
|
|
}
|
|
|
|
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "").Return(tenants, int64(len(tenants)), nil)
|
|
|
|
req := httptest.NewRequest("GET", "/tenants/export?includeIds=true&parentId="+parentID, nil)
|
|
resp, _ := app.Test(req)
|
|
body, _ := io.ReadAll(resp.Body)
|
|
text := string(body)
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
assert.Contains(t, text, "tenant_id,name,type,parent_tenant_id,parent_tenant_slug,slug,memo,email_domain,visibility,org_unit_type,worksmobile_sync")
|
|
assert.Contains(t, text, childID+",Child Org,ORGANIZATION,"+parentID+",parent-org,child-org,")
|
|
assert.Contains(t, text, grandchildID+",Leaf Team,USER_GROUP,"+childID+",child-org,leaf-team,")
|
|
assert.NotContains(t, text, unrelatedID)
|
|
assert.NotContains(t, text, "Parent Org")
|
|
mockSvc.AssertExpectations(t)
|
|
}
|
|
|
|
func TestTenantHandler_ExportTenantsCSV_HidesPrivateSubtreeForUnauthorizedUser(t *testing.T) {
|
|
app := fiber.New()
|
|
mockSvc := new(MockTenantService)
|
|
h := &TenantHandler{Service: mockSvc}
|
|
|
|
parent := func(id string) *string { return &id }
|
|
tenants := []domain.Tenant{
|
|
{ID: "family", Type: domain.TenantTypeCompanyGroup, Name: "한맥가족", Slug: "hanmac-family"},
|
|
{ID: "company", Type: domain.TenantTypeCompany, ParentID: parent("family"), Name: "한맥", Slug: "hanmac"},
|
|
{ID: "public-team", Type: domain.TenantTypeUserGroup, ParentID: parent("company"), Name: "공개팀", Slug: "public-team"},
|
|
{ID: "private-team", Type: domain.TenantTypeUserGroup, ParentID: parent("company"), Name: "비공개팀", Slug: "private-team", Config: domain.JSONMap{"visibility": "private"}},
|
|
{ID: "private-child", Type: domain.TenantTypeUserGroup, ParentID: parent("private-team"), Name: "비공개하위", Slug: "private-child"},
|
|
}
|
|
|
|
app.Use(func(c *fiber.Ctx) error {
|
|
c.Locals("user_profile", &domain.UserProfileResponse{
|
|
ID: "user-1",
|
|
Role: domain.RoleTenantAdmin,
|
|
TenantID: parent("company"),
|
|
})
|
|
return c.Next()
|
|
})
|
|
app.Get("/tenants/export", h.ExportTenantsCSV)
|
|
|
|
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "").Return(tenants, int64(len(tenants)), nil).Once()
|
|
|
|
req := httptest.NewRequest("GET", "/tenants/export?includeIds=true", nil)
|
|
resp, _ := app.Test(req)
|
|
body, _ := io.ReadAll(resp.Body)
|
|
text := string(body)
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
assert.Contains(t, text, "public-team")
|
|
assert.NotContains(t, text, "private-team")
|
|
assert.NotContains(t, text, "private-child")
|
|
mockSvc.AssertExpectations(t)
|
|
}
|
|
|
|
func TestTenantHandler_ImportTenantsCSVCreatesTenant(t *testing.T) {
|
|
app := fiber.New()
|
|
mockSvc := new(MockTenantService)
|
|
h := &TenantHandler{Service: mockSvc}
|
|
|
|
app.Post("/tenants/import", h.ImportTenantsCSV)
|
|
|
|
var body bytes.Buffer
|
|
writer := multipart.NewWriter(&body)
|
|
part, err := writer.CreateFormFile("file", "tenants.csv")
|
|
assert.NoError(t, err)
|
|
_, err = part.Write([]byte("tenant_id,name,type,parent_tenant_id,slug,memo,email_domain\n,Imported Tenant,COMPANY,parent-1,imported-tenant,Imported memo,imported.example.com;login.imported.example.com\n"))
|
|
assert.NoError(t, err)
|
|
assert.NoError(t, writer.Close())
|
|
|
|
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "").Return([]domain.Tenant{}, int64(0), nil).Once()
|
|
mockSvc.On(
|
|
"RegisterTenant",
|
|
mock.Anything,
|
|
"Imported Tenant",
|
|
"imported-tenant",
|
|
domain.TenantTypeCompany,
|
|
"Imported memo",
|
|
[]string{"imported.example.com", "login.imported.example.com"},
|
|
mock.MatchedBy(func(parentID *string) bool {
|
|
return parentID != nil && *parentID == "parent-1"
|
|
}),
|
|
"",
|
|
).Return(&domain.Tenant{ID: "imported-1", Name: "Imported Tenant", Slug: "imported-tenant"}, nil)
|
|
|
|
req := httptest.NewRequest("POST", "/tenants/import", &body)
|
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
|
resp, _ := app.Test(req)
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
var got map[string]any
|
|
json.NewDecoder(resp.Body).Decode(&got)
|
|
assert.Equal(t, float64(1), got["created"])
|
|
assert.Equal(t, float64(0), got["updated"])
|
|
assert.Equal(t, float64(0), got["failed"])
|
|
mockSvc.AssertExpectations(t)
|
|
}
|
|
|
|
func TestTenantHandler_ImportTenantsCSVResolvesParentSlugToID(t *testing.T) {
|
|
app := fiber.New()
|
|
mockSvc := new(MockTenantService)
|
|
h := &TenantHandler{Service: mockSvc}
|
|
|
|
app.Post("/tenants/import", h.ImportTenantsCSV)
|
|
|
|
var body bytes.Buffer
|
|
writer := multipart.NewWriter(&body)
|
|
part, err := writer.CreateFormFile("file", "tenants.csv")
|
|
assert.NoError(t, err)
|
|
_, err = part.Write([]byte("name,type,parent_tenant_slug,slug,memo,email_domain\nParent Tenant,COMPANY,,parent-slug,,\nChild Tenant,ORGANIZATION,parent-slug,child-slug,,\n"))
|
|
assert.NoError(t, err)
|
|
assert.NoError(t, writer.Close())
|
|
|
|
parentID := "parent-id"
|
|
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "").Return([]domain.Tenant{}, int64(0), nil).Once()
|
|
mockSvc.On(
|
|
"RegisterTenant",
|
|
mock.Anything,
|
|
"Parent Tenant",
|
|
"parent-slug",
|
|
domain.TenantTypeCompany,
|
|
"",
|
|
[]string{},
|
|
(*string)(nil),
|
|
"",
|
|
).Return(&domain.Tenant{ID: parentID, Name: "Parent Tenant", Slug: "parent-slug"}, nil).Once()
|
|
mockSvc.On(
|
|
"RegisterTenant",
|
|
mock.Anything,
|
|
"Child Tenant",
|
|
"child-slug",
|
|
domain.TenantTypeOrganization,
|
|
"",
|
|
[]string{},
|
|
mock.MatchedBy(func(got *string) bool {
|
|
return got != nil && *got == parentID
|
|
}),
|
|
"",
|
|
).Return(&domain.Tenant{ID: "child-id", Name: "Child Tenant", Slug: "child-slug"}, nil).Once()
|
|
|
|
req := httptest.NewRequest("POST", "/tenants/import", &body)
|
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
|
resp, _ := app.Test(req)
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
var got map[string]any
|
|
json.NewDecoder(resp.Body).Decode(&got)
|
|
assert.Equal(t, float64(2), got["created"])
|
|
assert.Equal(t, float64(0), got["failed"])
|
|
mockSvc.AssertExpectations(t)
|
|
}
|
|
|
|
func TestTenantHandler_ImportTenantsCSVDoesNotAssignCreatorAsOrganizationMember(t *testing.T) {
|
|
app := fiber.New()
|
|
mockSvc := new(MockTenantService)
|
|
h := &TenantHandler{Service: mockSvc}
|
|
|
|
app.Use(func(c *fiber.Ctx) error {
|
|
c.Locals("user_profile", &domain.UserProfileResponse{ID: "system-admin-id"})
|
|
return c.Next()
|
|
})
|
|
app.Post("/tenants/import", h.ImportTenantsCSV)
|
|
|
|
var body bytes.Buffer
|
|
writer := multipart.NewWriter(&body)
|
|
part, err := writer.CreateFormFile("file", "tenants.csv")
|
|
assert.NoError(t, err)
|
|
_, err = part.Write([]byte("name,type,parent_tenant_id,slug,memo,email_domain\nImported Org,ORGANIZATION,parent-1,imported-org,,\n"))
|
|
assert.NoError(t, err)
|
|
assert.NoError(t, writer.Close())
|
|
|
|
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "").Return([]domain.Tenant{}, int64(0), nil).Once()
|
|
mockSvc.On(
|
|
"RegisterTenant",
|
|
mock.Anything,
|
|
"Imported Org",
|
|
"imported-org",
|
|
domain.TenantTypeOrganization,
|
|
"",
|
|
[]string{},
|
|
mock.MatchedBy(func(parentID *string) bool {
|
|
return parentID != nil && *parentID == "parent-1"
|
|
}),
|
|
"",
|
|
).Return(&domain.Tenant{ID: "imported-org-id", Name: "Imported Org", Slug: "imported-org"}, nil).Once()
|
|
|
|
req := httptest.NewRequest("POST", "/tenants/import", &body)
|
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
|
resp, _ := app.Test(req)
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
var got map[string]any
|
|
json.NewDecoder(resp.Body).Decode(&got)
|
|
assert.Equal(t, float64(1), got["created"])
|
|
assert.Equal(t, float64(0), got["failed"])
|
|
mockSvc.AssertExpectations(t)
|
|
}
|
|
|
|
func TestNormalizeTenantTypeAllowsOrganization(t *testing.T) {
|
|
assert.Equal(t, domain.TenantTypeOrganization, normalizeTenantType("organization"))
|
|
}
|
|
|
|
func TestTenantCSVAllowedDomainsRoundTrip(t *testing.T) {
|
|
records, err := parseTenantCSVRecords(strings.NewReader(
|
|
"name,type,parent_tenant_slug,slug,memo,email_domain,visibility,org_unit_type,worksmobile_sync\n" +
|
|
"Hanmac,COMPANY,,hanmac,,\"samaneng.com, hanmaceng.co.kr;login.hmac.kr\",internal,센터,no\n",
|
|
))
|
|
|
|
assert.NoError(t, err)
|
|
assert.Len(t, records, 1)
|
|
assert.Equal(t, []string{"samaneng.com", "hanmaceng.co.kr", "login.hmac.kr"}, records[0].Domains)
|
|
assert.Equal(t, "internal", records[0].Visibility)
|
|
assert.Equal(t, "센터", records[0].OrgUnitType)
|
|
assert.Equal(t, "no", records[0].WorksmobileSync)
|
|
config, err := tenantCSVRecordConfig(records[0])
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, true, config["worksmobileExcluded"])
|
|
}
|
|
|
|
func TestNormalizeTenantDomainInputsSplitsCommaAndWhitespace(t *testing.T) {
|
|
got := normalizeTenantDomainInputs([]string{
|
|
"samaneng.com, hanmaceng.co.kr",
|
|
" LOGIN.HMAC.KR\nportal.hmac.kr ",
|
|
"samaneng.com",
|
|
})
|
|
|
|
assert.Equal(t, []string{
|
|
"samaneng.com",
|
|
"hanmaceng.co.kr",
|
|
"login.hmac.kr",
|
|
"portal.hmac.kr",
|
|
}, got)
|
|
}
|
|
|
|
func TestNormalizeTenantConfigForcesIndexedForLoginIDFields(t *testing.T) {
|
|
config, err := normalizeTenantConfig(map[string]any{
|
|
"userSchema": []any{
|
|
map[string]any{
|
|
"key": "emp_no",
|
|
"label": "사번",
|
|
"type": "text",
|
|
"indexed": false,
|
|
"isLoginId": true,
|
|
"maxLength": 20,
|
|
},
|
|
},
|
|
})
|
|
|
|
assert.NoError(t, err)
|
|
fields, ok := config["userSchema"].([]any)
|
|
assert.True(t, ok)
|
|
assert.Len(t, fields, 1)
|
|
|
|
field, ok := fields[0].(map[string]any)
|
|
assert.True(t, ok)
|
|
assert.Equal(t, true, field["indexed"])
|
|
assert.Equal(t, true, field["isLoginId"])
|
|
assert.NotContains(t, field, "maxLength")
|
|
}
|
|
|
|
func TestNormalizeTenantConfigRejectsNonTextLoginIDFields(t *testing.T) {
|
|
_, err := normalizeTenantConfig(map[string]any{
|
|
"userSchema": []any{
|
|
map[string]any{
|
|
"key": "emp_no",
|
|
"type": "number",
|
|
"isLoginId": true,
|
|
},
|
|
},
|
|
})
|
|
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "login ID fields must be text")
|
|
}
|
|
|
|
func TestNormalizeTenantConfigAcceptsTenantVisibilityAndOrgUnitType(t *testing.T) {
|
|
config, err := normalizeTenantConfig(map[string]any{
|
|
"visibility": "internal",
|
|
"orgUnitType": "센터",
|
|
"worksmobileExcluded": true,
|
|
})
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "internal", config["visibility"])
|
|
assert.Equal(t, "센터", config["orgUnitType"])
|
|
assert.Equal(t, true, config["worksmobileExcluded"])
|
|
}
|
|
|
|
func TestNormalizeTenantConfigAcceptsTaskForceAndExecutiveOrgUnitTypes(t *testing.T) {
|
|
for _, orgUnitType := range []string{"TF", "TF팀", "임원직속"} {
|
|
t.Run(orgUnitType, func(t *testing.T) {
|
|
config, err := normalizeTenantConfig(map[string]any{
|
|
"orgUnitType": orgUnitType,
|
|
})
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, orgUnitType, config["orgUnitType"])
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNormalizeTenantConfigRejectsInvalidTenantVisibility(t *testing.T) {
|
|
_, err := normalizeTenantConfig(map[string]any{
|
|
"visibility": "secret",
|
|
})
|
|
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "visibility must be public, internal, or private")
|
|
}
|
|
|
|
func TestValidateTenantOrgConfigScopeRequiresHanmacFamilyDescendant(t *testing.T) {
|
|
hanmacFamily := domain.Tenant{ID: "family", Slug: "hanmac-family", Type: domain.TenantTypeCompanyGroup}
|
|
saman := domain.Tenant{ID: "saman", Slug: "saman", Type: domain.TenantTypeCompany, ParentID: &hanmacFamily.ID}
|
|
outsider := domain.Tenant{ID: "outsider", Slug: "outsider", Type: domain.TenantTypeCompany}
|
|
|
|
err := validateTenantOrgConfigScope(saman, []domain.Tenant{hanmacFamily, saman}, domain.JSONMap{
|
|
"visibility": "private",
|
|
"orgUnitType": "팀",
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
err = validateTenantOrgConfigScope(outsider, []domain.Tenant{hanmacFamily, saman, outsider}, domain.JSONMap{
|
|
"visibility": "private",
|
|
})
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "only hanmac-family descendants")
|
|
}
|
|
|
|
func TestFilterPublicTenantsExcludesInternalPrivateAndDescendants(t *testing.T) {
|
|
root := domain.Tenant{ID: "root", Slug: "hanmac-family"}
|
|
publicTenant := domain.Tenant{ID: "public", Slug: "public", ParentID: &root.ID}
|
|
internalTenant := domain.Tenant{ID: "internal", Slug: "internal", ParentID: &root.ID, Config: domain.JSONMap{"visibility": "internal"}}
|
|
privateTenant := domain.Tenant{ID: "private", Slug: "private", ParentID: &root.ID, Config: domain.JSONMap{"visibility": "private"}}
|
|
privateChild := domain.Tenant{ID: "private-child", Slug: "private-child", ParentID: &privateTenant.ID}
|
|
|
|
filtered := filterPublicTenants([]domain.Tenant{
|
|
root,
|
|
publicTenant,
|
|
internalTenant,
|
|
privateTenant,
|
|
privateChild,
|
|
})
|
|
|
|
assert.Equal(t, []domain.Tenant{root, publicTenant}, filtered)
|
|
}
|
|
|
|
func TestTenantHandler_ApproveTenant(t *testing.T) {
|
|
app := fiber.New()
|
|
mockSvc := new(MockTenantService)
|
|
h := &TenantHandler{Service: mockSvc}
|
|
|
|
app.Post("/tenants/:id/approve", h.ApproveTenant)
|
|
|
|
mockSvc.On("ApproveTenant", mock.Anything, "t1").Return(nil)
|
|
|
|
req := httptest.NewRequest("POST", "/tenants/t1/approve", nil)
|
|
resp, _ := app.Test(req)
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
}
|
|
|
|
func (m *MockTenantService) DeleteTenantsBulk(ctx context.Context, tenantIDs []string) error {
|
|
args := m.Called(ctx, tenantIDs)
|
|
return args.Error(0)
|
|
}
|
|
|
|
func (m *MockTenantService) ListJoinedTenants(ctx context.Context, userID string) ([]domain.Tenant, error) {
|
|
args := m.Called(ctx, userID)
|
|
if args.Get(0) != nil {
|
|
return args.Get(0).([]domain.Tenant), args.Error(1)
|
|
}
|
|
return nil, args.Error(1)
|
|
}
|