test_redis_token_store.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. import json
  2. import os
  3. import tempfile
  4. import unittest
  5. from app import GatewayApp
  6. from config import GatewayConfig
  7. from services.auth_client import AuthClient
  8. from services.token_store import RedisTokenStore
  9. class FakeRedisClient:
  10. def __init__(self):
  11. self.values = {}
  12. self.expiries = {}
  13. self.deleted = []
  14. def set(self, key, value, ex=None):
  15. self.values[key] = value
  16. self.expiries[key] = ex
  17. return True
  18. def get(self, key):
  19. return self.values.get(key)
  20. def delete(self, key):
  21. self.deleted.append(key)
  22. self.values.pop(key, None)
  23. return 1
  24. class DummyTransport:
  25. def __init__(self):
  26. self.calls = []
  27. def post_json(self, url, payload, headers, timeout):
  28. self.calls.append(
  29. {
  30. 'url': url,
  31. 'payload': payload,
  32. 'headers': headers,
  33. 'timeout': timeout,
  34. }
  35. )
  36. return {
  37. 'code': 'MCP_0000',
  38. 'msg': 'success',
  39. 'data': {
  40. 'mcp_token': 'MT_session',
  41. 'expire_time': '2099-01-01T00:00:00',
  42. },
  43. }
  44. class RedisTokenStoreTest(unittest.TestCase):
  45. def test_gateway_config_reads_redis_store_settings_from_dotenv(self):
  46. with tempfile.TemporaryDirectory() as tmp_dir:
  47. dotenv_path = os.path.join(tmp_dir, '.env')
  48. with open(dotenv_path, 'w', encoding='utf-8') as file:
  49. file.write('FMS_AUTH_BASE=http://auth.example.test\n')
  50. file.write('FMS_TOOLS_BASE=http://tools.example.test\n')
  51. file.write('FMS_TOKEN_STORE=redis\n')
  52. file.write('FMS_REDIS_HOST=192.168.1.241\n')
  53. file.write('FMS_REDIS_PORT=6380\n')
  54. file.write('FMS_REDIS_DB=2\n')
  55. file.write('FMS_REDIS_PASSWORD=secret\n')
  56. file.write('FMS_REDIS_PREFIX=fms:mcp:workbuddy:\n')
  57. file.write('FMS_SESSION_KEY=pc-a:user-a\n')
  58. config = GatewayConfig.from_env(env={}, dotenv_path=dotenv_path)
  59. self.assertEqual('redis', config.token_store_type)
  60. self.assertEqual('192.168.1.241', config.redis_host)
  61. self.assertEqual(6380, config.redis_port)
  62. self.assertEqual(2, config.redis_db)
  63. self.assertEqual('secret', config.redis_password)
  64. self.assertEqual('fms:mcp:workbuddy:', config.redis_prefix)
  65. self.assertEqual('pc-a:user-a', config.session_key)
  66. def test_redis_token_store_isolates_tokens_by_session_key(self):
  67. redis = FakeRedisClient()
  68. first = RedisTokenStore(redis, prefix='fms:mcp:workbuddy:', session_key='pc-a:user-a')
  69. second = RedisTokenStore(redis, prefix='fms:mcp:workbuddy:', session_key='pc-b:user-b')
  70. first.save('MT_first', '2099-01-01T00:00:00')
  71. second.save('MT_second', '2099-01-01T00:00:00')
  72. self.assertEqual('MT_first', first.get()['token'])
  73. self.assertEqual('MT_second', second.get()['token'])
  74. self.assertIn('fms:mcp:workbuddy:pc-a:user-a', redis.values)
  75. self.assertIn('fms:mcp:workbuddy:pc-b:user-b', redis.values)
  76. self.assertNotEqual(redis.values['fms:mcp:workbuddy:pc-a:user-a'], redis.values['fms:mcp:workbuddy:pc-b:user-b'])
  77. first.clear()
  78. self.assertIsNone(first.get())
  79. self.assertEqual('MT_second', second.get()['token'])
  80. self.assertEqual(['fms:mcp:workbuddy:pc-a:user-a'], redis.deleted)
  81. def test_auth_client_refresh_persists_token_and_sends_bearer(self):
  82. transport = DummyTransport()
  83. redis = FakeRedisClient()
  84. store = RedisTokenStore(redis, prefix='fms:mcp:workbuddy:', session_key='pc-a:user-a')
  85. client = AuthClient(
  86. base_url='http://auth.example.test',
  87. client_type='workbuddy',
  88. token_store=store,
  89. transport=transport,
  90. timeout=9,
  91. session_key='pc-a:user-a',
  92. )
  93. client.refresh('MT_old')
  94. self.assertEqual('http://auth.example.test/mcp/auth/refresh', transport.calls[0]['url'])
  95. self.assertEqual({'mcp_token': 'MT_old'}, transport.calls[0]['payload'])
  96. self.assertEqual({'Authorization': 'Bearer MT_old'}, transport.calls[0]['headers'])
  97. self.assertEqual('MT_session', store.get()['token'])
  98. def test_gateway_from_config_uses_redis_token_store_when_configured(self):
  99. config = GatewayConfig(
  100. auth_base_url='http://auth.example.test',
  101. tools_base_url='http://tools.example.test',
  102. token_store_type='redis',
  103. redis_host='127.0.0.1',
  104. redis_port=6379,
  105. redis_db=0,
  106. redis_password='',
  107. redis_prefix='fms:mcp:workbuddy:',
  108. session_key='pc-a:user-a',
  109. )
  110. app = GatewayApp.from_config(config, redis_client=FakeRedisClient())
  111. self.assertIsInstance(app.token_store, RedisTokenStore)
  112. self.assertEqual('fms:mcp:workbuddy:pc-a:user-a', app.token_store.key)
  113. self.assertEqual('pc-a:user-a', app.auth_client.session_key)
  114. if __name__ == '__main__':
  115. unittest.main()