Problem/Motivation
Before releasing a stable version, we should add more tests.
Here's a plan by Claude Code:
Test plan: firebase_php module
Context
web/modules/orig/firebase_php is a Drupal contrib module (maintained by the
user, released on Drupal.org) that wraps kreait/firebase-php to send push
notifications. Today its only test is FirebasePhpInstallTest
(install/uninstall
smoke test). The module's real logic — building Firebase CloudMessages,
mapping send failures to typed exceptions, validating tokens, and validating
the credentials config — is completely untested. This plan adds focused
PHPUnit coverage of the most essential features so regressions are caught
in the GitLab CI pipeline (.gitlab-ci.yml already runs the test stage for
contrib modules automatically).
Decisions confirmed with the user:
- Cover three areas: (1) the messaging API logic, (2) the config-form
credentials validation, (3) the service constructor's credentials
resolution branches. Skip functional/route tests.
- Use native PHPUnit createMock for the Kreait\Firebase\Contract\Messaging
test double (no Prophecy).
- Tests must follow Drupal contrib conventions: @group firebase_php,
namespace Drupal\Tests\firebase_php\..., placed under tests/src/.
Testability challenge & approach
FirebasePhpMessagingService::__construct() does heavy work (reads config,
builds a real Kreait\Firebase\Factory, calls createMessaging()), so the
messaging API cannot be unit-tested by normal instantiation. Key levers:
- FirebasePhpMessagingService::$messaging is public and
FirebasePhpMessagingService::$config is protected.
- All public API methods funnel through $this->messaging->send() /
->sendMulticast() / ->validateRegistrationTokens().
So for the API unit test: build the subject with
(new \ReflectionClass(FirebasePhpMessagingApi::class))->newInstanceWithoutCon
structor(),
then inject a mocked Messaging into the public $messaging property and a
mocked ImmutableConfig into the protected $config (via reflection). Drive
the public methods and assert on what is passed to the mock. Because the
private helpers (createCloudMessage, setBadgeCount, etc.) run as part of the
public calls, capturing the CloudMessage argument to send() and inspecting
->jsonSerialize() verifies the build logic end-to-end.
Files to add
All under web/modules/orig/firebase_php/tests/src/.
1. Unit/Service/FirebasePhpMessagingApiTest.php (the essential core)
Drupal\Tests\firebase_php\Unit\Service\FirebasePhpMessagingApiTest extends
Drupal\Tests\UnitTestCase, @group firebase_php,
@coversDefaultClass \Drupal\firebase_php\Service\FirebasePhpMessagingApi.
Helper makeApi(Messaging $messaging, ?ImmutableConfig $config = null):
FirebasePhpMessagingApi
that uses newInstanceWithoutConstructor() and reflection to set
$messaging (public) and $config (protected). Mock the Kreait contract with
$this->createMock(\Kreait\Firebase\Contract\Messaging::class).
Cases:
- Single device — sendMessageSingleDevice() calls ->send() once; capture
the CloudMessage and assert jsonSerialize() contains token, the
notification (title/body/and notification_url→link), and that a default
sound is set (APNS payload present) when silent is falsey.
- Topic — sendMessageTopic() produces a topic key (not token).
- Data payload — passing $data surfaces a data key; omitting it does not.
- Badge count — badge_count > 0 sets the APNS badge and an Android
notification_count; badge_count = 0 sets APNS badge but no Android count
(covers the > 0 branch in setBadgeCount()).
- Silent — silent = TRUE does not add a default sound.
- Empty destination — empty/whitespace token (and empty topic) throws
FirebasePhpInvalidArgumentException (covers createMessageArray() guard).
- Multiple devices — sendMessageMultipleDevices() calls
->sendMulticast() once with the re-indexed token list and returns the
MulticastSendReport the mock returns; also assert string-keyed input is
re-indexed (uses array_values, builds the base message from [0]).
- InvalidMessage → 400 — mock ->send() to throw
(new InvalidMessage('bad'))->withErrors(['error' => ['code' => 400]]);
assert sendMessageSingleDevice() rethrows
FirebasePhpInvalidTokenException. (InvalidMessage is final; construct
with withErrors() — confirmed available in vendor.)
- InvalidMessage → other code — errors code != 400 rethrows
FirebasePhpException.
- validateToken — empty/whitespace throws
FirebasePhpInvalidArgumentException; when
validateRegistrationTokens() returns the token under valid, returns TRUE;
when only under invalid/unknown, returns FALSE. Use the config mock so
log_token_validation_result reads FALSE (no logging path). For the
exception-mapping cases, stub $config->get('logging_level') to 'none' so
the error-logging branch is skipped.
2. Unit/Form/FirebasePhpConfigurationFormValidateTest.php
Drupal\Tests\firebase_php\Unit\Form\...Test extends UnitTestCase,
@group firebase_php. validateForm() is pure logic over $form_state
values plus filesystem checks and $this->t(). Instantiate the form directly
(new FirebasePhpConfigurationForm(...) — it is a ConfigFormBase; pass the
constructor deps it needs, or build via newInstanceWithoutConstructor() if
the parent constructor is awkward) and call
setStringTranslation($this->getStringTranslationStub()). Mock
FormStateInterface: getValue('credentials') returns the case input; assert
setErrorByName() is/ isn't called via mock expectations
(expects($this->once())
vs ->expects($this->never())).
Cases:
- Empty string → no error (env-override path allowed).
- Pasted JSON (e.g. {"type":"service_account"}) → error.
- Non-existent path (/no/such/file.json) → error.
- public://creds.json → error (insecure). Note: this branch is only reached
for an existing readable file; if mocking the stream wrapper is heavy,
cover the JSON/missing-file branches which need no filesystem, and document
the public:// check as covered by the missing-file path it shares. Prefer
using vfsStream (bundled with Drupal core tests) or a real temp file via
sys_get_temp_dir() for the "valid file passes" and "unreadable" cases.
- Valid readable non-public temp file → no error.
3. Kernel/FirebasePhpCredentialsResolutionTest.php
Drupal\Tests\firebase_php\Kernel\...Test extends
Drupal\KernelTests\KernelTestBase, @group firebase_php,
protected static $modules = ['firebase_php']; and install the module config.
Tests the constructor branches in FirebasePhpMessagingService that run
before the real Kreait\Firebase\Factory is built (so no valid Firebase
credentials are required):
- Empty credentials config → requesting firebase_php.messaging from the
container throws FirebasePhpCredentialsNotFoundException (matches the
default install config of credentials: '').
- A non-empty value that is neither valid JSON nor an existing file (e.g.
'/definitely/not/a/file') → FirebasePhpCredentialsNotFoundException with
the "neither valid JSON ... nor an existing file path" message.
Set config with $this->config('firebase_php.settings')->set('credentials',
...)->save()
then $this->expectException(...) around
$this->container->get('firebase_php.messaging'). Do not attempt the
happy path (would require a structurally valid service-account JSON/RSA key
and
exercises only the vendor library).
What is intentionally NOT covered
- Real network sends and the success-path Factory build (would need live
Firebase credentials / vendor internals).
- The Send test message / Validate token forms' submit handlers and route
access (user opted out of functional tests).
- Logging-middleware wiring in the constructor (LoggingLevel::Debug/Standard
switch) — exercised indirectly; not asserted.
Comments
Comment #2
ptmkenny commentedComment #4
ptmkenny commentedAnd add some more tests:
Comment #6
ptmkenny commented