test_config_validation.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. #!/usr/bin/env python3
  2. """
  3. Test suite for configuration validation
  4. Tests the validate_config() function with various valid and invalid configs
  5. """
  6. import sys
  7. import os
  8. import unittest
  9. # Add parent directory to path
  10. sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
  11. from skill_seekers.cli.doc_scraper import validate_config
  12. class TestConfigValidation(unittest.TestCase):
  13. """Test configuration validation"""
  14. def test_valid_minimal_config(self):
  15. """Test valid minimal configuration"""
  16. config = {
  17. 'name': 'test-skill',
  18. 'base_url': 'https://example.com/'
  19. }
  20. errors, _ = validate_config(config)
  21. # Should have warnings about missing selectors, but no critical errors
  22. self.assertIsInstance(errors, list)
  23. def test_valid_complete_config(self):
  24. """Test valid complete configuration"""
  25. config = {
  26. 'name': 'godot',
  27. 'base_url': 'https://docs.godotengine.org/en/stable/',
  28. 'description': 'Godot Engine documentation',
  29. 'selectors': {
  30. 'main_content': 'div[role="main"]',
  31. 'title': 'title',
  32. 'code_blocks': 'pre code'
  33. },
  34. 'url_patterns': {
  35. 'include': ['/guide/', '/api/'],
  36. 'exclude': ['/blog/']
  37. },
  38. 'categories': {
  39. 'getting_started': ['intro', 'tutorial'],
  40. 'api': ['api', 'reference']
  41. },
  42. 'rate_limit': 0.5,
  43. 'max_pages': 500
  44. }
  45. errors, _ = validate_config(config)
  46. self.assertEqual(len(errors), 0, f"Valid config should have no errors, got: {errors}")
  47. def test_missing_name(self):
  48. """Test missing required field 'name'"""
  49. config = {
  50. 'base_url': 'https://example.com/'
  51. }
  52. errors, _ = validate_config(config)
  53. self.assertTrue(any('name' in error.lower() for error in errors))
  54. def test_missing_base_url(self):
  55. """Test missing required field 'base_url'"""
  56. config = {
  57. 'name': 'test'
  58. }
  59. errors, _ = validate_config(config)
  60. self.assertTrue(any('base_url' in error.lower() for error in errors))
  61. def test_invalid_name_special_chars(self):
  62. """Test invalid name with special characters"""
  63. config = {
  64. 'name': 'test@skill!',
  65. 'base_url': 'https://example.com/'
  66. }
  67. errors, _ = validate_config(config)
  68. self.assertTrue(any('invalid name' in error.lower() for error in errors))
  69. def test_valid_name_formats(self):
  70. """Test various valid name formats"""
  71. valid_names = ['test', 'test-skill', 'test_skill', 'TestSkill123', 'my-awesome-skill_v2']
  72. for name in valid_names:
  73. config = {
  74. 'name': name,
  75. 'base_url': 'https://example.com/'
  76. }
  77. errors, _ = validate_config(config)
  78. name_errors = [e for e in errors if 'invalid name' in e.lower()]
  79. self.assertEqual(len(name_errors), 0, f"Name '{name}' should be valid")
  80. def test_invalid_base_url_no_protocol(self):
  81. """Test invalid base_url without protocol"""
  82. config = {
  83. 'name': 'test',
  84. 'base_url': 'example.com'
  85. }
  86. errors, _ = validate_config(config)
  87. self.assertTrue(any('base_url' in error.lower() for error in errors))
  88. def test_valid_url_protocols(self):
  89. """Test valid URL protocols"""
  90. for protocol in ['http://', 'https://']:
  91. config = {
  92. 'name': 'test',
  93. 'base_url': f'{protocol}example.com/'
  94. }
  95. errors, _ = validate_config(config)
  96. url_errors = [e for e in errors if 'base_url' in e.lower() and 'invalid' in e.lower()]
  97. self.assertEqual(len(url_errors), 0, f"Protocol '{protocol}' should be valid")
  98. def test_invalid_selectors_not_dict(self):
  99. """Test invalid selectors (not a dictionary)"""
  100. config = {
  101. 'name': 'test',
  102. 'base_url': 'https://example.com/',
  103. 'selectors': 'invalid'
  104. }
  105. errors, _ = validate_config(config)
  106. self.assertTrue(any('selectors' in error.lower() and 'dictionary' in error.lower() for error in errors))
  107. def test_missing_recommended_selectors(self):
  108. """Test warning for missing recommended selectors"""
  109. config = {
  110. 'name': 'test',
  111. 'base_url': 'https://example.com/',
  112. 'selectors': {
  113. 'main_content': 'article'
  114. # Missing 'title' and 'code_blocks'
  115. }
  116. }
  117. _, warnings = validate_config(config)
  118. self.assertTrue(any('title' in warning.lower() for warning in warnings))
  119. self.assertTrue(any('code_blocks' in warning.lower() for warning in warnings))
  120. def test_invalid_url_patterns_not_dict(self):
  121. """Test invalid url_patterns (not a dictionary)"""
  122. config = {
  123. 'name': 'test',
  124. 'base_url': 'https://example.com/',
  125. 'url_patterns': []
  126. }
  127. errors, _ = validate_config(config)
  128. self.assertTrue(any('url_patterns' in error.lower() and 'dictionary' in error.lower() for error in errors))
  129. def test_invalid_url_patterns_include_not_list(self):
  130. """Test invalid url_patterns.include (not a list)"""
  131. config = {
  132. 'name': 'test',
  133. 'base_url': 'https://example.com/',
  134. 'url_patterns': {
  135. 'include': 'not-a-list'
  136. }
  137. }
  138. errors, _ = validate_config(config)
  139. self.assertTrue(any('include' in error.lower() and 'list' in error.lower() for error in errors))
  140. def test_invalid_categories_not_dict(self):
  141. """Test invalid categories (not a dictionary)"""
  142. config = {
  143. 'name': 'test',
  144. 'base_url': 'https://example.com/',
  145. 'categories': []
  146. }
  147. errors, _ = validate_config(config)
  148. self.assertTrue(any('categories' in error.lower() and 'dictionary' in error.lower() for error in errors))
  149. def test_invalid_category_keywords_not_list(self):
  150. """Test invalid category keywords (not a list)"""
  151. config = {
  152. 'name': 'test',
  153. 'base_url': 'https://example.com/',
  154. 'categories': {
  155. 'getting_started': 'not-a-list'
  156. }
  157. }
  158. errors, _ = validate_config(config)
  159. self.assertTrue(any('getting_started' in error.lower() and 'list' in error.lower() for error in errors))
  160. def test_invalid_rate_limit_negative(self):
  161. """Test invalid rate_limit (negative)"""
  162. config = {
  163. 'name': 'test',
  164. 'base_url': 'https://example.com/',
  165. 'rate_limit': -1
  166. }
  167. errors, _ = validate_config(config)
  168. self.assertTrue(any('rate_limit' in error.lower() for error in errors))
  169. def test_invalid_rate_limit_too_high(self):
  170. """Test invalid rate_limit (too high)"""
  171. config = {
  172. 'name': 'test',
  173. 'base_url': 'https://example.com/',
  174. 'rate_limit': 20
  175. }
  176. _, warnings = validate_config(config)
  177. self.assertTrue(any('rate_limit' in warning.lower() for warning in warnings))
  178. def test_invalid_rate_limit_not_number(self):
  179. """Test invalid rate_limit (not a number)"""
  180. config = {
  181. 'name': 'test',
  182. 'base_url': 'https://example.com/',
  183. 'rate_limit': 'fast'
  184. }
  185. errors, _ = validate_config(config)
  186. self.assertTrue(any('rate_limit' in error.lower() for error in errors))
  187. def test_valid_rate_limit_range(self):
  188. """Test valid rate_limit range"""
  189. for rate in [0, 0.1, 0.5, 1, 5, 10]:
  190. config = {
  191. 'name': 'test',
  192. 'base_url': 'https://example.com/',
  193. 'rate_limit': rate
  194. }
  195. errors, _ = validate_config(config)
  196. rate_errors = [e for e in errors if 'rate_limit' in e.lower()]
  197. self.assertEqual(len(rate_errors), 0, f"Rate limit {rate} should be valid")
  198. def test_invalid_max_pages_zero(self):
  199. """Test invalid max_pages (zero)"""
  200. config = {
  201. 'name': 'test',
  202. 'base_url': 'https://example.com/',
  203. 'max_pages': 0
  204. }
  205. errors, _ = validate_config(config)
  206. self.assertTrue(any('max_pages' in error.lower() for error in errors))
  207. def test_invalid_max_pages_too_high(self):
  208. """Test invalid max_pages (too high)"""
  209. config = {
  210. 'name': 'test',
  211. 'base_url': 'https://example.com/',
  212. 'max_pages': 20000
  213. }
  214. _, warnings = validate_config(config)
  215. self.assertTrue(any('max_pages' in warning.lower() for warning in warnings))
  216. def test_invalid_max_pages_not_int(self):
  217. """Test invalid max_pages (not an integer)"""
  218. config = {
  219. 'name': 'test',
  220. 'base_url': 'https://example.com/',
  221. 'max_pages': 'many'
  222. }
  223. errors, _ = validate_config(config)
  224. self.assertTrue(any('max_pages' in error.lower() for error in errors))
  225. def test_valid_max_pages_range(self):
  226. """Test valid max_pages range"""
  227. for max_p in [1, 10, 100, 500, 5000, 10000]:
  228. config = {
  229. 'name': 'test',
  230. 'base_url': 'https://example.com/',
  231. 'max_pages': max_p
  232. }
  233. errors, _ = validate_config(config)
  234. max_errors = [e for e in errors if 'max_pages' in e.lower()]
  235. self.assertEqual(len(max_errors), 0, f"Max pages {max_p} should be valid")
  236. def test_invalid_start_urls_not_list(self):
  237. """Test invalid start_urls (not a list)"""
  238. config = {
  239. 'name': 'test',
  240. 'base_url': 'https://example.com/',
  241. 'start_urls': 'https://example.com/page1'
  242. }
  243. errors, _ = validate_config(config)
  244. self.assertTrue(any('start_urls' in error.lower() and 'list' in error.lower() for error in errors))
  245. def test_invalid_start_urls_bad_protocol(self):
  246. """Test invalid start_urls (bad protocol)"""
  247. config = {
  248. 'name': 'test',
  249. 'base_url': 'https://example.com/',
  250. 'start_urls': ['ftp://example.com/page1']
  251. }
  252. errors, _ = validate_config(config)
  253. self.assertTrue(any('start_url' in error.lower() for error in errors))
  254. def test_valid_start_urls(self):
  255. """Test valid start_urls"""
  256. config = {
  257. 'name': 'test',
  258. 'base_url': 'https://example.com/',
  259. 'start_urls': [
  260. 'https://example.com/page1',
  261. 'http://example.com/page2',
  262. 'https://example.com/api/docs'
  263. ]
  264. }
  265. errors, _ = validate_config(config)
  266. url_errors = [e for e in errors if 'start_url' in e.lower()]
  267. self.assertEqual(len(url_errors), 0, "Valid start_urls should pass validation")
  268. def test_config_with_llms_txt_url(self):
  269. """Test config validation with explicit llms_txt_url"""
  270. config = {
  271. 'name': 'test',
  272. 'llms_txt_url': 'https://example.com/llms-full.txt',
  273. 'base_url': 'https://example.com/docs'
  274. }
  275. # Should be valid
  276. self.assertEqual(config.get('llms_txt_url'), 'https://example.com/llms-full.txt')
  277. def test_config_with_skip_llms_txt(self):
  278. """Test config validation accepts skip_llms_txt"""
  279. config = {
  280. 'name': 'test',
  281. 'base_url': 'https://example.com/docs',
  282. 'skip_llms_txt': True
  283. }
  284. errors, warnings = validate_config(config)
  285. self.assertEqual(errors, [])
  286. self.assertTrue(config.get('skip_llms_txt'))
  287. def test_config_with_skip_llms_txt_false(self):
  288. """Test config validation accepts skip_llms_txt as False"""
  289. config = {
  290. 'name': 'test',
  291. 'base_url': 'https://example.com/docs',
  292. 'skip_llms_txt': False
  293. }
  294. errors, warnings = validate_config(config)
  295. self.assertEqual(errors, [])
  296. self.assertFalse(config.get('skip_llms_txt'))
  297. if __name__ == '__main__':
  298. unittest.main()