Skip to content
This repository was archived by the owner on Feb 18, 2026. It is now read-only.

Commit dd6ecb7

Browse files
committed
Add Redis testcontainer and replace mocks with real Redis in tests
- Add RedisContainer to test fixtures alongside Cassandra and Postgres - Add redis_client fixture that provides async Redis client per test - Update TagAccessLoggerTagstoreProxy tests to use real Redis - Add test for counter increment behavior on multiple accesses
1 parent 57d34b1 commit dd6ecb7

2 files changed

Lines changed: 116 additions & 54 deletions

File tree

tests/conftest.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import pytest
1010
from testcontainers.cassandra import CassandraContainer
1111
from testcontainers.postgres import PostgresContainer
12+
from testcontainers.redis import RedisContainer
1213

1314
from tests import BaseTestCase
1415
from tests.cassandra.insert import load_test_data as cas_load_test_data
@@ -18,6 +19,7 @@
1819
_migration_timing_data = []
1920

2021
postgres = PostgresContainer("postgres:16-alpine")
22+
redis = RedisContainer("redis:7-alpine")
2123

2224
# Pre-baked Cassandra image with schemas and fast startup settings already configured
2325
# Build with: make build-test-cassandra
@@ -52,10 +54,12 @@ def gs_rest_db_setup(request):
5254

5355
postgres.start()
5456
cassandra.start()
57+
redis.start()
5558

5659
def remove_container():
5760
postgres.stop()
5861
cassandra.stop()
62+
redis.stop()
5963

6064
request.addfinalizer(remove_container)
6165

@@ -65,6 +69,10 @@ def remove_container():
6569
postgres_sync_url = postgres.get_connection_url()
6670
portgres_async_url = postgres_sync_url.replace("psycopg2", "asyncpg")
6771

72+
redis_host = redis.get_container_host_ip()
73+
redis_port = redis.get_exposed_port(6379)
74+
redis_url = f"redis://{redis_host}:{redis_port}"
75+
6876
config = {
6977
"logging": {"level": "DEBUG"},
7078
"database": {
@@ -93,6 +101,7 @@ def remove_container():
93101
},
94102
"gs-tagstore": {"url": portgres_async_url},
95103
"show_private_tags": {"on_header": {"Authorization": "x"}},
104+
"tag_access_logger": {"redis_url": redis_url},
96105
}
97106

98107
# Ugly hack to pass parameters
@@ -218,3 +227,18 @@ def migration_timing_report(request):
218227
}
219228
report_path.write_text(json.dumps(report, indent=2))
220229
write(f"Detailed report saved to: {report_path}")
230+
231+
232+
@pytest.fixture
233+
async def redis_client(gs_rest_db_setup):
234+
"""Provide an async Redis client for tests."""
235+
from redis import asyncio as aioredis
236+
237+
redis_host = redis.get_container_host_ip()
238+
redis_port = redis.get_exposed_port(6379)
239+
redis_url = f"redis://{redis_host}:{redis_port}"
240+
241+
client = await aioredis.from_url(redis_url)
242+
yield client
243+
await client.flushdb()
244+
await client.aclose()

tests/test_tag_access_logger.py

Lines changed: 92 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,8 @@ class TestShouldLogResult:
4141

4242
def setup_method(self):
4343
self.mock_tagstore_db = MagicMock()
44-
self.mock_redis = AsyncMock()
4544
self.proxy = TagAccessLoggerTagstoreProxy(
46-
self.mock_tagstore_db, self.mock_redis, "test_prefix"
45+
self.mock_tagstore_db, None, "test_prefix"
4746
)
4847

4948
def test_returns_false_for_none(self):
@@ -95,36 +94,33 @@ def test_returns_false_for_integer(self):
9594

9695

9796
class TestLogTagAccess:
98-
"""Tests for _log_tag_access method."""
99-
100-
def setup_method(self):
101-
self.mock_tagstore_db = MagicMock()
102-
self.mock_redis = AsyncMock()
103-
self.proxy = TagAccessLoggerTagstoreProxy(
104-
self.mock_tagstore_db, self.mock_redis, "tag_access"
105-
)
97+
"""Tests for _log_tag_access method with real Redis."""
10698

