Fix various problems in minecraft.authentication and its tests:

- Return value of _make_request() is treated as a requests.Request, when it is in fact a requests.Response.
- Some tests in test_authentication use assertRaises() incorrectly, resulting in testing code that never gets run.
- Other miscellaneous errors exposed by the above changes.

Additionally:
- YggdrasilError instances now have fields with specific error information, and _raise_from_response() populates them. (This will be useful for later changes.)
This commit is contained in:
joo 2017-03-31 12:59:43 +01:00
parent 73672401ef
commit 66a0603acf
4 changed files with 115 additions and 79 deletions

View File

@ -110,11 +110,11 @@ class AuthenticationToken(object):
"password": password
}
req = _make_request(AUTH_SERVER, "authenticate", payload)
res = _make_request(AUTH_SERVER, "authenticate", payload)
_raise_from_request(req)
_raise_from_response(res)
json_resp = req.json()
json_resp = res.json()
self.username = username
self.access_token = json_resp["accessToken"]
@ -145,13 +145,13 @@ class AuthenticationToken(object):
if self.client_token is None:
raise ValueError("'client_token' is not set!")
req = _make_request(AUTH_SERVER,
res = _make_request(AUTH_SERVER,
"refresh", {"accessToken": self.access_token,
"clientToken": self.client_token})
_raise_from_request(req)
_raise_from_response(res)
json_resp = req.json()
json_resp = res.json()
self.access_token = json_resp["accessToken"]
self.client_token = json_resp["clientToken"]
@ -177,12 +177,12 @@ class AuthenticationToken(object):
if self.access_token is None:
raise ValueError("'access_token' not set!")
req = _make_request(AUTH_SERVER, "validate",
res = _make_request(AUTH_SERVER, "validate",
{"accessToken": self.access_token})
# Validate returns 204 to indicate success
# http://wiki.vg/Authentication#Response_3
if req.status_code == 204:
if res.status_code == 204:
return True
@staticmethod
@ -202,10 +202,10 @@ class AuthenticationToken(object):
Raises:
minecraft.exceptions.YggdrasilError
"""
req = _make_request(AUTH_SERVER, "signout",
res = _make_request(AUTH_SERVER, "signout",
{"username": username, "password": password})
if _raise_from_request(req) is None:
if _raise_from_response(res) is None:
return True
def invalidate(self):
@ -219,12 +219,12 @@ class AuthenticationToken(object):
Raises:
:class:`minecraft.exceptions.YggdrasilError`
"""
req = _make_request(AUTH_SERVER, "invalidate",
res = _make_request(AUTH_SERVER, "invalidate",
{"accessToken": self.access_token,
"clientToken": self.client_token})
if req.status_code != 204:
_raise_from_request(req)
if res.status_code != 204:
_raise_from_response(res)
return True
def join(self, server_id):
@ -246,13 +246,13 @@ class AuthenticationToken(object):
err = "AuthenticationToken hasn't been authenticated yet!"
raise YggdrasilError(err)
req = _make_request(SESSION_SERVER, "join",
res = _make_request(SESSION_SERVER, "join",
{"accessToken": self.access_token,
"selectedProfile": self.profile.to_dict(),
"serverId": server_id})
if req.status_code != 204:
_raise_from_request(req)
if res.status_code != 204:
_raise_from_response(res)
return True
@ -268,31 +268,39 @@ def _make_request(server, endpoint, data):
Returns:
A `requests.Request` object.
"""
req = requests.post(server + "/" + endpoint, data=json.dumps(data),
res = requests.post(server + "/" + endpoint, data=json.dumps(data),
headers=HEADERS)
return req
return res
def _raise_from_request(req):
def _raise_from_response(res):
"""
Raises an appropriate `YggdrasilError` based on the `status_code` and
`json` of a `requests.Request` object.
"""
if req.status_code == requests.codes['ok']:
if res.status_code == requests.codes['ok']:
return None
exception = YggdrasilError()
exception.status_code = res.status_code
try:
json_resp = req.json()
if "error" not in json_resp and "errorMessage" not in json_resp:
raise YggdrasilError("Malformed error message.")
json_resp = res.json()
if not ("error" in json_resp and "errorMessage" in json_resp):
raise ValueError
except ValueError:
message = "[{status_code}] Malformed error message: '{response_text}'"
message = message.format(status_code=str(res.status_code),
response_text=res.text)
exception.args = (message,)
else:
message = "[{status_code}] {error}: '{error_message}'"
message = message.format(status_code=str(req.status_code),
message = message.format(status_code=str(res.status_code),
error=json_resp["error"],
error_message=json_resp["errorMessage"])
except ValueError as e:
message = "Unknown requests error. Status code: {}. Error: {}"
message.format(str(req.status_code), e)
exception.args = (message,)
exception.yggdrasil_error = json_resp["error"]
exception.yggdrasil_message = json_resp["errorMessage"]
exception.yggdrasil_cause = json_resp.get("cause")
raise YggdrasilError(message)
raise exception

View File

@ -7,6 +7,23 @@ class YggdrasilError(Exception):
"""
Base `Exception` for the Yggdrasil authentication service.
"""
def __init__(
self,
message=None,
status_code=None,
yggdrasil_error=None,
yggdrasil_message=None,
yggdrasil_cause=None,
*args
):
if message is not None:
args = (message,) + args
super(YggdrasilError, self).__init__(*args)
self.status_code = status_code
self.yggdrasil_error = yggdrasil_error
self.yggdrasil_message = yggdrasil_message
self.yggdrasil_cause = yggdrasil_cause
class VersionMismatch(Exception):

View File

@ -1,7 +1,7 @@
from minecraft.authentication import Profile
from minecraft.authentication import AuthenticationToken
from minecraft.authentication import _make_request
from minecraft.authentication import _raise_from_request
from minecraft.authentication import _raise_from_response
from minecraft.exceptions import YggdrasilError
import requests
@ -179,11 +179,11 @@ class AuthenticateAuthenticationToken(unittest.TestCase):
a = AuthenticationToken()
# We assume these aren't actual, valid credentials.
with self.assertRaises(YggdrasilError) as e:
with self.assertRaises(YggdrasilError) as cm:
a.authenticate("Billy", "The Goat")
err = "Invalid Credentials. Invalid username or password."
self.assertEqual(e.error, err)
err = "Invalid credentials. Invalid username or password."
self.assertEqual(cm.exception.yggdrasil_message, err)
@unittest.skipIf(should_skip_cred_test(),
"Need credentials to perform test.")
@ -196,8 +196,8 @@ class AuthenticateAuthenticationToken(unittest.TestCase):
class MakeRequest(unittest.TestCase):
def test_make_request_http_method(self):
req = _make_request(AUTHSERVER, "authenticate", {"Billy": "Bob"})
self.assertEqual(req.request.method, "POST")
res = _make_request(AUTHSERVER, "authenticate", {"Billy": "Bob"})
self.assertEqual(res.request.method, "POST")
def test_make_request_json_dump(self):
data = {"Marie": "McGee",
@ -208,63 +208,72 @@ class MakeRequest(unittest.TestCase):
"Listly": ["listling1", 2, "listling 3"]
}
req = _make_request(AUTHSERVER, "authenticate", data)
self.assertEqual(req.request.body, json.dumps(data))
res = _make_request(AUTHSERVER, "authenticate", data)
self.assertEqual(res.request.body, json.dumps(data))
def test_make_request_url(self):
URL = "https://authserver.mojang.com/authenticate"
req = _make_request(AUTHSERVER, "authenticate", {"Darling": "Diary"})
self.assertEqual(req.request.url, URL)
res = _make_request(AUTHSERVER, "authenticate", {"Darling": "Diary"})
self.assertEqual(res.request.url, URL)
class RaiseFromRequest(unittest.TestCase):
def test_raise_from_erroneous_request(self):
err_req = requests.Request()
err_req.status_code = 401
err_req.json = mock.MagicMock(
err_res = mock.NonCallableMock(requests.Response)
err_res.status_code = 401
err_res.json = mock.MagicMock(
return_value={"error": "ThisIsAnException",
"errorMessage": "Went wrong."})
err_res.text = json.dumps(err_res.json())
with self.assertRaises(YggdrasilError) as e:
_raise_from_request(err_req)
self.assertEqual(e, "[401]) ThisIsAnException: Went Wrong.")
with self.assertRaises(YggdrasilError) as cm:
_raise_from_response(err_res)
message = "[401] ThisIsAnException: 'Went wrong.'"
self.assertEqual(str(cm.exception), message)
def test_raise_invalid_json(self):
err_req = requests.Request()
err_req.status_code = 401
err_req.json = mock.MagicMock(
err_res = mock.NonCallableMock(requests.Response)
err_res.status_code = 401
err_res.json = mock.MagicMock(
side_effect=ValueError("no json could be decoded")
)
err_res.text = "{sample invalid json}"
with self.assertRaises(YggdrasilError) as e:
_raise_from_request(err_req)
self.assertTrue("Unknown requests error" in e)
with self.assertRaises(YggdrasilError) as cm:
_raise_from_response(err_res)
def test_raise_from_erroneous_request_without_error(self):
err_req = requests.Request()
err_req.status_code = 401
err_req.json = mock.MagicMock(return_value={"goldfish": "are pretty."})
message_start = "[401] Malformed error message"
self.assertTrue(str(cm.exception).startswith(message_start))
with self.assertRaises(YggdrasilError) as e:
_raise_from_request(err_req)
def test_raise_from_erroneous_response_without_error(self):
err_res = mock.NonCallableMock(requests.Response)
err_res.status_code = 401
err_res.json = mock.MagicMock(return_value={"goldfish": "are pretty."})
err_res.text = json.dumps(err_res.json())
self.assertEqual(e, "Malformed error message.")
with self.assertRaises(YggdrasilError) as cm:
_raise_from_response(err_res)
def test_raise_from_healthy_request(self):
req = requests.Request()
req.status_code = 200
req.json = mock.MagicMock(return_value={"vegetables": "are healthy."})
message_start = "[401] Malformed error message"
self.assertTrue(str(cm.exception).startswith(message_start))
self.assertIs(_raise_from_request(req), None)
def test_raise_from_healthy_response(self):
res = mock.NonCallableMock(requests.Response)
res.status_code = 200
res.json = mock.MagicMock(return_value={"vegetables": "are healthy."})
res.text = json.dumps(res.json())
self.assertIs(_raise_from_response(res), None)
class NormalConnectionProcedure(unittest.TestCase):
def test_login_connect_and_logout(self):
a = AuthenticationToken()
successful_req = requests.Request()
successful_req.status_code = 200
successful_req.json = mock.MagicMock(
successful_res = mock.NonCallableMock(requests.Response)
successful_res.status_code = 200
successful_res.json = mock.MagicMock(
return_value={"accessToken": "token",
"clientToken": "token",
"selectedProfile": {
@ -272,33 +281,35 @@ class NormalConnectionProcedure(unittest.TestCase):
"name": "asdf"
}}
)
successful_res.text = json.dumps(successful_res.json())
error_req = requests.Request()
error_req.status_code = 400
error_req.json = mock.MagicMock(
error_res = mock.NonCallableMock(requests.Response)
error_res.status_code = 400
error_res.json = mock.MagicMock(
return_value={
"error": "invalid request",
"errorMessage": "invalid request"
}
)
error_res.text = json.dumps(error_res.json())
def mocked_make_request(server, endpoint, data):
if endpoint == "authenticate":
return successful_req
return successful_res
if endpoint == "refresh" and data["accessToken"] == "token":
return successful_req
return successful_res
if (endpoint == "validate" and data["accessToken"] == "token") \
or endpoint == "join":
r = requests.Request()
r = requests.Response()
r.status_code = 204
r.raise_for_status = mock.MagicMock(return_value=None)
return r
if endpoint == "signout":
return successful_req
return successful_res
if endpoint == "invalidate":
return successful_req
return successful_res
return error_req
return error_res
# Test a successful sequence of events
with mock.patch("minecraft.authentication._make_request",
@ -327,7 +338,7 @@ class NormalConnectionProcedure(unittest.TestCase):
# Failures
with mock.patch("minecraft.authentication._make_request",
return_value=error_req) as _make_request_mock:
return_value=error_res) as _make_request_mock:
self.assertFalse(a.authenticated)
a.client_token = "token"

View File

@ -9,7 +9,7 @@ class RaiseYggdrasilError(unittest.TestCase):
raise YggdrasilError
def test_raise_yggdrasil_error_message(self):
with self.assertRaises(YggdrasilError) as e:
with self.assertRaises(YggdrasilError) as cm:
raise YggdrasilError("Error!")
self.assertEqual(str(e.exception), "Error!")
self.assertEqual(str(cm.exception), "Error!")