def setUp(self):
     self.bucket_name = 'my-bucket'
     self.simulator = RawSimulator()
     self.account_info = StubAccountInfo()
     self.api = B2Api(self.account_info, raw_api=self.simulator)
     (self.account_id, self.master_key) = self.simulator.create_account()
     self.api.authorize_account('production', self.account_id,
                                self.master_key)
     self.api_url = self.account_info.get_api_url()
     self.account_auth_token = self.account_info.get_account_auth_token()
     self.bucket = self.api.create_bucket('my-bucket', 'allPublic')
     self.bucket_id = self.bucket.id_
Esempio n. 2
0
 def setUp(self):
     self.bucket_name = 'my-bucket'
     self.simulator = RawSimulator()
     self.account_info = StubAccountInfo()
     self.api = B2Api(self.account_info, raw_api=self.simulator)
     self.api.authorize_account('production', 'my-account', 'good-app-key')
     self.bucket = self.api.create_bucket('my-bucket', 'allPublic')
class TestCaseWithBucket(TestBase):
    def setUp(self):
        self.bucket_name = 'my-bucket'
        self.simulator = RawSimulator()
        self.account_info = StubAccountInfo()
        self.api = B2Api(self.account_info, raw_api=self.simulator)
        (self.account_id, self.master_key) = self.simulator.create_account()
        self.api.authorize_account('production', self.account_id,
                                   self.master_key)
        self.api_url = self.account_info.get_api_url()
        self.account_auth_token = self.account_info.get_account_auth_token()
        self.bucket = self.api.create_bucket('my-bucket', 'allPublic')
        self.bucket_id = self.bucket.id_

    def assertBucketContents(self, expected, *args, **kwargs):
        """
        *args and **kwargs are passed to self.bucket.ls()
        """
        actual = [(info.file_name, info.size, info.action, folder)
                  for (info, folder) in self.bucket.ls(*args, **kwargs)]
        self.assertEqual(expected, actual)
Esempio n. 4
0
 def setUp(self):
     self.account_info = StubAccountInfo()
     self.cache = InMemoryCache()
     self.raw_api = RawSimulator()
     self.b2_api = B2Api(self.account_info, self.cache, self.raw_api)
Esempio n. 5
0
class TestConsoleTool(TestBase):
    def setUp(self):
        self.account_info = StubAccountInfo()
        self.cache = InMemoryCache()
        self.raw_api = RawSimulator()
        self.b2_api = B2Api(self.account_info, self.cache, self.raw_api)

    def test_authorize_with_bad_key(self):
        expected_stdout = '''
        Using http://production.example.com
        '''

        expected_stderr = '''
        ERROR: unable to authorize account: Invalid authorization token. Server said: invalid application key: bad-app-key (bad_auth_token)
        '''

        self._run_command(['authorize_account', 'my-account', 'bad-app-key'],
                          expected_stdout, expected_stderr, 1)

    def test_authorize_with_good_key(self):
        # Initial condition
        assert self.account_info.get_account_auth_token() is None

        # Authorize an account with a good api key.
        expected_stdout = """
        Using http://production.example.com
        """

        self._run_command(['authorize_account', 'my-account', 'good-app-key'],
                          expected_stdout, '', 0)

        # Auth token should be in account info now
        assert self.account_info.get_account_auth_token() is not None

    def test_help_with_bad_args(self):
        expected_stderr = '''

        b2 create_bucket <bucketName> [allPublic | allPrivate]

            Creates a new bucket.  Prints the ID of the bucket created.

        '''

        self._run_command(['create_bucket'], '', expected_stderr, 1)

    def test_clear_account(self):
        # Initial condition
        self._authorize_account()
        assert self.account_info.get_account_auth_token() is not None

        # Clearing the account should remove the auth token
        # from the account info.
        self._run_command(['clear_account'], '', '', 0)
        assert self.account_info.get_account_auth_token() is None

    def test_buckets(self):
        self._authorize_account()

        # Make a bucket with an illegal name
        expected_stdout = 'ERROR: Bad request: illegal bucket name: bad/bucket/name\n'
        self._run_command(['create_bucket', 'bad/bucket/name', 'allPublic'],
                          '', expected_stdout, 1)

        # Make two buckets
        self._run_command(['create_bucket', 'my-bucket', 'allPrivate'],
                          'bucket_0\n', '', 0)
        self._run_command(['create_bucket', 'your-bucket', 'allPrivate'],
                          'bucket_1\n', '', 0)

        # Update one of them
        expected_stdout = '''
        {
            "accountId": "my-account",
            "bucketId": "bucket_0",
            "bucketName": "my-bucket",
            "bucketType": "allPublic"
        }
        '''

        self._run_command(['update_bucket', 'my-bucket', 'allPublic'],
                          expected_stdout, '', 0)

        # Make sure they are there
        expected_stdout = '''
        bucket_0  allPublic   my-bucket
        bucket_1  allPrivate  your-bucket
        '''

        self._run_command(['list_buckets'], expected_stdout, '', 0)

        # Delete one
        expected_stdout = '''
        {
            "accountId": "my-account",
            "bucketId": "bucket_1",
            "bucketName": "your-bucket",
            "bucketType": "allPrivate"
        }
        '''

        self._run_command(['delete_bucket', 'your-bucket'], expected_stdout,
                          '', 0)

    def test_cancel_large_file(self):
        self._authorize_account()
        self._create_my_bucket()
        bucket = self.b2_api.get_bucket_by_name('my-bucket')
        file = bucket.start_large_file('file1', 'text/plain', {})
        self._run_command(['cancel_large_file', file.file_id],
                          '9999 canceled\n', '', 0)

    def test_cancel_all_large_file(self):
        self._authorize_account()
        self._create_my_bucket()
        bucket = self.b2_api.get_bucket_by_name('my-bucket')
        bucket.start_large_file('file1', 'text/plain', {})
        bucket.start_large_file('file2', 'text/plain', {})
        expected_stdout = '''
        9999 canceled
        9998 canceled
        '''

        self._run_command(['cancel_all_unfinished_large_files', 'my-bucket'],
                          expected_stdout, '', 0)

    def test_files(self):

        self._authorize_account()
        self._run_command(['create_bucket', 'my-bucket', 'allPublic'],
                          'bucket_0\n', '', 0)

        with TempDir() as temp_dir:
            local_file1 = self._make_local_file(temp_dir, 'file1.txt')

            # Upload a file
            expected_stdout = '''
            URL by file name: http://download.example.com/file/my-bucket/file1.txt
            URL by fileId: http://download.example.com/b2api/v1/b2_download_file_by_id?fileId=9999
            {
              "action": "upload",
              "fileId": "9999",
              "fileName": "file1.txt",
              "size": 11,
              "uploadTimestamp": 5000
            }
            '''

            self._run_command([
                'upload_file', '--noProgress', 'my-bucket', local_file1,
                'file1.txt'
            ], expected_stdout, '', 0)

            # Download by name
            local_download1 = os.path.join(temp_dir, 'download1.txt')
            expected_stdout = '''
            File name:    file1.txt
            File id:      9999
            File size:    11
            Content type: b2/x-auto
            Content sha1: 2aae6c35c94fcfb415dbe95f408b9ce91ee846ed
            checksum matches
            '''

            self._run_command([
                'download_file_by_name', '--noProgress', 'my-bucket',
                'file1.txt', local_download1
            ], expected_stdout, '', 0)
            self.assertEquals(six.b('hello world'),
                              self._read_file(local_download1))

            # Download file by ID.  (Same expected output as downloading by name)
            local_download2 = os.path.join(temp_dir, 'download2.txt')
            self._run_command([
                'download_file_by_id', '--noProgress', '9999', local_download2
            ], expected_stdout, '', 0)
            self.assertEquals(six.b('hello world'),
                              self._read_file(local_download2))

            # Hide the file
            expected_stdout = '''
            {
              "action": "hide",
              "fileId": "9998",
              "fileName": "file1.txt",
              "size": 0,
              "uploadTimestamp": 5001
            }
            '''

            self._run_command(['hide_file', 'my-bucket', 'file1.txt'],
                              expected_stdout, '', 0)

            # List the file versions
            expected_stdout = '''
            {
              "files": [
                {
                  "action": "hide",
                  "contentSha1": "none",
                  "contentType": null,
                  "fileId": "9998",
                  "fileInfo": {},
                  "fileName": "file1.txt",
                  "size": 0,
                  "uploadTimestamp": 5001
                },
                {
                  "action": "upload",
                  "contentSha1": "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed",
                  "contentType": "b2/x-auto",
                  "fileId": "9999",
                  "fileInfo": {},
                  "fileName": "file1.txt",
                  "size": 11,
                  "uploadTimestamp": 5000
                }
              ],
              "nextFileId": null,
              "nextFileName": null
            }
            '''

            self._run_command(['list_file_versions', 'my-bucket'],
                              expected_stdout, '', 0)

            # List the file names
            expected_stdout = '''
            {
              "files": [],
              "nextFileName": null
            }
            '''

            self._run_command(['list_file_names', 'my-bucket'],
                              expected_stdout, '', 0)

            # Delete one file version, passing the name in
            expected_stdout = '''
            {
              "action": "delete",
              "fileId": "9998",
              "fileName": "file1.txt"
            }
            '''

            self._run_command(['delete_file_version', 'file1.txt', '9998'],
                              expected_stdout, '', 0)

            # Delete one file version, not passing the name in
            expected_stdout = '''
            {
              "action": "delete",
              "fileId": "9999",
              "fileName": "file1.txt"
            }
            '''

            self._run_command(['delete_file_version', '9999'], expected_stdout,
                              '', 0)

    def test_list_parts_with_none(self):
        self._authorize_account()
        self._create_my_bucket()
        bucket = self.b2_api.get_bucket_by_name('my-bucket')
        file = bucket.start_large_file('file', 'text/plain', {})
        self._run_command(['list_parts', file.file_id], '', '', 0)

    def test_list_parts_with_parts(self):
        self._authorize_account()
        self._create_my_bucket()
        bucket = self.b2_api.get_bucket_by_name('my-bucket')
        file = bucket.start_large_file('file', 'text/plain', {})
        content = six.b('hello world')
        large_file_upload_state = mock.MagicMock()
        large_file_upload_state.has_error.return_value = False
        bucket._upload_part(file.file_id, 1, (0, 11),
                            UploadSourceBytes(content),
                            large_file_upload_state)
        bucket._upload_part(file.file_id, 3, (0, 11),
                            UploadSourceBytes(content),
                            large_file_upload_state)
        expected_stdout = '''
            1         11  2aae6c35c94fcfb415dbe95f408b9ce91ee846ed
            3         11  2aae6c35c94fcfb415dbe95f408b9ce91ee846ed
        '''

        self._run_command(['list_parts', file.file_id], expected_stdout, '', 0)

    def test_list_unfinished_large_files_with_none(self):
        self._authorize_account()
        self._create_my_bucket()
        self._run_command(['list_unfinished_large_files', 'my-bucket'], '', '',
                          0)

    def test_list_unfinished_large_files_with_some(self):
        self._authorize_account()
        self._create_my_bucket()
        api_url = self.account_info.get_api_url()
        auth_token = self.account_info.get_account_auth_token()
        self.raw_api.start_large_file(api_url, auth_token, 'bucket_0', 'file1',
                                      'text/plain', {})
        self.raw_api.start_large_file(api_url, auth_token, 'bucket_0', 'file2',
                                      'text/plain', {'color': 'blue'})
        self.raw_api.start_large_file(api_url, auth_token, 'bucket_0', 'file3',
                                      'application/json', {})
        expected_stdout = '''
        9999 file1 text/plain
        9998 file2 text/plain color=blue
        9997 file3 application/json
        '''

        self._run_command(['list_unfinished_large_files', 'my-bucket'],
                          expected_stdout, '', 0)

    def test_upload_large_file(self):
        self._authorize_account()
        self._create_my_bucket()
        min_part_size = self.account_info.get_minimum_part_size()
        file_size = min_part_size * 3

        with TempDir() as temp_dir:
            file_path = os.path.join(temp_dir, 'test.txt')
            text = six.u('*') * file_size
            with open(file_path, 'wb') as f:
                f.write(text.encode('utf-8'))
            expected_stdout = '''
            URL by file name: http://download.example.com/file/my-bucket/test.txt
            URL by fileId: http://download.example.com/b2api/v1/b2_download_file_by_id?fileId=9999
            {
              "action": "upload",
              "fileId": "9999",
              "fileName": "test.txt",
              "size": 600,
              "uploadTimestamp": 5000
            }
            '''

            self._run_command([
                'upload_file', '--noProgress', '--threads', '5', 'my-bucket',
                file_path, 'test.txt'
            ], expected_stdout, '', 0)

    def test_sync(self):
        self._authorize_account()
        self._create_my_bucket()

        with TempDir() as temp_dir:
            file_path = os.path.join(temp_dir, 'test.txt')
            with open(file_path, 'wb') as f:
                f.write(six.u('hello world').encode('utf-8'))
            expected_stdout = '''
            upload test.txt
            '''

            command = [
                'sync', '--threads', '5', '--noProgress', temp_dir,
                'b2://my-bucket'
            ]
            self._run_command(command, expected_stdout, '', 0)

    def test_sync_syntax_error(self):
        self._authorize_account()
        self._create_my_bucket()
        expected_stderr = 'ERROR: --includeRegex cannot be used without --excludeRegex at the same time\n'
        self._run_command([
            'sync', '--includeRegex', '.incl', 'non-existent-local-folder',
            'b2://my-bucket'
        ],
                          expected_stderr=expected_stderr,
                          expected_status=1)

    def test_sync_dry_run(self):
        self._authorize_account()
        self._create_my_bucket()

        with TempDir() as temp_dir:
            temp_file = self._make_local_file(temp_dir, 'test-dry-run.txt')

            # dry-run
            expected_stdout = '''
            upload test-dry-run.txt
            '''
            command = [
                'sync', '--noProgress', '--dryRun', temp_dir, 'b2://my-bucket'
            ]
            self._run_command(command, expected_stdout, '', 0)

            # file should not have been uploaded
            expected_stdout = '''
            {
              "files": [],
              "nextFileName": null
            }
            '''
            self._run_command(['list_file_names', 'my-bucket'],
                              expected_stdout, '', 0)

            # upload file
            expected_stdout = '''
            upload test-dry-run.txt
            '''
            command = ['sync', '--noProgress', temp_dir, 'b2://my-bucket']
            self._run_command(command, expected_stdout, '', 0)

            # file should have been uploaded
            mtime = file_mod_time_millis(temp_file)
            expected_stdout = '''
            {
              "files": [
                {
                  "action": "upload",
                  "contentSha1": "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed",
                  "contentType": "b2/x-auto",
                  "fileId": "9999",
                  "fileInfo": {
                    "src_last_modified_millis": "%d"
                  },
                  "fileName": "test-dry-run.txt",
                  "size": 11,
                  "uploadTimestamp": 5000
                }
              ],
              "nextFileName": null
            }
            ''' % (mtime)
            self._run_command(['list_file_names', 'my-bucket'],
                              expected_stdout, '', 0)

    def _authorize_account(self):
        """
        Prepare for a test by authorizing an account and getting an
        account auth token
        """
        self._run_command_no_checks(
            ['authorize_account', 'my-account', 'good-app-key'])

    def _create_my_bucket(self):
        self._run_command(['create_bucket', 'my-bucket', 'allPublic'],
                          'bucket_0\n', '', 0)

    def _run_command(self,
                     argv,
                     expected_stdout='',
                     expected_stderr='',
                     expected_status=0):
        """
        Runs one command using the ConsoleTool, checking stdout, stderr, and
        the returned status code.

        The ConsoleTool is stateless, so we can make a new one for each
        call, with a fresh stdout and stderr
        """
        expected_stdout = self._trim_leading_spaces(expected_stdout)
        expected_stderr = self._trim_leading_spaces(expected_stderr)
        stdout = six.StringIO()
        stderr = six.StringIO()
        console_tool = ConsoleTool(self.b2_api, stdout, stderr)
        actual_status = console_tool.run_command(['b2'] + argv)

        # The json module in Python 2.6 includes trailing spaces.  Later version of Python don't.
        actual_stdout = self._trim_trailing_spaces(stdout.getvalue())
        actual_stderr = self._trim_trailing_spaces(stderr.getvalue())

        if expected_stdout != actual_stdout:
            print(repr(expected_stdout))
            print(repr(actual_stdout))
        if expected_stderr != actual_stderr:
            print(repr(expected_stderr))
            print(repr(actual_stderr))

        self.assertEqual(expected_stdout, actual_stdout, 'stdout')
        self.assertEqual(expected_stderr, actual_stderr, 'stderr')
        self.assertEqual(expected_status, actual_status, 'exit status code')

    def _run_command_no_checks(self, argv):
        ConsoleTool(self.b2_api, six.StringIO(),
                    six.StringIO()).run_command(['b2'] + argv)

    def _trim_leading_spaces(self, s):
        """
        Takes the contents of a triple-quoted string, and removes the leading
        newline and leading spaces that come from it being indented with code.
        """
        # The first line starts on the line following the triple
        # quote, so the first line after splitting can be discarded.
        lines = s.split('\n')
        if lines[0] == '':
            lines = lines[1:]
        if len(lines) == 0:
            return ''

        # Count the leading spaces
        space_count = min(
            self._leading_spaces(line) for line in lines if line != '')

        # Remove the leading spaces from each line, based on the line
        # with the fewest leading spaces
        leading_spaces = ' ' * space_count
        assert all(
            line.startswith(leading_spaces) or line == ''
            for line in lines), 'all lines have leading spaces'
        return '\n'.join('' if line == '' else line[space_count:]
                         for line in lines)

    def _leading_spaces(self, s):
        space_count = 0
        while space_count < len(s) and s[space_count] == ' ':
            space_count += 1
        return space_count

    def _trim_trailing_spaces(self, s):
        return '\n'.join(line.rstrip() for line in s.split('\n'))

    def _make_local_file(self, temp_dir, file_name):
        local_path = os.path.join(temp_dir, file_name)
        with open(local_path, 'wb') as f:
            f.write(six.b('hello world'))
        return local_path

    def _read_file(self, local_path):
        with open(local_path, 'rb') as f:
            return f.read()
