Repository: inducer/relate

Issue:
Pretend facilities server errors for anonymous users
``` 
Internal Server Error: /time/set-pretend-facilities/

TypeError at /time/set-pretend-facilities/
'AnonymousUser' object is not iterable

Traceback:  

File "/home/andreas/my-relate-env3/lib/python3.5/site-packages/django/core/handlers/exception.py" in inner
  42.             response = get_response(request)

File "/home/andreas/my-relate-env3/lib/python3.5/site-packages/django/core/handlers/base.py" in _get_response
  187.                 response = self.process_exception_by_middleware(e, request)

File "/home/andreas/my-relate-env3/lib/python3.5/site-packages/django/core/handlers/base.py" in _get_response
  185.                 response = wrapped_callback(request, *callback_args, **callback_kwargs)

File "./course/views.py" in set_pretend_facilities
  521.             may_set_pretend_facility(request.user) or (

File "./course/views.py" in may_set_pretend_facility
  513.             roles__permissions__permission=pperm.set_pretend_facility

File "/home/andreas/my-relate-env3/lib/python3.5/site-packages/django/db/models/manager.py" in manager_method
  85.                 return getattr(self.get_queryset(), name)(*args, **kwargs)

File "/home/andreas/my-relate-env3/lib/python3.5/site-packages/django/db/models/query.py" in filter
  796.         return self._filter_or_exclude(False, *args, **kwargs)

File "/home/andreas/my-relate-env3/lib/python3.5/site-packages/django/db/models/query.py" in _filter_or_exclude
  814.             clone.query.add_q(Q(*args, **kwargs))

File "/home/andreas/my-relate-env3/lib/python3.5/site-packages/django/db/models/sql/query.py" in add_q
  1227.         clause, _ = self._add_q(q_object, self.used_aliases)

File "/home/andreas/my-relate-env3/lib/python3.5/site-packages/django/db/models/sql/query.py" in _add_q
  1253.                     allow_joins=allow_joins, split_subq=split_subq,

File "/home/andreas/my-relate-env3/lib/python3.5/site-packages/django/db/models/sql/query.py" in build_filter
  1158.             self.check_related_objects(field, value, opts)

File "/home/andreas/my-relate-env3/lib/python3.5/site-packages/django/db/models/sql/query.py" in check_related_objects
  1057.                 for v in value:

File "/home/andreas/my-relate-env3/lib/python3.5/site-packages/django/utils/functional.py" in inner
  235.         return func(self._wrapped, *args)

Exception Type: TypeError at /time/set-pretend-facilities/
Exception Value: 'AnonymousUser' object is not iterable
Request information:
USER: AnonymousUser
```


Discussion:
P.S., fake time server errors for anonymous users

Resolving patch:
diff --git a/course/auth.py b/course/auth.py
--- a/course/auth.py
+++ b/course/auth.py
@@ -266,6 +266,9 @@ def __init__(self, *args, **kwargs):
 
 
 def stop_impersonating(request):
+    if not request.user.is_authenticated:
+        raise PermissionDenied()
+
     if not hasattr(request, "relate_impersonate_original_user"):
         messages.add_message(request, messages.ERROR,
                 _("Not currently impersonating anyone."))
diff --git a/course/views.py b/course/views.py
--- a/course/views.py
+++ b/course/views.py
@@ -421,6 +421,7 @@ def may_set_fake_time(user):
             ).count() > 0
 
 
+@login_required
 def set_fake_time(request):
     # allow staff to set fake time when impersonating
     pre_imp_user = get_pre_impersonation_user(request)
@@ -514,6 +515,7 @@ def may_set_pretend_facility(user):
             ).count() > 0
 
 
+@login_required
 def set_pretend_facilities(request):
     # allow staff to set fake time when impersonating
     pre_imp_user = get_pre_impersonation_user(request)
@@ -1303,6 +1305,7 @@ def generate_ssh_keypair(request):
 
 # {{{ celery task monitoring
 
