import * as fc from 'fast-check'; /** * **Feature: backend-frontend-integration, Property 6: Refund Rejection Requires Reason** * **Validates: Requirements 4.4** * * *For any* refund rejection operation, the request SHALL be rejected * if the reason field is empty or contains only whitespace. */ /** * 模拟退款拒绝验证逻辑 * 这是一个纯函数,用于属性测试验证业务逻辑的正确性 */ function validateRejectReason(reason: string | null | undefined): { isValid: boolean; error?: string; } { // 验证拒绝理由非空 if (reason === null || reason === undefined) { return { isValid: false, error: '拒绝理由不能为空' }; } if (typeof reason !== 'string') { return { isValid: false, error: '拒绝理由必须是字符串' }; } if (reason.trim().length === 0) { return { isValid: false, error: '拒绝理由不能为空' }; } return { isValid: true }; } /** * 模拟退款拒绝操作 */ interface MockRefund { id: string; refundNo: string; status: 'Pending' | 'Approved' | 'Rejected' | 'Completed'; sellerId: string; } interface RejectRefundResult { success: boolean; refund?: MockRefund; error?: string; } /** * 纯函数:模拟退款拒绝逻辑 */ function simulateRejectRefund( refund: MockRefund, sellerId: string, reason: string | null | undefined ): RejectRefundResult { // 1. 验证拒绝理由 const reasonValidation = validateRejectReason(reason); if (!reasonValidation.isValid) { return { success: false, error: reasonValidation.error }; } // 2. 验证卖家权限 if (refund.sellerId !== sellerId) { return { success: false, error: '无权操作此退款单' }; } // 3. 验证退款状态 if (refund.status !== 'Pending') { return { success: false, error: `当前退款状态(${refund.status})不允许拒绝` }; } // 4. 执行拒绝操作 return { success: true, refund: { ...refund, status: 'Rejected' } }; } describe('RefundService Property Tests', () => { describe('Property 6: Refund Rejection Requires Reason', () => { it('空字符串作为拒绝理由时应该被拒绝', () => { fc.assert( fc.property( fc.uuid(), (sellerId) => { const refund: MockRefund = { id: 'test-id', refundNo: 'RF00000000000000000001', status: 'Pending', sellerId }; const result = simulateRejectRefund(refund, sellerId, ''); // 验证:空字符串应该导致失败 return result.success === false && result.error === '拒绝理由不能为空'; } ), { numRuns: 100 } ); }); it('仅包含空白字符的理由应该被拒绝', () => { fc.assert( fc.property( fc.uuid(), // 生成仅包含空白字符的字符串 fc.array(fc.constantFrom(' ', '\t', '\n', '\r'), { minLength: 1, maxLength: 20 }) .map(chars => chars.join('')), (sellerId, whitespaceReason) => { const refund: MockRefund = { id: 'test-id', refundNo: 'RF00000000000000000001', status: 'Pending', sellerId }; const result = simulateRejectRefund(refund, sellerId, whitespaceReason); // 验证:仅空白字符应该导致失败 return result.success === false && result.error === '拒绝理由不能为空'; } ), { numRuns: 100 } ); }); it('null 或 undefined 作为拒绝理由时应该被拒绝', () => { fc.assert( fc.property( fc.uuid(), fc.constantFrom(null as null, undefined as undefined), (sellerId, nullishReason) => { const refund: MockRefund = { id: 'test-id', refundNo: 'RF00000000000000000001', status: 'Pending', sellerId }; const result = simulateRejectRefund(refund, sellerId, nullishReason); // 验证:null/undefined 应该导致失败 return result.success === false && result.error === '拒绝理由不能为空'; } ), { numRuns: 100 } ); }); it('有效的非空理由应该允许拒绝操作', () => { fc.assert( fc.property( fc.uuid(), // 生成非空且不全是空白的字符串 fc.string({ minLength: 1, maxLength: 200 }).filter(s => s.trim().length > 0), (sellerId, validReason) => { const refund: MockRefund = { id: 'test-id', refundNo: 'RF00000000000000000001', status: 'Pending', sellerId }; const result = simulateRejectRefund(refund, sellerId, validReason); // 验证:有效理由应该成功 return result.success === true && result.refund?.status === 'Rejected'; } ), { numRuns: 100 } ); }); it('理由前后的空白应该被忽略,但中间内容有效则应该成功', () => { fc.assert( fc.property( fc.uuid(), // 生成带有前后空白的有效理由 fc.tuple( fc.array(fc.constantFrom(' ', '\t'), { minLength: 0, maxLength: 5 }).map(a => a.join('')), fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0), fc.array(fc.constantFrom(' ', '\t'), { minLength: 0, maxLength: 5 }).map(a => a.join('')) ).map(([prefix, content, suffix]) => prefix + content + suffix), (sellerId, reasonWithWhitespace) => { const refund: MockRefund = { id: 'test-id', refundNo: 'RF00000000000000000001', status: 'Pending', sellerId }; const result = simulateRejectRefund(refund, sellerId, reasonWithWhitespace); // 验证:有实际内容的理由应该成功 return result.success === true; } ), { numRuns: 100 } ); }); it('非 Pending 状态的退款单不能被拒绝', () => { fc.assert( fc.property( fc.uuid(), fc.constantFrom('Approved' as const, 'Rejected' as const, 'Completed' as const), fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0), (sellerId, nonPendingStatus, validReason) => { const refund: MockRefund = { id: 'test-id', refundNo: 'RF00000000000000000001', status: nonPendingStatus, sellerId }; const result = simulateRejectRefund(refund, sellerId, validReason); // 验证:非 Pending 状态应该失败 return result.success === false && result.error?.includes('不允许拒绝'); } ), { numRuns: 100 } ); }); it('非本店铺的退款单不能被拒绝', () => { fc.assert( fc.property( fc.uuid(), fc.uuid(), fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0), (refundSellerId, querySellerId, validReason) => { // 确保两个卖家ID不同 if (refundSellerId === querySellerId) return true; const refund: MockRefund = { id: 'test-id', refundNo: 'RF00000000000000000001', status: 'Pending', sellerId: refundSellerId }; const result = simulateRejectRefund(refund, querySellerId, validReason); // 验证:非本店铺应该失败 return result.success === false && result.error === '无权操作此退款单'; } ), { numRuns: 100 } ); }); }); });