import * as jwt from 'jsonwebtoken'; import { DEFAULT_TENANT_ID, TENANT_CONTEXT_KEY, getCardId, getTenantContext, getTenantId, isSuperTenant, setTenantContext, TenantContextInfo, } from '../../src/comm/context/tenant'; import { TenantMiddleware } from '../../src/comm/middleware/tenant'; import { CoolCommException } from '@cool-midway/core'; describe('多租户与组织架构上下文模块 (Tenant Context & Middleware)', () => { const mockJwtSecret = 'omnicard-secret-2026'; describe('1. 租户上下文工具函数 (comm/context/tenant.ts)', () => { it('空上下文或未设置时应安全返回 undefined 或 false', () => { const emptyCtx: any = {}; expect(getTenantId(emptyCtx)).toBeUndefined(); expect(getCardId(emptyCtx)).toBeUndefined(); expect(getTenantContext(emptyCtx)).toBeUndefined(); expect(isSuperTenant(emptyCtx)).toBe(false); expect(getTenantId(undefined as any)).toBeUndefined(); expect(getCardId(undefined as any)).toBeUndefined(); expect(getTenantContext(undefined as any)).toBeUndefined(); expect(isSuperTenant(undefined as any)).toBe(false); }); it('setTenantContext 应正确绑定上下文并与 ctx.admin / ctx.user 同步', () => { const ctx: any = { admin: { userId: 10, username: 'tester' }, user: { userId: 20 }, }; const tenantInfo: TenantContextInfo = { tenantId: 888, cardId: 999, userId: 10, isSuper: false, role: 'tenant_admin', }; setTenantContext(ctx, tenantInfo); expect(getTenantContext(ctx)).toEqual(tenantInfo); expect(ctx.tenant).toEqual(tenantInfo); expect(ctx[TENANT_CONTEXT_KEY]).toEqual(tenantInfo); expect(getTenantId(ctx)).toBe(888); expect(getCardId(ctx)).toBe(999); expect(isSuperTenant(ctx)).toBe(false); // 同步验证对 Cool-Admin 原生字段的兼容性 expect(ctx.admin.tenantId).toBe(888); expect(ctx.user.tenantId).toBe(888); }); it('当仅存在原生 ctx.admin.tenantId 或 ctx.user.tenantId 时,getTenantId 能够平滑兜底读取', () => { const adminCtx: any = { admin: { tenantId: 300 } }; expect(getTenantId(adminCtx)).toBe(300); const userCtx: any = { user: { tenantId: 'tenant_abc' } }; expect(getTenantId(userCtx)).toBe('tenant_abc'); }); it('isSuperTenant 应能准确识别超级管理员状态', () => { // 1. isSuper 显式为 true const superCtx1: any = {}; setTenantContext(superCtx1, { tenantId: 99, isSuper: true }); expect(isSuperTenant(superCtx1)).toBe(true); // 2. tenantId 为 DEFAULT_TENANT_ID (1) 或 'default' const superCtx2: any = {}; setTenantContext(superCtx2, { tenantId: DEFAULT_TENANT_ID }); expect(isSuperTenant(superCtx2)).toBe(true); const superCtx3: any = {}; setTenantContext(superCtx3, { tenantId: 'default' }); expect(isSuperTenant(superCtx3)).toBe(true); // 3. admin 用户名为 'admin' const superCtx4: any = { admin: { username: 'admin' } }; expect(isSuperTenant(superCtx4)).toBe(true); }); }); describe('2. 多租户拦截中间件 (comm/middleware/tenant.ts)', () => { let middleware: TenantMiddleware; beforeEach(() => { middleware = new TenantMiddleware(); middleware.prefix = ''; middleware.tenantConfig = { enable: true, strict: true, defaultTenantId: DEFAULT_TENANT_ID, ignoreUrls: ['/api/public/'], }; }); it('应能从 HTTP Header (x-tenant-id 与 x-card-id) 中解析并注入上下文', async () => { const ctx: any = { url: '/admin/crm/lead/page', headers: { 'x-tenant-id': '10086', 'x-card-id': '2001', }, get(key: string) { return this.headers[key.toLowerCase()]; }, }; let nextCalled = false; const next = async () => { nextCalled = true; }; const handler = middleware.resolve(); await handler(ctx, next); expect(nextCalled).toBe(true); expect(getTenantId(ctx)).toBe(10086); expect(getCardId(ctx)).toBe(2001); expect(ctx.tenant.tenantId).toBe(10086); }); it('应能从 JWT Authorization 头解析 tenantId 与 cardId', async () => { const token = jwt.sign( { userId: 101, tenantId: 666, cardId: 777, username: 'john' }, mockJwtSecret ); const ctx: any = { url: '/admin/crm/card/info', headers: { authorization: `Bearer ${token}`, }, get(key: string) { return this.headers[key.toLowerCase()]; }, }; let nextCalled = false; const next = async () => { nextCalled = true; }; const handler = middleware.resolve(); await handler(ctx, next); expect(nextCalled).toBe(true); expect(getTenantId(ctx)).toBe(666); expect(getCardId(ctx)).toBe(777); expect(ctx.tenant.userId).toBe(101); }); it('当 Header 与 JWT 同时存在时,Header 优先级高于 JWT', async () => { const token = jwt.sign( { userId: 101, tenantId: 666, cardId: 777 }, mockJwtSecret ); const ctx: any = { url: '/admin/crm/card/info', headers: { 'x-tenant-id': '9999', 'x-card-id': '8888', authorization: `Bearer ${token}`, }, get(key: string) { return this.headers[key.toLowerCase()]; }, }; const handler = middleware.resolve(); await handler(ctx, async () => {}); expect(getTenantId(ctx)).toBe(9999); expect(getCardId(ctx)).toBe(8888); }); it('针对免校验开放接口 (如 /admin/base/open/login),即便无租户信息也应直接放行', async () => { const ctx: any = { url: '/admin/base/open/login?type=pwd', headers: {}, get(key: string) { return this.headers[key.toLowerCase()]; }, }; let nextCalled = false; const next = async () => { nextCalled = true; }; const handler = middleware.resolve(); await handler(ctx, next); expect(nextCalled).toBe(true); expect(getTenantId(ctx)).toBeUndefined(); }); it('受保护路由在 strict=true 模式且无租户上下文时应抛出 400 异常', async () => { const ctx: any = { url: '/admin/crm/lead/page', headers: {}, get(key: string) { return this.headers[key.toLowerCase()]; }, }; const handler = middleware.resolve(); await expect(handler(ctx, async () => {})).rejects.toThrow(CoolCommException); expect(ctx.status).toBe(400); }); it('受保护路由在 strict=false 模式时应提供默认兜底租户 ID', async () => { middleware.tenantConfig.strict = false; middleware.tenantConfig.defaultTenantId = 1; const ctx: any = { url: '/admin/crm/lead/page', headers: {}, get(key: string) { return this.headers[key.toLowerCase()]; }, }; let nextCalled = false; const next = async () => { nextCalled = true; }; const handler = middleware.resolve(); await handler(ctx, next); expect(nextCalled).toBe(true); expect(getTenantId(ctx)).toBe(1); }); it('超级管理员访问受保护路由时,即便缺失显式 Header 也自动兜底为平台超级租户', async () => { const superAdminToken = jwt.sign( { userId: 1, username: 'admin' }, mockJwtSecret ); const ctx: any = { url: '/admin/crm/lead/page', headers: { authorization: `Bearer ${superAdminToken}`, }, get(key: string) { return this.headers[key.toLowerCase()]; }, }; let nextCalled = false; const next = async () => { nextCalled = true; }; const handler = middleware.resolve(); await handler(ctx, next); expect(nextCalled).toBe(true); expect(getTenantId(ctx)).toBe(DEFAULT_TENANT_ID); expect(isSuperTenant(ctx)).toBe(true); }); }); });