Skip to content

Commit ae69ceb

Browse files
committed
Add tests and brands webhook
1 parent 3c3de74 commit ae69ceb

4 files changed

Lines changed: 241 additions & 16 deletions

File tree

README.md

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -329,14 +329,31 @@ foreach ($client->iterateProducts(lang: [Language::ALL]) as $product) {
329329

330330
Pobo can notify your application when content changes in the administration. Register a webhook URL in the Pobo e-shop settings; Pobo POSTs to it with HMAC-SHA256 signed requests.
331331

332-
| Event | Constant | Fired when |
333-
|----------------------|-----------------------------------|----------------------------------------|
334-
| `Products.update` | `WebhookEvent::PRODUCTS_UPDATE` | a product was edited and saved in Pobo |
335-
| `Categories.update` | `WebhookEvent::CATEGORIES_UPDATE` | a category was edited |
336-
| `Blogs.update` | `WebhookEvent::BLOGS_UPDATE` | a blog was edited |
332+
### Supported events
333+
334+
| Event | Constant | Fired when |
335+
|------------------------|-------------------------------------|-------------------------------------|
336+
| `Products.create` | `WebhookEvent::PRODUCTS_CREATE` | a product was created |
337+
| `Products.update` | `WebhookEvent::PRODUCTS_UPDATE` | a product was edited |
338+
| `Products.delete` | `WebhookEvent::PRODUCTS_DELETE` | a product was soft-deleted |
339+
| `Categories.create` | `WebhookEvent::CATEGORIES_CREATE` | a category was created |
340+
| `Categories.update` | `WebhookEvent::CATEGORIES_UPDATE` | a category was edited |
341+
| `Categories.delete` | `WebhookEvent::CATEGORIES_DELETE` | a category was soft-deleted |
342+
| `Brands.create` | `WebhookEvent::BRANDS_CREATE` | a brand was created |
343+
| `Brands.update` | `WebhookEvent::BRANDS_UPDATE` | a brand was edited |
344+
| `Brands.delete` | `WebhookEvent::BRANDS_DELETE` | a brand was soft-deleted |
345+
| `Blogs.create` | `WebhookEvent::BLOGS_CREATE` | a blog was created |
346+
| `Blogs.update` | `WebhookEvent::BLOGS_UPDATE` | a blog was edited |
347+
| `Blogs.delete` | `WebhookEvent::BLOGS_DELETE` | a blog was soft-deleted |
348+
349+
The enum exposes `isCreate()` / `isUpdate()` / `isDelete()` helpers if you only need to branch on the lifecycle action regardless of entity type.
350+
351+
> **Note:** the API also defines `Languages.create/update/delete` events. The SDK does not expose them yet — payloads carrying these events will throw `WebhookException::unknownEvent()`.
337352
338353
The webhook is a notification only — it does not carry the changed entities. Use it as a trigger to re-sync via the export endpoints (typically combined with `lastUpdateFrom`).
339354

355+
### Basic usage
356+
340357
```php
341358
use Pobo\Sdk\WebhookHandler;
342359
use Pobo\Sdk\Enum\WebhookEvent;
@@ -348,9 +365,18 @@ try {
348365
$payload = $handler->handleFromGlobals();
349366

350367
match ($payload->event) {
351-
WebhookEvent::PRODUCTS_UPDATE => syncProducts($client),
352-
WebhookEvent::CATEGORIES_UPDATE => syncCategories($client),
353-
WebhookEvent::BLOGS_UPDATE => syncBlogs($client),
368+
WebhookEvent::PRODUCTS_CREATE,
369+
WebhookEvent::PRODUCTS_UPDATE,
370+
WebhookEvent::PRODUCTS_DELETE => syncProducts($client),
371+
WebhookEvent::CATEGORIES_CREATE,
372+
WebhookEvent::CATEGORIES_UPDATE,
373+
WebhookEvent::CATEGORIES_DELETE => syncCategories($client),
374+
WebhookEvent::BRANDS_CREATE,
375+
WebhookEvent::BRANDS_UPDATE,
376+
WebhookEvent::BRANDS_DELETE => syncBrands($client),
377+
WebhookEvent::BLOGS_CREATE,
378+
WebhookEvent::BLOGS_UPDATE,
379+
WebhookEvent::BLOGS_DELETE => syncBlogs($client),
354380
};
355381

356382
http_response_code(200);
@@ -362,6 +388,20 @@ try {
362388

363389
`$payload` exposes `event` (`WebhookEvent` enum), `timestamp` (`DateTimeInterface`), and `eshopId` (int).
364390

391+
### Branching only on lifecycle action
392+
393+
```php
394+
$payload = $handler->handleFromGlobals();
395+
396+
if ($payload->event->isDelete()) {
397+
invalidateCache($payload->event, $payload->eshopId);
398+
}
399+
400+
if ($payload->event->isCreate() || $payload->event->isUpdate()) {
401+
triggerResync($payload->event, $payload->eshopId);
402+
}
403+
```
404+
365405
## Error Handling
366406

367407
```php

src/Enum/WebhookEvent.php

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,21 @@
66

77
enum WebhookEvent: string
88
{
9+
case PRODUCTS_CREATE = 'Products.create';
910
case PRODUCTS_UPDATE = 'Products.update';
11+
case PRODUCTS_DELETE = 'Products.delete';
12+
13+
case CATEGORIES_CREATE = 'Categories.create';
1014
case CATEGORIES_UPDATE = 'Categories.update';
15+
case CATEGORIES_DELETE = 'Categories.delete';
16+
17+
case BRANDS_CREATE = 'Brands.create';
18+
case BRANDS_UPDATE = 'Brands.update';
19+
case BRANDS_DELETE = 'Brands.delete';
20+
21+
case BLOGS_CREATE = 'Blogs.create';
1122
case BLOGS_UPDATE = 'Blogs.update';
23+
case BLOGS_DELETE = 'Blogs.delete';
1224

1325
/**
1426
* @return array<string>
@@ -20,11 +32,21 @@ public static function values(): array
2032

2133
public static function fromString(string $value): ?self
2234
{
23-
return match ($value) {
24-
'Products.update' => self::PRODUCTS_UPDATE,
25-
'Categories.update' => self::CATEGORIES_UPDATE,
26-
'Blogs.update' => self::BLOGS_UPDATE,
27-
default => null,
28-
};
35+
return self::tryFrom($value);
36+
}
37+
38+
public function isCreate(): bool
39+
{
40+
return str_ends_with($this->value, '.create');
41+
}
42+
43+
public function isUpdate(): bool
44+
{
45+
return str_ends_with($this->value, '.update');
46+
}
47+
48+
public function isDelete(): bool
49+
{
50+
return str_ends_with($this->value, '.delete');
2951
}
3052
}

tests/Enum/WebhookEventTest.php

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,28 +11,70 @@ final class WebhookEventTest extends TestCase
1111
{
1212
public function testAllCasesExist(): void
1313
{
14+
$this->assertSame('Products.create', WebhookEvent::PRODUCTS_CREATE->value);
1415
$this->assertSame('Products.update', WebhookEvent::PRODUCTS_UPDATE->value);
16+
$this->assertSame('Products.delete', WebhookEvent::PRODUCTS_DELETE->value);
17+
18+
$this->assertSame('Categories.create', WebhookEvent::CATEGORIES_CREATE->value);
1519
$this->assertSame('Categories.update', WebhookEvent::CATEGORIES_UPDATE->value);
20+
$this->assertSame('Categories.delete', WebhookEvent::CATEGORIES_DELETE->value);
21+
22+
$this->assertSame('Brands.create', WebhookEvent::BRANDS_CREATE->value);
23+
$this->assertSame('Brands.update', WebhookEvent::BRANDS_UPDATE->value);
24+
$this->assertSame('Brands.delete', WebhookEvent::BRANDS_DELETE->value);
25+
26+
$this->assertSame('Blogs.create', WebhookEvent::BLOGS_CREATE->value);
1627
$this->assertSame('Blogs.update', WebhookEvent::BLOGS_UPDATE->value);
28+
$this->assertSame('Blogs.delete', WebhookEvent::BLOGS_DELETE->value);
1729
}
1830

1931
public function testValues(): void
2032
{
2133
$values = WebhookEvent::values();
2234

35+
$this->assertCount(12, $values);
36+
$this->assertContains('Products.create', $values);
2337
$this->assertContains('Products.update', $values);
38+
$this->assertContains('Products.delete', $values);
39+
$this->assertContains('Categories.create', $values);
2440
$this->assertContains('Categories.update', $values);
41+
$this->assertContains('Categories.delete', $values);
42+
$this->assertContains('Brands.create', $values);
43+
$this->assertContains('Brands.update', $values);
44+
$this->assertContains('Brands.delete', $values);
45+
$this->assertContains('Blogs.create', $values);
2546
$this->assertContains('Blogs.update', $values);
26-
$this->assertCount(3, $values);
47+
$this->assertContains('Blogs.delete', $values);
2748
}
2849

29-
public function testFromStringReturnsCorrectEnum(): void
50+
public function testFromStringReturnsCorrectEnumForExistingEvents(): void
3051
{
3152
$this->assertSame(WebhookEvent::PRODUCTS_UPDATE, WebhookEvent::fromString('Products.update'));
3253
$this->assertSame(WebhookEvent::CATEGORIES_UPDATE, WebhookEvent::fromString('Categories.update'));
3354
$this->assertSame(WebhookEvent::BLOGS_UPDATE, WebhookEvent::fromString('Blogs.update'));
3455
}
3556

57+
public function testFromStringReturnsCorrectEnumForCreateEvents(): void
58+
{
59+
$this->assertSame(WebhookEvent::PRODUCTS_CREATE, WebhookEvent::fromString('Products.create'));
60+
$this->assertSame(WebhookEvent::CATEGORIES_CREATE, WebhookEvent::fromString('Categories.create'));
61+
$this->assertSame(WebhookEvent::BRANDS_CREATE, WebhookEvent::fromString('Brands.create'));
62+
$this->assertSame(WebhookEvent::BLOGS_CREATE, WebhookEvent::fromString('Blogs.create'));
63+
}
64+
65+
public function testFromStringReturnsCorrectEnumForDeleteEvents(): void
66+
{
67+
$this->assertSame(WebhookEvent::PRODUCTS_DELETE, WebhookEvent::fromString('Products.delete'));
68+
$this->assertSame(WebhookEvent::CATEGORIES_DELETE, WebhookEvent::fromString('Categories.delete'));
69+
$this->assertSame(WebhookEvent::BRANDS_DELETE, WebhookEvent::fromString('Brands.delete'));
70+
$this->assertSame(WebhookEvent::BLOGS_DELETE, WebhookEvent::fromString('Blogs.delete'));
71+
}
72+
73+
public function testFromStringReturnsCorrectEnumForBrandUpdate(): void
74+
{
75+
$this->assertSame(WebhookEvent::BRANDS_UPDATE, WebhookEvent::fromString('Brands.update'));
76+
}
77+
3678
public function testFromStringReturnsNullForUnknown(): void
3779
{
3880
$this->assertNull(WebhookEvent::fromString('Unknown.event'));
@@ -41,5 +83,40 @@ public function testFromStringReturnsNullForUnknown(): void
4183
$this->assertNull(WebhookEvent::fromString('PRODUCTS.UPDATE'));
4284
$this->assertNull(WebhookEvent::fromString('blogs.update'));
4385
$this->assertNull(WebhookEvent::fromString('BLOGS.UPDATE'));
86+
$this->assertNull(WebhookEvent::fromString('brands.create'));
87+
$this->assertNull(WebhookEvent::fromString('Languages.create')); // not in SDK yet
88+
}
89+
90+
public function testIsCreateHelper(): void
91+
{
92+
$this->assertTrue(WebhookEvent::PRODUCTS_CREATE->isCreate());
93+
$this->assertTrue(WebhookEvent::CATEGORIES_CREATE->isCreate());
94+
$this->assertTrue(WebhookEvent::BRANDS_CREATE->isCreate());
95+
$this->assertTrue(WebhookEvent::BLOGS_CREATE->isCreate());
96+
97+
$this->assertFalse(WebhookEvent::PRODUCTS_UPDATE->isCreate());
98+
$this->assertFalse(WebhookEvent::BRANDS_DELETE->isCreate());
99+
}
100+
101+
public function testIsUpdateHelper(): void
102+
{
103+
$this->assertTrue(WebhookEvent::PRODUCTS_UPDATE->isUpdate());
104+
$this->assertTrue(WebhookEvent::CATEGORIES_UPDATE->isUpdate());
105+
$this->assertTrue(WebhookEvent::BRANDS_UPDATE->isUpdate());
106+
$this->assertTrue(WebhookEvent::BLOGS_UPDATE->isUpdate());
107+
108+
$this->assertFalse(WebhookEvent::PRODUCTS_CREATE->isUpdate());
109+
$this->assertFalse(WebhookEvent::BRANDS_DELETE->isUpdate());
110+
}
111+
112+
public function testIsDeleteHelper(): void
113+
{
114+
$this->assertTrue(WebhookEvent::PRODUCTS_DELETE->isDelete());
115+
$this->assertTrue(WebhookEvent::CATEGORIES_DELETE->isDelete());
116+
$this->assertTrue(WebhookEvent::BRANDS_DELETE->isDelete());
117+
$this->assertTrue(WebhookEvent::BLOGS_DELETE->isDelete());
118+
119+
$this->assertFalse(WebhookEvent::PRODUCTS_CREATE->isDelete());
120+
$this->assertFalse(WebhookEvent::BRANDS_UPDATE->isDelete());
44121
}
45122
}

tests/WebhookHandlerTest.php

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,4 +158,90 @@ public function testHandleWithDifferentTimestampFormats(): void
158158

159159
$this->assertInstanceOf(\DateTimeInterface::class, $result->timestamp);
160160
}
161+
162+
/**
163+
* @return array<string, array{0: string, 1: WebhookEvent}>
164+
*/
165+
public static function brandEventProvider(): array
166+
{
167+
return [
168+
'Brands.create' => ['Brands.create', WebhookEvent::BRANDS_CREATE],
169+
'Brands.update' => ['Brands.update', WebhookEvent::BRANDS_UPDATE],
170+
'Brands.delete' => ['Brands.delete', WebhookEvent::BRANDS_DELETE],
171+
];
172+
}
173+
174+
/**
175+
* @dataProvider brandEventProvider
176+
*/
177+
public function testHandleValidBrandEvent(string $eventValue, WebhookEvent $expected): void
178+
{
179+
$payload = json_encode([
180+
'event' => $eventValue,
181+
'timestamp' => '2026-05-05T14:30:00+02:00',
182+
'eshop_id' => 789,
183+
]);
184+
185+
$signature = hash_hmac('sha256', $payload, self::WEBHOOK_SECRET);
186+
187+
$result = $this->handler->handle($payload, $signature);
188+
189+
$this->assertSame($expected, $result->event);
190+
$this->assertSame(789, $result->eshopId);
191+
}
192+
193+
/**
194+
* @return array<string, array{0: string, 1: WebhookEvent}>
195+
*/
196+
public static function createDeleteEventProvider(): array
197+
{
198+
return [
199+
'Products.create' => ['Products.create', WebhookEvent::PRODUCTS_CREATE],
200+
'Products.delete' => ['Products.delete', WebhookEvent::PRODUCTS_DELETE],
201+
'Categories.create' => ['Categories.create', WebhookEvent::CATEGORIES_CREATE],
202+
'Categories.delete' => ['Categories.delete', WebhookEvent::CATEGORIES_DELETE],
203+
'Blogs.create' => ['Blogs.create', WebhookEvent::BLOGS_CREATE],
204+
'Blogs.delete' => ['Blogs.delete', WebhookEvent::BLOGS_DELETE],
205+
];
206+
}
207+
208+
/**
209+
* @dataProvider createDeleteEventProvider
210+
*/
211+
public function testHandleValidCreateOrDeleteEvent(string $eventValue, WebhookEvent $expected): void
212+
{
213+
$payload = json_encode([
214+
'event' => $eventValue,
215+
'timestamp' => '2026-05-05T14:30:00+02:00',
216+
'eshop_id' => 555,
217+
]);
218+
219+
$signature = hash_hmac('sha256', $payload, self::WEBHOOK_SECRET);
220+
221+
$result = $this->handler->handle($payload, $signature);
222+
223+
$this->assertSame($expected, $result->event);
224+
}
225+
226+
public function testHandleBrandsCreateMatchableInUserCode(): void
227+
{
228+
// Verifies the typical match() pattern works for brand events.
229+
$payload = json_encode([
230+
'event' => 'Brands.create',
231+
'timestamp' => '2026-05-05T14:30:00+02:00',
232+
'eshop_id' => 1,
233+
]);
234+
235+
$signature = hash_hmac('sha256', $payload, self::WEBHOOK_SECRET);
236+
$result = $this->handler->handle($payload, $signature);
237+
238+
$branch = match ($result->event) {
239+
WebhookEvent::BRANDS_CREATE => 'created',
240+
WebhookEvent::BRANDS_UPDATE => 'updated',
241+
WebhookEvent::BRANDS_DELETE => 'deleted',
242+
default => 'other',
243+
};
244+
245+
$this->assertSame('created', $branch);
246+
}
161247
}

0 commit comments

Comments
 (0)