test_auth_client.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. import os
  2. import os
  3. import tempfile
  4. import unittest
  5. from config import GatewayConfig
  6. from services.auth_client import AuthClient
  7. from services.token_store import InMemoryTokenStore
  8. class DummyTransport:
  9. def __init__(self):
  10. self.calls = []
  11. self.responses = [] # Queue of responses to return
  12. def post_json(self, url, payload, headers, timeout):
  13. self.calls.append(
  14. {
  15. 'url': url,
  16. 'payload': payload,
  17. 'headers': headers,
  18. 'timeout': timeout,
  19. }
  20. )
  21. # If custom responses are queued, return the next one
  22. if self.responses:
  23. return self.responses.pop(0)
  24. # Default responses
  25. if url.endswith('/mcp/auth/exchange'):
  26. return {
  27. 'code': 'MCP_0000',
  28. 'msg': 'success',
  29. 'data': {
  30. 'mcp_token': 'MT_exchange',
  31. 'expire_time': '2099-01-01T00:00:00',
  32. },
  33. }
  34. if url.endswith('/mcp/auth/refresh'):
  35. return {
  36. 'code': 'MCP_0000',
  37. 'msg': 'success',
  38. 'data': {
  39. 'mcp_token': 'MT_refresh',
  40. 'expire_time': '2099-01-02T00:00:00',
  41. },
  42. }
  43. return {
  44. 'code': 'MCP_0000',
  45. 'msg': 'success',
  46. 'data': {},
  47. }
  48. class AuthClientTest(unittest.TestCase):
  49. def test_gateway_config_reads_expected_env_keys(self):
  50. env = {
  51. 'MCP_AUTH_BASE_URL': 'http://base.example.test',
  52. 'MCP_TOOLS_BASE_URL': 'http://tools.example.test',
  53. 'MCP_CLIENT_TYPE': 'workbuddy',
  54. 'MCP_TIMEOUT_SECONDS': '9',
  55. 'MCP_REFRESH_SKEW_SECONDS': '120',
  56. }
  57. config = GatewayConfig.from_env(env, dotenv_path=os.path.join(tempfile.gettempdir(), 'missing-auth-client.env'))
  58. self.assertEqual('http://base.example.test', config.auth_base_url)
  59. self.assertEqual('http://tools.example.test', config.tools_base_url)
  60. self.assertEqual('workbuddy', config.client_type)
  61. self.assertEqual(9, config.timeout_seconds)
  62. self.assertEqual(120, config.refresh_skew_seconds)
  63. def test_auth_client_uses_auth_routes_and_updates_token_store(self):
  64. transport = DummyTransport()
  65. store = InMemoryTokenStore(refresh_skew_seconds=60)
  66. client = AuthClient(
  67. base_url='http://base.example.test',
  68. client_type='workbuddy',
  69. token_store=store,
  70. transport=transport,
  71. timeout=9,
  72. )
  73. exchange = client.exchange('AUTH123')
  74. refresh = client.refresh('MT_exchange')
  75. self.assertEqual('MT_refresh', store.get()['token'])
  76. revoke = client.revoke('MT_refresh')
  77. self.assertEqual('MT_exchange', exchange['data']['mcp_token'])
  78. self.assertEqual('MT_refresh', refresh['data']['mcp_token'])
  79. self.assertIsNone(store.get())
  80. self.assertEqual({}, revoke['data'])
  81. self.assertEqual('http://base.example.test/mcp/auth/exchange', transport.calls[0]['url'])
  82. self.assertEqual('http://base.example.test/mcp/auth/refresh', transport.calls[1]['url'])
  83. self.assertEqual('http://base.example.test/mcp/auth/revoke', transport.calls[2]['url'])
  84. self.assertEqual({'auth_code': 'AUTH123', 'client_type': 'workbuddy'}, transport.calls[0]['payload'])
  85. self.assertEqual({'mcp_token': 'MT_exchange'}, transport.calls[1]['payload'])
  86. self.assertEqual({'mcp_token': 'MT_refresh'}, transport.calls[2]['payload'])
  87. def test_refresh_and_revoke_send_bearer_header_and_revoke_clears_token(self):
  88. transport = DummyTransport()
  89. store = InMemoryTokenStore(refresh_skew_seconds=60)
  90. store.save('MT_exchange', '2099-01-01T00:00:00')
  91. client = AuthClient(
  92. base_url='http://base.example.test',
  93. client_type='workbuddy',
  94. token_store=store,
  95. transport=transport,
  96. timeout=9,
  97. )
  98. client.refresh('MT_exchange')
  99. client.revoke('MT_refresh')
  100. self.assertEqual('Bearer MT_exchange', transport.calls[0]['headers']['Authorization'])
  101. self.assertEqual('Bearer MT_refresh', transport.calls[1]['headers']['Authorization'])
  102. self.assertIsNone(store.get())
  103. def test_revoke_does_not_clear_token_on_failure(self):
  104. """Test that revoke does not clear token when response code is not MCP_0000"""
  105. transport = DummyTransport()
  106. transport.responses = [
  107. {'code': 'MCP_9999', 'msg': 'revoke failed'}
  108. ]
  109. store = InMemoryTokenStore(refresh_skew_seconds=60)
  110. store.save('MT_token', '2099-01-01T00:00:00')
  111. client = AuthClient(
  112. base_url='http://base.example.test',
  113. client_type='workbuddy',
  114. token_store=store,
  115. transport=transport,
  116. timeout=9,
  117. )
  118. result = client.revoke('MT_token')
  119. # Token should NOT be cleared on failure
  120. self.assertIsNotNone(store.get())
  121. self.assertEqual(result['code'], 'MCP_9999')
  122. def test_persist_token_handles_missing_expire_time(self):
  123. """Test that token is not persisted when expire_time is missing"""
  124. transport = DummyTransport()
  125. transport.responses = [
  126. {
  127. 'code': 'MCP_0000',
  128. 'data': {
  129. 'mcp_token': 'MT_no_expire'
  130. # Missing expire_time
  131. }
  132. }
  133. ]
  134. store = InMemoryTokenStore(refresh_skew_seconds=60)
  135. client = AuthClient(
  136. base_url='http://base.example.test',
  137. client_type='workbuddy',
  138. token_store=store,
  139. transport=transport,
  140. timeout=9,
  141. session_key='test_session'
  142. )
  143. client.exchange('AC_test')
  144. # Token should not be saved without expire_time
  145. self.assertIsNone(store.get())
  146. if __name__ == '__main__':
  147. unittest.main()