Example #1
0
class BrowserViewTest(testutil.AppTest):
  def setUp(self):
    super().setUp()

    self.domain = Domain(id='snarfed.org', tokens=['towkin']).put()
    FakeBrowserSource.gr_source = FakeGrSource()
    self.actor['fbs_id'] = '222yyy'
    self.source = FakeBrowserSource.new(actor=self.actor).put()
    self.auth = f'token=towkin&key={self.source.urlsafe().decode()}'
    self.other_source = FakeBrowserSource(id='333zzz', domains=['foo.com']).put()

    for a in self.activities:
      a['object']['author'] = self.actor

    self.activities_no_replies = copy.deepcopy(self.activities)
    for a in self.activities_no_replies:
      del a['object']['replies']
      del a['object']['tags']

  def post(self, path_query, auth=True, **kwargs):
    if auth and '?' not in path_query:
      path_query += f'?{self.auth}'
    return self.client.post(f'/fbs/browser/{path_query}', **kwargs)

  def test_status(self):
    resp = self.client.get(f'/fbs/browser/status?{self.auth}')
    self.assertEqual(200, resp.status_code, resp.get_data(as_text=True))

    self.assertEqual({
      'status': 'enabled',
      'poll-seconds': FakeBrowserSource.FAST_POLL.total_seconds(),
    }, resp.json)

  def test_homepage(self):
    resp = self.post('homepage', data='homepage html', auth=False)
    self.assertEqual(200, resp.status_code, resp.get_data(as_text=True))
    self.assertEqual('snarfed', resp.json)

  def test_homepage_no_logged_in_user(self):
    FakeBrowserSource.gr_source.actor = {}
    resp = self.post('homepage', data='not logged in', auth=False)
    self.assertEqual(400, resp.status_code)
    self.assertEqual("Scrape error: couldn't determine logged in FakeSource user or username",
                     resp.get_data(as_text=True))
    self.assertEqual('text/plain; charset=utf-8', resp.headers['Content-Type'])

  def test_profile_new_user(self):
    self.source.delete()

    self.expect_requests_get('https://snarfed.org/', '')
    self.mox.ReplayAll()

    resp = self.post('profile?token=towkin')

    self.assertEqual(200, resp.status_code, resp.get_data(as_text=True))
    self.assert_equals(self.source.urlsafe().decode(), resp.json)

    src = self.source.get()
    self.assertEqual('Ryan B', src.name)
    self.assertEqual(['https://snarfed.org/'], src.domain_urls)
    self.assertEqual(['snarfed.org'], src.domains)

  def test_profile_existing_user_update(self):
    self.assertIsNotNone(self.source.get())
    FakeBrowserSource.gr_source.actor.update({
      'displayName': 'Mrs. Foo',
      'image': {'url': 'http://foo/img'},
    })

    # for webmention discovery
    self.mox.ReplayAll()

    resp = self.post('profile?token=towkin')
    self.assertEqual(200, resp.status_code, resp.get_data(as_text=True))
    self.assert_equals(self.source.urlsafe().decode(), resp.json)

    src = self.source.get()
    self.assertEqual('Mrs. Foo', src.name)
    self.assertEqual('http://foo/img', src.picture)

  def test_profile_fall_back_to_scraped_to_actor(self):
    self.source.delete()

    self.mox.StubOutWithMock(FakeGrSource, 'scraped_to_activities')
    FakeGrSource.scraped_to_activities('').AndReturn(([], None))

    self.expect_requests_get('https://snarfed.org/', '')
    self.mox.ReplayAll()

    resp = self.post('profile?token=towkin')
    self.assertEqual(200, resp.status_code, resp.get_data(as_text=True))
    self.assert_equals(self.source.urlsafe().decode(), resp.json)

    src = self.source.get()
    self.assertEqual('Ryan B', src.name)
    self.assertEqual(['https://snarfed.org/'], src.domain_urls)
    self.assertEqual(['snarfed.org'], src.domains)

  def test_profile_no_scraped_actor(self):
    self.source.delete()
    FakeGrSource.actor = None
    resp = self.post('profile?token=towkin')
    self.assertEqual(400, resp.status_code, resp.get_data(as_text=True))
    self.assertEqual('Scrape error: missing actor!',
                     resp.get_data(as_text=True))

  def test_profile_private_account(self):
    FakeBrowserSource.gr_source.actor['to'] = \
      [{'objectType':'group', 'alias':'@private'}]
    resp = self.post('profile?token=towkin')
    self.assertEqual(400, resp.status_code)
    self.assertIn('Your FakeSource account is private. ', resp.get_data(as_text=True))

  def test_profile_links(self):
    # check HEAD requests, make sure we don't HEAD blocklisted domains
    self.unstub_requests_head()

    FakeBrowserSource.gr_source.actor.update({
      'url': 'https://patreon.com/bar',  # blocklisted
      'urls': [
        {'value': 'http://fa.ke/foo'},  # silo profile
        {'value': 'https://snarfed.org/'},
        {'value': 'http://another.com/me'},
        {'value': 'https://snarfed.org/'},  # duplicate
      ],
    })

    self.expect_requests_get('http://another.com')
    self.expect_requests_head('https://patreon.com/bar')
    self.expect_requests_head('http://another.com/me')
    self.expect_requests_get('http://another.com')
    self.expect_requests_get('https://snarfed.org/')
    self.mox.ReplayAll()

    resp = self.post('profile?token=towkin')
    self.assertEqual(200, resp.status_code)

    src = self.source.get()
    self.assertEqual(['https://snarfed.org/', 'http://another.com/me'], src.domain_urls)
    self.assertEqual(['snarfed.org', 'another.com'], src.domains)

  def test_profile_no_links(self):
    del FakeBrowserSource.gr_source.actor['url']
    resp = self.post('profile?token=towkin')
    self.assertEqual(400, resp.status_code)
    self.assertIn("No usable web sites found in your FakeSource profile.",
                  resp.get_data(as_text=True))

  def test_profile_missing_token(self):
    resp = self.post('profile', auth=False)
    self.assertEqual(400, resp.status_code)
    self.assertIn('Missing required parameter: token', resp.get_data(as_text=True))

  def test_profile_no_stored_token(self):
    self.domain.delete()
    resp = self.post('profile?token=towkin')
    self.assertEqual(400, resp.status_code)
    self.assertIn("Found link(s) to ['snarfed.org'] in your FakeSource profile",
                  resp.get_data(as_text=True))

  def test_profile_bad_token(self):
    resp = self.post('profile?token=nope')
    self.assertEqual(400, resp.status_code)
    self.assertIn("Found link(s) to ['snarfed.org'] in your FakeSource profile",
                  resp.get_data(as_text=True))

  def test_feed(self):
    resp = self.post('feed')
    self.assertEqual(200, resp.status_code, resp.get_data(as_text=True))
    self.assertEqual(self.activities_no_replies, util.trim_nulls(resp.json))

  def test_feed_empty(self):
    FakeGrSource.activities = []
    resp = self.post('feed')
    self.assertEqual(200, resp.status_code, resp.get_data(as_text=True))
    self.assertEqual([], resp.json)

  def test_feed_missing_token(self):
    resp = self.post(f'feed?key={self.source.urlsafe().decode()}')
    self.assertEqual(400, resp.status_code, resp.get_data(as_text=True))

  def test_feed_bad_token(self):
    resp = self.post(f'feed?token=nope&key={self.source.urlsafe().decode()}')
    self.assertEqual(403, resp.status_code, resp.get_data(as_text=True))
    self.assertEqual("Token nope is not authorized for any of: ['snarfed.org']",
                     resp.get_data(as_text=True))

  def test_feed_missing_key(self):
    resp = self.post('feed?token=towkin')
    self.assertEqual(400, resp.status_code, resp.get_data(as_text=True))

  def test_feed_bad_key(self):
    resp = self.post('feed?token=towkin&key=asdf')
    self.assertEqual(400, resp.status_code, resp.get_data(as_text=True))
    # this comes from util.load_source() since the urlsafe key is malformed
    self.assertEqual('Bad value for key', resp.get_data(as_text=True))

  def test_feed_token_domain_not_in_source(self):
    resp = self.post(
      f'feed?token=towkin&key={self.other_source.urlsafe().decode()}')
    self.assertEqual(403, resp.status_code, resp.get_data(as_text=True))

  def test_feed_no_public_posts(self):
    for a in FakeGrSource.activities:
      a['object']['to'] = [{'objectType':'group', 'alias':'@private'}]

    resp = self.post('feed')
    self.assertEqual(400, resp.status_code)
    self.assertIn('None of your recent FakeSource posts are public',
                  resp.get_data(as_text=True))

  def test_post(self):
    resp = self.post('post', data='silowe html')
    self.assertEqual(200, resp.status_code, resp.get_data(as_text=True))
    self.assert_equals(self.activities[0], util.trim_nulls(resp.json))

    activities = Activity.query().fetch()
    self.assertEqual(1, len(activities))
    self.assertEqual(self.source, activities[0].source)
    self.assert_equals(self.activities[0],
                       util.trim_nulls(json_loads(activities[0].activity_json)))
    self.assertEqual('silowe html', activities[0].html)

  def test_post_empty(self):
    FakeGrSource.activities = []
    resp = self.post('post')
    self.assertEqual(400, resp.status_code)
    self.assertEqual('Scrape error: no FakeSource post found in HTML',
                     resp.get_data(as_text=True))

  def test_post_updated(self):
    # existing activity with two comments, three tags, private audience
    activity = self.activities[0]
    obj = activity['object']
    reply = obj['replies']['items'][0]
    obj.update({
      'replies': {
        'items': [reply, copy.deepcopy(reply)],
        'totalItems': 2,
      },
      'to': [{'objectType': 'group', 'alias': '@private'}],
    })
    obj['replies']['items'][1]['id'] = 'abc'
    key = Activity(id=activity['id'], activity_json=json_dumps(activity)).put()

    # scraped activity has different second comment, new like, public audience
    public = [{'objectType': 'group', 'alias': '@public'}]
    orig_tags = copy.deepcopy(obj['tags'])
    like = obj['tags'][0]
    like_2 = copy.deepcopy(like)
    like_2['id'] += '-foo'
    obj.update({
      'tags': [like, like_2],
      'to': public,
    })
    obj['replies']['items'][1]['id'] = 'xyz'
    FakeGrSource.activities = [activity]

    resp = self.post('post')
    self.assertEqual(200, resp.status_code, resp.get_data(as_text=True))
    self.assert_equals(activity, util.trim_nulls(resp.json))

    merged = json_loads(key.get().activity_json)
    replies = merged['object']['replies']
    self.assert_equals(3, replies['totalItems'], replies)
    self.assert_equals([reply['id'], 'abc', 'xyz'],
                       [r['id'] for r in replies['items']])
    self.assert_equals(public, merged['object']['to'])
    self.assert_equals(orig_tags + [like_2], merged['object']['tags'])

  def test_post_missing_key(self):
    resp = self.post('post?token=towkin')
    self.assertEqual(400, resp.status_code, resp.get_data(as_text=True))

  def test_post_bad_key(self):
    resp = self.post('post?token=towkin&key=asdf')
    self.assertEqual(400, resp.status_code, resp.get_data(as_text=True))
    # this comes from util.load_source() since the urlsafe key is malformed
    self.assertEqual('Bad value for key', resp.get_data(as_text=True))

  def test_post_missing_token(self):
    resp = self.post(f'post?key={self.source.urlsafe().decode()}')
    self.assertEqual(400, resp.status_code, resp.get_data(as_text=True))
    self.assertIn('Missing required parameter: token', resp.get_data(as_text=True))

  def test_post_bad_token(self):
    resp = self.post(f'post?token=nope&key={self.source.urlsafe().decode()}')
    self.assertEqual(403, resp.status_code, resp.get_data(as_text=True))
    self.assertEqual("Token nope is not authorized for any of: ['snarfed.org']",
                     resp.get_data(as_text=True))

  def test_post_token_domain_not_in_source(self):
    resp = self.post(
      f'post?token=towkin&key={self.other_source.urlsafe().decode()}')
    self.assertEqual(403, resp.status_code, resp.get_data(as_text=True))

  def test_comments(self):
    key = Activity(id='tag:fa.ke,2013:123_456', source=self.source,
                   activity_json=json_dumps(self.activities[0])).put()
    comments = [{
      'objectType': 'comment',
      'content': '太可爱了。cute,@a_person, very cute',
      'id': '110',
      'url': 'https://www.instagram.com/p/ABC123/#comment-110',
    }]

    resp = self.post(f'comments?id=tag:fa.ke,2013:123_456&{self.auth}',
                     json=comments)
    self.assertEqual(200, resp.status_code, resp.get_data(as_text=True))
    self.assert_equals(comments, resp.json)

    stored = json_loads(key.get().activity_json)
    self.assert_equals(self.activities[0]['object']['replies']['items'] + comments,
                       stored['object']['replies']['items'])

  def test_reactions(self):
    key = Activity(id='tag:fa.ke,2013:123_456', source=self.source,
                   activity_json=json_dumps(self.activities[0])).put()
    likes = [{
      'objectType': 'activity',
      'verb': 'like',
      'id': 'new',
    }]

    resp = self.post(f'reactions?id=tag:fa.ke,2013:123_456&{self.auth}',
                     json=likes)
    self.assertEqual(200, resp.status_code, resp.get_data(as_text=True))
    self.assert_equals(likes, resp.json)

    stored = json_loads(key.get().activity_json)
    self.assert_equals(self.activities[0]['object']['tags'] + likes,
                       stored['object']['tags'])

  def test_reactions_bad_id(self):
    resp = self.post(f'reactions?id=789&{self.auth}')
    self.assertEqual(400, resp.status_code)
    self.assertEqual('Scrape error: expected id to be tag URI; got 789',
                     resp.get_data(as_text=True))

  def test_reactions_bad_scraped_data(self):
    Activity(id='tag:fa.ke,2013:123_456', source=self.source,
             activity_json=json_dumps(self.activities[0])).put()

    bad_json = '<html><not><json>'
    self.mox.StubOutWithMock(FakeGrSource, 'merge_scraped_reactions')
    FakeGrSource.merge_scraped_reactions(bad_json, mox.IgnoreArg()
                                         ).AndRaise((ValueError('fooey')))
    self.mox.ReplayAll()

    resp = self.post(f'reactions?id=tag:fa.ke,2013:123_456&{self.auth}',
                     data=bad_json)
    self.assertEqual(400, resp.status_code)
    self.assertEqual("Scrape error: couldn't parse extras: fooey",
                     resp.get_data(as_text=True))

  def test_reactions_no_activity(self):
    resp = self.post(f'reactions?id=tag:fa.ke,2013:789&{self.auth}')
    self.assertEqual(404, resp.status_code)
    self.assertEqual('No FakeSource post found for id tag:fa.ke,2013:789',
                     resp.get_data(as_text=True))

  def test_reactions_missing_token(self):
    resp = self.post(f'reactions?key={self.source.urlsafe().decode()}')
    self.assertEqual(400, resp.status_code, resp.get_data(as_text=True))

  def test_reactions_bad_token(self):
    resp = self.post(f'reactions?token=nope&key={self.source.urlsafe().decode()}')
    self.assertEqual(403, resp.status_code, resp.get_data(as_text=True))
    self.assertEqual("Token nope is not authorized for any of: ['snarfed.org']",
                     resp.get_data(as_text=True))

  def test_reactions_missing_key(self):
    resp = self.post('reactions?token=towkin')
    self.assertEqual(400, resp.status_code, resp.get_data(as_text=True))

  def test_reactions_bad_key(self):
    resp = self.post('reactions?token=towkin&key=asdf')
    self.assertEqual(400, resp.status_code, resp.get_data(as_text=True))

  def test_reactions_token_domain_not_in_source(self):
    resp = self.post(
      f'reactions?token=towkin&key={self.other_source.urlsafe().decode()}')
    self.assertEqual(403, resp.status_code, resp.get_data(as_text=True))

  def test_reactions_wrong_activity_source(self):
    Activity(id='tag:fa.ke,2013:123_456', source=self.other_source).put()
    resp = self.post(f'reactions?id=tag:fa.ke,2013:123_456&{self.auth}')
    self.assertEqual(403, resp.status_code)
    self.assertIn(
      "tag:fa.ke,2013:123_456 is owned by Key('FakeBrowserSource', '333zzz')",
      resp.get_data(as_text=True))

  def test_poll(self):
    self.expect_task('poll-now', source_key=self.source,
                     last_polled='1970-01-01-00-00-00')
    self.mox.ReplayAll()
    resp = self.post('poll')
    self.assertEqual(200, resp.status_code, resp.get_data(as_text=True))
    self.assertEqual('OK', resp.json)

  def test_poll_missing_token(self):
    resp = self.post(f'poll?key={self.source.urlsafe().decode()}')
    self.assertEqual(400, resp.status_code, resp.get_data(as_text=True))

  def test_poll_bad_token(self):
    resp = self.post(f'poll?token=nope&key={self.source.urlsafe().decode()}')
    self.assertEqual(403, resp.status_code, resp.get_data(as_text=True))
    self.assertEqual("Token nope is not authorized for any of: ['snarfed.org']",
                     resp.get_data(as_text=True))

  def test_poll_missing_key(self):
    resp = self.post('poll?token=towkin')
    self.assertEqual(400, resp.status_code, resp.get_data(as_text=True))

  def test_poll_bad_key(self):
    resp = self.post('poll?token=towkin&key=asdf')
    self.assertEqual(400, resp.status_code, resp.get_data(as_text=True))

  def test_poll_token_domain_not_in_source(self):
    resp = self.post(
      f'poll?token=towkin&key={self.other_source.urlsafe().decode()}')
    self.assertEqual(403, resp.status_code, resp.get_data(as_text=True))

  def test_token_domains(self):
    resp = self.post('token-domains?token=towkin')
    self.assertEqual(200, resp.status_code)
    self.assertEqual(['snarfed.org'], resp.json)

  def test_token_domains_missing(self):
    resp = self.post('token-domains?token=unknown')
    self.assertEqual(404, resp.status_code)
