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