This commit is contained in:
Sonny Bakker 2021-04-27 12:42:02 +02:00
parent 8498303006
commit 510f7187a8
6 changed files with 108 additions and 47 deletions

View file

@ -1,5 +1,9 @@
# Changelog # Changelog
## 0.3.13.7
- Check for Twitter error codes in response
## 0.3.13.6 ## 0.3.13.6
- Try to load sentry by default for all environments - Try to load sentry by default for all environments

View file

@ -1,6 +1,6 @@
{ {
"name": "newsreader", "name": "newsreader",
"version": "0.3.13.6", "version": "0.3.13.7",
"description": "Application for viewing RSS feeds", "description": "Application for viewing RSS feeds",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

View file

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "newsreader" name = "newsreader"
version = "0.3.13.6" version = "0.3.13.7"
description = "Webapplication for reading RSS feeds" description = "Webapplication for reading RSS feeds"
authors = ["Sonny <sonnyba871@gmail.com>"] authors = ["Sonny <sonnyba871@gmail.com>"]
license = "GPL-3.0" license = "GPL-3.0"

View file

@ -74,19 +74,24 @@ class TwitterClientTestCase(TestCase):
self.mocked_read.assert_called() self.mocked_read.assert_called()
def test_client_catches_stream_denied_exception(self): def test_client_catches_stream_denied_exception(self):
"""
Twitter also returns these responses for accounts which have been shutdown.
Therefore the error codes should be checked inside the response body.
See https://stackoverflow.com/questions/8357568/do-twitter-access-token-expire
"""
user = UserFactory( user = UserFactory(
twitter_oauth_token=str(uuid4()), twitter_oauth_token_secret=str(uuid4()) twitter_oauth_token=str(uuid4()), twitter_oauth_token_secret=str(uuid4())
) )
timeline = TwitterTimelineFactory(user=user) timeline = TwitterTimelineFactory(user=user)
self.mocked_read.side_effect = StreamDeniedException(message="Token expired") self.mocked_read.side_effect = StreamDeniedException(message="Not authorized")
with TwitterClient([timeline]) as client: with TwitterClient([timeline]) as client:
for data, stream in client: for data, stream in client:
with self.subTest(data=data, stream=stream): with self.subTest(data=data, stream=stream):
self.assertIsNone(data) self.assertIsNone(data)
self.assertIsNone(stream) self.assertIsNone(stream)
self.assertEquals(stream.rule.error, "Token expired") self.assertEquals(stream.rule.error, "Authorization required")
self.assertEquals(stream.rule.succeeded, False) self.assertEquals(stream.rule.succeeded, False)
self.mocked_read.assert_called() self.mocked_read.assert_called()
@ -94,8 +99,8 @@ class TwitterClientTestCase(TestCase):
user.refresh_from_db() user.refresh_from_db()
timeline.refresh_from_db() timeline.refresh_from_db()
self.assertIsNone(user.twitter_oauth_token) self.assertIsNotNone(user.twitter_oauth_token)
self.assertIsNone(user.twitter_oauth_token_secret) self.assertIsNotNone(user.twitter_oauth_token_secret)
def test_client_catches_stream_timed_out_exception(self): def test_client_catches_stream_timed_out_exception(self):
timeline = TwitterTimelineFactory() timeline = TwitterTimelineFactory()
@ -160,3 +165,31 @@ class TwitterClientTestCase(TestCase):
self.assertEquals(stream.rule.succeeded, False) self.assertEquals(stream.rule.succeeded, False)
self.mocked_read.assert_called() self.mocked_read.assert_called()
def test_client_catches_token_expired(self):
user = UserFactory(
twitter_oauth_token=str(uuid4()), twitter_oauth_token_secret=str(uuid4())
)
timeline = TwitterTimelineFactory(user=user)
response = Mock(json=lambda: {"errors": [{"code": 89}]})
self.mocked_read.side_effect = StreamDeniedException(
message="Not authorized", response=response
)
with TwitterClient([timeline]) as client:
for data, stream in client:
with self.subTest(data=data, stream=stream):
self.assertIsNone(data)
self.assertIsNone(stream)
self.assertEquals(stream.rule.error, "Authorization required")
self.assertEquals(stream.rule.succeeded, False)
self.mocked_read.assert_called()
user.refresh_from_db()
timeline.refresh_from_db()
self.assertIsNone(user.twitter_oauth_token)
self.assertIsNone(user.twitter_oauth_token_secret)

View file

@ -1,5 +1,5 @@
from datetime import datetime from datetime import datetime
from unittest.mock import patch from unittest.mock import Mock, patch
from uuid import uuid4 from uuid import uuid4
from django.test import TestCase from django.test import TestCase
@ -152,8 +152,8 @@ class TwitterCollectorTestCase(TestCase):
user = timeline.user user = timeline.user
self.assertIsNone(user.twitter_oauth_token) self.assertIsNotNone(user.twitter_oauth_token)
self.assertIsNone(user.twitter_oauth_token_secret) self.assertIsNotNone(user.twitter_oauth_token_secret)
def test_forbidden(self): def test_forbidden(self):
self.mocked_fetch.side_effect = StreamForbiddenException self.mocked_fetch.side_effect = StreamForbiddenException
@ -178,3 +178,25 @@ class TwitterCollectorTestCase(TestCase):
self.assertEquals(Post.objects.count(), 0) self.assertEquals(Post.objects.count(), 0)
self.assertEquals(timeline.succeeded, False) self.assertEquals(timeline.succeeded, False)
self.assertEquals(timeline.error, "Stream timed out") self.assertEquals(timeline.error, "Stream timed out")
def test_token_expired(self):
response = Mock(json=lambda: {"errors": [{"code": 89}]})
self.mocked_fetch.side_effect = StreamDeniedException(response=response)
timeline = TwitterTimelineFactory(
user__twitter_oauth_token=str(uuid4()),
user__twitter_oauth_token_secret=str(uuid4()),
)
collector = TwitterCollector()
collector.collect(rules=[timeline])
self.assertEquals(Post.objects.count(), 0)
self.assertEquals(timeline.succeeded, False)
self.assertEquals(timeline.error, "Stream does not have sufficient permissions")
user = timeline.user
self.assertIsNone(user.twitter_oauth_token)
self.assertIsNone(user.twitter_oauth_token_secret)

View file

@ -233,18 +233,34 @@ class TwitterClient(PostClient):
self.set_rule_error(stream.rule, e) self.set_rule_error(stream.rule, e)
break break
except StreamDeniedException as e: except (StreamNotFoundException, StreamTimeOutException) as e:
logger.warning( logger.warning(f"Request failed for {stream.rule.screen_name}")
f"Access token expired for user {stream.rule.user.pk}"
) self.set_rule_error(stream.rule, e)
continue
except StreamException as e:
logger.exception(f"Request failed for {stream.rule.screen_name}")
self.set_rule_error(stream.rule, e)
if not e.response:
continue
try:
response_data = e.response.json()
except JSONDecodeError:
continue
if "errors" in response_data:
errors = response_data["errors"]
token_expired = any(error["code"] == 89 for error in errors)
try: try:
import sentry_sdk import sentry_sdk
with sentry_sdk.push_scope() as scope: with sentry_sdk.push_scope() as scope:
scope.set_extra( scope.set_extra("content", response_data)
"content", e.response.content if e.response else None
)
sentry_sdk.capture_message( sentry_sdk.capture_message(
"Twitter authentication credentials reset" "Twitter authentication credentials reset"
) )
@ -268,20 +284,6 @@ class TwitterClient(PostClient):
[stream.rule.user.email], [stream.rule.user.email],
) )
self.set_rule_error(stream.rule, e)
break
except (StreamNotFoundException, StreamTimeOutException) as e:
logger.warning(f"Request failed for {stream.rule.screen_name}")
self.set_rule_error(stream.rule, e)
continue
except StreamException as e:
logger.exception(f"Request failed for {stream.rule.screen_name}")
self.set_rule_error(stream.rule, e)
continue continue
finally: finally:
stream.rule.last_run = timezone.now() stream.rule.last_run = timezone.now()