test_validate.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. import io
  2. import os
  3. import string
  4. import sys
  5. import pytest
  6. from flexmock import flexmock
  7. from borgmatic.config import validate as module
  8. def test_schema_filename_returns_plausible_path():
  9. schema_path = module.schema_filename()
  10. assert schema_path.endswith('/schema.yaml')
  11. def mock_config_and_schema(config_yaml, schema_yaml=None):
  12. '''
  13. Set up mocks for the given config config YAML string and the schema YAML string, or the default
  14. schema if no schema is provided. The idea is that that the code under test consumes these mocks
  15. when parsing the configuration.
  16. '''
  17. config_stream = io.StringIO(config_yaml)
  18. config_stream.name = 'config.yaml'
  19. if schema_yaml is None:
  20. schema_stream = open(module.schema_filename())
  21. else:
  22. schema_stream = io.StringIO(schema_yaml)
  23. schema_stream.name = 'schema.yaml'
  24. builtins = flexmock(sys.modules['builtins'])
  25. flexmock(module.os).should_receive('getcwd').and_return('/tmp')
  26. flexmock(module.os.path).should_receive('isabs').and_return(False)
  27. flexmock(module.os.path).should_receive('exists').and_return(True)
  28. builtins.should_receive('open').with_args('/tmp/config.yaml', encoding='utf-8').and_return(
  29. config_stream
  30. )
  31. builtins.should_receive('open').with_args('/tmp/schema.yaml', encoding='utf-8').and_return(
  32. schema_stream
  33. )
  34. def test_parse_configuration_transforms_file_into_mapping():
  35. mock_config_and_schema(
  36. '''
  37. source_directories:
  38. - /home
  39. - /etc
  40. repositories:
  41. - path: hostname.borg
  42. keep_minutely: 60
  43. keep_hourly: 24
  44. keep_daily: 7
  45. checks:
  46. - name: repository
  47. - name: archives
  48. ''',
  49. )
  50. config, config_paths, logs = module.parse_configuration(
  51. '/tmp/config.yaml',
  52. '/tmp/schema.yaml',
  53. arguments={'global': flexmock()},
  54. )
  55. assert config == {
  56. 'source_directories': ['/home', '/etc'],
  57. 'repositories': [{'path': 'hostname.borg'}],
  58. 'keep_daily': 7,
  59. 'keep_hourly': 24,
  60. 'keep_minutely': 60,
  61. 'checks': [{'name': 'repository'}, {'name': 'archives'}],
  62. 'bootstrap': {},
  63. }
  64. assert config_paths == {'/tmp/config.yaml'}
  65. assert logs == []
  66. def test_parse_configuration_passes_through_quoted_punctuation():
  67. escaped_punctuation = string.punctuation.replace('\\', r'\\').replace('"', r'\"')
  68. mock_config_and_schema(
  69. f'''
  70. source_directories:
  71. - "/home/{escaped_punctuation}"
  72. repositories:
  73. - path: test.borg
  74. ''',
  75. )
  76. config, config_paths, logs = module.parse_configuration(
  77. '/tmp/config.yaml',
  78. '/tmp/schema.yaml',
  79. arguments={'global': flexmock()},
  80. )
  81. assert config == {
  82. 'source_directories': [f'/home/{string.punctuation}'],
  83. 'repositories': [{'path': 'test.borg'}],
  84. 'bootstrap': {},
  85. }
  86. assert config_paths == {'/tmp/config.yaml'}
  87. assert logs == []
  88. def test_parse_configuration_with_schema_lacking_examples_does_not_raise():
  89. mock_config_and_schema(
  90. '''
  91. source_directories:
  92. - /home
  93. repositories:
  94. - path: hostname.borg
  95. ''',
  96. '''
  97. map:
  98. source_directories:
  99. required: true
  100. seq:
  101. - type: scalar
  102. repositories:
  103. required: true
  104. seq:
  105. - type: scalar
  106. ''',
  107. )
  108. module.parse_configuration(
  109. '/tmp/config.yaml',
  110. '/tmp/schema.yaml',
  111. arguments={'global': flexmock()},
  112. )
  113. def test_parse_configuration_inlines_include_inside_deprecated_section():
  114. mock_config_and_schema(
  115. '''
  116. source_directories:
  117. - /home
  118. repositories:
  119. - path: hostname.borg
  120. retention:
  121. !include include.yaml
  122. ''',
  123. )
  124. builtins = flexmock(sys.modules['builtins'])
  125. include_file = io.StringIO(
  126. '''
  127. keep_daily: 7
  128. keep_hourly: 24
  129. ''',
  130. )
  131. include_file.name = 'include.yaml'
  132. builtins.should_receive('open').with_args('/tmp/include.yaml', encoding='utf-8').and_return(
  133. include_file
  134. )
  135. config, config_paths, logs = module.parse_configuration(
  136. '/tmp/config.yaml',
  137. '/tmp/schema.yaml',
  138. arguments={'global': flexmock()},
  139. )
  140. assert config == {
  141. 'source_directories': ['/home'],
  142. 'repositories': [{'path': 'hostname.borg'}],
  143. 'keep_daily': 7,
  144. 'keep_hourly': 24,
  145. 'bootstrap': {},
  146. }
  147. assert config_paths == {'/tmp/include.yaml', '/tmp/config.yaml'}
  148. assert len(logs) == 1
  149. def test_parse_configuration_merges_include():
  150. mock_config_and_schema(
  151. '''
  152. source_directories:
  153. - /home
  154. repositories:
  155. - path: hostname.borg
  156. keep_daily: 1
  157. <<: !include include.yaml
  158. ''',
  159. )
  160. builtins = flexmock(sys.modules['builtins'])
  161. include_file = io.StringIO(
  162. '''
  163. keep_daily: 7
  164. keep_hourly: 24
  165. ''',
  166. )
  167. include_file.name = 'include.yaml'
  168. builtins.should_receive('open').with_args('/tmp/include.yaml', encoding='utf-8').and_return(
  169. include_file
  170. )
  171. config, config_paths, logs = module.parse_configuration(
  172. '/tmp/config.yaml',
  173. '/tmp/schema.yaml',
  174. arguments={'global': flexmock()},
  175. )
  176. assert config == {
  177. 'source_directories': ['/home'],
  178. 'repositories': [{'path': 'hostname.borg'}],
  179. 'keep_daily': 1,
  180. 'keep_hourly': 24,
  181. 'bootstrap': {},
  182. }
  183. assert config_paths == {'/tmp/include.yaml', '/tmp/config.yaml'}
  184. assert logs == []
  185. def test_parse_configuration_raises_for_missing_config_file():
  186. with pytest.raises(FileNotFoundError):
  187. module.parse_configuration(
  188. '/tmp/config.yaml',
  189. '/tmp/schema.yaml',
  190. arguments={'global': flexmock()},
  191. )
  192. def test_parse_configuration_raises_for_missing_schema_file():
  193. mock_config_and_schema('')
  194. builtins = flexmock(sys.modules['builtins'])
  195. builtins.should_receive('open').with_args('/tmp/config.yaml', encoding='utf-8').and_return(
  196. io.StringIO('foo: bar'),
  197. )
  198. builtins.should_receive('open').with_args('/tmp/schema.yaml', encoding='utf-8').and_raise(
  199. FileNotFoundError
  200. )
  201. with pytest.raises(FileNotFoundError):
  202. module.parse_configuration(
  203. '/tmp/config.yaml',
  204. '/tmp/schema.yaml',
  205. arguments={'global': flexmock()},
  206. )
  207. def test_parse_configuration_raises_for_syntax_error():
  208. mock_config_and_schema('foo:\nbar')
  209. with pytest.raises(ValueError):
  210. module.parse_configuration(
  211. '/tmp/config.yaml',
  212. '/tmp/schema.yaml',
  213. arguments={'global': flexmock()},
  214. )
  215. def test_parse_configuration_raises_for_validation_error():
  216. mock_config_and_schema(
  217. '''
  218. source_directories: yes
  219. repositories:
  220. - path: hostname.borg
  221. ''',
  222. )
  223. with pytest.raises(module.Validation_error):
  224. module.parse_configuration(
  225. '/tmp/config.yaml',
  226. '/tmp/schema.yaml',
  227. arguments={'global': flexmock()},
  228. )
  229. def test_parse_configuration_applies_overrides():
  230. mock_config_and_schema(
  231. '''
  232. source_directories:
  233. - /home
  234. repositories:
  235. - path: hostname.borg
  236. local_path: borg1
  237. ''',
  238. )
  239. config, config_paths, logs = module.parse_configuration(
  240. '/tmp/config.yaml',
  241. '/tmp/schema.yaml',
  242. arguments={'global': flexmock()},
  243. overrides=['local_path=borg2'],
  244. )
  245. assert config == {
  246. 'source_directories': ['/home'],
  247. 'repositories': [{'path': 'hostname.borg'}],
  248. 'local_path': 'borg2',
  249. 'bootstrap': {},
  250. }
  251. assert config_paths == {'/tmp/config.yaml'}
  252. assert logs == []
  253. def test_parse_configuration_applies_normalization_after_environment_variable_interpolation():
  254. mock_config_and_schema(
  255. '''
  256. location:
  257. source_directories:
  258. - /home
  259. repositories:
  260. - ${NO_EXIST:-user@hostname:repo}
  261. exclude_if_present: .nobackup
  262. ''',
  263. )
  264. flexmock(os).should_receive('getenv').replace_with(lambda variable_name, default: default)
  265. config, config_paths, logs = module.parse_configuration(
  266. '/tmp/config.yaml',
  267. '/tmp/schema.yaml',
  268. arguments={'global': flexmock()},
  269. )
  270. assert config == {
  271. 'source_directories': ['/home'],
  272. 'repositories': [{'path': 'ssh://user@hostname/./repo'}],
  273. 'exclude_if_present': ['.nobackup'],
  274. 'bootstrap': {},
  275. }
  276. assert config_paths == {'/tmp/config.yaml'}
  277. assert logs