| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398 |
- import * as fc from 'fast-check';
- /**
- * **Feature: backend-frontend-integration, Property 5: Cart Price Freshness**
- * **Validates: Requirements 5.2, 5.6**
- *
- * *For any* cart query, the returned product prices and stock levels SHALL reflect
- * the current values in the Product collection, not stale cached values.
- */
- // 模拟商品数据结构
- interface MockProduct {
- id: string;
- name: string;
- price: number;
- originalPrice?: number;
- stock: number;
- status: 'published' | 'draft' | 'archived';
- image: string;
- }
- // 模拟购物车项(存储的数据)
- interface MockCartItem {
- id: string;
- productId: string;
- quantity: number;
- addedAt: Date;
- // 添加时记录的价格(可能已过时)
- cachedPrice?: number;
- }
- // 模拟购物车项(包含实时商品数据)
- interface MockCartItemWithProduct {
- id: string;
- productId: string;
- quantity: number;
- addedAt: Date;
- product: {
- name: string;
- price: number;
- originalPrice?: number;
- image: string;
- stock: number;
- status: string;
- };
- priceChanged?: boolean;
- previousPrice?: number;
- stockIssue?: 'out_of_stock' | 'insufficient_stock';
- }
- /**
- * 纯函数:模拟获取购物车(包含实时商品数据)
- * 这是 CartService.getCart 的纯函数版本,用于属性测试
- */
- function simulateGetCart(
- cartItems: MockCartItem[],
- currentProducts: Map<string, MockProduct>
- ): MockCartItemWithProduct[] {
- const result: MockCartItemWithProduct[] = [];
- for (const item of cartItems) {
- const product = currentProducts.get(item.productId);
- if (!product) {
- // 商品已被删除
- result.push({
- id: item.id,
- productId: item.productId,
- quantity: item.quantity,
- addedAt: item.addedAt,
- product: {
- name: '商品已下架',
- price: 0,
- image: '',
- stock: 0,
- status: 'deleted'
- },
- stockIssue: 'out_of_stock'
- });
- continue;
- }
- // 使用当前商品数据(实时数据)
- const cartItemWithProduct: MockCartItemWithProduct = {
- id: item.id,
- productId: item.productId,
- quantity: item.quantity,
- addedAt: item.addedAt,
- product: {
- name: product.name,
- price: product.price,
- originalPrice: product.originalPrice,
- image: product.image,
- stock: product.stock,
- status: product.status
- }
- };
- // 检查价格变化
- if (item.cachedPrice !== undefined && item.cachedPrice !== product.price) {
- cartItemWithProduct.priceChanged = true;
- cartItemWithProduct.previousPrice = item.cachedPrice;
- }
- // 检查库存问题
- if (product.stock === 0) {
- cartItemWithProduct.stockIssue = 'out_of_stock';
- } else if (product.stock < item.quantity) {
- cartItemWithProduct.stockIssue = 'insufficient_stock';
- }
- // 检查商品状态
- if (product.status !== 'published') {
- cartItemWithProduct.stockIssue = 'out_of_stock';
- }
- result.push(cartItemWithProduct);
- }
- return result;
- }
- describe('CartService Property Tests', () => {
- // 生成有效商品的arbitrary
- const productArb: fc.Arbitrary<MockProduct> = fc.record({
- id: fc.uuid(),
- name: fc.string({ minLength: 1, maxLength: 50 }),
- price: fc.integer({ min: 1, max: 1000000 }).map(cents => cents / 100),
- originalPrice: fc.option(fc.integer({ min: 1, max: 1000000 }).map(cents => cents / 100), { nil: undefined }),
- stock: fc.integer({ min: 0, max: 1000 }),
- status: fc.constantFrom('published' as const, 'draft' as const, 'archived' as const),
- image: fc.webUrl()
- });
- describe('Property 5: Cart Price Freshness', () => {
- it('购物车返回的商品价格应与当前商品价格一致', () => {
- fc.assert(
- fc.property(
- // 生成1-5个商品
- fc.array(productArb, { minLength: 1, maxLength: 5 }),
- (products) => {
- // 创建商品映射
- const productMap = new Map<string, MockProduct>();
- products.forEach(p => productMap.set(p.id, p));
- // 为每个商品创建购物车项
- const cartItems: MockCartItem[] = products.map(p => ({
- id: `cart_item_${p.id}`,
- productId: p.id,
- quantity: Math.min(p.stock || 1, 5),
- addedAt: new Date(),
- cachedPrice: p.price * 0.9 // 模拟旧价格
- }));
- // 获取购物车
- const result = simulateGetCart(cartItems, productMap);
- // 验证:每个购物车项的商品价格应与当前商品价格一致
- for (const item of result) {
- const currentProduct = productMap.get(item.productId);
- if (currentProduct) {
- if (item.product.price !== currentProduct.price) {
- return false;
- }
- }
- }
- return true;
- }
- ),
- { numRuns: 100 }
- );
- });
- it('购物车返回的商品库存应与当前商品库存一致', () => {
- fc.assert(
- fc.property(
- fc.array(productArb, { minLength: 1, maxLength: 5 }),
- (products) => {
- const productMap = new Map<string, MockProduct>();
- products.forEach(p => productMap.set(p.id, p));
- const cartItems: MockCartItem[] = products.map(p => ({
- id: `cart_item_${p.id}`,
- productId: p.id,
- quantity: 1,
- addedAt: new Date()
- }));
- const result = simulateGetCart(cartItems, productMap);
- // 验证:每个购物车项的商品库存应与当前商品库存一致
- for (const item of result) {
- const currentProduct = productMap.get(item.productId);
- if (currentProduct) {
- if (item.product.stock !== currentProduct.stock) {
- return false;
- }
- }
- }
- return true;
- }
- ),
- { numRuns: 100 }
- );
- });
- it('商品价格变化时应正确标记priceChanged', () => {
- fc.assert(
- fc.property(
- fc.array(productArb.filter(p => p.status === 'published'), { minLength: 1, maxLength: 5 }),
- fc.integer({ min: 1, max: 100 }), // 价格变化百分比
- (products, priceChangePercent) => {
- const productMap = new Map<string, MockProduct>();
- products.forEach(p => productMap.set(p.id, p));
- // 创建购物车项,缓存的价格与当前价格不同
- const cartItems: MockCartItem[] = products.map(p => ({
- id: `cart_item_${p.id}`,
- productId: p.id,
- quantity: 1,
- addedAt: new Date(),
- cachedPrice: p.price * (1 + priceChangePercent / 100) // 缓存的是旧价格
- }));
- const result = simulateGetCart(cartItems, productMap);
- // 验证:如果缓存价格与当前价格不同,应标记priceChanged
- for (let i = 0; i < result.length; i++) {
- const item = result[i];
- const cartItem = cartItems[i];
- const currentProduct = productMap.get(item.productId);
-
- if (currentProduct && cartItem.cachedPrice !== undefined) {
- const pricesDiffer = cartItem.cachedPrice !== currentProduct.price;
- if (pricesDiffer && !item.priceChanged) {
- return false;
- }
- if (!pricesDiffer && item.priceChanged) {
- return false;
- }
- }
- }
- return true;
- }
- ),
- { numRuns: 100 }
- );
- });
- it('库存不足时应正确标记stockIssue', () => {
- fc.assert(
- fc.property(
- // 生成已发布且库存为0或较低的商品(排除下架商品以专注测试库存问题)
- fc.array(
- productArb
- .filter(p => p.status === 'published') // 只测试已发布的商品
- .map(p => ({ ...p, stock: Math.floor(Math.random() * 10) })),
- { minLength: 1, maxLength: 5 }
- ),
- (products) => {
- const productMap = new Map<string, MockProduct>();
- products.forEach(p => productMap.set(p.id, p));
- // 创建购物车项,数量可能超过库存
- const cartItems: MockCartItem[] = products.map(p => ({
- id: `cart_item_${p.id}`,
- productId: p.id,
- quantity: p.stock + 5, // 故意超过库存
- addedAt: new Date()
- }));
- const result = simulateGetCart(cartItems, productMap);
- // 验证:库存问题应正确标记
- for (let i = 0; i < result.length; i++) {
- const item = result[i];
- const currentProduct = productMap.get(item.productId);
-
- if (currentProduct) {
- if (currentProduct.stock === 0) {
- if (item.stockIssue !== 'out_of_stock') {
- return false;
- }
- } else if (currentProduct.stock < item.quantity) {
- if (item.stockIssue !== 'insufficient_stock') {
- return false;
- }
- }
- }
- }
- return true;
- }
- ),
- { numRuns: 100 }
- );
- });
- it('商品下架时应标记为out_of_stock', () => {
- fc.assert(
- fc.property(
- // 生成包含下架商品的列表
- fc.array(
- productArb.map(p => ({
- ...p,
- status: Math.random() > 0.5 ? 'published' as const : 'archived' as const
- })),
- { minLength: 1, maxLength: 5 }
- ),
- (products) => {
- const productMap = new Map<string, MockProduct>();
- products.forEach(p => productMap.set(p.id, p));
- const cartItems: MockCartItem[] = products.map(p => ({
- id: `cart_item_${p.id}`,
- productId: p.id,
- quantity: 1,
- addedAt: new Date()
- }));
- const result = simulateGetCart(cartItems, productMap);
- // 验证:下架商品应标记为out_of_stock
- for (const item of result) {
- const currentProduct = productMap.get(item.productId);
- if (currentProduct && currentProduct.status !== 'published') {
- if (item.stockIssue !== 'out_of_stock') {
- return false;
- }
- }
- }
- return true;
- }
- ),
- { numRuns: 100 }
- );
- });
- it('商品被删除时应返回默认值并标记为out_of_stock', () => {
- fc.assert(
- fc.property(
- fc.array(productArb, { minLength: 1, maxLength: 5 }),
- (products) => {
- // 只保留部分商品在映射中,模拟商品被删除
- const productMap = new Map<string, MockProduct>();
- products.slice(0, Math.ceil(products.length / 2)).forEach(p => productMap.set(p.id, p));
- // 为所有商品创建购物车项
- const cartItems: MockCartItem[] = products.map(p => ({
- id: `cart_item_${p.id}`,
- productId: p.id,
- quantity: 1,
- addedAt: new Date()
- }));
- const result = simulateGetCart(cartItems, productMap);
- // 验证:被删除的商品应返回默认值并标记为out_of_stock
- for (const item of result) {
- const currentProduct = productMap.get(item.productId);
- if (!currentProduct) {
- if (item.product.status !== 'deleted' || item.stockIssue !== 'out_of_stock') {
- return false;
- }
- }
- }
- return true;
- }
- ),
- { numRuns: 100 }
- );
- });
- it('购物车项数量应与输入一致', () => {
- fc.assert(
- fc.property(
- fc.array(productArb, { minLength: 0, maxLength: 10 }),
- (products) => {
- const productMap = new Map<string, MockProduct>();
- products.forEach(p => productMap.set(p.id, p));
- const cartItems: MockCartItem[] = products.map(p => ({
- id: `cart_item_${p.id}`,
- productId: p.id,
- quantity: 1,
- addedAt: new Date()
- }));
- const result = simulateGetCart(cartItems, productMap);
- // 验证:返回的购物车项数量应与输入一致
- return result.length === cartItems.length;
- }
- ),
- { numRuns: 100 }
- );
- });
- });
- });
|