Skip to content

Commit

Permalink
Add unit tests for the SecretsHandler
Browse files Browse the repository at this point in the history
  • Loading branch information
TheByronHimes committed Oct 28, 2024
1 parent 1fb7ec4 commit 210db82
Showing 1 changed file with 149 additions and 0 deletions.
149 changes: 149 additions & 0 deletions tests/unit/secrets/test_secrets_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# Copyright 2021 - 2024 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
# for the German Human Genome-Phenome Archive (GHGA)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Unit tests for the SecretsHandler class"""

from unittest.mock import Mock, call

import pytest
from hvac.exceptions import InvalidPath

from sms.core.secrets_handler import SecretsHandler
from tests.fixtures.config import DEFAULT_TEST_CONFIG


@pytest.mark.parametrize(
"secrets",
[[], ["key1"], ["key1", "key2"]],
ids=["empty", "single", "multiple"],
)
def test_get_secrets(monkeypatch: pytest.MonkeyPatch, secrets: list[str]):
"""Test get_secrets method without errors"""
# patch the hvac client with a mock
mock_client = Mock()
mock_client.secrets.kv.v2.list_secrets.return_value = {"data": {"keys": secrets}}
monkeypatch.setattr(SecretsHandler, "client", mock_client)
secrets_handler = SecretsHandler(config=DEFAULT_TEST_CONFIG)

assert secrets_handler.get_secrets() == secrets
mock_client.secrets.kv.v2.list_secrets.assert_called_once()


def test_get_secrets_error(monkeypatch: pytest.MonkeyPatch, caplog):
"""Test get_secrets method with an error"""
# patch the hvac client with a mock
mock_client = Mock()
mock_client.secrets.kv.v2.list_secrets.side_effect = InvalidPath("Invalid path")
monkeypatch.setattr(SecretsHandler, "client", mock_client)
secrets_handler = SecretsHandler(config=DEFAULT_TEST_CONFIG)

# Make sure the error is logged as a warning but an empty list is still returned
caplog.clear()
secrets = secrets_handler.get_secrets()
assert len(caplog.messages) == 1
assert caplog.messages[0] == (
"Invalid path error when fetching secrets. The path might be invalid,"
+ " or no secrets may exist."
)
assert caplog.records[0].levelname == "WARNING"
assert secrets == []
mock_client.secrets.kv.v2.list_secrets.assert_called_once()


def test_delete_secrets_error(monkeypatch: pytest.MonkeyPatch):
"""Test delete_secrets method on empty vault and without specifying secrets."""
# patch the hvac client with a mock
mock_client = Mock()
mock_client.secrets.kv.v2.list_secrets.side_effect = InvalidPath("Invalid path")
monkeypatch.setattr(SecretsHandler, "client", mock_client)
secrets_handler = SecretsHandler(config=DEFAULT_TEST_CONFIG)

# Call delete_secrets() without specifying secrets in order to trigger get_secrets
secrets_handler.delete_secrets()
mock_client.secrets.kv.v2.list_secrets.assert_called_once()
mock_client.secrets.kv.v2.delete_metadata_and_all_versions.assert_not_called()


@pytest.mark.parametrize(
"secrets_to_delete",
[
None,
[],
["key1"],
["key2"],
["key1", "key2"],
],
ids=[
"None",
"Empty",
"DeleteKey1",
"DeleteKey2",
"DeleteBoth",
],
)
@pytest.mark.parametrize(
"stored_secrets",
[[], ["key1"], ["key1", "key2"]],
ids=["Empty", "OneSecret", "TwoSecrets"],
)
def test_delete_successful(
monkeypatch: pytest.MonkeyPatch,
secrets_to_delete: list[str] | None,
stored_secrets: list[str],
):
"""Test delete_secrets method.
Use a variety of combinations of stored secrets and secrets to delete.
"""
# create a mock for the hvac client
list_stored_secrets = {"data": {"keys": stored_secrets}}
mock_client = Mock()

# list_secrets either returns all keys or raises an InvalidPath error
if stored_secrets:
mock_client.secrets.kv.v2.list_secrets.return_value = list_stored_secrets
else:
mock_client.secrets.kv.v2.list_secrets.side_effect = InvalidPath("Invalid path")

# apply the mock to the SecretsHandler
monkeypatch.setattr(SecretsHandler, "client", mock_client)
secrets_handler = SecretsHandler(config=DEFAULT_TEST_CONFIG)

# call delete_secrets()
secrets_handler.delete_secrets(secrets=secrets_to_delete)

# list_secrets is ONLY called if secrets_to_delete is None or empty list
if not secrets_to_delete:
mock_client.secrets.kv.v2.list_secrets.assert_called_once()
else:
mock_client.secrets.kv.v2.list_secrets.assert_not_called()

# delete_metadata_and_all_versions is called for each secret in secrets_to_delete
# if that's not supplied, then it is called for each value in list_secrets
calls = (
[
call(path=f"{DEFAULT_TEST_CONFIG.vault_path}/{secret}")
for secret in stored_secrets
]
if not secrets_to_delete
else [
call(path=f"{DEFAULT_TEST_CONFIG.vault_path}/{secret}")
for secret in secrets_to_delete
]
)
mock_client.secrets.kv.v2.delete_metadata_and_all_versions.assert_has_calls(
calls,
any_order=True,
)

0 comments on commit 210db82

Please sign in to comment.