Skip to content

Commit

Permalink
Tests
Browse files Browse the repository at this point in the history
  • Loading branch information
juditnovak committed Jun 28, 2023
1 parent fd5ec61 commit 2bac27f
Show file tree
Hide file tree
Showing 4 changed files with 209 additions and 2 deletions.
43 changes: 43 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Copyright 2023 Canonical Ltd.
# See LICENSE file for licensing details.
from importlib.metadata import version
from unittest.mock import PropertyMock

import pytest
from ops import JujuVersion


@pytest.fixture(autouse=True)
def juju_has_secrets(mocker):
"""This fixture will force the usage of secrets whenever run on Juju 3.x.
NOTE: This is needed, as normally JujuVersion is set to 0.0.0 in tests
(i.e. not the real juju version)
"""
if version("juju") < "3":
mocker.patch.object(
JujuVersion, "has_secrets", new_callable=PropertyMock
).return_value = False
return False
else:
mocker.patch.object(
JujuVersion, "has_secrets", new_callable=PropertyMock
).return_value = True
return True


@pytest.fixture
def only_with_juju_secrets(juju_has_secrets):
"""Pretty way to skip Juju 3 tests."""
if not juju_has_secrets:
pytest.skip("Secrets test only applies on Juju 3.x")


@pytest.fixture
def only_without_juju_secrets(juju_has_secrets):
"""Pretty way to skip Juju 2-specific tests.
Typically: to save CI time, when the same check were executed in a Juju 3-specific way already
"""
if juju_has_secrets:
pytest.skip("Skipping legacy secrets tests")
15 changes: 15 additions & 0 deletions tests/integration/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,21 @@ async def get_password(ops_test: OpsTest, unit_id: int, username="operator") ->
return action.results["password"]


async def set_password(
ops_test: OpsTest, unit_id: int, username: str = "operator", password: str = "secret"
) -> str:
"""Use the charm action to retrieve the password from provided unit.
Returns:
String with the password stored on the peer relation databag.
"""
action = await ops_test.model.units.get(f"{APP_NAME}/{unit_id}").run_action(
"set-password", **{"username": username, "password": password}
)
action = await action.wait()
return action.results


async def get_mongo_cmd(ops_test: OpsTest, unit_name: str):
ls_code, _, _ = await ops_test.juju(f"ssh --container {unit_name} ls /usr/bin/mongosh")

Expand Down
38 changes: 38 additions & 0 deletions tests/integration/test_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@
# Copyright 2023 Canonical Ltd.
# See LICENSE file for licensing details.

import json
import logging
import time
from unittest.mock import PropertyMock

import pytest
from lightkube import AsyncClient
from lightkube.resources.core_v1 import Pod
from ops import JujuVersion
from pymongo import MongoClient
from pytest_mock import MockerFixture
from pytest_operator.plugin import OpsTest

from .helpers import (
Expand All @@ -25,6 +29,7 @@
primary_host,
run_mongo_op,
secondary_mongo_uris_with_sync_delay,
set_password,
)

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -113,6 +118,39 @@ async def test_monitor_user(ops_test: OpsTest) -> None:
assert return_code == 0, f"command rs.conf() on monitor user does not work, error: {stderr}"


@pytest.mark.usefixtures("only_with_juju_secrets")
async def test_reset_and_get_password_secret(ops_test: OpsTest, mocker: MockerFixture) -> None:
"""Test verifies that we can set and retrieve the correct password using Juju 3.x secrets."""
mocker.patch.object(JujuVersion, "has_secrets", new_callable=PropertyMock).return_value = True
# Setting existing password
leader_id = await get_leader_id(ops_test)
result = await set_password(ops_test, unit_id=leader_id, username="operator", password="bla")

# Chopping off initial 'secret:' from the ID
secret_id = result["secret-id"].split(":")[1]

# Getting back the pw programmatically
password = await get_password(ops_test, unit_id=leader_id, username="operator")

# Getting back the pw from juju CLI
complete_command = f"show-secret {secret_id} --reveal --format=json"
_, stdout, _ = await ops_test.juju(*complete_command.split())
data = json.loads(stdout)

assert data[secret_id]["label"] == "app:operator-password"
assert data[secret_id]["content"]["Data"]["operator-password"] == password


@pytest.mark.usefixtures("only_without_juju_secrets")
async def test_reset_and_get_password_no_secret(ops_test: OpsTest, mocker) -> None:
"""Test verifies that we can set and retrieve the correct password using Juju 2.x."""
mocker.patch.object(JujuVersion, "has_secrets", new_callable=PropertyMock).return_value = False
leader_id = await get_leader_id(ops_test)
await set_password(ops_test, unit_id=leader_id, username="operator", password="bla")
password = await get_password(ops_test, unit_id=leader_id, username="operator")
assert password == "bla"


