Commit 9c791a95 by Jim Abramson

Merge pull request #2706 from edx/jsa/forum-delete-comments-acceptance

Add acceptance tests for forums comment deletion.
parents 9ffcbb88 bea463d9
from django.test import TestCase from django.test import TestCase
from django.test.client import Client from django.test.client import Client
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django_comment_common.models import (
Role, FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_STUDENT)
from django_comment_common.utils import seed_permissions_roles
from student.models import CourseEnrollment, UserProfile from student.models import CourseEnrollment, UserProfile
from util.testing import UrlResetMixin from util.testing import UrlResetMixin
from mock import patch from mock import patch
from django.core.urlresolvers import reverse, NoReverseMatch
class AutoAuthEnabledTestCase(UrlResetMixin, TestCase): class AutoAuthEnabledTestCase(UrlResetMixin, TestCase):
...@@ -103,6 +105,39 @@ class AutoAuthEnabledTestCase(UrlResetMixin, TestCase): ...@@ -103,6 +105,39 @@ class AutoAuthEnabledTestCase(UrlResetMixin, TestCase):
enrollment = CourseEnrollment.objects.get(course_id=course_id) enrollment = CourseEnrollment.objects.get(course_id=course_id)
self.assertEqual(enrollment.user.username, "test") self.assertEqual(enrollment.user.username, "test")
def test_set_roles(self):
course_id = "edX/Test101/2014_Spring"
seed_permissions_roles(course_id)
course_roles = dict((r.name, r) for r in Role.objects.filter(course_id=course_id))
self.assertEqual(len(course_roles), 4) # sanity check
# Student role is assigned by default on course enrollment.
self._auto_auth(username='a_student', course_id=course_id)
user = User.objects.get(username='a_student')
user_roles = user.roles.all()
self.assertEqual(len(user_roles), 1)
self.assertEqual(user_roles[0], course_roles[FORUM_ROLE_STUDENT])
self._auto_auth(username='a_moderator', course_id=course_id, roles='Moderator')
user = User.objects.get(username='a_moderator')
user_roles = user.roles.all()
self.assertEqual(
set(user_roles),
set([course_roles[FORUM_ROLE_STUDENT],
course_roles[FORUM_ROLE_MODERATOR]]))
# check multiple roles work.
self._auto_auth(username='an_admin', course_id=course_id,
roles='{},{}'.format(FORUM_ROLE_MODERATOR, FORUM_ROLE_ADMINISTRATOR))
user = User.objects.get(username='an_admin')
user_roles = user.roles.all()
self.assertEqual(
set(user_roles),
set([course_roles[FORUM_ROLE_STUDENT],
course_roles[FORUM_ROLE_MODERATOR],
course_roles[FORUM_ROLE_ADMINISTRATOR]]))
def _auto_auth(self, **params): def _auto_auth(self, **params):
""" """
Make a request to the auto-auth end-point and check Make a request to the auto-auth end-point and check
...@@ -113,8 +148,8 @@ class AutoAuthEnabledTestCase(UrlResetMixin, TestCase): ...@@ -113,8 +148,8 @@ class AutoAuthEnabledTestCase(UrlResetMixin, TestCase):
# Check that session and CSRF are set in the response # Check that session and CSRF are set in the response
for cookie in ['csrftoken', 'sessionid']: for cookie in ['csrftoken', 'sessionid']:
self.assertIn(cookie, response.cookies) #pylint: disable=E1103 self.assertIn(cookie, response.cookies) # pylint: disable=E1103
self.assertTrue(response.cookies[cookie].value) #pylint: disable=E1103 self.assertTrue(response.cookies[cookie].value) # pylint: disable=E1103
class AutoAuthDisabledTestCase(UrlResetMixin, TestCase): class AutoAuthDisabledTestCase(UrlResetMixin, TestCase):
......
...@@ -57,6 +57,8 @@ from collections import namedtuple ...@@ -57,6 +57,8 @@ from collections import namedtuple
from courseware.courses import get_courses, sort_by_announcement from courseware.courses import get_courses, sort_by_announcement
from courseware.access import has_access from courseware.access import has_access
from django_comment_common.models import Role
from external_auth.models import ExternalAuthMap from external_auth.models import ExternalAuthMap
import external_auth.views import external_auth.views
...@@ -1224,6 +1226,7 @@ def auto_auth(request): ...@@ -1224,6 +1226,7 @@ def auto_auth(request):
* `full_name` for the user profile (the user's full name; defaults to the username) * `full_name` for the user profile (the user's full name; defaults to the username)
* `staff`: Set to "true" to make the user global staff. * `staff`: Set to "true" to make the user global staff.
* `course_id`: Enroll the student in the course with `course_id` * `course_id`: Enroll the student in the course with `course_id`
* `roles`: Comma-separated list of roles to grant the student in the course with `course_id`
If username, email, or password are not provided, use If username, email, or password are not provided, use
randomly generated credentials. randomly generated credentials.
...@@ -1239,6 +1242,7 @@ def auto_auth(request): ...@@ -1239,6 +1242,7 @@ def auto_auth(request):
full_name = request.GET.get('full_name', username) full_name = request.GET.get('full_name', username)
is_staff = request.GET.get('staff', None) is_staff = request.GET.get('staff', None)
course_id = request.GET.get('course_id', None) course_id = request.GET.get('course_id', None)
role_names = [v.strip() for v in request.GET.get('roles', '').split(',') if v.strip()]
# Get or create the user object # Get or create the user object
post_data = { post_data = {
...@@ -1281,14 +1285,19 @@ def auto_auth(request): ...@@ -1281,14 +1285,19 @@ def auto_auth(request):
if course_id is not None: if course_id is not None:
CourseEnrollment.enroll(user, course_id) CourseEnrollment.enroll(user, course_id)
# Apply the roles
for role_name in role_names:
role = Role.objects.get(name=role_name, course_id=course_id)
user.roles.add(role)
# Log in as the user # Log in as the user
user = authenticate(username=username, password=password) user = authenticate(username=username, password=password)
login(request, user) login(request, user)
# Provide the user with a valid CSRF token # Provide the user with a valid CSRF token
# then return a 200 response # then return a 200 response
success_msg = u"Logged in user {0} ({1}) with password {2}".format( success_msg = u"Logged in user {0} ({1}) with password {2} and user_id {3}".format(
username, email, password username, email, password, user.id
) )
response = HttpResponse(success_msg) response = HttpResponse(success_msg)
response.set_cookie('csrftoken', csrf(request)['csrf_token']) response.set_cookie('csrftoken', csrf(request)['csrf_token'])
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
Stub implementation of cs_comments_service for acceptance tests Stub implementation of cs_comments_service for acceptance tests
""" """
from datetime import datetime
import re import re
import urlparse import urlparse
from .http import StubHttpRequestHandler, StubHttpService from .http import StubHttpRequestHandler, StubHttpService
...@@ -14,6 +13,7 @@ class StubCommentsServiceHandler(StubHttpRequestHandler): ...@@ -14,6 +13,7 @@ class StubCommentsServiceHandler(StubHttpRequestHandler):
"/api/v1/users/(?P<user_id>\\d+)$": self.do_user, "/api/v1/users/(?P<user_id>\\d+)$": self.do_user,
"/api/v1/threads$": self.do_threads, "/api/v1/threads$": self.do_threads,
"/api/v1/threads/(?P<thread_id>\\w+)$": self.do_thread, "/api/v1/threads/(?P<thread_id>\\w+)$": self.do_thread,
"/api/v1/comments/(?P<comment_id>\\w+)$": self.do_comment,
} }
path = urlparse.urlparse(self.path).path path = urlparse.urlparse(self.path).path
for pattern in pattern_handlers: for pattern in pattern_handlers:
...@@ -25,8 +25,13 @@ class StubCommentsServiceHandler(StubHttpRequestHandler): ...@@ -25,8 +25,13 @@ class StubCommentsServiceHandler(StubHttpRequestHandler):
self.send_response(404, content="404 Not Found") self.send_response(404, content="404 Not Found")
def do_PUT(self): def do_PUT(self):
if self.path.startswith('/set_config'):
return StubHttpRequestHandler.do_PUT(self)
self.send_response(204, "") self.send_response(204, "")
def do_DELETE(self):
self.send_json_response({})
def do_user(self, user_id): def do_user(self, user_id):
self.send_json_response({ self.send_json_response({
"id": user_id, "id": user_id,
...@@ -36,44 +41,28 @@ class StubCommentsServiceHandler(StubHttpRequestHandler): ...@@ -36,44 +41,28 @@ class StubCommentsServiceHandler(StubHttpRequestHandler):
}) })
def do_thread(self, thread_id): def do_thread(self, thread_id):
match = re.search("(?P<num>\\d+)_responses", thread_id) if thread_id in self.server.config.get('threads', {}):
resp_total = int(match.group("num")) if match else 0 thread = self.server.config['threads'][thread_id].copy()
thread = { params = urlparse.parse_qs(urlparse.urlparse(self.path).query)
"id": thread_id, if "recursive" in params and params["recursive"][0] == "True":
"commentable_id": "dummy", thread.setdefault('children', [])
"type": "thread", resp_total = thread.setdefault('resp_total', len(thread['children']))
"title": "Thread title", resp_skip = int(params.get("resp_skip", ["0"])[0])
"body": "Thread body", resp_limit = int(params.get("resp_limit", ["10000"])[0])
"created_at": datetime.utcnow().isoformat(), thread['children'] = thread['children'][resp_skip:(resp_skip + resp_limit)]
"unread_comments_count": 0, self.send_json_response(thread)
"comments_count": resp_total, else:
"votes": {"up_count": 0}, self.send_response(404, content="404 Not Found")
"abuse_flaggers": [],
"closed": "closed" in thread_id,
}
params = urlparse.parse_qs(urlparse.urlparse(self.path).query)
if "recursive" in params and params["recursive"][0] == "True":
thread["resp_total"] = resp_total
thread["children"] = []
resp_skip = int(params.get("resp_skip", ["0"])[0])
resp_limit = int(params.get("resp_limit", ["10000"])[0])
num_responses = min(resp_limit, resp_total - resp_skip)
self.log_message("Generating {} children; resp_limit={} resp_total={} resp_skip={}".format(num_responses, resp_limit, resp_total, resp_skip))
for i in range(num_responses):
response_id = str(resp_skip + i)
thread["children"].append({
"id": str(response_id),
"type": "comment",
"body": response_id,
"created_at": datetime.utcnow().isoformat(),
"votes": {"up_count": 0},
"abuse_flaggers": [],
})
self.send_json_response(thread)
def do_threads(self): def do_threads(self):
self.send_json_response({"collection": [], "page": 1, "num_pages": 1}) self.send_json_response({"collection": [], "page": 1, "num_pages": 1})
def do_comment(self, comment_id):
# django_comment_client calls GET comment before doing a DELETE, so that's what this is here to support.
if comment_id in self.server.config.get('comments', {}):
comment = self.server.config['comments'][comment_id]
self.send_json_response(comment)
class StubCommentsService(StubHttpService): class StubCommentsService(StubHttpService):
HANDLER_CLASS = StubCommentsServiceHandler HANDLER_CLASS = StubCommentsServiceHandler
...@@ -8,3 +8,6 @@ XQUEUE_STUB_URL = os.environ.get('xqueue_url', 'http://localhost:8040') ...@@ -8,3 +8,6 @@ XQUEUE_STUB_URL = os.environ.get('xqueue_url', 'http://localhost:8040')
# Get the URL of the Ora stub used in the test # Get the URL of the Ora stub used in the test
ORA_STUB_URL = os.environ.get('ora_url', 'http://localhost:8041') ORA_STUB_URL = os.environ.get('ora_url', 'http://localhost:8041')
# Get the URL of the comments service stub used in the test
COMMENTS_STUB_URL = os.environ.get('comments_url', 'http://localhost:4567')
"""
Tools for creating discussion content fixture data.
"""
from datetime import datetime
import json
import factory
import requests
from . import COMMENTS_STUB_URL
class ContentFactory(factory.Factory):
FACTORY_FOR = dict
id = None
user_id = "dummy-user-id"
username = "dummy-username"
course_id = "dummy-course-id"
commentable_id = "dummy-commentable-id"
anonymous = False
anonymous_to_peers = False
at_position_list = []
abuse_flaggers = []
created_at = datetime.utcnow().isoformat()
updated_at = datetime.utcnow().isoformat()
endorsed = False
closed = False
votes = {"up_count": 0}
class Thread(ContentFactory):
comments_count = 0
unread_comments_count = 0
title = "dummy thread title"
body = "dummy thread body"
type = "thread"
group_id = None
pinned = False
read = False
class Comment(ContentFactory):
thread_id = None
depth = 0
type = "comment"
body = "dummy comment body"
class Response(Comment):
depth = 1
body = "dummy response body"
class SingleThreadViewFixture(object):
def __init__(self, thread):
self.thread = thread
def addResponse(self, response, comments=[]):
response['children'] = comments
self.thread.setdefault('children', []).append(response)
self.thread['comments_count'] += len(comments) + 1
def _get_comment_map(self):
"""
Generate a dict mapping each response/comment in the thread
by its `id`.
"""
def _visit(obj):
res = []
for child in obj.get('children', []):
res.append((child['id'], child))
if 'children' in child:
res += _visit(child)
return res
return dict(_visit(self.thread))
def push(self):
"""
Push the data to the stub comments service.
"""
requests.put(
'{}/set_config'.format(COMMENTS_STUB_URL),
data={
"threads": json.dumps({self.thread['id']: self.thread}),
"comments": json.dumps(self._get_comment_map())
}
)
...@@ -53,10 +53,7 @@ class DiscussionSingleThreadPage(CoursePage): ...@@ -53,10 +53,7 @@ class DiscussionSingleThreadPage(CoursePage):
def has_add_response_button(self): def has_add_response_button(self):
"""Returns true if the add response button is visible, false otherwise""" """Returns true if the add response button is visible, false otherwise"""
return ( return self._is_element_visible(".add-response-btn")
self.is_css_present(".add-response-btn") and
self.css_map(".add-response-btn", lambda el: el.visible)[0]
)
def click_add_response_button(self): def click_add_response_button(self):
""" """
...@@ -68,3 +65,25 @@ class DiscussionSingleThreadPage(CoursePage): ...@@ -68,3 +65,25 @@ class DiscussionSingleThreadPage(CoursePage):
lambda: self.is_css_present("#wmd-input-reply-body-{thread_id}:focus".format(thread_id=self.thread_id)), lambda: self.is_css_present("#wmd-input-reply-body-{thread_id}:focus".format(thread_id=self.thread_id)),
"Response field received focus" "Response field received focus"
)) ))
def _is_element_visible(self, selector):
return (
self.is_css_present(selector) and
self.css_map(selector, lambda el: el.visible)[0]
)
def is_comment_visible(self, comment_id):
"""Returns true if the comment is viewable onscreen"""
return self._is_element_visible("#comment_{}".format(comment_id))
def is_comment_deletable(self, comment_id):
"""Returns true if the delete comment button is present, false otherwise"""
return self._is_element_visible("#comment_{} div.action-delete".format(comment_id))
def delete_comment(self, comment_id):
with self.handle_alert():
self.css_click("#comment_{} div.action-delete".format(comment_id))
fulfill(EmptyPromise(
lambda: not self.is_comment_visible(comment_id),
"Deleted comment was removed"
))
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
Auto-auth page (used to automatically log in during testing). Auto-auth page (used to automatically log in during testing).
""" """
import re
import urllib import urllib
from bok_choy.page_object import PageObject from bok_choy.page_object import PageObject
from . import BASE_URL from . import BASE_URL
...@@ -14,7 +15,7 @@ class AutoAuthPage(PageObject): ...@@ -14,7 +15,7 @@ class AutoAuthPage(PageObject):
this url will create a user and log them in. this url will create a user and log them in.
""" """
def __init__(self, browser, username=None, email=None, password=None, staff=None, course_id=None): def __init__(self, browser, username=None, email=None, password=None, staff=None, course_id=None, roles=None):
""" """
Auto-auth is an end-point for HTTP GET requests. Auto-auth is an end-point for HTTP GET requests.
By default, it will create accounts with random user credentials, By default, it will create accounts with random user credentials,
...@@ -47,6 +48,9 @@ class AutoAuthPage(PageObject): ...@@ -47,6 +48,9 @@ class AutoAuthPage(PageObject):
if course_id is not None: if course_id is not None:
self._params['course_id'] = course_id self._params['course_id'] = course_id
if roles is not None:
self._params['roles'] = roles
@property @property
def url(self): def url(self):
""" """
...@@ -62,3 +66,8 @@ class AutoAuthPage(PageObject): ...@@ -62,3 +66,8 @@ class AutoAuthPage(PageObject):
def is_browser_on_page(self): def is_browser_on_page(self):
return True return True
def get_user_id(self):
message = self.css_text('BODY')[0].strip()
match = re.search(r' user_id ([^$]+)$', message)
return match.groups()[0] if match else None
...@@ -6,6 +6,7 @@ from .helpers import UniqueCourseTest ...@@ -6,6 +6,7 @@ from .helpers import UniqueCourseTest
from ..pages.studio.auto_auth import AutoAuthPage from ..pages.studio.auto_auth import AutoAuthPage
from ..pages.lms.discussion_single_thread import DiscussionSingleThreadPage from ..pages.lms.discussion_single_thread import DiscussionSingleThreadPage
from ..fixtures.course import CourseFixture from ..fixtures.course import CourseFixture
from ..fixtures.discussion import SingleThreadViewFixture, Thread, Response, Comment
class DiscussionSingleThreadTest(UniqueCourseTest): class DiscussionSingleThreadTest(UniqueCourseTest):
...@@ -19,9 +20,16 @@ class DiscussionSingleThreadTest(UniqueCourseTest): ...@@ -19,9 +20,16 @@ class DiscussionSingleThreadTest(UniqueCourseTest):
# Create a course to register for # Create a course to register for
CourseFixture(**self.course_info).install() CourseFixture(**self.course_info).install()
AutoAuthPage(self.browser, course_id=self.course_id).visit() self.user_id = AutoAuthPage(self.browser, course_id=self.course_id).visit().get_user_id()
def setup_thread(self, thread, num_responses):
view = SingleThreadViewFixture(thread=thread)
for i in range(num_responses):
view.addResponse(Response(id=str(i), body=str(i)))
view.push()
def test_no_responses(self): def test_no_responses(self):
self.setup_thread(Thread(id="0_responses"), 0)
page = DiscussionSingleThreadPage(self.browser, self.course_id, "0_responses") page = DiscussionSingleThreadPage(self.browser, self.course_id, "0_responses")
page.visit() page.visit()
self.assertEqual(page.get_response_total_text(), "0 responses") self.assertEqual(page.get_response_total_text(), "0 responses")
...@@ -31,6 +39,7 @@ class DiscussionSingleThreadTest(UniqueCourseTest): ...@@ -31,6 +39,7 @@ class DiscussionSingleThreadTest(UniqueCourseTest):
self.assertIsNone(page.get_load_responses_button_text()) self.assertIsNone(page.get_load_responses_button_text())
def test_few_responses(self): def test_few_responses(self):
self.setup_thread(Thread(id="5_responses"), 5)
page = DiscussionSingleThreadPage(self.browser, self.course_id, "5_responses") page = DiscussionSingleThreadPage(self.browser, self.course_id, "5_responses")
page.visit() page.visit()
self.assertEqual(page.get_response_total_text(), "5 responses") self.assertEqual(page.get_response_total_text(), "5 responses")
...@@ -39,6 +48,7 @@ class DiscussionSingleThreadTest(UniqueCourseTest): ...@@ -39,6 +48,7 @@ class DiscussionSingleThreadTest(UniqueCourseTest):
self.assertIsNone(page.get_load_responses_button_text()) self.assertIsNone(page.get_load_responses_button_text())
def test_two_response_pages(self): def test_two_response_pages(self):
self.setup_thread(Thread(id="50_responses"), 50)
page = DiscussionSingleThreadPage(self.browser, self.course_id, "50_responses") page = DiscussionSingleThreadPage(self.browser, self.course_id, "50_responses")
page.visit() page.visit()
self.assertEqual(page.get_response_total_text(), "50 responses") self.assertEqual(page.get_response_total_text(), "50 responses")
...@@ -52,6 +62,7 @@ class DiscussionSingleThreadTest(UniqueCourseTest): ...@@ -52,6 +62,7 @@ class DiscussionSingleThreadTest(UniqueCourseTest):
self.assertEqual(page.get_load_responses_button_text(), None) self.assertEqual(page.get_load_responses_button_text(), None)
def test_three_response_pages(self): def test_three_response_pages(self):
self.setup_thread(Thread(id="150_responses"), 150)
page = DiscussionSingleThreadPage(self.browser, self.course_id, "150_responses") page = DiscussionSingleThreadPage(self.browser, self.course_id, "150_responses")
page.visit() page.visit()
self.assertEqual(page.get_response_total_text(), "150 responses") self.assertEqual(page.get_response_total_text(), "150 responses")
...@@ -70,12 +81,57 @@ class DiscussionSingleThreadTest(UniqueCourseTest): ...@@ -70,12 +81,57 @@ class DiscussionSingleThreadTest(UniqueCourseTest):
self.assertEqual(page.get_load_responses_button_text(), None) self.assertEqual(page.get_load_responses_button_text(), None)
def test_add_response_button(self): def test_add_response_button(self):
self.setup_thread(Thread(id="5_responses"), 5)
page = DiscussionSingleThreadPage(self.browser, self.course_id, "5_responses") page = DiscussionSingleThreadPage(self.browser, self.course_id, "5_responses")
page.visit() page.visit()
self.assertTrue(page.has_add_response_button()) self.assertTrue(page.has_add_response_button())
page.click_add_response_button() page.click_add_response_button()
def test_add_response_button_closed_thread(self): def test_add_response_button_closed_thread(self):
self.setup_thread(Thread(id="5_responses_closed", closed=True), 5)
page = DiscussionSingleThreadPage(self.browser, self.course_id, "5_responses_closed") page = DiscussionSingleThreadPage(self.browser, self.course_id, "5_responses_closed")
page.visit() page.visit()
self.assertFalse(page.has_add_response_button()) self.assertFalse(page.has_add_response_button())
class DiscussionCommentDeletionTest(UniqueCourseTest):
"""
Tests for deleting comments displayed beneath responses in the single thread view.
"""
def setUp(self):
super(DiscussionCommentDeletionTest, self).setUp()
# Create a course to register for
CourseFixture(**self.course_info).install()
def setup_user(self, roles=[]):
roles_str = ','.join(roles)
self.user_id = AutoAuthPage(self.browser, course_id=self.course_id, roles=roles_str).visit().get_user_id()
def setup_view(self):
view = SingleThreadViewFixture(Thread(id="comment_deletion_test_thread"))
view.addResponse(
Response(id="response1"),
[Comment(id="comment_other_author", user_id="other"), Comment(id="comment_self_author", user_id=self.user_id)])
view.push()
def test_comment_deletion_as_student(self):
self.setup_user()
self.setup_view()
page = DiscussionSingleThreadPage(self.browser, self.course_id, "comment_deletion_test_thread")
page.visit()
self.assertTrue(page.is_comment_deletable("comment_self_author"))
self.assertTrue(page.is_comment_visible("comment_other_author"))
self.assertFalse(page.is_comment_deletable("comment_other_author"))
page.delete_comment("comment_self_author")
def test_comment_deletion_as_moderator(self):
self.setup_user(roles=['Moderator'])
self.setup_view()
page = DiscussionSingleThreadPage(self.browser, self.course_id, "comment_deletion_test_thread")
page.visit()
self.assertTrue(page.is_comment_deletable("comment_self_author"))
self.assertTrue(page.is_comment_deletable("comment_other_author"))
page.delete_comment("comment_self_author")
page.delete_comment("comment_other_author")
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment