1# -*- coding: utf-8 -*-
2"""
3    test_websupport
4    ~~~~~~~~~~~~~~~
5
6    Test the Web Support Package
7
8    :copyright: Copyright 2007-2018 by the Sphinx team, see AUTHORS.
9    :license: BSD, see LICENSE for details.
10"""
11
12from sphinxcontrib.websupport import WebSupport
13from sphinxcontrib.websupport.errors import DocumentNotFoundError, \
14    CommentNotAllowedError, UserNotAuthorizedError
15from sphinxcontrib.websupport.storage import StorageBackend
16from sphinxcontrib.websupport.storage.differ import CombinedHtmlDiff
17try:
18    from sphinxcontrib.websupport.storage.sqlalchemystorage import Session, \
19        Comment, CommentVote
20    from sphinxcontrib.websupport.storage.sqlalchemy_db import Node
21    sqlalchemy_missing = False
22except ImportError:
23    sqlalchemy_missing = True
24
25import pytest
26from util import rootdir, tempdir
27
28
29@pytest.fixture
30def support(request):
31    settings = {
32        'srcdir': rootdir / 'root',
33        # to use same directory for 'builddir' in each 'support' fixture, using
34        # 'tempdir' (static) value instead of 'tempdir' fixture value.
35        # each test expect result of db value at previous test case.
36        'builddir': tempdir / 'websupport'
37    }
38    marker = request.node.get_closest_marker('support')
39    if marker:
40        settings.update(marker.kwargs)
41
42    support = WebSupport(**settings)
43    yield support
44
45
46with_support = pytest.mark.support
47
48
49class NullStorage(StorageBackend):
50    pass
51
52
53@with_support(storage=NullStorage())
54def test_no_srcdir(support):
55    # make sure the correct exception is raised if srcdir is not given.
56    with pytest.raises(RuntimeError):
57        support.build()
58
59
60@pytest.mark.skipif(sqlalchemy_missing, reason='needs sqlalchemy')
61@with_support()
62def test_build(support):
63    support.build()
64
65
66@pytest.mark.skipif(sqlalchemy_missing, reason='needs sqlalchemy')
67@with_support()
68def test_get_document(support):
69    with pytest.raises(DocumentNotFoundError):
70        support.get_document('nonexisting')
71
72    contents = support.get_document('contents')
73    assert contents['title'] and contents['body'] \
74        and contents['sidebar'] and contents['relbar']
75
76
77@pytest.mark.skipif(sqlalchemy_missing, reason='needs sqlalchemy')
78@with_support()
79def test_comments(support):
80    session = Session()
81    nodes = session.query(Node).all()
82    first_node = nodes[0]
83    second_node = nodes[1]
84
85    # Create a displayed comment and a non displayed comment.
86    comment = support.add_comment('First test comment',
87                                  node_id=first_node.id,
88                                  username='user_one')
89    hidden_comment = support.add_comment('Hidden comment',
90                                         node_id=first_node.id,
91                                         displayed=False)
92    # Make sure that comments can't be added to a comment where
93    # displayed == False, since it could break the algorithm that
94    # converts a nodes comments to a tree.
95    with pytest.raises(CommentNotAllowedError):
96        support.add_comment('Not allowed', parent_id=str(hidden_comment['id']))
97    # Add a displayed and not displayed child to the displayed comment.
98    support.add_comment('Child test comment', parent_id=str(comment['id']),
99                        username='user_one')
100    support.add_comment('Hidden child test comment',
101                        parent_id=str(comment['id']), displayed=False)
102    # Add a comment to another node to make sure it isn't returned later.
103    support.add_comment('Second test comment',
104                        node_id=second_node.id,
105                        username='user_two')
106
107    # Access the comments as a moderator.
108    data = support.get_data(first_node.id, moderator=True)
109    comments = data['comments']
110    children = comments[0]['children']
111    assert len(comments) == 2
112    assert comments[1]['text'] == '<p>Hidden comment</p>\n'
113    assert len(children) == 2
114    assert children[1]['text'] == '<p>Hidden child test comment</p>\n'
115
116    # Access the comments without being a moderator.
117    data = support.get_data(first_node.id)
118    comments = data['comments']
119    children = comments[0]['children']
120    assert len(comments) == 1
121    assert comments[0]['text'] == '<p>First test comment</p>\n'
122    assert len(children) == 1
123    assert children[0]['text'] == '<p>Child test comment</p>\n'
124
125
126@pytest.mark.skipif(sqlalchemy_missing, reason='needs sqlalchemy')
127@with_support()
128def test_user_delete_comments(support):
129    def get_comment():
130        session = Session()
131        node = session.query(Node).first()
132        session.close()
133        return support.get_data(node.id)['comments'][0]
134
135    comment = get_comment()
136    assert comment['username'] == 'user_one'
137    # Make sure other normal users can't delete someone elses comments.
138    with pytest.raises(UserNotAuthorizedError):
139        support.delete_comment(comment['id'], username='user_two')
140    # Now delete the comment using the correct username.
141    support.delete_comment(comment['id'], username='user_one')
142    comment = get_comment()
143    assert comment['username'] == '[deleted]'
144    assert comment['text'] == '[deleted]'
145
146
147called = False
148
149
150def moderation_callback(comment):
151    global called
152    called = True
153
154
155@pytest.mark.skipif(sqlalchemy_missing, reason='needs sqlalchemy')
156@with_support(moderation_callback=moderation_callback)
157def test_moderation(support):
158    session = Session()
159    nodes = session.query(Node).all()
160    node = nodes[7]
161    session.close()
162    accepted = support.add_comment('Accepted Comment', node_id=node.id,
163                                   displayed=False)
164    deleted = support.add_comment('Comment to delete', node_id=node.id,
165                                  displayed=False)
166    # Make sure the moderation_callback is called.
167    assert called
168    # Make sure the user must be a moderator.
169    with pytest.raises(UserNotAuthorizedError):
170        support.accept_comment(accepted['id'])
171    with pytest.raises(UserNotAuthorizedError):
172        support.delete_comment(deleted['id'])
173    support.accept_comment(accepted['id'], moderator=True)
174    support.delete_comment(deleted['id'], moderator=True)
175    comments = support.get_data(node.id)['comments']
176    assert len(comments) == 1
177    comments = support.get_data(node.id, moderator=True)['comments']
178    assert len(comments) == 1
179
180
181@pytest.mark.skipif(sqlalchemy_missing, reason='needs sqlalchemy')
182@with_support()
183def test_moderator_delete_comments(support):
184    def get_comment():
185        session = Session()
186        node = session.query(Node).first()
187        session.close()
188        return support.get_data(node.id, moderator=True)['comments'][1]
189
190    comment = get_comment()
191    support.delete_comment(comment['id'], username='user_two',
192                           moderator=True)
193    with pytest.raises(IndexError):
194        get_comment()
195
196
197@pytest.mark.skipif(sqlalchemy_missing, reason='needs sqlalchemy')
198@with_support()
199def test_update_username(support):
200    support.update_username('user_two', 'new_user_two')
201    session = Session()
202    comments = session.query(Comment).\
203        filter(Comment.username == 'user_two').all()
204    assert len(comments) == 0
205    votes = session.query(CommentVote).\
206        filter(CommentVote.username == 'user_two').all()
207    assert len(votes) == 0
208    comments = session.query(Comment).\
209        filter(Comment.username == 'new_user_two').all()
210    assert len(comments) == 1
211    votes = session.query(CommentVote).\
212        filter(CommentVote.username == 'new_user_two').all()
213    assert len(votes) == 0
214
215
216@pytest.mark.skipif(sqlalchemy_missing, reason='needs sqlalchemy')
217@with_support()
218def test_proposals(support):
219    session = Session()
220    node = session.query(Node).first()
221
222    data = support.get_data(node.id)
223
224    source = data['source']
225    proposal = source[:5] + source[10:15] + 'asdf' + source[15:]
226
227    support.add_comment('Proposal comment',
228                        node_id=node.id,
229                        proposal=proposal)
230
231
232@pytest.mark.skipif(sqlalchemy_missing, reason='needs sqlalchemy')
233@with_support()
234def test_voting(support):
235    session = Session()
236    nodes = session.query(Node).all()
237    node = nodes[0]
238
239    comment = support.get_data(node.id)['comments'][0]
240
241    def check_rating(val):
242        data = support.get_data(node.id)
243        comment = data['comments'][0]
244        assert comment['rating'] == val, '%s != %s' % (comment['rating'], val)
245
246    support.process_vote(comment['id'], 'user_one', '1')
247    support.process_vote(comment['id'], 'user_two', '1')
248    support.process_vote(comment['id'], 'user_three', '1')
249    check_rating(3)
250    support.process_vote(comment['id'], 'user_one', '-1')
251    check_rating(1)
252    support.process_vote(comment['id'], 'user_one', '0')
253    check_rating(2)
254
255    # Make sure a vote with value > 1 or < -1 can't be cast.
256    with pytest.raises(ValueError):
257        support.process_vote(comment['id'], 'user_one', '2')
258    with pytest.raises(ValueError):
259        support.process_vote(comment['id'], 'user_one', '-2')
260
261    # Make sure past voting data is associated with comments when they are
262    # fetched.
263    data = support.get_data(str(node.id), username='user_two')
264    comment = data['comments'][0]
265    assert comment['vote'] == 1, '%s != 1' % comment['vote']
266
267
268def test_differ():
269    source = 'Lorem ipsum dolor sit amet,\nconsectetur adipisicing elit,\n' \
270        'sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'
271    prop = 'Lorem dolor sit amet,\nconsectetur nihil adipisicing elit,\n' \
272        'sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'
273    differ = CombinedHtmlDiff(source, prop)
274    differ.make_html()
275