diff --git a/follow.py b/follow.py index 6d4f81e..ab4667a 100644 --- a/follow.py +++ b/follow.py @@ -114,7 +114,10 @@ class FollowStart(indieauth.Start): class FollowCallback(indieauth.Callback): - """IndieAuth callback to add a follower to an existing user.""" + """IndieAuth callback to add a follower to an existing user. + + TODO: unify with UnfollowCallback. + """ def finish(self, auth_entity, state=None): if not auth_entity: return @@ -209,7 +212,17 @@ class UnfollowCallback(indieauth.Callback): followee_id = follower.dest last_follow = json_loads(follower.last_follow) followee = last_follow['object'] - inbox = last_follow['object']['inbox'] + + if isinstance(followee, str): + # fetch as AS2 to get full followee with inbox + followee_id = followee + resp = common.get_as2(followee_id) + followee = resp.json() + + inbox = followee.get('inbox') + if not inbox: + flash(f"AS2 profile {followee_id} missing id or inbox") + return redirect(f'/user/{domain}/following') timestamp = NOW.replace(microsecond=0, tzinfo=None).isoformat() unfollow_id = common.host_url(f'/user/{domain}/following#undo-{timestamp}-{followee_id}') diff --git a/tests/test_follow.py b/tests/test_follow.py index 404a099..1161939 100644 --- a/tests/test_follow.py +++ b/tests/test_follow.py @@ -252,13 +252,34 @@ class UnfollowTest(testutil.TestCase): resp.headers['Location']) def test_callback(self, mock_get, mock_post): + mock_get.return_value = requests_response('') + self._test_callback(UNDO_FOLLOW, mock_get, mock_post) + + def test_callback_last_follow_object_str(self, mock_get, mock_post): + follower = self.follower.get() + follower.last_follow = json_dumps({ + **FOLLOW_ADDRESS, + 'object': FOLLOWEE['id'], + }) + follower.put() + + # oauth-dropins indieauth https://snarfed.org fetch for user json + mock_get.side_effect = ( + requests_response(''), + self.as2_resp(FOLLOWEE), # fetch to discover inbox + ) + + undo = copy.deepcopy(UNDO_FOLLOW) + undo['object']['object'] = FOLLOWEE['id'] + + self._test_callback(undo, mock_get, mock_post) + + def _test_callback(self, expected_undo, mock_get, mock_post): User.get_or_create('snarfed.org') mock_post.side_effect = ( requests_response('me=https://snarfed.org'), requests_response('OK'), # AP Undo Follow to inbox ) - # oauth-dropins indieauth https://snarfed.org fetch for user json - mock_get.return_value = requests_response('') state = util.encode_oauth_state({ 'endpoint': 'http://auth/endpoint', @@ -274,7 +295,7 @@ class UnfollowTest(testutil.TestCase): inbox_args, inbox_kwargs = mock_post.call_args_list[1] self.assertEqual(('http://bar/inbox',), inbox_args) - self.assert_equals(UNDO_FOLLOW, json_loads(inbox_kwargs['data'])) + self.assert_equals(expected_undo, json_loads(inbox_kwargs['data'])) follower = Follower.get_by_id('https://bar/id snarfed.org') self.assertEqual('inactive', follower.status) @@ -283,16 +304,4 @@ class UnfollowTest(testutil.TestCase): 'http://localhost/user/snarfed.org/following#undo-2022-01-02T03:04:05-https://bar/id', domains=['snarfed.org'], status='complete', source_protocol='ui', labels=['user', 'activity'], - as2=UNDO_FOLLOW, as1=as2.to_as1(UNDO_FOLLOW)) - - def test_callback_missing_user(self, mock_get, mock_post): - mock_post.return_value = requests_response('me=https://snarfed.org') - - state = util.encode_oauth_state({ - 'endpoint': 'http://auth/endpoint', - 'me': 'https://snarfed.org', - 'state': 'https://bar/id', - }) - with self.client: - resp = self.client.get(f'/unfollow/callback?code=my_code&state={state}') - self.assertEqual(400, resp.status_code) + as2=expected_undo, as1=as2.to_as1(expected_undo))