mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-06-28 03:00:23 -07:00
Some checks are pending
EOL CI / EOL Sanity (Ⓐ2.16) (push) Waiting to run
EOL CI / EOL Units (Ⓐ2.16+py2.7) (push) Waiting to run
EOL CI / EOL Units (Ⓐ2.16+py3.11) (push) Waiting to run
EOL CI / EOL Units (Ⓐ2.16+py3.6) (push) Waiting to run
EOL CI / EOL I (Ⓐ2.16+alpine3+py:azp/posix/1/) (push) Waiting to run
EOL CI / EOL I (Ⓐ2.16+alpine3+py:azp/posix/2/) (push) Waiting to run
EOL CI / EOL I (Ⓐ2.16+alpine3+py:azp/posix/3/) (push) Waiting to run
EOL CI / EOL I (Ⓐ2.16+fedora38+py:azp/posix/1/) (push) Waiting to run
EOL CI / EOL I (Ⓐ2.16+fedora38+py:azp/posix/2/) (push) Waiting to run
EOL CI / EOL I (Ⓐ2.16+fedora38+py:azp/posix/3/) (push) Waiting to run
EOL CI / EOL I (Ⓐ2.16+opensuse15+py:azp/posix/1/) (push) Waiting to run
EOL CI / EOL I (Ⓐ2.16+opensuse15+py:azp/posix/2/) (push) Waiting to run
EOL CI / EOL I (Ⓐ2.16+opensuse15+py:azp/posix/3/) (push) Waiting to run
nox / Run extra sanity tests (push) Waiting to run
* Added Jenkins credentials module to manage Jenkins credentials * Added Jenkins credentials module to manage Jenkins credentials * Added import error detection, adjusted indentation, and general enhancements. * Added py3 requirement and set files value to avoid errors * Added username to BOTMETA. Switched to format() instead of f strings to support py 2.7, improved delete function, and added function to read private key * Remove redundant message Co-authored-by: Felix Fontein <felix@fontein.de> * Replaced requests with ansible.module_utils.urls, merged check domain and credential functions, and made minor adjustments to documentation * Adjusted for py 2.7 compatibility * Replaced command with state. * Added managing credentials within a folder and made adjustments to documentation * Added unit and integration tests, added token managament, and adjusted documentation. * Added unit and integration tests, added token management, and adjusted documentation.(fix) * Fix BOTMETA.yml * Removed files and generate them at runtime. * moved id and token checks to required_if * Documentation changes, different test setup, and switched to Ansible testing tools * Fixed typos * Correct indentation. Co-authored-by: Felix Fontein <felix@fontein.de> --------- Co-authored-by: Felix Fontein <felix@fontein.de>
348 lines
11 KiB
Python
348 lines
11 KiB
Python
# Copyright (c) Ansible Project
|
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
from __future__ import absolute_import, division, print_function
|
|
|
|
__metaclass__ = type
|
|
|
|
from ansible_collections.community.general.plugins.modules import jenkins_credential
|
|
from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import (
|
|
MagicMock,
|
|
patch,
|
|
mock_open,
|
|
)
|
|
|
|
import json
|
|
import sys
|
|
|
|
if sys.version_info[0] == 3:
|
|
import builtins
|
|
open_path = "builtins.open"
|
|
else:
|
|
import __builtin__ as builtins
|
|
open_path = "__builtin__.open"
|
|
|
|
|
|
def test_validate_file_exist_passes_when_file_exists():
|
|
module = MagicMock()
|
|
with patch("os.path.exists", return_value=True):
|
|
jenkins_credential.validate_file_exist(module, "/some/file/path")
|
|
module.fail_json.assert_not_called()
|
|
|
|
|
|
def test_validate_file_exist_fails_when_file_missing():
|
|
module = MagicMock()
|
|
with patch("os.path.exists", return_value=False):
|
|
jenkins_credential.validate_file_exist(module, "/missing/file/path")
|
|
module.fail_json.assert_called_once_with(
|
|
msg="File not found: /missing/file/path"
|
|
)
|
|
|
|
|
|
@patch(
|
|
"ansible_collections.community.general.plugins.modules.jenkins_credential.fetch_url"
|
|
)
|
|
def test_get_jenkins_crumb_sets_crumb_header(fetch_mock):
|
|
module = MagicMock()
|
|
module.params = {"type": "file", "url": "http://localhost:8080"}
|
|
headers = {}
|
|
|
|
fake_response = MagicMock()
|
|
fake_response.read.return_value = json.dumps(
|
|
{"crumbRequestField": "crumb_field", "crumb": "abc123"}
|
|
).encode("utf-8")
|
|
|
|
fetch_mock.return_value = (
|
|
fake_response,
|
|
{"status": 200, "set-cookie": "JSESSIONID=something; Path=/"},
|
|
)
|
|
|
|
crumb_request_field, crumb, session_coockie = jenkins_credential.get_jenkins_crumb(
|
|
module, headers
|
|
)
|
|
|
|
assert "Cookie" not in headers
|
|
assert "crumb_field" in headers
|
|
assert crumb == "abc123"
|
|
assert headers[crumb_request_field] == crumb
|
|
|
|
|
|
@patch(
|
|
"ansible_collections.community.general.plugins.modules.jenkins_credential.fetch_url"
|
|
)
|
|
def test_get_jenkins_crumb_sets_cookie_if_type_token(fetch_mock):
|
|
module = MagicMock()
|
|
module.params = {"type": "token", "url": "http://localhost:8080"}
|
|
headers = {}
|
|
|
|
fake_response = MagicMock()
|
|
fake_response.read.return_value = json.dumps(
|
|
{"crumbRequestField": "crumb_field", "crumb": "secure"}
|
|
).encode("utf-8")
|
|
|
|
fetch_mock.return_value = (
|
|
fake_response,
|
|
{"status": 200, "set-cookie": "JSESSIONID=token-cookie; Path=/"},
|
|
)
|
|
|
|
crumb_request_field, crumb, session_cookie = jenkins_credential.get_jenkins_crumb(
|
|
module, headers
|
|
)
|
|
|
|
assert "crumb_field" in headers
|
|
assert crumb == "secure"
|
|
assert headers[crumb_request_field] == crumb
|
|
assert headers["Cookie"] == session_cookie
|
|
|
|
|
|
@patch(
|
|
"ansible_collections.community.general.plugins.modules.jenkins_credential.fetch_url"
|
|
)
|
|
def test_get_jenkins_crumb_fails_on_non_200_status(fetch_mock):
|
|
module = MagicMock()
|
|
module.params = {"type": "file", "url": "http://localhost:8080"}
|
|
headers = {}
|
|
|
|
fetch_mock.return_value = (MagicMock(), {"status": 403})
|
|
|
|
jenkins_credential.get_jenkins_crumb(module, headers)
|
|
|
|
module.fail_json.assert_called_once()
|
|
assert "Failed to fetch Jenkins crumb" in module.fail_json.call_args[1]["msg"]
|
|
|
|
|
|
@patch(
|
|
"ansible_collections.community.general.plugins.modules.jenkins_credential.fetch_url"
|
|
)
|
|
def test_get_jenkins_crumb_removes_job_from_url(fetch_mock):
|
|
module = MagicMock()
|
|
module.params = {"type": "file", "url": "http://localhost:8080/job/test"}
|
|
headers = {}
|
|
|
|
fake_response = MagicMock()
|
|
fake_response.read.return_value = json.dumps(
|
|
{"crumbRequestField": "Jenkins-Crumb", "crumb": "xyz"}
|
|
).encode("utf-8")
|
|
|
|
fetch_mock.return_value = (fake_response, {"status": 200, "set-cookie": ""})
|
|
|
|
jenkins_credential.get_jenkins_crumb(module, headers)
|
|
|
|
url_called = fetch_mock.call_args[0][1]
|
|
assert url_called == "http://localhost:8080/crumbIssuer/api/json"
|
|
|
|
|
|
def test_clean_data_removes_extraneous_fields():
|
|
data = {
|
|
"id": "cred1",
|
|
"description": "test",
|
|
"jenkins_user": "admin",
|
|
"token": "secret",
|
|
"url": "http://localhost:8080",
|
|
"file_path": None,
|
|
}
|
|
expected = {"id": "cred1", "description": "test"}
|
|
result = jenkins_credential.clean_data(data)
|
|
assert result == expected, "Expected {}, got {}".format(expected, result)
|
|
|
|
|
|
@patch(
|
|
"ansible_collections.community.general.plugins.modules.jenkins_credential.fetch_url"
|
|
)
|
|
def test_target_exists_returns_true_on_200(fetch_url_mock):
|
|
module = MagicMock()
|
|
module.params = {
|
|
"url": "http://localhost:8080",
|
|
"location": "system",
|
|
"scope": "_",
|
|
"id": "my-id",
|
|
"jenkins_user": "admin",
|
|
"token": "secret",
|
|
"type": "file",
|
|
}
|
|
|
|
fetch_url_mock.return_value = (MagicMock(), {"status": 200})
|
|
assert jenkins_credential.target_exists(module) is True
|
|
|
|
|
|
@patch(
|
|
"ansible_collections.community.general.plugins.modules.jenkins_credential.fetch_url"
|
|
)
|
|
def test_target_exists_returns_false_on_404(fetch_url_mock):
|
|
module = MagicMock()
|
|
module.params = {
|
|
"url": "http://localhost:8080",
|
|
"location": "system",
|
|
"scope": "_",
|
|
"id": "my-id",
|
|
"jenkins_user": "admin",
|
|
"token": "secret",
|
|
"type": "file",
|
|
}
|
|
|
|
fetch_url_mock.return_value = (MagicMock(), {"status": 404})
|
|
assert jenkins_credential.target_exists(module) is False
|
|
|
|
|
|
@patch(
|
|
"ansible_collections.community.general.plugins.modules.jenkins_credential.fetch_url"
|
|
)
|
|
def test_target_exists_calls_fail_json_on_unexpected_status(fetch_url_mock):
|
|
module = MagicMock()
|
|
module.params = {
|
|
"url": "http://localhost:8080",
|
|
"location": "system",
|
|
"scope": "_",
|
|
"id": "my-id",
|
|
"jenkins_user": "admin",
|
|
"token": "secret",
|
|
"type": "file",
|
|
}
|
|
|
|
fetch_url_mock.return_value = (MagicMock(), {"status": 500})
|
|
jenkins_credential.target_exists(module)
|
|
module.fail_json.assert_called_once()
|
|
assert "Unexpected status code" in module.fail_json.call_args[1]["msg"]
|
|
|
|
|
|
@patch(
|
|
"ansible_collections.community.general.plugins.modules.jenkins_credential.fetch_url"
|
|
)
|
|
def test_target_exists_skips_check_for_token_type(fetch_url_mock):
|
|
module = MagicMock()
|
|
module.params = {
|
|
"type": "token",
|
|
"url": "ignored",
|
|
"location": "ignored",
|
|
"scope": "ignored",
|
|
"id": "ignored",
|
|
"jenkins_user": "ignored",
|
|
"token": "ignored",
|
|
}
|
|
|
|
assert jenkins_credential.target_exists(module) is False
|
|
fetch_url_mock.assert_not_called()
|
|
|
|
|
|
@patch(
|
|
"ansible_collections.community.general.plugins.modules.jenkins_credential.fetch_url"
|
|
)
|
|
def test_delete_target_fails_deleting(fetch_mock):
|
|
module = MagicMock()
|
|
module.params = {
|
|
"type": "token",
|
|
"jenkins_user": "admin",
|
|
"url": "http://localhost:8080",
|
|
"id": "token-id",
|
|
"location": "system",
|
|
"scope": "_",
|
|
}
|
|
headers = {"Authorization": "Basic abc", "Content-Type": "whatever"}
|
|
|
|
fetch_mock.return_value = (MagicMock(), {"status": 500})
|
|
|
|
jenkins_credential.delete_target(module, headers)
|
|
|
|
module.fail_json.assert_called_once()
|
|
assert "Failed to delete" in module.fail_json.call_args[1]["msg"]
|
|
|
|
|
|
@patch(
|
|
"ansible_collections.community.general.plugins.modules.jenkins_credential.fetch_url",
|
|
side_effect=Exception("network error"),
|
|
)
|
|
def test_delete_target_raises_exception(fetch_mock):
|
|
module = MagicMock()
|
|
module.params = {
|
|
"type": "scope",
|
|
"jenkins_user": "admin",
|
|
"location": "system",
|
|
"url": "http://localhost:8080",
|
|
"id": "domain-id",
|
|
"scope": "_",
|
|
}
|
|
headers = {"Authorization": "Basic auth"}
|
|
|
|
jenkins_credential.delete_target(module, headers)
|
|
|
|
module.fail_json.assert_called_once()
|
|
assert "Exception during delete" in module.fail_json.call_args[1]["msg"]
|
|
assert "network error" in module.fail_json.call_args[1]["msg"]
|
|
|
|
|
|
def test_read_privateKey_returns_trimmed_contents():
|
|
module = MagicMock()
|
|
module.params = {"private_key_path": "/fake/path/key.pem"}
|
|
|
|
mocked_file = mock_open(
|
|
read_data="\n \t -----BEGIN PRIVATE KEY-----\nKEYDATA\n-----END PRIVATE KEY----- \n\n"
|
|
)
|
|
with patch(open_path, mocked_file):
|
|
result = jenkins_credential.read_privateKey(module)
|
|
|
|
expected = "-----BEGIN PRIVATE KEY-----\nKEYDATA\n-----END PRIVATE KEY-----"
|
|
|
|
assert result == expected
|
|
mocked_file.assert_called_once_with("/fake/path/key.pem", "r")
|
|
|
|
|
|
def test_read_privateKey_handles_file_read_error():
|
|
module = MagicMock()
|
|
module.params = {"private_key_path": "/invalid/path.pem"}
|
|
|
|
with patch(open_path, side_effect=IOError("cannot read file")):
|
|
jenkins_credential.read_privateKey(module)
|
|
|
|
module.fail_json.assert_called_once()
|
|
assert "Failed to read private key file" in module.fail_json.call_args[1]["msg"]
|
|
|
|
|
|
def test_embed_file_into_body_returns_multipart_fields():
|
|
module = MagicMock()
|
|
file_path = "/fake/path/secret.pem"
|
|
credentials = {"id": "my-id"}
|
|
fake_file_content = b"MY SECRET DATA"
|
|
|
|
mock = mock_open()
|
|
mock.return_value.read.return_value = fake_file_content
|
|
|
|
with patch("os.path.basename", return_value="secret.pem"), patch.object(
|
|
builtins, "open", mock
|
|
):
|
|
body, content_type = jenkins_credential.embed_file_into_body(
|
|
module, file_path, credentials.copy()
|
|
)
|
|
|
|
assert "multipart/form-data; boundary=" in content_type
|
|
|
|
# Check if file content is embedded in body
|
|
assert b"MY SECRET DATA" in body
|
|
assert b'filename="secret.pem"' in body
|
|
|
|
|
|
def test_embed_file_into_body_fails_when_file_unreadable():
|
|
module = MagicMock()
|
|
file_path = "/fake/path/missing.pem"
|
|
credentials = {"id": "something"}
|
|
|
|
with patch(open_path, side_effect=IOError("can't read file")):
|
|
jenkins_credential.embed_file_into_body(module, file_path, credentials)
|
|
|
|
module.fail_json.assert_called_once()
|
|
assert "Failed to read file" in module.fail_json.call_args[1]["msg"]
|
|
|
|
|
|
def test_embed_file_into_body_injects_file_keys_into_credentials():
|
|
module = MagicMock()
|
|
file_path = "/fake/path/file.txt"
|
|
credentials = {"id": "test"}
|
|
|
|
with patch(open_path, mock_open(read_data=b"1234")), patch(
|
|
"os.path.basename", return_value="file.txt"
|
|
):
|
|
|
|
jenkins_credential.embed_file_into_body(module, file_path, credentials)
|
|
|
|
assert credentials["file"] == "file0"
|
|
assert credentials["fileName"] == "file.txt"
|