2
2
3
3
import tempfile
4
4
from pathlib import Path
5
+ from typing import Type
5
6
6
7
from click .testing import CliRunner
7
8
from django .core .management import call_command
8
9
10
+ from sentry .incidents .models import (
11
+ AlertRule ,
12
+ AlertRuleActivity ,
13
+ AlertRuleExcludedProjects ,
14
+ AlertRuleTrigger ,
15
+ AlertRuleTriggerAction ,
16
+ AlertRuleTriggerExclusion ,
17
+ )
9
18
from sentry .models .environment import Environment
19
+ from sentry .models .organization import Organization
10
20
from sentry .monitors .models import Monitor , MonitorEnvironment , MonitorType , ScheduleType
11
21
from sentry .runner .commands .backup import import_ , validate
12
22
from sentry .silo import unguarded_write
23
+ from sentry .snuba .models import QuerySubscription , SnubaQuery , SnubaQueryEventType
13
24
from sentry .testutils import TransactionTestCase
25
+ from sentry .utils .json import JSONData
14
26
from tests .sentry .backup import ValidationError , tmp_export_to_file
15
27
16
28
29
+ def targets_models (* expected_models : Type ):
30
+ """A helper decorator that checks that every model that a test "targeted" was actually seen in
31
+ the output, ensuring that we're actually testing the thing we think we are. Additionally, this
32
+ decorator is easily legible to static analysis, which allows for static checks to ensure that
33
+ all `__include_in_export__ = True` models are being tested."""
34
+
35
+ def decorator (func ):
36
+ def wrapped (* args , ** kwargs ):
37
+ ret = func (* args , ** kwargs )
38
+ if ret is None :
39
+ return AssertionError (f"The test { func .__name__ } did not return its actual JSON" )
40
+ actual_model_names = {entry ["model" ] for entry in ret }
41
+ expected_model_names = {"sentry." + model .__name__ .lower () for model in expected_models }
42
+ notfound = sorted (expected_model_names - actual_model_names )
43
+ if len (notfound ) > 0 :
44
+ raise AssertionError (f"Some `@targets_models` entries were not used: { notfound } " )
45
+ return ret
46
+
47
+ return wrapped
48
+
49
+ return decorator
50
+
51
+
17
52
class ModelBackupTests (TransactionTestCase ):
18
53
"""Test the JSON-ification of models marked `__include_in_export__ = True`. Each test here
19
54
creates a fresh database, performs some writes to it, then exports that data into a temporary
@@ -26,16 +61,19 @@ def setUp(self):
26
61
# Reset the Django database.
27
62
call_command ("flush" , verbosity = 0 , interactive = False )
28
63
29
- def import_export_then_validate (self ):
64
+ def import_export_then_validate (self ) -> JSONData :
30
65
"""Test helper that validates that data imported from a temporary `.json` file correctly
31
- matches the actual outputted export data."""
66
+ matches the actual outputted export data.
67
+
68
+ Return the actual JSON, so that we may use the `@targets_models` decorator to ensure that
69
+ we have at least one instance of all the "tested for" models in the actual output."""
32
70
33
71
with tempfile .TemporaryDirectory () as tmpdir :
34
72
tmp_expect = Path (tmpdir ).joinpath (f"{ self ._testMethodName } .expect.json" )
35
73
tmp_actual = Path (tmpdir ).joinpath (f"{ self ._testMethodName } .actual.json" )
36
74
37
- # Export the current state of the database into the "expected" temporary file, then parse it
38
- # into a JSON object for comparison.
75
+ # Export the current state of the database into the "expected" temporary file, then
76
+ # parse it into a JSON object for comparison.
39
77
expect = tmp_export_to_file (tmp_expect )
40
78
41
79
# Write the contents of the "expected" JSON file into the now clean database.
@@ -52,6 +90,8 @@ def import_export_then_validate(self):
52
90
if res .findings :
53
91
raise ValidationError (res )
54
92
93
+ return actual
94
+
55
95
def create_monitor (self ):
56
96
"""Re-usable monitor object for test cases."""
57
97
@@ -65,42 +105,49 @@ def create_monitor(self):
65
105
config = {"schedule" : "* * * * *" , "schedule_type" : ScheduleType .CRONTAB },
66
106
)
67
107
108
+ @targets_models (AlertRule , QuerySubscription , SnubaQuery , SnubaQueryEventType )
68
109
def test_alert_rule (self ):
69
110
self .create_alert_rule ()
70
- self .import_export_then_validate ()
111
+ return self .import_export_then_validate ()
71
112
113
+ @targets_models (AlertRuleActivity , AlertRuleExcludedProjects )
72
114
def test_alert_rule_excluded_projects (self ):
73
115
user = self .create_user ()
74
116
org = self .create_organization (owner = user )
75
117
excluded = self .create_project (organization = org )
76
118
self .create_alert_rule (include_all_projects = True , excluded_projects = [excluded ])
77
- self .import_export_then_validate ()
119
+ return self .import_export_then_validate ()
78
120
121
+ @targets_models (AlertRuleTrigger , AlertRuleTriggerAction , AlertRuleTriggerExclusion )
79
122
def test_alert_rule_trigger (self ):
80
123
excluded = self .create_project ()
81
124
rule = self .create_alert_rule (include_all_projects = True )
82
125
trigger = self .create_alert_rule_trigger (alert_rule = rule , excluded_projects = [excluded ])
83
126
self .create_alert_rule_trigger_action (alert_rule_trigger = trigger )
84
- self .import_export_then_validate ()
127
+ return self .import_export_then_validate ()
85
128
129
+ @targets_models (Environment )
86
130
def test_environment (self ):
87
131
self .create_environment ()
88
- self .import_export_then_validate ()
132
+ return self .import_export_then_validate ()
89
133
134
+ @targets_models (Monitor )
90
135
def test_monitor (self ):
91
136
self .create_monitor ()
92
- self .import_export_then_validate ()
137
+ return self .import_export_then_validate ()
93
138
139
+ @targets_models (MonitorEnvironment )
94
140
def test_monitor_environment (self ):
95
141
monitor = self .create_monitor ()
96
142
env = Environment .objects .create (organization_id = monitor .organization_id , name = "test_env" )
97
143
MonitorEnvironment .objects .create (
98
144
monitor = monitor ,
99
145
environment = env ,
100
146
)
101
- self .import_export_then_validate ()
147
+ return self .import_export_then_validate ()
102
148
149
+ @targets_models (Organization )
103
150
def test_organization (self ):
104
151
user = self .create_user ()
105
152
self .create_organization (owner = user )
106
- self .import_export_then_validate ()
153
+ return self .import_export_then_validate ()
0 commit comments