diff --git a/CHANGES.md b/CHANGES.md index 77d6f55..337e0e7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,10 @@ # Changelog +## 2.1.4 + +* Bug fix (#41) that `github.get_contents` action would be failed when decode parameter is set, + and fix encoding processing problem in `github.create_file` and `github.update_file` actions. + ## 2.1.3 * Fix `update_branch_protection` action: dismissal users and teams can now be null in GitHub's API response. diff --git a/actions/create_file.py b/actions/create_file.py index d705fcb..e1177b5 100644 --- a/actions/create_file.py +++ b/actions/create_file.py @@ -1,5 +1,7 @@ +import base64 + from lib.base import BaseGithubAction -from lib.formatters import file_response_to_dict, decode_base64 +from lib.formatters import file_response_to_dict from lib.utils import prep_github_params_for_file_ops __all__ = [ @@ -13,7 +15,7 @@ def run(self, user, repo, path, message, content, branch=None, committer=None, a author, branch, committer = prep_github_params_for_file_ops(author, branch, committer) if encoding and encoding == 'base64': - content = decode_base64(content) + content = base64.b64encode(content.encode('utf-8')) user = self._client.get_user(user) repo = user.get_repo(repo) diff --git a/actions/lib/formatters.py b/actions/lib/formatters.py index 51a9d3b..4d2c2fb 100644 --- a/actions/lib/formatters.py +++ b/actions/lib/formatters.py @@ -11,7 +11,6 @@ 'user_to_dict', 'contents_to_dict', 'file_response_to_dict', - 'decode_base64' ] @@ -211,12 +210,8 @@ def contents_to_dict(contents, decode=False): elif item_type == 'submodule': data['submodule_git_url'] = item.submodule_git_url elif not directory: - encoding = item.encoding - content = item.content - data['encoding'] = encoding - if decode and encoding == 'base64': - content = decode_base64(content) - data['content'] = content + data['encoding'] = item.encoding + data['content'] = item.decoded_content.decode('utf-8') if decode else item.content data['size'] = item.size data['name'] = item.name @@ -234,23 +229,6 @@ def contents_to_dict(contents, decode=False): return result -def decode_base64(data): - """Decode base64, padding being optional. - - :param data: Base64 data as an ASCII byte string - :returns: The decoded byte string. - - """ - missing_padding = len(data) % 4 - if missing_padding != 0: - data += b'=' * (4 - missing_padding) - - import base64 - data = data.encode("utf-8") - data = base64.b64decode(data).decode("utf-8") - return data - - def file_response_to_dict(response): result = {'commit': response['commit'].sha} return result diff --git a/actions/update_file.py b/actions/update_file.py index b36fc45..60c12e0 100644 --- a/actions/update_file.py +++ b/actions/update_file.py @@ -1,5 +1,7 @@ +import base64 + from lib.base import BaseGithubAction -from lib.formatters import file_response_to_dict, decode_base64 +from lib.formatters import file_response_to_dict from lib.utils import prep_github_params_for_file_ops __all__ = [ @@ -13,7 +15,7 @@ def run(self, user, repo, path, message, content, sha, branch=None, committer=No author, branch, committer = prep_github_params_for_file_ops(author, branch, committer) if encoding and encoding == 'base64': - content = decode_base64(content) + content = base64.b64encode(content.encode('utf-8')) user = self._client.get_user(user) repo = user.get_repo(repo) diff --git a/pack.yaml b/pack.yaml index 3c5fd56..5200cf9 100644 --- a/pack.yaml +++ b/pack.yaml @@ -8,7 +8,7 @@ keywords: - git - scm - serverless -version: 2.1.3 +version: 2.1.4 python_versions: - "3" author : StackStorm, Inc. diff --git a/tests/test_action_create_file.py b/tests/test_action_create_file.py new file mode 100644 index 0000000..4a015c1 --- /dev/null +++ b/tests/test_action_create_file.py @@ -0,0 +1,55 @@ +import base64 + +from github_base_action_test_case import GitHubBaseActionTestCase +from create_file import CreateFileAction + +from github import Github +from unittest.mock import patch +from unittest.mock import Mock + + +class CreateFileTest(GitHubBaseActionTestCase): + __test__ = True + action_cls = CreateFileAction + + def setUp(self): + super(CreateFileTest, self).setUp() + + self._test_data = {} + # This is base parameters for running this action + self.action_params = { + 'user': 'st2-test', + 'repo': 'StackStorm-Test', + 'path': 'file-test', + 'message': 'commit message', + 'content': 'file content', + } + + def test_create_file(self): + def _side_effect(*args, **kwargs): + self._test_data['content'] = kwargs['content'] + return {'commit': Mock()} + + action = self.get_action_instance(self.full_config) + + with patch.object(Github, 'get_user', return_value=Mock()) as mock_user: + mock_user.return_value.get_repo.return_value.create_file.side_effect = _side_effect + action.run(**self.action_params) + + # This chceks sending content value is expected value + self.assertEqual(self._test_data['content'], self.action_params['content']) + + def test_create_file_with_encoding_param(self): + def _side_effect(*args, **kwargs): + self._test_data['content'] = kwargs['content'] + return {'commit': Mock()} + + action = self.get_action_instance(self.full_config) + + with patch.object(Github, 'get_user', return_value=Mock()) as mock_user: + mock_user.return_value.get_repo.return_value.create_file.side_effect = _side_effect + action.run(**dict(self.action_params, **{'encoding': 'base64'})) + + # This chceks sending content value is expected value + self.assertEqual(self._test_data['content'], + base64.b64encode(self.action_params['content'].encode('utf-8'))) diff --git a/tests/test_action_get_contents.py b/tests/test_action_get_contents.py new file mode 100644 index 0000000..0d86a0d --- /dev/null +++ b/tests/test_action_get_contents.py @@ -0,0 +1,114 @@ +from github_base_action_test_case import GitHubBaseActionTestCase +from get_contents import GetContentsAction + +from github import Github +from unittest.mock import patch +from unittest.mock import Mock + + +class AddCommentActionTestCase(GitHubBaseActionTestCase): + __test__ = True + action_cls = GetContentsAction + + def setUp(self): + super(AddCommentActionTestCase, self).setUp() + + # This is base parameters for running this action + self.action_params = { + 'user': 'st2-test', + 'repo': 'StackStorm-Test', + 'ref': 'HEAD', + 'path': 'file-test', + } + + # There are mock data-set of returned contents + _mock_data_base = { + 'size': 'test-size', + 'name': 'test-name', + 'path': 'test-path', + 'sha': 'test-sha', + 'url': 'https://example.com/test-url', + 'git_url': 'https://example.com/test-git_url', + 'html_url': 'https://example.com/test-html_url', + 'download_url': 'https://example.com/test-download_url', + } + self.mock_data_for_file = dict(_mock_data_base, **{ + 'type': 'file', + 'encoding': 'base64', + 'content': 'test-content', + }) + self.mock_data_for_symlink = dict(_mock_data_base, **{ + 'type': 'symlink', + 'target': 'test-target', + }) + self.mock_data_for_submodule = dict(_mock_data_base, **{ + 'type': 'submodule', + 'submodule_git_url': 'https://example.com/submodule_git_url', + }) + self.mock_data_for_directory = dict(_mock_data_base, **{ + 'type': 'directory', + }) + + def _confirm_returned_contents(self, action_params, mock_content, expected_content): + """ + This run github.get_contents action and confirm returned value is expected one. + """ + action = self.get_action_instance(self.full_config) + + # Configure mocks not to send requests to the GitHub + with patch.object(Github, 'get_user', return_value=Mock()) as mock_user: + mock_user.return_value.get_repo.return_value.get_contents.return_value = mock_content + + # Run github.get_contents action with decode parameter + result = action.run(**action_params) + + self.assertEqual(result, expected_content) + + def test_get_file(self): + mock_content = Mock() + for (key, value) in self.mock_data_for_file.items(): + setattr(mock_content, key, value) + + # run action and check returned contents + self._confirm_returned_contents(self.action_params, mock_content, self.mock_data_for_file) + + def test_get_file_with_decode_param(self): + mock_content = Mock() + for (key, value) in self.mock_data_for_file.items(): + setattr(mock_content, key, value) + + # This is in case of calling ContentFile.decoded_content + mock_content.decoded_content = b'test-decoded-content' + + # run action and check returned contents + params = dict(self.action_params, **{'decode': True}) + self._confirm_returned_contents(params, mock_content, dict(self.mock_data_for_file, **{ + 'content': 'test-decoded-content' + })) + + def test_get_directory(self): + mock_content = Mock() + for (key, value) in self.mock_data_for_directory.items(): + setattr(mock_content, key, value) + + # run action and check returned contents + self._confirm_returned_contents(self.action_params, [mock_content], + [self.mock_data_for_directory]) + + def test_get_symlink(self): + mock_content = Mock() + for (key, value) in self.mock_data_for_symlink.items(): + setattr(mock_content, key, value) + + # run action and check returned contents + self._confirm_returned_contents(self.action_params, [mock_content], + [self.mock_data_for_symlink]) + + def test_get_submodule(self): + mock_content = Mock() + for (key, value) in self.mock_data_for_submodule.items(): + setattr(mock_content, key, value) + + # run action and check returned contents + self._confirm_returned_contents(self.action_params, [mock_content], + [self.mock_data_for_submodule]) diff --git a/tests/test_action_update_file.py b/tests/test_action_update_file.py new file mode 100644 index 0000000..cbd5e98 --- /dev/null +++ b/tests/test_action_update_file.py @@ -0,0 +1,56 @@ +import base64 + +from github_base_action_test_case import GitHubBaseActionTestCase +from update_file import UpdateFileAction + +from github import Github +from unittest.mock import patch +from unittest.mock import Mock + + +class UpdateFileTest(GitHubBaseActionTestCase): + __test__ = True + action_cls = UpdateFileAction + + def setUp(self): + super(UpdateFileTest, self).setUp() + + self._test_data = {} + # This is base parameters for running this action + self.action_params = { + 'user': 'st2-test', + 'repo': 'StackStorm-Test', + 'path': 'file-test', + 'sha': 'test-key', + 'message': 'commit message', + 'content': 'file content', + } + + def test_update_file(self): + def _side_effect(*args, **kwargs): + self._test_data['content'] = kwargs['content'] + return {'commit': Mock()} + + action = self.get_action_instance(self.full_config) + + with patch.object(Github, 'get_user', return_value=Mock()) as mock_user: + mock_user.return_value.get_repo.return_value.update_file.side_effect = _side_effect + action.run(**self.action_params) + + # This chceks sending content value is expected value + self.assertEqual(self._test_data['content'], self.action_params['content']) + + def test_update_file_with_encoding_param(self): + def _side_effect(*args, **kwargs): + self._test_data['content'] = kwargs['content'] + return {'commit': Mock()} + + action = self.get_action_instance(self.full_config) + + with patch.object(Github, 'get_user', return_value=Mock()) as mock_user: + mock_user.return_value.get_repo.return_value.update_file.side_effect = _side_effect + action.run(**dict(self.action_params, **{'encoding': 'base64'})) + + # This chceks sending content value is expected value + self.assertEqual(self._test_data['content'], + base64.b64encode(self.action_params['content'].encode('utf-8')))