Esempio n. 6
0
class TestConsoleTool(TestBase):
    def setUp(self):
        self.account_info = StubAccountInfo()
        self.cache = InMemoryCache()
        self.raw_api = RawSimulator()
        self.b2_api = B2Api(self.account_info, self.cache, self.raw_api)

    def test_authorize_with_bad_key(self):
        expected_stdout = '''
        Using http://production.example.com
        '''

        expected_stderr = '''
        ERROR: unable to authorize account: Invalid authorization token. Server said: invalid application key: bad-app-key (bad_auth_token)
        '''

        self._run_command(['authorize_account', 'my-account', 'bad-app-key'],
                          expected_stdout, expected_stderr, 1)

    def test_authorize_with_good_key_using_hyphen(self):
        # Initial condition
        assert self.account_info.get_account_auth_token() is None

        # Authorize an account with a good api key.
        expected_stdout = """
        Using http://production.example.com
        """

        self._run_command(['authorize-account', 'my-account', 'good-app-key'],
                          expected_stdout, '', 0)

        # Auth token should be in account info now
        assert self.account_info.get_account_auth_token() is not None

    def test_authorize_with_good_key_using_underscore(self):
        # Initial condition
        assert self.account_info.get_account_auth_token() is None

        # Authorize an account with a good api key.
        expected_stdout = """
        Using http://production.example.com
        """

        self._run_command(['authorize-account', 'my-account', 'good-app-key'],
                          expected_stdout, '', 0)

        # Auth token should be in account info now
        assert self.account_info.get_account_auth_token() is not None

    def test_help_with_bad_args(self):
        expected_stderr = '''

        b2 list-parts <largeFileId>

            Lists all of the parts that have been uploaded for the given
            large file, which must be a file that was started but not
            finished or canceled.

        '''

        self._run_command(['list_parts'], '', expected_stderr, 1)

    def test_clear_account(self):
        # Initial condition
        self._authorize_account()
        assert self.account_info.get_account_auth_token() is not None

        # Clearing the account should remove the auth token
        # from the account info.
        self._run_command(['clear-account'], '', '', 0)
        assert self.account_info.get_account_auth_token() is None

    def test_buckets(self):
        self._authorize_account()

        # Make a bucket with an illegal name
        expected_stdout = 'ERROR: Bad request: illegal bucket name: bad/bucket/name\n'
        self._run_command(['create_bucket', 'bad/bucket/name', 'allPublic'],
                          '', expected_stdout, 1)

        # Make two buckets
        self._run_command(['create_bucket', 'my-bucket', 'allPrivate'],
                          'bucket_0\n', '', 0)
        self._run_command(['create_bucket', 'your-bucket', 'allPrivate'],
                          'bucket_1\n', '', 0)

        # Update one of them
        expected_stdout = '''
        {
            "accountId": "my-account",
            "bucketId": "bucket_0",
            "bucketInfo": {},
            "bucketName": "my-bucket",
            "bucketType": "allPublic",
            "corsRules": [],
            "lifecycleRules": [],
            "revision": 2
        }
        '''

        self._run_command(['update_bucket', 'my-bucket', 'allPublic'],
                          expected_stdout, '', 0)

        # Make sure they are there
        expected_stdout = '''
        bucket_0  allPublic   my-bucket
        bucket_1  allPrivate  your-bucket
        '''

        self._run_command(['list_buckets'], expected_stdout, '', 0)

        # Delete one
        expected_stdout = '''
        {
            "accountId": "my-account",
            "bucketId": "bucket_1",
            "bucketInfo": {},
            "bucketName": "your-bucket",
            "bucketType": "allPrivate",
            "corsRules": [],
            "lifecycleRules": [],
            "revision": 1
        }
        '''

        self._run_command(['delete_bucket', 'your-bucket'], expected_stdout,
                          '', 0)

    def test_bucket_info_from_json(self):

        self._authorize_account()
        self._run_command(['create_bucket', 'my-bucket', 'allPublic'],
                          'bucket_0\n', '', 0)

        bucket_info = {'color': 'blue'}

        expected_stdout = '''
            {
                "accountId": "my-account",
                "bucketId": "bucket_0",
                "bucketInfo": {
                    "color": "blue"
                },
                "bucketName": "my-bucket",
                "bucketType": "allPrivate",
                "corsRules": [],
                "lifecycleRules": [],
                "revision": 2
            }
            '''
        self._run_command([
            'update_bucket', '--bucketInfo',
            json.dumps(bucket_info), 'my-bucket', 'allPrivate'
        ], expected_stdout, '', 0)

    def test_cancel_large_file(self):
        self._authorize_account()
        self._create_my_bucket()
        bucket = self.b2_api.get_bucket_by_name('my-bucket')
        file = bucket.start_large_file('file1', 'text/plain', {})
        self._run_command(['cancel_large_file', file.file_id],
                          '9999 canceled\n', '', 0)

    def test_cancel_all_large_file(self):
        self._authorize_account()
        self._create_my_bucket()
        bucket = self.b2_api.get_bucket_by_name('my-bucket')
        bucket.start_large_file('file1', 'text/plain', {})
        bucket.start_large_file('file2', 'text/plain', {})
        expected_stdout = '''
        9999 canceled
        9998 canceled
        '''

        self._run_command(['cancel_all_unfinished_large_files', 'my-bucket'],
                          expected_stdout, '', 0)

    def test_files(self):

        self._authorize_account()
        self._run_command(['create_bucket', 'my-bucket', 'allPublic'],
                          'bucket_0\n', '', 0)

        with TempDir() as temp_dir:
            local_file1 = self._make_local_file(temp_dir, 'file1.txt')
            # For this test, use a mod time without millis.  My mac truncates
            # millis and just leaves seconds.
            mod_time = 1500111222
            os.utime(local_file1, (mod_time, mod_time))
            self.assertEqual(1500111222, os.path.getmtime(local_file1))

            # Upload a file
            expected_stdout = '''
            URL by file name: http://download.example.com/file/my-bucket/file1.txt
            URL by fileId: http://download.example.com/b2api/v1/b2_download_file_by_id?fileId=9999
            {
              "action": "upload",
              "fileId": "9999",
              "fileName": "file1.txt",
              "size": 11,
              "uploadTimestamp": 5000
            }
            '''

            self._run_command([
                'upload_file', '--noProgress', 'my-bucket', local_file1,
                'file1.txt'
            ], expected_stdout, '', 0)

            # Get file info
            mod_time_str = str(int(os.path.getmtime(local_file1) * 1000))
            expected_stdout = '''
            {
              "accountId": "my-account",
              "action": "upload",
              "bucketId": "bucket_0",
              "contentLength": 11,
              "contentSha1": "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed",
              "contentType": "b2/x-auto",
              "fileId": "9999",
              "fileInfo": {
                "src_last_modified_millis": "1500111222000"
              },
              "fileName": "file1.txt",
              "uploadTimestamp": 5000
            }
            '''

            self._run_command(['get_file_info', '9999'], expected_stdout, '',
                              0)

            # Download by name
            local_download1 = os.path.join(temp_dir, 'download1.txt')
            expected_stdout = '''
            File name:    file1.txt
            File id:      9999
            File size:    11
            Content type: b2/x-auto
            Content sha1: 2aae6c35c94fcfb415dbe95f408b9ce91ee846ed
            INFO src_last_modified_millis: 1500111222000
            checksum matches
            '''

            self._run_command([
                'download_file_by_name', '--noProgress', 'my-bucket',
                'file1.txt', local_download1
            ], expected_stdout, '', 0)
            self.assertEquals(six.b('hello world'),
                              self._read_file(local_download1))
            self.assertEquals(mod_time, os.path.getmtime(local_download1))

            # Download file by ID.  (Same expected output as downloading by name)
            local_download2 = os.path.join(temp_dir, 'download2.txt')
            self._run_command([
                'download_file_by_id', '--noProgress', '9999', local_download2
            ], expected_stdout, '', 0)
            self.assertEquals(six.b('hello world'),
                              self._read_file(local_download2))

            # Hide the file
            expected_stdout = '''
            {
              "action": "hide",
              "fileId": "9998",
              "fileName": "file1.txt",
              "size": 0,
              "uploadTimestamp": 5001
            }
            '''

            self._run_command(['hide_file', 'my-bucket', 'file1.txt'],
                              expected_stdout, '', 0)

            # List the file versions
            expected_stdout = '''
            {
              "files": [
                {
                  "action": "hide",
                  "contentSha1": "none",
                  "contentType": null,
                  "fileId": "9998",
                  "fileInfo": {},
                  "fileName": "file1.txt",
                  "size": 0,
                  "uploadTimestamp": 5001
                },
                {
                  "action": "upload",
                  "contentSha1": "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed",
                  "contentType": "b2/x-auto",
                  "fileId": "9999",
                  "fileInfo": {
                    "src_last_modified_millis": "%s"
                  },
                  "fileName": "file1.txt",
                  "size": 11,
                  "uploadTimestamp": 5000
                }
              ],
              "nextFileId": null,
              "nextFileName": null
            }
            ''' % (mod_time_str, )

            self._run_command(['list_file_versions', 'my-bucket'],
                              expected_stdout, '', 0)

            # List the file names
            expected_stdout = '''
            {
              "files": [],
              "nextFileName": null
            }
            '''

            self._run_command(['list_file_names', 'my-bucket'],
                              expected_stdout, '', 0)

            # Delete one file version, passing the name in
            expected_stdout = '''
            {
              "action": "delete",
              "fileId": "9998",
              "fileName": "file1.txt"
            }
            '''

            self._run_command(['delete_file_version', 'file1.txt', '9998'],
                              expected_stdout, '', 0)

            # Delete one file version, not passing the name in
            expected_stdout = '''
            {
              "action": "delete",
              "fileId": "9999",
              "fileName": "file1.txt"
            }
            '''

            self._run_command(['delete_file_version', '9999'], expected_stdout,
                              '', 0)

    def test_get_download_auth_defaults(self):
        self._authorize_account()
        self._create_my_bucket()
        self._run_command(['get_download_auth', 'my-bucket'],
                          'fake_download_auth_token_bucket_0__86400\n', '', 0)

    def test_get_download_auth_explicit(self):
        self._authorize_account()
        self._create_my_bucket()
        self._run_command([
            'get_download_auth', '--prefix', 'prefix', '--duration', '12345',
            'my-bucket'
        ], 'fake_download_auth_token_bucket_0_prefix_12345\n', '', 0)

    def test_get_download_auth_url(self):
        self._authorize_account()
        self._create_my_bucket()
        self._run_command([
            'get-download-url-with-auth', '--duration', '12345', 'my-bucket',
            'my-file'
        ], 'http://download.example.com/file/my-bucket/my-file?Authorization=fake_download_auth_token_bucket_0_my-file_12345\n',
                          '', 0)

    def test_get_download_auth_url_with_encoding(self):
        self._authorize_account()
        self._create_my_bucket()
        self._run_command([
            'get-download-url-with-auth', '--duration', '12345', 'my-bucket',
            u'\u81ea'
        ], u'http://download.example.com/file/my-bucket/%E8%87%AA?Authorization=fake_download_auth_token_bucket_0_%E8%87%AA_12345\n',
                          '', 0)

    def test_list_parts_with_none(self):
        self._authorize_account()
        self._create_my_bucket()
        bucket = self.b2_api.get_bucket_by_name('my-bucket')
        file = bucket.start_large_file('file', 'text/plain', {})
        self._run_command(['list_parts', file.file_id], '', '', 0)

    def test_list_parts_with_parts(self):
        self._authorize_account()
        self._create_my_bucket()
        bucket = self.b2_api.get_bucket_by_name('my-bucket')
        file = bucket.start_large_file('file', 'text/plain', {})
        content = six.b('hello world')
        large_file_upload_state = mock.MagicMock()
        large_file_upload_state.has_error.return_value = False
        bucket._upload_part(file.file_id, 1, (0, 11),
                            UploadSourceBytes(content),
                            large_file_upload_state)
        bucket._upload_part(file.file_id, 3, (0, 11),
                            UploadSourceBytes(content),
                            large_file_upload_state)
        expected_stdout = '''
            1         11  2aae6c35c94fcfb415dbe95f408b9ce91ee846ed
            3         11  2aae6c35c94fcfb415dbe95f408b9ce91ee846ed
        '''

        self._run_command(['list_parts', file.file_id], expected_stdout, '', 0)

    def test_list_unfinished_large_files_with_none(self):
        self._authorize_account()
        self._create_my_bucket()
        self._run_command(['list_unfinished_large_files', 'my-bucket'], '', '',
                          0)

    def test_list_unfinished_large_files_with_some(self):
        self._authorize_account()
        self._create_my_bucket()
        api_url = self.account_info.get_api_url()
        auth_token = self.account_info.get_account_auth_token()
        self.raw_api.start_large_file(api_url, auth_token, 'bucket_0', 'file1',
                                      'text/plain', {})
        self.raw_api.start_large_file(api_url, auth_token, 'bucket_0', 'file2',
                                      'text/plain', {'color': 'blue'})
        self.raw_api.start_large_file(api_url, auth_token, 'bucket_0', 'file3',
                                      'application/json', {})
        expected_stdout = '''
        9999 file1 text/plain
        9998 file2 text/plain color=blue
        9997 file3 application/json
        '''

        self._run_command(['list_unfinished_large_files', 'my-bucket'],
                          expected_stdout, '', 0)

    def test_upload_large_file(self):
        self._authorize_account()
        self._create_my_bucket()
        min_part_size = self.account_info.get_minimum_part_size()
        file_size = min_part_size * 3

        with TempDir() as temp_dir:
            file_path = os.path.join(temp_dir, 'test.txt')
            text = six.u('*') * file_size
            with open(file_path, 'wb') as f:
                f.write(text.encode('utf-8'))
            expected_stdout = '''
            URL by file name: http://download.example.com/file/my-bucket/test.txt
            URL by fileId: http://download.example.com/b2api/v1/b2_download_file_by_id?fileId=9999
            {
              "action": "upload",
              "fileId": "9999",
              "fileName": "test.txt",
              "size": 600,
              "uploadTimestamp": 5000
            }
            '''

            self._run_command([
                'upload_file', '--noProgress', '--threads', '5', 'my-bucket',
                file_path, 'test.txt'
            ], expected_stdout, '', 0)

    def test_get_account_info(self):
        self._authorize_account()
        expected_stdout = '''
        {
            "accountAuthToken": "AUTH:my-account",
            "accountId": "my-account",
            "apiUrl": "http://api.example.com",
            "applicationKey": "good-app-key",
            "downloadUrl": "http://download.example.com"
        }
        '''
        self._run_command(['get-account-info'], expected_stdout, '', 0)

    def test_get_bucket(self):
        self._authorize_account()
        self._create_my_bucket()
        expected_stdout = '''
        {
            "accountId": "my-account",
            "bucketId": "bucket_0",
            "bucketInfo": {},
            "bucketName": "my-bucket",
            "bucketType": "allPublic",
            "corsRules": [],
            "lifecycleRules": [],
            "revision": 1
        }
        '''
        self._run_command(['get-bucket', 'my-bucket'], expected_stdout, '', 0)

    def test_get_bucket_empty_show_size(self):
        self._authorize_account()
        self._create_my_bucket()
        expected_stdout = '''
        {
            "accountId": "my-account",
            "bucketId": "bucket_0",
            "bucketInfo": {},
            "bucketName": "my-bucket",
            "bucketType": "allPublic",
            "corsRules": [],
            "fileCount": 0,
            "lifecycleRules": [],
            "revision": 1,
            "totalSize": 0
        }
        '''
        self._run_command(['get-bucket', '--showSize', 'my-bucket'],
                          expected_stdout, '', 0)

    def test_get_bucket_one_item_show_size(self):
        self._authorize_account()
        self._create_my_bucket()
        with TempDir() as temp_dir:
            # Upload a standard test file.
            local_file1 = self._make_local_file(temp_dir, 'file1.txt')
            expected_stdout = '''
            URL by file name: http://download.example.com/file/my-bucket/file1.txt
            URL by fileId: http://download.example.com/b2api/v1/b2_download_file_by_id?fileId=9999
            {
              "action": "upload",
              "fileId": "9999",
              "fileName": "file1.txt",
              "size": 11,
              "uploadTimestamp": 5000
            }
            '''
            self._run_command([
                'upload_file', '--noProgress', 'my-bucket', local_file1,
                'file1.txt'
            ], expected_stdout, '', 0)

            # Now check the output of get-bucket against the canon.
            expected_stdout = '''
            {
                "accountId": "my-account",
                "bucketId": "bucket_0",
                "bucketInfo": {},
                "bucketName": "my-bucket",
                "bucketType": "allPublic",
                "corsRules": [],
                "fileCount": 1,
                "lifecycleRules": [],
                "revision": 1,
                "totalSize": 11
            }
            '''
            self._run_command(['get-bucket', '--showSize', 'my-bucket'],
                              expected_stdout, '', 0)

    def test_get_bucket_with_versions(self):
        self._authorize_account()
        self._create_my_bucket()

        # Put many versions of a file into the test bucket. Unroll the loop here for convenience.
        bucket = self.b2_api.get_bucket_by_name('my-bucket')
        bucket.upload(UploadSourceBytes(b'test'), 'test')
        bucket.upload(UploadSourceBytes(b'test'), 'test')
        bucket.upload(UploadSourceBytes(b'test'), 'test')
        bucket.upload(UploadSourceBytes(b'test'), 'test')
        bucket.upload(UploadSourceBytes(b'test'), 'test')
        bucket.upload(UploadSourceBytes(b'test'), 'test')
        bucket.upload(UploadSourceBytes(b'test'), 'test')
        bucket.upload(UploadSourceBytes(b'test'), 'test')
        bucket.upload(UploadSourceBytes(b'test'), 'test')
        bucket.upload(UploadSourceBytes(b'test'), 'test')

        # Now check the output of get-bucket against the canon.
        expected_stdout = '''
        {
            "accountId": "my-account",
            "bucketId": "bucket_0",
            "bucketInfo": {},
            "bucketName": "my-bucket",
            "bucketType": "allPublic",
            "corsRules": [],
            "fileCount": 10,
            "lifecycleRules": [],
            "revision": 1,
            "totalSize": 40
        }
        '''
        self._run_command(['get-bucket', '--showSize', 'my-bucket'],
                          expected_stdout, '', 0)

    def test_get_bucket_with_folders(self):
        self._authorize_account()
        self._create_my_bucket()

        # Create a hierarchical structure within the test bucket. Unroll the loop here for
        # convenience.
        bucket = self.b2_api.get_bucket_by_name('my-bucket')
        bucket.upload(UploadSourceBytes(b'test'), 'test')
        bucket.upload(UploadSourceBytes(b'test'), '1/test')
        bucket.upload(UploadSourceBytes(b'test'), '1/2/test')
        bucket.upload(UploadSourceBytes(b'test'), '1/2/3/test')
        bucket.upload(UploadSourceBytes(b'test'), '1/2/3/4/test')
        bucket.upload(UploadSourceBytes(b'test'), '1/2/3/4/5/test')
        bucket.upload(UploadSourceBytes(b'test'), '1/2/3/4/5/6/test')
        bucket.upload(UploadSourceBytes(b'test'), '1/2/3/4/5/6/7/test')
        bucket.upload(UploadSourceBytes(b'test'), '1/2/3/4/5/6/7/8/test')
        bucket.upload(UploadSourceBytes(b'test'), '1/2/3/4/5/6/7/8/9/test')
        bucket.upload(UploadSourceBytes(b'check'), 'check')
        bucket.upload(UploadSourceBytes(b'check'), '1/check')
        bucket.upload(UploadSourceBytes(b'check'), '1/2/check')
        bucket.upload(UploadSourceBytes(b'check'), '1/2/3/check')
        bucket.upload(UploadSourceBytes(b'check'), '1/2/3/4/check')
        bucket.upload(UploadSourceBytes(b'check'), '1/2/3/4/5/check')
        bucket.upload(UploadSourceBytes(b'check'), '1/2/3/4/5/6/check')
        bucket.upload(UploadSourceBytes(b'check'), '1/2/3/4/5/6/7/check')
        bucket.upload(UploadSourceBytes(b'check'), '1/2/3/4/5/6/7/8/check')
        bucket.upload(UploadSourceBytes(b'check'), '1/2/3/4/5/6/7/8/9/check')

        # Now check the output of get-bucket against the canon.
        expected_stdout = '''
        {
            "accountId": "my-account",
            "bucketId": "bucket_0",
            "bucketInfo": {},
            "bucketName": "my-bucket",
            "bucketType": "allPublic",
            "corsRules": [],
            "fileCount": 20,
            "lifecycleRules": [],
            "revision": 1,
            "totalSize": 90
        }
        '''
        self._run_command(['get-bucket', '--showSize', 'my-bucket'],
                          expected_stdout, '', 0)

    def test_get_bucket_with_hidden(self):
        self._authorize_account()
        self._create_my_bucket()

        # Put some files into the test bucket. Unroll the loop for convenience.
        bucket = self.b2_api.get_bucket_by_name('my-bucket')
        bucket.upload(UploadSourceBytes(b'test'), 'upload1')
        bucket.upload(UploadSourceBytes(b'test'), 'upload2')
        bucket.upload(UploadSourceBytes(b'test'), 'upload3')
        bucket.upload(UploadSourceBytes(b'test'), 'upload4')
        bucket.upload(UploadSourceBytes(b'test'), 'upload5')
        bucket.upload(UploadSourceBytes(b'test'), 'upload6')

        # Hide some new files. Don't check the results here; it will be clear enough that
        # something has failed if the output of 'get-bucket' does not match the canon.
        stdout, stderr = self._get_stdouterr()
        console_tool = ConsoleTool(self.b2_api, stdout, stderr)
        console_tool.run_command(['b2', 'hide_file', 'my-bucket', 'hidden1'])
        console_tool.run_command(['b2', 'hide_file', 'my-bucket', 'hidden2'])
        console_tool.run_command(['b2', 'hide_file', 'my-bucket', 'hidden3'])
        console_tool.run_command(['b2', 'hide_file', 'my-bucket', 'hidden4'])

        # Now check the output of get-bucket against the canon.
        expected_stdout = '''
        {
            "accountId": "my-account",
            "bucketId": "bucket_0",
            "bucketInfo": {},
            "bucketName": "my-bucket",
            "bucketType": "allPublic",
            "corsRules": [],
            "fileCount": 10,
            "lifecycleRules": [],
            "revision": 1,
            "totalSize": 24
        }
        '''
        self._run_command(['get-bucket', '--showSize', 'my-bucket'],
                          expected_stdout, '', 0)

    def test_get_bucket_complex(self):
        self._authorize_account()
        self._create_my_bucket()

        # Create a hierarchical structure within the test bucket. Unroll the loop here for
        # convenience.
        bucket = self.b2_api.get_bucket_by_name('my-bucket')
        bucket.upload(UploadSourceBytes(b'test'), 'test')
        bucket.upload(UploadSourceBytes(b'test'), 'test')
        bucket.upload(UploadSourceBytes(b'test'), '1/test')
        bucket.upload(UploadSourceBytes(b'test'), '1/test')
        bucket.upload(UploadSourceBytes(b'test'), '1/2/test')
        bucket.upload(UploadSourceBytes(b'test'), '1/2/test')
        bucket.upload(UploadSourceBytes(b'test'), '1/2/3/test')
        bucket.upload(UploadSourceBytes(b'test'), '1/2/3/test')
        bucket.upload(UploadSourceBytes(b'test'), '1/2/3/test')
        bucket.upload(UploadSourceBytes(b'test'), '1/2/3/test')
        bucket.upload(UploadSourceBytes(b'test'), '1/2/3/test')
        bucket.upload(UploadSourceBytes(b'check'), 'check')
        bucket.upload(UploadSourceBytes(b'check'), 'check')
        bucket.upload(UploadSourceBytes(b'check'), '1/check')
        bucket.upload(UploadSourceBytes(b'check'), '1/check')
        bucket.upload(UploadSourceBytes(b'check'), '1/2/check')
        bucket.upload(UploadSourceBytes(b'check'), '1/2/check')
        bucket.upload(UploadSourceBytes(b'check'), '1/2/check')
        bucket.upload(UploadSourceBytes(b'check'), '1/2/3/check')
        bucket.upload(UploadSourceBytes(b'check'), '1/2/3/4/check')
        bucket.upload(UploadSourceBytes(b'check'), '1/2/3/4/check')
        bucket.upload(UploadSourceBytes(b'check'), '1/2/3/4/check')

        # Hide some new files. Don't check the results here; it will be clear enough that
        # something has failed if the output of 'get-bucket' does not match the canon.
        stdout, stderr = self._get_stdouterr()
        console_tool = ConsoleTool(self.b2_api, stdout, stderr)
        console_tool.run_command(['b2', 'hide_file', 'my-bucket', '1/hidden1'])
        console_tool.run_command(['b2', 'hide_file', 'my-bucket', '1/hidden1'])
        console_tool.run_command(['b2', 'hide_file', 'my-bucket', '1/hidden2'])
        console_tool.run_command(
            ['b2', 'hide_file', 'my-bucket', '1/2/hidden3'])
        console_tool.run_command(
            ['b2', 'hide_file', 'my-bucket', '1/2/hidden3'])
        console_tool.run_command(
            ['b2', 'hide_file', 'my-bucket', '1/2/hidden3'])
        console_tool.run_command(
            ['b2', 'hide_file', 'my-bucket', '1/2/hidden3'])

        # Now check the output of get-bucket against the canon.
        expected_stdout = '''
        {
            "accountId": "my-account",
            "bucketId": "bucket_0",
            "bucketInfo": {},
            "bucketName": "my-bucket",
            "bucketType": "allPublic",
            "corsRules": [],
            "fileCount": 29,
            "lifecycleRules": [],
            "revision": 1,
            "totalSize": 99
        }
        '''
        self._run_command(['get-bucket', '--showSize', 'my-bucket'],
                          expected_stdout, '', 0)

    def test_sync(self):
        self._authorize_account()
        self._create_my_bucket()

        with TempDir() as temp_dir:
            file_path = os.path.join(temp_dir, 'test.txt')
            with open(file_path, 'wb') as f:
                f.write(six.u('hello world').encode('utf-8'))
            expected_stdout = '''
            upload test.txt
            '''

            command = [
                'sync', '--threads', '5', '--noProgress', temp_dir,
                'b2://my-bucket'
            ]
            self._run_command(command, expected_stdout, '', 0)

    def test_sync_empty_folder_when_not_enabled(self):
        self._authorize_account()
        self._create_my_bucket()
        with TempDir() as temp_dir:
            command = [
                'sync', '--threads', '1', '--noProgress', temp_dir,
                'b2://my-bucket'
            ]
            expected_stderr = 'ERROR: Directory %s is empty.  Use --allowEmptySource to sync anyway.\n' % temp_dir
            self._run_command(command, '', expected_stderr, 1)

    def test_sync_empty_folder_when_enabled(self):
        self._authorize_account()
        self._create_my_bucket()
        with TempDir() as temp_dir:
            command = [
                'sync', '--threads', '1', '--noProgress', '--allowEmptySource',
                temp_dir, 'b2://my-bucket'
            ]
            self._run_command(command, '', '', 0)

    def test_sync_syntax_error(self):
        self._authorize_account()
        self._create_my_bucket()
        expected_stderr = 'ERROR: --includeRegex cannot be used without --excludeRegex at the same time\n'
        self._run_command([
            'sync', '--includeRegex', '.incl', 'non-existent-local-folder',
            'b2://my-bucket'
        ],
                          expected_stderr=expected_stderr,
                          expected_status=1)

    def test_sync_dry_run(self):
        self._authorize_account()
        self._create_my_bucket()

        with TempDir() as temp_dir:
            temp_file = self._make_local_file(temp_dir, 'test-dry-run.txt')

            # dry-run
            expected_stdout = '''
            upload test-dry-run.txt
            '''
            command = [
                'sync', '--noProgress', '--dryRun', temp_dir, 'b2://my-bucket'
            ]
            self._run_command(command, expected_stdout, '', 0)

            # file should not have been uploaded
            expected_stdout = '''
            {
              "files": [],
              "nextFileName": null
            }
            '''
            self._run_command(['list_file_names', 'my-bucket'],
                              expected_stdout, '', 0)

            # upload file
            expected_stdout = '''
            upload test-dry-run.txt
            '''
            command = ['sync', '--noProgress', temp_dir, 'b2://my-bucket']
            self._run_command(command, expected_stdout, '', 0)

            # file should have been uploaded
            mtime = file_mod_time_millis(temp_file)
            expected_stdout = '''
            {
              "files": [
                {
                  "action": "upload",
                  "contentSha1": "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed",
                  "contentType": "b2/x-auto",
                  "fileId": "9999",
                  "fileInfo": {
                    "src_last_modified_millis": "%d"
                  },
                  "fileName": "test-dry-run.txt",
                  "size": 11,
                  "uploadTimestamp": 5000
                }
              ],
              "nextFileName": null
            }
            ''' % (mtime)
            self._run_command(['list_file_names', 'my-bucket'],
                              expected_stdout, '', 0)

    def test_ls(self):
        self._authorize_account()
        self._create_my_bucket()

        # Check with no files
        self._run_command(['ls', 'my-bucket'], '', '', 0)

        # Create some files, including files in a folder
        bucket = self.b2_api.get_bucket_by_name('my-bucket')
        bucket.upload(UploadSourceBytes(b''), 'a')
        bucket.upload(UploadSourceBytes(b' '), 'b/b1')
        bucket.upload(UploadSourceBytes(b'   '), 'b/b2')
        bucket.upload(UploadSourceBytes(b'     '), 'c')
        bucket.upload(UploadSourceBytes(b'      '), 'c')

        # Condensed output
        expected_stdout = '''
        a
        b/
        c
        '''
        self._run_command(['ls', 'my-bucket'], expected_stdout, '', 0)

        # Recursive output
        expected_stdout = '''
        a
        b/b1
        b/b2
        c
        '''
        self._run_command(['ls', '--recursive', 'my-bucket'], expected_stdout,
                          '', 0)

        # Check long output.   (The format expects full-length file ids, so it causes whitespace here)
        expected_stdout = '''
                                                                                       9999  upload  1970-01-01  00:00:05          0  a
                                                                                          -       -           -         -          0  b/
                                                                                       9995  upload  1970-01-01  00:00:05          6  c
        '''
        self._run_command(['ls', '--long', 'my-bucket'], expected_stdout, '',
                          0)

        # Check long versions output   (The format expects full-length file ids, so it causes whitespace here)
        expected_stdout = '''
                                                                                       9999  upload  1970-01-01  00:00:05          0  a
                                                                                          -       -           -         -          0  b/
                                                                                       9995  upload  1970-01-01  00:00:05          6  c
                                                                                       9996  upload  1970-01-01  00:00:05          5  c
        '''
        self._run_command(['ls', '--long', '--versions', 'my-bucket'],
                          expected_stdout, '', 0)

    def _authorize_account(self):
        """
        Prepare for a test by authorizing an account and getting an
        account auth token
        """
        self._run_command_no_checks(
            ['authorize_account', 'my-account', 'good-app-key'])

    def _create_my_bucket(self):
        self._run_command(['create_bucket', 'my-bucket', 'allPublic'],
                          'bucket_0\n', '', 0)

    def _run_command(self,
                     argv,
                     expected_stdout='',
                     expected_stderr='',
                     expected_status=0):
        """
        Runs one command using the ConsoleTool, checking stdout, stderr, and
        the returned status code.

        The ConsoleTool is stateless, so we can make a new one for each
        call, with a fresh stdout and stderr
        """
        expected_stdout = self._trim_leading_spaces(expected_stdout)
        expected_stderr = self._trim_leading_spaces(expected_stderr)
        stdout, stderr = self._get_stdouterr()
        console_tool = ConsoleTool(self.b2_api, stdout, stderr)
        actual_status = console_tool.run_command(['b2'] + argv)

        # The json module in Python 2.6 includes trailing spaces.  Later version of Python don't.
        actual_stdout = self._trim_trailing_spaces(stdout.getvalue())
        actual_stderr = self._trim_trailing_spaces(stderr.getvalue())

        if expected_stdout != actual_stdout:
            print('EXPECTED STDOUT:', repr(expected_stdout))
            print('ACTUAL STDOUT:  ', repr(actual_stdout))
        if expected_stderr != actual_stderr:
            print('EXPECTED STDERR:', repr(expected_stderr))
            print('ACTUAL STDERR:  ', repr(actual_stderr))

        self.assertEqual(expected_stdout, actual_stdout, 'stdout')
        self.assertEqual(expected_stderr, actual_stderr, 'stderr')
        self.assertEqual(expected_status, actual_status, 'exit status code')

    def test_bad_terminal(self):
        stdout = mock.MagicMock()
        stdout.write = mock.MagicMock(side_effect=[
            UnicodeEncodeError('codec', u'foo', 100, 105,
                               'artificial UnicodeEncodeError')
        ] + list(range(25)))
        stderr = mock.MagicMock()
        console_tool = ConsoleTool(self.b2_api, stdout, stderr)
        console_tool.run_command(
            ['b2', 'authorize_account', 'my-account', 'good-app-key'])

    def _get_stdouterr(self):
        class MyStringIO(six.StringIO):
            if six.PY2:  # python3 already has this attribute
                encoding = 'fake_encoding'

        stdout = MyStringIO()
        stderr = MyStringIO()
        return stdout, stderr

    def _run_command_no_checks(self, argv):
        stdout, stderr = self._get_stdouterr()
        ConsoleTool(self.b2_api, stdout, stderr).run_command(['b2'] + argv)

    def _trim_leading_spaces(self, s):
        """
        Takes the contents of a triple-quoted string, and removes the leading
        newline and leading spaces that come from it being indented with code.
        """
        # The first line starts on the line following the triple
        # quote, so the first line after splitting can be discarded.
        lines = s.split('\n')
        if lines[0] == '':
            lines = lines[1:]
        if len(lines) == 0:
            return ''

        # Count the leading spaces
        space_count = min(
            self._leading_spaces(line) for line in lines if line != '')

        # Remove the leading spaces from each line, based on the line
        # with the fewest leading spaces
        leading_spaces = ' ' * space_count
        assert all(
            line.startswith(leading_spaces) or line == ''
            for line in lines), 'all lines have leading spaces'
        return '\n'.join('' if line == '' else line[space_count:]
                         for line in lines)

    def _leading_spaces(self, s):
        space_count = 0
        while space_count < len(s) and s[space_count] == ' ':
            space_count += 1
        return space_count

    def _trim_trailing_spaces(self, s):
        return '\n'.join(line.rstrip() for line in s.split('\n'))

    def _make_local_file(self, temp_dir, file_name):
        local_path = os.path.join(temp_dir, file_name)
        with open(local_path, 'wb') as f:
            f.write(six.b('hello world'))
        return local_path

    def _read_file(self, local_path):
        with open(local_path, 'rb') as f:
            return f.read()
