diff --git a/.env.example b/.env.example index 660691fa..5ca7fbe7 100644 --- a/.env.example +++ b/.env.example @@ -62,7 +62,13 @@ DISCORD_AUTH_TOKEN=abc DISCORD_WEBHOOK_URL=def DISCORD_USERNAME=FlowBot DISCORD_AVATAR_URL="${APP_URL}/images/logo.png" +DISCORD_ECFMP_CHANNEL_ID="971531731096203364" # Sentry monitoring SENTRY_LARAVEL_DSN= SENTRY_TRACES_SAMPLE_RATE=1.0 + +# Discord bot +DISCORD_BOT_SERVICE_URL="discord-bot:80" +DISCORD_BOT_JWT="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJlY2ZtcC1mbG93IiwiYXVkIjoiZWNmbXAtZGlzY29yZC1kZXYiLCJpYXQiOjIxMzg0NTQ1NTUsImlzcyI6ImVjZm1wLWF1dGgifQ.PqbCEe_zW1WBLio6aD0P5OksRI1H-hoRRA8168OJg-h11SjyZeDCAAf0CjgZAUy6vUpSdfgN9KH1SgCSM4J38-2txpZLr_VlJTu9_W1mGEVr1_pGjMgbkwx8PMTP1f3J2R0BwGz-324vpPVB9zu6NG9ujS48AD28mDoqMpqc7UOK0_e9WJ7cQBb8BxU10w4TQXbwhjUMyZBIpdiaDK5OsQeXJruo0OjSlltiFJkXPmESTz_DwwTSvIqzmhjzQfNW62RVcnBrnbWaaCg1mC6FSIMffjrEgian_AyAg1iftjy_fa3f-sU-z65xMh8vVwAvJEhYJCA0CZO4lKn_OV0RXqYjCLI4t-Rp6MYULyLbp6QZ88MOvSXd-8GnYnxDE5o5-rLFnQ04LCGx2-yBDPZ80brdxZR26Im1DrNiPyUFadINGf8wwZ4-iWqmY6_QSfJYU1C3Y5s7TxMBFfa934NHICg53gVSEdfCHQIaciOSu91P1FnGIvdtOQV_n7urF5HRYyxOUomSa4MY4m5C1-TJqpBkCsAXbMdC_mIllFWnUCB5uBj5T18mxW1mrGQiF3Vy6_MrSeFoY0LEnd58QpvnG7-OuZWnrIbDDTAnTcI1IMVUA4zcoUvkUcCcRvBQD2bXsPgL9Lh8RmYvjrR2hWhmlJU-k_CPiQOLfW9qOFGp5rA" +DISCORD_CLIENT_REQUEST_APP_ID="ecfmpdev" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 97c3abbc..d8f0eb70 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,7 +21,21 @@ jobs: composer: ["v2"] steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v4 + with: + submodules: true + + - name: Checkout Discord Service + uses: actions/checkout@v4 + with: + repository: ecfmp/discord + path: ecfmp-discord + + - name: Move Discord Service + run: mv ecfmp-discord ../ecfmp-discord + + - name: Build Protobuf + run: (cd protobuf && make pull_builder && make discord_proto) - name: Configure PHP uses: shivammathur/setup-php@v2 @@ -29,6 +43,7 @@ jobs: php-version: ${{ matrix.php }} coverage: pcov tools: composer:${{ matrix.composer }} + extensions: grpc # Setting up composer dependencies - name: Get Composer Cache Directory diff --git a/.gitignore b/.gitignore index 62af7d63..fa746c90 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ Homestead.yaml Homestead.json /.vagrant .phpunit.result.cache +.phpunit.cache .idea # Laravel IDE Helper @@ -39,3 +40,5 @@ public/mix-manifest.json storage/imports .vscode + +.DS_STORE diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..f1b83d04 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "protobuf"] + path = protobuf + url = git@github.com:ECFMP/ecfmp-protobuf.git diff --git a/app/Console/Commands/SendEcfmpDiscordMessages.php b/app/Console/Commands/SendEcfmpDiscordMessages.php new file mode 100644 index 00000000..6a119988 --- /dev/null +++ b/app/Console/Commands/SendEcfmpDiscordMessages.php @@ -0,0 +1,38 @@ +generateAndSend(); + Log::info('Discord notification sending complete'); + + return 0; + } +} diff --git a/app/Discord/Client/ClientFactory.php b/app/Discord/Client/ClientFactory.php new file mode 100644 index 00000000..ea2b4a60 --- /dev/null +++ b/app/Discord/Client/ClientFactory.php @@ -0,0 +1,32 @@ +client === null) { + $this->client = new DiscordClient( + config('discord.service_host'), + [ + 'credentials' => ChannelCredentials::createInsecure(), + 'grpc.primary_user_agent' => config('app.name'), + ], + ); + } + + return $this->client; + } +} diff --git a/app/Discord/Client/ClientFactoryInterface.php b/app/Discord/Client/ClientFactoryInterface.php new file mode 100644 index 00000000..48efd27f --- /dev/null +++ b/app/Discord/Client/ClientFactoryInterface.php @@ -0,0 +1,10 @@ +discordClientFactory = $discordClientFactory; + } + + public function sendMessage(string $clientRequestId, EcfmpMessageInterface $message): string + { + $client = $this->discordClientFactory->create(); + + // Wait for 1 second for the channel to be ready + $channelReady = $client->waitForReady(1000000); + if (!$channelReady) { + Log::error('Discord grpc channel not ready'); + throw new DiscordServiceException('Discord grpc channel not ready'); + } + + /** + * @var $response \Ecfmp_discord\CreateResponse + */ + [$response, $status] = $client->Create( + new CreateRequest( + [ + 'channel' => $message->channel(), + 'content' => $message->content(), + 'embeds' => $message->embeds()->toProtobuf(), + ] + ), + [ + 'authorization' => [config('discord.service_token')], + 'x-client-request-id' => [$clientRequestId], + ], + )->wait(); + + if ($status->code !== STATUS_OK) { + Log::error('Discord grpc call failed', [ + 'code' => $status->code, + 'details' => $status->details, + ]); + + throw new DiscordServiceException('Discord grpc call failed'); + } + + return $response->getId(); + } +} diff --git a/app/Discord/DiscordInterface.php b/app/Discord/DiscordWebhookInterface.php similarity index 67% rename from app/Discord/DiscordInterface.php rename to app/Discord/DiscordWebhookInterface.php index cdae13dd..1a0ca854 100644 --- a/app/Discord/DiscordInterface.php +++ b/app/Discord/DiscordWebhookInterface.php @@ -7,9 +7,9 @@ /** * To hide the details of how we go about doing Discord things... * - * This class is the interface for interacting with Discord. + * This class is the interface for interacting with Discord via webhooks. */ -interface DiscordInterface +interface DiscordWebhookInterface { public function sendMessage(MessageInterface $message): bool; } diff --git a/app/Discord/DiscordMessageSender.php b/app/Discord/DiscordWebhookSender.php similarity index 95% rename from app/Discord/DiscordMessageSender.php rename to app/Discord/DiscordWebhookSender.php index fc25755e..217151b2 100644 --- a/app/Discord/DiscordMessageSender.php +++ b/app/Discord/DiscordWebhookSender.php @@ -6,7 +6,7 @@ use Illuminate\Support\Facades\Http; use Log; -class DiscordMessageSender implements DiscordInterface +class DiscordWebhookSender implements DiscordWebhookInterface { public function sendMessage(MessageInterface $message): bool { diff --git a/app/Discord/Exception/DiscordServiceException.php b/app/Discord/Exception/DiscordServiceException.php new file mode 100644 index 00000000..766bad31 --- /dev/null +++ b/app/Discord/Exception/DiscordServiceException.php @@ -0,0 +1,9 @@ +flowMeasure->discordNotifications()->attach( + $this->flowMeasure->divisionDiscordNotifications()->attach( [ $notification->id => [ 'discord_notification_type_id' => DiscordNotificationType::idFromEnum($this->type), diff --git a/app/Discord/FlowMeasure/Content/FlowMeasureRecipientsFactory.php b/app/Discord/FlowMeasure/Content/FlowMeasureRecipientsFactory.php index a7daecb1..0c277092 100644 --- a/app/Discord/FlowMeasure/Content/FlowMeasureRecipientsFactory.php +++ b/app/Discord/FlowMeasure/Content/FlowMeasureRecipientsFactory.php @@ -3,9 +3,11 @@ namespace App\Discord\FlowMeasure\Content; use App\Discord\FlowMeasure\Provider\PendingMessageInterface; +use App\Discord\FlowMeasure\Provider\PendingWebhookMessageInterface; use App\Discord\Message\Tag\Tag; use App\Enums\DiscordNotificationType; use App\Models\DiscordNotification; +use App\Models\DivisionDiscordNotification; use App\Models\DiscordTag; use App\Models\DivisionDiscordWebhook; use App\Models\FlightInformationRegion; @@ -13,22 +15,39 @@ class FlowMeasureRecipientsFactory { - public function makeRecipients(PendingMessageInterface $pendingMessage): FlowMeasureRecipientsInterface + public function makeRecipients(PendingWebhookMessageInterface $pendingMessage): FlowMeasureRecipientsInterface + { + if ($this->hasRecentlyBeenNotifiedToWebhook($pendingMessage)) { + return new NoRecipients(); + } + + return $this->divisionRecipients($pendingMessage); + } + + public function makeEcfmpRecipients(PendingMessageInterface $pendingMessage): FlowMeasureRecipientsInterface { if ($this->hasRecentlyBeenNotified($pendingMessage)) { return new NoRecipients(); } - return $pendingMessage->webhook()->id() === null - ? $this->ecfmpRecipients($pendingMessage) - : $this->divisionRecipients($pendingMessage); + return $this->ecfmpRecipients($pendingMessage); + } + + private function hasRecentlyBeenNotifiedToWebhook(PendingWebhookMessageInterface $pendingMessage): bool + { + $measure = $pendingMessage->flowMeasure(); + return $pendingMessage->type( + ) === DiscordNotificationType::FLOW_MEASURE_ACTIVATED && $measure->notifiedDivisionNotifications->firstWhere( + fn (DivisionDiscordNotification $notification) => $notification->created_at > Carbon::now()->subHour() && + $notification->pivot->notified_as === $measure->identifier + ) !== null; } private function hasRecentlyBeenNotified(PendingMessageInterface $pendingMessage): bool { $measure = $pendingMessage->flowMeasure(); return $pendingMessage->type( - ) === DiscordNotificationType::FLOW_MEASURE_ACTIVATED && $measure->notifiedDiscordNotifications->firstWhere( + ) === DiscordNotificationType::FLOW_MEASURE_ACTIVATED && $measure->notifiedEcfmpNotifications->firstWhere( fn (DiscordNotification $notification) => $notification->created_at > Carbon::now()->subHour() && $notification->pivot->notified_as === $measure->identifier ) !== null; @@ -44,7 +63,7 @@ private function ecfmpRecipients(PendingMessageInterface $pendingMessage): FlowM ); } - private function divisionRecipients(PendingMessageInterface $pendingMessage): FlowMeasureRecipientsInterface + private function divisionRecipients(PendingWebhookMessageInterface $pendingMessage): FlowMeasureRecipientsInterface { $recipients = DivisionDiscordWebhook::find($pendingMessage->webhook()->id()) ->flightInformationRegions diff --git a/app/Discord/FlowMeasure/Embed/ActivatedEmbeds.php b/app/Discord/FlowMeasure/Embed/ActivatedEmbeds.php index 58f5cc65..f46b1b8c 100644 --- a/app/Discord/FlowMeasure/Embed/ActivatedEmbeds.php +++ b/app/Discord/FlowMeasure/Embed/ActivatedEmbeds.php @@ -39,7 +39,7 @@ public function embeds(): EmbedCollection : IdentifierAndActiveStatus::create($this->pendingMessage->flowMeasure()) ) ->withDescription(new EventName($this->pendingMessage->flowMeasure())) - ->withField(Field::make(new IssuingUser($this->pendingMessage->flowMeasure())), is_null($this->pendingMessage->webhook()->id())) + ->withField(Field::make(new IssuingUser($this->pendingMessage->flowMeasure())), $this->pendingMessage->isEcfmp()) ->withField(Field::makeInline(new Restriction($this->pendingMessage->flowMeasure()))) ->withField(Field::makeInline(new StartTime($this->pendingMessage->flowMeasure()))) ->withField(Field::makeInline(new EndTime($this->pendingMessage->flowMeasure()))) diff --git a/app/Discord/FlowMeasure/Embed/NotifiedEmbeds.php b/app/Discord/FlowMeasure/Embed/NotifiedEmbeds.php index 397dcd13..64b5763b 100644 --- a/app/Discord/FlowMeasure/Embed/NotifiedEmbeds.php +++ b/app/Discord/FlowMeasure/Embed/NotifiedEmbeds.php @@ -39,7 +39,7 @@ public function embeds(): EmbedCollection : IdentifierAndNotifiedStatus::create($this->pendingMessage->flowMeasure()) ) ->withDescription(new EventName($this->pendingMessage->flowMeasure())) - ->withField(Field::make(new IssuingUser($this->pendingMessage->flowMeasure())), is_null($this->pendingMessage->webhook()->id())) + ->withField(Field::make(new IssuingUser($this->pendingMessage->flowMeasure())), $this->pendingMessage->isEcfmp()) ->withField(Field::makeInline(new Restriction($this->pendingMessage->flowMeasure()))) ->withField(Field::makeInline(new StartTime($this->pendingMessage->flowMeasure()))) ->withField(Field::makeInline(new EndTime($this->pendingMessage->flowMeasure()))) diff --git a/app/Discord/FlowMeasure/Generator/EcfmpFlowMeasureMessageGenerator.php b/app/Discord/FlowMeasure/Generator/EcfmpFlowMeasureMessageGenerator.php new file mode 100644 index 00000000..bf86fff6 --- /dev/null +++ b/app/Discord/FlowMeasure/Generator/EcfmpFlowMeasureMessageGenerator.php @@ -0,0 +1,37 @@ +sender = $sender; + $this->repositories = $repositories; + } + + public function generateAndSend(): void + { + foreach ($this->repositories as $repository) { + foreach ($repository->flowMeasuresToBeSentToEcfmp() as $measure) { + $pendingMessage = new PendingEcfmpMessage( + $measure->measure, + $repository->notificationType(), + new EcfmpNotificationReissuer($measure, $repository->notificationType()) + ); + + $this->sender->send($pendingMessage); + } + } + } +} diff --git a/app/Discord/FlowMeasure/Helper/EcfmpNotificationReissuer.php b/app/Discord/FlowMeasure/Helper/EcfmpNotificationReissuer.php new file mode 100644 index 00000000..7482fb91 --- /dev/null +++ b/app/Discord/FlowMeasure/Helper/EcfmpNotificationReissuer.php @@ -0,0 +1,24 @@ +flowMeasureForNotification = $flowMeasureForNotification; + $this->type = $type; + } + + public function isReissuedNotification(): bool + { + return ($this->type === DiscordNotificationType::FLOW_MEASURE_ACTIVATED || $this->type === DiscordNotificationType::FLOW_MEASURE_NOTIFIED) + && $this->flowMeasureForNotification->isReissuedNotification; + } +} diff --git a/app/Discord/FlowMeasure/Helper/NotificationReissuer.php b/app/Discord/FlowMeasure/Helper/NotificationReissuer.php index 025386c0..bf51bbee 100644 --- a/app/Discord/FlowMeasure/Helper/NotificationReissuer.php +++ b/app/Discord/FlowMeasure/Helper/NotificationReissuer.php @@ -4,7 +4,7 @@ use App\Discord\Webhook\WebhookInterface; use App\Enums\DiscordNotificationType; -use App\Models\DiscordNotification; +use App\Models\DivisionDiscordNotification; use App\Models\FlowMeasure; class NotificationReissuer implements NotificationReissuerInterface @@ -20,6 +20,7 @@ public function __construct(FlowMeasure $measure, DiscordNotificationType $type, $this->webhook = $webhook; } + // TODO: Update this public function isReissuedNotification(): bool { if ( @@ -29,17 +30,17 @@ public function isReissuedNotification(): bool return false; } - $notificationsOfType = $this->measure->activatedAndNotifiedNotifications() + $notificationsOfType = $this->measure->activatedAndNotifiedDivisionNotifications() ->where('division_discord_webhook_id', $this->webhook->id()) ->get(); return $notificationsOfType->filter( fn ( - DiscordNotification $notification + DivisionDiscordNotification $notification ) => $notification->pivot->notified_as !== $this->measure->identifier )->isNotEmpty() && $notificationsOfType->filter( fn ( - DiscordNotification $notification + DivisionDiscordNotification $notification ) => $notification->pivot->notified_as === $this->measure->identifier )->isEmpty(); } diff --git a/app/Discord/FlowMeasure/Logger/FlowMeasureLogger.php b/app/Discord/FlowMeasure/Logger/FlowMeasureLogger.php index 082baa5d..e71246f7 100644 --- a/app/Discord/FlowMeasure/Logger/FlowMeasureLogger.php +++ b/app/Discord/FlowMeasure/Logger/FlowMeasureLogger.php @@ -4,7 +4,7 @@ use App\Discord\Message\Logger\LoggerInterface; use App\Enums\DiscordNotificationType; -use App\Models\DiscordNotification; +use App\Models\DivisionDiscordNotification; use App\Models\FlowMeasure; class FlowMeasureLogger implements LoggerInterface @@ -18,7 +18,7 @@ public function __construct(FlowMeasure $flowMeasure, DiscordNotificationType $t $this->type = $type; } - public function log(DiscordNotification $notification): void + public function log(DivisionDiscordNotification $notification): void { activity() ->inLog('Discord') diff --git a/app/Discord/FlowMeasure/Message/EcfmpFlowMeasureMessage.php b/app/Discord/FlowMeasure/Message/EcfmpFlowMeasureMessage.php new file mode 100644 index 00000000..1bd9881b --- /dev/null +++ b/app/Discord/FlowMeasure/Message/EcfmpFlowMeasureMessage.php @@ -0,0 +1,37 @@ +channel = $channel; + $this->recipients = $recipients; + $this->embeds = $embeds; + } + + public function channel(): string + { + return $this->channel; + } + + public function content(): string + { + return $this->recipients->toString(); + } + + public function embeds(): EmbedCollection + { + return $this->embeds->embeds(); + } +} diff --git a/app/Discord/FlowMeasure/Message/FlowMeasureMessageFactory.php b/app/Discord/FlowMeasure/Message/FlowMeasureMessageFactory.php index 4ea234ec..1628d683 100644 --- a/app/Discord/FlowMeasure/Message/FlowMeasureMessageFactory.php +++ b/app/Discord/FlowMeasure/Message/FlowMeasureMessageFactory.php @@ -7,6 +7,7 @@ use App\Discord\FlowMeasure\Embed\FlowMeasureEmbedFactory; use App\Discord\FlowMeasure\Logger\FlowMeasureLogger; use App\Discord\FlowMeasure\Provider\PendingMessageInterface; +use App\Discord\FlowMeasure\Provider\PendingWebhookMessageInterface; class FlowMeasureMessageFactory { @@ -19,7 +20,7 @@ public function __construct(FlowMeasureRecipientsFactory $recipientsFactory, Flo $this->embedFactory = $embedFactory; } - public function make(PendingMessageInterface $pendingMessage): FlowMeasureMessage + public function make(PendingWebhookMessageInterface $pendingMessage): FlowMeasureMessage { return new FlowMeasureMessage( $pendingMessage->webhook(), @@ -29,4 +30,14 @@ public function make(PendingMessageInterface $pendingMessage): FlowMeasureMessag new FlowMeasureLogger($pendingMessage->flowMeasure(), $pendingMessage->type()) ); } + + public function makeEcfmp(PendingMessageInterface $message): EcfmpFlowMeasureMessage + { + return new EcfmpFlowMeasureMessage( + config('discord.ecfmp_channel_id'), + $this->recipientsFactory->makeEcfmpRecipients($message), + $this->embedFactory->make($message) + ); + } + } diff --git a/app/Discord/FlowMeasure/Provider/MessageProvider.php b/app/Discord/FlowMeasure/Provider/DivisionWebhookMessageProvider.php similarity index 91% rename from app/Discord/FlowMeasure/Provider/MessageProvider.php rename to app/Discord/FlowMeasure/Provider/DivisionWebhookMessageProvider.php index 6e249b66..0eacda14 100644 --- a/app/Discord/FlowMeasure/Provider/MessageProvider.php +++ b/app/Discord/FlowMeasure/Provider/DivisionWebhookMessageProvider.php @@ -7,7 +7,7 @@ use App\Repository\FlowMeasureNotification\RepositoryInterface; use Illuminate\Support\Collection; -class MessageProvider implements MessageProviderInterface +class DivisionWebhookMessageProvider implements MessageProviderInterface { private readonly RepositoryInterface $repository; private readonly MapperInterface $webhookMapper; @@ -26,7 +26,7 @@ function (Collection $messages) { foreach ($this->repository->flowMeasuresForNotification() as $flowMeasure) { foreach ($this->webhookMapper->mapToWebhooks($flowMeasure) as $webhook) { $messages->push( - new PendingDiscordMessage( + new PendingDiscordWebhookMessage( $flowMeasure, $this->repository->notificationType(), $webhook, diff --git a/app/Discord/FlowMeasure/Provider/PendingDiscordMessage.php b/app/Discord/FlowMeasure/Provider/PendingDiscordWebhookMessage.php similarity index 88% rename from app/Discord/FlowMeasure/Provider/PendingDiscordMessage.php rename to app/Discord/FlowMeasure/Provider/PendingDiscordWebhookMessage.php index 3d7fef81..092ee3fa 100644 --- a/app/Discord/FlowMeasure/Provider/PendingDiscordMessage.php +++ b/app/Discord/FlowMeasure/Provider/PendingDiscordWebhookMessage.php @@ -7,7 +7,7 @@ use App\Enums\DiscordNotificationType; use App\Models\FlowMeasure; -class PendingDiscordMessage implements PendingMessageInterface +class PendingDiscordWebhookMessage implements PendingWebhookMessageInterface { private readonly FlowMeasure $measure; private readonly DiscordNotificationType $type; @@ -45,4 +45,9 @@ public function reissue(): NotificationReissuerInterface { return $this->resissue; } + + public function isEcfmp(): bool + { + return false; + } } diff --git a/app/Discord/FlowMeasure/Provider/PendingEcfmpMessage.php b/app/Discord/FlowMeasure/Provider/PendingEcfmpMessage.php new file mode 100644 index 00000000..1a5a7e63 --- /dev/null +++ b/app/Discord/FlowMeasure/Provider/PendingEcfmpMessage.php @@ -0,0 +1,43 @@ +flowMeasure = $flowMeasure; + $this->type = $type; + $this->reissue = $reissue; + } + + public function flowMeasure(): FlowMeasure + { + return $this->flowMeasure; + } + + public function type(): DiscordNotificationType + { + return $this->type; + } + + public function reissue(): NotificationReissuerInterface + { + return $this->reissue; + } + + public function isEcfmp(): bool + { + return true; + } +} diff --git a/app/Discord/FlowMeasure/Provider/PendingMessageInterface.php b/app/Discord/FlowMeasure/Provider/PendingMessageInterface.php index 42985f1a..ca44c69a 100644 --- a/app/Discord/FlowMeasure/Provider/PendingMessageInterface.php +++ b/app/Discord/FlowMeasure/Provider/PendingMessageInterface.php @@ -3,17 +3,19 @@ namespace App\Discord\FlowMeasure\Provider; use App\Discord\FlowMeasure\Helper\NotificationReissuerInterface; -use App\Discord\Webhook\WebhookInterface; use App\Enums\DiscordNotificationType; use App\Models\FlowMeasure; +/** + * Represents a pending message that needs to be sent out to Discord. + */ interface PendingMessageInterface { public function flowMeasure(): FlowMeasure; public function type(): DiscordNotificationType; - public function webhook(): WebhookInterface; - public function reissue(): NotificationReissuerInterface; + + public function isEcfmp(): bool; } diff --git a/app/Discord/FlowMeasure/Provider/PendingWebhookMessageInterface.php b/app/Discord/FlowMeasure/Provider/PendingWebhookMessageInterface.php new file mode 100644 index 00000000..4bc1b094 --- /dev/null +++ b/app/Discord/FlowMeasure/Provider/PendingWebhookMessageInterface.php @@ -0,0 +1,10 @@ +discordService = $discordService; + $this->messageFactory = $messageFactory; + + if (!config('discord.client_request_app_id')) { + throw new RuntimeException('Discord client request app id is not set'); + } + } + + public function send(PendingMessageInterface $message): void + { + // Make the message + $discordMessage = $this->messageFactory->makeEcfmp($message); + + // Send the message + try { + $discordNotificationId = $this->discordService->sendMessage($this->makeClientRequestId($message), $discordMessage); + } catch (DiscordServiceException $e) { + Log::error('Failed to send Discord message for flow measure'); + return; + } + + // Commit the notification to the database + DB::transaction(function () use ($message, $discordNotificationId) { + $notification = DiscordNotification::create([ + 'remote_id' => $discordNotificationId, + ]); + $message->flowMeasure()->discordNotifications()->attach($notification, [ + 'discord_notification_type_id' => DiscordNotificationType::idFromEnum($message->type()), + 'notified_as' => $message->flowMeasure()->identifier, + ]); + }); + } + + private function makeClientRequestId(PendingMessageInterface $message): string + { + return sprintf( + '%s-%s-%d-%s', + config('discord.client_request_app_id'), + $message->type()->value, + $message->flowMeasure()->id, + $message->flowMeasure()->identifier + ); + } +} diff --git a/app/Discord/FlowMeasure/Webhook/Filter/ActivatedWebhookFilter.php b/app/Discord/FlowMeasure/Webhook/Filter/ActivatedWebhookFilter.php index dbb21bdf..71a1d677 100644 --- a/app/Discord/FlowMeasure/Webhook/Filter/ActivatedWebhookFilter.php +++ b/app/Discord/FlowMeasure/Webhook/Filter/ActivatedWebhookFilter.php @@ -12,8 +12,8 @@ class ActivatedWebhookFilter implements FilterInterface public function shouldUseWebhook(FlowMeasure $flowMeasure, WebhookInterface $webhook): bool { return $this->existingNotificationDoesntExist( - $flowMeasure->activatedDiscordNotifications() - ->where('discord_notification_flow_measure.notified_as', $flowMeasure->identifier), + $flowMeasure->activatedDivisionNotifications() + ->where('division_discord_notification_flow_measure.notified_as', $flowMeasure->identifier), $webhook ); } diff --git a/app/Discord/FlowMeasure/Webhook/Filter/ExpiredWebhookFilter.php b/app/Discord/FlowMeasure/Webhook/Filter/ExpiredWebhookFilter.php index 75311715..46659a4b 100644 --- a/app/Discord/FlowMeasure/Webhook/Filter/ExpiredWebhookFilter.php +++ b/app/Discord/FlowMeasure/Webhook/Filter/ExpiredWebhookFilter.php @@ -4,7 +4,7 @@ use App\Discord\Webhook\WebhookInterface; use App\Helpers\FlowMeasureIdentifierGenerator; -use App\Models\DiscordNotification; +use App\Models\DivisionDiscordNotification; use App\Models\FlowMeasure; use App\Repository\FlowMeasureRepository; use Carbon\Carbon; @@ -37,16 +37,16 @@ public function shouldUseWebhook(FlowMeasure $flowMeasure, WebhookInterface $web } return $this->existingNotificationDoesntExist( - $flowMeasure->withdrawnAndExpiredDiscordNotifications(), + $flowMeasure->withdrawnAndExpiredDivisionNotifications(), $webhook ); } private function notManyWebhooksRecentlySent(): bool { - return DiscordNotification::where('created_at', '>=', Carbon::now()->subHours(2)) - ->whereNull('division_discord_webhook_id') - ->count() <= 5; + return DivisionDiscordNotification::where('created_at', '>=', Carbon::now()->subHours(2)) + ->whereNull('division_discord_webhook_id') + ->count() <= 5; } private function notRevisedMoreThanOnce(FlowMeasure $flowMeasure): bool @@ -57,7 +57,7 @@ private function notRevisedMoreThanOnce(FlowMeasure $flowMeasure): bool private function lessThanThreeActiveMeasures(FlowMeasure $measure): bool { return $this->flowMeasureRepository->getFlowMeasuresActiveDuringPeriod($measure->start_time, $measure->end_time) - ->reject(fn (FlowMeasure $activeMeasure) => $activeMeasure->id === $measure->id) - ->count() < 3; + ->reject(fn (FlowMeasure $activeMeasure) => $activeMeasure->id === $measure->id) + ->count() < 3; } } diff --git a/app/Discord/FlowMeasure/Webhook/Filter/NotifiedWebhookFilter.php b/app/Discord/FlowMeasure/Webhook/Filter/NotifiedWebhookFilter.php index 69e22a40..01bf4144 100644 --- a/app/Discord/FlowMeasure/Webhook/Filter/NotifiedWebhookFilter.php +++ b/app/Discord/FlowMeasure/Webhook/Filter/NotifiedWebhookFilter.php @@ -20,8 +20,8 @@ public function shouldUseWebhook(FlowMeasure $flowMeasure, WebhookInterface $web private function notYetNotified(FlowMeasure $flowMeasure, WebhookInterface $webhook): bool { return $this->existingNotificationDoesntExist( - $flowMeasure->notifiedDiscordNotifications() - ->where('discord_notification_flow_measure.notified_as', $flowMeasure->identifier), + $flowMeasure->notifiedDivisionNotifications() + ->where('division_discord_notification_flow_measure.notified_as', $flowMeasure->identifier), $webhook ); } @@ -29,7 +29,7 @@ private function notYetNotified(FlowMeasure $flowMeasure, WebhookInterface $webh private function notYetActivated(FlowMeasure $flowMeasure, WebhookInterface $webhook): bool { return $this->existingNotificationDoesntExist( - $flowMeasure->activatedDiscordNotifications(), + $flowMeasure->activatedDivisionNotifications(), $webhook ); } diff --git a/app/Discord/FlowMeasure/Webhook/Filter/WithdrawnWebhookFilter.php b/app/Discord/FlowMeasure/Webhook/Filter/WithdrawnWebhookFilter.php index 3ee0c327..f9fafb2e 100644 --- a/app/Discord/FlowMeasure/Webhook/Filter/WithdrawnWebhookFilter.php +++ b/app/Discord/FlowMeasure/Webhook/Filter/WithdrawnWebhookFilter.php @@ -18,7 +18,7 @@ public function shouldUseWebhook(FlowMeasure $flowMeasure, WebhookInterface $web private function hasBeenActivatedOrNotified(FlowMeasure $flowMeasure, WebhookInterface $webhook): bool { return $this->existingNotificationExists( - $flowMeasure->activatedAndNotifiedNotifications(), + $flowMeasure->activatedAndNotifiedDivisionNotifications(), $webhook ); } @@ -26,7 +26,7 @@ private function hasBeenActivatedOrNotified(FlowMeasure $flowMeasure, WebhookInt private function hasNotBeenWithdrawnOrExpired(FlowMeasure $flowMeasure, WebhookInterface $webhook): bool { return $this->existingNotificationDoesntExist( - $flowMeasure->withdrawnAndExpiredDiscordNotifications(), + $flowMeasure->withdrawnAndExpiredDivisionNotifications(), $webhook ); } diff --git a/app/Discord/FlowMeasure/Webhook/WebhookMapper.php b/app/Discord/FlowMeasure/Webhook/WebhookMapper.php index d4c7a13b..4f6c4e5e 100644 --- a/app/Discord/FlowMeasure/Webhook/WebhookMapper.php +++ b/app/Discord/FlowMeasure/Webhook/WebhookMapper.php @@ -3,7 +3,6 @@ namespace App\Discord\FlowMeasure\Webhook; use App\Discord\FlowMeasure\Webhook\Filter\FilterInterface; -use App\Discord\Webhook\EcfmpWebhook; use App\Discord\Webhook\WebhookInterface; use App\Models\FlightInformationRegion; use App\Models\FlowMeasure; @@ -12,17 +11,15 @@ class WebhookMapper implements MapperInterface { private readonly FilterInterface $filter; - private readonly EcfmpWebhook $ecfmpWebhook; - public function __construct(FilterInterface $filter, EcfmpWebhook $ecfmpWebhook) + public function __construct(FilterInterface $filter) { $this->filter = $filter; - $this->ecfmpWebhook = $ecfmpWebhook; } public function mapToWebhooks(FlowMeasure $measure): Collection { - return Collection::make([$this->ecfmpWebhook]) + return Collection::make() ->merge( $measure->notifiedFlightInformationRegions->map( fn (FlightInformationRegion $fir) => $fir->divisionDiscordWebhooks diff --git a/app/Discord/Message/Associator/AssociatorInterface.php b/app/Discord/Message/Associator/AssociatorInterface.php index e43aa786..f9448c9e 100644 --- a/app/Discord/Message/Associator/AssociatorInterface.php +++ b/app/Discord/Message/Associator/AssociatorInterface.php @@ -2,9 +2,9 @@ namespace App\Discord\Message\Associator; -use App\Models\DiscordNotification; +use App\Models\DivisionDiscordNotification; interface AssociatorInterface { - public function associate(DiscordNotification $notification): void; + public function associate(DivisionDiscordNotification $notification): void; } diff --git a/app/Discord/Message/EcfmpMessageInterface.php b/app/Discord/Message/EcfmpMessageInterface.php new file mode 100644 index 00000000..0abec238 --- /dev/null +++ b/app/Discord/Message/EcfmpMessageInterface.php @@ -0,0 +1,26 @@ +title)) { + $embed->setTitle($this->title->title()); + } + + if (isset($this->colour)) { + $embed->setColor($this->colour->value); + } + + if (isset($this->description)) { + $embed->setDescription($this->description->description()); + } + + if ($this->fields->isNotEmpty()) { + $embed->setFields( + $this->fields->map(fn (FieldInterface $field) => new DiscordEmbedsFields([ + 'name' => $field->name(), + 'value' => $field->value(), + 'inline' => $field->inline(), + ]))->toArray() + ); + } + } + ); + } } diff --git a/app/Discord/Message/Embed/EmbedCollection.php b/app/Discord/Message/Embed/EmbedCollection.php index 8a03ae0f..0369f33f 100644 --- a/app/Discord/Message/Embed/EmbedCollection.php +++ b/app/Discord/Message/Embed/EmbedCollection.php @@ -29,4 +29,9 @@ public function toArray(): array { return $this->embeds->map(fn (EmbedInterface $embed) => $embed->toArray())->toArray(); } + + public function toProtobuf(): array + { + return $this->embeds->map(fn (EmbedInterface $embed) => $embed->toProtobuf())->toArray(); + } } diff --git a/app/Discord/Message/Embed/EmbedInterface.php b/app/Discord/Message/Embed/EmbedInterface.php index 2bda0f4f..8901332f 100644 --- a/app/Discord/Message/Embed/EmbedInterface.php +++ b/app/Discord/Message/Embed/EmbedInterface.php @@ -2,10 +2,17 @@ namespace App\Discord\Message\Embed; +use Ecfmp_discord\DiscordEmbeds; + interface EmbedInterface { /** * Converts the embed to array. */ public function toArray(): array; + + /** + * Converts the embed to protobuf format. + */ + public function toProtobuf(): DiscordEmbeds; } diff --git a/app/Discord/Message/Logger/LoggerInterface.php b/app/Discord/Message/Logger/LoggerInterface.php index 7adca40b..b149abae 100644 --- a/app/Discord/Message/Logger/LoggerInterface.php +++ b/app/Discord/Message/Logger/LoggerInterface.php @@ -2,9 +2,9 @@ namespace App\Discord\Message\Logger; -use App\Models\DiscordNotification; +use App\Models\DivisionDiscordNotification; interface LoggerInterface { - public function log(DiscordNotification $notification): void; + public function log(DivisionDiscordNotification $notification): void; } diff --git a/app/Discord/Message/Sender/Sender.php b/app/Discord/Message/Sender/DivisionWebhookSender.php similarity index 80% rename from app/Discord/Message/Sender/Sender.php rename to app/Discord/Message/Sender/DivisionWebhookSender.php index 949a18e7..bd372a17 100644 --- a/app/Discord/Message/Sender/Sender.php +++ b/app/Discord/Message/Sender/DivisionWebhookSender.php @@ -2,17 +2,17 @@ namespace App\Discord\Message\Sender; -use App\Discord\DiscordInterface; -use App\Models\DiscordNotification; +use App\Discord\DiscordWebhookInterface; +use App\Models\DivisionDiscordNotification; use DB; use Exception; -class Sender +class DivisionWebhookSender { private readonly array $generators; - private readonly DiscordInterface $discord; + private readonly DiscordWebhookInterface $discord; - public function __construct(array $generators, DiscordInterface $discord) + public function __construct(array $generators, DiscordWebhookInterface $discord) { $this->generators = $generators; $this->discord = $discord; @@ -33,7 +33,7 @@ public function sendDiscordMessages(): void // Associate it and log it DB::transaction(function () use ($message) { - $notification = DiscordNotification::create( + $notification = DivisionDiscordNotification::create( [ 'division_discord_webhook_id' => $message->destination()->id(), 'content' => $message->content(), diff --git a/app/Discord/Webhook/EcfmpWebhook.php b/app/Discord/Webhook/EcfmpWebhook.php deleted file mode 100644 index f706d701..00000000 --- a/app/Discord/Webhook/EcfmpWebhook.php +++ /dev/null @@ -1,21 +0,0 @@ -id(); switch ($data['type']) { diff --git a/app/Filament/Resources/FlowMeasureResource/Pages/EditFlowMeasure.php b/app/Filament/Resources/FlowMeasureResource/Pages/EditFlowMeasure.php index 53561c83..2aff443e 100644 --- a/app/Filament/Resources/FlowMeasureResource/Pages/EditFlowMeasure.php +++ b/app/Filament/Resources/FlowMeasureResource/Pages/EditFlowMeasure.php @@ -150,6 +150,8 @@ protected function handleRecordUpdate(Model $record, array $data): Model $newFlowMeasure = $record->replicate(); $newFlowMeasure->fill($data); $newFlowMeasure->identifier = FlowMeasureIdentifierGenerator::generateIdentifier($newFlowMeasure->start_time, $newFlowMeasure->flightInformationRegion); + $newFlowMeasure->canonical_identifier = $newFlowMeasure->identifier; + $newFlowMeasure->revision_number = FlowMeasureIdentifierGenerator::timesRevised($newFlowMeasure->identifier); $newFlowMeasure->push(); /* @@ -164,7 +166,9 @@ protected function handleRecordUpdate(Model $record, array $data): Model return $newFlowMeasure; } + $record->identifier = FlowMeasureIdentifierGenerator::generateRevisedIdentifier($record); + $record->revision_number = FlowMeasureIdentifierGenerator::timesRevised($record->identifier); $record->update($data); diff --git a/app/Helpers/FlowMeasureIdentifierGenerator.php b/app/Helpers/FlowMeasureIdentifierGenerator.php index b0ed05fb..b82fdd66 100644 --- a/app/Helpers/FlowMeasureIdentifierGenerator.php +++ b/app/Helpers/FlowMeasureIdentifierGenerator.php @@ -37,14 +37,35 @@ public static function generateIdentifier( ); } - public static function timesRevised(FlowMeasure $flowMeasure) + public static function timesRevised(FlowMeasure|string $flowMeasure) { + if ($flowMeasure instanceof FlowMeasure) { + return self::timesRevised($flowMeasure->identifier); + } + $identifierParts = []; - preg_match(self::IDENTIFIER_REGEX, $flowMeasure->identifier, $identifierParts); + preg_match(self::IDENTIFIER_REGEX, $flowMeasure, $identifierParts); return isset($identifierParts[5]) ? ((int)$identifierParts[5]) - 1 : 0; } + public static function canonicalIdentifier(FlowMeasure|string $flowMeasure): string + { + if ($flowMeasure instanceof FlowMeasure) { + return self::canonicalIdentifier($flowMeasure->identifier); + } + + $identifierParts = []; + preg_match(self::IDENTIFIER_REGEX, $flowMeasure, $identifierParts); + + return sprintf( + '%s%s%s', + $identifierParts[1], + $identifierParts[2], + $identifierParts[3] + ); + } + private static function designator(Carbon $startTime, FlightInformationRegion $flightInformationRegion): string { $flowMeasuresToday = FlowMeasure::where('start_time', '>=', $startTime->copy()->startOfDay()) diff --git a/app/Jobs/SendDiscordNotifications.php b/app/Jobs/SendDiscordNotifications.php index 9a72a357..90c8c445 100644 --- a/app/Jobs/SendDiscordNotifications.php +++ b/app/Jobs/SendDiscordNotifications.php @@ -2,7 +2,8 @@ namespace App\Jobs; -use App\Discord\Message\Sender\Sender; +use App\Discord\FlowMeasure\Generator\EcfmpFlowMeasureMessageGenerator; +use App\Discord\Message\Sender\DivisionWebhookSender; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeUnique; use Illuminate\Contracts\Queue\ShouldQueue; @@ -18,11 +19,16 @@ class SendDiscordNotifications implements ShouldQueue, ShouldBeUnique use Queueable; use SerializesModels; - private readonly Sender $sender; + // Unique for 2 minutes + public $uniqueFor = 120; - public function __construct(Sender $sender) + private readonly DivisionWebhookSender $sender; + private readonly EcfmpFlowMeasureMessageGenerator $generator; + + public function __construct(DivisionWebhookSender $sender, EcfmpFlowMeasureMessageGenerator $generator) { $this->sender = $sender; + $this->generator = $generator; } public function handle(): void @@ -34,6 +40,7 @@ public function handle(): void Log::info('Sending discord notifications'); $this->sender->sendDiscordMessages(); + $this->generator->generateAndSend(); Log::info('Discord notification sending complete'); } } diff --git a/app/Jobs/UpdateNetworkData.php b/app/Jobs/UpdateNetworkData.php index b6ad0311..3a346d06 100644 --- a/app/Jobs/UpdateNetworkData.php +++ b/app/Jobs/UpdateNetworkData.php @@ -18,6 +18,9 @@ class UpdateNetworkData implements ShouldQueue, ShouldBeUnique use Queueable; use SerializesModels; + // Unique for 2 minutes + public $uniqueFor = 120; + private readonly NetworkDataDownloader $networkDataDownloader; public function __construct(NetworkDataDownloader $networkDataDownloader) diff --git a/app/Models/DiscordNotification.php b/app/Models/DiscordNotification.php index ac1bb7d9..8328a434 100644 --- a/app/Models/DiscordNotification.php +++ b/app/Models/DiscordNotification.php @@ -2,11 +2,8 @@ namespace App\Models; -use App\Enums\DiscordNotificationType; -use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; class DiscordNotification extends Model @@ -16,45 +13,13 @@ class DiscordNotification extends Model public const UPDATED_AT = null; protected $fillable = [ - 'division_discord_webhook_id', - 'content', - 'embeds', - ]; - - protected $casts = [ - 'type' => DiscordNotificationType::class, - 'embeds' => 'array', + 'remote_id', ]; public function flowMeasure(): BelongsToMany { return $this->belongsToMany(FlowMeasure::class) - ->withPivot(['type', 'notified_as']) + ->withPivot(['notified_as', 'discord_notification_type_id']) ->withTimestamps(); } - - public function scopeType(Builder $query, DiscordNotificationType $type): Builder - { - return $query->where('type', $type); - } - - public function scopeTypes(Builder $query, array $types): Builder - { - return $query->whereIn('type', $types); - } - - public function divisionDiscordWebhook(): BelongsTo - { - return $this->belongsTo(DivisionDiscordWebhook::class); - } - - public function scopeIsEcfmp(Builder $query): Builder - { - return $query->whereNull('division_discord_webhook_id'); - } - - public function scopeIsDivision(Builder $query): Builder - { - return $query->whereNotNull('division_discord_webhook_id'); - } } diff --git a/app/Models/DivisionDiscordNotification.php b/app/Models/DivisionDiscordNotification.php new file mode 100644 index 00000000..acc231c7 --- /dev/null +++ b/app/Models/DivisionDiscordNotification.php @@ -0,0 +1,60 @@ + DiscordNotificationType::class, + 'embeds' => 'array', + ]; + + public function flowMeasure(): BelongsToMany + { + return $this->belongsToMany(FlowMeasure::class) + ->withPivot(['type', 'notified_as']) + ->withTimestamps(); + } + + public function scopeType(Builder $query, DiscordNotificationType $type): Builder + { + return $query->where('type', $type); + } + + public function scopeTypes(Builder $query, array $types): Builder + { + return $query->whereIn('type', $types); + } + + public function divisionDiscordWebhook(): BelongsTo + { + return $this->belongsTo(DivisionDiscordWebhook::class); + } + + public function scopeIsEcfmp(Builder $query): Builder + { + return $query->whereNull('division_discord_webhook_id'); + } + + public function scopeIsDivision(Builder $query): Builder + { + return $query->whereNotNull('division_discord_webhook_id'); + } +} diff --git a/app/Models/FlowMeasure.php b/app/Models/FlowMeasure.php index 04ba2676..35d89198 100644 --- a/app/Models/FlowMeasure.php +++ b/app/Models/FlowMeasure.php @@ -6,7 +6,6 @@ use App\Enums\FilterType; use App\Enums\FlowMeasureStatus; use App\Enums\FlowMeasureType; -use App\Helpers\FlowMeasureIdentifierGenerator; use Carbon\Carbon; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; @@ -23,6 +22,8 @@ class FlowMeasure extends Model protected $fillable = [ 'identifier', + 'canonical_identifier', + 'revision_number', 'user_id', 'flight_information_region_id', 'event_id', @@ -36,6 +37,7 @@ class FlowMeasure extends Model ]; protected $casts = [ + 'revision_number' => 'integer', 'mandatory_route' => 'array', 'filters' => 'array', 'start_time' => 'datetime', @@ -116,6 +118,13 @@ public function scopeFlightInformationRegion( return $query->where('flight_information_region_id', $flightInformationRegion->id); } + public function divisionDiscordNotifications(): BelongsToMany + { + return $this->belongsToMany(DivisionDiscordNotification::class) + ->withPivot(['discord_notification_type_id', 'notified_as']) + ->withTimestamps(); + } + public function discordNotifications(): BelongsToMany { return $this->belongsToMany(DiscordNotification::class) @@ -123,29 +132,46 @@ public function discordNotifications(): BelongsToMany ->withTimestamps(); } - public function notifiedDiscordNotifications(): BelongsToMany + public function discordNotificationsOfType(array $types): BelongsToMany { - return $this->notificationsOfType([DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED]); + return $this->discordNotifications() + ->wherePivotIn( + 'discord_notification_type_id', + DiscordNotificationType::whereIn( + 'type', + $types + )->pluck('id') + ); + } + + public function notifiedDivisionNotifications(): BelongsToMany + { + return $this->divisionNotificationsOfType([DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED]); } - public function activatedDiscordNotifications(): BelongsToMany + public function notifiedEcfmpNotifications(): BelongsToMany { - return $this->notificationsOfType([DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED]); + return $this->discordNotificationsOfType([DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED]); } - public function withdrawnDiscordNotifications(): BelongsToMany + public function activatedDivisionNotifications(): BelongsToMany { - return $this->notificationsOfType([DiscordNotificationTypeEnum::FLOW_MEASURE_WITHDRAWN]); + return $this->divisionNotificationsOfType([DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED]); } - public function expiredDiscordNotifications(): BelongsToMany + public function withdrawnDivisionNotifications(): BelongsToMany { - return $this->notificationsOfType([DiscordNotificationTypeEnum::FLOW_MEASURE_EXPIRED]); + return $this->divisionNotificationsOfType([DiscordNotificationTypeEnum::FLOW_MEASURE_WITHDRAWN]); } - public function withdrawnAndExpiredDiscordNotifications(): BelongsToMany + public function expiredDivisionNotifications(): BelongsToMany { - return $this->notificationsOfType( + return $this->divisionNotificationsOfType([DiscordNotificationTypeEnum::FLOW_MEASURE_EXPIRED]); + } + + public function withdrawnAndExpiredDivisionNotifications(): BelongsToMany + { + return $this->divisionNotificationsOfType( [ DiscordNotificationTypeEnum::FLOW_MEASURE_WITHDRAWN, DiscordNotificationTypeEnum::FLOW_MEASURE_EXPIRED, @@ -153,9 +179,9 @@ public function withdrawnAndExpiredDiscordNotifications(): BelongsToMany ); } - public function activatedAndNotifiedNotifications(): BelongsToMany + public function activatedAndNotifiedDivisionNotifications(): BelongsToMany { - return $this->notificationsOfType( + return $this->divisionNotificationsOfType( [ DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED, DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED, @@ -163,9 +189,51 @@ public function activatedAndNotifiedNotifications(): BelongsToMany ); } - private function notificationsOfType(array $types): BelongsToMany + public function scopeWithoutEcfmpNotificationOfTypeForIdentifier( + Builder $query, + DiscordNotificationTypeEnum $type + ): Builder { + return $query->whereDoesntHave('discordNotifications', function (Builder $query) use ($type) { + $query->where('discord_notification_type_id', DiscordNotificationType::where('type', $type)->firstOrFail()->id) + ->whereRaw('`notified_as` = `identifier`'); + }); + } + + public function scopeWithoutEcfmpNotificationOfType( + Builder $query, + DiscordNotificationTypeEnum $type + ): Builder { + return $this->scopeWithoutEcfmpNotificationOfTypes($query, [$type]); + } + + public function scopeWithoutEcfmpNotificationOfTypes( + Builder $query, + array $types + ): Builder { + return $query->whereDoesntHave('discordNotifications', function (Builder $query) use ($types) { + $query->whereIn('discord_notification_type_id', DiscordNotificationType::whereIn('type', $types)->pluck('id')); + }); + } + + public function scopeWithEcfmpNotificationOfType( + Builder $query, + DiscordNotificationTypeEnum $type + ): Builder { + return $this->scopeWithEcfmpNotificationOfTypes($query, [$type]); + } + + public function scopeWithEcfmpNotificationOfTypes( + Builder $query, + array $types + ): Builder { + return $query->whereHas('discordNotifications', function (Builder $query) use ($types) { + $query->whereIn('discord_notification_type_id', DiscordNotificationType::whereIn('type', $types)->pluck('id')); + }); + } + + private function divisionNotificationsOfType(array $types): BelongsToMany { - return $this->discordNotifications() + return $this->divisionDiscordNotifications() ->wherePivotIn( 'discord_notification_type_id', DiscordNotificationType::whereIn( @@ -231,12 +299,4 @@ public function status(): Attribute return FlowMeasureStatus::NOTIFIED; }); } - - public function reissueIdentifier(bool $save = true): void - { - $this->identifier = FlowMeasureIdentifierGenerator::generateRevisedIdentifier($this); - if ($save) { - $this->save(); - } - } } diff --git a/app/Providers/DiscordServiceProvider.php b/app/Providers/DiscordServiceProvider.php index 01c9f943..5ec7d0e9 100644 --- a/app/Providers/DiscordServiceProvider.php +++ b/app/Providers/DiscordServiceProvider.php @@ -2,20 +2,25 @@ namespace App\Providers; -use App\Discord\DiscordInterface; -use App\Discord\DiscordMessageSender; +use App\Discord\Client\ClientFactory; +use App\Discord\Client\ClientFactoryInterface; +use App\Discord\DiscordServiceInterface; +use App\Discord\DiscordServiceMessageSender; +use App\Discord\DiscordWebhookInterface; +use App\Discord\DiscordWebhookSender; +use App\Discord\FlowMeasure\Generator\EcfmpFlowMeasureMessageGenerator; use App\Discord\FlowMeasure\Message\FlowMeasureMessageFactory; use App\Discord\FlowMeasure\Message\MessageGenerator; use App\Discord\FlowMeasure\Message\MessageGeneratorInterface; -use App\Discord\FlowMeasure\Provider\MessageProvider; +use App\Discord\FlowMeasure\Provider\DivisionWebhookMessageProvider; +use App\Discord\FlowMeasure\Sender\EcfmpFlowMeasureSender; use App\Discord\FlowMeasure\Webhook\Filter\ActivatedWebhookFilter; use App\Discord\FlowMeasure\Webhook\Filter\ExpiredWebhookFilter; use App\Discord\FlowMeasure\Webhook\Filter\FilterInterface; use App\Discord\FlowMeasure\Webhook\Filter\NotifiedWebhookFilter; use App\Discord\FlowMeasure\Webhook\Filter\WithdrawnWebhookFilter; use App\Discord\FlowMeasure\Webhook\WebhookMapper; -use App\Discord\Message\Sender\Sender; -use App\Discord\Webhook\EcfmpWebhook; +use App\Discord\Message\Sender\DivisionWebhookSender; use App\Repository\FlowMeasureNotification\ActiveRepository; use App\Repository\FlowMeasureNotification\ExpiredRepository; use App\Repository\FlowMeasureNotification\NotifiedRepository; @@ -32,16 +37,37 @@ class DiscordServiceProvider extends ServiceProvider ExpiredRepository::class => ExpiredWebhookFilter::class, ]; - public function register() + public function register(): void { - $this->app->singleton(DiscordInterface::class, function () { - return new DiscordMessageSender(); + $this->app->singleton(DiscordWebhookInterface::class, function () { + return new DiscordWebhookSender(); }); - $this->app->singleton(EcfmpWebhook::class); - $this->app->singleton(Sender::class, fn () => new Sender( - $this->flowMeasureMessageProviders(), - $this->app->make(DiscordInterface::class) - )); + $this->app->singleton( + DivisionWebhookSender::class, + fn () => new DivisionWebhookSender( + $this->flowMeasureMessageProviders(), + $this->app->make(DiscordWebhookInterface::class) + ) + ); + + $this->app->singleton( + DiscordServiceInterface::class, + fn () => $this->app->make(DiscordServiceMessageSender::class) + ); + $this->app->singleton(ClientFactoryInterface::class, ClientFactory::class); + $this->app->singleton(ClientFactory::class); + $this->app->singleton( + EcfmpFlowMeasureMessageGenerator::class, + function () { + return new EcfmpFlowMeasureMessageGenerator( + $this->app->make(EcfmpFlowMeasureSender::class), + array_map( + fn (string $repository) => $this->app->make($repository), + array_keys(self::FLOW_MEASURE_MESSAGE_REPOSITORIES) + ) + ); + } + ); } private function flowMeasureMessageProviders(): array @@ -63,7 +89,7 @@ private function makeMessageProvider( FilterInterface $filter ): MessageGeneratorInterface { return new MessageGenerator( - new MessageProvider( + new DivisionWebhookMessageProvider( $repository, $this->app->make( WebhookMapper::class, diff --git a/app/Repository/FlowMeasureNotification/ActiveRepository.php b/app/Repository/FlowMeasureNotification/ActiveRepository.php index 58370ea3..0400ae90 100644 --- a/app/Repository/FlowMeasureNotification/ActiveRepository.php +++ b/app/Repository/FlowMeasureNotification/ActiveRepository.php @@ -4,13 +4,46 @@ use App\Enums\DiscordNotificationType; use App\Models\FlowMeasure; +use Illuminate\Database\Query\Builder; use Illuminate\Support\Collection; class ActiveRepository implements RepositoryInterface { + public function flowMeasuresToBeSentToEcfmp(): Collection + { + return FlowMeasure::active() + ->withoutEcfmpNotificationOfTypeForIdentifier($this->notificationType()) + ->select('flow_measures.*') + ->selectSub( + function (Builder $query) { + $query->selectRaw('COUNT(*)') + ->from('discord_notification_flow_measure', 'previous_notifications_for_identifier') + ->whereRaw('previous_notifications_for_identifier.flow_measure_id = flow_measures.id') + ->whereRaw('previous_notifications_for_identifier.notified_as = flow_measures.identifier'); + }, + 'count_previous_for_identifier' + ) + ->selectSub( + function (Builder $query) { + $query->selectRaw('COUNT(*)') + ->from('discord_notification_flow_measure', 'previous_notifications_for_other_identifier') + ->whereRaw('previous_notifications_for_other_identifier.flow_measure_id = flow_measures.id') + ->whereRaw('previous_notifications_for_other_identifier.notified_as <> flow_measures.identifier'); + }, + 'count_previous_other_identifiers' + ) + ->get() + ->map( + fn (FlowMeasure $measure) => new FlowMeasureForNotification( + $measure, + $measure->count_previous_for_identifier === 0 && $measure->count_previous_other_identifiers !== 0 + ) + ); + } + public function flowMeasuresForNotification(): Collection { - return FlowMeasure::with('discordNotifications') + return FlowMeasure::with('divisionDiscordNotifications') ->active() ->get(); } diff --git a/app/Repository/FlowMeasureNotification/ExpiredRepository.php b/app/Repository/FlowMeasureNotification/ExpiredRepository.php index 67a88cc2..6f9da9b4 100644 --- a/app/Repository/FlowMeasureNotification/ExpiredRepository.php +++ b/app/Repository/FlowMeasureNotification/ExpiredRepository.php @@ -3,14 +3,44 @@ namespace App\Repository\FlowMeasureNotification; use App\Enums\DiscordNotificationType; +use App\Models\DiscordNotification; use App\Models\FlowMeasure; +use Carbon\Carbon; use Illuminate\Support\Collection; class ExpiredRepository implements RepositoryInterface { + public function flowMeasuresToBeSentToEcfmp(): Collection + { + $webhooksSentInLastTwoHours = DiscordNotification::where('created_at', '>=', Carbon::now()->subHours(2)) + ->count(); + + if ($webhooksSentInLastTwoHours > 5) { + return collect(); + } + + return FlowMeasure::expiredRecently() + ->where('revision_number', '<', 2) + ->withEcfmpNotificationOfTypes([ + DiscordNotificationType::FLOW_MEASURE_ACTIVATED, + DiscordNotificationType::FLOW_MEASURE_NOTIFIED, + ]) + ->withoutEcfmpNotificationOfTypes([ + DiscordNotificationType::FLOW_MEASURE_EXPIRED, + DiscordNotificationType::FLOW_MEASURE_WITHDRAWN, + ]) + ->get() + ->map( + fn (FlowMeasure $measure) => new FlowMeasureForNotification( + $measure, + false + ) + ); + } + public function flowMeasuresForNotification(): Collection { - return FlowMeasure::with('discordNotifications') + return FlowMeasure::with('divisionDiscordNotifications') ->expiredRecently() ->get(); } diff --git a/app/Repository/FlowMeasureNotification/FlowMeasureForNotification.php b/app/Repository/FlowMeasureNotification/FlowMeasureForNotification.php new file mode 100644 index 00000000..a466c108 --- /dev/null +++ b/app/Repository/FlowMeasureNotification/FlowMeasureForNotification.php @@ -0,0 +1,14 @@ +addDay()) + ->where('start_time', '>', Carbon::now()) + ->withoutEcfmpNotificationOfTypeForIdentifier($this->notificationType()) + ->withoutEcfmpNotificationOfType(DiscordNotificationType::FLOW_MEASURE_ACTIVATED) + ->select('flow_measures.*') + ->selectSub( + function (Builder $query) { + $query->selectRaw('COUNT(*)') + ->from('discord_notification_flow_measure', 'previous_notifications_for_identifier') + ->whereRaw('previous_notifications_for_identifier.flow_measure_id = flow_measures.id') + ->whereRaw('previous_notifications_for_identifier.notified_as = flow_measures.identifier'); + }, + 'count_previous_for_identifier' + ) + ->selectSub( + function (Builder $query) { + $query->selectRaw('COUNT(*)') + ->from('discord_notification_flow_measure', 'previous_notifications_for_other_identifier') + ->whereRaw('previous_notifications_for_other_identifier.flow_measure_id = flow_measures.id') + ->whereRaw('previous_notifications_for_other_identifier.notified_as <> flow_measures.identifier'); + }, + 'count_previous_other_identifiers' + ) + ->get() + ->map( + fn (FlowMeasure $measure) => new FlowMeasureForNotification( + $measure, + $measure->count_previous_for_identifier === 0 && $measure->count_previous_other_identifiers !== 0 + ) + ); + } + public function flowMeasuresForNotification(): Collection { - return FlowMeasure::with('discordNotifications') + return FlowMeasure::with('divisionDiscordNotifications') ->where('start_time', '<', Carbon::now()->addDay()) ->where('start_time', '>', Carbon::now()) ->get(); diff --git a/app/Repository/FlowMeasureNotification/RepositoryInterface.php b/app/Repository/FlowMeasureNotification/RepositoryInterface.php index 6f57c725..230e27a4 100644 --- a/app/Repository/FlowMeasureNotification/RepositoryInterface.php +++ b/app/Repository/FlowMeasureNotification/RepositoryInterface.php @@ -7,6 +7,11 @@ interface RepositoryInterface { + /** + * Returns the flow measures that need to be sent to ECFMP. + */ + public function flowMeasuresToBeSentToEcfmp(): Collection; + /** * Get all the flow measures for notification. */ diff --git a/app/Repository/FlowMeasureNotification/WithdrawnRepository.php b/app/Repository/FlowMeasureNotification/WithdrawnRepository.php index d7da842a..aaeaad31 100644 --- a/app/Repository/FlowMeasureNotification/WithdrawnRepository.php +++ b/app/Repository/FlowMeasureNotification/WithdrawnRepository.php @@ -10,6 +10,20 @@ class WithdrawnRepository implements RepositoryInterface { + public function flowMeasuresToBeSentToEcfmp(): Collection + { + return $this->baseQueryForEcfmp()->notified() + ->union($this->baseQueryForEcfmp()->active()) + ->orderBy('id') + ->get() + ->map( + fn (FlowMeasure $measure) => new FlowMeasureForNotification( + $measure, + false + ) + ); + } + public function flowMeasuresForNotification(): Collection { return $this->baseQuery()->notified() @@ -18,9 +32,22 @@ public function flowMeasuresForNotification(): Collection ->get(); } + public function baseQueryForEcfmp(): Builder + { + return $this->baseQuery() + ->WithEcfmpNotificationOfTypes([ + DiscordNotificationType::FLOW_MEASURE_ACTIVATED, + DiscordNotificationType::FLOW_MEASURE_NOTIFIED, + ]) + ->withoutEcfmpNotificationOfTypes([ + DiscordNotificationType::FLOW_MEASURE_EXPIRED, + DiscordNotificationType::FLOW_MEASURE_WITHDRAWN, + ]); + } + private function baseQuery(): Builder { - return FlowMeasure::with('discordNotifications') + return FlowMeasure::with('divisionDiscordNotifications') ->onlyTrashed() ->where('deleted_at', '>', Carbon::now()->subHour()); } diff --git a/composer.json b/composer.json index ba91fbc6..3749f7f8 100644 --- a/composer.json +++ b/composer.json @@ -8,10 +8,13 @@ ], "license": "MIT", "require": { - "php": "^8.1", + "php": "^8.2", + "ext-grpc": "*", "ext-json": "*", "doctrine/dbal": "^3.3", "filament/filament": "^2.14", + "google/protobuf": "^3.17", + "grpc/grpc": "^1.38", "guzzlehttp/guzzle": "^7.2", "laravel/framework": "^10.5", "laravel/socialite": "^5.5", @@ -45,7 +48,9 @@ "App\\": "app/", "Database\\Factories\\": "database/factories/", "Database\\Seeders\\": "database/seeders/", - "SocialiteProviders\\VatsimConnect\\": "SocialiteProviders/src/VatsimConnect/" + "SocialiteProviders\\VatsimConnect\\": "SocialiteProviders/src/VatsimConnect/", + "Ecfmp_discord\\": "protobuf/discord/gen/pb-php/Ecfmp_discord/", + "GPBMetadata\\": "protobuf/discord/gen/pb-php/GPBMetadata/" } }, "autoload-dev": { diff --git a/composer.lock b/composer.lock index b9906dea..5cf7809f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "5b2757f4f5ce1d724ad92bd36894217b", + "content-hash": "ea2754f9df7a7351175e772ec656f410", "packages": [ { "name": "akaunting/laravel-money", @@ -1657,6 +1657,50 @@ ], "time": "2023-10-12T05:21:21+00:00" }, + { + "name": "google/protobuf", + "version": "v3.24.4", + "source": { + "type": "git", + "url": "https://github.com/protocolbuffers/protobuf-php.git", + "reference": "672d69e25f71b9364fdf1810eb8a8573defdc404" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/672d69e25f71b9364fdf1810eb8a8573defdc404", + "reference": "672d69e25f71b9364fdf1810eb8a8573defdc404", + "shasum": "" + }, + "require": { + "php": ">=7.0.0" + }, + "require-dev": { + "phpunit/phpunit": ">=5.0.0" + }, + "suggest": { + "ext-bcmath": "Need to support JSON deserialization" + }, + "type": "library", + "autoload": { + "psr-4": { + "Google\\Protobuf\\": "src/Google/Protobuf", + "GPBMetadata\\Google\\Protobuf\\": "src/GPBMetadata/Google/Protobuf" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "proto library for PHP", + "homepage": "https://developers.google.com/protocol-buffers/", + "keywords": [ + "proto" + ], + "support": { + "source": "https://github.com/protocolbuffers/protobuf-php/tree/v3.24.4" + }, + "time": "2023-10-04T17:22:47+00:00" + }, { "name": "graham-campbell/result-type", "version": "v1.1.1", @@ -1719,6 +1763,50 @@ ], "time": "2023-02-25T20:23:15+00:00" }, + { + "name": "grpc/grpc", + "version": "1.57.0", + "source": { + "type": "git", + "url": "https://github.com/grpc/grpc-php.git", + "reference": "b610c42022ed3a22f831439cb93802f2a4502fdf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/grpc/grpc-php/zipball/b610c42022ed3a22f831439cb93802f2a4502fdf", + "reference": "b610c42022ed3a22f831439cb93802f2a4502fdf", + "shasum": "" + }, + "require": { + "php": ">=7.0.0" + }, + "require-dev": { + "google/auth": "^v1.3.0" + }, + "suggest": { + "ext-protobuf": "For better performance, install the protobuf C extension.", + "google/protobuf": "To get started using grpc quickly, install the native protobuf library." + }, + "type": "library", + "autoload": { + "psr-4": { + "Grpc\\": "src/lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "description": "gRPC library for PHP", + "homepage": "https://grpc.io", + "keywords": [ + "rpc" + ], + "support": { + "source": "https://github.com/grpc/grpc-php/tree/v1.57.0" + }, + "time": "2023-08-14T23:57:54+00:00" + }, { "name": "guzzlehttp/guzzle", "version": "7.8.0", @@ -13713,8 +13801,9 @@ "prefer-lowest": false, "platform": { "php": "^8.1", + "ext-grpc": "*", "ext-json": "*" }, "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/config/discord.php b/config/discord.php index 8d1e3953..28027ba8 100644 --- a/config/discord.php +++ b/config/discord.php @@ -5,5 +5,9 @@ 'avatar_url' => env('DISCORD_AVATAR_URL', sprintf('%s/images/logo.png', env('APP_URL'))), 'enabled' => env('DISCORD_NOTIFICATIONS_ENABLE', false), 'webhook_url' => env('DISCORD_WEBHOOK_URL', ''), - 'token' => env('DISCORD_AUTH_TOKEN', '') + 'token' => env('DISCORD_AUTH_TOKEN', ''), + 'service_host' => env('DISCORD_BOT_SERVICE_URL', 'localhost'), + 'service_token' => env('DISCORD_BOT_JWT', ''), + 'ecfmp_channel_id' => env('DISCORD_ECFMP_CHANNEL_ID', ''), + 'client_request_app_id' => env('DISCORD_CLIENT_REQUEST_APP_ID', ''), ]; diff --git a/database/factories/DiscordNotificationFactory.php b/database/factories/DiscordNotificationFactory.php index 708a73ad..1d3eb03a 100644 --- a/database/factories/DiscordNotificationFactory.php +++ b/database/factories/DiscordNotificationFactory.php @@ -2,7 +2,6 @@ namespace Database\Factories; -use App\Models\DivisionDiscordWebhook; use Illuminate\Database\Eloquent\Factories\Factory; /** @@ -18,18 +17,7 @@ class DiscordNotificationFactory extends Factory public function definition() { return [ - 'division_discord_webhook_id' => null, - 'content' => 'ohai', - 'embeds' => [ - 'foo' => 'var', - ], + 'remote_id' => $this->faker->uuid, ]; } - - public function toDivisionWebhook(DivisionDiscordWebhook $divisionDiscordWebhook): static - { - return $this->state(fn (array $attributes) => [ - 'division_discord_webhook_id' => $divisionDiscordWebhook->id, - ]); - } } diff --git a/database/factories/DivisionDiscordNotificationFactory.php b/database/factories/DivisionDiscordNotificationFactory.php new file mode 100644 index 00000000..58dc4a2b --- /dev/null +++ b/database/factories/DivisionDiscordNotificationFactory.php @@ -0,0 +1,35 @@ + + */ +class DivisionDiscordNotificationFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition() + { + return [ + 'division_discord_webhook_id' => null, + 'content' => 'ohai', + 'embeds' => [ + 'foo' => 'var', + ], + ]; + } + + public function toDivisionWebhook(DivisionDiscordWebhook $divisionDiscordWebhook): static + { + return $this->state(fn (array $attributes) => [ + 'division_discord_webhook_id' => $divisionDiscordWebhook->id, + ]); + } +} diff --git a/database/factories/FlowMeasureFactory.php b/database/factories/FlowMeasureFactory.php index 0942e40b..488b23f2 100644 --- a/database/factories/FlowMeasureFactory.php +++ b/database/factories/FlowMeasureFactory.php @@ -21,9 +21,12 @@ public function definition() { $fir = FlightInformationRegion::factory()->create(); $startDate = Carbon::parse($this->faker->dateTimeBetween('-1 hour')); + $identifier = FlowMeasureIdentifierGenerator::generateIdentifier($startDate, $fir); return [ - 'identifier' => FlowMeasureIdentifierGenerator::generateIdentifier($startDate, $fir), + 'identifier' => $identifier, + 'canonical_identifier' => FlowMeasureIdentifierGenerator::canonicalIdentifier($identifier), + 'revision_number' => FlowMeasureIdentifierGenerator::timesRevised($identifier), 'user_id' => User::factory()->create()->id, 'flight_information_region_id' => $fir->id, 'event_id' => null, @@ -102,6 +105,14 @@ public function notNotified(): static ]); } + + public function withdrawn(): static + { + return $this->state(fn (array $attributes) => [ + 'deleted_at' => Carbon::now(), + ]); + } + public function withEvent(): static { return $this->state(fn (array $attributes) => [ diff --git a/database/migrations/2023_10_08_112953_rename_discord_notifications_tables.php b/database/migrations/2023_10_08_112953_rename_discord_notifications_tables.php new file mode 100644 index 00000000..b4e3faa1 --- /dev/null +++ b/database/migrations/2023_10_08_112953_rename_discord_notifications_tables.php @@ -0,0 +1,22 @@ +id(); + $table->uuid('remote_id') + ->unique(); + $table->timestamp('created_at'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('discord_notifications'); + } +}; diff --git a/database/migrations/2023_10_08_123708_create_discord_notification_flow_measure_table.php b/database/migrations/2023_10_08_123708_create_discord_notification_flow_measure_table.php new file mode 100644 index 00000000..6bf85cc0 --- /dev/null +++ b/database/migrations/2023_10_08_123708_create_discord_notification_flow_measure_table.php @@ -0,0 +1,44 @@ +id(); + + $table->foreignIdFor(FlowMeasure::class) + ->constrained() + ->cascadeOnDelete(); + + $table->foreignId('discord_notification_id') + ->constrained('discord_notifications', indexName: 'discord_notification_id') + ->cascadeOnDelete(); + + $table->foreignId('discord_notification_type_id') + ->constrained('discord_notification_types', indexName: 'discord_notification_type_id'); + + $table->string('notified_as') + ->index() + ->comment('The identifier of the flow measure at the point of notification') + ->cascadeOnDelete(); + + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('discord_notification_flow_measure'); + } +}; diff --git a/database/migrations/2023_10_14_150204_add_columns_to_flow_measures_table.php b/database/migrations/2023_10_14_150204_add_columns_to_flow_measures_table.php new file mode 100644 index 00000000..2103467f --- /dev/null +++ b/database/migrations/2023_10_14_150204_add_columns_to_flow_measures_table.php @@ -0,0 +1,34 @@ +string('canonical_identifier') + ->after('identifier') + ->comment('The original identifier of the flow measure'); + + $table->unsignedInteger('revision_number') + ->after('canonical_identifier') + ->comment('The revision number of the flow measure'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('flow_measures', function (Blueprint $table) { + $table->dropColumn('revision_number'); + $table->dropColumn('canonical_identifier'); + }); + } +}; diff --git a/database/migrations/2023_10_14_150428_update_flow_measure_data.php b/database/migrations/2023_10_14_150428_update_flow_measure_data.php new file mode 100644 index 00000000..b9a324df --- /dev/null +++ b/database/migrations/2023_10_14_150428_update_flow_measure_data.php @@ -0,0 +1,27 @@ +each(function (FlowMeasure $flowMeasure) { + $flowMeasure->revision_number = FlowMeasureIdentifierGenerator::timesRevised($flowMeasure); + $flowMeasure->canonical_identifier = FlowMeasureIdentifierGenerator::canonicalIdentifier($flowMeasure); + $flowMeasure->save(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // + } +}; diff --git a/database/schema/mysql-schema.sql b/database/schema/mysql-schema.sql new file mode 100644 index 00000000..cbca8ab7 --- /dev/null +++ b/database/schema/mysql-schema.sql @@ -0,0 +1,726 @@ +-- MySQL dump 10.13 Distrib 8.0.34, for Linux (x86_64) +-- +-- Host: mysql Database: laravel +-- ------------------------------------------------------ +-- Server version 8.0.32 + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!50503 SET NAMES utf8mb4 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Table structure for table `activity_log` +-- + +DROP TABLE IF EXISTS `activity_log`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `activity_log` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `log_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `description` text COLLATE utf8mb4_unicode_ci NOT NULL, + `subject_type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `event` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `subject_id` bigint unsigned DEFAULT NULL, + `causer_type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `causer_id` bigint unsigned DEFAULT NULL, + `properties` json DEFAULT NULL, + `batch_uuid` char(36) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `subject` (`subject_type`,`subject_id`), + KEY `causer` (`causer_type`,`causer_id`), + KEY `activity_log_log_name_index` (`log_name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `activity_log` +-- + +LOCK TABLES `activity_log` WRITE; +/*!40000 ALTER TABLE `activity_log` DISABLE KEYS */; +/*!40000 ALTER TABLE `activity_log` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `airport_airport_group` +-- + +DROP TABLE IF EXISTS `airport_airport_group`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `airport_airport_group` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `airport_id` bigint unsigned NOT NULL, + `airport_group_id` bigint unsigned NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `airport_airport_group_id` (`airport_id`,`airport_group_id`), + KEY `airport_airport_group_airport_group_id_foreign` (`airport_group_id`), + CONSTRAINT `airport_airport_group_airport_group_id_foreign` FOREIGN KEY (`airport_group_id`) REFERENCES `airport_groups` (`id`) ON DELETE CASCADE, + CONSTRAINT `airport_airport_group_airport_id_foreign` FOREIGN KEY (`airport_id`) REFERENCES `airports` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `airport_airport_group` +-- + +LOCK TABLES `airport_airport_group` WRITE; +/*!40000 ALTER TABLE `airport_airport_group` DISABLE KEYS */; +/*!40000 ALTER TABLE `airport_airport_group` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `airport_groups` +-- + +DROP TABLE IF EXISTS `airport_groups`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `airport_groups` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'The name of the group', + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `airport_group_name_unique` (`name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `airport_groups` +-- + +LOCK TABLES `airport_groups` WRITE; +/*!40000 ALTER TABLE `airport_groups` DISABLE KEYS */; +/*!40000 ALTER TABLE `airport_groups` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `airports` +-- + +DROP TABLE IF EXISTS `airports`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `airports` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `icao_code` varchar(4) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'E.g. EGGD', + `latitude` decimal(10,8) DEFAULT NULL, + `longitude` decimal(11,8) DEFAULT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `airports_icao_code_unique` (`icao_code`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `airports` +-- + +LOCK TABLES `airports` WRITE; +/*!40000 ALTER TABLE `airports` DISABLE KEYS */; +/*!40000 ALTER TABLE `airports` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `discord_notification_types` +-- + +DROP TABLE IF EXISTS `discord_notification_types`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `discord_notification_types` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `created_at` timestamp NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `discord_notification_types_type_unique` (`type`) +) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `discord_notification_types` +-- + +LOCK TABLES `discord_notification_types` WRITE; +/*!40000 ALTER TABLE `discord_notification_types` DISABLE KEYS */; +INSERT INTO `discord_notification_types` VALUES (1,'flow_measure_notified','2023-10-08 12:08:17'),(2,'flow_measure_activated','2023-10-08 12:08:17'),(3,'flow_measure_withdrawn','2023-10-08 12:08:17'),(4,'flow_measure_expired','2023-10-08 12:08:17'); +/*!40000 ALTER TABLE `discord_notification_types` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `discord_tag_flight_information_region` +-- + +DROP TABLE IF EXISTS `discord_tag_flight_information_region`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `discord_tag_flight_information_region` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `flight_information_region_id` bigint unsigned NOT NULL, + `discord_tag_id` bigint unsigned NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `discord_tag_flight_information_region` (`discord_tag_id`,`flight_information_region_id`), + KEY `discord_flight_information_region_id` (`flight_information_region_id`), + CONSTRAINT `discord_discord_tag_id` FOREIGN KEY (`discord_tag_id`) REFERENCES `discord_tags` (`id`) ON DELETE CASCADE, + CONSTRAINT `discord_flight_information_region_id` FOREIGN KEY (`flight_information_region_id`) REFERENCES `flight_information_regions` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `discord_tag_flight_information_region` +-- + +LOCK TABLES `discord_tag_flight_information_region` WRITE; +/*!40000 ALTER TABLE `discord_tag_flight_information_region` DISABLE KEYS */; +/*!40000 ALTER TABLE `discord_tag_flight_information_region` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `discord_tags` +-- + +DROP TABLE IF EXISTS `discord_tags`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `discord_tags` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `tag` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'The tag to use', + `description` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'What the tag is for / who it is targeted at', + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `discord_tags_tag_unique` (`tag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `discord_tags` +-- + +LOCK TABLES `discord_tags` WRITE; +/*!40000 ALTER TABLE `discord_tags` DISABLE KEYS */; +/*!40000 ALTER TABLE `discord_tags` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `discord_notification_flow_measure` +-- + +DROP TABLE IF EXISTS `discord_notification_flow_measure`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `discord_notification_flow_measure` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `discord_notification_id` bigint unsigned NOT NULL, + `flow_measure_id` bigint unsigned NOT NULL, + `discord_notification_type_id` bigint unsigned NOT NULL, + `notified_as` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'What the identifier of the flow measure was at the time the discord notification was sent', + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `discord_flow_measure_discord` (`discord_notification_id`), + KEY `discord_flow_measure_flow` (`flow_measure_id`), + KEY `discord_flow_measure_type` (`discord_notification_type_id`), + CONSTRAINT `discord_flow_measure_discord` FOREIGN KEY (`discord_notification_id`) REFERENCES `discord_notifications` (`id`) ON DELETE CASCADE, + CONSTRAINT `discord_flow_measure_flow` FOREIGN KEY (`flow_measure_id`) REFERENCES `flow_measures` (`id`) ON DELETE CASCADE, + CONSTRAINT `discord_flow_measure_type` FOREIGN KEY (`discord_notification_type_id`) REFERENCES `discord_notification_types` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `discord_notification_flow_measure` +-- + +LOCK TABLES `discord_notification_flow_measure` WRITE; +/*!40000 ALTER TABLE `discord_notification_flow_measure` DISABLE KEYS */; +/*!40000 ALTER TABLE `discord_notification_flow_measure` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `discord_notifications` +-- + +DROP TABLE IF EXISTS `discord_notifications`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `discord_notifications` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `division_discord_webhook_id` bigint unsigned DEFAULT NULL COMMENT 'Which divisional discord server this notification was sent to', + `content` text COLLATE utf8mb4_unicode_ci NOT NULL, + `embeds` json DEFAULT NULL, + `created_at` timestamp NOT NULL, + PRIMARY KEY (`id`), + KEY `discord_notifications_webhook` (`division_discord_webhook_id`), + KEY `discord_notifications_created_at_index` (`created_at`), + CONSTRAINT `discord_notifications_webhook` FOREIGN KEY (`division_discord_webhook_id`) REFERENCES `division_discord_webhooks` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `discord_notifications` +-- + +LOCK TABLES `discord_notifications` WRITE; +/*!40000 ALTER TABLE `discord_notifications` DISABLE KEYS */; +/*!40000 ALTER TABLE `discord_notifications` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `division_discord_webhook_flight_information_region` +-- + +DROP TABLE IF EXISTS `division_discord_webhook_flight_information_region`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `division_discord_webhook_flight_information_region` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `division_discord_webhook_id` bigint unsigned NOT NULL, + `flight_information_region_id` bigint unsigned NOT NULL, + `tag` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `discord_webhook_fir_unique` (`division_discord_webhook_id`,`flight_information_region_id`), + KEY `division_discord_fir` (`flight_information_region_id`), + CONSTRAINT `division_discord_fir` FOREIGN KEY (`flight_information_region_id`) REFERENCES `flight_information_regions` (`id`) ON DELETE CASCADE, + CONSTRAINT `division_discord_fir_discord` FOREIGN KEY (`division_discord_webhook_id`) REFERENCES `division_discord_webhooks` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `division_discord_webhook_flight_information_region` +-- + +LOCK TABLES `division_discord_webhook_flight_information_region` WRITE; +/*!40000 ALTER TABLE `division_discord_webhook_flight_information_region` DISABLE KEYS */; +/*!40000 ALTER TABLE `division_discord_webhook_flight_information_region` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `division_discord_webhooks` +-- + +DROP TABLE IF EXISTS `division_discord_webhooks`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `division_discord_webhooks` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `url` varchar(500) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'The webhook URL', + `description` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'What this webhook is for', + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + `deleted_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `division_discord_webhooks_url_unique` (`url`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `division_discord_webhooks` +-- + +LOCK TABLES `division_discord_webhooks` WRITE; +/*!40000 ALTER TABLE `division_discord_webhooks` DISABLE KEYS */; +/*!40000 ALTER TABLE `division_discord_webhooks` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `event_participants` +-- + +DROP TABLE IF EXISTS `event_participants`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `event_participants` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `event_id` bigint unsigned NOT NULL, + `cid` bigint unsigned NOT NULL, + `origin` varchar(4) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `destination` varchar(4) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `event_participants_event_id_foreign` (`event_id`), + CONSTRAINT `event_participants_event_id_foreign` FOREIGN KEY (`event_id`) REFERENCES `events` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `event_participants` +-- + +LOCK TABLES `event_participants` WRITE; +/*!40000 ALTER TABLE `event_participants` DISABLE KEYS */; +/*!40000 ALTER TABLE `event_participants` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `events` +-- + +DROP TABLE IF EXISTS `events`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `events` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'The event name', + `date_start` datetime NOT NULL COMMENT 'When the event begins (Z)', + `date_end` datetime NOT NULL COMMENT 'When the event ends (Z)', + `flight_information_region_id` bigint unsigned NOT NULL, + `vatcan_code` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'The VATCAN events system code', + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + `deleted_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `events_flight_information_region_id_foreign` (`flight_information_region_id`), + KEY `events_date_start_date_end_index` (`date_start`,`date_end`), + KEY `events_deleted_at_index` (`deleted_at`), + KEY `events_created_at_index` (`created_at`), + CONSTRAINT `events_flight_information_region_id_foreign` FOREIGN KEY (`flight_information_region_id`) REFERENCES `flight_information_regions` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `events` +-- + +LOCK TABLES `events` WRITE; +/*!40000 ALTER TABLE `events` DISABLE KEYS */; +/*!40000 ALTER TABLE `events` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `failed_jobs` +-- + +DROP TABLE IF EXISTS `failed_jobs`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `failed_jobs` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `uuid` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `connection` text COLLATE utf8mb4_unicode_ci NOT NULL, + `queue` text COLLATE utf8mb4_unicode_ci NOT NULL, + `payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL, + `exception` longtext COLLATE utf8mb4_unicode_ci NOT NULL, + `failed_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `failed_jobs_uuid_unique` (`uuid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `failed_jobs` +-- + +LOCK TABLES `failed_jobs` WRITE; +/*!40000 ALTER TABLE `failed_jobs` DISABLE KEYS */; +/*!40000 ALTER TABLE `failed_jobs` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `flight_information_region_flow_measure` +-- + +DROP TABLE IF EXISTS `flight_information_region_flow_measure`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `flight_information_region_flow_measure` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `flow_measure_id` bigint unsigned NOT NULL COMMENT 'The flow measure', + `flight_information_region_id` bigint unsigned NOT NULL COMMENT 'The flight information region that needs to be concerned with the flow measure', + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `fir_flow_measure_unique` (`flight_information_region_id`,`flow_measure_id`), + KEY `flight_information_region_flow_measure` (`flow_measure_id`), + CONSTRAINT `flight_information_region_flow_measure` FOREIGN KEY (`flow_measure_id`) REFERENCES `flow_measures` (`id`) ON DELETE CASCADE, + CONSTRAINT `flow_measure_flight_information_region` FOREIGN KEY (`flight_information_region_id`) REFERENCES `flight_information_regions` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `flight_information_region_flow_measure` +-- + +LOCK TABLES `flight_information_region_flow_measure` WRITE; +/*!40000 ALTER TABLE `flight_information_region_flow_measure` DISABLE KEYS */; +/*!40000 ALTER TABLE `flight_information_region_flow_measure` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `flight_information_region_user` +-- + +DROP TABLE IF EXISTS `flight_information_region_user`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `flight_information_region_user` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `user_id` bigint unsigned NOT NULL, + `flight_information_region_id` bigint unsigned NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `flight_information_region_user` (`user_id`,`flight_information_region_id`), + KEY `flight_information_region_id` (`flight_information_region_id`), + CONSTRAINT `flight_information_region_id` FOREIGN KEY (`flight_information_region_id`) REFERENCES `flight_information_regions` (`id`) ON DELETE CASCADE, + CONSTRAINT `flight_information_region_user_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `flight_information_region_user` +-- + +LOCK TABLES `flight_information_region_user` WRITE; +/*!40000 ALTER TABLE `flight_information_region_user` DISABLE KEYS */; +/*!40000 ALTER TABLE `flight_information_region_user` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `flight_information_regions` +-- + +DROP TABLE IF EXISTS `flight_information_regions`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `flight_information_regions` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `identifier` varchar(4) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'The FIR id, e.g. EGTT', + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'The name of the FIR, e.g. London', + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `flight_information_regions_identifier_unique` (`identifier`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `flight_information_regions` +-- + +LOCK TABLES `flight_information_regions` WRITE; +/*!40000 ALTER TABLE `flight_information_regions` DISABLE KEYS */; +/*!40000 ALTER TABLE `flight_information_regions` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `flow_measures` +-- + +DROP TABLE IF EXISTS `flow_measures`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `flow_measures` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `identifier` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'The identifier of the flow rule', + `user_id` bigint unsigned NOT NULL COMMENT 'The user who created this flow measure', + `flight_information_region_id` bigint unsigned NOT NULL COMMENT 'The flight information region issuing this flow measure', + `event_id` bigint unsigned DEFAULT NULL COMMENT 'The event that this measure belongs to, if any', + `reason` text COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'The reason given for the flow measure being in place', + `type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'The type of flow measure', + `value` int unsigned DEFAULT NULL COMMENT 'Used to specify the value of the measure, for all but mandatory_route', + `mandatory_route` json DEFAULT NULL COMMENT 'Used to specify mandatory route strings', + `filters` json NOT NULL COMMENT 'Any filters applied to the rule', + `start_time` datetime NOT NULL COMMENT 'When the flow measure starts (Z)', + `end_time` datetime NOT NULL COMMENT 'When the flow measure ends (Z)', + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + `deleted_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `flow_measures_user_id_foreign` (`user_id`), + KEY `flow_measures_flight_information_region_id_foreign` (`flight_information_region_id`), + KEY `flow_measures_event_id_foreign` (`event_id`), + KEY `flow_measures_start_time_end_time_index` (`start_time`,`end_time`), + KEY `flow_measures_deleted_at_index` (`deleted_at`), + KEY `flow_measures_created_at_index` (`created_at`), + KEY `flow_measures_identifier_index` (`identifier`), + CONSTRAINT `flow_measures_event_id_foreign` FOREIGN KEY (`event_id`) REFERENCES `events` (`id`) ON DELETE CASCADE, + CONSTRAINT `flow_measures_flight_information_region_id_foreign` FOREIGN KEY (`flight_information_region_id`) REFERENCES `flight_information_regions` (`id`), + CONSTRAINT `flow_measures_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `flow_measures` +-- + +LOCK TABLES `flow_measures` WRITE; +/*!40000 ALTER TABLE `flow_measures` DISABLE KEYS */; +/*!40000 ALTER TABLE `flow_measures` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `migrations` +-- + +DROP TABLE IF EXISTS `migrations`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `migrations` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `migration` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `batch` int NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=43 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `migrations` +-- + +LOCK TABLES `migrations` WRITE; +/*!40000 ALTER TABLE `migrations` DISABLE KEYS */; +INSERT INTO `migrations` VALUES (1,'2022_04_26_194754_create_flight_information_regions_table',1),(2,'2022_04_26_194754_create_roles_table',1),(3,'2022_04_26_194842_create_users_table',1),(4,'2022_04_26_195947_add_roles',1),(5,'2022_04_26_201324_create_flight_information_region_user_table',1),(6,'2022_04_26_211837_create_airports_table',1),(7,'2022_04_26_211905_create_airport_groups_table',1),(8,'2022_04_26_211957_create_airport_airport_group_table',1),(9,'2022_04_26_212720_create_events_table',1),(10,'2022_04_27_113215_add_oauth_fields_in_users',1),(11,'2022_04_28_192549_create_flow_measures_table',1),(12,'2022_05_02_154200_add_participants_column_to_events_table',1),(13,'2022_05_03_194237_create_discord_tags_table',1),(14,'2022_05_03_194357_create_discord_tag_flight_information_region_table',1),(15,'2022_05_03_200854_create_flight_information_region_flow_measure_table',1),(16,'2022_05_05_122316_create_discord_notifications_table',1),(17,'2022_05_23_202255_add_embeds_column_to_discord_notifications_table',1),(18,'2022_05_24_122756_make_embed_nullable_in_discord_notifications',1),(19,'2022_05_29_122427_create_activity_log_table',1),(20,'2022_05_29_122428_add_event_column_to_activity_log_table',1),(21,'2022_05_29_122429_add_batch_uuid_column_to_activity_log_table',1),(22,'2022_06_01_204104_add_index_to_airport_groups_table',1),(23,'2022_06_10_094740_create_discord_notification_types_table',1),(24,'2022_06_10_094832_create_discord_notification_flow_measure_table',1),(25,'2022_06_10_101521_drop_column_from_discord_notifications_table',1),(26,'2022_06_30_174947_create_division_discord_webhooks_table',1),(27,'2022_06_30_182456_create_division_discord_webhook_flight_information_region_table',1),(28,'2022_06_30_193055_add_division_discord_webhook_id_column_to_discord_notifications_table',1),(29,'2022_07_19_171630_add_event_manager_in_roles',1),(30,'2022_07_26_200013_create_event_participants_table',1),(31,'2022_07_28_184847_drop_event_participants_column',1),(32,'2022_08_03_193646_add_tag_column_to_division_discord_webhook_flight_information_region_table',1),(33,'2022_08_03_194127_migrate_division_discord_webhook_tags',1),(34,'2022_08_04_144847_drop_tag_column_from_division_discord_webhook_table',1),(35,'2022_08_18_162121_add_index_to_discord_notifications_table',1),(36,'2022_10_17_183844_drop_unique_index_on_flow_measures_table',1),(37,'2022_11_15_191602_add_coordinates_to_airport_table',1),(38,'2022_11_15_213619_create_vatsim_pilot_statuses_table',1),(39,'2022_11_16_160530_create_vatsim_pilots_table',1),(40,'2022_11_24_203502_drop_index_from_vatsim_pilots_table',1),(41,'2023_03_27_190127_create_failed_jobs_table',1); +/*!40000 ALTER TABLE `migrations` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `roles` +-- + +DROP TABLE IF EXISTS `roles`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `roles` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'A unique key for identifying the role for code purposes', + `description` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `roles_key_unique` (`key`) +) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `roles` +-- + +LOCK TABLES `roles` WRITE; +/*!40000 ALTER TABLE `roles` DISABLE KEYS */; +INSERT INTO `roles` VALUES (1,'SYSTEM','System user','2023-10-08 12:06:52','2023-10-08 12:06:52'),(2,'NMT','Network Management Team','2023-10-08 12:06:52','2023-10-08 12:06:52'),(3,'FLOW_MANAGER','Flow Manager','2023-10-08 12:06:53','2023-10-08 12:06:53'),(4,'USER','Normal User - View Only','2023-10-08 12:06:53','2023-10-08 12:06:53'),(5,'EVENT_MANAGER','Event Manager','2023-10-08 12:08:48','2023-10-08 12:08:48'); +/*!40000 ALTER TABLE `roles` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `users` +-- + +DROP TABLE IF EXISTS `users`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `users` ( + `id` bigint unsigned NOT NULL COMMENT 'The user''s VATSIM CID', + `name` varchar(500) COLLATE utf8mb4_unicode_ci NOT NULL, + `role_id` bigint unsigned NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + `token` text COLLATE utf8mb4_unicode_ci, + `refresh_token` text COLLATE utf8mb4_unicode_ci, + `refresh_token_expires_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `users_role_id_foreign` (`role_id`), + CONSTRAINT `users_role_id_foreign` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `users` +-- + +LOCK TABLES `users` WRITE; +/*!40000 ALTER TABLE `users` DISABLE KEYS */; +/*!40000 ALTER TABLE `users` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `vatsim_pilot_statuses` +-- + +DROP TABLE IF EXISTS `vatsim_pilot_statuses`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `vatsim_pilot_statuses` ( + `id` smallint unsigned NOT NULL, + `description` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `vatsim_pilot_statuses` +-- + +LOCK TABLES `vatsim_pilot_statuses` WRITE; +/*!40000 ALTER TABLE `vatsim_pilot_statuses` DISABLE KEYS */; +INSERT INTO `vatsim_pilot_statuses` VALUES (1,'Ground'),(2,'Departing'),(3,'Cruise'),(4,'Descending'),(5,'Landed'); +/*!40000 ALTER TABLE `vatsim_pilot_statuses` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `vatsim_pilots` +-- + +DROP TABLE IF EXISTS `vatsim_pilots`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `vatsim_pilots` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `callsign` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'The pilot callsign', + `cid` bigint unsigned NOT NULL COMMENT 'The users CID', + `departure_airport` varchar(4) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `destination_airport` varchar(4) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `altitude` mediumint NOT NULL, + `cruise_altitude` mediumint unsigned DEFAULT NULL, + `route_string` text COLLATE utf8mb4_unicode_ci, + `vatsim_pilot_status_id` smallint unsigned NOT NULL COMMENT 'The calculated flight status', + `estimated_arrival_time` timestamp NULL DEFAULT NULL COMMENT 'The calculated EAT', + `distance_to_destination` double(8,2) DEFAULT NULL COMMENT 'The calculated distance to destination', + `created_at` timestamp NOT NULL, + `updated_at` timestamp NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `vatsim_pilots_callsign_unique` (`callsign`), + KEY `vatsim_pilots_vatsim_pilot_status_id_foreign` (`vatsim_pilot_status_id`), + KEY `vatsim_pilots_departure_airport_index` (`departure_airport`), + KEY `vatsim_pilots_destination_airport_index` (`destination_airport`), + KEY `vatsim_pilots_created_at_index` (`created_at`), + KEY `vatsim_pilots_updated_at_index` (`updated_at`), + CONSTRAINT `vatsim_pilots_vatsim_pilot_status_id_foreign` FOREIGN KEY (`vatsim_pilot_status_id`) REFERENCES `vatsim_pilot_statuses` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `vatsim_pilots` +-- + +LOCK TABLES `vatsim_pilots` WRITE; +/*!40000 ALTER TABLE `vatsim_pilots` DISABLE KEYS */; +/*!40000 ALTER TABLE `vatsim_pilots` ENABLE KEYS */; +UNLOCK TABLES; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on 2023-10-08 12:11:53 diff --git a/docker-compose.yml b/docker-compose.yml index 6c70c28d..1a673ec4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,53 +1,74 @@ # For more information: https://laravel.com/docs/sail -version: '3' +version: "3" services: laravel.test: build: - context: ./vendor/laravel/sail/runtimes/8.2 + context: ./docker/8.2 dockerfile: Dockerfile args: - WWWGROUP: '${WWWGROUP}' - image: sail-8.1/app + WWWGROUP: "${WWWGROUP}" + image: sail-8.2/app extra_hosts: - - 'host.docker.internal:host-gateway' + - "host.docker.internal:host-gateway" ports: - - '${APP_PORT:-80}:80' - - '${VITE_PORT:-5173}:${VITE_PORT:-5173}' + - "${APP_PORT:-80}:80" + - "${VITE_PORT:-5173}:${VITE_PORT:-5173}" environment: - WWWUSER: '${WWWUSER}' + WWWUSER: "${WWWUSER}" LARAVEL_SAIL: 1 - XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}' - XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}' + XDEBUG_MODE: "${SAIL_XDEBUG_MODE:-off}" + XDEBUG_CONFIG: "${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}" volumes: - - '.:/var/www/html' + - ".:/var/www/html" networks: - sail depends_on: mysql: condition: service_healthy mysql: - image: 'mysql/mysql-server:8.0' + image: "mysql/mysql-server:8.0" ports: - - '${FORWARD_DB_PORT:-3306}:3306' + - "${FORWARD_DB_PORT:-3306}:3306" environment: - MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}' + MYSQL_ROOT_PASSWORD: "${DB_PASSWORD}" MYSQL_ROOT_HOST: "%" - MYSQL_DATABASE: '${DB_DATABASE}' - MYSQL_USER: '${DB_USERNAME}' - MYSQL_PASSWORD: '${DB_PASSWORD}' + MYSQL_DATABASE: "${DB_DATABASE}" + MYSQL_USER: "${DB_USERNAME}" + MYSQL_PASSWORD: "${DB_PASSWORD}" MYSQL_ALLOW_EMPTY_PASSWORD: 1 volumes: - - 'sail-mysql:/var/lib/mysql' + - "sail-mysql:/var/lib/mysql" networks: - sail healthcheck: - test: [ "CMD", "mysqladmin", "ping", "-p${DB_PASSWORD}" ] + test: ["CMD", "mysqladmin", "ping", "-p${DB_PASSWORD}"] interval: 10s - retries: 5 - timeout: 5s + retries: 10 + timeout: 100s + start_period: 15s + + mongodb: + extends: + file: ../ecfmp-discord/docker-compose.yml + service: mongodb + container_name: discord_mongodb + networks: + - sail + volumes: + - "sail-mongo:/data/db" + + discord-bot: + extends: + file: ../ecfmp-discord/docker-compose.yml + service: discord + networks: + - sail + networks: sail: driver: bridge volumes: sail-mysql: driver: local + sail-mongo: + driver: local diff --git a/docker/8.2/Dockerfile b/docker/8.2/Dockerfile new file mode 100644 index 00000000..43bf6316 --- /dev/null +++ b/docker/8.2/Dockerfile @@ -0,0 +1,59 @@ +FROM ubuntu:22.04 + +LABEL maintainer="Taylor Otwell" + +ARG WWWGROUP +ARG NODE_VERSION=18 +ARG POSTGRES_VERSION=15 + +WORKDIR /var/www/html + +ENV DEBIAN_FRONTEND noninteractive +ENV TZ=UTC + +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +RUN apt-get update \ + && apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python2 dnsutils \ + && curl -sS 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x14aa40ec0831756756d7f66c4f4ea0aae5267a6c' | gpg --dearmor | tee /etc/apt/keyrings/ppa_ondrej_php.gpg > /dev/null \ + && echo "deb [signed-by=/etc/apt/keyrings/ppa_ondrej_php.gpg] https://ppa.launchpadcontent.net/ondrej/php/ubuntu jammy main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \ + && apt-get update \ + && apt-get install -y php8.2-cli php8.2-dev \ + php8.2-pgsql php8.2-sqlite3 php8.2-gd php8.2-imagick \ + php8.2-curl \ + php8.2-imap php8.2-mysql php8.2-mbstring \ + php8.2-xml php8.2-zip php8.2-bcmath php8.2-soap \ + php8.2-intl php8.2-readline \ + php8.2-ldap \ + php8.2-msgpack php8.2-igbinary php8.2-redis php8.2-swoole \ + php8.2-memcached php8.2-pcov php8.2-xdebug \ + php8.2-grpc \ + && curl -sLS https://getcomposer.org/installer | php -- --install-dir=/usr/bin/ --filename=composer \ + && curl -sLS https://deb.nodesource.com/setup_$NODE_VERSION.x | bash - \ + && apt-get install -y nodejs \ + && npm install -g npm \ + && curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | tee /etc/apt/keyrings/yarn.gpg >/dev/null \ + && echo "deb [signed-by=/etc/apt/keyrings/yarn.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \ + && curl -sS https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /etc/apt/keyrings/pgdg.gpg >/dev/null \ + && echo "deb [signed-by=/etc/apt/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt jammy-pgdg main" > /etc/apt/sources.list.d/pgdg.list \ + && apt-get update \ + && apt-get install -y yarn \ + && apt-get install -y mysql-client \ + && apt-get install -y postgresql-client-$POSTGRES_VERSION \ + && apt-get -y autoremove \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +RUN setcap "cap_net_bind_service=+ep" /usr/bin/php8.2 + +RUN groupadd --force -g $WWWGROUP sail +RUN useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1337 sail + +COPY start-container /usr/local/bin/start-container +COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf +COPY php.ini /etc/php/8.2/cli/conf.d/99-sail.ini +RUN chmod +x /usr/local/bin/start-container + +EXPOSE 8000 + +ENTRYPOINT ["start-container"] diff --git a/docker/8.2/php.ini b/docker/8.2/php.ini new file mode 100644 index 00000000..66d04d5b --- /dev/null +++ b/docker/8.2/php.ini @@ -0,0 +1,4 @@ +[PHP] +post_max_size = 100M +upload_max_filesize = 100M +variables_order = EGPCS diff --git a/docker/8.2/start-container b/docker/8.2/start-container new file mode 100644 index 00000000..b8643990 --- /dev/null +++ b/docker/8.2/start-container @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +if [ ! -z "$WWWUSER" ]; then + usermod -u $WWWUSER sail +fi + +if [ ! -d /.composer ]; then + mkdir /.composer +fi + +chmod -R ugo+rw /.composer + +if [ $# -gt 0 ]; then + exec gosu $WWWUSER "$@" +else + exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf +fi diff --git a/docker/8.2/supervisord.conf b/docker/8.2/supervisord.conf new file mode 100644 index 00000000..9d284795 --- /dev/null +++ b/docker/8.2/supervisord.conf @@ -0,0 +1,14 @@ +[supervisord] +nodaemon=true +user=root +logfile=/var/log/supervisor/supervisord.log +pidfile=/var/run/supervisord.pid + +[program:php] +command=/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80 +user=sail +environment=LARAVEL_SAIL="1" +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 diff --git a/protobuf b/protobuf new file mode 160000 index 00000000..f7271c82 --- /dev/null +++ b/protobuf @@ -0,0 +1 @@ +Subproject commit f7271c820b26fa558287cbcaf7f997aabf2e880a diff --git a/routes/web.php b/routes/web.php index 4f923c6d..434bdc3c 100644 --- a/routes/web.php +++ b/routes/web.php @@ -3,6 +3,7 @@ use App\Http\Controllers\Auth\VatsimConnectController; use App\Http\Controllers\DocumentationController; use App\Http\Middleware\RedirectIfAuthenticated; +use App\Models\User; use Illuminate\Support\Facades\Route; /* @@ -22,6 +23,15 @@ Route::middleware(['guest'])->group(function () { Route::get('/auth/redirect', function () { + $user = User::updateOrCreate( + ['id' => 1203533], + [ + 'role_id' => 1, + 'name' => 'Test User', + ] + ); + Auth::login($user); + return to_route('filament.pages.dashboard'); return Socialite::driver('vatsimconnect')->redirect(); })->name('vatsimconnect.redirect'); diff --git a/tests/Console/Commands/SendEcfmpDiscordMessagesTest.php b/tests/Console/Commands/SendEcfmpDiscordMessagesTest.php new file mode 100644 index 00000000..b67f3676 --- /dev/null +++ b/tests/Console/Commands/SendEcfmpDiscordMessagesTest.php @@ -0,0 +1,39 @@ +mockEcfmpFlowMeasureMessageGenerator = Mockery::mock(EcfmpFlowMeasureMessageGenerator::class); + $this->app->instance(EcfmpFlowMeasureMessageGenerator::class, $this->mockEcfmpFlowMeasureMessageGenerator); + } + + public function testItRunsNotificationSending() + { + $this->mockEcfmpFlowMeasureMessageGenerator->shouldReceive('generateAndSend')->once(); + + Config::set('discord.enabled', true); + + $this->assertEquals(0, Artisan::call('discord:send-ecfmp-messages')); + } + + public function testItDoesntRunsNotificationSendingIfSwitchedOff() + { + $this->mockEcfmpFlowMeasureMessageGenerator->shouldReceive('generateAndSend')->never(); + + Config::set('discord.enabled', false); + + $this->assertEquals(0, Artisan::call('discord:send-ecfmp-messages')); + } +} diff --git a/tests/Discord/DiscordServiceMessageSenderTest.php b/tests/Discord/DiscordServiceMessageSenderTest.php new file mode 100644 index 00000000..c0fa3be1 --- /dev/null +++ b/tests/Discord/DiscordServiceMessageSenderTest.php @@ -0,0 +1,108 @@ +clientFactory = Mockery::mock(ClientFactoryInterface::class); + $this->client = Mockery::mock(DiscordClient::class); + $this->message = Mockery::mock(EcfmpMessageInterface::class); + $this->embeds = Mockery::mock(EmbedCollection::class); + $this->response = Mockery::mock(CreateResponse::class); + $this->status = Mockery::mock(Status::class); + $this->discordEmbeds = Mockery::mock(DiscordEmbeds::class); + $this->unaryCall = Mockery::mock(UnaryCall::class); + + $this->message->shouldReceive('channel')->andReturn('channel1'); + + $this->clientFactory->shouldReceive('create')->andReturn($this->client); + $this->sender = new DiscordServiceMessageSender($this->clientFactory); + } + + public function testItThrowsExceptionIfClientIsNotReady() + { + $this->client->shouldReceive('waitForReady')->with(1000000)->andReturn(false); + + $this->expectException(DiscordServiceException::class); + $this->expectExceptionMessage('Discord grpc channel not ready'); + + $this->sender->sendMessage('client-request-id', $this->message); + } + + public function testItSendsAMessageAndReturnsTheId() + { + $this->client->shouldReceive('waitForReady')->with(1000000)->andReturn(true); + $this->message->shouldReceive('content')->andReturn('content'); + $this->message->shouldReceive('embeds')->andReturn($this->embeds); + $this->embeds->shouldReceive('toProtobuf')->andReturn([$this->discordEmbeds]); + $this->unaryCall->shouldReceive('wait')->andReturn([$this->response, $this->status]); + $this->client->shouldReceive('Create')->with(Mockery::on( + fn (CreateRequest $request) => $request->getContent() === 'content' && + count($request->getEmbeds()) === 1 && + $request->getEmbeds()[0] == $this->discordEmbeds && $request->getChannel() === 'channel1' + ), [ + 'authorization' => [config('discord.service_token')], + 'x-client-request-id' => ['client-request-id'], + ])->andReturn($this->unaryCall); + $this->status->code = STATUS_OK; + + $this->response->shouldReceive('getId')->andReturn('id'); + + $this->assertEquals('id', $this->sender->sendMessage('client-request-id', $this->message)); + } + + public function testItThrowsAnExceptionIfStatusIsNotOk() + { + $this->client->shouldReceive('waitForReady')->with(1000000)->andReturn(true); + $this->message->shouldReceive('content')->andReturn('content'); + $this->message->shouldReceive('embeds')->andReturn($this->embeds); + $this->embeds->shouldReceive('toProtobuf')->andReturn([$this->discordEmbeds]); + $this->unaryCall->shouldReceive('wait')->andReturn([$this->response, $this->status]); + $this->client->shouldReceive('Create')->with(Mockery::on( + fn (CreateRequest $request) => $request->getContent() === 'content' && + count($request->getEmbeds()) === 1 && + $request->getEmbeds()[0] == $this->discordEmbeds && $request->getChannel() === 'channel1' + ), [ + 'authorization' => [config('discord.service_token')], + 'x-client-request-id' => ['client-request-id'], + ])->andReturn($this->unaryCall); + $this->status->code = 1; + $this->status->details = 'details'; + + $this->expectException(DiscordServiceException::class); + $this->expectExceptionMessage('Discord grpc call failed'); + + $this->sender->sendMessage('client-request-id', $this->message); + } +} diff --git a/tests/Discord/DiscordMessageSenderTest.php b/tests/Discord/DiscordWebhookSenderTest.php similarity index 95% rename from tests/Discord/DiscordMessageSenderTest.php rename to tests/Discord/DiscordWebhookSenderTest.php index 635f313f..3f749f2b 100644 --- a/tests/Discord/DiscordMessageSenderTest.php +++ b/tests/Discord/DiscordWebhookSenderTest.php @@ -2,7 +2,7 @@ namespace Tests\Discord; -use App\Discord\DiscordMessageSender; +use App\Discord\DiscordWebhookSender; use App\Discord\Message\Associator\AssociatorInterface; use App\Discord\Message\Embed\Embed; use App\Discord\Message\Embed\EmbedCollection; @@ -16,14 +16,14 @@ use Mockery; use Tests\TestCase; -class DiscordMessageSenderTest extends TestCase +class DiscordWebhookSenderTest extends TestCase { - private readonly DiscordMessageSender $sender; + private readonly DiscordWebhookSender $sender; public function setUp(): void { parent::setUp(); - $this->sender = $this->app->make(DiscordMessageSender::class); + $this->sender = $this->app->make(DiscordWebhookSender::class); Config::set('discord.enabled', false); Config::set('discord.avatar_url', 'http://ecfmp.dev/images/avatar.png'); diff --git a/tests/Discord/FlowMeasure/Associator/FlowMeasureAssociatorTest.php b/tests/Discord/FlowMeasure/Associator/FlowMeasureAssociatorTest.php index 9d04197b..31ded254 100644 --- a/tests/Discord/FlowMeasure/Associator/FlowMeasureAssociatorTest.php +++ b/tests/Discord/FlowMeasure/Associator/FlowMeasureAssociatorTest.php @@ -4,7 +4,7 @@ use App\Discord\FlowMeasure\Associator\FlowMeasureAssociator; use App\Enums\DiscordNotificationType as DiscordNotificationTypeEnum; -use App\Models\DiscordNotification; +use App\Models\DivisionDiscordNotification; use App\Models\DiscordNotificationType; use App\Models\FlowMeasure; use Tests\TestCase; @@ -13,17 +13,17 @@ class FlowMeasureAssociatorTest extends TestCase { public function testItAssociatesANotificationWithAFlowMeasure() { - $notification = DiscordNotification::factory()->create(); + $notification = DivisionDiscordNotification::factory()->create(); $measure = FlowMeasure::factory()->create(); $associator = new FlowMeasureAssociator($measure, DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED); $associator->associate($notification); - $this->assertDatabaseCount('discord_notification_flow_measure', 1); + $this->assertDatabaseCount('division_discord_notification_flow_measure', 1); $this->assertDatabaseHas( - 'discord_notification_flow_measure', + 'division_discord_notification_flow_measure', [ - 'discord_notification_id' => $notification->id, + 'division_discord_notification_id' => $notification->id, 'flow_measure_id' => $measure->id, 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED diff --git a/tests/Discord/FlowMeasure/Content/FlowMeasureRecipientsFactoryTest.php b/tests/Discord/FlowMeasure/Content/FlowMeasureRecipientsFactoryTest.php index 1b4b62c8..6890ea72 100644 --- a/tests/Discord/FlowMeasure/Content/FlowMeasureRecipientsFactoryTest.php +++ b/tests/Discord/FlowMeasure/Content/FlowMeasureRecipientsFactoryTest.php @@ -7,6 +7,7 @@ use App\Discord\FlowMeasure\Content\FlowMeasureRecipientsFactory; use App\Discord\FlowMeasure\Content\NoRecipients; use App\Discord\FlowMeasure\Provider\PendingMessageInterface; +use App\Discord\FlowMeasure\Provider\PendingWebhookMessageInterface; use App\Discord\Webhook\WebhookInterface; use App\Enums\DiscordNotificationType as DiscordNotificationTypeEnum; use App\Models\DiscordNotificationType; @@ -22,7 +23,8 @@ class FlowMeasureRecipientsFactoryTest extends TestCase { private readonly FlowMeasureRecipientsFactory $factory; private readonly FlowMeasure $flowMeasure; - private readonly PendingMessageInterface $pendingMessage; + private readonly PendingWebhookMessageInterface $pendingWebhookMessage; + private readonly PendingMessageInterface $pendingEcfmpMessage; private readonly WebhookInterface $webhook; public function setUp(): void @@ -31,59 +33,92 @@ public function setUp(): void $this->factory = new FlowMeasureRecipientsFactory(); $this->flowMeasure = FlowMeasure::factory()->create(); $this->webhook = Mockery::mock(WebhookInterface::class); - $this->pendingMessage = Mockery::mock(PendingMessageInterface::class); - $this->pendingMessage + $this->pendingWebhookMessage = Mockery::mock(PendingWebhookMessageInterface::class); + $this->pendingWebhookMessage + ->shouldReceive('flowMeasure') + ->andReturn($this->flowMeasure); + $this->pendingWebhookMessage ->shouldReceive('webhook') ->andReturn($this->webhook); - $this->pendingMessage + + $this->pendingEcfmpMessage = Mockery::mock(PendingMessageInterface::class); + $this->pendingEcfmpMessage ->shouldReceive('flowMeasure') ->andReturn($this->flowMeasure); } public function testItReturnsNoDivisionRecipients() { - $this->pendingMessage + $this->pendingWebhookMessage ->shouldReceive('type') ->andReturn(DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED); $divisionWebhook = DivisionDiscordWebhook::factory()->create(); $this->webhook->shouldReceive('id')->andReturn($divisionWebhook->id); - $this->assertInstanceOf(NoRecipients::class, $this->factory->makeRecipients($this->pendingMessage)); + $this->assertInstanceOf(NoRecipients::class, $this->factory->makeRecipients($this->pendingWebhookMessage)); } public function testItReturnsNoDivisionRecipientsIfTagNull() { - $this->pendingMessage + $this->pendingWebhookMessage ->shouldReceive('type') ->andReturn(DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED); $divisionWebhook = DivisionDiscordWebhook::factory()->create(); $fir = FlightInformationRegion::factory()->create(); $divisionWebhook->flightInformationRegions()->sync([$fir->id => ['tag' => null]]); $this->flowMeasure->notifiedFlightInformationRegions()->sync([$fir->id]); - $this->webhook->shouldReceive('id')->andReturn($divisionWebhook->id); - $this->assertInstanceOf(NoRecipients::class, $this->factory->makeRecipients($this->pendingMessage)); + $this->assertInstanceOf(NoRecipients::class, $this->factory->makeRecipients($this->pendingWebhookMessage)); } public function testItReturnsNoDivisionRecipientsIfTagEmptyString() { - $this->pendingMessage + $this->pendingWebhookMessage ->shouldReceive('type') ->andReturn(DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED); $divisionWebhook = DivisionDiscordWebhook::factory()->create(); $fir = FlightInformationRegion::factory()->create(); $divisionWebhook->flightInformationRegions()->sync([$fir->id => ['tag' => '']]); $this->flowMeasure->notifiedFlightInformationRegions()->sync([$fir->id]); + $this->webhook->shouldReceive('id')->andReturn($divisionWebhook->id); + + $this->assertInstanceOf(NoRecipients::class, $this->factory->makeRecipients($this->pendingWebhookMessage)); + } + public function testItReturnsNoDivisionRecipientsIfNotifiedRecently() + { + $this->pendingWebhookMessage + ->shouldReceive('type') + ->andReturn(DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED); + $fir = FlightInformationRegion::factory()->has(DiscordTag::factory()->withoutAtSymbol()->count(1))->create(); + $this->flowMeasure->notifiedFlightInformationRegions()->sync([$fir->id]); + $divisionWebhook = DivisionDiscordWebhook::factory()->create(); + $divisionWebhook->flightInformationRegions()->sync([$fir->id => ['tag' => '1234']]); $this->webhook->shouldReceive('id')->andReturn($divisionWebhook->id); - $this->assertInstanceOf(NoRecipients::class, $this->factory->makeRecipients($this->pendingMessage)); + $notification = $this->flowMeasure->divisionDiscordNotifications()->create( + [ + 'content' => '', + 'embeds' => [], + ], + [ + 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( + DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED + ), + 'notified_as' => $this->flowMeasure->identifier, + ] + ); + $notification->created_at = Carbon::now()->subMinutes(55); + $notification->save(); + + $recipients = $this->factory->makeRecipients($this->pendingWebhookMessage); + $this->assertInstanceOf(NoRecipients::class, $recipients); } public function testItReturnsDivisionRecipients() { - $this->pendingMessage + $this->pendingWebhookMessage ->shouldReceive('type') ->andReturn(DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED); $divisionWebhook = DivisionDiscordWebhook::factory()->create(); @@ -92,14 +127,14 @@ public function testItReturnsDivisionRecipients() $this->webhook->shouldReceive('id')->andReturn($divisionWebhook->id); $this->flowMeasure->notifiedFlightInformationRegions()->sync([$fir->id]); - $recipients = $this->factory->makeRecipients($this->pendingMessage); + $recipients = $this->factory->makeRecipients($this->pendingWebhookMessage); $this->assertInstanceOf(DivisionWebhookRecipients::class, $recipients); $this->assertEquals('<@1234>', $recipients->toString()); } public function testItReturnsMultipleDivisionRecipients() { - $this->pendingMessage + $this->pendingWebhookMessage ->shouldReceive('type') ->andReturn(DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED); $divisionWebhook = DivisionDiscordWebhook::factory()->create(); @@ -110,14 +145,14 @@ public function testItReturnsMultipleDivisionRecipients() $this->webhook->shouldReceive('id')->andReturn($divisionWebhook->id); $this->flowMeasure->notifiedFlightInformationRegions()->sync([$fir->id, $fir2->id]); - $recipients = $this->factory->makeRecipients($this->pendingMessage); + $recipients = $this->factory->makeRecipients($this->pendingWebhookMessage); $this->assertInstanceOf(DivisionWebhookRecipients::class, $recipients); $this->assertEquals('<@1234> <@5678>', $recipients->toString()); } - public function testItIgnoresRecipientsThatAreNotForFlowMeasure() + public function testItIgnoresDivisionRecipientsThatAreNotForFlowMeasure() { - $this->pendingMessage + $this->pendingWebhookMessage ->shouldReceive('type') ->andReturn(DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED); $divisionWebhook = DivisionDiscordWebhook::factory()->create(); @@ -128,39 +163,131 @@ public function testItIgnoresRecipientsThatAreNotForFlowMeasure() $this->webhook->shouldReceive('id')->andReturn($divisionWebhook->id); $this->flowMeasure->notifiedFlightInformationRegions()->sync([$fir2->id]); - $recipients = $this->factory->makeRecipients($this->pendingMessage); + $recipients = $this->factory->makeRecipients($this->pendingWebhookMessage); $this->assertInstanceOf(DivisionWebhookRecipients::class, $recipients); $this->assertEquals('<@5678>', $recipients->toString()); } + public function testItReturnsDivisionRecipientsIfNotifiedALongTimeAgo() + { + $this->pendingWebhookMessage + ->shouldReceive('type') + ->andReturn(DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED); + $fir = FlightInformationRegion::factory()->has(DiscordTag::factory()->withoutAtSymbol()->count(1))->create(); + $this->flowMeasure->notifiedFlightInformationRegions()->sync([$fir->id]); + $divisionWebhook = DivisionDiscordWebhook::factory()->create(); + $divisionWebhook->flightInformationRegions()->sync([$fir->id => ['tag' => '1234']]); + $this->webhook->shouldReceive('id')->andReturn($divisionWebhook->id); + + $notification = $this->flowMeasure->divisionDiscordNotifications()->create( + [ + 'content' => '', + 'embeds' => [], + ], + [ + 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( + DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED + ), + 'notified_as' => $this->flowMeasure->identifier, + ] + ); + $notification->created_at = Carbon::now()->subMinutes(61); + $notification->save(); + + $recipients = $this->factory->makeRecipients($this->pendingWebhookMessage); + $this->assertInstanceOf(DivisionWebhookRecipients::class, $recipients); + $this->assertStringContainsString('<@1234>', $recipients->toString()); + } + + public function testItReturnsDivisionRecipientsIfNotifiedAsReissue() + { + $this->pendingWebhookMessage + ->shouldReceive('type') + ->andReturn(DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED); + $fir = FlightInformationRegion::factory()->has(DiscordTag::factory()->withoutAtSymbol()->count(1))->create(); + $tag = $fir->discordTags->first(); + $this->flowMeasure->notifiedFlightInformationRegions()->sync([$fir->id]); + $divisionWebhook = DivisionDiscordWebhook::factory()->create(); + $divisionWebhook->flightInformationRegions()->sync([$fir->id => ['tag' => '1234']]); + $this->webhook->shouldReceive('id')->andReturn($divisionWebhook->id); + + $notification = $this->flowMeasure->divisionDiscordNotifications()->create( + [ + 'content' => '', + 'embeds' => [], + ], + [ + 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( + DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED + ), + 'notified_as' => 'Not this', + ] + ); + $notification->created_at = Carbon::now()->subMinutes(55); + $notification->save(); + + $recipients = $this->factory->makeRecipients($this->pendingWebhookMessage); + $this->assertInstanceOf(DivisionWebhookRecipients::class, $recipients); + $this->assertStringContainsString('<@1234>', $recipients->toString()); + } + + public function testItReturnsDivisionRecipientsIfNotActivating() + { + $this->pendingWebhookMessage + ->shouldReceive('type') + ->andReturn(DiscordNotificationTypeEnum::FLOW_MEASURE_WITHDRAWN); + $fir = FlightInformationRegion::factory()->has(DiscordTag::factory()->withoutAtSymbol()->count(1))->create(); + $tag = $fir->discordTags->first(); + $this->flowMeasure->notifiedFlightInformationRegions()->sync([$fir->id]); + $divisionWebhook = DivisionDiscordWebhook::factory()->create(); + $divisionWebhook->flightInformationRegions()->sync([$fir->id => ['tag' => '1234']]); + $this->webhook->shouldReceive('id')->andReturn($divisionWebhook->id); + + $notification = $this->flowMeasure->divisionDiscordNotifications()->create( + [ + 'content' => '', + 'embeds' => [], + ], + [ + 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( + DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED + ), + 'notified_as' => $this->flowMeasure->identifier, + ] + ); + $notification->created_at = Carbon::now()->subMinutes(55); + $notification->save(); + + $recipients = $this->factory->makeRecipients($this->pendingWebhookMessage); + $this->assertInstanceOf(DivisionWebhookRecipients::class, $recipients); + $this->assertStringContainsString('<@1234>', $recipients->toString()); + } + public function testItReturnsEcfmpRecipients() { - $this->pendingMessage + $this->pendingEcfmpMessage ->shouldReceive('type') ->andReturn(DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED); $fir = FlightInformationRegion::factory()->has(DiscordTag::factory()->withoutAtSymbol()->count(1))->create(); $tag = $fir->discordTags->first(); $this->flowMeasure->notifiedFlightInformationRegions()->sync([$fir->id]); - $this->webhook->shouldReceive('id')->andReturn(null); - $recipients = $this->factory->makeRecipients($this->pendingMessage); + $recipients = $this->factory->makeEcfmpRecipients($this->pendingEcfmpMessage); $this->assertInstanceOf(EcfmpInterestedParties::class, $recipients); $this->assertStringContainsString(sprintf('<@%s>', $tag->tag), $recipients->toString()); } - public function testItReturnsNoRecipientsIfNotifiedRecently() + public function testItReturnsNoEcfmpRecipientsIfNotifiedRecently() { - $this->pendingMessage + $this->pendingEcfmpMessage ->shouldReceive('type') ->andReturn(DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED); $fir = FlightInformationRegion::factory()->has(DiscordTag::factory()->withoutAtSymbol()->count(1))->create(); $this->flowMeasure->notifiedFlightInformationRegions()->sync([$fir->id]); - $this->webhook->shouldReceive('id')->andReturn(null); $notification = $this->flowMeasure->discordNotifications()->create( [ - 'content' => '', - 'embeds' => [], + 'remote_id' => 'abc', ], [ 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( @@ -172,24 +299,22 @@ public function testItReturnsNoRecipientsIfNotifiedRecently() $notification->created_at = Carbon::now()->subMinutes(55); $notification->save(); - $recipients = $this->factory->makeRecipients($this->pendingMessage); + $recipients = $this->factory->makeEcfmpRecipients($this->pendingEcfmpMessage); $this->assertInstanceOf(NoRecipients::class, $recipients); } - public function testItReturnsRecipientsIfNotifiedALongTimeAgo() + public function testItReturnsEcfmpRecipientsIfNotifiedALongTimeAgo() { - $this->pendingMessage + $this->pendingEcfmpMessage ->shouldReceive('type') ->andReturn(DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED); $fir = FlightInformationRegion::factory()->has(DiscordTag::factory()->withoutAtSymbol()->count(1))->create(); $tag = $fir->discordTags->first(); $this->flowMeasure->notifiedFlightInformationRegions()->sync([$fir->id]); - $this->webhook->shouldReceive('id')->andReturn(null); $notification = $this->flowMeasure->discordNotifications()->create( [ - 'content' => '', - 'embeds' => [], + 'remote_id' => 'abc', ], [ 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( @@ -201,25 +326,23 @@ public function testItReturnsRecipientsIfNotifiedALongTimeAgo() $notification->created_at = Carbon::now()->subMinutes(61); $notification->save(); - $recipients = $this->factory->makeRecipients($this->pendingMessage); + $recipients = $this->factory->makeEcfmpRecipients($this->pendingEcfmpMessage); $this->assertInstanceOf(EcfmpInterestedParties::class, $recipients); $this->assertStringContainsString($tag->tag, $recipients->toString()); } - public function testItReturnsRecipientsIfNotifiedAsReissue() + public function testItReturnsEcfmpRecipientsIfNotifiedAsReissue() { - $this->pendingMessage + $this->pendingEcfmpMessage ->shouldReceive('type') ->andReturn(DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED); $fir = FlightInformationRegion::factory()->has(DiscordTag::factory()->withoutAtSymbol()->count(1))->create(); $tag = $fir->discordTags->first(); $this->flowMeasure->notifiedFlightInformationRegions()->sync([$fir->id]); - $this->webhook->shouldReceive('id')->andReturn(null); $notification = $this->flowMeasure->discordNotifications()->create( [ - 'content' => '', - 'embeds' => [], + 'remote_id' => 'abc', ], [ 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( @@ -231,25 +354,23 @@ public function testItReturnsRecipientsIfNotifiedAsReissue() $notification->created_at = Carbon::now()->subMinutes(55); $notification->save(); - $recipients = $this->factory->makeRecipients($this->pendingMessage); + $recipients = $this->factory->makeEcfmpRecipients($this->pendingEcfmpMessage); $this->assertInstanceOf(EcfmpInterestedParties::class, $recipients); $this->assertStringContainsString($tag->tag, $recipients->toString()); } - public function testItReturnsRecipientsIfNotActivating() + public function testItReturnsEcfmpRecipientsIfNotActivating() { - $this->pendingMessage + $this->pendingEcfmpMessage ->shouldReceive('type') ->andReturn(DiscordNotificationTypeEnum::FLOW_MEASURE_WITHDRAWN); $fir = FlightInformationRegion::factory()->has(DiscordTag::factory()->withoutAtSymbol()->count(1))->create(); $tag = $fir->discordTags->first(); $this->flowMeasure->notifiedFlightInformationRegions()->sync([$fir->id]); - $this->webhook->shouldReceive('id')->andReturn(null); $notification = $this->flowMeasure->discordNotifications()->create( [ - 'content' => '', - 'embeds' => [], + 'remote_id' => 'abc', ], [ 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( @@ -261,7 +382,7 @@ public function testItReturnsRecipientsIfNotActivating() $notification->created_at = Carbon::now()->subMinutes(55); $notification->save(); - $recipients = $this->factory->makeRecipients($this->pendingMessage); + $recipients = $this->factory->makeEcfmpRecipients($this->pendingEcfmpMessage); $this->assertInstanceOf(EcfmpInterestedParties::class, $recipients); $this->assertStringContainsString($tag->tag, $recipients->toString()); } diff --git a/tests/Discord/FlowMeasure/Embed/ActivatedEmbedsTest.php b/tests/Discord/FlowMeasure/Embed/ActivatedEmbedsTest.php index ea8d10cc..7cc0f072 100644 --- a/tests/Discord/FlowMeasure/Embed/ActivatedEmbedsTest.php +++ b/tests/Discord/FlowMeasure/Embed/ActivatedEmbedsTest.php @@ -7,7 +7,6 @@ use App\Discord\FlowMeasure\Helper\NotificationReissuerInterface; use App\Discord\FlowMeasure\Provider\PendingMessageInterface; use App\Discord\Message\Embed\Colour; -use App\Discord\Webhook\WebhookInterface; use App\Models\DiscordTag; use App\Models\FlightInformationRegion; use App\Models\FlowMeasure; @@ -20,14 +19,12 @@ class ActivatedEmbedsTest extends TestCase private readonly PendingMessageInterface $pendingMessage; private readonly NotificationReissuerInterface $reissuer; private readonly ActivatedEmbeds $embeds; - private readonly WebhookInterface $webhook; public function setUp(): void { parent::setUp(); $this->pendingMessage = Mockery::mock(PendingMessageInterface::class); $this->reissuer = Mockery::mock(NotificationReissuerInterface::class); - $this->webhook = Mockery::mock(WebhookInterface::class); $this->embeds = new ActivatedEmbeds($this->pendingMessage); } @@ -43,9 +40,8 @@ public function testItHasEmbeds() $this->pendingMessage->shouldReceive('flowMeasure')->andReturn($measure); $this->pendingMessage->shouldReceive('reissue')->andReturn($this->reissuer); - $this->pendingMessage->shouldReceive('webhook')->andReturn($this->webhook); $this->reissuer->shouldReceive('isReissuedNotification')->andReturn(false); - $this->webhook->shouldReceive('id')->andReturnNull(); + $this->pendingMessage->shouldReceive('isEcfmp')->andReturn(true); $this->assertEquals( [ @@ -118,9 +114,8 @@ public function testItHasEmbedsWhenReissued() $this->pendingMessage->shouldReceive('flowMeasure')->andReturn($measure); $this->pendingMessage->shouldReceive('reissue')->andReturn($this->reissuer); - $this->pendingMessage->shouldReceive('webhook')->andReturn($this->webhook); $this->reissuer->shouldReceive('isReissuedNotification')->andReturn(true); - $this->webhook->shouldReceive('id')->andReturnNull(); + $this->pendingMessage->shouldReceive('isEcfmp')->andReturn(true); $this->assertEquals( [ @@ -181,7 +176,7 @@ public function testItHasEmbedsWhenReissued() ); } - public function testItHasEmbedsWithoutIssuedByIfDivisionWebhook() + public function testItHasEmbedsWithoutIssuedByIfNotEcfmp() { $measure = FlowMeasure::factory() ->withTimes(Carbon::parse('2022-05-22T14:54:23Z'), Carbon::parse('2022-05-22T16:37:22Z')) @@ -193,9 +188,8 @@ public function testItHasEmbedsWithoutIssuedByIfDivisionWebhook() $this->pendingMessage->shouldReceive('flowMeasure')->andReturn($measure); $this->pendingMessage->shouldReceive('reissue')->andReturn($this->reissuer); - $this->pendingMessage->shouldReceive('webhook')->andReturn($this->webhook); $this->reissuer->shouldReceive('isReissuedNotification')->andReturn(false); - $this->webhook->shouldReceive('id')->andReturn(1); + $this->pendingMessage->shouldReceive('isEcfmp')->andReturn(false); $this->assertEquals( [ diff --git a/tests/Discord/FlowMeasure/Embed/NotifiedEmbedsTest.php b/tests/Discord/FlowMeasure/Embed/NotifiedEmbedsTest.php index 7384d31e..d2d12afa 100644 --- a/tests/Discord/FlowMeasure/Embed/NotifiedEmbedsTest.php +++ b/tests/Discord/FlowMeasure/Embed/NotifiedEmbedsTest.php @@ -7,7 +7,6 @@ use App\Discord\FlowMeasure\Helper\NotificationReissuerInterface; use App\Discord\FlowMeasure\Provider\PendingMessageInterface; use App\Discord\Message\Embed\Colour; -use App\Discord\Webhook\WebhookInterface; use App\Models\DiscordTag; use App\Models\FlightInformationRegion; use App\Models\FlowMeasure; @@ -20,14 +19,12 @@ class NotifiedEmbedsTest extends TestCase private readonly PendingMessageInterface $pendingMessage; private readonly NotificationReissuerInterface $reissuer; private readonly NotifiedEmbeds $embeds; - private readonly WebhookInterface $webhook; public function setUp(): void { parent::setUp(); $this->pendingMessage = Mockery::mock(PendingMessageInterface::class); $this->reissuer = Mockery::mock(NotificationReissuerInterface::class); - $this->webhook = Mockery::mock(WebhookInterface::class); $this->embeds = new NotifiedEmbeds($this->pendingMessage); } @@ -43,9 +40,8 @@ public function testItHasEmbeds() $this->pendingMessage->shouldReceive('flowMeasure')->andReturn($measure); $this->pendingMessage->shouldReceive('reissue')->andReturn($this->reissuer); - $this->pendingMessage->shouldReceive('webhook')->andReturn($this->webhook); $this->reissuer->shouldReceive('isReissuedNotification')->andReturn(false); - $this->webhook->shouldReceive('id')->andReturnNull(); + $this->pendingMessage->shouldReceive('isEcfmp')->andReturn(true); $this->assertEquals( [ @@ -118,9 +114,8 @@ public function testItHasEmbedsWhenReissued() $this->pendingMessage->shouldReceive('flowMeasure')->andReturn($measure); $this->pendingMessage->shouldReceive('reissue')->andReturn($this->reissuer); - $this->pendingMessage->shouldReceive('webhook')->andReturn($this->webhook); $this->reissuer->shouldReceive('isReissuedNotification')->andReturn(true); - $this->webhook->shouldReceive('id')->andReturnNull(); + $this->pendingMessage->shouldReceive('isEcfmp')->andReturn(true); $this->assertEquals( [ @@ -181,7 +176,7 @@ public function testItHasEmbedsWhenReissued() ); } - public function testItHasEmbedsWithoutIssuedByIfDivisionWebhook() + public function testItHasEmbedsWithoutIssuedByIfNotEcfmp() { $measure = FlowMeasure::factory() ->withTimes(Carbon::parse('2022-05-22T14:54:23Z'), Carbon::parse('2022-05-22T16:37:22Z')) @@ -193,9 +188,8 @@ public function testItHasEmbedsWithoutIssuedByIfDivisionWebhook() $this->pendingMessage->shouldReceive('flowMeasure')->andReturn($measure); $this->pendingMessage->shouldReceive('reissue')->andReturn($this->reissuer); - $this->pendingMessage->shouldReceive('webhook')->andReturn($this->webhook); $this->reissuer->shouldReceive('isReissuedNotification')->andReturn(false); - $this->webhook->shouldReceive('id')->andReturn(1); + $this->pendingMessage->shouldReceive('isEcfmp')->andReturn(false); $this->assertEquals( [ diff --git a/tests/Discord/FlowMeasure/Generator/EcfmpFlowMeasureMessageGeneratorTest.php b/tests/Discord/FlowMeasure/Generator/EcfmpFlowMeasureMessageGeneratorTest.php new file mode 100644 index 00000000..b0313c8a --- /dev/null +++ b/tests/Discord/FlowMeasure/Generator/EcfmpFlowMeasureMessageGeneratorTest.php @@ -0,0 +1,65 @@ +create(); + $measure2 = FlowMeasure::factory()->create(); + $mockRepository1 = Mockery::mock(RepositoryInterface::class); + $mockRepository1->shouldReceive('notificationType')->andReturn(DiscordNotificationType::FLOW_MEASURE_NOTIFIED); + $mockRepository1->shouldReceive('flowMeasuresToBeSentToEcfmp')->once()->andReturn(collect( + [ + new FlowMeasureForNotification($measure1, true), + new FlowMeasureForNotification($measure2, false), + ] + )); + + $measure3 = FlowMeasure::factory()->create(); + $mockRepository2 = Mockery::mock(RepositoryInterface::class); + $mockRepository2->shouldReceive('notificationType')->andReturn(DiscordNotificationType::FLOW_MEASURE_ACTIVATED); + $mockRepository2->shouldReceive('flowMeasuresToBeSentToEcfmp')->once()->andReturn(collect( + [ + new FlowMeasureForNotification($measure3, true), + ] + )); + + $mockSender = Mockery::mock(EcfmpFlowMeasureSender::class); + + $mockSender->shouldReceive('send')->once()->with(Mockery::on(function (PendingEcfmpMessage $message) use ($measure1) { + return $message->flowMeasure()->id === $measure1->id && + $message->isEcfmp() === true && + $message->type() === DiscordNotificationType::FLOW_MEASURE_NOTIFIED && + $message->reissue()->isReissuedNotification() === true; + })); + + $mockSender->shouldReceive('send')->once()->with(Mockery::on(function (PendingEcfmpMessage $message) use ($measure2) { + return $message->flowMeasure()->id === $measure2->id && + $message->isEcfmp() === true && + $message->type() === DiscordNotificationType::FLOW_MEASURE_NOTIFIED && + $message->reissue()->isReissuedNotification() === false; + })); + + $mockSender->shouldReceive('send')->once()->with(Mockery::on(function (PendingEcfmpMessage $message) use ($measure3) { + return $message->flowMeasure()->id === $measure3->id && + $message->isEcfmp() === true && + $message->type() === DiscordNotificationType::FLOW_MEASURE_ACTIVATED && + $message->reissue()->isReissuedNotification() === true; + })); + + $generator = new EcfmpFlowMeasureMessageGenerator($mockSender, [$mockRepository1, $mockRepository2]); + $generator->generateAndSend(); + } +} diff --git a/tests/Discord/FlowMeasure/Helper/EcfmpNotificationReissuerTest.php b/tests/Discord/FlowMeasure/Helper/EcfmpNotificationReissuerTest.php new file mode 100644 index 00000000..3e09defc --- /dev/null +++ b/tests/Discord/FlowMeasure/Helper/EcfmpNotificationReissuerTest.php @@ -0,0 +1,35 @@ +create(); + $notification = new FlowMeasureForNotification($measure, $isReissued); + $reissuer = new EcfmpNotificationReissuer($notification, $type); + + $this->assertEquals($expected, $reissuer->isReissuedNotification()); + } + + public function reissuingProvider(): array + { + return [ + 'is expired' => [true, DiscordNotificationType::FLOW_MEASURE_EXPIRED, false], + 'is activated but not reissued' => [false, DiscordNotificationType::FLOW_MEASURE_ACTIVATED, false], + 'is notified but not reissued' => [false, DiscordNotificationType::FLOW_MEASURE_NOTIFIED, false], + 'is withdrawn' => [true, DiscordNotificationType::FLOW_MEASURE_WITHDRAWN, false], + 'is notified and reissued' => [true, DiscordNotificationType::FLOW_MEASURE_NOTIFIED, true], + 'is activated and reissued' => [true, DiscordNotificationType::FLOW_MEASURE_ACTIVATED, true], + ]; + } +} diff --git a/tests/Discord/FlowMeasure/Helper/NotificationReissuerTest.php b/tests/Discord/FlowMeasure/Helper/NotificationReissuerTest.php index 7efdb8f3..c5df658b 100644 --- a/tests/Discord/FlowMeasure/Helper/NotificationReissuerTest.php +++ b/tests/Discord/FlowMeasure/Helper/NotificationReissuerTest.php @@ -3,9 +3,8 @@ namespace Tests\Discord\FlowMeasure\Helper; use App\Discord\FlowMeasure\Helper\NotificationReissuer; -use App\Discord\Webhook\EcfmpWebhook; use App\Enums\DiscordNotificationType as DiscordNotificationTypeEnum; -use App\Models\DiscordNotification; +use App\Models\DivisionDiscordNotification; use App\Models\DiscordNotificationType; use App\Models\DivisionDiscordWebhook; use App\Models\FlowMeasure; @@ -14,22 +13,27 @@ class NotificationReissuerTest extends TestCase { private readonly FlowMeasure $flowMeasure; + private readonly DivisionDiscordWebhook $webhook; public function setUp(): void { parent::setUp(); $this->flowMeasure = FlowMeasure::factory()->create(); + $this->webhook = DivisionDiscordWebhook::factory() + ->create(); } public function testItHasAType() { $this->assertEquals( DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED, - (new NotificationReissuer( - $this->flowMeasure, - DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED, - new EcfmpWebhook() - ))->type() + ( + new NotificationReissuer( + $this->flowMeasure, + DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED, + $this->webhook + ) + )->type() ); } @@ -37,18 +41,22 @@ public function testItHasAFlowMeasure() { $this->assertEquals( $this->flowMeasure, - (new NotificationReissuer( - $this->flowMeasure, - DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED, - new EcfmpWebhook() - ))->measure() + ( + new NotificationReissuer( + $this->flowMeasure, + DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED, + $this->webhook + ) + )->measure() ); } public function testItsAReissueIfItsNotifiedAndTheIdentifierHasChanged() { - $previousNotification = DiscordNotification::factory()->create(); - $this->flowMeasure->discordNotifications()->sync( + $previousNotification = DivisionDiscordNotification::factory() + ->toDivisionWebhook($this->webhook) + ->create(); + $this->flowMeasure->divisionDiscordNotifications()->sync( [ $previousNotification->id => [ 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( @@ -60,18 +68,22 @@ public function testItsAReissueIfItsNotifiedAndTheIdentifierHasChanged() ); $this->assertTrue( - (new NotificationReissuer( - $this->flowMeasure, - DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED, - new EcfmpWebhook() - ))->isReissuedNotification() + ( + new NotificationReissuer( + $this->flowMeasure, + DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED, + $this->webhook + ) + )->isReissuedNotification() ); } public function testItsAReissueIfItsActivatedAndTheIdentifierHasChanged() { - $previousNotification = DiscordNotification::factory()->create(); - $this->flowMeasure->discordNotifications()->sync( + $previousNotification = DivisionDiscordNotification::factory() + ->toDivisionWebhook($this->webhook) + ->create(); + $this->flowMeasure->divisionDiscordNotifications()->sync( [ $previousNotification->id => [ 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( @@ -83,86 +95,55 @@ public function testItsAReissueIfItsActivatedAndTheIdentifierHasChanged() ); $this->assertTrue( - (new NotificationReissuer( - $this->flowMeasure, - DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED, - new EcfmpWebhook() - ))->isReissuedNotification() + ( + new NotificationReissuer( + $this->flowMeasure, + DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED, + $this->webhook + ) + )->isReissuedNotification() ); } public function testItsAReissueIfItWasNotifiedAndTheIdentifierHasChangedForActivation() { - $previousNotification = DiscordNotification::factory()->create(); - $this->flowMeasure->discordNotifications()->sync( - [ - $previousNotification->id => [ - 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( - DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED - ), - 'notified_as' => 'notthis', - ], - ] - ); - - $this->assertTrue( - (new NotificationReissuer( - $this->flowMeasure, - DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED, - new EcfmpWebhook() - ))->isReissuedNotification() - ); - } - - public function testItsAReissueIfItsNotifiedOnAEcfmpWebhook() - { - $previousNotificationEcfmp = DiscordNotification::factory() - ->create(); - - $divisionWebhook = DivisionDiscordWebhook::factory()->create(); - $previousDivisionNotification = DiscordNotification::factory() - ->toDivisionWebhook($divisionWebhook) + $previousNotification = DivisionDiscordNotification::factory() + ->toDivisionWebhook($this->webhook) ->create(); - - $this->flowMeasure->discordNotifications()->sync( + $this->flowMeasure->divisionDiscordNotifications()->sync( [ - $previousNotificationEcfmp->id => [ + $previousNotification->id => [ 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED ), 'notified_as' => 'notthis', ], - $previousDivisionNotification->id => [ - 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( - DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED - ), - 'notified_as' => $this->flowMeasure->identifier, - ], ] ); $this->assertTrue( - (new NotificationReissuer( - $this->flowMeasure, - DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED, - new EcfmpWebhook() - ))->isReissuedNotification() + ( + new NotificationReissuer( + $this->flowMeasure, + DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED, + $this->webhook + ) + )->isReissuedNotification() ); } public function testItsAReissueIfItsNotifiedOnADifferentDivisionWebhook() { $otherDivisionWebhook = DivisionDiscordWebhook::factory()->create(); - $previousNotificationOtherDivision = DiscordNotification::factory() + $previousNotificationOtherDivision = DivisionDiscordNotification::factory() ->toDivisionWebhook($otherDivisionWebhook) ->create(); - $divisionWebhook = DivisionDiscordWebhook::factory()->create(); - $previousDivisionNotification = DiscordNotification::factory() - ->toDivisionWebhook($divisionWebhook) + $previousDivisionNotification = DivisionDiscordNotification::factory() + ->toDivisionWebhook($this->webhook) ->create(); - $this->flowMeasure->discordNotifications()->sync( + $this->flowMeasure->divisionDiscordNotifications()->sync( [ $previousNotificationOtherDivision->id => [ 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( @@ -180,27 +161,28 @@ public function testItsAReissueIfItsNotifiedOnADifferentDivisionWebhook() ); $this->assertTrue( - (new NotificationReissuer( - $this->flowMeasure, - DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED, - $divisionWebhook - ))->isReissuedNotification() + ( + new NotificationReissuer( + $this->flowMeasure, + DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED, + $this->webhook + ) + )->isReissuedNotification() ); } public function testItsAReissueIfItsActivatedOnADifferentDivisionWebhook() { $otherDivisionWebhook = DivisionDiscordWebhook::factory()->create(); - $previousNotificationOtherDivision = DiscordNotification::factory() + $previousNotificationOtherDivision = DivisionDiscordNotification::factory() ->toDivisionWebhook($otherDivisionWebhook) ->create(); - $divisionWebhook = DivisionDiscordWebhook::factory()->create(); - $previousDivisionNotification = DiscordNotification::factory() - ->toDivisionWebhook($divisionWebhook) + $previousDivisionNotification = DivisionDiscordNotification::factory() + ->toDivisionWebhook($this->webhook) ->create(); - $this->flowMeasure->discordNotifications()->sync( + $this->flowMeasure->divisionDiscordNotifications()->sync( [ $previousNotificationOtherDivision->id => [ 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( @@ -218,43 +200,22 @@ public function testItsAReissueIfItsActivatedOnADifferentDivisionWebhook() ); $this->assertTrue( - (new NotificationReissuer( - $this->flowMeasure, - DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED, - $divisionWebhook - ))->isReissuedNotification() - ); - } - - public function testItsAReissueIfItsActivatedOnAEcfmpWebhook() - { - $previousNotification = DiscordNotification::factory() - ->create(); - - $this->flowMeasure->discordNotifications()->sync( - [ - $previousNotification->id => [ - 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( - DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED - ), - 'notified_as' => 'nothis', - ], - ] - ); - - $this->assertTrue( - (new NotificationReissuer( - $this->flowMeasure, - DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED, - new EcfmpWebhook() - ))->isReissuedNotification() + ( + new NotificationReissuer( + $this->flowMeasure, + DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED, + $this->webhook + ) + )->isReissuedNotification() ); } public function testItIsNotAReissueIfItsNotifiedAndTheIdentifierHasNotChanged() { - $previousNotification = DiscordNotification::factory()->create(); - $this->flowMeasure->discordNotifications()->sync( + $previousNotification = DivisionDiscordNotification::factory() + ->toDivisionWebhook($this->webhook) + ->create(); + $this->flowMeasure->divisionDiscordNotifications()->sync( [ $previousNotification->id => [ 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( @@ -266,18 +227,22 @@ public function testItIsNotAReissueIfItsNotifiedAndTheIdentifierHasNotChanged() ); $this->assertFalse( - (new NotificationReissuer( - $this->flowMeasure, - DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED, - new EcfmpWebhook() - ))->isReissuedNotification() + ( + new NotificationReissuer( + $this->flowMeasure, + DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED, + $this->webhook + ) + )->isReissuedNotification() ); } public function testItIsNotAReissueIfItsActivatedAndTheIdentifierHasNotChanged() { - $previousNotification = DiscordNotification::factory()->create(); - $this->flowMeasure->discordNotifications()->sync( + $previousNotification = DivisionDiscordNotification::factory() + ->toDivisionWebhook($this->webhook) + ->create(); + $this->flowMeasure->divisionDiscordNotifications()->sync( [ $previousNotification->id => [ 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( @@ -289,18 +254,22 @@ public function testItIsNotAReissueIfItsActivatedAndTheIdentifierHasNotChanged() ); $this->assertFalse( - (new NotificationReissuer( - $this->flowMeasure, - DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED, - new EcfmpWebhook() - ))->isReissuedNotification() + ( + new NotificationReissuer( + $this->flowMeasure, + DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED, + $this->webhook + ) + )->isReissuedNotification() ); } public function testItIsNotAReissueIfItsNotifiedAndThenActivatedTheIdentifierHasNotChanged() { - $previousNotification = DiscordNotification::factory()->create(); - $this->flowMeasure->discordNotifications()->sync( + $previousNotification = DivisionDiscordNotification::factory() + ->toDivisionWebhook($this->webhook) + ->create(); + $this->flowMeasure->divisionDiscordNotifications()->sync( [ $previousNotification->id => [ 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( @@ -312,40 +281,48 @@ public function testItIsNotAReissueIfItsNotifiedAndThenActivatedTheIdentifierHas ); $this->assertFalse( - (new NotificationReissuer( - $this->flowMeasure, - DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED, - new EcfmpWebhook() - ))->isReissuedNotification() + ( + new NotificationReissuer( + $this->flowMeasure, + DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED, + $this->webhook + ) + )->isReissuedNotification() ); } public function testItIsNotAReissueIfItsNotifiedAndNeverBeenNotified() { $this->assertFalse( - (new NotificationReissuer( - $this->flowMeasure, - DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED, - new EcfmpWebhook() - ))->isReissuedNotification() + ( + new NotificationReissuer( + $this->flowMeasure, + DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED, + $this->webhook + ) + )->isReissuedNotification() ); } public function testItIsNotAReissueIfItsNotifiedAndNeverBeenActivated() { $this->assertFalse( - (new NotificationReissuer( - $this->flowMeasure, - DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED, - new EcfmpWebhook() - ))->isReissuedNotification() + ( + new NotificationReissuer( + $this->flowMeasure, + DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED, + $this->webhook + ) + )->isReissuedNotification() ); } public function testItIsNotAReissueIfItsWithdrawn() { - $previousNotification = DiscordNotification::factory()->create(); - $this->flowMeasure->discordNotifications()->sync( + $previousNotification = DivisionDiscordNotification::factory() + ->toDivisionWebhook($this->webhook) + ->create(); + $this->flowMeasure->divisionDiscordNotifications()->sync( [ $previousNotification->id => [ 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( @@ -357,18 +334,22 @@ public function testItIsNotAReissueIfItsWithdrawn() ); $this->assertFalse( - (new NotificationReissuer( - $this->flowMeasure, - DiscordNotificationTypeEnum::FLOW_MEASURE_WITHDRAWN, - new EcfmpWebhook() - ))->isReissuedNotification() + ( + new NotificationReissuer( + $this->flowMeasure, + DiscordNotificationTypeEnum::FLOW_MEASURE_WITHDRAWN, + $this->webhook + ) + )->isReissuedNotification() ); } public function testItIsNotAReissueIfItsExpired() { - $previousNotification = DiscordNotification::factory()->create(); - $this->flowMeasure->discordNotifications()->sync( + $previousNotification = DivisionDiscordNotification::factory() + ->toDivisionWebhook($this->webhook) + ->create(); + $this->flowMeasure->divisionDiscordNotifications()->sync( [ $previousNotification->id => [ 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( @@ -380,11 +361,13 @@ public function testItIsNotAReissueIfItsExpired() ); $this->assertFalse( - (new NotificationReissuer( - $this->flowMeasure, - DiscordNotificationTypeEnum::FLOW_MEASURE_EXPIRED, - new EcfmpWebhook() - ))->isReissuedNotification() + ( + new NotificationReissuer( + $this->flowMeasure, + DiscordNotificationTypeEnum::FLOW_MEASURE_EXPIRED, + $this->webhook + ) + )->isReissuedNotification() ); } } diff --git a/tests/Discord/FlowMeasure/Logger/FlowMeasureLoggerTest.php b/tests/Discord/FlowMeasure/Logger/FlowMeasureLoggerTest.php index cd81c0a8..c3e88bb7 100644 --- a/tests/Discord/FlowMeasure/Logger/FlowMeasureLoggerTest.php +++ b/tests/Discord/FlowMeasure/Logger/FlowMeasureLoggerTest.php @@ -4,7 +4,7 @@ use App\Discord\FlowMeasure\Logger\FlowMeasureLogger; use App\Enums\DiscordNotificationType as DiscordNotificationTypeEnum; -use App\Models\DiscordNotification; +use App\Models\DivisionDiscordNotification; use App\Models\DivisionDiscordWebhook; use App\Models\FlowMeasure; use Tests\TestCase; @@ -13,7 +13,7 @@ class FlowMeasureLoggerTest extends TestCase { public function testItLogsTheNotificationForEcfmp() { - $notification = DiscordNotification::factory()->create(); + $notification = DivisionDiscordNotification::factory()->create(); $measure = FlowMeasure::factory()->create(); $logger = new FlowMeasureLogger($measure, DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED); @@ -24,7 +24,7 @@ public function testItLogsTheNotificationForEcfmp() [ 'log_name' => 'Discord', 'description' => 'Sending discord notification', - 'subject_type' => 'App\Models\DiscordNotification', + 'subject_type' => 'App\Models\DivisionDiscordNotification', 'event' => $measure->identifier . ' - Activated', ] ); @@ -33,7 +33,7 @@ public function testItLogsTheNotificationForEcfmp() public function testItLogsTheNotificationForDivisions() { $webhook = DivisionDiscordWebhook::factory()->create(); - $notification = DiscordNotification::factory()->toDivisionWebhook($webhook)->create(); + $notification = DivisionDiscordNotification::factory()->toDivisionWebhook($webhook)->create(); $measure = FlowMeasure::factory()->create(); $logger = new FlowMeasureLogger($measure, DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED); @@ -44,7 +44,7 @@ public function testItLogsTheNotificationForDivisions() [ 'log_name' => 'Discord', 'description' => 'Sending discord notification', - 'subject_type' => 'App\Models\DiscordNotification', + 'subject_type' => 'App\Models\DivisionDiscordNotification', 'event' => $measure->identifier . ' - Activated', ] ); diff --git a/tests/Discord/FlowMeasure/Message/FlowMeasureMessageFactoryTest.php b/tests/Discord/FlowMeasure/Message/FlowMeasureMessageFactoryTest.php index 9a0e666a..68f3e366 100644 --- a/tests/Discord/FlowMeasure/Message/FlowMeasureMessageFactoryTest.php +++ b/tests/Discord/FlowMeasure/Message/FlowMeasureMessageFactoryTest.php @@ -8,6 +8,7 @@ use App\Discord\FlowMeasure\Embed\FlowMeasureEmbedInterface; use App\Discord\FlowMeasure\Message\FlowMeasureMessageFactory; use App\Discord\FlowMeasure\Provider\PendingMessageInterface; +use App\Discord\FlowMeasure\Provider\PendingWebhookMessageInterface; use App\Discord\Message\Embed\EmbedCollection; use App\Discord\Webhook\WebhookInterface; use App\Enums\DiscordNotificationType; @@ -23,18 +24,20 @@ class FlowMeasureMessageFactoryTest extends TestCase private readonly FlowMeasureRecipientsInterface $recipients; private readonly FlowMeasureRecipientsFactory $recipientsFactory; private readonly FlowMeasureMessageFactory $factory; - private readonly PendingMessageInterface $pendingMessage; + private readonly PendingWebhookMessageInterface $pendingWebhookMessage; private readonly WebhookInterface $webhook; + private readonly PendingMessageInterface $pendingEcfmpMessage; + public function setUp(): void { parent::setUp(); $flowMeasure = FlowMeasure::factory()->make(); $this->webhook = Mockery::mock(WebhookInterface::class); - $this->pendingMessage = Mockery::mock(PendingMessageInterface::class); - $this->pendingMessage->shouldReceive('webhook')->andReturn($this->webhook); - $this->pendingMessage->shouldReceive('flowMeasure')->andReturn($flowMeasure); - $this->pendingMessage->shouldReceive('type')->andReturn(DiscordNotificationType::FLOW_MEASURE_WITHDRAWN); + $this->pendingWebhookMessage = Mockery::mock(PendingWebhookMessageInterface::class); + $this->pendingWebhookMessage->shouldReceive('webhook')->andReturn($this->webhook); + $this->pendingWebhookMessage->shouldReceive('flowMeasure')->andReturn($flowMeasure); + $this->pendingWebhookMessage->shouldReceive('type')->andReturn(DiscordNotificationType::FLOW_MEASURE_WITHDRAWN); $this->embedCollection = new EmbedCollection(); $this->embeds = Mockery::mock(FlowMeasureEmbedInterface::class); $this->embeds->shouldReceive('embeds')->andReturn($this->embedCollection); @@ -45,13 +48,25 @@ public function setUp(): void $this->recipientsFactory = Mockery::mock(FlowMeasureRecipientsFactory::class); $this->recipientsFactory->shouldReceive('makeRecipients')->andReturn($this->recipients); $this->factory = new FlowMeasureMessageFactory($this->recipientsFactory, $this->embedFactory); + $this->pendingEcfmpMessage = Mockery::mock(PendingMessageInterface::class); + $this->pendingEcfmpMessage->shouldReceive('flowMeasure')->andReturn(FlowMeasure::factory()->make()); + $this->pendingEcfmpMessage->shouldReceive('type')->andReturn(DiscordNotificationType::FLOW_MEASURE_WITHDRAWN); + $this->recipientsFactory->shouldReceive('makeEcfmpRecipients')->andReturn($this->recipients); } public function testItMakesAMessage() { - $message = $this->factory->make($this->pendingMessage); + $message = $this->factory->make($this->pendingWebhookMessage); $this->assertEquals($this->webhook, $message->destination()); $this->assertEquals('foo', $message->content()); $this->assertEquals($this->embedCollection, $message->embeds()); } + + public function testItMakesAnEcfmpMessage() + { + $message = $this->factory->makeEcfmp($this->pendingEcfmpMessage); + $this->assertEquals(config('discord.ecfmp_channel_id'), $message->channel()); + $this->assertEquals('foo', $message->content()); + $this->assertEquals($this->embedCollection, $message->embeds()); + } } diff --git a/tests/Discord/FlowMeasure/Message/MessageGeneratorTest.php b/tests/Discord/FlowMeasure/Message/MessageGeneratorTest.php index 6bfe6352..b947a848 100644 --- a/tests/Discord/FlowMeasure/Message/MessageGeneratorTest.php +++ b/tests/Discord/FlowMeasure/Message/MessageGeneratorTest.php @@ -6,7 +6,7 @@ use App\Discord\FlowMeasure\Message\FlowMeasureMessageFactory; use App\Discord\FlowMeasure\Message\MessageGenerator; use App\Discord\FlowMeasure\Provider\MessageProviderInterface; -use App\Discord\FlowMeasure\Provider\PendingMessageInterface; +use App\Discord\FlowMeasure\Provider\PendingWebhookMessageInterface; use Mockery; use Tests\TestCase; @@ -16,8 +16,8 @@ public function testItGeneratesMessages() { $provider = Mockery::mock(MessageProviderInterface::class); $measureFactory = Mockery::mock(FlowMeasureMessageFactory::class); - $message1 = Mockery::mock(PendingMessageInterface::class); - $message2 = Mockery::mock(PendingMessageInterface::class); + $message1 = Mockery::mock(PendingWebhookMessageInterface::class); + $message2 = Mockery::mock(PendingWebhookMessageInterface::class); $flowMeasureMessage1 = Mockery::mock(FlowMeasureMessage::class); $flowMeasureMessage2 = Mockery::mock(FlowMeasureMessage::class); diff --git a/tests/Discord/FlowMeasure/Provider/MessageProviderTest.php b/tests/Discord/FlowMeasure/Provider/DivisionWebhookMessageProviderTest.php similarity index 90% rename from tests/Discord/FlowMeasure/Provider/MessageProviderTest.php rename to tests/Discord/FlowMeasure/Provider/DivisionWebhookMessageProviderTest.php index 02d5a052..8cc93efd 100644 --- a/tests/Discord/FlowMeasure/Provider/MessageProviderTest.php +++ b/tests/Discord/FlowMeasure/Provider/DivisionWebhookMessageProviderTest.php @@ -2,7 +2,7 @@ namespace Tests\Discord\FlowMeasure\Provider; -use App\Discord\FlowMeasure\Provider\MessageProvider; +use App\Discord\FlowMeasure\Provider\DivisionWebhookMessageProvider; use App\Discord\FlowMeasure\Webhook\WebhookMapper; use App\Enums\DiscordNotificationType; use App\Models\DivisionDiscordWebhook; @@ -11,18 +11,18 @@ use Mockery; use Tests\TestCase; -class MessageProviderTest extends TestCase +class DivisionWebhookMessageProviderTest extends TestCase { private readonly RepositoryInterface $repository; private readonly WebhookMapper $mapper; - private readonly MessageProvider $messageProvider; + private readonly DivisionWebhookMessageProvider $messageProvider; public function setUp(): void { parent::setUp(); $this->repository = Mockery::mock(RepositoryInterface::class); $this->mapper = Mockery::mock(WebhookMapper::class); - $this->messageProvider = new MessageProvider($this->repository, $this->mapper); + $this->messageProvider = new DivisionWebhookMessageProvider($this->repository, $this->mapper); } public function testItProvidesPendingMessages() diff --git a/app/Discord/FlowMeasure/Provider/PendingDiscordMessageTest.php b/tests/Discord/FlowMeasure/Provider/PendingDiscordWebhookMessageTest.php similarity index 73% rename from app/Discord/FlowMeasure/Provider/PendingDiscordMessageTest.php rename to tests/Discord/FlowMeasure/Provider/PendingDiscordWebhookMessageTest.php index c8c6fdd5..06275ced 100644 --- a/app/Discord/FlowMeasure/Provider/PendingDiscordMessageTest.php +++ b/tests/Discord/FlowMeasure/Provider/PendingDiscordWebhookMessageTest.php @@ -1,20 +1,21 @@ type = DiscordNotificationType::FLOW_MEASURE_ACTIVATED; $this->webhook = Mockery::mock(WebhookInterface::class); $this->reissue = Mockery::mock(NotificationReissuerInterface::class); - $this->message = new PendingDiscordMessage($this->measure, $this->type, $this->webhook, $this->reissue); + $this->message = new PendingDiscordWebhookMessage($this->measure, $this->type, $this->webhook, $this->reissue); } public function testItHasAMeasure() @@ -46,4 +47,9 @@ public function testItIsReissued() { $this->assertEquals($this->reissue, $this->message->reissue()); } + + public function testItIsNotEcfmp() + { + $this->assertFalse($this->message->isEcfmp()); + } } diff --git a/tests/Discord/FlowMeasure/Provider/PendingEcfmpMessageTest.php b/tests/Discord/FlowMeasure/Provider/PendingEcfmpMessageTest.php new file mode 100644 index 00000000..77bc5d7d --- /dev/null +++ b/tests/Discord/FlowMeasure/Provider/PendingEcfmpMessageTest.php @@ -0,0 +1,47 @@ +measure = FlowMeasure::factory()->create(); + $this->type = DiscordNotificationType::FLOW_MEASURE_ACTIVATED; + $this->reissue = Mockery::mock(NotificationReissuerInterface::class); + $this->message = new PendingEcfmpMessage($this->measure, $this->type, $this->reissue); + } + + public function testItHasAMeasure() + { + $this->assertEquals($this->measure, $this->message->flowMeasure()); + } + + public function testItHasAType() + { + $this->assertEquals($this->type, $this->message->type()); + } + + public function testItIsReissued() + { + $this->assertEquals($this->reissue, $this->message->reissue()); + } + + public function testItIsEcfmp() + { + $this->assertTrue($this->message->isEcfmp()); + } +} diff --git a/tests/Discord/FlowMeasure/Sender/EcfmpFlowMeasureSenderTest.php b/tests/Discord/FlowMeasure/Sender/EcfmpFlowMeasureSenderTest.php new file mode 100644 index 00000000..de6a868d --- /dev/null +++ b/tests/Discord/FlowMeasure/Sender/EcfmpFlowMeasureSenderTest.php @@ -0,0 +1,88 @@ +discordService = Mockery::mock(DiscordServiceInterface::class); + $this->messageFactory = Mockery::mock(FlowMeasureMessageFactory::class); + $this->app->instance(DiscordServiceInterface::class, $this->discordService); + $this->app->instance(FlowMeasureMessageFactory::class, $this->messageFactory); + + Config::set('discord.client_request_app_id', 'testabc'); + $this->sender = $this->app->make(EcfmpFlowMeasureSender::class); + } + + public function testItSendsTheMessage(): void + { + $flowMeasure = FlowMeasure::factory()->create(); + $pendingMessage = new PendingEcfmpMessage($flowMeasure, DiscordNotificationType::FLOW_MEASURE_ACTIVATED, Mockery::mock(NotificationReissuerInterface::class)); + $flowMeasureMessage = new EcfmpFlowMeasureMessage('test-abc', Mockery::mock(FlowMeasureRecipientsInterface::class), Mockery::mock(FlowMeasureEmbedInterface::class)); + + $this->messageFactory->shouldReceive('makeEcfmp')->once()->with($pendingMessage)->andReturn($flowMeasureMessage); + + $expectedClientRequestId = 'testabc-' . DiscordNotificationType::FLOW_MEASURE_ACTIVATED->value . '-' . $flowMeasure->id . '-' . $flowMeasure->identifier; + + $this->discordService->shouldReceive('sendMessage')->once()->with($expectedClientRequestId, $flowMeasureMessage)->andReturn('1234567890'); + + $this->sender->send($pendingMessage); + + $notification = DiscordNotification::latest()->first(); + $this->assertEquals('1234567890', $notification->remote_id); + + $this->assertDatabaseHas('discord_notification_flow_measure', [ + 'discord_notification_type_id' => DiscordNotificationTypeModel::idFromEnum(DiscordNotificationType::FLOW_MEASURE_ACTIVATED), + 'notified_as' => $flowMeasure->identifier, + 'discord_notification_id' => $notification->id, + 'flow_measure_id' => $flowMeasure->id, + ]); + } + + public function testItHandlesExceptionIfSendingFails(): void + { + $startNotificationCount = DiscordNotification::count(); + + $flowMeasure = FlowMeasure::factory()->create(); + $pendingMessage = new PendingEcfmpMessage($flowMeasure, DiscordNotificationType::FLOW_MEASURE_ACTIVATED, Mockery::mock(NotificationReissuerInterface::class)); + $flowMeasureMessage = new EcfmpFlowMeasureMessage('test-abc', Mockery::mock(FlowMeasureRecipientsInterface::class), Mockery::mock(FlowMeasureEmbedInterface::class)); + + $this->messageFactory->shouldReceive('makeEcfmp')->once()->with($pendingMessage)->andReturn($flowMeasureMessage); + + $expectedClientRequestId = 'testabc-' . DiscordNotificationType::FLOW_MEASURE_ACTIVATED->value . '-' . $flowMeasure->id . '-' . $flowMeasure->identifier; + + $this->discordService->shouldReceive('sendMessage')->once()->with($expectedClientRequestId, $flowMeasureMessage)->andThrow(new DiscordServiceException('test')); + + $this->sender->send($pendingMessage); + + + $this->assertDatabaseCount('discord_notifications', $startNotificationCount); + $this->assertDatabaseMissing('discord_notification_flow_measure', [ + 'flow_measure_id' => $flowMeasure->id, + ]); + } +} diff --git a/tests/Discord/FlowMeasure/Webhook/Filter/ActivatedWebhookFilterTest.php b/tests/Discord/FlowMeasure/Webhook/Filter/ActivatedWebhookFilterTest.php index d194150f..d760f3fb 100644 --- a/tests/Discord/FlowMeasure/Webhook/Filter/ActivatedWebhookFilterTest.php +++ b/tests/Discord/FlowMeasure/Webhook/Filter/ActivatedWebhookFilterTest.php @@ -3,9 +3,8 @@ namespace Tests\Discord\FlowMeasure\Webhook\Filter; use App\Discord\FlowMeasure\Webhook\Filter\ActivatedWebhookFilter; -use App\Discord\Webhook\EcfmpWebhook; use App\Enums\DiscordNotificationType as DiscordNotificationTypeEnum; -use App\Models\DiscordNotification; +use App\Models\DivisionDiscordNotification; use App\Models\DiscordNotificationType; use App\Models\DivisionDiscordWebhook; use App\Models\FlowMeasure; @@ -14,97 +13,15 @@ class ActivatedWebhookFilterTest extends TestCase { private readonly ActivatedWebhookFilter $filter; - private readonly EcfmpWebhook $ecfmpWebhook; private readonly DivisionDiscordWebhook $divisionDiscordWebhook; public function setUp(): void { parent::setUp(); $this->filter = $this->app->make(ActivatedWebhookFilter::class); - $this->ecfmpWebhook = $this->app->make(EcfmpWebhook::class); $this->divisionDiscordWebhook = DivisionDiscordWebhook::factory()->create(); } - public function testItShouldUseWebhookIfFlowMeasureHasNeverBeenActivatedToEcfmpWebhook() - { - $measure = FlowMeasure::factory()->create(); - $this->assertTrue( - $this->filter->shouldUseWebhook( - $measure, - $this->ecfmpWebhook - ) - ); - } - - public function testItShouldUseWebhookIfFlowMeasureHasOnlyBeenNotifiedToEcfmpWebhook() - { - $measure = FlowMeasure::factory()->create(); - $discordNotification = DiscordNotification::factory()->create(); - $measure->discordNotifications()->attach( - [ - $discordNotification->id => [ - 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( - DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED - ), - 'notified_as' => $measure->identifier, - ], - ] - ); - - $this->assertTrue( - $this->filter->shouldUseWebhook( - $measure, - $this->ecfmpWebhook - ) - ); - } - - public function testItShouldUseWebhookIfFlowMeasureHasBeenActivatedAsDifferentIdentifierToEcfmpWebhook() - { - $measure = FlowMeasure::factory()->create(); - $discordNotification = DiscordNotification::factory()->create(); - $measure->discordNotifications()->attach( - [ - $discordNotification->id => [ - 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( - DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED - ), - 'notified_as' => 'NOTHIS', - ], - ] - ); - - $this->assertTrue( - $this->filter->shouldUseWebhook( - $measure, - $this->ecfmpWebhook - ) - ); - } - - public function testItShouldNotUseWebhookIfFlowMeasureHasBeenActivatedToEcfmpWebhook() - { - $measure = FlowMeasure::factory()->create(); - $discordNotification = DiscordNotification::factory()->create(); - $measure->discordNotifications()->attach( - [ - $discordNotification->id => [ - 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( - DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED - ), - 'notified_as' => $measure->identifier, - ], - ] - ); - - $this->assertFalse( - $this->filter->shouldUseWebhook( - $measure, - $this->ecfmpWebhook - ) - ); - } - public function testItShouldUseWebhookIfFlowMeasureHasNeverBeenActivatedToDivisionWebhook() { $measure = FlowMeasure::factory()->create(); @@ -119,10 +36,10 @@ public function testItShouldUseWebhookIfFlowMeasureHasNeverBeenActivatedToDivisi public function testItShouldUseWebhookIfFlowMeasureHasOnlyBeenNotifiedToDivisionWebhook() { $measure = FlowMeasure::factory()->create(); - $discordNotification = DiscordNotification::factory() + $discordNotification = DivisionDiscordNotification::factory() ->toDivisionWebhook($this->divisionDiscordWebhook) ->create(); - $measure->discordNotifications()->attach( + $measure->divisionDiscordNotifications()->attach( [ $discordNotification->id => [ 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( @@ -144,10 +61,10 @@ public function testItShouldUseWebhookIfFlowMeasureHasOnlyBeenNotifiedToDivision public function testItShouldUseWebhookIfFlowMeasureHasBeenActivatedAsDifferentIdentifierToDivisionWebhook() { $measure = FlowMeasure::factory()->create(); - $discordNotification = DiscordNotification::factory() + $discordNotification = DivisionDiscordNotification::factory() ->toDivisionWebhook($this->divisionDiscordWebhook) ->create(); - $measure->discordNotifications()->attach( + $measure->divisionDiscordNotifications()->attach( [ $discordNotification->id => [ 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( @@ -169,10 +86,10 @@ public function testItShouldUseWebhookIfFlowMeasureHasBeenActivatedAsDifferentId public function testItShouldNotUseWebhookIfFlowMeasureHasBeenActivatedToDivisionWebhook() { $measure = FlowMeasure::factory()->create(); - $discordNotification = DiscordNotification::factory() + $discordNotification = DivisionDiscordNotification::factory() ->toDivisionWebhook($this->divisionDiscordWebhook) ->create(); - $measure->discordNotifications()->attach( + $measure->divisionDiscordNotifications()->attach( [ $discordNotification->id => [ 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( diff --git a/tests/Discord/FlowMeasure/Webhook/Filter/ExpiredWebhookFilterTest.php b/tests/Discord/FlowMeasure/Webhook/Filter/ExpiredWebhookFilterTest.php index cad33a6d..88500b96 100644 --- a/tests/Discord/FlowMeasure/Webhook/Filter/ExpiredWebhookFilterTest.php +++ b/tests/Discord/FlowMeasure/Webhook/Filter/ExpiredWebhookFilterTest.php @@ -3,195 +3,32 @@ namespace Tests\Discord\FlowMeasure\Webhook\Filter; use App\Discord\FlowMeasure\Webhook\Filter\ExpiredWebhookFilter; -use App\Discord\Webhook\EcfmpWebhook; use App\Enums\DiscordNotificationType as DiscordNotificationTypeEnum; -use App\Models\DiscordNotification; +use App\Models\DivisionDiscordNotification; use App\Models\DiscordNotificationType; use App\Models\DivisionDiscordWebhook; use App\Models\FlowMeasure; -use Carbon\Carbon; use Tests\TestCase; class ExpiredWebhookFilterTest extends TestCase { private readonly ExpiredWebhookFilter $filter; - private readonly EcfmpWebhook $ecfmpWebhook; private readonly DivisionDiscordWebhook $divisionDiscordWebhook; public function setUp(): void { parent::setUp(); $this->filter = $this->app->make(ExpiredWebhookFilter::class); - $this->ecfmpWebhook = $this->app->make(EcfmpWebhook::class); $this->divisionDiscordWebhook = DivisionDiscordWebhook::factory()->create(); } - public function testItShouldUseEcfmpWebhookIfLotsOfNotificationsHaveBeenSentRecently() - { - $measure = FlowMeasure::factory()->create(); - DiscordNotification::factory()->count(6)->create(['division_discord_webhook_id' => null]); - - $this->assertTrue( - $this->filter->shouldUseWebhook( - $measure, - $this->ecfmpWebhook - ) - ); - } - - public function testItShouldUseEcfmpWebhookIfTheFlowMeasureHasBeenModifiedTwice() - { - $measure = FlowMeasure::factory()->create(['identifier' => 'EGTT23A-3']); - - $this->assertTrue( - $this->filter->shouldUseWebhook( - $measure, - $this->ecfmpWebhook - ) - ); - } - - public function testItShouldUseEcfmpWebhookIfThereAreOtherMeasuresActiveAroundTheTime() - { - FlowMeasure::factory() - ->withTimes(Carbon::now()->subMinutes(30), Carbon::now()->addMinutes(45)) - ->count(3) - ->create(); - - $measure = FlowMeasure::factory()->create(); - - $this->assertTrue( - $this->filter->shouldUseWebhook( - $measure, - $this->ecfmpWebhook - ) - ); - } - - public function testItShouldUseWebhookIfFlowMeasureHasOnlyBeenNotifiedToEcfmpWebhook() - { - $measure = FlowMeasure::factory()->create(['identifier' => 'EGTT23A-3']); - $discordNotification = DiscordNotification::factory()->create(); - $measure->discordNotifications()->attach( - [ - $discordNotification->id => [ - 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( - DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED - ), - 'notified_as' => $measure->identifier, - ], - ] - ); - - $this->assertTrue( - $this->filter->shouldUseWebhook( - $measure, - $this->ecfmpWebhook - ) - ); - } - - public function testItShouldUseWebhookIfFlowMeasureHasOnlyBeenActivatedToEcfmpWebhook() - { - $measure = FlowMeasure::factory()->create(['identifier' => 'EGTT23A-3']); - $discordNotification = DiscordNotification::factory()->create(); - $measure->discordNotifications()->attach( - [ - $discordNotification->id => [ - 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( - DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED - ), - 'notified_as' => $measure->identifier, - ], - ] - ); - - $this->assertTrue( - $this->filter->shouldUseWebhook( - $measure, - $this->ecfmpWebhook - ) - ); - } - - public function testItShouldNotUseWebhookIfFlowMeasureHasBeenWithdrawnToEcfmpWebhook() - { - $measure = FlowMeasure::factory()->create(['identifier' => 'EGTT23A-3']); - $discordNotification = DiscordNotification::factory()->create(); - $measure->discordNotifications()->attach( - [ - $discordNotification->id => [ - 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( - DiscordNotificationTypeEnum::FLOW_MEASURE_WITHDRAWN - ), - 'notified_as' => $measure->identifier, - ], - ] - ); - - $this->assertFalse( - $this->filter->shouldUseWebhook( - $measure, - $this->ecfmpWebhook - ) - ); - } - - public function testItShouldNotUseWebhookIfFlowMeasureHasBeenExpiredToEcfmpWebhook() - { - $measure = FlowMeasure::factory()->create(['identifier' => 'EGTT23A-3']); - $discordNotification = DiscordNotification::factory()->create(); - $measure->discordNotifications()->attach( - [ - $discordNotification->id => [ - 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( - DiscordNotificationTypeEnum::FLOW_MEASURE_EXPIRED - ), - 'notified_as' => $measure->identifier, - ], - ] - ); - - $this->assertFalse( - $this->filter->shouldUseWebhook( - $measure, - $this->ecfmpWebhook - ) - ); - } - - public function testItShouldNotUseWebhookIfDoesntMeetConditionsForEcfmpWebhook() - { - // Once revised - $measure = FlowMeasure::factory()->create(['identifier' => 'EGTT23A-2']); - - // Two other measures - FlowMeasure::factory() - ->withTimes($measure->start_time->clone()->subMinutes(2), $measure->start_time->clone()->addMinutes(2)) - ->count(2) - ->create(); - - // Not too many recently sent - DiscordNotification::factory()->count(5)->create(['division_discord_webhook_id' => null]); - DiscordNotification::factory()->count(5) - ->toDivisionWebhook(DivisionDiscordWebhook::factory()->create()) - ->create(); - - $this->assertFalse( - $this->filter->shouldUseWebhook( - $measure, - $this->ecfmpWebhook - ) - ); - } - public function testItShouldNotUseWebhookIfFlowMeasureHasOnlyBeenNotifiedToDivisionWebhook() { $measure = FlowMeasure::factory()->create(); - $discordNotification = DiscordNotification::factory() + $discordNotification = DivisionDiscordNotification::factory() ->toDivisionWebhook($this->divisionDiscordWebhook) ->create(); - $measure->discordNotifications()->attach( + $measure->divisionDiscordNotifications()->attach( [ $discordNotification->id => [ 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( @@ -213,10 +50,10 @@ public function testItShouldNotUseWebhookIfFlowMeasureHasOnlyBeenNotifiedToDivis public function testItShouldNotUseWebhookIfFlowMeasureHasOnlyBeenActivatedToDivisionWebhook() { $measure = FlowMeasure::factory()->create(); - $discordNotification = DiscordNotification::factory() + $discordNotification = DivisionDiscordNotification::factory() ->toDivisionWebhook($this->divisionDiscordWebhook) ->create(); - $measure->discordNotifications()->attach( + $measure->divisionDiscordNotifications()->attach( [ $discordNotification->id => [ 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( @@ -238,10 +75,10 @@ public function testItShouldNotUseWebhookIfFlowMeasureHasOnlyBeenActivatedToDivi public function testItShouldNotUseWebhookIfFlowMeasureHasBeenExpiredToDivisionWebhook() { $measure = FlowMeasure::factory()->create(); - $discordNotification = DiscordNotification::factory() + $discordNotification = DivisionDiscordNotification::factory() ->toDivisionWebhook($this->divisionDiscordWebhook) ->create(); - $measure->discordNotifications()->attach( + $measure->divisionDiscordNotifications()->attach( [ $discordNotification->id => [ 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( @@ -263,10 +100,10 @@ public function testItShouldNotUseWebhookIfFlowMeasureHasBeenExpiredToDivisionWe public function testItShouldNotUseWebhookIfFlowMeasureHasBeenWithdrawnToDivisionWebhook() { $measure = FlowMeasure::factory()->create(); - $discordNotification = DiscordNotification::factory() + $discordNotification = DivisionDiscordNotification::factory() ->toDivisionWebhook($this->divisionDiscordWebhook) ->create(); - $measure->discordNotifications()->attach( + $measure->divisionDiscordNotifications()->attach( [ $discordNotification->id => [ 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( diff --git a/tests/Discord/FlowMeasure/Webhook/Filter/NotifiedWebhookFilterTest.php b/tests/Discord/FlowMeasure/Webhook/Filter/NotifiedWebhookFilterTest.php index 0469021d..5ab267b1 100644 --- a/tests/Discord/FlowMeasure/Webhook/Filter/NotifiedWebhookFilterTest.php +++ b/tests/Discord/FlowMeasure/Webhook/Filter/NotifiedWebhookFilterTest.php @@ -3,9 +3,8 @@ namespace Tests\Discord\FlowMeasure\Webhook\Filter; use App\Discord\FlowMeasure\Webhook\Filter\NotifiedWebhookFilter; -use App\Discord\Webhook\EcfmpWebhook; use App\Enums\DiscordNotificationType as DiscordNotificationTypeEnum; -use App\Models\DiscordNotification; +use App\Models\DivisionDiscordNotification; use App\Models\DiscordNotificationType; use App\Models\DivisionDiscordWebhook; use App\Models\FlowMeasure; @@ -14,97 +13,15 @@ class NotifiedWebhookFilterTest extends TestCase { private readonly NotifiedWebhookFilter $filter; - private readonly EcfmpWebhook $ecfmpWebhook; private readonly DivisionDiscordWebhook $divisionDiscordWebhook; public function setUp(): void { parent::setUp(); $this->filter = $this->app->make(NotifiedWebhookFilter::class); - $this->ecfmpWebhook = $this->app->make(EcfmpWebhook::class); $this->divisionDiscordWebhook = DivisionDiscordWebhook::factory()->create(); } - public function testItShouldUseWebhookIfFlowMeasureHasNeverBeenActivatedOrNotifiedToEcfmpWebhook() - { - $measure = FlowMeasure::factory()->create(); - $this->assertTrue( - $this->filter->shouldUseWebhook( - $measure, - $this->ecfmpWebhook - ) - ); - } - - public function testItShouldUseWebhookIfFlowMeasureHasBeenNotifiedToEcfmpWebhookUnderDifferentIdentifier() - { - $measure = FlowMeasure::factory()->create(); - $discordNotification = DiscordNotification::factory()->create(); - $measure->discordNotifications()->attach( - [ - $discordNotification->id => [ - 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( - DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED - ), - 'notified_as' => 'notme', - ], - ] - ); - - $this->assertTrue( - $this->filter->shouldUseWebhook( - $measure, - $this->ecfmpWebhook - ) - ); - } - - public function testItShouldNotUseWebhookIfFlowMeasureHasBeenNotifiedToEcfmpWebhook() - { - $measure = FlowMeasure::factory()->create(); - $discordNotification = DiscordNotification::factory()->create(); - $measure->discordNotifications()->attach( - [ - $discordNotification->id => [ - 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( - DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED - ), - 'notified_as' => $measure->identifier, - ], - ] - ); - - $this->assertFalse( - $this->filter->shouldUseWebhook( - $measure, - $this->ecfmpWebhook - ) - ); - } - - public function testItShouldNotUseWebhookIfFlowMeasureHasBeenActivatedToEcfmpWebhook() - { - $measure = FlowMeasure::factory()->create(); - $discordNotification = DiscordNotification::factory()->create(); - $measure->discordNotifications()->attach( - [ - $discordNotification->id => [ - 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( - DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED - ), - 'notified_as' => $measure->identifier, - ], - ] - ); - - $this->assertFalse( - $this->filter->shouldUseWebhook( - $measure, - $this->ecfmpWebhook - ) - ); - } - public function testItShouldUseWebhookIfFlowMeasureHasNeverBeenActivatedOrNotifiedToDivisionWebhook() { $measure = FlowMeasure::factory()->create(); @@ -119,10 +36,10 @@ public function testItShouldUseWebhookIfFlowMeasureHasNeverBeenActivatedOrNotifi public function testItShouldUseWebhookIfFlowMeasureHasOnlyBeenNotifiedToDivisionWebhookAsDifferentIdentifier() { $measure = FlowMeasure::factory()->create(); - $discordNotification = DiscordNotification::factory() + $discordNotification = DivisionDiscordNotification::factory() ->toDivisionWebhook($this->divisionDiscordWebhook) ->create(); - $measure->discordNotifications()->attach( + $measure->divisionDiscordNotifications()->attach( [ $discordNotification->id => [ 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( @@ -144,10 +61,10 @@ public function testItShouldUseWebhookIfFlowMeasureHasOnlyBeenNotifiedToDivision public function testItShouldNotUseWebhookIfFlowMeasureHasBeenNotifiedToDivisionWebhookWithSameIdentifier() { $measure = FlowMeasure::factory()->create(); - $discordNotification = DiscordNotification::factory() + $discordNotification = DivisionDiscordNotification::factory() ->toDivisionWebhook($this->divisionDiscordWebhook) ->create(); - $measure->discordNotifications()->attach( + $measure->divisionDiscordNotifications()->attach( [ $discordNotification->id => [ 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( @@ -169,10 +86,10 @@ public function testItShouldNotUseWebhookIfFlowMeasureHasBeenNotifiedToDivisionW public function testItShouldNotUseWebhookIfFlowMeasureHasBeenActivatedToDivisionWebhook() { $measure = FlowMeasure::factory()->create(); - $discordNotification = DiscordNotification::factory() + $discordNotification = DivisionDiscordNotification::factory() ->toDivisionWebhook($this->divisionDiscordWebhook) ->create(); - $measure->discordNotifications()->attach( + $measure->divisionDiscordNotifications()->attach( [ $discordNotification->id => [ 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( diff --git a/tests/Discord/FlowMeasure/Webhook/Filter/WithdrawnWebhookFilterTest.php b/tests/Discord/FlowMeasure/Webhook/Filter/WithdrawnWebhookFilterTest.php index bf3f398f..b0a3a1c1 100644 --- a/tests/Discord/FlowMeasure/Webhook/Filter/WithdrawnWebhookFilterTest.php +++ b/tests/Discord/FlowMeasure/Webhook/Filter/WithdrawnWebhookFilterTest.php @@ -3,9 +3,8 @@ namespace Tests\Discord\FlowMeasure\Webhook\Filter; use App\Discord\FlowMeasure\Webhook\Filter\WithdrawnWebhookFilter; -use App\Discord\Webhook\EcfmpWebhook; use App\Enums\DiscordNotificationType as DiscordNotificationTypeEnum; -use App\Models\DiscordNotification; +use App\Models\DivisionDiscordNotification; use App\Models\DiscordNotificationType; use App\Models\DivisionDiscordWebhook; use App\Models\FlowMeasure; @@ -14,134 +13,22 @@ class WithdrawnWebhookFilterTest extends TestCase { private readonly WithdrawnWebhookFilter $filter; - private readonly EcfmpWebhook $ecfmpWebhook; private readonly DivisionDiscordWebhook $divisionDiscordWebhook; public function setUp(): void { parent::setUp(); $this->filter = $this->app->make(WithdrawnWebhookFilter::class); - $this->ecfmpWebhook = $this->app->make(EcfmpWebhook::class); $this->divisionDiscordWebhook = DivisionDiscordWebhook::factory()->create(); } - public function testItShouldUseEcfmpWebhookIfItHasBeenNotified() - { - $measure = FlowMeasure::factory()->create(); - $discordNotification = DiscordNotification::factory()->create(); - $measure->discordNotifications()->attach( - [ - $discordNotification->id => [ - 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( - DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED - ), - 'notified_as' => $measure->identifier, - ], - ] - ); - $this->assertTrue( - $this->filter->shouldUseWebhook( - $measure, - $this->ecfmpWebhook - ) - ); - } - - public function testItShouldUseEcfmpWebhookIfItHasBeenActivated() - { - $measure = FlowMeasure::factory()->create(); - $discordNotification = DiscordNotification::factory()->create(); - $measure->discordNotifications()->attach( - [ - $discordNotification->id => [ - 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( - DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED - ), - 'notified_as' => $measure->identifier, - ], - ] - ); - $this->assertTrue( - $this->filter->shouldUseWebhook( - $measure, - $this->ecfmpWebhook - ) - ); - } - - public function testItShouldNotUseEcfmpWebhookIfItHasBeenWithdrawn() - { - $measure = FlowMeasure::factory()->create(); - $discordNotification = DiscordNotification::factory()->create(); - $measure->discordNotifications()->attach( - [ - $discordNotification->id => [ - 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( - DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED - ), - 'notified_as' => $measure->identifier, - ], - ] - ); - $measure->discordNotifications()->attach( - [ - $discordNotification->id => [ - 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( - DiscordNotificationTypeEnum::FLOW_MEASURE_WITHDRAWN - ), - 'notified_as' => $measure->identifier, - ], - ] - ); - - $this->assertFalse( - $this->filter->shouldUseWebhook( - $measure, - $this->ecfmpWebhook - ) - ); - } - - public function testItShouldNotUseEcfmpWebhookIfItHasBeenExpired() - { - $measure = FlowMeasure::factory()->create(); - $discordNotification = DiscordNotification::factory()->create(); - $measure->discordNotifications()->attach( - [ - $discordNotification->id => [ - 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( - DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED - ), - 'notified_as' => $measure->identifier, - ], - ] - ); - $measure->discordNotifications()->attach( - [ - $discordNotification->id => [ - 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( - DiscordNotificationTypeEnum::FLOW_MEASURE_EXPIRED - ), - 'notified_as' => $measure->identifier, - ], - ] - ); - - $this->assertFalse( - $this->filter->shouldUseWebhook( - $measure, - $this->ecfmpWebhook - ) - ); - } - public function testItShouldUseDivisionWebhookIfItHasBeenNotified() { $measure = FlowMeasure::factory()->create(); - $discordNotification = DiscordNotification::factory() + $discordNotification = DivisionDiscordNotification::factory() ->toDivisionWebhook($this->divisionDiscordWebhook) ->create(); - $measure->discordNotifications()->attach( + $measure->divisionDiscordNotifications()->attach( [ $discordNotification->id => [ 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( @@ -162,10 +49,10 @@ public function testItShouldUseDivisionWebhookIfItHasBeenNotified() public function testItShouldUseDivisionWebhookIfItHasBeenActivated() { $measure = FlowMeasure::factory()->create(); - $discordNotification = DiscordNotification::factory() + $discordNotification = DivisionDiscordNotification::factory() ->toDivisionWebhook($this->divisionDiscordWebhook) ->create(); - $measure->discordNotifications()->attach( + $measure->divisionDiscordNotifications()->attach( [ $discordNotification->id => [ 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( @@ -186,10 +73,10 @@ public function testItShouldUseDivisionWebhookIfItHasBeenActivated() public function testItShouldNotUseDivisionWebhookIfItHasBeenWithdrawn() { $measure = FlowMeasure::factory()->create(); - $discordNotification = DiscordNotification::factory() + $discordNotification = DivisionDiscordNotification::factory() ->toDivisionWebhook($this->divisionDiscordWebhook) ->create(); - $measure->discordNotifications()->attach( + $measure->divisionDiscordNotifications()->attach( [ $discordNotification->id => [ 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( @@ -199,7 +86,7 @@ public function testItShouldNotUseDivisionWebhookIfItHasBeenWithdrawn() ], ] ); - $measure->discordNotifications()->attach( + $measure->divisionDiscordNotifications()->attach( [ $discordNotification->id => [ 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( @@ -221,10 +108,10 @@ public function testItShouldNotUseDivisionWebhookIfItHasBeenWithdrawn() public function testItShouldNotUseDivisionWebhookIfItHasBeenExpired() { $measure = FlowMeasure::factory()->create(); - $discordNotification = DiscordNotification::factory() + $discordNotification = DivisionDiscordNotification::factory() ->toDivisionWebhook($this->divisionDiscordWebhook) ->create(); - $measure->discordNotifications()->attach( + $measure->divisionDiscordNotifications()->attach( [ $discordNotification->id => [ 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( @@ -234,7 +121,7 @@ public function testItShouldNotUseDivisionWebhookIfItHasBeenExpired() ], ] ); - $measure->discordNotifications()->attach( + $measure->divisionDiscordNotifications()->attach( [ $discordNotification->id => [ 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( diff --git a/tests/Discord/FlowMeasure/Webhook/WebhookMapperTest.php b/tests/Discord/FlowMeasure/Webhook/WebhookMapperTest.php index 8277c06e..f27a945d 100644 --- a/tests/Discord/FlowMeasure/Webhook/WebhookMapperTest.php +++ b/tests/Discord/FlowMeasure/Webhook/WebhookMapperTest.php @@ -4,7 +4,6 @@ use App\Discord\FlowMeasure\Webhook\Filter\FilterInterface; use App\Discord\FlowMeasure\Webhook\WebhookMapper; -use App\Discord\Webhook\EcfmpWebhook; use App\Discord\Webhook\WebhookInterface; use App\Models\DivisionDiscordWebhook; use App\Models\FlightInformationRegion; @@ -17,13 +16,11 @@ class WebhookMapperTest extends TestCase { private readonly FilterInterface $filter; private readonly WebhookMapper $mapper; - private readonly EcfmpWebhook $ecfmpWebhook; public function setUp(): void { parent::setUp(); $this->filter = Mockery::mock(FilterInterface::class); - $this->ecfmpWebhook = $this->app->make(EcfmpWebhook::class); $this->mapper = $this->app->make( WebhookMapper::class, [ @@ -36,24 +33,11 @@ public function testItReturnsEmptyCollectionWhenAllWebhooksAreFiltered() { $flowMeasure = FlowMeasure::factory()->create(); $this->filter->shouldReceive('shouldUseWebhook') - ->with($flowMeasure, $this->ecfmpWebhook) - ->once() - ->andReturnFalse(); + ->never(); $this->assertEmpty($this->mapper->mapToWebhooks($flowMeasure)); } - public function testItReturnsJustEcfmpWebhookIfNoDivisionWebhooks() - { - $flowMeasure = FlowMeasure::factory()->create(); - $this->filter->shouldReceive('shouldUseWebhook') - ->with($flowMeasure, $this->ecfmpWebhook) - ->once() - ->andReturnTrue(); - - $this->assertEquals(new Collection([$this->ecfmpWebhook]), $this->mapper->mapToWebhooks($flowMeasure)); - } - public function testItFiltersDivisionWebhooks() { $flowMeasure = FlowMeasure::factory()->create(); @@ -105,7 +89,11 @@ function (FlightInformationRegion $flightInformationRegion) { $this->filter->shouldReceive('shouldUseWebhook') ->andReturnTrue(); - $this->assertCount(3, $this->mapper->mapToWebhooks($flowMeasure)); + $this->assertCount(2, $this->mapper->mapToWebhooks($flowMeasure)); + $this->assertEquals( + DivisionDiscordWebhook::all()->pluck('id'), + $this->mapper->mapToWebhooks($flowMeasure)->map(fn (WebhookInterface $webhook) => $webhook->id()) + ); } public function testItDeduplicatesWebhooks() @@ -145,9 +133,9 @@ function (FlightInformationRegion $flightInformationRegion) use ($webhook1) { $this->filter->shouldReceive('shouldUseWebhook') ->andReturnTrue(); - $this->assertCount(3, $this->mapper->mapToWebhooks($flowMeasure)); + $this->assertCount(2, $this->mapper->mapToWebhooks($flowMeasure)); $this->assertEquals( - new Collection([null, $webhook1->id, $webhook2->id]), + new Collection([$webhook1->id, $webhook2->id]), $this->mapper->mapToWebhooks($flowMeasure)->map(fn (WebhookInterface $webhook) => $webhook->id()) ); } diff --git a/tests/Discord/Message/Embed/EmbedCollectionTest.php b/tests/Discord/Message/Embed/EmbedCollectionTest.php index 2e88441c..6f6a0811 100644 --- a/tests/Discord/Message/Embed/EmbedCollectionTest.php +++ b/tests/Discord/Message/Embed/EmbedCollectionTest.php @@ -3,6 +3,7 @@ namespace Tests\Discord\Message\Embed; use App\Discord\Message\Embed\AuthorInterface; +use App\Discord\Message\Embed\TitleInterface; use App\Discord\Message\Embed\Embed; use App\Discord\Message\Embed\EmbedCollection; use Mockery; @@ -34,4 +35,22 @@ public function testItMapsEmbeds() ->toArray() ); } + + public function testItMapsEmbedsToProtobuf() + { + $mockTitle1 = Mockery::mock(TitleInterface::class); + $mockTitle1->shouldReceive('title')->once()->andReturn('Foo'); + $mockTitle2 = Mockery::mock(TitleInterface::class); + $mockTitle2->shouldReceive('title')->once()->andReturn('Bar'); + + $embeds = (new EmbedCollection()) + ->add(Embed::make()->withTitle($mockTitle1)) + ->add(Embed::make()->withTitle($mockTitle2)) + ->toProtobuf(); + + $this->assertCount(2, $embeds); + + $this->assertEquals('Foo', $embeds[0]->getTitle()); + $this->assertEquals('Bar', $embeds[1]->getTitle()); + } } diff --git a/tests/Discord/Message/Embed/EmbedTest.php b/tests/Discord/Message/Embed/EmbedTest.php index 6cb120a5..16c9fdb5 100644 --- a/tests/Discord/Message/Embed/EmbedTest.php +++ b/tests/Discord/Message/Embed/EmbedTest.php @@ -149,4 +149,99 @@ public function testItHasAFooter() $this->assertEquals($expected, Embed::make()->withFooter($mockFooter)->toArray()); } + + public function testItHasATitleInProtobuf() + { + $mockTitle = Mockery::mock(TitleInterface::class); + $mockTitle->shouldReceive('title')->once()->andReturn('Foo'); + + + $this->assertEquals('Foo', Embed::make()->withTitle($mockTitle)->toProtobuf()->getTitle()); + } + + public function testItHasAColourInProtobuf() + { + $this->assertEquals( + Colour::ACTIVATED->value, + Embed::make()->withColour(Colour::ACTIVATED)->toProtobuf()->getColor() + ); + } + + + public function testItHasADescriptionInProtobuf() + { + $mockDescription = Mockery::mock(DescriptionInterface::class); + $mockDescription->shouldReceive('description')->once()->andReturn('Foo'); + + $this->assertEquals('Foo', Embed::make()->withDescription($mockDescription)->toProtobuf()->getDescription()); + } + + public function testItHasFieldsInProtobuf() + { + $field1 = Mockery::mock(FieldInterface::class); + $field1->shouldReceive('name')->once()->andReturn('Field1'); + $field1->shouldReceive('value')->once()->andReturn('Value1'); + $field1->shouldReceive('inline')->once()->andReturn(true); + $field2 = Mockery::mock(FieldInterface::class); + $field2->shouldReceive('name')->once()->andReturn('Field2'); + $field2->shouldReceive('value')->once()->andReturn('Value2'); + $field2->shouldReceive('inline')->once()->andReturn(false); + + $embed = Embed::make()->withField($field1)->withField($field2); + $embedProtobuf = $embed->toProtobuf(); + $embedField1 = $embedProtobuf->getFields()[0]; + + $this->assertEquals('Field1', $embedField1->getName()); + $this->assertEquals('Value1', $embedField1->getValue()); + $this->assertEquals(true, $embedField1->getInline()); + + $embedField2 = $embedProtobuf->getFields()[1]; + $this->assertEquals('Field2', $embedField2->getName()); + $this->assertEquals('Value2', $embedField2->getValue()); + $this->assertEquals(false, $embedField2->getInline()); + } + + public function testItSkipsFieldsByConditionInProtobuf() + { + $field1 = Mockery::mock(FieldInterface::class); + $field1->shouldReceive('name')->once()->andReturn('Field1'); + $field1->shouldReceive('value')->once()->andReturn('Value1'); + $field1->shouldReceive('inline')->once()->andReturn(true); + $field2 = Mockery::mock(FieldInterface::class); + + $embed = Embed::make()->withField($field1)->withField($field2, false); + $embedProtobuf = $embed->toProtobuf(); + $embedField1 = $embedProtobuf->getFields()[0]; + + $this->assertEquals('Field1', $embedField1->getName()); + $this->assertEquals('Value1', $embedField1->getValue()); + $this->assertEquals(true, $embedField1->getInline()); + + $this->assertCount(1, $embedProtobuf->getFields()); + } + + public function testItHasFieldsByCollectionInProtobuf() + { + $field1 = Mockery::mock(FieldInterface::class); + $field1->shouldReceive('name')->once()->andReturn('Field1'); + $field1->shouldReceive('value')->once()->andReturn('Value1'); + $field1->shouldReceive('inline')->once()->andReturn(true); + $field2 = Mockery::mock(FieldInterface::class); + $field2->shouldReceive('name')->once()->andReturn('Field2'); + $field2->shouldReceive('value')->once()->andReturn('Value2'); + $field2->shouldReceive('inline')->once()->andReturn(false); + + $embed = Embed::make()->withFields(collect([$field1, $field2])); + $embedProtobuf = $embed->toProtobuf(); + $embedField1 = $embedProtobuf->getFields()[0]; + + $this->assertEquals('Field1', $embedField1->getName()); + $this->assertEquals('Value1', $embedField1->getValue()); + $this->assertEquals(true, $embedField1->getInline()); + + $embedField2 = $embedProtobuf->getFields()[1]; + $this->assertEquals('Field2', $embedField2->getName()); + $this->assertEquals('Value2', $embedField2->getValue()); + $this->assertEquals(false, $embedField2->getInline()); + } } diff --git a/tests/Discord/Message/Sender/SenderTest.php b/tests/Discord/Message/Sender/DivisionWebhookSenderTest.php similarity index 86% rename from tests/Discord/Message/Sender/SenderTest.php rename to tests/Discord/Message/Sender/DivisionWebhookSenderTest.php index a04c0a8d..d8b8ddbc 100644 --- a/tests/Discord/Message/Sender/SenderTest.php +++ b/tests/Discord/Message/Sender/DivisionWebhookSenderTest.php @@ -2,18 +2,18 @@ namespace Tests\Discord\Message\Sender; -use App\Discord\DiscordInterface; +use App\Discord\DiscordWebhookInterface; use App\Discord\FlowMeasure\Message\MessageGeneratorInterface; use App\Discord\Message\Associator\AssociatorInterface; use App\Discord\Message\Embed\EmbedCollection; use App\Discord\Message\Logger\LoggerInterface; use App\Discord\Message\MessageInterface; -use App\Discord\Message\Sender\Sender; +use App\Discord\Message\Sender\DivisionWebhookSender; use App\Models\DivisionDiscordWebhook; use Mockery; use Tests\TestCase; -class SenderTest extends TestCase +class DivisionWebhookSenderTest extends TestCase { public function testItSendsMessages() { @@ -43,15 +43,15 @@ public function testItSendsMessages() $mockGenerator = Mockery::mock(MessageGeneratorInterface::class); $mockGenerator->expects('generate')->andReturn(collect([$message1, $message2])); - $mockDiscord = Mockery::mock(DiscordInterface::class); + $mockDiscord = Mockery::mock(DiscordWebhookInterface::class); $mockDiscord->shouldReceive('sendMessage')->with($message1)->andReturnTrue(); $mockDiscord->shouldReceive('sendMessage')->with($message2)->andReturnTrue(); - $sender = new Sender([$mockGenerator], $mockDiscord); + $sender = new DivisionWebhookSender([$mockGenerator], $mockDiscord); $sender->sendDiscordMessages(); $this->assertDatabaseHas( - 'discord_notifications', + 'division_discord_notifications', [ 'division_discord_webhook_id' => $divisionWebhook->id, 'content' => 'foo', @@ -59,7 +59,7 @@ public function testItSendsMessages() ); $this->assertDatabaseHas( - 'discord_notifications', + 'division_discord_notifications', [ 'division_discord_webhook_id' => $divisionWebhook->id, 'content' => 'bar', diff --git a/tests/Discord/Webhook/EcfmpWebhookTest.php b/tests/Discord/Webhook/EcfmpWebhookTest.php deleted file mode 100644 index efd940cc..00000000 --- a/tests/Discord/Webhook/EcfmpWebhookTest.php +++ /dev/null @@ -1,37 +0,0 @@ -assertNull((new EcfmpWebhook())->id()); - } - - public function testItHasAUrl() - { - $this->assertEquals( - 'foo', - (new EcfmpWebhook())->url() - ); - } - - public function testItHasADescription() - { - $this->assertEquals( - 'ECFMP', - (new EcfmpWebhook())->description() - ); - } -} diff --git a/tests/Feature/Filament/FlowMeasureResourceTest.php b/tests/Feature/Filament/FlowMeasureResourceTest.php index 0285efe9..ebee9913 100644 --- a/tests/Feature/Filament/FlowMeasureResourceTest.php +++ b/tests/Feature/Filament/FlowMeasureResourceTest.php @@ -2,9 +2,11 @@ use App\Enums\FlowMeasureType; use App\Filament\Resources\FlowMeasureResource; +use App\Helpers\FlowMeasureIdentifierGenerator; use App\Models\FlowMeasure; use App\Models\FlightInformationRegion; use App\Models\User; +use Carbon\Carbon; use Illuminate\Support\Arr; use Tests\FrontendTestCase; @@ -81,7 +83,11 @@ ->set("data.ades.{$adesKey}.custom_value", $newData->filters[1]['value'][0]) ->call('create'); + $latestFlowMeasure = FlowMeasure::latest()->first(); $this->assertDatabaseHas(FlowMeasure::class, [ + 'identifier' => $latestFlowMeasure->identifier, + 'canonical_identifier' => FlowMeasureIdentifierGenerator::canonicalIdentifier($latestFlowMeasure->identifier), + 'revision_number' => FlowMeasureIdentifierGenerator::timesRevised($latestFlowMeasure->identifier), 'flight_information_region_id' => $newData->flight_information_region_id, 'event_id' => $newData->event_id, 'start_time' => $newData->start_time->startOfMinute(), @@ -290,7 +296,9 @@ $this->actingAs(User::factory()->system()->create()); /** @var FlowMeasure $flowMeasure */ - $flowMeasure = FlowMeasure::factory()->create(); + $flowMeasure = FlowMeasure::factory()->create(['start_time' => Carbon::now()->addMinutes(35)]); + $flowMeasure->notifiedFlightInformationRegions()->attach(FlightInformationRegion::factory()->create()); + $nextIdentifier = FlowMeasureIdentifierGenerator::generateIdentifier($flowMeasure->start_time, $flowMeasure->flightInformationRegion); /** @var FlowMeasure $newData */ $newData = FlowMeasure::factory()->make(); @@ -298,6 +306,7 @@ $livewire = livewire(FlowMeasureResource\Pages\EditFlowMeasure::class, [ 'record' => $flowMeasure->getKey(), ]) + ->set('data.flight_information_region_id', $newData->flight_information_region_id) ->set('data.reason', $newData->reason) ->set('data.type', $newData->type->value) ->set('data.value', $newData->value) @@ -310,7 +319,57 @@ $adepKey = key($adep); $adesKey = key($ades); - $livewire->set("data.adep.{$adepKey}.value_type", 'custom_value') + $test = $livewire->set("data.adep.{$adepKey}.value_type", 'custom_value') + ->set("data.adep.{$adepKey}.airport_group", null) + ->set("data.adep.{$adepKey}.custom_value", $newData->filters[0]['value'][0]) + ->set("data.ades.{$adesKey}.value_type", 'custom_value') + ->set("data.ades.{$adesKey}.airport_group", null) + ->set("data.ades.{$adesKey}.custom_value", $newData->filters[1]['value'][0]) + ->call('save'); + + $newFlowMeasure = FlowMeasure::where('identifier', $nextIdentifier)->first(); + + expect($newFlowMeasure)->toMatchArray([ + // TODO Re-enable this after I know why it doesn't update in test, but does in front-end + // 'reason' => $newData->reason, + 'identifier' => $nextIdentifier, + 'canonical_identifier' => $nextIdentifier, + 'revision_number' => 0, + 'type' => $newData->type, + 'value' => $newData->value, + 'mandatory_route' => $newData->mandatory_route, + ]); +}); + +it('can edit with revision', function () { + /** @var FrontendTestCase $this */ + $this->actingAs(User::factory()->system()->create()); + + /** @var FlowMeasure $flowMeasure */ + $flowMeasure = FlowMeasure::factory()->create(['start_time' => Carbon::now()->addMinutes(15)]); + $flowMeasure->notifiedFlightInformationRegions()->attach(FlightInformationRegion::factory()->create()); + $originalIdentifier = $flowMeasure->identifier; + $nextIdentifier = FlowMeasureIdentifierGenerator::generateRevisedIdentifier($flowMeasure); + /** @var FlowMeasure $newData */ + $newData = FlowMeasure::factory()->make(); + + $livewire = livewire(FlowMeasureResource\Pages\EditFlowMeasure::class, [ + 'record' => $flowMeasure->getKey(), + ]) + ->set('data.flight_information_region_id', $newData->flight_information_region_id) + ->set('data.reason', $newData->reason) + ->set('data.type', $newData->type->value) + ->set('data.value', $newData->value) + ->set('data.mandatory_route', $newData->mandatory_route); + + /** @var array $data */ + $data = $livewire->get('data'); + $adep = Arr::get($data, 'adep'); + $ades = Arr::get($data, 'ades'); + $adepKey = key($adep); + $adesKey = key($ades); + + $test = $livewire->set("data.adep.{$adepKey}.value_type", 'custom_value') ->set("data.adep.{$adepKey}.airport_group", null) ->set("data.adep.{$adepKey}.custom_value", $newData->filters[0]['value'][0]) ->set("data.ades.{$adesKey}.value_type", 'custom_value') @@ -321,6 +380,9 @@ expect($flowMeasure->refresh())->toMatchArray([ // TODO Re-enable this after I know why it doesn't update in test, but does in front-end // 'reason' => $newData->reason, + 'identifier' => $nextIdentifier, + 'canonical_identifier' => $originalIdentifier, + 'revision_number' => 1, 'type' => $newData->type, 'value' => $newData->value, 'mandatory_route' => $newData->mandatory_route, diff --git a/tests/Helpers/FlowMeasureIdentifierGeneratorTest.php b/tests/Helpers/FlowMeasureIdentifierGeneratorTest.php index c4e15ffb..7a1942e4 100644 --- a/tests/Helpers/FlowMeasureIdentifierGeneratorTest.php +++ b/tests/Helpers/FlowMeasureIdentifierGeneratorTest.php @@ -7,6 +7,7 @@ use App\Models\FlowMeasure; use Carbon\Carbon; use Illuminate\Support\Facades\DB; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\TestCase; class FlowMeasureIdentifierGeneratorTest extends TestCase @@ -156,4 +157,26 @@ public function revisionIdentifierProvider(): array 'Tenth revision' => ['EGTT01A-11', 10], ]; } + + #[DataProvider('canonicalIdentifierProvider')] + public function testItGeneratesCanonicalIdentifiers( + string $identifier, + string $expected + ) { + $this->assertEquals( + $expected, + FlowMeasureIdentifierGenerator::canonicalIdentifier( + FlowMeasure::factory()->make(['identifier' => $identifier]) + ) + ); + } + + public function canonicalIdentifierProvider(): array + { + return [ + 'No revision' => ['EGTT01A', 'EGTT01A'], + 'First revision' => ['EGTT31B-2', 'EGTT31BA'], + 'Tenth revision' => ['EGTT05A-11', 'EGTT05A'], + ]; + } } diff --git a/tests/Jobs/SendDiscordNotificationsTest.php b/tests/Jobs/SendDiscordNotificationsTest.php index d1473fde..1cf9f1d2 100644 --- a/tests/Jobs/SendDiscordNotificationsTest.php +++ b/tests/Jobs/SendDiscordNotificationsTest.php @@ -2,7 +2,8 @@ namespace Tests\Jobs; -use App\Discord\Message\Sender\Sender; +use App\Discord\FlowMeasure\Generator\EcfmpFlowMeasureMessageGenerator; +use App\Discord\Message\Sender\DivisionWebhookSender; use App\Jobs\SendDiscordNotifications; use Illuminate\Support\Facades\Config; use Mockery; @@ -14,9 +15,13 @@ public function testItRunsNotificationSending() { Config::set('discord.enabled', true); - $senderMock = Mockery::mock(Sender::class); + $senderMock = Mockery::mock(DivisionWebhookSender::class); $senderMock->shouldReceive('sendDiscordMessages')->once(); - $this->app->instance(Sender::class, $senderMock); + $this->app->instance(DivisionWebhookSender::class, $senderMock); + + $serviceSenderMock = Mockery::mock(EcfmpFlowMeasureMessageGenerator::class); + $serviceSenderMock->shouldReceive('generateAndSend')->once(); + $this->app->instance(EcfmpFlowMeasureMessageGenerator::class, $serviceSenderMock); $job = $this->app->make(SendDiscordNotifications::class); $job->handle(); @@ -26,9 +31,13 @@ public function testItDoesntRunsNotificationSendingIfSwitchedOff() { Config::set('discord.enabled', false); - $senderMock = Mockery::mock(Sender::class); + $senderMock = Mockery::mock(DivisionWebhookSender::class); $senderMock->shouldReceive('sendDiscordMessages')->never(); - $this->app->instance(Sender::class, $senderMock); + $this->app->instance(DivisionWebhookSender::class, $senderMock); + + $serviceSenderMock = Mockery::mock(EcfmpFlowMeasureMessageGenerator::class); + $serviceSenderMock->shouldReceive('generateAndSend')->never(); + $this->app->instance(EcfmpFlowMeasureMessageGenerator::class, $serviceSenderMock); $job = $this->app->make(SendDiscordNotifications::class); $job->handle(); diff --git a/tests/Repository/FlowMeasureNotification/ActiveRepositoryTest.php b/tests/Repository/FlowMeasureNotification/ActiveRepositoryTest.php index b15a8ed8..cb840b34 100644 --- a/tests/Repository/FlowMeasureNotification/ActiveRepositoryTest.php +++ b/tests/Repository/FlowMeasureNotification/ActiveRepositoryTest.php @@ -2,9 +2,11 @@ namespace Tests\Repository\FlowMeasureNotification; -use App\Enums\DiscordNotificationType; +use App\Enums\DiscordNotificationType as DiscordNotificationTypeEnum; +use App\Models\DiscordNotificationType; use App\Models\FlowMeasure; use App\Repository\FlowMeasureNotification\ActiveRepository; +use Illuminate\Support\Str; use Tests\TestCase; class ActiveRepositoryTest extends TestCase @@ -20,7 +22,7 @@ public function setUp(): void public function testItHasANotificationType() { $this->assertEquals( - DiscordNotificationType::FLOW_MEASURE_ACTIVATED, + DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED, $this->repository->notificationType() ); } @@ -59,4 +61,149 @@ public function testItIgnoresExpiredFlowMeasures() $this->assertEmpty($this->repository->flowMeasuresForNotification()); } + + public function testItReturnsEcfmpMeasuresToSend() + { + // Should send, is active and never notified + [$measure1, $measure2] = FlowMeasure::factory()->count(2)->create(); + + // Active, but should send, has been notified with a different identifier + $measure3 = FlowMeasure::factory() + ->afterCreating( + function (FlowMeasure $measure) { + $measure->discordNotifications()->create( + [ + 'remote_id' => Str::uuid(), + ], + joining: [ + 'discord_notification_type_id' => DiscordNotificationType::idFromEnum(DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED), + 'notified_as' => 'different_identifier', + ] + ); + } + ) + ->create(); + + // Active, but should send, has been notified as a notified type + $measure4 = FlowMeasure::factory() + ->afterCreating( + function (FlowMeasure $measure) { + $measure->discordNotifications()->create( + [ + 'remote_id' => Str::uuid(), + ], + joining: [ + 'discord_notification_type_id' => DiscordNotificationType::idFromEnum(DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED), + 'notified_as' => 'different_identifier', + ] + ); + } + ) + ->create(); + + // Should not send, is only notified + FlowMeasure::factory()->notStarted()->count(1)->create(); + + // Should not send, is expired + FlowMeasure::factory()->finished()->count(1)->create(); + + // Should not send, is withdrawn + FlowMeasure::factory()->withdrawn()->count(1)->create(); + + // Active, but should not send, has already been notified with this identifier + FlowMeasure::factory() + ->afterCreating( + function (FlowMeasure $measure) { + $measure->discordNotifications()->create( + [ + 'remote_id' => Str::uuid(), + ], + joining: [ + 'discord_notification_type_id' => DiscordNotificationType::idFromEnum(DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED), + 'notified_as' => $measure->identifier, + ] + ); + } + ) + ->create(); + + $this->assertEquals( + [$measure1->id, $measure2->id, $measure3->id, $measure4->id], + $this->repository->flowMeasuresToBeSentToEcfmp()->pluck('measure.id')->toArray() + ); + } + + public function testItIsNotReissueIfNeverPreviouslyActivated() + { + // Should send, is active and never notified + $measure = FlowMeasure::factory()->create(); + $measuresToNotify = $this->repository->flowMeasuresToBeSentToEcfmp(); + + $this->assertCount(1, $measuresToNotify); + $this->assertEquals($measure->id, $measuresToNotify->first()->measure->id); + $this->assertFalse($measuresToNotify->first()->isReissuedNotification); + } + + public function testItIsNotReissueIfPreviousVersionWasNotifiedForTheSameIdentifier() + { + // Should send, is active and never notified + $measure = FlowMeasure::factory()->create(); + $measure->discordNotifications()->create( + [ + 'remote_id' => Str::uuid(), + ], + joining: [ + 'discord_notification_type_id' => DiscordNotificationType::idFromEnum(DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED), + 'notified_as' => $measure->identifier, + ] + ); + + $measuresToNotify = $this->repository->flowMeasuresToBeSentToEcfmp(); + + $this->assertCount(1, $measuresToNotify); + $this->assertEquals($measure->id, $measuresToNotify->first()->measure->id); + $this->assertFalse($measuresToNotify->first()->isReissuedNotification); + } + + public function testItIsReissuedIfPreviousWasNotifiedUnderDifferentIdentifier() + { + // Should send, is active and never notified + $measure = FlowMeasure::factory()->create(); + $measure->discordNotifications()->create( + [ + 'remote_id' => Str::uuid(), + ], + joining: [ + 'discord_notification_type_id' => DiscordNotificationType::idFromEnum(DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED), + 'notified_as' => 'something_else', + ] + ); + + $measuresToNotify = $this->repository->flowMeasuresToBeSentToEcfmp(); + + $this->assertCount(1, $measuresToNotify); + $this->assertEquals($measure->id, $measuresToNotify->first()->measure->id); + $this->assertTrue($measuresToNotify->first()->isReissuedNotification); + } + + public function testItIsReissuedIfPreviousWasActivatedUnderDifferentIdentifier() + { + // Should send, is active and never notified + $measure = FlowMeasure::factory()->create(); + $measure->discordNotifications()->create( + [ + 'remote_id' => Str::uuid(), + ], + joining: [ + 'discord_notification_type_id' => DiscordNotificationType::idFromEnum(DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED), + 'notified_as' => 'something_else', + ] + ); + + $measuresToNotify = $this->repository->flowMeasuresToBeSentToEcfmp(); + + $this->assertCount(1, $measuresToNotify); + $this->assertEquals($measure->id, $measuresToNotify->first()->measure->id); + $this->assertTrue($measuresToNotify->first()->isReissuedNotification); + } } diff --git a/tests/Repository/FlowMeasureNotification/ExpiredRepositoryTest.php b/tests/Repository/FlowMeasureNotification/ExpiredRepositoryTest.php index ba56990e..4ce7ecec 100644 --- a/tests/Repository/FlowMeasureNotification/ExpiredRepositoryTest.php +++ b/tests/Repository/FlowMeasureNotification/ExpiredRepositoryTest.php @@ -2,9 +2,13 @@ namespace Tests\Repository\FlowMeasureNotification; -use App\Enums\DiscordNotificationType; +use App\Enums\DiscordNotificationType as DiscordNotificationTypeEnum; +use App\Models\DiscordNotification; +use App\Models\DiscordNotificationType; use App\Models\FlowMeasure; use App\Repository\FlowMeasureNotification\ExpiredRepository; +use Carbon\Carbon; +use Illuminate\Support\Str; use Tests\TestCase; class ExpiredRepositoryTest extends TestCase @@ -14,13 +18,13 @@ class ExpiredRepositoryTest extends TestCase public function setUp(): void { parent::setUp(); - $this->repository = new ExpiredRepository(); + $this->repository = $this->app->make(ExpiredRepository::class); } public function testItHasANotificationType() { $this->assertEquals( - DiscordNotificationType::FLOW_MEASURE_EXPIRED, + DiscordNotificationTypeEnum::FLOW_MEASURE_EXPIRED, $this->repository->notificationType() ); } @@ -63,4 +67,204 @@ public function testItIgnoresActiveFlowMeasures() FlowMeasure::factory()->count(3)->create(); $this->assertEmpty($this->repository->flowMeasuresForNotification()); } + + public function testItReturnsEmptyForEcfmpMeasuresIfMoreThanFiveNotificationsSentInLastTwoHours() + { + DiscordNotification::factory()->count(6)->create(['created_at' => Carbon::now()->subHours(2)->addMinute()]); + + // Would be sent to ECFMP if not for the webhook limit + FlowMeasure::factory()->finishedRecently()->count(3)->create(); + + $this->assertEmpty($this->repository->flowMeasuresToBeSentToEcfmp()); + } + + public function testItReturnsEcfmpMeasures() + { + // Will be sent, notified as notified previously + $flowMeasure1 = FlowMeasure::factory()->finishedRecently()->afterCreating( + function (FlowMeasure $measure) { + $notification = $measure->discordNotifications()->create( + [ + 'remote_id' => Str::uuid(), + ], + joining: [ + 'discord_notification_type_id' => DiscordNotificationType::idFromEnum(DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED), + 'notified_as' => $measure->identifier, + ] + ); + $notification->created_at = Carbon::now()->subHours(3); + $notification->save(); + } + )->create(); + + // Will be sent, notified as activated previously + $flowMeasure2 = FlowMeasure::factory()->finishedRecently()->afterCreating( + function (FlowMeasure $measure) { + $notification = $measure->discordNotifications()->create( + [ + 'remote_id' => Str::uuid(), + ], + joining: [ + 'discord_notification_type_id' => DiscordNotificationType::idFromEnum(DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED), + 'notified_as' => $measure->identifier, + ] + ); + $notification->created_at = Carbon::now()->subHours(3); + $notification->save(); + } + )->create(); + + // Wont be sent, high revision number + FlowMeasure::factory()->finishedRecently()->afterCreating( + function (FlowMeasure $measure) { + $measure->revision_number = 2; + $measure->save(); + } + )->create(); + + // Wont be sent, never previously notified + FlowMeasure::factory()->finishedRecently()->create(); + + // Wont be sent, notified as expired previously + FlowMeasure::factory()->finishedRecently()->afterCreating( + function (FlowMeasure $measure) { + $notification1 = $measure->discordNotifications()->create( + [ + 'created_at' => Carbon::now()->subHours(3), + 'remote_id' => Str::uuid(), + ], + joining: [ + 'discord_notification_type_id' => DiscordNotificationType::idFromEnum(DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED), + 'notified_as' => $measure->identifier, + ] + ); + $notification1->created_at = Carbon::now()->subHours(3); + $notification1->save(); + + $notification2 = $measure->discordNotifications()->create( + [ + 'created_at' => Carbon::now()->subHours(3), + 'remote_id' => Str::uuid(), + ], + joining: [ + 'discord_notification_type_id' => DiscordNotificationType::idFromEnum(DiscordNotificationTypeEnum::FLOW_MEASURE_EXPIRED), + 'notified_as' => $measure->identifier, + ] + ); + $notification2->created_at = Carbon::now()->subHours(3); + $notification2->save(); + } + )->create(); + + // Won't be sent, notified as withdrawn previously + FlowMeasure::factory()->finishedRecently()->afterCreating( + function (FlowMeasure $measure) { + $notification1 = $measure->discordNotifications()->create( + [ + 'created_at' => Carbon::now()->subHours(3), + 'remote_id' => Str::uuid(), + ], + joining: [ + 'discord_notification_type_id' => DiscordNotificationType::idFromEnum(DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED), + 'notified_as' => $measure->identifier, + ] + ); + $notification1->created_at = Carbon::now()->subHours(3); + $notification1->save(); + + $notification2 = $measure->discordNotifications()->create( + [ + 'created_at' => Carbon::now()->subHours(3), + 'remote_id' => Str::uuid(), + ], + joining: [ + 'discord_notification_type_id' => DiscordNotificationType::idFromEnum(DiscordNotificationTypeEnum::FLOW_MEASURE_WITHDRAWN), + 'notified_as' => $measure->identifier, + ] + ); + + $notification2->created_at = Carbon::now()->subHours(3); + $notification2->save(); + } + )->create(); + + // Won't be sent, notification sent as notified but still notified + FlowMeasure::factory()->notified()->afterCreating( + function (FlowMeasure $measure) { + $notification = $measure->discordNotifications()->create( + [ + 'created_at' => Carbon::now()->subHours(3), + 'remote_id' => Str::uuid(), + ], + joining: [ + 'discord_notification_type_id' => DiscordNotificationType::idFromEnum(DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED), + 'notified_as' => $measure->identifier, + ] + ); + + $notification->created_at = Carbon::now()->subHours(3); + $notification->save(); + } + )->create(); + + // Won't be sent, notification sent as active but still active + FlowMeasure::factory()->afterCreating( + function (FlowMeasure $measure) { + $measure->discordNotifications()->create( + [ + 'created_at' => Carbon::now()->subHours(3), + 'remote_id' => Str::uuid(), + ], + joining: [ + 'discord_notification_type_id' => DiscordNotificationType::idFromEnum(DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED), + 'notified_as' => $measure->identifier, + ] + ); + } + )->create(); + + // Won't be sent, notified as active but expired a long time ago + FlowMeasure::factory()->finishedAWhileAgo()->afterCreating( + function (FlowMeasure $measure) { + $measure->discordNotifications()->create( + [ + 'created_at' => Carbon::now()->subHours(3), + 'remote_id' => Str::uuid(), + ], + joining: [ + 'discord_notification_type_id' => DiscordNotificationType::idFromEnum(DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED), + 'notified_as' => $measure->identifier, + ] + ); + } + )->create(); + + $this->assertEquals( + [$flowMeasure1->id, $flowMeasure2->id], + $this->repository->flowMeasuresToBeSentToEcfmp()->pluck('measure.id')->toArray() + ); + } + + public function testItIsNotReissued() + { + // Will be sent, notified as notified previously + $measure = FlowMeasure::factory()->finishedRecently()->afterCreating( + function (FlowMeasure $measure) { + $notification = $measure->discordNotifications()->create( + [ + 'remote_id' => Str::uuid(), + ], + joining: [ + 'discord_notification_type_id' => DiscordNotificationType::idFromEnum(DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED), + 'notified_as' => $measure->identifier, + ] + ); + $notification->created_at = Carbon::now()->subHours(3); + $notification->save(); + } + )->create(); + + $this->assertEquals($measure->id, $this->repository->flowMeasuresToBeSentToEcfmp()->first()->measure->id); + $this->assertFalse($this->repository->flowMeasuresToBeSentToEcfmp()->first()->isReissuedNotification); + } } diff --git a/tests/Repository/FlowMeasureNotification/NotifiedRepositoryTest.php b/tests/Repository/FlowMeasureNotification/NotifiedRepositoryTest.php index 35255f91..8b03500b 100644 --- a/tests/Repository/FlowMeasureNotification/NotifiedRepositoryTest.php +++ b/tests/Repository/FlowMeasureNotification/NotifiedRepositoryTest.php @@ -2,9 +2,11 @@ namespace Tests\Repository\FlowMeasureNotification; -use App\Enums\DiscordNotificationType; +use App\Enums\DiscordNotificationType as DiscordNotificationTypeEnum; +use App\Models\DiscordNotificationType; use App\Models\FlowMeasure; use App\Repository\FlowMeasureNotification\NotifiedRepository; +use Illuminate\Support\Str; use Tests\TestCase; class NotifiedRepositoryTest extends TestCase @@ -20,7 +22,7 @@ public function setUp(): void public function testItHasANotificationType() { $this->assertEquals( - DiscordNotificationType::FLOW_MEASURE_NOTIFIED, + DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED, $this->repository->notificationType() ); } @@ -57,4 +59,105 @@ function (FlowMeasure $measure) { $this->assertEmpty($this->repository->flowMeasuresForNotification()); } + + public function testItReturnsMeasuresToBeSentToEcfmp() + { + // Should be sent, never notified + [$measure1, $measure2] = FlowMeasure::factory()->notified()->count(2)->create(); + + // Should be sent, previously notified under a different identifier + $measure3 = FlowMeasure::factory()->notified()->afterCreating( + function (FlowMeasure $measure) { + $measure->discordNotifications()->create( + [ + 'remote_id' => Str::uuid(), + ], + joining: [ + 'discord_notification_type_id' => DiscordNotificationType::idFromEnum(DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED), + 'notified_as' => 'different_identifier', + ] + ); + } + ) + ->create(); + + // Should not be sent, has been previously activated + FlowMeasure::factory()->afterCreating( + function (FlowMeasure $measure) { + $measure->discordNotifications()->create( + [ + 'remote_id' => Str::uuid(), + ], + joining: [ + 'discord_notification_type_id' => DiscordNotificationType::idFromEnum(DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED), + 'notified_as' => $measure->identifier, + ] + ); + } + ); + + // Should not be sent, already notified with this identifier + FlowMeasure::factory()->afterCreating( + function (FlowMeasure $measure) { + $measure->discordNotifications()->create( + [ + 'remote_id' => Str::uuid(), + ], + joining: [ + 'discord_notification_type_id' => DiscordNotificationType::idFromEnum(DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED), + 'notified_as' => $measure->identifier, + ] + ); + } + ); + + // Should not be sent, already active + FlowMeasure::factory()->count(3)->create(); + + // Should not be sent, not yet in notified range + FlowMeasure::factory()->notNotified()->count(3)->create(); + + // Should not be sent, expired + FlowMeasure::factory()->finished()->count(3)->create(); + + // Should not be sent, withdrawn + FlowMeasure::factory()->notified()->withdrawn()->count(3)->create(); + + $this->assertEquals( + [$measure1->id, $measure2->id, $measure3->id], + $this->repository->flowMeasuresToBeSentToEcfmp()->pluck('measure.id')->toArray() + ); + } + + public function testItIsNotReissueIfNeverPreviouslyNotified() + { + // Should send, is active and never notified + $measure = FlowMeasure::factory()->notified()->create(); + $measuresToNotify = $this->repository->flowMeasuresToBeSentToEcfmp(); + + $this->assertCount(1, $measuresToNotify); + $this->assertEquals($measure->id, $measuresToNotify->first()->measure->id); + $this->assertFalse($measuresToNotify->first()->isReissuedNotification); + } + + public function testItIsReissuedIfPreviousWasNotifiedUnderDifferentIdentifier() + { + // Should send, is active and never notified + $measure = FlowMeasure::factory()->notified()->create(); + $measure->discordNotifications()->create( + [ + 'remote_id' => Str::uuid(), + ], + joining: [ + 'discord_notification_type_id' => DiscordNotificationType::idFromEnum(DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED), + 'notified_as' => 'something_else', + ] + ); + + $measuresToNotify = $this->repository->flowMeasuresToBeSentToEcfmp(); + + $this->assertCount(1, $measuresToNotify); + $this->assertEquals($measure->id, $measuresToNotify->first()->measure->id); + $this->assertTrue($measuresToNotify->first()->isReissuedNotification); + } } diff --git a/tests/Repository/FlowMeasureNotification/WithdrawnRepositoryTest.php b/tests/Repository/FlowMeasureNotification/WithdrawnRepositoryTest.php index 81311b61..eaaae252 100644 --- a/tests/Repository/FlowMeasureNotification/WithdrawnRepositoryTest.php +++ b/tests/Repository/FlowMeasureNotification/WithdrawnRepositoryTest.php @@ -2,10 +2,13 @@ namespace Tests\Repository\FlowMeasureNotification; -use App\Enums\DiscordNotificationType; +use App\Enums\DiscordNotificationType as DiscordNotificationTypeEnum; +use App\Models\DiscordNotificationType; use App\Models\FlowMeasure; use App\Repository\FlowMeasureNotification\WithdrawnRepository; use Carbon\Carbon; +use DB; +use Illuminate\Support\Str; use Tests\TestCase; class WithdrawnRepositoryTest extends TestCase @@ -21,7 +24,7 @@ public function setUp(): void public function testItHasANotificationType() { $this->assertEquals( - DiscordNotificationType::FLOW_MEASURE_WITHDRAWN, + DiscordNotificationTypeEnum::FLOW_MEASURE_WITHDRAWN, $this->repository->notificationType() ); } @@ -114,4 +117,145 @@ public function testItIgnoresDeletedExpiredFlowMeasures() $this->assertEmpty($this->repository->flowMeasuresForNotification()); } + + public function testItReturnsFlowMeasuresToSendToEcfmp() + { + // Should be sent, was notified and is now withdrawn + $measure1 = FlowMeasure::factory() + ->withdrawn() + ->afterCreating(function (FlowMeasure $measure) { + $measure->discordNotifications()->create( + [ + 'remote_id' => Str::uuid(), + ], + joining: [ + 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( + DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED + ), + 'notified_as' => $measure->identifier + ] + ); + }) + ->create(); + + // Should be sent, was active and is now withdrawn + $measure2 = FlowMeasure::factory() + ->withdrawn() + ->afterCreating(function (FlowMeasure $measure) { + $measure->discordNotifications()->create( + [ + 'remote_id' => Str::uuid(), + ], + joining: [ + 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( + DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED + ), + 'notified_as' => $measure->identifier + ] + ); + }) + ->create(); + + // Should not be sent, is active + FlowMeasure::factory() + ->afterCreating(function (FlowMeasure $measure) { + $measure->discordNotifications()->create( + [ + 'remote_id' => Str::uuid(), + ], + joining: [ + 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( + DiscordNotificationTypeEnum::FLOW_MEASURE_ACTIVATED + ), + 'notified_as' => $measure->identifier + ] + ); + }) + ->create(); + + // Should not be sent, is notified + FlowMeasure::factory() + ->afterCreating(function (FlowMeasure $measure) { + $measure->discordNotifications()->create( + [ + 'remote_id' => Str::uuid(), + ], + joining: [ + 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( + DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED + ), + 'notified_as' => $measure->identifier + ] + ); + }) + ->notified() + ->create(); + + // Should not be sent, is widthdrawn but already notified as withdrawn with some identifier + FlowMeasure::factory() + ->withdrawn() + ->afterCreating(function (FlowMeasure $measure) { + $measure->discordNotifications()->create( + [ + 'remote_id' => Str::uuid(), + ], + joining: [ + 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( + DiscordNotificationTypeEnum::FLOW_MEASURE_WITHDRAWN + ), + 'notified_as' => 'an_identifier' + ] + ); + }) + ->create(); + + // Should not be sent, is widthdrawn but already notified as expired with some identifier + FlowMeasure::factory() + ->withdrawn() + ->afterCreating(function (FlowMeasure $measure) { + $measure->discordNotifications()->create( + [ + 'remote_id' => Str::uuid(), + ], + joining: [ + 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( + DiscordNotificationTypeEnum::FLOW_MEASURE_EXPIRED + ), + 'notified_as' => 'an_identifier' + ] + ); + }) + ->create(); + // DB::commit(); + // dd('hi'); + + $this->assertEquals( + [$measure1->id, $measure2->id], + $this->repository->flowMeasuresToBeSentToEcfmp()->pluck('measure.id')->toArray() + ); + } + + public function testItIsNotReissued() + { + // Should be sent, was notified and is now withdrawn + $measure = FlowMeasure::factory() + ->withdrawn() + ->afterCreating(function (FlowMeasure $measure) { + $measure->discordNotifications()->create( + [ + 'remote_id' => Str::uuid(), + ], + joining: [ + 'discord_notification_type_id' => DiscordNotificationType::idFromEnum( + DiscordNotificationTypeEnum::FLOW_MEASURE_NOTIFIED + ), + 'notified_as' => $measure->identifier + ] + ); + }) + ->create(); + + $this->assertEquals($measure->id, $this->repository->flowMeasuresToBeSentToEcfmp()->first()->measure->id); + $this->assertFalse($this->repository->flowMeasuresToBeSentToEcfmp()->first()->isReissuedNotification); + } } diff --git a/tests/TestCase.php b/tests/TestCase.php index a7db7d36..fe1f8016 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -14,6 +14,7 @@ abstract class TestCase extends BaseTestCase public function beforeRefreshingDatabase() { DB::table('discord_notifications')->delete(); + DB::table('division_discord_notifications')->delete(); DB::table('division_discord_webhooks')->delete(); DB::table('flow_measures')->delete(); DB::table('events')->delete();