104 lines
5.5 KiB
TypeScript
104 lines
5.5 KiB
TypeScript
/** @jest-environment node */
|
|
|
|
import type { ConnectionPool } from 'mssql'
|
|
import {
|
|
fetchPerformanceSourceRows,
|
|
metricSourceForBrand,
|
|
readTmallStoreSourceRanges,
|
|
sourceSnapshotFilter,
|
|
type SourceMetricRow,
|
|
} from '@/lib/performance-conversion-source'
|
|
|
|
const request = { input: jest.fn(), query: jest.fn() }
|
|
const pool = { request: () => request } as unknown as ConnectionPool
|
|
const productId = '1076069062942'
|
|
function sourceRow(overrides = {}) {
|
|
return {
|
|
sourceRowId: '10', metricDate: '2026-10-20', productId, sourceBrand: '朴美家具旗舰店',
|
|
...overrides,
|
|
} as SourceMetricRow
|
|
}
|
|
|
|
describe('performance conversion source selection and SQL scope', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks()
|
|
request.input.mockReturnValue(request)
|
|
request.query.mockResolvedValue({ recordset: [] })
|
|
})
|
|
|
|
it.each([
|
|
['顾家', '顾家家居小件家具旗舰店'],
|
|
['索菲亚', '索菲亚实木风旗舰店'],
|
|
['朴美', '朴美家具旗舰店'],
|
|
] as const)('binds %s only to its verified store', async (brand, store) => {
|
|
await fetchPerformanceSourceRows(pool, metricSourceForBrand(brand), productId, '2026-09-01', '2026-10-20')
|
|
expect(request.input).toHaveBeenCalledWith('store', expect.anything(), store)
|
|
const query = request.query.mock.calls[0][0]
|
|
expect(query).toContain('FROM [rpt].[TmallStoreOverall] o')
|
|
expect(query).toContain('LEFT JOIN [rpt].[TmallStoreBusinessRoi] r')
|
|
expect(query).toContain('r.[店铺名称] = o.[店铺名称] AND r.[统计日期] = o.[统计日期]')
|
|
expect(query).toContain('LTRIM(RTRIM(r.[商品ID])) = LTRIM(RTRIM(o.[商品ID]))')
|
|
expect(query).toContain('o.[主图整体点击率]')
|
|
expect(query).toContain('o.[支付买家数]')
|
|
expect(query).toContain('r.[展现量]')
|
|
expect(query).toContain('r.[推广ROI]')
|
|
expect(query).not.toContain('PC端曝光量')
|
|
expect(query).not.toContain(store) // Store/product/date values are parameterized.
|
|
expect(query).not.toContain(productId)
|
|
})
|
|
|
|
it.each(['亿家达', '卓禾', '欧意朗'] as const)('leaves %s on the original source', async (brand) => {
|
|
const source = metricSourceForBrand(brand)
|
|
expect(source).toEqual({ kind: 'legacy' })
|
|
await fetchPerformanceSourceRows(pool, source, productId, '2026-09-01', '2026-10-20')
|
|
expect(request.query.mock.calls[0][0]).toContain('FROM [rpt].[TmallSrcBusinessRoi]')
|
|
expect(request.input).not.toHaveBeenCalledWith('store', expect.anything(), expect.anything())
|
|
})
|
|
|
|
it('scopes local snapshots by source and brand, without mixing legacy IDs', () => {
|
|
expect(sourceSnapshotFilter(metricSourceForBrand('朴美'))).toEqual({ sourceKey: { startsWith: 'tmall-store-v1:朴美:' } })
|
|
expect(sourceSnapshotFilter(metricSourceForBrand('索菲亚'))).toEqual({ sourceKey: { startsWith: 'tmall-store-v1:索菲亚:' } })
|
|
expect(sourceSnapshotFilter(metricSourceForBrand('亿家达'))).toEqual({ NOT: { sourceKey: { startsWith: 'tmall-store-v1:' } } })
|
|
})
|
|
|
|
it('refuses ambiguous same-store/product/date joins instead of multiplying totals', async () => {
|
|
request.query.mockResolvedValue({ recordset: [sourceRow(), sourceRow({ sourceRowId: '11' })] })
|
|
await expect(fetchPerformanceSourceRows(pool, metricSourceForBrand('朴美'), productId, '2026-10-01', '2026-10-20'))
|
|
.rejects.toThrow('重复记录')
|
|
})
|
|
|
|
it.each([
|
|
{ productId: '999999' }, { sourceBrand: '顾家家居小件家具旗舰店' },
|
|
{ metricDate: '2026-09-30' }, { metricDate: null },
|
|
])('rejects an unexpected join key or date: %j', async (overrides) => {
|
|
request.query.mockResolvedValue({ recordset: [sourceRow(overrides)] })
|
|
await expect(fetchPerformanceSourceRows(pool, metricSourceForBrand('朴美'), productId, '2026-10-01', '2026-10-20'))
|
|
.rejects.toThrow('关联键或日期无效')
|
|
})
|
|
|
|
it('does not silently truncate a result and then purge uncopied snapshots', async () => {
|
|
request.query.mockResolvedValue({ recordset: Array.from({ length: 5001 }, () => sourceRow()) })
|
|
await expect(fetchPerformanceSourceRows(pool, metricSourceForBrand('朴美'), productId, '2026-10-01', '2026-10-20'))
|
|
.rejects.toThrow('安全上限')
|
|
})
|
|
|
|
it('keeps the sources independent and leaves absent supplemental fields as null', async () => {
|
|
const row = sourceRow({ promotionSourceRowId: null, impressions: null, roi: null, clickRate: '0.02' })
|
|
request.query.mockResolvedValue({ recordset: [row] })
|
|
const rows = await fetchPerformanceSourceRows(pool, metricSourceForBrand('朴美'), productId, '2026-10-01', '2026-10-20')
|
|
expect(rows[0]).toMatchObject({ impressions: null, roi: null, clickRate: '0.02' })
|
|
})
|
|
|
|
it('returns retention boundaries and uses the slower view for correction progress', async () => {
|
|
request.query.mockResolvedValue({ recordset: [
|
|
{ sourceKind: 'overall', sourceStore: '朴美家具旗舰店', firstDate: '2026-09-20', lastDate: '2026-10-20' },
|
|
{ sourceKind: 'roi', sourceStore: '朴美家具旗舰店', firstDate: '2026-09-20', lastDate: '2026-10-18' },
|
|
{ sourceKind: 'overall', sourceStore: '索菲亚实木风旗舰店', firstDate: '2026-09-20', lastDate: '2026-10-20' },
|
|
] })
|
|
const ranges = await readTmallStoreSourceRanges(pool, '2026-10-22')
|
|
expect(ranges.get('朴美')).toEqual({ firstDate: '2026-09-20', lastDate: '2026-10-20', correctionThrough: '2026-10-18' })
|
|
expect(ranges.get('索菲亚')).toEqual({ firstDate: '2026-09-20', lastDate: '2026-10-20', correctionThrough: null })
|
|
expect(ranges.has('顾家')).toBe(false)
|
|
})
|
|
})
|