| 1 | # -*- coding: utf-8 -*- |
|---|
| 2 | # |
|---|
| 3 | # Copyright (C) 2011 Odd Simon Simonsen <oddsimons@gmail.com> |
|---|
| 4 | # Copyright (C) 2012-2014 Steffen Hoffmann <hoff.st@web.de> |
|---|
| 5 | # |
|---|
| 6 | # This software is licensed as described in the file COPYING, which |
|---|
| 7 | # you should have received as part of this distribution. |
|---|
| 8 | # |
|---|
| 9 | |
|---|
| 10 | import shutil |
|---|
| 11 | import tempfile |
|---|
| 12 | import unittest |
|---|
| 13 | |
|---|
| 14 | from trac.perm import PermissionError, PermissionSystem |
|---|
| 15 | from trac.resource import Resource |
|---|
| 16 | from trac.test import EnvironmentStub, MockRequest |
|---|
| 17 | from trac.wiki.test import wikisyntax_test_suite |
|---|
| 18 | |
|---|
| 19 | from tractags.api import TagSystem |
|---|
| 20 | from tractags.db import TagSetup |
|---|
| 21 | from tractags.wiki import WikiTagProvider |
|---|
| 22 | |
|---|
| 23 | |
|---|
| 24 | def _revert_tractags_schema_init(env): |
|---|
| 25 | with env.db_transaction as db: |
|---|
| 26 | db("DROP TABLE IF EXISTS tags") |
|---|
| 27 | db("DROP TABLE IF EXISTS tags_change") |
|---|
| 28 | db("DELETE FROM system WHERE name='tags_version'") |
|---|
| 29 | db("DELETE FROM permission WHERE action %s" % db.like(), |
|---|
| 30 | ('TAGS_%',)) |
|---|
| 31 | |
|---|
| 32 | |
|---|
| 33 | def _insert_tags(env, tagspace, name, tags): |
|---|
| 34 | args = [(tagspace, name, tag) for tag in tags] |
|---|
| 35 | with env.db_transaction as db: |
|---|
| 36 | db.executemany(""" |
|---|
| 37 | INSERT INTO tags (tagspace,name,tag) VALUES (%s,%s,%s) |
|---|
| 38 | """, args) |
|---|
| 39 | |
|---|
| 40 | |
|---|
| 41 | TEST_NOPERM = u""" |
|---|
| 42 | ============================== link rendering without view permission |
|---|
| 43 | tag:onetag |
|---|
| 44 | ------------------------------ |
|---|
| 45 | <p> |
|---|
| 46 | <a href="/tags/onetag">tag:onetag</a> |
|---|
| 47 | </p> |
|---|
| 48 | ------------------------------ |
|---|
| 49 | """ |
|---|
| 50 | |
|---|
| 51 | TEST_CASES = u""" |
|---|
| 52 | ============================== tag: link resolver for single tag |
|---|
| 53 | tag:onetag |
|---|
| 54 | tag:2ndtag n' more |
|---|
| 55 | tag:a.really?_\wild-thing! |
|---|
| 56 | # regression test for ticket !#9057 |
|---|
| 57 | tag:single'quote |
|---|
| 58 | ------------------------------ |
|---|
| 59 | <p> |
|---|
| 60 | <a href="/tags/onetag">tag:onetag</a> |
|---|
| 61 | <a href="/tags/2ndtag">tag:2ndtag</a> n' more |
|---|
| 62 | <a href="/tags/a.really%3F_%5Cwild-thing">tag:a.really?_\wild-thing</a>! |
|---|
| 63 | # regression test for ticket #9057 |
|---|
| 64 | <a href="/tags/single\'quote">tag:single\'quote</a> |
|---|
| 65 | </p> |
|---|
| 66 | ------------------------------ |
|---|
| 67 | ============================== tagged: alternative link markup |
|---|
| 68 | tagged:onetag |
|---|
| 69 | tagged:tagged: |
|---|
| 70 | ------------------------------ |
|---|
| 71 | <p> |
|---|
| 72 | <a href="/tags/onetag">tagged:onetag</a> |
|---|
| 73 | <a href="/tags/tagged">tagged:tagged</a>: |
|---|
| 74 | </p> |
|---|
| 75 | ------------------------------ |
|---|
| 76 | ============================== tag extraction from tag links |
|---|
| 77 | # Trailing non-letter character must be ignored. |
|---|
| 78 | tag:onetag, |
|---|
| 79 | tag:onetag. |
|---|
| 80 | tag:onetag; |
|---|
| 81 | tag:onetag: |
|---|
| 82 | tag:onetag! |
|---|
| 83 | tag:onetag? |
|---|
| 84 | # Multiple trailing non-letter characters should be removed too. |
|---|
| 85 | tag:onetag.. |
|---|
| 86 | tag:onetag... |
|---|
| 87 | ------------------------------ |
|---|
| 88 | <p> |
|---|
| 89 | # Trailing non-letter character must be ignored. |
|---|
| 90 | <a href="/tags/onetag">tag:onetag</a>, |
|---|
| 91 | <a href="/tags/onetag">tag:onetag</a>. |
|---|
| 92 | <a href="/tags/onetag">tag:onetag</a>; |
|---|
| 93 | <a href="/tags/onetag">tag:onetag</a>: |
|---|
| 94 | <a href="/tags/onetag">tag:onetag</a>! |
|---|
| 95 | <a href="/tags/onetag">tag:onetag</a>? |
|---|
| 96 | # Multiple trailing non-letter characters should be removed too. |
|---|
| 97 | <a href="/tags/onetag">tag:onetag</a>.. |
|---|
| 98 | <a href="/tags/onetag">tag:onetag</a>... |
|---|
| 99 | </p> |
|---|
| 100 | ------------------------------ |
|---|
| 101 | ============================== bracketed TracWiki tag links |
|---|
| 102 | [tagged:onetag] |
|---|
| 103 | [tag:onetag label] |
|---|
| 104 | [tag:onetag multi-word tag: label] |
|---|
| 105 | [tag:onetag surrounding whitespace stripped ] |
|---|
| 106 | [tag:' onetag ' " 'surrounding whitespace stripped '"] |
|---|
| 107 | # Trailing non-letter character |
|---|
| 108 | moved to [tag:'onetag'. label] too, if quoted |
|---|
| 109 | ![tag:disabled link] |
|---|
| 110 | ------------------------------ |
|---|
| 111 | <p> |
|---|
| 112 | <a href="/tags/onetag">onetag</a> |
|---|
| 113 | <a href="/tags/onetag">label</a> |
|---|
| 114 | <a href="/tags/onetag">multi-word tag: label</a> |
|---|
| 115 | <a href="/tags/onetag">surrounding whitespace stripped</a> |
|---|
| 116 | <a href="/tags/onetag">surrounding whitespace stripped</a> |
|---|
| 117 | # Trailing non-letter character |
|---|
| 118 | moved to <a href="/tags/onetag">. label</a> too, if quoted |
|---|
| 119 | [tag:disabled link] |
|---|
| 120 | </p> |
|---|
| 121 | ------------------------------ |
|---|
| 122 | ============================== link to non-existent tag |
|---|
| 123 | tag:missing |
|---|
| 124 | [tag:missing] |
|---|
| 125 | [tag:missing wanted tag] |
|---|
| 126 | ------------------------------ |
|---|
| 127 | <p> |
|---|
| 128 | <a class="missing tags" href="/tags/missing" rel="nofollow">tag:missing?</a> |
|---|
| 129 | <a class="missing tags" href="/tags/missing" rel="nofollow">missing?</a> |
|---|
| 130 | <a class="missing tags" href="/tags/missing" rel="nofollow">wanted tag?</a> |
|---|
| 131 | </p> |
|---|
| 132 | ------------------------------ |
|---|
| 133 | ============================== quoting in tag link resolver |
|---|
| 134 | tagged:'onetag' |
|---|
| 135 | tag:'"heavily-quoted"' |
|---|
| 136 | ------------------------------ |
|---|
| 137 | <p> |
|---|
| 138 | <a href="/tags/onetag">tagged:'onetag'</a> |
|---|
| 139 | <a href="/tags/heavily-quoted">tag:'"heavily-quoted"'</a> |
|---|
| 140 | </p> |
|---|
| 141 | ------------------------------ |
|---|
| 142 | ============================== query expression in tag: link resolver |
|---|
| 143 | tag:'onetag 2ndtag' |
|---|
| 144 | tag:"onetag 2ndtag" |
|---|
| 145 | ------------------------------ |
|---|
| 146 | <p> |
|---|
| 147 | <a href="/tags/onetag%202ndtag">tag:\'onetag 2ndtag\'</a> |
|---|
| 148 | <a href="/tags/onetag%202ndtag">tag:\"onetag 2ndtag\"</a> |
|---|
| 149 | </p> |
|---|
| 150 | ------------------------------ |
|---|
| 151 | ============================== query with realm in tag: link resolver |
|---|
| 152 | tag:'onetag realm:wiki 2ndtag' |
|---|
| 153 | [tagged:'realm:wiki onetag' label] |
|---|
| 154 | ------------------------------ |
|---|
| 155 | <p> |
|---|
| 156 | <a href="/tags?wiki=on&q=onetag+2ndtag">tag:\'onetag realm:wiki 2ndtag\'</a> |
|---|
| 157 | <a href="/tags?wiki=on&q=onetag">label</a> |
|---|
| 158 | </p> |
|---|
| 159 | ------------------------------ |
|---|
| 160 | ============================== tag links in complex wiki markup |
|---|
| 161 | Linking to a list of resources [tagged with onetag] requires valid syntax |
|---|
| 162 | to get [tagged:onetag rendered]. |
|---|
| 163 | Some [tag:"onetag or single'quote "wir]edly"' |
|---|
| 164 | [labeled tag] wiki link is still allowed. |
|---|
| 165 | ------------------------------ |
|---|
| 166 | <p> |
|---|
| 167 | Linking to a list of resources [tagged with onetag] requires valid syntax |
|---|
| 168 | to get <a href="/tags/onetag">rendered</a>. |
|---|
| 169 | Some <a href="/tags/onetag%20or%20single'quote">wir</a>edly"' |
|---|
| 170 | [labeled tag] wiki link is still allowed. |
|---|
| 171 | </p> |
|---|
| 172 | ------------------------------ |
|---|
| 173 | """ |
|---|
| 174 | |
|---|
| 175 | |
|---|
| 176 | class WikiTagProviderTestCase(unittest.TestCase): |
|---|
| 177 | |
|---|
| 178 | def setUp(self): |
|---|
| 179 | self.env = EnvironmentStub(default_data=True, |
|---|
| 180 | path=tempfile.mkdtemp(), |
|---|
| 181 | enable=['trac.*', 'tractags.*']) |
|---|
| 182 | setup = TagSetup(self.env) |
|---|
| 183 | # Current tractags schema is partially setup with enabled component. |
|---|
| 184 | # Revert these changes for getting a clean setup. |
|---|
| 185 | self._revert_tractags_schema_init() |
|---|
| 186 | setup.upgrade_environment() |
|---|
| 187 | |
|---|
| 188 | self.perms = PermissionSystem(self.env) |
|---|
| 189 | self.tag_s = TagSystem(self.env) |
|---|
| 190 | self.tag_wp = WikiTagProvider(self.env) |
|---|
| 191 | |
|---|
| 192 | # Populate table with initial test data. |
|---|
| 193 | self.env.db_transaction(""" |
|---|
| 194 | INSERT INTO tags (tagspace, name, tag) |
|---|
| 195 | VALUES ('wiki', 'WikiStart', 'tag1') |
|---|
| 196 | """) |
|---|
| 197 | |
|---|
| 198 | self.realm = 'wiki' |
|---|
| 199 | self.tags = ['tag1'] |
|---|
| 200 | |
|---|
| 201 | def tearDown(self): |
|---|
| 202 | self.env.shutdown() |
|---|
| 203 | shutil.rmtree(self.env.path) |
|---|
| 204 | |
|---|
| 205 | # Helpers |
|---|
| 206 | |
|---|
| 207 | def _revert_tractags_schema_init(self): |
|---|
| 208 | _revert_tractags_schema_init(self.env) |
|---|
| 209 | |
|---|
| 210 | # Tests |
|---|
| 211 | |
|---|
| 212 | def test_get_tags(self): |
|---|
| 213 | resource = Resource('wiki', 'WikiStart') |
|---|
| 214 | req = MockRequest(self.env, authname='editor') |
|---|
| 215 | self.assertEquals([tag for tag in |
|---|
| 216 | self.tag_wp.get_resource_tags(req, resource)], |
|---|
| 217 | self.tags) |
|---|
| 218 | |
|---|
| 219 | def test_exclude_template_tags(self): |
|---|
| 220 | # Populate table with more test data. |
|---|
| 221 | req = MockRequest(self.env, authname='editor') |
|---|
| 222 | self.env.db_transaction(""" |
|---|
| 223 | INSERT INTO tags (tagspace, name, tag) |
|---|
| 224 | VALUES ('wiki', 'PageTemplates/Template', 'tag2') |
|---|
| 225 | """) |
|---|
| 226 | tags = ['tag1', 'tag2'] |
|---|
| 227 | self.assertEquals(list(self.tag_s.get_all_tags(req).keys()), self.tags) |
|---|
| 228 | self.env.config.set('tags', 'query_exclude_wiki_templates', False) |
|---|
| 229 | self.assertEquals(list(self.tag_s.get_all_tags(req).keys()), tags) |
|---|
| 230 | |
|---|
| 231 | def test_set_tags_no_perms(self): |
|---|
| 232 | resource = Resource('wiki', 'TaggedPage') |
|---|
| 233 | req = MockRequest(self.env, authname='anonymous') |
|---|
| 234 | self.assertRaises(PermissionError, self.tag_wp.set_resource_tags, |
|---|
| 235 | req, resource, self.tags) |
|---|
| 236 | |
|---|
| 237 | def test_set_tags(self): |
|---|
| 238 | resource = Resource('wiki', 'TaggedPage') |
|---|
| 239 | req = MockRequest(self.env, authname='editor') |
|---|
| 240 | # Shouldn't raise an error with appropriate permission. |
|---|
| 241 | self.tag_wp.set_resource_tags(req, resource, self.tags) |
|---|
| 242 | self.tag_wp.set_resource_tags(req, resource, ['tag2']) |
|---|
| 243 | # Check change records. |
|---|
| 244 | rows = self.env.db_query(""" |
|---|
| 245 | SELECT author,oldtags,newtags FROM tags_change |
|---|
| 246 | WHERE tagspace=%s AND name=%s |
|---|
| 247 | ORDER by time DESC |
|---|
| 248 | """, ('wiki', 'TaggedPage')) |
|---|
| 249 | self.assertEqual(rows[0], ('editor', 'tag1', 'tag2')) |
|---|
| 250 | self.assertEqual(rows[1], ('editor', '', 'tag1')) |
|---|
| 251 | |
|---|
| 252 | |
|---|
| 253 | def wiki_setup(tc): |
|---|
| 254 | tc.env.enable_component('tractags') |
|---|
| 255 | tc.env.path = tempfile.mkdtemp() |
|---|
| 256 | _revert_tractags_schema_init(tc.env) |
|---|
| 257 | TagSetup(tc.env).upgrade_environment() |
|---|
| 258 | |
|---|
| 259 | tags = ('2ndtag', 'a.really?_\wild-thing', 'heavily-quoted', |
|---|
| 260 | 'onetag', 'tagged', "single'quote") |
|---|
| 261 | _insert_tags(tc.env, 'wiki', 'TestPage', tags) |
|---|
| 262 | |
|---|
| 263 | # Enable big diff output. |
|---|
| 264 | tc.maxDiff = None |
|---|
| 265 | |
|---|
| 266 | |
|---|
| 267 | def wiki_setup_no_perm(tc): |
|---|
| 268 | wiki_setup(tc) |
|---|
| 269 | with tc.env.db_transaction as db: |
|---|
| 270 | tc.env.db_transaction("DELETE FROM permission WHERE action %s" |
|---|
| 271 | % db.like(), ('TAGS_%',)) |
|---|
| 272 | |
|---|
| 273 | |
|---|
| 274 | def wiki_teardown(tc): |
|---|
| 275 | tc.env.reset_db() |
|---|
| 276 | tc.env.shutdown() |
|---|
| 277 | shutil.rmtree(tc.env.path) |
|---|
| 278 | |
|---|
| 279 | |
|---|
| 280 | def test_suite(): |
|---|
| 281 | suite = unittest.TestSuite() |
|---|
| 282 | suite.addTest(unittest.makeSuite(WikiTagProviderTestCase)) |
|---|
| 283 | suite.addTest(wikisyntax_test_suite(TEST_CASES, wiki_setup, |
|---|
| 284 | __file__, wiki_teardown)) |
|---|
| 285 | suite.addTest(wikisyntax_test_suite(TEST_NOPERM, wiki_setup_no_perm, |
|---|
| 286 | __file__, wiki_teardown)) |
|---|
| 287 | return suite |
|---|
| 288 | |
|---|
| 289 | |
|---|
| 290 | if __name__ == '__main__': |
|---|
| 291 | unittest.main(defaultTest='test_suite') |
|---|