Files
LorenEteval_Furious/tests/test_subscription_manager.py
T
2026-08-30 19:13:43 +08:00

839 lines
30 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Copyright (C) 2024present Loren Eteval & contributors <loren.eteval@proton.me>
#
# This file is part of Furious.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Verify subscription workflow ownership outside table widgets."""
import os
os.environ.setdefault('QT_QPA_PLATFORM', 'offscreen')
from Furious.Repository import Storage
from Furious.Repository.Subscriptions import SubscriptionGroup
from Furious.Qt import gettext
from Furious.Service.SubscriptionManager import SubscriptionManager
from Furious.Window.SubscriptionPage import SubscriptionPage
from Furious.Widget.SubscriptionTableView import SubscriptionTableView
from tests.support import application, processQtEvents
from PySide6 import QtCore, QtTest, QtWidgets
from shiboken6 import isValid
from types import SimpleNamespace
from unittest import TestCase, mock
class _Payload:
"""Expose the minimal QNetworkReply byte-array contract."""
def __init__(self, value):
self._value = value
def data(self):
return self._value
class _Reply:
"""Provide deterministic response data and failure diagnostics."""
def __init__(self, value=b'', error='request failed'):
self._value = value
self._error = error
def readAll(self):
return _Payload(self._value)
def errorString(self):
return self._error
class _AbortableReply:
"""Record cancellation of one exact service-owned network reply."""
def __init__(self):
self.aborted = False
def abort(self):
self.aborted = True
class SubscriptionManagerTest(TestCase):
"""Protect decoding, failure, and stable-ID scheduling boundaries."""
@classmethod
def setUpClass(cls):
cls.application = application()
@staticmethod
def _manager(subscriptions=None):
subscriptions = subscriptions if subscriptions is not None else {}
with mock.patch(
'Furious.Service.SubscriptionManager.Storage.UserSubs',
return_value=subscriptions,
):
return SubscriptionManager()
@staticmethod
def _subscription(**overrides):
"""Return one enabled five-minute subscription definition."""
subscription = {
'remark': 'Group A',
'webURL': 'https://invalid.test/a',
'autoupdate': 'Every 5 mins',
'proxy': '',
'enabled': True,
}
subscription.update(overrides)
return subscription
def testSuccessfulAndInvalidPayloadsProduceSemanticBatchInputs(self):
manager = self._manager()
profile = SimpleNamespace(itemRemark='profile')
imported = SimpleNamespace(
decoderId='decoder',
profiles=(profile,),
rejectedItems=2,
)
manager.importer = SimpleNamespace(
importPayload=mock.Mock(return_value=imported)
)
successful = []
failed = []
with mock.patch(
'Furious.Service.SubscriptionManager.Storage.SubscriptionGroup',
return_value=None,
):
manager.successCallback(
_Reply(b'payload'),
unique='group-a',
remark='Group A',
webURL='https://invalid.test/subscription',
successArgs=successful,
failureArgs=failed,
)
self.assertEqual(len(successful), 1)
self.assertEqual(successful[0]['profiles'], (profile,))
self.assertEqual(successful[0]['decoderId'], 'decoder')
self.assertEqual(failed, [])
manager.importer = SimpleNamespace(importPayload=mock.Mock(return_value=None))
with mock.patch(
'Furious.Service.SubscriptionManager.Storage.SubscriptionGroup',
return_value=None,
):
manager.successCallback(
_Reply(b'invalid'),
unique='group-a',
successArgs=successful,
failureArgs=failed,
)
self.assertEqual(failed[-1]['error'], 'UnsupportedSubscriptionFormat')
manager.deleteLater()
def testRequestFailureIsDataForPresentationNotAWidgetSideEffect(self):
manager = self._manager()
failed = []
with mock.patch(
'Furious.Service.SubscriptionManager.Storage.SubscriptionGroup',
return_value=None,
):
manager.failureCallback(
_Reply(error='offline'),
unique='group-a',
remark='Group A',
webURL='https://invalid.test/subscription',
failureArgs=failed,
)
self.assertEqual(failed[-1]['error'], 'offline')
self.assertFalse(hasattr(manager, 'table'))
manager.deleteLater()
def testSubscriptionDiagnosticsExcludeConfiguredURL(self):
"""Identify a request without logging its credential-bearing URL."""
manager = self._manager()
profile = SimpleNamespace(itemRemark='profile')
manager.importer = SimpleNamespace(
importPayload=mock.Mock(
return_value=SimpleNamespace(
decoderId='decoder',
profiles=(profile,),
rejectedItems=0,
)
)
)
configuredURL = 'https://example.invalid/subscription?token=value'
with mock.patch('Furious.Service.SubscriptionManager.logger.info') as infoLog:
manager.successCallback(
_Reply(b'payload'),
unique='group-a',
remark='Group A',
webURL=configuredURL,
successArgs=[],
failureArgs=[],
)
with mock.patch('Furious.Service.SubscriptionManager.logger.error') as errorLog:
manager.failureCallback(
_Reply(error='offline'),
unique='group-a',
remark='Group A',
webURL=configuredURL,
failureArgs=[],
)
self.assertIn('Group A', infoLog.call_args.args[0])
self.assertIn('Group A', errorLog.call_args.args[0])
self.assertIn('group-a', infoLog.call_args.args[0])
self.assertIn('group-a', errorLog.call_args.args[0])
self.assertNotIn(configuredURL, infoLog.call_args.args[0])
self.assertNotIn(configuredURL, errorLog.call_args.args[0])
self.assertNotIn('token=value', infoLog.call_args.args[0])
self.assertNotIn('token=value', errorLog.call_args.args[0])
manager.deleteLater()
def testStaleRequestCompletionCannotMutateCurrentSubscription(self):
subscriptions = {
'group-a': {
'webURL': 'https://invalid.test/current',
'enabled': True,
}
}
manager = self._manager(subscriptions)
manager._requestVersions['group-a'] = 2
manager.importer = SimpleNamespace(importPayload=mock.Mock())
with mock.patch(
'Furious.Service.SubscriptionManager.Storage.UserSubs',
return_value=subscriptions,
):
manager.successCallback(
_Reply(b'stale'),
unique='group-a',
webURL='https://invalid.test/current',
requestVersion=1,
successArgs=[],
failureArgs=[],
)
manager.importer.importPayload.assert_not_called()
manager.deleteLater()
def testStaleBatchEntriesAreNotReconciledOrReported(self):
subscriptions = {
'group-a': {
'webURL': 'https://invalid.test/current',
'enabled': True,
}
}
manager = self._manager(subscriptions)
manager._requestVersions['group-a'] = 2
manager._synchronizeProfiles = mock.Mock()
completed = []
manager.updateCompleted.connect(completed.append)
stale = {
'unique': 'group-a',
'webURL': 'https://invalid.test/current',
'requestVersion': 1,
'profiles': (),
}
with mock.patch(
'Furious.Service.SubscriptionManager.Storage.UserSubs',
return_value=subscriptions,
):
manager.handleSynchronizationResults(
successArgs=[stale],
failureArgs=[{'error': 'offline', **stale}],
)
manager._synchronizeProfiles.assert_not_called()
self.assertEqual(completed, [])
manager.deleteLater()
def testStaleDecodedResultCannotCommitSubscriptionMetadata(self):
"""Delay group metadata writes until the final current-request check."""
subscriptions = {
'group-a': {
'webURL': 'https://invalid.test/current',
'enabled': True,
}
}
manager = self._manager(subscriptions)
manager._requestVersions['group-a'] = 1
profile = SimpleNamespace(itemRemark='profile')
manager.importer = SimpleNamespace(
importPayload=mock.Mock(
return_value=SimpleNamespace(
decoderId='decoder',
profiles=(profile,),
rejectedItems=0,
)
)
)
successful = []
failed = []
with mock.patch(
'Furious.Service.SubscriptionManager.Storage.UserSubs',
return_value=subscriptions,
):
manager.successCallback(
_Reply(b'payload'),
unique='group-a',
webURL='https://invalid.test/current',
requestVersion=1,
successArgs=successful,
failureArgs=failed,
)
manager._requestVersions['group-a'] = 2
with (
mock.patch(
'Furious.Service.SubscriptionManager.Storage.UserSubs',
return_value=subscriptions,
),
mock.patch(
'Furious.Service.SubscriptionManager.Storage.upsertSubscriptionGroup'
) as upsert,
):
manager.handleSynchronizationResults(
successArgs=successful,
failureArgs=failed,
)
upsert.assert_not_called()
manager.deleteLater()
def testOneSynchronizationFailureDoesNotAbortOtherGroups(self):
"""Isolate one group's preparation failure from the rest of a batch."""
manager = self._manager()
committed = SimpleNamespace(profileIds=('profile-id',))
manager._isCurrentRequest = mock.Mock(return_value=True)
manager._synchronizeProfiles = mock.Mock(
side_effect=(RuntimeError('injected failure'), committed)
)
completed = []
committedSubscriptions = []
manager.updateCompleted.connect(completed.append)
manager.subscriptionCommitted.connect(committedSubscriptions.append)
failed = {'unique': 'group-a', 'profiles': ()}
successful = {'unique': 'group-b', 'profiles': ()}
with mock.patch(
'Furious.Service.SubscriptionManager.Storage.SubscriptionGroup',
return_value=None,
):
manager.handleSynchronizationResults(
successArgs=[failed, successful],
failureArgs=[],
)
self.assertEqual(manager._synchronizeProfiles.call_count, 2)
self.assertEqual(len(completed), 1)
self.assertEqual(completed[0].successful[0]['unique'], 'group-b')
self.assertEqual(completed[0].failed[0]['unique'], 'group-a')
self.assertIn('injected failure', completed[0].failed[0]['error'])
self.assertEqual(committedSubscriptions, ['group-b'])
manager.deleteLater()
def testCommittedMetadataFailureDoesNotAbortOtherGroups(self):
"""Treat status metadata as post-commit and keep processing the batch."""
manager = self._manager()
result = SimpleNamespace(profileIds=('profile-id',))
manager._isCurrentRequest = mock.Mock(return_value=True)
manager._synchronizeProfiles = mock.Mock(return_value=result)
completed = []
manager.updateCompleted.connect(completed.append)
with (
mock.patch(
'Furious.Service.SubscriptionManager.Storage.SubscriptionGroup',
side_effect=(SimpleNamespace(), SimpleNamespace()),
),
mock.patch(
'Furious.Service.SubscriptionManager.Storage.upsertSubscriptionGroup',
side_effect=(RuntimeError('metadata write failed'), None),
) as upsert,
):
manager.handleSynchronizationResults(
successArgs=[
{'unique': 'group-a', 'profiles': ()},
{'unique': 'group-b', 'profiles': ()},
],
failureArgs=[],
)
self.assertEqual(upsert.call_count, 2)
self.assertEqual(len(completed), 1)
self.assertEqual(
[item['unique'] for item in completed[0].successful],
['group-a', 'group-b'],
)
manager.deleteLater()
def testPostCommitConnectionFailureDoesNotUndoSynchronization(self):
"""Keep a committed reconciliation successful if reconnect later fails."""
manager = self._manager()
active = SimpleNamespace(
metadata=SimpleNamespace(profileId='active-profile'),
itemSubscription='group-a',
itemSubscriptionManaged=True,
)
servers = [active]
result = SimpleNamespace(
profileIds=('active-profile',),
changedProfileIds=('active-profile',),
)
manager.synchronizer.reconcile = mock.Mock(return_value=result)
controller = SimpleNamespace(
isConnected=mock.Mock(return_value=True),
startDisconnection=mock.Mock(),
startReconnection=mock.Mock(side_effect=RuntimeError('reconnect failed')),
)
with (
mock.patch(
'Furious.Service.SubscriptionManager.Storage.UserServers',
return_value=servers,
),
mock.patch(
'Furious.Service.SubscriptionManager.Storage.UserActivatedItemIndex',
return_value=0,
),
mock.patch(
'Furious.Service.SubscriptionManager.AppConnectionController',
return_value=controller,
),
mock.patch(
'Furious.Service.SubscriptionManager.AppSettings.set',
side_effect=RuntimeError('settings write failed'),
),
):
committed = manager._synchronizeProfiles('group-a', ())
self.assertIs(committed, result)
controller.startReconnection.assert_called_once_with()
manager.deleteLater()
def testCancellationInvalidatesAndAbortsOnlyTheSelectedSubscription(self):
manager = self._manager()
groupAReply = _AbortableReply()
groupBReply = _AbortableReply()
manager._requestVersions.update({'group-a': 1, 'group-b': 4})
manager._activeReplies.update(
{groupAReply: groupAReply, groupBReply: groupBReply}
)
manager._replySubscriptions.update(
{groupAReply: 'group-a', groupBReply: 'group-b'}
)
manager.cancelUpdates('group-a')
self.assertTrue(groupAReply.aborted)
self.assertFalse(groupBReply.aborted)
self.assertEqual(manager._requestVersions, {'group-a': 2, 'group-b': 4})
manager._activeReplies.clear()
manager._replySubscriptions.clear()
manager.deleteLater()
def testUnchangedAutoUpdateReconciliationPreservesDeadlineAndConnection(self):
"""Make repeated full reconciliation a true scheduler no-op."""
subscriptions = {'group-a': self._subscription()}
manager = self._manager(subscriptions)
timer = manager._autoUpdateTimers['group-a']
timerId = timer.timerId()
QtTest.QTest.qWait(40)
remainingBefore = timer.remainingTime()
with (
mock.patch(
'Furious.Service.SubscriptionManager.Storage.UserSubs',
return_value=subscriptions,
),
mock.patch(
'Furious.Service.SubscriptionManager.logger.info'
) as lifecycleLog,
):
for _index in range(100):
manager.refreshAutoUpdates()
self.assertIs(manager._autoUpdateTimers['group-a'], timer)
self.assertEqual(timer.timerId(), timerId)
self.assertEqual(timer.property('subscriptionId'), 'group-a')
self.assertEqual(len(manager._autoUpdateTimers), 1)
self.assertLessEqual(timer.remainingTime(), remainingBefore + 5)
lifecycleLog.assert_not_called()
manager.configureHttpProxy = mock.Mock()
manager.updateSubsByUnique = mock.Mock()
with mock.patch(
'Furious.Service.SubscriptionManager.Storage.UserSubs',
return_value=subscriptions,
):
timer.timeout.emit()
manager.updateSubsByUnique.assert_called_once_with(
'group-a', showMessageBox=False
)
manager.deleteLater()
def testAutoUpdatePolicyTransitionsReuseTimerAndLogOnlyRealChanges(self):
"""Start, reschedule, and stop exactly when policy state changes."""
subscriptions = {
'group-a': self._subscription(autoupdate='Never'),
}
manager = self._manager(subscriptions)
timer = manager._autoUpdateTimers['group-a']
with (
mock.patch(
'Furious.Service.SubscriptionManager.Storage.UserSubs',
return_value=subscriptions,
),
mock.patch(
'Furious.Service.SubscriptionManager.logger.info'
) as lifecycleLog,
):
manager.configureAutoUpdate('group-a')
lifecycleLog.assert_not_called()
subscriptions['group-a']['autoupdate'] = 'Every 5 mins'
manager.configureAutoUpdate('group-a')
self.assertTrue(timer.isActive())
self.assertEqual(timer.interval(), 5 * 60 * 1000)
self.assertIn('start auto update job', lifecycleLog.call_args.args[0])
activeTimerId = timer.timerId()
manager.configureAutoUpdate('group-a')
self.assertEqual(timer.timerId(), activeTimerId)
self.assertEqual(lifecycleLog.call_count, 1)
subscriptions['group-a']['autoupdate'] = 'Every 10 mins'
manager.configureAutoUpdate('group-a')
self.assertIs(manager._autoUpdateTimers['group-a'], timer)
self.assertEqual(timer.interval(), 10 * 60 * 1000)
self.assertIn('reschedule auto update job', lifecycleLog.call_args.args[0])
subscriptions['group-a']['enabled'] = False
manager.configureAutoUpdate('group-a')
self.assertFalse(timer.isActive())
self.assertIn('stop auto update job', lifecycleLog.call_args.args[0])
manager.configureAutoUpdate('group-a')
self.assertEqual(lifecycleLog.call_count, 3)
subscriptions['group-a']['enabled'] = True
manager.configureAutoUpdate('group-a')
self.assertTrue(timer.isActive())
self.assertIn('start auto update job', lifecycleLog.call_args.args[0])
self.assertEqual(lifecycleLog.call_count, 4)
manager.deleteLater()
def testUnrelatedSubscriptionEditDoesNotRestartItsTimer(self):
"""Keep the real table edit path outside unchanged timer deadlines."""
subscriptions = {'group-a': self._subscription()}
manager = self._manager(subscriptions)
timer = manager._autoUpdateTimers['group-a']
QtTest.QTest.qWait(40)
remainingBefore = timer.remainingTime()
timerId = timer.timerId()
def group(unique):
"""Return the exact in-memory group edited by the table."""
value = subscriptions.get(unique)
return (
SubscriptionGroup.fromMapping(unique, value)
if value is not None
else None
)
def upsert(value):
"""Persist the edited group into the isolated test repository."""
subscriptions[value.id] = value.toMapping()
with (
mock.patch.object(Storage, 'UserSubs', return_value=subscriptions),
mock.patch.object(Storage, 'SubscriptionGroup', side_effect=group),
mock.patch.object(Storage, 'upsertSubscriptionGroup', side_effect=upsert),
):
table = SubscriptionTableView(subscriptionManager=manager)
table.appendNewItem(
unique='group-a',
remark='Renamed Group',
webURL=subscriptions['group-a']['webURL'],
enabled=True,
autoupdate='Every 5 mins',
proxy='',
userAgent='',
filter='',
lastUpdated='',
)
self.assertEqual(subscriptions['group-a']['remark'], 'Renamed Group')
self.assertIs(manager._autoUpdateTimers['group-a'], timer)
self.assertEqual(timer.timerId(), timerId)
self.assertLessEqual(timer.remainingTime(), remainingBefore + 5)
table.deleteLater()
manager.deleteLater()
def testSubscriptionTableExposesPersistedSynchronizationStatus(self):
"""Present the existing sync state without inventing a UI-owned copy."""
subscriptions = {
'group-a': self._subscription(lastSyncStatus=''),
}
with (
mock.patch.object(Storage, 'UserSubs', return_value=subscriptions),
mock.patch(
'Furious.Widget.SubscriptionTableView._',
side_effect=lambda text: text,
),
):
table = SubscriptionTableView()
model = table.sourceModel
statusColumn = table.ItemKey.index('lastSyncStatus')
statusIndex = model.index(0, statusColumn)
for persisted, expected in (
('', 'Never'),
('syncing', 'Updating...'),
('success', 'Updated'),
('error', 'Update Failed'),
('future-value', 'Never'),
):
with self.subTest(persisted=persisted):
subscriptions['group-a']['lastSyncStatus'] = persisted
self.assertEqual(
model.data(statusIndex, QtCore.Qt.ItemDataRole.DisplayRole),
expected,
)
self.assertFalse(
model.flags(statusIndex) & QtCore.Qt.ItemFlag.ItemIsEditable
)
table.deleteLater()
self.assertEqual(
gettext('Sync Status', 'RU'),
'Статус синхронизации',
)
self.assertEqual(gettext('Updating...', 'RU'), 'Обновление...')
self.assertEqual(gettext('Updated', 'ZH'), '已更新')
self.assertEqual(gettext('Update Failed', 'ZH'), '更新失败')
def testTargetedPolicyChangeDoesNotDisturbOtherSubscriptions(self):
"""Reconcile only the edited subscription's schedule."""
subscriptions = {
'group-a': self._subscription(),
'group-b': self._subscription(
remark='Group B',
webURL='https://invalid.test/b',
autoupdate='Every 10 mins',
),
}
manager = self._manager(subscriptions)
groupATimer = manager._autoUpdateTimers['group-a']
groupBTimer = manager._autoUpdateTimers['group-b']
groupBTimerId = groupBTimer.timerId()
QtTest.QTest.qWait(40)
groupBRemainingBefore = groupBTimer.remainingTime()
subscriptions['group-a']['autoupdate'] = 'Every 10 mins'
with mock.patch(
'Furious.Service.SubscriptionManager.Storage.UserSubs',
return_value=subscriptions,
):
manager.configureAutoUpdate('group-a')
self.assertIs(manager._autoUpdateTimers['group-a'], groupATimer)
self.assertEqual(groupATimer.interval(), 10 * 60 * 1000)
self.assertIs(manager._autoUpdateTimers['group-b'], groupBTimer)
self.assertEqual(groupBTimer.timerId(), groupBTimerId)
self.assertLessEqual(groupBTimer.remainingTime(), groupBRemainingBefore + 5)
self.assertEqual(len(manager._autoUpdateTimers), 2)
manager.deleteLater()
def testRemovingSubscriptionDestroysOnlyItsTimerAndCancelsItsReply(self):
"""Release one removed subscription without disturbing its sibling."""
subscriptions = {
'group-a': self._subscription(),
'group-b': self._subscription(
remark='Group B',
webURL='https://invalid.test/b',
),
}
manager = self._manager(subscriptions)
groupATimer = manager._autoUpdateTimers['group-a']
groupBTimer = manager._autoUpdateTimers['group-b']
groupBTimerId = groupBTimer.timerId()
groupAReply = _AbortableReply()
groupBReply = _AbortableReply()
destroyed = []
groupATimer.destroyed.connect(lambda *_args: destroyed.append(True))
manager._activeReplies.update(
{groupAReply: groupAReply, groupBReply: groupBReply}
)
manager._replySubscriptions.update(
{groupAReply: 'group-a', groupBReply: 'group-b'}
)
subscriptions.pop('group-a')
with mock.patch(
'Furious.Service.SubscriptionManager.Storage.UserSubs',
return_value=subscriptions,
):
manager.removeAutoUpdate('group-a')
self.assertTrue(groupAReply.aborted)
self.assertFalse(groupBReply.aborted)
self.assertNotIn('group-a', manager._autoUpdateTimers)
self.assertIs(manager._autoUpdateTimers['group-b'], groupBTimer)
self.assertEqual(groupBTimer.timerId(), groupBTimerId)
self.assertTrue(groupBTimer.isActive())
processQtEvents()
self.assertEqual(destroyed, [True])
self.assertFalse(isValid(groupATimer))
manager._activeReplies.clear()
manager._replySubscriptions.clear()
manager.deleteLater()
def testPageNavigationIsPresentationOnlyForAutoUpdateScheduler(self):
"""Keep page show/hide cycles outside scheduler policy ownership."""
subscriptions = {'group-a': self._subscription()}
with mock.patch.object(Storage, 'UserSubs', return_value=subscriptions):
manager = SubscriptionManager()
timer = manager._autoUpdateTimers['group-a']
timerId = timer.timerId()
manager.refreshAutoUpdates = mock.Mock(
side_effect=AssertionError(
'page presentation must not reconcile background schedules'
)
)
serverTable = SimpleNamespace(subsManager=manager)
page = SubscriptionPage(serverTable)
placeholder = QtWidgets.QWidget()
stack = QtWidgets.QStackedWidget()
stack.addWidget(page)
stack.addWidget(placeholder)
stack.show()
for _index in range(20):
stack.setCurrentWidget(placeholder)
processQtEvents(1)
stack.setCurrentWidget(page)
processQtEvents(1)
self.assertIs(page.table.subsManager, manager)
self.assertIs(manager._autoUpdateTimers['group-a'], timer)
self.assertEqual(timer.timerId(), timerId)
self.assertEqual(len(manager._autoUpdateTimers), 1)
manager.refreshAutoUpdates.assert_not_called()
stack.deleteLater()
manager.deleteLater()
processQtEvents()
def testManagerDestructionDestroysItsServiceOwnedTimers(self):
"""Let QObject parent ownership release all scheduler resources."""
subscriptions = {'group-a': self._subscription()}
manager = self._manager(subscriptions)
timer = manager._autoUpdateTimers['group-a']
destroyed = []
manager.destroyed.connect(lambda *_args: destroyed.append('manager'))
timer.destroyed.connect(lambda *_args: destroyed.append('timer'))
manager.deleteLater()
processQtEvents()
self.assertFalse(isValid(manager))
self.assertFalse(isValid(timer))
self.assertCountEqual(destroyed, ('manager', 'timer'))
def testMissingSubscriptionRemovalPrunesTimerDuringFullReconciliation(self):
"""Retain removal behavior while unchanged groups remain untouched."""
subscriptions = {'group-a': self._subscription()}
manager = self._manager(subscriptions)
timer = manager._autoUpdateTimers['group-a']
subscriptions.clear()
with mock.patch(
'Furious.Service.SubscriptionManager.Storage.UserSubs',
return_value=subscriptions,
):
manager.refreshAutoUpdates()
self.assertEqual(manager._autoUpdateTimers, {})
self.assertFalse(timer.isActive())
manager.deleteLater()
def testDeletedSubscriptionVersionIsPrunedAfterItsReplyFinishes(self):
"""Release stale-request bookkeeping after the last exact owner ends."""
manager = self._manager()
reply = _AbortableReply()
manager._requestVersions['deleted-group'] = 4
manager._activeReplies[reply] = reply
manager._replySubscriptions[reply] = 'deleted-group'
with mock.patch(
'Furious.Service.SubscriptionManager.Storage.UserSubs',
return_value={},
):
manager._pruneRequestVersion('deleted-group')
self.assertIn('deleted-group', manager._requestVersions)
manager._activeReplies.pop(reply)
manager._replySubscriptions.pop(reply)
manager._pruneRequestVersion('deleted-group')
self.assertNotIn('deleted-group', manager._requestVersions)
manager.deleteLater()