import { getMetadataArgsStorage } from 'typeorm'; import { CoolCommException } from '@cool-midway/core'; import { BaseEntity } from '../../../src/comm/entity/base'; import { TeamDomainEntity } from '../../../src/modules/team/entity/domain'; import { TeamDomainService } from '../../../src/modules/team/service/domain'; import { AdminTeamDomainController } from '../../../src/modules/team/controller/admin/domain'; import { SslCertMockInfo } from '../../../src/comm/types/omni'; describe('Point 22: 独立企业二级域名绑定向导与 SSL 证书检测 单元测试', () => { const metadata = getMetadataArgsStorage(); // ========================================== // 1. 实体定义与数据库字段映射测试 // ========================================== describe('1. 实体定义与数据库字段映射测试', () => { it('TeamDomainEntity 表名映射应为 team_domain 且继承 BaseEntity', () => { const table = metadata.tables.find(t => t.target === TeamDomainEntity); expect(table).toBeDefined(); expect(table?.name).toBe('team_domain'); expect(TeamDomainEntity.prototype instanceof BaseEntity).toBe(true); }); it('TeamDomainEntity 应映射核心列名与类型约束', () => { const columns = metadata.columns.filter(c => c.target === TeamDomainEntity); const customDomainCol = columns.find(c => c.propertyName === 'customDomain'); const sslStatusCol = columns.find(c => c.propertyName === 'sslStatus'); const cnameTargetCol = columns.find(c => c.propertyName === 'cnameTarget'); const sslExpireTimeCol = columns.find(c => c.propertyName === 'sslExpireTime'); expect(customDomainCol).toBeDefined(); expect(customDomainCol?.options.name || customDomainCol?.propertyName).toBe('custom_domain'); expect(sslStatusCol).toBeDefined(); expect(sslStatusCol?.options.name || sslStatusCol?.propertyName).toBe('ssl_status'); expect(cnameTargetCol).toBeDefined(); expect(cnameTargetCol?.options.name || cnameTargetCol?.propertyName).toBe('cname_target'); expect(sslExpireTimeCol).toBeDefined(); expect(sslExpireTimeCol?.options.name || sslExpireTimeCol?.propertyName).toBe('ssl_expire_time'); }); }); // ========================================== // 2. 域名格式正则严格校验 (validateDomainFormat) // ========================================== describe('2. 域名格式正则严格校验 (validateDomainFormat)', () => { let service: TeamDomainService; beforeEach(() => { service = new TeamDomainService(); }); it('合法企业独立二级与多级域名应通过校验', () => { const validDomains = [ 'card.mycompany.com', 'portal.omnicard.ai', 'sales.global-biz.co.uk', 'vip.vip-shop.cn', 'a.b.c.example.org', ]; for (const domain of validDomains) { const result = service.validateDomainFormat(domain); expect(result.valid).toBe(true); expect(result.reason).toBeUndefined(); } }); it('空值或纯空格域名应判定为非法', () => { expect(service.validateDomainFormat('').valid).toBe(false); expect(service.validateDomainFormat(' ').valid).toBe(false); }); it('拦截包含端口号的域名', () => { const result = service.validateDomainFormat('card.example.com:8080'); expect(result.valid).toBe(false); expect(result.reason).toContain('端口'); }); it('拦截带有 HTTP/HTTPS 协议前缀的输入', () => { expect(service.validateDomainFormat('http://card.example.com').valid).toBe(false); expect(service.validateDomainFormat('https://card.example.com').valid).toBe(false); expect(service.validateDomainFormat('card.example.com/').valid).toBe(false); }); it('拦截 IPv4 与 IPv6 地址输入', () => { const ipList = ['192.168.1.1', '10.0.0.1', '127.0.0.1', '::1', '2001:db8::1']; for (const ip of ipList) { const result = service.validateDomainFormat(ip); expect(result.valid).toBe(false); expect(result.reason).toMatch(/IP/i); } }); it('拦截内部保留域名及特定后缀', () => { const reservedList = [ 'localhost', 'card.localhost', 'service.local', 'api.internal', 'dev.lan', 'test.test', 'demo.example', 'mock.invalid', ]; for (const domain of reservedList) { const result = service.validateDomainFormat(domain); expect(result.valid).toBe(false); expect(result.reason).toContain('保留'); } }); it('拦截含有下划线、空格或特殊字符的非法域名', () => { const invalidChars = [ 'card_domain.com', 'my brand.com', '@portal.com', 'card#name.com', '-bad.com', 'bad-.com', '.bad.com', 'bad.com.', ]; for (const domain of invalidChars) { const result = service.validateDomainFormat(domain); expect(result.valid).toBe(false); } }); it('拦截未达到二级域名的单层主机名', () => { const result = service.validateDomainFormat('singlehostname'); expect(result.valid).toBe(false); }); }); // ========================================== // 3. DNS CNAME 记录核验 (verifyDnsRecord) // ========================================== describe('3. DNS CNAME 记录核验 (verifyDnsRecord)', () => { let service: TeamDomainService; beforeEach(() => { service = new TeamDomainService(); }); it('当 CNAME 记录匹配预期 targetCname 时返回 verified=true (支持大小写与尾部点自动归一化)', async () => { const res = await service.verifyDnsRecord('card.mycompany.com', 'cname.omnicard.ai', { mockRecords: ['cname.omnicard.ai.'], }); expect(res.verified).toBe(true); expect(res.targetCname).toBe('cname.omnicard.ai'); expect(res.records).toEqual(['cname.omnicard.ai.']); }); it('当解析结果为大写或混合且包含目标时匹配成功', async () => { const res = await service.verifyDnsRecord('card.mycompany.com', 'CNAME.OMNICARD.AI', { mockRecords: ['cname.omnicard.ai'], }); expect(res.verified).toBe(true); }); it('当 CNAME 记录不匹配时返回 verified=false', async () => { const res = await service.verifyDnsRecord('card.mycompany.com', 'cname.omnicard.ai', { mockRecords: ['wrong-cname.otherdomain.com'], }); expect(res.verified).toBe(false); expect(res.message).toContain('不匹配'); }); it('模拟或真实解析发生空记录或错误时友好降级为 verified=false', async () => { const res = await service.verifyDnsRecord('card.mycompany.com', 'cname.omnicard.ai', { mockRecords: [], }); expect(res.verified).toBe(false); expect(res.records).toEqual([]); }); }); // ========================================== // 4. SSL 证书检测与过期时间计算 (checkSslCertificate) // ========================================== describe('4. SSL 证书状态与过期时间计算 (checkSslCertificate)', () => { let service: TeamDomainService; beforeEach(() => { service = new TeamDomainService(); }); it('有效证书应计算准确的剩余天数并标记 status=active', async () => { const futureDate = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(); const pastDate = new Date(Date.now() - 10 * 24 * 60 * 60 * 1000).toISOString(); const mockCert: SslCertMockInfo = { issuer: "Let's Encrypt Authority X3", validFrom: pastDate, validTo: futureDate, authorized: true, }; const res = await service.checkSslCertificate('card.mycompany.com', 443, { mockCert }); expect(res.valid).toBe(true); expect(res.status).toBe('active'); expect(res.expired).toBe(false); expect(res.daysRemaining).toBeGreaterThanOrEqual(29); expect(res.issuer).toBe("Let's Encrypt Authority X3"); }); it('过期证书应标记 status=failed 且 expired=true', async () => { const pastDate = new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(); const olderDate = new Date(Date.now() - 95 * 24 * 60 * 60 * 1000).toISOString(); const mockCert: SslCertMockInfo = { issuer: 'DigiCert Global Root CA', validFrom: olderDate, validTo: pastDate, authorized: true, }; const res = await service.checkSslCertificate('card.mycompany.com', 443, { mockCert }); expect(res.valid).toBe(false); expect(res.status).toBe('failed'); expect(res.expired).toBe(true); expect(res.daysRemaining).toBeLessThanOrEqual(0); expect(res.errorMessage).toContain('过期'); }); it('未经授权/未受信任证书应标记 status=failed', async () => { const futureDate = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(); const mockCert: SslCertMockInfo = { issuer: 'Self-Signed Untrusted Root', validFrom: new Date().toISOString(), validTo: futureDate, authorized: false, }; const res = await service.checkSslCertificate('card.mycompany.com', 443, { mockCert }); expect(res.valid).toBe(false); expect(res.status).toBe('failed'); expect(res.errorMessage).toBeDefined(); }); }); // ========================================== // 5. 绑定域名持久化业务逻辑 (bindDomain) // ========================================== describe('5. 绑定域名持久化业务逻辑 (bindDomain)', () => { let service: TeamDomainService; let domains: TeamDomainEntity[]; let nextId = 1; beforeEach(() => { domains = []; nextId = 1; // 预置已被租户 88 绑定的域名 const existing = new TeamDomainEntity(); existing.id = nextId++; existing.tenantId = 88; existing.customDomain = 'taken.brand.com'; existing.sslStatus = 'active'; domains.push(existing); const mockRepo = { findOne: jest.fn(async (options: { where: Partial }) => { return ( domains.find(d => { if (options.where.customDomain && d.customDomain !== options.where.customDomain) { return false; } if (options.where.tenantId && d.tenantId !== options.where.tenantId) { return false; } return true; }) || null ); }), save: jest.fn(async (entity: TeamDomainEntity) => { const idx = domains.findIndex( d => (entity.id && d.id === entity.id) || d.customDomain === entity.customDomain ); if (idx >= 0) { domains[idx] = Object.assign(domains[idx], entity); return domains[idx]; } const saved = Object.assign(new TeamDomainEntity(), entity, { id: nextId++, createTime: new Date(), updateTime: new Date(), }); domains.push(saved); return saved; }), }; service = new TeamDomainService(); service.teamDomainEntity = mockRepo as unknown as TeamDomainService['teamDomainEntity']; }); it('缺少 tenantId 时抛出异常', async () => { await expect(service.bindDomain(0, 'card.company.com')).rejects.toThrow(CoolCommException); }); it('格式非法时阻断持久化并抛出异常', async () => { await expect(service.bindDomain(1, 'http://bad-url.com:8080')).rejects.toThrow( CoolCommException ); }); it('域名已被其他租户占用时抛出防冲突异常', async () => { await expect(service.bindDomain(99, 'taken.brand.com')).rejects.toThrow('已被其他企业绑定'); }); it('新租户首次绑定成功插入并保持 pending 状态', async () => { const bound = await service.bindDomain(99, 'VIP.NewCompany.com', { cnameTarget: 'cname.omnicard.ai', }); expect(bound.id).toBeDefined(); expect(bound.tenantId).toBe(99); expect(bound.customDomain).toBe('vip.newcompany.com'); expect(bound.sslStatus).toBe('pending'); expect(bound.cnameTarget).toBe('cname.omnicard.ai'); }); it('同一租户重复绑定或更新现有记录', async () => { const bound1 = await service.bindDomain(88, 'taken.brand.com', { cnameTarget: 'new-cname.omnicard.ai', }); expect(bound1.id).toBe(1); expect(bound1.cnameTarget).toBe('new-cname.omnicard.ai'); }); }); // ========================================== // 6. 控制器接口测试 (AdminTeamDomainController) // ========================================== describe('6. 控制器接口调用测试 (AdminTeamDomainController)', () => { let controller: AdminTeamDomainController; let service: TeamDomainService; beforeEach(() => { service = new TeamDomainService(); service.validateDomainFormat = jest.fn((domain: string) => ({ valid: domain === 'card.example.com', reason: domain === 'card.example.com' ? undefined : '域名格式非法', })); service.verifyDnsRecord = jest.fn(async (domain: string, targetCname: string) => ({ verified: true, records: [targetCname], targetCname, })); service.checkSslCertificate = jest.fn(async () => ({ valid: true, status: 'active' as const, daysRemaining: 60, })); service.bindDomain = jest.fn(async (tenantId: number, domain: string) => { const entity = new TeamDomainEntity(); entity.id = 1; entity.tenantId = tenantId; entity.customDomain = domain; entity.sslStatus = 'pending'; return entity; }); controller = new AdminTeamDomainController(); controller.teamDomainService = service; controller.ok = (data: unknown) => ({ code: 1000, message: 'success', data, }); ( controller as unknown as { ctx: { admin: { userId: number; tenantId?: number } } } ).ctx = { admin: { userId: 1, tenantId: 10 }, }; }); type MockApiResponse = { code: number; message: string; data: T }; it('POST /validate 返回校验结果', async () => { const res = (await controller.validate({ domain: 'card.example.com' })) as unknown as MockApiResponse<{ valid: boolean }>; expect(res.code).toBe(1000); expect(res.data.valid).toBe(true); }); it('POST /verify-dns 返回 DNS CNAME 核验结果', async () => { const res = (await controller.verifyDns({ domain: 'card.example.com', targetCname: 'cname.omnicard.ai', })) as unknown as MockApiResponse<{ verified: boolean }>; expect(res.code).toBe(1000); expect(res.data.verified).toBe(true); }); it('POST /check-ssl 返回 SSL 状态', async () => { const res = (await controller.checkSsl({ domain: 'card.example.com' })) as unknown as MockApiResponse<{ status: string }>; expect(res.code).toBe(1000); expect(res.data.status).toBe('active'); }); it('POST /bind 绑定企业独立二级域名', async () => { const res = await controller.bind({ domain: 'card.example.com' }); expect(res.code).toBe(1000); expect(service.bindDomain).toHaveBeenCalledWith(10, 'card.example.com', expect.any(Object)); }); }); });