45 - Add Forgejo CI workflow

This commit is contained in:
myrmidex 2026-08-17 13:30:49 +02:00
parent a8b0ed1dc8
commit c4ae6c2e69
133 changed files with 3568 additions and 605 deletions

25
.env.testing Normal file
View file

@ -0,0 +1,25 @@
APP_NAME=DishPlanner
APP_ENV=testing
APP_KEY=base64:2ADW3imsmRMo+UDw3GR6852wQu37/aNK1ooxEdaJIC0=
APP_DEBUG=true
APP_URL=http://localhost
APP_MAINTENANCE_DRIVER=file
BCRYPT_ROUNDS=4
LOG_CHANNEL=stack
LOG_STACK=single
DB_CONNECTION=sqlite
DB_DATABASE=:memory:
SESSION_DRIVER=array
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=sync
CACHE_STORE=array
MAIL_MAILER=array

37
.forgejo/workflows/ci.yml Normal file
View file

@ -0,0 +1,37 @@
name: CI
on:
push:
branches: ['release/*']
pull_request:
branches: [main, 'release/*']
jobs:
ci:
runs-on: docker
container:
image: forge.lvl0.xyz/lvl0/dishplanner-ci:php8.3-1
steps:
- uses: https://data.forgejo.org/actions/checkout@v4
- name: Cache Composer dependencies
uses: https://data.forgejo.org/actions/cache@v4
with:
path: ~/.cache/composer
key: composer-${{ hashFiles('composer.lock') }}
restore-keys: composer-
- name: Install dependencies
run: composer install --no-interaction --prefer-dist
- name: Prepare environment
run: cp .env.testing .env
- name: Lint
run: vendor/bin/pint --test
- name: Static analysis
run: vendor/bin/phpstan analyse --memory-limit=1G
- name: Tests
run: php -d memory_limit=512M vendor/bin/phpunit

View file

@ -17,11 +17,11 @@ public function execute(array $data): User
{
try {
// Validate required fields first
if (!isset($data['name']) || empty($data['name'])) {
if (! isset($data['name']) || empty($data['name'])) {
throw new InvalidArgumentException('Name is required');
}
if (!isset($data['planner_id']) || empty($data['planner_id'])) {
if (! isset($data['planner_id']) || empty($data['planner_id'])) {
throw new InvalidArgumentException('Planner ID is required');
}
@ -38,7 +38,7 @@ public function execute(array $data): User
'planner_id' => $data['planner_id'],
]);
if (!$user) {
if (! $user) {
throw new Exception('User creation returned null');
}
@ -50,7 +50,7 @@ public function execute(array $data): User
// Verify the user was actually created
$createdUser = User::find($user->id);
if (!$createdUser) {
if (! $createdUser) {
throw new Exception('User creation did not persist to database');
}

View file

@ -29,7 +29,7 @@ public function execute(User $user, array $data): bool
'user_id' => $user->id,
]);
if (!$result) {
if (! $result) {
throw new \Exception('User update returned false');
}

View file

@ -24,6 +24,7 @@ public function handle(): int
if ($planners->isEmpty()) {
$this->warn('No planners found. Aborting schedule generation.');
return self::FAILURE;
}

View file

@ -13,8 +13,7 @@ public function response(
?array $payload = null,
array|string|null $errors = null,
int $statusCode = 200,
): JsonResponse
{
): JsonResponse {
return response()->json(resolve(OutputService::class)->response($success, $payload, $errors), $statusCode);
}

View file

@ -17,7 +17,7 @@ public function toArray(Request $request): array
'recurrences' => $this->recurrences->map(fn ($recurrence) => [
'id' => $recurrence->id,
'type' => $recurrence->recurrence_type,
'value' => $recurrence->getValue()
'value' => $recurrence->getValue(),
]),
];
}

View file

@ -12,14 +12,18 @@ class DishesList extends Component
use WithPagination;
public $showCreateModal = false;
public $showEditModal = false;
public $showDeleteModal = false;
public $editingDish = null;
public $deletingDish = null;
// Form fields
public $name = '';
public $selectedUsers = [];
protected $rules = [
@ -39,7 +43,7 @@ public function render()
return view('livewire.dishes.dishes-list', [
'dishes' => $dishes,
'users' => $users
'users' => $users,
]);
}
@ -60,7 +64,7 @@ public function store()
]);
// Attach selected users
if (!empty($this->selectedUsers)) {
if (! empty($this->selectedUsers)) {
$dish->users()->attach($this->selectedUsers);
}
@ -126,7 +130,7 @@ public function toggleAllUsers(): void
if (count($this->selectedUsers) === $users->count()) {
$this->selectedUsers = [];
} else {
$this->selectedUsers = $users->pluck('id')->map(fn($id) => (string) $id)->toArray();
$this->selectedUsers = $users->pluck('id')->map(fn ($id) => (string) $id)->toArray();
}
}
}

View file

