blob: 351ec62f8fbf8bd918cb734fe8b3ec0289a1850b [file] [log] [blame]
Copybara854996b2021-09-07 19:36:02 +00001# Copyright 2016 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style
3# license that can be found in the LICENSE file or at
4# https://developers.google.com/open-source/licenses/bsd
5
6"""Tests for the spam service."""
7from __future__ import print_function
8from __future__ import division
9from __future__ import absolute_import
10
11import mock
12import unittest
13
Adrià Vilanova Martínez9f9ade52022-10-10 23:20:11 +020014try:
15 from mox3 import mox
16except ImportError:
17 import mox
Copybara854996b2021-09-07 19:36:02 +000018
19from google.appengine.ext import testbed
20
21import settings
22from framework import sql
23from framework import framework_constants
24from proto import user_pb2
25from proto import tracker_pb2
26from services import spam_svc
27from testing import fake
28from mock import Mock
29
30
31def assert_unreached():
32 raise Exception('This code should not have been called.') # pragma: no cover
33
34
35class SpamServiceTest(unittest.TestCase):
36
37 def setUp(self):
38 self.testbed = testbed.Testbed()
39 self.testbed.activate()
40
41 self.mox = mox.Mox()
42 self.mock_report_tbl = self.mox.CreateMock(sql.SQLTableManager)
43 self.mock_verdict_tbl = self.mox.CreateMock(sql.SQLTableManager)
44 self.mock_issue_tbl = self.mox.CreateMock(sql.SQLTableManager)
45 self.cnxn = self.mox.CreateMock(sql.MonorailConnection)
46 self.issue_service = fake.IssueService()
47 self.spam_service = spam_svc.SpamService()
48 self.spam_service.report_tbl = self.mock_report_tbl
49 self.spam_service.verdict_tbl = self.mock_verdict_tbl
50 self.spam_service.issue_tbl = self.mock_issue_tbl
51
52 self.spam_service.report_tbl.Delete = Mock()
53 self.spam_service.verdict_tbl.Delete = Mock()
54
55 def tearDown(self):
56 self.testbed.deactivate()
57 self.mox.UnsetStubs()
58 self.mox.ResetAll()
59
60 def testLookupIssuesFlaggers(self):
61 self.mock_report_tbl.Select(
62 self.cnxn, cols=['issue_id', 'user_id', 'comment_id'],
63 issue_id=[234, 567, 890]).AndReturn([
64 [234, 111, None],
65 [234, 222, 1],
66 [567, 333, None]])
67 self.mox.ReplayAll()
68
69 reporters = (
70 self.spam_service.LookupIssuesFlaggers(self.cnxn, [234, 567, 890]))
71 self.mox.VerifyAll()
72 self.assertEqual({
73 234: ([111], {1: [222]}),
74 567: ([333], {}),
75 }, reporters)
76
77 def testLookupIssueFlaggers(self):
78 self.mock_report_tbl.Select(
79 self.cnxn, cols=['issue_id', 'user_id', 'comment_id'],
80 issue_id=[234]).AndReturn(
81 [[234, 111, None], [234, 222, 1]])
82 self.mox.ReplayAll()
83
84 issue_reporters, comment_reporters = (
85 self.spam_service.LookupIssueFlaggers(self.cnxn, 234))
86 self.mox.VerifyAll()
87 self.assertItemsEqual([111], issue_reporters)
88 self.assertEqual({1: [222]}, comment_reporters)
89
90 def testFlagIssues_overThresh(self):
91 issue = fake.MakeTestIssue(
92 project_id=789,
93 local_id=1,
94 reporter_id=111,
95 owner_id=456,
96 summary='sum',
97 status='Live',
98 issue_id=78901,
99 project_name='proj')
100 issue.assume_stale = False # We will store this issue.
101
102 self.mock_report_tbl.InsertRows(self.cnxn,
103 ['issue_id', 'reported_user_id', 'user_id'],
104 [(78901, 111, 111)], ignore=True)
105
106 self.mock_report_tbl.Select(self.cnxn,
107 cols=['issue_id', 'COUNT(*)'], group_by=['issue_id'],
108 issue_id=[78901]).AndReturn([(78901, settings.spam_flag_thresh)])
109 self.mock_verdict_tbl.Select(
110 self.cnxn, cols=['issue_id', 'reason', 'MAX(created)'],
111 group_by=['issue_id'], issue_id=[78901], comment_id=None).AndReturn([])
112 self.mock_verdict_tbl.InsertRows(
113 self.cnxn, ['issue_id', 'is_spam', 'reason', 'project_id'],
114 [(78901, True, 'threshold', 789)], ignore=True)
115
116 self.mox.ReplayAll()
117 self.spam_service.FlagIssues(
118 self.cnxn, self.issue_service, [issue], 111, True)
119 self.mox.VerifyAll()
120 self.assertIn(issue, self.issue_service.updated_issues)
121
122 self.assertEqual(
123 1,
124 self.spam_service.issue_actions.get(
125 fields={
126 'type': 'flag',
127 'reporter_id': str(111),
128 'issue': 'proj:1'
129 }))
130
131 def testFlagIssues_underThresh(self):
132 issue = fake.MakeTestIssue(
133 project_id=789,
134 local_id=1,
135 reporter_id=111,
136 owner_id=456,
137 summary='sum',
138 status='Live',
139 issue_id=78901,
140 project_name='proj')
141
142 self.mock_report_tbl.InsertRows(self.cnxn,
143 ['issue_id', 'reported_user_id', 'user_id'],
144 [(78901, 111, 111)], ignore=True)
145
146 self.mock_report_tbl.Select(self.cnxn,
147 cols=['issue_id', 'COUNT(*)'], group_by=['issue_id'],
148 issue_id=[78901]).AndReturn([(78901, settings.spam_flag_thresh - 1)])
149
150 self.mock_verdict_tbl.Select(
151 self.cnxn, cols=['issue_id', 'reason', 'MAX(created)'],
152 group_by=['issue_id'], issue_id=[78901], comment_id=None).AndReturn([])
153
154 self.mox.ReplayAll()
155 self.spam_service.FlagIssues(
156 self.cnxn, self.issue_service, [issue], 111, True)
157 self.mox.VerifyAll()
158
159 self.assertNotIn(issue, self.issue_service.updated_issues)
160 self.assertIsNone(
161 self.spam_service.issue_actions.get(
162 fields={
163 'type': 'flag',
164 'reporter_id': str(111),
165 'issue': 'proj:1'
166 }))
167
168 def testUnflagIssue_overThresh(self):
169 issue = fake.MakeTestIssue(
170 project_id=789, local_id=1, reporter_id=111, owner_id=456,
171 summary='sum', status='Live', issue_id=78901, is_spam=True)
172 self.mock_report_tbl.Delete(self.cnxn, issue_id=[issue.issue_id],
173 comment_id=None, user_id=111)
174 self.mock_report_tbl.Select(self.cnxn,
175 cols=['issue_id', 'COUNT(*)'], group_by=['issue_id'],
176 issue_id=[78901]).AndReturn([(78901, settings.spam_flag_thresh)])
177
178 self.mock_verdict_tbl.Select(
179 self.cnxn, cols=['issue_id', 'reason', 'MAX(created)'],
180 group_by=['issue_id'], issue_id=[78901], comment_id=None).AndReturn([])
181
182 self.mox.ReplayAll()
183 self.spam_service.FlagIssues(
184 self.cnxn, self.issue_service, [issue], 111, False)
185 self.mox.VerifyAll()
186
187 self.assertNotIn(issue, self.issue_service.updated_issues)
188 self.assertEqual(True, issue.is_spam)
189
190 def testUnflagIssue_underThresh(self):
191 """A non-member un-flagging an issue as spam should not be able
192 to overturn the verdict to ham. This is different from previous
193 behavior. See https://crbug.com/monorail/2232 for details."""
194 issue = fake.MakeTestIssue(
195 project_id=789, local_id=1, reporter_id=111, owner_id=456,
196 summary='sum', status='Live', issue_id=78901, is_spam=True)
197 issue.assume_stale = False # We will store this issue.
198 self.mock_report_tbl.Delete(self.cnxn, issue_id=[issue.issue_id],
199 comment_id=None, user_id=111)
200 self.mock_report_tbl.Select(self.cnxn,
201 cols=['issue_id', 'COUNT(*)'], group_by=['issue_id'],
202 issue_id=[78901]).AndReturn([(78901, settings.spam_flag_thresh - 1)])
203
204 self.mock_verdict_tbl.Select(
205 self.cnxn, cols=['issue_id', 'reason', 'MAX(created)'],
206 group_by=['issue_id'], issue_id=[78901], comment_id=None).AndReturn([])
207
208 self.mox.ReplayAll()
209 self.spam_service.FlagIssues(
210 self.cnxn, self.issue_service, [issue], 111, False)
211 self.mox.VerifyAll()
212
213 self.assertNotIn(issue, self.issue_service.updated_issues)
214 self.assertEqual(True, issue.is_spam)
215
216 def testUnflagIssue_underThreshNoManualOverride(self):
217 issue = fake.MakeTestIssue(
218 project_id=789, local_id=1, reporter_id=111, owner_id=456,
219 summary='sum', status='Live', issue_id=78901, is_spam=True)
220 self.mock_report_tbl.Delete(self.cnxn, issue_id=[issue.issue_id],
221 comment_id=None, user_id=111)
222 self.mock_report_tbl.Select(self.cnxn,
223 cols=['issue_id', 'COUNT(*)'], group_by=['issue_id'],
224 issue_id=[78901]).AndReturn([(78901, settings.spam_flag_thresh - 1)])
225
226 self.mock_verdict_tbl.Select(
227 self.cnxn, cols=['issue_id', 'reason', 'MAX(created)'],
228 group_by=['issue_id'], comment_id=None,
229 issue_id=[78901]).AndReturn([(78901, 'manual', '')])
230
231 self.mox.ReplayAll()
232 self.spam_service.FlagIssues(
233 self.cnxn, self.issue_service, [issue], 111, False)
234 self.mox.VerifyAll()
235
236 self.assertNotIn(issue, self.issue_service.updated_issues)
237 self.assertEqual(True, issue.is_spam)
238
Copybara854996b2021-09-07 19:36:02 +0000239 def testIsExempt_RegularUser(self):
240 author = user_pb2.MakeUser(111, email='test@example.com')
241 self.assertFalse(self.spam_service._IsExempt(author, False))
242 author = user_pb2.MakeUser(111, email='test@chromium.org.example.com')
243 self.assertFalse(self.spam_service._IsExempt(author, False))
244
245 def testIsExempt_ProjectMember(self):
246 author = user_pb2.MakeUser(111, email='test@example.com')
247 self.assertTrue(self.spam_service._IsExempt(author, True))
248
249 def testIsExempt_AllowlistedDomain(self):
250 author = user_pb2.MakeUser(111, email='test@google.com')
251 self.assertTrue(self.spam_service._IsExempt(author, False))
252
253 def testClassifyIssue_spam(self):
254 issue = fake.MakeTestIssue(
255 project_id=789, local_id=1, reporter_id=111, owner_id=456,
256 summary='sum', status='Live', issue_id=78901, is_spam=True)
257 self.spam_service._predict = lambda body: 1.0
258
259 # Prevent missing service inits to fail the test.
260 self.spam_service.ml_engine = True
261
262 comment_pb = tracker_pb2.IssueComment()
263 comment_pb.content = "this is spam"
264 reporter = user_pb2.MakeUser(111, email='test@test.com')
265 res = self.spam_service.ClassifyIssue(issue, comment_pb, reporter, False)
266 self.assertEqual(1.0, res['confidence_is_spam'])
267
268 reporter.email = 'test@chromium.org.spam.com'
269 res = self.spam_service.ClassifyIssue(issue, comment_pb, reporter, False)
270 self.assertEqual(1.0, res['confidence_is_spam'])
271
272 reporter.email = 'test.google.com@test.com'
273 res = self.spam_service.ClassifyIssue(issue, comment_pb, reporter, False)
274 self.assertEqual(1.0, res['confidence_is_spam'])
275
276 def testClassifyIssue_Allowlisted(self):
277 issue = fake.MakeTestIssue(
278 project_id=789, local_id=1, reporter_id=111, owner_id=456,
279 summary='sum', status='Live', issue_id=78901, is_spam=True)
280 self.spam_service._predict = assert_unreached
281
282 # Prevent missing service inits to fail the test.
283 self.spam_service.ml_engine = True
284
285 comment_pb = tracker_pb2.IssueComment()
286 comment_pb.content = "this is spam"
287 reporter = user_pb2.MakeUser(111, email='test@google.com')
288 res = self.spam_service.ClassifyIssue(issue, comment_pb, reporter, False)
289 self.assertEqual(0.0, res['confidence_is_spam'])
290 reporter.email = 'test@chromium.org'
291 res = self.spam_service.ClassifyIssue(issue, comment_pb, reporter, False)
292 self.assertEqual(0.0, res['confidence_is_spam'])
293
294 def testClassifyComment_spam(self):
295 self.spam_service._predict = lambda body: 1.0
296
297 # Prevent missing service inits to fail the test.
298 self.spam_service.ml_engine = True
299
300 commenter = user_pb2.MakeUser(111, email='test@test.com')
301 res = self.spam_service.ClassifyComment('this is spam', commenter, False)
302 self.assertEqual(1.0, res['confidence_is_spam'])
303
304 commenter.email = 'test@chromium.org.spam.com'
305 res = self.spam_service.ClassifyComment('this is spam', commenter, False)
306 self.assertEqual(1.0, res['confidence_is_spam'])
307
308 commenter.email = 'test.google.com@test.com'
309 res = self.spam_service.ClassifyComment('this is spam', commenter, False)
310 self.assertEqual(1.0, res['confidence_is_spam'])
311
312 def testClassifyComment_Allowlisted(self):
313 self.spam_service._predict = assert_unreached
314
315 # Prevent missing service inits to fail the test.
316 self.spam_service.ml_engine = True
317
318 commenter = user_pb2.MakeUser(111, email='test@google.com')
319 res = self.spam_service.ClassifyComment('this is spam', commenter, False)
320 self.assertEqual(0.0, res['confidence_is_spam'])
321
322 commenter.email = 'test@chromium.org'
323 res = self.spam_service.ClassifyComment('this is spam', commenter, False)
324 self.assertEqual(0.0, res['confidence_is_spam'])
325
326 def test_ham_classification(self):
327 actual = self.spam_service.ham_classification()
328 self.assertEqual(actual['confidence_is_spam'], 0.0)
329 self.assertEqual(actual['failed_open'], False)
330
331 def testExpungeUsersInSpam(self):
332 user_ids = [3, 4, 5]
333 self.spam_service.ExpungeUsersInSpam(self.cnxn, user_ids=user_ids)
334
335 self.spam_service.report_tbl.Delete.assert_has_calls(
336 [
337 mock.call(self.cnxn, reported_user_id=user_ids, commit=False),
338 mock.call(self.cnxn, user_id=user_ids, commit=False)
339 ])
340 self.spam_service.verdict_tbl.Delete.assert_called_once_with(
341 self.cnxn, user_id=user_ids, commit=False)
342
343 def testLookupIssueVerdicts(self):
344 self.spam_service.verdict_tbl.Select = Mock(return_value=[
345 [5, 10], [4, 11], [6, 12],
346 ])
347 actual = self.spam_service.LookupIssueVerdicts(self.cnxn, [4, 5, 6])
348
349 self.spam_service.verdict_tbl.Select.assert_called_once_with(
350 self.cnxn, cols=['issue_id', 'reason', 'MAX(created)'],
351 issue_id=[4, 5, 6], comment_id=None, group_by=['issue_id'])
352 self.assertEqual(actual, {
353 5: 10,
354 4: 11,
355 6: 12,
356 })