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:

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 5e55a57e..4cd5cc03 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -22,13 +22,35 @@ formatted links that link to the relevant place in the code overview.
 
 .. begin_changelog_body
 
-UNRELEASED
+Unreleased
 ----------
  * **[BUGFIX]** Fixed login password prompt issue on windows (#485).
  * **[BUGFIX]** Fixed unicode user-agent issue (#483).
  * **[BUGFIX]** Fix duplicate request issue with comment and submission streams
    (#501).
+ * **[BUGFIX]** Stopped :meth:`praw.objects.Redditor.friend` from raising
+   LoginRequired when using OAuth.
+ * **[BUGFIX]** Stopped a json-parsing error from occuring in cases where
+   reddit's response to a request was an empty string. :meth:`request_json`
+   will now simply return that empty string.
  * **[CHANGE]** Added messages to all PRAW exceptions (#491).
+ * **[CHANGE]** Made it easier to send JSON dumps instead of form-encoded data
+   for http requests. Some api-v1 endpoints require the request body to be in
+   the json format.
+ * **[CHANGE]** Moved and deprecated
+   :meth:`praw.objects.LoggedInRedditor.get_friends` to
+   :class:`praw.AuthenticatedReddit`, leaving a pointer in its place.
+   Previously, `get_friends` was difficult to access because the only instance
+   of `LoggedInRedditor` was the reddit session's `user` attribute, which is
+   only instantiated if the user has the "identity" scope. By moving
+   `get_friends` to the reddit session, it can be used without having to
+   manipulate a :class:`praw.objects.Redditor` intsance's class.
+ * **[FEATURE]** Added support for adding "notes" to your friends. Users with
+   reddit Gold can set the ``note`` parameter of 
+   :meth:`praw.objects.Redditor.friend`. 300 character max enforced by reddit.
+ * **[FEATURE]** New :meth:`praw.objects.Redditor.get_friend_info` to see info
+   about one of your friends. Includes their name, ID, when you added them, and
+   if you have reddit Gold, your note about them.
 
 PRAW 3.2.1
 ----------
diff --git a/praw/__init__.py b/praw/__init__.py
index 398d64b5..6a31d2dc 100644
--- 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 @@ class BaseReddit(object):
         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 @@ class AuthenticatedReddit(OAuth2Reddit, UnauthenticatedReddit):
         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
index 7967eb24..62eefe7c 100644
--- 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
index f6a0822a..9f9d557b 100755
--- a/praw/objects.py
+++ b/praw/objects.py
@@ -843,14 +843,38 @@ class Redditor(Gildable, Messageable, Refreshable):
             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 @@ class Redditor(Gildable, Messageable, Refreshable):
         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 @@ class Redditor(Gildable, Messageable, Refreshable):
         :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 @@ class LoggedInRedditor(Redditor):
                 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
index 00000000..b0a7cc0e
--- /dev/null
+++ b/tests/cassettes/test_friends_oauth.json
@@ -0,0 +1,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
index 03700625..c6e5ad6f 100644
--- a/tests/helper.py
+++ b/tests/helper.py
@@ -7,6 +7,7 @@ import time
 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 @@ class BodyMatcher(BaseMatcher):
         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
index a7326063..a1452a9d 100644
--- 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 @@ class RedditorTest(PRAWTest):
 
     @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 @@ class RedditorTest(PRAWTest):
     @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)