182 lines
5.8 KiB
TypeScript
182 lines
5.8 KiB
TypeScript
/**
|
|
* Property-based tests for order delete functionality
|
|
* **Feature: order-delete, Property 2: Deleted orders are excluded from list queries**
|
|
* **Validates: Requirements 1.3**
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest'
|
|
import * as fc from 'fast-check'
|
|
|
|
/**
|
|
* Order status constants
|
|
* 1: 待支付 (pending)
|
|
* 2: 已支付 (paid)
|
|
* 3: 已取消 (cancelled)
|
|
* 4: 已退款 (refunded)
|
|
*/
|
|
const ORDER_STATUS = {
|
|
PENDING: 1,
|
|
PAID: 2,
|
|
CANCELLED: 3,
|
|
REFUNDED: 4
|
|
} as const
|
|
|
|
/**
|
|
* Simulates the canDeleteOrder function from the frontend
|
|
* Orders with status 2 (paid) cannot be deleted
|
|
*/
|
|
function canDeleteOrder(status: number): boolean {
|
|
return status !== ORDER_STATUS.PAID
|
|
}
|
|
|
|
/**
|
|
* Simulates filtering orders that are not deleted
|
|
* This represents the backend behavior that the frontend relies on
|
|
*/
|
|
function filterNonDeletedOrders<T extends { isDeleted: boolean }>(orders: T[]): T[] {
|
|
return orders.filter(order => !order.isDeleted)
|
|
}
|
|
|
|
/**
|
|
* Generates a mock order for testing
|
|
*/
|
|
const orderArbitrary = fc.record({
|
|
orderId: fc.integer({ min: 1, max: 1000000 }),
|
|
orderNo: fc.string({ minLength: 10, maxLength: 20 }),
|
|
status: fc.constantFrom(ORDER_STATUS.PENDING, ORDER_STATUS.PAID, ORDER_STATUS.CANCELLED, ORDER_STATUS.REFUNDED),
|
|
isDeleted: fc.boolean(),
|
|
amount: fc.float({ min: Math.fround(0.01), max: Math.fround(10000), noNaN: true }),
|
|
})
|
|
|
|
describe('Order Delete Property Tests', () => {
|
|
/**
|
|
* Property 2: Deleted orders are excluded from list queries
|
|
* *For any* order query, the result SHALL NOT include orders where IsDeleted is true.
|
|
* **Validates: Requirements 1.3**
|
|
*/
|
|
describe('Property 2: Deleted orders are excluded from list queries', () => {
|
|
it('should never include deleted orders in filtered results', () => {
|
|
fc.assert(
|
|
fc.property(
|
|
fc.array(orderArbitrary, { minLength: 0, maxLength: 50 }),
|
|
(orders) => {
|
|
const filteredOrders = filterNonDeletedOrders(orders)
|
|
|
|
// Property: No order in the result should have isDeleted = true
|
|
return filteredOrders.every(order => order.isDeleted === false)
|
|
}
|
|
),
|
|
{ numRuns: 100 }
|
|
)
|
|
})
|
|
|
|
it('should include all non-deleted orders in filtered results', () => {
|
|
fc.assert(
|
|
fc.property(
|
|
fc.array(orderArbitrary, { minLength: 0, maxLength: 50 }),
|
|
(orders) => {
|
|
const filteredOrders = filterNonDeletedOrders(orders)
|
|
const nonDeletedOrders = orders.filter(o => !o.isDeleted)
|
|
|
|
// Property: The count of filtered orders should equal the count of non-deleted orders
|
|
return filteredOrders.length === nonDeletedOrders.length
|
|
}
|
|
),
|
|
{ numRuns: 100 }
|
|
)
|
|
})
|
|
|
|
it('should preserve order data integrity after filtering', () => {
|
|
fc.assert(
|
|
fc.property(
|
|
fc.array(orderArbitrary, { minLength: 1, maxLength: 50 }),
|
|
(orders) => {
|
|
const filteredOrders = filterNonDeletedOrders(orders)
|
|
|
|
// Property: Each filtered order should exist in the original list with same data
|
|
return filteredOrders.every(filtered =>
|
|
orders.some(original =>
|
|
original.orderId === filtered.orderId &&
|
|
original.orderNo === filtered.orderNo &&
|
|
original.status === filtered.status &&
|
|
original.amount === filtered.amount
|
|
)
|
|
)
|
|
}
|
|
),
|
|
{ numRuns: 100 }
|
|
)
|
|
})
|
|
|
|
it('should return empty array when all orders are deleted', () => {
|
|
fc.assert(
|
|
fc.property(
|
|
fc.array(
|
|
fc.record({
|
|
orderId: fc.integer({ min: 1, max: 1000000 }),
|
|
orderNo: fc.string({ minLength: 10, maxLength: 20 }),
|
|
status: fc.constantFrom(ORDER_STATUS.PENDING, ORDER_STATUS.PAID, ORDER_STATUS.CANCELLED, ORDER_STATUS.REFUNDED),
|
|
isDeleted: fc.constant(true), // All orders are deleted
|
|
amount: fc.float({ min: Math.fround(0.01), max: Math.fround(10000), noNaN: true }),
|
|
}),
|
|
{ minLength: 1, maxLength: 20 }
|
|
),
|
|
(allDeletedOrders) => {
|
|
const filteredOrders = filterNonDeletedOrders(allDeletedOrders)
|
|
|
|
// Property: When all orders are deleted, result should be empty
|
|
return filteredOrders.length === 0
|
|
}
|
|
),
|
|
{ numRuns: 100 }
|
|
)
|
|
})
|
|
})
|
|
|
|
/**
|
|
* Additional property: Order deletion eligibility based on status
|
|
* This validates the frontend's canDeleteOrder function
|
|
* **Validates: Requirements 2.1, 2.2**
|
|
*/
|
|
describe('Order deletion eligibility', () => {
|
|
it('should only allow deletion of non-paid orders', () => {
|
|
fc.assert(
|
|
fc.property(
|
|
fc.constantFrom(ORDER_STATUS.PENDING, ORDER_STATUS.PAID, ORDER_STATUS.CANCELLED, ORDER_STATUS.REFUNDED),
|
|
(status) => {
|
|
const canDelete = canDeleteOrder(status)
|
|
|
|
// Property: canDelete should be true if and only if status is not PAID
|
|
return canDelete === (status !== ORDER_STATUS.PAID)
|
|
}
|
|
),
|
|
{ numRuns: 100 }
|
|
)
|
|
})
|
|
|
|
it('should never allow deletion of paid orders', () => {
|
|
fc.assert(
|
|
fc.property(
|
|
fc.constant(ORDER_STATUS.PAID),
|
|
(status) => {
|
|
return canDeleteOrder(status) === false
|
|
}
|
|
),
|
|
{ numRuns: 100 }
|
|
)
|
|
})
|
|
|
|
it('should always allow deletion of pending, cancelled, and refunded orders', () => {
|
|
fc.assert(
|
|
fc.property(
|
|
fc.constantFrom(ORDER_STATUS.PENDING, ORDER_STATUS.CANCELLED, ORDER_STATUS.REFUNDED),
|
|
(status) => {
|
|
return canDeleteOrder(status) === true
|
|
}
|
|
),
|
|
{ numRuns: 100 }
|
|
)
|
|
})
|
|
})
|
|
})
|