From 4c8ae68d69a815925d13b5511248943a4ae63932 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Sat, 14 Nov 2026 16:32:55 +0000 Subject: [PATCH 1/5] Test that a licence key is needed only for the updates that download data-updates.md lines 107 to 109 and 392 say a licence key is needed only for polling (auto_update) and the update on start-up, and that the build fails naming the option to turn off when either is on with no key. The tests build the Device Detection and IP Intelligence engines with no key and nothing that downloads, with the engine defaults, with each option on and no key, through the pipeline builder with no key, and with an update URL other than the Distributor. The IP Intelligence test that asserted a missing key is always refused goes, as the new tests replace it. --- .../tests/test_licence_key_rule.py | 89 ++++++++++++++++++ .../tests/test_ipintelligence.py | 11 --- .../tests/test_licence_key_rule.py | 91 +++++++++++++++++++ 3 files changed, 180 insertions(+), 11 deletions(-) create mode 100644 device-detection/fiftyone_devicedetection_onpremise/tests/test_licence_key_rule.py create mode 100644 ip-intelligence/fiftyone_ipintelligence_onpremise/tests/test_licence_key_rule.py diff --git a/device-detection/fiftyone_devicedetection_onpremise/tests/test_licence_key_rule.py b/device-detection/fiftyone_devicedetection_onpremise/tests/test_licence_key_rule.py new file mode 100644 index 00000000..a0d4e3c1 --- /dev/null +++ b/device-detection/fiftyone_devicedetection_onpremise/tests/test_licence_key_rule.py @@ -0,0 +1,89 @@ +# ********************************************************************* +# This Original Work is copyright of 51 Degrees Mobile Experts Limited. +# Copyright 2026 51 Degrees Mobile Experts Limited, Davidson House, +# Forbury Square, Reading, Berkshire, United Kingdom RG1 3EU. +# +# This Original Work is licensed under the European Union Public Licence +# (EUPL) v.1.2 and is subject to its terms as set out below. +# +# If a copy of the EUPL was not distributed with this file, You can obtain +# one at https://opensource.org/licenses/EUPL-1.2. +# +# The 'Compatible Licences' set out in the Appendix to the EUPL (as may be +# amended by the European Commission) shall be deemed incompatible for +# the purposes of the Work and the provisions of the compatibility +# clause in Article 5 of the EUPL shall not apply. +# +# If using the Work as, or as part of, a network application, by +# including the attribution notice(s) required under Article 5 of the EUPL +# in the end user terms of the application under an appropriate heading, +# such notice(s) shall fulfill the requirements of that article. +# ********************************************************************* + +import unittest + +from fiftyone_devicedetection_onpremise.devicedetection_onpremise import DeviceDetectionOnPremise +from fiftyone_devicedetection_onpremise.devicedetection_onpremise_pipelinebuilder import DeviceDetectionOnPremisePipelineBuilder + +data_file = "../../cxx/data/device-detection/51Degrees-LiteV4.1.hash" + + +class LicenceKeyRuleTests(unittest.TestCase): + """! + data-updates.md lines 107 to 109 and 392. A licence key is needed only + by polling (auto_update) and the update on start-up, which download from + the Distributor, and the build fails naming the option to turn off when + one of them is on with no key. The file system watcher needs no key. + """ + + def build(self, **options): + engine = DeviceDetectionOnPremise(data_file_path=data_file, **options) + self.addCleanup(engine.close) + return engine + + def test_no_key_is_needed_when_nothing_downloads(self): + for key in (None, ""): + with self.subTest(key=key): + engine = self.build( + licence_keys=key, + auto_update=False, + update_on_start=False, + file_system_watcher=True) + self.assertFalse(engine.data_file.auto_update) + self.assertFalse(engine.data_file.update_on_start) + self.assertTrue(engine.data_file.file_system_watcher) + + def test_no_key_is_needed_with_the_engine_defaults(self): + self.build() + + def test_a_download_with_no_key_is_refused_naming_the_option(self): + cases = [ + (None, {"auto_update": True}, "(auto_update)"), + ("", {"auto_update": True}, "(auto_update)"), + ("", {"update_on_start": True}, "(update_on_start)"), + ("", {"auto_update": True, "update_on_start": True}, + "(auto_update, update_on_start)")] + for key, options, named in cases: + with self.subTest(key=key, options=options): + with self.assertRaises(Exception) as context: + self.build(licence_keys=key, **options) + message = str(context.exception) + self.assertIn(named, message) + self.assertIn("require a licence key", message) + self.assertIn("to False", message) + + def test_the_pipeline_builder_needs_no_key_by_default(self): + pipeline = DeviceDetectionOnPremisePipelineBuilder( + data_file_path=data_file, usage_sharing=False).build() + self.addCleanup(pipeline.close) + self.assertIn("device", pipeline.flow_elements_list) + + def test_an_update_url_other_than_the_distributor_needs_no_key(self): + engine = self.build( + data_update_url="https://updates.example.com/data.hash", + auto_update=True) + self.assertTrue(engine.data_file.auto_update) + + +if __name__ == "__main__": + unittest.main() diff --git a/ip-intelligence/fiftyone_ipintelligence_onpremise/tests/test_ipintelligence.py b/ip-intelligence/fiftyone_ipintelligence_onpremise/tests/test_ipintelligence.py index 097bcbea..61115338 100644 --- a/ip-intelligence/fiftyone_ipintelligence_onpremise/tests/test_ipintelligence.py +++ b/ip-intelligence/fiftyone_ipintelligence_onpremise/tests/test_ipintelligence.py @@ -182,17 +182,6 @@ def test_wrong_data_file_extension(self): self.assertIn(".ipi", str(context.exception)) - def test_missing_licence_key(self): - """! - The engine requires the licence key argument (an empty string - disables automatic updates). - """ - - with self.assertRaises(Exception) as context: - IpIntelligenceOnPremise(data_file_path=data_file) - - self.assertIn("licence key is required", str(context.exception)) - class AsnDataFileTests(unittest.TestCase): """! diff --git a/ip-intelligence/fiftyone_ipintelligence_onpremise/tests/test_licence_key_rule.py b/ip-intelligence/fiftyone_ipintelligence_onpremise/tests/test_licence_key_rule.py new file mode 100644 index 00000000..b74dbadd --- /dev/null +++ b/ip-intelligence/fiftyone_ipintelligence_onpremise/tests/test_licence_key_rule.py @@ -0,0 +1,91 @@ +# ********************************************************************* +# This Original Work is copyright of 51 Degrees Mobile Experts Limited. +# Copyright 2026 51 Degrees Mobile Experts Limited, Davidson House, +# Forbury Square, Reading, Berkshire, United Kingdom RG1 3EU. +# +# This Original Work is licensed under the European Union Public Licence +# (EUPL) v.1.2 and is subject to its terms as set out below. +# +# If a copy of the EUPL was not distributed with this file, You can obtain +# one at https://opensource.org/licenses/EUPL-1.2. +# +# The 'Compatible Licences' set out in the Appendix to the EUPL (as may be +# amended by the European Commission) shall be deemed incompatible for +# the purposes of the Work and the provisions of the compatibility +# clause in Article 5 of the EUPL shall not apply. +# +# If using the Work as, or as part of, a network application, by +# including the attribution notice(s) required under Article 5 of the EUPL +# in the end user terms of the application under an appropriate heading, +# such notice(s) shall fulfill the requirements of that article. +# ********************************************************************* + +import unittest + +from fiftyone_ipintelligence_onpremise.ipintelligence_onpremise import IpIntelligenceOnPremise +from fiftyone_ipintelligence_onpremise.ipintelligence_onpremise_pipelinebuilder import IpIntelligenceOnPremisePipelineBuilder + +from .constants import TestConstants + +data_file = TestConstants.get_data_file_path() + + +class LicenceKeyRuleTests(unittest.TestCase): + """! + data-updates.md lines 107 to 109 and 392. A licence key is needed only + by polling (auto_update) and the update on start-up, which download from + the Distributor, and the build fails naming the option to turn off when + one of them is on with no key. The file system watcher needs no key. + """ + + def build(self, **options): + engine = IpIntelligenceOnPremise(data_file_path=data_file, **options) + self.addCleanup(engine.close) + return engine + + def test_no_key_is_needed_when_nothing_downloads(self): + for key in (None, ""): + with self.subTest(key=key): + engine = self.build( + licence_keys=key, + auto_update=False, + update_on_start=False, + file_system_watcher=True) + self.assertFalse(engine.data_file.auto_update) + self.assertFalse(engine.data_file.update_on_start) + self.assertTrue(engine.data_file.file_system_watcher) + + def test_no_key_is_needed_with_the_engine_defaults(self): + self.build() + + def test_a_download_with_no_key_is_refused_naming_the_option(self): + cases = [ + (None, {"auto_update": True}, "(auto_update)"), + ("", {"auto_update": True}, "(auto_update)"), + ("", {"update_on_start": True}, "(update_on_start)"), + ("", {"auto_update": True, "update_on_start": True}, + "(auto_update, update_on_start)")] + for key, options, named in cases: + with self.subTest(key=key, options=options): + with self.assertRaises(Exception) as context: + self.build(licence_keys=key, **options) + message = str(context.exception) + self.assertIn(named, message) + self.assertIn("require a licence key", message) + self.assertIn("to False", message) + + def test_the_pipeline_builder_needs_no_key_by_default(self): + pipeline = IpIntelligenceOnPremisePipelineBuilder( + data_file_path=data_file, usage_sharing=False).build() + self.addCleanup(pipeline.close) + self.assertIn("ip", pipeline.flow_elements_list) + + def test_an_update_url_other_than_the_distributor_needs_no_key(self): + engine = self.build( + data_update_url="https://updates.example.com/data.ipi", + auto_update=True) + self.assertTrue(engine.data_file.auto_update) + + +if __name__ == "__main__": + unittest.main() From 8ba46b39f5362487deaea3558746102b8b529b50 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Sat, 14 Nov 2026 16:35:25 +0000 Subject: [PATCH 2/5] Need a licence key only for the updates that download An on-premise engine refused to build when no licence_keys was passed, whatever its settings, and with an empty key it turned auto_update and update_on_start off without saying so, even for an update URL other than the Distributor. data-updates.md lines 107 to 109 and 392 say a key is needed only by polling and the update on start-up, which download from the Distributor, and that the build then fails with a configuration error naming the option to turn off. Both engines now build with no key unless auto_update or update_on_start is on with the Distributor's address, and then refuse naming those options, as .NET does. The file system watcher needs no key, and an update URL other than the Distributor needs none either. --- .../devicedetection_pipelinebuilder.py | 5 ++- .../devicedetection_onpremise.py | 42 ++++++++++++------- ...vicedetection_onpremise_pipelinebuilder.py | 5 ++- .../ipintelligence_pipelinebuilder.py | 5 ++- .../ipintelligence_onpremise.py | 42 ++++++++++++------- ...pintelligence_onpremise_pipelinebuilder.py | 5 ++- 6 files changed, 72 insertions(+), 32 deletions(-) diff --git a/device-detection/fiftyone_devicedetection/src/fiftyone_devicedetection/devicedetection_pipelinebuilder.py b/device-detection/fiftyone_devicedetection/src/fiftyone_devicedetection/devicedetection_pipelinebuilder.py index 1a735f87..e227fdd2 100644 --- a/device-detection/fiftyone_devicedetection/src/fiftyone_devicedetection/devicedetection_pipelinebuilder.py +++ b/device-detection/fiftyone_devicedetection/src/fiftyone_devicedetection/devicedetection_pipelinebuilder.py @@ -80,7 +80,10 @@ def __init__(self, settings = None, usage_sharing = True, **kwargs): @type restricted_properties: list @param restricted_properties: List of property keys to limit the engine to @type license_keys: string - @param licence_keys: licencekeys to use for the data file update service + @param licence_keys: licence keys to use for the data file update service. + Needed only when auto_update or update_on_start is on with the default + data_update_url, and the engine refuses to build without one then. The + file system watcher needs no key. @type download: bool @param download: whether to download the datafile or keep it in memory when it is returned from the datafile update service @type max_matched_useragent_length: int diff --git a/device-detection/fiftyone_devicedetection_onpremise/src/fiftyone_devicedetection_onpremise/devicedetection_onpremise.py b/device-detection/fiftyone_devicedetection_onpremise/src/fiftyone_devicedetection_onpremise/devicedetection_onpremise.py index 0fd31aac..a110cfbe 100644 --- a/device-detection/fiftyone_devicedetection_onpremise/src/fiftyone_devicedetection_onpremise/devicedetection_onpremise.py +++ b/device-detection/fiftyone_devicedetection_onpremise/src/fiftyone_devicedetection_onpremise/devicedetection_onpremise.py @@ -36,6 +36,16 @@ from .swig_data import SwigData from .devicedetection_datafile import DeviceDetectionDataFile +# The Distributor, which serves data files only to a licence key. +DISTRIBUTOR_URL = "https://distributor.51degrees.com/api/v2/download" + +LICENCE_KEY_REQUIRED_FOR_UPDATES = ( + "Some enabled features ({}) require a licence key but one has not been " + "supplied. Supply one with licence_keys, or set {} to False to build " + "without one. The file system watcher needs no licence key. A key can be " + "obtained from the 51Degrees website: " + "https://51degrees.com/pricing?utm_source=code&utm_medium=comment&utm_campaign=51degrees-python&utm_content=fiftyone_devicedetection_onpremise-src-fiftyone_devicedetection_onpremise-devicedetection_onpremise.py&utm_term=licence-key-required-for-updates") + class DeviceDetectionOnPremise(Engine): """! @@ -66,7 +76,7 @@ def __init__( reuse_temp_file=False, concurrency=multiprocessing.cpu_count(), update_on_start=False, - data_update_url="https://distributor.51degrees.com/api/v2/download", + data_update_url=DISTRIBUTOR_URL, use_predictive_graph=None, use_performance_graph=None, data_update_use_url_formatter=True, @@ -87,7 +97,10 @@ def __init__( @type restricted_properties: list @param restricted_properties: List of property keys to limit the engine to @type license_keys: string - @param licence_keys: licencekeys to use for the data file update service + @param licence_keys: licence keys to use for the data file update service. + Needed only when auto_update or update_on_start is on with the default + data_update_url, and the engine refuses to build without one then. The + file system watcher needs no key. @type download: bool @param download: whether to download the datafile or keep it in memory when it is returned from the datafile update service @type max_matched_useragent_length: int @@ -151,6 +164,19 @@ def __init__( if not data_file_path and not data: raise Exception("data_file_path or data is required") + # Only polling and the update on start-up download from the + # Distributor, so only they need a licence key. The file system + # watcher needs none. + auto_update = bool(auto_update) + update_on_start = bool(update_on_start) + if data_update_url == DISTRIBUTOR_URL and not licence_keys: + need_key = [name for name, on in ( + ("auto_update", auto_update), + ("update_on_start", update_on_start)) if on] + if need_key: + raise Exception(LICENCE_KEY_REQUIRED_FOR_UPDATES.format( + ", ".join(need_key), " and ".join(need_key))) + if data_file_path: if os.path.isfile(data_file_path) == False and not update_on_start: raise Exception("There is no file at " + data_file_path) @@ -162,9 +188,6 @@ def __init__( else: self.data = data - if not licence_keys and licence_keys != "": - raise Exception("licence key is required. A key can be obtained from the 51Degrees website: https://51degrees.com/pricing?utm_source=code&utm_medium=comment&utm_campaign=51degrees-python&utm_content=fiftyone_devicedetection_onpremise-src-fiftyone_devicedetection_onpremise-devicedetection_onpremise.py&utm_term=licence-key-required. If you do not wish to use a key then you can specify an empty string, but this will cause automatic updates to be disabled.") - # Create SWIG wrapper vector for restricted properties and add restricted_properties_vector = VectorStringSwig() @@ -251,15 +274,6 @@ def __init__( self.properties_list = properties_list self.data_file_path = data_file_path - # Disable features that require a licence key if one was not supplied. - - if licence_keys: - auto_update = auto_update and (len(licence_keys) > 0) - update_on_start = update_on_start and (len(licence_keys) > 0) - else: - auto_update = False - update_on_start = False - if auto_update or update_on_start or file_system_watcher: # Construct DataFile diff --git a/device-detection/fiftyone_devicedetection_onpremise/src/fiftyone_devicedetection_onpremise/devicedetection_onpremise_pipelinebuilder.py b/device-detection/fiftyone_devicedetection_onpremise/src/fiftyone_devicedetection_onpremise/devicedetection_onpremise_pipelinebuilder.py index 28ce1b70..e3589f2f 100644 --- a/device-detection/fiftyone_devicedetection_onpremise/src/fiftyone_devicedetection_onpremise/devicedetection_onpremise_pipelinebuilder.py +++ b/device-detection/fiftyone_devicedetection_onpremise/src/fiftyone_devicedetection_onpremise/devicedetection_onpremise_pipelinebuilder.py @@ -77,7 +77,10 @@ def __init__(self, settings = None, usage_sharing = True, **kwargs): @type restricted_properties: list @param restricted_properties: List of property keys to limit the engine to @type license_keys: string - @param licence_keys: licencekeys to use for the data file update service + @param licence_keys: licence keys to use for the data file update service. + Needed only when auto_update or update_on_start is on with the default + data_update_url, and the engine refuses to build without one then. The + file system watcher needs no key. @type download: bool @param download: whether to download the datafile or keep it in memory when it is returned from the datafile update service @type max_matched_useragent_length: int diff --git a/ip-intelligence/fiftyone_ipintelligence/src/fiftyone_ipintelligence/ipintelligence_pipelinebuilder.py b/ip-intelligence/fiftyone_ipintelligence/src/fiftyone_ipintelligence/ipintelligence_pipelinebuilder.py index e17ee5c5..d5ac3c0e 100644 --- a/ip-intelligence/fiftyone_ipintelligence/src/fiftyone_ipintelligence/ipintelligence_pipelinebuilder.py +++ b/ip-intelligence/fiftyone_ipintelligence/src/fiftyone_ipintelligence/ipintelligence_pipelinebuilder.py @@ -78,7 +78,10 @@ def __init__(self, settings = None, usage_sharing = True, **kwargs): @type restricted_properties: list @param restricted_properties: List of property keys to limit the engine to @type licence_keys: string - @param licence_keys: licencekeys to use for the data file update service + @param licence_keys: licence keys to use for the data file update service. + Needed only when auto_update or update_on_start is on with the default + data_update_url, and the engine refuses to build without one then. The + file system watcher needs no key. @type download: bool @param download: whether to download the datafile or keep it in memory when it is returned from the datafile update service @type performance_profile: string diff --git a/ip-intelligence/fiftyone_ipintelligence_onpremise/src/fiftyone_ipintelligence_onpremise/ipintelligence_onpremise.py b/ip-intelligence/fiftyone_ipintelligence_onpremise/src/fiftyone_ipintelligence_onpremise/ipintelligence_onpremise.py index 22d9b806..5f15365e 100644 --- a/ip-intelligence/fiftyone_ipintelligence_onpremise/src/fiftyone_ipintelligence_onpremise/ipintelligence_onpremise.py +++ b/ip-intelligence/fiftyone_ipintelligence_onpremise/src/fiftyone_ipintelligence_onpremise/ipintelligence_onpremise.py @@ -37,6 +37,16 @@ from .swig_data import SwigData from .ipintelligence_datafile import IpIntelligenceDataFile +# The Distributor, which serves data files only to a licence key. +DISTRIBUTOR_URL = "https://distributor.51degrees.com/api/v2/download" + +LICENCE_KEY_REQUIRED_FOR_UPDATES = ( + "Some enabled features ({}) require a licence key but one has not been " + "supplied. Supply one with licence_keys, or set {} to False to build " + "without one. The file system watcher needs no licence key. A key can be " + "obtained from the 51Degrees website: " + "https://51degrees.com/pricing?utm_source=code&utm_medium=comment&utm_campaign=51degrees-python&utm_content=fiftyone_ipintelligence_onpremise-src-fiftyone_ipintelligence_onpremise-ipintelligence_onpremise.py&utm_term=licence-key-required-for-updates") + class IpIntelligenceOnPremise(Engine): """! @@ -62,7 +72,7 @@ def __init__( reuse_temp_file=False, concurrency=multiprocessing.cpu_count(), update_on_start=False, - data_update_url="https://distributor.51degrees.com/api/v2/download", + data_update_url=DISTRIBUTOR_URL, data_update_use_url_formatter=True, data_update_verify_md5=True, data_update_product_type=None, @@ -81,7 +91,10 @@ def __init__( @type restricted_properties: list @param restricted_properties: List of property keys to limit the engine to @type licence_keys: string - @param licence_keys: licencekeys to use for the data file update service + @param licence_keys: licence keys to use for the data file update service. + Needed only when auto_update or update_on_start is on with the default + data_update_url, and the engine refuses to build without one then. The + file system watcher needs no key. @type download: bool @param download: whether to download the datafile or keep it in memory when it is returned from the datafile update service @type performance_profile: string @@ -121,6 +134,19 @@ def __init__( if not data_file_path and not data: raise Exception("data_file_path or data is required") + # Only polling and the update on start-up download from the + # Distributor, so only they need a licence key. The file system + # watcher needs none. + auto_update = bool(auto_update) + update_on_start = bool(update_on_start) + if data_update_url == DISTRIBUTOR_URL and not licence_keys: + need_key = [name for name, on in ( + ("auto_update", auto_update), + ("update_on_start", update_on_start)) if on] + if need_key: + raise Exception(LICENCE_KEY_REQUIRED_FOR_UPDATES.format( + ", ".join(need_key), " and ".join(need_key))) + if data_file_path: if os.path.isfile(data_file_path) == False and not update_on_start: raise Exception("There is no file at " + data_file_path) @@ -132,9 +158,6 @@ def __init__( else: self.data = data - if not licence_keys and licence_keys != "": - raise Exception("licence key is required. A key can be obtained from the 51Degrees website: https://51degrees.com/pricing?utm_source=code&utm_medium=comment&utm_campaign=51degrees-python&utm_content=fiftyone_ipintelligence_onpremise-src-fiftyone_ipintelligence_onpremise-ipintelligence_onpremise.py&utm_term=licence-key-required. If you do not wish to use a key then you can specify an empty string, but this will cause automatic updates to be disabled.") - # Create SWIG wrapper vector for restricted properties and add restricted_properties_vector = VectorStringSwig() @@ -197,15 +220,6 @@ def __init__( self.properties_list = properties_list self.data_file_path = data_file_path - # Disable features that require a licence key if one was not supplied. - - if licence_keys: - auto_update = auto_update and (len(licence_keys) > 0) - update_on_start = update_on_start and (len(licence_keys) > 0) - else: - auto_update = False - update_on_start = False - if auto_update or update_on_start or file_system_watcher: # Construct DataFile diff --git a/ip-intelligence/fiftyone_ipintelligence_onpremise/src/fiftyone_ipintelligence_onpremise/ipintelligence_onpremise_pipelinebuilder.py b/ip-intelligence/fiftyone_ipintelligence_onpremise/src/fiftyone_ipintelligence_onpremise/ipintelligence_onpremise_pipelinebuilder.py index 182d39a8..1ff2ace4 100644 --- a/ip-intelligence/fiftyone_ipintelligence_onpremise/src/fiftyone_ipintelligence_onpremise/ipintelligence_onpremise_pipelinebuilder.py +++ b/ip-intelligence/fiftyone_ipintelligence_onpremise/src/fiftyone_ipintelligence_onpremise/ipintelligence_onpremise_pipelinebuilder.py @@ -58,7 +58,10 @@ def __init__(self, settings = None, usage_sharing = True, **kwargs): @type restricted_properties: list @param restricted_properties: List of property keys to limit the engine to @type licence_keys: string - @param licence_keys: licencekeys to use for the data file update service + @param licence_keys: licence keys to use for the data file update service. + Needed only when auto_update or update_on_start is on with the default + data_update_url, and the engine refuses to build without one then. The + file system watcher needs no key. @type download: bool @param download: whether to download the datafile or keep it in memory when it is returned from the datafile update service @type performance_profile: string From 8fe7b6b12658b16fb9903a213c04013e3ced9ceb Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Sat, 14 Nov 2026 23:57:07 +0000 Subject: [PATCH 3/5] Test that the file system watcher reloads a replaced file with no licence key --- .../tests/test_watcher_without_licence_key.py | 103 ++++++++++++++++++ .../tests/test_watcher_without_licence_key.py | 101 +++++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 device-detection/fiftyone_devicedetection_onpremise/tests/test_watcher_without_licence_key.py create mode 100644 ip-intelligence/fiftyone_ipintelligence_onpremise/tests/test_watcher_without_licence_key.py diff --git a/device-detection/fiftyone_devicedetection_onpremise/tests/test_watcher_without_licence_key.py b/device-detection/fiftyone_devicedetection_onpremise/tests/test_watcher_without_licence_key.py new file mode 100644 index 00000000..e27c498f --- /dev/null +++ b/device-detection/fiftyone_devicedetection_onpremise/tests/test_watcher_without_licence_key.py @@ -0,0 +1,103 @@ +# ********************************************************************* +# This Original Work is copyright of 51 Degrees Mobile Experts Limited. +# Copyright 2026 51 Degrees Mobile Experts Limited, Davidson House, +# Forbury Square, Reading, Berkshire, United Kingdom RG1 3EU. +# +# This Original Work is licensed under the European Union Public Licence +# (EUPL) v.1.2 and is subject to its terms as set out below. +# +# If a copy of the EUPL was not distributed with this file, You can obtain +# one at https://opensource.org/licenses/EUPL-1.2. +# +# The 'Compatible Licences' set out in the Appendix to the EUPL (as may be +# amended by the European Commission) shall be deemed incompatible for +# the purposes of the Work and the provisions of the compatibility +# clause in Article 5 of the EUPL shall not apply. +# +# If using the Work as, or as part of, a network application, by +# including the attribution notice(s) required under Article 5 of the EUPL +# in the end user terms of the application under an appropriate heading, +# such notice(s) shall fulfill the requirements of that article. +# ********************************************************************* + +import os +import shutil +import tempfile +import threading +import unittest + +from fiftyone_pipeline_engines.datafile_update_service import \ + DataFileUpdateService, UpdateStatus +from fiftyone_devicedetection_onpremise.devicedetection_onpremise_pipelinebuilder import \ + DeviceDetectionOnPremisePipelineBuilder + +data_file = "../../cxx/data/device-detection/51Degrees-LiteV4.1.hash" + +mobile_ua = "Mozilla/5.0 (iPhone; CPU iPhone OS 11_2 like Mac OS X) AppleWebKit/604.4.7 (KHTML, like Gecko) Mobile/15C114" + +# How often the watcher looks at the file in these tests. +WATCHER_INTERVAL_SECONDS = 0.5 + +# The longest the tests wait for the watcher to see a replaced file and the +# engine to reload it, set well above a reload of the Lite file on the +# slowest runner. +RELOAD_WAIT_SECONDS = 120 + + +class WatcherWithoutLicenceKeyTests(unittest.TestCase): + """! + The file system watcher downloads nothing, so it needs no licence key. + """ + + def setUp(self): + folder = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, folder, True) + self.path = os.path.join(folder, "51Degrees-LiteV4.1.hash") + shutil.copyfile(data_file, self.path) + + def build(self, **settings): + pipeline = DeviceDetectionOnPremisePipelineBuilder( + data_file_path=self.path, + usage_sharing=False, + performance_profile="MaxPerformance", + **settings).build() + self.addCleanup(pipeline.close) + return pipeline + + def test_the_watcher_runs_with_no_licence_key(self): + pipeline = self.build(file_system_watcher=True) + watched = pipeline.get_element("device").data_file + self.assertTrue(watched.file_system_watcher) + self.assertFalse(watched.auto_update) + self.assertFalse(watched.update_on_start) + + def test_a_replaced_file_is_loaded_with_no_licence_key(self): + service = DataFileUpdateService() + service.FILE_SYSTEM_WATCHER_INTERVAL_SECONDS = \ + WATCHER_INTERVAL_SECONDS + statuses = [] + reloaded = threading.Event() + + def on_complete(status, watched): + statuses.append(status) + reloaded.set() + + service.on_complete(on_complete) + pipeline = self.build( + file_system_watcher=True, data_file_update_service=service) + + # Replace the file, and move its time on so that a file system + # that keeps times to the second still sees a change. + shutil.copyfile(data_file, self.path) + later = os.stat(self.path).st_mtime + 10 + os.utime(self.path, (later, later)) + + self.assertTrue( + reloaded.wait(RELOAD_WAIT_SECONDS), + "The watcher did not report the replaced file.") + self.assertEqual([UpdateStatus.AUTO_UPDATE_SUCCESS], statuses) + + with pipeline.create_flowdata() as flowdata: + flowdata.evidence.add("header.user-agent", mobile_ua) + flowdata.process() + self.assertTrue(flowdata.device.ismobile.value()) diff --git a/ip-intelligence/fiftyone_ipintelligence_onpremise/tests/test_watcher_without_licence_key.py b/ip-intelligence/fiftyone_ipintelligence_onpremise/tests/test_watcher_without_licence_key.py new file mode 100644 index 00000000..8eb55364 --- /dev/null +++ b/ip-intelligence/fiftyone_ipintelligence_onpremise/tests/test_watcher_without_licence_key.py @@ -0,0 +1,101 @@ +# ********************************************************************* +# This Original Work is copyright of 51 Degrees Mobile Experts Limited. +# Copyright 2026 51 Degrees Mobile Experts Limited, Davidson House, +# Forbury Square, Reading, Berkshire, United Kingdom RG1 3EU. +# +# This Original Work is licensed under the European Union Public Licence +# (EUPL) v.1.2 and is subject to its terms as set out below. +# +# If a copy of the EUPL was not distributed with this file, You can obtain +# one at https://opensource.org/licenses/EUPL-1.2. +# +# The 'Compatible Licences' set out in the Appendix to the EUPL (as may be +# amended by the European Commission) shall be deemed incompatible for +# the purposes of the Work and the provisions of the compatibility +# clause in Article 5 of the EUPL shall not apply. +# +# If using the Work as, or as part of, a network application, by +# including the attribution notice(s) required under Article 5 of the EUPL +# in the end user terms of the application under an appropriate heading, +# such notice(s) shall fulfill the requirements of that article. +# ********************************************************************* + +import os +import threading +import unittest + +from fiftyone_pipeline_engines.datafile_update_service import \ + DataFileUpdateService, UpdateStatus +from fiftyone_ipintelligence_onpremise.ipintelligence_onpremise_pipelinebuilder import \ + IpIntelligenceOnPremisePipelineBuilder + +from .constants import TestConstants + +data_file = TestConstants.get_data_file_path() + +# How often the watcher looks at the file in these tests. +WATCHER_INTERVAL_SECONDS = 0.5 + +# The longest the tests wait for the watcher to see a changed file and the +# engine to reload it, set well above a reload of the Lite file on the +# slowest runner. +RELOAD_WAIT_SECONDS = 120 + + +@unittest.skipUnless( + os.path.isfile(data_file), + "No IP intelligence data file found at '%s'. Run ci/fetch-assets.ps1 " + "or set the _51DEGREES_IPI_PATH environment variable." % data_file) +class WatcherWithoutLicenceKeyTests(unittest.TestCase): + """! + The file system watcher downloads nothing, so it needs no licence key. + """ + + def build(self, **settings): + pipeline = IpIntelligenceOnPremisePipelineBuilder( + data_file_path=data_file, + usage_sharing=False, + **settings).build() + self.addCleanup(pipeline.close) + return pipeline + + def test_the_watcher_runs_with_no_licence_key(self): + pipeline = self.build(file_system_watcher=True) + watched = pipeline.get_element("ip").data_file + self.assertTrue(watched.file_system_watcher) + self.assertFalse(watched.auto_update) + self.assertFalse(watched.update_on_start) + + def test_a_changed_file_is_loaded_with_no_licence_key(self): + service = DataFileUpdateService() + service.FILE_SYSTEM_WATCHER_INTERVAL_SECONDS = \ + WATCHER_INTERVAL_SECONDS + statuses = [] + reloaded = threading.Event() + + def on_complete(status, watched): + statuses.append(status) + reloaded.set() + + service.on_complete(on_complete) + pipeline = self.build( + file_system_watcher=True, data_file_update_service=service) + + # The data file can be large, so it is marked as written to by + # moving its time on rather than copied, and its time is put back + # afterwards. + before = os.stat(data_file) + self.addCleanup( + os.utime, data_file, (before.st_atime, before.st_mtime)) + later = before.st_mtime + 10 + os.utime(data_file, (later, later)) + + self.assertTrue( + reloaded.wait(RELOAD_WAIT_SECONDS), + "The watcher did not report the changed file.") + self.assertEqual([UpdateStatus.AUTO_UPDATE_SUCCESS], statuses) + + with pipeline.create_flowdata() as flowdata: + flowdata.evidence.add("query.client-ip", "8.8.8.8") + flowdata.process() + self.assertEqual({}, flowdata.errors) From 5a1cb583b2f4af16d4f3e7677c4b0b5093dd4ecf Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Sun, 15 Nov 2026 00:57:59 +0000 Subject: [PATCH 4/5] Say only what the watcher tests' wait is for --- .../tests/test_watcher_without_licence_key.py | 6 +++--- .../tests/test_watcher_without_licence_key.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/device-detection/fiftyone_devicedetection_onpremise/tests/test_watcher_without_licence_key.py b/device-detection/fiftyone_devicedetection_onpremise/tests/test_watcher_without_licence_key.py index e27c498f..725159b3 100644 --- a/device-detection/fiftyone_devicedetection_onpremise/tests/test_watcher_without_licence_key.py +++ b/device-detection/fiftyone_devicedetection_onpremise/tests/test_watcher_without_licence_key.py @@ -38,9 +38,9 @@ # How often the watcher looks at the file in these tests. WATCHER_INTERVAL_SECONDS = 0.5 -# The longest the tests wait for the watcher to see a replaced file and the -# engine to reload it, set well above a reload of the Lite file on the -# slowest runner. +# The longest the tests wait for the watcher to report the file and the +# engine to reload it. It only stops a test that would otherwise wait for +# ever, and no time is asserted. RELOAD_WAIT_SECONDS = 120 diff --git a/ip-intelligence/fiftyone_ipintelligence_onpremise/tests/test_watcher_without_licence_key.py b/ip-intelligence/fiftyone_ipintelligence_onpremise/tests/test_watcher_without_licence_key.py index 8eb55364..f4c3c601 100644 --- a/ip-intelligence/fiftyone_ipintelligence_onpremise/tests/test_watcher_without_licence_key.py +++ b/ip-intelligence/fiftyone_ipintelligence_onpremise/tests/test_watcher_without_licence_key.py @@ -36,9 +36,9 @@ # How often the watcher looks at the file in these tests. WATCHER_INTERVAL_SECONDS = 0.5 -# The longest the tests wait for the watcher to see a changed file and the -# engine to reload it, set well above a reload of the Lite file on the -# slowest runner. +# The longest the tests wait for the watcher to report the file and the +# engine to reload it. It only stops a test that would otherwise wait for +# ever, and no time is asserted. RELOAD_WAIT_SECONDS = 120 From 0238cf6b23c65cf11676b0cbaea3868c70ce7622 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Sun, 15 Nov 2026 00:58:17 +0000 Subject: [PATCH 5/5] Say the watcher tests' wait is a guard that a reload must finish within --- .../tests/test_watcher_without_licence_key.py | 4 ++-- .../tests/test_watcher_without_licence_key.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/device-detection/fiftyone_devicedetection_onpremise/tests/test_watcher_without_licence_key.py b/device-detection/fiftyone_devicedetection_onpremise/tests/test_watcher_without_licence_key.py index 725159b3..86cd183f 100644 --- a/device-detection/fiftyone_devicedetection_onpremise/tests/test_watcher_without_licence_key.py +++ b/device-detection/fiftyone_devicedetection_onpremise/tests/test_watcher_without_licence_key.py @@ -39,8 +39,8 @@ WATCHER_INTERVAL_SECONDS = 0.5 # The longest the tests wait for the watcher to report the file and the -# engine to reload it. It only stops a test that would otherwise wait for -# ever, and no time is asserted. +# engine to reload it. It is a guard that stops a test waiting for ever, and +# a reload that takes longer than it fails the test. RELOAD_WAIT_SECONDS = 120 diff --git a/ip-intelligence/fiftyone_ipintelligence_onpremise/tests/test_watcher_without_licence_key.py b/ip-intelligence/fiftyone_ipintelligence_onpremise/tests/test_watcher_without_licence_key.py index f4c3c601..7c0c68b8 100644 --- a/ip-intelligence/fiftyone_ipintelligence_onpremise/tests/test_watcher_without_licence_key.py +++ b/ip-intelligence/fiftyone_ipintelligence_onpremise/tests/test_watcher_without_licence_key.py @@ -37,8 +37,8 @@ WATCHER_INTERVAL_SECONDS = 0.5 # The longest the tests wait for the watcher to report the file and the -# engine to reload it. It only stops a test that would otherwise wait for -# ever, and no time is asserted. +# engine to reload it. It is a guard that stops a test waiting for ever, and +# a reload that takes longer than it fails the test. RELOAD_WAIT_SECONDS = 120