10799
@pytest.mark.asyncio
108-
async def test_increments_redis_key_with_correct_format(self):
109-
"""Should call redis.incr with correctly formatted key."""
110-
tag = create_mock_tag(
111-
identifier="1ABC123", creator="iknaio", network="btc"
100+
async def test_increments_redis_key_with_correct_format(self, redis_client):
101+
"""Should increment Redis key with correctly formatted key."""
102+
mock_tagstore_db = MagicMock()
103+
proxy = TagAccessLoggerTagstoreProxy(
104+
mock_tagstore_db, redis_client, "tag_access"
112105
)
106+
tag = create_mock_tag(identifier="1ABC123", creator="iknaio", network="btc")
113107

114108
with patch("gsrest.dependencies.time") as mock_time:
115109
mock_time.localtime.return_value = None
116110
mock_time.strftime.return_value = "2025-01-27"
117111

118-
await self.proxy._log_tag_access("get_tags", tag)
112+
await proxy._log_tag_access("get_tags", tag)
119113

120114
expected_key = "tag_access|2025-01-27|iknaio|btc|1ABC123"
121-
self.mock_redis.incr.assert_called_once_with(expected_key)
115+
value = await redis_client.get(expected_key)
116+
assert value == b"1"
122117

123118
@pytest.mark.asyncio
124-
async def test_uses_configured_prefix(self):
119+
async def test_uses_configured_prefix(self, redis_client):
125120
"""Should use the configured prefix in the Redis key."""
121+
mock_tagstore_db = MagicMock()
126122
proxy = TagAccessLoggerTagstoreProxy(
127-
self.mock_tagstore_db, self.mock_redis, "custom_prefix"
123+
mock_tagstore_db, redis_client, "custom_prefix"
128124
)
129125
tag = create_mock_tag(identifier="addr1", creator="creator1", network="eth")
130126

@@ -135,102 +131,144 @@ async def test_uses_configured_prefix(self):
135131
await proxy._log_tag_access("some_method", tag)
136132

137133
expected_key = "custom_prefix|2025-12-31|creator1|eth|addr1"
138-
self.mock_redis.incr.assert_called_once_with(expected_key)
134+
value = await redis_client.get(expected_key)
135+
assert value == b"1"
139136

137+
@pytest.mark.asyncio
138+
async def test_increments_counter_on_multiple_accesses(self, redis_client):
139+
"""Should increment counter each time the same tag is accessed."""
140+
mock_tagstore_db = MagicMock()
141+
proxy = TagAccessLoggerTagstoreProxy(
142+
mock_tagstore_db, redis_client, "tag_access"
143+
)
144+
tag = create_mock_tag(identifier="addr1", creator="creator1", network="btc")
140145

141-
class TestProxyMethodCalls:
142-
"""Tests for __getattr__ proxy behavior."""
146+
with patch("gsrest.dependencies.time") as mock_time:
147+
mock_time.localtime.return_value = None
148+
mock_time.strftime.return_value = "2025-01-27"
143149

144-
def setup_method(self):
145-
self.mock_tagstore_db = MagicMock()
146-
self.mock_redis = AsyncMock()
147-
self.proxy = TagAccessLoggerTagstoreProxy(
148-
self.mock_tagstore_db, self.mock_redis, "test_prefix"
149-
)
150+
await proxy._log_tag_access("get_tags", tag)
151+
await proxy._log_tag_access("get_tags", tag)
152+
await proxy._log_tag_access("get_tags", tag)
153+
154+
expected_key = "tag_access|2025-01-27|creator1|btc|addr1"
155+
value = await redis_client.get(expected_key)
156+
assert value == b"3"
157+
158+
159+
class TestProxyMethodCalls:
160+
"""Tests for __getattr__ proxy behavior with real Redis."""
150161

151162
@pytest.mark.asyncio
152-
async def test_proxies_method_call_to_underlying_db(self):
163+
async def test_proxies_method_call_to_underlying_db(self, redis_client):
153164
"""Should proxy method calls to the underlying tagstore_db."""
165+
mock_tagstore_db = MagicMock()
154166
mock_method = AsyncMock(return_value=None)
155-
self.mock_tagstore_db.get_tags = mock_method
167+
mock_tagstore_db.get_tags = mock_method
168+
proxy = TagAccessLoggerTagstoreProxy(
169+
mock_tagstore_db, redis_client, "test_prefix"
170+
)
156171

157-
await self.proxy.get_tags("btc", "some_address")
172+
await proxy.get_tags("btc", "some_address")
158173

159174
mock_method.assert_called_once_with("btc", "some_address")
160175

161176
@pytest.mark.asyncio
162-
async def test_returns_result_from_underlying_method(self):
177+
async def test_returns_result_from_underlying_method(self, redis_client):
163178
"""Should return the result from the underlying method."""
179+
mock_tagstore_db = MagicMock()
164180
expected_result = {"data": "test"}
165-
self.mock_tagstore_db.some_method = AsyncMock(return_value=expected_result)
181+
mock_tagstore_db.some_method = AsyncMock(return_value=expected_result)
182+
proxy = TagAccessLoggerTagstoreProxy(
183+
mock_tagstore_db, redis_client, "test_prefix"
184+
)
166185

167-
result = await self.proxy.some_method()
186+
result = await proxy.some_method()
168187

169188
assert result == expected_result
170189

171190
@pytest.mark.asyncio
172-
async def test_logs_single_tag_result(self):
191+
async def test_logs_single_tag_result(self, redis_client):
173192
"""Should log access when method returns a single TagPublic."""
193+
mock_tagstore_db = MagicMock()
174194
tag = create_mock_tag(identifier="addr1", creator="creator1", network="btc")
175-
self.mock_tagstore_db.get_tag = AsyncMock(return_value=tag)
195+
mock_tagstore_db.get_tag = AsyncMock(return_value=tag)
196+
proxy = TagAccessLoggerTagstoreProxy(
197+
mock_tagstore_db, redis_client, "test_prefix"
198+
)
176199

177200
with patch("gsrest.dependencies.time") as mock_time:
178201
mock_time.localtime.return_value = None
179202
mock_time.strftime.return_value = "2025-01-27"
180203

181-
result = await self.proxy.get_tag("btc", "addr1")
204+
result = await proxy.get_tag("btc", "addr1")
182205

183206
assert result == tag
184-
self.mock_redis.incr.assert_called_once()
185-
call_args = self.mock_redis.incr.call_args[0][0]
186-
assert "test_prefix|2025-01-27|creator1|btc|addr1" == call_args
207+
expected_key = "test_prefix|2025-01-27|creator1|btc|addr1"
208+
value = await redis_client.get(expected_key)
209+
assert value == b"1"
187210

188211
@pytest.mark.asyncio
189-
async def test_logs_each_tag_in_list_result(self):
212+
async def test_logs_each_tag_in_list_result(self, redis_client):
190213
"""Should log access for each tag when method returns a list."""
214+
mock_tagstore_db = MagicMock()
191215
tags = [
192216
create_mock_tag(identifier="addr1", creator="creator1", network="btc"),
193217
create_mock_tag(identifier="addr2", creator="creator2", network="btc"),
194218
]
195-
self.mock_tagstore_db.get_tags = AsyncMock(return_value=tags)
219+
mock_tagstore_db.get_tags = AsyncMock(return_value=tags)
220+
proxy = TagAccessLoggerTagstoreProxy(
221+
mock_tagstore_db, redis_client, "test_prefix"
222+
)
196223

197224
with patch("gsrest.dependencies.time") as mock_time:
198225
mock_time.localtime.return_value = None
199226
mock_time.strftime.return_value = "2025-01-27"
200227

201-
result = await self.proxy.get_tags("btc", ["addr1", "addr2"])
228+
result = await proxy.get_tags("btc", ["addr1", "addr2"])
202229

203230
assert result == tags
204-
assert self.mock_redis.incr.call_count == 2
231+
value1 = await redis_client.get("test_prefix|2025-01-27|creator1|btc|addr1")
232+
value2 = await redis_client.get("test_prefix|2025-01-27|creator2|btc|addr2")
233+
assert value1 == b"1"
234+
assert value2 == b"1"
205235

206236
@pytest.mark.asyncio
207-
async def test_does_not_log_non_tag_results(self):
237+
async def test_does_not_log_non_tag_results(self, redis_client):
208238
"""Should not log when method returns non-TagPublic results."""
209-
self.mock_tagstore_db.get_count = AsyncMock(return_value=42)
239+
mock_tagstore_db = MagicMock()
240+
mock_tagstore_db.get_count = AsyncMock(return_value=42)
241+
proxy = TagAccessLoggerTagstoreProxy(
242+
mock_tagstore_db, redis_client, "test_prefix"
243+
)
210244

211-
result = await self.proxy.get_count()
245+
result = await proxy.get_count()
212246

213247
assert result == 42
214-
self.mock_redis.incr.assert_not_called()
248+
keys = await redis_client.keys("test_prefix|*")
249+
assert keys == []
215250

216251
@pytest.mark.asyncio
217252
async def test_does_not_log_when_redis_client_is_none(self):
218253
"""Should not attempt to log when redis_client is None."""
219-
proxy = TagAccessLoggerTagstoreProxy(
220-
self.mock_tagstore_db, None, "test_prefix"
221-
)
254+
mock_tagstore_db = MagicMock()
255+
proxy = TagAccessLoggerTagstoreProxy(mock_tagstore_db, None, "test_prefix")
222256
tag = create_mock_tag()
223-
self.mock_tagstore_db.get_tag = AsyncMock(return_value=tag)
257+
mock_tagstore_db.get_tag = AsyncMock(return_value=tag)
224258

225259
result = await proxy.get_tag("btc", "addr1")
226260

227261
assert result == tag
228262
# No exception should be raised
229263

230-
def test_proxies_non_callable_attributes(self):
264+
def test_proxies_non_callable_attributes(self, redis_client):
231265
"""Should proxy non-callable attributes directly."""
232-
self.mock_tagstore_db.some_attribute = "test_value"
266+
mock_tagstore_db = MagicMock()
267+
mock_tagstore_db.some_attribute = "test_value"
268+
proxy = TagAccessLoggerTagstoreProxy(
269+
mock_tagstore_db, redis_client, "test_prefix"
270+
)
233271

234-
result = self.proxy.some_attribute
272+
result = proxy.some_attribute
235273

236274
assert result == "test_value"

0 commit comments

Comments
 (0)