mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-09-22 23:08:08 +03:00
Test backend editor compatibility
Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
+3
-2
@@ -20,6 +20,7 @@ clients, or real proxy cores.
|
||||
| SOCKS and SIP002 Shadowsocks codecs and generated round trips | `test_socks_uri.py`, `test_shadowsocks_uri.py` |
|
||||
| Subscription workflow, timers, stale requests, and reconciliation | `test_subscription_manager.py`, `test_subscription_sync.py` |
|
||||
| External process launch, output, shutdown, threads, TUN metadata | `test_external_core.py` |
|
||||
| Backend structured-editor observational load and unknown-value preservation | `test_backend_editor_contract.py` |
|
||||
| Xray/Hysteria2 native-TUN ownership and proxy-only stripping | `test_native_tun_semantics.py` |
|
||||
| Rolling metrics, stable buckets, lazy rendering, and hover | `test_metrics_behavior.py` |
|
||||
| Proxy-only endpoint discovery, caching, and presentation | `test_endpoint_info.py` |
|
||||
@@ -75,7 +76,7 @@ Then run the desired test tier.
|
||||
python -m unittest discover -s tests -v
|
||||
|
||||
# Regular logic, persistence, plugin, controller, codec, and UI regressions
|
||||
python -m unittest tests.test_interface tests.test_models_and_services tests.test_architecture_refactors tests.test_plugin_architecture tests.test_controllers tests.test_subscription_manager tests.test_subscription_sync tests.test_socks_uri tests.test_shadowsocks_uri tests.test_native_tun_semantics tests.test_metrics_behavior tests.test_endpoint_info tests.test_service_runtime tests.test_frozenlib tests.test_isolation_and_navigation tests.test_ui_behavior -v
|
||||
python -m unittest tests.test_interface tests.test_models_and_services tests.test_architecture_refactors tests.test_plugin_architecture tests.test_controllers tests.test_subscription_manager tests.test_subscription_sync tests.test_socks_uri tests.test_shadowsocks_uri tests.test_backend_editor_contract tests.test_native_tun_semantics tests.test_metrics_behavior tests.test_endpoint_info tests.test_service_runtime tests.test_frozenlib tests.test_isolation_and_navigation tests.test_ui_behavior -v
|
||||
|
||||
# Direct Qt/process integration and destruction/lifetime checks
|
||||
python -m unittest tests.test_external_core tests.test_qt_lifetime -v
|
||||
@@ -84,7 +85,7 @@ python -m unittest tests.test_external_core tests.test_qt_lifetime -v
|
||||
python -m unittest tests.test_qt_stress tests.test_process_stress -v
|
||||
|
||||
# Shared-state order-independence spot check
|
||||
python -m unittest tests.test_ui_behavior tests.test_isolation_and_navigation tests.test_frozenlib tests.test_service_runtime tests.test_endpoint_info tests.test_metrics_behavior tests.test_native_tun_semantics tests.test_shadowsocks_uri tests.test_socks_uri tests.test_subscription_sync tests.test_subscription_manager tests.test_controllers tests.test_plugin_architecture tests.test_architecture_refactors tests.test_models_and_services tests.test_interface -v
|
||||
python -m unittest tests.test_ui_behavior tests.test_isolation_and_navigation tests.test_frozenlib tests.test_service_runtime tests.test_endpoint_info tests.test_metrics_behavior tests.test_native_tun_semantics tests.test_backend_editor_contract tests.test_shadowsocks_uri tests.test_socks_uri tests.test_subscription_sync tests.test_subscription_manager tests.test_controllers tests.test_plugin_architecture tests.test_architecture_refactors tests.test_models_and_services tests.test_interface -v
|
||||
python -m unittest discover -s tests -v
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
# Copyright (C) 2024–present 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/>.
|
||||
|
||||
"""Protect observational loading and lossless backend editor round trips."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Backends.Configuration import ConfigXray
|
||||
from Furious.Backends.Hysteria1.Editor import GuiHy1ItemBasicProtocol
|
||||
from Furious.Backends.Xray.TlsEditor import GuiVTLSQGroupBox
|
||||
from Furious.Backends.Xray.TransportEditor import GuiVTransportQGroupBox
|
||||
|
||||
from tests.support import application, collectAtBoundary
|
||||
|
||||
import copy
|
||||
import unittest
|
||||
|
||||
|
||||
class BackendEditorContractTest(unittest.TestCase):
|
||||
"""Verify mature editors preserve values they do not yet understand."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""Create the process-wide Qt application used by real editor widgets."""
|
||||
application()
|
||||
|
||||
def tearDown(self):
|
||||
"""Process deferred Qt deletion between real-widget tests."""
|
||||
collectAtBoundary()
|
||||
|
||||
@staticmethod
|
||||
def xrayConfiguration(streamSettings: dict) -> ConfigXray:
|
||||
"""Return a minimal Xray document with one tagged proxy outbound."""
|
||||
return ConfigXray(
|
||||
{
|
||||
'outbounds': [
|
||||
{
|
||||
'tag': 'proxy',
|
||||
'protocol': 'vless',
|
||||
'streamSettings': copy.deepcopy(streamSettings),
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
def testHysteria1UnknownProtocolIsVisibleAndPreserved(self):
|
||||
"""Round-trip a future Hysteria 1 protocol through the shared combo."""
|
||||
binding = GuiHy1ItemBasicProtocol(title='Protocol', translatable=False)
|
||||
configuration = {'protocol': 'future-protocol'}
|
||||
original = copy.deepcopy(configuration)
|
||||
optionCount = binding._input.count()
|
||||
|
||||
binding.factoryToInput(configuration)
|
||||
|
||||
self.assertEqual(binding.text(), 'future-protocol')
|
||||
self.assertEqual(binding._input.count(), optionCount)
|
||||
self.assertEqual(binding._input.findText('future-protocol'), -1)
|
||||
self.assertEqual(configuration, original)
|
||||
self.assertFalse(binding.inputToFactory(configuration))
|
||||
self.assertEqual(configuration, original)
|
||||
|
||||
for widget in binding.widgets():
|
||||
widget.deleteLater()
|
||||
|
||||
def testXrayUnknownTransportIsVisibleAndPreserved(self):
|
||||
"""Keep an unsupported transport and its settings untouched."""
|
||||
configuration = self.xrayConfiguration(
|
||||
{
|
||||
'network': 'future-transport',
|
||||
'futureTransportSettings': {
|
||||
'token': 'preserve',
|
||||
'futureField': 7,
|
||||
},
|
||||
}
|
||||
)
|
||||
original = copy.deepcopy(configuration)
|
||||
group = GuiVTransportQGroupBox()
|
||||
|
||||
group.factoryToInput(configuration)
|
||||
|
||||
self.assertEqual(configuration, original)
|
||||
self.assertEqual(
|
||||
group.page(group.currentIndex()).networkText(), 'future-transport'
|
||||
)
|
||||
self.assertFalse(group.inputToFactory(configuration))
|
||||
self.assertEqual(configuration, original)
|
||||
|
||||
group.deleteLater()
|
||||
|
||||
def testXrayTransportAliasIsNormalizedWhileLoading(self):
|
||||
"""Normalize legacy upstream aliases while preserving their settings."""
|
||||
configuration = self.xrayConfiguration(
|
||||
{
|
||||
'network': 'http',
|
||||
'httpSettings': {
|
||||
'host': ['example.com'],
|
||||
'path': '/future-safe',
|
||||
'futureField': {'preserve': True},
|
||||
},
|
||||
}
|
||||
)
|
||||
group = GuiVTransportQGroupBox()
|
||||
|
||||
group.factoryToInput(configuration)
|
||||
|
||||
self.assertEqual(
|
||||
configuration.proxyStreamSettingsObject['network'],
|
||||
'h2',
|
||||
)
|
||||
self.assertEqual(
|
||||
configuration.proxyStreamSettingsObject['httpSettings'],
|
||||
{
|
||||
'host': ['example.com'],
|
||||
'path': '/future-safe',
|
||||
'futureField': {'preserve': True},
|
||||
},
|
||||
)
|
||||
self.assertEqual(group.page(group.currentIndex()).networkText(), 'h2')
|
||||
self.assertFalse(group.inputToFactory(configuration))
|
||||
|
||||
group.deleteLater()
|
||||
|
||||
def testXrayUnknownSecurityIsVisibleAndPreserved(self):
|
||||
"""Keep unsupported TLS modes and sibling settings untouched."""
|
||||
configuration = self.xrayConfiguration(
|
||||
{
|
||||
'network': 'tcp',
|
||||
'security': 'future-security',
|
||||
'future-securitySettings': {
|
||||
'certificate': 'preserve',
|
||||
'futureField': True,
|
||||
},
|
||||
}
|
||||
)
|
||||
original = copy.deepcopy(configuration)
|
||||
group = GuiVTLSQGroupBox()
|
||||
|
||||
group.factoryToInput(configuration)
|
||||
|
||||
self.assertEqual(configuration, original)
|
||||
self.assertEqual(
|
||||
group.page(group.currentIndex())._containers[0].text(),
|
||||
'future-security',
|
||||
)
|
||||
self.assertFalse(group.inputToFactory(configuration))
|
||||
self.assertEqual(configuration, original)
|
||||
|
||||
group.deleteLater()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -478,10 +478,113 @@ class Hysteria2CompatibilityTest(unittest.TestCase):
|
||||
self.assertTrue(editor.inputToFactory(profile))
|
||||
self.assertEqual(profile.connection['realm']['ipMode'], 'v6')
|
||||
self.assertEqual(profile.connection['obfs']['type'], 'salamander')
|
||||
self.assertEqual(
|
||||
profile.connection['obfs']['future-obfs'],
|
||||
{'token': 'discard'},
|
||||
self.assertNotIn('future-obfs', profile.connection['obfs'])
|
||||
|
||||
editor.close()
|
||||
|
||||
def testUnknownCompactEditorValuesRoundTripUntouched(self):
|
||||
"""Display and preserve future enum and tagged-union values verbatim."""
|
||||
profile = self.profile(
|
||||
{
|
||||
'server': 'example.com:443',
|
||||
'auth': 'secret',
|
||||
'realm': {
|
||||
'ipMode': 'future-mode',
|
||||
'futureRealmField': {'preserve': True},
|
||||
},
|
||||
'congestion': {
|
||||
'type': 'future-congestion',
|
||||
'bbrProfile': 'future-profile',
|
||||
'futureCongestionField': 7,
|
||||
},
|
||||
'obfs': {
|
||||
'type': 'future-obfs',
|
||||
'future-obfs': {
|
||||
'token': 'preserve',
|
||||
'futureOption': True,
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
original = copy.deepcopy(profile.connection)
|
||||
editor = Hysteria2Editor()
|
||||
|
||||
editor.factoryToInput(profile)
|
||||
|
||||
self.assertEqual(profile.connection, original)
|
||||
self.assertEqual(
|
||||
self.binding(editor, ('realm', 'ipMode')).text(),
|
||||
'future-mode',
|
||||
)
|
||||
self.assertEqual(
|
||||
editor.basicGroup._containers[3].bindings[0].text(),
|
||||
'future-congestion',
|
||||
)
|
||||
self.assertEqual(
|
||||
editor.basicGroup._containers[3].bindings[1].text(),
|
||||
'future-profile',
|
||||
)
|
||||
self.assertEqual(
|
||||
editor.advancedGroup.obfsItem.page(
|
||||
editor.advancedGroup.obfsItem.currentIndex()
|
||||
).obfsTypeText(),
|
||||
'future-obfs',
|
||||
)
|
||||
self.assertFalse(editor.inputToFactory(profile))
|
||||
self.assertEqual(profile.connection, original)
|
||||
|
||||
editor.close()
|
||||
|
||||
def testClearingCongestionFieldPreservesUnknownSiblings(self):
|
||||
"""Remove only the represented congestion leaf when clearing it."""
|
||||
profile = self.profile(
|
||||
{
|
||||
'server': 'example.com:443',
|
||||
'auth': 'secret',
|
||||
'congestion': {
|
||||
'type': 'bbr',
|
||||
'bbrProfile': 'fast',
|
||||
'futureCongestionField': {'preserve': True},
|
||||
},
|
||||
}
|
||||
)
|
||||
editor = Hysteria2Editor()
|
||||
editor.factoryToInput(profile)
|
||||
|
||||
editor.basicGroup._containers[3].bindings[0].setText('')
|
||||
|
||||
self.assertTrue(editor.inputToFactory(profile))
|
||||
self.assertNotIn('type', profile.connection['congestion'])
|
||||
self.assertEqual(profile.connection['congestion']['bbrProfile'], 'fast')
|
||||
self.assertEqual(
|
||||
profile.connection['congestion']['futureCongestionField'],
|
||||
{'preserve': True},
|
||||
)
|
||||
|
||||
editor.close()
|
||||
|
||||
def testImplicitGeckoPacketDefaultsRemainAbsent(self):
|
||||
"""Do not materialize effective Gecko defaults during an untouched save."""
|
||||
profile = self.profile(
|
||||
{
|
||||
'server': 'example.com:443',
|
||||
'auth': 'secret',
|
||||
'obfs': {
|
||||
'type': 'gecko',
|
||||
'gecko': {
|
||||
'password': 'secret',
|
||||
'futureGeckoField': {'preserve': True},
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
original = copy.deepcopy(profile.connection)
|
||||
editor = Hysteria2Editor()
|
||||
|
||||
editor.factoryToInput(profile)
|
||||
|
||||
self.assertFalse(editor.inputToFactory(profile))
|
||||
self.assertEqual(profile.connection, original)
|
||||
|
||||
editor.close()
|
||||
|
||||
|
||||
@@ -98,6 +98,9 @@ class EditorMappingTest(unittest.TestCase):
|
||||
'environment': {'TOKEN': 'one=two', 'UNICODE': '测试'},
|
||||
'useApplicationTun2socks': True,
|
||||
'tunRemoteAddress': '2001:db8::42',
|
||||
'futureExternalCoreField': {
|
||||
'nested': ['preserve', 7],
|
||||
},
|
||||
}
|
||||
)
|
||||
profile = ServerProfile.fromConfiguration(
|
||||
@@ -107,8 +110,11 @@ class EditorMappingTest(unittest.TestCase):
|
||||
|
||||
with isolatedSettings():
|
||||
editor = ExternalCoreEditor()
|
||||
original = copy.deepcopy(profile.connection)
|
||||
editor.factoryToInput(profile)
|
||||
|
||||
self.assertEqual(profile.connection, original)
|
||||
|
||||
self.assertEqual(
|
||||
editor._argumentsInput.values(),
|
||||
['--config', 'name with spaces.json'],
|
||||
@@ -148,6 +154,10 @@ class EditorMappingTest(unittest.TestCase):
|
||||
profile.connection.tunRemoteAddress(),
|
||||
'server.example.com',
|
||||
)
|
||||
self.assertEqual(
|
||||
profile.connection['futureExternalCoreField'],
|
||||
{'nested': ['preserve', 7]},
|
||||
)
|
||||
|
||||
editor.close()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user