feat: استيراد كود سيرو إلى تريبز (سيرو @ecfe7568) — بلا تعديل
قرار المالك 2026-07-27: باك إند سيرو PHP هو المعتمد، وتطبيقاته المجرّبة ميدانياً تحل محل إعادة البناء المؤرشفة. سيرو نفسه لم يُمسّ. الخريطة: backend · payment_server · loction_server · ride_server · passenger_server · docker · dashboard · stress_test → الجذر siro_rider → apps/rider siro_driver → apps/driver siro_admin → dashboards/admin siro_service → dashboards/service android_bot → apps/android_bot socialBot → apps/socialBot نُسخ المتعقَّب في git سيرو فقط عبر `git archive` (3,198 ملفاً / ~169 م.ب) لا `cp -r` — فاستُثنيت مخلفات البناء تلقائياً. بلا أي تعديل محتوى عمداً: كل ما يلي يصير فرقاً مقروءاً مقابل المصدر. لم يُستورد وسببه: siromove.com (الموقع التسويقي يبقى marketing/ في تريبز، سيرو فيه 8 ملفات) · docs و planning (تريبز له docs/ الخاص) · deploy.sh (ليس نشراً على سيرفر بل `git add . && git push origin --all` — فخّ في مستودع آخر) · transit_dashboard (بانتظار قرار مصير backend-transit و dashboards/transit-web). ⚠️ لا يبني بعد — ثلاثة نواقص متوقعة ومقصودة: 1. `.env` و `lib/env/env.g.dart` غير متعقَّبين في سيرو (أسرار لكل مستأجر): كل تطبيق فلاتر يحتاج .env خاصاً ثم توليد env.g.dart بـ build_runner. 2. إعدادات Firebase (9 ملفات google-services.json و GoogleService-Info.plist) يستبعدها .gitignore تريبز — ولكل مستأجر مشروع Firebase خاص أصلاً. 3. apps/driver في سيرو يشير إلى `../../Intaleq/packages/get` خارج المستودع → يجب ضمّ الحزم داخله أسوة بـ apps/rider. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
9909d9b4c1
commit
4d8414c96b
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
/**
|
||||
* generate_mock_data.php
|
||||
* Standalone JWT & Price Token generator for Stress Test.
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL & ~E_WARNING & ~E_NOTICE);
|
||||
ini_set('display_errors', '0');
|
||||
|
||||
$_SERVER['REQUEST_METHOD'] = $_SERVER['REQUEST_METHOD'] ?? 'POST';
|
||||
$_SERVER['HTTP_HOST'] = $_SERVER['HTTP_HOST'] ?? 'localhost';
|
||||
$_SERVER['REMOTE_ADDR'] = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
|
||||
|
||||
// Load bootstrap if available for EncryptionHelper
|
||||
$bootstrapPath = dirname(__DIR__) . '/backend/core/bootstrap.php';
|
||||
if (file_exists($bootstrapPath)) {
|
||||
try {
|
||||
require_once $bootstrapPath;
|
||||
} catch (Throwable $e) {}
|
||||
}
|
||||
|
||||
// Fallback .env loader
|
||||
$loadedFiles = [];
|
||||
function loadEnvFile(string $path): void {
|
||||
global $loadedFiles;
|
||||
if (!file_exists($path)) {
|
||||
$loadedFiles[] = "NOT_FOUND: $path";
|
||||
return;
|
||||
}
|
||||
$loadedFiles[] = "LOADED: $path";
|
||||
foreach (file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
||||
$line = trim($line);
|
||||
if (empty($line) || $line[0] === '#' || strpos($line, '=') === false) continue;
|
||||
[$name, $value] = explode('=', $line, 2);
|
||||
$val = trim($value, "\"'");
|
||||
putenv(trim($name) . '=' . $val);
|
||||
$_ENV[trim($name)] = $val;
|
||||
$_SERVER[trim($name)] = $val;
|
||||
}
|
||||
}
|
||||
|
||||
loadEnvFile(dirname(__DIR__) . '/docker/.env');
|
||||
loadEnvFile(dirname(__DIR__) . '/backend/.env');
|
||||
|
||||
function getJwtSecret(): string {
|
||||
$keyPath = getenv('JWT_SECRET_KEY_PATH');
|
||||
if ($keyPath && file_exists($keyPath)) {
|
||||
return trim(file_get_contents($keyPath));
|
||||
}
|
||||
$key = getenv('JWT_SECRET_KEY') ?: ($_ENV['JWT_SECRET_KEY'] ?? '');
|
||||
if ($key) return $key;
|
||||
$key = getenv('JWT_SECRET') ?: ($_ENV['JWT_SECRET'] ?? '');
|
||||
if ($key) return $key;
|
||||
return '';
|
||||
}
|
||||
|
||||
function base64url_encode(string $data): string {
|
||||
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
|
||||
}
|
||||
|
||||
function generate_jwt(string $secret, array $payload): string {
|
||||
$header = base64url_encode(json_encode(['alg' => 'HS256', 'typ' => 'JWT']));
|
||||
$body = base64url_encode(json_encode($payload));
|
||||
$sig = base64url_encode(hash_hmac('sha256', "$header.$body", $secret, true));
|
||||
return "$header.$body.$sig";
|
||||
}
|
||||
|
||||
$secret = getJwtSecret();
|
||||
if (empty($secret)) {
|
||||
echo json_encode(['error' => 'JWT secret not found in any .env', 'jwt' => '']);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
$userId = $argv[1] ?? '999999';
|
||||
$role = $argv[2] ?? 'driver';
|
||||
|
||||
$aud = ($role === 'driver') ? (getenv('allowedDriver2') ?: 'driver-app:ios') : (getenv('allowed2') ?: 'passenger-app:ios');
|
||||
$fpPepper = getenv('FP_PEPPER') ?: ($_ENV['FP_PEPPER'] ?? '');
|
||||
$fpHeader = 'mock_device_fp_' . $userId;
|
||||
$fpInToken = (!empty($fpPepper)) ? hash('sha256', $fpHeader . $fpPepper) : null;
|
||||
|
||||
$payload = [
|
||||
'iss' => getenv('APP_ISSUER') ?: 'SiroApp',
|
||||
'aud' => $aud,
|
||||
'user_id' => (string)$userId,
|
||||
'sub' => (string)$userId,
|
||||
'role' => $role,
|
||||
'token_type' => 'access',
|
||||
'jti' => bin2hex(random_bytes(16)),
|
||||
'iat' => time(),
|
||||
'exp' => time() + 86400,
|
||||
];
|
||||
if ($fpInToken) {
|
||||
$payload['fingerPrint'] = $fpInToken;
|
||||
}
|
||||
|
||||
$jwt = generate_jwt($secret, $payload);
|
||||
|
||||
// Generate encrypted price token for passenger matching add_ride.php
|
||||
$priceToken = '';
|
||||
if ($role === 'passenger') {
|
||||
$tokenData = json_encode([
|
||||
'passenger_id' => (string)$userId,
|
||||
'start_location' => '31.95,35.91',
|
||||
'end_location' => '31.96,35.92',
|
||||
'expires' => time() + 86400,
|
||||
'prices' => [
|
||||
'Economy' => [
|
||||
'price' => '5',
|
||||
'driver_price' => '4.5'
|
||||
]
|
||||
]
|
||||
]);
|
||||
if (isset($GLOBALS['encryptionHelper']) && is_object($GLOBALS['encryptionHelper'])) {
|
||||
$priceToken = $GLOBALS['encryptionHelper']->encryptData($tokenData);
|
||||
} elseif (isset($encryptionHelper) && is_object($encryptionHelper)) {
|
||||
$priceToken = $encryptionHelper->encryptData($tokenData);
|
||||
} else {
|
||||
try {
|
||||
$encKey = getenv('ENCRYPTION_KEY') ?: $secret;
|
||||
$keyBytes = substr(hash('sha256', $encKey, true), 0, 32);
|
||||
$iv = random_bytes(12);
|
||||
$tag = '';
|
||||
$ciphertext = openssl_encrypt($tokenData, 'aes-256-gcm', $keyBytes, OPENSSL_RAW_DATA, $iv, $tag);
|
||||
$priceToken = 'GCM:' . base64_encode($iv) . ':' . base64_encode($ciphertext . $tag);
|
||||
} catch (Throwable $e) {
|
||||
$priceToken = 'dummy';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'user_id' => $userId,
|
||||
'role' => $role,
|
||||
'jwt' => $jwt,
|
||||
'fp' => $fpHeader,
|
||||
'price_token' => $priceToken
|
||||
]);
|
||||
@@ -0,0 +1,392 @@
|
||||
const io = require('socket.io-client');
|
||||
const axios = require('axios');
|
||||
const { execSync } = require('child_process');
|
||||
const yargs = require('yargs/yargs');
|
||||
const { hideBin } = require('yargs/helpers');
|
||||
|
||||
const argv = yargs(hideBin(process.argv))
|
||||
.option('trips', {
|
||||
alias: 't',
|
||||
type: 'number',
|
||||
description: 'Number of concurrent trips to simulate',
|
||||
default: 1
|
||||
})
|
||||
.option('base-url', {
|
||||
alias: 'b',
|
||||
type: 'string',
|
||||
description: 'Base URL for backend APIs',
|
||||
default: 'http://localhost/backend'
|
||||
})
|
||||
.option('socket-url', {
|
||||
alias: 's',
|
||||
type: 'string',
|
||||
description: 'Location server socket URL',
|
||||
default: 'http://localhost:2020'
|
||||
})
|
||||
.option('debug', {
|
||||
alias: 'd',
|
||||
type: 'boolean',
|
||||
description: 'Enable debug logging',
|
||||
default: false
|
||||
})
|
||||
.argv;
|
||||
|
||||
function debug(msg) {
|
||||
if (argv.debug) console.log(` [DEBUG] ${msg}`);
|
||||
}
|
||||
|
||||
// Helper to get JWT and FP for mock user
|
||||
function getMockJwt(userId, role = 'driver') {
|
||||
try {
|
||||
const output = execSync(`php generate_mock_data.php ${userId} ${role}`, {
|
||||
timeout: 5000
|
||||
}).toString();
|
||||
debug(`JWT output for ${role} ${userId}: ${output.substring(0, 80)}...`);
|
||||
const jsonString = output.substring(output.indexOf('{'));
|
||||
const parsed = JSON.parse(jsonString);
|
||||
if (!parsed.jwt) {
|
||||
console.error(`[Error] JWT generation returned empty for ${role} ${userId}: ${output}`);
|
||||
return { jwt: '', fp: '' };
|
||||
}
|
||||
return {
|
||||
jwt: parsed.jwt,
|
||||
fp: parsed.fp || `mock_device_fp_${userId}`,
|
||||
price_token: parsed.price_token || ''
|
||||
};
|
||||
} catch (err) {
|
||||
console.error(`[Error] Failed to generate JWT for ${role} ${userId}:`, err.message);
|
||||
return { jwt: '', fp: '' };
|
||||
}
|
||||
}
|
||||
|
||||
let metrics = {
|
||||
successful: 0,
|
||||
failed: 0,
|
||||
connectionErrors: 0,
|
||||
apiErrors: 0,
|
||||
jwtErrors: 0,
|
||||
totalTime: 0
|
||||
};
|
||||
|
||||
async function simulateTrip(tripIndex) {
|
||||
const driverId = 900000 + tripIndex;
|
||||
const passengerId = 800000 + tripIndex;
|
||||
const driverAuth = getMockJwt(driverId, 'driver');
|
||||
const passengerAuth = getMockJwt(passengerId, 'passenger');
|
||||
|
||||
if (!driverAuth.jwt || !passengerAuth.jwt) {
|
||||
console.error(`[Trip ${tripIndex}] Skipping — JWT generation failed.`);
|
||||
metrics.jwtErrors++;
|
||||
metrics.failed++;
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(`[Trip ${tripIndex}] Starting... Driver: ${driverId}, Passenger: ${passengerId}`);
|
||||
const tripStartTime = Date.now();
|
||||
|
||||
// 1. Connect Driver to Socket
|
||||
const socketUrl = argv.socketUrl;
|
||||
debug(`Connecting to socket at: ${socketUrl}`);
|
||||
|
||||
const socket = io(socketUrl, {
|
||||
query: {
|
||||
driver_id: String(driverId),
|
||||
platform: 'android',
|
||||
jwt: driverAuth.jwt
|
||||
},
|
||||
reconnection: false,
|
||||
timeout: 10000,
|
||||
forceNew: true
|
||||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
let rideId = null;
|
||||
let resolved = false;
|
||||
|
||||
function finish(success) {
|
||||
if (resolved) return;
|
||||
resolved = true;
|
||||
try { socket.disconnect(); } catch(e) {}
|
||||
resolve(success);
|
||||
}
|
||||
|
||||
socket.on('connect_error', (err) => {
|
||||
console.error(`[Trip ${tripIndex}] ❌ Socket Connection Error: ${err.message}`);
|
||||
metrics.connectionErrors++;
|
||||
metrics.failed++;
|
||||
finish(false);
|
||||
});
|
||||
|
||||
socket.on('error', (err) => {
|
||||
console.error(`[Trip ${tripIndex}] ❌ Socket Error: ${err}`);
|
||||
metrics.connectionErrors++;
|
||||
metrics.failed++;
|
||||
finish(false);
|
||||
});
|
||||
|
||||
socket.on('connect', async () => {
|
||||
console.log(`[Trip ${tripIndex}] ✅ Driver connected to socket.`);
|
||||
|
||||
// Register driver location in Redis available pool
|
||||
socket.emit('update_location', {
|
||||
lat: 31.95,
|
||||
lng: 35.91,
|
||||
heading: 90,
|
||||
speed: 0,
|
||||
status: 'off'
|
||||
});
|
||||
|
||||
await new Promise(r => setTimeout(r, 750));
|
||||
|
||||
// 2. Passenger Requests Ride
|
||||
try {
|
||||
const addRideFormData = new URLSearchParams({
|
||||
passenger_id: String(passengerId),
|
||||
start_location: '31.95,35.91',
|
||||
end_location: '31.96,35.92',
|
||||
price: '5',
|
||||
price_token: passengerAuth.price_token || 'dummy',
|
||||
distance: '2',
|
||||
carType: 'Economy',
|
||||
passenger_name: 'Test Passenger',
|
||||
passenger_phone: '0790000000',
|
||||
start_name: 'Start',
|
||||
end_name: 'End'
|
||||
});
|
||||
|
||||
const reqStart = Date.now();
|
||||
const res = await axios.post(
|
||||
`${argv.baseUrl}/ride/rides/add_ride.php`,
|
||||
addRideFormData.toString(),
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Authorization': `Bearer ${passengerAuth.jwt}`,
|
||||
'X-Device-FP': passengerAuth.fp
|
||||
},
|
||||
timeout: 10000
|
||||
}
|
||||
);
|
||||
debug(`add_ride response: ${JSON.stringify(res.data).substring(0, 100)}`);
|
||||
console.log(`[Trip ${tripIndex}] Passenger requested ride. (API: ${Date.now() - reqStart}ms)`);
|
||||
} catch (err) {
|
||||
console.error(`[Trip ${tripIndex}] ❌ API Error (add_ride): ${err.message}`);
|
||||
if (err.response) debug(`Response: ${JSON.stringify(err.response.data).substring(0, 200)}`);
|
||||
metrics.apiErrors++;
|
||||
metrics.failed++;
|
||||
finish(false);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('market_new_ride', async (payload) => {
|
||||
rideId = payload.id || payload[0]?.id;
|
||||
console.log(`[Trip ${tripIndex}] 📨 Received ride offer #${rideId}. Accepting...`);
|
||||
|
||||
try {
|
||||
const acceptFormData = new URLSearchParams({
|
||||
id: String(rideId),
|
||||
driver_id: String(driverId),
|
||||
status: 'accepted'
|
||||
});
|
||||
|
||||
const acceptStart = Date.now();
|
||||
await axios.post(
|
||||
`${argv.baseUrl}/ride/rides/acceptRide.php`,
|
||||
acceptFormData.toString(),
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Authorization': `Bearer ${driverAuth.jwt}`,
|
||||
'X-Device-FP': driverAuth.fp
|
||||
},
|
||||
timeout: 10000
|
||||
}
|
||||
);
|
||||
console.log(`[Trip ${tripIndex}] Accepted ride #${rideId}. (API: ${Date.now() - acceptStart}ms)`);
|
||||
|
||||
// 4. Start Ride API Call
|
||||
try {
|
||||
const startFormData = new URLSearchParams({ id: String(rideId), driver_id: String(driverId), status: 'Begin' });
|
||||
await axios.post(
|
||||
`${argv.baseUrl}/ride/rides/start_ride.php`,
|
||||
startFormData.toString(),
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Authorization': `Bearer ${driverAuth.jwt}`,
|
||||
'X-Device-FP': driverAuth.fp
|
||||
},
|
||||
timeout: 10000
|
||||
}
|
||||
);
|
||||
console.log(`[Trip ${tripIndex}] 🚗 Started ride #${rideId}`);
|
||||
} catch (e) {
|
||||
debug(`start_ride error: ${e.message}`);
|
||||
}
|
||||
|
||||
// 5. Simulate Driving (4 Random GPS Location Updates)
|
||||
let currentLat = 31.95;
|
||||
let currentLng = 35.91;
|
||||
let steps = 0;
|
||||
|
||||
const driveInterval = setInterval(async () => {
|
||||
steps++;
|
||||
currentLat += 0.0005 + (Math.random() * 0.0005);
|
||||
currentLng += 0.0005 + (Math.random() * 0.0005);
|
||||
|
||||
socket.emit('update_location', {
|
||||
lat: currentLat,
|
||||
lng: currentLng,
|
||||
heading: Math.floor(Math.random() * 360),
|
||||
speed: 40 + Math.floor(Math.random() * 20),
|
||||
status: 'on'
|
||||
});
|
||||
|
||||
console.log(`[Trip ${tripIndex}] 📍 Driver GPS update #${steps}: (${currentLat.toFixed(4)}, ${currentLng.toFixed(4)})`);
|
||||
|
||||
if (steps >= 4) {
|
||||
clearInterval(driveInterval);
|
||||
|
||||
// 6. Finish Ride API Call
|
||||
try {
|
||||
const finishFormData = new URLSearchParams({
|
||||
rideId: String(rideId),
|
||||
driver_id: String(driverId),
|
||||
passengerId: String(passengerId),
|
||||
status: 'Finished',
|
||||
actualDistance: '2.5',
|
||||
actualDuration: '10',
|
||||
country_code: 'Jordan'
|
||||
});
|
||||
|
||||
await axios.post(
|
||||
`${argv.baseUrl}/ride/rides/finish_ride_updates.php`,
|
||||
finishFormData.toString(),
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Authorization': `Bearer ${driverAuth.jwt}`,
|
||||
'X-Device-FP': driverAuth.fp
|
||||
},
|
||||
timeout: 10000
|
||||
}
|
||||
);
|
||||
console.log(`[Trip ${tripIndex}] 🏁 Finished ride #${rideId} successfully.`);
|
||||
} catch (e) {
|
||||
debug(`finish_ride error: ${e.message}`);
|
||||
}
|
||||
|
||||
metrics.successful++;
|
||||
finish(true);
|
||||
}
|
||||
}, 500);
|
||||
|
||||
} catch (err) {
|
||||
console.error(`[Trip ${tripIndex}] ❌ API Error (acceptRide): ${err.message}`);
|
||||
metrics.apiErrors++;
|
||||
metrics.failed++;
|
||||
finish(false);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('disconnect', (reason) => {
|
||||
debug(`[Trip ${tripIndex}] Disconnected: ${reason}`);
|
||||
});
|
||||
|
||||
// Timeout fallback
|
||||
setTimeout(() => {
|
||||
if (!resolved) {
|
||||
console.error(`[Trip ${tripIndex}] ⏰ Timeout: No 'market_new_ride' received within 20s.`);
|
||||
metrics.failed++;
|
||||
finish(false);
|
||||
}
|
||||
}, 20000);
|
||||
});
|
||||
}
|
||||
|
||||
async function runTest() {
|
||||
console.log(`\n======================================================`);
|
||||
console.log(`🚀 Siro Stress Test`);
|
||||
console.log(`======================================================`);
|
||||
console.log(`📡 Backend API: ${argv.baseUrl}`);
|
||||
console.log(`🔌 Socket URL: ${argv.socketUrl}`);
|
||||
console.log(`🔢 Trips: ${argv.trips}`);
|
||||
console.log(`🐛 Debug: ${argv.debug ? 'ON' : 'OFF'}`);
|
||||
console.log(`------------------------------------------------------`);
|
||||
|
||||
console.log(`\n🔑 Pre-flight: Testing JWT generation...`);
|
||||
const testJwt = getMockJwt(999999, 'driver');
|
||||
if (!testJwt.jwt) {
|
||||
console.error(`❌ FATAL: Cannot generate JWT tokens.`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(` ✅ JWT generation works.`);
|
||||
|
||||
console.log(`\n🔌 Pre-flight: Testing socket connection...`);
|
||||
const testResult = await new Promise((resolve) => {
|
||||
const testSocket = io(argv.socketUrl, {
|
||||
query: { driver_id: '999999', platform: 'android', jwt: testJwt.jwt },
|
||||
reconnection: false,
|
||||
timeout: 5000,
|
||||
forceNew: true
|
||||
});
|
||||
testSocket.on('connect', () => {
|
||||
console.log(` ✅ Socket connection successful!`);
|
||||
testSocket.disconnect();
|
||||
resolve(true);
|
||||
});
|
||||
testSocket.on('connect_error', (err) => {
|
||||
console.error(` ❌ Socket connection failed: ${err.message}`);
|
||||
testSocket.disconnect();
|
||||
resolve(false);
|
||||
});
|
||||
testSocket.on('error', (err) => {
|
||||
console.error(` ❌ Socket error: ${err}`);
|
||||
testSocket.disconnect();
|
||||
resolve(false);
|
||||
});
|
||||
setTimeout(() => {
|
||||
testSocket.disconnect();
|
||||
resolve(false);
|
||||
}, 6000);
|
||||
});
|
||||
|
||||
if (!testResult) {
|
||||
console.error(`\n❌ Cannot proceed — socket server unreachable. Aborting.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`\n------------------------------------------------------`);
|
||||
console.log(`🏁 Starting ${argv.trips} concurrent trips...\n`);
|
||||
|
||||
const startTime = Date.now();
|
||||
const promises = [];
|
||||
|
||||
for (let i = 1; i <= argv.trips; i++) {
|
||||
promises.push(simulateTrip(i));
|
||||
await new Promise(r => setTimeout(r, 50));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
const durationSec = ((Date.now() - startTime) / 1000).toFixed(2);
|
||||
const tps = metrics.successful > 0 ? (metrics.successful / durationSec).toFixed(2) : '0.00';
|
||||
|
||||
console.log(`\n======================================================`);
|
||||
console.log(`📊 STRESS TEST RESULTS`);
|
||||
console.log(`======================================================`);
|
||||
console.log(`⏱️ Duration: ${durationSec}s`);
|
||||
console.log(`⚡ Throughput: ${tps} trips/sec`);
|
||||
console.log(`✅ Successful: ${metrics.successful} / ${argv.trips}`);
|
||||
console.log(`❌ Failed: ${metrics.failed} / ${argv.trips}`);
|
||||
if (metrics.failed > 0) {
|
||||
console.log(` ├─ JWT Errors: ${metrics.jwtErrors}`);
|
||||
console.log(` ├─ Socket Errors: ${metrics.connectionErrors}`);
|
||||
console.log(` └─ API Errors: ${metrics.apiErrors}`);
|
||||
}
|
||||
console.log(`======================================================\n`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
runTest();
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "siro-stress-test",
|
||||
"version": "1.0.0",
|
||||
"description": "Stress testing suite for Siro ride-hailing services",
|
||||
"main": "load_test.js",
|
||||
"scripts": {
|
||||
"test": "node load_test.js",
|
||||
"test:multi": "node load_test.js --trips=100"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.6.8",
|
||||
"socket.io-client": "^2.4.0",
|
||||
"uuid": "^9.0.1",
|
||||
"yargs": "^17.7.2",
|
||||
"dotenv": "^16.4.5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* rate_test.js — اختبار ضغط بمعدل مستمر (رحلات/ساعة) مع percentiles
|
||||
* ==================================================================
|
||||
* يكمل load_test.js (الذي يبقى لفحص الدخان بدفعة واحدة). الفروق:
|
||||
* 1. معدل وصول مستمر: --rate رحلة/ساعة طوال --duration دقيقة (مع ramp)
|
||||
* 2. توليد JWT داخل Node (HS256 عبر crypto) — لا exec لعملية PHP لكل رحلة
|
||||
* (كانت execSync تخنق مولّد الحمل نفسه فوق ~2 رحلة/ثانية)
|
||||
* 3. قياس p50/p95/p99 لكل نقطة نداء + زمن العرض + الرحلة كاملة
|
||||
*
|
||||
* أمثلة:
|
||||
* node rate_test.js --rate 10000 --duration 10 -b http://localhost:8080/backend -s http://localhost:2020
|
||||
* node rate_test.js --rate 20000 --duration 5 --env ../backend/.env
|
||||
*/
|
||||
|
||||
const io = require('socket.io-client');
|
||||
const axios = require('axios');
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const yargs = require('yargs/yargs');
|
||||
const { hideBin } = require('yargs/helpers');
|
||||
|
||||
const argv = yargs(hideBin(process.argv))
|
||||
.option('rate', { alias: 'r', type: 'number', default: 10000, description: 'رحلات في الساعة' })
|
||||
.option('duration', { alias: 'd', type: 'number', default: 10, description: 'مدة الاختبار بالدقائق' })
|
||||
.option('ramp', { type: 'number', default: 1, description: 'دقائق التصعيد التدريجي' })
|
||||
.option('base-url', { alias: 'b', type: 'string', default: 'http://localhost:8080/backend' })
|
||||
.option('socket-url', { alias: 's', type: 'string', default: 'http://localhost:2020' })
|
||||
.option('env', { type: 'string', default: path.join(__dirname, '..', 'backend', '.env'), description: 'ملف .env لقراءة سر JWT' })
|
||||
.option('jwt-secret', { type: 'string', description: 'تجاوز مباشر لسر JWT' })
|
||||
.argv;
|
||||
|
||||
// ── سر JWT: نفس أولويات generate_mock_data.php ──
|
||||
function loadSecret() {
|
||||
if (argv.jwtSecret) return argv.jwtSecret;
|
||||
const env = {};
|
||||
try {
|
||||
for (const line of fs.readFileSync(argv.env, 'utf8').split('\n')) {
|
||||
const t = line.trim();
|
||||
if (!t || t.startsWith('#') || !t.includes('=')) continue;
|
||||
const i = t.indexOf('=');
|
||||
env[t.slice(0, i).trim()] = t.slice(i + 1).trim().replace(/^["']|["']$/g, '');
|
||||
}
|
||||
} catch (e) { /* يسقط للفحص أدناه */ }
|
||||
if (env.JWT_SECRET_KEY_PATH && fs.existsSync(env.JWT_SECRET_KEY_PATH))
|
||||
return fs.readFileSync(env.JWT_SECRET_KEY_PATH, 'utf8').trim();
|
||||
return env.JWT_SECRET_KEY || env.JWT_SECRET || '';
|
||||
}
|
||||
|
||||
const b64u = (buf) => Buffer.from(buf).toString('base64url');
|
||||
function makeJwt(secret, driverId) {
|
||||
const header = b64u(JSON.stringify({ alg: 'HS256', typ: 'JWT' }));
|
||||
const body = b64u(JSON.stringify({ sub: String(driverId), role: 'driver', iat: Math.floor(Date.now() / 1000), exp: Math.floor(Date.now() / 1000) + 86400 }));
|
||||
const sig = crypto.createHmac('sha256', secret).update(`${header}.${body}`).digest('base64url');
|
||||
return `${header}.${body}.${sig}`;
|
||||
}
|
||||
|
||||
// ── مقاييس ──
|
||||
const lat = { socket_connect: [], add_ride: [], offer_delay: [], accept: [], start: [], finish: [], trip_e2e: [] };
|
||||
const counters = { launched: 0, ok: 0, fail: 0, socketErr: 0, apiErr: 0, timeout: 0 };
|
||||
let active = 0;
|
||||
|
||||
function pct(arr, p) {
|
||||
if (!arr.length) return 0;
|
||||
const s = [...arr].sort((a, b) => a - b);
|
||||
return s[Math.min(s.length - 1, Math.floor((p / 100) * s.length))];
|
||||
}
|
||||
const fmt = (a) => `p50=${pct(a, 50)}ms p95=${pct(a, 95)}ms p99=${pct(a, 99)}ms max=${Math.max(0, ...a)}ms (n=${a.length})`;
|
||||
|
||||
async function post(name, url, form) {
|
||||
const t0 = Date.now();
|
||||
try {
|
||||
const res = await axios.post(url, new URLSearchParams(form).toString(), {
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, timeout: 15000,
|
||||
});
|
||||
lat[name].push(Date.now() - t0);
|
||||
return res;
|
||||
} catch (e) {
|
||||
counters.apiErr++;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
function simulateTrip(i, secret) {
|
||||
const driverId = 900000 + i, passengerId = 800000 + i;
|
||||
const jwt = makeJwt(secret, driverId);
|
||||
const t0 = Date.now();
|
||||
active++;
|
||||
counters.launched++;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
let done = false, addRideAt = 0;
|
||||
const socket = io(argv.socketUrl, {
|
||||
query: { driver_id: String(driverId), platform: 'android', jwt },
|
||||
reconnection: false, timeout: 10000, forceNew: true,
|
||||
});
|
||||
const finish = (ok) => {
|
||||
if (done) return;
|
||||
done = true;
|
||||
ok ? counters.ok++ : counters.fail++;
|
||||
if (ok) lat.trip_e2e.push(Date.now() - t0);
|
||||
active--;
|
||||
try { socket.disconnect(); } catch (_) {}
|
||||
resolve(ok);
|
||||
};
|
||||
|
||||
socket.on('connect_error', () => { counters.socketErr++; finish(false); });
|
||||
socket.on('error', () => { counters.socketErr++; finish(false); });
|
||||
|
||||
socket.on('connect', async () => {
|
||||
lat.socket_connect.push(Date.now() - t0);
|
||||
try {
|
||||
addRideAt = Date.now();
|
||||
await post('add_ride', `${argv.baseUrl}/ride/rides/add_ride.php`, {
|
||||
passenger_id: String(passengerId),
|
||||
start_location: '31.95,35.91', end_location: '31.96,35.92',
|
||||
price: '5', price_token: 'dummy', distance: '2', carType: 'Economy',
|
||||
passenger_name: 'Test Passenger', passenger_phone: '0790000000',
|
||||
start_name: 'Start', end_name: 'End',
|
||||
});
|
||||
} catch (_) { finish(false); }
|
||||
});
|
||||
|
||||
socket.on('market_new_ride', async (payload) => {
|
||||
const rideId = payload.id || payload[0]?.id;
|
||||
if (!rideId) return finish(false);
|
||||
lat.offer_delay.push(Date.now() - addRideAt);
|
||||
try {
|
||||
await post('accept', `${argv.baseUrl}/ride/rides/acceptRide.php`, { id: String(rideId), driver_id: String(driverId), status: 'accepted' });
|
||||
let steps = 0;
|
||||
const iv = setInterval(async () => {
|
||||
steps++;
|
||||
socket.emit('update_location', { lat: 31.95 + steps * 0.001, lng: 35.91 + steps * 0.001, heading: 90, speed: 40, status: 'on' });
|
||||
try {
|
||||
if (steps === 3) await post('start', `${argv.baseUrl}/ride/rides/start_ride.php`, { id: String(rideId), driver_id: String(driverId), status: 'start' });
|
||||
if (steps === 6) {
|
||||
clearInterval(iv);
|
||||
await post('finish', `${argv.baseUrl}/ride/rides/finish_ride_updates.php`, { id: String(rideId), driver_id: String(driverId), status: 'finish' });
|
||||
finish(true);
|
||||
}
|
||||
} catch (_) { clearInterval(iv); finish(false); }
|
||||
}, 1000);
|
||||
} catch (_) { finish(false); }
|
||||
});
|
||||
|
||||
setTimeout(() => { if (!done) { counters.timeout++; finish(false); } }, 45000);
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const secret = loadSecret();
|
||||
if (!secret) {
|
||||
console.error(`❌ لا يوجد سر JWT — مرّر --jwt-secret أو --env لملف فيه JWT_SECRET_KEY (جرّبت: ${argv.env})`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const perSec = argv.rate / 3600;
|
||||
const totalMs = argv.duration * 60 * 1000;
|
||||
const rampMs = Math.min(argv.ramp * 60 * 1000, totalMs);
|
||||
console.log(`\n🚀 اختبار معدل مستمر: ${argv.rate} رحلة/ساعة (${perSec.toFixed(2)}/ث) لمدة ${argv.duration} دقيقة (تصعيد ${argv.ramp} د)`);
|
||||
console.log(`📡 API: ${argv.baseUrl}\n🔌 Socket: ${argv.socketUrl}\n`);
|
||||
|
||||
const t0 = Date.now();
|
||||
let i = 0;
|
||||
const inflight = [];
|
||||
|
||||
const progress = setInterval(() => {
|
||||
const el = ((Date.now() - t0) / 1000).toFixed(0);
|
||||
console.log(`[${el}s] أُطلق=${counters.launched} نجح=${counters.ok} فشل=${counters.fail} جارٍ=${active} | add_ride p95=${pct(lat.add_ride, 95)}ms عرض p95=${pct(lat.offer_delay, 95)}ms`);
|
||||
}, 10000);
|
||||
|
||||
// مجدول مصحح الانحراف: يحسب كم رحلة كان يجب أن تكون قد انطلقت ويطلق الفرق
|
||||
while (Date.now() - t0 < totalMs) {
|
||||
const el = Date.now() - t0;
|
||||
const rampFactor = el < rampMs ? el / rampMs : 1;
|
||||
const due = Math.floor((el / 1000) * perSec * rampFactor);
|
||||
while (i < due) { i++; inflight.push(simulateTrip(i, secret)); }
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
}
|
||||
|
||||
console.log(`\n⏳ توقف الإطلاق (${counters.launched} رحلة) — انتظار الرحلات الجارية (${active})…`);
|
||||
await Promise.allSettled(inflight);
|
||||
clearInterval(progress);
|
||||
|
||||
const durMin = ((Date.now() - t0) / 60000).toFixed(1);
|
||||
const achieved = Math.round((counters.ok / ((Date.now() - t0) / 3600000)));
|
||||
console.log(`\n${'='.repeat(60)}\n📊 النتائج (${durMin} دقيقة)\n${'='.repeat(60)}`);
|
||||
console.log(`أُطلق: ${counters.launched} | نجح: ${counters.ok} | فشل: ${counters.fail} (سوكيت=${counters.socketErr} api=${counters.apiErr} مهلة=${counters.timeout})`);
|
||||
console.log(`المعدل المحقق فعلياً: ~${achieved} رحلة/ساعة\n`);
|
||||
for (const [k, v] of Object.entries(lat)) console.log(` ${k.padEnd(15)} ${fmt(v)}`);
|
||||
console.log(`${'='.repeat(60)}`);
|
||||
const pass = counters.fail / Math.max(1, counters.launched) < 0.01 && pct(lat.add_ride, 95) < 500;
|
||||
console.log(pass ? '✅ الحكم: تحت العتبة (فشل <1% و p95<500ms)' : '⚠️ الحكم: فوق العتبة — انظر أبطأ نقطة أعلاه وسجل slow.log في MySQL');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user