151 lines
5.7 KiB
TypeScript
151 lines
5.7 KiB
TypeScript
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();
|
||
});
|
||
});
|