mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-08-03 04:34:24 -07:00
jenkins_credentials: new module to manage Jenkins credentials (#10170)
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
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>
This commit is contained in:
parent
e37cd1a015
commit
52cd104962
12 changed files with 1921 additions and 0 deletions
348
tests/unit/plugins/modules/test_jenkins_credential.py
Normal file
348
tests/unit/plugins/modules/test_jenkins_credential.py
Normal file
|
@ -0,0 +1,348 @@
|
|||
# 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"
|
Loading…
Add table
Add a link
Reference in a new issue