1import fileinput
2import os
3import shutil
4import unittest
5import sys
6try:
7    from StringIO import StringIO
8except ImportError:
9    from io import StringIO
10from mock import patch, MagicMock, call
11from six import assertRaisesRegex
12from txclib.commands import _set_source_file, _set_translation, cmd_pull, \
13    cmd_init, cmd_config, cmd_status, cmd_help, UnInitializedError
14from txclib.cmdline import main
15from txclib.parsers import MAPPING, MAPPINGREMOTE, MAPPINGBULK
16
17
18class TestCommands(unittest.TestCase):
19    def test_cmd_pull_return_exception_when_dir_not_initialized(self):
20        """Test when tx is not instantiated, that proper error is thrown"""
21        with self.assertRaises(UnInitializedError):
22            cmd_pull([], None)
23
24    def test_set_source_file_when_dir_not_initialized(self):
25        with self.assertRaises(UnInitializedError):
26            _set_source_file(path_to_tx=None, resource='dummy_resource.en',
27                             lang='en', path_to_file='dummy')
28
29    def test_set_translation_when_dir_not_initialized(self):
30        with self.assertRaises(UnInitializedError):
31            _set_translation(path_to_tx=None, resource="dummy_resource.md",
32                             lang='en', path_to_file='invalid')
33
34
35class TestStatusCommand(unittest.TestCase):
36    @patch('txclib.commands.project')
37    def test_status(self, mock_p):
38        """Test status command"""
39        mock_project = MagicMock()
40        mock_project.get_chosen_resources.return_value = ['foo.bar']
41        mock_project.get_resource_files.return_value = {
42            "fr": "translations/foo.bar/fr.po",
43            "de": "translations/foo.bar/de.po"
44        }
45        mock_p.Project.return_value = mock_project
46        cmd_status([], None)
47        mock_project.get_chosen_resources.assert_called_once_with([])
48        self.assertEqual(mock_project.get_resource_files.call_count, 1)
49
50
51class TestHelpCommand(unittest.TestCase):
52    def test_help(self):
53        out = StringIO()
54        sys.stdout = out
55        cmd_help([], None)
56        output = out.getvalue().strip()
57        self.assertTrue(
58            all(
59                c in output for c in
60                ['delete', 'help', 'init', 'pull', 'push', 'set', 'status']
61            )
62        )
63
64        # call for specific command
65        with patch('txclib.commands.cmd_pull', spec=cmd_pull) as pull_mock:
66            cmd_help(['pull'], None)
67            pull_mock.assert_called_once_with(['--help'], None)
68
69
70class TestInitCommand(unittest.TestCase):
71
72    def setUp(self):
73        self.curr_dir = os.getcwd()
74        self.config_file = '.tx/config'
75        os.chdir('./tests/project_dir/')
76
77    def tearDown(self, *args, **kwargs):
78        shutil.rmtree('.tx', ignore_errors=False, onerror=None)
79        os.chdir(self.curr_dir)
80        super(TestInitCommand, self).tearDown(*args, **kwargs)
81
82    def test_init(self):
83        argv = []
84        config_text = "[main]\nhost = https://www.transifex.com\n\n"
85        with patch('txclib.commands.project.Project'):
86            with patch('txclib.commands.cmd_config') as set_mock:
87                cmd_init(argv, '')
88                set_mock.assert_called_once_with([], os.getcwd())
89        self.assertTrue(os.path.exists('./.tx'))
90        self.assertTrue(os.path.exists('./.tx/config'))
91        self.assertEqual(open(self.config_file).read(), config_text)
92
93    def test_init_skipsetup(self):
94        argv = ['--skipsetup']
95        with patch('txclib.commands.project.Project'):
96            with patch('txclib.commands.cmd_config') as set_mock:
97                cmd_init(argv, '')
98                self.assertEqual(set_mock.call_count, 0)
99        self.assertTrue(os.path.exists('./.tx'))
100        self.assertTrue(os.path.exists('./.tx/config'))
101
102    @patch('txclib.commands.utils.confirm')
103    def test_init_save_N(self, confirm_mock):
104        os.mkdir('./.tx')
105        open('./.tx/config', 'a').close()
106        argv = []
107        confirm_mock.return_value = False
108        with patch('txclib.commands.project.Project') as project_mock:
109                cmd_init(argv, '')
110                self.assertEqual(project_mock.call_count, 0)
111        self.assertTrue(os.path.exists('./.tx'))
112        self.assertEqual(confirm_mock.call_count, 1)
113
114    @patch('txclib.commands.utils.confirm')
115    def test_init_save_y(self, confirm_mock):
116        os.mkdir('./.tx')
117        open('./.tx/config', 'a').close()
118        argv = []
119        confirm_mock.return_value = True
120        with patch('txclib.commands.project.Project'):
121            with patch('txclib.commands.cmd_config') as set_mock:
122                cmd_init(argv, '')
123                set_mock.assert_called()
124        self.assertTrue(os.path.exists('./.tx'))
125        self.assertEqual(confirm_mock.call_count, 1)
126
127    def test_init_force_save(self):
128        os.mkdir('./.tx')
129        argv = ['--force-save']
130        with patch('txclib.commands.project.Project'):
131            with patch('txclib.commands.cmd_config') as set_mock:
132                cmd_init(argv, '')
133                set_mock.assert_called()
134        self.assertTrue(os.path.exists('./.tx'))
135        self.assertTrue(os.path.exists('./.tx/config'))
136
137
138class TestPullCommand(unittest.TestCase):
139    @patch('txclib.utils.get_current_branch')
140    @patch('txclib.commands.logger')
141    @patch('txclib.commands.project.Project')
142    def test_pull_with_branch_no_git_repo(self, project_mock, log_mock, bmock):
143        bmock.return_value = None
144        pr_instance = MagicMock()
145        project_mock.return_value = pr_instance
146        with self.assertRaises(SystemExit):
147            cmd_pull(['--branch'], '.')
148        log_mock.error.assert_called_once_with(
149            "You specified the --branch option but current "
150            "directory does not seem to belong in any git repo.")
151        self.assertEqual(pr_instance.pull.call_count, 0)
152
153    @patch('txclib.utils.get_current_branch')
154    @patch('txclib.commands.logger')
155    @patch('txclib.commands.project.Project')
156    def test_pull_branch_git_repo(self, project_mock, log_mock, bmock):
157        bmock.return_value = 'a-branch'
158        pr_instance = MagicMock()
159        project_mock.return_value = pr_instance
160        cmd_pull(['--branch'], '.')
161        self.assertEqual(pr_instance.pull.call_count, 1)
162        pull_call = call(
163            branch='a-branch', fetchall=False, fetchsource=False,
164            force=False, languages=[], minimum_perc=None, mode=None,
165            overwrite=True, pseudo=False, resources=[], skip=False,
166            xliff=False, parallel=False, no_interactive=False
167        )
168        pr_instance.pull.assert_has_calls([pull_call])
169
170    @patch('txclib.utils.get_current_branch')
171    @patch('txclib.commands.logger')
172    @patch('txclib.commands.project.Project')
173    def test_pull_with_branch_and_branchname_option(
174        self, project_mock, log_mock, bmock
175    ):
176        pr_instance = MagicMock()
177        project_mock.return_value = pr_instance
178        bmock.return_value = None
179        cmd_pull(['--branch', 'somebranch'], '.')
180        self.assertEqual(pr_instance.pull.call_count, 1)
181        pull_call = call(
182            branch='somebranch', fetchall=False, fetchsource=False,
183            force=False, languages=[], minimum_perc=None, mode=None,
184            overwrite=True, pseudo=False, resources=[], skip=False,
185            xliff=False, parallel=False, no_interactive=False
186        )
187        pr_instance.pull.assert_has_calls([pull_call])
188
189    @patch('txclib.commands.project')
190    def test_pull_with_no_interactive(self, project_mock):
191        pr_instance = MagicMock()
192        pr_instance.pull.return_value = True
193        project_mock.Project.return_value = pr_instance
194        cmd_pull(['--no-interactive'], '.')
195        pull_call = call(
196            fetchall=False, force=False, minimum_perc=None,
197            skip=False, no_interactive=True, resources=[], pseudo=False,
198            languages=[], fetchsource=False, mode=None, branch=None,
199            xliff=False, parallel=False, overwrite=True
200        )
201        self.assertEqual(pr_instance.pull.call_count, 1)
202        pr_instance.pull.assert_has_calls([pull_call])
203
204
205class TestConfigCommand(unittest.TestCase):
206
207    def setUp(self):
208        self.curr_dir = os.getcwd()
209        os.chdir('./tests/project_dir/')
210        os.mkdir('.tx')
211        self.path_to_tx = os.getcwd()
212        self.config_file = '.tx/config'
213        self.config_fd = open(self.config_file, "w")
214        self.config_fd.write("[main]\nhost = https://foo.var\n")
215        self.config_fd.close()
216
217    def tearDown(self, *args, **kwargs):
218        shutil.rmtree('.tx', ignore_errors=False, onerror=None)
219        os.chdir(self.curr_dir)
220        super(TestConfigCommand, self).tearDown(*args, **kwargs)
221
222    def test_bare_set_too_few_arguments(self):
223        with self.assertRaises(SystemExit):
224            args = ["-r", "project1.resource1"]
225            cmd_config(args, None)
226
227    def test_bare_set_source_no_file(self):
228        with self.assertRaises(SystemExit):
229            args = ["-r", "project1.resource1", '--is-source', '-l', 'en']
230            cmd_config(args, None)
231
232        with self.assertRaises(Exception):
233            args = ['-r', 'project1.resource1', '--source', '-l', 'en',
234                    'noexistent-file.txt']
235            cmd_config(args, self.path_to_tx)
236
237    def test_bare_set_source_file(self):
238        expected = ("[main]\nhost = https://foo.var\n\n[project1.resource1]\n"
239                    "source_file = test.txt\nsource_lang = en\n\n")
240        args = ["-r", "project1.resource1", '--source', '-l', 'en', 'test.txt']
241        cmd_config(args, self.path_to_tx)
242        with open(self.config_file) as config:
243            self.assertEqual(config.read(), expected)
244
245        # set translation file for de
246        expected = ("[main]\nhost = https://foo.var\n\n[project1.resource1]\n"
247                    "source_file = test.txt\nsource_lang = en\n"
248                    "trans.de = translations/de.txt\n\n")
249        args = ["-r", "project1.resource1", '-l', 'de', 'translations/de.txt']
250        cmd_config(args, self.path_to_tx)
251        with open(self.config_file) as config:
252            self.assertEqual(config.read(), expected)
253
254    def test_auto_locale_no_expression(self):
255        error_msg = "You need to specify an expression"
256        with assertRaisesRegex(self, Exception, error_msg):
257            args = [MAPPING, "-r", "project1.resource1",
258                    '--source-language', 'en']
259            cmd_config(args, self.path_to_tx)
260
261    def test_auto_locale(self):
262        expected = "[main]\nhost = https://foo.var\n"
263        args = [MAPPING, "-r", "project1.resource1", '--source-language',
264                'en', 'translations/<lang>/test.txt']
265        cmd_config(args, self.path_to_tx)
266        with open(self.config_file) as config:
267            self.assertEqual(config.read(), expected)
268
269    def test_auto_locale_is_backwards_compatible(self):
270        expected = ("[main]\nhost = https://foo.var\n\n[project1.resource1]\n"
271                    "file_filter = translations/<lang>/test.txt\n"
272                    "source_file = translations/en/test.txt\n"
273                    "source_lang = en\n\n")
274
275        args = ["--auto-local", "-r", "project1.resource1",
276                '--source-language', 'en', '--execute',
277                'translations/<lang>/test.txt']
278        cmd_config(args, self.path_to_tx)
279        with open(self.config_file) as config:
280            self.assertEqual(config.read(), expected)
281
282    def test_auto_locale_execute(self):
283        expected = ("[main]\nhost = https://foo.var\n\n[project1.resource1]\n"
284                    "file_filter = translations/<lang>/test.txt\n"
285                    "source_file = translations/en/test.txt\n"
286                    "source_lang = en\n\n")
287
288        args = [MAPPING, "-r", "project1.resource1", '--source-language',
289                'en', '--execute', 'translations/<lang>/test.txt']
290        cmd_config(args, self.path_to_tx)
291        with open(self.config_file) as config:
292            self.assertEqual(config.read(), expected)
293
294    def test_auto_remote_invalid_url(self):
295        # no project_url
296        args = [MAPPINGREMOTE]
297        with self.assertRaises(SystemExit):
298            cmd_config(args, self.path_to_tx)
299
300        # invalid project_url
301        args = [MAPPINGREMOTE, "http://some.random.url/"]
302        with self.assertRaises(Exception):
303            cmd_config(args, self.path_to_tx)
304
305    @patch('txclib.utils.get_details')
306    @patch('txclib.project.Project._extension_for')
307    def test_auto_remote_project(self, extension_mock, get_details_mock):
308        # change the host to tx
309        open(self.config_file, "w").write(
310            '[main]\nhost = https://www.transifex.com\n'
311        )
312        expected = ("[main]\nhost = https://www.transifex.com\n\n"
313                    "[proj.resource_1]\n"
314                    "file_filter = translations/proj.resource_1/<lang>.txt\n"
315                    "source_lang = fr\ntype = TXT\n\n[proj.resource_2]\n"
316                    "file_filter = translations/proj.resource_2/<lang>.txt\n"
317                    "source_lang = fr\ntype = TXT\n\n")
318        extension_mock.return_value = ".txt"
319        get_details_mock.side_effect = [
320            # project details
321            {
322                'resources': [
323                    {'slug': 'resource_1', 'name': 'resource 1'},
324                    {'slug': 'resource_2', 'name': 'resource 2'}
325                ]
326            },
327            # resources details
328            {
329                'source_language_code': 'fr',
330                'i18n_type': 'TXT',
331                'slug': 'resource_1',
332            }, {
333                'source_language_code': 'fr',
334                'i18n_type': 'TXT',
335                'slug': 'resource_2',
336            }
337        ]
338        args = [MAPPINGREMOTE, "https://www.transifex.com/test-org/proj/"]
339        cmd_config(args, self.path_to_tx)
340        with open(self.config_file) as config:
341            self.assertEqual(config.read(), expected)
342
343    @patch('txclib.utils.get_details')
344    @patch('txclib.project.Project._extension_for')
345    def test_auto_remote_is_backwards_compatible(self, extension_mock,
346                                                 get_details_mock):
347        # change the host to tx
348        open(self.config_file, "w").write(
349            '[main]\nhost = https://www.transifex.com\n'
350        )
351        expected = ("[main]\nhost = https://www.transifex.com\n\n"
352                    "[proj.resource_1]\n"
353                    "file_filter = translations/proj.resource_1/<lang>.txt\n"
354                    "source_lang = fr\ntype = TXT\n\n[proj.resource_2]\n"
355                    "file_filter = translations/proj.resource_2/<lang>.txt\n"
356                    "source_lang = fr\ntype = TXT\n\n")
357        extension_mock.return_value = ".txt"
358        get_details_mock.side_effect = [
359            # project details
360            {
361                'resources': [
362                    {'slug': 'resource_1', 'name': 'resource 1'},
363                    {'slug': 'resource_2', 'name': 'resource 2'}
364                ]
365            },
366            # resources details
367            {
368                'source_language_code': 'fr',
369                'i18n_type': 'TXT',
370                'slug': 'resource_1',
371            }, {
372                'source_language_code': 'fr',
373                'i18n_type': 'TXT',
374                'slug': 'resource_2',
375            }
376        ]
377        args = ["--auto-remote", "https://www.transifex.com/test-org/proj/"]
378        cmd_config(args, self.path_to_tx)
379        with open(self.config_file) as config:
380            self.assertEqual(config.read(), expected)
381
382    def test_bulk_missing_options(self):
383        with self.assertRaises(SystemExit):
384            args = [MAPPINGBULK]
385            cmd_config(args, self.path_to_tx)
386
387        with self.assertRaises(SystemExit):
388            args = [MAPPINGBULK, "-p", "test-project"]
389            cmd_config(args, self.path_to_tx)
390
391        with self.assertRaises(SystemExit):
392            args = [MAPPINGBULK, "-p", "test-project", "--source-language",
393                    "en", "--t", "TXT", "--file-extension", ".txt"]
394            cmd_config(args, self.path_to_tx)
395
396    def test_bulk(self):
397        expected = ("[main]\nhost = https://foo.var\n\n"
398                    "[test-project.translations_en_test]\n"
399                    "file_filter = translations/<lang>/en/test.txt\n"
400                    "source_file = translations/en/test.txt\n"
401                    "source_lang = en\ntype = TXT\n\n")
402        args = [MAPPINGBULK, "-p", "test-project", "--source-file-dir",
403                "translations", "--source-language", "en", "-t", "TXT",
404                "--file-extension", ".txt", "--execute", "--expression",
405                "translations/<lang>/{filepath}/{filename}{extension}"]
406        cmd_config(args, self.path_to_tx)
407        with open(self.config_file) as config:
408            self.assertEqual(config.read(), expected)
409
410    def test_invalid_expression(self):
411        args = [MAPPINGBULK, "-p", "test-project", "--source-file-dir",
412                "translations", "--source-language", "en", "-t", "TXT",
413                "--file-extension", ".txt", "--execute", "--expression",
414                "expression/{filename}{extension}"]
415        with self.assertRaises(SystemExit):
416            cmd_config(args, self.path_to_tx)
417
418    def test_invalid_expression_status(self):
419        """
420        Test how the client handles invalid expressions in file_filter
421        """
422        valid_expression = "test_expressions/<lang>/test.txt"
423        invalid_expression = "test_expressions/{filename}{ext}"
424        args = [MAPPINGBULK, "-p", "test-project", "--source-file-dir",
425                "test_expressions/en", "--source-language", "en", "-t", "TXT",
426                "--file-extension", ".txt", "--execute", "--expression",
427                valid_expression]
428        # Configure the client using a valid expression
429        cmd_config(args, self.path_to_tx)
430
431        # A trick to capture the standard output
432        sys_stdout = sys.stdout
433        out = StringIO()
434        sys.stdout = out
435        # Check for the expected status using the valid expression
436        cmd_status([], None)
437        output = out.getvalue().strip()
438        sys.stdout = sys_stdout
439        self.assertTrue("test_expressions/en/test.txt" in output)
440        self.assertTrue("test_expressions/es/test.txt" in output)
441
442        # Change the configuration file "by hand" and replace the file_filter
443        # with an invalid expression
444        for line in fileinput.input(self.config_file, inplace=True):
445            print(line.replace(valid_expression, invalid_expression))
446
447        sys_stdout = sys.stdout
448        out = StringIO()
449        sys.stdout = out
450        cmd_status([], None)
451        output = out.getvalue().strip()
452        sys.stdout = sys_stdout
453        # Check that the Spanish translation file in not tracked
454        self.assertTrue("test_expressions/en/test.txt" in output)
455        self.assertFalse("test_expressions/es/test.txt" in output)
456
457
458class TestMainCommand(unittest.TestCase):
459    def test_call_tx_with_no_command(self):
460        with self.assertRaises(SystemExit):
461            main(['tx'])
462
463    def test_call_tx_with_invalid_command(self):
464        with self.assertRaises(SystemExit):
465            main(['tx', 'random'])
466