+@login_required
 def monitor_task(request, task_id):
     from celery.result import AsyncResult
     async_res = AsyncResult(task_id)


Test patch:
diff --git a/tests/base_test_mixins.py b/tests/base_test_mixins.py
--- a/tests/base_test_mixins.py
+++ b/tests/base_test_mixins.py
@@ -24,6 +24,7 @@
 
 import six
 import os
+import datetime
 from django.conf import settings
 from django.test import Client, override_settings
 from django.urls import reverse, resolve
@@ -222,6 +223,59 @@ def create_superuser(cls):
         return get_user_model().objects.create_superuser(
                                                 **cls.create_superuser_kwargs)
 
+    def get_fake_time_url(self):
+        return reverse("relate-set_fake_time")
+
+    # todo: impersonate test
+
+    def get_set_fake_time(self):
+        return self.c.get(self.get_fake_time_url())
+
+    def post_set_fake_time(self, data, follow=True):
+        return self.c.post(self.get_fake_time_url(), data, follow=follow)
+
+    def assertSessionFakeTimeEqual(self, session, expected_date_time):  # noqa
+        fake_time_timestamp = session.get("relate_fake_time", None)
+        if fake_time_timestamp is None:
+            faked_time = None
+            if expected_date_time is not None:
+                raise AssertionError(
+                    "the session doesn't have 'relate_fake_time' attribute")
+        else:
+            faked_time = datetime.datetime.fromtimestamp(fake_time_timestamp)
+        self.assertEqual(faked_time, expected_date_time)
+
+    def assertSessionFakeTimeIsNone(self, session):  # noqa
+        self.assertSessionFakeTimeEqual(session, None)
+
+    def get_set_pretend_facilities_url(self):
+        return reverse("relate-set_pretend_facilities")
+
+    def get_set_pretend_facilities(self):
+        return self.c.get(self.get_set_pretend_facilities_url())
+
+    def post_set_pretend_facilities(self, data, follow=True):
+        return self.c.post(self.get_set_pretend_facilities_url(), data,
+                           follow=follow)
+
+    def assertSessionPretendFacilitiesContains(self, session, expected_facilities):  # noqa
+        pretended = session.get("relate_pretend_facilities", None)
+        if expected_facilities is None:
+            return self.assertIsNone(pretended)
+        if pretended is None:
+            raise AssertionError(
+                "the session doesn't have "
+                "'relate_pretend_facilities' attribute")
+
+        if isinstance(expected_facilities, (list, tuple)):
+            self.assertTrue(set(expected_facilities).issubset(set(pretended)))
+        else:
+            self.assertTrue(expected_facilities in pretended)
+
+    def assertSessionPretendFacilitiesIsNone(self, session):  # noqa
+        pretended = session.get("relate_pretend_facilities", None)
+        self.assertIsNone(pretended)
+
 
 class CoursesTestMixinBase(SuperuserCreateMixin):
 