Esempio n. 7
0
 def setUp(self):
     self.account_info = InMemoryAccountInfo()
     self.cache = DummyCache()
     self.raw_api = RawSimulator()
     self.api = B2Api(self.account_info, self.cache, self.raw_api)
     (self.account_id, self.master_key) = self.raw_api.create_account()
Esempio n. 8
0
class TestApi(TestBase):
    def setUp(self):
        self.account_info = InMemoryAccountInfo()
        self.cache = DummyCache()
        self.raw_api = RawSimulator()
        self.api = B2Api(self.account_info, self.cache, self.raw_api)
        (self.account_id, self.master_key) = self.raw_api.create_account()

    def test_list_buckets(self):
        self._authorize_account()
        self.api.create_bucket('bucket1', 'allPrivate')
        self.api.create_bucket('bucket2', 'allPrivate')
        self.assertEqual(
            ['bucket1', 'bucket2'],
            [b.name for b in self.api.list_buckets()],
        )

    def test_list_buckets_with_name(self):
        self._authorize_account()
        self.api.create_bucket('bucket1', 'allPrivate')
        self.api.create_bucket('bucket2', 'allPrivate')
        self.assertEqual(
            ['bucket1'],
            [b.name for b in self.api.list_buckets(bucket_name='bucket1')],
        )

    def test_list_buckets_with_restriction(self):
        self._authorize_account()
        bucket1 = self.api.create_bucket('bucket1', 'allPrivate')
        self.api.create_bucket('bucket2', 'allPrivate')
        key = self.api.create_key(['listBuckets'], 'key1', bucket_id=bucket1.id_)
        self.api.authorize_account('production', key['applicationKeyId'], key['applicationKey'])
        self.assertEqual(
            ['bucket1'],
            [b.name for b in self.api.list_buckets(bucket_name=bucket1.name)],
        )

    def test_get_bucket_by_name_with_bucket_restriction(self):
        self._authorize_account()
        bucket1 = self.api.create_bucket('bucket1', 'allPrivate')
        key = self.api.create_key(['listBuckets'], 'key1', bucket_id=bucket1.id_)
        self.api.authorize_account('production', key['applicationKeyId'], key['applicationKey'])
        self.assertEqual(
            bucket1.id_,
            self.api.get_bucket_by_name('bucket1').id_,
        )

    def test_list_buckets_with_restriction_and_wrong_name(self):
        self._authorize_account()
        bucket1 = self.api.create_bucket('bucket1', 'allPrivate')
        bucket2 = self.api.create_bucket('bucket2', 'allPrivate')
        key = self.api.create_key(['listBuckets'], 'key1', bucket_id=bucket1.id_)
        self.api.authorize_account('production', key['applicationKeyId'], key['applicationKey'])
        with self.assertRaises(RestrictedBucket):
            self.api.list_buckets(bucket_name=bucket2.name)

    def test_list_buckets_with_restriction_and_no_name(self):
        self._authorize_account()
        bucket1 = self.api.create_bucket('bucket1', 'allPrivate')
        self.api.create_bucket('bucket2', 'allPrivate')
        key = self.api.create_key(['listBuckets'], 'key1', bucket_id=bucket1.id_)
        self.api.authorize_account('production', key['applicationKeyId'], key['applicationKey'])
        with self.assertRaises(RestrictedBucket):
            self.api.list_buckets()

    def _authorize_account(self):
        self.api.authorize_account('production', self.account_id, self.master_key)
 def setUp(self):
     self.account_info = StubAccountInfo()
     self.cache = InMemoryCache()
     self.raw_api = RawSimulator()
     self.b2_api = B2Api(self.account_info, self.cache, self.raw_api)