async def test_scale_up(ops_test: OpsTest):
"""Tests juju add-unit functionality.
Expand Down
115 changes: 113 additions & 2 deletions tests/unit/test_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import logging
import unittest
from unittest import mock
from unittest.mock import patch
from unittest.mock import MagicMock, patch

from charms.mongodb.v0.helpers import CONF_DIR, DATA_DIR, KEY_FILE
from ops.model import ModelError
Expand Down Expand Up @@ -33,6 +33,7 @@
class TestCharm(unittest.TestCase):
@patch_network_get(private_address="1.1.1.1")
def setUp(self):
self.maxDiff = None
self.harness = Harness(MongoDBCharm)
mongo_resource = {
"registrypath": "mongo:4.4",
Expand All @@ -44,6 +45,11 @@ def setUp(self):
self.charm = self.harness.charm
self.addCleanup(self.harness.cleanup)

def _setup_secrets(self):
self.harness.set_leader(True)
self.harness.charm._generate_secrets()
self.harness.set_leader(False)

@patch("charm.MongoDBCharm._pull_licenses")
@patch("ops.framework.EventBase.defer")
@patch("charm.MongoDBCharm._set_data_dir_permissions")
Expand Down Expand Up @@ -618,6 +624,28 @@ def test_start_init_operator_user_after_second_call(self, connection, oversee_us

defer.assert_not_called()

def test_get_password(self):
self._setup_secrets()

assert isinstance(self.harness.charm.get_secret("app", "monitor-password"), str)
assert self.harness.charm.get_secret("app", "non-existing-secret") is None

def test_set_password(self):
"""NOTE: currently ops.testing seems to allow for non-leader to set secrets too!"""
self._setup_secrets()

# Getting current password
self.harness.charm.set_secret("app", "monitor-password", "bla")
assert self.harness.charm.get_secret("app", "monitor-password") == "bla"

def test_delete_password(self):
"""NOTE: currently ops.testing seems to allow for non-leader to remove secrets too!"""
self._setup_secrets()

assert self.harness.charm.get_secret("app", "monitor-password")
self.harness.charm.remove_secret("app", "monitor-password")
assert self.harness.charm.get_secret("app", "monitor-password") is None

@patch("charm.MongoDBConnection")
@patch("charm.MongoDBCharm._connect_mongodb_exporter")
def test_connect_to_mongo_exporter_on_set_password(self, connect_exporter, connection):
Expand All @@ -632,6 +660,89 @@ def test_connect_to_mongo_exporter_on_set_password(self, connect_exporter, conne
self.harness.charm._on_set_password(action_event)
connect_exporter.assert_called()

@patch("charm.MongoDBConnection")
@patch("charm.MongoDBCharm._connect_mongodb_exporter")
def test_event_set_password_secrets(self, connect_exporter, connection):
"""Test _connect_mongodb_exporter is called when the password is set for 'montior' user.
Furthermore: in Juju 3.x we want to use secrets
"""
pw = "bla"

self.harness.set_leader(True)

action_event = mock.Mock()
action_event.set_results = MagicMock()
action_event.params = {"username": "monitor", "password": pw}
self.harness.charm._on_set_password(action_event)
connect_exporter.assert_called()

action_event.set_results.assert_called()
args_pw_set = action_event.set_results.call_args.args[0]
assert "secret-id" in args_pw_set

action_event.params = {"username": "monitor"}
self.harness.charm._on_get_password(action_event)
args_pw = action_event.set_results.call_args.args[0]
assert "password" in args_pw
assert args_pw["password"] == pw

@patch("charm.MongoDBConnection")
@patch("charm.MongoDBCharm._connect_mongodb_exporter")
def test_event_auto_reset_password_secrets_when_no_pw_value_shipped(
self, connect_exporter, connection
):
"""Test _connect_mongodb_exporter is called when the password is set for 'montior' user.
Furthermore: in Juju 3.x we want to use secrets
"""
self._setup_secrets()
self.harness.set_leader(True)

action_event = mock.Mock()
action_event.set_results = MagicMock()

# Getting current password
action_event.params = {"username": "monitor"}
self.harness.charm._on_get_password(action_event)
args_pw = action_event.set_results.call_args.args[0]
assert "password" in args_pw
pw1 = args_pw["password"]

# No password value was shipped
action_event.params = {"username": "monitor"}
self.harness.charm._on_set_password(action_event)
connect_exporter.assert_called()

# New password was generated
action_event.params = {"username": "monitor"}
self.harness.charm._on_get_password(action_event)
args_pw = action_event.set_results.call_args.args[0]
assert "password" in args_pw
pw2 = args_pw["password"]

# a new password was created
assert pw1 != pw2

@patch("charm.MongoDBConnection")
@patch("charm.MongoDBCharm._connect_mongodb_exporter")
def test_event_any_unit_can_get_password_secrets(self, connect_exporter, connection):
"""Test _connect_mongodb_exporter is called when the password is set for 'montior' user.
Furthermore: in Juju 3.x we want to use secrets
"""
self._setup_secrets()

action_event = mock.Mock()
action_event.set_results = MagicMock()

# Getting current password
action_event.params = {"username": "monitor"}
self.harness.charm._on_get_password(action_event)
args_pw = action_event.set_results.call_args.args[0]
assert "password" in args_pw
assert args_pw["password"]

@patch("charm.MongoDBCharm._pull_licenses")
@patch("ops.framework.EventBase.defer")
@patch("charm.MongoDBCharm._set_data_dir_permissions")
Expand All @@ -643,7 +754,7 @@ def test__connect_mongodb_exporter_success(
container = self.harness.model.unit.get_container("mongod")
self.harness.set_can_connect(container, True)
self.harness.charm.on.mongod_pebble_ready.emit(container)
password = self.harness.charm.app_peer_data["monitor-password"]
password = self.harness.charm.get_secret("app", "monitor-password")

uri_template = "mongodb://monitor:{password}@mongodb-k8s-0.mongodb-k8s-endpoints/?replicaSet=mongodb-k8s&authSource=admin"

Expand Down

0 comments on commit 2bac27f

Please sign in to comment.