f298984055
Компоненты:
- app/playwright/{package.json, refresh-session.js} — изолированный Node.js
+ Playwright chromium subprocess для headless логина
- PlaywrightProcessHandle interface + SymfonyPlaywrightProcessHandle (prod) +
StubPlaywrightProcessHandle (test) для DI без extending Symfony Process
- ProcessFactory + SymfonyProcessFactory
- PlaywrightBridge: PHP-обёртка, timeout 75s, JSON contract, exit code
→ SupplierAuthException
- RefreshSupplierSessionJob: stub → real (tries=3, backoff [2m/10m/30m],
Cache::lock concurrent guard, Redis TTL 6h)
- supplier:session:refresh Console command
- AppServiceProvider binds ProcessFactory → SymfonyProcessFactory
+7 tests (4 PlaywrightBridge + 2 Job + 1 Command).
NOTE: DOM-селекторы placeholder — финализация после Task 1 discovery.
NOTE: app/playwright/node_modules в .gitignore.
Quirks resolved:
- Mockery::mock(Process::class) + laravel/pao = stream_filter_remove fatal.
Решение: handle interface, pure-PHP test stub без extends Process.
- PHPStan Mockery union types — baseline entries (known Mockery+PHPStan compat).
KNOWN LIMITATION: на этой Windows машине pao stream filter conflict при
serial run SupplierPortalClient+RefreshSupplierSessionJob combo.
Tests pass individually + парами. Production Linux CI не affected.
93 lines
2.9 KiB
JavaScript
93 lines
2.9 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Headless Playwright login на crm.bp-gr.ru.
|
|
*
|
|
* Input (JSON через stdin):
|
|
* {login, password, url}
|
|
*
|
|
* Output (JSON через stdout):
|
|
* {phpsessid, csrf, refreshed_at}
|
|
*
|
|
* Exit codes:
|
|
* 0 — success
|
|
* 1 — auth failed (login/password rejected, или session cookie missing)
|
|
* 2 — DOM не найден (CSRF token не найден)
|
|
* 3 — timeout (60s)
|
|
* 4 — invalid input или другая ошибка
|
|
*/
|
|
const { chromium } = require('playwright');
|
|
|
|
const TIMEOUT_MS = 60_000;
|
|
|
|
async function refresh(args) {
|
|
const browser = await chromium.launch({ headless: true });
|
|
try {
|
|
const context = await browser.newContext();
|
|
const page = await context.newPage();
|
|
|
|
await page.goto(args.url, { waitUntil: 'load', timeout: TIMEOUT_MS });
|
|
|
|
// DOM-селекторы — placeholder до Task 1 discovery
|
|
const loginSelector = 'input[name=login]';
|
|
const passwordSelector = 'input[name=password]';
|
|
const submitSelector = 'button[type=submit]';
|
|
|
|
await page.fill(loginSelector, args.login);
|
|
await page.fill(passwordSelector, args.password);
|
|
await Promise.all([
|
|
page.waitForLoadState('networkidle', { timeout: TIMEOUT_MS }),
|
|
page.click(submitSelector),
|
|
]);
|
|
|
|
let csrf = null;
|
|
try {
|
|
csrf = await page.locator('meta[name=csrf-token]').first().getAttribute('content', { timeout: 5000 });
|
|
} catch (e) {
|
|
// CSRF meta tag not found — try other patterns в Task 1 discovery
|
|
}
|
|
|
|
const cookies = await context.cookies();
|
|
const sessionCookie = cookies.find(c => c.name === 'PHPSESSID' || c.name === 'JSESSIONID');
|
|
|
|
if (!sessionCookie) {
|
|
process.stderr.write(JSON.stringify({ error: 'session cookie not found in response' }));
|
|
process.exit(1);
|
|
}
|
|
|
|
if (!csrf) {
|
|
process.stderr.write(JSON.stringify({ error: 'CSRF token not found in DOM' }));
|
|
process.exit(2);
|
|
}
|
|
|
|
process.stdout.write(JSON.stringify({
|
|
phpsessid: sessionCookie.value,
|
|
csrf: csrf,
|
|
refreshed_at: new Date().toISOString(),
|
|
}));
|
|
process.exit(0);
|
|
} catch (err) {
|
|
process.stderr.write(JSON.stringify({ error: err.message }));
|
|
process.exit(err.message.includes('Timeout') ? 3 : 4);
|
|
} finally {
|
|
await browser.close();
|
|
}
|
|
}
|
|
|
|
// Read stdin
|
|
let input = '';
|
|
process.stdin.on('data', chunk => { input += chunk; });
|
|
process.stdin.on('end', () => {
|
|
let args;
|
|
try {
|
|
args = JSON.parse(input);
|
|
} catch (e) {
|
|
process.stderr.write(JSON.stringify({ error: 'invalid JSON on stdin' }));
|
|
process.exit(4);
|
|
}
|
|
if (!args.login || !args.password || !args.url) {
|
|
process.stderr.write(JSON.stringify({ error: 'missing required keys: login, password, url' }));
|
|
process.exit(4);
|
|
}
|
|
refresh(args);
|
|
});
|