| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262 |
- 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 }
- );
- });
- });
- });
|