From 4da88c238504618fcf57f9b02e96c76563bddb63 Mon Sep 17 00:00:00 2001 From: BitterPanda63 Date: Thu, 15 May 2025 14:30:17 +0200 Subject: [PATCH 1/7] Create a new Users storage object and add loads of tests --- aikido_zen/background_process/users.py | 59 ------ aikido_zen/background_process/users_test.py | 71 ------- aikido_zen/storage/users.py | 32 +++ aikido_zen/storage/users_test.py | 219 ++++++++++++++++++++ 4 files changed, 251 insertions(+), 130 deletions(-) delete mode 100644 aikido_zen/background_process/users.py delete mode 100644 aikido_zen/background_process/users_test.py create mode 100644 aikido_zen/storage/users.py create mode 100644 aikido_zen/storage/users_test.py diff --git a/aikido_zen/background_process/users.py b/aikido_zen/background_process/users.py deleted file mode 100644 index 82f133cd1..000000000 --- a/aikido_zen/background_process/users.py +++ /dev/null @@ -1,59 +0,0 @@ -""" -Export the Users class -""" - -import aikido_zen.helpers.get_current_unixtime_ms as t - - -class Users: - """ - Class that holds users for the background process - """ - - def __init__(self, max_entries=1000): - self.max_entries = max_entries - self.users = {} - - def add_user(self, user): - """Store a user""" - user_id = user["id"] - current_time = t.get_unixtime_ms() - - existing = self.users.get(user_id) - if existing: - existing["name"] = user.get("name") - existing["lastIpAddress"] = user.get("lastIpAddress") - existing["lastSeenAt"] = current_time - return - - if len(self.users) >= self.max_entries: - # Remove the first added user (FIFO) - first_added_key = next(iter(self.users)) - del self.users[first_added_key] - - self.users[user_id] = { - "id": user_id, - "name": user.get("name"), - "lastIpAddress": user.get("lastIpAddress"), - "firstSeenAt": current_time, - "lastSeenAt": current_time, - } - - def as_array(self): - """ - Give all user entries back as an array - """ - return [ - { - "id": user["id"], - "name": user["name"], - "lastIpAddress": user["lastIpAddress"], - "firstSeenAt": user["firstSeenAt"], - "lastSeenAt": user["lastSeenAt"], - } - for user in self.users.values() - ] - - def clear(self): - """Clear out all users""" - self.users.clear() diff --git a/aikido_zen/background_process/users_test.py b/aikido_zen/background_process/users_test.py deleted file mode 100644 index 3b72187ac..000000000 --- a/aikido_zen/background_process/users_test.py +++ /dev/null @@ -1,71 +0,0 @@ -import time -import pytest -from .users import Users # Assuming the Users class is in a file named users.py - - -@pytest.fixture -def users(): - """Fixture to create a Users instance with a max of 2 entries.""" - return Users(max_entries=2) - - -def test_users(users, monkeypatch): - monkeypatch.setattr( - "aikido_zen.helpers.get_current_unixtime_ms.get_unixtime_ms", lambda: 1 - ) - assert users.as_array() == [] - - users.add_user({"id": "1", "name": "John", "lastIpAddress": "::1"}) - user1 = users.as_array()[0] - assert user1["id"] == "1" - assert user1["name"] == "John" - assert user1["lastIpAddress"] == "::1" - assert user1["lastSeenAt"] == user1["firstSeenAt"] == 1 - - # Simulate the passage of time - monkeypatch.setattr( - "aikido_zen.helpers.get_current_unixtime_ms.get_unixtime_ms", lambda: 2 - ) - users.add_user({"id": "1", "name": "John Doe", "lastIpAddress": "1.2.3.4"}) - user1_updated = users.as_array()[0] - assert user1_updated["id"] == "1" - assert user1_updated["name"] == "John Doe" - assert user1_updated["lastIpAddress"] == "1.2.3.4" - assert user1_updated["lastSeenAt"] == 2 - assert user1_updated["firstSeenAt"] == 1 - - users.add_user({"id": "2", "name": "Jane", "lastIpAddress": "1.2.3.4"}) - user2 = users.as_array()[1] - assert user2["id"] == "2" - assert user2["name"] == "Jane" - assert user2["lastIpAddress"] == "1.2.3.4" - assert ( - user2["lastSeenAt"] >= user2["firstSeenAt"] - ) # lastSeenAt should be >= firstSeenAt - assert ( - user2["lastSeenAt"] == user2["firstSeenAt"] - ) # Initially, they should be equal - - users.add_user({"id": "3", "name": "Alice", "lastIpAddress": "1.2.3.4"}) - user2_updated = users.as_array()[0] # Jane should still be the first user - user3 = users.as_array()[1] # Alice should be the second user - assert user2_updated["id"] == "2" - assert user2_updated["name"] == "Jane" - assert user2_updated["lastIpAddress"] == "1.2.3.4" - assert ( - user2_updated["lastSeenAt"] >= user2_updated["firstSeenAt"] - ) # lastSeenAt should be >= firstSeenAt - assert ( - user2_updated["lastSeenAt"] == user2_updated["firstSeenAt"] - ) # Should still be equal - - assert user3["id"] == "3" - assert user3["name"] == "Alice" - assert user3["lastIpAddress"] == "1.2.3.4" - assert ( - user3["lastSeenAt"] >= user3["firstSeenAt"] - ) # lastSeenAt should be >= firstSeenAt - assert user3["lastSeenAt"] == user3["firstSeenAt"] # Should be equal - - users.clear() - assert users.as_array() == [] diff --git a/aikido_zen/storage/users.py b/aikido_zen/storage/users.py new file mode 100644 index 000000000..714919c1d --- /dev/null +++ b/aikido_zen/storage/users.py @@ -0,0 +1,32 @@ +class Users: + def __init__(self, max_entries=1000): + self.max_entries = max_entries + self.users = {} + + def add_user(self, user_id, user_name, user_ip, current_time): + self.ensure_max_entries() + + first_seen_at = current_time + if self.users.get(user_id): + # Use the first_seen_at timestamp of the existing user + first_seen_at = self.users.get(user_id).get("firstSeenAt") + + self.users[user_id] = { + "id": user_id, + "name": user_name, + "lastIpAddress": user_ip, + "firstSeenAt": first_seen_at, + "lastSeenAt": current_time, + } + + def ensure_max_entries(self): + if len(self.users) >= self.max_entries: + # Remove the first added user (FIFO) + first_added_key = next(iter(self.users)) + del self.users[first_added_key] + + def as_array(self): + return [dict(user) for user in self.users.values()] + + def clear(self): + self.users.clear() diff --git a/aikido_zen/storage/users_test.py b/aikido_zen/storage/users_test.py new file mode 100644 index 000000000..8be52f0eb --- /dev/null +++ b/aikido_zen/storage/users_test.py @@ -0,0 +1,219 @@ +import pytest +from datetime import datetime +from .users import Users + + +@pytest.fixture +def users(): + return Users(max_entries=3) + + +def test_add_user(users): + user_id = "1" + user_name = "Test User" + user_ip = "127.0.0.1" + current_time = datetime.now() + + users.add_user(user_id, user_name, user_ip, current_time) + + assert user_id in users.users + assert users.users[user_id]["name"] == user_name + assert users.users[user_id]["lastIpAddress"] == user_ip + assert users.users[user_id]["firstSeenAt"] == current_time + assert users.users[user_id]["lastSeenAt"] == current_time + + +def test_add_existing_user(users): + user_id = "1" + user_name = "Test User" + user_ip = "127.0.0.1" + current_time = datetime.now() + + users.add_user(user_id, user_name, user_ip, current_time) + + new_user_ip = "192.168.1.1" + new_current_time = datetime.now() + + users.add_user(user_id, user_name, new_user_ip, new_current_time) + + assert user_id in users.users + assert users.users[user_id]["name"] == user_name + assert users.users[user_id]["lastIpAddress"] == new_user_ip + assert users.users[user_id]["firstSeenAt"] == current_time + assert users.users[user_id]["lastSeenAt"] == new_current_time + + +def test_ensure_max_entries(users): + user_id_1 = "1" + user_name_1 = "Test User 1" + user_ip_1 = "127.0.0.1" + current_time_1 = datetime.now() + + user_id_2 = "2" + user_name_2 = "Test User 2" + user_ip_2 = "192.168.1.1" + current_time_2 = datetime.now() + + user_id_3 = "3" + user_name_3 = "Test User 3" + user_ip_3 = "10.0.0.1" + current_time_3 = datetime.now() + + user_id_4 = "4" + user_name_4 = "Test User 4" + user_ip_4 = "172.16.0.1" + current_time_4 = datetime.now() + + users.add_user(user_id_1, user_name_1, user_ip_1, current_time_1) + users.add_user(user_id_2, user_name_2, user_ip_2, current_time_2) + users.add_user(user_id_3, user_name_3, user_ip_3, current_time_3) + users.add_user(user_id_4, user_name_4, user_ip_4, current_time_4) + + assert user_id_1 not in users.users + assert user_id_2 in users.users + assert user_id_3 in users.users + assert user_id_4 in users.users + + +def test_as_array(users): + user_id = "1" + user_name = "Test User" + user_ip = "127.0.0.1" + current_time = datetime.now() + + users.add_user(user_id, user_name, user_ip, current_time) + + user_array = users.as_array() + + assert len(user_array) == 1 + assert user_array[0]["id"] == user_id + assert user_array[0]["name"] == user_name + assert user_array[0]["lastIpAddress"] == user_ip + assert user_array[0]["firstSeenAt"] == current_time + assert user_array[0]["lastSeenAt"] == current_time + + +def test_clear(users): + user_id = "1" + user_name = "Test User" + user_ip = "127.0.0.1" + current_time = datetime.now() + + users.add_user(user_id, user_name, user_ip, current_time) + + users.clear() + + assert len(users.users) == 0 + + +def test_add_user_with_different_times(users): + user_id = "1" + user_name = "Test User" + user_ip = "127.0.0.1" + current_time_1 = datetime(2023, 1, 1, 12, 0, 0) + current_time_2 = datetime(2023, 1, 2, 12, 0, 0) + + users.add_user(user_id, user_name, user_ip, current_time_1) + users.add_user(user_id, user_name, user_ip, current_time_2) + + assert user_id in users.users + assert users.users[user_id]["firstSeenAt"] == current_time_1 + assert users.users[user_id]["lastSeenAt"] == current_time_2 + + +def test_add_multiple_users(users): + user_id_1 = "1" + user_name_1 = "Test User 1" + user_ip_1 = "127.0.0.1" + current_time_1 = datetime.now() + + user_id_2 = "2" + user_name_2 = "Test User 2" + user_ip_2 = "192.168.1.1" + current_time_2 = datetime.now() + + users.add_user(user_id_1, user_name_1, user_ip_1, current_time_1) + users.add_user(user_id_2, user_name_2, user_ip_2, current_time_2) + + assert user_id_1 in users.users + assert user_id_2 in users.users + assert users.users[user_id_1]["name"] == user_name_1 + assert users.users[user_id_2]["name"] == user_name_2 + + +def test_ensure_max_entries_with_duplicate_users(users): + user_id = "1" + user_name = "Test User" + user_ip = "127.0.0.1" + current_time = datetime.now() + + users.add_user(user_id, user_name, user_ip, current_time) + users.add_user(user_id, user_name, user_ip, current_time) + users.add_user(user_id, user_name, user_ip, current_time) + + assert len(users.users) == 1 + assert user_id in users.users + + +def test_as_array_with_multiple_users(users): + user_id_1 = "1" + user_name_1 = "Test User 1" + user_ip_1 = "127.0.0.1" + current_time_1 = datetime.now() + + user_id_2 = "2" + user_name_2 = "Test User 2" + user_ip_2 = "192.168.1.1" + current_time_2 = datetime.now() + + users.add_user(user_id_1, user_name_1, user_ip_1, current_time_1) + users.add_user(user_id_2, user_name_2, user_ip_2, current_time_2) + + user_array = users.as_array() + + assert len(user_array) == 2 + assert user_array[0]["id"] == user_id_1 + assert user_array[1]["id"] == user_id_2 + + +def test_clear_with_multiple_users(users): + user_id_1 = "1" + user_name_1 = "Test User 1" + user_ip_1 = "127.0.0.1" + current_time_1 = datetime.now() + + user_id_2 = "2" + user_name_2 = "Test User 2" + user_ip_2 = "192.168.1.1" + current_time_2 = datetime.now() + + users.add_user(user_id_1, user_name_1, user_ip_1, current_time_1) + users.add_user(user_id_2, user_name_2, user_ip_2, current_time_2) + + users.clear() + + assert len(users.users) == 0 + + +def test_add_user_with_empty_name(users): + user_id = "1" + user_name = "" + user_ip = "127.0.0.1" + current_time = datetime.now() + + users.add_user(user_id, user_name, user_ip, current_time) + + assert user_id in users.users + assert users.users[user_id]["name"] == user_name + + +def test_add_user_with_empty_ip(users): + user_id = "1" + user_name = "Test User" + user_ip = "" + current_time = datetime.now() + + users.add_user(user_id, user_name, user_ip, current_time) + + assert user_id in users.users + assert users.users[user_id]["lastIpAddress"] == user_ip From 78e3f421fefa448017b02b0795c5c9491d7c55cf Mon Sep 17 00:00:00 2001 From: BitterPanda63 Date: Thu, 15 May 2025 14:30:42 +0200 Subject: [PATCH 2/7] cloud_connection_manager update: users now in "storage" --- .../background_process/cloud_connection_manager/__init__.py | 2 +- .../background_process/cloud_connection_manager/init_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aikido_zen/background_process/cloud_connection_manager/__init__.py b/aikido_zen/background_process/cloud_connection_manager/__init__.py index 4cd2feebb..16f6a9bb4 100644 --- a/aikido_zen/background_process/cloud_connection_manager/__init__.py +++ b/aikido_zen/background_process/cloud_connection_manager/__init__.py @@ -7,7 +7,7 @@ from .update_firewall_lists import update_firewall_lists from ..api.http_api import ReportingApiHTTP from ..service_config import ServiceConfig -from ..users import Users +from aikido_zen.storage.users import Users from aikido_zen.storage.hostnames import Hostnames from ..realtime.start_polling_for_changes import start_polling_for_changes from ..statistics import Statistics diff --git a/aikido_zen/background_process/cloud_connection_manager/init_test.py b/aikido_zen/background_process/cloud_connection_manager/init_test.py index 657ce9b05..ac43fd4fc 100644 --- a/aikido_zen/background_process/cloud_connection_manager/init_test.py +++ b/aikido_zen/background_process/cloud_connection_manager/init_test.py @@ -2,7 +2,7 @@ from aikido_zen.helpers.token import Token from aikido_zen.background_process.api.http_api import ReportingApiHTTP from aikido_zen.background_process.service_config import ServiceConfig -from aikido_zen.background_process.users import Users +from aikido_zen.storage.users import Users from aikido_zen.storage.hostnames import Hostnames from aikido_zen.ratelimiting.rate_limiter import RateLimiter from aikido_zen.background_process.statistics import Statistics From 37410fe8d9f280c4519aae8aaafbd9f59d1b1ac5 Mon Sep 17 00:00:00 2001 From: BitterPanda63 Date: Thu, 15 May 2025 14:31:43 +0200 Subject: [PATCH 3/7] Delete the USER command --- .../background_process/commands/__init__.py | 2 -- .../background_process/commands/user.py | 7 ----- .../background_process/commands/user_test.py | 30 ------------------- 3 files changed, 39 deletions(-) delete mode 100644 aikido_zen/background_process/commands/user.py delete mode 100644 aikido_zen/background_process/commands/user_test.py diff --git a/aikido_zen/background_process/commands/__init__.py b/aikido_zen/background_process/commands/__init__.py index a643d3ede..a604de795 100644 --- a/aikido_zen/background_process/commands/__init__.py +++ b/aikido_zen/background_process/commands/__init__.py @@ -3,7 +3,6 @@ from aikido_zen.helpers.logging import logger from .attack import process_attack from .read_property import process_read_property -from .user import process_user from .should_ratelimit import process_should_ratelimit from .kill import process_kill from .statistics import process_statistics @@ -14,7 +13,6 @@ # This maps to a tuple : (function, returns_data?) # Commands that don't return data : "ATTACK": (process_attack, False), - "USER": (process_user, False), "KILL": (process_kill, False), "STATISTICS": (process_statistics, False), # Commands that return data : diff --git a/aikido_zen/background_process/commands/user.py b/aikido_zen/background_process/commands/user.py deleted file mode 100644 index a5039d877..000000000 --- a/aikido_zen/background_process/commands/user.py +++ /dev/null @@ -1,7 +0,0 @@ -"""Exports `process_user`""" - - -def process_user(connection_manager, data, queue=None): - """Adds a user to the users object of the connection_manager""" - if connection_manager: - connection_manager.users.add_user(data) diff --git a/aikido_zen/background_process/commands/user_test.py b/aikido_zen/background_process/commands/user_test.py deleted file mode 100644 index 47fbf7c46..000000000 --- a/aikido_zen/background_process/commands/user_test.py +++ /dev/null @@ -1,30 +0,0 @@ -import pytest -from unittest.mock import MagicMock -from .user import process_user - - -@pytest.fixture -def mock_connection_manager(): - """Fixture to create a mock connection_manager with a users attribute.""" - connection_manager = MagicMock() - connection_manager.users = MagicMock() - return connection_manager - - -def test_process_user_adds_user(mock_connection_manager): - """Test that process_user adds a user when connection_manager is present.""" - user_data = {"username": "test_user", "email": "test@example.com"} - - process_user( - mock_connection_manager, user_data - ) # conn is not used in this function - - # Check that the add_user method was called with the correct arguments - mock_connection_manager.users.add_user.assert_called_once_with(user_data) - - -def test_process_user_no_connection_manager(): - """Test that process_user does nothing when connection_manager is not present.""" - user_data = {"username": "test_user", "email": "test@example.com"} - - process_user(None, user_data) # conn is not used in this function From f5e04ccd79275d603117661eae597a783efe65fe Mon Sep 17 00:00:00 2001 From: BitterPanda63 Date: Thu, 15 May 2025 14:54:38 +0200 Subject: [PATCH 4/7] Thread cache now has users, and set_user calls thread cache --- aikido_zen/context/users.py | 12 +++++-- aikido_zen/context/users_test.py | 53 +++++++++++++++++++++++++++++++ aikido_zen/thread/thread_cache.py | 4 +++ 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/aikido_zen/context/users.py b/aikido_zen/context/users.py index 29e51adc5..598082736 100644 --- a/aikido_zen/context/users.py +++ b/aikido_zen/context/users.py @@ -5,6 +5,8 @@ from aikido_zen.helpers.logging import logger from aikido_zen.background_process import get_comms from . import get_current_context +from aikido_zen.thread.thread_cache import get_cache +from ..helpers.get_current_unixtime_ms import get_unixtime_ms def set_user(user): @@ -30,8 +32,14 @@ def set_user(user): context.user = validated_user # Send validated_user object to background process : - if get_comms(): - get_comms().send_data_to_bg_process("USER", validated_user) + cache = get_cache() + if cache: + cache.users.add_user( + user_id=validated_user["id"], + user_name=validated_user["name"], + user_ip=validated_user["lastIpAddress"], + current_time=get_unixtime_ms(), + ) def validate_user(user): diff --git a/aikido_zen/context/users_test.py b/aikido_zen/context/users_test.py index 6eee7a417..f6187df69 100644 --- a/aikido_zen/context/users_test.py +++ b/aikido_zen/context/users_test.py @@ -1,16 +1,21 @@ +import time + import pytest from . import current_context, Context from .users import validate_user, set_user from .. import should_block_request +from ..thread.thread_cache import get_cache @pytest.fixture(autouse=True) def run_around_tests(): + get_cache().reset() yield # Make sure to reset context and cache after every test so it does not # interfere with other tests current_context.set(None) + get_cache().reset() def set_context_and_lifecycle(): @@ -115,6 +120,13 @@ def test_set_valid_user(): "lastIpAddress": "198.51.100.23", } + assert len(get_cache().users.as_array()) == 1 + user_1 = get_cache().users.as_array()[0] + assert user_1["id"] == "456" + assert user_1["lastIpAddress"] == "198.51.100.23" + assert user_1["name"] == "Bob" + assert user_1["firstSeenAt"] == user_1["lastSeenAt"] + def test_re_set_valid_user(): context1 = set_context_and_lifecycle() @@ -128,6 +140,12 @@ def test_re_set_valid_user(): "name": "Bob", "lastIpAddress": "198.51.100.23", } + assert len(get_cache().users.as_array()) == 1 + user_1 = get_cache().users.as_array()[0] + assert user_1["id"] == "456" + assert user_1["lastIpAddress"] == "198.51.100.23" + assert user_1["name"] == "Bob" + assert user_1["firstSeenAt"] == user_1["lastSeenAt"] user = {"id": "1000", "name": "Alice"} set_user(user) @@ -138,6 +156,14 @@ def test_re_set_valid_user(): "lastIpAddress": "198.51.100.23", } + assert len(get_cache().users.as_array()) == 2 + assert get_cache().users.as_array()[0] == user_1 + user_2 = get_cache().users.as_array()[1] + assert user_2["id"] == "1000" + assert user_2["lastIpAddress"] == "198.51.100.23" + assert user_2["name"] == "Alice" + assert user_2["firstSeenAt"] == user_1["lastSeenAt"] + def test_after_middleware(caplog): context1 = set_context_and_lifecycle() @@ -154,3 +180,30 @@ def test_after_middleware(caplog): "name": "Bob", "lastIpAddress": "198.51.100.23", } + + +def test_set_valid_user_twice(): + context1 = set_context_and_lifecycle() + assert context1.user is None + + user = {"id": 456, "name": "Bob"} + set_user(user) + + assert context1.user == { + "id": "456", + "name": "Bob", + "lastIpAddress": "198.51.100.23", + } + assert len(get_cache().users.as_array()) == 1 + first_seen_at = int(get_cache().users.as_array()[0]["firstSeenAt"]) + + time.sleep(1) + set_user(user) # 2nd time + + assert len(get_cache().users.as_array()) == 1 + user_1 = get_cache().users.as_array()[0] + assert user_1["id"] == "456" + assert user_1["lastIpAddress"] == "198.51.100.23" + assert user_1["name"] == "Bob" + assert user_1["firstSeenAt"] != user_1["lastSeenAt"] + assert user_1["firstSeenAt"] == first_seen_at diff --git a/aikido_zen/thread/thread_cache.py b/aikido_zen/thread/thread_cache.py index 747e09a23..6e6940144 100644 --- a/aikido_zen/thread/thread_cache.py +++ b/aikido_zen/thread/thread_cache.py @@ -6,6 +6,7 @@ from aikido_zen.context import get_current_context from aikido_zen.helpers.logging import logger from aikido_zen.storage.hostnames import Hostnames +from aikido_zen.storage.users import Users from aikido_zen.thread import process_worker_loader @@ -16,6 +17,7 @@ class ThreadCache: def __init__(self): self.hostnames = Hostnames(200) + self.users = Users(1000) self.reset() # Initialize values def is_bypassed_ip(self, ip): @@ -42,6 +44,7 @@ def reset(self): self.reqs = 0 self.middleware_installed = False self.hostnames.clear() + self.users.clear() def renew(self): if not comms.get_comms(): @@ -55,6 +58,7 @@ def renew(self): "reqs": self.reqs, "middleware_installed": self.middleware_installed, "hostnames": self.hostnames.as_array(), + "users": self.users.as_array(), }, receive=True, ) From f56e81b4ca7199d337ae87a751c66b6b6ef87b2e Mon Sep 17 00:00:00 2001 From: BitterPanda63 Date: Thu, 15 May 2025 15:17:51 +0200 Subject: [PATCH 5/7] Add a new add_user_from_entry function to Users object --- aikido_zen/storage/users.py | 10 ++++ aikido_zen/storage/users_test.py | 86 ++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/aikido_zen/storage/users.py b/aikido_zen/storage/users.py index 714919c1d..90f0ae49d 100644 --- a/aikido_zen/storage/users.py +++ b/aikido_zen/storage/users.py @@ -19,6 +19,16 @@ def add_user(self, user_id, user_name, user_ip, current_time): "lastSeenAt": current_time, } + def add_user_from_entry(self, user_entry): + self.ensure_max_entries() + + existing_user = self.users.get(user_entry["id"]) + if existing_user: + # Use the firstSeenAt timestamp of the existing user + user_entry["firstSeenAt"] = existing_user["firstSeenAt"] + + self.users[user_entry["id"]] = user_entry + def ensure_max_entries(self): if len(self.users) >= self.max_entries: # Remove the first added user (FIFO) diff --git a/aikido_zen/storage/users_test.py b/aikido_zen/storage/users_test.py index 8be52f0eb..03fd3700a 100644 --- a/aikido_zen/storage/users_test.py +++ b/aikido_zen/storage/users_test.py @@ -1,3 +1,5 @@ +import time + import pytest from datetime import datetime from .users import Users @@ -217,3 +219,87 @@ def test_add_user_with_empty_ip(users): assert user_id in users.users assert users.users[user_id]["lastIpAddress"] == user_ip + + +def test_add_user_from_entry_new_user(users): + user_entry = { + "id": "1", + "name": "Test User", + "lastIpAddress": "127.0.0.1", + "firstSeenAt": datetime.now(), + "lastSeenAt": datetime.now(), + } + + users.add_user_from_entry(user_entry) + + assert "1" in users.users + assert users.users["1"]["name"] == "Test User" + assert users.users["1"]["lastIpAddress"] == "127.0.0.1" + + +def test_add_user_from_entry_existing_user(users): + user_id = "1" + user_name = "Test User" + user_ip = "127.0.0.1" + current_time = datetime.now() + + users.add_user(user_id, user_name, user_ip, current_time) + + new_user_entry = { + "id": "1", + "name": "Updated User", + "lastIpAddress": "192.168.1.1", + "firstSeenAt": datetime.now(), + "lastSeenAt": datetime.now(), + } + time.sleep(0.2) + users.add_user_from_entry(new_user_entry) + + assert "1" in users.users + assert users.users["1"]["name"] == "Updated User" + assert users.users["1"]["lastIpAddress"] == "192.168.1.1" + assert users.users["1"]["firstSeenAt"] == current_time + + +def test_add_user_from_entry_ensure_max_entries(users): + user_entry_1 = { + "id": "1", + "name": "Test User 1", + "lastIpAddress": "127.0.0.1", + "firstSeenAt": datetime.now(), + "lastSeenAt": datetime.now(), + } + + user_entry_2 = { + "id": "2", + "name": "Test User 2", + "lastIpAddress": "192.168.1.1", + "firstSeenAt": datetime.now(), + "lastSeenAt": datetime.now(), + } + + user_entry_3 = { + "id": "3", + "name": "Test User 3", + "lastIpAddress": "10.0.0.1", + "firstSeenAt": datetime.now(), + "lastSeenAt": datetime.now(), + } + + user_entry_4 = { + "id": "4", + "name": "Test User 4", + "lastIpAddress": "172.16.0.1", + "firstSeenAt": datetime.now(), + "lastSeenAt": datetime.now(), + } + + users.add_user_from_entry(user_entry_1) + users.add_user_from_entry(user_entry_2) + users.add_user_from_entry(user_entry_3) + users.add_user_from_entry(user_entry_4) + + assert "1" not in users.users + assert "2" in users.users + assert "3" in users.users + assert "4" in users.users From 666fc59a44c9bbab76d4bb181024b8d215147538 Mon Sep 17 00:00:00 2001 From: BitterPanda63 Date: Thu, 15 May 2025 15:17:59 +0200 Subject: [PATCH 6/7] Sync data sync with this new function --- aikido_zen/background_process/commands/sync_data.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/aikido_zen/background_process/commands/sync_data.py b/aikido_zen/background_process/commands/sync_data.py index b4f814a06..ebba56b31 100644 --- a/aikido_zen/background_process/commands/sync_data.py +++ b/aikido_zen/background_process/commands/sync_data.py @@ -40,6 +40,10 @@ def process_sync_data(connection_manager, data, conn, queue=None): hostnames_entry["hits"], ) + # Sync users + for user_entry in data.get("users", list()): + connection_manager.users.add_user_from_entry(user_entry) + if connection_manager.conf.last_updated_at > 0: # Only report data if the config has been fetched. return { From 58dedda94fe14caf132c76f43b9e0df03858f78d Mon Sep 17 00:00:00 2001 From: BitterPanda63 Date: Thu, 15 May 2025 15:37:59 +0200 Subject: [PATCH 7/7] Fix thread_cache test cases --- aikido_zen/context/users.py | 9 ++--- aikido_zen/thread/thread_cache_test.py | 55 +++++++++++++++++++++++++- 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/aikido_zen/context/users.py b/aikido_zen/context/users.py index 598082736..0b61c1567 100644 --- a/aikido_zen/context/users.py +++ b/aikido_zen/context/users.py @@ -3,10 +3,9 @@ """ from aikido_zen.helpers.logging import logger -from aikido_zen.background_process import get_comms from . import get_current_context -from aikido_zen.thread.thread_cache import get_cache -from ..helpers.get_current_unixtime_ms import get_unixtime_ms +import aikido_zen.thread.thread_cache as thread_cache +import aikido_zen.helpers.get_current_unixtime_ms as t def set_user(user): @@ -32,13 +31,13 @@ def set_user(user): context.user = validated_user # Send validated_user object to background process : - cache = get_cache() + cache = thread_cache.get_cache() if cache: cache.users.add_user( user_id=validated_user["id"], user_name=validated_user["name"], user_ip=validated_user["lastIpAddress"], - current_time=get_unixtime_ms(), + current_time=t.get_unixtime_ms(), ) diff --git a/aikido_zen/thread/thread_cache_test.py b/aikido_zen/thread/thread_cache_test.py index 8a4d2612e..3eca67ce3 100644 --- a/aikido_zen/thread/thread_cache_test.py +++ b/aikido_zen/thread/thread_cache_test.py @@ -2,6 +2,7 @@ from unittest.mock import patch, MagicMock from aikido_zen.background_process.routes import Routes from .thread_cache import ThreadCache, get_cache +from .. import set_user from ..background_process.service_config import ServiceConfig from ..context import current_context, Context from aikido_zen.helpers.iplist import IPList @@ -15,7 +16,8 @@ def thread_cache(): class Context2(Context): def __init__(self): - pass + self.executed_middleware = False + self.remote_address = "5.6.7.8" @pytest.fixture(autouse=True) @@ -235,6 +237,55 @@ def test_renew_called_with_correct_args(mock_get_comms, thread_cache: ThreadCach "reqs": 1, "middleware_installed": False, "hostnames": [], + "users": [], + }, + receive=True, + ) + + +@patch("aikido_zen.background_process.comms.get_comms") +def test_sync_data_for_users(mock_get_comms, thread_cache: ThreadCache): + """Test that renew calls send_data_to_bg_process with correct arguments.""" + mock_comms = MagicMock() + mock_get_comms.return_value = mock_comms + Context2().set_as_current_context() + + # Setup initial state + thread_cache.increment_stats() + with patch("aikido_zen.thread.thread_cache.get_cache", return_value=thread_cache): + with patch( + "aikido_zen.helpers.get_current_unixtime_ms.get_unixtime_ms", return_value=1 + ): + set_user({"id": "123", "name": "test"}) + set_user({"id": "567", "name": "test"}) + + # Call renew + thread_cache.renew() + + # Assert that send_data_to_bg_process was called with the correct arguments + mock_comms.send_data_to_bg_process.assert_called_once_with( + action="SYNC_DATA", + obj={ + "current_routes": {}, + "reqs": 1, + "middleware_installed": False, + "hostnames": [], + "users": [ + { + "id": "123", + "name": "test", + "lastIpAddress": "5.6.7.8", + "firstSeenAt": 1, + "lastSeenAt": 1, + }, + { + "id": "567", + "name": "test", + "lastIpAddress": "5.6.7.8", + "firstSeenAt": 1, + "lastSeenAt": 1, + }, + ], }, receive=True, ) @@ -257,6 +308,7 @@ def test_renew_called_with_empty_routes(mock_get_comms, thread_cache: ThreadCach "reqs": 0, "middleware_installed": False, "hostnames": [], + "users": [], }, receive=True, ) @@ -282,6 +334,7 @@ def test_renew_called_with_no_requests(mock_get_comms, thread_cache: ThreadCache "reqs": 0, "middleware_installed": False, "hostnames": [], + "users": [], }, receive=True, )