Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame] | 1 | # Copyright 2016 The Chromium Authors |
| 2 | # Use of this source code is governed by a BSD-style license that can be |
| 3 | # found in the LICENSE file. |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 4 | |
| 5 | """Unittest for the autolink feature.""" |
| 6 | from __future__ import print_function |
| 7 | from __future__ import division |
| 8 | from __future__ import absolute_import |
| 9 | |
| 10 | import re |
| 11 | import unittest |
| 12 | |
| 13 | from features import autolink |
| 14 | from features import autolink_constants |
| 15 | from framework import template_helpers |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame] | 16 | from mrproto import tracker_pb2 |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 17 | from testing import fake |
| 18 | from testing import testing_helpers |
| 19 | |
| 20 | |
| 21 | SIMPLE_EMAIL_RE = re.compile(r'([a-z]+)@([a-z]+)\.com') |
| 22 | OVER_AMBITIOUS_DOMAIN_RE = re.compile(r'([a-z]+)\.(com|net|org)') |
| 23 | |
| 24 | |
| 25 | class AutolinkTest(unittest.TestCase): |
| 26 | |
| 27 | def RegisterEmailCallbacks(self, aa): |
| 28 | |
| 29 | def LookupUsers(_mr, all_addresses): |
| 30 | """Return user objects for only users who are at trusted domains.""" |
| 31 | return [addr for addr in all_addresses |
| 32 | if addr.endswith('@example.com')] |
| 33 | |
| 34 | def Match2Addresses(_mr, match): |
| 35 | return [match.group(0)] |
| 36 | |
| 37 | def MakeMailtoLink(_mr, match, comp_ref_artifacts): |
| 38 | email = match.group(0) |
| 39 | if comp_ref_artifacts and email in comp_ref_artifacts: |
| 40 | return [template_helpers.TextRun( |
| 41 | tag='a', href='mailto:%s' % email, content=email)] |
| 42 | else: |
| 43 | return [template_helpers.TextRun('%s AT %s.com' % match.group(1, 2))] |
| 44 | |
| 45 | aa.RegisterComponent('testcomp', |
| 46 | LookupUsers, |
| 47 | Match2Addresses, |
| 48 | {SIMPLE_EMAIL_RE: MakeMailtoLink}) |
| 49 | |
| 50 | def RegisterDomainCallbacks(self, aa): |
| 51 | |
| 52 | def LookupDomains(_mr, _all_refs): |
| 53 | """Return business objects for only real domains. Always just True.""" |
| 54 | return True # We don't have domain business objects, accept anything. |
| 55 | |
| 56 | def Match2Domains(_mr, match): |
| 57 | return [match.group(0)] |
| 58 | |
| 59 | def MakeHyperLink(_mr, match, _comp_ref_artifacts): |
| 60 | domain = match.group(0) |
| 61 | return [template_helpers.TextRun(tag='a', href=domain, content=domain)] |
| 62 | |
| 63 | aa.RegisterComponent('testcomp2', |
| 64 | LookupDomains, |
| 65 | Match2Domains, |
| 66 | {OVER_AMBITIOUS_DOMAIN_RE: MakeHyperLink}) |
| 67 | |
| 68 | def setUp(self): |
| 69 | self.aa = autolink.Autolink() |
| 70 | self.RegisterEmailCallbacks(self.aa) |
| 71 | self.comment1 = ('Feel free to contact me at a@other.com, ' |
| 72 | 'or b@example.com, or c@example.org.') |
| 73 | self.comment2 = 'no matches in this comment' |
| 74 | self.comment3 = 'just matches with no ref: a@other.com, c@example.org' |
| 75 | self.comments = [self.comment1, self.comment2, self.comment3] |
| 76 | |
| 77 | def testRegisterComponent(self): |
| 78 | self.assertIn('testcomp', self.aa.registry) |
| 79 | |
| 80 | def testGetAllReferencedArtifacts(self): |
| 81 | all_ref_artifacts = self.aa.GetAllReferencedArtifacts( |
| 82 | None, self.comments) |
| 83 | |
| 84 | self.assertIn('testcomp', all_ref_artifacts) |
| 85 | comp_refs = all_ref_artifacts['testcomp'] |
| 86 | self.assertIn('b@example.com', comp_refs) |
| 87 | self.assertTrue(len(comp_refs) == 1) |
| 88 | |
| 89 | def testGetAllReferencedArtifacts_TooBig(self): |
| 90 | all_ref_artifacts = self.aa.GetAllReferencedArtifacts( |
| 91 | None, self.comments, max_total_length=10) |
| 92 | |
| 93 | self.assertEqual(autolink.SKIP_LOOKUPS, all_ref_artifacts) |
| 94 | |
| 95 | def testMarkupAutolinks(self): |
| 96 | all_ref_artifacts = self.aa.GetAllReferencedArtifacts(None, self.comments) |
| 97 | result = self.aa.MarkupAutolinks( |
| 98 | None, [template_helpers.TextRun(self.comment1)], all_ref_artifacts) |
| 99 | self.assertEqual('Feel free to contact me at ', result[0].content) |
| 100 | self.assertEqual('a AT other.com', result[1].content) |
| 101 | self.assertEqual(', or ', result[2].content) |
| 102 | self.assertEqual('b@example.com', result[3].content) |
| 103 | self.assertEqual('mailto:b@example.com', result[3].href) |
| 104 | self.assertEqual(', or c@example.org.', result[4].content) |
| 105 | |
| 106 | result = self.aa.MarkupAutolinks( |
| 107 | None, [template_helpers.TextRun(self.comment2)], all_ref_artifacts) |
| 108 | self.assertEqual('no matches in this comment', result[0].content) |
| 109 | |
| 110 | result = self.aa.MarkupAutolinks( |
| 111 | None, [template_helpers.TextRun(self.comment3)], all_ref_artifacts) |
| 112 | self.assertEqual('just matches with no ref: ', result[0].content) |
| 113 | self.assertEqual('a AT other.com', result[1].content) |
| 114 | self.assertEqual(', c@example.org', result[2].content) |
| 115 | |
| 116 | def testNonnestedAutolinks(self): |
| 117 | """Test that when a substitution yields plain text, others are applied.""" |
| 118 | self.RegisterDomainCallbacks(self.aa) |
| 119 | all_ref_artifacts = self.aa.GetAllReferencedArtifacts(None, self.comments) |
| 120 | result = self.aa.MarkupAutolinks( |
| 121 | None, [template_helpers.TextRun(self.comment1)], all_ref_artifacts) |
| 122 | self.assertEqual('Feel free to contact me at ', result[0].content) |
| 123 | self.assertEqual('a AT ', result[1].content) |
| 124 | self.assertEqual('other.com', result[2].content) |
| 125 | self.assertEqual('other.com', result[2].href) |
| 126 | self.assertEqual(', or ', result[3].content) |
| 127 | self.assertEqual('b@example.com', result[4].content) |
| 128 | self.assertEqual('mailto:b@example.com', result[4].href) |
| 129 | self.assertEqual(', or c@', result[5].content) |
| 130 | self.assertEqual('example.org', result[6].content) |
| 131 | self.assertEqual('example.org', result[6].href) |
| 132 | self.assertEqual('.', result[7].content) |
| 133 | |
| 134 | result = self.aa.MarkupAutolinks( |
| 135 | None, [template_helpers.TextRun(self.comment2)], all_ref_artifacts) |
| 136 | self.assertEqual('no matches in this comment', result[0].content) |
| 137 | result = self.aa.MarkupAutolinks( |
| 138 | None, [template_helpers.TextRun(self.comment3)], all_ref_artifacts) |
| 139 | self.assertEqual('just matches with no ref: ', result[0].content) |
| 140 | self.assertEqual('a AT ', result[1].content) |
| 141 | self.assertEqual('other.com', result[2].content) |
| 142 | self.assertEqual('other.com', result[2].href) |
| 143 | self.assertEqual(', c@', result[3].content) |
| 144 | self.assertEqual('example.org', result[4].content) |
| 145 | self.assertEqual('example.org', result[4].href) |
| 146 | |
| 147 | def testMarkupAutolinks_TooBig(self): |
| 148 | """If the issue has too much text, we just do regex-based autolinking.""" |
| 149 | all_ref_artifacts = self.aa.GetAllReferencedArtifacts( |
| 150 | None, self.comments, max_total_length=10) |
| 151 | result = self.aa.MarkupAutolinks( |
| 152 | None, [template_helpers.TextRun(self.comment1)], all_ref_artifacts) |
| 153 | self.assertEqual(5, len(result)) |
| 154 | self.assertEqual('Feel free to contact me at ', result[0].content) |
| 155 | # The test autolink handlers in this file do not link email addresses. |
| 156 | self.assertEqual('a AT other.com', result[1].content) |
| 157 | self.assertIsNone(result[1].href) |
| 158 | |
| 159 | class EmailAutolinkTest(unittest.TestCase): |
| 160 | |
| 161 | def setUp(self): |
| 162 | self.user_1 = 'fake user' # Note: no User fields are accessed. |
| 163 | |
| 164 | def DoLinkify( |
| 165 | self, content, filter_re=autolink_constants.IS_IMPLIED_EMAIL_RE): |
| 166 | """Calls the LinkifyEmail method and returns the result. |
| 167 | |
| 168 | Args: |
| 169 | content: string with a hyperlink. |
| 170 | |
| 171 | Returns: |
| 172 | A list of TextRuns with some runs having the embedded email hyperlinked. |
| 173 | Or, None if no link was detected. |
| 174 | """ |
| 175 | match = filter_re.search(content) |
| 176 | if not match: |
| 177 | return None |
| 178 | |
| 179 | return autolink.LinkifyEmail(None, match, {'one@example.com': self.user_1}) |
| 180 | |
| 181 | def testLinkifyEmail(self): |
| 182 | """Test that an address is autolinked when put in the given context.""" |
| 183 | test = 'one@ or @one' |
| 184 | result = self.DoLinkify('Have you met %s' % test) |
| 185 | self.assertEqual(None, result) |
| 186 | |
| 187 | test = 'one@example.com' |
| 188 | result = self.DoLinkify('Have you met %s' % test) |
| 189 | self.assertEqual('/u/' + test, result[0].href) |
| 190 | self.assertEqual(test, result[0].content) |
| 191 | |
| 192 | test = 'alias@example.com' |
| 193 | result = self.DoLinkify('Please also CC %s' % test) |
| 194 | self.assertEqual('mailto:' + test, result[0].href) |
| 195 | self.assertEqual(test, result[0].content) |
| 196 | |
| 197 | result = self.DoLinkify('Reviewed-By: Test Person <%s>' % test) |
| 198 | self.assertEqual('mailto:' + test, result[0].href) |
| 199 | self.assertEqual(test, result[0].content) |
| 200 | |
| 201 | |
| 202 | class URLAutolinkTest(unittest.TestCase): |
| 203 | |
| 204 | def DoLinkify(self, content, filter_re=autolink_constants.IS_A_LINK_RE): |
| 205 | """Calls the linkify method and returns the result. |
| 206 | |
| 207 | Args: |
| 208 | content: string with a hyperlink. |
| 209 | |
| 210 | Returns: |
| 211 | A list of TextRuns with some runs will have the embedded URL hyperlinked. |
| 212 | Or, None if no link was detected. |
| 213 | """ |
| 214 | match = filter_re.search(content) |
| 215 | if not match: |
| 216 | return None |
| 217 | |
| 218 | return autolink.Linkify(None, match, None) |
| 219 | |
| 220 | def testLinkify(self): |
| 221 | """Test that given url is autolinked when put in the given context.""" |
| 222 | # Disallow the linking of URLs with user names and passwords. |
| 223 | test = 'http://user:pass@www.yahoo.com' |
| 224 | result = self.DoLinkify('What about %s' % test) |
| 225 | self.assertEqual(None, result[0].tag) |
| 226 | self.assertEqual(None, result[0].href) |
| 227 | self.assertEqual(test, result[0].content) |
| 228 | |
| 229 | # Disallow the linking of non-HTTP(S) links |
| 230 | test = 'nntp://news.google.com' |
| 231 | result = self.DoLinkify('%s' % test) |
| 232 | self.assertEqual(None, result) |
| 233 | |
| 234 | # Disallow the linking of file links |
| 235 | test = 'file://C:/Windows/System32/cmd.exe' |
| 236 | result = self.DoLinkify('%s' % test) |
| 237 | self.assertEqual(None, result) |
| 238 | |
| 239 | # Test some known URLs |
| 240 | test = 'http://www.example.com' |
| 241 | result = self.DoLinkify('What about %s' % test) |
| 242 | self.assertEqual(test, result[0].href) |
| 243 | self.assertEqual(test, result[0].content) |
| 244 | |
| 245 | def testLinkify_FTP(self): |
| 246 | """Test that FTP urls are linked.""" |
| 247 | # Check for a standard ftp link |
| 248 | test = 'ftp://ftp.example.com' |
| 249 | result = self.DoLinkify('%s' % test) |
| 250 | self.assertEqual(test, result[0].href) |
| 251 | self.assertEqual(test, result[0].content) |
| 252 | |
| 253 | def testLinkify_Email(self): |
| 254 | """Test that mailto: urls are linked.""" |
| 255 | test = 'mailto:user@example.com' |
| 256 | result = self.DoLinkify('%s' % test) |
| 257 | self.assertEqual(test, result[0].href) |
| 258 | self.assertEqual(test, result[0].content) |
| 259 | |
| 260 | def testLinkify_ShortLink(self): |
| 261 | """Test that shortlinks are linked.""" |
| 262 | test = 'http://go/monorail' |
| 263 | result = self.DoLinkify( |
| 264 | '%s' % test, filter_re=autolink_constants.IS_A_SHORT_LINK_RE) |
| 265 | self.assertEqual(test, result[0].href) |
| 266 | self.assertEqual(test, result[0].content) |
| 267 | |
| 268 | test = 'go/monorail' |
| 269 | result = self.DoLinkify( |
| 270 | '%s' % test, filter_re=autolink_constants.IS_A_SHORT_LINK_RE) |
| 271 | self.assertEqual('http://' + test, result[0].href) |
| 272 | self.assertEqual(test, result[0].content) |
| 273 | |
| 274 | test = 'b/12345' |
| 275 | result = self.DoLinkify( |
| 276 | '%s' % test, filter_re=autolink_constants.IS_A_NUMERIC_SHORT_LINK_RE) |
| 277 | self.assertEqual('http://' + test, result[0].href) |
| 278 | self.assertEqual(test, result[0].content) |
| 279 | |
| 280 | test = 'http://b/12345' |
| 281 | result = self.DoLinkify( |
| 282 | '%s' % test, filter_re=autolink_constants.IS_A_NUMERIC_SHORT_LINK_RE) |
| 283 | self.assertEqual(test, result[0].href) |
| 284 | self.assertEqual(test, result[0].content) |
| 285 | |
| 286 | test = '/b/12345' |
| 287 | result = self.DoLinkify( |
| 288 | '%s' % test, filter_re=autolink_constants.IS_A_SHORT_LINK_RE) |
| 289 | self.assertIsNone(result) |
| 290 | |
| 291 | test = '/b/12345' |
| 292 | result = self.DoLinkify( |
| 293 | '%s' % test, filter_re=autolink_constants.IS_A_NUMERIC_SHORT_LINK_RE) |
| 294 | self.assertIsNone(result) |
| 295 | |
| 296 | test = 'b/secondFileInDiff' |
| 297 | result = self.DoLinkify( |
| 298 | '%s' % test, filter_re=autolink_constants.IS_A_NUMERIC_SHORT_LINK_RE) |
| 299 | self.assertIsNone(result) |
| 300 | |
| 301 | def testLinkify_ImpliedLink(self): |
| 302 | """Test that text with .com, .org, .net, and .edu are linked.""" |
| 303 | test = 'google.org' |
| 304 | result = self.DoLinkify( |
| 305 | '%s' % test, filter_re=autolink_constants.IS_IMPLIED_LINK_RE) |
| 306 | self.assertEqual('http://' + test, result[0].href) |
| 307 | self.assertEqual(test, result[0].content) |
| 308 | |
| 309 | test = 'code.google.com/p/chromium' |
| 310 | result = self.DoLinkify( |
| 311 | '%s' % test, filter_re=autolink_constants.IS_IMPLIED_LINK_RE) |
| 312 | self.assertEqual('http://' + test, result[0].href) |
| 313 | self.assertEqual(test, result[0].content) |
| 314 | |
| 315 | # This is not a domain, it is a directory or something. |
| 316 | test = 'build.out/p/chromium' |
| 317 | result = self.DoLinkify( |
| 318 | '%s' % test, filter_re=autolink_constants.IS_IMPLIED_LINK_RE) |
| 319 | self.assertEqual(None, result) |
| 320 | |
| 321 | # We do not link the NNTP scheme, and the domain name part of it will not |
| 322 | # be linked as an HTTP link because it is preceeded by "/". |
| 323 | test = 'nntp://news.google.com' |
| 324 | result = self.DoLinkify( |
| 325 | '%s' % test, filter_re=autolink_constants.IS_IMPLIED_LINK_RE) |
| 326 | self.assertIsNone(result) |
| 327 | |
| 328 | def testLinkify_Context(self): |
| 329 | """Test that surrounding syntax is not considered part of the url.""" |
| 330 | test = 'http://www.example.com' |
| 331 | |
| 332 | # Check for a link followed by a comma at end of English phrase. |
| 333 | result = self.DoLinkify('The URL %s, points to a great website.' % test) |
| 334 | self.assertEqual(test, result[0].href) |
| 335 | self.assertEqual(test, result[0].content) |
| 336 | self.assertEqual(',', result[1].content) |
| 337 | |
| 338 | # Check for a link followed by a period at end of English sentence. |
| 339 | result = self.DoLinkify('The best site ever, %s.' % test) |
| 340 | self.assertEqual(test, result[0].href) |
| 341 | self.assertEqual(test, result[0].content) |
| 342 | self.assertEqual('.', result[1].content) |
| 343 | |
| 344 | # Check for a link in paranthesis (), [], or {} |
| 345 | result = self.DoLinkify('My fav site (%s).' % test) |
| 346 | self.assertEqual(test, result[0].href) |
| 347 | self.assertEqual(test, result[0].content) |
| 348 | self.assertEqual(').', result[1].content) |
| 349 | |
| 350 | result = self.DoLinkify('My fav site [%s].' % test) |
| 351 | self.assertEqual(test, result[0].href) |
| 352 | self.assertEqual(test, result[0].content) |
| 353 | self.assertEqual('].', result[1].content) |
| 354 | |
| 355 | result = self.DoLinkify('My fav site {%s}.' % test) |
| 356 | self.assertEqual(test, result[0].href) |
| 357 | self.assertEqual(test, result[0].content) |
| 358 | self.assertEqual('}.', result[1].content) |
| 359 | |
| 360 | # Check for a link with trailing colon |
| 361 | result = self.DoLinkify('Hit %s: you will love it.' % test) |
| 362 | self.assertEqual(test, result[0].href) |
| 363 | self.assertEqual(test, result[0].content) |
| 364 | self.assertEqual(':', result[1].content) |
| 365 | |
| 366 | # Check link with commas in query string, but don't include trailing comma. |
| 367 | test = 'http://www.example.com/?v=1,2,3' |
| 368 | result = self.DoLinkify('Try %s, ok?' % test) |
| 369 | self.assertEqual(test, result[0].href) |
| 370 | self.assertEqual(test, result[0].content) |
| 371 | |
| 372 | # Check link surrounded by angle-brackets. |
| 373 | result = self.DoLinkify('<%s>' % test) |
| 374 | self.assertEqual(test, result[0].href) |
| 375 | self.assertEqual(test, result[0].content) |
| 376 | self.assertEqual('>', result[1].content) |
| 377 | |
| 378 | # Check link surrounded by double-quotes. |
| 379 | result = self.DoLinkify('"%s"' % test) |
| 380 | self.assertEqual(test, result[0].href) |
| 381 | self.assertEqual(test, result[0].content) |
| 382 | self.assertEqual('"', result[1].content) |
| 383 | |
| 384 | # Check link with embedded double-quotes. |
| 385 | test = 'http://www.example.com/?q="a+b+c"' |
| 386 | result = self.DoLinkify('Try %s, ok?' % test) |
| 387 | self.assertEqual(test, result[0].href) |
| 388 | self.assertEqual(test, result[0].content) |
| 389 | self.assertEqual(',', result[1].content) |
| 390 | |
| 391 | # Check link surrounded by single-quotes. |
| 392 | result = self.DoLinkify("'%s'" % test) |
| 393 | self.assertEqual(test, result[0].href) |
| 394 | self.assertEqual(test, result[0].content) |
| 395 | self.assertEqual("'", result[1].content) |
| 396 | |
| 397 | # Check link with embedded single-quotes. |
| 398 | test = "http://www.example.com/?q='a+b+c'" |
| 399 | result = self.DoLinkify('Try %s, ok?' % test) |
| 400 | self.assertEqual(test, result[0].href) |
| 401 | self.assertEqual(test, result[0].content) |
| 402 | self.assertEqual(',', result[1].content) |
| 403 | |
| 404 | # Check link with embedded parens. |
| 405 | test = 'http://www.example.com/funky(foo)and(bar).asp' |
| 406 | result = self.DoLinkify('Try %s, ok?' % test) |
| 407 | self.assertEqual(test, result[0].href) |
| 408 | self.assertEqual(test, result[0].content) |
| 409 | self.assertEqual(',', result[1].content) |
| 410 | |
| 411 | test = 'http://www.example.com/funky(foo)and(bar).asp' |
| 412 | result = self.DoLinkify('My fav site <%s>' % test) |
| 413 | self.assertEqual(test, result[0].href) |
| 414 | self.assertEqual(test, result[0].content) |
| 415 | self.assertEqual('>', result[1].content) |
| 416 | |
| 417 | # Check link with embedded brackets and braces. |
| 418 | test = 'http://www.example.com/funky[foo]and{bar}.asp' |
| 419 | result = self.DoLinkify('My fav site <%s>' % test) |
| 420 | self.assertEqual(test, result[0].href) |
| 421 | self.assertEqual(test, result[0].content) |
| 422 | self.assertEqual('>', result[1].content) |
| 423 | |
| 424 | # Check link with mismatched delimeters inside it or outside it. |
| 425 | test = 'http://www.example.com/funky"(foo]and>bar}.asp' |
| 426 | result = self.DoLinkify('My fav site <%s>' % test) |
| 427 | self.assertEqual(test, result[0].href) |
| 428 | self.assertEqual(test, result[0].content) |
| 429 | self.assertEqual('>', result[1].content) |
| 430 | |
| 431 | test = 'http://www.example.com/funky"(foo]and>bar}.asp' |
| 432 | result = self.DoLinkify('My fav site {%s' % test) |
| 433 | self.assertEqual(test, result[0].href) |
| 434 | self.assertEqual(test, result[0].content) |
| 435 | |
| 436 | test = 'http://www.example.com/funky"(foo]and>bar}.asp' |
| 437 | result = self.DoLinkify('My fav site %s}' % test) |
| 438 | self.assertEqual(test, result[0].href) |
| 439 | self.assertEqual(test, result[0].content) |
| 440 | self.assertEqual('}', result[1].content) |
| 441 | |
| 442 | # Link as part of an HTML example. |
| 443 | test = 'http://www.example.com/' |
| 444 | result = self.DoLinkify('<a href="%s">' % test) |
| 445 | self.assertEqual(test, result[0].href) |
| 446 | self.assertEqual(test, result[0].content) |
| 447 | self.assertEqual('">', result[1].content) |
| 448 | |
| 449 | # Link nested in an HTML tag. |
| 450 | result = self.DoLinkify('<span>%s</span>' % test) |
| 451 | self.assertEqual(test, result[0].href) |
| 452 | self.assertEqual(test, result[0].content) |
| 453 | |
| 454 | # Link followed by HTML tag - same bug as above. |
| 455 | result = self.DoLinkify('%s<span>foo</span>' % test) |
| 456 | self.assertEqual(test, result[0].href) |
| 457 | self.assertEqual(test, result[0].content) |
| 458 | |
| 459 | # Link followed by unescaped HTML tag. |
| 460 | result = self.DoLinkify('%s<span>foo</span>' % test) |
| 461 | self.assertEqual(test, result[0].href) |
| 462 | self.assertEqual(test, result[0].content) |
| 463 | |
| 464 | # Link surrounded by multiple delimiters. |
| 465 | result = self.DoLinkify('(e.g. <%s>)' % test) |
| 466 | self.assertEqual(test, result[0].href) |
| 467 | self.assertEqual(test, result[0].content) |
| 468 | result = self.DoLinkify('(e.g. <%s>),' % test) |
| 469 | self.assertEqual(test, result[0].href) |
| 470 | self.assertEqual(test, result[0].content) |
| 471 | |
| 472 | def testLinkify_ContextOnBadLink(self): |
| 473 | """Test that surrounding text retained in cases where we don't link url.""" |
| 474 | test = 'http://bad=example' |
| 475 | result = self.DoLinkify('<a href="%s">' % test) |
| 476 | self.assertEqual(None, result[0].href) |
| 477 | self.assertEqual(test + '">', result[0].content) |
| 478 | self.assertEqual(1, len(result)) |
| 479 | |
| 480 | def testLinkify_UnicodeContext(self): |
| 481 | """Test that unicode context does not mess up the link.""" |
| 482 | test = 'http://www.example.com' |
| 483 | |
| 484 | # This string has a non-breaking space \xa0. |
| 485 | result = self.DoLinkify(u'The correct RFC link is\xa0%s' % test) |
| 486 | self.assertEqual(test, result[0].content) |
| 487 | self.assertEqual(test, result[0].href) |
| 488 | |
| 489 | def testLinkify_UnicodeLink(self): |
| 490 | """Test that unicode in a link is OK.""" |
| 491 | test = u'http://www.example.com?q=division\xc3\xb7sign' |
| 492 | |
| 493 | # This string has a non-breaking space \xa0. |
| 494 | result = self.DoLinkify(u'The unicode link is %s' % test) |
| 495 | self.assertEqual(test, result[0].content) |
| 496 | self.assertEqual(test, result[0].href) |
| 497 | |
| 498 | def testLinkify_LinkTextEscapingDisabled(self): |
| 499 | """Test that url-like things that miss validation aren't linked.""" |
| 500 | # Link matched by the regex but not accepted by the validator. |
| 501 | test = 'http://bad_domain/reportdetail?reportid=35aa03e04772358b' |
| 502 | result = self.DoLinkify('<span>%s</span>' % test) |
| 503 | self.assertEqual(None, result[0].href) |
| 504 | self.assertEqual(test, result[0].content) |
| 505 | |
| 506 | |
| 507 | def _Issue(project_name, local_id, summary, status): |
| 508 | issue = tracker_pb2.Issue() |
| 509 | issue.project_name = project_name |
| 510 | issue.local_id = local_id |
| 511 | issue.summary = summary |
| 512 | issue.status = status |
| 513 | return issue |
| 514 | |
| 515 | |
| 516 | class TrackerAutolinkTest(unittest.TestCase): |
| 517 | |
| 518 | COMMENT_TEXT = ( |
| 519 | 'This relates to issue 1, issue #2, and issue3 \n' |
| 520 | 'as well as bug 4, bug #5, and bug6 \n' |
| 521 | 'with issue other-project:12 and issue other-project#13. \n' |
| 522 | 'Watch out for issues 21, 22, and 23 with oxford comma. \n' |
| 523 | 'And also bugs 31, 32 and 33 with no oxford comma.\n' |
| 524 | 'Here comes crbug.com/123 and crbug.com/monorail/456.\n' |
| 525 | 'We do not match when an issue\n' |
| 526 | '999. Is split across lines.' |
| 527 | ) |
| 528 | |
| 529 | def testExtractProjectAndIssueIdNormal(self): |
| 530 | mr = testing_helpers.MakeMonorailRequest( |
| 531 | path='/p/proj/issues/detail?id=1') |
| 532 | ref_batches = [] |
| 533 | for match in autolink._ISSUE_REF_RE.finditer(self.COMMENT_TEXT): |
| 534 | new_refs = autolink.ExtractProjectAndIssueIdsNormal(mr, match) |
| 535 | ref_batches.append(new_refs) |
| 536 | |
| 537 | self.assertEqual( |
| 538 | ref_batches, [ |
| 539 | [(None, 1)], |
| 540 | [(None, 2)], |
| 541 | [(None, 3)], |
| 542 | [(None, 4)], |
| 543 | [(None, 5)], |
| 544 | [(None, 6)], |
| 545 | [('other-project', 12)], |
| 546 | [('other-project', 13)], |
| 547 | [(None, 21), (None, 22), (None, 23)], |
| 548 | [(None, 31), (None, 32), (None, 33)], |
| 549 | ]) |
| 550 | |
| 551 | |
| 552 | def testExtractProjectAndIssueIdCrbug(self): |
| 553 | mr = testing_helpers.MakeMonorailRequest( |
| 554 | path='/p/proj/issues/detail?id=1') |
| 555 | ref_batches = [] |
| 556 | for match in autolink._CRBUG_REF_RE.finditer(self.COMMENT_TEXT): |
| 557 | new_refs = autolink.ExtractProjectAndIssueIdsCrBug(mr, match) |
| 558 | ref_batches.append(new_refs) |
| 559 | |
| 560 | self.assertEqual(ref_batches, [ |
| 561 | [('chromium', 123)], |
| 562 | [('monorail', 456)], |
| 563 | ]) |
| 564 | |
| 565 | def DoReplaceIssueRef( |
| 566 | self, content, regex=autolink._ISSUE_REF_RE, |
| 567 | single_issue_regex=autolink._SINGLE_ISSUE_REF_RE, |
| 568 | default_project_name=None): |
| 569 | """Calls the ReplaceIssueRef method and returns the result. |
| 570 | |
| 571 | Args: |
| 572 | content: string that may have a textual reference to an issue. |
| 573 | regex: optional regex to use instead of _ISSUE_REF_RE. |
| 574 | |
| 575 | Returns: |
| 576 | A list of TextRuns with some runs will have the reference hyperlinked. |
| 577 | Or, None if no reference detected. |
| 578 | """ |
| 579 | match = regex.search(content) |
| 580 | if not match: |
| 581 | return None |
| 582 | |
| 583 | open_dict = {'proj:1': _Issue('proj', 1, 'summary-PROJ-1', 'New'), |
| 584 | # Assume there is no issue 3 in PROJ |
| 585 | 'proj:4': _Issue('proj', 4, 'summary-PROJ-4', 'New'), |
| 586 | 'proj:6': _Issue('proj', 6, 'summary-PROJ-6', 'New'), |
| 587 | 'other-project:12': _Issue('other-project', 12, |
| 588 | 'summary-OP-12', 'Accepted'), |
| 589 | } |
| 590 | closed_dict = {'proj:2': _Issue('proj', 2, 'summary-PROJ-2', 'Fixed'), |
| 591 | 'proj:5': _Issue('proj', 5, 'summary-PROJ-5', 'Fixed'), |
| 592 | 'other-project:13': _Issue('other-project', 13, |
| 593 | 'summary-OP-12', 'Invalid'), |
| 594 | 'chromium:13': _Issue('chromium', 13, |
| 595 | 'summary-Cr-13', 'Invalid'), |
| 596 | } |
| 597 | comp_ref_artifacts = (open_dict, closed_dict,) |
| 598 | |
| 599 | replacement_runs = autolink._ReplaceIssueRef( |
| 600 | match, comp_ref_artifacts, single_issue_regex, default_project_name) |
| 601 | return replacement_runs |
| 602 | |
| 603 | def testReplaceIssueRef_NoMatch(self): |
| 604 | result = self.DoReplaceIssueRef('What is this all about?') |
| 605 | self.assertIsNone(result) |
| 606 | |
| 607 | def testReplaceIssueRef_Normal(self): |
| 608 | result = self.DoReplaceIssueRef( |
| 609 | 'This relates to issue 1', default_project_name='proj') |
| 610 | self.assertEqual('/p/proj/issues/detail?id=1', result[0].href) |
| 611 | self.assertEqual('issue 1', result[0].content) |
| 612 | self.assertEqual(None, result[0].css_class) |
| 613 | self.assertEqual('summary-PROJ-1', result[0].title) |
| 614 | self.assertEqual('a', result[0].tag) |
| 615 | |
| 616 | result = self.DoReplaceIssueRef( |
| 617 | ', issue #2', default_project_name='proj') |
| 618 | self.assertEqual('/p/proj/issues/detail?id=2', result[0].href) |
| 619 | self.assertEqual(' issue #2 ', result[0].content) |
| 620 | self.assertEqual('closed_ref', result[0].css_class) |
| 621 | self.assertEqual('summary-PROJ-2', result[0].title) |
| 622 | self.assertEqual('a', result[0].tag) |
| 623 | |
| 624 | result = self.DoReplaceIssueRef( |
| 625 | ', and issue3 ', default_project_name='proj') |
| 626 | self.assertEqual(None, result[0].href) # There is no issue 3 |
| 627 | self.assertEqual('issue3', result[0].content) |
| 628 | |
| 629 | result = self.DoReplaceIssueRef( |
| 630 | 'as well as bug 4', default_project_name='proj') |
| 631 | self.assertEqual('/p/proj/issues/detail?id=4', result[0].href) |
| 632 | self.assertEqual('bug 4', result[0].content) |
| 633 | |
| 634 | result = self.DoReplaceIssueRef( |
| 635 | ', bug #5, ', default_project_name='proj') |
| 636 | self.assertEqual('/p/proj/issues/detail?id=5', result[0].href) |
| 637 | self.assertEqual(' bug #5 ', result[0].content) |
| 638 | |
| 639 | result = self.DoReplaceIssueRef( |
| 640 | 'and bug6', default_project_name='proj') |
| 641 | self.assertEqual('/p/proj/issues/detail?id=6', result[0].href) |
| 642 | self.assertEqual('bug6', result[0].content) |
| 643 | |
| 644 | result = self.DoReplaceIssueRef( |
| 645 | 'with issue other-project:12', default_project_name='proj') |
| 646 | self.assertEqual('/p/other-project/issues/detail?id=12', result[0].href) |
| 647 | self.assertEqual('issue other-project:12', result[0].content) |
| 648 | |
| 649 | result = self.DoReplaceIssueRef( |
| 650 | 'and issue other-project#13', default_project_name='proj') |
| 651 | self.assertEqual('/p/other-project/issues/detail?id=13', result[0].href) |
| 652 | self.assertEqual(' issue other-project#13 ', result[0].content) |
| 653 | |
| 654 | def testReplaceIssueRef_CrBug(self): |
| 655 | result = self.DoReplaceIssueRef( |
| 656 | 'and crbug.com/other-project/13', regex=autolink._CRBUG_REF_RE, |
| 657 | single_issue_regex=autolink._CRBUG_REF_RE, |
| 658 | default_project_name='chromium') |
| 659 | self.assertEqual('/p/other-project/issues/detail?id=13', result[0].href) |
| 660 | self.assertEqual(' crbug.com/other-project/13 ', result[0].content) |
| 661 | |
| 662 | result = self.DoReplaceIssueRef( |
| 663 | 'and http://crbug.com/13', regex=autolink._CRBUG_REF_RE, |
| 664 | single_issue_regex=autolink._CRBUG_REF_RE, |
| 665 | default_project_name='chromium') |
| 666 | self.assertEqual('/p/chromium/issues/detail?id=13', result[0].href) |
| 667 | self.assertEqual(' http://crbug.com/13 ', result[0].content) |
| 668 | |
| 669 | result = self.DoReplaceIssueRef( |
| 670 | 'and http://crbug.com/13#c17', regex=autolink._CRBUG_REF_RE, |
| 671 | single_issue_regex=autolink._CRBUG_REF_RE, |
| 672 | default_project_name='chromium') |
| 673 | self.assertEqual('/p/chromium/issues/detail?id=13#c17', result[0].href) |
| 674 | self.assertEqual(' http://crbug.com/13#c17 ', result[0].content) |
| 675 | |
| 676 | def testParseProjectNameMatch(self): |
| 677 | golden = 'project-name' |
| 678 | variations = ['%s', ' %s', '%s ', '%s:', '%s#', '%s#:', '%s:#', '%s :#', |
| 679 | '\t%s', '%s\t', '\t%s\t', '\t\t%s\t\t', '\n%s', '%s\n', |
| 680 | '\n%s\n', '\n\n%s\n\n', '\t\n%s', '\n\t%s', '%s\t\n', |
| 681 | '%s\n\t', '\t\n%s#', '\n\t%s#', '%s\t\n#', '%s\n\t#', |
| 682 | '\t\n%s:', '\n\t%s:', '%s\t\n:', '%s\n\t:' |
| 683 | ] |
| 684 | |
| 685 | # First pass checks all valid project name results |
| 686 | for pattern in variations: |
| 687 | self.assertEqual( |
| 688 | golden, autolink._ParseProjectNameMatch(pattern % golden)) |
| 689 | |
| 690 | # Second pass tests all inputs that should result in None |
| 691 | for pattern in variations: |
| 692 | self.assertTrue( |
| 693 | autolink._ParseProjectNameMatch(pattern % '') in [None, '']) |
| 694 | |
| 695 | |
| 696 | class VCAutolinkTest(unittest.TestCase): |
| 697 | |
| 698 | GIT_HASH_1 = '1' * 40 |
| 699 | GIT_HASH_2 = '2' * 40 |
| 700 | GIT_HASH_3 = 'a1' * 20 |
| 701 | GIT_COMMENT_TEXT = ( |
| 702 | 'This is a fix for r%s and R%s, by r2d2, who also authored revision %s, ' |
| 703 | 'revision #%s, revision %s, and revision %s' % ( |
| 704 | GIT_HASH_1, GIT_HASH_2, GIT_HASH_3, |
| 705 | GIT_HASH_1.upper(), GIT_HASH_2.upper(), GIT_HASH_3.upper())) |
| 706 | SVN_COMMENT_TEXT = ( |
| 707 | 'This is a fix for r12 and R3400, by r2d2, who also authored ' |
| 708 | 'revision r4, ' |
| 709 | 'revision #1234567, revision 789, and revision 9025. If you have ' |
| 710 | 'questions, call me at 18005551212') |
| 711 | |
| 712 | def testGetReferencedRevisions(self): |
| 713 | refs = ['1', '2', '3'] |
| 714 | # For now, we do not look up revision objects, result is always None |
| 715 | self.assertIsNone(autolink.GetReferencedRevisions(None, refs)) |
| 716 | |
| 717 | def testExtractGitHashes(self): |
| 718 | refs = [] |
| 719 | for match in autolink._GIT_HASH_RE.finditer(self.GIT_COMMENT_TEXT): |
| 720 | new_refs = autolink.ExtractRevNums(None, match) |
| 721 | refs.extend(new_refs) |
| 722 | |
| 723 | self.assertEqual( |
| 724 | refs, [ |
| 725 | self.GIT_HASH_1, self.GIT_HASH_2, self.GIT_HASH_3, |
| 726 | self.GIT_HASH_1.upper(), |
| 727 | self.GIT_HASH_2.upper(), |
| 728 | self.GIT_HASH_3.upper() |
| 729 | ]) |
| 730 | |
| 731 | def testExtractRevNums(self): |
| 732 | refs = [] |
| 733 | for match in autolink._SVN_REF_RE.finditer(self.SVN_COMMENT_TEXT): |
| 734 | new_refs = autolink.ExtractRevNums(None, match) |
| 735 | refs.extend(new_refs) |
| 736 | |
| 737 | # Note that we only autolink rNNNN with at least 4 digits. |
| 738 | self.assertEqual(refs, ['3400', '1234567', '9025']) |
| 739 | |
| 740 | |
| 741 | def DoReplaceRevisionRef(self, content, project=None): |
| 742 | """Calls the ReplaceRevisionRef method and returns the result. |
| 743 | |
| 744 | Args: |
| 745 | content: string with a hyperlink. |
| 746 | project: optional project. |
| 747 | |
| 748 | Returns: |
| 749 | A list of TextRuns with some runs will have the embedded URL hyperlinked. |
| 750 | Or, None if no link was detected. |
| 751 | """ |
| 752 | match = autolink._GIT_HASH_RE.search(content) |
| 753 | if not match: |
| 754 | return None |
| 755 | |
| 756 | mr = testing_helpers.MakeMonorailRequest( |
| 757 | path='/p/proj/source/detail?r=1', project=project) |
| 758 | replacement_runs = autolink.ReplaceRevisionRef(mr, match, None) |
| 759 | return replacement_runs |
| 760 | |
| 761 | def testReplaceRevisionRef(self): |
| 762 | result = self.DoReplaceRevisionRef( |
| 763 | 'This is a fix for r%s' % self.GIT_HASH_1) |
| 764 | self.assertEqual('https://crrev.com/%s' % self.GIT_HASH_1, result[0].href) |
| 765 | self.assertEqual('r%s' % self.GIT_HASH_1, result[0].content) |
| 766 | |
| 767 | result = self.DoReplaceRevisionRef( |
| 768 | 'and R%s, by r2d2, who ' % self.GIT_HASH_2) |
| 769 | self.assertEqual('https://crrev.com/%s' % self.GIT_HASH_2, result[0].href) |
| 770 | self.assertEqual('R%s' % self.GIT_HASH_2, result[0].content) |
| 771 | |
| 772 | result = self.DoReplaceRevisionRef('by r2d2, who ') |
| 773 | self.assertEqual(None, result) |
| 774 | |
| 775 | result = self.DoReplaceRevisionRef( |
| 776 | 'also authored revision %s, ' % self.GIT_HASH_3) |
| 777 | self.assertEqual('https://crrev.com/%s' % self.GIT_HASH_3, result[0].href) |
| 778 | self.assertEqual('revision %s' % self.GIT_HASH_3, result[0].content) |
| 779 | |
| 780 | result = self.DoReplaceRevisionRef( |
| 781 | 'revision #%s, ' % self.GIT_HASH_1.upper()) |
| 782 | self.assertEqual( |
| 783 | 'https://crrev.com/%s' % self.GIT_HASH_1.upper(), result[0].href) |
| 784 | self.assertEqual( |
| 785 | 'revision #%s' % self.GIT_HASH_1.upper(), result[0].content) |
| 786 | |
| 787 | result = self.DoReplaceRevisionRef( |
| 788 | 'revision %s, ' % self.GIT_HASH_2.upper()) |
| 789 | self.assertEqual( |
| 790 | 'https://crrev.com/%s' % self.GIT_HASH_2.upper(), result[0].href) |
| 791 | self.assertEqual('revision %s' % self.GIT_HASH_2.upper(), result[0].content) |
| 792 | |
| 793 | result = self.DoReplaceRevisionRef( |
| 794 | 'and revision %s' % self.GIT_HASH_3.upper()) |
| 795 | self.assertEqual( |
| 796 | 'https://crrev.com/%s' % self.GIT_HASH_3.upper(), result[0].href) |
| 797 | self.assertEqual('revision %s' % self.GIT_HASH_3.upper(), result[0].content) |
| 798 | |
| 799 | def testReplaceRevisionRef_CustomURL(self): |
| 800 | """A project can override the URL used for revision links.""" |
| 801 | project = fake.Project() |
| 802 | project.revision_url_format = 'http://example.com/+/{revnum}' |
| 803 | result = self.DoReplaceRevisionRef( |
| 804 | 'This is a fix for r%s' % self.GIT_HASH_1, project=project) |
| 805 | self.assertEqual( |
| 806 | 'http://example.com/+/%s' % self.GIT_HASH_1, result[0].href) |
| 807 | self.assertEqual('r%s' % self.GIT_HASH_1, result[0].content) |