test_clone.py 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. """Test clone"""
  2. import os
  3. import re
  4. import pytest
  5. BOOTSTRAP_CODE = 123
  6. BOOTSTRAP_MSG = 'Bootstrap successful'
  7. @pytest.mark.usefixtures('remote')
  8. @pytest.mark.parametrize(
  9. 'good_remote, repo_exists, force, conflicts', [
  10. (False, False, False, False),
  11. (True, False, False, False),
  12. (True, True, False, False),
  13. (True, True, True, False),
  14. (True, False, False, True),
  15. ], ids=[
  16. 'bad remote',
  17. 'simple',
  18. 'existing repo',
  19. '-f',
  20. 'conflicts',
  21. ])
  22. def test_clone(
  23. runner, paths, yadm_y, repo_config, ds1,
  24. good_remote, repo_exists, force, conflicts):
  25. """Test basic clone operation"""
  26. # determine remote url
  27. remote_url = f'file://{paths.remote}'
  28. if not good_remote:
  29. remote_url = 'file://bad_remote'
  30. old_repo = None
  31. if repo_exists:
  32. # put a repo in the way
  33. paths.repo.mkdir()
  34. old_repo = paths.repo.join('old_repo')
  35. old_repo.write('old_repo')
  36. if conflicts:
  37. ds1.tracked[0].relative.write('conflict')
  38. assert ds1.tracked[0].relative.exists()
  39. # run the clone command
  40. args = ['clone', '-w', paths.work]
  41. if force:
  42. args += ['-f']
  43. args += [remote_url]
  44. run = runner(command=yadm_y(*args))
  45. if not good_remote:
  46. # clone should fail
  47. assert run.failure
  48. assert run.err != ''
  49. assert 'Unable to fetch origin' in run.out
  50. assert not paths.repo.exists()
  51. elif repo_exists and not force:
  52. # can't overwrite data
  53. assert run.failure
  54. assert run.err == ''
  55. assert 'Git repo already exists' in run.out
  56. else:
  57. # clone should succeed, and repo should be configured properly
  58. assert successful_clone(run, paths, repo_config)
  59. # ensure conflicts are handled properly
  60. if conflicts:
  61. assert 'NOTE' in run.out
  62. assert 'Merging origin/master failed' in run.out
  63. assert 'Conflicts preserved' in run.out
  64. # confirm correct Git origin
  65. run = runner(
  66. command=('git', 'remote', '-v', 'show'),
  67. env={'GIT_DIR': paths.repo})
  68. assert run.success
  69. assert run.err == ''
  70. assert f'origin\t{remote_url}' in run.out
  71. # ensure conflicts are really preserved
  72. if conflicts:
  73. # test to see if the work tree is actually "clean"
  74. run = runner(
  75. command=yadm_y('status', '-uno', '--porcelain'),
  76. cwd=paths.work)
  77. assert run.success
  78. assert run.err == ''
  79. assert run.out == '', 'worktree has unexpected changes'
  80. # test to see if the conflicts are stashed
  81. run = runner(command=yadm_y('stash', 'list'), cwd=paths.work)
  82. assert run.success
  83. assert run.err == ''
  84. assert 'Conflicts preserved' in run.out, 'conflicts not stashed'
  85. # verify content of the stashed conflicts
  86. run = runner(command=yadm_y('stash', 'show', '-p'), cwd=paths.work)
  87. assert run.success
  88. assert run.err == ''
  89. assert '\n+conflict' in run.out, 'conflicts not stashed'
  90. # another force-related assertion
  91. if old_repo:
  92. if force:
  93. assert not old_repo.exists()
  94. else:
  95. assert old_repo.exists()
  96. @pytest.mark.usefixtures('remote')
  97. @pytest.mark.parametrize(
  98. 'bs_exists, bs_param, answer', [
  99. (False, '--bootstrap', None),
  100. (True, '--bootstrap', None),
  101. (True, '--no-bootstrap', None),
  102. (True, None, 'n'),
  103. (True, None, 'y'),
  104. ], ids=[
  105. 'force, missing',
  106. 'force, existing',
  107. 'prevent',
  108. 'existing, answer n',
  109. 'existing, answer y',
  110. ])
  111. def test_clone_bootstrap(
  112. runner, paths, yadm_y, repo_config, bs_exists, bs_param, answer):
  113. """Test bootstrap clone features"""
  114. # establish a bootstrap
  115. create_bootstrap(paths, bs_exists)
  116. # run the clone command
  117. args = ['clone', '-w', paths.work]
  118. if bs_param:
  119. args += [bs_param]
  120. args += [f'file://{paths.remote}']
  121. expect = []
  122. if answer:
  123. expect.append(('Would you like to execute it now', answer))
  124. run = runner(command=yadm_y(*args), expect=expect)
  125. if answer:
  126. assert 'Would you like to execute it now' in run.out
  127. expected_code = 0
  128. if bs_exists and bs_param != '--no-bootstrap':
  129. expected_code = BOOTSTRAP_CODE
  130. if answer == 'y':
  131. expected_code = BOOTSTRAP_CODE
  132. assert BOOTSTRAP_MSG in run.out
  133. elif answer == 'n':
  134. expected_code = 0
  135. assert BOOTSTRAP_MSG not in run.out
  136. assert successful_clone(run, paths, repo_config, expected_code)
  137. if not bs_exists:
  138. assert BOOTSTRAP_MSG not in run.out
  139. def create_bootstrap(paths, exists):
  140. """Create bootstrap file for test"""
  141. if exists:
  142. paths.bootstrap.write(
  143. '#!/bin/sh\n'
  144. f'echo {BOOTSTRAP_MSG}\n'
  145. f'exit {BOOTSTRAP_CODE}\n')
  146. paths.bootstrap.chmod(0o775)
  147. assert paths.bootstrap.exists()
  148. else:
  149. assert not paths.bootstrap.exists()
  150. @pytest.mark.usefixtures('remote')
  151. @pytest.mark.parametrize(
  152. 'private_type, in_repo, in_work', [
  153. ('ssh', False, True),
  154. ('gnupg', False, True),
  155. ('ssh', True, True),
  156. ('gnupg', True, True),
  157. ('ssh', True, False),
  158. ('gnupg', True, False),
  159. ], ids=[
  160. 'open ssh, not tracked',
  161. 'open gnupg, not tracked',
  162. 'open ssh, tracked',
  163. 'open gnupg, tracked',
  164. 'missing ssh, tracked',
  165. 'missing gnupg, tracked',
  166. ])
  167. def test_clone_perms(
  168. runner, yadm_y, paths, repo_config,
  169. private_type, in_repo, in_work):
  170. """Test clone permission-related functions"""
  171. # update remote repo to include private data
  172. if in_repo:
  173. rpath = paths.work.mkdir(f'.{private_type}').join('related')
  174. rpath.write('related')
  175. os.system(f'GIT_DIR="{paths.remote}" git add {rpath}')
  176. os.system(f'GIT_DIR="{paths.remote}" git commit -m "{rpath}"')
  177. rpath.remove()
  178. # ensure local private data is insecure at the start
  179. if in_work:
  180. pdir = paths.work.join(f'.{private_type}')
  181. if not pdir.exists():
  182. pdir.mkdir()
  183. pfile = pdir.join('existing')
  184. pfile.write('existing')
  185. pdir.chmod(0o777)
  186. pfile.chmod(0o777)
  187. else:
  188. paths.work.remove()
  189. paths.work.mkdir()
  190. run = runner(
  191. yadm_y('clone', '-d', '-w', paths.work, f'file://{paths.remote}'))
  192. assert successful_clone(run, paths, repo_config)
  193. if in_work:
  194. # private directories which already exist, should be left as they are,
  195. # which in this test is "insecure".
  196. assert re.search(
  197. f'initial private dir perms drwxrwxrwx.+.{private_type}',
  198. run.out)
  199. assert re.search(
  200. f'pre-merge private dir perms drwxrwxrwx.+.{private_type}',
  201. run.out)
  202. assert re.search(
  203. f'post-merge private dir perms drwxrwxrwx.+.{private_type}',
  204. run.out)
  205. else:
  206. # private directories which are created, should be done prior to
  207. # merging, and with secure permissions.
  208. assert 'initial private dir perms' not in run.out
  209. assert re.search(
  210. f'pre-merge private dir perms drwx------.+.{private_type}',
  211. run.out)
  212. assert re.search(
  213. f'post-merge private dir perms drwx------.+.{private_type}',
  214. run.out)
  215. # standard perms still apply afterwards unless disabled with auto.perms
  216. assert oct(
  217. paths.work.join(f'.{private_type}').stat().mode).endswith('00'), (
  218. f'.{private_type} has not been secured by auto.perms')
  219. def successful_clone(run, paths, repo_config, expected_code=0):
  220. """Assert clone is successful"""
  221. assert run.code == expected_code
  222. assert 'Initialized' in run.out
  223. assert oct(paths.repo.stat().mode).endswith('00'), 'Repo is not secured'
  224. assert repo_config('core.bare') == 'false'
  225. assert repo_config('status.showUntrackedFiles') == 'no'
  226. assert repo_config('yadm.managed') == 'true'
  227. return True
  228. @pytest.fixture()
  229. def remote(paths, ds1_repo_copy):
  230. """Function scoped remote (based on ds1)"""
  231. # pylint: disable=unused-argument
  232. # This is ignored because
  233. # @pytest.mark.usefixtures('ds1_remote_copy')
  234. # cannot be applied to another fixture.
  235. paths.remote.remove()
  236. paths.repo.move(paths.remote)
  237. return None