From cb68f6cf0237469ffc0c2e445ab1dfaa8dfe0fc0 Mon Sep 17 00:00:00 2001 From: Thomas Laure Date: Sat, 16 May 2026 22:59:08 +0200 Subject: [PATCH 1/3] Fix caregiver push alert delivery --- .github/workflows/ci.yaml | 4 +--- src/Infrastructure/Push/FcmPushGateway.php | 17 ++++++++++++++++ tests/Behat/ApiContext.php | 13 ++++++------ .../Infrastructure/FcmPushGatewayTest.php | 20 ++++++++++++++++++- 4 files changed, 44 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f0d5c23..6ce22c0 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -3,16 +3,14 @@ name: CI on: push: branches: - - master - main pull_request: branches: - - master - main workflow_dispatch: ~ concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} cancel-in-progress: true jobs: diff --git a/src/Infrastructure/Push/FcmPushGateway.php b/src/Infrastructure/Push/FcmPushGateway.php index 93a43a2..88dcf28 100644 --- a/src/Infrastructure/Push/FcmPushGateway.php +++ b/src/Infrastructure/Push/FcmPushGateway.php @@ -49,14 +49,31 @@ public function send(string $fcmToken, string $alertId, string $fallTimestamp, ? $payload = [ 'message' => [ 'token' => $fcmToken, + 'notification' => [ + 'title' => 'Fall detected', + 'body' => 'Tap to view and acknowledge the alert.', + ], 'data' => $data, 'android' => [ 'priority' => 'high', + 'notification' => [ + 'channel_id' => 'fall_alerts', + 'click_action' => 'FLUTTER_NOTIFICATION_CLICK', + ], ], 'apns' => [ 'headers' => [ 'apns-priority' => '10', ], + 'payload' => [ + 'aps' => [ + 'alert' => [ + 'title' => 'Fall detected', + 'body' => 'Tap to view and acknowledge the alert.', + ], + 'sound' => 'default', + ], + ], ], ], ]; diff --git a/tests/Behat/ApiContext.php b/tests/Behat/ApiContext.php index 672fd24..3b528e1 100644 --- a/tests/Behat/ApiContext.php +++ b/tests/Behat/ApiContext.php @@ -13,6 +13,7 @@ use RuntimeException; use Symfony\Bundle\FrameworkBundle\KernelBrowser; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\KernelInterface; final class ApiContext implements Context @@ -62,7 +63,7 @@ public function resetScenarioState(BeforeScenarioScope $scope): void */ public function iRegisterAProtectedPersonDevice(): void { - $this->sendRequest('POST', '/api/v1/devices/register', [ + $this->sendRequest(Request::METHOD_POST, '/api/v1/devices/register', [ 'platform' => 'ios', 'appVersion' => '1.0.0', ]); @@ -75,7 +76,7 @@ public function iRegisterAProtectedPersonDevice(): void */ public function iRegisterACaregiverDevice(): void { - $this->sendRequest('POST', '/api/v1/devices/register', [ + $this->sendRequest(Request::METHOD_POST, '/api/v1/devices/register', [ 'platform' => 'android', 'appVersion' => '1.0.0', 'deviceType' => 'caregiver', @@ -109,7 +110,7 @@ public function iAmAuthenticatedAsCaregiver(): void */ public function iSendAPostRequestTo(string $path): void { - $this->sendRequest('POST', $this->interpolate($path), []); + $this->sendRequest(Request::METHOD_POST, $this->interpolate($path), []); } /** @@ -119,7 +120,7 @@ public function iSendAPostRequestToWith(string $path, PyStringNode $body): void { /** @var array $data */ $data = json_decode($body->getRaw(), true, 512, JSON_THROW_ON_ERROR); - $this->sendRequest('POST', $this->interpolate($path), $data); + $this->sendRequest(Request::METHOD_POST, $this->interpolate($path), $data); } /** @@ -129,7 +130,7 @@ public function iSendAPutRequestToWith(string $path, PyStringNode $body): void { /** @var array $data */ $data = json_decode($body->getRaw(), true, 512, JSON_THROW_ON_ERROR); - $this->sendRequest('PUT', $this->interpolate($path), $data); + $this->sendRequest(Request::METHOD_PUT, $this->interpolate($path), $data); } /** @@ -137,7 +138,7 @@ public function iSendAPutRequestToWith(string $path, PyStringNode $body): void */ public function iSendAGetRequestTo(string $path): void { - $this->sendRequest('GET', $this->interpolate($path), null); + $this->sendRequest(Request::METHOD_GET, $this->interpolate($path), null); } // ─── Then ────────────────────────────────────────────────────────────────── diff --git a/tests/Unit/Infrastructure/FcmPushGatewayTest.php b/tests/Unit/Infrastructure/FcmPushGatewayTest.php index 47dba68..8d4ada8 100644 --- a/tests/Unit/Infrastructure/FcmPushGatewayTest.php +++ b/tests/Unit/Infrastructure/FcmPushGatewayTest.php @@ -28,7 +28,8 @@ public function itUsesBase64UrlEncodedJwtForOauthAssertion(): void openssl_pkey_export($privateKey, $privateKeyPem); $assertion = null; - $client = new MockHttpClient(static function (string $method, string $url, array $options) use (&$assertion): MockResponse { + $fcmPayload = null; + $client = new MockHttpClient(static function (string $method, string $url, array $options) use (&$assertion, &$fcmPayload): MockResponse { if ('https://oauth2.googleapis.com/token' === $url) { parse_str((string) $options['body'], $body); $assertion = is_string($body['assertion'] ?? null) ? $body['assertion'] : null; @@ -36,6 +37,9 @@ public function itUsesBase64UrlEncodedJwtForOauthAssertion(): void return new MockResponse(json_encode(['access_token' => 'access-token']) ?: '{}'); } + $decodedPayload = json_decode((string) $options['body'], true); + $fcmPayload = is_array($decodedPayload) ? $decodedPayload : null; + return new MockResponse(json_encode(['name' => 'projects/project-id/messages/message-id']) ?: '{}'); }); @@ -57,5 +61,19 @@ public function itUsesBase64UrlEncodedJwtForOauthAssertion(): void foreach ($segments as $segment) { self::assertDoesNotMatchRegularExpression('/[+=\/]/', $segment); } + + self::assertIsArray($fcmPayload); + self::assertSame( + 'Fall detected', + $fcmPayload['message']['notification']['title'] ?? null, + ); + self::assertSame( + 'high', + $fcmPayload['message']['android']['priority'] ?? null, + ); + self::assertSame( + 'FLUTTER_NOTIFICATION_CLICK', + $fcmPayload['message']['android']['notification']['click_action'] ?? null, + ); } } From 714dbe09fb01feacddc15d5b3efe3410bd67645b Mon Sep 17 00:00:00 2001 From: Thomas Laure Date: Sat, 16 May 2026 23:14:35 +0200 Subject: [PATCH 2/3] Raise API coverage gate to 90 percent --- .github/workflows/ci.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6ce22c0..65b2531 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -57,7 +57,7 @@ jobs: - name: Run PHPUnit with coverage run: docker compose -f compose.test.yaml exec -T app php -d pcov.enabled=1 vendor/bin/phpunit --testsuite=unit --coverage-clover var/coverage/clover.xml --coverage-text - - name: Check coverage threshold (80%) + - name: Check coverage threshold (90%) run: | COVERAGE=$(docker compose -f compose.test.yaml exec -T app php -r " \$xml = simplexml_load_file('var/coverage/clover.xml'); @@ -67,8 +67,8 @@ jobs: echo \$statements > 0 ? round((\$covered / \$statements) * 100, 2) : 0; ") echo "Line coverage: ${COVERAGE}%" - if [ "$(echo "${COVERAGE} < 80" | bc)" -eq 1 ]; then - echo "::error::Coverage ${COVERAGE}% is below the 80% threshold" + if [ "$(echo "${COVERAGE} < 90" | bc)" -eq 1 ]; then + echo "::error::Coverage ${COVERAGE}% is below the 90% threshold" exit 1 fi From d72f26a22f451b0c1e81d6a7ef5ce690526543b6 Mon Sep 17 00:00:00 2001 From: Thomas Laure Date: Sat, 16 May 2026 23:26:18 +0200 Subject: [PATCH 3/3] Measure API coverage on full test suite --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 65b2531..a5e1ae4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -55,7 +55,7 @@ jobs: run: docker compose -f compose.test.yaml exec -T app vendor/bin/phpunit - name: Run PHPUnit with coverage - run: docker compose -f compose.test.yaml exec -T app php -d pcov.enabled=1 vendor/bin/phpunit --testsuite=unit --coverage-clover var/coverage/clover.xml --coverage-text + run: docker compose -f compose.test.yaml exec -T app php -d pcov.enabled=1 vendor/bin/phpunit --coverage-clover var/coverage/clover.xml --coverage-text - name: Check coverage threshold (90%) run: |