import { describe, expect, it } from 'vitest'; import { Prisma, type PrismaClient } from '@prisma/client'; import { CONFIG_ACTIVE_VERSION_KEY } from '@lisglosips/redis'; import { publishPendingConfig } from './publisher.js'; class MemoryRedisMulti { constructor(private readonly writes: Array<{ command: string; key: string; value?: string }>) {} set(key: string, value: string) { this.writes.push({ command: 'set', key, value }); return this; } rpush(key: string, value: string) { this.writes.push({ command: 'rpush', key, value }); return this; } sadd(key: string, value: string) { this.writes.push({ command: 'sadd', key, value }); return this; } async exec() { return []; } } class MemoryRedis { writes: Array<{ command: string; key: string; value?: string }> = []; async get(key: string) { return key === CONFIG_ACTIVE_VERSION_KEY ? null : null; } multi() { return new MemoryRedisMulti(this.writes); } } function prismaFixture(): PrismaClient { const now = new Date('2026-06-24T03:20:00.000Z'); return { outboxEvent: { findMany: async () => [ { id: 'out_number_library', aggregateType: 'number_library_config', status: 'PENDING', availableAt: now, createdAt: now } ], updateMany: async () => ({ count: 1 }) }, $transaction: async (operations: Array>) => Promise.all(operations), customer: { findMany: async () => [ { id: 'cus_1', status: 'ENABLED', balance: new Prisma.Decimal('10'), creditLimit: new Prisma.Decimal('0'), minBalance: new Prisma.Decimal('0') } ] }, businessPrefix: { findMany: async () => [ { id: 'bp_1', prefix: '671', name: '业务671', priority: 10, status: 'ENABLED' } ] }, customerGateway: { findMany: async () => [ { id: 'cgw_1', customerId: 'cus_1', authMode: 'IP', sourceIp: '100.93.185.30', ips: [ { sourceIp: '100.93.185.30' }, { sourceIp: '100.93.185.31' } ], sipUsername: null, sipDomain: null, sipHa1: null, lineGroupId: 'lg_1', billingCycleSec: 6, cycleRate: new Prisma.Decimal('0.008'), callerMatchMode: 'PREFIXES', callerPrefixes: [{ prefix: '021' }], calleeMatchMode: 'BUSINESS_PREFIXES', businessPrefixes: [ { businessPrefix: { id: 'bp_1', prefix: '671', name: '业务671', priority: 10, status: 'ENABLED' } } ], status: 'ENABLED' } ] }, customerGatewayPolicy: { findMany: async () => [ { id: 'cgp_1', customerId: 'cus_1', gatewayId: 'cgw_1', lineGroupId: 'lg_1', priority: 1, callerMode: 'ANY', callerValue: null, calleeMode: 'ANY', calleeValue: null, status: 'ENABLED' } ] }, vendorGateway: { findMany: async () => [ { id: 'vgw_1', vendorId: 'ven_1', authMode: 'IP', host: '100.93.185.30', port: 50620, transport: 'udp', sipUsername: null, sipHa1: null, cpsLimit: 10, concurrencyLimit: 30, billingCycleSec: 6, cycleRate: new Prisma.Decimal('0.012'), landingCalleePrefix: '86', status: 'ENABLED', forbiddenPeriods: [], codecs: [], prefixRules: [], callerRewritePool: [ { caller: '02160010001', weight: 70, status: 'ENABLED' }, { caller: '02160010002', weight: 30, status: 'ENABLED' } ], blockedRegions: [{ regionScope: 'CITY', provinceCode: '340000', provinceName: '安徽省', cityCode: '340100', cityName: '合肥市' }] } ] }, landingLineGroup: { findMany: async () => [ { id: 'lg_1', name: '默认线路组', status: 'ENABLED', items: [{ id: 'lgi_1', vendorGatewayId: 'vgw_1', priority: 1, weight: 1, concurrencyCap: 30, status: 'ENABLED' }] } ] }, geoCity: { findMany: async () => [ { code: '340100', provinceCode: '340000', provinceName: '安徽省', cityCode: '340100', cityName: '合肥市', cityLevel: 'PREFECTURE', status: 'ENABLED' } ] }, phoneNumberSegment: { findMany: async () => [ { segment7: '1380013', cityCode: '340100', provinceName: '安徽省', cityName: '合肥市', carrier: 'MOBILE', city: { provinceCode: '340000' } } ] }, phoneAreaCode: { findMany: async () => [ { areaCode: '0551', cityCode: '340100', provinceName: '安徽省', cityName: '合肥市', city: { provinceCode: '340000' } } ] }, carrierPrefixRule: { findMany: async () => [ { prefix: '138', carrier: 'MOBILE', priority: 100 } ] } } as unknown as PrismaClient; } describe('S42 config publisher number library snapshot', () => { it('publishes number library lookup keys and gateway blocked-region sets', async () => { const redis = new MemoryRedis(); const now = new Date('2026-06-24T03:20:00.000Z'); const result = await publishPendingConfig(prismaFixture(), redis as never, now); const version = `${now.getTime()}`; expect(result.published).toBe(true); expect(result.manifest).toMatchObject({ schemaVersion: 2, businessPrefixCount: 1, customerGatewayIpCount: 2, customerGatewayBusinessPrefixCount: 1, customerGatewayCallerPrefixCount: 1, callerRewriteCount: 2, cityCount: 1, phoneSegmentCount: 1, areaCodeCount: 1, carrierPrefixRuleCount: 1, blockedRegionCount: 1 }); expect(redis.writes).toEqual( expect.arrayContaining([ expect.objectContaining({ command: 'set', key: `cfg:v:${version}:business_prefix:bp_1` }), expect.objectContaining({ command: 'set', key: `cfg:v:${version}:business_prefix_value:671`, value: 'bp_1' }), expect.objectContaining({ command: 'rpush', key: `cfg:v:${version}:business_prefixes` }), expect.objectContaining({ command: 'set', key: `cfg:v:${version}:auth:ip:100.93.185.30`, value: 'cgw_1' }), expect.objectContaining({ command: 'rpush', key: `cfg:v:${version}:auth:ip:100.93.185.31:gateways`, value: 'cgw_1' }), expect.objectContaining({ command: 'rpush', key: `cfg:v:${version}:customer_gateway:cgw_1:caller_prefixes`, value: '021' }), expect.objectContaining({ command: 'set', key: `cfg:v:${version}:customer_gateway:cgw_1:line_group`, value: 'lg_1' }), expect.objectContaining({ command: 'rpush', key: `cfg:v:${version}:vendor_gateway:vgw_1:caller_rewrite_pool` }), expect.objectContaining({ command: 'set', key: `cfg:v:${version}:phone_segment:1380013` }), expect.objectContaining({ command: 'set', key: `cfg:v:${version}:area_code:0551` }), expect.objectContaining({ command: 'set', key: `cfg:v:${version}:carrier_prefix:138` }), expect.objectContaining({ command: 'sadd', key: `cfg:v:${version}:vendor_gateway:vgw_1:blocked_city_codes`, value: '340100' }) ]) ); }); });