Skip to content
Merged
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
32 changes: 32 additions & 0 deletions src/sentry/integrations/repository/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,26 @@
from typing import Any


class NotificationMessageValidationError(Exception):
"""
Base error that is raised when there is a validation error
"""

pass


class MessageIdentifierWithErrorValidationError(NotificationMessageValidationError):
"""
Raised when a NotificationMessage has an error with a message identifier.
A NotificationMessage can only have a message identifier when it is successful; therefore if error details exist,
it means that the NotificationMessage was NOT successful, which implies that it should not have the value.
"""

message = (
"cannot create a new notification message with message identifier when an error exists"
)


@dataclass(frozen=True)
class BaseNotificationMessage:
"""
Expand Down Expand Up @@ -30,3 +50,15 @@ class BaseNewNotificationMessage:
error_code: int | None = None
message_identifier: str | None = None
parent_notification_message_id: int | None = None

def get_validation_error(self) -> Exception | None:
"""
Helper method for getting any potential validation errors based on the state of the data.
There are particular restrictions about the various fields, and this is to help the user check before
trying to instantiate a new instance in the datastore.
"""
if self.message_identifier is not None:
if self.error_code is not None or self.error_details is not None:
return MessageIdentifierWithErrorValidationError()

return None
40 changes: 17 additions & 23 deletions src/sentry/integrations/repository/metric_alert.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,21 @@

from sentry.incidents.models.alert_rule import AlertRuleTriggerAction
from sentry.incidents.models.incident import Incident
from sentry.integrations.repository.base import BaseNewNotificationMessage, BaseNotificationMessage
from sentry.integrations.repository.base import (
BaseNewNotificationMessage,
BaseNotificationMessage,
NotificationMessageValidationError,
)
from sentry.models.notificationmessage import NotificationMessage

_default_logger: Logger = getLogger(__name__)


@dataclass(frozen=True)
class MetricAlertNotificationMessage(BaseNotificationMessage):
# TODO(Yash): do we really need this entire model, or can we whittle it down to what we need?
incident: Incident | None = None
# TODO(Yash): do we really need this entire model, or can we whittle it down to what we need?
trigger_action: AlertRuleTriggerAction | None = None

@classmethod
Expand All @@ -34,45 +40,33 @@ def from_model(cls, instance: NotificationMessage) -> MetricAlertNotificationMes
)


class NewMetricAlertNotificationMessageValidationError(Exception):
class NewMetricAlertNotificationMessageValidationError(NotificationMessageValidationError):
pass


class IncidentAndTriggerActionValidationError(NewMetricAlertNotificationMessageValidationError):
message = "both incident and trigger action need to exist together with a reference"


class MessageIdentifierWithErrorValidationError(NewMetricAlertNotificationMessageValidationError):
message = (
"cannot create a new notification message with message identifier when an error exists"
)


@dataclass
class NewMetricAlertNotificationMessage(BaseNewNotificationMessage):
incident_id: int | None = None
trigger_action_id: int | None = None

def get_validation_error(self) -> Exception | None:
"""
Helper method for getting any potential validation errors based on the state of the data.
There are particular restrictions about the various fields, and this is to help the user check before
trying to instantiate a new instance in the datastore.
"""
if self.message_identifier is not None:
if self.error_code is not None or self.error_details is not None:
return MessageIdentifierWithErrorValidationError()
error = super().get_validation_error()
if error is not None:
return error

if self.message_identifier is not None:
# If a message_identifier exists, that means a successful notification happened for an incident and trigger
# This means that neither of them can be empty
if self.incident_id is None or self.trigger_action_id is None:
return IncidentAndTriggerActionValidationError()

incident_exists_without_trigger = (
self.incident_id is not None and self.trigger_action_id is None
)
trigger_exists_without_incident = (
self.incident_id is None and self.trigger_action_id is not None
)
if incident_exists_without_trigger or trigger_exists_without_incident:
# We can create a NotificationMessage if it has both, or neither, of incident and trigger.
# The following is an XNOR check for incident and trigger
if (self.incident_id is not None) != (self.trigger_action_id is not None):
Copy link
Member

Choose a reason for hiding this comment

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

nice nice

return IncidentAndTriggerActionValidationError()

return None
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import pytest

from sentry.integrations.repository.base import (
BaseNewNotificationMessage,
MessageIdentifierWithErrorValidationError,
)


class TestGetValidationError:
@classmethod
def _raises_error_for_obj(
cls, obj: BaseNewNotificationMessage, expected_error: type[Exception]
) -> None:
error = obj.get_validation_error()
assert error is not None
with pytest.raises(expected_error):
raise error

def test_returns_error_when_message_identifier_has_error_code(self) -> None:
obj = BaseNewNotificationMessage(
message_identifier="abc",
error_code=400,
)
self._raises_error_for_obj(obj, MessageIdentifierWithErrorValidationError)

def test_returns_error_when_message_identifier_has_error_details(self) -> None:
obj = BaseNewNotificationMessage(
message_identifier="abc",
error_details={"some_key": 123},
)
self._raises_error_for_obj(obj, MessageIdentifierWithErrorValidationError)

def test_returns_error_when_message_identifier_has_error(self) -> None:
obj = BaseNewNotificationMessage(
message_identifier="abc",
error_code=400,
error_details={"some_key": 123},
)
self._raises_error_for_obj(obj, MessageIdentifierWithErrorValidationError)

def test_simple(self) -> None:
obj = BaseNewNotificationMessage()
error = obj.get_validation_error()
assert error is None
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import pytest

from sentry.integrations.repository.base import MessageIdentifierWithErrorValidationError
from sentry.integrations.repository.metric_alert import (
IncidentAndTriggerActionValidationError,
MessageIdentifierWithErrorValidationError,
NewMetricAlertNotificationMessage,
)

Expand Down