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,133 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "**/Modules/**"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Modular Architecture - Instrukcje
|
|
6
|
+
|
|
7
|
+
## Struktura modulu
|
|
8
|
+
|
|
9
|
+
Kazdy modul biznesowy ma oddzielny katalog w `Modules/`:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
Modules/Feature/
|
|
13
|
+
├── Config/
|
|
14
|
+
│ └── config.php # Konfiguracja modulu
|
|
15
|
+
├── Console/ # Artisan commands
|
|
16
|
+
├── Database/
|
|
17
|
+
│ ├── Filters/ # Custom query filters (Spatie)
|
|
18
|
+
│ ├── Migrations/ # Migracje modulu
|
|
19
|
+
│ ├── Repositories/ # Data access (CQRS)
|
|
20
|
+
│ │ ├── EloquentFeatureCommandRepository.php
|
|
21
|
+
│ │ └── EloquentFeatureQueryRepository.php
|
|
22
|
+
│ ├── Factories/ # Model factories (testy)
|
|
23
|
+
│ └── Scopes/ # Global scopes
|
|
24
|
+
├── Domain/ # Logika domenowa (DDD)
|
|
25
|
+
│ ├── DTO/ # Data Transfer Objects
|
|
26
|
+
│ ├── Services/ # Domain services
|
|
27
|
+
│ ├── Events/ # Domain events
|
|
28
|
+
│ └── ValueObjects/ # Value Objects
|
|
29
|
+
├── Entities/ # Eloquent models
|
|
30
|
+
├── Emails/ # Mailable classes
|
|
31
|
+
├── Http/
|
|
32
|
+
│ ├── Controllers/ # Endpointy API
|
|
33
|
+
│ ├── Policies/ # Autoryzacja
|
|
34
|
+
│ ├── Requests/ # Form Requests (walidacja)
|
|
35
|
+
│ └── Resources/ # API Resources (transformery)
|
|
36
|
+
├── Jobs/ # Queued jobs
|
|
37
|
+
├── Listeners/ # Event listeners
|
|
38
|
+
├── Providers/
|
|
39
|
+
│ ├── FeatureServiceProvider.php # Rejestracja modulu
|
|
40
|
+
│ └── RouteServiceProvider.php # Routing modulu
|
|
41
|
+
├── Routes/
|
|
42
|
+
│ └── api.php # Endpointy API modulu
|
|
43
|
+
├── Tests/
|
|
44
|
+
│ ├── Feature/ # Integration tests
|
|
45
|
+
│ ├── Unit/ # Unit tests
|
|
46
|
+
│ └── Fixtures/ # Test fixtures
|
|
47
|
+
├── module.json # Metadane modulu
|
|
48
|
+
└── composer.json # Zaleznosci modulu
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## module.json
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"name": "Feature",
|
|
56
|
+
"alias": "feature",
|
|
57
|
+
"description": "Opis modulu",
|
|
58
|
+
"active": 1,
|
|
59
|
+
"order": 0,
|
|
60
|
+
"providers": [
|
|
61
|
+
"Modules\\Feature\\Providers\\FeatureServiceProvider"
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## ServiceProvider
|
|
67
|
+
|
|
68
|
+
```php
|
|
69
|
+
declare(strict_types=1);
|
|
70
|
+
|
|
71
|
+
namespace Modules\Feature\Providers;
|
|
72
|
+
|
|
73
|
+
use Illuminate\Support\ServiceProvider;
|
|
74
|
+
use Illuminate\Support\Facades\Gate;
|
|
75
|
+
|
|
76
|
+
class FeatureServiceProvider extends ServiceProvider
|
|
77
|
+
{
|
|
78
|
+
public function boot(): void
|
|
79
|
+
{
|
|
80
|
+
$this->loadMigrationsFrom(__DIR__ . '/../Database/Migrations');
|
|
81
|
+
$this->registerTranslations();
|
|
82
|
+
$this->registerConfig();
|
|
83
|
+
|
|
84
|
+
// Policies
|
|
85
|
+
Gate::define('feature-admin', FeaturePolicy::class . '@admin');
|
|
86
|
+
|
|
87
|
+
// Commands
|
|
88
|
+
$this->commands([
|
|
89
|
+
SyncFeatureDataCommand::class,
|
|
90
|
+
]);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public function register(): void
|
|
94
|
+
{
|
|
95
|
+
$this->app->register(RouteServiceProvider::class);
|
|
96
|
+
|
|
97
|
+
// Bindings (interface → implementation)
|
|
98
|
+
$this->app->bind(
|
|
99
|
+
FeatureCommandRepository::class,
|
|
100
|
+
EloquentFeatureCommandRepository::class,
|
|
101
|
+
);
|
|
102
|
+
$this->app->bind(
|
|
103
|
+
FeatureQueryRepository::class,
|
|
104
|
+
EloquentFeatureQueryRepository::class,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Routing modulu
|
|
111
|
+
|
|
112
|
+
```php
|
|
113
|
+
// Routes/api.php
|
|
114
|
+
use Illuminate\Support\Facades\Route;
|
|
115
|
+
|
|
116
|
+
Route::middleware(['auth:api'])->prefix('feature')->group(function () {
|
|
117
|
+
Route::get('/', [FeatureController::class, 'index']);
|
|
118
|
+
Route::get('/{featureId}', [FeatureController::class, 'show']);
|
|
119
|
+
Route::post('/', [FeatureController::class, 'store']);
|
|
120
|
+
Route::put('/{featureId}', [FeatureController::class, 'update']);
|
|
121
|
+
Route::delete('/{featureId}', [FeatureController::class, 'destroy']);
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Zasady
|
|
126
|
+
|
|
127
|
+
1. **Jeden modul = jedna domena biznesowa** - nie mieszaj odpowiedzialnosci
|
|
128
|
+
2. **Modul jest samodzielny** - ma swoje migracje, routing, testy, providery
|
|
129
|
+
3. **Komunikacja miedzy modulami** - przez Events/Listeners lub interfejsy, nie bezposrednie zaleznosci
|
|
130
|
+
4. **CQRS** - oddzielne repozytoria Command (zapis) i Query (odczyt)
|
|
131
|
+
5. **Nowy modul**: `php artisan module:make NazwaModulu`, potem dostosuj strukture
|
|
132
|
+
6. **Bindingi** - interfejs → implementacja w ServiceProvider `register()`
|
|
133
|
+
7. **Testy w module** - `Tests/Feature/` i `Tests/Unit/` w katalogu modulu
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "**/*Service*.php,**/*Repository*.php,**/DTO/**,**/ValueObjects/**"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Service, Repository, DTO, Value Objects - Instrukcje
|
|
6
|
+
|
|
7
|
+
## Service Layer
|
|
8
|
+
|
|
9
|
+
Serwisy koordynuja logike biznesowa. Kontroler deleguje do serwisu, serwis uzywa repozytoriow.
|
|
10
|
+
|
|
11
|
+
```php
|
|
12
|
+
declare(strict_types=1);
|
|
13
|
+
|
|
14
|
+
namespace Modules\Feature\Domain\Services;
|
|
15
|
+
|
|
16
|
+
readonly class FeatureService
|
|
17
|
+
{
|
|
18
|
+
public function __construct(
|
|
19
|
+
private FeatureCommandRepository $commandRepository,
|
|
20
|
+
private FeatureQueryRepository $queryRepository,
|
|
21
|
+
) {}
|
|
22
|
+
|
|
23
|
+
public function create(FeatureDTO $dto): Feature
|
|
24
|
+
{
|
|
25
|
+
$feature = Feature::create($dto);
|
|
26
|
+
$this->commandRepository->save($feature);
|
|
27
|
+
|
|
28
|
+
event(new FeatureCreated($feature));
|
|
29
|
+
|
|
30
|
+
return $feature;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public function getForUser(int $userId, int $limit): LengthAwarePaginator
|
|
34
|
+
{
|
|
35
|
+
return $this->queryRepository->getByUser($userId, $limit);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Zasady Service
|
|
41
|
+
|
|
42
|
+
- `readonly class` - serwis nie ma mutowalnego stanu
|
|
43
|
+
- Constructor injection dla zaleznosci
|
|
44
|
+
- Jedna publiczna metoda = jedna operacja biznesowa
|
|
45
|
+
- Serwis **nie** zwraca Response - to robi kontroler
|
|
46
|
+
- Serwis moze emitowac Events
|
|
47
|
+
|
|
48
|
+
## Repository Pattern (CQRS)
|
|
49
|
+
|
|
50
|
+
Oddziel operacje zapisu (Command) od odczytu (Query):
|
|
51
|
+
|
|
52
|
+
### Command Repository (zapis)
|
|
53
|
+
|
|
54
|
+
```php
|
|
55
|
+
declare(strict_types=1);
|
|
56
|
+
|
|
57
|
+
namespace Modules\Feature\Database\Repositories;
|
|
58
|
+
|
|
59
|
+
interface FeatureCommandRepository
|
|
60
|
+
{
|
|
61
|
+
public function save(Feature $feature): void;
|
|
62
|
+
public function delete(UuidInterface $id): void;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
class EloquentFeatureCommandRepository implements FeatureCommandRepository
|
|
66
|
+
{
|
|
67
|
+
public function save(Feature $feature): void
|
|
68
|
+
{
|
|
69
|
+
$model = FeatureModel::query()
|
|
70
|
+
->where(FeatureModel::COLUMN_ID, $feature->getId()->toString())
|
|
71
|
+
->first();
|
|
72
|
+
|
|
73
|
+
if (!$model) {
|
|
74
|
+
$model = new FeatureModel();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
$model->fill($feature->toArray())->save();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public function delete(UuidInterface $id): void
|
|
81
|
+
{
|
|
82
|
+
FeatureModel::query()
|
|
83
|
+
->where(FeatureModel::COLUMN_ID, $id->toString())
|
|
84
|
+
->delete();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Query Repository (odczyt)
|
|
90
|
+
|
|
91
|
+
```php
|
|
92
|
+
declare(strict_types=1);
|
|
93
|
+
|
|
94
|
+
namespace Modules\Feature\Database\Repositories;
|
|
95
|
+
|
|
96
|
+
interface FeatureQueryRepository
|
|
97
|
+
{
|
|
98
|
+
/** @return LengthAwarePaginator<FeatureModel> */
|
|
99
|
+
public function getByUser(int $userId, int $limit): LengthAwarePaginator;
|
|
100
|
+
|
|
101
|
+
public function findById(UuidInterface $id): ?FeatureModel;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
class EloquentFeatureQueryRepository implements FeatureQueryRepository
|
|
105
|
+
{
|
|
106
|
+
/** @return LengthAwarePaginator<FeatureModel> */
|
|
107
|
+
public function getByUser(int $userId, int $limit): LengthAwarePaginator
|
|
108
|
+
{
|
|
109
|
+
return FeatureModel::query()
|
|
110
|
+
->where(FeatureModel::COLUMN_USER_ID, $userId)
|
|
111
|
+
->with(FeatureModel::RELATION_TYPE)
|
|
112
|
+
->orderByDesc(FeatureModel::COLUMN_CREATED_AT)
|
|
113
|
+
->paginate($limit);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## DTO (Data Transfer Object)
|
|
119
|
+
|
|
120
|
+
Przenosi dane miedzy warstwami bez logiki biznesowej:
|
|
121
|
+
|
|
122
|
+
```php
|
|
123
|
+
declare(strict_types=1);
|
|
124
|
+
|
|
125
|
+
namespace Modules\Feature\Domain\DTO;
|
|
126
|
+
|
|
127
|
+
readonly class FeatureDTO
|
|
128
|
+
{
|
|
129
|
+
public function __construct(
|
|
130
|
+
public string $name,
|
|
131
|
+
public string $description,
|
|
132
|
+
public int $userId,
|
|
133
|
+
public ?int $categoryId = null,
|
|
134
|
+
) {}
|
|
135
|
+
|
|
136
|
+
public static function fromRequest(StoreFeatureRequest $request): self
|
|
137
|
+
{
|
|
138
|
+
return new self(
|
|
139
|
+
name: $request->validated('name'),
|
|
140
|
+
description: $request->validated('description'),
|
|
141
|
+
userId: $request->user()->getId(),
|
|
142
|
+
categoryId: $request->validated('category_id'),
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Value Objects
|
|
149
|
+
|
|
150
|
+
Reprezentuja wartosc domenowa - niemutowalne, porownywane przez wartosc:
|
|
151
|
+
|
|
152
|
+
```php
|
|
153
|
+
declare(strict_types=1);
|
|
154
|
+
|
|
155
|
+
namespace Modules\Feature\Domain\ValueObjects;
|
|
156
|
+
|
|
157
|
+
readonly class DateRange
|
|
158
|
+
{
|
|
159
|
+
public function __construct(
|
|
160
|
+
public Carbon $start,
|
|
161
|
+
public Carbon $end,
|
|
162
|
+
) {
|
|
163
|
+
if ($start->isAfter($end)) {
|
|
164
|
+
throw new \InvalidArgumentException('Start date must be before end date');
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
public function overlaps(self $other): bool
|
|
169
|
+
{
|
|
170
|
+
return $this->start->isBefore($other->end)
|
|
171
|
+
&& $this->end->isAfter($other->start);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
public function days(): int
|
|
175
|
+
{
|
|
176
|
+
return $this->start->diffInDays($this->end);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Kontroler - integracja
|
|
182
|
+
|
|
183
|
+
```php
|
|
184
|
+
declare(strict_types=1);
|
|
185
|
+
|
|
186
|
+
namespace Modules\Feature\Http\Controllers;
|
|
187
|
+
|
|
188
|
+
class FeatureController extends Controller
|
|
189
|
+
{
|
|
190
|
+
public function __construct(
|
|
191
|
+
private readonly FeatureService $service,
|
|
192
|
+
) {}
|
|
193
|
+
|
|
194
|
+
public function store(StoreFeatureRequest $request): JsonResponse
|
|
195
|
+
{
|
|
196
|
+
$dto = FeatureDTO::fromRequest($request);
|
|
197
|
+
$feature = $this->service->create($dto);
|
|
198
|
+
|
|
199
|
+
return FeatureResource::make($feature)
|
|
200
|
+
->response()
|
|
201
|
+
->setStatusCode(Response::HTTP_CREATED);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Zasady
|
|
207
|
+
|
|
208
|
+
1. **Service** - `readonly class`, business logic, emituje events
|
|
209
|
+
2. **Command Repository** - tylko zapis (save, delete, update)
|
|
210
|
+
3. **Query Repository** - tylko odczyt, eager loading, paginacja
|
|
211
|
+
4. **DTO** - `readonly class`, statyczny `fromRequest()`, brak logiki
|
|
212
|
+
5. **Value Object** - `readonly class`, walidacja w konstruktorze, metody porownania
|
|
213
|
+
6. **Interfejsy** - repozytorium ma interfejs, implementacja w `Eloquent*Repository`
|
|
214
|
+
7. **Binding** - interfejs → implementacja rejestrowany w ServiceProvider
|
|
215
|
+
8. **Kontroler nie zawiera logiki** - deleguje do serwisu, zwraca Resource
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "**/*Test*.php,**/tests/**,**/*Factory*.php"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# PHP Testing (PHPUnit) - Instrukcje
|
|
6
|
+
|
|
7
|
+
## Uruchamianie testow
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Wszystkie testy
|
|
11
|
+
docker exec {{DOCKER_CONTAINER}} php artisan test
|
|
12
|
+
|
|
13
|
+
# Konkretny plik
|
|
14
|
+
docker exec {{DOCKER_CONTAINER}} php artisan test --filter=FeatureServiceTest
|
|
15
|
+
|
|
16
|
+
# Konkretna metoda
|
|
17
|
+
docker exec {{DOCKER_CONTAINER}} php artisan test --filter=test_user_can_create_feature
|
|
18
|
+
|
|
19
|
+
# Z coverage
|
|
20
|
+
docker exec {{DOCKER_CONTAINER}} php artisan test --coverage
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Feature Tests (integracyjne)
|
|
24
|
+
|
|
25
|
+
Testuja caly flow: request → kontroler → serwis → baza → response.
|
|
26
|
+
|
|
27
|
+
```php
|
|
28
|
+
declare(strict_types=1);
|
|
29
|
+
|
|
30
|
+
namespace Modules\Feature\Tests\Feature;
|
|
31
|
+
|
|
32
|
+
use Tests\TestCase;
|
|
33
|
+
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
|
34
|
+
|
|
35
|
+
class FeatureControllerTest extends TestCase
|
|
36
|
+
{
|
|
37
|
+
use DatabaseTransactions;
|
|
38
|
+
|
|
39
|
+
private User $user;
|
|
40
|
+
private Feature $feature;
|
|
41
|
+
|
|
42
|
+
protected function setUp(): void
|
|
43
|
+
{
|
|
44
|
+
parent::setUp();
|
|
45
|
+
$this->user = User::factory()->create();
|
|
46
|
+
$this->feature = Feature::factory()->create(['user_id' => $this->user->getId()]);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public function test_user_can_list_features(): void
|
|
50
|
+
{
|
|
51
|
+
// Arrange
|
|
52
|
+
Feature::factory()->count(5)->create(['user_id' => $this->user->getId()]);
|
|
53
|
+
|
|
54
|
+
// Act
|
|
55
|
+
$response = $this->actingAs($this->user)
|
|
56
|
+
->getJson('/api/features?page=1&limit=10');
|
|
57
|
+
|
|
58
|
+
// Assert
|
|
59
|
+
$response->assertOk()
|
|
60
|
+
->assertJsonStructure([
|
|
61
|
+
'data' => [['id', 'name', 'status', 'createdAt']],
|
|
62
|
+
'meta' => ['total', 'perPage', 'currentPage'],
|
|
63
|
+
])
|
|
64
|
+
->assertJsonCount(6, 'data');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public function test_user_can_create_feature(): void
|
|
68
|
+
{
|
|
69
|
+
// Arrange
|
|
70
|
+
$payload = [
|
|
71
|
+
'name' => 'New Feature',
|
|
72
|
+
'description' => 'Description',
|
|
73
|
+
'status' => 'draft',
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
// Act
|
|
77
|
+
$response = $this->actingAs($this->user)
|
|
78
|
+
->postJson('/api/features', $payload);
|
|
79
|
+
|
|
80
|
+
// Assert
|
|
81
|
+
$response->assertCreated()
|
|
82
|
+
->assertJsonFragment(['name' => 'New Feature']);
|
|
83
|
+
|
|
84
|
+
$this->assertDatabaseHas('features', [
|
|
85
|
+
'name' => 'New Feature',
|
|
86
|
+
'user_id' => $this->user->getId(),
|
|
87
|
+
]);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
public function test_unauthorized_user_cannot_delete(): void
|
|
91
|
+
{
|
|
92
|
+
$otherUser = User::factory()->create();
|
|
93
|
+
|
|
94
|
+
$response = $this->actingAs($otherUser)
|
|
95
|
+
->deleteJson("/api/features/{$this->feature->getId()}");
|
|
96
|
+
|
|
97
|
+
$response->assertForbidden();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Unit Tests
|
|
103
|
+
|
|
104
|
+
Testuja izolowana logike bez bazy i HTTP:
|
|
105
|
+
|
|
106
|
+
```php
|
|
107
|
+
declare(strict_types=1);
|
|
108
|
+
|
|
109
|
+
namespace Modules\Feature\Tests\Unit;
|
|
110
|
+
|
|
111
|
+
use PHPUnit\Framework\TestCase;
|
|
112
|
+
|
|
113
|
+
class FeatureDurationCalculatorTest extends TestCase
|
|
114
|
+
{
|
|
115
|
+
private FeatureDurationCalculator $calculator;
|
|
116
|
+
|
|
117
|
+
protected function setUp(): void
|
|
118
|
+
{
|
|
119
|
+
parent::setUp();
|
|
120
|
+
$this->calculator = new FeatureDurationCalculator();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
public function test_calculates_business_days(): void
|
|
124
|
+
{
|
|
125
|
+
$start = Carbon::parse('2025-01-06'); // Poniedzialek
|
|
126
|
+
$end = Carbon::parse('2025-01-10'); // Piatek
|
|
127
|
+
|
|
128
|
+
$result = $this->calculator->businessDays($start, $end);
|
|
129
|
+
|
|
130
|
+
$this->assertEquals(5, $result);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* @dataProvider durationDataProvider
|
|
135
|
+
*/
|
|
136
|
+
public function test_duration_calculation(
|
|
137
|
+
string $start,
|
|
138
|
+
string $end,
|
|
139
|
+
int $expectedDays,
|
|
140
|
+
): void {
|
|
141
|
+
$result = $this->calculator->businessDays(
|
|
142
|
+
Carbon::parse($start),
|
|
143
|
+
Carbon::parse($end),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
$this->assertEquals($expectedDays, $result);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
public static function durationDataProvider(): array
|
|
150
|
+
{
|
|
151
|
+
return [
|
|
152
|
+
'full week' => ['2025-01-06', '2025-01-10', 5],
|
|
153
|
+
'with weekend' => ['2025-01-06', '2025-01-13', 6],
|
|
154
|
+
'same day' => ['2025-01-06', '2025-01-06', 1],
|
|
155
|
+
'weekend only' => ['2025-01-11', '2025-01-12', 0],
|
|
156
|
+
];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Model Factories
|
|
162
|
+
|
|
163
|
+
```php
|
|
164
|
+
declare(strict_types=1);
|
|
165
|
+
|
|
166
|
+
namespace Modules\Feature\Database\Factories;
|
|
167
|
+
|
|
168
|
+
use Illuminate\Database\Eloquent\Factories\Factory;
|
|
169
|
+
|
|
170
|
+
class FeatureFactory extends Factory
|
|
171
|
+
{
|
|
172
|
+
protected $model = FeatureModel::class;
|
|
173
|
+
|
|
174
|
+
public function definition(): array
|
|
175
|
+
{
|
|
176
|
+
return [
|
|
177
|
+
'name' => fake()->sentence(3),
|
|
178
|
+
'description' => fake()->paragraph(),
|
|
179
|
+
'status' => fake()->randomElement(FeatureStatus::cases()),
|
|
180
|
+
'user_id' => User::factory(),
|
|
181
|
+
'created_at' => fake()->dateTimeBetween('-1 year'),
|
|
182
|
+
];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
public function draft(): static
|
|
186
|
+
{
|
|
187
|
+
return $this->state(['status' => FeatureStatus::DRAFT]);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
public function published(): static
|
|
191
|
+
{
|
|
192
|
+
return $this->state(['status' => FeatureStatus::PUBLISHED]);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
public function withCategory(int $categoryId): static
|
|
196
|
+
{
|
|
197
|
+
return $this->state(['category_id' => $categoryId]);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Laravel Fakes
|
|
203
|
+
|
|
204
|
+
```php
|
|
205
|
+
public function test_sends_notification_on_create(): void
|
|
206
|
+
{
|
|
207
|
+
Notification::fake();
|
|
208
|
+
|
|
209
|
+
$this->actingAs($this->user)
|
|
210
|
+
->postJson('/api/features', $this->validPayload());
|
|
211
|
+
|
|
212
|
+
Notification::assertSentTo($this->user, FeatureCreatedNotification::class);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
public function test_dispatches_export_job(): void
|
|
216
|
+
{
|
|
217
|
+
Queue::fake();
|
|
218
|
+
|
|
219
|
+
$this->actingAs($this->user)
|
|
220
|
+
->postJson('/api/features/export');
|
|
221
|
+
|
|
222
|
+
Queue::assertPushed(ExportFeatureJob::class, function ($job) {
|
|
223
|
+
return $job->userId === $this->user->getId();
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
public function test_fires_event_on_update(): void
|
|
228
|
+
{
|
|
229
|
+
Event::fake([FeatureUpdated::class]);
|
|
230
|
+
|
|
231
|
+
$this->actingAs($this->user)
|
|
232
|
+
->putJson("/api/features/{$this->feature->getId()}", $this->validPayload());
|
|
233
|
+
|
|
234
|
+
Event::assertDispatched(FeatureUpdated::class);
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Test Helpers / Fixtures
|
|
239
|
+
|
|
240
|
+
```php
|
|
241
|
+
// Tests/Fixtures/FeatureFixtures.php
|
|
242
|
+
trait FeatureFixtures
|
|
243
|
+
{
|
|
244
|
+
protected function createFeatureWithRelations(array $overrides = []): FeatureModel
|
|
245
|
+
{
|
|
246
|
+
$category = Category::factory()->create();
|
|
247
|
+
$user = User::factory()->create();
|
|
248
|
+
|
|
249
|
+
return Feature::factory()
|
|
250
|
+
->withCategory($category->getId())
|
|
251
|
+
->create(array_merge([
|
|
252
|
+
'user_id' => $user->getId(),
|
|
253
|
+
], $overrides));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
protected function validPayload(array $overrides = []): array
|
|
257
|
+
{
|
|
258
|
+
return array_merge([
|
|
259
|
+
'name' => 'Test Feature',
|
|
260
|
+
'description' => 'Test description',
|
|
261
|
+
'status' => 'draft',
|
|
262
|
+
], $overrides);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Zasady
|
|
268
|
+
|
|
269
|
+
1. **`test_` prefix + snake_case** - `test_user_can_create_feature()`
|
|
270
|
+
2. **AAA pattern** - Arrange, Act, Assert (komentarze opcjonalne)
|
|
271
|
+
3. **`DatabaseTransactions`** - trait dla Feature tests, auto-rollback
|
|
272
|
+
4. **`setUp()`** - tworzenie wspolnych danych, nie w kazdym tescie
|
|
273
|
+
5. **Data Providers** - dla wielu scenariuszy z roznymi danymi
|
|
274
|
+
6. **Factory states** - `->draft()`, `->published()` zamiast surowych state
|
|
275
|
+
7. **Laravel fakes** - `Notification::fake()`, `Queue::fake()`, `Event::fake()`, `Mail::fake()`
|
|
276
|
+
8. **assertDatabaseHas/Missing** - sprawdzaj stan bazy po operacjach
|
|
277
|
+
9. **actingAs()** - autentykuj uzytkownika w Feature tests
|
|
278
|
+
10. **Testy w module** - `Modules/Feature/Tests/`, nie w globalnym `tests/`
|