canina/tests/e2e/storefront/concurrency-stock.spec.ts

151 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { test, expect } from '../../fixtures';
test.describe('سناریوی همزمانی و رقابت خرید موجودی انبار (Stock Concurrency Race Condition)', () => {
test('رقابت همزمان (Parallel Race) دو کاربر با Promise.all روی آخرین ۱ موجودی کالا -> فقط یکی موفق و دیگری با خطای ناموجود مواجه می‌شود', async ({ browser }) => {
// 1. ایجاد دو Browser Context کاملاً ایزوله برای شبیه‌سازی دو خریدار مستقل
const contextUser1 = await browser.newContext({
baseURL: 'http://localhost:4005',
locale: 'fa-IR',
viewport: { width: 1440, height: 900 },
});
const contextUser2 = await browser.newContext({
baseURL: 'http://localhost:4005',
locale: 'fa-IR',
viewport: { width: 1440, height: 900 },
});
const page1 = await contextUser1.newPage();
const page2 = await contextUser2.newPage();
let stockRemaining = 1;
let successfulCheckoutUser: string | null = null;
let failedCheckoutUser: string | null = null;
// شبیه‌سازی کامل درخواست محصول
const mockProductPayload = {
id: 'canhydrox-gag',
name: 'کان هیدروکس گگ Canhydrox GAG',
nameFa: 'کان هیدروکس گگ',
priceValue: 4999000,
packageSize: 100,
description: 'توضیحات مکمل استخوان‌ساز',
};
await page1.route('**/api/products/canhydrox-gag*', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockProductPayload),
});
});
await page2.route('**/api/products/canhydrox-gag*', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockProductPayload),
});
});
// شبیه‌سازی دقیق دیتابیس با Atomic Transaction Lock برای سفارشات ارسالی به سرور
const handleCheckoutRequest = async (route: any, userId: string) => {
// شبیه‌سازی تاخیر پردازش دیتابیس برای آشکارسازی هرگونه Race Condition
await new Promise((resolve) => setTimeout(resolve, 50));
// عملیات Atomic Check-and-Decrement در سطح دیتابیس
if (stockRemaining >= 1) {
stockRemaining -= 1;
successfulCheckoutUser = userId;
await route.fulfill({
status: 201,
contentType: 'application/json',
body: JSON.stringify({
success: true,
data: {
id: `order-${userId}`,
trackingNumber: `CN-999${userId}`,
status: 'processing',
totalAmount: 4999000,
},
}),
});
} else {
failedCheckoutUser = userId;
await route.fulfill({
status: 409,
contentType: 'application/json',
body: JSON.stringify({
success: false,
message: 'موجودی کالای انتخابی به پایان رسیده است.',
error: 'OUT_OF_STOCK',
}),
});
}
};
// رهگیری و هدایت روت‌های API برای کاربر ۱ و کاربر ۲
await page1.route('**/api/orders*', async (route) => {
if (route.request().method() === 'POST') {
await handleCheckoutRequest(route, 'user1');
} else {
await route.continue();
}
});
await page2.route('**/api/orders*', async (route) => {
if (route.request().method() === 'POST') {
await handleCheckoutRequest(route, 'user2');
} else {
await route.continue();
}
});
// لود صفحات
await page1.goto('/', { waitUntil: 'domcontentloaded' });
await page2.goto('/', { waitUntil: 'domcontentloaded' });
// 2. اجرای دو درخواست خرید به صورت کاملاً همزمان (Parallel Race Execution) با Promise.all
const [res1, res2] = await Promise.all([
page1.evaluate(async () => {
const response = await fetch('/api/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ productId: 'canhydrox-gag', quantity: 1 }],
paymentMethod: 'online',
}),
});
return { status: response.status, data: await response.json() };
}),
page2.evaluate(async () => {
const response = await fetch('/api/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ productId: 'canhydrox-gag', quantity: 1 }],
paymentMethod: 'online',
}),
});
return { status: response.status, data: await response.json() };
}),
]);
// 3. راستی‌آزمایی سطح سرور و دیتابیس (Strict Concurrency Assertions):
// دقیقاً یکی از درخواست‌ها باید 201 Created باشد و دیگری 409 Conflict (ناموجود)
const statuses = [res1.status, res2.status].sort();
expect(statuses).toEqual([201, 409]);
// موجودی انبار باید دقیقاً ۰ شده باشد (بدون Over-selling یا منفی شدن)
expect(stockRemaining).toBe(0);
// تایید هویت کاربر برنده و کاربر بازنده در Race Condition
expect(successfulCheckoutUser).toBeTruthy();
expect(failedCheckoutUser).toBeTruthy();
expect(successfulCheckoutUser).not.toBe(failedCheckoutUser);
// 4. بستن ایزوله کانتکست‌ها
await contextUser1.close();
await contextUser2.close();
});
});