diff --git a/tests/test_views.py b/tests/test_views.py
new file mode 100644
--- /dev/null
+++ b/tests/test_views.py
@@ -0,0 +1,131 @@
+from __future__ import division
+
+__copyright__ = "Copyright (C) 2017 Dong Zhuang"
+
+__license__ = """
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+"""
+
+from django.test import TestCase
+from django.test.utils import override_settings
+import datetime
+
+from .base_test_mixins import (
+    SingleCourseTestMixin,
+)
+
+DATE_TIME_PICKER_TIME_FORMAT = "%Y-%m-%d %H:%M"
+
+RELATE_FACILITIES = {
+    # intentionally to be different from local_settings.example.py
+    "test_center1": {
+        "ip_ranges": [
+            "192.168.100.0/24",
+            ],
+        "exams_only": False,
+    },
+}
+
+
+class TestSetFakeTime(SingleCourseTestMixin, TestCase):
+    fake_time = datetime.datetime(2038, 12, 31, 0, 0, 0, 0)
+    set_fake_time_data = {"time": fake_time.strftime(DATE_TIME_PICKER_TIME_FORMAT),
+                          "set": ['']}
+    unset_fake_time_data = {"time": set_fake_time_data["time"], "unset": ['']}
+
+    def test_set_fake_time_by_anonymous(self):
+        with self.temporarily_switch_to_user(None):
+            resp = self.get_set_fake_time()
+            self.assertEqual(resp.status_code, 302)
+
+            resp = self.post_set_fake_time(self.set_fake_time_data, follow=False)
+            self.assertEqual(resp.status_code, 302)
+            self.assertSessionFakeTimeIsNone(self.c.session)
+
+    def test_set_fake_time_no_pperm(self):
+        with self.temporarily_switch_to_user(self.student_participation.user):
+            resp = self.get_set_fake_time()
+            self.assertEqual(resp.status_code, 403)
+
+            resp = self.post_set_fake_time(self.set_fake_time_data)
+            self.assertEqual(resp.status_code, 403)
+            self.assertSessionFakeTimeIsNone(self.c.session)
+
+    def test_set_fake_time_by_instructor(self):
+        with self.temporarily_switch_to_user(self.instructor_participation.user):
+            resp = self.get_set_fake_time()
+            self.assertEqual(resp.status_code, 200)
+
+            # set fake time
+            resp = self.post_set_fake_time(self.set_fake_time_data)
+            self.assertEqual(resp.status_code, 200)
+            self.assertSessionFakeTimeEqual(self.c.session, self.fake_time)
+
+            # unset fake time
+            resp = self.post_set_fake_time(self.unset_fake_time_data)
+            self.assertEqual(resp.status_code, 200)
+            self.assertSessionFakeTimeIsNone(self.c.session)
+
+
+@override_settings(RELATE_FACILITIES=RELATE_FACILITIES)
+class TestSetPretendFacilities(SingleCourseTestMixin, TestCase):
+    set_pretend_facilities_data = {
+        "facilities": ["test_center1"],
+        "custom_facilities": [],
+        "add_pretend_facilities_header": ["on"],
+        "set": ['']}
+    unset_pretend_facilities_data = set_pretend_facilities_data.copy()
+    unset_pretend_facilities_data.pop("set")
+    unset_pretend_facilities_data["unset"] = ['']
+
+    def test_pretend_facilities_by_anonymous(self):
+        with self.temporarily_switch_to_user(None):
+            resp = self.get_set_pretend_facilities()
+            self.assertEqual(resp.status_code, 302)
+
+            resp = self.post_set_pretend_facilities(
+                self.set_pretend_facilities_data, follow=False)
+            self.assertEqual(resp.status_code, 302)
+            self.assertSessionPretendFacilitiesIsNone(self.c.session)
+
+    def test_pretend_facilities_no_pperm(self):
+        with self.temporarily_switch_to_user(self.student_participation.user):
+            resp = self.get_set_pretend_facilities()
+            self.assertEqual(resp.status_code, 403)
+
+            resp = self.post_set_pretend_facilities(
+                self.set_pretend_facilities_data)
+            self.assertEqual(resp.status_code, 403)
+            self.assertSessionPretendFacilitiesIsNone(self.c.session)
+
+    def test_pretend_facilities_by_instructor(self):
+        with self.temporarily_switch_to_user(self.instructor_participation.user):
+            resp = self.get_set_pretend_facilities()
+            self.assertEqual(resp.status_code, 200)
+
+            resp = self.post_set_pretend_facilities(
+                self.set_pretend_facilities_data)
+            self.assertEqual(resp.status_code, 200)
+            self.assertSessionPretendFacilitiesContains(self.c.session,
+                                                        "test_center1")
+
+            resp = self.post_set_pretend_facilities(
+                self.unset_pretend_facilities_data)
+            self.assertEqual(resp.status_code, 200)
+            self.assertSessionPretendFacilitiesIsNone(self.c.session)