Example #2
0
class BrowserHandlerTest(ModelsTest):
    app = webapp2.WSGIApplication(browser.routes(FakeBrowserSource))

    def setUp(self):
        super().setUp()
        self.domain = Domain(id='snarfed.org', tokens=['towkin']).put()
        FakeBrowserSource.gr_source = FakeGrSource()
        self.actor['fbs_id'] = '222yyy'
        self.source = FakeBrowserSource.new(self.handler,
                                            actor=self.actor).put()
        self.auth = f'token=towkin&key={self.source.urlsafe().decode()}'
        self.other_source = FakeBrowserSource(id='333zzz',
                                              domains=['foo.com']).put()

        for a in self.activities:
            a['object']['author'] = self.actor

        self.activities_no_extras = copy.deepcopy(self.activities)
        for a in self.activities_no_extras:
            del a['object']['tags']

        self.activities_no_replies = copy.deepcopy(self.activities_no_extras)
        for a in self.activities_no_replies:
            del a['object']['replies']

    def get_response(self, path_query, auth=True, **kwargs):
        if auth and '?' not in path_query:
            path_query += f'?{self.auth}'
        return self.app.get_response(f'/fbs/browser/{path_query}',
                                     method='POST',
                                     **kwargs)

    def test_status(self):
        resp = self.get_response('status')
        self.assertEqual(200, resp.status_int, resp.text)

        self.assertEqual(
            {
                'status': 'enabled',
                'poll-seconds': FakeBrowserSource.FAST_POLL.total_seconds(),
            }, resp.json)

    def test_homepage(self):
        resp = self.get_response('homepage', text='homepage html', auth=False)
        self.assertEqual(200, resp.status_int, resp.text)
        self.assertEqual('snarfed', resp.json)

    def test_homepage_no_logged_in_user(self):
        FakeBrowserSource.gr_source.actor = {}
        resp = self.get_response('homepage', text='not logged in', auth=False)
        self.assertEqual(400, resp.status_int)
        self.assertIn("Couldn't determine logged in FakeSource user",
                      resp.text)

    def test_profile_new_user(self):
        self.source.delete()

        self.expect_requests_get('https://snarfed.org/', '')
        self.mox.ReplayAll()

        resp = self.get_response('profile?token=towkin')

        self.assertEqual(200, resp.status_int, resp.text)
        self.assert_equals(self.source.urlsafe().decode(), resp.json)

        src = self.source.get()
        self.assertEqual('Ryan B', src.name)
        self.assertEqual(['https://snarfed.org/'], src.domain_urls)
        self.assertEqual(['snarfed.org'], src.domains)

    def test_profile_existing_user_update(self):
        self.assertIsNotNone(self.source.get())
        FakeBrowserSource.gr_source.actor.update({
            'displayName': 'Mrs. Foo',
            'image': {
                'url': 'http://foo/img'
            },
        })

        # for webmention discovery
        self.mox.ReplayAll()

        resp = self.get_response('profile?token=towkin')
        self.assertEqual(200, resp.status_int, resp.text)
        self.assert_equals(self.source.urlsafe().decode(), resp.json)

        src = self.source.get()
        self.assertEqual('Mrs. Foo', src.name)
        self.assertEqual('http://foo/img', src.picture)

    def test_profile_fall_back_to_scraped_to_actor(self):
        self.source.delete()

        self.mox.StubOutWithMock(FakeGrSource, 'scraped_to_activities')
        FakeGrSource.scraped_to_activities('').AndReturn(([], None))

        self.expect_requests_get('https://snarfed.org/', '')
        self.mox.ReplayAll()

        resp = self.get_response('profile?token=towkin')
        self.assertEqual(200, resp.status_int, resp.text)
        self.assert_equals(self.source.urlsafe().decode(), resp.json)

        src = self.source.get()
        self.assertEqual('Ryan B', src.name)
        self.assertEqual(['https://snarfed.org/'], src.domain_urls)
        self.assertEqual(['snarfed.org'], src.domains)

    def test_profile_no_scraped_actor(self):
        self.source.delete()
        FakeGrSource.actor = None
        resp = self.get_response('profile?token=towkin')
        self.assertEqual(400, resp.status_int, resp.text)
        self.assertIn('Missing actor', resp.text)

    def test_profile_private_account(self):
        FakeBrowserSource.gr_source.actor['to'] = \
          [{'objectType':'group', 'alias':'@private'}]
        resp = self.get_response('profile?token=towkin')
        self.assertEqual(400, resp.status_int)
        self.assertIn('Your FakeSource account is private.', resp.text)

    def test_profile_missing_token(self):
        resp = self.get_response('profile', auth=False)
        self.assertEqual(400, resp.status_int)
        self.assertIn('Missing required parameter: token', resp.text)

    def test_profile_no_stored_token(self):
        self.domain.delete()
        resp = self.get_response('profile?token=towkin')
        self.assertEqual(403, resp.status_int)
        self.assertIn("towkin is not authorized for any of: {'snarfed.org'}",
                      resp.text)

    def test_profile_bad_token(self):
        resp = self.get_response('profile?token=nope')
        self.assertEqual(403, resp.status_int)
        self.assertIn("nope is not authorized for any of: {'snarfed.org'}",
                      resp.text)

    def test_feed(self):
        resp = self.get_response('feed')
        self.assertEqual(200, resp.status_int, resp.text)
        self.assertEqual(self.activities_no_replies,
                         util.trim_nulls(resp.json))

    def test_feed_empty(self):
        FakeGrSource.activities = []
        resp = self.get_response('feed')
        self.assertEqual(200, resp.status_int, resp.text)
        self.assertEqual([], resp.json)

    def test_feed_missing_token(self):
        resp = self.get_response('feed?key={self.source.urlsafe().decode()}')
        self.assertEqual(400, resp.status_int, resp.text)

    def test_feed_bad_token(self):
        resp = self.get_response(
            f'feed?token=nope&key={self.source.urlsafe().decode()}')
        self.assertEqual(403, resp.status_int, resp.text)
        self.assertIn("nope is not authorized for any of: ['snarfed.org']",
                      resp.text)

    def test_feed_missing_key(self):
        resp = self.get_response('feed?token=towkin')
        self.assertEqual(400, resp.status_int, resp.text)

    def test_feed_bad_key(self):
        resp = self.get_response('feed?token=towkin&key=asdf')
        self.assertEqual(400, resp.status_int, resp.text)
        # this comes from util.load_source() since the urlsafe key is malformed
        self.assertIn('Bad value for key', resp.text)

    def test_feed_token_domain_not_in_source(self):
        resp = self.get_response(
            f'feed?token=towkin&key={self.other_source.urlsafe().decode()}')
        self.assertEqual(403, resp.status_int, resp.text)

    def test_post(self):
        resp = self.get_response('post', text='silowe html')
        self.assertEqual(200, resp.status_int, resp.text)
        self.assert_equals(self.activities_no_extras[0],
                           util.trim_nulls(resp.json))

        activities = Activity.query().fetch()
        self.assertEqual(1, len(activities))
        self.assertEqual(self.source, activities[0].source)
        self.assert_equals(
            self.activities_no_extras[0],
            util.trim_nulls(json_loads(activities[0].activity_json)))
        self.assertEqual('silowe html', activities[0].html)

    def test_post_empty(self):
        FakeGrSource.activities = []
        resp = self.get_response('post')
        self.assertEqual(400, resp.status_int)
        self.assertIn('No FakeSource post found in HTML', resp.text)

    def test_post_merge_comments(self):
        # existing activity with two comments
        activity = self.activities_no_extras[0]
        reply = self.activities[0]['object']['replies']['items'][0]
        activity['object']['replies'] = {
            'items': [reply, copy.deepcopy(reply)],
            'totalItems': 2,
        }
        activity['object']['replies']['items'][1]['id'] = 'abc'
        key = Activity(id=activity['id'],
                       activity_json=json_dumps(activity)).put()

        # scraped activity has different second comment
        activity['object']['replies']['items'][1]['id'] = 'xyz'
        FakeGrSource.activities = [activity]

        resp = self.get_response('post')
        self.assertEqual(200, resp.status_int, resp.text)
        self.assert_equals(activity, util.trim_nulls(resp.json))

        merged = json_loads(key.get().activity_json)
        replies = merged['object']['replies']
        self.assert_equals(3, replies['totalItems'], replies)
        self.assert_equals([reply['id'], 'abc', 'xyz'],
                           [r['id'] for r in replies['items']])

    def test_post_missing_key(self):
        resp = self.get_response('post?token=towkin')
        self.assertEqual(400, resp.status_int, resp.text)

    def test_post_bad_key(self):
        resp = self.get_response('post?token=towkin&key=asdf')
        self.assertEqual(400, resp.status_int, resp.text)
        # this comes from util.load_source() since the urlsafe key is malformed
        self.assertIn('Bad value for key', resp.text)

    def test_post_missing_token(self):
        resp = self.get_response(f'post?key={self.source.urlsafe().decode()}')
        self.assertEqual(400, resp.status_int, resp.text)
        self.assertIn('Missing required parameter: token', resp.text)

    def test_post_bad_token(self):
        resp = self.get_response(
            f'post?token=nope&key={self.source.urlsafe().decode()}')
        self.assertEqual(403, resp.status_int, resp.text)
        self.assertIn("nope is not authorized for any of: ['snarfed.org']",
                      resp.text)

    def test_post_token_domain_not_in_source(self):
        resp = self.get_response(
            f'post?token=towkin&key={self.other_source.urlsafe().decode()}')
        self.assertEqual(403, resp.status_int, resp.text)

    def test_reactions(self):
        key = Activity(id='tag:fa.ke,2013:123_456',
                       source=self.source,
                       activity_json=json_dumps(self.activities[0])).put()
        like = FakeBrowserSource.gr_source.like = {
            'objectType': 'activity',
            'verb': 'like',
            'id': 'new',
        }

        resp = self.get_response(
            f'reactions?id=tag:fa.ke,2013:123_456&{self.auth}')
        self.assertEqual(200, resp.status_int, resp.text)
        self.assert_equals([like], resp.json)

        stored = json_loads(key.get().activity_json)
        self.assert_equals(self.activities[0]['object']['tags'] + [like],
                           stored['object']['tags'])

    def test_reactions_bad_id(self):
        resp = self.get_response(f'reactions?id=789&{self.auth}')
        self.assertEqual(400, resp.status_int)
        self.assertIn('Expected id to be tag URI', resp.text)

    def test_reactions_bad_scraped_data(self):
        Activity(id='tag:fa.ke,2013:123_456',
                 source=self.source,
                 activity_json=json_dumps(self.activities[0])).put()

        bad_json = '<html><not><json>'
        self.mox.StubOutWithMock(FakeGrSource, 'merge_scraped_reactions')
        FakeGrSource.merge_scraped_reactions(bad_json,
                                             mox.IgnoreArg()).AndRaise(
                                                 (ValueError('fooey')))
        self.mox.ReplayAll()

        resp = self.get_response(
            f'reactions?id=tag:fa.ke,2013:123_456&{self.auth}', text=bad_json)
        self.assertEqual(400, resp.status_int)
        self.assertIn("Couldn't parse scraped reactions: fooey", resp.text)

    def test_reactions_no_activity(self):
        resp = self.get_response(
            f'reactions?id=tag:fa.ke,2013:789&{self.auth}')
        self.assertEqual(404, resp.status_int)
        self.assertIn('No FakeSource post found for id tag:fa.ke,2013:789',
                      resp.text)

    def test_reactions_missing_token(self):
        resp = self.get_response(
            f'reactions?key={self.source.urlsafe().decode()}')
        self.assertEqual(400, resp.status_int, resp.text)

    def test_reactions_bad_token(self):
        resp = self.get_response(
            f'reactions?token=nope&key={self.source.urlsafe().decode()}')
        self.assertEqual(403, resp.status_int, resp.text)
        self.assertIn("nope is not authorized for any of: ['snarfed.org']",
                      resp.text)

    def test_reactions_missing_key(self):
        resp = self.get_response('reactions?token=towkin')
        self.assertEqual(400, resp.status_int, resp.text)

    def test_reactions_bad_key(self):
        resp = self.get_response('reactions?token=towkin&key=asdf')
        self.assertEqual(400, resp.status_int, resp.text)

    def test_reactions_token_domain_not_in_source(self):
        resp = self.get_response(
            f'reactions?token=towkin&key={self.other_source.urlsafe().decode()}'
        )
        self.assertEqual(403, resp.status_int, resp.text)

    def test_reactions_wrong_activity_source(self):
        Activity(id='tag:fa.ke,2013:123_456', source=self.other_source).put()
        resp = self.get_response(
            f'reactions?id=tag:fa.ke,2013:123_456&{self.auth}')
        self.assertEqual(403, resp.status_int)
        self.assertIn(
            "tag:fa.ke,2013:123_456 is owned by Key('FakeBrowserSource', '333zzz')",
            resp.text)

    def test_poll(self):
        self.expect_task('poll',
                         eta_seconds=0,
                         source_key=self.source,
                         last_polled='1970-01-01-00-00-00')
        self.mox.ReplayAll()
        resp = self.get_response('poll')
        self.assertEqual(200, resp.status_int, resp.text)
        self.assertEqual('OK', resp.json)

    def test_poll_missing_token(self):
        resp = self.get_response('poll?key={self.source.urlsafe().decode()}')
        self.assertEqual(400, resp.status_int, resp.text)

    def test_poll_bad_token(self):
        resp = self.get_response(
            f'poll?token=nope&key={self.source.urlsafe().decode()}')
        self.assertEqual(403, resp.status_int, resp.text)
        self.assertIn("nope is not authorized for any of: ['snarfed.org']",
                      resp.text)

    def test_poll_missing_key(self):
        resp = self.get_response('poll?token=towkin')
        self.assertEqual(400, resp.status_int, resp.text)

    def test_poll_bad_key(self):
        resp = self.get_response('poll?token=towkin&key=asdf')
        self.assertEqual(400, resp.status_int, resp.text)

    def test_poll_token_domain_not_in_source(self):
        resp = self.get_response(
            f'poll?token=towkin&key={self.other_source.urlsafe().decode()}')
        self.assertEqual(403, resp.status_int, resp.text)

    def test_token_domains(self):
        resp = self.get_response('token-domains?token=towkin')
        self.assertEqual(200, resp.status_int)
        self.assertEqual(['snarfed.org'], resp.json)

    def test_token_domains_missing(self):
        resp = self.get_response('token-domains?token=unknown')
        self.assertEqual(404, resp.status_int)
