Add acceptance tests for forums comment deletion.
JIRA: FOR-472
This commit is contained in:
@@ -57,6 +57,8 @@ from collections import namedtuple
|
||||
from courseware.courses import get_courses, sort_by_announcement
|
||||
from courseware.access import has_access
|
||||
|
||||
from django_comment_common.models import Role
|
||||
|
||||
from external_auth.models import ExternalAuthMap
|
||||
import external_auth.views
|
||||
|
||||
@@ -1211,6 +1213,7 @@ def auto_auth(request):
|
||||
* `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.
|
||||
* `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
|
||||
randomly generated credentials.
|
||||
@@ -1226,6 +1229,7 @@ def auto_auth(request):
|
||||
full_name = request.GET.get('full_name', username)
|
||||
is_staff = request.GET.get('staff', 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
|
||||
post_data = {
|
||||
@@ -1268,6 +1272,11 @@ def auto_auth(request):
|
||||
if course_id is not None:
|
||||
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
|
||||
user = authenticate(username=username, password=password)
|
||||
login(request, user)
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
Stub implementation of cs_comments_service for acceptance tests
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
import re
|
||||
import urlparse
|
||||
from .http import StubHttpRequestHandler, StubHttpService
|
||||
@@ -14,6 +13,7 @@ class StubCommentsServiceHandler(StubHttpRequestHandler):
|
||||
"/api/v1/users/(?P<user_id>\\d+)$": self.do_user,
|
||||
"/api/v1/threads$": self.do_threads,
|
||||
"/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
|
||||
for pattern in pattern_handlers:
|
||||
@@ -25,8 +25,13 @@ class StubCommentsServiceHandler(StubHttpRequestHandler):
|
||||
self.send_response(404, content="404 Not Found")
|
||||
|
||||
def do_PUT(self):
|
||||
if self.path.startswith('/set_config'):
|
||||
return StubHttpRequestHandler.do_PUT(self)
|
||||
self.send_response(204, "")
|
||||
|
||||
def do_DELETE(self):
|
||||
self.send_json_response({})
|
||||
|
||||
def do_user(self, user_id):
|
||||
self.send_json_response({
|
||||
"id": user_id,
|
||||
@@ -36,44 +41,28 @@ class StubCommentsServiceHandler(StubHttpRequestHandler):
|
||||
})
|
||||
|
||||
def do_thread(self, thread_id):
|
||||
match = re.search("(?P<num>\\d+)_responses", thread_id)
|
||||
resp_total = int(match.group("num")) if match else 0
|
||||
thread = {
|
||||
"id": thread_id,
|
||||
"commentable_id": "dummy",
|
||||
"type": "thread",
|
||||
"title": "Thread title",
|
||||
"body": "Thread body",
|
||||
"created_at": datetime.utcnow().isoformat(),
|
||||
"unread_comments_count": 0,
|
||||
"comments_count": resp_total,
|
||||
"votes": {"up_count": 0},
|
||||
"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)
|
||||
if thread_id in self.server.config.get('threads', {}):
|
||||
thread = self.server.config['threads'][thread_id].copy()
|
||||
params = urlparse.parse_qs(urlparse.urlparse(self.path).query)
|
||||
if "recursive" in params and params["recursive"][0] == "True":
|
||||
thread.setdefault('children', [])
|
||||
resp_total = thread.setdefault('resp_total', len(thread['children']))
|
||||
resp_skip = int(params.get("resp_skip", ["0"])[0])
|
||||
resp_limit = int(params.get("resp_limit", ["10000"])[0])
|
||||
thread['children'] = thread['children'][resp_skip:(resp_skip + resp_limit)]
|
||||
self.send_json_response(thread)
|
||||
else:
|
||||
self.send_response(404, content="404 Not Found")
|
||||
|
||||
def do_threads(self):
|
||||
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):
|
||||
HANDLER_CLASS = StubCommentsServiceHandler
|
||||
|
||||
@@ -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
|
||||
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')
|
||||
|
||||
89
common/test/acceptance/fixtures/discussion.py
Normal file
89
common/test/acceptance/fixtures/discussion.py
Normal file
@@ -0,0 +1,89 @@
|
||||
"""
|
||||
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):
|
||||
|
||||
def has_add_response_button(self):
|
||||
"""Returns true if the add response button is visible, false otherwise"""
|
||||
return (
|
||||
self.is_css_present(".add-response-btn") and
|
||||
self.css_map(".add-response-btn", lambda el: el.visible)[0]
|
||||
)
|
||||
return self._is_element_visible(".add-response-btn")
|
||||
|
||||
def click_add_response_button(self):
|
||||
"""
|
||||
@@ -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)),
|
||||
"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"
|
||||
))
|
||||
|
||||
@@ -6,6 +6,7 @@ from .helpers import UniqueCourseTest
|
||||
from ..pages.studio.auto_auth import AutoAuthPage
|
||||
from ..pages.lms.discussion_single_thread import DiscussionSingleThreadPage
|
||||
from ..fixtures.course import CourseFixture
|
||||
from ..fixtures.discussion import SingleThreadViewFixture, Thread, Response, Comment
|
||||
|
||||
|
||||
class DiscussionSingleThreadTest(UniqueCourseTest):
|
||||
@@ -19,9 +20,16 @@ class DiscussionSingleThreadTest(UniqueCourseTest):
|
||||
# Create a course to register for
|
||||
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):
|
||||
self.setup_thread(Thread(id="0_responses"), 0)
|
||||
page = DiscussionSingleThreadPage(self.browser, self.course_id, "0_responses")
|
||||
page.visit()
|
||||
self.assertEqual(page.get_response_total_text(), "0 responses")
|
||||
@@ -31,6 +39,7 @@ class DiscussionSingleThreadTest(UniqueCourseTest):
|
||||
self.assertIsNone(page.get_load_responses_button_text())
|
||||
|
||||
def test_few_responses(self):
|
||||
self.setup_thread(Thread(id="5_responses"), 5)
|
||||
page = DiscussionSingleThreadPage(self.browser, self.course_id, "5_responses")
|
||||
page.visit()
|
||||
self.assertEqual(page.get_response_total_text(), "5 responses")
|
||||
@@ -39,6 +48,7 @@ class DiscussionSingleThreadTest(UniqueCourseTest):
|
||||
self.assertIsNone(page.get_load_responses_button_text())
|
||||
|
||||
def test_two_response_pages(self):
|
||||
self.setup_thread(Thread(id="50_responses"), 50)
|
||||
page = DiscussionSingleThreadPage(self.browser, self.course_id, "50_responses")
|
||||
page.visit()
|
||||
self.assertEqual(page.get_response_total_text(), "50 responses")
|
||||
@@ -52,6 +62,7 @@ class DiscussionSingleThreadTest(UniqueCourseTest):
|
||||
self.assertEqual(page.get_load_responses_button_text(), None)
|
||||
|
||||
def test_three_response_pages(self):
|
||||
self.setup_thread(Thread(id="150_responses"), 150)
|
||||
page = DiscussionSingleThreadPage(self.browser, self.course_id, "150_responses")
|
||||
page.visit()
|
||||
self.assertEqual(page.get_response_total_text(), "150 responses")
|
||||
@@ -70,12 +81,57 @@ class DiscussionSingleThreadTest(UniqueCourseTest):
|
||||
self.assertEqual(page.get_load_responses_button_text(), None)
|
||||
|
||||
def test_add_response_button(self):
|
||||
self.setup_thread(Thread(id="5_responses"), 5)
|
||||
page = DiscussionSingleThreadPage(self.browser, self.course_id, "5_responses")
|
||||
page.visit()
|
||||
self.assertTrue(page.has_add_response_button())
|
||||
page.click_add_response_button()
|
||||
|
||||
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.visit()
|
||||
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")
|
||||
|
||||
Reference in New Issue
Block a user