Complete the transfer of maintainer hat to myself.
[dput.git] / test / test_methods.py
blob9c4fcab7a66ec86036db53b1ade703392f80caf4
1 # -*- coding: utf-8; -*-
3 # test/test_methods.py
4 # Part of ‘dput’, a Debian package upload toolkit.
6 # Copyright © 2015–2016 Ben Finney <ben+python@benfinney.id.au>
8 # This is free software: you may copy, modify, and/or distribute this work
9 # under the terms of the GNU General Public License as published by the
10 # Free Software Foundation; version 3 of that license or any later version.
11 # No warranty expressed or implied. See the file ‘LICENSE.GPL-3’ for details.
13 """ Unit tests for upload method behaviour. """
15 from __future__ import (absolute_import, unicode_literals)
17 import sys
18 import os
19 import os.path
20 import subprocess
21 import io
22 import stat
23 import pkgutil
24 import importlib
25 import collections
26 import tempfile
27 import textwrap
28 import doctest
29 import getpass
30 import ftplib
32 if sys.version_info >= (3, 3):
33 import urllib.parse as urlparse
34 elif sys.version_info >= (3, 0):
35 raise RuntimeError("Python 3 earlier than 3.3 is not supported.")
36 else:
37 import urlparse
39 import testtools
40 import testscenarios
41 import httpretty
43 __package__ = str("test")
44 __import__(__package__)
46 sys.path.insert(1, os.path.dirname(os.path.dirname(__file__)))
47 import dput.dput
48 import dput.helper.dputhelper
49 import dput.methods
50 import dput.methods.local
51 import dput.methods.ftp
52 import dput.methods.http
53 import dput.methods.https
54 import dput.methods.scp
55 import dput.methods.rsync
57 from .helper import (
58 mock,
59 FakeSystemExit,
60 patch_system_interfaces,
61 EXIT_STATUS_FAILURE,
62 FileDouble,
63 patch_os_stat,
64 patch_os_lstat,
65 setup_file_double_behaviour,
66 patch_subprocess_check_call,
67 ARG_ANY,
68 SubprocessDouble,
70 from .test_dputhelper import (
71 patch_filewithprogress,
75 class import_upload_functions_TestCase(
76 testscenarios.WithScenarios,
77 testtools.TestCase):
78 """ Test cases for `import_upload_functions` function. """
80 scenarios = [
81 ('empty', {
82 'module_names': [],
83 }),
84 ('one', {
85 'module_names': ["foo"],
86 }),
87 ('three', {
88 'module_names': ["foo", "bar", "baz"],
89 }),
92 for (scenario_name, scenario) in scenarios:
93 modules_by_name = collections.OrderedDict()
94 iter_modules_result = []
95 for module_name in scenario['module_names']:
96 module = mock.Mock()
97 module.__name__ = module_name
98 module.upload = mock.Mock()
99 modules_by_name[module_name] = module
100 module_entry = (module, module_name, False)
101 iter_modules_result.append(module_entry)
102 scenario['modules_by_name'] = modules_by_name
103 scenario['iter_modules_result'] = iter_modules_result
104 del scenario_name, scenario
106 def setUp(self):
107 """ Set up test fixtures. """
108 super(import_upload_functions_TestCase, self).setUp()
109 patch_system_interfaces(self)
111 self.patch_import_functions()
113 def patch_import_functions(self):
114 """ Patch import functions used by the function. """
115 self.patch_pkgutil_iter_modules()
116 self.patch_importlib_import_module()
118 def patch_pkgutil_iter_modules(self):
119 """ Patch `pkgutil.iter_modules` function for this test case. """
120 func_patcher = mock.patch.object(
121 pkgutil, "iter_modules", autospec=True)
122 func_patcher.start()
123 self.addCleanup(func_patcher.stop)
124 pkgutil.iter_modules.return_value = self.iter_modules_result
126 def patch_importlib_import_module(self):
127 """ Patch `importlib.import_module` function for this test case. """
128 func_patcher = mock.patch.object(
129 importlib, "import_module", autospec=True)
130 func_patcher.start()
131 self.addCleanup(func_patcher.stop)
133 def fake_import_module(full_name):
134 module_name = full_name.split(".")[-1]
135 module = self.modules_by_name[module_name]
136 return module
138 importlib.import_module.side_effect = fake_import_module
140 @mock.patch.object(dput.dput, 'debug', new=True)
141 def test_emits_debug_message_for_modules_found(self):
142 """ Should emit a debug message for the modules found. """
143 expected_message = "D: modules_found: {names!r}".format(
144 names=self.module_names)
145 dput.dput.import_upload_functions()
146 self.assertIn(expected_message, sys.stdout.getvalue())
148 @mock.patch.object(dput.dput, 'debug', new=True)
149 def test_emits_debug_message_for_each_import(self):
150 """ Should emit a debug message for each module imported. """
151 dput.dput.import_upload_functions()
152 for (module_name, module) in self.modules_by_name.items():
153 expected_message = "D: Module: {name} ({module!r})".format(
154 name=module_name, module=module)
155 self.assertIn(expected_message, sys.stdout.getvalue())
157 @mock.patch.object(dput.dput, 'debug', new=True)
158 def test_emits_debug_message_for_each_upload_method(self):
159 """ Should emit a debug message for each upload method. """
160 dput.dput.import_upload_functions()
161 for module_name in self.module_names:
162 expected_message = "D: Method name: {name}".format(
163 name=module_name)
164 self.assertIn(expected_message, sys.stdout.getvalue())
166 def test_returns_expected_function_mapping(self):
167 """ Should return expected mapping of upload functions. """
168 result = dput.dput.import_upload_functions()
169 expected_result = {
170 name: self.modules_by_name[name].upload
171 for name in self.module_names}
172 self.assertEqual(expected_result, result)
175 def patch_getpass_getpass(testcase):
176 """ Patch the `getpass.getpass` function for the test case. """
177 func_patcher = mock.patch.object(getpass, "getpass", autospec=True)
178 func_patcher.start()
179 testcase.addCleanup(func_patcher.stop)
182 class upload_TestCase(
183 testscenarios.WithScenarios,
184 testtools.TestCase):
185 """ Base for test cases for method modules `upload` functions. """
187 files_scenarios = [
188 ('file-list-empty', {
189 'paths_to_upload': [],
191 ('file-list-one', {
192 'paths_to_upload': [tempfile.mktemp()],
194 ('file-list-three', {
195 'paths_to_upload': [tempfile.mktemp() for __ in range(3)],
199 check_call_scenarios = [
200 ('check-call-success', {
201 'check_call_scenario_name': 'success',
203 ('check-call-failure', {
204 'check_call_scenario_name': 'failure',
205 'check_call_error': subprocess.CalledProcessError,
209 check_call_success_scenarios = [
210 (name, params) for (name, params) in check_call_scenarios
211 if 'check_call_error' not in params]
213 incoming_scenarios = [
214 ('incoming-simple', {
215 'incoming_path': tempfile.mktemp(),
217 ('incoming-no-leading-slash', {
218 'incoming_path': tempfile.mktemp().lstrip("/"),
220 ('incoming-has-trailing-slash', {
221 'incoming_path': tempfile.mktemp() + "/",
225 def setUp(self):
226 """ Set up test fixtures. """
227 super(upload_TestCase, self).setUp()
228 patch_system_interfaces(self)
230 patch_subprocess_check_call(self)
232 patch_os_stat(self)
233 self.set_file_doubles()
234 setup_file_double_behaviour(self)
236 patch_filewithprogress(self)
238 self.set_test_args()
240 def set_file_doubles(self):
241 """ Set the file doubles for this test case. """
242 for path in self.paths_to_upload:
243 fake_file = getattr(self, 'fake_file', None)
244 double = FileDouble(path, fake_file)
245 double.set_os_stat_scenario(
246 getattr(self, 'os_stat_scenario_name', "okay"))
247 double.register_for_testcase(self)
249 func_patcher = mock.patch.object(
250 double.fake_file, "close", autospec=True)
251 func_patcher.start()
252 self.addCleanup(func_patcher.stop)
254 def set_test_args(self):
255 """ Set the arguments for the test call to the function. """
256 raise NotImplementedError
258 def get_command_args_from_latest_check_call(self):
259 """ Get command line arguments from latest `subprocess.check_call`. """
260 latest_call = subprocess.check_call.call_args
261 (args, kwargs) = latest_call
262 command_args = args[0]
263 return command_args
266 class local_upload_TestCase(upload_TestCase):
267 """ Test cases for `methods.local.upload` function. """
269 scenarios = testscenarios.multiply_scenarios(
270 upload_TestCase.incoming_scenarios,
271 upload_TestCase.files_scenarios,
272 upload_TestCase.check_call_success_scenarios)
274 command_file_path = os.path.join("/usr/bin", "install")
276 def setUp(self):
277 """ Set up test fixtures. """
278 super(local_upload_TestCase, self).setUp()
280 self.set_subprocess_double()
282 def set_test_args(self):
283 """ Set the arguments for the test call to the function. """
284 self.test_args = dict(
285 fqdn=object(),
286 login=object(),
287 incoming=self.incoming_path,
288 files_to_upload=self.paths_to_upload,
289 debug=None,
290 compress=object(),
291 progress=object(),
294 def set_subprocess_double(self):
295 """ Set the test double for the subprocess. """
296 argv = [self.command_file_path, "-m", ARG_ANY, ARG_ANY]
297 double = SubprocessDouble(self.command_file_path, argv=argv)
298 double.register_for_testcase(self)
299 double.set_subprocess_check_call_scenario(
300 self.check_call_scenario_name)
301 self.subprocess_double = double
303 def test_calls_check_call_with_install_command(self):
304 """ Should call `subprocess.check_call` to invoke ‘install’. """
305 dput.methods.local.upload(**self.test_args)
306 command_args = self.get_command_args_from_latest_check_call()
307 expected_command = self.command_file_path
308 self.assertEqual(expected_command, command_args[0])
310 def test_calls_check_call_with_mode_option_in_command(self):
311 """ Should call `subprocess.check_call`, invoke command with mode. """
312 dput.methods.local.upload(**self.test_args)
313 command_args = self.get_command_args_from_latest_check_call()
314 expected_mode = (
315 stat.S_IRUSR | stat.S_IWUSR
316 | stat.S_IRGRP
317 | stat.S_IROTH)
318 expected_mode_text = "{mode:04o}".format(mode=expected_mode)[-3:]
319 expected_option_args = ["-m", expected_mode_text]
320 self.assertEqual(expected_option_args, command_args[1:3])
322 def test_calls_check_call_with_file_paths_in_command(self):
323 """ Should call `subprocess.check_call` with file paths in command. """
324 dput.methods.local.upload(**self.test_args)
325 command_args = self.get_command_args_from_latest_check_call()
326 self.assertEqual(self.paths_to_upload, command_args[3:-1])
328 def test_calls_check_call_with_incoming_path_as_final_arg(self):
329 """ Should call `subprocess.check_call` with incoming path. """
330 dput.methods.local.upload(**self.test_args)
331 command_args = self.get_command_args_from_latest_check_call()
332 self.assertEqual(self.incoming_path, command_args[-1])
334 def test_emits_debug_message_for_upload_command(self):
335 """ Should emit a debug message for the upload command. """
336 self.test_args['debug'] = True
337 dput.methods.local.upload(**self.test_args)
338 expected_message = textwrap.dedent("""\
339 D: Uploading with cp to {dir_path}
340 D: ...
341 """).format(dir_path=self.incoming_path)
342 self.assertThat(
343 sys.stdout.getvalue(),
344 testtools.matchers.DocTestMatches(
345 expected_message, flags=doctest.ELLIPSIS))
347 def test_calls_sys_exit_if_check_call_returns_nonzero(self):
348 """ Should call `sys.exit` if `subprocess.check_call` fails. """
349 self.subprocess_double.set_subprocess_check_call_scenario('failure')
350 with testtools.ExpectedException(FakeSystemExit):
351 dput.methods.local.upload(**self.test_args)
352 expected_output = textwrap.dedent("""\
353 Error while uploading.
354 """)
355 self.assertIn(expected_output, sys.stdout.getvalue())
356 sys.exit.assert_called_with(EXIT_STATUS_FAILURE)
359 class ftp_upload_TestCase(upload_TestCase):
360 """ Test cases for `methods.ftp.upload` function. """
362 scenarios = NotImplemented
364 def setUp(self):
365 """ Set up test fixtures. """
366 super(ftp_upload_TestCase, self).setUp()
368 self.set_ftp_client()
369 self.patch_ftplib_ftp()
371 patch_getpass_getpass(self)
372 self.fake_password = self.getUniqueString()
373 getpass.getpass.return_value = self.fake_password
374 if not hasattr(self, 'expected_password'):
375 self.expected_password = self.fake_password
377 def set_test_args(self):
378 """ Set the arguments for the test call to the function. """
379 if not hasattr(self, 'progress_type'):
380 self.progress_type = 0
381 self.test_args = dict(
382 fqdn=self.getUniqueString(),
383 login=self.login,
384 incoming=self.incoming_path,
385 files_to_upload=self.paths_to_upload,
386 debug=False,
387 ftp_mode=self.ftp_mode,
388 progress=self.progress_type,
389 port=object(),
392 def set_ftp_client(self):
393 """ Set the FTP client double. """
394 self.ftp_client = mock.MagicMock(name="FTP")
396 def patch_ftplib_ftp(self):
397 """ Patch `ftplib.FTP` class for this test case. """
398 patcher = mock.patch.object(ftplib, "FTP", autospec=True)
399 patcher.start()
400 self.addCleanup(patcher.stop)
402 ftplib.FTP.return_value = self.ftp_client
405 class ftp_upload_NormalFilesTestCase(ftp_upload_TestCase):
406 """ Test cases for `methods.ftp.upload` function, upload normal files. """
408 login_scenarios = [
409 ('anonymous', {
410 'login': "anonymous",
411 'expected_password': "dput@packages.debian.org",
413 ('username', {
414 'login': "lorem",
418 ftp_client_scenarios = [
419 ('default', {
420 'ftp_mode': False,
422 ('passive-mode', {
423 'ftp_mode': True,
427 scenarios = testscenarios.multiply_scenarios(
428 upload_TestCase.incoming_scenarios,
429 upload_TestCase.files_scenarios,
430 login_scenarios, ftp_client_scenarios)
432 def test_emits_debug_message_for_connect(self):
433 """ Should emit debug message for successful connect. """
434 self.test_args['debug'] = True
435 dput.methods.ftp.upload(**self.test_args)
436 expected_fqdn = self.test_args['fqdn']
437 expected_output = textwrap.dedent("""\
438 D: FTP-Connection to host: {fqdn}
439 """).format(fqdn=expected_fqdn)
440 self.assertIn(expected_output, sys.stdout.getvalue())
442 def test_calls_ftp_connect_with_expected_args(self):
443 """ Should call `FTP.connect` with expected args. """
444 dput.methods.ftp.upload(**self.test_args)
445 expected_args = (
446 self.test_args['fqdn'],
447 self.test_args['port'],
449 self.ftp_client.connect.assert_called_with(*expected_args)
451 def test_emits_error_message_when_ftp_connect_error(self):
452 """ Should emit error message when `FTP.connect` raises error. """
453 self.ftp_client.connect.side_effect = ftplib.error_temp
454 try:
455 dput.methods.ftp.upload(**self.test_args)
456 except FakeSystemExit:
457 pass
458 expected_output = "Connection failed, aborting"
459 self.assertIn(expected_output, sys.stdout.getvalue())
461 def test_calls_sys_exit_when_ftp_connect_permission_error(self):
462 """ Should call `sys.exit` when `FTP.connect` raises error. """
463 self.ftp_client.connect.side_effect = ftplib.error_temp
464 with testtools.ExpectedException(FakeSystemExit):
465 dput.methods.ftp.upload(**self.test_args)
466 sys.exit.assert_called_with(EXIT_STATUS_FAILURE)
468 def test_calls_ftp_login_with_expected_args(self):
469 """ Should call `FTP.login` with expected args. """
470 dput.methods.ftp.upload(**self.test_args)
471 expected_args = (
472 self.test_args['login'],
473 self.expected_password,
475 self.ftp_client.login.assert_called_with(*expected_args)
477 def test_emits_error_message_when_ftp_login_permission_error(self):
478 """ Should emit error message when `FTP.login` permission error. """
479 self.ftp_client.login.side_effect = ftplib.error_perm
480 try:
481 dput.methods.ftp.upload(**self.test_args)
482 except FakeSystemExit:
483 pass
484 expected_output = "Wrong Password"
485 self.assertIn(expected_output, sys.stdout.getvalue())
487 def test_calls_sys_exit_when_ftp_login_permission_error(self):
488 """ Should call `sys.exit` when `FTP.login` permission error. """
489 self.ftp_client.login.side_effect = ftplib.error_perm
490 with testtools.ExpectedException(FakeSystemExit):
491 dput.methods.ftp.upload(**self.test_args)
492 sys.exit.assert_called_with(EXIT_STATUS_FAILURE)
494 def test_emits_error_message_when_ftp_login_eof_error(self):
495 """ Should emit error message when `FTP.login` EOF error. """
496 self.ftp_client.login.side_effect = EOFError
497 try:
498 dput.methods.ftp.upload(**self.test_args)
499 except FakeSystemExit:
500 pass
501 expected_output = "Server closed the connection"
502 self.assertIn(expected_output, sys.stdout.getvalue())
504 def test_calls_sys_exit_when_ftp_login_eof_error(self):
505 """ Should call `sys.exit` when `FTP.login` EOF error. """
506 self.ftp_client.login.side_effect = EOFError
507 with testtools.ExpectedException(FakeSystemExit):
508 dput.methods.ftp.upload(**self.test_args)
509 sys.exit.assert_called_with(EXIT_STATUS_FAILURE)
511 def test_calls_ftp_set_pasv_with_expected_args(self):
512 """ Should call `FTP.set_pasv` with expected args. """
513 dput.methods.ftp.upload(**self.test_args)
514 expected_mode = bool(self.test_args['ftp_mode'])
515 expected_args = (expected_mode,)
516 self.ftp_client.set_pasv.assert_called_with(*expected_args)
518 def test_calls_ftp_cwd_with_expected_args(self):
519 """ Should call `FTP.cwd` with expected args. """
520 dput.methods.ftp.upload(**self.test_args)
521 expected_path = self.incoming_path
522 expected_args = (expected_path,)
523 self.ftp_client.cwd.assert_called_with(*expected_args)
525 def test_emits_debug_message_for_cwd(self):
526 """ Should emit debug message for successful `FTP.cwd`. """
527 self.test_args['debug'] = True
528 dput.methods.ftp.upload(**self.test_args)
529 expected_output = textwrap.dedent("""\
530 D: Directory to upload to: {path}
531 """).format(path=self.incoming_path)
532 self.assertIn(expected_output, sys.stdout.getvalue())
534 def test_emits_error_message_when_destination_directory_not_found(self):
535 """ Should emit error message when destination directory not found. """
536 error = ftplib.error_perm("550 Not Found")
537 self.ftp_client.cwd.side_effect = error
538 try:
539 dput.methods.ftp.upload(**self.test_args)
540 except FakeSystemExit:
541 pass
542 expected_output = "Directory to upload to does not exist."
543 self.assertIn(expected_output, sys.stdout.getvalue())
545 def test_calls_sys_exit_when_ftp_cwd_permission_error(self):
546 """ Should call `sys.exit` when `FTP.cwd` permission error. """
547 error = ftplib.error_perm("550 Not Found")
548 self.ftp_client.cwd.side_effect = error
549 with testtools.ExpectedException(FakeSystemExit):
550 dput.methods.ftp.upload(**self.test_args)
551 sys.exit.assert_called_with(EXIT_STATUS_FAILURE)
553 def test_propagates_exception_when_ftp_cwd_permission_error(self):
554 """ Should call `sys.exit` when `FTP.cwd` permission error. """
555 error = ftplib.error_perm("500 Bad Stuff Happened")
556 self.ftp_client.cwd.side_effect = error
557 with testtools.ExpectedException(error.__class__):
558 dput.methods.ftp.upload(**self.test_args)
560 def test_propagates_exception_when_ftp_cwd_eof_error(self):
561 """ Should call `sys.exit` when `FTP.cwd` EOF error. """
562 error = EOFError()
563 self.ftp_client.cwd.side_effect = error
564 with testtools.ExpectedException(error.__class__):
565 dput.methods.ftp.upload(**self.test_args)
567 def test_emits_debug_message_for_each_file(self):
568 """ Should emit debug message for each file to upload. """
569 self.test_args['debug'] = True
570 dput.methods.ftp.upload(**self.test_args)
571 expected_output = "".join(textwrap.dedent("""\
572 D: Uploading File: {path}
573 Uploading {filename}: done.
574 """).format(path=path, filename=os.path.basename(path))
575 for path in self.paths_to_upload)
576 self.assertIn(expected_output, sys.stdout.getvalue())
578 def test_calls_ftp_storbinary_for_each_file(self):
579 """ Should call `FTP.storbinary` for each file to upload. """
580 dput.methods.ftp.upload(**self.test_args)
581 registry = FileDouble.get_registry_for_testcase(self)
582 expected_blocksize = 1024
583 expected_calls = [
584 mock.call(
585 "STOR {filename}".format(filename=os.path.basename(path)),
586 registry[path].fake_file, expected_blocksize)
587 for path in self.paths_to_upload]
588 self.ftp_client.storbinary.assert_has_calls(
589 expected_calls, any_order=True)
591 def test_calls_close_for_each_file(self):
592 """ Should call `file.close` for each file to upload. """
593 dput.methods.ftp.upload(**self.test_args)
594 registry = FileDouble.get_registry_for_testcase(self)
595 for path in self.paths_to_upload:
596 fake_file = registry[path].fake_file
597 fake_file.close.assert_called_with()
600 class ftp_upload_ErrorTestCase(ftp_upload_TestCase):
601 """ Test cases for `methods.ftp.upload` function, error conditions. """
603 login_scenarios = [
604 ('anonymous', {
605 'login': "anonymous",
606 'expected_password': "dput@packages.debian.org",
610 ftp_client_scenarios = [
611 ('default', {
612 'ftp_mode': False,
616 progress_scenarios = [
617 ('progress-type-0', {
618 'progress_type': 0,
620 ('progress-type-1', {
621 'progress_type': 1,
623 ('progress-type-2', {
624 'progress_type': 2,
628 files_scenarios = list(
629 (scenario_name, scenario) for (scenario_name, scenario)
630 in upload_TestCase.files_scenarios
631 if scenario['paths_to_upload'])
633 scenarios = testscenarios.multiply_scenarios(
634 upload_TestCase.incoming_scenarios,
635 files_scenarios,
636 login_scenarios, ftp_client_scenarios, progress_scenarios)
638 def test_emits_warning_when_remote_file_exists(self):
639 """ Should emit a warning message when remote file exists. """
640 error = ftplib.error_perm("553 Exists")
641 self.ftp_client.storbinary.side_effect = error
642 dput.methods.ftp.upload(**self.test_args)
643 for path in self.paths_to_upload:
644 expected_output = textwrap.dedent("""\
645 Leaving existing {path} on the server and continuing
646 """).format(path=os.path.basename(path))
647 self.expectThat(
648 sys.stdout.getvalue(),
649 testtools.matchers.Contains(expected_output))
651 def test_omits_sys_exit_when_remote_file_exists(self):
652 """ Should omit call to `sys.exit` when remote file exists. """
653 error = ftplib.error_perm("553 Exists")
654 self.ftp_client.storbinary.side_effect = error
655 dput.methods.ftp.upload(**self.test_args)
656 self.assertFalse(sys.exit.called)
658 def test_emits_error_message_when_storbinary_failure(self):
659 """ Should emit an error message when `FTP.storbinary` failure. """
660 error = ftplib.error_perm("504 Weird Stuff Happened")
661 self.ftp_client.storbinary.side_effect = error
662 try:
663 dput.methods.ftp.upload(**self.test_args)
664 except FakeSystemExit:
665 pass
666 for path in self.paths_to_upload[:1]:
667 expected_output = (
668 "Note: This error might indicate a problem with"
669 " your passive_ftp setting.\n")
670 self.expectThat(
671 sys.stdout.getvalue(),
672 testtools.matchers.Contains(expected_output))
674 def test_calls_sys_exit_when_storbinary_failure(self):
675 """ Should call `sys.exit` when `FTP.storbinary` failure. """
676 error = ftplib.error_perm("504 Weird Stuff Happened")
677 self.ftp_client.storbinary.side_effect = error
678 with testtools.ExpectedException(FakeSystemExit):
679 dput.methods.ftp.upload(**self.test_args)
680 sys.exit.assert_called_with(EXIT_STATUS_FAILURE)
682 def test_emits_debug_message_when_open_failure(self):
683 """ Should emit a debug message when `builtins.open` failure. """
684 self.test_args['debug'] = True
685 registry = FileDouble.get_registry_for_testcase(self)
686 for path in self.paths_to_upload:
687 double = registry[path]
688 double.set_open_scenario('nonexist')
689 try:
690 dput.methods.ftp.upload(**self.test_args)
691 except EnvironmentError:
692 pass
693 expected_output = (
694 "D: Should exit silently now, but"
695 " will throw exception for debug.")
696 self.assertIn(expected_output, sys.stdout.getvalue())
698 def test_propagates_error_from_storbinary_for_debug(self):
699 """ Should propagate error from `FTP.storbinary` when debug. """
700 self.test_args['debug'] = True
701 error = ftplib.error_perm("504 Weird Stuff Happened")
702 self.ftp_client.storbinary.side_effect = error
703 with testtools.ExpectedException(error.__class__):
704 dput.methods.ftp.upload(**self.test_args)
706 def test_propagates_error_from_quit_for_debug(self):
707 """ Should propagate error from `FTP.quit` when debug. """
708 self.test_args['debug'] = True
709 error = ftplib.error_perm("504 Weird Stuff Happened")
710 self.ftp_client.quit.side_effect = error
711 with testtools.ExpectedException(error.__class__):
712 dput.methods.ftp.upload(**self.test_args)
715 def make_expected_filewithprogress_attributes_by_path(testcase, attrs):
716 """ Make a mapping from path to expected FileWithProgress attribs. """
717 expected_attributes_by_path = {}
718 registry = FileDouble.get_registry_for_testcase(testcase)
719 for path in testcase.paths_to_upload:
720 file_double = registry[path]
721 expected_attributes = {
722 'f': file_double.fake_file,
723 'size': file_double.stat_result.st_size,
725 expected_attributes.update(attrs)
726 expected_attributes_by_path[path] = expected_attributes
728 return expected_attributes_by_path
731 class ftp_upload_ProgressTestCase(ftp_upload_TestCase):
732 """ Test cases for `methods.ftp.upload` function, with progress meter. """
734 login_scenarios = [
735 ('anonymous', {
736 'login': "anonymous",
737 'expected_password': "dput@packages.debian.org",
741 ftp_client_scenarios = [
742 ('default', {
743 'ftp_mode': False,
747 progress_scenarios = [
748 ('progress-type-1', {
749 'progress_type': 1,
751 ('progress-type-2', {
752 'progress_type': 2,
756 scenarios = testscenarios.multiply_scenarios(
757 upload_TestCase.incoming_scenarios,
758 upload_TestCase.files_scenarios,
759 login_scenarios, ftp_client_scenarios, progress_scenarios)
761 def test_calls_storbinary_with_filewithprogress(self):
762 """ Should use a `FileWithProgress` to call `FTP.storbinary`. """
763 dput.methods.ftp.upload(**self.test_args)
764 expected_calls = [
765 mock.call(
766 mock.ANY, self.fake_filewithprogress,
767 mock.ANY)
768 for path in self.paths_to_upload]
769 self.ftp_client.storbinary.assert_has_calls(
770 expected_calls, any_order=True)
772 def test_filewithprogress_has_expected_attributes(self):
773 """ Should have expected attributes on the `FileWithProgress`. """
774 expected_attributes_by_path = (
775 make_expected_filewithprogress_attributes_by_path(
776 self, {'ptype': self.progress_type}))
777 dput.methods.ftp.upload(**self.test_args)
778 for call in self.ftp_client.storbinary.mock_calls:
779 (__, call_args, call_kwargs) = call
780 (__, stor_file, __) = call_args
781 path = stor_file.f.name
782 expected_attributes = expected_attributes_by_path[path]
783 stor_file_attributes = {
784 name: getattr(stor_file, name)
785 for name in expected_attributes}
786 self.expectThat(
787 expected_attributes,
788 testtools.matchers.Equals(stor_file_attributes))
790 def test_filewithprogress_has_sentinel_size_when_stat_failure(self):
791 """ Should have sentinel `size` value when `os.stat` failure. """
792 expected_attributes_by_path = (
793 make_expected_filewithprogress_attributes_by_path(
794 self, {'size': -1}))
795 registry = FileDouble.get_registry_for_testcase(self)
796 for path in self.paths_to_upload:
797 double = registry[path]
798 double.set_os_stat_scenario('notfound_error')
799 dput.methods.ftp.upload(**self.test_args)
800 for call in self.ftp_client.storbinary.mock_calls:
801 (__, call_args, call_kwargs) = call
802 (__, stor_file, __) = call_args
803 path = stor_file.f.name
804 expected_attributes = expected_attributes_by_path[path]
805 stor_file_attributes = {
806 name: getattr(stor_file, name)
807 for name in expected_attributes}
808 self.expectThat(
809 expected_attributes,
810 testtools.matchers.Equals(stor_file_attributes))
812 def test_emits_debug_message_when_stat_failure(self):
813 """ Should have sentinel `size` value when `os.stat` failure. """
814 self.test_args['debug'] = True
815 registry = FileDouble.get_registry_for_testcase(self)
816 for path in self.paths_to_upload:
817 double = registry[path]
818 double.set_os_stat_scenario('notfound_error')
819 dput.methods.ftp.upload(**self.test_args)
820 for path in self.paths_to_upload:
821 expected_output = textwrap.dedent("""\
822 D: Determining size of file '{path}' failed
823 """).format(path=path)
824 self.expectThat(
825 sys.stdout.getvalue(),
826 testtools.matchers.Contains(expected_output))
829 class http_upload_TestCase(upload_TestCase):
830 """ Base for test cases for `methods.http.upload` function. """
832 scenarios = NotImplemented
834 protocol_scenarios = [
835 ('http', {
836 'function_to_test': dput.methods.http.upload,
837 'protocol': "http",
838 'protocol_version': "HTTP/1.0",
840 ('https', {
841 'function_to_test': dput.methods.https.upload,
842 'protocol': "https",
843 'protocol_version': "HTTP/1.0",
847 login_scenarios = [
848 ('username', {
849 'login': "lorem",
853 def setUp(self):
854 """ Set up test fixtures. """
855 super(http_upload_TestCase, self).setUp()
857 httpretty.enable()
858 self.addCleanup(httpretty.disable)
860 self.set_response_header_fields()
861 self.patch_put_requests()
863 patch_getpass_getpass(self)
864 self.fake_password = self.getUniqueString()
865 getpass.getpass.return_value = self.fake_password
866 if not hasattr(self, 'expected_password'):
867 self.expected_password = self.fake_password
869 def set_test_args(self):
870 """ Set the arguments for the test call to the function. """
871 if not hasattr(self, 'progress_type'):
872 self.progress_type = 0
873 self.test_args = dict(
874 fqdn=self.getUniqueString(),
875 login=self.login,
876 incoming=self.incoming_path,
877 files_to_upload=self.paths_to_upload,
878 debug=False,
879 dummy=object(),
880 progress=self.progress_type,
882 if self.function_to_test is dput.methods.http.upload:
883 self.test_args['protocol'] = self.protocol
885 def make_upload_uri(self, file_name):
886 """ Make the URI for a file for upload. """
887 uri = urlparse.urlunsplit([
888 self.protocol, self.test_args['fqdn'],
889 os.path.join(os.path.sep, self.incoming_path, file_name),
890 None, None])
891 return uri
893 def set_response_header_fields(self):
894 """ Set the header fields for the HTTP response. """
895 if not hasattr(self, 'response_header_fields'):
896 self.response_header_fields = {}
898 def patch_put_requests(self):
899 """ Patch the HTTP PUT requests. """
900 self.path_by_request_uri = {}
901 for path in self.paths_to_upload:
902 upload_uri = self.make_upload_uri(os.path.basename(path))
903 self.path_by_request_uri[upload_uri] = path
904 response_body = ""
905 httpretty.register_uri(
906 httpretty.PUT, upload_uri,
907 status=self.status_code, body=response_body)
910 class http_upload_SuccessTestCase(http_upload_TestCase):
911 """ Success test cases for `methods.http.upload` function. """
913 response_scenarios = [
914 ('okay', {
915 'status_code': 200,
916 'status_reason': "Okay",
918 ('chatter', {
919 'status_code': 203,
920 'status_reason': "Non-Authoritative Information",
924 auth_scenarios = [
925 ('auth-accepted', {
926 'auth_response_status_code': 200,
927 'auth_response_status_reason': "Okay",
931 size_scenarios = [
932 ('size-empty', {
933 'fake_file': io.BytesIO(),
935 ('size-1k', {
936 'fake_file': io.BytesIO(
937 b"Lorem ipsum, dolor sit amet.___\n" * 32),
939 ('size-100k', {
940 'fake_file': io.BytesIO(
941 b"Lorem ipsum, dolor sit amet.___\n" * 3200),
945 incoming_scenarios = list(upload_TestCase.incoming_scenarios)
946 for (scenario_name, scenario) in incoming_scenarios:
947 scenario['expected_url_path_prefix'] = os.path.join(
948 os.path.sep, scenario['incoming_path'])
949 del scenario_name, scenario
951 scenarios = testscenarios.multiply_scenarios(
952 upload_TestCase.files_scenarios,
953 size_scenarios,
954 upload_TestCase.incoming_scenarios,
955 http_upload_TestCase.protocol_scenarios,
956 http_upload_TestCase.login_scenarios,
957 response_scenarios, auth_scenarios)
959 def test_emits_debug_message_for_upload(self):
960 """ Should emit debug message for upload. """
961 self.test_args['debug'] = True
962 self.function_to_test(**self.test_args)
963 for path in self.paths_to_upload:
964 expected_uri = self.make_upload_uri(os.path.basename(path))
965 expected_output = textwrap.dedent("""\
966 D: HTTP-PUT to URL: {uri}
967 """).format(uri=expected_uri)
968 self.expectThat(
969 sys.stdout.getvalue(),
970 testtools.matchers.Contains(expected_output))
972 def test_request_has_expected_fields(self):
973 """ Should send request with expected fields in header. """
974 if not self.paths_to_upload:
975 self.skipTest("No files to upload")
976 self.function_to_test(**self.test_args)
977 registry = FileDouble.get_registry_for_testcase(self)
978 path = self.paths_to_upload[-1]
979 double = registry[path]
980 request = httpretty.last_request()
981 expected_fields = {
982 'User-Agent': "dput",
983 'Connection': "close",
984 'Content-Length': "{size:d}".format(
985 size=len(double.fake_file.getvalue())),
987 for (name, value) in expected_fields.items():
988 self.expectThat(
989 request.headers.get(name),
990 testtools.matchers.Equals(value))
993 class http_upload_ProgressTestCase(http_upload_TestCase):
994 """ Test cases for `methods.http.upload` function, with progress meter. """
996 files_scenarios = list(
997 (scenario_name, scenario) for (scenario_name, scenario)
998 in upload_TestCase.files_scenarios
999 if scenario['paths_to_upload'])
1001 response_scenarios = [
1002 ('okay', {
1003 'status_code': 200,
1004 'status_reason': "Okay",
1008 progress_scenarios = [
1009 ('progress-type-1', {
1010 'progress_type': 1,
1012 ('progress-type-2', {
1013 'progress_type': 2,
1017 scenarios = testscenarios.multiply_scenarios(
1018 files_scenarios,
1019 progress_scenarios,
1020 upload_TestCase.incoming_scenarios,
1021 http_upload_TestCase.protocol_scenarios,
1022 http_upload_TestCase.login_scenarios,
1023 http_upload_SuccessTestCase.auth_scenarios,
1024 response_scenarios)
1026 def test_filewithprogress_has_expected_attributes(self):
1027 """ Should have expected attributes on the `FileWithProgress`. """
1028 expected_attributes_by_path = (
1029 make_expected_filewithprogress_attributes_by_path(
1030 self, {'ptype': self.progress_type}))
1031 self.function_to_test(**self.test_args)
1032 path = self.paths_to_upload[-1]
1033 expected_attributes = expected_attributes_by_path[path]
1034 fake_file_attributes = {
1035 name: getattr(self.fake_filewithprogress, name)
1036 for name in expected_attributes}
1037 self.expectThat(
1038 expected_attributes,
1039 testtools.matchers.Equals(fake_file_attributes))
1042 class http_upload_UnknownProtocolTestCase(http_upload_TestCase):
1043 """ Test cases for `methods.http.upload` function, unknown protocol. """
1045 files_scenarios = list(
1046 (scenario_name, scenario) for (scenario_name, scenario)
1047 in upload_TestCase.files_scenarios
1048 if scenario['paths_to_upload'])
1050 protocol_scenarios = [
1051 ('protocol-bogus', {
1052 'function_to_test': dput.methods.http.upload,
1053 'protocol': "b0gUs",
1054 'protocol_version': "b0gUs",
1055 'expected_exit_status': EXIT_STATUS_FAILURE,
1059 response_scenarios = [
1060 (scenario_name, scenario) for (scenario_name, scenario)
1061 in http_upload_SuccessTestCase.response_scenarios
1062 if scenario['status_code'] == 200]
1064 scenarios = testscenarios.multiply_scenarios(
1065 files_scenarios,
1066 upload_TestCase.incoming_scenarios,
1067 protocol_scenarios,
1068 http_upload_TestCase.login_scenarios,
1069 response_scenarios,
1070 http_upload_SuccessTestCase.auth_scenarios)
1072 def test_emits_error_message_when_unknown_protocol(self):
1073 """ Should emit error message when unknown protocol. """
1074 try:
1075 self.function_to_test(**self.test_args)
1076 except FakeSystemExit:
1077 pass
1078 expected_output = "Wrong protocol for upload "
1079 self.assertIn(expected_output, sys.stderr.getvalue())
1081 def test_calls_sys_exit_when_unknown_protocol(self):
1082 """ Should call `sys.exit` when unknown protocol. """
1083 with testtools.ExpectedException(FakeSystemExit):
1084 self.function_to_test(**self.test_args)
1085 sys.exit.assert_called_with(self.expected_exit_status)
1088 class http_upload_FileStatFailureTestCase(http_upload_TestCase):
1089 """ Test cases for `methods.http.upload` function, `os.stat` failure. """
1091 files_scenarios = list(
1092 (scenario_name, scenario) for (scenario_name, scenario)
1093 in upload_TestCase.files_scenarios
1094 if scenario['paths_to_upload'])
1096 os_stat_scenarios = [
1097 ('os-stat-notfound', {
1098 'os_stat_scenario_name': "notfound_error",
1099 'expected_exit_status': EXIT_STATUS_FAILURE,
1101 ('os-stat-denied', {
1102 'os_stat_scenario_name': "denied_error",
1103 'expected_exit_status': EXIT_STATUS_FAILURE,
1107 response_scenarios = list(
1108 (scenario_name, scenario) for (scenario_name, scenario)
1109 in http_upload_SuccessTestCase.response_scenarios
1110 if scenario['status_code'] == 200)
1112 scenarios = testscenarios.multiply_scenarios(
1113 files_scenarios,
1114 os_stat_scenarios,
1115 upload_TestCase.incoming_scenarios,
1116 http_upload_TestCase.protocol_scenarios,
1117 http_upload_TestCase.login_scenarios,
1118 response_scenarios,
1119 http_upload_SuccessTestCase.auth_scenarios)
1121 def test_emits_error_message(self):
1122 """ Should emit error message when `os.stat` failure. """
1123 try:
1124 self.function_to_test(**self.test_args)
1125 except FakeSystemExit:
1126 pass
1127 expected_output = textwrap.dedent("""\
1128 Determining size of file '{path}' failed
1129 """).format(path=self.paths_to_upload[0])
1130 self.assertIn(expected_output, sys.stderr.getvalue())
1132 def test_calls_sys_exit_with_expected_exit_status(self):
1133 """ Should call `sys.exit` with expected exit status. """
1134 with testtools.ExpectedException(FakeSystemExit):
1135 self.function_to_test(**self.test_args)
1136 sys.exit.assert_called_with(self.expected_exit_status)
1139 class http_upload_ResponseErrorTestCase(http_upload_TestCase):
1140 """ Error test cases for `methods.http.upload` function. """
1142 files_scenarios = list(
1143 (scenario_name, scenario) for (scenario_name, scenario)
1144 in upload_TestCase.files_scenarios
1145 if scenario['paths_to_upload'])
1147 response_scenarios = [
1148 ('server-error', {
1149 'status_code': 500,
1150 'status_reason': "Internal Server Error",
1151 'auth_response_status_code': 200,
1152 'auth_response_status_reason': "Okay",
1153 'expected_exit_status': EXIT_STATUS_FAILURE,
1157 scenarios = testscenarios.multiply_scenarios(
1158 files_scenarios,
1159 upload_TestCase.incoming_scenarios,
1160 http_upload_TestCase.protocol_scenarios,
1161 http_upload_TestCase.login_scenarios,
1162 response_scenarios)
1164 def test_emits_error_message_when_response_status_error(self):
1165 """ Should emit debug message when response status is error. """
1166 try:
1167 self.function_to_test(**self.test_args)
1168 except FakeSystemExit:
1169 pass
1170 expected_output = textwrap.dedent("""\
1171 Upload failed: {status} {reason}
1172 """).format(status=self.status_code, reason=self.status_reason)
1173 self.assertIn(expected_output, sys.stdout.getvalue())
1175 def test_calls_sys_exit_when_response_status_error(self):
1176 """ Should call `sys.exit` when response status is error. """
1177 with testtools.ExpectedException(FakeSystemExit):
1178 self.function_to_test(**self.test_args)
1179 sys.exit.assert_called_with(self.expected_exit_status)
1182 def make_host_spec(username, host):
1183 """ Make an SSH host specification. """
1184 host_spec = host
1185 if username != "*":
1186 host_spec = "{user}@{fqdn}".format(user=username, fqdn=host)
1187 return host_spec
1190 def make_remote_spec(username, host, dir_path):
1191 """ Make an SCP remote specification. """
1192 host_spec = make_host_spec(username, host)
1193 remote_spec = "{host}:{dir}".format(host=host_spec, dir=dir_path)
1194 return remote_spec
1197 class ssh_channel_upload_TestCase(upload_TestCase):
1198 """ Base for test cases for upload over SSH channel. """
1200 function_to_test = NotImplemented
1202 scenarios = NotImplemented
1204 login_scenarios = [
1205 ('login-username', {
1206 'login': "lorem",
1208 ('login-wildcard', {
1209 'login': "*",
1213 stat_mode_scenarios = [
1214 ('stat-mode-default', {}),
1215 ('stat-mode-0620', {
1216 'stat_mode': 0o0620,
1217 'expected_ssh_chmod': True,
1221 def set_upload_file_modes(self):
1222 """ Set filesystem modes for upload files. """
1223 registry = FileDouble.get_registry_for_testcase(self)
1224 if hasattr(self, 'stat_mode'):
1225 for path in self.paths_to_upload:
1226 file_double = registry[path]
1227 file_double.stat_result = file_double.stat_result._replace(
1228 st_mode=self.stat_mode)
1230 def set_ssh_chmod_subprocess_double(self):
1231 """ Set the ‘ssh … chmod’ test double for the subprocess. """
1232 command_file_path = "/usr/bin/ssh"
1233 argv = [os.path.basename(command_file_path)]
1234 argv.extend(self.expected_ssh_options)
1235 argv.append(make_host_spec(
1236 username=self.login, host=self.test_args['fqdn']))
1237 argv.extend(["chmod", "0644"])
1238 argv.extend(
1239 os.path.join(self.incoming_path, os.path.basename(path))
1240 for path in self.paths_to_upload)
1241 double = SubprocessDouble(command_file_path, argv=argv)
1242 double.register_for_testcase(self)
1243 check_call_scenario_name = getattr(
1244 self, 'ssh_chmod_check_call_scenario_name', "success")
1245 double.set_subprocess_check_call_scenario(check_call_scenario_name)
1246 self.ssh_chmod_subprocess_double = double
1249 class scp_upload_TestCase(ssh_channel_upload_TestCase):
1250 """ Test cases for `methods.scp.upload` function. """
1252 function_to_test = staticmethod(dput.methods.scp.upload)
1254 scenarios = NotImplemented
1256 ssh_config_scenarios = [
1257 ('ssh-opts-none', {
1258 'ssh_config_options': [],
1259 'expected_ssh_options': [],
1261 ('ssh-opts-one', {
1262 'ssh_config_options': ["foo"],
1263 'expected_ssh_options': ["-o", "foo"],
1265 ('ssh-opts-three', {
1266 'ssh_config_options': ["foo", "bar", "baz"],
1267 'expected_ssh_options': [
1268 "-o", "foo", "-o", "bar", "-o", "baz"],
1272 def setUp(self):
1273 """ Set up test fixtures. """
1274 super(scp_upload_TestCase, self).setUp()
1276 patch_os_lstat(self)
1277 self.set_upload_file_modes()
1279 self.set_scp_subprocess_double()
1280 self.set_ssh_chmod_subprocess_double()
1282 def set_test_args(self):
1283 """ Set the arguments for the test call to the function. """
1284 self.test_args = dict(
1285 fqdn=self.getUniqueString(),
1286 login=self.login,
1287 incoming=self.incoming_path,
1288 files_to_upload=self.paths_to_upload,
1289 debug=None,
1290 compress=self.compress,
1291 ssh_config_options=self.ssh_config_options,
1292 progress=object(),
1295 def set_scp_subprocess_double(self):
1296 """ Set the ‘scp’ test double for the subprocess. """
1297 command_file_path = "/usr/bin/scp"
1298 argv = [os.path.basename(command_file_path), "-p"]
1299 argv.extend(self.scp_compress_options)
1300 argv.extend(self.expected_ssh_options)
1301 argv.extend(self.paths_to_upload)
1302 argv.append(make_remote_spec(
1303 username=self.login, host=self.test_args['fqdn'],
1304 dir_path=self.incoming_path))
1305 double = SubprocessDouble(command_file_path, argv=argv)
1306 double.register_for_testcase(self)
1307 check_call_scenario_name = getattr(
1308 self, 'scp_subprocess_check_call_scenario_name', "success")
1309 double.set_subprocess_check_call_scenario(check_call_scenario_name)
1310 self.scp_subprocess_double = double
1313 class scp_upload_ScpTestCase(scp_upload_TestCase):
1314 """ Test cases for `methods.scp.upload` function, with ‘scp’ command. """
1316 compress_scenarios = [
1317 ('compress-false', {
1318 'compress': False,
1319 'scp_compress_options': [],
1321 ('compress-true', {
1322 'compress': True,
1323 'scp_compress_options': ["-C"],
1327 scenarios = testscenarios.multiply_scenarios(
1328 upload_TestCase.files_scenarios,
1329 upload_TestCase.incoming_scenarios,
1330 ssh_channel_upload_TestCase.login_scenarios,
1331 ssh_channel_upload_TestCase.stat_mode_scenarios,
1332 compress_scenarios,
1333 scp_upload_TestCase.ssh_config_scenarios)
1335 def test_emits_debug_message_for_upload(self):
1336 """ Should emit debug message for files upload. """
1337 self.test_args['debug'] = True
1338 self.function_to_test(**self.test_args)
1339 expected_output = textwrap.dedent("""\
1340 D: Uploading with scp to {host}:{incoming}
1341 """).format(
1342 host=make_host_spec(
1343 username=self.login, host=self.test_args['fqdn']),
1344 incoming=self.incoming_path)
1345 self.assertIn(expected_output, sys.stdout.getvalue())
1347 def test_calls_check_call_with_expected_scp_command(self):
1348 """ Should call `subprocess.check_call` with ‘scp’ command. """
1349 self.function_to_test(**self.test_args)
1350 expected_call = mock.call(self.scp_subprocess_double.argv)
1351 self.assertIn(expected_call, subprocess.check_call.mock_calls)
1353 def test_emits_error_message_when_scp_failure(self):
1354 """ Should emit error message when ‘scp’ command fails. """
1355 double = self.scp_subprocess_double
1356 double.set_subprocess_check_call_scenario("failure")
1357 try:
1358 self.function_to_test(**self.test_args)
1359 except FakeSystemExit:
1360 pass
1361 expected_output = "Error while uploading."
1362 self.assertIn(expected_output, sys.stdout.getvalue())
1364 def test_calls_sys_exit_when_scp_failure(self):
1365 """ Should call `sys.exit` when ‘scp’ command fails. """
1366 double = self.scp_subprocess_double
1367 double.set_subprocess_check_call_scenario("failure")
1368 with testtools.ExpectedException(FakeSystemExit):
1369 self.function_to_test(**self.test_args)
1370 sys.exit.assert_called_with(EXIT_STATUS_FAILURE)
1373 class scp_upload_ChmodTestCase(scp_upload_TestCase):
1374 """ Test cases for `methods.scp.upload` function, with ‘ssh … chmod’. """
1376 files_scenarios = list(
1377 (scenario_name, scenario) for (scenario_name, scenario)
1378 in upload_TestCase.files_scenarios
1379 if scenario['paths_to_upload'])
1381 stat_mode_scenarios = list(
1382 (scenario_name, scenario) for (scenario_name, scenario)
1383 in ssh_channel_upload_TestCase.stat_mode_scenarios
1384 if 'expected_ssh_chmod' in scenario)
1386 compress_scenarios = [
1387 ('compress-false', {
1388 'compress': False,
1389 'scp_compress_options': [],
1393 scenarios = testscenarios.multiply_scenarios(
1394 files_scenarios,
1395 upload_TestCase.incoming_scenarios,
1396 ssh_channel_upload_TestCase.login_scenarios,
1397 stat_mode_scenarios,
1398 compress_scenarios,
1399 scp_upload_TestCase.ssh_config_scenarios)
1401 def test_emits_debug_message_for_fixing_permissions(self):
1402 """ Should emit debug message for fixing file permissions . """
1403 self.test_args['debug'] = True
1404 self.function_to_test(**self.test_args)
1405 expected_output = "D: Fixing some permissions"
1406 self.assertIn(expected_output, sys.stdout.getvalue())
1408 def test_calls_check_call_with_expected_ssh_chmod_command(self):
1409 """ Should call `subprocess.check_call` with ‘ssh … chmod’ command. """
1410 self.function_to_test(**self.test_args)
1411 expected_call = mock.call(self.ssh_chmod_subprocess_double.argv)
1412 self.assertIn(expected_call, subprocess.check_call.mock_calls)
1414 def test_emits_error_message_when_ssh_chmod_failure(self):
1415 """ Should emit error message when ‘ssh … chmod’ command fails. """
1416 double = self.ssh_chmod_subprocess_double
1417 double.set_subprocess_check_call_scenario("failure")
1418 try:
1419 self.function_to_test(**self.test_args)
1420 except FakeSystemExit:
1421 pass
1422 expected_output = "Error while fixing permissions."
1423 self.assertIn(expected_output, sys.stdout.getvalue())
1425 def test_calls_sys_exit_when_ssh_chmod_failure(self):
1426 """ Should call `sys.exit` when ‘ssh … chmod’ command fails. """
1427 double = self.ssh_chmod_subprocess_double
1428 double.set_subprocess_check_call_scenario("failure")
1429 with testtools.ExpectedException(FakeSystemExit):
1430 self.function_to_test(**self.test_args)
1431 sys.exit.assert_called_with(EXIT_STATUS_FAILURE)
1434 class rsync_upload_TestCase(ssh_channel_upload_TestCase):
1435 """ Test cases for `methods.rsync.upload` function. """
1437 function_to_test = staticmethod(dput.methods.rsync.upload)
1439 scenarios = testscenarios.multiply_scenarios(
1440 upload_TestCase.files_scenarios,
1441 upload_TestCase.incoming_scenarios,
1442 ssh_channel_upload_TestCase.login_scenarios,
1443 ssh_channel_upload_TestCase.stat_mode_scenarios)
1445 def setUp(self):
1446 """ Set up test fixtures. """
1447 super(rsync_upload_TestCase, self).setUp()
1449 self.set_rsync_subprocess_double()
1451 self.expected_ssh_options = []
1452 self.set_ssh_chmod_subprocess_double()
1454 def set_test_args(self):
1455 """ Set the arguments for the test call to the function. """
1456 self.test_args = dict(
1457 fqdn=self.getUniqueString(),
1458 login=self.login,
1459 incoming=self.incoming_path,
1460 files_to_upload=self.paths_to_upload,
1461 debug=False,
1462 dummy=object(),
1463 progress=object(),
1466 def set_rsync_subprocess_double(self):
1467 """ Set the ‘rsync’ test double for the subprocess. """
1468 command_file_path = "/usr/bin/rsync"
1469 argv = [os.path.basename(command_file_path)]
1470 argv.extend(self.paths_to_upload)
1471 argv.extend([
1472 "--copy-links", "--progress", "--partial",
1473 "-zave", "ssh -x"])
1474 argv.append(make_remote_spec(
1475 username=self.login, host=self.test_args['fqdn'],
1476 dir_path=self.incoming_path))
1477 double = SubprocessDouble(command_file_path, argv=argv)
1478 double.register_for_testcase(self)
1479 check_call_scenario_name = getattr(
1480 self, 'rsync_check_call_scenario_name', "success")
1481 double.set_subprocess_check_call_scenario(check_call_scenario_name)
1482 self.rsync_subprocess_double = double
1484 def test_emits_debug_message_for_upload(self):
1485 """ Should emit debug message for files upload. """
1486 self.test_args['debug'] = True
1487 self.function_to_test(**self.test_args)
1488 expected_output = textwrap.dedent("""\
1489 D: Uploading with rsync to {host}:{incoming}
1490 """).format(
1491 host=make_host_spec(
1492 username=self.login, host=self.test_args['fqdn']),
1493 incoming=self.incoming_path)
1494 self.assertIn(expected_output, sys.stdout.getvalue())
1496 def test_calls_check_call_with_expected_rsync_command(self):
1497 """ Should call `subprocess.check_call` with ‘rsync’ command. """
1498 self.function_to_test(**self.test_args)
1499 expected_call = mock.call(self.rsync_subprocess_double.argv)
1500 self.assertIn(expected_call, subprocess.check_call.mock_calls)
1502 def test_emits_error_message_when_rsync_failure(self):
1503 """ Should emit error message when ‘rsync’ command fails. """
1504 double = self.rsync_subprocess_double
1505 double.set_subprocess_check_call_scenario("failure")
1506 try:
1507 self.function_to_test(**self.test_args)
1508 except FakeSystemExit:
1509 pass
1510 expected_output = "Error while uploading."
1511 self.assertIn(expected_output, sys.stdout.getvalue())
1513 def test_calls_sys_exit_when_rsync_failure(self):
1514 """ Should call `sys.exit` when ‘rsync’ command fails. """
1515 double = self.rsync_subprocess_double
1516 double.set_subprocess_check_call_scenario("failure")
1517 with testtools.ExpectedException(FakeSystemExit):
1518 self.function_to_test(**self.test_args)
1519 sys.exit.assert_called_with(EXIT_STATUS_FAILURE)
1521 def test_emits_debug_message_for_fixing_permissions(self):
1522 """ Should emit debug message for fixing file permissions . """
1523 self.test_args['debug'] = True
1524 self.function_to_test(**self.test_args)
1525 expected_output = textwrap.dedent("""\
1526 D: Fixing file permissions with {host}
1527 """).format(
1528 host=make_host_spec(
1529 username=self.login, host=self.test_args['fqdn']))
1530 self.assertIn(expected_output, sys.stdout.getvalue())
1532 def test_calls_check_call_with_expected_ssh_chmod_command(self):
1533 """ Should call `subprocess.check_call` with ‘ssh … chmod’ command. """
1534 self.function_to_test(**self.test_args)
1535 expected_call = mock.call(list(self.ssh_chmod_subprocess_double.argv))
1536 self.assertIn(expected_call, subprocess.check_call.mock_calls)
1538 def test_emits_error_message_when_ssh_chmod_failure(self):
1539 """ Should emit error message when ‘ssh … chmod’ command fails. """
1540 double = self.ssh_chmod_subprocess_double
1541 double.set_subprocess_check_call_scenario("failure")
1542 try:
1543 self.function_to_test(**self.test_args)
1544 except FakeSystemExit:
1545 pass
1546 expected_output = "Error while fixing permission."
1547 self.assertIn(expected_output, sys.stdout.getvalue())
1549 def test_calls_sys_exit_when_ssh_chmod_failure(self):
1550 """ Should call `sys.exit` when ‘ssh … chmod’ command fails. """
1551 double = self.ssh_chmod_subprocess_double
1552 double.set_subprocess_check_call_scenario("failure")
1553 with testtools.ExpectedException(FakeSystemExit):
1554 self.function_to_test(**self.test_args)
1555 sys.exit.assert_called_with(EXIT_STATUS_FAILURE)
1558 # Local variables:
1559 # coding: utf-8
1560 # mode: python
1561 # End:
1562 # vim: fileencoding=utf-8 filetype=python :