Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 15 additions & 12 deletions lib/charms/mysql/v0/mysql.py
Original file line number Diff line number Diff line change
Expand Up @@ -571,23 +571,26 @@ def _on_set_password(self, event: ActionEvent) -> None:

def _get_cluster_status(self, event: ActionEvent) -> None:
"""Action used to retrieve the cluster status."""
if event.params.get("cluster-set"):
logger.debug("Getting cluster set status")
status = self._mysql.get_cluster_set_status(extended=0)
else:
logger.debug("Getting cluster status")
status = self._mysql.get_cluster_status()
try:
if event.params.get("cluster-set"):
logger.debug("Getting cluster set status")
status = self._mysql.get_cluster_set_status(extended=0)
else:
logger.debug("Getting cluster status")
status = self._mysql.get_cluster_status()

if not status:
event.fail("Failed to read cluster status. See logs for more information.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@arjun11-malik make sure that the output change is not being misinterpreted on the the failing tests

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’ve double-checked every integration test that invokes get-cluster-status—they only wait for the action to complete and then read out results["status"]. None of them inspect the old success=False payload or rely on its exact shape, so switching to event.fail() on empty status won’t change their behavior.

return

if status:
event.set_results({
"success": True,
"status": status,
})
else:
event.set_results({
"success": False,
"message": "Failed to read cluster status. See logs for more information.",
})

except Exception:
logger.exception("Failed to read cluster status")
event.fail("Failed to read cluster status. See logs for more information.")

def _on_promote_to_primary(self, event: ActionEvent) -> None:
"""Action for setting this unit as the cluster primary."""
Expand Down
99 changes: 99 additions & 0 deletions tests/unit/test_action_get_cluster_status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Copyright 2025 Canonical Ltd.
# See LICENSE file for licensing details.

from unittest.mock import Mock, PropertyMock, patch

import pytest
from ops.charm import ActionEvent
from ops.testing import Harness

from charm import MySQLOperatorCharm


class FakeMySQLBackend:
"""Simulates the real MySQL backend, either returning a dict or raising."""

def __init__(self, response=None, error=None):
self._response = response
self._error = error

def get_cluster_status(self):
"""Return the preset response or raise the preset error."""
if self._error:
raise self._error
return self._response


@pytest.fixture
def harness():
"""Start the charm so harness.charm exists and peer databag works."""
h = Harness(MySQLOperatorCharm)
h.begin()
return h


def make_event():
"""Create a dummy ActionEvent with spies on set_results() and fail()."""
evt = Mock(spec=ActionEvent)
evt.set_results = Mock()
evt.fail = Mock()
evt.params = {} # ensure .params.get() won't AttributeError
return evt


def test_get_cluster_status_action_success(harness):
"""On success, the action wraps and forwards the status dict."""
# Prepare peer-databag so handler finds a cluster-name
rel = harness.add_relation("database-peers", "database-peers")
harness.update_relation_data(rel, harness.charm.app.name, {"cluster-name": "my-cluster"})

# Patch out the MySQL backend to return a known dict
sample = {"clusterrole": "primary", "status": "ok"}
fake = FakeMySQLBackend(response=sample)
with patch.object(MySQLOperatorCharm, "_mysql", new_callable=PropertyMock, return_value=fake):
evt = make_event()

# Invoke the action
harness.charm._get_cluster_status(evt)

# Expect set_results called once with {'success': True, 'status': sample}
evt.set_results.assert_called_once_with({"success": True, "status": sample})
evt.fail.assert_not_called()


def test_get_cluster_status_action_failure(harness):
"""On backend error, the action calls event.fail() and does not set_results()."""
# Seed peer-databag for cluster-name lookup
rel = harness.add_relation("database-peers", "database-peers")
harness.update_relation_data(rel, harness.charm.app.name, {"cluster-name": "my-cluster"})

# Patch MySQL backend to always raise
fake = FakeMySQLBackend(error=RuntimeError("boom"))
with patch.object(MySQLOperatorCharm, "_mysql", new_callable=PropertyMock, return_value=fake):
evt = make_event()

# Invoke the action
harness.charm._get_cluster_status(evt)

# It should report failure and never set_results
evt.fail.assert_called_once()
args, _ = evt.fail.call_args
assert "Failed to read cluster status" in args[0]

evt.set_results.assert_not_called()


def test_get_cluster_status_action_none_return(harness):
"""When the backend returns None (no error), the action should fail."""
rel = harness.add_relation("database-peers", "database-peers")
harness.update_relation_data(rel, harness.charm.app.name, {"cluster-name": "my-cluster"})

fake = FakeMySQLBackend(response=None) # Simulate silent failure
with patch.object(MySQLOperatorCharm, "_mysql", new_callable=PropertyMock, return_value=fake):
evt = make_event()
harness.charm._get_cluster_status(evt)

evt.fail.assert_called_once_with(
"Failed to read cluster status. See logs for more information."
)
evt.set_results.assert_not_called()
Loading