test_features_txt_client.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. import requests
  2. from unittest.mock import Mock, patch
  3. import dill
  4. from metadata_manager.firmware_server.features import (
  5. FeaturesTxtClient,
  6. parse_features_txt,
  7. )
  8. class TestParseFeaturesTxt:
  9. def test_parses_enabled_and_disabled_features(self):
  10. text = "\n".join([
  11. "HAL_LOGGING_ENABLED",
  12. "!HAL_PROFILER_ENABLED",
  13. "# comment",
  14. "",
  15. "AP_FENCE_ENABLED",
  16. ])
  17. assert parse_features_txt(text) == {
  18. "HAL_LOGGING_ENABLED": 1,
  19. "HAL_PROFILER_ENABLED": 0,
  20. "AP_FENCE_ENABLED": 1,
  21. }
  22. class TestFeaturesTxtClient:
  23. def test_get_defaults_fetches_and_caches_with_etag(self):
  24. client = FeaturesTxtClient(caching_enabled=True)
  25. client._redis_client = Mock()
  26. client._redis_client.get.return_value = None
  27. response = Mock(
  28. status_code=200,
  29. text="HAL_LOGGING_ENABLED\n!HAL_PROFILER_ENABLED",
  30. headers={"ETag": '"abc"'},
  31. )
  32. response.raise_for_status = Mock()
  33. with patch(
  34. "metadata_manager.firmware_server.features.requests.get",
  35. return_value=response,
  36. ) as mock_get:
  37. result = client.get_defaults("https://example.com/features.txt")
  38. assert result == {
  39. "HAL_LOGGING_ENABLED": 1,
  40. "HAL_PROFILER_ENABLED": 0,
  41. }
  42. mock_get.assert_called_once()
  43. client._redis_client.set.assert_called_once()
  44. stored = dill.loads(client._redis_client.set.call_args.kwargs["value"])
  45. assert stored == {
  46. "etag": '"abc"',
  47. "defaults": result,
  48. }
  49. def test_get_defaults_skips_cache_without_etag(self):
  50. client = FeaturesTxtClient(caching_enabled=True)
  51. client._redis_client = Mock()
  52. client._redis_client.get.return_value = None
  53. response = Mock(status_code=200, text="HAL_LOGGING_ENABLED", headers={})
  54. response.raise_for_status = Mock()
  55. with patch(
  56. "metadata_manager.firmware_server.features.requests.get",
  57. return_value=response,
  58. ):
  59. result = client.get_defaults("https://example.com/features.txt")
  60. assert result == {"HAL_LOGGING_ENABLED": 1}
  61. client._redis_client.set.assert_not_called()
  62. def test_get_defaults_revalidates_cached_entry_with_if_none_match(self):
  63. client = FeaturesTxtClient(caching_enabled=True)
  64. client._redis_client = Mock()
  65. cached = {
  66. "etag": '"abc"',
  67. "defaults": {"HAL_LOGGING_ENABLED": 1},
  68. }
  69. client._redis_client.get.return_value = dill.dumps(cached)
  70. response = Mock(status_code=304, headers={})
  71. response.raise_for_status = Mock()
  72. with patch(
  73. "metadata_manager.firmware_server.features.requests.get",
  74. return_value=response,
  75. ) as mock_get:
  76. result = client.get_defaults("https://example.com/features.txt")
  77. assert result == cached["defaults"]
  78. mock_get.assert_called_once_with(
  79. "https://example.com/features.txt",
  80. headers={
  81. "User-Agent": "CustomBuild/1.0",
  82. "Accept-Encoding": "identity",
  83. "If-None-Match": '"abc"',
  84. },
  85. timeout=30,
  86. )
  87. client._redis_client.set.assert_not_called()
  88. def test_get_defaults_updates_cache_when_etag_changes(self):
  89. client = FeaturesTxtClient(caching_enabled=True)
  90. client._redis_client = Mock()
  91. cached = {
  92. "etag": '"old"',
  93. "defaults": {"OLD_FEATURE": 1},
  94. }
  95. client._redis_client.get.return_value = dill.dumps(cached)
  96. response = Mock(
  97. status_code=200,
  98. text="NEW_FEATURE\n!OLD_FEATURE",
  99. headers={"ETag": '"new"'},
  100. )
  101. response.raise_for_status = Mock()
  102. with patch(
  103. "metadata_manager.firmware_server.features.requests.get",
  104. return_value=response,
  105. ) as mock_get:
  106. result = client.get_defaults("https://example.com/features.txt")
  107. assert result == {"NEW_FEATURE": 1, "OLD_FEATURE": 0}
  108. mock_get.assert_called_once_with(
  109. "https://example.com/features.txt",
  110. headers={
  111. "User-Agent": "CustomBuild/1.0",
  112. "Accept-Encoding": "identity",
  113. "If-None-Match": '"old"',
  114. },
  115. timeout=30,
  116. )
  117. stored = dill.loads(client._redis_client.set.call_args.kwargs["value"])
  118. assert stored["etag"] == '"new"'
  119. assert stored["defaults"] == result
  120. def test_get_defaults_returns_none_on_fetch_failure(self):
  121. client = FeaturesTxtClient(caching_enabled=False)
  122. with patch(
  123. "metadata_manager.firmware_server.features.requests.get",
  124. side_effect=requests.RequestException("network error"),
  125. ):
  126. result = client.get_defaults("https://example.com/features.txt")
  127. assert result is None
  128. def test_get_defaults_does_not_cache_failure_when_caching_enabled(self):
  129. client = FeaturesTxtClient(caching_enabled=True)
  130. client._redis_client = Mock()
  131. client._redis_client.get.return_value = None
  132. with patch(
  133. "metadata_manager.firmware_server.features.requests.get",
  134. side_effect=requests.RequestException("network error"),
  135. ):
  136. result = client.get_defaults("https://example.com/missing/features.txt")
  137. assert result is None
  138. client._redis_client.set.assert_not_called()