class TestConsoleTool(unittest.TestCase):
    def setUp(self):
        self.account_info = StubAccountInfo()
        self.cache = InMemoryCache()
        self.raw_api = RawSimulator()
        self.b2_api = B2Api(self.account_info, self.cache, self.raw_api)

    def test_authorize_with_bad_key(self):
        expected_stdout = '''
        Using http://production.example.com
        '''

        expected_stderr = '''
        ERROR: unable to authorize account: Invalid authorization token. Server said: invalid application key: bad-app-key (bad_auth_token)
        '''

        self._run_command(
            ['authorize_account', 'my-account', 'bad-app-key'], expected_stdout, expected_stderr, 1
        )

    def test_authorize_with_good_key(self):
        # Initial condition
        assert self.account_info.get_account_auth_token() is None

        # Authorize an account with a good api key.
        expected_stdout = """
        Using http://production.example.com
        """

        self._run_command(
            ['authorize_account', 'my-account', 'good-app-key'], expected_stdout, '', 0
        )

        # Auth token should be in account info now
        assert self.account_info.get_account_auth_token() is not None

    def test_help_with_bad_args(self):
        expected_stderr = '''

        b2 create_bucket <bucketName> [allPublic | allPrivate]

            Creates a new bucket.  Prints the ID of the bucket created.

        '''

        self._run_command(['create_bucket'], '', expected_stderr, 1)

    def test_clear_account(self):
        # Initial condition
        self._authorize_account()
        assert self.account_info.get_account_auth_token() is not None

        # Clearing the account should remove the auth token
        # from the account info.
        self._run_command(['clear_account'], '', '', 0)
        assert self.account_info.get_account_auth_token() is None

    def test_buckets(self):
        self._authorize_account()

        # Make a bucket with an illegal name
        expected_stdout = 'ERROR: Bad request: illegal bucket name: bad/bucket/name\n'
        self._run_command(['create_bucket', 'bad/bucket/name', 'allPublic'], '', expected_stdout, 1)

        # Make two buckets
        self._run_command(['create_bucket', 'my-bucket', 'allPrivate'], 'bucket_0\n', '', 0)
        self._run_command(['create_bucket', 'your-bucket', 'allPrivate'], 'bucket_1\n', '', 0)

        # Update one of them
        expected_stdout = '''
        {
            "accountId": "my-account",
            "bucketId": "bucket_0",
            "bucketName": "my-bucket",
            "bucketType": "allPublic"
        }
        '''

        self._run_command(['update_bucket', 'my-bucket', 'allPublic'], expected_stdout, '', 0)

        # Make sure they are there
        expected_stdout = '''
        bucket_0  allPublic   my-bucket
        bucket_1  allPrivate  your-bucket
        '''

        self._run_command(['list_buckets'], expected_stdout, '', 0)

        # Delete one
        expected_stdout = '''
        {
            "accountId": "my-account",
            "bucketId": "bucket_1",
            "bucketName": "your-bucket",
            "bucketType": "allPrivate"
        }
        '''

        self._run_command(['delete_bucket', 'your-bucket'], expected_stdout, '', 0)

    def test_cancel_large_file(self):
        self._authorize_account()
        self._create_my_bucket()
        bucket = self.b2_api.get_bucket_by_name('my-bucket')
        file = bucket.start_large_file('file1', 'text/plain', {})
        self._run_command(['cancel_large_file', file.file_id], '9999 canceled\n', '', 0)

    def test_cancel_all_large_file(self):
        self._authorize_account()
        self._create_my_bucket()
        bucket = self.b2_api.get_bucket_by_name('my-bucket')
        bucket.start_large_file('file1', 'text/plain', {})
        bucket.start_large_file('file2', 'text/plain', {})
        expected_stdout = '''
        9999 canceled
        9998 canceled
        '''

        self._run_command(
            ['cancel_all_unfinished_large_files', 'my-bucket'], expected_stdout, '', 0
        )

    def test_files(self):

        self._authorize_account()
        self._run_command(['create_bucket', 'my-bucket', 'allPublic'], 'bucket_0\n', '', 0)

        with TempDir() as temp_dir:
            local_file1 = self._make_local_file(temp_dir, 'file1.txt')

            # Upload a file
            expected_stdout = '''
            URL by file name: http://download.example.com/file/my-bucket/file1.txt
            URL by fileId: http://download.example.com/b2api/v1/b2_download_file_by_id?fileId=9999
            {
              "action": "upload",
              "fileId": "9999",
              "fileName": "file1.txt",
              "size": 11,
              "uploadTimestamp": 5000
            }
            '''

            self._run_command(
                [
                    'upload_file', '--noProgress', 'my-bucket', local_file1, 'file1.txt'
                ], expected_stdout, '', 0
            )

            # Download by name
            local_download1 = os.path.join(temp_dir, 'download1.txt')
            expected_stdout = '''
            File name:    file1.txt
            File id:      9999
            File size:    11
            Content type: b2/x-auto
            Content sha1: 2aae6c35c94fcfb415dbe95f408b9ce91ee846ed
            checksum matches
            '''

            self._run_command(
                [
                    'download_file_by_name', '--noProgress', 'my-bucket', 'file1.txt',
                    local_download1
                ], expected_stdout, '', 0
            )
            self.assertEquals(six.b('hello world'), self._read_file(local_download1))

            # Download file by ID.  (Same expected output as downloading by name)
            local_download2 = os.path.join(temp_dir, 'download2.txt')
            self._run_command(
                [
                    'download_file_by_id', '--noProgress', '9999', local_download2
                ], expected_stdout, '', 0
            )
            self.assertEquals(six.b('hello world'), self._read_file(local_download2))

            # Hide the file
            expected_stdout = '''
            {
              "action": "hide",
              "fileId": "9998",
              "fileName": "file1.txt",
              "size": 0,
              "uploadTimestamp": 5001
            }
            '''

            self._run_command(['hide_file', 'my-bucket', 'file1.txt'], expected_stdout, '', 0)

            # List the file versions
            expected_stdout = '''
            {
              "files": [
                {
                  "action": "hide",
                  "contentSha1": "none",
                  "contentType": null,
                  "fileId": "9998",
                  "fileInfo": {},
                  "fileName": "file1.txt",
                  "size": 0,
                  "uploadTimestamp": 5001
                },
                {
                  "action": "upload",
                  "contentSha1": "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed",
                  "contentType": "b2/x-auto",
                  "fileId": "9999",
                  "fileInfo": {},
                  "fileName": "file1.txt",
                  "size": 11,
                  "uploadTimestamp": 5000
                }
              ],
              "nextFileId": null,
              "nextFileName": null
            }
            '''

            self._run_command(['list_file_versions', 'my-bucket'], expected_stdout, '', 0)

            # List the file names
            expected_stdout = '''
            {
              "files": [],
              "nextFileName": null
            }
            '''

            self._run_command(['list_file_names', 'my-bucket'], expected_stdout, '', 0)

            # Delete one file version
            expected_stdout = '''
            {
              "action": "delete",
              "fileId": "9998",
              "fileName": "file1.txt"
            }
            '''

            self._run_command(['delete_file_version', 'file1.txt', '9998'], expected_stdout, '', 0)

    def test_list_parts_with_none(self):
        self._authorize_account()
        self._create_my_bucket()
        bucket = self.b2_api.get_bucket_by_name('my-bucket')
        file = bucket.start_large_file('file', 'text/plain', {})
        self._run_command(['list_parts', file.file_id], '', '', 0)

    def test_list_parts_with_parts(self):
        self._authorize_account()
        self._create_my_bucket()
        bucket = self.b2_api.get_bucket_by_name('my-bucket')
        file = bucket.start_large_file('file', 'text/plain', {})
        content = six.b('hello world')
        large_file_upload_state = mock.MagicMock()
        large_file_upload_state.has_error.return_value = False
        bucket._upload_part(
            file.file_id, 1, (0, 11), UploadSourceBytes(content), large_file_upload_state
        )
        bucket._upload_part(
            file.file_id, 3, (0, 11), UploadSourceBytes(content), large_file_upload_state
        )
        expected_stdout = '''
            1         11  2aae6c35c94fcfb415dbe95f408b9ce91ee846ed
            3         11  2aae6c35c94fcfb415dbe95f408b9ce91ee846ed
        '''

        self._run_command(['list_parts', file.file_id], expected_stdout, '', 0)

    def test_list_unfinished_large_files_with_none(self):
        self._authorize_account()
        self._create_my_bucket()
        self._run_command(['list_unfinished_large_files', 'my-bucket'], '', '', 0)

    def test_list_unfinished_large_files_with_some(self):
        self._authorize_account()
        self._create_my_bucket()
        api_url = self.account_info.get_api_url()
        auth_token = self.account_info.get_account_auth_token()
        self.raw_api.start_large_file(api_url, auth_token, 'bucket_0', 'file1', 'text/plain', {})
        self.raw_api.start_large_file(
            api_url, auth_token, 'bucket_0', 'file2', 'text/plain', {'color': 'blue'}
        )
        self.raw_api.start_large_file(
            api_url, auth_token, 'bucket_0', 'file3', 'application/json', {}
        )
        expected_stdout = '''
        9999 file1 text/plain
        9998 file2 text/plain color=blue
        9997 file3 application/json
        '''

        self._run_command(['list_unfinished_large_files', 'my-bucket'], expected_stdout, '', 0)

    def test_upload_large_file(self):
        self._authorize_account()
        self._create_my_bucket()
        min_part_size = self.account_info.get_minimum_part_size()
        file_size = min_part_size * 3

        with TempDir() as temp_dir:
            file_path = os.path.join(temp_dir, 'test.txt')
            text = six.u('*') * file_size
            with open(file_path, 'wb') as f:
                f.write(text.encode('utf-8'))
            expected_stdout = '''
            URL by file name: http://download.example.com/file/my-bucket/test.txt
            URL by fileId: http://download.example.com/b2api/v1/b2_download_file_by_id?fileId=9999
            {
              "action": "upload",
              "fileId": "9999",
              "fileName": "test.txt",
              "size": 600,
              "uploadTimestamp": 5000
            }
            '''

            self._run_command(
                [
                    'upload_file', '--noProgress', '--threads', '5', 'my-bucket', file_path,
                    'test.txt'
                ], expected_stdout, '', 0
            )

    def test_sync(self):
        self._authorize_account()
        self._create_my_bucket()

        with TempDir() as temp_dir:
            file_path = os.path.join(temp_dir, 'test.txt')
            with open(file_path, 'wb') as f:
                f.write(six.u('hello world').encode('utf-8'))
            expected_stdout = '''
            upload test.txt
            '''

            command = ['sync', '--threads', '5', '--noProgress', temp_dir, 'b2://my-bucket']
            self._run_command(command, expected_stdout, '', 0)

    def _authorize_account(self):
        """
        Prepare for a test by authorizing an account and getting an
        account auth token
        """
        self._run_command_no_checks(['authorize_account', 'my-account', 'good-app-key'])

    def _create_my_bucket(self):
        self._run_command(['create_bucket', 'my-bucket', 'allPublic'], 'bucket_0\n', '', 0)

    def _run_command(self, argv, expected_stdout='', expected_stderr='', expected_status=0):
        """
        Runs one command using the ConsoleTool, checking stdout, stderr, and
        the returned status code.

        The ConsoleTool is stateless, so we can make a new one for each
        call, with a fresh stdout and stderr
        """
        expected_stdout = self._trim_leading_spaces(expected_stdout)
        expected_stderr = self._trim_leading_spaces(expected_stderr)
        stdout = six.StringIO()
        stderr = six.StringIO()
        console_tool = ConsoleTool(self.b2_api, stdout, stderr)
        actual_status = console_tool.run_command(['b2'] + argv)

        # The json module in Python 2.6 includes trailing spaces.  Later version of Python don't.
        actual_stdout = self._trim_trailing_spaces(stdout.getvalue())
        actual_stderr = self._trim_trailing_spaces(stderr.getvalue())

        if expected_stdout != actual_stdout:
            print(repr(expected_stdout))
            print(repr(actual_stdout))
        if expected_stderr != actual_stderr:
            print(repr(expected_stderr))
            print(repr(actual_stderr))

        self.assertEqual(expected_stdout, actual_stdout, 'stdout')
        self.assertEqual(expected_stderr, actual_stderr, 'stderr')
        self.assertEqual(expected_status, actual_status, 'exit status code')

    def _run_command_no_checks(self, argv):
        ConsoleTool(self.b2_api, six.StringIO(), six.StringIO()).run_command(['b2'] + argv)

    def _trim_leading_spaces(self, s):
        """
        Takes the contents of a triple-quoted string, and removes the leading
        newline and leading spaces that come from it being indented with code.
        """
        # The first line starts on the line following the triple
        # quote, so the first line after splitting can be discarded.
        lines = s.split('\n')
        if lines[0] == '':
            lines = lines[1:]
        if len(lines) == 0:
            return ''

        # Count the leading spaces
        space_count = min(self._leading_spaces(line) for line in lines if line != '')

        # Remove the leading spaces from each line, based on the line
        # with the fewest leading spaces
        leading_spaces = ' ' * space_count
        assert all(line.startswith(leading_spaces) or line == ''
                   for line in lines), 'all lines have leading spaces'
        return '\n'.join('' if line == '' else line[space_count:] for line in lines)

    def _leading_spaces(self, s):
        space_count = 0
        while space_count < len(s) and s[space_count] == ' ':
            space_count += 1
        return space_count

    def _trim_trailing_spaces(self, s):
        return '\n'.join(line.rstrip() for line in s.split('\n'))

    def _make_local_file(self, temp_dir, file_name):
        local_path = os.path.join(temp_dir, file_name)
        with open(local_path, 'wb') as f:
            f.write(six.b('hello world'))
        return local_path

    def _read_file(self, local_path):
        with open(local_path, 'rb') as f:
            return f.read()
