-
-
Notifications
You must be signed in to change notification settings - Fork 4.4k
chore(aci): enforce config schema without subclassing #81979
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 9 commits
c71000f
e04047c
22fd0ff
2981601
0ce41d2
af1fafd
c9f5d3d
c49a9e8
f8737dd
cfd0f0f
667c172
1b33d57
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,8 @@ | |
|
||
from django.conf import settings | ||
from django.db import models | ||
from django.db.models.signals import pre_save | ||
from django.dispatch import receiver | ||
|
||
from sentry.backup.scopes import RelocationScope | ||
from sentry.db.models import DefaultFieldsModel, FlexibleForeignKey, region_silo_model, sane_repr | ||
|
@@ -36,7 +38,8 @@ class Workflow(DefaultFieldsModel, OwnerModel, JSONConfigBase): | |
|
||
@property | ||
def CONFIG_SCHEMA(self) -> dict[str, Any]: | ||
cathteng marked this conversation as resolved.
Show resolved
Hide resolved
|
||
raise NotImplementedError('Subclasses must define a "CONFIG_SCHEMA" attribute') | ||
# TODO: fill in | ||
return {} | ||
Comment on lines
+41
to
+42
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is the plan to use subclasses here? Won't we run into the same problem we proxy models? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. there isn't a type on workflow so all of them should have the same config schema There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not too clear on what these configs are going to be... If they're consistent for all Workflows this is fine though |
||
|
||
__repr__ = sane_repr("name", "organization_id") | ||
|
||
|
@@ -60,3 +63,9 @@ def evaluate_trigger_conditions(self, evt: GroupEvent) -> bool: | |
|
||
evaluation, _ = evaluate_condition_group(self.when_condition_group, evt) | ||
return evaluation | ||
|
||
|
||
@receiver(pre_save, sender=Workflow) | ||
def enforce_config_schema(sender, instance: Workflow, **kwargs): | ||
config_schema = instance.CONFIG_SCHEMA | ||
instance.validate_config(config_schema) | ||
cathteng marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
from dataclasses import dataclass | ||
from unittest.mock import PropertyMock, patch | ||
|
||
import pytest | ||
from jsonschema import ValidationError | ||
|
||
from sentry.issues.grouptype import GroupCategory, GroupType | ||
from sentry.utils.registry import NoRegistrationExistsError | ||
from tests.sentry.issues.test_grouptype import BaseGroupTypeTest | ||
|
||
|
||
class TestJsonConfigBase(BaseGroupTypeTest): | ||
def setUp(self): | ||
super().setUp() | ||
self.correct_config = { | ||
"username": "user123", | ||
"email": "[email protected]", | ||
"fullName": "John Doe", | ||
"age": 30, | ||
"location": "Cityville", | ||
"interests": ["Travel", "Technology"], | ||
} | ||
|
||
@dataclass(frozen=True) | ||
class TestGroupType(GroupType): | ||
type_id = 1 | ||
slug = "test" | ||
description = "Test" | ||
category = GroupCategory.ERROR.value | ||
detector_config_schema = self.example_schema | ||
|
||
@pytest.fixture(autouse=True) | ||
def initialize_configs(self): | ||
self.example_schema = { | ||
"$id": "https://example.com/user-profile.schema.json", | ||
"$schema": "https://json-schema.org/draft/2020-12/schema", | ||
"description": "A representation of a user profile", | ||
"type": "object", | ||
"required": ["username", "email"], | ||
"properties": { | ||
"username": {"type": "string"}, | ||
"email": {"type": "string", "format": "email"}, | ||
"fullName": {"type": "string"}, | ||
"age": {"type": "integer", "minimum": 0}, | ||
"location": {"type": "string"}, | ||
"interests": {"type": "array", "items": {"type": "string"}}, | ||
}, | ||
} | ||
with ( | ||
patch( | ||
"sentry.workflow_engine.models.Workflow.CONFIG_SCHEMA", | ||
return_value=self.example_schema, | ||
new_callable=PropertyMock, | ||
), | ||
): | ||
# Run test case | ||
yield | ||
|
||
|
||
class TestDetectorConfig(TestJsonConfigBase): | ||
def test_detector_no_registration(self): | ||
with pytest.raises(NoRegistrationExistsError): | ||
self.create_detector(name="test_detector", type="no_registration") | ||
|
||
def test_detector_mismatched_schema(self): | ||
with pytest.raises(ValidationError): | ||
self.create_detector(name="test_detector", type="test", config={"hi": "there"}) | ||
|
||
def test_detector_correct_schema(self): | ||
self.create_detector(name="test_detector", type="test", config=self.correct_config) | ||
|
||
|
||
class TestWorkflowConfig(TestJsonConfigBase): | ||
def test_workflow_mismatched_schema(self): | ||
with pytest.raises(ValidationError): | ||
self.create_workflow( | ||
organization=self.organization, name="test_workflow", config={"hi": "there"} | ||
) | ||
|
||
def test_workflow_correct_schema(self): | ||
self.create_workflow( | ||
organization=self.organization, name="test_workflow", config=self.correct_config | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -121,9 +121,16 @@ def test_state_results_multi_group(self, mock_produce_occurrence_to_kafka): | |
) | ||
|
||
def test_no_issue_type(self): | ||
detector = self.create_detector(type="invalid slug") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. with this PR we can no longer create a detector with an invalid slug, so substituting with creating a valid detector and mocking that the GroupType gets deleted |
||
detector = self.create_detector(type=self.handler_state_type.slug) | ||
data_packet = self.build_data_packet() | ||
with mock.patch("sentry.workflow_engine.models.detector.logger") as mock_logger: | ||
with ( | ||
mock.patch("sentry.workflow_engine.models.detector.logger") as mock_logger, | ||
mock.patch( | ||
"sentry.workflow_engine.models.Detector.group_type", | ||
return_value=None, | ||
new_callable=mock.PropertyMock, | ||
), | ||
): | ||
results = process_detectors(data_packet, [detector]) | ||
assert mock_logger.error.call_args[0][0] == "No registered grouptype for detector" | ||
assert results == [] | ||
|
@@ -326,7 +333,7 @@ def test_above_below_threshold(self): | |
} | ||
|
||
def test_no_condition_group(self): | ||
detector = self.create_detector() | ||
detector = self.create_detector(type=self.handler_type.slug) | ||
handler = MockDetectorStateHandler(detector) | ||
with mock.patch( | ||
"sentry.workflow_engine.handlers.detector.stateful.metrics" | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's starting to feel like we just need something like
DetectorConfig
that encapsulates all of these. Probably something for a separate pr