|
1 | | -# Plan: 5 neue Matcher hinzufügen |
| 1 | +# Plan: withAttributes-Doku in README und SKILL.md präzisieren |
2 | 2 |
|
3 | 3 | ## Context |
4 | 4 |
|
5 | | -Aktuell gibt es 8 Matcher-Klassen (HttpMethod, Url, Host, Domain, Subdomain, HttpAttribute, BodyHash, Closure). Es fehlen Matcher für häufige Anwendungsfälle: reiner Pfad ohne Host, Query-Parameter (Hash und Einzelwert), Request-Header, und JSON-Body-Feldwerte ohne Hashing. |
| 5 | +Die README-Sektion "Via `withAttributes`" (Zeile 64–80) zeigt `withAttributes(['replay' => 'products'])` ohne zu erklären, dass `replay` ein **reserviertes Attribut** ist, das im `ReplayNamer` (Zeile 24–29) hardcoded Priorität über alle Matcher hat. Ein Leser könnte denken, jedes beliebige Attribut funktioniert automatisch — braucht aber für andere Keys `matchBy('attribute:key')`. |
6 | 6 |
|
7 | | -## Neue Matcher |
| 7 | +## Änderungen |
8 | 8 |
|
9 | | -### 1. `PathMatcher` — Config-String: `path` |
| 9 | +### 1. `README.md` (Zeile 64–80) |
10 | 10 |
|
11 | | -Nur der Pfad ohne Host. Komplement zu `host`. |
| 11 | +Die `withAttributes`-Sektion erweitern: |
| 12 | +- Erklären, dass `replay` ein reserviertes Attribut ist, das alle Matcher umgeht |
| 13 | +- Ein zweites Beispiel ergänzen, das zeigt, wie man eigene Attribute mit `matchBy('attribute:key')` nutzt |
12 | 14 |
|
13 | | -```php |
14 | | -class PathMatcher implements NameMatcher |
15 | | -{ |
16 | | - public function resolve(Request $request): ?string |
17 | | - { |
18 | | - $parsed = parse_url($request->url()); |
19 | | - $path = trim($parsed['path'] ?? '', '/'); |
| 15 | +**Vorher:** |
| 16 | +```markdown |
| 17 | +#### Via `withAttributes` |
20 | 18 |
|
21 | | - return $path !== '' ? $path : null; |
22 | | - } |
23 | | -} |
24 | | -``` |
25 | | - |
26 | | -| Input | Output | |
27 | | -|---|---| |
28 | | -| `https://shop.myshopify.com/api/v1/products` | `api/v1/products` | |
29 | | -| `https://example.com` | `null` | |
30 | | - |
31 | | ---- |
32 | | - |
33 | | -### 2. `QueryHashMatcher` — Config-Strings: `query_hash`, `query_hash:key1,key2` |
34 | | - |
35 | | -Hash der Query-Parameter. Pendant zu `body_hash` für GET-Requests. |
36 | | - |
37 | | -```php |
38 | | -class QueryHashMatcher implements NameMatcher |
39 | | -{ |
40 | | - /** @var list<string> */ |
41 | | - protected array $keys; |
| 19 | +Give each request a unique name using Laravel's `withAttributes`: |
42 | 20 |
|
43 | | - public function __construct(array $keys = []) { $this->keys = $keys; } |
| 21 | +\```php |
| 22 | +it('fetches products and orders via GraphQL', function () { |
| 23 | + Http::replay(); |
44 | 24 |
|
45 | | - public function resolve(Request $request): ?string |
46 | | - { |
47 | | - $parsed = parse_url($request->url()); |
48 | | - $queryString = $parsed['query'] ?? ''; |
| 25 | + $products = Http::withAttributes(['replay' => 'products']) |
| 26 | + ->post('https://shopify.com/graphql', ['query' => '{products{...}}']); |
49 | 27 |
|
50 | | - if ($queryString === '') { |
51 | | - return null; |
52 | | - } |
| 28 | + $orders = Http::withAttributes(['replay' => 'orders']) |
| 29 | + ->post('https://shopify.com/graphql', ['query' => '{orders{...}}']); |
| 30 | +}); |
| 31 | +\``` |
53 | 32 |
|
54 | | - parse_str($queryString, $params); |
55 | | - |
56 | | - if ($this->keys !== []) { |
57 | | - $subset = []; |
58 | | - foreach ($this->keys as $key) { |
59 | | - $subset[$key] = Arr::get($params, $key); |
60 | | - } |
61 | | - $params = $subset; |
62 | | - } |
63 | | - |
64 | | - return substr(md5(json_encode($params) ?: ''), 0, 6); |
65 | | - } |
66 | | -} |
| 33 | +This stores the responses as `products.json` and `orders.json`. |
67 | 34 | ``` |
68 | 35 |
|
69 | | -| Config | Input | Output | |
70 | | -|---|---|---| |
71 | | -| `query_hash` | `?page=2&limit=10` | `a3f1b2` | |
72 | | -| `query_hash:page` | `?page=2&limit=10` | `b4c2d1` (nur page) | |
73 | | -| — | keine Query-Params | `null` | |
| 36 | +**Nachher:** |
| 37 | +```markdown |
| 38 | +#### Via `withAttributes` |
74 | 39 |
|
75 | | ---- |
| 40 | +The `replay` attribute is a **reserved key** that always takes priority over all matchers — no `matchBy` configuration needed: |
76 | 41 |
|
77 | | -### 3. `QueryParamMatcher` — Config-String: `query:key` |
| 42 | +\```php |
| 43 | +it('fetches products and orders via GraphQL', function () { |
| 44 | + Http::replay(); |
78 | 45 |
|
79 | | -Einzelnen Query-Parameter-Wert extrahieren. |
| 46 | + $products = Http::withAttributes(['replay' => 'products']) |
| 47 | + ->post('https://shopify.com/graphql', ['query' => '{products{...}}']); |
80 | 48 |
|
81 | | -```php |
82 | | -class QueryParamMatcher implements NameMatcher |
83 | | -{ |
84 | | - public function __construct(protected string $key) {} |
| 49 | + $orders = Http::withAttributes(['replay' => 'orders']) |
| 50 | + ->post('https://shopify.com/graphql', ['query' => '{orders{...}}']); |
| 51 | +}); |
| 52 | +\``` |
85 | 53 |
|
86 | | - public function resolve(Request $request): ?string |
87 | | - { |
88 | | - $parsed = parse_url($request->url()); |
89 | | - $queryString = $parsed['query'] ?? ''; |
90 | | - parse_str($queryString, $params); |
| 54 | +This stores the responses as `products.json` and `orders.json`. |
91 | 55 |
|
92 | | - $value = Arr::get($params, $this->key); |
| 56 | +For custom attribute keys, use `matchBy('attribute:key')`: |
93 | 57 |
|
94 | | - if ($value === null || $value === '') { |
95 | | - return null; |
96 | | - } |
| 58 | +\```php |
| 59 | +it('uses a custom attribute for naming', function () { |
| 60 | + Http::replay()->matchBy('method', 'attribute:operation'); |
97 | 61 |
|
98 | | - return (string) $value; |
99 | | - } |
100 | | -} |
| 62 | + Http::withAttributes(['operation' => 'getProducts']) |
| 63 | + ->post('https://shopify.com/graphql', ['query' => '{products{...}}']); |
| 64 | +}); |
| 65 | +\``` |
101 | 66 | ``` |
102 | 67 |
|
103 | | -| Config | Input | Output | |
104 | | -|---|---|---| |
105 | | -| `query:action` | `?action=getProducts` | `getProducts` | |
106 | | -| `query:page` | `?page=3` | `3` | |
107 | | -| `query:missing` | `?foo=bar` | `null` | |
108 | | - |
109 | | ---- |
110 | | - |
111 | | -### 4. `HeaderMatcher` — Config-String: `header:key` |
112 | | - |
113 | | -Wert eines bestimmten Request-Headers. |
114 | | - |
115 | | -```php |
116 | | -class HeaderMatcher implements NameMatcher |
117 | | -{ |
118 | | - public function __construct(protected string $key) {} |
119 | | - |
120 | | - public function resolve(Request $request): ?string |
121 | | - { |
122 | | - $value = $request->header($this->key); |
| 68 | +### 2. `resources/boost/skills/http-replay-testing/SKILL.md` (Zeile 76–81) |
123 | 69 |
|
124 | | - if ($value === '') { |
125 | | - return null; |
126 | | - } |
| 70 | +Analog anpassen — `replay` als reserviertes Attribut kennzeichnen: |
127 | 71 |
|
128 | | - return $value; |
129 | | - } |
130 | | -} |
| 72 | +**Vorher:** |
| 73 | +```markdown |
| 74 | +**1. withAttributes** (explicit naming): |
| 75 | +\```php |
| 76 | +Http::withAttributes(['replay' => 'products']) |
| 77 | + ->post('https://shop.com/graphql', ['query' => '{products{...}}']); |
| 78 | +// Stored as: products.json |
| 79 | +\``` |
131 | 80 | ``` |
132 | 81 |
|
133 | | -`Request::header(string)` gibt den String-Wert zurück oder `''` wenn nicht vorhanden. |
134 | | - |
135 | | -| Config | Input | Output | |
136 | | -|---|---|---| |
137 | | -| `header:X-Api-Version` | `X-Api-Version: v2` | `v2` | |
138 | | -| `header:Accept` | `Accept: application/json` | `application/json` | |
139 | | -| `header:Missing` | (nicht gesetzt) | `null` | |
140 | | - |
141 | | ---- |
142 | | - |
143 | | -### 5. `BodyFieldMatcher` — Config-String: `body_field:path` |
144 | | - |
145 | | -Konkreten Wert aus dem JSON-Body per Dot-Notation extrahieren (nicht hashen). |
146 | | - |
147 | | -```php |
148 | | -class BodyFieldMatcher implements NameMatcher |
149 | | -{ |
150 | | - public function __construct(protected string $path) {} |
151 | | - |
152 | | - public function resolve(Request $request): ?string |
153 | | - { |
154 | | - $data = json_decode($request->body(), true); |
155 | | - |
156 | | - if (! is_array($data)) { |
157 | | - return null; |
158 | | - } |
159 | | - |
160 | | - $value = Arr::get($data, $this->path); |
161 | | - |
162 | | - if ($value === null || $value === '') { |
163 | | - return null; |
164 | | - } |
165 | | - |
166 | | - return (string) $value; |
167 | | - } |
168 | | -} |
169 | | -``` |
170 | | - |
171 | | -| Config | Input Body | Output | |
172 | | -|---|---|---| |
173 | | -| `body_field:operationName` | `{"operationName": "GetProducts", ...}` | `GetProducts` | |
174 | | -| `body_field:variables.id` | `{"variables": {"id": "123"}}` | `123` | |
175 | | -| `body_field:missing` | `{"foo": "bar"}` | `null` | |
176 | | -| `body_field:op` | `plain text` | `null` | |
177 | | - |
178 | | -## Änderungen in `ReplayNamer::parseMatchers()` |
179 | | - |
180 | | -Neue Cases in der `match(true)` Expression: |
181 | | - |
182 | | -```php |
183 | | -$field === 'path' => new PathMatcher, |
184 | | -$field === 'query_hash' => new QueryHashMatcher, |
185 | | -str_starts_with($field, 'query_hash:') => new QueryHashMatcher( |
186 | | - explode(',', substr($field, strlen('query_hash:'))) |
187 | | -), |
188 | | -str_starts_with($field, 'query:') => new QueryParamMatcher( |
189 | | - substr($field, strlen('query:')) |
190 | | -), |
191 | | -str_starts_with($field, 'header:') => new HeaderMatcher( |
192 | | - substr($field, strlen('header:')) |
193 | | -), |
194 | | -str_starts_with($field, 'body_field:') => new BodyFieldMatcher( |
195 | | - substr($field, strlen('body_field:')) |
196 | | -), |
| 82 | +**Nachher:** |
| 83 | +```markdown |
| 84 | +**1. withAttributes** (`replay` is a reserved key — always takes priority, no `matchBy` needed): |
| 85 | +\```php |
| 86 | +Http::withAttributes(['replay' => 'products']) |
| 87 | + ->post('https://shop.com/graphql', ['query' => '{products{...}}']); |
| 88 | +// Stored as: products.json |
| 89 | +\``` |
| 90 | + |
| 91 | +For custom attributes, use `matchBy('attribute:key')`: |
| 92 | +\```php |
| 93 | +Http::replay()->matchBy('method', 'attribute:operation'); |
| 94 | +Http::withAttributes(['operation' => 'getProducts']) |
| 95 | + ->post('https://shop.com/graphql', ['query' => '{products{...}}']); |
| 96 | +\``` |
197 | 97 | ``` |
198 | 98 |
|
199 | 99 | ## Betroffene Dateien |
200 | 100 |
|
201 | 101 | | Datei | Änderung | |
202 | 102 | |---|---| |
203 | | -| `src/Matchers/PathMatcher.php` | **NEU** | |
204 | | -| `src/Matchers/QueryHashMatcher.php` | **NEU** | |
205 | | -| `src/Matchers/QueryParamMatcher.php` | **NEU** | |
206 | | -| `src/Matchers/HeaderMatcher.php` | **NEU** | |
207 | | -| `src/Matchers/BodyFieldMatcher.php` | **NEU** | |
208 | | -| `src/ReplayNamer.php` | 5 neue Cases in `parseMatchers()` + Imports | |
209 | | -| `tests/Matchers/PathMatcherTest.php` | **NEU** | |
210 | | -| `tests/Matchers/QueryHashMatcherTest.php` | **NEU** | |
211 | | -| `tests/Matchers/QueryParamMatcherTest.php` | **NEU** | |
212 | | -| `tests/Matchers/HeaderMatcherTest.php` | **NEU** | |
213 | | -| `tests/Matchers/BodyFieldMatcherTest.php` | **NEU** | |
214 | | -| `tests/ReplayNamerTest.php` | Tests für neue Config-Strings | |
215 | | -| `config/http-replay.php` | Kommentar bei `match_by` erweitern | |
216 | | -| `README.md` | Matcher-Tabelle erweitern | |
217 | | -| `resources/boost/skills/http-replay-testing/SKILL.md` | Matcher-Tabelle erweitern | |
| 103 | +| `README.md` | withAttributes-Sektion erweitern (Zeile 64–80) | |
| 104 | +| `resources/boost/skills/http-replay-testing/SKILL.md` | withAttributes-Sektion erweitern (Zeile 76–81) | |
218 | 105 |
|
219 | 106 | ## Verifikation |
220 | 107 |
|
221 | | -```bash |
222 | | -composer test |
223 | | -composer analyse |
224 | | -``` |
| 108 | +Reine Doku-Änderung — kein Code betroffen. Kurze Prüfung: |
| 109 | +- README lesen und sicherstellen, dass die Beispiele korrekt sind |
| 110 | +- SKILL.md lesen und Konsistenz mit README prüfen |
0 commit comments