class TestConsoleTool(TestBase):
    def setUp(self):
        self.account_info = StubAccountInfo()
        self.cache = InMemoryCache()
        self.raw_api = RawSimulator()
        self.b2_api = B2Api(self.account_info, self.cache, self.raw_api)

    def test_authorize_with_bad_key(self):
        expected_stdout = '''
        Using http://production.example.com
        '''

        expected_stderr = '''
        ERROR: unable to authorize account: Invalid authorization token. Server said: invalid application key: bad-app-key (bad_auth_token)
        '''

        self._run_command(
            ['authorize_account', 'my-account', 'bad-app-key'], expected_stdout, expected_stderr, 1
        )

    def test_authorize_with_good_key_using_hyphen(self):
        # Initial condition
        assert self.account_info.get_account_auth_token() is None

        # Authorize an account with a good api key.
        expected_stdout = """
        Using http://production.example.com
        """

        self._run_command(
            ['authorize-account', 'my-account', 'good-app-key'], expected_stdout, '', 0
        )

        # Auth token should be in account info now
        assert self.account_info.get_account_auth_token() is not None

    def test_authorize_with_good_key_using_underscore(self):
        # Initial condition
        assert self.account_info.get_account_auth_token() is None

        # Authorize an account with a good api key.
        expected_stdout = """
        Using http://production.example.com
        """

        self._run_command(
            ['authorize-account', 'my-account', 'good-app-key'], expected_stdout, '', 0
        )

        # Auth token should be in account info now
        assert self.account_info.get_account_auth_token() is not None

    def test_help_with_bad_args(self):
        expected_stderr = '''

        b2 list-parts <largeFileId>

            Lists all of the parts that have been uploaded for the given
            large file, which must be a file that was started but not
            finished or canceled.

        '''

        self._run_command(['list_parts'], '', expected_stderr, 1)

    def test_clear_account(self):
        # Initial condition
        self._authorize_account()
        assert self.account_info.get_account_auth_token() is not None

        # Clearing the account should remove the auth token
        # from the account info.
        self._run_command(['clear-account'], '', '', 0)
        assert self.account_info.get_account_auth_token() is None

    def test_buckets(self):
        self._authorize_account()

        # Make a bucket with an illegal name
        expected_stdout = 'ERROR: Bad request: illegal bucket name: bad/bucket/name\n'
        self._run_command(['create_bucket', 'bad/bucket/name', 'allPublic'], '', expected_stdout, 1)

        # Make two buckets
        self._run_command(['create_bucket', 'my-bucket', 'allPrivate'], 'bucket_0\n', '', 0)
        self._run_command(['create_bucket', 'your-bucket', 'allPrivate'], 'bucket_1\n', '', 0)

        # Update one of them
        expected_stdout = '''
        {
            "accountId": "my-account",
            "bucketId": "bucket_0",
            "bucketInfo": {},
            "bucketName": "my-bucket",
            "bucketType": "allPublic",
            "lifecycleRules": [],
            "revision": 2
        }
        '''

        self._run_command(['update_bucket', 'my-bucket', 'allPublic'], expected_stdout, '', 0)

        # Make sure they are there
        expected_stdout = '''
        bucket_0  allPublic   my-bucket
        bucket_1  allPrivate  your-bucket
        '''

        self._run_command(['list_buckets'], expected_stdout, '', 0)

        # Delete one
        expected_stdout = '''
        {
            "accountId": "my-account",
            "bucketId": "bucket_1",
            "bucketInfo": {},
            "bucketName": "your-bucket",
            "bucketType": "allPrivate",
            "lifecycleRules": [],
            "revision": 1
        }
        '''

        self._run_command(['delete_bucket', 'your-bucket'], expected_stdout, '', 0)

    def test_bucket_info_from_json(self):

        self._authorize_account()
        self._run_command(['create_bucket', 'my-bucket', 'allPublic'], 'bucket_0\n', '', 0)

        bucket_info = {'color': 'blue'}

        expected_stdout = '''
            {
                "accountId": "my-account",
                "bucketId": "bucket_0",
                "bucketInfo": {
                    "color": "blue"
                },
                "bucketName": "my-bucket",
                "bucketType": "allPrivate",
                "lifecycleRules": [],
                "revision": 2
            }
            '''
        self._run_command(
            ['update_bucket', '--bucketInfo', json.dumps(bucket_info), 'my-bucket', 'allPrivate'],
            expected_stdout, '', 0
        )

    def test_cancel_large_file(self):
        self._authorize_account()
        self._create_my_bucket()
        bucket = self.b2_api.get_bucket_by_name('my-bucket')
        file = bucket.start_large_file('file1', 'text/plain', {})
        self._run_command(['cancel_large_file', file.file_id], '9999 canceled\n', '', 0)

    def test_cancel_all_large_file(self):
        self._authorize_account()
        self._create_my_bucket()
        bucket = self.b2_api.get_bucket_by_name('my-bucket')
        bucket.start_large_file('file1', 'text/plain', {})
        bucket.start_large_file('file2', 'text/plain', {})
        expected_stdout = '''
        9999 canceled
        9998 canceled
        '''

        self._run_command(
            ['cancel_all_unfinished_large_files', 'my-bucket'], expected_stdout, '', 0
        )

    def test_files(self):

        self._authorize_account()
        self._run_command(['create_bucket', 'my-bucket', 'allPublic'], 'bucket_0\n', '', 0)

        with TempDir() as temp_dir:
            local_file1 = self._make_local_file(temp_dir, 'file1.txt')

            # Upload a file
            expected_stdout = '''
            URL by file name: http://download.example.com/file/my-bucket/file1.txt
            URL by fileId: http://download.example.com/b2api/v1/b2_download_file_by_id?fileId=9999
            {
              "action": "upload",
              "fileId": "9999",
              "fileName": "file1.txt",
              "size": 11,
              "uploadTimestamp": 5000
            }
            '''

            self._run_command(
                ['upload_file', '--noProgress', 'my-bucket', local_file1, 'file1.txt'],
                expected_stdout, '', 0
            )

            # Get file info
            mod_time_str = str(int(os.path.getmtime(local_file1) * 1000))
            expected_stdout = '''
            {
              "accountId": "my-account",
              "action": "upload",
              "bucketId": "bucket_0",
              "contentLength": 11,
              "contentSha1": "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed",
              "contentType": "b2/x-auto",
              "fileId": "9999",
              "fileInfo": {
                "src_last_modified_millis": "%s"
              },
              "fileName": "file1.txt",
              "uploadTimestamp": 5000
            }
            ''' % (mod_time_str,)

            self._run_command(['get_file_info', '9999'], expected_stdout, '', 0)

            # Download by name
            local_download1 = os.path.join(temp_dir, 'download1.txt')
            expected_stdout = '''
            File name:    file1.txt
            File id:      9999
            File size:    11
            Content type: b2/x-auto
            Content sha1: 2aae6c35c94fcfb415dbe95f408b9ce91ee846ed
            INFO src_last_modified_millis: %s
            checksum matches
            ''' % (mod_time_str,)

            self._run_command(
                [
                    'download_file_by_name', '--noProgress', 'my-bucket', 'file1.txt',
                    local_download1
                ], expected_stdout, '', 0
            )
            self.assertEquals(six.b('hello world'), self._read_file(local_download1))

            # Download file by ID.  (Same expected output as downloading by name)
            local_download2 = os.path.join(temp_dir, 'download2.txt')
            self._run_command(
                ['download_file_by_id', '--noProgress', '9999', local_download2], expected_stdout,
                '', 0
            )
            self.assertEquals(six.b('hello world'), self._read_file(local_download2))

            # Hide the file
            expected_stdout = '''
            {
              "action": "hide",
              "fileId": "9998",
              "fileName": "file1.txt",
              "size": 0,
              "uploadTimestamp": 5001
            }
            '''

            self._run_command(['hide_file', 'my-bucket', 'file1.txt'], expected_stdout, '', 0)

            # List the file versions
            expected_stdout = '''
            {
              "files": [
                {
                  "action": "hide",
                  "contentSha1": "none",
                  "contentType": null,
                  "fileId": "9998",
                  "fileInfo": {},
                  "fileName": "file1.txt",
                  "size": 0,
                  "uploadTimestamp": 5001
                },
                {
                  "action": "upload",
                  "contentSha1": "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed",
                  "contentType": "b2/x-auto",
                  "fileId": "9999",
                  "fileInfo": {
                    "src_last_modified_millis": "%s"
                  },
                  "fileName": "file1.txt",
                  "size": 11,
                  "uploadTimestamp": 5000
                }
              ],
              "nextFileId": null,
              "nextFileName": null
            }
            ''' % (mod_time_str,)

            self._run_command(['list_file_versions', 'my-bucket'], expected_stdout, '', 0)

            # List the file names
            expected_stdout = '''
            {
              "files": [],
              "nextFileName": null
            }
            '''

            self._run_command(['list_file_names', 'my-bucket'], expected_stdout, '', 0)

            # Delete one file version, passing the name in
            expected_stdout = '''
            {
              "action": "delete",
              "fileId": "9998",
              "fileName": "file1.txt"
            }
            '''

            self._run_command(['delete_file_version', 'file1.txt', '9998'], expected_stdout, '', 0)

            # Delete one file version, not passing the name in
            expected_stdout = '''
            {
              "action": "delete",
              "fileId": "9999",
              "fileName": "file1.txt"
            }
            '''

            self._run_command(['delete_file_version', '9999'], expected_stdout, '', 0)

    def test_get_download_auth_defaults(self):
        self._authorize_account()
        self._create_my_bucket()
        self._run_command(
            ['get_download_auth', 'my-bucket'], 'fake_download_auth_token_bucket_0__86400\n', '', 0
        )

    def test_get_download_auth_explicit(self):
        self._authorize_account()
        self._create_my_bucket()
        self._run_command(
            ['get_download_auth', '--prefix', 'prefix', '--duration', '12345', 'my-bucket'],
            'fake_download_auth_token_bucket_0_prefix_12345\n', '', 0
        )

    def test_list_parts_with_none(self):
        self._authorize_account()
        self._create_my_bucket()
        bucket = self.b2_api.get_bucket_by_name('my-bucket')
        file = bucket.start_large_file('file', 'text/plain', {})
        self._run_command(['list_parts', file.file_id], '', '', 0)

    def test_list_parts_with_parts(self):
        self._authorize_account()
        self._create_my_bucket()
        bucket = self.b2_api.get_bucket_by_name('my-bucket')
        file = bucket.start_large_file('file', 'text/plain', {})
        content = six.b('hello world')
        large_file_upload_state = mock.MagicMock()
        large_file_upload_state.has_error.return_value = False
        bucket._upload_part(
            file.file_id, 1, (0, 11), UploadSourceBytes(content), large_file_upload_state
        )
        bucket._upload_part(
            file.file_id, 3, (0, 11), UploadSourceBytes(content), large_file_upload_state
        )
        expected_stdout = '''
            1         11  2aae6c35c94fcfb415dbe95f408b9ce91ee846ed
            3         11  2aae6c35c94fcfb415dbe95f408b9ce91ee846ed
        '''

        self._run_command(['list_parts', file.file_id], expected_stdout, '', 0)

    def test_list_unfinished_large_files_with_none(self):
        self._authorize_account()
        self._create_my_bucket()
        self._run_command(['list_unfinished_large_files', 'my-bucket'], '', '', 0)

    def test_list_unfinished_large_files_with_some(self):
        self._authorize_account()
        self._create_my_bucket()
        api_url = self.account_info.get_api_url()
        auth_token = self.account_info.get_account_auth_token()
        self.raw_api.start_large_file(api_url, auth_token, 'bucket_0', 'file1', 'text/plain', {})
        self.raw_api.start_large_file(
            api_url, auth_token, 'bucket_0', 'file2', 'text/plain', {'color': 'blue'}
        )
        self.raw_api.start_large_file(
            api_url, auth_token, 'bucket_0', 'file3', 'application/json', {}
        )
        expected_stdout = '''
        9999 file1 text/plain
        9998 file2 text/plain color=blue
        9997 file3 application/json
        '''

        self._run_command(['list_unfinished_large_files', 'my-bucket'], expected_stdout, '', 0)

    def test_upload_large_file(self):
        self._authorize_account()
        self._create_my_bucket()
        min_part_size = self.account_info.get_minimum_part_size()
        file_size = min_part_size * 3

        with TempDir() as temp_dir:
            file_path = os.path.join(temp_dir, 'test.txt')
            text = six.u('*') * file_size
            with open(file_path, 'wb') as f:
                f.write(text.encode('utf-8'))
            expected_stdout = '''
            URL by file name: http://download.example.com/file/my-bucket/test.txt
            URL by fileId: http://download.example.com/b2api/v1/b2_download_file_by_id?fileId=9999
            {
              "action": "upload",
              "fileId": "9999",
              "fileName": "test.txt",
              "size": 600,
              "uploadTimestamp": 5000
            }
            '''

            self._run_command(
                [
                    'upload_file', '--noProgress', '--threads', '5', 'my-bucket', file_path,
                    'test.txt'
                ], expected_stdout, '', 0
            )

    def test_show_account_info(self):
        self._authorize_account()
        expected_stdout = '''
        Account ID:         my-account
        Application Key:    good-app-key
        Account Auth Token: AUTH:my-account
        API URL:            http://api.example.com
        Download URL:       http://download.example.com
        '''
        self._run_command(['show-account-info'], expected_stdout, '', 0)

    def test_sync(self):
        self._authorize_account()
        self._create_my_bucket()

        with TempDir() as temp_dir:
            file_path = os.path.join(temp_dir, 'test.txt')
            with open(file_path, 'wb') as f:
                f.write(six.u('hello world').encode('utf-8'))
            expected_stdout = '''
            upload test.txt
            '''

            command = ['sync', '--threads', '5', '--noProgress', temp_dir, 'b2://my-bucket']
            self._run_command(command, expected_stdout, '', 0)

    def test_sync_syntax_error(self):
        self._authorize_account()
        self._create_my_bucket()
        expected_stderr = 'ERROR: --includeRegex cannot be used without --excludeRegex at the same time\n'
        self._run_command(
            ['sync', '--includeRegex', '.incl', 'non-existent-local-folder', 'b2://my-bucket'],
            expected_stderr=expected_stderr,
            expected_status=1
        )

    def test_sync_dry_run(self):
        self._authorize_account()
        self._create_my_bucket()

        with TempDir() as temp_dir:
            temp_file = self._make_local_file(temp_dir, 'test-dry-run.txt')

            # dry-run
            expected_stdout = '''
            upload test-dry-run.txt
            '''
            command = ['sync', '--noProgress', '--dryRun', temp_dir, 'b2://my-bucket']
            self._run_command(command, expected_stdout, '', 0)

            # file should not have been uploaded
            expected_stdout = '''
            {
              "files": [],
              "nextFileName": null
            }
            '''
            self._run_command(['list_file_names', 'my-bucket'], expected_stdout, '', 0)

            # upload file
            expected_stdout = '''
            upload test-dry-run.txt
            '''
            command = ['sync', '--noProgress', temp_dir, 'b2://my-bucket']
            self._run_command(command, expected_stdout, '', 0)

            # file should have been uploaded
            mtime = file_mod_time_millis(temp_file)
            expected_stdout = '''
            {
              "files": [
                {
                  "action": "upload",
                  "contentSha1": "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed",
                  "contentType": "b2/x-auto",
                  "fileId": "9999",
                  "fileInfo": {
                    "src_last_modified_millis": "%d"
                  },
                  "fileName": "test-dry-run.txt",
                  "size": 11,
                  "uploadTimestamp": 5000
                }
              ],
              "nextFileName": null
            }
            ''' % (mtime)
            self._run_command(['list_file_names', 'my-bucket'], expected_stdout, '', 0)

    def _authorize_account(self):
        """
        Prepare for a test by authorizing an account and getting an
        account auth token
        """
        self._run_command_no_checks(['authorize_account', 'my-account', 'good-app-key'])

    def _create_my_bucket(self):
        self._run_command(['create_bucket', 'my-bucket', 'allPublic'], 'bucket_0\n', '', 0)

    def _run_command(self, argv, expected_stdout='', expected_stderr='', expected_status=0):
        """
        Runs one command using the ConsoleTool, checking stdout, stderr, and
        the returned status code.

        The ConsoleTool is stateless, so we can make a new one for each
        call, with a fresh stdout and stderr
        """
        expected_stdout = self._trim_leading_spaces(expected_stdout)
        expected_stderr = self._trim_leading_spaces(expected_stderr)
        stdout, stderr = self._get_stdouterr()
        console_tool = ConsoleTool(self.b2_api, stdout, stderr)
        actual_status = console_tool.run_command(['b2'] + argv)

        # The json module in Python 2.6 includes trailing spaces.  Later version of Python don't.
        actual_stdout = self._trim_trailing_spaces(stdout.getvalue())
        actual_stderr = self._trim_trailing_spaces(stderr.getvalue())

        if expected_stdout != actual_stdout:
            print(repr(expected_stdout))
            print(repr(actual_stdout))
        if expected_stderr != actual_stderr:
            print(repr(expected_stderr))
            print(repr(actual_stderr))

        self.assertEqual(expected_stdout, actual_stdout, 'stdout')
        self.assertEqual(expected_stderr, actual_stderr, 'stderr')
        self.assertEqual(expected_status, actual_status, 'exit status code')

    def test_bad_terminal(self):
        stdout = mock.MagicMock()
        stdout.write = mock.MagicMock(
            side_effect=[
                UnicodeEncodeError('codec', u'foo', 100, 105, 'artificial UnicodeEncodeError')
            ] + list(range(25))
        )
        stderr = mock.MagicMock()
        console_tool = ConsoleTool(self.b2_api, stdout, stderr)
        console_tool.run_command(['b2', 'authorize_account', 'my-account', 'good-app-key'])

    def _get_stdouterr(self):
        class MyStringIO(six.StringIO):
            if six.PY2:  # python3 already has this attribute
                encoding = 'fake_encoding'

        stdout = MyStringIO()
        stderr = MyStringIO()
        return stdout, stderr

    def _run_command_no_checks(self, argv):
        stdout, stderr = self._get_stdouterr()
        ConsoleTool(self.b2_api, stdout, stderr).run_command(['b2'] + argv)

    def _trim_leading_spaces(self, s):
        """
        Takes the contents of a triple-quoted string, and removes the leading
        newline and leading spaces that come from it being indented with code.
        """
        # The first line starts on the line following the triple
        # quote, so the first line after splitting can be discarded.
        lines = s.split('\n')
        if lines[0] == '':
            lines = lines[1:]
        if len(lines) == 0:
            return ''

        # Count the leading spaces
        space_count = min(self._leading_spaces(line) for line in lines if line != '')

        # Remove the leading spaces from each line, based on the line
        # with the fewest leading spaces
        leading_spaces = ' ' * space_count
        assert all(line.startswith(leading_spaces) or line == ''
                   for line in lines), 'all lines have leading spaces'
        return '\n'.join('' if line == '' else line[space_count:] for line in lines)

    def _leading_spaces(self, s):
        space_count = 0
        while space_count < len(s) and s[space_count] == ' ':
            space_count += 1
        return space_count

    def _trim_trailing_spaces(self, s):
        return '\n'.join(line.rstrip() for line in s.split('\n'))

    def _make_local_file(self, temp_dir, file_name):
        local_path = os.path.join(temp_dir, file_name)
        with open(local_path, 'wb') as f:
            f.write(six.b('hello world'))
        return local_path

    def _read_file(self, local_path):
        with open(local_path, 'rb') as f:
            return f.read()