From f5fc5bb8456b371e947fd35d4233527682815f56 Mon Sep 17 00:00:00 2001 From: Bertrand Lanson Date: Sat, 14 Sep 2024 13:13:43 +0200 Subject: [PATCH] feat: add unit tests for public_cloud_config_info module --- .gitea/workflows/.gitkeep | 0 .gitea/workflows/pull-request-open.yml | 53 ++++ .gitea/workflows/release.yml | 54 ++++ .gitignore | 19 +- molecule/.gitkeep | 0 plugins/.gitkeep | 0 plugins/modules/public_cloud_config_info.py | 2 +- tests/requirements.txt | 5 + .../modules/test_public_cloud_config_info.py | 292 ++++++++++++++++++ 9 files changed, 416 insertions(+), 9 deletions(-) delete mode 100644 .gitea/workflows/.gitkeep create mode 100644 .gitea/workflows/pull-request-open.yml create mode 100644 .gitea/workflows/release.yml delete mode 100644 molecule/.gitkeep delete mode 100644 plugins/.gitkeep create mode 100644 tests/requirements.txt create mode 100644 tests/unit/plugins/modules/test_public_cloud_config_info.py diff --git a/.gitea/workflows/.gitkeep b/.gitea/workflows/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/.gitea/workflows/pull-request-open.yml b/.gitea/workflows/pull-request-open.yml new file mode 100644 index 0000000..5037893 --- /dev/null +++ b/.gitea/workflows/pull-request-open.yml @@ -0,0 +1,53 @@ +--- +name: pull-requests-open +on: + pull_request: + types: + - opened + - edited + - synchronize + branches: + - main + - develop + +jobs: + commit-history-check: + name: Check commit compliance + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + + - name: Install commitizen + run: pip3 install commitizen + shell: bash + working-directory: ${{ gitea.workspace }} + + - name: Verify commit message compliance + run: | + echo "cz check --rev-range origin/${{ gitea.event.pull_request.base.ref }}.." + cz check --rev-range origin/${{ gitea.event.pull_request.base.ref }}.. + shell: bash + working-directory: ${{ gitea.workspace }} + + run-tests: + name: Run tests + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install requirements + run: pip3 install -r tests/requirements.txt + shell: bash + working-directory: ${{ gitea.workspace }} + + - name: Run ansible unit tests + uses: ansible-community/ansible-test-gh-action@release/v1 + with: + ansible-core-version: v2.17.4 + testing-type: units + collection-src-directory: ${{ gitea.workspace }} diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..e05f041 --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,54 @@ +--- +name: release +on: + push: + branches: + - main + +jobs: + do-release: + if: "!startsWith(github.event.head_commit.message, 'bump:')" + runs-on: ubuntu-latest + name: Bump version and create changelog with commitizen + steps: + - name: Get secrets from vault + id: import-secrets + uses: hashicorp/vault-action@v3 + with: + url: "https://vault.ednz.fr" + method: approle + roleId: ${{ secrets.VAULT_APPROLE_ID }} + secretId: ${{ secrets.VAULT_APPROLE_SECRET_ID }} + secrets: | + kv/data/applications/gitea/users/actions username | GITEA_ACTIONS_USERNAME ; + kv/data/applications/gitea/users/actions token_write | GITEA_ACTIONS_TOKEN ; + + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ steps.import-secrets.outputs.GITEA_ACTIONS_TOKEN }} + + - name: Install commitizen + run: pip3 install commitizen + shell: bash + working-directory: ${{ gitea.workspace }} + + - name: Configure git credentials + uses: oleksiyrudenko/gha-git-credentials@v2 + with: + global: true + name: "Gitea-Actions Bot" + email: "gitea-actions@ednz.fr" + actor: ${{ steps.import-secrets.outputs.GITEA_ACTIONS_USERNAME }} + token: ${{ steps.import-secrets.outputs.GITEA_ACTIONS_TOKEN }} + + - name: Do release + run: cz -nr 21 bump --yes + shell: bash + working-directory: ${{ gitea.workspace }} + + - name: Push release + run: git push && git push --tags + shell: bash + working-directory: ${{ gitea.workspace }} diff --git a/.gitignore b/.gitignore index cb534dd..2ae2ad8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,14 @@ # ignore molecule/testinfra pycache **/__pycache__ + +# ignore vscode files .vscode -roles/ednz_cloud.* -vault_config.yml -consul_config.yml -**/certificates/** -**/secrets/credentials.yml -**/secrets/credentials.decrypt.yml -**/secrets/vault.yml -**/.ansible-vault + +# ignore test output (auto-generated) +tests/output/** + +# ignore virtual environments +.env +.venv +env +venv diff --git a/molecule/.gitkeep b/molecule/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/plugins/.gitkeep b/plugins/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/plugins/modules/public_cloud_config_info.py b/plugins/modules/public_cloud_config_info.py index 2fd77f4..030ffc5 100644 --- a/plugins/modules/public_cloud_config_info.py +++ b/plugins/modules/public_cloud_config_info.py @@ -79,7 +79,7 @@ def run_module(): account_id=dict(type="str", required=True), ) - result = dict(changed=False, public_cloud_config={}) + result = dict(changed=False, config={}) module = AnsibleModule(argument_spec=module_args, supports_check_mode=True) diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 0000000..01e96bc --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1,5 @@ +ansible==10.3.0 +requests==2.32.3 +pytest==8.3.3 +pytest-xdist==3.6.1 +coverage==7.3.2 diff --git a/tests/unit/plugins/modules/test_public_cloud_config_info.py b/tests/unit/plugins/modules/test_public_cloud_config_info.py new file mode 100644 index 0000000..1e401c8 --- /dev/null +++ b/tests/unit/plugins/modules/test_public_cloud_config_info.py @@ -0,0 +1,292 @@ +import unittest +from unittest.mock import patch, MagicMock +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.ednz_cloud.infomaniak.plugins.modules import ( + public_cloud_config_info, +) + + +class TestPublicCloudConfigInfoModule(unittest.TestCase): + """ + Unit tests for the public_cloud_config_info Ansible module. + """ + + @patch( + "ansible_collections.ednz_cloud.infomaniak.plugins.modules.public_cloud_config_info.AnsibleModule" + ) + @patch( + "ansible_collections.ednz_cloud.infomaniak.plugins.modules.public_cloud_config_info.InfomaniakAPIClient" + ) + def test_successful_retrieval(self, mock_client_class, mock_ansible_module): + """ + Test a successful retrieval of public cloud configuration. + """ + # Mock API response data + api_response_data = { + "result": "success", + "data": { + "free_tier": 300, + "free_tier_used": 24.34, + "account_resource_level": 2, + "valid_from": 1707584356, + "valid_to": 1717192799, + "project_count": 2, + }, + } + + # Mock the InfomaniakAPIClient instance + mock_client_instance = MagicMock() + mock_client_instance.get.return_value = api_response_data + mock_client_class.return_value = mock_client_instance + + # Mock AnsibleModule parameters + mock_module_instance = MagicMock() + mock_module_instance.params = { + "api_token": "mock_api_token", + "account_id": "mock_account_id", + } + mock_module_instance.check_mode = False # Simulate non-check mode + mock_ansible_module.return_value = mock_module_instance + + # Mock exit_json to prevent actual exit + mock_module_instance.exit_json = MagicMock() + + # Execute the run_module function + public_cloud_config_info.run_module() + + # Assert that the InfomaniakAPIClient was initialized with the correct token + mock_client_class.assert_called_with( + api_version="1", api_token="mock_api_token" + ) + + # Assert that the correct API call was made + mock_client_instance.get.assert_called_once_with( + "/public_clouds/config", params={"account_id": "mock_account_id"} + ) + + # Assert exit_json is called once with the expected data + mock_module_instance.exit_json.assert_called_once_with( + changed=False, config=api_response_data["data"] + ) + + @patch( + "ansible_collections.ednz_cloud.infomaniak.plugins.modules.public_cloud_config_info.AnsibleModule" + ) + @patch( + "ansible_collections.ednz_cloud.infomaniak.plugins.modules.public_cloud_config_info.InfomaniakAPIClient" + ) + def test_api_failure(self, mock_client_class, mock_ansible_module): + """ + Test failure scenario where API returns an error result. + """ + + # Mock API failure response + mock_client_instance = MagicMock() + mock_client_instance.get.side_effect = Exception( + "API request failed with result: error" + ) + mock_client_class.return_value = mock_client_instance + + # Mock AnsibleModule parameters + mock_module_instance = MagicMock() + mock_module_instance.params = { + "api_token": "mock_api_token", + "account_id": "invalid_account_id", + } + mock_module_instance.check_mode = False # Simulate non-check mode + mock_ansible_module.return_value = mock_module_instance + + # Mock fail_json to prevent actual exit + mock_module_instance.fail_json = MagicMock() + + # Execute the run_module function + public_cloud_config_info.run_module() + + # Assert fail_json was called with the expected error message + mock_module_instance.fail_json.assert_called_once_with( + msg="API request failed with result: error" + ) + + @patch( + "ansible_collections.ednz_cloud.infomaniak.plugins.modules.public_cloud_config_info.AnsibleModule" + ) + def test_missing_required_params(self, mock_ansible_module): + """ + Test behavior when required parameters are missing. + """ + # Mock the AnsibleModule instance + mock_module_instance = MagicMock() + + # Simulate missing 'api_token' in the parameters + mock_module_instance.params = { + "account_id": "mock_account_id", # Missing api_token + } + + # Simulate Ansible behavior: fail_json should be called when 'api_token' is missing + mock_ansible_module.return_value = mock_module_instance + + # Execute the run_module function and expect KeyError + with self.assertRaises(KeyError): + public_cloud_config_info.run_module() + + @patch( + "ansible_collections.ednz_cloud.infomaniak.plugins.modules.public_cloud_config_info.AnsibleModule" + ) + @patch( + "ansible_collections.ednz_cloud.infomaniak.plugins.modules.public_cloud_config_info.InfomaniakAPIClient" + ) + def test_empty_api_response(self, mock_client_class, mock_ansible_module): + """ + Test behavior when the API returns an empty response. + """ + # Mock API response data + api_response_data = {"result": "success", "data": {}} # Empty data + + # Mock the InfomaniakAPIClient instance + mock_client_instance = MagicMock() + mock_client_instance.get.return_value = api_response_data + mock_client_class.return_value = mock_client_instance + + # Mock AnsibleModule parameters + mock_module_instance = MagicMock() + mock_module_instance.params = { + "api_token": "mock_api_token", + "account_id": "mock_account_id", + } + mock_module_instance.check_mode = False # Simulate non-check mode + mock_ansible_module.return_value = mock_module_instance + + # Execute the run_module function + public_cloud_config_info.run_module() + + # Assert exit_json is called once with empty config data + mock_module_instance.exit_json.assert_called_once_with(changed=False, config={}) + + @patch( + "ansible_collections.ednz_cloud.infomaniak.plugins.modules.public_cloud_config_info.AnsibleModule" + ) + @patch( + "ansible_collections.ednz_cloud.infomaniak.plugins.modules.public_cloud_config_info.InfomaniakAPIClient" + ) + def test_unexpected_api_response_format( + self, mock_client_class, mock_ansible_module + ): + """ + Test behavior when the API response format is unexpected. + """ + # Mock an unexpected API response (missing 'result') + api_response_data = { + "data": { + "free_tier": 300, + } + } + + # Mock the InfomaniakAPIClient instance + mock_client_instance = MagicMock() + mock_client_instance.get.return_value = api_response_data + mock_client_class.return_value = mock_client_instance + + # Mock AnsibleModule parameters + mock_module_instance = MagicMock() + mock_module_instance.params = { + "api_token": "mock_api_token", + "account_id": "mock_account_id", + } + mock_ansible_module.return_value = mock_module_instance + + # Execute the run_module function + public_cloud_config_info.run_module() + + # Assert fail_json is called with an appropriate error + mock_module_instance.fail_json.assert_called_once_with( + msg="API request failed with result: None" + ) + + @patch( + "ansible_collections.ednz_cloud.infomaniak.plugins.modules.public_cloud_config_info.AnsibleModule" + ) + def test_check_mode(self, mock_ansible_module): + """ + Test behavior when check_mode is enabled. + """ + # Mock AnsibleModule parameters + mock_module_instance = MagicMock() + mock_module_instance.params = { + "api_token": "mock_api_token", + "account_id": "mock_account_id", + } + mock_module_instance.check_mode = True # Simulate check mode + mock_ansible_module.return_value = mock_module_instance + + # Execute the run_module function + public_cloud_config_info.run_module() + + # Assert exit_json is called with the check_mode message + mock_module_instance.exit_json.assert_called_once_with( + changed=False, + msg="Check mode: No changes made, would retrieve public cloud config.", + ) + + @patch( + "ansible_collections.ednz_cloud.infomaniak.plugins.modules.public_cloud_config_info.AnsibleModule" + ) + @patch( + "ansible_collections.ednz_cloud.infomaniak.plugins.modules.public_cloud_config_info.InfomaniakAPIClient" + ) + def test_network_timeout(self, mock_client_class, mock_ansible_module): + """ + Test behavior when a network timeout occurs. + """ + # Simulate a network timeout error + mock_client_instance = MagicMock() + mock_client_instance.get.side_effect = Exception("Request timeout") + mock_client_class.return_value = mock_client_instance + + # Mock AnsibleModule parameters + mock_module_instance = MagicMock() + mock_module_instance.params = { + "api_token": "mock_api_token", + "account_id": "mock_account_id", + } + mock_ansible_module.return_value = mock_module_instance + + # Execute the run_module function + public_cloud_config_info.run_module() + + # Assert fail_json is called with the timeout error message + mock_module_instance.fail_json.assert_called_once_with(msg="Request timeout") + + @patch( + "ansible_collections.ednz_cloud.infomaniak.plugins.modules.public_cloud_config_info.AnsibleModule" + ) + @patch( + "ansible_collections.ednz_cloud.infomaniak.plugins.modules.public_cloud_config_info.InfomaniakAPIClient" + ) + def test_invalid_json_response(self, mock_client_class, mock_ansible_module): + """ + Test behavior when the API returns an invalid JSON response. + """ + # Mock the InfomaniakAPIClient instance to raise a JSONDecodeError + mock_client_instance = MagicMock() + mock_client_instance.get.side_effect = Exception("Invalid JSON response") + mock_client_class.return_value = mock_client_instance + + # Mock AnsibleModule parameters + mock_module_instance = MagicMock() + mock_module_instance.params = { + "api_token": "mock_api_token", + "account_id": "mock_account_id", + } + mock_ansible_module.return_value = mock_module_instance + + # Execute the run_module function + public_cloud_config_info.run_module() + + # Assert fail_json is called with the appropriate error message + mock_module_instance.fail_json.assert_called_once_with( + msg="Invalid JSON response" + ) + + +if __name__ == "__main__": + unittest.main()