From 32a92131a1481a8dbce0a0cb927b3e82181c3be0 Mon Sep 17 00:00:00 2001 From: Jonathan Danse Date: Thu, 8 Oct 2026 08:56:02 +0200 Subject: [PATCH 1/7] =?UTF-8?q?docs(spec):=20une=20seule=20r=C3=A8gle=20de?= =?UTF-8?q?=20r=C3=A9solution=20des=20URL=20du=20back-office?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../specs/2026-10-08-bo-url-resolve-design.md | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 docs/superpowers/specs/2026-10-08-bo-url-resolve-design.md diff --git a/docs/superpowers/specs/2026-10-08-bo-url-resolve-design.md b/docs/superpowers/specs/2026-10-08-bo-url-resolve-design.md new file mode 100644 index 0000000..58015f6 --- /dev/null +++ b/docs/superpowers/specs/2026-10-08-bo-url-resolve-design.md @@ -0,0 +1,87 @@ +# Une seule règle de résolution des URL du back-office : design + +Date : 2026-10-08 +Statut : validé, à implémenter + +Fait suite à `2026-10-07-suite-urls-design.md` (PR #123, release #124). + +## Problème + +La lib complète une URL du back-office relative de deux façons : + +- **`TestsSuite::loadGlobals()`** (toutes les suites, CLI et CI) colle `PRESTAFLOW_BO_URL`, ou le défaut `admin-dev/`, derrière `PRESTAFLOW_FO_URL`. Toute valeur qui ne commence pas par `http://` ou `https://` en minuscules est prise pour un chemin. +- **`VisualTestsSuite::resolveBackOfficeUrl()`** (propriété `$backOfficeUrl`, puis `$shopUrl` seule) applique la règle de l'app, `App\Support\BackOfficeUrl::resolve()`. + +Le même `PRESTAFLOW_BO_URL` peut donc viser un autre back-office selon le chemin : + +| Valeur | `loadGlobals()` aujourd'hui | Règle de l'app | +|---|---|---| +| `/admin` (FO `https://x/`) | `https://x//admin/` | `https://x/admin/` | +| `admin-dev/` (FO `https://x/?lang=2`) | `https://x/?lang=2/admin-dev/` | `https://x/admin-dev/` | +| `//autre/admin` | `https://x///autre/admin/` | `https://autre/admin/` | +| `HTTPS://x/admin` | `https://x/HTTPS://x/admin/` | `HTTPS://x/admin/` | + +## Décisions + +| Sujet | Décision | +|---|---| +| Portée | Une seule règle de **résolution** pour `loadGlobals()` et les suites visuelles. | +| Refus | Aucun nouveau refus pour les variables d'environnement : une valeur exotique, même avec des identifiants, est résolue sans erreur. Les refus restent propres aux propriétés `$shopUrl` / `$backOfficeUrl` du fichier de suite. | +| Emplacement | Nouvel utilitaire `PrestaFlow\Library\Utils\BackOfficeUrl`, à côté de `Env`. | +| Compatibilité | `VisualTestsSuite::resolveBackOfficeUrl()` reste publique, avec la même signature, et délègue à l'utilitaire. | + +## Design + +### `PrestaFlow\Library\Utils\BackOfficeUrl` + +```php +final class BackOfficeUrl +{ + public static function resolve(string $backOffice, string $frontOffice): string; + public static function isRelative(string $backOffice): bool; +} +``` + +- `resolve()` reprend sans changement le corps actuel de `VisualTestsSuite::resolveBackOfficeUrl()` : + - les deux valeurs sont nettoyées des espaces en bordure ; + - une URL absolue `http(s)`, schéma insensible à la casse, est gardée telle quelle ; + - une URL `//hôte/…` prend le schéma de l'URL FO ; + - un autre chemin s'ajoute à l'URL FO, privée de sa requête et de son fragment, en gardant son chemin, avec un seul `/` entre les deux ; + - le résultat se termine toujours par `/`. +- `isRelative()` renvoie vrai si la valeur dépend de l'URL FO, autrement dit si elle n'est ni absolue `http(s)` ni `//hôte`. Une valeur vide après nettoyage n'est pas relative. + +### `TestsSuite::loadGlobals()` + +- `PRESTAFLOW_FO_URL` ne change pas : il reçoit seulement un `/` final. +- L'URL BO vaut `PRESTAFLOW_BO_URL`, ou `admin-dev/` si la variable est absente. Elle passe par `BackOfficeUrl::resolve($bo, $frontOfficeUrl)`. +- `$backOfficeRelative` vaut la valeur brute nettoyée quand `BackOfficeUrl::isRelative()` est vrai, sinon `null`. Avec `$shopUrl` seule, un `//hôte` ou une URL absolue ne suit donc plus l'URL FO du fichier. C'est cohérent : leur hôte ne dépend pas de la FO. +- `PRESTAFLOW_BO_URL` défini mais vide : l'URL FO elle-même, comme aujourd'hui, avec un `/` final. Ce cas n'est pas relatif. + +### `VisualTestsSuite` + +- `resolveBackOfficeUrl()` devient `return BackOfficeUrl::resolve($backOffice, $frontOffice);`, avec un docblock qui renvoie à l'utilitaire. +- `applySuiteUrls()`, `assertSuiteUrl()` et `isSafeRelativePath()` ne changent pas. + +## Tests + +- **Unitaires `BackOfficeUrl`** : + - le jeu de cas de `resolve()` aujourd'hui dans `VisualTestsSuiteUrlsTest` (13 cas repris de `BackOfficeUrlTest` de l'app) est déplacé dans `tests/Unit/Utils/BackOfficeUrlTest.php` ; + - `isRelative()` est vrai pour `admin/`, `/admin` et `admin-dev/`, faux pour `http://x/a`, `HTTPS://x/a`, `//x/a` et `''`. +- **`loadGlobals()`**, via des variables d'environnement, sans Chrome : chaque ligne du tableau « Problème », le défaut `admin-dev/`, une URL absolue gardée, et `$backOfficeRelative` (relatif, `//hôte`, absolu, défaut). +- `VisualTestsSuiteUrlsTest` reste vert : `resolveBackOfficeUrl()` délègue, et le comportement de `$shopUrl` / `$backOfficeUrl` ne change pas. +- Suite complète verte en PHP 8.4 et 8.1. + +## Documentation + +README : ajouter sous la ligne qui mentionne `PRESTAFLOW_BO_URL` une phrase qui renvoie à la règle commune. Exemple : « A relative `PRESTAFLOW_BO_URL` follows the same rule as `$backOfficeUrl` below. » La règle elle-même reste décrite une seule fois, dans la section « Shop and back-office URLs in the suite file ». + +## Livraison + +1. PR vers `dev`, puis release `dev` → `main`, en commits de merge, sur accord de l'utilisateur. +2. App : bump de `prestaflow/php-library`, `composer.lock` seul. Aucun autre changement dans l'app. + +## Hors périmètre + +- Valider ou refuser les variables d'environnement. +- `PRESTAFLOW_FO_URL` : requête, fragment ou schéma. +- `App\Support\BackOfficeUrl` côté app, qui reste la référence de la règle. From efb9084b1c12f675a162e8cc17f1b103dd39d533 Mon Sep 17 00:00:00 2001 From: Jonathan Danse Date: Thu, 8 Oct 2026 09:08:47 +0200 Subject: [PATCH 2/7] =?UTF-8?q?docs(plan):=20une=20seule=20r=C3=A8gle=20de?= =?UTF-8?q?=20r=C3=A9solution=20des=20URL=20du=20back-office?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../plans/2026-10-08-bo-url-resolve.md | 441 ++++++++++++++++++ .../2026-10-08-bo-url-resolve.md.tasks.json | 30 ++ 2 files changed, 471 insertions(+) create mode 100644 docs/superpowers/plans/2026-10-08-bo-url-resolve.md create mode 100644 docs/superpowers/plans/2026-10-08-bo-url-resolve.md.tasks.json diff --git a/docs/superpowers/plans/2026-10-08-bo-url-resolve.md b/docs/superpowers/plans/2026-10-08-bo-url-resolve.md new file mode 100644 index 0000000..70ddef5 --- /dev/null +++ b/docs/superpowers/plans/2026-10-08-bo-url-resolve.md @@ -0,0 +1,441 @@ +# Une seule règle de résolution des URL du back-office : plan d'implémentation + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:subagent-driven-development (recommended) or superpowers-extended-cc:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** `TestsSuite::loadGlobals()` résout un `PRESTAFLOW_BO_URL` relatif avec la même règle que les suites visuelles et que l'app : un utilitaire `BackOfficeUrl`, aucun nouveau refus. + +**Architecture :** +- Nouvel utilitaire statique pur `PrestaFlow\Library\Utils\BackOfficeUrl`, avec deux méthodes : + - `resolve()` : corps actuel de `VisualTestsSuite::resolveBackOfficeUrl()` ; + - `isRelative()`. +- `VisualTestsSuite::resolveBackOfficeUrl()` délègue à `resolve()`. +- `loadGlobals()` appelle les deux méthodes de l'utilitaire. + +**Tech Stack :** PHP 8.1+, PHPUnit 10 (PHP 8.4 et 8.1). + +**Spec :** `docs/superpowers/specs/2026-10-08-bo-url-resolve-design.md` + +--- + +## Contexte implémenteur + +- **Dépôt :** worktree `/Users/jonathan/Documents/GitHub/PrestaFlow/php-library-bo-resolve`, branche `feat/bo-url-resolve`, partie de `origin/dev` (`514c443`). Lance toutes les commandes depuis ce dossier. +- **Toujours `git --no-replace-objects`.** Ne touche jamais `/Users/jonathan/Documents/GitHub/PrestaFlow/php-library`, le checkout de l'utilisateur. +- **Dépendances :** `composer install -q` si `vendor/` manque. +- **Tests :** + - redirige toujours la sortie vers le scratchpad `SP` : `php vendor/bin/phpunit > "$SP/.txt" 2>&1; tail -3 "$SP/.txt"` ; + - PHP 8.4 avec `php`, PHP 8.1 avec `"$HOME/Library/Application Support/Herd/bin/php81"` ; + - aucun test ne lance Chrome ; + - base : 668 tests au départ. +- **Fichiers :** + - écris-les avec Write/Edit ou python, jamais par redirection shell (un hook supprime les commentaires) ; + - lis-les avec Read, `sed -n` ou `rtk proxy git …`. +- **Commits :** + - chemins explicites ; jamais `git add -A`, `git add .` ni `commit -a` ; + - messages en français, terminés par une ligne vide puis `Co-Authored-By: Claude Opus 5.5 `. +- **Environnement des tests :** + - les tests de `loadGlobals()` écrivent dans `$_ENV`, puis restaurent la valeur d'avant ; + - `loadGlobals()` charge aussi les `.env*` du dépôt avec Dotenv immutable : une variable déjà présente dans `$_ENV`, même vide, n'est pas réécrite. Définis donc explicitement `PRESTAFLOW_FO_URL` et `PRESTAFLOW_BO_URL` dans chaque cas, sauf le cas « absente ». + +--- + +### Task 1 : utilitaire `BackOfficeUrl` et délégation de `VisualTestsSuite` + +**Goal :** créer `PrestaFlow\Library\Utils\BackOfficeUrl::resolve()` et `::isRelative()`, testés seuls. `VisualTestsSuite::resolveBackOfficeUrl()` délègue à `resolve()` sans changer de comportement. + +**Files :** +- Create : `src/Utils/BackOfficeUrl.php` +- Create : `tests/Unit/Utils/BackOfficeUrlTest.php` +- Modify : `src/Tests/VisualTestsSuite.php`, méthode `resolveBackOfficeUrl()` (vers la ligne 485) +- Modify : `tests/Unit/Visual/VisualTestsSuiteUrlsTest.php` (le fournisseur `backOfficeUrlCases()` est déplacé) + +**Acceptance Criteria :** +- [ ] `BackOfficeUrl::resolve()` rend exactement les résultats des 13 cas actuels de `backOfficeUrlCases()`. +- [ ] `BackOfficeUrl::isRelative()` : + - vrai pour `admin/`, `/admin`, `admin-dev/` et ` admin ` ; + - faux pour `http://x/a`, `HTTPS://x/a`, `//x/a`, `''` et `' '`. +- [ ] `VisualTestsSuite::resolveBackOfficeUrl()` existe toujours (publique, statique, même signature) et délègue. Un test le vérifie sur un cas. +- [ ] `VisualTestsSuiteUrlsTest` reste vert. +- [ ] Suite complète verte en 8.4 et 8.1. + +**Verify :** `php vendor/bin/phpunit --filter 'BackOfficeUrlTest|VisualTestsSuiteUrlsTest' > "$SP/bo-t1.txt" 2>&1; tail -3 "$SP/bo-t1.txt"` → `OK` + +**Steps :** + +- [ ] **Step 1 : test qui échoue.** Crée `tests/Unit/Utils/BackOfficeUrlTest.php` : + +```php + + */ + public static function resolveCases(): array + { + return [ + 'absolue gardée' => ['http://shop.test/admin-dev/', 'http://other.test/', 'http://shop.test/admin-dev/'], + 'absolue rognée' => [' https://shop.test/admin123 ', '', 'https://shop.test/admin123/'], + 'absolue majuscules' => ['HTTP://shop.test/a', '', 'HTTP://shop.test/a/'], + 'relative sans slash' => ['admin-dev', 'http://shop.test', 'http://shop.test/admin-dev/'], + 'relative' => ['admin-dev/', 'http://shop.test/', 'http://shop.test/admin-dev/'], + 'relative à slash initial' => ['/admin-dev/', 'http://shop.test/', 'http://shop.test/admin-dev/'], + 'relative sous chemin' => ['admin/sub', ' http://shop.test/fr/ ', 'http://shop.test/fr/admin/sub/'], + 'protocole https' => ['//h.test/admin', 'https://fo.test', 'https://h.test/admin/'], + 'protocole majuscules' => ['//h.test/admin', 'HTTP://fo.test/fr', 'http://h.test/admin/'], + 'requête et fragment FO ignorés' => ['admin', 'http://fo.test/fr?x=1#a', 'http://fo.test/fr/admin/'], + 'fragment FO seul' => ['/admin', 'http://fo.test/#frag', 'http://fo.test/admin/'], + 'point dans un segment non initial' => ['/admin.v2/', 'http://fo.test', 'http://fo.test/admin.v2/'], + 'segment à point' => ['admin/v2.1/', 'http://fo.test', 'http://fo.test/admin/v2.1/'], + ]; + } + + /** @dataProvider resolveCases */ + public function test_resolve(string $backOffice, string $frontOffice, string $expected): void + { + $this->assertSame($expected, BackOfficeUrl::resolve($backOffice, $frontOffice)); + } + + /** @return array */ + public static function relativeCases(): array + { + return [ + 'chemin' => ['admin/', true], + 'slash initial' => ['/admin', true], + 'défaut' => ['admin-dev/', true], + 'chemin rogné' => [' admin ', true], + 'absolue' => ['http://x/a', false], + 'absolue majuscules' => ['HTTPS://x/a', false], + 'protocole' => ['//x/a', false], + 'vide' => ['', false], + 'blanc' => [' ', false], + ]; + } + + /** @dataProvider relativeCases */ + public function test_is_relative(string $backOffice, bool $expected): void + { + $this->assertSame($expected, BackOfficeUrl::isRelative($backOffice)); + } + + public function test_visual_suite_keeps_delegating(): void + { + $this->assertSame('http://fo.test/fr/admin/', VisualTestsSuite::resolveBackOfficeUrl('admin', 'http://fo.test/fr?x=1#a')); + } +} +``` + +- [ ] **Step 2 : constater l'échec.** `php vendor/bin/phpunit --filter BackOfficeUrlTest > "$SP/bo-t1-red.txt" 2>&1; tail -5 "$SP/bo-t1-red.txt"` → erreurs `Class "PrestaFlow\Library\Utils\BackOfficeUrl" not found`. + +- [ ] **Step 3 : utilitaire.** Crée `src/Utils/BackOfficeUrl.php` : + +```php + "$SP/bo-t1.txt" 2>&1; tail -3 "$SP/bo-t1.txt"` → `OK`. Lance ensuite la suite complète en 8.4 et en 8.1 (`bo-t1-84.txt`, `bo-t1-81.txt`). Total attendu : 668 − 13 + 23 = 678 tests. + +- [ ] **Step 7 : commit.** + +```bash +git --no-replace-objects add src/Utils/BackOfficeUrl.php tests/Unit/Utils/BackOfficeUrlTest.php src/Tests/VisualTestsSuite.php tests/Unit/Visual/VisualTestsSuiteUrlsTest.php +git --no-replace-objects commit -m "refactor(urls): règle de l'URL du back-office dans l'utilitaire BackOfficeUrl + +Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +### Task 2 : `loadGlobals()` utilise `BackOfficeUrl`, et README + +**Goal :** `PRESTAFLOW_BO_URL`, ou le défaut `admin-dev/`, est résolu par `BackOfficeUrl::resolve()`. `$backOfficeRelative` suit `BackOfficeUrl::isRelative()`. Le README renvoie à la règle commune. + +**Files :** +- Modify : `src/Tests/TestsSuite.php` (bloc `$backOfficeUrl` de `loadGlobals()`, vers les lignes 1188-1196, et le `use`) +- Create : `tests/Unit/Tests/LoadGlobalsBackOfficeUrlTest.php` +- Modify : `README.md` (ligne « The back-office area also uses `PRESTAFLOW_BO_URL` … ») + +**Acceptance Criteria :** +- [ ] Avec `PRESTAFLOW_FO_URL=https://x/` : + - `/admin` → `https://x/admin/` ; + - `//autre.test/admin` → `https://autre.test/admin/` ; + - `HTTPS://bo.test/admin` → `HTTPS://bo.test/admin/` ; + - `admin123` → `https://x/admin123/`. +- [ ] Avec `PRESTAFLOW_FO_URL=https://x/?lang=2` et `PRESTAFLOW_BO_URL=admin-dev/` → `https://x/admin-dev/`. L'URL FO reste `https://x/?lang=2/`, inchangée. +- [ ] `PRESTAFLOW_BO_URL` absente → `admin-dev/`, et `$backOfficeRelative` vaut `admin-dev/`. +- [ ] `PRESTAFLOW_BO_URL` vide → l'URL FO, et `$backOfficeRelative` vaut `null`. +- [ ] `$backOfficeRelative` : + - vaut la valeur rognée pour un chemin relatif ; + - vaut `null` pour une URL absolue et pour `//hôte`. +- [ ] Les tests `test_cli_*` de `VisualTestsSuiteUrlsTest` restent verts. +- [ ] Suite complète verte en 8.4 et 8.1. + +**Verify :** `php vendor/bin/phpunit --filter 'LoadGlobalsBackOfficeUrlTest|VisualTestsSuiteUrlsTest' > "$SP/bo-t2.txt" 2>&1; tail -3 "$SP/bo-t2.txt"` → `OK` + +**Steps :** + +- [ ] **Step 1 : test qui échoue.** Crée `tests/Unit/Tests/LoadGlobalsBackOfficeUrlTest.php` : + +```php + $env + */ + private function suite(array $env): TestsSuite + { + $saved = []; + foreach ($env as $key => $value) { + $saved[$key] = array_key_exists($key, $_ENV) ? $_ENV[$key] : null; + if ($value === null) { + unset($_ENV[$key]); + putenv($key); + } else { + $_ENV[$key] = $value; + } + } + try { + return new class (loadGlobals: true, getBrowser: false) extends TestsSuite { + public function relative(): ?string + { + return $this->backOfficeRelative; + } + }; + } finally { + foreach ($saved as $key => $value) { + if ($value === null) { + unset($_ENV[$key]); + } else { + $_ENV[$key] = $value; + } + } + } + } + + /** @return array */ + public static function cases(): array + { + return [ + 'slash initial' => ['https://x/', '/admin', 'https://x/admin/', '/admin'], + 'relatif' => ['https://x/', 'admin123', 'https://x/admin123/', 'admin123'], + 'requête FO ignorée' => ['https://x/?lang=2', 'admin-dev/', 'https://x/admin-dev/', 'admin-dev/'], + 'protocole' => ['https://x/', '//autre.test/admin', 'https://autre.test/admin/', null], + 'absolue majuscules' => ['https://x/', 'HTTPS://bo.test/admin', 'HTTPS://bo.test/admin/', null], + 'absolue' => ['https://x/', 'https://bo.test/admin/', 'https://bo.test/admin/', null], + 'relatif rogné' => ['https://x/', ' admin ', 'https://x/admin/', 'admin'], + 'vide' => ['https://x/', '', 'https://x/', null], + ]; + } + + /** @dataProvider cases */ + public function test_back_office_url_follows_the_single_rule(string $fo, string $bo, string $expected, ?string $relative): void + { + $suite = $this->suite(['PRESTAFLOW_FO_URL' => $fo, 'PRESTAFLOW_BO_URL' => $bo]); + + $this->assertSame($expected, $suite->getGlobals()['BO']['URL']); + $this->assertSame($relative, $suite->relative()); + } + + public function test_front_office_url_is_unchanged(): void + { + $suite = $this->suite(['PRESTAFLOW_FO_URL' => 'https://x/?lang=2', 'PRESTAFLOW_BO_URL' => 'admin-dev/']); + + $this->assertSame('https://x/?lang=2/', $suite->getGlobals()['FO']['URL']); + } + + public function test_missing_back_office_url_defaults_to_admin_dev(): void + { + $suite = $this->suite(['PRESTAFLOW_FO_URL' => 'https://x/fr?y=1', 'PRESTAFLOW_BO_URL' => null]); + + $this->assertSame('https://x/fr/admin-dev/', $suite->getGlobals()['BO']['URL']); + $this->assertSame('admin-dev/', $suite->relative()); + } +} +``` + + Si un `.env*` du dépôt définit `PRESTAFLOW_BO_URL` et fait échouer `test_missing_back_office_url_defaults_to_admin_dev`, marque ce test sauté (`markTestSkipped`) quand `getenv('PRESTAFLOW_BO_URL') !== false` après la construction, et signale-le dans ton rapport. Ne modifie aucun `.env*`. + +- [ ] **Step 2 : constater l'échec.** `php vendor/bin/phpunit --filter LoadGlobalsBackOfficeUrlTest > "$SP/bo-t2-red.txt" 2>&1; tail -15 "$SP/bo-t2-red.txt"`. Échecs attendus : + - `slash initial` : `https://x//admin/` ; + - `requête FO ignorée` ; + - `protocole` ; + - `absolue majuscules` ; + - `relatif rogné` ; + - le `$relative` des cas `protocole` et `vide` ; + - `test_missing_back_office_url_defaults_to_admin_dev`, à cause de la requête FO. + +- [ ] **Step 3 : implémentation.** Dans `src/Tests/TestsSuite.php` : + - ajoute `use PrestaFlow\Library\Utils\BackOfficeUrl;` au-dessus de `use PrestaFlow\Library\Utils\Env;` ; + - dans `loadGlobals()`, remplace ce bloc : + +```php + $backOfficeUrl = Env::get('PRESTAFLOW_BO_URL', $frontOfficeUrl . 'admin-dev/'); + $this->backOfficeRelative = Env::has('PRESTAFLOW_BO_URL') ? null : 'admin-dev/'; + if (!str_starts_with($backOfficeUrl, 'https://') && !str_starts_with($backOfficeUrl, 'http://')) { + $this->backOfficeRelative = (string) $backOfficeUrl; + $backOfficeUrl = $frontOfficeUrl . $backOfficeUrl; + } + if (!str_ends_with($backOfficeUrl, '/')) { + $backOfficeUrl .= '/'; + } +``` + +par : + +```php + // Règle unique (BackOfficeUrl, la même que l'app et que $backOfficeUrl) : + // chemin relatif complété par l'URL FO sans requête ni fragment, + // `//hôte` au schéma de la FO, absolue gardée. Aucun refus ici. + $rawBackOffice = (string) Env::get('PRESTAFLOW_BO_URL', 'admin-dev/'); + $this->backOfficeRelative = BackOfficeUrl::isRelative($rawBackOffice) ? trim($rawBackOffice) : null; + $backOfficeUrl = BackOfficeUrl::resolve($rawBackOffice, $frontOfficeUrl); +``` + + Garde le commentaire du docblock de `$backOfficeRelative` vers la ligne 268. Mets-le à jour s'il le faut : « PRESTAFLOW_BO_URL relative (chemin, ni absolue ni `//hôte`), ou défaut `admin-dev/` ». + +- [ ] **Step 4 : vert.** `php vendor/bin/phpunit --filter 'LoadGlobalsBackOfficeUrlTest|VisualTestsSuiteUrlsTest' > "$SP/bo-t2.txt" 2>&1; tail -3 "$SP/bo-t2.txt"` → `OK`. + +- [ ] **Step 5 : README.** Remplace la ligne : + +``` +The back-office area also uses `PRESTAFLOW_BO_URL` (admin URL), `PRESTAFLOW_BO_EMAIL` and `PRESTAFLOW_BO_PASSWD` (login). +``` + +par : + +``` +The back-office area also uses `PRESTAFLOW_BO_URL` (admin URL, default `admin-dev/`), `PRESTAFLOW_BO_EMAIL` and `PRESTAFLOW_BO_PASSWD` (login). A relative `PRESTAFLOW_BO_URL` is completed from `PRESTAFLOW_FO_URL` with the same rule as `$backOfficeUrl` below (shop path kept, query and fragment dropped, `//host` takes the shop scheme); nothing is refused for environment variables. +``` + +- [ ] **Step 6 : suite complète** en 8.4 et en 8.1 (`bo-t2-84.txt`, `bo-t2-81.txt`). Total attendu : 678 + 10 = 688 tests (1 test sauté en 8.1, comme au départ). + +- [ ] **Step 7 : commit.** + +```bash +git --no-replace-objects add src/Tests/TestsSuite.php tests/Unit/Tests/LoadGlobalsBackOfficeUrlTest.php README.md +git --no-replace-objects commit -m "fix(urls): PRESTAFLOW_BO_URL relative résolue avec la règle unique du back-office + +Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +### Task 3 : livraison (coordinateur) + +**Goal :** branche relue et verte, PR vers `dev`. La release et le bump de l'app se font sur accord de l'utilisateur. + +**Files :** `composer.lock` de l'app, au bump seulement. + +**Acceptance Criteria :** +- [ ] Revue finale faite, suite complète verte en 8.4 et 8.1. +- [ ] PR « Une seule règle de résolution des URL du back-office » vers `dev`. +- [ ] Sur demande de l'utilisateur : + - merge de la PR, puis release `dev` → `main`, en commits de merge ; + - puis, dans un worktree de l'app partant de `origin/main`, `composer update prestaflow/php-library` (seul `composer.lock` doit changer) ; + - suite PHP de l'app verte ; + - PR app. + +**Verify :** `gh pr view --repo PrestaFlow/php-library --json baseRefName,state` → `dev`, `OPEN`. + +**Steps :** +- [ ] `git --no-replace-objects push -u origin feat/bo-url-resolve`. +- [ ] `gh pr create --repo PrestaFlow/php-library --base dev --title "Une seule règle de résolution des URL du back-office" --body-file "$SP/bo-pr.md"`. Le corps contient : + - le tableau avant / après de la spec ; + - « aucun nouveau refus » ; + - `VisualTestsSuite::resolveBackOfficeUrl()` gardée ; + - puis `🤖 Generated with [Claude Code](https://claude.com/claude-code)`. +- [ ] Merges, release et bump : seulement sur demande, dans l'ordre ci-dessus. + +--- + +## Auto-relecture : couverture de la spec + +| Spec | Tâche | +|---|---| +| Utilitaire `BackOfficeUrl::resolve()` / `isRelative()` | 1 | +| `resolveBackOfficeUrl()` gardée, qui délègue | 1 | +| `loadGlobals()` : résolution, `$backOfficeRelative`, valeur vide, FO inchangée | 2 | +| Aucun nouveau refus | 2 (aucune exception ajoutée) | +| Tests : cas déplacés, `isRelative`, tableau « Problème », défaut | 1, 2 | +| README | 2 | +| Livraison lib puis bump de l'app | 3 | diff --git a/docs/superpowers/plans/2026-10-08-bo-url-resolve.md.tasks.json b/docs/superpowers/plans/2026-10-08-bo-url-resolve.md.tasks.json new file mode 100644 index 0000000..01e59a7 --- /dev/null +++ b/docs/superpowers/plans/2026-10-08-bo-url-resolve.md.tasks.json @@ -0,0 +1,30 @@ +{ + "planPath": "docs/superpowers/plans/2026-10-08-bo-url-resolve.md", + "tasks": [ + { + "id": 1, + "subject": "Task 1: utilitaire BackOfficeUrl et délégation de VisualTestsSuite", + "status": "pending", + "description": "**Goal:** BackOfficeUrl::resolve()/isRelative() testés ; VisualTestsSuite::resolveBackOfficeUrl() délègue.\n\n```json:metadata\n{\"files\": [\"src/Utils/BackOfficeUrl.php\", \"tests/Unit/Utils/BackOfficeUrlTest.php\", \"src/Tests/VisualTestsSuite.php\", \"tests/Unit/Visual/VisualTestsSuiteUrlsTest.php\"], \"verifyCommand\": \"php vendor/bin/phpunit --filter 'BackOfficeUrlTest|VisualTestsSuiteUrlsTest'\", \"acceptanceCriteria\": [\"13 cas resolve\", \"isRelative\", \"délégation\", \"678 tests 8.4/8.1\"]}\n```" + }, + { + "id": 2, + "subject": "Task 2: loadGlobals() utilise BackOfficeUrl, et README", + "status": "pending", + "blockedBy": [ + 1 + ], + "description": "**Goal:** PRESTAFLOW_BO_URL résolue par BackOfficeUrl::resolve(), backOfficeRelative par isRelative(), README.\n\n```json:metadata\n{\"files\": [\"src/Tests/TestsSuite.php\", \"tests/Unit/Tests/LoadGlobalsBackOfficeUrlTest.php\", \"README.md\"], \"verifyCommand\": \"php vendor/bin/phpunit --filter 'LoadGlobalsBackOfficeUrlTest|VisualTestsSuiteUrlsTest'\", \"acceptanceCriteria\": [\"tableau Problème\", \"défaut admin-dev\", \"vide\", \"688 tests 8.4/8.1\"]}\n```" + }, + { + "id": 3, + "subject": "Task 3: livraison (coordinateur)", + "status": "pending", + "blockedBy": [ + 2 + ], + "description": "**Goal:** revue finale, PR vers dev ; release et bump app sur accord.\n\n```json:metadata\n{\"files\": [], \"verifyCommand\": \"gh pr view --repo PrestaFlow/php-library\", \"acceptanceCriteria\": [\"PR dev ouverte\"]}\n```" + } + ], + "lastUpdated": "2026-10-08T09:08:47.280220+02:00" +} \ No newline at end of file From 0a0b01041a4369c630a1061c188a5f65e0a7eb36 Mon Sep 17 00:00:00 2001 From: Jonathan Danse Date: Thu, 8 Oct 2026 09:12:55 +0200 Subject: [PATCH 3/7] =?UTF-8?q?refactor(urls):=20r=C3=A8gle=20de=20l'URL?= =?UTF-8?q?=20du=20back-office=20dans=20l'utilitaire=20BackOfficeUrl?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- src/Tests/VisualTestsSuite.php | 20 ++---- src/Utils/BackOfficeUrl.php | 45 +++++++++++++ tests/Unit/Utils/BackOfficeUrlTest.php | 67 +++++++++++++++++++ .../Unit/Visual/VisualTestsSuiteUrlsTest.php | 26 ------- 4 files changed, 116 insertions(+), 42 deletions(-) create mode 100644 src/Utils/BackOfficeUrl.php create mode 100644 tests/Unit/Utils/BackOfficeUrlTest.php diff --git a/src/Tests/VisualTestsSuite.php b/src/Tests/VisualTestsSuite.php index 924974b..9cc4630 100644 --- a/src/Tests/VisualTestsSuite.php +++ b/src/Tests/VisualTestsSuite.php @@ -7,6 +7,7 @@ use PrestaFlow\Library\Exceptions\BackOfficeTimeoutException; use PrestaFlow\Library\Exceptions\TimeoutException; use PrestaFlow\Library\Expects\Expect; +use PrestaFlow\Library\Utils\BackOfficeUrl; use PrestaFlow\Library\Utils\Env; use PrestaFlow\Library\Visual\VisualDevices; @@ -477,25 +478,12 @@ public function applySuiteUrls(): void } /** - * URL BO effective, règle de l'app (App\Support\BackOfficeUrl::resolve) : - * absolue http(s) gardée ; relative au protocole (`//hôte/admin`) : schéma - * de l'URL FO ; relative (`admin123/`, `/admin123`) : URL FO sans requête - * ni fragment, puis le chemin. Toujours terminée par « / ». + * URL BO effective : voir BackOfficeUrl::resolve() (règle unique, la même que + * loadGlobals() et que l'app). Gardée publique pour compatibilité. */ public static function resolveBackOfficeUrl(string $backOffice, string $frontOffice): string { - $backOffice = trim($backOffice); - $frontOffice = trim($frontOffice); - if (str_starts_with($backOffice, '//')) { - if (preg_match('#^(https?)://#i', $frontOffice, $m) === 1) { - $backOffice = strtolower($m[1]).':'.$backOffice; - } - } elseif (preg_match('#^https?://#i', $backOffice) !== 1) { - $frontOffice = (string) preg_replace('/[?#].*\z/s', '', $frontOffice); - $backOffice = rtrim($frontOffice, '/').'/'.ltrim($backOffice, '/'); - } - - return str_ends_with($backOffice, '/') ? $backOffice : $backOffice.'/'; + return BackOfficeUrl::resolve($backOffice, $frontOffice); } /** Refuse blancs, identifiants, schéma non http(s) et chemin relatif non sûr, sans citer l'URL (elle peut porter un mot de passe). */ diff --git a/src/Utils/BackOfficeUrl.php b/src/Utils/BackOfficeUrl.php new file mode 100644 index 0000000..5db4777 --- /dev/null +++ b/src/Utils/BackOfficeUrl.php @@ -0,0 +1,45 @@ + + */ + public static function resolveCases(): array + { + return [ + 'absolue gardée' => ['http://shop.test/admin-dev/', 'http://other.test/', 'http://shop.test/admin-dev/'], + 'absolue rognée' => [' https://shop.test/admin123 ', '', 'https://shop.test/admin123/'], + 'absolue majuscules' => ['HTTP://shop.test/a', '', 'HTTP://shop.test/a/'], + 'relative sans slash' => ['admin-dev', 'http://shop.test', 'http://shop.test/admin-dev/'], + 'relative' => ['admin-dev/', 'http://shop.test/', 'http://shop.test/admin-dev/'], + 'relative à slash initial' => ['/admin-dev/', 'http://shop.test/', 'http://shop.test/admin-dev/'], + 'relative sous chemin' => ['admin/sub', ' http://shop.test/fr/ ', 'http://shop.test/fr/admin/sub/'], + 'protocole https' => ['//h.test/admin', 'https://fo.test', 'https://h.test/admin/'], + 'protocole majuscules' => ['//h.test/admin', 'HTTP://fo.test/fr', 'http://h.test/admin/'], + 'requête et fragment FO ignorés' => ['admin', 'http://fo.test/fr?x=1#a', 'http://fo.test/fr/admin/'], + 'fragment FO seul' => ['/admin', 'http://fo.test/#frag', 'http://fo.test/admin/'], + 'point dans un segment non initial' => ['/admin.v2/', 'http://fo.test', 'http://fo.test/admin.v2/'], + 'segment à point' => ['admin/v2.1/', 'http://fo.test', 'http://fo.test/admin/v2.1/'], + ]; + } + + /** @dataProvider resolveCases */ + public function test_resolve(string $backOffice, string $frontOffice, string $expected): void + { + $this->assertSame($expected, BackOfficeUrl::resolve($backOffice, $frontOffice)); + } + + /** @return array */ + public static function relativeCases(): array + { + return [ + 'chemin' => ['admin/', true], + 'slash initial' => ['/admin', true], + 'défaut' => ['admin-dev/', true], + 'chemin rogné' => [' admin ', true], + 'absolue' => ['http://x/a', false], + 'absolue majuscules' => ['HTTPS://x/a', false], + 'protocole' => ['//x/a', false], + 'vide' => ['', false], + 'blanc' => [' ', false], + ]; + } + + /** @dataProvider relativeCases */ + public function test_is_relative(string $backOffice, bool $expected): void + { + $this->assertSame($expected, BackOfficeUrl::isRelative($backOffice)); + } + + public function test_visual_suite_keeps_delegating(): void + { + $this->assertSame('http://fo.test/fr/admin/', VisualTestsSuite::resolveBackOfficeUrl('admin', 'http://fo.test/fr?x=1#a')); + } +} diff --git a/tests/Unit/Visual/VisualTestsSuiteUrlsTest.php b/tests/Unit/Visual/VisualTestsSuiteUrlsTest.php index 543d449..8ff26e0 100644 --- a/tests/Unit/Visual/VisualTestsSuiteUrlsTest.php +++ b/tests/Unit/Visual/VisualTestsSuiteUrlsTest.php @@ -283,32 +283,6 @@ public function test_query_and_fragment_of_the_globals_shop_url_are_dropped_for_ $this->assertSame('http://fo.test/fr/admin123/', $suite->getGlobals()['BO']['URL']); } - /** @return array */ - public static function backOfficeUrlCases(): array - { - return [ - 'absolue gardée' => ['http://shop.test/admin-dev/', 'http://other.test/', 'http://shop.test/admin-dev/'], - 'absolue rognée' => [' https://shop.test/admin123 ', '', 'https://shop.test/admin123/'], - 'absolue majuscules' => ['HTTP://shop.test/a', '', 'HTTP://shop.test/a/'], - 'relative sans slash' => ['admin-dev', 'http://shop.test', 'http://shop.test/admin-dev/'], - 'relative' => ['admin-dev/', 'http://shop.test/', 'http://shop.test/admin-dev/'], - 'relative à slash initial' => ['/admin-dev/', 'http://shop.test/', 'http://shop.test/admin-dev/'], - 'relative sous chemin' => ['admin/sub', ' http://shop.test/fr/ ', 'http://shop.test/fr/admin/sub/'], - 'protocole https' => ['//h.test/admin', 'https://fo.test', 'https://h.test/admin/'], - 'protocole majuscules' => ['//h.test/admin', 'HTTP://fo.test/fr', 'http://h.test/admin/'], - 'requête et fragment FO ignorés' => ['admin', 'http://fo.test/fr?x=1#a', 'http://fo.test/fr/admin/'], - 'fragment FO seul' => ['/admin', 'http://fo.test/#frag', 'http://fo.test/admin/'], - 'point dans un segment non initial' => ['/admin.v2/', 'http://fo.test', 'http://fo.test/admin.v2/'], - 'segment à point' => ['admin/v2.1/', 'http://fo.test', 'http://fo.test/admin/v2.1/'], - ]; - } - - /** @dataProvider backOfficeUrlCases */ - public function test_resolve_back_office_url(string $backOffice, string $frontOffice, string $expected): void - { - $this->assertSame($expected, VisualTestsSuite::resolveBackOfficeUrl($backOffice, $frontOffice)); - } - private function cliSuite(array $env, ?string $shopUrl): VisualTestsSuite { $keys = ['PRESTAFLOW_FO_URL', 'PRESTAFLOW_BO_URL']; From 8ecbc6ab9a8b74967ef616df231a182f608e3c6d Mon Sep 17 00:00:00 2001 From: Jonathan Danse Date: Thu, 8 Oct 2026 09:15:54 +0200 Subject: [PATCH 4/7] =?UTF-8?q?docs(urls):=20port=C3=A9e=20exacte=20de=20B?= =?UTF-8?q?ackOfficeUrl,=20cas=20limites=20test=C3=A9s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- src/Tests/VisualTestsSuite.php | 5 +++-- src/Utils/BackOfficeUrl.php | 10 +++++++--- tests/Unit/Utils/BackOfficeUrlTest.php | 4 ++++ 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/Tests/VisualTestsSuite.php b/src/Tests/VisualTestsSuite.php index 9cc4630..968d29a 100644 --- a/src/Tests/VisualTestsSuite.php +++ b/src/Tests/VisualTestsSuite.php @@ -478,8 +478,9 @@ public function applySuiteUrls(): void } /** - * URL BO effective : voir BackOfficeUrl::resolve() (règle unique, la même que - * loadGlobals() et que l'app). Gardée publique pour compatibilité. + * URL BO effective : voir BackOfficeUrl::resolve() (règle unique ; même règle que + * App\Support\BackOfficeUrl::resolve() pour les valeurs valides, rien n'est refusé ici). + * Gardée publique pour compatibilité. */ public static function resolveBackOfficeUrl(string $backOffice, string $frontOffice): string { diff --git a/src/Utils/BackOfficeUrl.php b/src/Utils/BackOfficeUrl.php index 5db4777..cd4a5c6 100644 --- a/src/Utils/BackOfficeUrl.php +++ b/src/Utils/BackOfficeUrl.php @@ -3,8 +3,9 @@ namespace PrestaFlow\Library\Utils; /** - * Règle unique de l'URL du back-office, la même que App\Support\BackOfficeUrl::resolve() - * dans l'app PrestaFlow. Sert à loadGlobals() (PRESTAFLOW_BO_URL) et aux suites + * Règle unique de l'URL du back-office : même règle que App\Support\BackOfficeUrl::resolve() + * (app PrestaFlow) pour les valeurs valides (absolue, `//hôte`, chemin sûr) ; ici rien n'est refusé. + * Sert à loadGlobals() (PRESTAFLOW_BO_URL) et aux suites * visuelles ($backOfficeUrl). Ne refuse rien : la validation reste à l'appelant. */ final class BackOfficeUrl @@ -33,7 +34,10 @@ public static function resolve(string $backOffice, string $frontOffice): string return str_ends_with($backOffice, '/') ? $backOffice : $backOffice.'/'; } - /** Vrai si l'URL BO dépend de l'URL FO : ni absolue http(s), ni `//hôte`, ni vide. */ + /** + * Vrai si la valeur est un chemin à coller à l'URL FO : ni absolue http(s), ni `//hôte`. + * Une valeur vide n'est pas relative par convention : resolve() rend alors l'URL FO elle-même. + */ public static function isRelative(string $backOffice): bool { $backOffice = trim($backOffice); diff --git a/tests/Unit/Utils/BackOfficeUrlTest.php b/tests/Unit/Utils/BackOfficeUrlTest.php index 7295f83..f482e66 100644 --- a/tests/Unit/Utils/BackOfficeUrlTest.php +++ b/tests/Unit/Utils/BackOfficeUrlTest.php @@ -28,6 +28,7 @@ public static function resolveCases(): array 'requête et fragment FO ignorés' => ['admin', 'http://fo.test/fr?x=1#a', 'http://fo.test/fr/admin/'], 'fragment FO seul' => ['/admin', 'http://fo.test/#frag', 'http://fo.test/admin/'], 'point dans un segment non initial' => ['/admin.v2/', 'http://fo.test', 'http://fo.test/admin.v2/'], + 'vide = FO' => ['', 'http://fo.test/fr?x=1', 'http://fo.test/fr/'], 'segment à point' => ['admin/v2.1/', 'http://fo.test', 'http://fo.test/admin/v2.1/'], ]; } @@ -51,6 +52,9 @@ public static function relativeCases(): array 'protocole' => ['//x/a', false], 'vide' => ['', false], 'blanc' => [' ', false], + 'autre schéma' => ['ftp://x', true], + 'un seul slash' => ['http:/x', true], + 'protocole entouré de blancs' => ["\t//x\n", false], ]; } From 1de8f05dee356a65b57e79116799524ea9e61248 Mon Sep 17 00:00:00 2001 From: Jonathan Danse Date: Thu, 8 Oct 2026 09:17:20 +0200 Subject: [PATCH 5/7] =?UTF-8?q?fix(urls):=20PRESTAFLOW=5FBO=5FURL=20relati?= =?UTF-8?q?ve=20r=C3=A9solue=20avec=20la=20r=C3=A8gle=20unique=20du=20back?= =?UTF-8?q?-office?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- src/Tests/TestsSuite.php | 20 ++--- .../Tests/LoadGlobalsBackOfficeUrlTest.php | 84 +++++++++++++++++++ 3 files changed, 94 insertions(+), 12 deletions(-) create mode 100644 tests/Unit/Tests/LoadGlobalsBackOfficeUrlTest.php diff --git a/README.md b/README.md index 1ee9c61..b625739 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ A visual suite with `protected string $area = 'bo';` captures the back office. E | `auth` | `true` | `false`: captured logged out (the login page). Logged-out checkpoints run first; the suite then logs in once with `PRESTAFLOW_BO_EMAIL` / `PRESTAFLOW_BO_PASSWD`. | | `hide` | `[]` | Selectors set to `display: none` during the capture (popups, modal backdrops). Works in both areas. | -The back-office area also uses `PRESTAFLOW_BO_URL` (admin URL), `PRESTAFLOW_BO_EMAIL` and `PRESTAFLOW_BO_PASSWD` (login). +The back-office area also uses `PRESTAFLOW_BO_URL` (admin URL, default `admin-dev/`), `PRESTAFLOW_BO_EMAIL` and `PRESTAFLOW_BO_PASSWD` (login). A relative `PRESTAFLOW_BO_URL` is completed from `PRESTAFLOW_FO_URL` with the same rule as `$backOfficeUrl` below (shop path kept, query and fragment dropped, `//host` takes the shop scheme); nothing is refused for environment variables. CSS transitions are frozen during back-office captures (`protected ?bool $freezeTransitions`; `null` = on for `bo`, off for `fo`). See `src/Tests/Suites/Visual/BackOffice.php`. diff --git a/src/Tests/TestsSuite.php b/src/Tests/TestsSuite.php index 0154059..605f4c9 100644 --- a/src/Tests/TestsSuite.php +++ b/src/Tests/TestsSuite.php @@ -17,6 +17,7 @@ use PrestaFlow\Library\Traits\ImportPage; use PrestaFlow\Library\Traits\Locale; use PrestaFlow\Library\Traits\Version; +use PrestaFlow\Library\Utils\BackOfficeUrl; use PrestaFlow\Library\Utils\Env; use PrestaFlow\Library\Utils\Output; use PrestaFlow\Library\Utils\OutputStates; @@ -267,8 +268,8 @@ public static function resetBrowser(): void /** * Valeur brute de l'URL BO quand loadGlobals() l'a complétée à partir de - * l'URL FO de l'environnement (PRESTAFLOW_BO_URL relative, ou défaut - * `admin-dev/`) ; null sinon (BO absolue, globals fournies). Permet à + * l'URL FO de l'environnement (PRESTAFLOW_BO_URL relative : chemin, ni + * absolue ni `//hôte`, ou défaut `admin-dev/`) ; null sinon (BO absolue, globals fournies). Permet à * VisualTestsSuite::applySuiteUrls() de suivre une $shopUrl du fichier. */ protected ?string $backOfficeRelative = null; @@ -1185,15 +1186,12 @@ public function loadGlobals() $frontOfficeUrl .= '/'; } - $backOfficeUrl = Env::get('PRESTAFLOW_BO_URL', $frontOfficeUrl . 'admin-dev/'); - $this->backOfficeRelative = Env::has('PRESTAFLOW_BO_URL') ? null : 'admin-dev/'; - if (!str_starts_with($backOfficeUrl, 'https://') && !str_starts_with($backOfficeUrl, 'http://')) { - $this->backOfficeRelative = (string) $backOfficeUrl; - $backOfficeUrl = $frontOfficeUrl . $backOfficeUrl; - } - if (!str_ends_with($backOfficeUrl, '/')) { - $backOfficeUrl .= '/'; - } + // Règle unique (BackOfficeUrl, la même que l'app et que $backOfficeUrl) : + // chemin relatif complété par l'URL FO sans requête ni fragment, + // `//hôte` au schéma de la FO, absolue gardée. Aucun refus ici. + $rawBackOffice = (string) Env::get('PRESTAFLOW_BO_URL', 'admin-dev/'); + $this->backOfficeRelative = BackOfficeUrl::isRelative($rawBackOffice) ? trim($rawBackOffice) : null; + $backOfficeUrl = BackOfficeUrl::resolve($rawBackOffice, $frontOfficeUrl); $this->globals = [ 'PS_VERSION' => Env::get('PRESTAFLOW_PS_VERSION', '8.1.0'), diff --git a/tests/Unit/Tests/LoadGlobalsBackOfficeUrlTest.php b/tests/Unit/Tests/LoadGlobalsBackOfficeUrlTest.php new file mode 100644 index 0000000..76f3210 --- /dev/null +++ b/tests/Unit/Tests/LoadGlobalsBackOfficeUrlTest.php @@ -0,0 +1,84 @@ + $env + */ + private function suite(array $env): TestsSuite + { + $saved = []; + foreach ($env as $key => $value) { + $saved[$key] = array_key_exists($key, $_ENV) ? $_ENV[$key] : null; + if ($value === null) { + unset($_ENV[$key]); + putenv($key); + } else { + $_ENV[$key] = $value; + } + } + try { + return new class (loadGlobals: true, getBrowser: false) extends TestsSuite { + public function relative(): ?string + { + return $this->backOfficeRelative; + } + }; + } finally { + foreach ($saved as $key => $value) { + if ($value === null) { + unset($_ENV[$key]); + } else { + $_ENV[$key] = $value; + } + } + } + } + + /** @return array */ + public static function cases(): array + { + return [ + 'slash initial' => ['https://x/', '/admin', 'https://x/admin/', '/admin'], + 'relatif' => ['https://x/', 'admin123', 'https://x/admin123/', 'admin123'], + 'requête FO ignorée' => ['https://x/?lang=2', 'admin-dev/', 'https://x/admin-dev/', 'admin-dev/'], + 'protocole' => ['https://x/', '//autre.test/admin', 'https://autre.test/admin/', null], + 'absolue majuscules' => ['https://x/', 'HTTPS://bo.test/admin', 'HTTPS://bo.test/admin/', null], + 'absolue' => ['https://x/', 'https://bo.test/admin/', 'https://bo.test/admin/', null], + 'relatif rogné' => ['https://x/', ' admin ', 'https://x/admin/', 'admin'], + 'vide' => ['https://x/', '', 'https://x/', null], + ]; + } + + /** @dataProvider cases */ + public function test_back_office_url_follows_the_single_rule(string $fo, string $bo, string $expected, ?string $relative): void + { + $suite = $this->suite(['PRESTAFLOW_FO_URL' => $fo, 'PRESTAFLOW_BO_URL' => $bo]); + + $this->assertSame($expected, $suite->getGlobals()['BO']['URL']); + $this->assertSame($relative, $suite->relative()); + } + + public function test_front_office_url_is_unchanged(): void + { + $suite = $this->suite(['PRESTAFLOW_FO_URL' => 'https://x/?lang=2', 'PRESTAFLOW_BO_URL' => 'admin-dev/']); + + $this->assertSame('https://x/?lang=2/', $suite->getGlobals()['FO']['URL']); + } + + public function test_missing_back_office_url_defaults_to_admin_dev(): void + { + $suite = $this->suite(['PRESTAFLOW_FO_URL' => 'https://x/fr?y=1', 'PRESTAFLOW_BO_URL' => null]); + + $this->assertSame('https://x/fr/admin-dev/', $suite->getGlobals()['BO']['URL']); + $this->assertSame('admin-dev/', $suite->relative()); + } +} From df747017466dc88dbeb1ac24117ebbd1127079e0 Mon Sep 17 00:00:00 2001 From: Jonathan Danse Date: Thu, 8 Oct 2026 09:22:04 +0200 Subject: [PATCH 6/7] fix(urls): BO vide toujours suivie par la shopUrl, isolation des tests, README Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- .../specs/2026-10-08-bo-url-resolve-design.md | 2 +- src/Tests/TestsSuite.php | 9 ++++-- .../Tests/LoadGlobalsBackOfficeUrlTest.php | 22 ++++++++++--- .../Unit/Visual/VisualTestsSuiteUrlsTest.php | 31 ++++++++++++++++--- 5 files changed, 52 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index b625739..cf87d8f 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ protected ?string $backOfficeUrl = 'admin123/'; // relative: appended to the eff - Only `http(s)` URLs with a host; no whitespace; `$shopUrl` takes no query (`?`) or fragment (`#`). - A relative back-office URL is appended to the effective shop URL: the shop path is kept, its query and fragment are dropped (`http://x.test/shop/` + `admin123/` gives `http://x.test/shop/admin123/`). `//host/admin` takes the shop URL scheme. A relative path must be safe: no `?`/`#`, no segment starting with a dot, no dot in the first segment unless it starts with `/` (`shop.test/admin` is refused, not guessed). It needs an `http(s)` shop URL, else it is refused. -- When only `$shopUrl` is set, a back-office URL that the library completed from the environment shop URL (relative `PRESTAFLOW_BO_URL`, or the default `admin-dev/`) follows the new shop URL; an absolute `PRESTAFLOW_BO_URL`, or one sent by the app, is kept. +- When only `$shopUrl` is set, a back-office URL that the library completed from the environment shop URL (relative `PRESTAFLOW_BO_URL`, or the default `admin-dev/`) follows the new shop URL; an absolute or `//host` `PRESTAFLOW_BO_URL`, or one sent by the app, is kept. - Credentials (`user:pass@`) are refused at launch, without echoing the URL: use `PRESTAFLOW_BASIC_USER` / `PRESTAFLOW_BASIC_PASS`. The same goes for every other refusal: the message names the property, never the value. ### Visual picker API diff --git a/docs/superpowers/specs/2026-10-08-bo-url-resolve-design.md b/docs/superpowers/specs/2026-10-08-bo-url-resolve-design.md index 58015f6..f7525cd 100644 --- a/docs/superpowers/specs/2026-10-08-bo-url-resolve-design.md +++ b/docs/superpowers/specs/2026-10-08-bo-url-resolve-design.md @@ -55,7 +55,7 @@ final class BackOfficeUrl - `PRESTAFLOW_FO_URL` ne change pas : il reçoit seulement un `/` final. - L'URL BO vaut `PRESTAFLOW_BO_URL`, ou `admin-dev/` si la variable est absente. Elle passe par `BackOfficeUrl::resolve($bo, $frontOfficeUrl)`. - `$backOfficeRelative` vaut la valeur brute nettoyée quand `BackOfficeUrl::isRelative()` est vrai, sinon `null`. Avec `$shopUrl` seule, un `//hôte` ou une URL absolue ne suit donc plus l'URL FO du fichier. C'est cohérent : leur hôte ne dépend pas de la FO. -- `PRESTAFLOW_BO_URL` défini mais vide : l'URL FO elle-même, comme aujourd'hui, avec un `/` final. Ce cas n'est pas relatif. +- `PRESTAFLOW_BO_URL` défini mais vide : l'URL FO elle-même, comme aujourd'hui ; `$backOfficeRelative` vaut `''`, pour que la BO suive une `$shopUrl` du fichier (comportement de `dev` conservé). ### `VisualTestsSuite` diff --git a/src/Tests/TestsSuite.php b/src/Tests/TestsSuite.php index 605f4c9..f9d58e8 100644 --- a/src/Tests/TestsSuite.php +++ b/src/Tests/TestsSuite.php @@ -269,8 +269,9 @@ public static function resetBrowser(): void /** * Valeur brute de l'URL BO quand loadGlobals() l'a complétée à partir de * l'URL FO de l'environnement (PRESTAFLOW_BO_URL relative : chemin, ni - * absolue ni `//hôte`, ou défaut `admin-dev/`) ; null sinon (BO absolue, globals fournies). Permet à - * VisualTestsSuite::applySuiteUrls() de suivre une $shopUrl du fichier. + * absolue ni `//hôte`, vide, ou défaut `admin-dev/`) ; null sinon (BO + * absolue, globals fournies). Permet à VisualTestsSuite::applySuiteUrls() + * de suivre une $shopUrl du fichier. */ protected ?string $backOfficeRelative = null; @@ -1190,7 +1191,9 @@ public function loadGlobals() // chemin relatif complété par l'URL FO sans requête ni fragment, // `//hôte` au schéma de la FO, absolue gardée. Aucun refus ici. $rawBackOffice = (string) Env::get('PRESTAFLOW_BO_URL', 'admin-dev/'); - $this->backOfficeRelative = BackOfficeUrl::isRelative($rawBackOffice) ? trim($rawBackOffice) : null; + $trimmedBackOffice = trim($rawBackOffice); + // Une valeur vide reste « relative » ('') : la BO suit alors une $shopUrl du fichier. + $this->backOfficeRelative = ($trimmedBackOffice === '' || BackOfficeUrl::isRelative($trimmedBackOffice)) ? $trimmedBackOffice : null; $backOfficeUrl = BackOfficeUrl::resolve($rawBackOffice, $frontOfficeUrl); $this->globals = [ diff --git a/tests/Unit/Tests/LoadGlobalsBackOfficeUrlTest.php b/tests/Unit/Tests/LoadGlobalsBackOfficeUrlTest.php index 76f3210..fbee031 100644 --- a/tests/Unit/Tests/LoadGlobalsBackOfficeUrlTest.php +++ b/tests/Unit/Tests/LoadGlobalsBackOfficeUrlTest.php @@ -17,7 +17,10 @@ private function suite(array $env): TestsSuite { $saved = []; foreach ($env as $key => $value) { - $saved[$key] = array_key_exists($key, $_ENV) ? $_ENV[$key] : null; + $saved[$key] = [ + array_key_exists($key, $_ENV) ? $_ENV[$key] : null, + getenv($key), + ]; if ($value === null) { unset($_ENV[$key]); putenv($key); @@ -33,11 +36,16 @@ public function relative(): ?string } }; } finally { - foreach ($saved as $key => $value) { - if ($value === null) { + foreach ($saved as $key => [$envValue, $processValue]) { + if ($envValue === null) { unset($_ENV[$key]); } else { - $_ENV[$key] = $value; + $_ENV[$key] = $envValue; + } + if ($processValue === false) { + putenv($key); + } else { + putenv($key . '=' . $processValue); } } } @@ -54,7 +62,7 @@ public static function cases(): array 'absolue majuscules' => ['https://x/', 'HTTPS://bo.test/admin', 'HTTPS://bo.test/admin/', null], 'absolue' => ['https://x/', 'https://bo.test/admin/', 'https://bo.test/admin/', null], 'relatif rogné' => ['https://x/', ' admin ', 'https://x/admin/', 'admin'], - 'vide' => ['https://x/', '', 'https://x/', null], + 'vide' => ['https://x/', '', 'https://x/', ''], ]; } @@ -78,6 +86,10 @@ public function test_missing_back_office_url_defaults_to_admin_dev(): void { $suite = $this->suite(['PRESTAFLOW_FO_URL' => 'https://x/fr?y=1', 'PRESTAFLOW_BO_URL' => null]); + if (getenv('PRESTAFLOW_BO_URL') !== false || isset($_ENV['PRESTAFLOW_BO_URL'])) { + $this->markTestSkipped('Un .env* définit PRESTAFLOW_BO_URL : le cas « absente » est faussé.'); + } + $this->assertSame('https://x/fr/admin-dev/', $suite->getGlobals()['BO']['URL']); $this->assertSame('admin-dev/', $suite->relative()); } diff --git a/tests/Unit/Visual/VisualTestsSuiteUrlsTest.php b/tests/Unit/Visual/VisualTestsSuiteUrlsTest.php index 8ff26e0..327e132 100644 --- a/tests/Unit/Visual/VisualTestsSuiteUrlsTest.php +++ b/tests/Unit/Visual/VisualTestsSuiteUrlsTest.php @@ -288,8 +288,9 @@ private function cliSuite(array $env, ?string $shopUrl): VisualTestsSuite $keys = ['PRESTAFLOW_FO_URL', 'PRESTAFLOW_BO_URL']; $saved = []; foreach ($keys as $key) { - $saved[$key] = array_key_exists($key, $_ENV) ? $_ENV[$key] : null; + $saved[$key] = [array_key_exists($key, $_ENV) ? $_ENV[$key] : null, getenv($key)]; unset($_ENV[$key]); + putenv($key); } foreach ($env as $key => $value) { $_ENV[$key] = $value; @@ -310,11 +311,16 @@ public function pin(?string $shopUrl): void } }; } finally { - foreach ($saved as $key => $value) { - if ($value === null) { + foreach ($saved as $key => [$envValue, $processValue]) { + if ($envValue === null) { unset($_ENV[$key]); } else { - $_ENV[$key] = $value; + $_ENV[$key] = $envValue; + } + if ($processValue === false) { + putenv($key); + } else { + putenv($key . '=' . $processValue); } } } @@ -350,6 +356,23 @@ public function test_cli_absolute_back_office_url_is_kept_with_the_suite_shop_ur $this->assertSame('https://bo.env.test/admin/', $suite->getGlobals()['BO']['URL']); } + public function test_cli_empty_back_office_url_follows_the_suite_shop_url(): void + { + $suite = $this->cliSuite(['PRESTAFLOW_FO_URL' => 'https://env.test/', 'PRESTAFLOW_BO_URL' => ''], 'http://localhost:8093'); + $this->assertSame('https://env.test/', $suite->getGlobals()['BO']['URL']); + $suite->applySuiteUrls(); + + $this->assertSame('http://localhost:8093/', $suite->getGlobals()['BO']['URL']); + } + + public function test_cli_protocol_relative_back_office_url_is_kept_with_the_suite_shop_url(): void + { + $suite = $this->cliSuite(['PRESTAFLOW_FO_URL' => 'https://env.test/', 'PRESTAFLOW_BO_URL' => '//bo.env.test/admin'], 'http://localhost:8093'); + $suite->applySuiteUrls(); + + $this->assertSame('https://bo.env.test/admin/', $suite->getGlobals()['BO']['URL']); + } + public function test_provided_globals_back_office_url_is_kept_with_the_suite_shop_url(): void { // Cas de l'app : setGlobals() fournit FO et BO ; $shopUrl ne touche pas à BO. From ef29667d8ba7eeb4cdc1f8dc9730f29ffdd5799f Mon Sep 17 00:00:00 2001 From: Jonathan Danse Date: Thu, 8 Oct 2026 09:22:09 +0200 Subject: [PATCH 7/7] =?UTF-8?q?docs(plan):=20suivi=20des=20t=C3=A2ches=20d?= =?UTF-8?q?e=20la=20r=C3=A8gle=20unique=20des=20URL=20du=20back-office?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../plans/2026-10-08-bo-url-resolve.md.tasks.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/superpowers/plans/2026-10-08-bo-url-resolve.md.tasks.json b/docs/superpowers/plans/2026-10-08-bo-url-resolve.md.tasks.json index 01e59a7..600e3f8 100644 --- a/docs/superpowers/plans/2026-10-08-bo-url-resolve.md.tasks.json +++ b/docs/superpowers/plans/2026-10-08-bo-url-resolve.md.tasks.json @@ -4,13 +4,13 @@ { "id": 1, "subject": "Task 1: utilitaire BackOfficeUrl et délégation de VisualTestsSuite", - "status": "pending", + "status": "completed", "description": "**Goal:** BackOfficeUrl::resolve()/isRelative() testés ; VisualTestsSuite::resolveBackOfficeUrl() délègue.\n\n```json:metadata\n{\"files\": [\"src/Utils/BackOfficeUrl.php\", \"tests/Unit/Utils/BackOfficeUrlTest.php\", \"src/Tests/VisualTestsSuite.php\", \"tests/Unit/Visual/VisualTestsSuiteUrlsTest.php\"], \"verifyCommand\": \"php vendor/bin/phpunit --filter 'BackOfficeUrlTest|VisualTestsSuiteUrlsTest'\", \"acceptanceCriteria\": [\"13 cas resolve\", \"isRelative\", \"délégation\", \"678 tests 8.4/8.1\"]}\n```" }, { "id": 2, "subject": "Task 2: loadGlobals() utilise BackOfficeUrl, et README", - "status": "pending", + "status": "completed", "blockedBy": [ 1 ], @@ -26,5 +26,5 @@ "description": "**Goal:** revue finale, PR vers dev ; release et bump app sur accord.\n\n```json:metadata\n{\"files\": [], \"verifyCommand\": \"gh pr view --repo PrestaFlow/php-library\", \"acceptanceCriteria\": [\"PR dev ouverte\"]}\n```" } ], - "lastUpdated": "2026-10-08T09:08:47.280220+02:00" -} \ No newline at end of file + "lastUpdated": "2026-10-08T09:22:09.024342+02:00" +}