symfonia-ai-tools 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +489 -0
- package/bin/cli.mjs +35 -0
- package/lib/installer.mjs +495 -0
- package/lib/questions.mjs +332 -0
- package/lib/ui.mjs +76 -0
- package/lib/utils.mjs +231 -0
- package/package.json +26 -0
- package/templates/base/CLAUDE.md +34 -0
- package/templates/base/_ai/_guidelines_header.md +70 -0
- package/templates/base/_ai/context/README.md +20 -0
- package/templates/base/_ai/prompts/codereview.prompt.md +324 -0
- package/templates/base/_ai/prompts/duplicate-code-analysis.prompt.md +128 -0
- package/templates/base/_ai/prompts/figma-analysis.prompt.md +155 -0
- package/templates/base/_ai/prompts/security-review.prompt.md +46 -0
- package/templates/base/_ai/skills/README.md +80 -0
- package/templates/base/_ai/skills/TEMPLATE.md +106 -0
- package/templates/base/_ai/skills/babysit-prs/SKILL.md +105 -0
- package/templates/base/_ai/skills/debug/SKILL.md +93 -0
- package/templates/base/_ai/skills/fill-worklogs/SKILL.md +158 -0
- package/templates/base/_ai/skills/hotfix/SKILL.md +52 -0
- package/templates/base/_ai/skills/jira-task/SKILL.md +170 -0
- package/templates/base/_ai/skills/my-prs/SKILL.md +78 -0
- package/templates/base/_ai/skills/pr-dashboard/SKILL.md +43 -0
- package/templates/base/_ai/skills/pr-prepare/SKILL.md +106 -0
- package/templates/base/_ai/skills/refactor/SKILL.md +87 -0
- package/templates/base/_ai/skills/write-tests/SKILL.md +109 -0
- package/templates/base/_claude/settings.local.json +37 -0
- package/templates/base/_cursor/rules/global.mdc +7 -0
- package/templates/base/_editorconfig +18 -0
- package/templates/base/_gemini/settings.json +3 -0
- package/templates/base/_github/copilot-instructions.md +1 -0
- package/templates/base/_github/pull_request_template.md +23 -0
- package/templates/base/_gitignore +22 -0
- package/templates/base/_junie/guidelines.md +1 -0
- package/templates/base/commit-instructions.md +92 -0
- package/templates/packs/docker/_ai/instructions/docker.instructions.md +193 -0
- package/templates/packs/docker/_guidelines.md +10 -0
- package/templates/packs/docker/pack.json +8 -0
- package/templates/packs/laravel/_ai/instructions/api-resource.instructions.md +251 -0
- package/templates/packs/laravel/_ai/instructions/module.instructions.md +133 -0
- package/templates/packs/laravel/_ai/instructions/service-repository.instructions.md +215 -0
- package/templates/packs/laravel/_ai/instructions/testing.instructions.md +278 -0
- package/templates/packs/laravel/_ai/skills/migration/SKILL.md +172 -0
- package/templates/packs/laravel/_ai/skills/new-endpoint/SKILL.md +165 -0
- package/templates/packs/laravel/_ai/skills/new-module/SKILL.md +208 -0
- package/templates/packs/laravel/_ai/skills/queued-job/SKILL.md +248 -0
- package/templates/packs/laravel/_ai/skills/testing-feature/SKILL.md +196 -0
- package/templates/packs/laravel/_ai/skills/testing-manual/SKILL.md +186 -0
- package/templates/packs/laravel/_ai/skills/testing-unit/SKILL.md +200 -0
- package/templates/packs/laravel/_guidelines.md +25 -0
- package/templates/packs/laravel/pack.json +6 -0
- package/templates/packs/playwright/_ai/instructions/playwright.instructions.md +219 -0
- package/templates/packs/playwright/_ai/skills/playwright/README.md +194 -0
- package/templates/packs/playwright/_ai/skills/playwright/SKILL.md +1245 -0
- package/templates/packs/playwright/_ai/skills/playwright-codereview/SKILL.md +642 -0
- package/templates/packs/playwright/_ai/skills/playwright-record/README.md +87 -0
- package/templates/packs/playwright/_ai/skills/playwright-record/SKILL.md +564 -0
- package/templates/packs/playwright/_guidelines.md +12 -0
- package/templates/packs/playwright/pack.json +9 -0
- package/templates/packs/storybook/_ai/instructions/storybook.instructions.md +181 -0
- package/templates/packs/storybook/pack.json +6 -0
- package/templates/packs/vitest/_ai/instructions/vitest.instructions.md +688 -0
- package/templates/packs/vitest/pack.json +6 -0
- package/templates/packs/vue3/_ai/instructions/api.instructions.md +163 -0
- package/templates/packs/vue3/_ai/instructions/coding-conventions.instructions.md +160 -0
- package/templates/packs/vue3/_ai/instructions/composables.instructions.md +218 -0
- package/templates/packs/vue3/_ai/instructions/forms.instructions.md +227 -0
- package/templates/packs/vue3/_ai/instructions/store.instructions.md +504 -0
- package/templates/packs/vue3/_ai/instructions/vue.instructions.md +339 -0
- package/templates/packs/vue3/_ai/skills/api-integration/SKILL.md +195 -0
- package/templates/packs/vue3/_ai/skills/new-component/SKILL.md +133 -0
- package/templates/packs/vue3/_ai/skills/new-module/SKILL.md +177 -0
- package/templates/packs/vue3/_guidelines.md +45 -0
- package/templates/packs/vue3/pack.json +11 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# Skill: Queued Job (Laravel)
|
|
2
|
+
|
|
3
|
+
## Trigger
|
|
4
|
+
Use when creating asynchronous background processing: jobs, events with listeners, scheduled tasks.
|
|
5
|
+
|
|
6
|
+
## Input
|
|
7
|
+
- What the job does (e.g. "send export email", "sync external API")
|
|
8
|
+
- Trigger: event / manual dispatch / schedule
|
|
9
|
+
- Input data needed
|
|
10
|
+
- Expected output / side effects
|
|
11
|
+
|
|
12
|
+
## Steps
|
|
13
|
+
|
|
14
|
+
### 1. Decide architecture
|
|
15
|
+
|
|
16
|
+
- **Simple async task** → Job only
|
|
17
|
+
- **Something happened, react to it** → Event + Listener(s)
|
|
18
|
+
- **Recurring task** → Job + Schedule
|
|
19
|
+
- **Multiple reactions to one trigger** → Event + multiple Listeners
|
|
20
|
+
|
|
21
|
+
### 2. Create Event (if event-driven)
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
docker exec {{DOCKER_CONTAINER}} php artisan make:event [Entity][Action]Event --path=Modules/[Module]/Domain/Events
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```php
|
|
28
|
+
declare(strict_types=1);
|
|
29
|
+
|
|
30
|
+
namespace Modules\[Module]\Domain\Events;
|
|
31
|
+
|
|
32
|
+
use Illuminate\Foundation\Events\Dispatchable;
|
|
33
|
+
use Illuminate\Queue\SerializesModels;
|
|
34
|
+
|
|
35
|
+
class [Entity][Action]Event
|
|
36
|
+
{
|
|
37
|
+
use Dispatchable;
|
|
38
|
+
use SerializesModels;
|
|
39
|
+
|
|
40
|
+
public function __construct(
|
|
41
|
+
public readonly int $entityId,
|
|
42
|
+
public readonly int $userId,
|
|
43
|
+
// ... minimal data needed by listeners
|
|
44
|
+
) {}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Rules:
|
|
49
|
+
- Pass IDs, not full models (serialization safety)
|
|
50
|
+
- Keep payload minimal — listeners query fresh data
|
|
51
|
+
- Use `readonly` properties
|
|
52
|
+
|
|
53
|
+
### 3. Create Job
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
docker exec {{DOCKER_CONTAINER}} php artisan make:job [Action][Entity]Job --path=Modules/[Module]/Domain/Jobs
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```php
|
|
60
|
+
declare(strict_types=1);
|
|
61
|
+
|
|
62
|
+
namespace Modules\[Module]\Domain\Jobs;
|
|
63
|
+
|
|
64
|
+
use Illuminate\Bus\Queueable;
|
|
65
|
+
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
66
|
+
use Illuminate\Foundation\Bus\Dispatchable;
|
|
67
|
+
use Illuminate\Queue\InteractsWithQueue;
|
|
68
|
+
use Illuminate\Queue\SerializesModels;
|
|
69
|
+
|
|
70
|
+
class [Action][Entity]Job implements ShouldQueue
|
|
71
|
+
{
|
|
72
|
+
use Dispatchable;
|
|
73
|
+
use InteractsWithQueue;
|
|
74
|
+
use Queueable;
|
|
75
|
+
use SerializesModels;
|
|
76
|
+
|
|
77
|
+
public int $tries = 3;
|
|
78
|
+
public int $backoff = 60; // seconds between retries
|
|
79
|
+
public int $timeout = 120; // max execution time
|
|
80
|
+
|
|
81
|
+
public function __construct(
|
|
82
|
+
private readonly int $entityId,
|
|
83
|
+
private readonly int $userId,
|
|
84
|
+
) {}
|
|
85
|
+
|
|
86
|
+
public function handle([Entity]Service $service): void
|
|
87
|
+
{
|
|
88
|
+
// Fetch fresh data
|
|
89
|
+
$entity = $service->findOrFail($this->entityId);
|
|
90
|
+
|
|
91
|
+
// Business logic
|
|
92
|
+
$service->processAction($entity);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
public function failed(\Throwable $exception): void
|
|
96
|
+
{
|
|
97
|
+
// Log failure, notify admin, update status
|
|
98
|
+
\Log::error("[Action][Entity]Job failed", [
|
|
99
|
+
'entity_id' => $this->entityId,
|
|
100
|
+
'error' => $exception->getMessage(),
|
|
101
|
+
]);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Rules:
|
|
107
|
+
- Always set `$tries`, `$backoff`, `$timeout`
|
|
108
|
+
- Implement `failed()` for error handling
|
|
109
|
+
- Use constructor injection for dependencies in `handle()`
|
|
110
|
+
- Pass IDs in constructor, fetch fresh data in `handle()`
|
|
111
|
+
- Job must be idempotent (safe to retry)
|
|
112
|
+
|
|
113
|
+
### 4. Create Listener (if event-driven)
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
docker exec {{DOCKER_CONTAINER}} php artisan make:listener [Action]On[Event]Listener --path=Modules/[Module]/Domain/Listeners
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
```php
|
|
120
|
+
declare(strict_types=1);
|
|
121
|
+
|
|
122
|
+
namespace Modules\[Module]\Domain\Listeners;
|
|
123
|
+
|
|
124
|
+
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
125
|
+
use Modules\[Module]\Domain\Events\[Entity][Action]Event;
|
|
126
|
+
|
|
127
|
+
class [Action]On[Event]Listener implements ShouldQueue
|
|
128
|
+
{
|
|
129
|
+
public int $tries = 3;
|
|
130
|
+
public int $backoff = 60;
|
|
131
|
+
|
|
132
|
+
public function __construct(
|
|
133
|
+
private readonly [Entity]Service $service,
|
|
134
|
+
) {}
|
|
135
|
+
|
|
136
|
+
public function handle([Entity][Action]Event $event): void
|
|
137
|
+
{
|
|
138
|
+
$this->service->doAction($event->entityId);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
public function shouldQueue([Entity][Action]Event $event): bool
|
|
142
|
+
{
|
|
143
|
+
// Optional: conditionally skip processing
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### 5. Register Event-Listener mapping
|
|
150
|
+
|
|
151
|
+
In module's `ServiceProvider`:
|
|
152
|
+
|
|
153
|
+
```php
|
|
154
|
+
protected $listen = [
|
|
155
|
+
[Entity][Action]Event::class => [
|
|
156
|
+
[Action]On[Event]Listener::class,
|
|
157
|
+
],
|
|
158
|
+
];
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### 6. Dispatch
|
|
162
|
+
|
|
163
|
+
From service/controller:
|
|
164
|
+
|
|
165
|
+
```php
|
|
166
|
+
// Direct job dispatch
|
|
167
|
+
[Action][Entity]Job::dispatch($entity->getId(), $user->getId());
|
|
168
|
+
|
|
169
|
+
// With delay
|
|
170
|
+
[Action][Entity]Job::dispatch($entityId, $userId)
|
|
171
|
+
->delay(now()->addMinutes(5));
|
|
172
|
+
|
|
173
|
+
// Event dispatch
|
|
174
|
+
[Entity][Action]Event::dispatch($entity->getId(), $user->getId());
|
|
175
|
+
|
|
176
|
+
// On specific queue
|
|
177
|
+
[Action][Entity]Job::dispatch($entityId, $userId)
|
|
178
|
+
->onQueue('exports');
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### 7. Schedule (if recurring)
|
|
182
|
+
|
|
183
|
+
In `Console/Kernel.php` or module's ServiceProvider:
|
|
184
|
+
|
|
185
|
+
```php
|
|
186
|
+
$schedule->job(new [Action][Entity]Job)->dailyAt('03:00');
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### 8. Write tests
|
|
190
|
+
|
|
191
|
+
```php
|
|
192
|
+
// Test job dispatched
|
|
193
|
+
public function test_action_dispatches_job(): void
|
|
194
|
+
{
|
|
195
|
+
Queue::fake();
|
|
196
|
+
|
|
197
|
+
$this->service->triggerAction($this->entity);
|
|
198
|
+
|
|
199
|
+
Queue::assertPushed([Action][Entity]Job::class, function ($job) {
|
|
200
|
+
return $job->entityId === $this->entity->getId();
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Test job execution
|
|
205
|
+
public function test_job_processes_entity(): void
|
|
206
|
+
{
|
|
207
|
+
$job = new [Action][Entity]Job($this->entity->getId(), $this->user->getId());
|
|
208
|
+
$job->handle(app([Entity]Service::class));
|
|
209
|
+
|
|
210
|
+
$this->entity->refresh();
|
|
211
|
+
$this->assertEquals('processed', $this->entity->getStatus()->value);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Test event triggers listeners
|
|
215
|
+
public function test_event_triggers_listener(): void
|
|
216
|
+
{
|
|
217
|
+
Event::fake();
|
|
218
|
+
|
|
219
|
+
[Entity][Action]Event::dispatch($this->entity->getId(), $this->user->getId());
|
|
220
|
+
|
|
221
|
+
Event::assertDispatched([Entity][Action]Event::class);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Test idempotency
|
|
225
|
+
public function test_job_is_idempotent(): void
|
|
226
|
+
{
|
|
227
|
+
$job = new [Action][Entity]Job($this->entity->getId(), $this->user->getId());
|
|
228
|
+
$service = app([Entity]Service::class);
|
|
229
|
+
|
|
230
|
+
$job->handle($service);
|
|
231
|
+
$job->handle($service); // second run should not break
|
|
232
|
+
|
|
233
|
+
// Assert expected state (same as single run)
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### 9. Verification checklist
|
|
238
|
+
|
|
239
|
+
- [ ] `declare(strict_types=1)` in all files
|
|
240
|
+
- [ ] Job has `$tries`, `$backoff`, `$timeout`
|
|
241
|
+
- [ ] Job implements `failed()` method
|
|
242
|
+
- [ ] Job is idempotent (safe to retry)
|
|
243
|
+
- [ ] Constructor takes IDs, `handle()` fetches fresh data
|
|
244
|
+
- [ ] Event uses `readonly` properties with minimal payload
|
|
245
|
+
- [ ] Listener registered in ServiceProvider
|
|
246
|
+
- [ ] Tests: dispatch, execution, idempotency
|
|
247
|
+
- [ ] `Queue::fake()` / `Event::fake()` in tests
|
|
248
|
+
- [ ] Error handling logs enough context for debugging
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# Skill: Laravel Feature Tests
|
|
2
|
+
|
|
3
|
+
## Trigger
|
|
4
|
+
Use when writing feature tests — HTTP controller tests, service integration tests with real database.
|
|
5
|
+
|
|
6
|
+
## File Location & Namespace
|
|
7
|
+
|
|
8
|
+
- Controllers: `Modules/{Module}/Tests/Feature/Controllers/{ControllerName}Test.php`
|
|
9
|
+
- Services: `Modules/{Module}/Tests/Feature/Services/{ServiceName}Test.php`
|
|
10
|
+
- Namespace mirrors path: `Modules\{Module}\Tests\Feature\Controllers` / `Services`
|
|
11
|
+
|
|
12
|
+
## Base Class
|
|
13
|
+
|
|
14
|
+
Feature tests extend `Tests\TestCase` which provides:
|
|
15
|
+
- `DatabaseTransactions` trait (auto-rollback, no manual cleanup needed)
|
|
16
|
+
- `$this->user` — a factory-created user
|
|
17
|
+
- `$this->companyId` — the user's company ID
|
|
18
|
+
|
|
19
|
+
**Never** extend `PHPUnit\Framework\TestCase` for feature tests — always use `Tests\TestCase`.
|
|
20
|
+
|
|
21
|
+
## Controller Test Structure
|
|
22
|
+
|
|
23
|
+
```php
|
|
24
|
+
<?php
|
|
25
|
+
|
|
26
|
+
declare(strict_types=1);
|
|
27
|
+
|
|
28
|
+
namespace Modules\{Module}\Tests\Feature\Controllers;
|
|
29
|
+
|
|
30
|
+
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
|
|
31
|
+
use Tests\TestCase;
|
|
32
|
+
|
|
33
|
+
class {Resource}ControllerTest extends TestCase
|
|
34
|
+
{
|
|
35
|
+
protected function setUp(): void
|
|
36
|
+
{
|
|
37
|
+
parent::setUp();
|
|
38
|
+
|
|
39
|
+
$this->actingAs($this->user, 'api-passport');
|
|
40
|
+
$this->withHeader('X-Requested-With', 'XMLHttpRequest');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// --- all public test methods first ---
|
|
44
|
+
|
|
45
|
+
public function test_index_returns_ok_with_permission(): void
|
|
46
|
+
{
|
|
47
|
+
$this->grantPermission();
|
|
48
|
+
|
|
49
|
+
$response = $this->getJson('/api/v4/{resource}?page=1&limit=10');
|
|
50
|
+
|
|
51
|
+
$response->assertOk();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public function test_index_returns_403_without_permission(): void
|
|
55
|
+
{
|
|
56
|
+
$response = $this->getJson('/api/v4/{resource}?page=1&limit=10');
|
|
57
|
+
|
|
58
|
+
$response->assertForbidden();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// --- private helper methods at BOTTOM ---
|
|
62
|
+
|
|
63
|
+
private function grantPermission(): void
|
|
64
|
+
{
|
|
65
|
+
// Grant module permission to test user
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Service Test Structure
|
|
71
|
+
|
|
72
|
+
```php
|
|
73
|
+
<?php
|
|
74
|
+
|
|
75
|
+
declare(strict_types=1);
|
|
76
|
+
|
|
77
|
+
namespace Modules\{Module}\Tests\Feature\Services;
|
|
78
|
+
|
|
79
|
+
use Tests\TestCase;
|
|
80
|
+
|
|
81
|
+
class {ServiceName}Test extends TestCase
|
|
82
|
+
{
|
|
83
|
+
public function test_store_creates_entity(): void
|
|
84
|
+
{
|
|
85
|
+
$result = $this->createService()->store($this->companyId, $data);
|
|
86
|
+
|
|
87
|
+
$this->assertDatabaseHas(Entity::TABLE_NAME, [
|
|
88
|
+
Entity::COLUMN_ID => $result->getId(),
|
|
89
|
+
]);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private function createService(): {ServiceName}
|
|
93
|
+
{
|
|
94
|
+
return app({ServiceName}::class);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Required Conventions
|
|
100
|
+
|
|
101
|
+
### Method naming
|
|
102
|
+
- Prefix: `test_` + `snake_case` description
|
|
103
|
+
- No `@test` annotation
|
|
104
|
+
- All methods must have explicit return type (`: void` for tests)
|
|
105
|
+
|
|
106
|
+
### Class structure order
|
|
107
|
+
1. Properties (`private`, `protected`)
|
|
108
|
+
2. `setUp()` / `tearDown()`
|
|
109
|
+
3. All `public` test methods
|
|
110
|
+
4. All `private` helper methods — **always at the bottom**
|
|
111
|
+
|
|
112
|
+
### HTTP status codes
|
|
113
|
+
Always use `Symfony\Component\HttpFoundation\Response as SymfonyResponse` constants or Laravel shortcuts:
|
|
114
|
+
```php
|
|
115
|
+
$response->assertOk(); // 200
|
|
116
|
+
$response->assertForbidden(); // 403
|
|
117
|
+
$response->assertNotFound(); // 404
|
|
118
|
+
$response->assertUnprocessable(); // 422
|
|
119
|
+
$response->assertNoContent(); // 204
|
|
120
|
+
$response->assertStatus(SymfonyResponse::HTTP_CREATED); // 201
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Closure type hints
|
|
124
|
+
```php
|
|
125
|
+
use Mockery\MockInterface;
|
|
126
|
+
$this->mock(Repository::class, function (MockInterface $mock): void { ... });
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Mocking
|
|
130
|
+
Use Laravel fakes for framework services:
|
|
131
|
+
```php
|
|
132
|
+
Event::fake();
|
|
133
|
+
Bus::fake();
|
|
134
|
+
Mail::fake();
|
|
135
|
+
Queue::fake();
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Data Resilience Rules
|
|
139
|
+
|
|
140
|
+
### Never delete pre-existing data
|
|
141
|
+
`DatabaseTransactions` handles cleanup. Never call `->delete()` or `truncate()` in setUp or tests.
|
|
142
|
+
|
|
143
|
+
### Before/after delta for counts
|
|
144
|
+
```php
|
|
145
|
+
// Bad — breaks if pre-existing data exists
|
|
146
|
+
$response->assertJsonCount(0, 'data');
|
|
147
|
+
|
|
148
|
+
// Good — delta pattern
|
|
149
|
+
$beforeTotal = $this->getJson($url)->json('meta.total');
|
|
150
|
+
// ... create 3 items ...
|
|
151
|
+
$afterTotal = $this->getJson($url)->json('meta.total');
|
|
152
|
+
$this->assertSame($beforeTotal + 3, $afterTotal);
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Search by identifier, not position
|
|
156
|
+
```php
|
|
157
|
+
// Bad
|
|
158
|
+
$this->assertSame('Expected', $data[0]['name']);
|
|
159
|
+
|
|
160
|
+
// Good
|
|
161
|
+
$found = collect($data)->firstWhere('id', $entity->getId());
|
|
162
|
+
$this->assertNotNull($found);
|
|
163
|
+
$this->assertSame('Expected', $found['name']);
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Other company IDs
|
|
167
|
+
Use factory-created companies, never arithmetic:
|
|
168
|
+
```php
|
|
169
|
+
// Bad
|
|
170
|
+
$otherCompanyId = $this->companyId + 999;
|
|
171
|
+
|
|
172
|
+
// Good
|
|
173
|
+
$otherCompanyId = UserModel::factory()->create()->getCompanyId();
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Controller Test Coverage Checklist
|
|
177
|
+
|
|
178
|
+
| Scenario | Expected |
|
|
179
|
+
|----------|----------|
|
|
180
|
+
| Happy path with permission | 200 / 201 / 204 |
|
|
181
|
+
| Without permission | 403 |
|
|
182
|
+
| Unauthenticated | 401 |
|
|
183
|
+
| Validation errors | 422 |
|
|
184
|
+
| Resource not found | 404 |
|
|
185
|
+
| Different company access | 403 |
|
|
186
|
+
| Response JSON structure | `assertJsonStructure(...)` |
|
|
187
|
+
|
|
188
|
+
## Running Tests
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
# Single test class
|
|
192
|
+
docker exec {{DOCKER_CONTAINER}} php artisan test --filter='{TestClassName}'
|
|
193
|
+
|
|
194
|
+
# Entire module
|
|
195
|
+
docker exec {{DOCKER_CONTAINER}} php artisan test --filter='Modules\\{Module}\\Tests'
|
|
196
|
+
```
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# Skill: Manual HTTP Tests
|
|
2
|
+
|
|
3
|
+
## Trigger
|
|
4
|
+
Use when creating or modifying API endpoints and manual `.http` test files need to be created or updated.
|
|
5
|
+
|
|
6
|
+
## Overview
|
|
7
|
+
|
|
8
|
+
Manual HTTP tests use JetBrains HTTP Client (`.http` files) to test API endpoints directly from PhpStorm/IntelliJ. They complement PHPUnit tests by providing quick, interactive verification of endpoints.
|
|
9
|
+
|
|
10
|
+
## File Location
|
|
11
|
+
|
|
12
|
+
- Place `.http` files in `Modules/{Module}/Tests/Manual/`
|
|
13
|
+
- One file per resource is recommended (e.g., `users.http`)
|
|
14
|
+
|
|
15
|
+
## Authentication
|
|
16
|
+
|
|
17
|
+
The project uses OAuth 2.0 configured in `http-client.env.json` (copy from `http-client.env.json.example` in project root).
|
|
18
|
+
|
|
19
|
+
- Use in requests: `Authorization: Bearer {{$auth.token("app")}}`
|
|
20
|
+
- Requests testing unauthorized access must **not** include the `Authorization` header
|
|
21
|
+
|
|
22
|
+
## Request Naming Convention
|
|
23
|
+
|
|
24
|
+
Format: `### {Module} - {Action} {Resource} - {Scenario}`
|
|
25
|
+
|
|
26
|
+
Where:
|
|
27
|
+
- `{Module}` — Laravel module name
|
|
28
|
+
- `{Action}` — HTTP action (Create, Get, List, Count, Update, Delete, Archive, Restore, etc.)
|
|
29
|
+
- `{Resource}` — resource name
|
|
30
|
+
- `{Scenario}` — test scenario (e.g., Valid Request, Missing Name, Without Authorization)
|
|
31
|
+
|
|
32
|
+
Examples:
|
|
33
|
+
- `### Users - Create User - Valid Request`
|
|
34
|
+
- `### Users - Get User - Non-existent`
|
|
35
|
+
- `### Users - Delete User - Without Authorization`
|
|
36
|
+
|
|
37
|
+
## Required Headers
|
|
38
|
+
|
|
39
|
+
Every request must include:
|
|
40
|
+
```
|
|
41
|
+
Accept: application/json
|
|
42
|
+
X-Requested-With: XMLHttpRequest
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
For requests with body, also include:
|
|
46
|
+
```
|
|
47
|
+
Content-Type: application/json
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Response Handlers
|
|
51
|
+
|
|
52
|
+
Every request must have a `client.test()` response handler asserting the expected status code:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
### Users - Create User - Valid Request
|
|
56
|
+
POST {{baseUrl}}/api/v4/users
|
|
57
|
+
Authorization: Bearer {{$auth.token("app")}}
|
|
58
|
+
Content-Type: application/json
|
|
59
|
+
Accept: application/json
|
|
60
|
+
X-Requested-With: XMLHttpRequest
|
|
61
|
+
|
|
62
|
+
{
|
|
63
|
+
"name": "John Doe",
|
|
64
|
+
"email": "john@example.com"
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
> {%
|
|
68
|
+
client.test("Returns 201 Created", function () {
|
|
69
|
+
client.assert(response.status === 201, "Expected 201, got " + response.status);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
client.global.set("userId", response.body.data.id);
|
|
73
|
+
%}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Passing Data Between Requests
|
|
77
|
+
|
|
78
|
+
Use `client.global.set()` to save IDs or values from responses:
|
|
79
|
+
|
|
80
|
+
```javascript
|
|
81
|
+
// In create request handler:
|
|
82
|
+
client.global.set("userId", response.body.data.id);
|
|
83
|
+
|
|
84
|
+
// In subsequent request URL:
|
|
85
|
+
GET {{baseUrl}}/api/v4/users/{{userId}}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Test Coverage Checklist
|
|
89
|
+
|
|
90
|
+
For each endpoint, include tests for:
|
|
91
|
+
|
|
92
|
+
| Scenario | Expected Status |
|
|
93
|
+
|----------|----------------|
|
|
94
|
+
| Valid request (happy path) | 200 / 201 / 204 |
|
|
95
|
+
| Validation errors (missing/invalid fields) | 422 |
|
|
96
|
+
| Resource not found | 404 |
|
|
97
|
+
| Without authorization (no `Authorization` header) | 401 |
|
|
98
|
+
| Business logic errors | 400 |
|
|
99
|
+
|
|
100
|
+
## Things to Avoid
|
|
101
|
+
|
|
102
|
+
- Do **not** use `### ===...` section separators — they render as separate empty requests in PhpStorm's test runner
|
|
103
|
+
- Do **not** hardcode entity IDs — use `client.global.set()` / `client.global.get()` from a create request
|
|
104
|
+
- Do **not** create `http-client.env.json` manually — copy from the example file in project root
|
|
105
|
+
|
|
106
|
+
## Full Example
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
### Users - Create User - Valid Request
|
|
110
|
+
POST {{baseUrl}}/api/v4/users
|
|
111
|
+
Authorization: Bearer {{$auth.token("app")}}
|
|
112
|
+
Content-Type: application/json
|
|
113
|
+
Accept: application/json
|
|
114
|
+
X-Requested-With: XMLHttpRequest
|
|
115
|
+
|
|
116
|
+
{
|
|
117
|
+
"name": "John Doe",
|
|
118
|
+
"email": "john@example.com"
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
> {%
|
|
122
|
+
client.test("Returns 201 Created", function () {
|
|
123
|
+
client.assert(response.status === 201, "Expected 201, got " + response.status);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
client.global.set("userId", response.body.data.id);
|
|
127
|
+
%}
|
|
128
|
+
|
|
129
|
+
### Users - Create User - Missing Name
|
|
130
|
+
POST {{baseUrl}}/api/v4/users
|
|
131
|
+
Authorization: Bearer {{$auth.token("app")}}
|
|
132
|
+
Content-Type: application/json
|
|
133
|
+
Accept: application/json
|
|
134
|
+
X-Requested-With: XMLHttpRequest
|
|
135
|
+
|
|
136
|
+
{
|
|
137
|
+
"email": "john@example.com"
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
> {%
|
|
141
|
+
client.test("Returns 422 validation error", function () {
|
|
142
|
+
client.assert(response.status === 422, "Expected 422, got " + response.status);
|
|
143
|
+
});
|
|
144
|
+
%}
|
|
145
|
+
|
|
146
|
+
### Users - Create User - Without Authorization
|
|
147
|
+
POST {{baseUrl}}/api/v4/users
|
|
148
|
+
Content-Type: application/json
|
|
149
|
+
Accept: application/json
|
|
150
|
+
X-Requested-With: XMLHttpRequest
|
|
151
|
+
|
|
152
|
+
{
|
|
153
|
+
"name": "John Doe",
|
|
154
|
+
"email": "john@example.com"
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
> {%
|
|
158
|
+
client.test("Returns 401 Unauthorized", function () {
|
|
159
|
+
client.assert(response.status === 401, "Expected 401, got " + response.status);
|
|
160
|
+
});
|
|
161
|
+
%}
|
|
162
|
+
|
|
163
|
+
### Users - Get User - By ID
|
|
164
|
+
GET {{baseUrl}}/api/v4/users/{{userId}}
|
|
165
|
+
Authorization: Bearer {{$auth.token("app")}}
|
|
166
|
+
Accept: application/json
|
|
167
|
+
X-Requested-With: XMLHttpRequest
|
|
168
|
+
|
|
169
|
+
> {%
|
|
170
|
+
client.test("Returns 200 OK", function () {
|
|
171
|
+
client.assert(response.status === 200, "Expected 200, got " + response.status);
|
|
172
|
+
});
|
|
173
|
+
%}
|
|
174
|
+
|
|
175
|
+
### Users - Delete User - Valid Request
|
|
176
|
+
DELETE {{baseUrl}}/api/v4/users/{{userId}}
|
|
177
|
+
Authorization: Bearer {{$auth.token("app")}}
|
|
178
|
+
Accept: application/json
|
|
179
|
+
X-Requested-With: XMLHttpRequest
|
|
180
|
+
|
|
181
|
+
> {%
|
|
182
|
+
client.test("Returns 200 OK (deleted)", function () {
|
|
183
|
+
client.assert(response.status === 200, "Expected 200, got " + response.status);
|
|
184
|
+
});
|
|
185
|
+
%}
|
|
186
|
+
```
|