1# -*- coding: utf-8 -*-
2from __future__ import unicode_literals
3
4import curses
5from collections import OrderedDict
6
7import pytest
8
9from tuir.submission_page import SubmissionPage
10from tuir.docs import FOOTER_SUBMISSION
11
12try:
13    from unittest import mock
14except ImportError:
15    import mock
16
17
18PROMPTS = OrderedDict([
19    ('prompt_1', 'comments/571dw3'),
20    ('prompt_2', '///comments/571dw3'),
21    ('prompt_3', '/comments/571dw3'),
22    ('prompt_4', '/r/pics/comments/571dw3/'),
23    ('prompt_5', 'https://www.reddit.com/r/pics/comments/571dw3/at_disneyland'),
24])
25
26
27def test_submission_page_construct(reddit, terminal, config, oauth):
28    window = terminal.stdscr.subwin
29    url = ('https://www.reddit.com/r/Python/comments/2xmo63/'
30           'a_python_terminal_viewer_for_browsing_reddit')
31
32    with terminal.loader():
33        page = SubmissionPage(reddit, terminal, config, oauth, url=url)
34    assert terminal.loader.exception is None
35
36    # Toggle the second comment so we can check the draw more comments method
37    page.content.toggle(1)
38
39    # Set some special flags to make sure that we can draw them
40    submission_data = page.content.get(-1)
41    submission_data['gold'] = 1
42    submission_data['stickied'] = True
43    submission_data['saved'] = True
44    submission_data['flair'] = 'flair'
45
46    # Set some special flags to make sure that we can draw them
47    comment_data = page.content.get(0)
48    comment_data['gold'] = 3
49    comment_data['stickied'] = True
50    comment_data['saved'] = True
51    comment_data['flair'] = 'flair'
52
53    page.draw()
54
55    #  Title
56    title = url[:terminal.stdscr.ncols-1].encode('utf-8')
57    window.addstr.assert_any_call(0, 0, title)
58
59    # Banner
60    menu = '[1]hot         [2]top         [3]rising         [4]new         [5]controversial'
61    window.addstr.assert_any_call(0, 0, menu.encode('utf-8'))
62
63    # Footer - The text is longer than the default terminal width
64    text = FOOTER_SUBMISSION.strip()[:79]
65    window.addstr.assert_any_call(0, 0, text.encode('utf-8'))
66
67    # Submission
68    submission_data = page.content.get(-1)
69    text = submission_data['title'].encode('utf-8')
70    window.subwin.addstr.assert_any_call(1, 1, text, 2097152)
71    assert window.subwin.border.called
72
73    # Comment
74    comment_data = page.content.get(0)
75    text = comment_data['split_body'][0].encode('utf-8')
76    window.subwin.addstr.assert_any_call(1, 1, text, curses.A_NORMAL)
77
78    # More Comments
79    comment_data = page.content.get(1)
80    text = comment_data['body'].encode('utf-8')
81    window.subwin.addstr.assert_any_call(0, 1, text, curses.A_NORMAL)
82
83    # Cursor should not be drawn when the page is first opened
84    assert not any(args[0][3] == curses.A_REVERSE
85                   for args in window.subwin.addch.call_args_list)
86
87    # Reload with a smaller terminal window
88    terminal.stdscr.ncols = 20
89    terminal.stdscr.nlines = 10
90    with terminal.loader():
91        page = SubmissionPage(reddit, terminal, config, oauth, url=url)
92    assert terminal.loader.exception is None
93    page.draw()
94
95
96def test_submission_refresh(submission_page):
97
98    # Should be able to refresh content
99    submission_page.refresh_content()
100
101
102def test_submission_exit(submission_page):
103
104    # Exiting should set active to false
105    submission_page.active = True
106    submission_page.controller.trigger('h')
107    assert not submission_page.active
108
109
110def test_submission_unauthenticated(submission_page, terminal):
111
112    # Unauthenticated commands
113    methods = [
114        'a',  # Upvote
115        'z',  # Downvote
116        'c',  # Comment
117        'e',  # Edit
118        'd',  # Delete
119        'w',  # Save
120    ]
121    for ch in methods:
122        submission_page.controller.trigger(ch)
123        text = 'Not logged in'.encode('utf-8')
124        terminal.stdscr.subwin.addstr.assert_called_with(1, 1, text)
125
126
127def test_submission_open(submission_page, terminal):
128
129    # Open the selected link with the web browser
130    with mock.patch.object(terminal, 'open_browser'):
131        submission_page.controller.trigger(terminal.RETURN)
132        assert terminal.open_browser.called
133
134
135def test_submission_prompt(submission_page, terminal):
136
137    # Prompt for a different subreddit
138    with mock.patch.object(terminal, 'prompt_input'):
139        # Valid input
140        submission_page.active = True
141        submission_page.selected_page = None
142        terminal.prompt_input.return_value = 'front/top'
143        submission_page.controller.trigger('/')
144
145        submission_page.handle_selected_page()
146        assert not submission_page.active
147        assert submission_page.selected_page
148
149        # Invalid input
150        submission_page.active = True
151        submission_page.selected_page = None
152        terminal.prompt_input.return_value = 'front/pot'
153        submission_page.controller.trigger('/')
154
155        submission_page.handle_selected_page()
156        assert submission_page.active
157        assert not submission_page.selected_page
158
159
160@pytest.mark.parametrize('prompt', PROMPTS.values(), ids=list(PROMPTS))
161def test_submission_prompt_submission(submission_page, terminal, prompt):
162
163    # Navigate to a different submission from inside a submission
164    with mock.patch.object(terminal, 'prompt_input'):
165        terminal.prompt_input.return_value = prompt
166        submission_page.content.order = 'top'
167        submission_page.controller.trigger('/')
168        assert not terminal.loader.exception
169
170        submission_page.handle_selected_page()
171        assert not submission_page.active
172        assert submission_page.selected_page
173
174        assert submission_page.selected_page.content.order is None
175        data = submission_page.selected_page.content.get(-1)
176        assert data['object'].id == '571dw3'
177
178
179def test_submission_order(submission_page):
180
181    submission_page.controller.trigger('1')
182    assert submission_page.content.order == 'hot'
183    submission_page.controller.trigger('2')
184    assert submission_page.content.order == 'top'
185    submission_page.controller.trigger('3')
186    assert submission_page.content.order == 'rising'
187    submission_page.controller.trigger('4')
188    assert submission_page.content.order == 'new'
189    submission_page.controller.trigger('5')
190    assert submission_page.content.order == 'controversial'
191
192    # Shouldn't be able to sort the submission page by gilded
193    submission_page.controller.trigger('6')
194    assert submission_page.content.order == 'controversial'
195
196
197def test_submission_move_top_bottom(submission_page):
198
199    submission_page.controller.trigger('G')
200    assert submission_page.nav.absolute_index == 44
201
202    submission_page.controller.trigger('g')
203    submission_page.controller.trigger('g')
204    assert submission_page.nav.absolute_index == -1
205
206
207def test_submission_move_sibling_parent(submission_page):
208
209    # Jump to sibling
210    with mock.patch.object(submission_page, 'clear_input_queue'):
211        submission_page.controller.trigger('j')
212        submission_page.controller.trigger('J')
213    assert submission_page.nav.absolute_index == 7
214
215    # Jump to parent
216    with mock.patch.object(submission_page, 'clear_input_queue'):
217        submission_page.controller.trigger('k')
218        submission_page.controller.trigger('k')
219        submission_page.controller.trigger('K')
220    assert submission_page.nav.absolute_index == 0
221
222
223def test_submission_pager(submission_page, terminal):
224
225    # View a submission with the pager
226    with mock.patch.object(terminal, 'open_pager'):
227        submission_page.controller.trigger('l')
228        assert terminal.open_pager.called
229
230    # Move down to the first comment
231    with mock.patch.object(submission_page, 'clear_input_queue'):
232        submission_page.controller.trigger('j')
233
234    # View a comment with the pager
235    with mock.patch.object(terminal, 'open_pager'):
236        submission_page.controller.trigger('l')
237        assert terminal.open_pager.called
238
239
240def test_submission_comment_not_enough_space(submission_page, terminal):
241
242    # The first comment is 10 lines, shrink the screen so that it won't fit.
243    # Setting the terminal to 10 lines means that there will only be 8 lines
244    # available (after subtracting the header and footer) to draw the comment.
245    terminal.stdscr.nlines = 10
246
247    # Select the first comment
248    with mock.patch.object(submission_page, 'clear_input_queue'):
249        submission_page.move_cursor_down()
250
251    submission_page.draw()
252
253    text = '(Not enough space to display)'.encode('ascii')
254    window = terminal.stdscr.subwin
255    window.subwin.addstr.assert_any_call(6, 1, text, curses.A_NORMAL)
256
257
258def test_submission_vote(submission_page, refresh_token):
259
260    # Log in
261    submission_page.config.refresh_token = refresh_token
262    submission_page.oauth.authorize()
263
264    # Test voting on the submission
265    with mock.patch('tuir.packages.praw.objects.Submission.upvote') as upvote,            \
266            mock.patch('tuir.packages.praw.objects.Submission.downvote') as downvote,     \
267            mock.patch('tuir.packages.praw.objects.Submission.clear_vote') as clear_vote:
268
269        data = submission_page.get_selected_item()
270        data['object'].archived = False
271
272        # Upvote
273        submission_page.controller.trigger('a')
274        assert upvote.called
275        assert data['likes'] is True
276
277        # Clear vote
278        submission_page.controller.trigger('a')
279        assert clear_vote.called
280        assert data['likes'] is None
281
282        # Upvote
283        submission_page.controller.trigger('a')
284        assert upvote.called
285        assert data['likes'] is True
286
287        # Downvote
288        submission_page.controller.trigger('z')
289        assert downvote.called
290        assert data['likes'] is False
291
292        # Clear vote
293        submission_page.controller.trigger('z')
294        assert clear_vote.called
295        assert data['likes'] is None
296
297        # Upvote - exception
298        upvote.side_effect = KeyboardInterrupt
299        submission_page.controller.trigger('a')
300        assert data['likes'] is None
301
302        # Downvote - exception
303        downvote.side_effect = KeyboardInterrupt
304        submission_page.controller.trigger('a')
305        assert data['likes'] is None
306
307
308def test_submission_vote_archived(submission_page, refresh_token, terminal):
309
310    # Log in
311    submission_page.config.refresh_token = refresh_token
312    submission_page.oauth.authorize()
313
314    # Load an archived submission
315    archived_url = 'https://www.reddit.com/r/IAmA/comments/z1c9z/'
316    submission_page.refresh_content(name=archived_url)
317
318    with mock.patch.object(terminal, 'show_notification') as show_notification:
319        data = submission_page.get_selected_item()
320
321        # Upvote the submission
322        show_notification.reset_mock()
323        submission_page.controller.trigger('a')
324        show_notification.assert_called_with('Voting disabled for archived post', style='Error')
325        assert data['likes'] is None
326
327        # Downvote the submission
328        show_notification.reset_mock()
329        submission_page.controller.trigger('z')
330        show_notification.assert_called_with('Voting disabled for archived post', style='Error')
331        assert data['likes'] is None
332
333
334def test_submission_save(submission_page, refresh_token):
335
336    # Log in
337    submission_page.config.refresh_token = refresh_token
338    submission_page.oauth.authorize()
339
340    # Test save on the submission
341    with mock.patch('tuir.packages.praw.objects.Submission.save') as save,        \
342            mock.patch('tuir.packages.praw.objects.Submission.unsave') as unsave:
343
344        data = submission_page.content.get(submission_page.nav.absolute_index)
345
346        # Save
347        submission_page.controller.trigger('w')
348        assert save.called
349        assert data['saved'] is True
350
351        # Unsave
352        submission_page.controller.trigger('w')
353        assert unsave.called
354        assert data['saved'] is False
355
356        # Save - exception
357        save.side_effect = KeyboardInterrupt
358        submission_page.controller.trigger('w')
359        assert data['saved'] is False
360
361
362def test_submission_comment_save(submission_page, terminal, refresh_token):
363
364    # Log in
365    submission_page.config.refresh_token = refresh_token
366    submission_page.oauth.authorize()
367
368    # Move down to the first comment
369    with mock.patch.object(submission_page, 'clear_input_queue'):
370        submission_page.controller.trigger('j')
371
372    # Test save on the comment submission
373    with mock.patch('tuir.packages.praw.objects.Comment.save') as save,        \
374            mock.patch('tuir.packages.praw.objects.Comment.unsave') as unsave:
375
376        data = submission_page.content.get(submission_page.nav.absolute_index)
377
378        # Save
379        submission_page.controller.trigger('w')
380        assert save.called
381        assert data['saved'] is True
382
383        # Unsave
384        submission_page.controller.trigger('w')
385        assert unsave.called
386        assert data['saved'] is False
387
388        # Save - exception
389        save.side_effect = KeyboardInterrupt
390        submission_page.controller.trigger('w')
391        assert data['saved'] is False
392
393
394def test_submission_comment(submission_page, terminal, refresh_token):
395
396    # Log in
397    submission_page.config.refresh_token = refresh_token
398    submission_page.oauth.authorize()
399
400    # Leave a comment
401    with mock.patch('tuir.packages.praw.objects.Submission.add_comment') as add_comment, \
402            mock.patch.object(terminal, 'open_editor') as open_editor,                  \
403            mock.patch('time.sleep'):
404        open_editor.return_value.__enter__.return_value = 'comment text'
405        submission_page.controller.trigger('c')
406        assert open_editor.called
407        add_comment.assert_called_with('comment text')
408
409
410def test_submission_delete(submission_page, terminal, refresh_token):
411
412    # Log in
413    submission_page.config.refresh_token = refresh_token
414    submission_page.oauth.authorize()
415
416    # Can't delete the submission
417    curses.flash.reset_mock()
418    submission_page.controller.trigger('d')
419    assert curses.flash.called
420
421    # Move down to the first comment
422    with mock.patch.object(submission_page, 'clear_input_queue'):
423        submission_page.controller.trigger('j')
424
425    # Try to delete the first comment - wrong author
426    curses.flash.reset_mock()
427    submission_page.controller.trigger('d')
428    assert curses.flash.called
429
430    # Spoof the author and try to delete again
431    data = submission_page.content.get(submission_page.nav.absolute_index)
432    data['author'] = submission_page.reddit.user.name
433    with mock.patch('tuir.packages.praw.objects.Comment.delete') as delete,  \
434            mock.patch.object(terminal.stdscr, 'getch') as getch,           \
435            mock.patch('time.sleep'):
436        getch.return_value = ord('y')
437        submission_page.controller.trigger('d')
438        assert delete.called
439
440
441def test_submission_edit(submission_page, terminal, refresh_token):
442
443    # Log in
444    submission_page.config.refresh_token = refresh_token
445    submission_page.oauth.authorize()
446
447    # Try to edit the submission - wrong author
448    data = submission_page.content.get(submission_page.nav.absolute_index)
449    data['author'] = 'some other person'
450    curses.flash.reset_mock()
451    submission_page.controller.trigger('e')
452    assert curses.flash.called
453
454    # Spoof the submission and try to edit again
455    data = submission_page.content.get(submission_page.nav.absolute_index)
456    data['author'] = submission_page.reddit.user.name
457    with mock.patch('tuir.packages.praw.objects.Submission.edit') as edit,  \
458            mock.patch.object(terminal, 'open_editor') as open_editor,     \
459            mock.patch('time.sleep'):
460        open_editor.return_value.__enter__.return_value = 'submission text'
461
462        submission_page.controller.trigger('e')
463        assert open_editor.called
464        edit.assert_called_with('submission text')
465
466    # Move down to the first comment
467    with mock.patch.object(submission_page, 'clear_input_queue'):
468        submission_page.controller.trigger('j')
469
470    # Spoof the author and edit the comment
471    data = submission_page.content.get(submission_page.nav.absolute_index)
472    data['author'] = submission_page.reddit.user.name
473    with mock.patch('tuir.packages.praw.objects.Comment.edit') as edit, \
474            mock.patch.object(terminal, 'open_editor') as open_editor, \
475            mock.patch('time.sleep'):
476        open_editor.return_value.__enter__.return_value = 'comment text'
477
478        submission_page.controller.trigger('e')
479        assert open_editor.called
480        edit.assert_called_with('comment text')
481
482
483def test_submission_urlview(submission_page, terminal, refresh_token):
484
485    # Log in
486    submission_page.config.refresh_token = refresh_token
487    submission_page.oauth.authorize()
488
489    # Submission case
490    data = submission_page.content.get(submission_page.nav.absolute_index)
491    data['body'] = 'test comment body  ❤'
492    with mock.patch.object(terminal, 'open_urlview') as open_urlview:
493        submission_page.controller.trigger('b')
494        open_urlview.assert_called_with('test comment body  ❤')
495
496    # Subreddit case
497    data = submission_page.content.get(submission_page.nav.absolute_index)
498    data['text'] = ''
499    data['body'] = ''
500    data['url_full'] = 'http://test.url.com  ❤'
501    with mock.patch.object(terminal, 'open_urlview') as open_urlview, \
502            mock.patch('subprocess.Popen'):
503        submission_page.controller.trigger('b')
504        open_urlview.assert_called_with('http://test.url.com  ❤')
505
506
507def test_submission_prompt_and_select_link(submission_page, terminal):
508
509    # A link submission should return the URL that it's pointing to
510    link = submission_page.prompt_and_select_link()
511    assert link == 'https://github.com/michael-lazar/rtv'
512
513    with mock.patch.object(submission_page, 'clear_input_queue'):
514        submission_page.controller.trigger('j')
515
516    # The first comment doesn't have any links in the comment body
517    link = submission_page.prompt_and_select_link()
518    data = submission_page.get_selected_item()
519    assert link == data['permalink']
520
521    with mock.patch.object(submission_page, 'clear_input_queue'):
522        submission_page.controller.trigger('j')
523
524    # The second comment has a link embedded in the comment body, and
525    # the user is prompted to select which link to open
526    with mock.patch.object(terminal, 'prompt_user_to_select_link') as prompt:
527        prompt.return_value = 'https://selected_link'
528
529        link = submission_page.prompt_and_select_link()
530        data = submission_page.get_selected_item()
531
532        assert link == prompt.return_value
533
534        embedded_url = 'http://peterdowns.com/posts/first-time-with-pypi.html'
535        assert prompt.call_args[0][0] == [
536            {'text': 'Permalink', 'href': data['permalink']},
537            {'text': 'Relevant tutorial', 'href': embedded_url}
538        ]
539
540    submission_page.controller.trigger(' ')
541
542    # The comment is now hidden so there are no links to select
543    link = submission_page.prompt_and_select_link()
544    assert link is None
545