Example #3
0
class InstagramTest(ModelsTest):
    def setUp(self):
        super(InstagramTest, self).setUp()
        self.handler.messages = []
        self.inst = Instagram.new(
            self.handler,
            actor={
                'objectType': 'person',
                'id': 'tag:instagram.com,2013:420973239',
                'username': '******',
                'displayName': 'Ryan Barrett',
                'url': 'https://snarfed.org/',
                'image': {
                    'url': 'http://pic.ture/url'
                },
                # ...
            })

        self.domain = Domain(id='snarfed.org', tokens=['towkin']).put()

    def expect_webmention_discovery(self):
        return self.expect_requests_get('https://snarfed.org/',
                                        '',
                                        stream=None,
                                        verify=False)

    def store_activity(self):
        activity = copy.deepcopy(HTML_PHOTO_ACTIVITY)
        activity['actor']['url'] = 'http://snarfed.org/'
        activity_json = json_dumps(activity)
        return Activity(id='tag:instagram.com,2013:123_456',
                        activity_json=activity_json).put()

    def test_new(self):
        self.assertIsNone(self.inst.auth_entity)
        self.assertEqual('snarfed', self.inst.key.string_id())
        self.assertEqual('http://pic.ture/url', self.inst.picture)
        self.assertEqual('https://www.instagram.com/snarfed/', self.inst.url)
        self.assertEqual('https://www.instagram.com/snarfed/',
                         self.inst.silo_url())
        self.assertEqual('Ryan Barrett', self.inst.name)
        self.assertEqual('snarfed (Instagram)', self.inst.label())

    def test_canonicalize_url(self):
        self.unstub_requests_head()
        for url in (
                'http://www.instagram.com/p/abcd',
                'https://www.instagram.com/p/abcd',
                'https://www.instagram.com/p/abcd/',
                'https://instagram.com/p/abcd',
        ):
            self.assertEqual('https://www.instagram.com/p/abcd/',
                             self.inst.canonicalize_url(url))

        self.assertIsNone(
            self.inst.canonicalize_url('https://www.foo.com/p/abcd/'))

    def test_canonicalize_url_approve_checks_full_url(self):
        """...specifically, that the regex ends with a $
    https://github.com/snarfed/bridgy/issues/686
    """
        self.assertEqual(
            'https://www.instagram.com/p/abcd/123/',
            self.inst.canonicalize_url('https://www.instagram.com/p/abcd/123'))

    def test_get_activities_response_activity_id(self):
        Activity(id='tag:instagram.com,2013:123',
                 activity_json=json_dumps({'foo': 'bar'})).put()

        resp = self.inst.get_activities_response(activity_id='123')
        self.assertEqual([{'foo': 'bar'}], resp['items'])

    def test_get_activities_response_no_activity_id(self):
        Activity(id='tag:instagram.com,2013:123',
                 source=self.inst.key,
                 activity_json=json_dumps({'foo': 'bar'})).put()
        Activity(id='tag:instagram.com,2013:456',
                 source=self.inst.key,
                 activity_json=json_dumps({'baz': 'biff'})).put()

        other = Instagram.new(self.handler, actor={'username': '******'}).put()
        Activity(id='tag:instagram.com,2013:789',
                 source=other,
                 activity_json=json_dumps({'boo': 'bah'})).put()

        resp = self.inst.get_activities_response()
        self.assert_equals([{'foo': 'bar'}, {'baz': 'biff'}], resp['items'])

    def test_get_activities_response_no_stored_activity(self):
        resp = self.inst.get_activities_response(activity_id='123')
        self.assertEqual([], resp['items'])

    def test_get_comment(self):
        self.assert_equals(
            HTML_VIDEO_EXTRA_COMMENT_OBJ,
            self.inst.get_comment('020', activity=HTML_VIDEO_ACTIVITY_FULL))

    def test_get_comment_no_matching_id(self):
        self.assertIsNone(
            self.inst.get_comment('333', activity=HTML_VIDEO_ACTIVITY_FULL))

    def test_get_comment_no_activity_kwarg(self):
        self.assertIsNone(self.inst.get_comment('020'))

    def test_get_like(self):
        self.assert_equals(
            LIKE_OBJS[1],
            self.inst.get_like('unused',
                               '123',
                               '9',
                               activity=HTML_PHOTO_ACTIVITY_LIKES))

    def test_get_like_no_matching_user(self):
        self.assertIsNone(
            self.inst.get_like('unused',
                               '123',
                               '222',
                               activity=HTML_PHOTO_ACTIVITY_LIKES))

    def test_get_like_no_activity_kwarg(self):
        self.assertIsNone(self.inst.get_like('unused', '123', '9'))

    def test_homepage(self):
        resp = app.application.get_response('/instagram/browser/homepage',
                                            method='POST',
                                            text=HTML_FEED_COMPLETE)
        self.assertEqual(200, resp.status_int)
        self.assertEqual('snarfed', resp.json)

    def test_homepage_bad_html(self):
        resp = app.application.get_response('/instagram/browser/homepage',
                                            method='POST',
                                            text='not a logged in IG feed')
        self.assertEqual(400, resp.status_int)
        self.assertIn("Couldn't determine logged in Instagram user", resp.text)

    def test_profile_new_user(self):
        self.assertIsNone(Instagram.get_by_id('snarfed'))

        self.expect_webmention_discovery()
        self.mox.ReplayAll()

        resp = app.application.get_response(
            '/instagram/browser/profile?token=towkin',
            method='POST',
            text=HTML_PROFILE_COMPLETE)

        self.assertEqual(200, resp.status_int)
        self.assertEqual([HTML_PHOTO_ACTIVITY, HTML_VIDEO_ACTIVITY], resp.json)

        ig = Instagram.get_by_id('snarfed')
        self.assertEqual('Ryan B', ig.name)
        self.assertEqual(
            'https://scontent-sjc2-1.cdninstagram.com/hphotos-xfa1/t51.2885-19/11373714_959073410822287_2004790583_a.jpg',
            ig.picture)
        self.assertEqual('https://www.instagram.com/snarfed/', ig.url)
        self.assertEqual(['https://snarfed.org/'], ig.domain_urls)
        self.assertEqual(['snarfed.org'], ig.domains)

    def test_profile_private_account(self):
        resp = app.application.get_response(
            '/instagram/browser/profile?token=towkin',
            method='POST',
            text=HTML_PROFILE_PRIVATE_COMPLETE)
        self.assertEqual(400, resp.status_int)
        self.assertIn('Your Instagram account is private.', resp.text)

    def test_profile_missing_token(self):
        resp = app.application.get_response('/instagram/browser/profile',
                                            method='POST',
                                            text=HTML_PROFILE_COMPLETE)
        self.assertEqual(400, resp.status_int)
        self.assertIn('Missing required parameter: token', resp.text)

    def test_profile_no_stored_token(self):
        self.domain.delete()
        resp = app.application.get_response(
            '/instagram/browser/profile?token=towkin',
            method='POST',
            text=HTML_PROFILE_COMPLETE)
        self.assertEqual(400, resp.status_int)
        self.assertIn("towkin is not authorized for any of: {'snarfed.org'}",
                      resp.text)

    def test_profile_bad_token(self):
        resp = app.application.get_response(
            '/instagram/browser/profile?token=nope',
            method='POST',
            text=HTML_PROFILE_COMPLETE)
        self.assertEqual(400, resp.status_int)
        self.assertIn('', resp.text)

    def test_post(self):
        source = Instagram.create_new(self.handler, actor={'username': '******'})

        resp = app.application.get_response(
            '/instagram/browser/post?token=towkin',
            method='POST',
            text=HTML_VIDEO_COMPLETE)
        self.assertEqual(200, resp.status_int, resp.text)
        self.assertEqual(HTML_VIDEO_ACTIVITY_FULL, resp.json)

        activities = Activity.query().fetch()
        self.assertEqual(1, len(activities))
        self.assertEqual(source.key, activities[0].source)
        self.assertEqual(HTML_VIDEO_ACTIVITY_FULL,
                         json_loads(activities[0].activity_json))

    def test_post_no_source(self):
        resp = app.application.get_response(
            '/instagram/browser/post?token=towkin',
            method='POST',
            text=HTML_VIDEO_COMPLETE)
        self.assertEqual(404, resp.status_int)
        self.assertIn('No account found for Instagram user jc', resp.text)

    def test_post_empty(self):
        empty = HTML_HEADER + json_dumps({'config': HTML_VIEWER_CONFIG
                                          }) + HTML_FOOTER
        resp = app.application.get_response(
            '/instagram/browser/post?token=towkin', method='POST', text=empty)
        self.assertEqual(400, resp.status_int)
        self.assertIn('Expected 1 Instagram post', resp.text)

    def test_post_missing_token(self):
        empty = HTML_HEADER + json_dumps({'config': HTML_VIEWER_CONFIG
                                          }) + HTML_FOOTER
        resp = app.application.get_response('/instagram/browser/post',
                                            method='POST',
                                            text=empty)
        self.assertEqual(400, resp.status_int)
        self.assertIn('Missing required parameter: token', resp.text)

    def test_post_merge_comments(self):
        source = Instagram.create_new(self.handler, actor={'username': '******'})

        # existing activity with one of the two comments in HTML_VIDEO_COMPLETE
        existing_activity = copy.deepcopy(HTML_VIDEO_ACTIVITY)
        existing_activity['object']['replies'] = {
            'totalItems': 1,
            'items': [COMMENT_OBJS[0]],
        }
        activity_key = Activity(
            id='tag:instagram.com,2013:789_456',
            activity_json=json_dumps(existing_activity)).put()

        # send HTML_VIDEO_COMPLETE to /post, check that the response and stored
        # activity have both of its comments
        resp = app.application.get_response(
            '/instagram/browser/post?token=towkin',
            method='POST',
            text=HTML_VIDEO_COMPLETE)

        self.assertEqual(200, resp.status_int, resp.text)
        self.assert_equals(HTML_VIDEO_ACTIVITY_FULL, resp.json)

        activity = activity_key.get()
        self.assert_equals(HTML_VIDEO_ACTIVITY_FULL,
                           json_loads(activity.activity_json))

    def test_likes(self):
        key = self.store_activity()

        resp = app.application.get_response(
            '/instagram/browser/likes?id=tag:instagram.com,2013:123_456&token=towkin',
            method='POST',
            text=json_dumps(HTML_PHOTO_LIKES_RESPONSE))
        self.assertEqual(200, resp.status_int, resp.text)

        expected_likes = copy.deepcopy(LIKE_OBJS)
        for like in expected_likes:
            del like['object']
            del like['url']
        self.assertEqual(expected_likes, resp.json)

        activity = json_loads(key.get().activity_json)
        self.assertEqual(expected_likes, activity['object']['tags'])

    def test_likes_bad_id(self):
        resp = app.application.get_response(
            '/instagram/browser/likes?id=789&token=towkin',
            method='POST',
            text=json_dumps(HTML_PHOTO_LIKES_RESPONSE))
        self.assertEqual(400, resp.status_int)
        self.assertIn('Expected id to be tag URI', resp.text)

    def test_likes_no_activity(self):
        resp = app.application.get_response(
            '/instagram/browser/likes?id=tag:instagram.com,2013:789&token=towkin',
            method='POST',
            text=json_dumps(HTML_PHOTO_LIKES_RESPONSE))
        self.assertEqual(404, resp.status_int)
        self.assertIn(
            'No Instagram post found for id tag:instagram.com,2013:789',
            resp.text)

    def test_likes_bad_token(self):
        self.store_activity()
        resp = app.application.get_response(
            '/instagram/browser/likes?id=tag:instagram.com,2013:123_456&token=nope',
            method='POST',
            text=json_dumps(HTML_PHOTO_LIKES_RESPONSE))
        self.assertEqual(400, resp.status_int)
        self.assertIn("nope is not authorized for any of: {'snarfed.org'}",
                      resp.text)

    def test_poll(self):
        source = Instagram.create_new(self.handler,
                                      actor={'username': '******'})
        self.expect_task('poll',
                         eta_seconds=0,
                         source_key=source.key,
                         last_polled='1970-01-01-00-00-00')
        self.mox.ReplayAll()
        resp = app.application.get_response(
            '/instagram/browser/poll?username=snarfed', method='POST')
        self.assertEqual(200, resp.status_int, resp.text)
        self.assertEqual('OK', resp.json)

    def test_poll_no_source(self):
        self.stub_create_task()
        resp = app.application.get_response(
            '/instagram/browser/poll?username=snarfed', method='POST')
        self.assertEqual(404, resp.status_int)
        self.assertIn('No account found for Instagram user snarfed', resp.text)

    def test_token_domains(self):
        resp = app.application.get_response(
            '/instagram/browser/token-domains?token=towkin', method='POST')
        self.assertEqual(200, resp.status_int)
        self.assertEqual(['snarfed.org'], resp.json)

    def test_token_domains_missing(self):
        resp = app.application.get_response(
            '/instagram/browser/token-domains?token=unknown', method='POST')
        self.assertEqual(404, resp.status_int)