What
TestTimersBroker.create_publisher_fake_subscriber scans 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 for anything declared on a TimersRouter, the scan finds nothing, is_real comes back False, and the fallback runs:
subscriber = broker.subscriber(publisher.config.topic)
publisher.config.topic is the unprefixed topic, so with a prefixed router the fake lands on a topic nothing publishes to.
Why it matters
is_real=True is what makes upstream mirror the real handler's calls into the publisher's recorder. With a router it is always False, and behind a prefix the fake never fires at all, so publisher.mock records nothing:
broker = TimersBroker()
router = TimersRouter(prefix="app:")
sub = router.subscriber("reminders")
@sub
async def handle(body: str) -> None: ...
pub = router.publisher("reminders")
broker.include_router(router)
async with TestTimersBroker(broker):
await pub.publish("ping", activate_in=timedelta(0))
pub.mock.assert_called_once_with("ping")
AssertionError: Expected 'mock' to be called once. Called 0 times.
The publish did happen. Anyone testing a prefixed router cannot assert on it.
Observed against faststream 0.7.6 at a011d8c. Three configurations, same script:
| declaration |
broker._subscribers |
broker.subscribers |
subscribers after entering the test broker |
publisher.mock |
| direct on broker |
['reminders'] |
['reminders'] |
['reminders'] |
records |
| router, no prefix |
[] |
['reminders'] |
['reminders', 'reminders'] |
records |
router, prefix app: |
[] |
['app:reminders'] |
['reminders', 'app:reminders'] |
silent |
The no-prefix row is wrong too — it builds a duplicate subscriber on a topic that already had one — but it happens to keep working, because the fake carries no user handler and the topic still matches. Only the prefixed row fails visibly.
It also leaks into the AsyncAPI document as a stray reminders:PublisherResponseSubscriber channel next to the correct app:reminders:* ones. That was how this surfaced, during #85.
Fix
Scan broker.subscribers rather than broker._subscribers, so router-registered endpoints are visible. The fallback should register the topic the publisher actually resolves to rather than the raw one, or the match should key off full_topic on both sides consistently.
Tests
TDD: a failing test first. The table above is the shape worth locking in — a test that only covers the direct-on-broker case passes today and would have passed throughout.
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.
What
TestTimersBroker.create_publisher_fake_subscriberscansbroker._subscribersto find the real subscriber a publisher should be wired to. That list holds only endpoints registered directly on the broker — upstream'sRegistrator.subscribersis what includes routers':So for anything declared on a
TimersRouter, the scan finds nothing,is_realcomes backFalse, and the fallback runs:publisher.config.topicis the unprefixed topic, so with a prefixed router the fake lands on a topic nothing publishes to.Why it matters
is_real=Trueis what makes upstream mirror the real handler's calls into the publisher's recorder. With a router it is alwaysFalse, and behind a prefix the fake never fires at all, sopublisher.mockrecords nothing:The publish did happen. Anyone testing a prefixed router cannot assert on it.
Observed against faststream 0.7.6 at
a011d8c. Three configurations, same script:broker._subscribersbroker.subscriberspublisher.mock['reminders']['reminders']['reminders'][]['reminders']['reminders', 'reminders']app:[]['app:reminders']['reminders', 'app:reminders']The no-prefix row is wrong too — it builds a duplicate subscriber on a topic that already had one — but it happens to keep working, because the fake carries no user handler and the topic still matches. Only the prefixed row fails visibly.
It also leaks into the AsyncAPI document as a stray
reminders:PublisherResponseSubscriberchannel next to the correctapp:reminders:*ones. That was how this surfaced, during #85.Fix
Scan
broker.subscribersrather thanbroker._subscribers, so router-registered endpoints are visible. The fallback should register the topic the publisher actually resolves to rather than the raw one, or the match should key offfull_topicon both sides consistently.Tests
TDD: a failing test first. The table above is the shape worth locking in — a test that only covers the direct-on-broker case passes today and would have passed throughout.
Not a 0.7.6 regression
Pre-existing.
Registrator.subscribershas had the router-aware definition throughout 0.7; nothing in #85 touched this path.