274 行
8.1 KiB
TypeScript
274 行
8.1 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|