def setUp(self):
     self.bucket_name = 'my-bucket'
     self.simulator = RawSimulator()
     self.account_info = StubAccountInfo()
     self.api_url = self.account_info.get_api_url()
     self.account_auth_token = self.account_info.get_account_auth_token()
     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(unittest.TestCase):
    def setUp(self):
        self.bucket_name = 'my-bucket'
        self.simulator = RawSimulator()
        self.account_info = StubAccountInfo()
        self.api_url = self.account_info.get_api_url()
        self.account_auth_token = self.account_info.get_account_auth_token()
        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')
 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
            {
              "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 = '''
            {
              "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
            {
              "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()
 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_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_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
            {
              "fileId": "9999",
              "fileName": "file1.txt",
              "size": 11
            }
            '''

            self._run_command(
                ['upload_file', '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', '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', '9999', local_download2], expected_stdout, '', 0
            )
            self.assertEquals(six.b('hello world'), self._read_file(local_download2))

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

            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 _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 _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:]

        # Count the leading spaces
        space_count = 0
        while 0 < len(lines) and space_count < len(lines[0]) and lines[0][space_count] == ' ':
            space_count += 1

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

    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(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_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_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
            {
              "fileId": "9999",
              "fileName": "file1.txt",
              "size": 11
            }
            '''

            self._run_command(
                ['upload_file', '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', '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', '9999', local_download2],
                              expected_stdout, '', 0)
            self.assertEquals(six.b('hello world'),
                              self._read_file(local_download2))

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

            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 _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 _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:]

        # Count the leading spaces
        space_count = 0
        while 0 < len(lines) and space_count < len(
                lines[0]) and lines[0][space_count] == ' ':
            space_count += 1

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

    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(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()