Skip to content

Commit 8963f6b

Browse files
committed
Clarify that replay is a reserved attribute key in withAttributes docs
1 parent 946e350 commit 8963f6b

3 files changed

Lines changed: 93 additions & 189 deletions

File tree

Lines changed: 73 additions & 187 deletions
Original file line numberDiff line numberDiff line change
@@ -1,224 +1,110 @@
1-
# Plan: 5 neue Matcher hinzufügen
1+
# Plan: withAttributes-Doku in README und SKILL.md präzisieren
22

33
## Context
44

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')`.
66

7-
## Neue Matcher
7+
## Änderungen
88

9-
### 1. `PathMatcher` — Config-String: `path`
9+
### 1. `README.md` (Zeile 64–80)
1010

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
1214

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`
2018

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`:
4220

43-
public function __construct(array $keys = []) { $this->keys = $keys; }
21+
\```php
22+
it('fetches products and orders via GraphQL', function () {
23+
Http::replay();
4424

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{...}}']);
4927

50-
if ($queryString === '') {
51-
return null;
52-
}
28+
$orders = Http::withAttributes(['replay' => 'orders'])
29+
->post('https://shopify.com/graphql', ['query' => '{orders{...}}']);
30+
});
31+
\```
5332

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`.
6734
```
6835

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`
7439

75-
---
40+
The `replay` attribute is a **reserved key** that always takes priority over all matchers — no `matchBy` configuration needed:
7641

77-
### 3. `QueryParamMatcher` — Config-String: `query:key`
42+
\```php
43+
it('fetches products and orders via GraphQL', function () {
44+
Http::replay();
7845

79-
Einzelnen Query-Parameter-Wert extrahieren.
46+
$products = Http::withAttributes(['replay' => 'products'])
47+
->post('https://shopify.com/graphql', ['query' => '{products{...}}']);
8048

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+
\```
8553

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`.
9155

92-
$value = Arr::get($params, $this->key);
56+
For custom attribute keys, use `matchBy('attribute:key')`:
9357

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');
9761

98-
return (string) $value;
99-
}
100-
}
62+
Http::withAttributes(['operation' => 'getProducts'])
63+
->post('https://shopify.com/graphql', ['query' => '{products{...}}']);
64+
});
65+
\```
10166
```
10267

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)
12369

124-
if ($value === '') {
125-
return null;
126-
}
70+
Analog anpassen — `replay` als reserviertes Attribut kennzeichnen:
12771

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+
\```
13180
```
13281

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+
\```
19797
```
19898

19999
## Betroffene Dateien
200100

201101
| Datei | Änderung |
202102
|---|---|
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) |
218105

219106
## Verifikation
220107

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

README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ When multiple requests go to the same URL (e.g. GraphQL endpoints), you need to
6363

6464
#### Via `withAttributes`
6565

66-
Give each request a unique name using Laravel's `withAttributes`:
66+
The `replay` attribute is a **reserved key** that always takes priority over all matchers — no `matchBy` configuration needed:
6767

6868
```php
6969
it('fetches products and orders via GraphQL', function () {
@@ -79,6 +79,17 @@ it('fetches products and orders via GraphQL', function () {
7979

8080
This stores the responses as `products.json` and `orders.json`.
8181

82+
For custom attribute keys, use `matchBy('attribute:key')`:
83+
84+
```php
85+
it('uses a custom attribute for naming', function () {
86+
Http::replay()->matchBy('method', 'attribute:operation');
87+
88+
Http::withAttributes(['operation' => 'getProducts'])
89+
->post('https://shopify.com/graphql', ['query' => '{products{...}}']);
90+
});
91+
```
92+
8293
#### Via `matchBy` with Body Hash
8394

8495
Automatically distinguish requests by including the request body hash in the filename:

resources/boost/skills/http-replay-testing/SKILL.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,20 @@ Default: `['method', 'url']`. Aliases: `'http_method'`, `'http_attribute:key'`.
7373

7474
Three approaches:
7575

76-
**1. withAttributes** (explicit naming):
76+
**1. withAttributes** (`replay` is a reserved key — always takes priority, no `matchBy` needed):
7777
```php
7878
Http::withAttributes(['replay' => 'products'])
7979
->post('https://shop.com/graphql', ['query' => '{products{...}}']);
8080
// Stored as: products.json
8181
```
8282

83+
For custom attributes, use `matchBy('attribute:key')`:
84+
```php
85+
Http::replay()->matchBy('method', 'attribute:operation');
86+
Http::withAttributes(['operation' => 'getProducts'])
87+
->post('https://shop.com/graphql', ['query' => '{products{...}}']);
88+
```
89+
8390
**2. Body hash** (automatic):
8491
```php
8592
Http::replay()->matchBy('url', 'body_hash');

0 commit comments

Comments
 (0)