diff --git a/server/plugins/README.md b/server/plugins/README.md index 0660d98b..8b9eea11 100755 --- a/server/plugins/README.md +++ b/server/plugins/README.md @@ -1 +1 @@ -Plugins docs have been relocated. Check the new [Plugins Overview location](./PLUGINS_OVERVIEW.md). \ No newline at end of file +Plugins docs have been relocated. Check the new [Plugins Overview location](../../docs/PLUGINS_OVERVIEW.md). \ No newline at end of file diff --git a/server/plugins/_publisher_email/README.md b/server/plugins/_publisher_email/README.md index fc25cc06..c098d167 100755 --- a/server/plugins/_publisher_email/README.md +++ b/server/plugins/_publisher_email/README.md @@ -1,6 +1,6 @@ ## Overview -A simple EMail (SMTP) notification gateway publisher. Check the [SMTP docs](./SMTP.md) for additional help. +A simple EMail (SMTP) notification gateway publisher. Check the [SMTP docs](https://docs.netalertx.com/SMTP) for additional help. ### Usage diff --git a/server/plugins/sync/README.md b/server/plugins/sync/README.md index e2c5e9eb..8e201d29 100755 --- a/server/plugins/sync/README.md +++ b/server/plugins/sync/README.md @@ -7,7 +7,7 @@ The synchronization plugin is designed to synchronize data across multiple insta > [!TIP] -> This plugin is usually used if you need to monitor inaccessible networks (WAN, VLAN etc.). Read the [Remote networks documentation](./REMOTE_NETWORKS.md) for more details about these scenarios. +> This plugin is usually used if you need to monitor inaccessible networks (WAN, VLAN etc.). Read the [Remote networks documentation](https://docs.netalertx.com/REMOTE_NETWORKS) for more details about these scenarios. ### Synchronization Modes diff --git a/server/plugins/unifi_api_import/unifi_api_import.py b/server/plugins/unifi_api_import/unifi_api_import.py index 1fe7e00e..d7d64a04 100755 --- a/server/plugins/unifi_api_import/unifi_api_import.py +++ b/server/plugins/unifi_api_import/unifi_api_import.py @@ -149,7 +149,7 @@ def get_device_data(site, api): wifi_broadcasts = wifi_broadcasts_resp.get("data", []) mylog( - 'verbose', + 'trace', [f'[{pluginName}] WIFI BROADCASTS: ' f'{json.dumps(wifi_broadcasts_resp, indent=2)}'] ) diff --git a/server/workflows/conditions.py b/server/workflows/conditions.py index db6d1299..46f74fd7 100755 --- a/server/workflows/conditions.py +++ b/server/workflows/conditions.py @@ -21,8 +21,13 @@ class Condition: appEvent_value = ( trigger.event[self.field] if self.field in trigger.event.keys() else None ) + # trigger.object is None when the referenced Devices/Plugins_Objects row no + # longer exists by the time this event is processed (e.g. deleted between + # AppEvent creation and workflow processing) - fall back to the event value only. eveObj_value = ( - trigger.object[self.field] if self.field in trigger.object.keys() else None + trigger.object[self.field] + if trigger.object is not None and self.field in trigger.object.keys() + else None ) # proceed only if value found diff --git a/test/backend/test_workflows.py b/test/backend/test_workflows.py index c075aaa4..73cb2132 100644 --- a/test/backend/test_workflows.py +++ b/test/backend/test_workflows.py @@ -432,5 +432,38 @@ class TestCascadePrevention(unittest.TestCase): self.assertIn("guid-mutated", mgr._mutated_guids) +class TestConditionHandlesMissingTriggerObject(unittest.TestCase): + """Regression test: trigger.object is None whenever the AppEvent's + objectGuid no longer matches any Devices/Plugins_Objects row by the time + the event is processed (e.g. the device was deleted between AppEvent + creation and workflow processing - see workflows/triggers.py's own None + branches). Condition.evaluate() must not crash on it.""" + + def _make_trigger(self, event, obj=None): + from types import SimpleNamespace + return SimpleNamespace(event=event, object=obj) + + def test_condition_on_missing_field_returns_false_not_crash(self): + from workflows.conditions import Condition + + event = _make_app_event() + trigger = self._make_trigger(event, obj=None) + + condition = Condition({"field": "devLocation", "operator": "equals", "value": "Office"}) + # must not raise AttributeError: 'NoneType' object has no attribute 'keys' + self.assertFalse(condition.evaluate(trigger)) + + def test_condition_on_event_field_still_works_when_object_missing(self): + """A field present on the AppEvent itself must still evaluate correctly + even when trigger.object is None.""" + from workflows.conditions import Condition + + event = _make_app_event(event_type="update") + trigger = self._make_trigger(event, obj=None) + + condition = Condition({"field": "appEventType", "operator": "equals", "value": "update"}) + self.assertTrue(condition.evaluate(trigger)) + + if __name__ == "__main__": unittest.main()