You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by he...@apache.org on 2015/05/29 22:40:34 UTC

[12/45] allura git commit: [#7878] Used 2to3 to see what issues would come up

http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/tests/functional/test_discuss.py
----------------------------------------------------------------------
diff --git a/tests/functional/test_discuss.py b/tests/functional/test_discuss.py
new file mode 100644
index 0000000..2f5425b
--- /dev/null
+++ b/tests/functional/test_discuss.py
@@ -0,0 +1,327 @@
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
+#       Licensed to the Apache Software Foundation (ASF) under one
+#       or more contributor license agreements.  See the NOTICE file
+#       distributed with this work for additional information
+#       regarding copyright ownership.  The ASF licenses this file
+#       to you under the Apache License, Version 2.0 (the
+#       "License"); you may not use this file except in compliance
+#       with the License.  You may obtain a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#       Unless required by applicable law or agreed to in writing,
+#       software distributed under the License is distributed on an
+#       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#       KIND, either express or implied.  See the License for the
+#       specific language governing permissions and limitations
+#       under the License.
+
+import os
+from mock import patch
+from nose.tools import assert_in, assert_not_in, assert_equal
+
+from ming.odm import session
+
+from allura.tests import TestController
+from allura import model as M
+
+
+class TestDiscuss(TestController):
+
+    def test_subscribe_unsubscribe(self):
+        home = self.app.get('/wiki/_discuss/')
+        subscribed = [i for i in home.html.findAll('input')
+                      if i.get('type') == 'checkbox'][0]
+        assert 'checked' not in subscribed.attrMap
+        link = [a for a in home.html.findAll('a')
+                if 'thread' in a['href']][0]
+        params = {
+            'threads-0._id': link['href'][len('/p/test/wiki/_discuss/thread/'):-1],
+            'threads-0.subscription': 'on'}
+        r = self.app.post('/wiki/_discuss/subscribe',
+                          params=params,
+                          headers={'Referer': '/wiki/_discuss/'})
+        r = r.follow()
+        subscribed = [i for i in r.html.findAll('input')
+                      if i.get('type') == 'checkbox'][0]
+        assert 'checked' in subscribed.attrMap
+        params = {
+            'threads-0._id': link['href'][len('/p/test/wiki/_discuss/thread/'):-1]
+        }
+        r = self.app.post('/wiki/_discuss/subscribe',
+                          params=params,
+                          headers={'Referer': '/wiki/_discuss/'})
+        r = r.follow()
+        subscribed = [i for i in r.html.findAll('input')
+                      if i.get('type') == 'checkbox'][0]
+        assert 'checked' not in subscribed.attrMap
+
+    def _make_post(self, text):
+        home = self.app.get('/wiki/_discuss/')
+        thread_link = [a for a in home.html.findAll('a')
+                       if 'thread' in a['href']][0]['href']
+        thread = self.app.get(thread_link)
+        for f in thread.html.findAll('form'):
+            if f.get('action', '').endswith('/post'):
+                break
+        params = dict()
+        inputs = f.findAll('input')
+        for field in inputs:
+            if 'name' in field:
+                params[field['name']] = 'value' in field and field['value'] or ''
+        params[f.find('textarea')['name']] = text
+        r = self.app.post(f['action'].encode('utf-8'), params=params,
+                          headers={'Referer': thread_link.encode("utf-8")},
+                          extra_environ=dict(username='root'))
+        r = r.follow()
+        return r
+
+    @patch('allura.controllers.discuss.g.spam_checker.submit_spam')
+    def test_post(self, submit_spam):
+        home = self.app.get('/wiki/_discuss/')
+        thread_link = [a for a in home.html.findAll('a')
+                       if 'thread' in a['href']][0]['href']
+        r = self._make_post('This is a post')
+        assert 'This is a post' in r, r
+        post_link = str(
+            r.html.find('div', {'class': 'edit_post_form reply'}).find('form')['action'])
+        r = self.app.get(post_link[:-2], status=302)
+        r = self.app.get(post_link)
+        post_form = r.html.find('form', {'action': post_link})
+        params = dict()
+        inputs = post_form.findAll('input')
+        for field in inputs:
+            if 'name' in field:
+                params[field['name']] = 'value' in field and field['value'] or ''
+        params[post_form.find('textarea')['name']] = 'This is a new post'
+        r = self.app.post(post_link,
+                          params=params,
+                          headers={'Referer': thread_link.encode("utf-8")})
+        r = r.follow()
+        assert 'This is a new post' in r, r
+        r = self.app.get(post_link)
+        assert str(r).count('This is a new post') == 3
+        post_form = r.html.find('form', {'action': post_link + 'reply'})
+        params = dict()
+        inputs = post_form.findAll('input')
+        for field in inputs:
+            if 'name' in field:
+                params[field['name']] = 'value' in field and field['value'] or ''
+        params[post_form.find('textarea')['name']] = 'Tis a reply'
+        r = self.app.post(post_link + 'reply',
+                          params=params,
+                          headers={'Referer': post_link.encode("utf-8")})
+        r = self.app.get(thread_link)
+        assert 'Tis a reply' in r, r
+        permalinks = [post.find('form')['action'].encode('utf-8')
+                      for post in r.html.findAll('div', {'class': 'edit_post_form reply'})]
+        self.app.post(permalinks[1] + 'flag')
+        self.app.post(permalinks[1] + 'moderate', params=dict(delete='delete'))
+        self.app.post(permalinks[0] + 'moderate', params=dict(spam='spam'))
+        assert submit_spam.call_args[0] == (
+            'This is a new post',), submit_spam.call_args[0]
+
+    def test_permissions(self):
+        home = self.app.get('/wiki/_discuss/')
+        thread_url = [a for a in home.html.findAll('a')
+                      if 'thread' in a['href']][0]['href']
+        thread_id = thread_url.rstrip('/').split('/')[-1]
+        thread = M.Thread.query.get(_id=thread_id)
+
+        # ok initially
+        non_admin = 'test-user'
+        self.app.get(thread_url, status=200,
+                     extra_environ=dict(username=non_admin))
+
+        # set wiki page private
+        from forgewiki.model import Page
+        # need to look up the page directly, so ming is aware of our change
+        page = Page.query.get(_id=thread.ref.artifact._id)
+        project = M.Project.query.get(shortname='test')
+        role_admin = M.ProjectRole.by_name('Admin', project)._id
+        page.acl = [
+            M.ACE.allow(role_admin, M.ALL_PERMISSIONS),
+            M.DENY_ALL,
+        ]
+
+        self.app.get(thread_url, status=200,  # ok
+                     extra_environ=dict(username='test-admin'))
+        self.app.get(thread_url, status=403,  # forbidden
+                     extra_environ=dict(username=non_admin))
+
+    def test_spam_link(self):
+        r = self._make_post('Test post')
+        assert '<span>Spam</span>' in r
+        r = self.app.get('/wiki/_discuss/',
+                         extra_environ={'username': 'test-user-1'})
+        assert '<span>Spam</span>' not in r, 'User without moderate perm must not see Spam link'
+
+    @patch('allura.controllers.discuss.g.spam_checker.submit_spam')
+    def test_moderate(self, submit_spam):
+        r = self._make_post('Test post')
+        post_link = str(
+            r.html.find('div', {'class': 'edit_post_form reply'}).find('form')['action'])
+        post = M.Post.query.find().first()
+        post.status = 'pending'
+        self.app.post(post_link + 'moderate', params=dict(spam='spam'))
+        assert submit_spam.call_args[0] == (
+            'Test post',), submit_spam.call_args[0]
+        post = M.Post.query.find().first()
+        assert post.status == 'spam'
+        self.app.post(post_link + 'moderate', params=dict(approve='approve'))
+        post = M.Post.query.find().first()
+        assert post.status == 'ok'
+        self.app.post(post_link + 'moderate', params=dict(delete='delete'))
+        assert M.Post.query.find().count() == 1
+        assert M.Post.query.find({'deleted': False}).count() == 0
+
+    @patch.object(M.Thread, 'is_spam')
+    def test_feed_does_not_include_comments_held_for_moderation(self, is_spam):
+        is_spam.return_value = True
+        r = self._make_post('Post needs moderation!')
+        post_link = str(
+            r.html.find('div', {'class': 'edit_post_form reply'}).find('form')['action'])
+        post = M.Post.query.find().first()
+        assert_equal(post.status, 'pending')
+        r = self.app.get('/wiki/feed.rss')
+        assert_not_in('Post needs moderation!', r)
+
+        self.app.post(post_link + 'moderate', params=dict(approve='approve'))
+        post = M.Post.query.find().first()
+        assert_equal(post.status, 'ok')
+        r = self.app.get('/wiki/feed.rss')
+        assert_in('Post needs moderation!', r)
+
+    def test_post_paging(self):
+        home = self.app.get('/wiki/_discuss/')
+        thread_link = [a for a in home.html.findAll('a')
+                       if 'thread' in a['href']][0]['href']
+        # just make sure it doesn't 500
+        self.app.get('%s?limit=50&page=0' % thread_link)
+
+    @patch('allura.controllers.discuss.g.director.create_activity')
+    def test_edit_post(self, create_activity):
+        r = self._make_post('This is a post')
+        assert create_activity.call_count == 1, create_activity.call_count
+        assert create_activity.call_args[0][1] == 'posted'
+        create_activity.reset_mock()
+        thread_url = r.request.url
+        reply_form = r.html.find(
+            'div', {'class': 'edit_post_form reply'}).find('form')
+        post_link = str(reply_form['action'])
+        assert 'This is a post' in str(
+            r.html.find('div', {'class': 'display_post'}))
+        assert 'Last edit:' not in str(
+            r.html.find('div', {'class': 'display_post'}))
+        params = dict()
+        inputs = reply_form.findAll('input')
+        for field in inputs:
+            if 'name' in field:
+                params[field['name']] = 'value' in field and field['value'] or ''
+        params[reply_form.find('textarea')['name']] = 'zzz'
+        self.app.post(post_link, params)
+        assert create_activity.call_count == 1, create_activity.call_count
+        assert create_activity.call_args[0][1] == 'modified'
+        r = self.app.get(thread_url)
+        assert 'zzz' in str(r.html.find('div', {'class': 'display_post'}))
+        assert 'Last edit: Test Admin less than 1 minute ago' in str(
+            r.html.find('div', {'class': 'display_post'}))
+
+    def test_deleted_post(self):
+        r = self._make_post('This is a post')
+        reply_form = r.html.find(
+            'div', {'class': 'edit_post_form reply'}).find('form')
+        post_link = str(reply_form['action']).rstrip('/')
+        _, slug = post_link.rsplit('/', 1)
+        r = self.app.get(post_link, status=200)
+        post = M.Post.query.get(slug=slug)
+        post.deleted = True
+        session(post).flush(post)
+        r = self.app.get(post_link, status=404)
+
+
+class TestAttachment(TestController):
+
+    def setUp(self):
+        super(TestAttachment, self).setUp()
+        home = self.app.get('/wiki/_discuss/')
+        self.thread_link = [a['href'].encode("utf-8")
+                            for a in home.html.findAll('a')
+                            if 'thread' in a['href']][0]
+        thread = self.app.get(self.thread_link)
+        for f in thread.html.findAll('form'):
+            if f.get('action', '').endswith('/post'):
+                break
+        self.post_form_link = f['action'].encode('utf-8')
+        params = dict()
+        inputs = f.findAll('input')
+        for field in inputs:
+            if 'name' in field:
+                params[field['name']] = 'value' in field and field['value'] or ''
+        params[f.find('textarea')['name']] = 'Test Post'
+        r = self.app.post(f['action'].encode('utf-8'), params=params,
+                          headers={'Referer': self.thread_link})
+        r = r.follow()
+        self.post_link = str(
+            r.html.find('div', {'class': 'edit_post_form reply'}).find('form')['action'])
+
+    def attach_link(self):
+        r = self.app.get(self.thread_link)
+        for alink in r.html.findAll('a'):
+            if 'attachment' in alink['href']:
+                alink = str(alink['href'])
+                return alink
+        else:
+            assert False, 'attachment link not found'
+
+    def test_attach(self):
+        r = self.app.post(self.post_link + 'attach',
+                          upload_files=[('file_info', 'test.txt', 'HiThere!')])
+        r = self.app.get(self.thread_link)
+        assert '<div class="attachment_thumb">' in r
+        alink = self.attach_link()
+        r = self.app.get(alink)
+        assert r.content_disposition == 'attachment;filename="test.txt"', 'Attachments should force download'
+        r = self.app.post(self.post_link + 'attach',
+                          upload_files=[('file_info', 'test.o12', 'HiThere!')])
+        r = self.app.post(alink, params=dict(delete='on'))
+
+    @patch('allura.model.discuss.Post.notify')
+    def test_reply_attach(self, notify):
+        notify.return_value = True
+        r = self.app.get(self.thread_link)
+        post_form = r.html.find('form', {'action': self.post_link + 'reply'})
+        params = dict()
+        inputs = post_form.findAll('input')
+
+        for field in inputs:
+            if 'name' in field and (field['name'] != 'file_info'):
+                params[field['name']] = 'value' in field and field['value'] or ''
+        params[post_form.find('textarea')['name']] = 'Reply'
+        r = self.app.post(self.post_link + 'reply',
+                          params=params,
+                          upload_files=[('file_info', 'test.txt', 'HiThere!')])
+        r = self.app.get(self.thread_link)
+        assert "test.txt" in r
+
+    def test_deleted_post_attachment(self):
+        f = os.path.join(os.path.dirname(__file__), '..', 'data', 'user.png')
+        with open(f) as f:
+            pic = f.read()
+        self.app.post(
+            self.post_link + 'attach',
+            upload_files=[('file_info', 'user.png', pic)])
+        alink = self.attach_link()
+        thumblink = alink + '/thumb'
+        self.app.get(alink, status=200)
+        self.app.get(thumblink, status=200)
+        _, slug = self.post_link.rstrip('/').rsplit('/', 1)
+        post = M.Post.query.get(slug=slug)
+        assert post, 'Could not find post for {} {}'.format(slug, self.post_link)
+        post.deleted = True
+        session(post).flush(post)
+        self.app.get(alink, status=404)
+        self.app.get(thumblink, status=404)

http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/tests/functional/test_feeds.py
----------------------------------------------------------------------
diff --git a/tests/functional/test_feeds.py b/tests/functional/test_feeds.py
new file mode 100644
index 0000000..d17252a
--- /dev/null
+++ b/tests/functional/test_feeds.py
@@ -0,0 +1,100 @@
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
+#       Licensed to the Apache Software Foundation (ASF) under one
+#       or more contributor license agreements.  See the NOTICE file
+#       distributed with this work for additional information
+#       regarding copyright ownership.  The ASF licenses this file
+#       to you under the Apache License, Version 2.0 (the
+#       "License"); you may not use this file except in compliance
+#       with the License.  You may obtain a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#       Unless required by applicable law or agreed to in writing,
+#       software distributed under the License is distributed on an
+#       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#       KIND, either express or implied.  See the License for the
+#       specific language governing permissions and limitations
+#       under the License.
+
+from formencode.variabledecode import variable_encode
+
+from allura.tests import TestController
+from allura.tests import decorators as td
+
+
+class TestFeeds(TestController):
+
+    def setUp(self):
+        TestController.setUp(self)
+        self._setUp()
+
+    @td.with_wiki
+    @td.with_tracker
+    def _setUp(self):
+        self.app.get('/wiki/')
+        self.app.get('/bugs/')
+        self.app.post(
+            '/bugs/save_ticket',
+            params=variable_encode(dict(
+                ticket_form=dict(
+                    ticket_num='',
+                    labels='',
+                    assigned_to='',
+                    milestone='',
+                    summary='This is a ticket',
+                    status='open',
+                    description='This is a description'))),
+            status=302)
+        title = 'Descri\xe7\xe3o e Arquitetura'.encode('utf-8')
+        self.app.post(
+            '/wiki/%s/update' % title,
+            params=dict(
+                title=title,
+                text="Nothing much",
+                labels='',
+            ),
+            status=302)
+        self.app.get('/wiki/%s/' % title)
+
+    def test_project_feed(self):
+        self.app.get('/feed.rss')
+        self.app.get('/feed.atom')
+
+    @td.with_wiki
+    def test_wiki_feed(self):
+        self.app.get('/wiki/feed.rss')
+        self.app.get('/wiki/feed.atom')
+
+    @td.with_wiki
+    def test_wiki_page_feed(self):
+        self.app.post('/wiki/Root/update', params={
+            'title': 'Root',
+            'text': '',
+            'labels': '',
+            'viewable_by-0.id': 'all'})
+        self.app.get('/wiki/Root/feed.rss')
+        self.app.get('/wiki/Root/feed.atom')
+
+    @td.with_tracker
+    def test_ticket_list_feed(self):
+        self.app.get('/bugs/feed.rss')
+        self.app.get('/bugs/feed.atom')
+
+    @td.with_tracker
+    def test_ticket_feed(self):
+        self.app.get('/bugs/1/feed.rss')
+        r = self.app.get('/bugs/1/feed.atom')
+        self.app.post('/bugs/1/update_ticket', params=dict(
+            assigned_to='',
+            ticket_num='',
+            labels='',
+            summary='This is a new ticket',
+            status='unread',
+            milestone='',
+            description='This is another description'), extra_environ=dict(username='root'))
+        r = self.app.get('/bugs/1/feed.atom')
+        assert '=&amp;gt' in r
+        assert '\n+' in r

http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/tests/functional/test_gravatar.py
----------------------------------------------------------------------
diff --git a/tests/functional/test_gravatar.py b/tests/functional/test_gravatar.py
new file mode 100644
index 0000000..2d2d597
--- /dev/null
+++ b/tests/functional/test_gravatar.py
@@ -0,0 +1,55 @@
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
+#       Licensed to the Apache Software Foundation (ASF) under one
+#       or more contributor license agreements.  See the NOTICE file
+#       distributed with this work for additional information
+#       regarding copyright ownership.  The ASF licenses this file
+#       to you under the Apache License, Version 2.0 (the
+#       "License"); you may not use this file except in compliance
+#       with the License.  You may obtain a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#       Unless required by applicable law or agreed to in writing,
+#       software distributed under the License is distributed on an
+#       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#       KIND, either express or implied.  See the License for the
+#       specific language governing permissions and limitations
+#       under the License.
+
+from urllib.parse import urlparse, parse_qs
+
+from allura.tests import TestController
+import allura.lib.gravatar as gravatar
+
+
+class TestGravatar(TestController):
+
+    def test_id(self):
+        email = 'Wolf@example.com'
+        expected_id = 'd3514940ac1b2051c8aa42970d17e3fe'
+        actual_id = gravatar.id(email)
+        assert expected_id == actual_id
+
+    def test_unicode_id(self):
+        email = 'Vin\u00EDcius@example.com'
+        expected_id = 'e00968255d68523b034a6a39c522efdb'
+        actual_id = gravatar.id(email)
+        assert expected_id == actual_id, 'Expected gravitar ID %s, got %s' % (
+            repr(expected_id), repr(actual_id))
+
+    def test_url(self):
+        email = 'Wolf@example.com'
+        expected_id = 'd3514940ac1b2051c8aa42970d17e3fe'
+        url = urlparse(gravatar.url(email=email))
+        assert url.netloc == 'secure.gravatar.com'
+        assert url.path == '/avatar/' + expected_id
+
+    def test_defaults(self):
+        email = 'Wolf@example.com'
+        url = urlparse(gravatar.url(email=email, rating='x'))
+        query = parse_qs(url.query)
+        assert 'r' not in query
+        assert query['rating'] == ['x']

http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/tests/functional/test_home.py
----------------------------------------------------------------------
diff --git a/tests/functional/test_home.py b/tests/functional/test_home.py
new file mode 100644
index 0000000..e02ef01
--- /dev/null
+++ b/tests/functional/test_home.py
@@ -0,0 +1,171 @@
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
+#       Licensed to the Apache Software Foundation (ASF) under one
+#       or more contributor license agreements.  See the NOTICE file
+#       distributed with this work for additional information
+#       regarding copyright ownership.  The ASF licenses this file
+#       to you under the Apache License, Version 2.0 (the
+#       "License"); you may not use this file except in compliance
+#       with the License.  You may obtain a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#       Unless required by applicable law or agreed to in writing,
+#       software distributed under the License is distributed on an
+#       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#       KIND, either express or implied.  See the License for the
+#       specific language governing permissions and limitations
+#       under the License.
+
+import json
+import re
+
+from pylons import tmpl_context as c
+
+from allura.tests import TestController
+from allura.tests import decorators as td
+from allura import model as M
+from ming.orm import ThreadLocalORMSession
+
+from nose.tools import assert_equal, assert_not_in
+
+
+class TestProjectHome(TestController):
+
+    @td.with_wiki
+    def test_project_nav(self):
+        response = self.app.get('/p/test/_nav.json')
+        root = self.app.get('/p/test/wiki/').follow()
+        assert re.search(r'<!-- Server: \S+ -->',
+                         str(root.html)), 'Missing Server comment'
+        nav_links = root.html.find('div', dict(id='top_nav')).findAll('a')
+        assert len(nav_links) == len(response.json['menu'])
+        for nl, entry in zip(nav_links, response.json['menu']):
+            assert nl['href'] == entry['url']
+
+    @td.with_wiki
+    def test_project_group_nav(self):
+        c.user = M.User.by_username('test-admin')
+        p = M.Project.query.get(shortname='test')
+        c.project = p
+        if 'wiki2' and not p.app_instance('wiki2'):
+            c.app = p.install_app('wiki', 'wiki2', 'wiki2', 9)
+
+        response = self.app.get('/p/test/_nav.json')
+        menu = response.json['menu']
+        wikis = menu[-2]['children']
+        assert_equal(len(wikis), 2)
+        assert {'url': '/p/test/wiki/', 'name': 'Wiki', 'icon':
+                'tool-wiki', 'tool_name': 'wiki'} in wikis, wikis
+        assert {'url': '/p/test/wiki2/', 'name': 'wiki2', 'icon':
+                'tool-wiki', 'tool_name': 'wiki'} in wikis, wikis
+
+    def test_sitemap_limit_per_tool(self):
+        """Test that sitemap is limited to max of 10 items per tool type."""
+        c.user = M.User.by_username('test-admin')
+        p = M.Project.query.get(shortname='test')
+        c.project = p
+        for i in range(11):
+            mnt = 'wiki' + str(i)
+            p.install_app('wiki', mnt, mnt, 10 + i)
+
+        response = self.app.get('/p/test/_nav.json')
+        menu = response.json['menu']
+        wikis = menu[-2]['children']
+        assert_equal(len(wikis), 10)
+
+    @td.with_wiki
+    def test_project_group_nav_more_than_ten(self):
+        for i in range(1, 15):
+            tool_name = "wiki%s" % str(i)
+            c.user = M.User.by_username('test-admin')
+            p = M.Project.query.get(shortname='test')
+            c.project = p
+            if tool_name and not p.app_instance(tool_name):
+                c.app = p.install_app('wiki', tool_name, tool_name, i)
+        response = self.app.get('/p/test/_nav.json')
+        menu = response.json['menu']
+        wiki_menu = [m for m in menu if m['tool_name'] == 'wiki'][0]
+        assert_equal(len(wiki_menu['children']), 10)
+        assert {'url': '/p/test/_list/wiki', 'name': 'More...',
+                'icon': 'tool-wiki', 'tool_name': 'wiki'} in wiki_menu['children']
+
+    @td.with_wiki
+    def test_neighborhood_home(self):
+        self.app.get('/p/test/wiki/', status=302)
+        self.app.get('/adobe/test/wiki/', status=404)
+        self.app.get('/adobe/no_such_project/wiki/', status=404)
+
+    @td.with_user_project('test-admin')
+    def test_user_subproject_home_not_profile(self):
+        u_proj = M.Project.query.get(shortname='u/test-admin')
+        u_proj.new_subproject('sub1')
+        ThreadLocalORMSession.flush_all()
+
+        r = self.app.get('/u/test-admin/sub1/')
+        assert r.location.endswith('admin/'), r.location
+        assert_not_in('Profile', r.follow().body)
+
+    def test_user_search(self):
+        r = self.app.get('/p/test/user_search?term=test', status=200)
+        j = json.loads(r.body)
+        assert j['users'][0]['id'].startswith('test')
+
+    def test_user_search_for_disabled_user(self):
+        user = M.User.by_username('test-admin')
+        user.disabled = True
+        ThreadLocalORMSession.flush_all()
+        r = self.app.get('/p/test/user_search?term=test', status=200)
+        j = json.loads(r.body)
+        assert j == {'users': []}
+
+    def test_user_search_noparam(self):
+        self.app.get('/p/test/user_search', status=400)
+
+    def test_user_search_shortparam(self):
+        self.app.get('/p/test/user_search?term=ad', status=400)
+
+    def test_users(self):
+        r = self.app.get('/p/test/users', status=200)
+        j = json.loads(r.body)
+        expected = [{
+            'value': 'test-admin',
+            'label': 'Test Admin (test-admin)'
+        }]
+        assert_equal(j['options'], expected)
+
+    def test_members(self):
+        nbhd = M.Neighborhood.query.get(name='Projects')
+        self.app.post('/admin/groups/create', params={'name': 'B_role'})
+        test_project = M.Project.query.get(
+            shortname='test', neighborhood_id=nbhd._id)
+        test_project.add_user(M.User.by_username('test-user-1'), ['B_role'])
+        test_project.add_user(M.User.by_username('test-user'), ['Developer'])
+        test_project.add_user(M.User.by_username('test-user-0'), ['Member'])
+        test_project.add_user(M.User.by_username('test-user-2'), ['Member'])
+        test_project.add_user(M.User.by_username('test-user-3'), ['Member'])
+        test_project.add_user(M.User.by_username('test-user-3'), ['Developer'])
+        test_project.add_user(M.User.by_username('test-user-4'), ['Admin'])
+        ThreadLocalORMSession.flush_all()
+        r = self.app.get('/p/test/_members/')
+
+        assert '<td>Test Admin</td>' in r
+        assert '<td><a href="/u/test-admin/">test-admin</a></td>' in r
+        assert '<td>Admin</td>' in r
+        tr = r.html.findAll('tr')
+        assert "<td>Test Admin</td>" in str(tr[1])
+        assert "<td>Test User 4</td>" in str(tr[2])
+        assert "<td>Test User</td>" in str(tr[3])
+        assert "<td>Test User 3</td>" in str(tr[4])
+        assert "<td>Test User 0</td>" in str(tr[5])
+        assert "<td>Test User 1</td>" in str(tr[6])
+        assert "<td>Test User 2</td>" in str(tr[7])
+
+    def test_members_anonymous(self):
+        r = self.app.get('/p/test/_members/',
+                         extra_environ=dict(username='*anonymous'))
+        assert '<td>Test Admin</td>' in r
+        assert '<td><a href="/u/test-admin/">test-admin</a></td>' in r
+        assert '<td>Admin</td>' in r

http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/tests/functional/test_neighborhood.py
----------------------------------------------------------------------
diff --git a/tests/functional/test_neighborhood.py b/tests/functional/test_neighborhood.py
new file mode 100644
index 0000000..64049ad
--- /dev/null
+++ b/tests/functional/test_neighborhood.py
@@ -0,0 +1,1057 @@
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
+#       Licensed to the Apache Software Foundation (ASF) under one
+#       or more contributor license agreements.  See the NOTICE file
+#       distributed with this work for additional information
+#       regarding copyright ownership.  The ASF licenses this file
+#       to you under the Apache License, Version 2.0 (the
+#       "License"); you may not use this file except in compliance
+#       with the License.  You may obtain a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#       Unless required by applicable law or agreed to in writing,
+#       software distributed under the License is distributed on an
+#       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#       KIND, either express or implied.  See the License for the
+#       specific language governing permissions and limitations
+#       under the License.
+
+import json
+import os
+from io import StringIO
+import urllib.request, urllib.error, urllib.parse
+
+import PIL
+from mock import patch
+from tg import config
+from nose.tools import assert_equal, assert_in, assert_not_equal
+from ming.orm.ormsession import ThreadLocalORMSession
+from paste.httpexceptions import HTTPFound
+from pylons import app_globals as g
+
+import allura
+from allura import model as M
+from allura.tests import TestController
+from allura.tests import decorators as td
+from allura.lib import helpers as h
+from alluratest.controller import setup_trove_categories
+
+
+class TestNeighborhood(TestController):
+
+    def test_home_project(self):
+        r = self.app.get('/adobe/wiki/')
+        assert r.location.endswith('/adobe/wiki/Home/')
+        r = r.follow()
+        assert 'This is the "Adobe" neighborhood' in str(r), str(r)
+        r = self.app.get(
+            '/adobe/admin/', extra_environ=dict(username='test-user'),
+            status=403)
+
+    def test_redirect(self):
+        r = self.app.post('/adobe/_admin/update',
+                          params=dict(redirect='wiki/Home/'),
+                          extra_environ=dict(username='root'))
+        r = self.app.get('/adobe/')
+        assert r.location.endswith('/adobe/wiki/Home/')
+
+    def test_admin(self):
+        r = self.app.get('/adobe/_admin/', extra_environ=dict(username='root'))
+        r = self.app.get('/adobe/_admin/overview',
+                         extra_environ=dict(username='root'))
+        r = self.app.get('/adobe/_admin/accolades',
+                         extra_environ=dict(username='root'))
+        neighborhood = M.Neighborhood.query.get(name='Adobe')
+        neighborhood.features['google_analytics'] = True
+        r = self.app.post('/adobe/_admin/update',
+                          params=dict(name='Mozq1', css='',
+                                      homepage='# MozQ1!', tracking_id='U-123456'),
+                          extra_environ=dict(username='root'))
+        r = self.app.post('/adobe/_admin/update',
+                          params=dict(name='Mozq1', css='',
+                                      homepage='# MozQ1!\n[Root]'),
+                          extra_environ=dict(username='root'))
+        # make sure project_template is validated as proper json
+        r = self.app.post('/adobe/_admin/update',
+                          params=dict(project_template='{'),
+                          extra_environ=dict(username='root'))
+        assert 'Invalid JSON' in r
+
+    def test_admin_overview_audit_log(self):
+
+        def check_log(message):
+            return M.AuditLog.query.find({'message': message}).count() == 1
+
+        nbhd = M.Neighborhood.query.get(name='Projects')
+        nbhd.features['css'] = 'custom'
+        nbhd.features['google_analytics'] = True
+        params = {
+            'name': 'Pjs',
+            'redirect': 'http://fake.org/',
+            'show_title': 'false',
+            'allow_browse': 'false',
+            'css': '.class { border: 1px; }',
+            'tracking_id': 'U-123456',
+            'homepage': '[Homepage]',
+            'project_list_url': 'http://fake.org/project_list',
+            'project_template': '{"name": "template"}',
+            'anchored_tools': 'wiki:Wiki',
+            'prohibited_tools': 'wiki, tickets'
+
+        }
+        self.app.post('/p/_admin/update', params=params,
+                      extra_environ=dict(username='root'))
+        # must get as many log records as many values are updated
+        assert M.AuditLog.query.find().count() == len(params)
+
+        assert check_log('change neighborhood name to Pjs')
+        assert check_log('change neighborhood redirect to http://fake.org/')
+        assert check_log('change neighborhood show title to False')
+        assert check_log('change neighborhood allow browse to False')
+        assert check_log('change neighborhood css to .class { border: 1px; }')
+        assert check_log('change neighborhood homepage to [Homepage]')
+        assert check_log('change neighborhood project list url to '
+                         'http://fake.org/project_list')
+
+        assert check_log('change neighborhood project template to '
+                         '{"name": "template"}')
+        assert check_log('update neighborhood tracking_id')
+        assert check_log('update neighborhood prohibited tools')
+
+    def test_prohibited_tools(self):
+        self.app.post('/p/_admin/update',
+                          params=dict(name='Projects',
+                          prohibited_tools='wiki, tickets'),
+                          extra_environ=dict(username='root'))
+
+        r = self.app.get('/p/_admin/overview', extra_environ=dict(username='root'))
+        assert 'wiki, tickets' in r
+
+        r = self.app.get('/p/test/admin/tools')
+        assert ' <span class="tool_title">Wiki</span><br />' not in r
+        assert ' <span class="tool_title">Tickets</span><br />' not in r
+
+        r = self.app.post('/p/_admin/update',
+                          params=dict(name='Projects',
+                          prohibited_tools='wiki, test'),
+                          extra_environ=dict(username='root'))
+        assert 'error' in self.webflash(r), self.webflash(r)
+
+    @td.with_wiki
+    def test_anchored_tools(self):
+        neighborhood = M.Neighborhood.query.get(name='Projects')
+
+        r = self.app.post('/p/_admin/update',
+                          params=dict(name='Projects',
+                                      anchored_tools='wiki:Wiki, tickets:Ticket'),
+                          extra_environ=dict(username='root'))
+        assert 'error' not in self.webflash(r)
+        r = self.app.post('/p/_admin/update',
+                          params=dict(name='Projects',
+                                      anchored_tools='w!iki:Wiki, tickets:Ticket'),
+                          extra_environ=dict(username='root'))
+        assert 'error' in self.webflash(r)
+        assert_equal(neighborhood.anchored_tools, 'wiki:Wiki, tickets:Ticket')
+
+        r = self.app.post('/p/_admin/update',
+                          params=dict(name='Projects',
+                                      anchored_tools='wiki:Wiki,'),
+                          extra_environ=dict(username='root'))
+        assert 'error' in self.webflash(r)
+        assert_equal(neighborhood.anchored_tools, 'wiki:Wiki, tickets:Ticket')
+
+        r = self.app.post('/p/_admin/update',
+                          params=dict(name='Projects',
+                                      anchored_tools='badname,'),
+                          extra_environ=dict(username='root'))
+        assert 'error' in self.webflash(r)
+        assert_equal(neighborhood.anchored_tools, 'wiki:Wiki, tickets:Ticket')
+
+        r = self.app.get('/p/test/admin/overview')
+        top_nav = r.html.find(id='top_nav')
+        assert top_nav.find(href='/p/test/wiki/'), top_nav
+        assert top_nav.find(href='/p/test/tickets/'), top_nav
+
+        r = self.app.get('/p/test/admin/tools')
+        assert '<div class="fleft isnt_sorted">' in r
+        delete_tool = r.html.find('a', {'class': 'mount_delete'})
+        assert_equal(len(delete_tool), 1)
+
+    def test_show_title(self):
+        r = self.app.get('/adobe/_admin/overview',
+                         extra_environ=dict(username='root'))
+        neighborhood = M.Neighborhood.query.get(name='Adobe')
+        # if not set show_title must be True
+        assert neighborhood.show_title
+        # title should be present
+        assert 'class="project_title"' in str(r)
+        r = self.app.post('/adobe/_admin/update',
+                          params=dict(name='Mozq1', css='',
+                                      homepage='# MozQ1!',
+                                      tracking_id='U-123456',
+                                      show_title='false'),
+                          extra_environ=dict(username='root'))
+        # no title now
+        r = self.app.get('/adobe/', extra_environ=dict(username='root'))
+        assert 'class="project_title"' not in str(r)
+        r = self.app.get('/adobe/wiki/Home/',
+                         extra_environ=dict(username='root'))
+        assert 'class="project_title"' not in str(r)
+
+        # title must be present on project page
+        r = self.app.get('/adobe/adobe-1/admin/',
+                         extra_environ=dict(username='root'))
+        assert 'class="project_title"' in str(r)
+
+    def test_admin_stats_del_count(self):
+        neighborhood = M.Neighborhood.query.get(name='Adobe')
+        proj = M.Project.query.get(neighborhood_id=neighborhood._id)
+        proj.deleted = True
+        ThreadLocalORMSession.flush_all()
+        r = self.app.get('/adobe/_admin/stats/',
+                         extra_environ=dict(username='root'))
+        assert 'Deleted: 1' in r
+        assert 'Private: 0' in r
+
+    def test_admin_stats_priv_count(self):
+        neighborhood = M.Neighborhood.query.get(name='Adobe')
+        proj = M.Project.query.get(neighborhood_id=neighborhood._id)
+        proj.deleted = False
+        proj.private = True
+        ThreadLocalORMSession.flush_all()
+        r = self.app.get('/adobe/_admin/stats/',
+                         extra_environ=dict(username='root'))
+        assert 'Deleted: 0' in r
+        assert 'Private: 1' in r
+
+    def test_admin_stats_adminlist(self):
+        neighborhood = M.Neighborhood.query.get(name='Adobe')
+        proj = M.Project.query.get(neighborhood_id=neighborhood._id)
+        proj.private = False
+        ThreadLocalORMSession.flush_all()
+        r = self.app.get('/adobe/_admin/stats/adminlist',
+                         extra_environ=dict(username='root'))
+        pq = M.Project.query.find(
+            dict(neighborhood_id=neighborhood._id, deleted=False))
+        pq.sort('name')
+        projects = pq.skip(0).limit(int(25)).all()
+        for proj in projects:
+            admin_role = M.ProjectRole.query.get(
+                project_id=proj.root_project._id, name='Admin')
+            if admin_role is None:
+                continue
+            user_role_list = M.ProjectRole.query.find(
+                dict(project_id=proj.root_project._id, name=None)).all()
+            for ur in user_role_list:
+                if ur.user is not None and admin_role._id in ur.roles:
+                    assert proj.name in r
+                    assert ur.user.username in r
+
+    def test_icon(self):
+        file_name = 'neo-icon-set-454545-256x350.png'
+        file_path = os.path.join(
+            allura.__path__[0], 'nf', 'allura', 'images', file_name)
+        file_data = file(file_path).read()
+        upload = ('icon', file_name, file_data)
+
+        r = self.app.get('/adobe/_admin/', extra_environ=dict(username='root'))
+        r = self.app.post('/adobe/_admin/update',
+                          params=dict(name='Mozq1', css='',
+                                      homepage='# MozQ1'),
+                          extra_environ=dict(username='root'), upload_files=[upload])
+        r = self.app.get('/adobe/icon')
+        image = PIL.Image.open(StringIO(r.body))
+        assert image.size == (48, 48)
+
+        r = self.app.get('/adobe/icon?foo=bar')
+
+    def test_google_analytics(self):
+        # analytics allowed
+        neighborhood = M.Neighborhood.query.get(name='Adobe')
+        neighborhood.features['google_analytics'] = True
+        r = self.app.get('/adobe/_admin/overview',
+                         extra_environ=dict(username='root'))
+        assert 'Analytics Tracking ID' in r
+        r = self.app.get('/adobe/adobe-1/admin/overview',
+                         extra_environ=dict(username='root'))
+        assert 'Analytics Tracking ID' in r
+        r = self.app.post('/adobe/_admin/update',
+                          params=dict(name='Adobe', css='',
+                                      homepage='# MozQ1', tracking_id='U-123456'),
+                          extra_environ=dict(username='root'))
+        r = self.app.post('/adobe/adobe-1/admin/update',
+                          params=dict(tracking_id='U-654321'),
+                          extra_environ=dict(username='root'))
+        r = self.app.get('/adobe/adobe-1/admin/overview',
+                         extra_environ=dict(username='root'))
+        assert "_add_tracking('nbhd', 'U-123456');" in r, r
+        assert "_add_tracking('proj', 'U-654321');" in r
+        # analytics not allowed
+        neighborhood = M.Neighborhood.query.get(name='Adobe')
+        neighborhood.features['google_analytics'] = False
+        r = self.app.get('/adobe/_admin/overview',
+                         extra_environ=dict(username='root'))
+        assert 'Analytics Tracking ID' not in r
+        r = self.app.get('/adobe/adobe-1/admin/overview',
+                         extra_environ=dict(username='root'))
+        assert 'Analytics Tracking ID' not in r
+        r = self.app.get('/adobe/adobe-1/admin/overview',
+                         extra_environ=dict(username='root'))
+        assert "_add_tracking('nbhd', 'U-123456');" not in r
+        assert "_add_tracking('proj', 'U-654321');" not in r
+
+    def test_custom_css(self):
+        test_css = '.test{color:red;}'
+        custom_css = 'Custom CSS'
+
+        neighborhood = M.Neighborhood.query.get(name='Adobe')
+        neighborhood.css = test_css
+        neighborhood.features['css'] = 'none'
+        r = self.app.get('/adobe/')
+        assert test_css not in r
+        r = self.app.get('/adobe/_admin/overview',
+                         extra_environ=dict(username='root'))
+        assert custom_css not in r
+
+        neighborhood = M.Neighborhood.query.get(name='Adobe')
+        neighborhood.features['css'] = 'picker'
+        r = self.app.get('/adobe/')
+        while isinstance(r.response, HTTPFound):
+            r = r.follow()
+        assert test_css in r
+        r = self.app.get('/adobe/_admin/overview',
+                         extra_environ=dict(username='root'))
+        assert custom_css in r
+
+        neighborhood = M.Neighborhood.query.get(name='Adobe')
+        neighborhood.features['css'] = 'custom'
+        r = self.app.get('/adobe/')
+        while isinstance(r.response, HTTPFound):
+            r = r.follow()
+        assert test_css in r
+        r = self.app.get('/adobe/_admin/overview',
+                         extra_environ=dict(username='root'))
+        assert custom_css in r
+
+    def test_picker_css(self):
+        neighborhood = M.Neighborhood.query.get(name='Adobe')
+        neighborhood.features['css'] = 'picker'
+
+        r = self.app.get('/adobe/_admin/overview',
+                         extra_environ=dict(username='root'))
+        assert 'Project title, font' in r
+        assert 'Project title, color' in r
+        assert 'Bar on top' in r
+        assert 'Title bar, background' in r
+        assert 'Title bar, foreground' in r
+
+        r = self.app.post('/adobe/_admin/update',
+                          params={'name': 'Adobe',
+                                  'css': '',
+                                  'homepage': '',
+                                  'css-projecttitlefont': 'arial,sans-serif',
+                                  'css-projecttitlecolor': 'green',
+                                  'css-barontop': '#555555',
+                                  'css-titlebarbackground': '#333',
+                                  'css-titlebarcolor': '#444',
+                                  'css-addopt-icon-theme': 'dark'},
+                          extra_environ=dict(username='root'), upload_files=[])
+        neighborhood = M.Neighborhood.query.get(name='Adobe')
+        assert '/*projecttitlefont*/.project_title{font-family:arial,sans-serif;}' in neighborhood.css
+        assert '/*projecttitlecolor*/.project_title{color:green;}' in neighborhood.css
+        assert '/*barontop*/.pad h2.colored {background-color:#555555; background-image: none;}' in neighborhood.css
+        assert '/*titlebarbackground*/.pad h2.title{background-color:#333; background-image: none;}' in neighborhood.css
+        assert "/*titlebarcolor*/.pad h2.title, .pad h2.title small a {color:#444;} "\
+               ".pad h2.dark small b.ico {background-image: "\
+               "url('/nf/_ew_/theme/allura/images/neo-icon-set-ffffff-256x350.png');}" in neighborhood.css
+
+    def test_max_projects(self):
+        # Set max value to unlimit
+        neighborhood = M.Neighborhood.query.get(name='Projects')
+        neighborhood.features['max_projects'] = None
+        r = self.app.post('/p/register',
+                          params=dict(
+                              project_unixname='maxproject1', project_name='Max project1',
+                              project_description='', neighborhood='Projects'),
+                          antispam=True,
+                          extra_environ=dict(username='root'), status=302)
+        assert '/p/maxproject1/admin' in r.location
+
+        # Set max value to 0
+        neighborhood = M.Neighborhood.query.get(name='Projects')
+        neighborhood.features['max_projects'] = 0
+        r = self.app.post('/p/register',
+                          params=dict(
+                              project_unixname='maxproject2', project_name='Max project2',
+                              project_description='', neighborhood='Projects'),
+                          antispam=True,
+                          extra_environ=dict(username='root'))
+        while isinstance(r.response, HTTPFound):
+            r = r.follow()
+        assert 'You have exceeded the maximum number of projects' in r
+
+    def test_project_rate_limit(self):
+        # Set rate limit to unlimit
+        with h.push_config(config, **{'project.rate_limits': '{}'}):
+            r = self.app.post('/p/register',
+                              params=dict(
+                                  project_unixname='rateproject1', project_name='Rate project1',
+                                  project_description='', neighborhood='Projects'),
+                              antispam=True,
+                              extra_environ=dict(username='test-user-1'), status=302)
+            assert '/p/rateproject1/admin' in r.location
+
+        # Set rate limit to 1 in first hour of user account
+        with h.push_config(config, **{'project.rate_limits': '{"3600": 1}'}):
+            r = self.app.post('/p/register',
+                              params=dict(
+                                  project_unixname='rateproject2', project_name='Rate project2',
+                                  project_description='', neighborhood='Projects'),
+                              antispam=True,
+                              extra_environ=dict(username='test-user-1'))
+            while isinstance(r.response, HTTPFound):
+                r = r.follow()
+            assert 'Project creation rate limit exceeded.  Please try again later.' in r
+
+    def test_project_rate_limit_admin(self):
+        # Set rate limit to unlimit
+        with h.push_config(config, **{'project.rate_limits': '{}'}):
+            r = self.app.post('/p/register',
+                              params=dict(
+                                  project_unixname='rateproject1', project_name='Rate project1',
+                                  project_description='', neighborhood='Projects'),
+                              antispam=True,
+                              extra_environ=dict(username='root'), status=302)
+            assert '/p/rateproject1/admin' in r.location
+
+        # Set rate limit to 1 in first hour of user account
+        with h.push_config(config, **{'project.rate_limits': '{"3600": 1}'}):
+            r = self.app.post('/p/register',
+                              params=dict(
+                                  project_unixname='rateproject2', project_name='Rate project2',
+                                  project_description='', neighborhood='Projects'),
+                              antispam=True,
+                              extra_environ=dict(username='root'))
+            assert '/p/rateproject2/admin' in r.location
+
+    def test_invite(self):
+        p_nbhd_id = str(M.Neighborhood.query.get(name='Projects')._id)
+        r = self.app.get('/adobe/_moderate/',
+                         extra_environ=dict(username='root'))
+        r = self.app.post('/adobe/_moderate/invite',
+                          params=dict(pid='adobe-1', invite='on',
+                                      neighborhood_id=p_nbhd_id),
+                          extra_environ=dict(username='root'))
+        r = self.app.get(r.location, extra_environ=dict(username='root'))
+        assert 'error' in r
+        r = self.app.post('/adobe/_moderate/invite',
+                          params=dict(pid='no_such_user',
+                                      invite='on', neighborhood_id=p_nbhd_id),
+                          extra_environ=dict(username='root'))
+        r = self.app.get(r.location, extra_environ=dict(username='root'))
+        assert 'error' in r
+        r = self.app.post('/adobe/_moderate/invite',
+                          params=dict(pid='test', invite='on',
+                                      neighborhood_id=p_nbhd_id),
+                          extra_environ=dict(username='root'))
+        r = self.app.get(r.location, extra_environ=dict(username='root'))
+        assert 'invited' in r, r
+        assert 'warning' not in r
+        r = self.app.post('/adobe/_moderate/invite',
+                          params=dict(pid='test', invite='on',
+                                      neighborhood_id=p_nbhd_id),
+                          extra_environ=dict(username='root'))
+        r = self.app.get(r.location, extra_environ=dict(username='root'))
+        assert 'warning' in r
+        r = self.app.post('/adobe/_moderate/invite',
+                          params=dict(pid='test', uninvite='on',
+                                      neighborhood_id=p_nbhd_id),
+                          extra_environ=dict(username='root'))
+        r = self.app.get(r.location, extra_environ=dict(username='root'))
+        assert 'uninvited' in r
+        assert 'warning' not in r
+        r = self.app.post('/adobe/_moderate/invite',
+                          params=dict(pid='test', uninvite='on',
+                                      neighborhood_id=p_nbhd_id),
+                          extra_environ=dict(username='root'))
+        r = self.app.get(r.location, extra_environ=dict(username='root'))
+        assert 'warning' in r
+        r = self.app.post('/adobe/_moderate/invite',
+                          params=dict(pid='test', invite='on',
+                                      neighborhood_id=p_nbhd_id),
+                          extra_environ=dict(username='root'))
+        r = self.app.get(r.location, extra_environ=dict(username='root'))
+        assert 'invited' in r
+        assert 'warning' not in r
+
+    def test_evict(self):
+        r = self.app.get('/adobe/_moderate/',
+                         extra_environ=dict(username='root'))
+        r = self.app.post('/adobe/_moderate/evict',
+                          params=dict(pid='test'),
+                          extra_environ=dict(username='root'))
+        r = self.app.get(r.location, extra_environ=dict(username='root'))
+        assert 'error' in r
+        r = self.app.post('/adobe/_moderate/evict',
+                          params=dict(pid='adobe-1'),
+                          extra_environ=dict(username='root'))
+        r = self.app.get(r.location, extra_environ=dict(username='root'))
+        assert 'adobe-1 evicted to Projects' in r
+
+    def test_home(self):
+        self.app.get('/adobe/')
+
+    def test_register(self):
+        r = self.app.get('/adobe/register', status=405)
+        r = self.app.post('/adobe/register',
+                          params=dict(
+                              project_unixname='', project_name='Nothing',
+                              project_description='', neighborhood='Adobe'),
+                          antispam=True,
+                          extra_environ=dict(username='root'))
+        assert r.html.find('div', {'class': 'error'}
+                           ).string == 'Please use only letters, numbers, and dashes 3-15 characters long.'
+        r = self.app.post('/adobe/register',
+                          params=dict(
+                              project_unixname='mymoz', project_name='My Moz',
+                              project_description='', neighborhood='Adobe'),
+                          antispam=True,
+                          extra_environ=dict(username='*anonymous'),
+                          status=302)
+        r = self.app.post('/adobe/register',
+                          params=dict(
+                              project_unixname='foo.mymoz', project_name='My Moz',
+                              project_description='', neighborhood='Adobe'),
+                          antispam=True,
+                          extra_environ=dict(username='root'))
+        assert r.html.find('div', {'class': 'error'}
+                           ).string == 'Please use only letters, numbers, and dashes 3-15 characters long.'
+        r = self.app.post('/p/register',
+                          params=dict(
+                              project_unixname='test', project_name='Tester',
+                              project_description='', neighborhood='Projects'),
+                          antispam=True,
+                          extra_environ=dict(username='root'))
+        assert r.html.find('div', {'class': 'error'}
+                           ).string == 'This project name is taken.'
+        r = self.app.post('/adobe/register',
+                          params=dict(
+                              project_unixname='mymoz', project_name='My Moz',
+                              project_description='', neighborhood='Adobe'),
+                          antispam=True,
+                          extra_environ=dict(username='root'),
+                          status=302)
+
+    def test_register_private_fails_for_anon(self):
+        r = self.app.post(
+            '/p/register',
+            params=dict(
+                project_unixname='mymoz',
+                project_name='My Moz',
+                project_description='',
+                neighborhood='Projects',
+                private_project='on'),
+            antispam=True,
+            extra_environ=dict(username='*anonymous'),
+            status=302)
+        assert config.get('auth.login_url', '/auth/') in r.location, r.location
+
+    def test_register_private_fails_for_non_admin(self):
+        self.app.post(
+            '/p/register',
+            params=dict(
+                project_unixname='mymoz',
+                project_name='My Moz',
+                project_description='',
+                neighborhood='Projects',
+                private_project='on'),
+            antispam=True,
+            extra_environ=dict(username='test-user'),
+            status=403)
+
+    def test_register_private_fails_for_non_private_neighborhood(self):
+        # Turn off private
+        neighborhood = M.Neighborhood.query.get(name='Projects')
+        neighborhood.features['private_projects'] = False
+        r = self.app.get('/p/add_project', extra_environ=dict(username='root'))
+        assert 'private_project' not in r
+
+        r = self.app.post(
+            '/p/register',
+            params=dict(
+                project_unixname='myprivate1',
+                project_name='My Priv1',
+                project_description='',
+                neighborhood='Projects',
+                private_project='on'),
+            antispam=True,
+            extra_environ=dict(username='root'))
+        cookies = r.headers.getall('Set-Cookie')
+        flash_msg_cookies = list(map(urllib.parse.unquote, cookies))
+
+        assert any('Internal Error' in cookie for cookie in flash_msg_cookies)
+
+        proj = M.Project.query.get(
+            shortname='myprivate1', neighborhood_id=neighborhood._id)
+        assert proj is None
+
+        # Turn on private
+        neighborhood = M.Neighborhood.query.get(name='Projects')
+        neighborhood.features['private_projects'] = True
+        r = self.app.get('/p/add_project', extra_environ=dict(username='root'))
+        assert 'private_project' in r
+
+        self.app.post(
+            '/p/register',
+            params=dict(
+                project_unixname='myprivate2',
+                project_name='My Priv2',
+                project_description='',
+                neighborhood='Projects',
+                private_project='on'),
+            antispam=True,
+            extra_environ=dict(username='root'))
+
+        proj = M.Project.query.get(
+            shortname='myprivate2', neighborhood_id=neighborhood._id)
+        assert proj.private
+
+    def test_register_private_ok(self):
+        r = self.app.post(
+            '/p/register',
+            params=dict(
+                project_unixname='mymoz',
+                project_name='My Moz',
+                project_description='',
+                neighborhood='Projects',
+                private_project='on',
+                tools='wiki'),
+            antispam=True,
+            extra_environ=dict(username='root'),
+            status=302)
+        assert config.get('auth.login_url',
+                          '/auth/') not in r.location, r.location
+        r = self.app.get(
+            '/p/mymoz/wiki/',
+            extra_environ=dict(username='root')).follow(extra_environ=dict(username='root'), status=200)
+        r = self.app.get(
+            '/p/mymoz/wiki/',
+            extra_environ=dict(username='*anonymous'),
+            status=302)
+        assert config.get('auth.login_url', '/auth/') in r.location, r.location
+        self.app.get(
+            '/p/mymoz/wiki/',
+            extra_environ=dict(username='test-user'),
+            status=403)
+
+    def test_project_template(self):
+        setup_trove_categories()
+        icon_url = 'file://' + \
+            os.path.join(allura.__path__[0], 'nf', 'allura',
+                         'images', 'neo-icon-set-454545-256x350.png')
+        test_groups = [{
+            "name": "Viewer",  # group will be created, all params are valid
+            "permissions": ["read"],
+            "usernames": ["user01"]
+        }, {
+            "name": "",  # group won't be created - invalid name
+            "permissions": ["read"],
+            "usernames": ["user01"]
+        }, {
+            "name": "TestGroup1",  # group won't be created - invalid perm name
+            "permissions": ["foobar"],
+            "usernames": ["user01"]
+        }, {
+            "name": "TestGroup2",  # will be created; 'inspect' perm ignored
+            "permissions": ["read", "inspect"],
+            "usernames": ["user01", "user02"]
+        }, {
+            "name": "TestGroup3",  # will be created with no users in group
+            "permissions": ["admin"]
+        }]
+        r = self.app.post('/adobe/_admin/update', params=dict(name='Mozq1',
+                                                              css='', homepage='# MozQ1!\n[Root]', project_template="""{
+                "private":true,
+                "icon":{
+                    "url":"%s",
+                    "filename":"icon.png"
+                },
+                "tools":{
+                    "wiki":{
+                        "label":"Wiki",
+                        "mount_point":"wiki",
+                        "options":{
+                            "show_right_bar":false,
+                            "show_left_bar":false,
+                            "show_discussion":false,
+                            "some_url": "http://foo.com/$shortname/"
+                        },
+                        "home_text":"My home text!"
+                    },
+                    "discussion":{"label":"Discussion","mount_point":"discussion"},
+                    "blog":{"label":"News","mount_point":"news","options":{
+                    "show_discussion":false
+                    }},
+                    "admin":{"label":"Admin","mount_point":"admin"}
+                },
+                "tool_order":["wiki","discussion","news","admin"],
+                "labels":["mmi"],
+                "trove_cats":{
+                    "topic":[247],
+                    "developmentstatus":[11]
+                },
+                "groups": %s
+                }""" % (icon_url, json.dumps(test_groups))),
+            extra_environ=dict(username='root'))
+        r = self.app.post(
+            '/adobe/register',
+            params=dict(
+                project_unixname='testtemp',
+                project_name='Test Template',
+                project_description='',
+                neighborhood='Mozq1',
+                private_project='off'),
+            antispam=True,
+            extra_environ=dict(username='root'),
+            status=302).follow()
+        p = M.Project.query.get(shortname='testtemp')
+        # make sure the correct tools got installed in the right order
+        top_nav = r.html.find('div', {'id': 'top_nav'})
+        assert top_nav.contents[1].contents[1].contents[
+            1]['href'] == '/adobe/testtemp/wiki/'
+        assert 'Wiki' in top_nav.contents[
+            1].contents[1].contents[1].contents[0]
+        assert top_nav.contents[1].contents[3].contents[
+            1]['href'] == '/adobe/testtemp/discussion/'
+        assert 'Discussion' in top_nav.contents[
+            1].contents[3].contents[1].contents[0]
+        assert top_nav.contents[1].contents[5].contents[
+            1]['href'] == '/adobe/testtemp/news/'
+        assert 'News' in top_nav.contents[
+            1].contents[5].contents[1].contents[0]
+        assert top_nav.contents[1].contents[7].contents[
+            1]['href'] == '/adobe/testtemp/admin/'
+        assert 'Admin' in top_nav.contents[
+            1].contents[7].contents[1].contents[0]
+        # make sure project is private
+        r = self.app.get(
+            '/adobe/testtemp/wiki/',
+            extra_environ=dict(username='root')).follow(extra_environ=dict(username='root'), status=200)
+        r = self.app.get(
+            '/adobe/testtemp/wiki/',
+            extra_environ=dict(username='*anonymous'),
+            status=302)
+        # check the labels and trove cats
+        r = self.app.get('/adobe/testtemp/admin/trove')
+        assert 'mmi' in r
+        assert 'Topic :: Communications :: Telephony' in r
+        assert 'Development Status :: 5 - Production/Stable' in r
+        # check the wiki text
+        r = self.app.get('/adobe/testtemp/wiki/').follow()
+        assert "My home text!" in r
+        # check tool options
+        opts = p.app_config('wiki').options
+        assert_equal(False, opts.show_discussion)
+        assert_equal(False, opts.show_left_bar)
+        assert_equal(False, opts.show_right_bar)
+        assert_equal("http://foo.com/testtemp/", opts.some_url)
+        # check that custom groups/perms/users were setup correctly
+        roles = p.named_roles
+        for group in test_groups:
+            name = group.get('name')
+            permissions = group.get('permissions', [])
+            usernames = group.get('usernames', [])
+            if name in ('Viewer', 'TestGroup2', 'TestGroup3'):
+                role = M.ProjectRole.by_name(name, project=p)
+                # confirm role created in project
+                assert role in roles
+                for perm in permissions:
+                    # confirm valid permissions added to role, and invalid
+                    # permissions ignored
+                    if perm in p.permissions:
+                        assert M.ACE.allow(role._id, perm) in p.acl
+                    else:
+                        assert M.ACE.allow(role._id, perm) not in p.acl
+                # confirm valid users received role
+                for username in usernames:
+                    user = M.User.by_username(username)
+                    if user and user._id:
+                        assert role in M.ProjectRole.by_user(
+                            user, project=p).roles
+            # confirm roles with invalid json data are not created
+            if name in ('', 'TestGroup1'):
+                assert name not in roles
+
+    def test_projects_anchored_tools(self):
+        r = self.app.post('/adobe/_admin/update', params=dict(name='Adobe',
+                                                              css='',
+                                                              homepage='# Adobe!\n[Root]',
+                                                              project_template="""{
+                "private":true,
+                "tools":{
+                    "wiki":{
+                        "label":"Wiki",
+                        "mount_point":"wiki",
+                        "options":{
+                            "show_right_bar":false,
+                            "show_left_bar":false,
+                            "show_discussion":false,
+                            "some_url": "http://foo.com/$shortname/"
+                        },
+                        "home_text":"My home text!"
+                    },
+                    "admin":{"label":"Admin","mount_point":"admin"}
+                },
+                "tool_order":["wiki","admin"],
+
+                }""" ),
+                          extra_environ=dict(username='root'))
+        neighborhood = M.Neighborhood.query.get(name='Adobe')
+        neighborhood.anchored_tools = 'wiki:Wiki'
+        r = self.app.post(
+            '/adobe/register',
+            params=dict(
+                project_unixname='testtemp',
+                project_name='Test Template',
+                project_description='',
+                neighborhood='Adobe',
+                private_project='off'),
+            antispam=True,
+            extra_environ=dict(username='root'))
+        r = self.app.get('/adobe/testtemp/admin/tools')
+        assert r.html.find('div', id='top_nav').find(
+            'a', href='/adobe/testtemp/wiki/'), r.html
+        assert r.html.find('div', id='top_nav').find(
+            'a', href='/adobe/testtemp/admin/'), r.html
+
+    def test_name_suggest(self):
+        r = self.app.get('/p/suggest_name?project_name=My+Moz')
+        assert_equal(r.json, dict(suggested_name='mymoz'))
+        r = self.app.get('/p/suggest_name?project_name=Te%st!')
+        assert_equal(r.json, dict(suggested_name='test'))
+
+    def test_name_check(self):
+        for name in ('My+Moz', 'Te%st!', 'ab', 'a' * 16):
+            r = self.app.get(
+                '/p/check_names?neighborhood=Projects&project_unixname=%s' % name)
+            assert_equal(
+                r.json, {'project_unixname': 'Please use only letters, numbers, and dashes 3-15 characters long.'})
+        r = self.app.get(
+            '/p/check_names?neighborhood=Projects&project_unixname=mymoz')
+        assert_equal(r.json, {})
+        r = self.app.get(
+            '/p/check_names?neighborhood=Projects&project_unixname=test')
+        assert_equal(r.json,
+                     {'project_unixname': 'This project name is taken.'})
+
+    @td.with_tool('test/sub1', 'Wiki', 'wiki')
+    def test_neighborhood_project(self):
+        self.app.get('/adobe/adobe-1/admin/', status=200)
+        self.app.get('/p/test/sub1/wiki/')
+        self.app.get('/p/test/sub1/', status=302)
+        self.app.get('/p/test/no-such-app/', status=404)
+
+    def test_neighborhood_namespace(self):
+        # p/test exists, so try creating adobe/test
+        self.app.get('/adobe/test/wiki/', status=404)
+        r = self.app.post('/adobe/register',
+                          params=dict(
+                              project_unixname='test', project_name='Test again',
+                              project_description='', neighborhood='Adobe', tools='wiki'),
+                          antispam=True,
+                          extra_environ=dict(username='root'))
+        assert r.status_int == 302, r.html.find(
+            'div', {'class': 'error'}).string
+        r = self.app.get('/adobe/test/wiki/').follow(status=200)
+
+    def test_neighborhood_awards(self):
+        file_name = 'adobe_icon.png'
+        file_path = os.path.join(
+            allura.__path__[0], 'public', 'nf', 'images', file_name)
+        file_data = file(file_path).read()
+        upload = ('icon', file_name, file_data)
+
+        r = self.app.get('/adobe/_admin/awards',
+                         extra_environ=dict(username='root'))
+        r = self.app.post('/adobe/_admin/awards/create',
+                          params=dict(short='FOO', full='A basic foo award'),
+                          extra_environ=dict(username='root'), upload_files=[upload])
+        r = self.app.post('/adobe/_admin/awards/create',
+                          params=dict(short='BAR',
+                                      full='A basic bar award with no icon'),
+                          extra_environ=dict(username='root'))
+        foo_id = str(M.Award.query.find(dict(short='FOO')).first()._id)
+        bar_id = str(M.Award.query.find(dict(short='BAR')).first()._id)
+        r = self.app.post('/adobe/_admin/awards/%s/update' % bar_id,
+                          params=dict(short='BAR2',
+                                      full='Updated description.'),
+                          extra_environ=dict(username='root')).follow().follow()
+        assert 'BAR2' in r
+        assert 'Updated description.' in r
+        r = self.app.get('/adobe/_admin/awards/%s' %
+                         foo_id, extra_environ=dict(username='root'))
+        r = self.app.get('/adobe/_admin/awards/%s/icon' %
+                         foo_id, extra_environ=dict(username='root'))
+        image = PIL.Image.open(StringIO(r.body))
+        assert image.size == (48, 48)
+        self.app.post('/adobe/_admin/awards/grant',
+                params=dict(grant='FOO', recipient='adobe-1',
+                            url='http://award.org', comment='Winner!'),
+                      extra_environ=dict(username='root'))
+        r = self.app.get('/adobe/_admin/accolades',
+                         extra_environ=dict(username='root'))
+        assert_in('Winner!', r)
+        assert_in('http://award.org', r)
+        self.app.get('/adobe/_admin/awards/%s/adobe-1' %
+                     foo_id, extra_environ=dict(username='root'))
+        self.app.post('/adobe/_admin/awards/%s/adobe-1/revoke' % foo_id,
+                      extra_environ=dict(username='root'))
+        self.app.post('/adobe/_admin/awards/%s/delete' % foo_id,
+                      extra_environ=dict(username='root'))
+
+    def test_add_a_project_link(self):
+        from pylons import tmpl_context as c
+        # Install Home tool for all neighborhoods
+        for nb in M.Neighborhood.query.find().all():
+            p = nb.neighborhood_project
+            with h.push_config(c, user=M.User.query.get()):
+                p.install_app('home', 'home', 'Home', ordinal=0)
+        r = self.app.get('/p/')
+        assert 'Add a Project' in r
+        r = self.app.get('/u/', extra_environ=dict(username='test-user'))
+        assert 'Add a Project' not in r
+        r = self.app.get('/adobe/', extra_environ=dict(username='test-user'))
+        assert 'Add a Project' not in r
+        r = self.app.get('/u/', extra_environ=dict(username='root'))
+        assert 'Add a Project' in r
+        r = self.app.get('/adobe/', extra_environ=dict(username='root'))
+        assert 'Add a Project' in r
+
+    def test_help(self):
+        r = self.app.get('/p/_admin/help/',
+                         extra_environ=dict(username='root'))
+        assert 'macro' in r
+
+    @td.with_user_project('test-user')
+    def test_profile_tools(self):
+        r = self.app.get('/u/test-user/',
+                         extra_environ=dict(username='test-user')).follow()
+        assert r.html.find('div', 'profile-section tools').find(
+            'a', href='/u/test-user/profile/'), r.html
+
+    def test_user_project_creates_on_demand(self):
+        M.User.register(dict(username='donald-duck'), make_project=False)
+        ThreadLocalORMSession.flush_all()
+        self.app.get('/u/donald-duck/')
+
+    def test_disabled_user_has_no_user_project(self):
+        M.User.register(dict(username='donald-duck'))
+        self.app.get('/u/donald-duck/')  # assert it's there
+        M.User.query.update(dict(username='donald-duck'),
+                            {'$set': {'disabled': True}})
+        self.app.get('/u/donald-duck/', status=404)
+
+    def test_more_projects_link(self):
+        r = self.app.get('/adobe/adobe-1/admin/')
+        link = r.html.find(
+            'div', {'class': 'neighborhood_title_link'}).find('a')
+        assert 'View More Projects' in str(link)
+        assert link['href'] == '/adobe/'
+
+
+class TestPhoneVerificationOnProjectRegistration(TestController):
+
+    def test_phone_verification_fragment_renders(self):
+        self.app.get('/p/phone_verification_fragment', status=200)
+        self.app.get('/adobe/phone_verification_fragment', status=200)
+
+    def test_verify_phone_no_params(self):
+        with h.push_config(config, **{'project.verify_phone': 'true'}):
+            self.app.get('/p/verify_phone', status=404)
+
+    def test_verify_phone_error(self):
+        with h.push_config(config, **{'project.verify_phone': 'true'}):
+            r = self.app.get('/p/verify_phone', {'number': '1234567890'})
+            expected = {'status': 'error',
+                        'error': 'Phone service is not configured'}
+            assert_equal(r.json, expected)
+            rid = r.session.get('phone_verification.request_id')
+            hash = r.session.get('phone_verification.number_hash')
+            assert_equal(rid, None)
+            assert_equal(hash, None)
+
+    @patch.object(g, 'phone_service', autospec=True)
+    def test_verify_phone(self, phone_service):
+        with h.push_config(config, **{'project.verify_phone': 'true'}):
+            phone_service.verify.return_value = {
+                'request_id': 'request-id', 'status': 'ok'}
+            r = self.app.get('/p/verify_phone', {'number': '1234567890'})
+            phone_service.verify.assert_called_once_with('1234567890')
+            assert_equal(r.json, {'status': 'ok'})
+            rid = r.session.get('phone_verification.request_id')
+            hash = r.session.get('phone_verification.number_hash')
+            assert_equal(rid, 'request-id')
+            assert_equal(hash, '01b307acba4f54f55aafc33bb06bbbf6ca803e9a')
+
+    def test_check_phone_verification_no_params(self):
+        with h.push_config(config, **{'project.verify_phone': 'true'}):
+            self.app.get('/p/check_phone_verification', status=404)
+
+    @patch.object(g, 'phone_service', autospec=True)
+    def test_check_phone_verification_error(self, phone_service):
+        with h.push_config(config, **{'project.verify_phone': 'true'}):
+            phone_service.check.return_value = {'status': 'error'}
+            req_id = 'request-id'
+
+            # make request to verify first to initialize session
+            phone_service.verify.return_value = {
+                'request_id': req_id, 'status': 'ok'}
+            r = self.app.get('/p/verify_phone', {'number': '1234567890'})
+
+            r = self.app.get('/p/check_phone_verification', {'pin': '1234'})
+            assert_equal(r.json, {'status': 'error'})
+            phone_service.check.assert_called_once_with(req_id, '1234')
+
+            user = M.User.by_username('test-admin')
+            hash = user.get_tool_data('phone_verification', 'number_hash')
+            assert_equal(hash, None)
+
+    @patch.object(g, 'phone_service', autospec=True)
+    def test_check_phone_verification_ok(self, phone_service):
+        with h.push_config(config, **{'project.verify_phone': 'true'}):
+            phone_service.check.return_value = {'status': 'ok'}
+            req_id = 'request-id'
+
+            # make request to verify first to initialize session
+            phone_service.verify.return_value = {
+                'request_id': req_id, 'status': 'ok'}
+            r = self.app.get('/p/verify_phone', {'number': '1234567890'})
+
+            r = self.app.get('/p/check_phone_verification', {'pin': '1234'})
+            assert_equal(r.json, {'status': 'ok'})
+            phone_service.check.assert_called_once_with(req_id, '1234')
+
+            user = M.User.by_username('test-admin')
+            hash = user.get_tool_data('phone_verification', 'number_hash')
+            assert_equal(hash, '01b307acba4f54f55aafc33bb06bbbf6ca803e9a')
+
+    def test_register_phone_not_verified(self):
+        with h.push_config(config, **{'project.verify_phone': 'true'}):
+            r = self.app.post(
+                '/p/register',
+                params=dict(
+                    project_unixname='phonetest',
+                    project_name='Phone Test',
+                    project_description='',
+                    neighborhood='Projects'),
+                extra_environ=dict(username='test-user'),
+                antispam=True)
+            overlay = r.html.find('div', {'id': 'phone_verification_overlay'})
+            assert_not_equal(overlay, None)
+            header = overlay.find('h2')
+            iframe = overlay.find('iframe')
+            assert_equal(header.getText(), 'Phone Verification Required')
+            assert_equal(iframe.get('src'), '/p/phone_verification_fragment')