From c66d1ab3d21c46129c00cfb22c2f54c77eed2b24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20Ioni=C8=9B=C4=83?= Date: Fri, 19 Jan 2024 13:59:02 +0100 Subject: [PATCH] refactor: process protocols --- .github/workflows/tests.yml | 82 +++++++ .../Commands/ProcessProtocolsCommand.php} | 95 ++++++--- app/Console/Kernel.php | 6 +- composer.json | 2 +- phpunit.xml.dist | 4 +- tests/Feature/ExampleTest.php | 23 -- tests/Feature/ProtocolsTest.php | 200 ++++++++++++++++++ 7 files changed, 349 insertions(+), 63 deletions(-) create mode 100644 .github/workflows/tests.yml rename app/{Jobs/ProcessProtocolsJob.php => Console/Commands/ProcessProtocolsCommand.php} (57%) delete mode 100644 tests/Feature/ExampleTest.php create mode 100644 tests/Feature/ProtocolsTest.php diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..d6c50f0 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,82 @@ +name: Tests + +on: [push] + +jobs: + test: + name: PHP ${{ matrix.php-version }} + runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + php-version: + - "8.2" + + env: + extensions: mbstring, pdo, pdo_mysql, intl, gd + + services: + mysql: + image: mysql:8 + env: + MYSQL_DATABASE: tests + MYSQL_ALLOW_EMPTY_PASSWORD: yes + ports: + - 3306/tcp + options: >- + --health-cmd "mysqladmin ping" + --health-interval 10s + --health-timeout 5s + --health-retries 3 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup cache environment + id: extcache + uses: shivammathur/cache-extensions@v1 + with: + php-version: ${{ matrix.php-version }} + extensions: ${{ env.extensions }} + key: php-extensions-cache + + - name: Cache extensions + uses: actions/cache@v3 + with: + path: ${{ steps.extcache.outputs.dir }} + key: ${{ steps.extcache.outputs.key }} + restore-keys: ${{ steps.extcache.outputs.key }} + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + extensions: ${{ env.extensions }} + coverage: pcov + tools: composer:v2 + + - name: Get composer cache directory + id: composercache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache composer dependencies + uses: actions/cache@v3 + with: + path: ${{ steps.composercache.outputs.dir }} + key: dependencies-composer-${{ hashFiles('composer.lock') }}-php-${{ matrix.php-version }} + restore-keys: dependencies-composer- + + - name: Install composer dependencies + run: composer install --prefer-dist --no-interaction + + - name: Setup env + run: | + cp .env.ci .env + php artisan key:generate --ansi + + - name: Run tests + run: php artisan test + env: + DB_PORT: ${{ job.services.mysql.ports[3306] }} diff --git a/app/Jobs/ProcessProtocolsJob.php b/app/Console/Commands/ProcessProtocolsCommand.php similarity index 57% rename from app/Jobs/ProcessProtocolsJob.php rename to app/Console/Commands/ProcessProtocolsCommand.php index ca68451..ca26046 100644 --- a/app/Jobs/ProcessProtocolsJob.php +++ b/app/Console/Commands/ProcessProtocolsCommand.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\Jobs; +namespace App\Console\Commands; use App\Enum\UserRole; use App\Models\Organisation; @@ -11,23 +11,52 @@ use App\Notifications\ExpiringProtocol; use App\Notifications\SummaryExpiredProtocols; use App\Notifications\SummaryExpiringProtocols; -use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldQueue; -use Illuminate\Foundation\Bus\Dispatchable; -use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\SerializesModels; +use Illuminate\Console\Command; +use Illuminate\Contracts\Console\Isolatable; use Illuminate\Support\Collection; -class ProcessProtocolsJob implements ShouldQueue +class ProcessProtocolsCommand extends Command implements Isolatable { - use Dispatchable; - use InteractsWithQueue; - use Queueable; - use SerializesModels; + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'app:protocols'; + + /** + * The console command description. + * + * @var string + */ + protected $description = 'Process protocols that are about to expire'; public Collection $admins; - public function __construct() + public Collection $organisations; + + /** + * Execute the console command. + */ + public function handle(): int + { + $this->loadPlatformAdmins(); + $this->loadActiveOrganisations(); + + logger()->info(sprintf( + 'ProcessProtocols: starting check on %d active organisations...', + $this->organisations->count() + )); + + $this->handleExpiringProtocols(); + $this->handleExpiredProtocols(); + + logger()->info('ProcessProtocols: done!'); + + return static::SUCCESS; + } + + private function loadPlatformAdmins(): void { $this->admins = User::query() ->withoutGlobalScopes() @@ -35,12 +64,9 @@ public function __construct() ->get(); } - /** - * Execute the job. - */ - public function handle(): void + private function loadActiveOrganisations(): void { - $organisations = Organisation::query() + $this->organisations = Organisation::query() ->whereActive() ->select(['id', 'name', 'email', 'contact_person']) ->withOnly([ @@ -51,32 +77,29 @@ public function handle(): void ]) ->withLastProtocolExpiresAt() ->get(); - - logger()->info(sprintf('ProcessProtocolsJob: starting check on %d organisations...', $organisations->count())); - - $this->handleExpiringProtocols($organisations); - $this->handleExpiredProtocols($organisations); - - logger()->info('ProcessProtocolsJob: done!'); } - private function handleExpiringProtocols(Collection $organisations): void + private function handleExpiringProtocols(): void { $checkDate = today()->addDays(30); logger()->info(sprintf( - 'ProcessProtocolsJob: checking for protocols expiring in 30 days (%s)...', + 'ProcessProtocols: checking for protocols expiring in 30 days (%s)...', $checkDate->format('Y-m-d') )); - $organisations - ->filter(fn (Organisation $organisation) => $checkDate->isSameDay($organisation->last_protocol_expires_at)) + $this->organisations + ->filter( + fn (Organisation $organisation) => $organisation + ->last_protocol_expires_at + ?->isSameDay($checkDate) ?? true + ) ->each(function (Organisation $organisation) { $this->sendNotification(ExpiringProtocol::class, $organisation); }) ->tap(function (Collection $organisations) { logger()->info(sprintf( - 'ProcessProtocolsJob: found %d organisations with expiring protocols: %s', + 'ProcessProtocols: found %d organisations with expiring protocols: %s', $organisations->count(), $organisations->pluck('id')->join(', ') )); @@ -85,17 +108,21 @@ private function handleExpiringProtocols(Collection $organisations): void }); } - private function handleExpiredProtocols(Collection $organisations): void + private function handleExpiredProtocols(): void { $checkDate = today(); logger()->info(sprintf( - 'ProcessProtocolsJob: checking for protocols expiring today (%s)...', + 'ProcessProtocols: checking for protocols expiring today (%s)...', $checkDate->format('Y-m-d') )); - $organisations - ->filter(fn (Organisation $organisation) => $checkDate->isSameDay($organisation->last_protocol_expires_at)) + $this->organisations + ->filter( + fn (Organisation $organisation) => $organisation + ->last_protocol_expires_at + ?->lte($checkDate) ?? true + ) ->each(function (Organisation $organisation) { $organisation->setInactive(); @@ -103,7 +130,7 @@ private function handleExpiredProtocols(Collection $organisations): void }) ->tap(function (Collection $organisations) { logger()->info(sprintf( - 'ProcessProtocolsJob: found %d organisations with expired protocols: %s', + 'ProcessProtocols: found %d organisations with expired protocols: %s', $organisations->count(), $organisations->pluck('id')->join(', ') )); diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 0bf5fd3..6eb9f6e 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -4,7 +4,7 @@ namespace App\Console; -use App\Jobs\ProcessProtocolsJob; +use App\Console\Commands\ProcessProtocolsCommand; use Illuminate\Console\Scheduling\Schedule; use Illuminate\Foundation\Console\Kernel as ConsoleKernel; @@ -18,12 +18,12 @@ class Kernel extends ConsoleKernel */ protected function schedule(Schedule $schedule) { - $schedule->job(new ProcessProtocolsJob) + $schedule->command(ProcessProtocolsCommand::class) ->daily() ->at('06:00') ->timezone('Europe/Bucharest') ->withoutOverlapping() - ->sentryMonitor('process-protocols-job'); + ->sentryMonitor('process-protocols'); } /** diff --git a/composer.json b/composer.json index c1d4210..57af909 100644 --- a/composer.json +++ b/composer.json @@ -62,7 +62,7 @@ "post-create-project-cmd": [ "@php artisan key:generate --ansi" ], - "test": "XDEBUG_MODE=coverage ./vendor/bin/phpunit --colors=always" + "test": "@php artisan test" }, "extra": { "laravel": { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index a9ff412..14a2f98 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -30,8 +30,8 @@ - - + + diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php deleted file mode 100644 index 7cbf57a..0000000 --- a/tests/Feature/ExampleTest.php +++ /dev/null @@ -1,23 +0,0 @@ -get('/'); - - $response->assertStatus(302); - } -} diff --git a/tests/Feature/ProtocolsTest.php b/tests/Feature/ProtocolsTest.php new file mode 100644 index 0000000..650f7cd --- /dev/null +++ b/tests/Feature/ProtocolsTest.php @@ -0,0 +1,200 @@ +platformAdmin() + ->create(); + } + + /** @test */ + public function it_does_not_send_notifications_for_protocols_expiring_in_less_than_30_days(): void + { + $document = Document::factory() + ->protocol() + ->state([ + 'expires_at' => today()->addDays(29), + ]) + ->create(); + + $this->artisan(ProcessProtocolsCommand::class) + ->assertSuccessful(); + + Notification::assertNotSentTo( + $document->organisation, + ExpiringProtocol::class + ); + + Notification::assertNotSentTo( + $document->organisation + ->users + ->where('role', UserRole::ORG_ADMIN), + ExpiringProtocol::class + ); + + Notification::assertNotSentTo( + $this->getPlatformAdmins(), + SummaryExpiringProtocols::class + ); + } + + /** @test */ + public function it_sends_notifications_for_protocols_expiring_in_exactly_30_days(): void + { + $document = Document::factory() + ->protocol() + ->state([ + 'expires_at' => today()->addDays(30), + ]) + ->create(); + + $this->artisan(ProcessProtocolsCommand::class) + ->assertSuccessful(); + + Notification::assertSentTo( + $document->organisation, + ExpiringProtocol::class + ); + + Notification::assertSentTo( + $document->organisation + ->users + ->where('role', UserRole::ORG_ADMIN), + ExpiringProtocol::class + ); + + Notification::assertSentTo( + $this->getPlatformAdmins(), + SummaryExpiringProtocols::class + ); + } + + /** @test */ + public function it_does_not_send_notifications_for_protocols_expiring_in_more_than_30_days(): void + { + $document = Document::factory() + ->protocol() + ->state([ + 'expires_at' => today()->addDays(31), + ]) + ->create(); + + $this->artisan(ProcessProtocolsCommand::class) + ->assertSuccessful(); + + Notification::assertNotSentTo( + $document->organisation, + ExpiringProtocol::class + ); + + Notification::assertNotSentTo( + $document->organisation + ->users + ->where('role', UserRole::ORG_ADMIN), + ExpiringProtocol::class + ); + + Notification::assertNotSentTo( + $this->getPlatformAdmins(), + SummaryExpiringProtocols::class + ); + } + + /** @test */ + public function it_sends_notifications_for_protocols_that_expire_today(): void + { + $document = Document::factory() + ->protocol() + ->state([ + 'expires_at' => today(), + ]) + ->create(); + + $this->artisan(ProcessProtocolsCommand::class) + ->assertSuccessful(); + + Notification::assertSentTo( + $document->organisation, + ExpiredProtocol::class + ); + + Notification::assertSentTo( + $document->organisation + ->users + ->where('role', UserRole::ORG_ADMIN), + ExpiredProtocol::class + ); + + Notification::assertSentTo( + $this->getPlatformAdmins(), + SummaryExpiredProtocols::class + ); + } + + /** @test */ + public function it_sends_notifications_for_protocols_that_have_expired_in_the_past(): void + { + $document = Document::factory() + ->protocol() + ->state([ + 'expires_at' => today()->subDays(3), + ]) + ->create(); + + $this->artisan(ProcessProtocolsCommand::class) + ->assertSuccessful(); + + Notification::assertSentTo( + $document->organisation, + ExpiredProtocol::class + ); + + Notification::assertSentTo( + $document->organisation + ->users + ->where('role', UserRole::ORG_ADMIN), + ExpiredProtocol::class + ); + + Notification::assertSentTo( + $this->getPlatformAdmins(), + SummaryExpiredProtocols::class + ); + } + + private function getPlatformAdmins(): Collection + { + return User::query() + ->withoutGlobalScopes() + ->role(UserRole::PLATFORM_ADMIN) + ->get(); + } +}