Re-organise source for better maintenance.
[dput.git] / test / test_methods.py
blob9118e720ae1cf4ed3d332df15c3fa41b456115ae
1 # -*- coding: utf-8; -*-
3 # test/test_methods.py
4 # Part of ‘dput’, a Debian package upload toolkit.
6 # This is free software, and you are welcome to redistribute it under
7 # certain conditions; see the end of this file for copyright
8 # information, grant of license, and disclaimer of warranty.
10 """ Unit tests for upload method behaviour. """
12 from __future__ import (absolute_import, unicode_literals)
14 import sys
15 import os
16 import os.path
17 import subprocess
18 import io
19 import stat
20 import pkgutil
21 import importlib
22 import collections
23 import tempfile
24 import textwrap
25 import doctest
26 import getpass
27 import ftplib
29 if sys.version_info >= (3, 3):
30 import urllib.parse as urlparse
31 elif sys.version_info >= (3, 0):
32 raise RuntimeError("Python 3 earlier than 3.3 is not supported.")
33 else:
34 import urlparse
36 import testtools
37 import testscenarios
38 import httpretty
40 __package__ = str("test")
41 __import__(__package__)
43 sys.path.insert(1, os.path.dirname(os.path.dirname(__file__)))
44 import dput.dput
45 import dput.helper.dputhelper
46 import dput.methods
47 import dput.methods.local
48 import dput.methods.ftp
49 import dput.methods.http
50 import dput.methods.https
51 import dput.methods.scp
52 import dput.methods.rsync
54 from .helper import (
55 mock,
56 FakeSystemExit,
57 patch_system_interfaces,
58 EXIT_STATUS_FAILURE,
59 FileDouble,
60 patch_os_stat,
61 patch_os_lstat,
62 setup_file_double_behaviour,
63 patch_subprocess_check_call,
64 ARG_ANY,
65 SubprocessDouble,
67 from .test_dputhelper import (
68 patch_filewithprogress,
72 class import_upload_functions_TestCase(
73 testscenarios.WithScenarios,
74 testtools.TestCase):
75 """ Test cases for `import_upload_functions` function. """
77 scenarios = [
78 ('empty', {
79 'module_names': [],
80 }),
81 ('one', {
82 'module_names': ["foo"],
83 }),
84 ('three', {
85 'module_names': ["foo", "bar", "baz"],
86 }),
89 for (scenario_name, scenario) in scenarios:
90 modules_by_name = collections.OrderedDict()
91 iter_modules_result = []
92 for module_name in scenario['module_names']:
93 module = mock.Mock()
94 module.__name__ = module_name
95 module.upload = mock.Mock()
96 modules_by_name[module_name] = module
97 module_entry = (module, module_name, False)
98 iter_modules_result.append(module_entry)
99 scenario['modules_by_name'] = modules_by_name
100 scenario['iter_modules_result'] = iter_modules_result
101 del scenario_name, scenario
103 def setUp(self):
104 """ Set up test fixtures. """
105 super(import_upload_functions_TestCase, self).setUp()
106 patch_system_interfaces(self)
108 self.patch_import_functions()
110 def patch_import_functions(self):
111 """ Patch import functions used by the function. """
112 self.patch_pkgutil_iter_modules()
113 self.patch_importlib_import_module()
115 def patch_pkgutil_iter_modules(self):
116 """ Patch `pkgutil.iter_modules` function for this test case. """
117 func_patcher = mock.patch.object(
118 pkgutil, "iter_modules", autospec=True)
119 func_patcher.start()
120 self.addCleanup(func_patcher.stop)
121 pkgutil.iter_modules.return_value = self.iter_modules_result
123 def patch_importlib_import_module(self):
124 """ Patch `importlib.import_module` function for this test case. """
125 func_patcher = mock.patch.object(
126 importlib, "import_module", autospec=True)
127 func_patcher.start()
128 self.addCleanup(func_patcher.stop)
130 def fake_import_module(full_name):
131 module_name = full_name.split(".")[-1]
132 module = self.modules_by_name[module_name]
133 return module
135 importlib.import_module.side_effect = fake_import_module
137 @mock.patch.object(dput.dput, 'debug', new=True)
138 def test_emits_debug_message_for_modules_found(self):
139 """ Should emit a debug message for the modules found. """
140 expected_message = "D: modules_found: {names!r}".format(
141 names=self.module_names)
142 dput.dput.import_upload_functions()
143 self.assertIn(expected_message, sys.stdout.getvalue())
145 @mock.patch.object(dput.dput, 'debug', new=True)
146 def test_emits_debug_message_for_each_import(self):
147 """ Should emit a debug message for each module imported. """
148 dput.dput.import_upload_functions()
149 for (module_name, module) in self.modules_by_name.items():
150 expected_message = "D: Module: {name} ({module!r})".format(
151 name=module_name, module=module)
152 self.assertIn(expected_message, sys.stdout.getvalue())
154 @mock.patch.object(dput.dput, 'debug', new=True)
155 def test_emits_debug_message_for_each_upload_method(self):
156 """ Should emit a debug message for each upload method. """
157 dput.dput.import_upload_functions()
158 for module_name in self.module_names:
159 expected_message = "D: Method name: {name}".format(
160 name=module_name)
161 self.assertIn(expected_message, sys.stdout.getvalue())
163 def test_returns_expected_function_mapping(self):
164 """ Should return expected mapping of upload functions. """
165 result = dput.dput.import_upload_functions()
166 expected_result = {
167 name: self.modules_by_name[name].upload
168 for name in self.module_names}
169 self.assertEqual(expected_result, result)
172 def patch_getpass_getpass(testcase):
173 """ Patch the `getpass.getpass` function for the test case. """
174 func_patcher = mock.patch.object(getpass, "getpass", autospec=True)
175 func_patcher.start()
176 testcase.addCleanup(func_patcher.stop)
179 class upload_TestCase(
180 testscenarios.WithScenarios,
181 testtools.TestCase):
182 """ Base for test cases for method modules `upload` functions. """
184 files_scenarios = [
185 ('file-list-empty', {
186 'paths_to_upload': [],
188 ('file-list-one', {
189 'paths_to_upload': [tempfile.mktemp()],
191 ('file-list-three', {
192 'paths_to_upload': [tempfile.mktemp() for __ in range(3)],
196 check_call_scenarios = [
197 ('check-call-success', {
198 'check_call_scenario_name': 'success',
200 ('check-call-failure', {
201 'check_call_scenario_name': 'failure',
202 'check_call_error': subprocess.CalledProcessError,
206 check_call_success_scenarios = [
207 (name, params) for (name, params) in check_call_scenarios
208 if 'check_call_error' not in params]
210 incoming_scenarios = [
211 ('incoming-simple', {
212 'incoming_path': tempfile.mktemp(),
214 ('incoming-no-leading-slash', {
215 'incoming_path': tempfile.mktemp().lstrip("/"),
217 ('incoming-has-trailing-slash', {
218 'incoming_path': tempfile.mktemp() + "/",
222 def setUp(self):
223 """ Set up test fixtures. """
224 super(upload_TestCase, self).setUp()
225 patch_system_interfaces(self)
227 patch_subprocess_check_call(self)
229 patch_os_stat(self)
230 self.set_file_doubles()
231 setup_file_double_behaviour(self)
233 patch_filewithprogress(self)
235 self.set_test_args()
237 def set_file_doubles(self):
238 """ Set the file doubles for this test case. """
239 for path in self.paths_to_upload:
240 fake_file = getattr(self, 'fake_file', None)
241 double = FileDouble(path, fake_file)
242 double.set_os_stat_scenario(
243 getattr(self, 'os_stat_scenario_name', "okay"))
244 double.register_for_testcase(self)
246 func_patcher = mock.patch.object(
247 double.fake_file, "close", autospec=True)
248 func_patcher.start()
249 self.addCleanup(func_patcher.stop)
251 def set_test_args(self):
252 """ Set the arguments for the test call to the function. """
253 raise NotImplementedError
255 def get_command_args_from_latest_check_call(self):
256 """ Get command line arguments from latest `subprocess.check_call`. """
257 latest_call = subprocess.check_call.call_args
258 (args, kwargs) = latest_call
259 command_args = args[0]
260 return command_args
263 class local_upload_TestCase(upload_TestCase):
264 """ Test cases for `methods.local.upload` function. """
266 scenarios = testscenarios.multiply_scenarios(
267 upload_TestCase.incoming_scenarios,
268 upload_TestCase.files_scenarios,
269 upload_TestCase.check_call_success_scenarios)
271 command_file_path = os.path.join("/usr/bin", "install")
273 def setUp(self):
274 """ Set up test fixtures. """
275 super(local_upload_TestCase, self).setUp()
277 self.set_subprocess_double()
279 def set_test_args(self):
280 """ Set the arguments for the test call to the function. """
281 self.test_args = dict(
282 fqdn=object(),
283 login=object(),
284 incoming=self.incoming_path,
285 files_to_upload=self.paths_to_upload,
286 debug=None,
287 compress=object(),
288 progress=object(),
291 def set_subprocess_double(self):
292 """ Set the test double for the subprocess. """
293 argv = [self.command_file_path, "-m", ARG_ANY, ARG_ANY]
294 double = SubprocessDouble(self.command_file_path, argv=argv)
295 double.register_for_testcase(self)
296 double.set_subprocess_check_call_scenario(
297 self.check_call_scenario_name)
298 self.subprocess_double = double
300 def test_calls_check_call_with_install_command(self):
301 """ Should call `subprocess.check_call` to invoke ‘install’. """
302 dput.methods.local.upload(**self.test_args)
303 command_args = self.get_command_args_from_latest_check_call()
304 expected_command = self.command_file_path
305 self.assertEqual(expected_command, command_args[0])
307 def test_calls_check_call_with_mode_option_in_command(self):
308 """ Should call `subprocess.check_call`, invoke command with mode. """
309 dput.methods.local.upload(**self.test_args)
310 command_args = self.get_command_args_from_latest_check_call()
311 expected_mode = (
312 stat.S_IRUSR | stat.S_IWUSR
313 | stat.S_IRGRP
314 | stat.S_IROTH)
315 expected_mode_text = "{mode:04o}".format(mode=expected_mode)[-3:]
316 expected_option_args = ["-m", expected_mode_text]
317 self.assertEqual(expected_option_args, command_args[1:3])
319 def test_calls_check_call_with_file_paths_in_command(self):
320 """ Should call `subprocess.check_call` with file paths in command. """
321 dput.methods.local.upload(**self.test_args)
322 command_args = self.get_command_args_from_latest_check_call()
323 self.assertEqual(self.paths_to_upload, command_args[3:-1])
325 def test_calls_check_call_with_incoming_path_as_final_arg(self):
326 """ Should call `subprocess.check_call` with incoming path. """
327 dput.methods.local.upload(**self.test_args)
328 command_args = self.get_command_args_from_latest_check_call()
329 self.assertEqual(self.incoming_path, command_args[-1])
331 def test_emits_debug_message_for_upload_command(self):
332 """ Should emit a debug message for the upload command. """
333 self.test_args['debug'] = True
334 dput.methods.local.upload(**self.test_args)
335 expected_message = textwrap.dedent("""\
336 D: Uploading with cp to {dir_path}
337 D: ...
338 """).format(dir_path=self.incoming_path)
339 self.assertThat(
340 sys.stdout.getvalue(),
341 testtools.matchers.DocTestMatches(
342 expected_message, flags=doctest.ELLIPSIS))
344 def test_calls_sys_exit_if_check_call_returns_nonzero(self):
345 """ Should call `sys.exit` if `subprocess.check_call` fails. """
346 self.subprocess_double.set_subprocess_check_call_scenario('failure')
347 with testtools.ExpectedException(FakeSystemExit):
348 dput.methods.local.upload(**self.test_args)
349 expected_output = textwrap.dedent("""\
350 Error while uploading.
351 """)
352 self.assertIn(expected_output, sys.stdout.getvalue())
353 sys.exit.assert_called_with(EXIT_STATUS_FAILURE)
356 class ftp_upload_TestCase(upload_TestCase):
357 """ Test cases for `methods.ftp.upload` function. """
359 scenarios = NotImplemented
361 def setUp(self):
362 """ Set up test fixtures. """
363 super(ftp_upload_TestCase, self).setUp()
365 self.set_ftp_client()
366 self.patch_ftplib_ftp()
368 patch_getpass_getpass(self)
369 self.fake_password = self.getUniqueString()
370 getpass.getpass.return_value = self.fake_password
371 if not hasattr(self, 'expected_password'):
372 self.expected_password = self.fake_password
374 def set_test_args(self):
375 """ Set the arguments for the test call to the function. """
376 if not hasattr(self, 'progress_type'):
377 self.progress_type = 0
378 self.test_args = dict(
379 fqdn=self.getUniqueString(),
380 login=self.login,
381 incoming=self.incoming_path,
382 files_to_upload=self.paths_to_upload,
383 debug=False,
384 ftp_mode=self.ftp_mode,
385 progress=self.progress_type,
386 port=object(),
389 def set_ftp_client(self):
390 """ Set the FTP client double. """
391 self.ftp_client = mock.MagicMock(name="FTP")
393 def patch_ftplib_ftp(self):
394 """ Patch `ftplib.FTP` class for this test case. """
395 patcher = mock.patch.object(ftplib, "FTP", autospec=True)
396 patcher.start()
397 self.addCleanup(patcher.stop)
399 ftplib.FTP.return_value = self.ftp_client
402 class ftp_upload_NormalFilesTestCase(ftp_upload_TestCase):
403 """ Test cases for `methods.ftp.upload` function, upload normal files. """
405 login_scenarios = [
406 ('anonymous', {
407 'login': "anonymous",
408 'expected_password': "dput@packages.debian.org",
410 ('username', {
411 'login': "lorem",
415 ftp_client_scenarios = [
416 ('default', {
417 'ftp_mode': False,
419 ('passive-mode', {
420 'ftp_mode': True,
424 scenarios = testscenarios.multiply_scenarios(
425 upload_TestCase.incoming_scenarios,
426 upload_TestCase.files_scenarios,
427 login_scenarios, ftp_client_scenarios)
429 def test_emits_debug_message_for_connect(self):
430 """ Should emit debug message for successful connect. """
431 self.test_args['debug'] = True
432 dput.methods.ftp.upload(**self.test_args)
433 expected_fqdn = self.test_args['fqdn']
434 expected_output = textwrap.dedent("""\
435 D: FTP-Connection to host: {fqdn}
436 """).format(fqdn=expected_fqdn)
437 self.assertIn(expected_output, sys.stdout.getvalue())
439 def test_calls_ftp_connect_with_expected_args(self):
440 """ Should call `FTP.connect` with expected args. """
441 dput.methods.ftp.upload(**self.test_args)
442 expected_args = (
443 self.test_args['fqdn'],
444 self.test_args['port'],
446 self.ftp_client.connect.assert_called_with(*expected_args)
448 def test_emits_error_message_when_ftp_connect_error(self):
449 """ Should emit error message when `FTP.connect` raises error. """
450 self.ftp_client.connect.side_effect = ftplib.error_temp
451 try:
452 dput.methods.ftp.upload(**self.test_args)
453 except FakeSystemExit:
454 pass
455 expected_output = "Connection failed, aborting"
456 self.assertIn(expected_output, sys.stdout.getvalue())
458 def test_calls_sys_exit_when_ftp_connect_permission_error(self):
459 """ Should call `sys.exit` when `FTP.connect` raises error. """
460 self.ftp_client.connect.side_effect = ftplib.error_temp
461 with testtools.ExpectedException(FakeSystemExit):
462 dput.methods.ftp.upload(**self.test_args)
463 sys.exit.assert_called_with(EXIT_STATUS_FAILURE)
465 def test_calls_ftp_login_with_expected_args(self):
466 """ Should call `FTP.login` with expected args. """
467 dput.methods.ftp.upload(**self.test_args)
468 expected_args = (
469 self.test_args['login'],
470 self.expected_password,
472 self.ftp_client.login.assert_called_with(*expected_args)
474 def test_emits_error_message_when_ftp_login_permission_error(self):
475 """ Should emit error message when `FTP.login` permission error. """
476 self.ftp_client.login.side_effect = ftplib.error_perm
477 try:
478 dput.methods.ftp.upload(**self.test_args)
479 except FakeSystemExit:
480 pass
481 expected_output = "Wrong Password"
482 self.assertIn(expected_output, sys.stdout.getvalue())
484 def test_calls_sys_exit_when_ftp_login_permission_error(self):
485 """ Should call `sys.exit` when `FTP.login` permission error. """
486 self.ftp_client.login.side_effect = ftplib.error_perm
487 with testtools.ExpectedException(FakeSystemExit):
488 dput.methods.ftp.upload(**self.test_args)
489 sys.exit.assert_called_with(EXIT_STATUS_FAILURE)
491 def test_emits_error_message_when_ftp_login_eof_error(self):
492 """ Should emit error message when `FTP.login` EOF error. """
493 self.ftp_client.login.side_effect = EOFError
494 try:
495 dput.methods.ftp.upload(**self.test_args)
496 except FakeSystemExit:
497 pass
498 expected_output = "Server closed the connection"
499 self.assertIn(expected_output, sys.stdout.getvalue())
501 def test_calls_sys_exit_when_ftp_login_eof_error(self):
502 """ Should call `sys.exit` when `FTP.login` EOF error. """
503 self.ftp_client.login.side_effect = EOFError
504 with testtools.ExpectedException(FakeSystemExit):
505 dput.methods.ftp.upload(**self.test_args)
506 sys.exit.assert_called_with(EXIT_STATUS_FAILURE)
508 def test_calls_ftp_set_pasv_with_expected_args(self):
509 """ Should call `FTP.set_pasv` with expected args. """
510 dput.methods.ftp.upload(**self.test_args)
511 expected_mode = bool(self.test_args['ftp_mode'])
512 expected_args = (expected_mode,)
513 self.ftp_client.set_pasv.assert_called_with(*expected_args)
515 def test_calls_ftp_cwd_with_expected_args(self):
516 """ Should call `FTP.cwd` with expected args. """
517 dput.methods.ftp.upload(**self.test_args)
518 expected_path = self.incoming_path
519 expected_args = (expected_path,)
520 self.ftp_client.cwd.assert_called_with(*expected_args)
522 def test_emits_debug_message_for_cwd(self):
523 """ Should emit debug message for successful `FTP.cwd`. """
524 self.test_args['debug'] = True
525 dput.methods.ftp.upload(**self.test_args)
526 expected_output = textwrap.dedent("""\
527 D: Directory to upload to: {path}
528 """).format(path=self.incoming_path)
529 self.assertIn(expected_output, sys.stdout.getvalue())
531 def test_emits_error_message_when_destination_directory_not_found(self):
532 """ Should emit error message when destination directory not found. """
533 error = ftplib.error_perm("550 Not Found")
534 self.ftp_client.cwd.side_effect = error
535 try:
536 dput.methods.ftp.upload(**self.test_args)
537 except FakeSystemExit:
538 pass
539 expected_output = "Directory to upload to does not exist."
540 self.assertIn(expected_output, sys.stdout.getvalue())
542 def test_calls_sys_exit_when_ftp_cwd_permission_error(self):
543 """ Should call `sys.exit` when `FTP.cwd` permission error. """
544 error = ftplib.error_perm("550 Not Found")
545 self.ftp_client.cwd.side_effect = error
546 with testtools.ExpectedException(FakeSystemExit):
547 dput.methods.ftp.upload(**self.test_args)
548 sys.exit.assert_called_with(EXIT_STATUS_FAILURE)
550 def test_propagates_exception_when_ftp_cwd_permission_error(self):
551 """ Should call `sys.exit` when `FTP.cwd` permission error. """
552 error = ftplib.error_perm("500 Bad Stuff Happened")
553 self.ftp_client.cwd.side_effect = error
554 with testtools.ExpectedException(error.__class__):
555 dput.methods.ftp.upload(**self.test_args)
557 def test_propagates_exception_when_ftp_cwd_eof_error(self):
558 """ Should call `sys.exit` when `FTP.cwd` EOF error. """
559 error = EOFError()
560 self.ftp_client.cwd.side_effect = error
561 with testtools.ExpectedException(error.__class__):
562 dput.methods.ftp.upload(**self.test_args)
564 def test_emits_debug_message_for_each_file(self):
565 """ Should emit debug message for each file to upload. """
566 self.test_args['debug'] = True
567 dput.methods.ftp.upload(**self.test_args)
568 expected_output = "".join(textwrap.dedent("""\
569 D: Uploading File: {path}
570 Uploading {filename}: done.
571 """).format(path=path, filename=os.path.basename(path))
572 for path in self.paths_to_upload)
573 self.assertIn(expected_output, sys.stdout.getvalue())
575 def test_calls_ftp_storbinary_for_each_file(self):
576 """ Should call `FTP.storbinary` for each file to upload. """
577 dput.methods.ftp.upload(**self.test_args)
578 registry = FileDouble.get_registry_for_testcase(self)
579 expected_blocksize = 1024
580 expected_calls = [
581 mock.call(
582 "STOR {filename}".format(filename=os.path.basename(path)),
583 registry[path].fake_file, expected_blocksize)
584 for path in self.paths_to_upload]
585 self.ftp_client.storbinary.assert_has_calls(
586 expected_calls, any_order=True)
588 def test_calls_close_for_each_file(self):
589 """ Should call `file.close` for each file to upload. """
590 dput.methods.ftp.upload(**self.test_args)
591 registry = FileDouble.get_registry_for_testcase(self)
592 for path in self.paths_to_upload:
593 fake_file = registry[path].fake_file
594 fake_file.close.assert_called_with()
597 class ftp_upload_ErrorTestCase(ftp_upload_TestCase):
598 """ Test cases for `methods.ftp.upload` function, error conditions. """
600 login_scenarios = [
601 ('anonymous', {
602 'login': "anonymous",
603 'expected_password': "dput@packages.debian.org",
607 ftp_client_scenarios = [
608 ('default', {
609 'ftp_mode': False,
613 progress_scenarios = [
614 ('progress-type-0', {
615 'progress_type': 0,
617 ('progress-type-1', {
618 'progress_type': 1,
620 ('progress-type-2', {
621 'progress_type': 2,
625 files_scenarios = list(
626 (scenario_name, scenario) for (scenario_name, scenario)
627 in upload_TestCase.files_scenarios
628 if scenario['paths_to_upload'])
630 scenarios = testscenarios.multiply_scenarios(
631 upload_TestCase.incoming_scenarios,
632 files_scenarios,
633 login_scenarios, ftp_client_scenarios, progress_scenarios)
635 def test_emits_warning_when_remote_file_exists(self):
636 """ Should emit a warning message when remote file exists. """
637 error = ftplib.error_perm("553 Exists")
638 self.ftp_client.storbinary.side_effect = error
639 dput.methods.ftp.upload(**self.test_args)
640 for path in self.paths_to_upload:
641 expected_output = textwrap.dedent("""\
642 Leaving existing {path} on the server and continuing
643 """).format(path=os.path.basename(path))
644 self.expectThat(
645 sys.stdout.getvalue(),
646 testtools.matchers.Contains(expected_output))
648 def test_omits_sys_exit_when_remote_file_exists(self):
649 """ Should omit call to `sys.exit` when remote file exists. """
650 error = ftplib.error_perm("553 Exists")
651 self.ftp_client.storbinary.side_effect = error
652 dput.methods.ftp.upload(**self.test_args)
653 self.assertFalse(sys.exit.called)
655 def test_emits_error_message_when_storbinary_failure(self):
656 """ Should emit an error message when `FTP.storbinary` failure. """
657 error = ftplib.error_perm("504 Weird Stuff Happened")
658 self.ftp_client.storbinary.side_effect = error
659 try:
660 dput.methods.ftp.upload(**self.test_args)
661 except FakeSystemExit:
662 pass
663 for path in self.paths_to_upload[:1]:
664 expected_output = (
665 "Note: This error might indicate a problem with"
666 " your passive_ftp setting.\n")
667 self.expectThat(
668 sys.stdout.getvalue(),
669 testtools.matchers.Contains(expected_output))
671 def test_calls_sys_exit_when_storbinary_failure(self):
672 """ Should call `sys.exit` when `FTP.storbinary` failure. """
673 error = ftplib.error_perm("504 Weird Stuff Happened")
674 self.ftp_client.storbinary.side_effect = error
675 with testtools.ExpectedException(FakeSystemExit):
676 dput.methods.ftp.upload(**self.test_args)
677 sys.exit.assert_called_with(EXIT_STATUS_FAILURE)
679 def test_emits_debug_message_when_open_failure(self):
680 """ Should emit a debug message when `builtins.open` failure. """
681 self.test_args['debug'] = True
682 registry = FileDouble.get_registry_for_testcase(self)
683 for path in self.paths_to_upload:
684 double = registry[path]
685 double.set_open_scenario('nonexist')
686 try:
687 dput.methods.ftp.upload(**self.test_args)
688 except EnvironmentError:
689 pass
690 expected_output = (
691 "D: Should exit silently now, but"
692 " will throw exception for debug.")
693 self.assertIn(expected_output, sys.stdout.getvalue())
695 def test_propagates_error_from_storbinary_for_debug(self):
696 """ Should propagate error from `FTP.storbinary` when debug. """
697 self.test_args['debug'] = True
698 error = ftplib.error_perm("504 Weird Stuff Happened")
699 self.ftp_client.storbinary.side_effect = error
700 with testtools.ExpectedException(error.__class__):
701 dput.methods.ftp.upload(**self.test_args)
703 def test_propagates_error_from_quit_for_debug(self):
704 """ Should propagate error from `FTP.quit` when debug. """
705 self.test_args['debug'] = True
706 error = ftplib.error_perm("504 Weird Stuff Happened")
707 self.ftp_client.quit.side_effect = error
708 with testtools.ExpectedException(error.__class__):
709 dput.methods.ftp.upload(**self.test_args)
712 def make_expected_filewithprogress_attributes_by_path(testcase, attrs):
713 """ Make a mapping from path to expected FileWithProgress attribs. """
714 expected_attributes_by_path = {}
715 registry = FileDouble.get_registry_for_testcase(testcase)
716 for path in testcase.paths_to_upload:
717 file_double = registry[path]
718 expected_attributes = {
719 'f': file_double.fake_file,
720 'size': file_double.stat_result.st_size,
722 expected_attributes.update(attrs)
723 expected_attributes_by_path[path] = expected_attributes
725 return expected_attributes_by_path
728 class ftp_upload_ProgressTestCase(ftp_upload_TestCase):
729 """ Test cases for `methods.ftp.upload` function, with progress meter. """
731 login_scenarios = [
732 ('anonymous', {
733 'login': "anonymous",
734 'expected_password': "dput@packages.debian.org",
738 ftp_client_scenarios = [
739 ('default', {
740 'ftp_mode': False,
744 progress_scenarios = [
745 ('progress-type-1', {
746 'progress_type': 1,
748 ('progress-type-2', {
749 'progress_type': 2,
753 scenarios = testscenarios.multiply_scenarios(
754 upload_TestCase.incoming_scenarios,
755 upload_TestCase.files_scenarios,
756 login_scenarios, ftp_client_scenarios, progress_scenarios)
758 def test_calls_storbinary_with_filewithprogress(self):
759 """ Should use a `FileWithProgress` to call `FTP.storbinary`. """
760 dput.methods.ftp.upload(**self.test_args)
761 expected_calls = [
762 mock.call(
763 mock.ANY, self.fake_filewithprogress,
764 mock.ANY)
765 for path in self.paths_to_upload]
766 self.ftp_client.storbinary.assert_has_calls(
767 expected_calls, any_order=True)
769 def test_filewithprogress_has_expected_attributes(self):
770 """ Should have expected attributes on the `FileWithProgress`. """
771 expected_attributes_by_path = (
772 make_expected_filewithprogress_attributes_by_path(
773 self, {'ptype': self.progress_type}))
774 dput.methods.ftp.upload(**self.test_args)
775 for call in self.ftp_client.storbinary.mock_calls:
776 (__, call_args, call_kwargs) = call
777 (__, stor_file, __) = call_args
778 path = stor_file.f.name
779 expected_attributes = expected_attributes_by_path[path]
780 stor_file_attributes = {
781 name: getattr(stor_file, name)
782 for name in expected_attributes}
783 self.expectThat(
784 expected_attributes,
785 testtools.matchers.Equals(stor_file_attributes))
787 def test_filewithprogress_has_sentinel_size_when_stat_failure(self):
788 """ Should have sentinel `size` value when `os.stat` failure. """
789 expected_attributes_by_path = (
790 make_expected_filewithprogress_attributes_by_path(
791 self, {'size': -1}))
792 registry = FileDouble.get_registry_for_testcase(self)
793 for path in self.paths_to_upload:
794 double = registry[path]
795 double.set_os_stat_scenario('notfound_error')
796 dput.methods.ftp.upload(**self.test_args)
797 for call in self.ftp_client.storbinary.mock_calls:
798 (__, call_args, call_kwargs) = call
799 (__, stor_file, __) = call_args
800 path = stor_file.f.name
801 expected_attributes = expected_attributes_by_path[path]
802 stor_file_attributes = {
803 name: getattr(stor_file, name)
804 for name in expected_attributes}
805 self.expectThat(
806 expected_attributes,
807 testtools.matchers.Equals(stor_file_attributes))
809 def test_emits_debug_message_when_stat_failure(self):
810 """ Should have sentinel `size` value when `os.stat` failure. """
811 self.test_args['debug'] = True
812 registry = FileDouble.get_registry_for_testcase(self)
813 for path in self.paths_to_upload:
814 double = registry[path]
815 double.set_os_stat_scenario('notfound_error')
816 dput.methods.ftp.upload(**self.test_args)
817 for path in self.paths_to_upload:
818 expected_output = textwrap.dedent("""\
819 D: Determining size of file '{path}' failed
820 """).format(path=path)
821 self.expectThat(
822 sys.stdout.getvalue(),
823 testtools.matchers.Contains(expected_output))
826 class http_upload_TestCase(upload_TestCase):
827 """ Base for test cases for `methods.http.upload` function. """
829 scenarios = NotImplemented
831 protocol_scenarios = [
832 ('http', {
833 'function_to_test': dput.methods.http.upload,
834 'protocol': "http",
835 'protocol_version': "HTTP/1.0",
837 ('https', {
838 'function_to_test': dput.methods.https.upload,
839 'protocol': "https",
840 'protocol_version': "HTTP/1.0",
844 login_scenarios = [
845 ('username', {
846 'login': "lorem",
850 def setUp(self):
851 """ Set up test fixtures. """
852 super(http_upload_TestCase, self).setUp()
854 httpretty.enable()
855 self.addCleanup(httpretty.disable)
857 self.set_response_header_fields()
858 self.patch_put_requests()
860 patch_getpass_getpass(self)
861 self.fake_password = self.getUniqueString()
862 getpass.getpass.return_value = self.fake_password
863 if not hasattr(self, 'expected_password'):
864 self.expected_password = self.fake_password
866 def set_test_args(self):
867 """ Set the arguments for the test call to the function. """
868 if not hasattr(self, 'progress_type'):
869 self.progress_type = 0
870 self.test_args = dict(
871 fqdn=self.getUniqueString(),
872 login=self.login,
873 incoming=self.incoming_path,
874 files_to_upload=self.paths_to_upload,
875 debug=False,
876 dummy=object(),
877 progress=self.progress_type,
879 if self.function_to_test is dput.methods.http.upload:
880 self.test_args['protocol'] = self.protocol
882 def make_upload_uri(self, file_name):
883 """ Make the URI for a file for upload. """
884 uri = urlparse.urlunsplit([
885 self.protocol, self.test_args['fqdn'],
886 os.path.join(os.path.sep, self.incoming_path, file_name),
887 None, None])
888 return uri
890 def set_response_header_fields(self):
891 """ Set the header fields for the HTTP response. """
892 if not hasattr(self, 'response_header_fields'):
893 self.response_header_fields = {}
895 def patch_put_requests(self):
896 """ Patch the HTTP PUT requests. """
897 self.path_by_request_uri = {}
898 for path in self.paths_to_upload:
899 upload_uri = self.make_upload_uri(os.path.basename(path))
900 self.path_by_request_uri[upload_uri] = path
901 response_body = ""
902 httpretty.register_uri(
903 httpretty.PUT, upload_uri,
904 status=self.status_code, body=response_body)
907 class http_upload_SuccessTestCase(http_upload_TestCase):
908 """ Success test cases for `methods.http.upload` function. """
910 response_scenarios = [
911 ('okay', {
912 'status_code': 200,
913 'status_reason': "Okay",
915 ('chatter', {
916 'status_code': 203,
917 'status_reason': "Non-Authoritative Information",
921 auth_scenarios = [
922 ('auth-accepted', {
923 'auth_response_status_code': 200,
924 'auth_response_status_reason': "Okay",
928 size_scenarios = [
929 ('size-empty', {
930 'fake_file': io.BytesIO(),
932 ('size-1k', {
933 'fake_file': io.BytesIO(
934 b"Lorem ipsum, dolor sit amet.___\n" * 32),
936 ('size-100k', {
937 'fake_file': io.BytesIO(
938 b"Lorem ipsum, dolor sit amet.___\n" * 3200),
942 incoming_scenarios = list(upload_TestCase.incoming_scenarios)
943 for (scenario_name, scenario) in incoming_scenarios:
944 scenario['expected_url_path_prefix'] = os.path.join(
945 os.path.sep, scenario['incoming_path'])
946 del scenario_name, scenario
948 scenarios = testscenarios.multiply_scenarios(
949 upload_TestCase.files_scenarios,
950 size_scenarios,
951 upload_TestCase.incoming_scenarios,
952 http_upload_TestCase.protocol_scenarios,
953 http_upload_TestCase.login_scenarios,
954 response_scenarios, auth_scenarios)
956 def test_emits_debug_message_for_upload(self):
957 """ Should emit debug message for upload. """
958 self.test_args['debug'] = True
959 self.function_to_test(**self.test_args)
960 for path in self.paths_to_upload:
961 expected_uri = self.make_upload_uri(os.path.basename(path))
962 expected_output = textwrap.dedent("""\
963 D: HTTP-PUT to URL: {uri}
964 """).format(uri=expected_uri)
965 self.expectThat(
966 sys.stdout.getvalue(),
967 testtools.matchers.Contains(expected_output))
969 def test_request_has_expected_fields(self):
970 """ Should send request with expected fields in header. """
971 if not self.paths_to_upload:
972 self.skipTest("No files to upload")
973 self.function_to_test(**self.test_args)
974 registry = FileDouble.get_registry_for_testcase(self)
975 path = self.paths_to_upload[-1]
976 double = registry[path]
977 request = httpretty.last_request()
978 expected_fields = {
979 'User-Agent': "dput",
980 'Connection': "close",
981 'Content-Length': "{size:d}".format(
982 size=len(double.fake_file.getvalue())),
984 for (name, value) in expected_fields.items():
985 self.expectThat(
986 request.headers.get(name),
987 testtools.matchers.Equals(value))
990 class http_upload_ProgressTestCase(http_upload_TestCase):
991 """ Test cases for `methods.http.upload` function, with progress meter. """
993 files_scenarios = list(
994 (scenario_name, scenario) for (scenario_name, scenario)
995 in upload_TestCase.files_scenarios
996 if scenario['paths_to_upload'])
998 response_scenarios = [
999 ('okay', {
1000 'status_code': 200,
1001 'status_reason': "Okay",
1005 progress_scenarios = [
1006 ('progress-type-1', {
1007 'progress_type': 1,
1009 ('progress-type-2', {
1010 'progress_type': 2,
1014 scenarios = testscenarios.multiply_scenarios(
1015 files_scenarios,
1016 progress_scenarios,
1017 upload_TestCase.incoming_scenarios,
1018 http_upload_TestCase.protocol_scenarios,
1019 http_upload_TestCase.login_scenarios,
1020 http_upload_SuccessTestCase.auth_scenarios,
1021 response_scenarios)
1023 def test_filewithprogress_has_expected_attributes(self):
1024 """ Should have expected attributes on the `FileWithProgress`. """
1025 expected_attributes_by_path = (
1026 make_expected_filewithprogress_attributes_by_path(
1027 self, {'ptype': self.progress_type}))
1028 self.function_to_test(**self.test_args)
1029 path = self.paths_to_upload[-1]
1030 expected_attributes = expected_attributes_by_path[path]
1031 fake_file_attributes = {
1032 name: getattr(self.fake_filewithprogress, name)
1033 for name in expected_attributes}
1034 self.expectThat(
1035 expected_attributes,
1036 testtools.matchers.Equals(fake_file_attributes))
1039 class http_upload_UnknownProtocolTestCase(http_upload_TestCase):
1040 """ Test cases for `methods.http.upload` function, unknown protocol. """
1042 files_scenarios = list(
1043 (scenario_name, scenario) for (scenario_name, scenario)
1044 in upload_TestCase.files_scenarios
1045 if scenario['paths_to_upload'])
1047 protocol_scenarios = [
1048 ('protocol-bogus', {
1049 'function_to_test': dput.methods.http.upload,
1050 'protocol': "b0gUs",
1051 'protocol_version': "b0gUs",
1052 'expected_exit_status': EXIT_STATUS_FAILURE,
1056 response_scenarios = [
1057 (scenario_name, scenario) for (scenario_name, scenario)
1058 in http_upload_SuccessTestCase.response_scenarios
1059 if scenario['status_code'] == 200]
1061 scenarios = testscenarios.multiply_scenarios(
1062 files_scenarios,
1063 upload_TestCase.incoming_scenarios,
1064 protocol_scenarios,
1065 http_upload_TestCase.login_scenarios,
1066 response_scenarios,
1067 http_upload_SuccessTestCase.auth_scenarios)
1069 def test_emits_error_message_when_unknown_protocol(self):
1070 """ Should emit error message when unknown protocol. """
1071 try:
1072 self.function_to_test(**self.test_args)
1073 except FakeSystemExit:
1074 pass
1075 expected_output = "Wrong protocol for upload "
1076 self.assertIn(expected_output, sys.stderr.getvalue())
1078 def test_calls_sys_exit_when_unknown_protocol(self):
1079 """ Should call `sys.exit` when unknown protocol. """
1080 with testtools.ExpectedException(FakeSystemExit):
1081 self.function_to_test(**self.test_args)
1082 sys.exit.assert_called_with(self.expected_exit_status)
1085 class http_upload_FileStatFailureTestCase(http_upload_TestCase):
1086 """ Test cases for `methods.http.upload` function, `os.stat` failure. """
1088 files_scenarios = list(
1089 (scenario_name, scenario) for (scenario_name, scenario)
1090 in upload_TestCase.files_scenarios
1091 if scenario['paths_to_upload'])
1093 os_stat_scenarios = [
1094 ('os-stat-notfound', {
1095 'os_stat_scenario_name': "notfound_error",
1096 'expected_exit_status': EXIT_STATUS_FAILURE,
1098 ('os-stat-denied', {
1099 'os_stat_scenario_name': "denied_error",
1100 'expected_exit_status': EXIT_STATUS_FAILURE,
1104 response_scenarios = list(
1105 (scenario_name, scenario) for (scenario_name, scenario)
1106 in http_upload_SuccessTestCase.response_scenarios
1107 if scenario['status_code'] == 200)
1109 scenarios = testscenarios.multiply_scenarios(
1110 files_scenarios,
1111 os_stat_scenarios,
1112 upload_TestCase.incoming_scenarios,
1113 http_upload_TestCase.protocol_scenarios,
1114 http_upload_TestCase.login_scenarios,
1115 response_scenarios,
1116 http_upload_SuccessTestCase.auth_scenarios)
1118 def test_emits_error_message(self):
1119 """ Should emit error message when `os.stat` failure. """
1120 try:
1121 self.function_to_test(**self.test_args)
1122 except FakeSystemExit:
1123 pass
1124 expected_output = textwrap.dedent("""\
1125 Determining size of file '{path}' failed
1126 """).format(path=self.paths_to_upload[0])
1127 self.assertIn(expected_output, sys.stderr.getvalue())
1129 def test_calls_sys_exit_with_expected_exit_status(self):
1130 """ Should call `sys.exit` with expected exit status. """
1131 with testtools.ExpectedException(FakeSystemExit):
1132 self.function_to_test(**self.test_args)
1133 sys.exit.assert_called_with(self.expected_exit_status)
1136 class http_upload_ResponseErrorTestCase(http_upload_TestCase):
1137 """ Error test cases for `methods.http.upload` function. """
1139 files_scenarios = list(
1140 (scenario_name, scenario) for (scenario_name, scenario)
1141 in upload_TestCase.files_scenarios
1142 if scenario['paths_to_upload'])
1144 response_scenarios = [
1145 ('server-error', {
1146 'status_code': 500,
1147 'status_reason': "Internal Server Error",
1148 'auth_response_status_code': 200,
1149 'auth_response_status_reason': "Okay",
1150 'expected_exit_status': EXIT_STATUS_FAILURE,
1154 scenarios = testscenarios.multiply_scenarios(
1155 files_scenarios,
1156 upload_TestCase.incoming_scenarios,
1157 http_upload_TestCase.protocol_scenarios,
1158 http_upload_TestCase.login_scenarios,
1159 response_scenarios)
1161 def test_emits_error_message_when_response_status_error(self):
1162 """ Should emit debug message when response status is error. """
1163 try:
1164 self.function_to_test(**self.test_args)
1165 except FakeSystemExit:
1166 pass
1167 expected_output = textwrap.dedent("""\
1168 Upload failed: {status} {reason}
1169 """).format(status=self.status_code, reason=self.status_reason)
1170 self.assertIn(expected_output, sys.stdout.getvalue())
1172 def test_calls_sys_exit_when_response_status_error(self):
1173 """ Should call `sys.exit` when response status is error. """
1174 with testtools.ExpectedException(FakeSystemExit):
1175 self.function_to_test(**self.test_args)
1176 sys.exit.assert_called_with(self.expected_exit_status)
1179 def make_host_spec(username, host):
1180 """ Make an SSH host specification. """
1181 host_spec = host
1182 if username != "*":
1183 host_spec = "{user}@{fqdn}".format(user=username, fqdn=host)
1184 return host_spec
1187 def make_remote_spec(username, host, dir_path):
1188 """ Make an SCP remote specification. """
1189 host_spec = make_host_spec(username, host)
1190 remote_spec = "{host}:{dir}".format(host=host_spec, dir=dir_path)
1191 return remote_spec
1194 class ssh_channel_upload_TestCase(upload_TestCase):
1195 """ Base for test cases for upload over SSH channel. """
1197 function_to_test = NotImplemented
1199 scenarios = NotImplemented
1201 login_scenarios = [
1202 ('login-username', {
1203 'login': "lorem",
1205 ('login-wildcard', {
1206 'login': "*",
1210 stat_mode_scenarios = [
1211 ('stat-mode-default', {}),
1212 ('stat-mode-0620', {
1213 'stat_mode': 0o0620,
1214 'expected_ssh_chmod': True,
1218 def set_upload_file_modes(self):
1219 """ Set filesystem modes for upload files. """
1220 registry = FileDouble.get_registry_for_testcase(self)
1221 if hasattr(self, 'stat_mode'):
1222 for path in self.paths_to_upload:
1223 file_double = registry[path]
1224 file_double.stat_result = file_double.stat_result._replace(
1225 st_mode=self.stat_mode)
1227 def set_ssh_chmod_subprocess_double(self):
1228 """ Set the ‘ssh … chmod’ test double for the subprocess. """
1229 command_file_path = "/usr/bin/ssh"
1230 argv = [os.path.basename(command_file_path)]
1231 argv.extend(self.expected_ssh_options)
1232 argv.append(make_host_spec(
1233 username=self.login, host=self.test_args['fqdn']))
1234 argv.extend(["chmod", "0644"])
1235 argv.extend(
1236 os.path.join(self.incoming_path, os.path.basename(path))
1237 for path in self.paths_to_upload)
1238 double = SubprocessDouble(command_file_path, argv=argv)
1239 double.register_for_testcase(self)
1240 check_call_scenario_name = getattr(
1241 self, 'ssh_chmod_check_call_scenario_name', "success")
1242 double.set_subprocess_check_call_scenario(check_call_scenario_name)
1243 self.ssh_chmod_subprocess_double = double
1246 class scp_upload_TestCase(ssh_channel_upload_TestCase):
1247 """ Test cases for `methods.scp.upload` function. """
1249 function_to_test = staticmethod(dput.methods.scp.upload)
1251 scenarios = NotImplemented
1253 ssh_config_scenarios = [
1254 ('ssh-opts-none', {
1255 'ssh_config_options': [],
1256 'expected_ssh_options': [],
1258 ('ssh-opts-one', {
1259 'ssh_config_options': ["foo"],
1260 'expected_ssh_options': ["-o", "foo"],
1262 ('ssh-opts-three', {
1263 'ssh_config_options': ["foo", "bar", "baz"],
1264 'expected_ssh_options': [
1265 "-o", "foo", "-o", "bar", "-o", "baz"],
1269 def setUp(self):
1270 """ Set up test fixtures. """
1271 super(scp_upload_TestCase, self).setUp()
1273 patch_os_lstat(self)
1274 self.set_upload_file_modes()
1276 self.set_scp_subprocess_double()
1277 self.set_ssh_chmod_subprocess_double()
1279 def set_test_args(self):
1280 """ Set the arguments for the test call to the function. """
1281 self.test_args = dict(
1282 fqdn=self.getUniqueString(),
1283 login=self.login,
1284 incoming=self.incoming_path,
1285 files_to_upload=self.paths_to_upload,
1286 debug=None,
1287 compress=self.compress,
1288 ssh_config_options=self.ssh_config_options,
1289 progress=object(),
1292 def set_scp_subprocess_double(self):
1293 """ Set the ‘scp’ test double for the subprocess. """
1294 command_file_path = "/usr/bin/scp"
1295 argv = [os.path.basename(command_file_path), "-p"]
1296 argv.extend(self.scp_compress_options)
1297 argv.extend(self.expected_ssh_options)
1298 argv.extend(self.paths_to_upload)
1299 argv.append(make_remote_spec(
1300 username=self.login, host=self.test_args['fqdn'],
1301 dir_path=self.incoming_path))
1302 double = SubprocessDouble(command_file_path, argv=argv)
1303 double.register_for_testcase(self)
1304 check_call_scenario_name = getattr(
1305 self, 'scp_subprocess_check_call_scenario_name', "success")
1306 double.set_subprocess_check_call_scenario(check_call_scenario_name)
1307 self.scp_subprocess_double = double
1310 class scp_upload_ScpTestCase(scp_upload_TestCase):
1311 """ Test cases for `methods.scp.upload` function, with ‘scp’ command. """
1313 compress_scenarios = [
1314 ('compress-false', {
1315 'compress': False,
1316 'scp_compress_options': [],
1318 ('compress-true', {
1319 'compress': True,
1320 'scp_compress_options': ["-C"],
1324 scenarios = testscenarios.multiply_scenarios(
1325 upload_TestCase.files_scenarios,
1326 upload_TestCase.incoming_scenarios,
1327 ssh_channel_upload_TestCase.login_scenarios,
1328 ssh_channel_upload_TestCase.stat_mode_scenarios,
1329 compress_scenarios,
1330 scp_upload_TestCase.ssh_config_scenarios)
1332 def test_emits_debug_message_for_upload(self):
1333 """ Should emit debug message for files upload. """
1334 self.test_args['debug'] = True
1335 self.function_to_test(**self.test_args)
1336 expected_output = textwrap.dedent("""\
1337 D: Uploading with scp to {host}:{incoming}
1338 """).format(
1339 host=make_host_spec(
1340 username=self.login, host=self.test_args['fqdn']),
1341 incoming=self.incoming_path)
1342 self.assertIn(expected_output, sys.stdout.getvalue())
1344 def test_calls_check_call_with_expected_scp_command(self):
1345 """ Should call `subprocess.check_call` with ‘scp’ command. """
1346 self.function_to_test(**self.test_args)
1347 expected_call = mock.call(self.scp_subprocess_double.argv)
1348 self.assertIn(expected_call, subprocess.check_call.mock_calls)
1350 def test_emits_error_message_when_scp_failure(self):
1351 """ Should emit error message when ‘scp’ command fails. """
1352 double = self.scp_subprocess_double
1353 double.set_subprocess_check_call_scenario("failure")
1354 try:
1355 self.function_to_test(**self.test_args)
1356 except FakeSystemExit:
1357 pass
1358 expected_output = "Error while uploading."
1359 self.assertIn(expected_output, sys.stdout.getvalue())
1361 def test_calls_sys_exit_when_scp_failure(self):
1362 """ Should call `sys.exit` when ‘scp’ command fails. """
1363 double = self.scp_subprocess_double
1364 double.set_subprocess_check_call_scenario("failure")
1365 with testtools.ExpectedException(FakeSystemExit):
1366 self.function_to_test(**self.test_args)
1367 sys.exit.assert_called_with(EXIT_STATUS_FAILURE)
1370 class scp_upload_ChmodTestCase(scp_upload_TestCase):
1371 """ Test cases for `methods.scp.upload` function, with ‘ssh … chmod’. """
1373 files_scenarios = list(
1374 (scenario_name, scenario) for (scenario_name, scenario)
1375 in upload_TestCase.files_scenarios
1376 if scenario['paths_to_upload'])
1378 stat_mode_scenarios = list(
1379 (scenario_name, scenario) for (scenario_name, scenario)
1380 in ssh_channel_upload_TestCase.stat_mode_scenarios
1381 if 'expected_ssh_chmod' in scenario)
1383 compress_scenarios = [
1384 ('compress-false', {
1385 'compress': False,
1386 'scp_compress_options': [],
1390 scenarios = testscenarios.multiply_scenarios(
1391 files_scenarios,
1392 upload_TestCase.incoming_scenarios,
1393 ssh_channel_upload_TestCase.login_scenarios,
1394 stat_mode_scenarios,
1395 compress_scenarios,
1396 scp_upload_TestCase.ssh_config_scenarios)
1398 def test_emits_debug_message_for_fixing_permissions(self):
1399 """ Should emit debug message for fixing file permissions . """
1400 self.test_args['debug'] = True
1401 self.function_to_test(**self.test_args)
1402 expected_output = "D: Fixing some permissions"
1403 self.assertIn(expected_output, sys.stdout.getvalue())
1405 def test_calls_check_call_with_expected_ssh_chmod_command(self):
1406 """ Should call `subprocess.check_call` with ‘ssh … chmod’ command. """
1407 self.function_to_test(**self.test_args)
1408 expected_call = mock.call(self.ssh_chmod_subprocess_double.argv)
1409 self.assertIn(expected_call, subprocess.check_call.mock_calls)
1411 def test_emits_error_message_when_ssh_chmod_failure(self):
1412 """ Should emit error message when ‘ssh … chmod’ command fails. """
1413 double = self.ssh_chmod_subprocess_double
1414 double.set_subprocess_check_call_scenario("failure")
1415 try:
1416 self.function_to_test(**self.test_args)
1417 except FakeSystemExit:
1418 pass
1419 expected_output = "Error while fixing permissions."
1420 self.assertIn(expected_output, sys.stdout.getvalue())
1422 def test_calls_sys_exit_when_ssh_chmod_failure(self):
1423 """ Should call `sys.exit` when ‘ssh … chmod’ command fails. """
1424 double = self.ssh_chmod_subprocess_double
1425 double.set_subprocess_check_call_scenario("failure")
1426 with testtools.ExpectedException(FakeSystemExit):
1427 self.function_to_test(**self.test_args)
1428 sys.exit.assert_called_with(EXIT_STATUS_FAILURE)
1431 class rsync_upload_TestCase(ssh_channel_upload_TestCase):
1432 """ Test cases for `methods.rsync.upload` function. """
1434 function_to_test = staticmethod(dput.methods.rsync.upload)
1436 scenarios = testscenarios.multiply_scenarios(
1437 upload_TestCase.files_scenarios,
1438 upload_TestCase.incoming_scenarios,
1439 ssh_channel_upload_TestCase.login_scenarios,
1440 ssh_channel_upload_TestCase.stat_mode_scenarios)
1442 def setUp(self):
1443 """ Set up test fixtures. """
1444 super(rsync_upload_TestCase, self).setUp()
1446 self.set_rsync_subprocess_double()
1448 self.expected_ssh_options = []
1449 self.set_ssh_chmod_subprocess_double()
1451 def set_test_args(self):
1452 """ Set the arguments for the test call to the function. """
1453 self.test_args = dict(
1454 fqdn=self.getUniqueString(),
1455 login=self.login,
1456 incoming=self.incoming_path,
1457 files_to_upload=self.paths_to_upload,
1458 debug=False,
1459 dummy=object(),
1460 progress=object(),
1463 def set_rsync_subprocess_double(self):
1464 """ Set the ‘rsync’ test double for the subprocess. """
1465 command_file_path = "/usr/bin/rsync"
1466 argv = [os.path.basename(command_file_path)]
1467 argv.extend(self.paths_to_upload)
1468 argv.extend([
1469 "--copy-links", "--progress", "--partial",
1470 "-zave", "ssh -x"])
1471 argv.append(make_remote_spec(
1472 username=self.login, host=self.test_args['fqdn'],
1473 dir_path=self.incoming_path))
1474 double = SubprocessDouble(command_file_path, argv=argv)
1475 double.register_for_testcase(self)
1476 check_call_scenario_name = getattr(
1477 self, 'rsync_check_call_scenario_name', "success")
1478 double.set_subprocess_check_call_scenario(check_call_scenario_name)
1479 self.rsync_subprocess_double = double
1481 def test_emits_debug_message_for_upload(self):
1482 """ Should emit debug message for files upload. """
1483 self.test_args['debug'] = True
1484 self.function_to_test(**self.test_args)
1485 expected_output = textwrap.dedent("""\
1486 D: Uploading with rsync to {host}:{incoming}
1487 """).format(
1488 host=make_host_spec(
1489 username=self.login, host=self.test_args['fqdn']),
1490 incoming=self.incoming_path)
1491 self.assertIn(expected_output, sys.stdout.getvalue())
1493 def test_calls_check_call_with_expected_rsync_command(self):
1494 """ Should call `subprocess.check_call` with ‘rsync’ command. """
1495 self.function_to_test(**self.test_args)
1496 expected_call = mock.call(self.rsync_subprocess_double.argv)
1497 self.assertIn(expected_call, subprocess.check_call.mock_calls)
1499 def test_emits_error_message_when_rsync_failure(self):
1500 """ Should emit error message when ‘rsync’ command fails. """
1501 double = self.rsync_subprocess_double
1502 double.set_subprocess_check_call_scenario("failure")
1503 try:
1504 self.function_to_test(**self.test_args)
1505 except FakeSystemExit:
1506 pass
1507 expected_output = "Error while uploading."
1508 self.assertIn(expected_output, sys.stdout.getvalue())
1510 def test_calls_sys_exit_when_rsync_failure(self):
1511 """ Should call `sys.exit` when ‘rsync’ command fails. """
1512 double = self.rsync_subprocess_double
1513 double.set_subprocess_check_call_scenario("failure")
1514 with testtools.ExpectedException(FakeSystemExit):
1515 self.function_to_test(**self.test_args)
1516 sys.exit.assert_called_with(EXIT_STATUS_FAILURE)
1518 def test_emits_debug_message_for_fixing_permissions(self):
1519 """ Should emit debug message for fixing file permissions . """
1520 self.test_args['debug'] = True
1521 self.function_to_test(**self.test_args)
1522 expected_output = textwrap.dedent("""\
1523 D: Fixing file permissions with {host}
1524 """).format(
1525 host=make_host_spec(
1526 username=self.login, host=self.test_args['fqdn']))
1527 self.assertIn(expected_output, sys.stdout.getvalue())
1529 def test_calls_check_call_with_expected_ssh_chmod_command(self):
1530 """ Should call `subprocess.check_call` with ‘ssh … chmod’ command. """
1531 self.function_to_test(**self.test_args)
1532 expected_call = mock.call(list(self.ssh_chmod_subprocess_double.argv))
1533 self.assertIn(expected_call, subprocess.check_call.mock_calls)
1535 def test_emits_error_message_when_ssh_chmod_failure(self):
1536 """ Should emit error message when ‘ssh … chmod’ command fails. """
1537 double = self.ssh_chmod_subprocess_double
1538 double.set_subprocess_check_call_scenario("failure")
1539 try:
1540 self.function_to_test(**self.test_args)
1541 except FakeSystemExit:
1542 pass
1543 expected_output = "Error while fixing permission."
1544 self.assertIn(expected_output, sys.stdout.getvalue())
1546 def test_calls_sys_exit_when_ssh_chmod_failure(self):
1547 """ Should call `sys.exit` when ‘ssh … chmod’ command fails. """
1548 double = self.ssh_chmod_subprocess_double
1549 double.set_subprocess_check_call_scenario("failure")
1550 with testtools.ExpectedException(FakeSystemExit):
1551 self.function_to_test(**self.test_args)
1552 sys.exit.assert_called_with(EXIT_STATUS_FAILURE)
1555 # Copyright © 2015–2016 Ben Finney <bignose@debian.org>
1557 # This is free software: you may copy, modify, and/or distribute this work
1558 # under the terms of the GNU General Public License as published by the
1559 # Free Software Foundation; version 3 of that license or any later version.
1560 # No warranty expressed or implied. See the file ‘LICENSE.GPL-3’ for details.
1563 # Local variables:
1564 # coding: utf-8
1565 # mode: python
1566 # End:
1567 # vim: fileencoding=utf-8 filetype=python :