Repository: praw-dev/praw

Issue:
Redditor.friend() raises LoginRequired through OAuth; doesn't use suggested URL
I decided to try playing around with reddit's Friend system today, and found that Redditor.friend() raises a LoginRequired when used through OAuth.

First, I found that internal.py [chooses a restrict_access decorator with scope=None](https://github.com/praw-dev/praw/blob/c05bab70ff4b26967ca0b114436e0edad24aa6aa/praw/internal.py#L89) causing [all these checks](https://github.com/praw-dev/praw/blob/31d4984fd66930dbc886dfedb27c835dc6609262/praw/decorators.py#L329-L342) to fail until it finally gets to `elif login` and raises the exception. I tried changing that to scope='subscribers', but this causes an OAuthException with the message _"praw.errors.OAuthException: Bearer realm="reddit", error="invalid_request" on url https://oauth.reddit.com/api/friend/.json"_

&nbsp;

At this point I looked at the [api docs](https://www.reddit.com/dev/api/oauth#POST_api_friend) and saw that they actually want you to use [PUT /api/v1/me/friends/username](https://www.reddit.com/dev/api/oauth#PUT_api_v1_me_friends_%7Busername%7D) instead (ostensibly so that /api/friend will one day control everything except adding friends). I tried this:

```
@restrict_access(scope='subscribe')
def friend(self):
    # friend_v1 = 'api/v1/me/friends/%s'
    url = self.reddit_session.config['friend_v1'] % self.name
    data = {'name': self.name, 'note': 'u'}
    method = 'PUT'
    self.reddit_session.evict(self.reddit_session.config['friends'])
    self.reddit_session.request_json(url, data=data, method=method)
```

But I'm still getting HTTPException 400 errors.

&nbsp;

I'd love to make a pull request, but I'm not sure what to try next. Any insight?



Discussion:
> This endpoint expects JSON data of this format

I think you need to manually build the json request body. You may be able to do that by doing `data=json.dumps(data)`, but `request_json` or one if its methods may mess things up by trying to add other parameters to data (you'll have to work around that).

Wow, that did it. Why on earth do they want a string?

The json format is much simpler than x-www-form-urlencoded and many modern APIs use it. It's just confusing because the content type is not consistent across all of reddit's endpoints (I think there is even discrepancy within v1).


Resolving patch:
diff --git a/praw/__init__.py b/praw/__init__.py
--- a/praw/__init__.py
+++ b/praw/__init__.py
@@ -102,6 +102,7 @@ class Config(object):  # pylint: disable=R0903
                  'flairselector':       'api/flairselector/',
                  'flairtemplate':       'api/flairtemplate/',
                  'friend':              'api/friend/',
+                 'friend_v1':           'api/v1/me/friends/{user}',
                  'friends':             'prefs/friends/',
                  'gild_thing':          'api/v1/gold/gild/{fullname}/',
                  'gild_user':           'api/v1/gold/give/{username}/',
@@ -612,6 +613,12 @@ def request_json(self, url, params=None, data=None, as_objects=True,
         hook = self._json_reddit_objecter if as_objects else None
         # Request url just needs to be available for the objecter to use
         self._request_url = url  # pylint: disable=W0201
+
+        if response == '':
+            # Some of the v1 urls don't return anything, even when they're
+            # successful.
+            return response
+
         data = json.loads(response, object_hook=hook)
         delattr(self, '_request_url')
         # Update the modhash
@@ -1321,6 +1328,12 @@ def get_flair_choices(self, subreddit, link=None):
         data = {'r':  six.text_type(subreddit), 'link': link}
         return self.request_json(self.config['flairselector'], data=data)
 
+    @decorators.restrict_access(scope='read', login=True)
+    def get_friends(self, **params):
+        """Return a UserList of Redditors with whom the user is friends."""
+        url = self.config['friends']
+        return self.request_json(url, params=params)[0]
+
     @decorators.restrict_access(scope='identity', oauth_only=True)
     def get_me(self):
         """Return a LoggedInRedditor object.
diff --git a/praw/internal.py b/praw/internal.py
--- a/praw/internal.py
+++ b/praw/internal.py
@@ -158,10 +158,15 @@ def _prepare_request(reddit_session, url, params, data, auth, files,
     # Most POST requests require adding `api_type` and `uh` to the data.
     if data is True:
         data = {}
-    if not auth:
-        data.setdefault('api_type', 'json')
-        if reddit_session.modhash:
-            data.setdefault('uh', reddit_session.modhash)
+
+    if isinstance(data, dict):
+        if not auth:
+            data.setdefault('api_type', 'json')
+            if reddit_session.modhash:
+                data.setdefault('uh', reddit_session.modhash)
+    else:
+        request.headers.setdefault('Content-Type', 'application/json')
+
     request.data = data
     request.files = files
     return request
diff --git a/praw/objects.py b/praw/objects.py
--- a/praw/objects.py
+++ b/praw/objects.py
@@ -843,14 +843,38 @@ def _post_populate(self, fetch):
             self._case_name = self.name
             self.name = tmp
 
-    def friend(self):
+    @restrict_access(scope='subscribe')
+    def friend(self, note=None, _unfriend=False):
         """Friend the user.
 
+        :param note: A personal note about the user. Requires reddit Gold.
+        :param _unfriend: Unfriend the user. Please use :meth:`unfriend`
+            instead of setting this parameter manually.
+
         :returns: The json response from the server.
 
         """
         self.reddit_session.evict(self.reddit_session.config['friends'])
-        return _modify_relationship('friend')(self.reddit_session.user, self)
+
+        # Requests through password auth use /api/friend
+        # Requests through oauth use /api/v1/me/friends/%username%
+        if not self.reddit_session.is_oauth_session():
+            modifier = _modify_relationship('friend', unlink=_unfriend)
+            data = {'note': note} if note else {}
+            return modifier(self.reddit_session.user, self, **data)
+
+        url = self.reddit_session.config['friend_v1'].format(user=self.name)
+        # This endpoint wants the data to be a string instead of an actual
+        # dictionary, although it is not required to have any content for adds.
+        # Unfriending does require the 'id' key.
+        if _unfriend:
+            data = {'id': self.name}
+        else:
+            # We cannot send a null or empty note string.
+            data = {'note': note} if note else {}
+        data = dumps(data)
+        method = 'DELETE' if _unfriend else 'PUT'
+        return self.reddit_session.request_json(url, data=data, method=method)
 
     def get_disliked(self, *args, **kwargs):
         """Return a listing of the Submissions the user has downvoted.
@@ -881,6 +905,20 @@ def get_downvoted(self, *args, **kwargs):
         kwargs['_use_oauth'] = self.reddit_session.is_oauth_session()
         return _get_redditor_listing('downvoted')(self, *args, **kwargs)
 
+    @restrict_access(scope='mysubreddits')
+    def get_friend_info(self):
+        """Return information about this friend, including personal notes.
+
+        The personal note can be added or overwritten with :meth:friend, but
+            only if the user has reddit Gold.
+
+        :returns: The json response from the server.
+
+        """
+        url = self.reddit_session.config['friend_v1'].format(user=self.name)
+        data = {'id': self.name}
+        return self.reddit_session.request_json(url, data=data, method='GET')
+
     def get_liked(self, *args, **kwargs):
         """Return a listing of the Submissions the user has upvoted.
 
@@ -935,9 +973,7 @@ def unfriend(self):
         :returns: The json response from the server.
 
         """
-        self.reddit_session.evict(self.reddit_session.config['friends'])
-        return _modify_relationship('friend', unlink=True)(
-            self.reddit_session.user, self)
+        return self.friend(_unfriend=True)
 
 
 class LoggedInRedditor(Redditor):
@@ -965,10 +1001,16 @@ def get_cached_moderated_reddits(self):
                 self._mod_subs[six.text_type(sub).lower()] = sub
         return self._mod_subs
 
+    @deprecated(':meth:`get_friends` has been moved to '
+                ':class:`praw.AuthenticatedReddit` and will be removed from '
+                ':class:`objects.LoggedInRedditor` in PRAW v4.0.0')
     def get_friends(self, **params):
-        """Return a UserList of Redditors with whom the user has friended."""
-        url = self.reddit_session.config['friends']
-        return self.reddit_session.request_json(url, params=params)[0]
+        """Return a UserList of Redditors with whom the user is friends.
+
+        This method has been moved to :class:`praw.AuthenticatedReddit.
+
+        """
+        return self.reddit_session.get_friends(**params)
 
 
 class ModAction(RedditContentObject):


Test patch:
diff --git a/tests/cassettes/test_friends_oauth.json b/tests/cassettes/test_friends_oauth.json
new file mode 100644
--- /dev/null
+++ b/tests/cassettes/test_friends_oauth.json
@@ -0,0 +1 @@
+{"recorded_with": "betamax/0.4.2", "http_interactions": [{"response": {"body": {"encoding": "UTF-8", "base64_string": "H4sIAAAAAAAAA6pWSkxOTi0uji/Jz07NU7JSUDI3NjCyMDPXdS4oDgxwKfByzfILMU8yi6wqiAo3yzYLtyzNV9JRUAKrjy+pLEgFaUpKTSxKLQKJp1YUZBalFsdnggwzNjMw0FFQKk7OhygrLk0qTi7KTEpVqgUAAAD//wMAZlrF43kAAAA="}, "url": "https://api.reddit.com/api/v1/access_token/", "headers": {"connection": ["keep-alive"], "server": ["cloudflare-nginx"], "set-cookie": ["__cfduid=dbfab20b40c347a3f6ae70df0dc539d481440570943; expires=Thu, 25-Aug-16 06:35:43 GMT; path=/; domain=.reddit.com; HttpOnly"], "transfer-encoding": ["chunked"], "content-type": ["application/json; charset=UTF-8"], "x-moose": ["majestic"], "content-encoding": ["gzip"], "date": ["Wed, 26 Aug 2015 06:35:43 GMT"], "cache-control": ["max-age=0, must-revalidate"], "x-xss-protection": ["1; mode=block"], "strict-transport-security": ["max-age=15552000; includeSubDomains; preload"], "x-content-type-options": ["nosniff"], "x-frame-options": ["SAMEORIGIN"], "cf-ray": ["21bd84aa3c7f1413-LAX"]}, "status": {"code": 200, "message": "OK"}}, "recorded_at": "2015-08-26T06:35:41", "request": {"body": {"string": "grant_type=refresh_token&redirect_uri=https%3A%2F%2F127.0.0.1%3A65010%2Fauthorize_callback&refresh_token=LlqwOLjyu_l6GMZIBqhcLWB0hAE", "encoding": "utf-8"}, "uri": "https://api.reddit.com/api/v1/access_token/", "method": "POST", "headers": {"Authorization": ["Basic c3RKbFVTVWJQUWU1bFE6aVUtTHNPenlKSDdCRFZvcS1xT1dORXEyenVJ"], "Accept": ["*/*"], "Connection": ["keep-alive"], "Accept-Encoding": ["gzip, deflate"], "User-Agent": ["PRAW_test_suite PRAW/3.2.1 Python/3.4.3 b'Windows-7-6.1.7601-SP1'"], "Content-Type": ["application/x-www-form-urlencoded"], "Content-Length": ["132"]}}}, {"response": {"body": {"string": "{\"fields\": [\"note\"], \"explanation\": \"you must have an active reddit gold subscription to do that\", \"reason\": \"GOLD_REQUIRED\"}", "encoding": "UTF-8"}, "url": "https://oauth.reddit.com/api/v1/me/friends/PyAPITestUser3.json", "headers": {"x-ratelimit-used": ["2"], "server": ["cloudflare-nginx"], "set-cookie": ["__cfduid=d51d9956cff66a4d8321b1e82c334037e1440570943; expires=Thu, 25-Aug-16 06:35:43 GMT; path=/; domain=.reddit.com; HttpOnly"], "x-frame-options": ["SAMEORIGIN"], "content-type": ["application/json; charset=UTF-8"], "x-moose": ["majestic"], "x-ratelimit-remaining": ["598.0"], "x-ua-compatible": ["IE=edge"], "x-xss-protection": ["1; mode=block"], "cache-control": ["private, s-maxage=0, max-age=0, must-revalidate", "max-age=0, must-revalidate"], "connection": ["keep-alive"], "date": ["Wed, 26 Aug 2015 06:35:43 GMT"], "x-ratelimit-reset": ["257"], "strict-transport-security": ["max-age=15552000; includeSubDomains; preload"], "x-content-type-options": ["nosniff"], "content-length": ["125"], "expires": ["-1"], "cf-ray": ["21bd84ad935022a0-LAX"]}, "status": {"code": 400, "message": "Bad Request"}}, "recorded_at": "2015-08-26T06:35:41", "request": {"body": {"string": "{\"note\": \"note\"}", "encoding": "utf-8"}, "uri": "https://oauth.reddit.com/api/v1/me/friends/PyAPITestUser3.json", "method": "PUT", "headers": {"Authorization": ["bearer 7302867-CpsQPDpJEjNT7b6YzpZW6k6W9uo"], "Accept": ["*/*"], "Connection": ["keep-alive"], "Accept-Encoding": ["gzip, deflate"], "User-Agent": ["PRAW_test_suite PRAW/3.2.1 Python/3.4.3 b'Windows-7-6.1.7601-SP1'"], "Content-Type": ["application/json"], "Content-Length": ["16"]}}}, {"response": {"body": {"string": "{\"date\": 1440570943.0, \"name\": \"PyAPITestUser3\", \"id\": \"t2_6c1xj\"}", "encoding": "UTF-8"}, "url": "https://oauth.reddit.com/api/v1/me/friends/PyAPITestUser3.json", "headers": {"x-ratelimit-used": ["3"], "server": ["cloudflare-nginx"], "set-cookie": ["__cfduid=d51d9956cff66a4d8321b1e82c334037e1440570943; expires=Thu, 25-Aug-16 06:35:43 GMT; path=/; domain=.reddit.com; HttpOnly"], "x-frame-options": ["SAMEORIGIN"], "content-type": ["application/json; charset=UTF-8"], "x-moose": ["majestic"], "x-ratelimit-reset": ["257"], "x-ratelimit-remaining": ["597.0"], "x-xss-protection": ["1; mode=block"], "cache-control": ["private, s-maxage=0, max-age=0, must-revalidate", "max-age=0, must-revalidate"], "connection": ["keep-alive"], "date": ["Wed, 26 Aug 2015 06:35:43 GMT"], "strict-transport-security": ["max-age=15552000; includeSubDomains; preload"], "x-content-type-options": ["nosniff"], "content-length": ["66"], "expires": ["-1"], "cf-ray": ["21bd84ae735822a0-LAX"]}, "status": {"code": 201, "message": "Created"}}, "recorded_at": "2015-08-26T06:35:42", "request": {"body": {"string": "{}", "encoding": "utf-8"}, "uri": "https://oauth.reddit.com/api/v1/me/friends/PyAPITestUser3.json", "method": "PUT", "headers": {"Authorization": ["bearer 7302867-CpsQPDpJEjNT7b6YzpZW6k6W9uo"], "Accept": ["*/*"], "Connection": ["keep-alive"], "Accept-Encoding": ["gzip, deflate"], "User-Agent": ["PRAW_test_suite PRAW/3.2.1 Python/3.4.3 b'Windows-7-6.1.7601-SP1'"], "Content-Type": ["application/json"], "Content-Length": ["2"]}}}, {"response": {"body": {"encoding": "UTF-8", "base64_string": "H4sIAAAAAAAAA2SPsQqDMBRFfyW8WUqNWKhbx24O7fxIzUt91SSQxNIi/nuJQxFcD+fAvTMM7DQ0ApKEQoBWSUEjZnDKUsbt99JebxTTPVKossIRTWBaK6PGSIWALpBKlElZSVnX8lxXh2MhoGdNaIK3GPzDp7hvcErdvhvZDTioYPOaMtveWnLpz7LEEZ9+3O7giNZvQa8ivimwYdJIVvEIjUhhWuX196krPy9Ylh8AAAD//wMAK4nJkwoBAAA="}, "url": "https://api.reddit.com/user/PyAPITestUser3/about/.json", "headers": {"connection": ["keep-alive"], "server": ["cloudflare-nginx"], "x-frame-options": ["SAMEORIGIN"], "transfer-encoding": ["chunked"], "content-type": ["application/json; charset=UTF-8"], "x-moose": ["majestic"], "x-ua-compatible": ["IE=edge"], "content-encoding": ["gzip"], "cache-control": ["max-age=0, must-revalidate"], "access-control-allow-origin": ["*"], "date": ["Wed, 26 Aug 2015 06:35:44 GMT"], "x-reddit-tracking": ["https://pixel.redditmedia.com/pixel/of_destiny.png?v=a1F8dTU0p4RfH24s95%2BiRD1fBaZBy1d2xVjexwqCj%2B%2FZ8Q6CupfRyQ9gjwW%2B%2BjAn%2BCuS9y7didg%3D"], "x-xss-protection": ["1; mode=block"], "x-content-type-options": ["nosniff"], "strict-transport-security": ["max-age=15552000; includeSubDomains; preload"], "access-control-expose-headers": ["X-Reddit-Tracking, X-Moose"], "cf-ray": ["21bd84af6cba1413-LAX"]}, "status": {"code": 200, "message": "OK"}}, "recorded_at": "2015-08-26T06:35:42", "request": {"body": {"string": "", "encoding": "utf-8"}, "uri": "https://api.reddit.com/user/PyAPITestUser3/about/.json", "method": "GET", "headers": {"User-Agent": ["PRAW_test_suite PRAW/3.2.1 Python/3.4.3 b'Windows-7-6.1.7601-SP1'"], "Accept": ["*/*"], "Connection": ["keep-alive"], "Cookie": ["__cfduid=d51d9956cff66a4d8321b1e82c334037e1440570943"], "Accept-Encoding": ["gzip, deflate"]}}}, {"response": {"body": {"encoding": "UTF-8", "base64_string": "H4sIAAAAAAAAA6pWSkxOTi0uji/Jz07NU7JSUDI3NjCyMDPX9XML180p0/Wryo4PqLRMDXQpScxx83ZyyXDJVtJRUAKrjy+pLEgFaUpKTSxKLQKJp1YUZBalFsdnggwzNjMw0FFQKk7OhyjLrSwuTSpKTUnJLClWqgUAAAD//wMA6XtVbnwAAAA="}, "url": "https://api.reddit.com/api/v1/access_token/", "headers": {"connection": ["keep-alive"], "server": ["cloudflare-nginx"], "x-frame-options": ["SAMEORIGIN"], "transfer-encoding": ["chunked"], "content-type": ["application/json; charset=UTF-8"], "x-moose": ["majestic"], "content-encoding": ["gzip"], "date": ["Wed, 26 Aug 2015 06:35:44 GMT"], "cache-control": ["max-age=0, must-revalidate"], "x-xss-protection": ["1; mode=block"], "strict-transport-security": ["max-age=15552000; includeSubDomains; preload"], "x-content-type-options": ["nosniff"], "cf-ray": ["21bd84b18cc71413-LAX"]}, "status": {"code": 200, "message": "OK"}}, "recorded_at": "2015-08-26T06:35:42", "request": {"body": {"string": "grant_type=refresh_token&redirect_uri=https%3A%2F%2F127.0.0.1%3A65010%2Fauthorize_callback&refresh_token=O7tfWhqem6fQZqxhoTiLca1s7VA", "encoding": "utf-8"}, "uri": "https://api.reddit.com/api/v1/access_token/", "method": "POST", "headers": {"Authorization": ["Basic c3RKbFVTVWJQUWU1bFE6aVUtTHNPenlKSDdCRFZvcS1xT1dORXEyenVJ"], "Accept": ["*/*"], "Connection": ["keep-alive"], "Cookie": ["__cfduid=d51d9956cff66a4d8321b1e82c334037e1440570943"], "User-Agent": ["PRAW_test_suite PRAW/3.2.1 Python/3.4.3 b'Windows-7-6.1.7601-SP1'"], "Content-Type": ["application/x-www-form-urlencoded"], "Content-Length": ["132"], "Accept-Encoding": ["gzip, deflate"]}}}, {"response": {"body": {"encoding": "UTF-8", "base64_string": "H4sIAAAAAAAAA6pWSkksSVWyUjA0MTEwNTewNDHWM9BRUMpLzAWJKgVUOgZ4hqQWl4QWpxYZK+koKGWmgMRLjOLNkg0rspRqAQAAAP//AwC0C6M5QgAAAA=="}, "url": "https://oauth.reddit.com/api/v1/me/friends/PyAPITestUser3.json", "headers": {"x-ratelimit-used": ["4"], "server": ["cloudflare-nginx"], "set-cookie": ["__cfduid=dcfacd553cdaabb519e3a258c4c7f962d1440570944; expires=Thu, 25-Aug-16 06:35:44 GMT; path=/; domain=.reddit.com; HttpOnly"], "transfer-encoding": ["chunked"], "content-type": ["application/json; charset=UTF-8"], "x-moose": ["majestic"], "x-ratelimit-reset": ["256"], "x-ratelimit-remaining": ["596.0"], "date": ["Wed, 26 Aug 2015 06:35:44 GMT"], "cache-control": ["private, s-maxage=0, max-age=0, must-revalidate", "max-age=0, must-revalidate"], "strict-transport-security": ["max-age=15552000; includeSubDomains; preload"], "content-encoding": ["gzip"], "connection": ["keep-alive"], "x-xss-protection": ["1; mode=block"], "x-content-type-options": ["nosniff"], "x-frame-options": ["SAMEORIGIN"], "expires": ["-1"], "cf-ray": ["21bd84b2636a22a0-LAX"]}, "status": {"code": 200, "message": "OK"}}, "recorded_at": "2015-08-26T06:35:42", "request": {"body": {"string": "", "encoding": "utf-8"}, "uri": "https://oauth.reddit.com/api/v1/me/friends/PyAPITestUser3.json", "method": "GET", "headers": {"Authorization": ["bearer 7302867-NFW-lv-Nzk_Py9eQDtalFKBDhDk"], "User-Agent": ["PRAW_test_suite PRAW/3.2.1 Python/3.4.3 b'Windows-7-6.1.7601-SP1'"], "Connection": ["keep-alive"], "Accept": ["*/*"], "Accept-Encoding": ["gzip, deflate"]}}}, {"response": {"body": {"encoding": "UTF-8", "base64_string": "H4sIAAAAAAAAA6pWSkxOTi0uji/Jz07NU7JSUDI3NjCyMDPXrTJzNs30rzQK9NR1KQiNN3dPD3XJN9J1DPLIV9JRUAKrjy+pLEgFaUpKTSxKLQKJp1YUZBalFsdnggwzNjMw0FFQKk7OhygrSk1MUaoFAAAA//8DAFnIJVd0AAAA"}, "url": "https://api.reddit.com/api/v1/access_token/", "headers": {"connection": ["keep-alive"], "server": ["cloudflare-nginx"], "x-frame-options": ["SAMEORIGIN"], "transfer-encoding": ["chunked"], "content-type": ["application/json; charset=UTF-8"], "x-moose": ["majestic"], "content-encoding": ["gzip"], "date": ["Wed, 26 Aug 2015 06:35:45 GMT"], "cache-control": ["max-age=0, must-revalidate"], "x-xss-protection": ["1; mode=block"], "strict-transport-security": ["max-age=15552000; includeSubDomains; preload"], "x-content-type-options": ["nosniff"], "cf-ray": ["21bd84b49ce71413-LAX"]}, "status": {"code": 200, "message": "OK"}}, "recorded_at": "2015-08-26T06:35:43", "request": {"body": {"string": "grant_type=refresh_token&redirect_uri=https%3A%2F%2F127.0.0.1%3A65010%2Fauthorize_callback&refresh_token=_mmtb8YjDym0eC26G-rTxXUMea0", "encoding": "utf-8"}, "uri": "https://api.reddit.com/api/v1/access_token/", "method": "POST", "headers": {"Authorization": ["Basic c3RKbFVTVWJQUWU1bFE6aVUtTHNPenlKSDdCRFZvcS1xT1dORXEyenVJ"], "Accept": ["*/*"], "Connection": ["keep-alive"], "Cookie": ["__cfduid=dcfacd553cdaabb519e3a258c4c7f962d1440570944"], "User-Agent": ["PRAW_test_suite PRAW/3.2.1 Python/3.4.3 b'Windows-7-6.1.7601-SP1'"], "Content-Type": ["application/x-www-form-urlencoded"], "Content-Length": ["132"], "Accept-Encoding": ["gzip, deflate"]}}}, {"response": {"body": {"encoding": "UTF-8", "base64_string": "H4sIAAAAAAAAA4yOPQvCMBgG/8rLMxdJbVI1m6Pg0EGnEqQfAVM1SpNBDfnvkm7ZXA/uuDbgZuwISTg7PR+N8ygIY+c7SAoYruY+ztpCUhsS1pBUcs74VnBRrVhBsN0jUfT9UyfZLDm/vlSvb60RC8pMsWE7npvNZ98cTtr59FBljXoo3xOiikvm71cVo/oBAAD//wMA8Hm2kdwAAAA="}, "url": "https://oauth.reddit.com/prefs/friends/.json", "headers": {"x-ratelimit-used": ["5"], "server": ["cloudflare-nginx"], "set-cookie": ["__cfduid=dffa64eb3980af2982a62afec11f2fbe11440570945; expires=Thu, 25-Aug-16 06:35:45 GMT; path=/; domain=.reddit.com; HttpOnly"], "transfer-encoding": ["chunked"], "content-type": ["application/json; charset=UTF-8"], "x-moose": ["majestic"], "x-ratelimit-remaining": ["595.0"], "x-ua-compatible": ["IE=edge"], "date": ["Wed, 26 Aug 2015 06:35:45 GMT"], "connection": ["keep-alive"], "cache-control": ["private, s-maxage=0, max-age=0, must-revalidate", "max-age=0, must-revalidate"], "strict-transport-security": ["max-age=15552000; includeSubDomains; preload"], "content-encoding": ["gzip"], "x-ratelimit-reset": ["255"], "x-reddit-tracking": ["https://pixel.redditmedia.com/pixel/of_destiny.png?v=z%2FUU9bl2%2FDxqL2Ui%2BXXj%2Fud5RIcTJqljWl9%2BQCEyV2voMh1ns0FvhGp%2BsSS7bZCNeva0U%2FpmKT6YW6fhEgLpKZe5fztZndwh"], "x-xss-protection": ["1; mode=block"], "x-content-type-options": ["nosniff"], "x-frame-options": ["SAMEORIGIN"], "expires": ["-1"], "cf-ray": ["21bd84b6e38722a0-LAX"]}, "status": {"code": 200, "message": "OK"}}, "recorded_at": "2015-08-26T06:35:43", "request": {"body": {"string": "", "encoding": "utf-8"}, "uri": "https://oauth.reddit.com/prefs/friends/.json", "method": "GET", "headers": {"Authorization": ["bearer 7302867-z6C5iOy2QI-DpU_7GgUDo2-ARHo"], "User-Agent": ["PRAW_test_suite PRAW/3.2.1 Python/3.4.3 b'Windows-7-6.1.7601-SP1'"], "Connection": ["keep-alive"], "Accept": ["*/*"], "Accept-Encoding": ["gzip, deflate"]}}}, {"response": {"body": {"string": "", "encoding": "UTF-8"}, "url": "https://oauth.reddit.com/api/v1/me/friends/PyAPITestUser3.json", "headers": {"x-ratelimit-used": ["6"], "server": ["cloudflare-nginx"], "set-cookie": ["__cfduid=dffa64eb3980af2982a62afec11f2fbe11440570945; expires=Thu, 25-Aug-16 06:35:45 GMT; path=/; domain=.reddit.com; HttpOnly"], "x-frame-options": ["SAMEORIGIN"], "content-type": ["application/json; charset=UTF-8"], "x-moose": ["majestic"], "x-ratelimit-reset": ["255"], "x-ratelimit-remaining": ["594.0"], "x-xss-protection": ["1; mode=block"], "cache-control": ["private, s-maxage=0, max-age=0, must-revalidate", "max-age=0, must-revalidate"], "connection": ["keep-alive"], "date": ["Wed, 26 Aug 2015 06:35:45 GMT"], "strict-transport-security": ["max-age=15552000; includeSubDomains; preload"], "x-content-type-options": ["nosniff"], "content-length": ["0"], "expires": ["-1"], "cf-ray": ["21bd84b9139322a0-LAX"]}, "status": {"code": 204, "message": "No Content"}}, "recorded_at": "2015-08-26T06:35:44", "request": {"body": {"string": "{\"id\": \"PyAPITestUser3\"}", "encoding": "utf-8"}, "uri": "https://oauth.reddit.com/api/v1/me/friends/PyAPITestUser3.json", "method": "DELETE", "headers": {"Authorization": ["bearer 7302867-CpsQPDpJEjNT7b6YzpZW6k6W9uo"], "Accept": ["*/*"], "Connection": ["keep-alive"], "Accept-Encoding": ["gzip, deflate"], "User-Agent": ["PRAW_test_suite PRAW/3.2.1 Python/3.4.3 b'Windows-7-6.1.7601-SP1'"], "Content-Type": ["application/json"], "Content-Length": ["24"]}}}, {"response": {"body": {"encoding": "UTF-8", "base64_string": "H4sIAAAAAAAAA2SPsQqDMBRFfyW8WUqNWKhbx24O7fxIzUt91SSQxNIi/nuJQxFcD+fAvTMM7DQ0ApKEQoBWSUEjZnDKUsbt99JebxTTPVKossIRTWBaK6PGSIWALpBKlElZSVnX8lxXh2MhoGdNaIK3GPzDp7hvcErdvhvZDTioYPOaMtveWnLpz7LEEZ9+3O7giNZvQa8ivimwYdJIVvEIjUhhWuX196krPy9Ylh8AAAD//wMAK4nJkwoBAAA="}, "url": "https://api.reddit.com/user/PyAPITestUser3/about/.json?uniq=1", "headers": {"connection": ["keep-alive"], "server": ["cloudflare-nginx"], "x-frame-options": ["SAMEORIGIN"], "transfer-encoding": ["chunked"], "content-type": ["application/json; charset=UTF-8"], "x-moose": ["majestic"], "x-ua-compatible": ["IE=edge"], "content-encoding": ["gzip"], "cache-control": ["max-age=0, must-revalidate"], "access-control-allow-origin": ["*"], "date": ["Wed, 26 Aug 2015 06:35:46 GMT"], "x-reddit-tracking": ["https://pixel.redditmedia.com/pixel/of_destiny.png?v=DjXAucekmGU%2FnCJKF2pS6zxzUmQhnQfTxCR7K3JVyyBuaTVh9x%2FgM4uRwRfzENLRP0lB30DU0aQ%3D"], "x-xss-protection": ["1; mode=block"], "x-content-type-options": ["nosniff"], "strict-transport-security": ["max-age=15552000; includeSubDomains; preload"], "access-control-expose-headers": ["X-Reddit-Tracking, X-Moose"], "cf-ray": ["21bd84bb5d1a1413-LAX"]}, "status": {"code": 200, "message": "OK"}}, "recorded_at": "2015-08-26T06:35:44", "request": {"body": {"string": "", "encoding": "utf-8"}, "uri": "https://api.reddit.com/user/PyAPITestUser3/about/.json?uniq=1", "method": "GET", "headers": {"User-Agent": ["PRAW_test_suite PRAW/3.2.1 Python/3.4.3 b'Windows-7-6.1.7601-SP1'"], "Accept": ["*/*"], "Connection": ["keep-alive"], "Cookie": ["__cfduid=dffa64eb3980af2982a62afec11f2fbe11440570945"], "Accept-Encoding": ["gzip, deflate"]}}}]}
\ No newline at end of file
diff --git a/tests/helper.py b/tests/helper.py
--- a/tests/helper.py
+++ b/tests/helper.py
@@ -7,6 +7,7 @@
 import unittest
 from betamax import Betamax, BaseMatcher
 from betamax_matchers.form_urlencoded import URLEncodedBodyMatcher
+from betamax_matchers.json_body import JSONBodyMatcher
 from functools import wraps
 from praw import Reddit
 from requests.compat import urljoin
@@ -30,7 +31,8 @@ def match(self, request, recorded_request):
         to_compare = request.copy()
         to_compare.body = text_type(to_compare.body)
 
-        return URLEncodedBodyMatcher().match(to_compare, recorded_request)
+        return URLEncodedBodyMatcher().match(to_compare, recorded_request) or \
+            JSONBodyMatcher().match(to_compare, recorded_request)
 
 
 class PRAWTest(unittest.TestCase):
diff --git a/tests/test_redditor.py b/tests/test_redditor.py
--- a/tests/test_redditor.py
+++ b/tests/test_redditor.py
@@ -1,9 +1,10 @@
 """Tests for Redditor class."""
 
 from __future__ import print_function, unicode_literals
+from praw import errors
 from praw.objects import LoggedInRedditor
 from six import text_type
-from .helper import PRAWTest, betamax
+from .helper import OAuthPRAWTest, PRAWTest, betamax
 
 
 class RedditorTest(PRAWTest):
@@ -13,16 +14,16 @@ def betamax_init(self):
 
     @betamax()
     def test_add_remove_friends(self):
-        friends = self.r.user.get_friends()
+        friends = self.r.get_friends()
         redditor = friends[0] if friends else self.other_user
         redditor.unfriend()
 
         self.delay_for_listing_update()
-        self.assertTrue(redditor not in self.r.user.get_friends(u=1))
+        self.assertTrue(redditor not in self.r.get_friends(u=1))
         redditor.friend()
 
         self.delay_for_listing_update()
-        self.assertTrue(redditor in self.r.user.get_friends(u=2))
+        self.assertTrue(redditor in self.r.get_friends(u=2))
 
     @betamax()
     def test_duplicate_login(self):
@@ -94,3 +95,28 @@ def test_redditor_comparison(self):
     @betamax()
     def test_user_set_on_login(self):
         self.assertTrue(isinstance(self.r.user, LoggedInRedditor))
+
+
+class OAuthRedditorTest(OAuthPRAWTest):
+    @betamax()
+    def test_friends_oauth(self):
+        self.r.refresh_access_information(self.refresh_token['subscribe'])
+        user = self.r.get_redditor(self.other_user_name)
+
+        # Only Gold users can include personal notes
+        self.assertRaises(errors.HTTPException, user.friend, 'note')
+
+        friendship = user.friend()
+        self.assertEqual(friendship['id'], user.fullname)
+
+        self.r.refresh_access_information(self.refresh_token['mysubreddits'])
+        friendship2 = user.get_friend_info()
+        self.assertEqual(friendship, friendship2)
+
+        self.r.refresh_access_information(self.refresh_token['read'])
+        friends = list(self.r.get_friends())
+        self.assertTrue(user in friends)
+
+        self.r.refresh_access_information(self.refresh_token['subscribe'])
+        user.unfriend()
+        self.assertFalse(user.refresh().is_friend)