Skip to content

fix(testing): wire publishers to router-registered subscribers - #87

Merged
lesnik512 merged 1 commit into
mainfrom
fix/test-broker-router-subscribers
Sep 20, 2026
Merged

lesnik512 merged 1 commit into
mainfrom
fix/test-broker-router-subscribers

Conversation

@lesnik512

Copy link
Copy Markdown
Member

Closes #86.

The bug

TestTimersBroker.create_publisher_fake_subscriber scanned broker._subscribers to find the real subscriber a publisher should be wired to. That list holds only endpoints registered directly on the broker — upstream's Registrator.subscribers is what includes routers':

@property
def subscribers(self) -> list["SubscriberUsecase[MsgType]"]:
    return [*self._subscribers, *(sub for r in self.routers for sub in r.subscribers)]

So anything declared on a TimersRouter was never matched, is_real came back False, and the fallback ran broker.subscriber(publisher.config.topic) — the unprefixed topic. Behind a prefix the fake landed on a topic nothing publishes to.

is_real=True is what makes upstream mirror the real handler's calls into the publisher's recorder, so the user-visible failure was:

async with TestTimersBroker(broker):
    await publisher.publish("ping", activate_in=timedelta(0))
    publisher.mock.assert_called_once_with("ping")
AssertionError: Expected 'mock' to be called once. Called 0 times.

on a publish that did happen. Anyone testing a prefixed router could not assert on their publisher.

The fix

Scan broker.subscribers, and register the fallback under full_topic rather than the raw topic. Both halves are needed: the first finds the router's subscriber, the second puts the fake on the right topic when there genuinely isn't one.

TimersBroker.subscribers now narrows the base's list[SubscriberUsecase[TimerMessage]] to list[TimersSubscriber], which is what _subscribers: list[TimersSubscriber] already claimed one line above. Narrowing at the broker rather than casting at each call site also retired an existing typing.cast in FakeTimersProducer.publish, which ty then flagged as redundant.

Before / after

Same script, three declarations:

declaration subscribers after entering the test broker publisher.mock
direct on broker ['reminders'] → unchanged records → records
router, no prefix ['reminders', 'reminders']['reminders'] records → records
router, prefix app: ['reminders', 'app:reminders']['app:reminders'] silent → records

The no-prefix row was wrong too — a duplicate subscriber on a topic that already had one — it just happened to keep working, because the fake carries no user handler and the topic still matched. Only the prefixed row failed visibly.

The stray channel this surfaced through in #85 is gone as well:

channels: {"app:reminders:Handle": "app:reminders", "app:reminders:Publisher": "app:reminders"}

Previously a third reminders:PublisherResponseSubscriber sat alongside those.

Tests

Written first, red before the change:

FAILED tests/test_unit.py::test_publisher_is_wired_to_a_subscriber_declared_on_a_prefixed_router
  AssertionError: Expected 'mock' to be called once. Called 0 times.
FAILED tests/test_unit.py::test_router_publisher_does_not_add_a_second_subscriber[no-prefix]
  AssertionError: assert ['reminders', 'reminders'] == ['reminders']
FAILED tests/test_unit.py::test_router_publisher_does_not_add_a_second_subscriber[prefixed]
  AssertionError: assert ['reminders', 'app:reminders'] == ['app:reminders']

The existing coverage of this method was test_create_publisher_fake_subscriber_is_instance_method, which checks the signature only. Every behavioural path through it went through a broker with no router, so the whole router branch was unexercised while the gate read 100%.

Not a 0.7.6 regression

Pre-existing. Registrator.subscribers has had the router-aware definition throughout 0.7; nothing in #85 touched this path.

Verification

just lint clean (eof-fixer, ruff format, ruff check, ty). Full suite against Redis 8: 170 passed, 100% coverage.

create_publisher_fake_subscriber scanned broker._subscribers, which omits
every router's, so a router publisher never matched its real subscriber and
got a fake built on the unprefixed topic. Behind a prefix that topic has no
publisher, so publisher.mock recorded nothing.

TimersBroker.subscribers narrows the base's return type, which also retires
a cast in the fake producer.

Closes #86
@lesnik512
lesnik512 merged commit 47c97b0 into main Sep 20, 2026
8 checks passed
@lesnik512
lesnik512 deleted the fix/test-broker-router-subscribers branch September 20, 2026 12:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TestTimersBroker ignores router-registered subscribers, breaking publisher.mock behind a prefix

1 participant