@ -19,25 +19,39 @@
class ScheduleCalendar extends Component
{
public $currentMonth;
public $currentYear;
public $calendarDays = [];
public $showRegenerateModal = false;
public $regenerateDate = null;
public $regenerateUserId = null;
// Edit dish modal
public $showEditDishModal = false;
public $editDate = null;
public $editUserId = null;
public $selectedDishId = null;
public $availableDishes = [];
// Add dish modal
public $showAddDishModal = false;
public $addDate = null;
public $addUserIds = [];
public $addSelectedDishId = null;
public $addAvailableUsers = [];
public $addAvailableDishes = [];
public function mount(): void
@ -61,7 +75,7 @@ public function refreshCalendar(): void
public function loadCalendar(): void
{
$service = new ScheduleCalendarService();
$service = new ScheduleCalendarService;
$this->calendarDays = $service->getCalendarDays(
auth()->user(),
$this->currentMonth,
@ -93,8 +107,9 @@ public function nextMonth(): void
public function regenerateForUserDate($date, $userId): void
{
if (!$this->authorizeUser($userId)) {
if (! $this->authorizeUser($userId)) {
session()->flash('error', 'Unauthorized action.');
return;
}
@ -106,12 +121,13 @@ public function regenerateForUserDate($date, $userId): void
public function confirmRegenerate(): void
{
try {
if (!$this->authorizeUser($this->regenerateUserId)) {
if (! $this->authorizeUser($this->regenerateUserId)) {
session()->flash('error', 'Unauthorized action.');
return;
}
$action = new DeleteScheduledUserDishForDateAction();
$action = new DeleteScheduledUserDishForDateAction;
$action->execute(
auth()->user(),
Carbon::parse($this->regenerateDate),
@ -131,12 +147,13 @@ public function confirmRegenerate(): void
public function skipDay($date, $userId): void
{
try {
if (!$this->authorizeUser($userId)) {
if (! $this->authorizeUser($userId)) {
session()->flash('error', 'Unauthorized action.');
return;
}
$action = new SkipScheduledUserDishForDateAction();
$action = new SkipScheduledUserDishForDateAction;
$action->execute(
auth()->user(),
Carbon::parse($date),
@ -155,6 +172,7 @@ public function skipDay($date, $userId): void
private function authorizeUser(int $userId): bool
{
$user = User::find($userId);
return $user && $user->planner_id === auth()->id();
}
@ -179,8 +197,9 @@ public function cancel(): void
public function removeDish($date, $userId): void
{
try {
if (!$this->authorizeUser($userId)) {
if (! $this->authorizeUser($userId)) {
session()->flash('error', 'Unauthorized action.');
return;
}
@ -223,7 +242,7 @@ public function toggleAllUsers(): void
if (count($this->addUserIds) === count($this->addAvailableUsers)) {
$this->addUserIds = [];
} else {
$this->addUserIds = $this->addAvailableUsers->pluck('id')->map(fn($id) => (string) $id)->toArray();
$this->addUserIds = $this->addAvailableUsers->pluck('id')->map(fn ($id) => (string) $id)->toArray();
}
$this->updateAvailableDishes();
}
@ -252,11 +271,13 @@ public function saveAddDish(): void
try {
if (empty($this->addUserIds)) {
session()->flash('error', 'Please select at least one user.');
return;
}
if (!$this->addSelectedDishId) {
if (! $this->addSelectedDishId) {
session()->flash('error', 'Please select a dish.');
return;
}
@ -273,8 +294,9 @@ public function saveAddDish(): void
$skippedCount = 0;
foreach ($this->addUserIds as $userId) {
if (!$this->authorizeUser((int) $userId)) {
if (! $this->authorizeUser((int) $userId)) {
$skippedCount++;
continue;
}
@ -285,6 +307,7 @@ public function saveAddDish(): void
if ($existing) {
$skippedCount++;
continue;
}
@ -293,8 +316,9 @@ public function saveAddDish(): void
->where('dish_id', $this->addSelectedDishId)
->first();
if (!$userDish) {
if (! $userDish) {
$skippedCount++;
continue;
}
@ -336,8 +360,9 @@ private function closeAddDishModal(): void
public function editDish($date, $userId): void
{
if (!$this->authorizeUser($userId)) {
if (! $this->authorizeUser($userId)) {
session()->flash('error', 'Unauthorized action.');
return;
}
@ -370,13 +395,15 @@ public function editDish($date, $userId): void
public function saveDish(): void
{
try {
if (!$this->authorizeUser($this->editUserId)) {
if (! $this->authorizeUser($this->editUserId)) {
session()->flash('error', 'Unauthorized action.');
return;
}
if (!$this->selectedDishId) {
if (! $this->selectedDishId) {
session()->flash('error', 'Please select a dish.');
return;
}
@ -394,8 +421,9 @@ public function saveDish(): void
->where('dish_id', $this->selectedDishId)
->first();
if (!$userDish) {
if (! $userDish) {
session()->flash('error', 'This dish is not assigned to this user.');
return;
}
@ -427,7 +455,8 @@ public function saveDish(): void
public function getMonthNameProperty(): string
{
$service = new ScheduleCalendarService();
$service = new ScheduleCalendarService;
return $service->getMonthName($this->currentMonth, $this->currentYear);
}
}

View file

@ -7,19 +7,27 @@
use DishPlanner\Schedule\Actions\ClearScheduleForMonthAction;
use DishPlanner\Schedule\Actions\GenerateScheduleForMonthAction;
use DishPlanner\Schedule\Actions\RegenerateScheduleForDateForUsersAction;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Facades\Log;
use Livewire\Component;
class ScheduleGenerator extends Component
{
private const YEARS_IN_PAST = 1;
private const YEARS_IN_FUTURE = 5;
public $selectedMonth;
public $selectedYear;
public $selectedUsers = [];
public $clearExisting = true;
public $showAdvancedOptions = false;
public $isGenerating = false;
public function mount(): void
@ -32,7 +40,7 @@ public function mount(): void
->toArray();
}
public function render(): \Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
public function render(): Factory|View
{
$users = User::where('planner_id', auth()->id())
->orderBy('name')
@ -43,7 +51,7 @@ public function render(): \Illuminate\Contracts\View\Factory|\Illuminate\Contrac
return view('livewire.schedule.schedule-generator', [
'users' => $users,
'months' => $this->getMonthNames(),
'years' => $years
'years' => $years,
]);
}
@ -52,13 +60,13 @@ public function generate(): void
$this->validate([
'selectedUsers' => 'required|array|min:1',
'selectedMonth' => 'required|integer|min:1|max:12',
'selectedYear' => 'required|integer|min:' . (now()->year - self::YEARS_IN_PAST) . '|max:' . (now()->year + self::YEARS_IN_FUTURE),
'selectedYear' => 'required|integer|min:'.(now()->year - self::YEARS_IN_PAST).'|max:'.(now()->year + self::YEARS_IN_FUTURE),
]);
$this->isGenerating = true;
try {
$action = new GenerateScheduleForMonthAction();
$action = new GenerateScheduleForMonthAction;
$action->execute(
auth()->user(),
$this->selectedMonth,
@ -70,8 +78,8 @@ public function generate(): void
$this->isGenerating = false;
$this->dispatch('schedule-generated');
session()->flash('success', 'Schedule generated successfully for ' .
$this->getSelectedMonthName() . ' ' . $this->selectedYear);
session()->flash('success', 'Schedule generated successfully for '.
$this->getSelectedMonthName().' '.$this->selectedYear);
} catch (\Exception $e) {
$this->isGenerating = false;
@ -83,7 +91,7 @@ public function generate(): void
public function regenerateForDate($date): void
{
try {
$action = new RegenerateScheduleForDateForUsersAction();
$action = new RegenerateScheduleForDateForUsersAction;
$action->execute(
auth()->user(),
Carbon::parse($date),
@ -91,7 +99,7 @@ public function regenerateForDate($date): void
);
$this->dispatch('schedule-generated');
session()->flash('success', 'Schedule regenerated for ' . Carbon::parse($date)->format('M d, Y'));
session()->flash('success', 'Schedule regenerated for '.Carbon::parse($date)->format('M d, Y'));
} catch (\Exception $e) {
Log::error('Schedule regeneration failed', ['exception' => $e, 'date' => $date]);
@ -102,7 +110,7 @@ public function regenerateForDate($date): void
public function clearMonth(): void
{
try {
$action = new ClearScheduleForMonthAction();
$action = new ClearScheduleForMonthAction;
$action->execute(
auth()->user(),
$this->selectedMonth,
@ -111,8 +119,8 @@ public function clearMonth(): void
);
$this->dispatch('schedule-generated');
session()->flash('success', 'Schedule cleared for ' .
$this->getSelectedMonthName() . ' ' . $this->selectedYear);
session()->flash('success', 'Schedule cleared for '.
$this->getSelectedMonthName().' '.$this->selectedYear);
} catch (\Exception $e) {
Log::error('Clear month failed', ['exception' => $e]);
@ -122,7 +130,7 @@ public function clearMonth(): void
public function toggleAdvancedOptions()
{
$this->showAdvancedOptions = !$this->showAdvancedOptions;
$this->showAdvancedOptions = ! $this->showAdvancedOptions;
}
private function getMonthNames(): array
@ -130,7 +138,7 @@ private function getMonthNames(): array
return [
1 => 'January', 2 => 'February', 3 => 'March', 4 => 'April',
5 => 'May', 6 => 'June', 7 => 'July', 8 => 'August',
9 => 'September', 10 => 'October', 11 => 'November', 12 => 'December'
9 => 'September', 10 => 'October', 11 => 'November', 12 => 'December',
];
}

View file

@ -2,10 +2,10 @@
namespace App\Livewire\Users;
use App\Models\User;
use App\Actions\User\CreateUserAction;
use App\Actions\User\DeleteUserAction;
use App\Actions\User\EditUserAction;
use App\Models\User;
use Exception;
use Illuminate\Contracts\View\View;
use Livewire\Component;
@ -16,10 +16,13 @@ class UsersList extends Component
use WithPagination;
public bool $showCreateModal = false;
public bool $showEditModal = false;
public bool $showDeleteModal = false;
public ?User $editingUser = null;
public ?User $deletingUser = null;
// Form fields
@ -36,7 +39,7 @@ public function render(): View
->paginate(10);
return view('livewire.users.users-list', [
'users' => $users
'users' => $users,
]);
}
@ -52,7 +55,7 @@ public function store(): void
$this->validate();
try {
(new CreateUserAction())->execute([
(new CreateUserAction)->execute([
'name' => $this->name,
'planner_id' => auth()->id(),
]);
@ -62,7 +65,7 @@ public function store(): void
session()->flash('success', 'User created successfully.');
} catch (Exception $e) {
session()->flash('error', 'Failed to create user: ' . $e->getMessage());
session()->flash('error', 'Failed to create user: '.$e->getMessage());
}
}
@ -79,7 +82,7 @@ public function update(): void
$this->validate();
try {
(new EditUserAction())->execute($this->editingUser, ['name' => $this->name]);
(new EditUserAction)->execute($this->editingUser, ['name' => $this->name]);
$this->showEditModal = false;
$this->reset(['name', 'editingUser']);
@ -89,7 +92,7 @@ public function update(): void
// Force component to re-render with fresh data
$this->resetPage();
} catch (Exception $e) {
session()->flash('error', 'Failed to update user: ' . $e->getMessage());
session()->flash('error', 'Failed to update user: '.$e->getMessage());
}
}
@ -102,7 +105,7 @@ public function confirmDelete(User $user): void
public function delete(): void
{
try {
(new DeleteUserAction())->execute($this->deletingUser);
(new DeleteUserAction)->execute($this->deletingUser);
$this->showDeleteModal = false;
$this->deletingUser = null;
@ -112,7 +115,7 @@ public function delete(): void
// Force component to re-render with fresh data
$this->resetPage();
} catch (Exception $e) {
session()->flash('error', 'Failed to delete user: ' . $e->getMessage());
session()->flash('error', 'Failed to delete user: '.$e->getMessage());
}
}

View file

@ -21,6 +21,7 @@
* @property Carbon $updated_at
* @property Collection<User> $users
* @property Collection<UserDish> $userDishes
*
* @method static create(array $data)
* @method static findOrFail(int $dish_id)
* @method static DishFactory factory($count = null, $state = [])

View file

@ -13,6 +13,7 @@
* @property int $id
* @property static PlannerFactory factory($count = null, $state = [])
* @property Collection<User> $users
*
* @method static first()
* @method static create(array $array)
*/

View file

@ -22,8 +22,9 @@
* @property Dish $dish
* @property User $user
* @property Carbon $date
* @property boolean $is_skipped
* @property bool $is_skipped
* @property Collection<ScheduledUserDish> $scheduledUserDishes
*
* @method static create(array $array)
* @method static Builder where(array|Closure|Expression|string $column, mixed $operator = null, mixed $value = null, string $boolean = 'and')
* @method static ScheduleFactory factory($count = null, $state = [])
@ -38,6 +39,8 @@ class Schedule extends Model
public $timestamps = false;
protected $dateFormat = 'Y-m-d';
protected $fillable = ['planner_id', 'date', 'is_skipped'];
protected $casts = [

View file

@ -17,6 +17,7 @@
* @property int $user_dish_id
* @property UserDish $userDish
* @property bool $is_skipped
*
* @method static create(array $array)
* @method static ScheduledUserDishFactory factory($count = null, $state = [])
* @method static firstOrCreate(array $array, array $array1)
@ -29,7 +30,7 @@ class ScheduledUserDish extends Model
'schedule_id',
'user_id',
'user_dish_id',
'is_skipped'
'is_skipped',
];
protected $casts = [

View file

@ -6,10 +6,10 @@
use Database\Factories\UserFactory;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Model;
/**
* @property int $id
@ -17,6 +17,7 @@
* @property string $name
* @property Collection<Dish> $dishes
* @property Collection<UserDish> $userDishes
*
* @method static User findOrFail(int $user_id)
* @method static UserFactory factory($count = null, $state = [])
* @method static create(array $array)

View file

@ -17,6 +17,7 @@
* @method static UserDish|null find(int|null $user_dish_id)
* @method static create(array $array)
* @method static where(string $string, int $id)
*
* @property int $id
* @property int $dish_id
* @property int $user_id

View file

@ -36,7 +36,7 @@ public function getValue(): int
return match ($this->recurrence_type) {
WeeklyRecurrence::class => $this->recurrence->weekday->value,
MinimumRecurrence::class => $this->recurrence->days,
default => throw new InvalidRecurrenceTypeException()
default => throw new InvalidRecurrenceTypeException
};
}
}

View file

@ -12,10 +12,11 @@
/**
* @property int $weekday
*
* @method static create(array $array)
* @method static WeeklyRecurrenceFactory factory($count = null, $state = [])
*/
class WeeklyRecurrence extends Model implements RecurrenceInterface, FixedRecurrenceInterface
class WeeklyRecurrence extends Model implements FixedRecurrenceInterface, RecurrenceInterface
{
/** @use HasFactory<WeeklyRecurrenceFactory> */
use HasFactory;

View file

@ -16,7 +16,6 @@
use Illuminate\Contracts\Debug\ExceptionHandler;
use Illuminate\Foundation\Exceptions\Handler as BaseHandler;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider;
use Throwable;
@ -25,7 +24,8 @@ class AppServiceProvider extends ServiceProvider
public function register(): void
{
$this->app->bind(ExceptionHandler::class, function ($app) {
return new class($app) extends BaseHandler {
return new class($app) extends BaseHandler
{
public function render($request, Throwable $e)
{
// Handle specific custom exception

View file

@ -2,7 +2,6 @@
namespace App\Services;
class OutputService
{
public function response(bool $success = true, ?array $payload = null, array|string|null $errors = null): array

View file

@ -1,5 +1,7 @@
<?php
use App\Providers\AppServiceProvider;
return [
App\Providers\AppServiceProvider::class,
AppServiceProvider::class,
];

View file

@ -17,12 +17,14 @@
},
"require-dev": {
"fakerphp/faker": "^1.23",
"larastan/larastan": "^3.10",
"laravel/dusk": "^8.3",
"laravel/pail": "^1.1",
"laravel/pint": "^1.13",
"laravel/sail": "^1.26",
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.1",
"phpstan/phpstan-mockery": "^2.0",
"phpunit/phpunit": "^11.0.1"
},
"autoload": {

View file

@ -1,5 +1,7 @@
<?php
use App\Models\Planner;
return [
/*
@ -50,7 +52,7 @@
'providers' => [
'planners' => [
'driver' => 'eloquent',
'model' => App\Models\Planner::class,
'model' => Planner::class,
],
// 'users' => [

View file

@ -1,5 +1,8 @@
<?php
use Illuminate\Cookie\Middleware\EncryptCookies;
use Illuminate\Foundation\Http\Middleware\ValidateCsrfToken;
use Laravel\Sanctum\Http\Middleware\AuthenticateSession;
use Laravel\Sanctum\Sanctum;
return [
@ -75,9 +78,9 @@
*/
'middleware' => [
'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class,
'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class,
'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
'authenticate_session' => AuthenticateSession::class,
'encrypt_cookies' => EncryptCookies::class,
'validate_csrf_token' => ValidateCsrfToken::class,
],
];

View file

@ -3,8 +3,8 @@
namespace Database\Factories;
use App\Models\Dish;
use App\Models\UserDish;
use App\Models\User;
use App\Models\UserDish;
use Illuminate\Database\Eloquent\Factories\Factory;
/**

View file

@ -2,7 +2,6 @@
namespace Database\Seeders;
use App\Models\User;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;

View file

@ -14,7 +14,7 @@ public function run(): void
[
'name' => 'Admin',
'email' => 'admin@test.com',
'password' => 'password'
'password' => 'password',
],
])->each(fn (array $data) => Planner::create([
'name' => $data['name'],

View file

@ -41,11 +41,10 @@ private function createScheduleForPeriod(CarbonPeriod $period): void
$planner = Planner::all()->first() ?? Planner::factory()->create();
collect($period)
->each(fn (Carbon $date) =>
User::query()
->each(fn (Carbon $date) => User::query()
->inRandomOrder()
->get()
->each(fn (User $user) => (new CreateScheduledUserDishAction())
->each(fn (User $user) => (new CreateScheduledUserDishAction)
->execute(
planner: $planner,
schedule: resolve(ScheduleRepository::class)->findOrCreate($planner, $date),

View file

@ -16,7 +16,6 @@ public function run(): void
->each(fn (string $name) => User::factory()->create([
'planner_id' => $planner->id,
'name' => $name,
]))
;
]));
}
}

2839
phpstan-baseline.neon Normal file

File diff suppressed because it is too large Load diff

16
phpstan.neon Normal file
View file

@ -0,0 +1,16 @@
includes:
- vendor/larastan/larastan/extension.neon
- vendor/phpstan/phpstan-mockery/extension.neon
- phpstan-baseline.neon
parameters:
level: 7
paths:
- app/
- src/
- tests/
excludePaths:
- bootstrap/*.php
- storage/*

View file

@ -23,13 +23,6 @@
<directory>app/Providers</directory>
</exclude>
</source>
<coverage>
<report>
<html outputDirectory="coverage"/>
<text outputFile="coverage/coverage.txt" showOnlySummary="true"/>
<clover outputFile="coverage/clover.xml"/>
</report>
</coverage>
<php>
<env name="APP_ENV" value="testing"/>
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
@ -40,12 +33,8 @@
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="TELESCOPE_ENABLED" value="false"/>
<env name="DB_CONNECTION" value="mysql"/>
<env name="DB_HOST" value="mysql"/>
<env name="DB_PORT" value="3306"/>
<env name="DB_DATABASE" value="testing"/>
<env name="DB_USERNAME" value="sail"/>
<env name="DB_PASSWORD" value="password"/>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
</php>
</phpunit>

3
pint.json Normal file
View file

@ -0,0 +1,3 @@
{
"preset": "laravel"
}

View file

@ -5,12 +5,12 @@
Route::group([
'as' => 'api.',
], function () {
require __DIR__ . '/api/auth.php';
require __DIR__.'/api/auth.php';
Route::middleware('auth:sanctum')->group(function () {
require __DIR__ . '/api/users.php';
require __DIR__ . '/api/dishes.php';
require __DIR__ . '/api/schedule.php';
require __DIR__ . '/api/scheduledUserDishes.php';
require __DIR__.'/api/users.php';
require __DIR__.'/api/dishes.php';
require __DIR__.'/api/schedule.php';
require __DIR__.'/api/scheduledUserDishes.php';
});
});

View file

@ -18,4 +18,3 @@
->json($request->user())
)->name('me');
});

View file

@ -1,8 +1,8 @@
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Auth\LoginController;
use App\Http\Controllers\Auth\RegisterController;
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return redirect()->route('dashboard');

View file

@ -81,7 +81,7 @@ pkgs.mkShell {
}
dev-test() {
podman-compose exec -T app php -d memory_limit=512M vendor/bin/phpunit "$@"
podman-compose exec -T app env $(grep -vE '^\s*(#|$)' .env.testing) php -d memory_limit=512M vendor/bin/phpunit "$@"
}
dev-fix-permissions() {

View file

@ -17,7 +17,7 @@ public function login(Request $request): JsonResponse
'password' => ['required'],
]);
if (!Auth::attempt($credentials)) {
if (! Auth::attempt($credentials)) {
return response()->json([
'message' => 'The provided credentials are incorrect.',
], 401);

View file

@ -31,7 +31,7 @@ public function index(): JsonResponse
public function store(StoreDishRequest $request): JsonResponse
{
$dish = (new CreateDishAction())->execute($request->validated());
$dish = (new CreateDishAction)->execute($request->validated());
return $this->success(['dish' => new DishResource($dish)]);
}
@ -47,7 +47,7 @@ public function update(UpdateDishRequest $request, Dish $dish): JsonResponse
{
Gate::authorize('update', $dish);
$dish = (new UpdateDishAction())->execute($dish, $request->validated());
$dish = (new UpdateDishAction)->execute($dish, $request->validated());
return $this->success(['dish' => new DishResource($dish)]);
}
@ -56,14 +56,14 @@ public function destroy(Dish $dish): JsonResponse
{
Gate::authorize('delete', $dish);
(new DeleteDishAction())->execute($dish);
(new DeleteDishAction)->execute($dish);
return $this->success(null);
}
public function syncUsers(SyncUsersRequest $request, Dish $dish): JsonResponse
{
(new SyncUsersAction())->execute($dish, Arr::get($request->validated(), 'users', []));
(new SyncUsersAction)->execute($dish, Arr::get($request->validated(), 'users', []));
return $this->success(['dish' => new DishResource($dish->refresh())]);
}
@ -72,14 +72,14 @@ public function addUsers(AddUsersToDishRequest $request, Dish $dish): JsonRespon
{
Gate::authorize('update', $dish);
(new AddUsersToDishAction())->execute($dish, Arr::get($request->validated(), 'users', []));
(new AddUsersToDishAction)->execute($dish, Arr::get($request->validated(), 'users', []));
return $this->success(['dish' => new DishResource($dish->refresh())]);
}
public function removeUsers(RemoveUsersFromDishRequest $request, Dish $dish): JsonResponse
{
(new RemoveUsersFromDishAction())->execute($dish, Arr::get($request->validated(), 'users', []));
(new RemoveUsersFromDishAction)->execute($dish, Arr::get($request->validated(), 'users', []));
return $this->success(['dish' => new DishResource($dish->refresh())]);
}

View file

@ -7,5 +7,6 @@
class InvalidDishException extends CustomException
{
protected $message = 'INVALID_DISH';
protected $code = 422;
}

View file

@ -11,7 +11,7 @@ class DraftScheduleForDateAction
public function execute(Schedule $schedule): Schedule
{
User::all()
->reject(fn($user) => $schedule
->reject(fn ($user) => $schedule
->scheduledUserDishes
->map(fn (ScheduledUserDish $scheduledUserDish) => $scheduledUserDish->userDish?->user)
->filter()

View file

@ -7,7 +7,6 @@
use App\Models\ScheduledUserDish;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
class GenerateScheduleForMonthAction
@ -84,7 +83,7 @@ private function generateSchedulesForPeriod(
);
foreach ($userIds as $userId) {
if (!isset($userDishesMap[$userId]) || $userDishesMap[$userId]->isEmpty()) {
if (! isset($userDishesMap[$userId]) || $userDishesMap[$userId]->isEmpty()) {
continue;
}

View file

@ -29,7 +29,7 @@ public function execute(Planner $planner, Schedule $schedule, User $user, bool $
);
}
if (!$overwrite && $scheduledUserDish->userDish) {
if (! $overwrite && $scheduledUserDish->userDish) {
return $scheduledUserDish;
}

View file

@ -5,8 +5,6 @@
use App\Http\Controllers\Api\ApiController;
use App\Models\Planner;
use App\Models\Schedule;
use Carbon\CarbonPeriod;
use DishPlanner\Schedule\Actions\DraftScheduleForPeriodAction;
use DishPlanner\Schedule\Actions\GenerateScheduleForPeriodAction;
use DishPlanner\Schedule\Actions\UpdateScheduleAction;
use DishPlanner\Schedule\Repositories\ScheduleRepository;
@ -83,7 +81,7 @@ public function generate(GenerateScheduleRequest $request): JsonResponse
/** @var Planner $planner */
$planner = auth()->user();
(new GenerateScheduleForPeriodAction())->execute($planner, $request->get('overwrite', false));
(new GenerateScheduleForPeriodAction)->execute($planner, $request->get('overwrite', false));
return $this->success(null);
}

View file

@ -37,7 +37,7 @@ public function __invoke(ScheduleUserDishRequest $request, Carbon $date): JsonRe
->first();
if (! $scheduledUserDish) {
$scheduledUserDish = new ScheduledUserDish();
$scheduledUserDish = new ScheduledUserDish;
}
abort_if(

View file

@ -12,7 +12,7 @@ public function rules(): array
'user_dish_id' => [
'required_without:skipped',
'exists:user_dishes,id',
'nullable'
'nullable',
],
'user_id' => ['required', 'exists:users,id'],
'skipped' => ['required_if:user_dish_id,null', 'boolean'],

View file

@ -5,7 +5,7 @@
use Illuminate\Foundation\Http\FormRequest;
/**
* @property boolean $is_skipped
* @property bool $is_skipped
*/
class UpdateScheduleRequest extends FormRequest
{

View file

@ -43,7 +43,7 @@ private function buildCalendarDays(int $year, int $month, int $daysInMonth, Coll
'date' => $date,
'isToday' => $date->isToday(),
'scheduledDishes' => $scheduledDishes,
'isEmpty' => $scheduledDishes->isEmpty()
'isEmpty' => $scheduledDishes->isEmpty(),
];
} else {
$calendarDays[] = [
@ -51,7 +51,7 @@ private function buildCalendarDays(int $year, int $month, int $daysInMonth, Coll
'date' => null,
'isToday' => false,
'scheduledDishes' => collect(),
'isEmpty' => true
'isEmpty' => true,
];
}
}

View file

@ -31,7 +31,7 @@ public function generate(Planner $planner): void
$users->each(function (User $user) use ($date, $planner, $scheduleRepository, $userDishRepository) {
$schedule = $scheduleRepository->findOrCreate($planner, $date);
(new CreateScheduledUserDishAction())->execute(
(new CreateScheduledUserDishAction)->execute(
planner: $planner,
schedule: $schedule,
userDish: $userDishRepository->getRandomForDate($user, $date)

View file

@ -16,7 +16,7 @@ class CreateScheduledUserDishAction
public function execute(Planner $planner, Schedule $schedule, UserDish $userDish): ScheduledUserDish
{
if ($userDish->dish->planner_id !== $planner->id || $userDish->user->planner_id !== $planner->id) {
throw new InvalidPlannerException();
throw new InvalidPlannerException;
}
return ScheduledUserDish::create([

View file

@ -36,7 +36,7 @@ public function create(CreateScheduleRequest $request): JsonResponse
$schedule = resolve(ScheduleRepository::class)->findOrCreate($planner, $date);
try {
$scheduledUserDish = (new CreateScheduledUserDishAction())->execute(
$scheduledUserDish = (new CreateScheduledUserDishAction)->execute(
planner: $planner,
schedule: $schedule,
userDish: $userDish,
@ -46,7 +46,7 @@ public function create(CreateScheduleRequest $request): JsonResponse
}
return $this->success([
'scheduled_user_dish' => new ScheduledUserDishResource($scheduledUserDish)
'scheduled_user_dish' => new ScheduledUserDishResource($scheduledUserDish),
]);
}
@ -63,7 +63,7 @@ public function update(UpdateScheduledUserDishRequest $request, ScheduledUserDis
{
Gate::authorize('update', $scheduledUserDish);
(new UpdateScheduledUserDishAction())->execute(
(new UpdateScheduledUserDishAction)->execute(
scheduledUserDish: $scheduledUserDish,
userDish: UserDish::find($request->user_dish_id),
isSkipped: $request->is_skipped ?? null,
@ -78,7 +78,7 @@ public function delete(ScheduledUserDish $scheduledUserDish): JsonResponse
{
Gate::authorize('delete', $scheduledUserDish);
(new DeleteScheduledUserDishAction())->execute($scheduledUserDish);
(new DeleteScheduledUserDishAction)->execute($scheduledUserDish);
return $this->success(null);
}

View file

@ -4,7 +4,6 @@
use App\Models\Planner;
use App\Models\ScheduledUserDish;
use DishPlanner\UserDish\Policies\UserDishPolicy;
use Illuminate\Support\Facades\Gate;
class ScheduledUserDishPolicy

View file

@ -2,7 +2,6 @@
namespace DishPlanner\User\Actions;
use App\Models\Planner;
use App\Models\User;
class DeleteUserAction

View file

@ -2,7 +2,6 @@
namespace DishPlanner\User\Actions;
use App\Models\Planner;
use App\Models\User;
class UpdateUserAction

View file

@ -33,7 +33,7 @@ public function create(CreateUserRequest $request): JsonResponse
$requestData = $request->validated();
$user = (new CreateUserAction())
$user = (new CreateUserAction)
->execute($planner, Arr::get($requestData, 'name'));
return $this->success(['user' => new UserResource($user)]);
@ -43,7 +43,7 @@ public function update(UpdateUserRequest $request, User $user): JsonResponse
{
Gate::authorize('update', $user);
$user = (new UpdateUserAction())
$user = (new UpdateUserAction)
->execute($user, Arr::get($request->validated(), 'name'));
return $this->success(['user' => new UserResource($user)]);
@ -53,7 +53,7 @@ public function delete(User $user): JsonResponse
{
Gate::authorize('delete', $user);
(new DeleteUserAction())->execute($user);
(new DeleteUserAction)->execute($user);
return $this->success(null, 201);
}

View file

@ -17,8 +17,8 @@ class CreateFixedRecurrenceAction
*/
public function execute(UserDish $userDish, string $recurrenceType, int $value): void
{
if (!in_array($recurrenceType, self::FIXED_RECURRENCES)) {
throw new InvalidRecurrenceTypeException();
if (! in_array($recurrenceType, self::FIXED_RECURRENCES)) {
throw new InvalidRecurrenceTypeException;
}
$recurrence = $recurrenceType::create([

View file

@ -15,7 +15,7 @@ class CreateMinimumRecurrenceAction
public function execute(UserDish $userDish, string $recurrenceType, int $recurrenceValue): void
{
if ($recurrenceType !== MinimumRecurrence::class) {
throw new InvalidRecurrenceTypeException();
throw new InvalidRecurrenceTypeException;
}
$existingRecurrenceForDay = $userDish

View file

@ -40,11 +40,11 @@ private function addRecurrences(UserDish $userDish, array $data): void
}
if ($recurrenceType === WeeklyRecurrence::class) {
(new CreateFixedRecurrenceAction())->execute($userDish, $recurrenceType, $recurrenceValue);
(new CreateFixedRecurrenceAction)->execute($userDish, $recurrenceType, $recurrenceValue);
} elseif ($recurrenceType === MinimumRecurrence::class) {
(new CreateMinimumRecurrenceAction())->execute($userDish, $recurrenceType, $recurrenceValue);
(new CreateMinimumRecurrenceAction)->execute($userDish, $recurrenceType, $recurrenceValue);
} else {
throw new InvalidRecurrenceTypeException();
throw new InvalidRecurrenceTypeException;
}
}
}

View file

@ -14,7 +14,7 @@ class DeleteFixedRecurrenceAction
public function execute(RecurrenceInterface $recurrence): void
{
if (! $recurrence instanceof WeeklyRecurrence) {
throw new InvalidRecurrenceTypeException();
throw new InvalidRecurrenceTypeException;
}
$recurrence->delete();

View file

@ -15,7 +15,7 @@ class DeleteMinimumRecurrenceAction
public function execute(RecurrenceInterface $recurrence): void
{
if (! $recurrence instanceof MinimumRecurrence) {
throw new InvalidRecurrenceTypeException();
throw new InvalidRecurrenceTypeException;
}
UserDishRecurrence::query()

View file

@ -32,9 +32,9 @@ public function execute(UserDish $userDish, Collection $recurrences): UserDish
}
match ($recurrenceType) {
WeeklyRecurrence::class => (new CreateFixedRecurrenceAction())->execute($userDish, $recurrenceType, $recurrenceValue),
MinimumRecurrence::class => (new CreateMinimumRecurrenceAction())->execute($userDish, $recurrenceType, $recurrenceValue),
default => throw new InvalidRecurrenceTypeException(),
WeeklyRecurrence::class => (new CreateFixedRecurrenceAction)->execute($userDish, $recurrenceType, $recurrenceValue),
MinimumRecurrence::class => (new CreateMinimumRecurrenceAction)->execute($userDish, $recurrenceType, $recurrenceValue),
default => throw new InvalidRecurrenceTypeException,
};
});

View file

@ -15,7 +15,7 @@ class UpdateFixedRecurrenceAction
public function execute(RecurrenceInterface $recurrence, array $data): RecurrenceInterface
{
if (! $recurrence instanceof WeeklyRecurrence) {
throw new InvalidRecurrenceTypeException();
throw new InvalidRecurrenceTypeException;
}
$weekday = Arr::get($data, 'recurrence_data.weekday');

View file

@ -15,7 +15,7 @@ class UpdateMinimumRecurrenceAction
public function execute(RecurrenceInterface $recurrence, array $data): RecurrenceInterface
{
if (! $recurrence instanceof MinimumRecurrence) {
throw new InvalidRecurrenceTypeException();
throw new InvalidRecurrenceTypeException;
}
$days = Arr::get($data, 'recurrence_data.days');

View file

@ -20,7 +20,7 @@ public function __invoke(Request $request)
$userDishes = $userDishRepository->getAllForPlanner($planner);
return $this->success([
'user_dishes' => UserDishResource::collection($userDishes)->collection->toArray()
'user_dishes' => UserDishResource::collection($userDishes)->collection->toArray(),
]);
}
}

View file

@ -48,7 +48,7 @@ public function show(User $user, Dish $dish): JsonResponse
*/
public function store(CreateUserDishRequest $request, User $user, Dish $dish): JsonResponse
{
$userDish = (new CreateUserDishAction())->execute($dish, $user, $request->validated());
$userDish = (new CreateUserDishAction)->execute($dish, $user, $request->validated());
return $this->success([
'user_dish' => new UserDishResource($userDish),
@ -57,7 +57,7 @@ public function store(CreateUserDishRequest $request, User $user, Dish $dish): J
public function destroy(User $user, Dish $dish): JsonResponse
{
(new DeleteUserDishAction())->execute($user, $dish);
(new DeleteUserDishAction)->execute($user, $dish);
return $this->success(null);
}

View file

@ -35,7 +35,7 @@ public function store(StoreUserDishRecurrenceRequest $request, User $user, Dish
$recurrences = collect($request->validated());
(new SyncRecurrencesForUserDishAction())->execute($userDish, $recurrences);
(new SyncRecurrencesForUserDishAction)->execute($userDish, $recurrences);
return $this->success([
'user_dish' => new UserDishResource($userDish->refresh()),
@ -51,9 +51,9 @@ public function update(UpdateUserDishFixedRecurrenceRequest $request, UserDish $
$recurrence = $recurrenceClass::findOrFail($recurrenceId);
if ($recurrence instanceof WeeklyRecurrence) {
(new UpdateFixedRecurrenceAction())->execute($recurrence, $request->validated());
(new UpdateFixedRecurrenceAction)->execute($recurrence, $request->validated());
} elseif ($recurrenceClass === MinimumRecurrence::class) {
(new UpdateMinimumRecurrenceAction())->execute($recurrence, $request->validated());
(new UpdateMinimumRecurrenceAction)->execute($recurrence, $request->validated());
} else {
return $this->error('invalid recurrence type');
}
@ -72,9 +72,9 @@ public function destroy(UserDish $userDish, string $recurrenceType, int $recurre
$recurrence = $recurrenceClass::findOrFail($recurrenceId);
if ($recurrence instanceof WeeklyRecurrence) {
(new DeleteFixedRecurrenceAction())->execute($recurrence);
(new DeleteFixedRecurrenceAction)->execute($recurrence);
} elseif ($recurrenceClass === MinimumRecurrence::class) {
(new DeleteMinimumRecurrenceAction())->execute($recurrence);
(new DeleteMinimumRecurrenceAction)->execute($recurrence);
} else {
return $this->error('invalid recurrence type');
}

View file

@ -2,5 +2,4 @@
namespace DishPlanner\UserDish\Interfaces;
interface FixedRecurrenceInterface
{}
interface FixedRecurrenceInterface {}

View file

@ -2,5 +2,4 @@
namespace DishPlanner\UserDish\Interfaces;
interface RecurrenceInterface
{}
interface RecurrenceInterface {}

View file

@ -12,7 +12,6 @@
use App\Models\WeeklyRecurrence;
use Carbon\Carbon;
use Carbon\CarbonPeriod;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Collection as SupportCollection;
@ -70,7 +69,7 @@ public function findInterferingUserDishes(User $user, Carbon $date): Collection
->get()
->flatMap(fn (Schedule $schedule) => $schedule->scheduledUserDishes)
->filter(fn (ScheduledUserDish $scheduledUserDish) => $scheduledUserDish->userDish->user_id === $user->id)
->filter(fn(ScheduledUserDish $scheduledUserDish) => $scheduledUserDish->userDish->recurrences->contains('recurrence_type', MinimumRecurrence::class))
->filter(fn (ScheduledUserDish $scheduledUserDish) => $scheduledUserDish->userDish->recurrences->contains('recurrence_type', MinimumRecurrence::class))
->filter(function (ScheduledUserDish $scheduledUserDish) use ($date) {
$minimum = $scheduledUserDish->userDish
->recurrences

View file

@ -19,7 +19,7 @@ public function rules(): array
MinimumRecurrence::class,
WeeklyRecurrence::class,
]),
'required_with:*.recurrence_value'
'required_with:*.recurrence_value',
],
'*.value' => ['sometimes', 'integer', 'required_with:*.recurrence_type'],
];

View file

@ -14,14 +14,14 @@ public function rules(): array
'recurrence_type' => [
'required',
'string',
'in:' . implode(',', [
'in:'.implode(',', [
MinimumRecurrence::class,
WeeklyRecurrence::class,
]),
],
'recurrence_data' => 'required|array',
'recurrence_data.days' => 'required_if:recurrence_type,' . MinimumRecurrence::class . '|integer|min:1',
'recurrence_data.weekday' => 'required_if:recurrence_type,' . WeeklyRecurrence::class . '|integer|between:0,6',
'recurrence_data.days' => 'required_if:recurrence_type,'.MinimumRecurrence::class.'|integer|min:1',
'recurrence_data.weekday' => 'required_if:recurrence_type,'.WeeklyRecurrence::class.'|integer|between:0,6',
];
}

View file

@ -2,15 +2,17 @@
namespace Tests\Browser\Auth;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
use App\Models\Planner;
use Illuminate\Support\Facades\Hash;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
class LoginTest extends DuskTestCase
{
protected static $testPlanner = null;
protected static $testEmail = null;
protected static $testPassword = 'password';
protected function ensureTestPlannerExists(): void
@ -26,7 +28,7 @@ protected function ensureTestPlannerExists(): void
}
}
public function testSuccessfulLogin(): void
public function test_successful_login(): void
{
$this->ensureTestPlannerExists();
@ -44,7 +46,7 @@ public function testSuccessfulLogin(): void
});
}
public function testLoginWithWrongCredentials(): void
public function test_login_with_wrong_credentials(): void
{
$this->ensureTestPlannerExists();
@ -63,7 +65,7 @@ public function testLoginWithWrongCredentials(): void
});
}
public function testLoginFormRequiredFields(): void
public function test_login_form_required_fields(): void
{
$this->browse(function (Browser $browser) {
$browser->driver->manage()->deleteAllCookies();

View file

@ -3,16 +3,17 @@
namespace Tests\Browser\Dishes;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
use Tests\Browser\Pages\DishesPage;
use Tests\Browser\Components\DishModal;
use Tests\Browser\LoginHelpers;
use Tests\Browser\Pages\DishesPage;
use Tests\DuskTestCase;
class CreateDishFormValidationTest extends DuskTestCase
{
use LoginHelpers;
protected static $createDishFormValidationTestPlanner = null;
protected static $createDishFormValidationTestEmail = null;
protected function setUp(): void
@ -31,7 +32,7 @@ protected function tearDown(): void
parent::tearDown();
}
public function testCreateDishFormValidation(): void
public function test_create_dish_form_validation(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToDishes($browser);

View file

@ -3,16 +3,17 @@
namespace Tests\Browser\Dishes;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
use Tests\Browser\Pages\DishesPage;
use Tests\Browser\Components\DishModal;
use Tests\Browser\LoginHelpers;
use Tests\Browser\Pages\DishesPage;
use Tests\DuskTestCase;
class CreateDishSuccessTest extends DuskTestCase
{
use LoginHelpers;
protected static $createDishSuccessTestPlanner = null;
protected static $createDishSuccessTestEmail = null;
protected function setUp(): void
@ -31,10 +32,10 @@ protected function tearDown(): void
parent::tearDown();
}
public function testCanCreateDishSuccessfully(): void
public function test_can_create_dish_successfully(): void
{
$this->browse(function (Browser $browser) {
$dishName = 'Test Dish ' . uniqid();
$dishName = 'Test Dish '.uniqid();
$this->loginAndGoToDishes($browser);

View file

@ -3,16 +3,16 @@
namespace Tests\Browser\Dishes;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
use Tests\Browser\Pages\DishesPage;
use Tests\Browser\Components\DishModal;
use Tests\Browser\LoginHelpers;
use Tests\Browser\Pages\DishesPage;
use Tests\DuskTestCase;
class CreateDishTest extends DuskTestCase
{
use LoginHelpers;
protected static $createDishTestPlanner = null;
protected static $createDishTestEmail = null;
protected function setUp(): void
@ -31,7 +31,7 @@ protected function tearDown(): void
parent::tearDown();
}
public function testCanAccessDishesPage(): void
public function test_can_access_dishes_page(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToDishes($browser);

View file

@ -2,16 +2,17 @@
namespace Tests\Browser\Dishes;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
use Tests\Browser\LoginHelpers;
use App\Models\Planner;
use Laravel\Dusk\Browser;
use Tests\Browser\LoginHelpers;
use Tests\DuskTestCase;
class DeleteDishTest extends DuskTestCase
{
use LoginHelpers;
protected static $deleteDishTestPlanner = null;
protected static $deleteDishTestEmail = null;
protected function setUp(): void
@ -30,7 +31,7 @@ protected function tearDown(): void
parent::tearDown();
}
public function testCanAccessDeleteFeature(): void
public function test_can_access_delete_feature(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToDishes($browser)

View file

@ -3,14 +3,15 @@
namespace Tests\Browser\Dishes;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
use Tests\Browser\LoginHelpers;
use Tests\DuskTestCase;
class DishDeletionSafetyTest extends DuskTestCase
{
use LoginHelpers;
protected static $dishDeletionSafetyTestPlanner = null;
protected static $dishDeletionSafetyTestEmail = null;
protected function setUp(): void
@ -29,7 +30,7 @@ protected function tearDown(): void
parent::tearDown();
}
public function testDeletionSafetyFeatures(): void
public function test_deletion_safety_features(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToDishes($browser);

View file

@ -2,16 +2,17 @@
namespace Tests\Browser\Dishes;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
use Tests\Browser\LoginHelpers;
use App\Models\Planner;
use Laravel\Dusk\Browser;
use Tests\Browser\LoginHelpers;
use Tests\DuskTestCase;
class EditDishTest extends DuskTestCase
{
use LoginHelpers;
protected static $editDishTestPlanner = null;
protected static $editDishTestEmail = null;
protected function setUp(): void
@ -30,7 +31,7 @@ protected function tearDown(): void
parent::tearDown();
}
public function testCanAccessEditFeature(): void
public function test_can_access_edit_feature(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToDishes($browser)
@ -43,7 +44,7 @@ public function testCanAccessEditFeature(): void
});
}
public function testEditModalComponents(): void
public function test_edit_modal_components(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToDishes($browser)
@ -52,7 +53,7 @@ public function testEditModalComponents(): void
});
}
public function testDishesPageStructure(): void
public function test_dishes_page_structure(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToDishes($browser)

View file

@ -2,25 +2,29 @@
namespace Tests\Browser;
use App\Models\Planner;
use Illuminate\Support\Facades\Hash;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
trait LoginHelpers
{
protected static $testPlanner = null;
protected static $testEmail = null;
protected static $testPassword = 'password';
protected function ensureTestPlannerExists(): void
{
// Always create a fresh planner for each test class to avoid session conflicts
if (self::$testPlanner === null || !self::$testPlanner->exists) {
if (self::$testPlanner === null || ! self::$testPlanner->exists) {
// Generate unique email for this test run
self::$testEmail = fake()->unique()->safeEmail();
self::$testPlanner = \App\Models\Planner::factory()->create([
self::$testPlanner = Planner::factory()->create([
'email' => self::$testEmail,
'password' => \Illuminate\Support\Facades\Hash::make(self::$testPassword),
'password' => Hash::make(self::$testPassword),
]);
}
}
@ -41,7 +45,7 @@ protected function loginAndNavigate(Browser $browser, string $page = '/dashboard
->press('Sign In')
->waitForLocation('/dashboard', DuskTestCase::TIMEOUT_MEDIUM) // Wait for successful login redirect
->pause(DuskTestCase::PAUSE_SHORT) // Brief pause for any initialization
->visit('http://dishplanner_app:8000' . $page)
->visit('http://dishplanner_app:8000'.$page)
->pause(DuskTestCase::PAUSE_MEDIUM); // Let Livewire components initialize
}

View file

@ -84,7 +84,7 @@ public function selectUser(Browser $browser, string $userName): void
$browser->check("input[type='checkbox'][value]", $userName);
}
public function assertSuccessMessage(Browser $browser, string $message = null): void
public function assertSuccessMessage(Browser $browser, ?string $message = null): void
{
if ($message) {
$browser->assertSee($message);

View file

@ -2,9 +2,9 @@
namespace Tests\Browser;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class RedirectTest extends DuskTestCase
{
@ -13,7 +13,7 @@ class RedirectTest extends DuskTestCase
/**
* Test that unauthenticated users are redirected to login
*/
public function testUnauthenticatedRedirectsToLogin()
public function test_unauthenticated_redirects_to_login()
{
$this->browse(function (Browser $browser) {
$browser->visit('http://dishplanner_app:8000/dashboard')
@ -25,7 +25,7 @@ public function testUnauthenticatedRedirectsToLogin()
/**
* Test that login page loads correctly
*/
public function testLoginPageLoads()
public function test_login_page_loads()
{
$this->browse(function (Browser $browser) {
$browser->visit('http://dishplanner_app:8000/login')

View file

@ -7,15 +7,19 @@
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
use Tests\Browser\Pages\SchedulePage;
use Tests\DuskTestCase;
class GenerateScheduleTest extends DuskTestCase
{
protected static $planner = null;
protected static $email = null;
protected static $password = 'password';
protected static $user = null;
protected static $dish = null;
protected function setUp(): void
@ -64,7 +68,7 @@ protected function loginAsPlanner(Browser $browser): Browser
->pause(DuskTestCase::PAUSE_MEDIUM);
}
public function testCanGenerateScheduleWithUserAndDish(): void
public function test_can_generate_schedule_with_user_and_dish(): void
{
$this->browse(function (Browser $browser) {
$this->loginAsPlanner($browser);
@ -78,7 +82,7 @@ public function testCanGenerateScheduleWithUserAndDish(): void
});
}
public function testGeneratedScheduleShowsDishOnCalendar(): void
public function test_generated_schedule_shows_dish_on_calendar(): void
{
$this->browse(function (Browser $browser) {
$this->loginAsPlanner($browser);
@ -91,7 +95,7 @@ public function testGeneratedScheduleShowsDishOnCalendar(): void
});
}
public function testCanClearMonthSchedule(): void
public function test_can_clear_month_schedule(): void
{
$this->browse(function (Browser $browser) {
$this->loginAsPlanner($browser);
@ -109,7 +113,7 @@ public function testCanClearMonthSchedule(): void
});
}
public function testUserSelectionAffectsGeneration(): void
public function test_user_selection_affects_generation(): void
{
$this->browse(function (Browser $browser) {
$this->loginAsPlanner($browser);
@ -118,7 +122,7 @@ public function testUserSelectionAffectsGeneration(): void
// Verify the user checkbox is present
->assertSee('Test User')
// User should be selected by default
->assertChecked("input[value='" . self::$user->id . "']");
->assertChecked("input[value='".self::$user->id."']");
});
}
}

View file

@ -3,15 +3,16 @@
namespace Tests\Browser\Schedule;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
use Tests\Browser\Pages\SchedulePage;
use Tests\Browser\LoginHelpers;
use Tests\Browser\Pages\SchedulePage;
use Tests\DuskTestCase;
class SchedulePageTest extends DuskTestCase
{
use LoginHelpers;
protected static $schedulePageTestPlanner = null;
protected static $schedulePageTestEmail = null;
protected function setUp(): void
@ -28,7 +29,7 @@ protected function tearDown(): void
parent::tearDown();
}
public function testCanAccessSchedulePage(): void
public function test_can_access_schedule_page(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToSchedule($browser);
@ -39,7 +40,7 @@ public function testCanAccessSchedulePage(): void
});
}
public function testSchedulePageHasMonthNavigation(): void
public function test_schedule_page_has_month_navigation(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToSchedule($browser);
@ -51,7 +52,7 @@ public function testSchedulePageHasMonthNavigation(): void
});
}
public function testCanNavigateToNextMonth(): void
public function test_can_navigate_to_next_month(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToSchedule($browser);
@ -64,7 +65,7 @@ public function testCanNavigateToNextMonth(): void
});
}
public function testCanNavigateToPreviousMonth(): void
public function test_can_navigate_to_previous_month(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToSchedule($browser);
@ -77,7 +78,7 @@ public function testCanNavigateToPreviousMonth(): void
});
}
public function testScheduleGeneratorShowsUserSelection(): void
public function test_schedule_generator_shows_user_selection(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToSchedule($browser);
@ -89,7 +90,7 @@ public function testScheduleGeneratorShowsUserSelection(): void
});
}
public function testCalendarDisplaysDaysOfWeek(): void
public function test_calendar_displays_days_of_week(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToSchedule($browser);

View file

@ -3,15 +3,16 @@
namespace Tests\Browser\Users;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
use Tests\Browser\Pages\UsersPage;
use Tests\Browser\LoginHelpers;
use Tests\Browser\Pages\UsersPage;
use Tests\DuskTestCase;
class CreateUserTest extends DuskTestCase
{
use LoginHelpers;
protected static $createUserTestPlanner = null;
protected static $createUserTestEmail = null;
protected function setUp(): void
@ -30,7 +31,7 @@ protected function tearDown(): void
parent::tearDown();
}
public function testCanAccessUsersPage(): void
public function test_can_access_users_page(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToUsers($browser);
@ -41,7 +42,7 @@ public function testCanAccessUsersPage(): void
});
}
public function testCanOpenCreateUserModal(): void
public function test_can_open_create_user_modal(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToUsers($browser);
@ -55,7 +56,7 @@ public function testCanOpenCreateUserModal(): void
});
}
public function testCreateUserFormValidation(): void
public function test_create_user_form_validation(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToUsers($browser);
@ -68,10 +69,10 @@ public function testCreateUserFormValidation(): void
});
}
public function testCanCreateUser(): void
public function test_can_create_user(): void
{
$this->browse(function (Browser $browser) {
$userName = 'TestCreate_' . uniqid();
$userName = 'TestCreate_'.uniqid();
$this->loginAndGoToUsers($browser);
@ -85,7 +86,7 @@ public function testCanCreateUser(): void
});
}
public function testCanCancelUserCreation(): void
public function test_can_cancel_user_creation(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToUsers($browser);

View file

@ -5,7 +5,6 @@
use Facebook\WebDriver\Chrome\ChromeOptions;
use Facebook\WebDriver\Remote\DesiredCapabilities;
use Facebook\WebDriver\Remote\RemoteWebDriver;
use Illuminate\Support\Collection;
use Laravel\Dusk\TestCase as BaseTestCase;
use PHPUnit\Framework\Attributes\BeforeClass;
@ -13,8 +12,11 @@ abstract class DuskTestCase extends BaseTestCase
{
// Timeout constants for consistent timing across all Dusk tests
public const TIMEOUT_SHORT = 2; // 2 seconds max for most operations
public const TIMEOUT_MEDIUM = 3; // 3 seconds for slower operations
public const PAUSE_SHORT = 500; // 0.5 seconds for quick pauses
public const PAUSE_MEDIUM = 1000; // 1 second for medium pauses
/**

View file

@ -2,10 +2,10 @@
namespace Tests\Feature;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
use Tests\TestCase;
use App\Models\Planner;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AuthenticationTest extends TestCase
{
@ -61,10 +61,6 @@ public function test_session_is_created_on_login_page(): void
// Check if CSRF token is generated
$this->assertNotNull(csrf_token());
// Check session driver
$sessionDriver = config('session.driver');
$this->assertNotEquals('array', $sessionDriver, 'Session driver should not be array for authentication');
$response->assertStatus(200);
$response->assertSessionHasNoErrors();
}

View file

@ -3,7 +3,6 @@
namespace Tests\Feature\Dish;
use App\Models\Dish;
use App\Models\Planner;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Str;
use Illuminate\Testing\Fluent\AssertableJson;

View file

@ -64,7 +64,6 @@ public function test_it_deletes_user_dishes_when_deleting_a_dish(): void
$this->assertDatabaseEmpty(UserDish::class);
}
public function test_planner_cannot_delete_dish_from_other_planner(): void
{
$planner = $this->planner;

View file

@ -2,9 +2,9 @@
namespace Tests\Feature;
use Tests\TestCase;
use App\Models\Planner;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class RegistrationTest extends TestCase
{

View file

@ -3,12 +3,11 @@
namespace Tests\Feature\Schedule;
use App\Models\Dish;
use App\Models\Planner;
use App\Models\UserDish;
use App\Models\UserDishRecurrence;
use App\Models\Schedule;
use App\Models\ScheduledUserDish;
use App\Models\User;
use App\Models\UserDish;
use App\Models\UserDishRecurrence;
use App\Models\WeeklyRecurrence;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Carbon;
@ -115,6 +114,7 @@ public function test_fresh_schedule_adheres_to_fixed_recurrences(): void
$this->assertContains($targetUserDish->id, $targetScheduledUserDishes);
}
public function test_schedule_can_be_overwritten(): void
{
$planner = $this->planner;
@ -132,8 +132,7 @@ public function test_schedule_can_be_overwritten(): void
});
// Assert that every user has `UserDish` records
$users->each(fn (User $user) =>
$this->assertNotEmpty($user->refresh()->userDishes)
$users->each(fn (User $user) => $this->assertNotEmpty($user->refresh()->userDishes)
);
$scheduleDay = Schedule::factory()
@ -172,7 +171,7 @@ public function test_schedule_can_be_overwritten(): void
$this->assertDatabaseCount(Schedule::class, 14);
$freshScheduleDay = Schedule::query()->where('date', $scheduleDay->date)->first();
$freshScheduleDay = Schedule::query()->whereDate('date', $scheduleDay->date)->first();
$this->assertNotEquals(
$originalUserDishes,
$freshScheduleDay->scheduledUserDishes->map(fn (ScheduledUserDish $scheduledUserDish) => [
@ -235,7 +234,6 @@ public function test_fixed_recurrence_takes_precedence_during_overwrite(): void
$this->assertDatabaseCount(Schedule::class, 1);
$this->assertDatabaseCount(ScheduledUserDish::class, 2);
$this
->actingAs($planner)
->post(route('api.schedule.generate'), [

View file

@ -38,6 +38,7 @@ public function test_full_calendar_dishes_list_for_a_given_date_range(): void
$schedule = Schedule::factory()->planner($planner)->date($date)->create();
$users->each(function (User $user) use ($schedule) {
$randomUserDish = $user->userDishes->random();
return $schedule->scheduledUserDishes()->create([
'user_dish_id' => $randomUserDish->id,
'user_id' => $randomUserDish->user->id,
@ -80,6 +81,7 @@ public function test_it_does_not_show_dishes_of_other_planner(): void
$schedule = Schedule::factory()->planner($otherPlanner)->date($date)->create();
$users->each(function (User $user) use ($schedule) {
$randomUserDish = $user->userDishes->random();
return $schedule->scheduledUserDishes()->create([
'user_dish_id' => $randomUserDish->id,
'user_id' => $randomUserDish->user->id,

View file

@ -33,11 +33,11 @@ public function test_user_can_mark_day_as_skipped(): void
$dishes->each(fn (Dish $dish) => $dish->users()->attach($users));
ScheduledUserDish::factory()
->schedule($schedule)
->userDish($dishes->random()->userDishes->random())
->userDish($userOne->userDishes->firstOrFail())
->create([]);
ScheduledUserDish::factory()
->schedule($schedule)
->userDish($dishes->random()->userDishes->random())
->userDish($userTwo->userDishes->firstOrFail())
->create([]);
$schedule->refresh();

View file

@ -75,7 +75,6 @@ public function test_planner_can_schedule_user_dishes(): void
->where('errors', null)
);
$this->assertDatabaseCount(Schedule::class, 1);
$this->assertDatabaseHas(Schedule::class, [
'date' => $scheduleDate,
@ -116,11 +115,10 @@ public function test_planner_cannot_schedule_user_dishes_from_other_planner(): v
->where('success', false)
->whereNull('payload')
->where('errors', [
"This action is unauthorized."
'This action is unauthorized.',
])
);
$this->assertDatabaseEmpty(Schedule::class);
$this->assertDatabaseEmpty(ScheduledUserDish::class);
}

View file

@ -79,7 +79,7 @@ public function test_planner_cannot_delete_a_scheduled_dish_of_another_planner()
->where('success', false)
->where('payload', null)
->where('errors', [
"This action is unauthorized."
'This action is unauthorized.',
])
);

View file

@ -129,7 +129,7 @@ public function test_planner_cannot_read_scheduled_user_dish_from_other_planner(
->where('success', false)
->where('payload', null)
->where('errors', [
"This action is unauthorized."
'This action is unauthorized.',
])
);
}

View file

@ -16,9 +16,9 @@
class UpdateScheduledUserDishTest extends TestCase
{
use DishesTestTrait;
use HasPlanner;
use RefreshDatabase;
use DishesTestTrait;
use ScheduledDishesTestTrait;
protected function setUp(): void
@ -126,7 +126,7 @@ public function test_planner_cannot_update_dish_of_other_planner(): void
->where('success', false)
->where('payload', null)
->where('errors', [
"This action is unauthorized."
'This action is unauthorized.',
])
);
}

View file

@ -2,7 +2,6 @@
namespace Tests\Feature\User;
use App\Models\Planner;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Testing\Fluent\AssertableJson;

View file

@ -52,7 +52,7 @@ public function test_planner_cannot_update_user_of_other_planner(): void
->assertJson(fn (AssertableJson $json) => $json
->where('success', false)
->where('payload', null)
->where('errors', ["MODEL_NOT_FOUND"])
->where('errors', ['MODEL_NOT_FOUND'])
);
}
}

View file

@ -5,7 +5,6 @@
use App\Models\Dish;
use App\Models\Planner;
use App\Models\User;
use App\Models\UserDish;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Testing\Fluent\AssertableJson;
use Tests\TestCase;
@ -62,5 +61,4 @@ public function test_planner_cannot_see_user_dishes_from_other_planner(): void
->where('errors', null)
);
}
}

Some files were not shown because too many files have changed in this diff Show more