test_http.py 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. # Copyright 2010 Canonical Ltd.
  2. # This file is part of launchpadlib.
  3. #
  4. # launchpadlib is free software: you can redistribute it and/or modify it
  5. # under the terms of the GNU Lesser General Public License as published by the
  6. # Free Software Foundation, version 3 of the License.
  7. #
  8. # launchpadlib is distributed in the hope that it will be useful, but WITHOUT
  9. # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  10. # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
  11. # for more details.
  12. #
  13. # You should have received a copy of the GNU Lesser General Public License
  14. # along with launchpadlib. If not, see <http://www.gnu.org/licenses/>.
  15. """Tests for the LaunchpadOAuthAwareHTTP class."""
  16. from collections import deque
  17. from json import dumps
  18. import tempfile
  19. import unittest
  20. try:
  21. from json import JSONDecodeError
  22. except ImportError:
  23. JSONDecodeError = ValueError
  24. from launchpadlib.errors import Unauthorized
  25. from launchpadlib.credentials import UnencryptedFileCredentialStore
  26. from launchpadlib.launchpad import (
  27. Launchpad,
  28. LaunchpadOAuthAwareHttp,
  29. )
  30. from launchpadlib.testing.helpers import NoNetworkAuthorizationEngine
  31. # The simplest WADL that looks like a representation of the service root.
  32. SIMPLE_WADL = b"""<?xml version="1.0"?>
  33. <application xmlns="http://research.sun.com/wadl/2006/10">
  34. <resources base="http://www.example.com/">
  35. <resource path="" type="#service-root"/>
  36. </resources>
  37. <resource_type id="service-root">
  38. <method name="GET" id="service-root-get">
  39. <response>
  40. <representation href="#service-root-json"/>
  41. </response>
  42. </method>
  43. </resource_type>
  44. <representation id="service-root-json" mediaType="application/json"/>
  45. </application>
  46. """
  47. # The simplest JSON that looks like a representation of the service root.
  48. SIMPLE_JSON = dumps({}).encode("utf-8")
  49. class Response:
  50. """A fake HTTP response object."""
  51. def __init__(self, status, content):
  52. self.status = status
  53. self.content = content
  54. class SimulatedResponsesHttp(LaunchpadOAuthAwareHttp):
  55. """Responds to HTTP requests by shifting responses off a stack."""
  56. def __init__(self, responses, *args):
  57. """Constructor.
  58. :param responses: A list of HttpResponse objects to use
  59. in response to requests.
  60. """
  61. super(SimulatedResponsesHttp, self).__init__(*args)
  62. self.sent_responses = []
  63. self.unsent_responses = responses
  64. self.cache = None
  65. def _request(self, *args):
  66. response = self.unsent_responses.popleft()
  67. self.sent_responses.append(response)
  68. return self.retry_on_bad_token(response, response.content, *args)
  69. class SimulatedResponsesLaunchpad(Launchpad):
  70. # Every Http object generated by this class will return these
  71. # responses, in order.
  72. responses = []
  73. def httpFactory(self, *args):
  74. return SimulatedResponsesHttp(
  75. deque(self.responses), self, self.authorization_engine, *args
  76. )
  77. @classmethod
  78. def credential_store_factory(cls, credential_save_failed):
  79. return UnencryptedFileCredentialStore(
  80. tempfile.mkstemp()[1], credential_save_failed
  81. )
  82. class SimulatedResponsesTestCase(unittest.TestCase):
  83. """Test cases that give fake responses to launchpad's HTTP requests."""
  84. def setUp(self):
  85. """Clear out the list of simulated responses."""
  86. SimulatedResponsesLaunchpad.responses = []
  87. self.engine = NoNetworkAuthorizationEngine(
  88. "http://api.example.com/", "application name"
  89. )
  90. def launchpad_with_responses(self, *responses):
  91. """Use simulated HTTP responses to get a Launchpad object.
  92. The given Response objects will be sent, in order, in response
  93. to launchpadlib's requests.
  94. :param responses: Some number of Response objects.
  95. :return: The Launchpad object, assuming that errors in the
  96. simulated requests didn't prevent one from being created.
  97. """
  98. SimulatedResponsesLaunchpad.responses = responses
  99. return SimulatedResponsesLaunchpad.login_with(
  100. "application name", authorization_engine=self.engine
  101. )
  102. class TestAbilityToParseData(SimulatedResponsesTestCase):
  103. """Test launchpadlib's ability to handle the sample data.
  104. To create a Launchpad object, two HTTP requests must succeed and
  105. return usable data: the requests for the WADL and JSON
  106. representations of the service root. This test shows that the
  107. minimal data in SIMPLE_WADL and SIMPLE_JSON is good enough to
  108. create a Launchpad object.
  109. """
  110. def test_minimal_data(self):
  111. """Make sure that launchpadlib can use the minimal data."""
  112. self.launchpad_with_responses(
  113. Response(200, SIMPLE_WADL), Response(200, SIMPLE_JSON)
  114. )
  115. def test_bad_wadl(self):
  116. """Show that bad WADL causes an exception."""
  117. self.assertRaises(
  118. SyntaxError,
  119. self.launchpad_with_responses,
  120. Response(200, b"This is not WADL."),
  121. Response(200, SIMPLE_JSON),
  122. )
  123. def test_bad_json(self):
  124. """Show that bad JSON causes an exception."""
  125. self.assertRaises(
  126. JSONDecodeError,
  127. self.launchpad_with_responses,
  128. Response(200, SIMPLE_WADL),
  129. Response(200, b"This is not JSON."),
  130. )
  131. class TestTokenFailureDuringRequest(SimulatedResponsesTestCase):
  132. """Test access token failures during a request.
  133. launchpadlib makes two HTTP requests on startup, to get the WADL
  134. and JSON representations of the service root. If Launchpad
  135. receives a 401 error during this process, it will acquire a fresh
  136. access token and try again.
  137. """
  138. def test_good_token(self):
  139. """If our token is good, we never get another one."""
  140. SimulatedResponsesLaunchpad.responses = [
  141. Response(200, SIMPLE_WADL),
  142. Response(200, SIMPLE_JSON),
  143. ]
  144. self.assertEqual(self.engine.access_tokens_obtained, 0)
  145. SimulatedResponsesLaunchpad.login_with(
  146. "application name", authorization_engine=self.engine
  147. )
  148. self.assertEqual(self.engine.access_tokens_obtained, 1)
  149. def test_bad_token(self):
  150. """If our token is bad, we get another one."""
  151. SimulatedResponsesLaunchpad.responses = [
  152. Response(401, b"Invalid token."),
  153. Response(200, SIMPLE_WADL),
  154. Response(200, SIMPLE_JSON),
  155. ]
  156. self.assertEqual(self.engine.access_tokens_obtained, 0)
  157. SimulatedResponsesLaunchpad.login_with(
  158. "application name", authorization_engine=self.engine
  159. )
  160. self.assertEqual(self.engine.access_tokens_obtained, 2)
  161. def test_expired_token(self):
  162. """If our token is expired, we get another one."""
  163. SimulatedResponsesLaunchpad.responses = [
  164. Response(401, b"Expired token."),
  165. Response(200, SIMPLE_WADL),
  166. Response(200, SIMPLE_JSON),
  167. ]
  168. self.assertEqual(self.engine.access_tokens_obtained, 0)
  169. SimulatedResponsesLaunchpad.login_with(
  170. "application name", authorization_engine=self.engine
  171. )
  172. self.assertEqual(self.engine.access_tokens_obtained, 2)
  173. def test_unknown_token(self):
  174. """If our token is unknown, we get another one."""
  175. SimulatedResponsesLaunchpad.responses = [
  176. Response(401, b"Unknown access token."),
  177. Response(200, SIMPLE_WADL),
  178. Response(200, SIMPLE_JSON),
  179. ]
  180. self.assertEqual(self.engine.access_tokens_obtained, 0)
  181. SimulatedResponsesLaunchpad.login_with(
  182. "application name", authorization_engine=self.engine
  183. )
  184. self.assertEqual(self.engine.access_tokens_obtained, 2)
  185. def test_delayed_error(self):
  186. """We get another token no matter when the error happens."""
  187. SimulatedResponsesLaunchpad.responses = [
  188. Response(200, SIMPLE_WADL),
  189. Response(401, b"Expired token."),
  190. Response(200, SIMPLE_JSON),
  191. ]
  192. self.assertEqual(self.engine.access_tokens_obtained, 0)
  193. SimulatedResponsesLaunchpad.login_with(
  194. "application name", authorization_engine=self.engine
  195. )
  196. self.assertEqual(self.engine.access_tokens_obtained, 2)
  197. def test_many_errors(self):
  198. """We'll keep getting new tokens as long as tokens are the problem."""
  199. SimulatedResponsesLaunchpad.responses = [
  200. Response(401, b"Invalid token."),
  201. Response(200, SIMPLE_WADL),
  202. Response(401, b"Expired token."),
  203. Response(401, b"Invalid token."),
  204. Response(200, SIMPLE_JSON),
  205. ]
  206. self.assertEqual(self.engine.access_tokens_obtained, 0)
  207. SimulatedResponsesLaunchpad.login_with(
  208. "application name", authorization_engine=self.engine
  209. )
  210. self.assertEqual(self.engine.access_tokens_obtained, 4)
  211. def test_other_unauthorized(self):
  212. """If the token is not at fault, a 401 error raises an exception."""
  213. SimulatedResponsesLaunchpad.responses = [
  214. Response(401, b"Some other error.")
  215. ]
  216. self.assertRaises(
  217. Unauthorized,
  218. SimulatedResponsesLaunchpad.login_with,
  219. "application name",
  220. authorization_engine=self.engine,
  221. )