test_launchpad.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. # Copyright 2008 Canonical Ltd.
  2. # This file is part of launchpadlib.
  3. #
  4. # launchpadlib is free software: you can redistribute it and/or modify
  5. # it under the terms of the GNU Lesser General Public License as
  6. # published by the Free Software Foundation, either version 3 of the
  7. # License, or (at your option) any later version.
  8. #
  9. # launchpadlib is distributed in the hope that it will be useful, but
  10. # WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  12. # Lesser General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU Lesser General Public
  15. # License along with launchpadlib. If not, see
  16. # <http://www.gnu.org/licenses/>.
  17. from datetime import datetime
  18. from testresources import ResourcedTestCase
  19. from launchpadlib.testing.launchpad import (
  20. FakeLaunchpad,
  21. FakeResource,
  22. FakeRoot,
  23. IntegrityError,
  24. )
  25. from launchpadlib.testing.resources import (
  26. FakeLaunchpadResource,
  27. get_application,
  28. )
  29. class FakeRootTest(ResourcedTestCase):
  30. def test_create_root_resource(self):
  31. root_resource = FakeRoot(get_application())
  32. self.assertTrue(isinstance(root_resource, FakeResource))
  33. class FakeResourceTest(ResourcedTestCase):
  34. resources = [("launchpad", FakeLaunchpadResource())]
  35. def test_repr_entry(self):
  36. """A custom C{__repr__} is provided for L{FakeEntry}s."""
  37. bug = dict()
  38. self.launchpad.bugs = dict(entries=[bug])
  39. [bug] = list(self.launchpad.bugs)
  40. self.assertEqual(
  41. "<FakeEntry bug object at %s>" % hex(id(bug)), repr(bug)
  42. )
  43. def test_repr_collection(self):
  44. """A custom C{__repr__} is provided for L{FakeCollection}s."""
  45. branches = dict(total_size="test-branch")
  46. self.launchpad.me = dict(getBranches=lambda statuses: branches)
  47. branches = self.launchpad.me.getBranches([])
  48. obj_id = hex(id(branches))
  49. self.assertEqual(
  50. "<FakeCollection branch-page-resource object at %s>" % obj_id,
  51. repr(branches),
  52. )
  53. def test_repr_with_name(self):
  54. """
  55. If the fake has a C{name} property it's included in the repr string to
  56. make it easier to figure out what it is.
  57. """
  58. self.launchpad.me = dict(name="foo")
  59. person = self.launchpad.me
  60. self.assertEqual(
  61. "<FakeEntry person foo at %s>" % hex(id(person)), repr(person)
  62. )
  63. def test_repr_with_id(self):
  64. """
  65. If the fake has an C{id} property it's included in the repr string to
  66. make it easier to figure out what it is.
  67. """
  68. bug = dict(id="1", title="Bug #1")
  69. self.launchpad.bugs = dict(entries=[bug])
  70. [bug] = list(self.launchpad.bugs)
  71. self.assertEqual("<FakeEntry bug 1 at %s>" % hex(id(bug)), repr(bug))
  72. class FakeLaunchpadTest(ResourcedTestCase):
  73. resources = [("launchpad", FakeLaunchpadResource())]
  74. def test_wb_instantiate_without_application(self):
  75. """
  76. The builtin WADL definition is used if the C{application} is not
  77. provided during instantiation.
  78. """
  79. credentials = object()
  80. launchpad = FakeLaunchpad(credentials)
  81. self.assertEqual(credentials, launchpad.credentials)
  82. self.assertEqual(get_application(), launchpad._application)
  83. def test_instantiate_with_everything(self):
  84. """
  85. L{FakeLaunchpad} takes the same parameters as L{Launchpad} during
  86. instantiation, with the addition of an C{application} parameter. The
  87. optional parameters are discarded when the object is instantiated.
  88. """
  89. credentials = object()
  90. launchpad = FakeLaunchpad(
  91. credentials,
  92. service_root=None,
  93. cache=None,
  94. timeout=None,
  95. proxy_info=None,
  96. application=get_application(),
  97. )
  98. self.assertEqual(credentials, launchpad.credentials)
  99. def test_instantiate_with_credentials(self):
  100. """A L{FakeLaunchpad} can be instantiated with credentials."""
  101. credentials = object()
  102. launchpad = FakeLaunchpad(credentials, application=get_application())
  103. self.assertEqual(credentials, launchpad.credentials)
  104. def test_instantiate_without_credentials(self):
  105. """
  106. A L{FakeLaunchpad} instantiated without credentials has its
  107. C{credentials} attribute set to C{None}.
  108. """
  109. self.assertIsNone(self.launchpad.credentials)
  110. def test_set_undefined_property(self):
  111. """
  112. An L{IntegrityError} is raised if an attribute is set on a
  113. L{FakeLaunchpad} instance that isn't present in the WADL definition.
  114. """
  115. self.assertRaises(
  116. IntegrityError, setattr, self.launchpad, "foo", "bar"
  117. )
  118. def test_get_undefined_resource(self):
  119. """
  120. An L{AttributeError} is raised if an attribute is accessed on a
  121. L{FakeLaunchpad} instance that doesn't exist.
  122. """
  123. self.launchpad.me = dict(display_name="Foo")
  124. self.assertRaises(AttributeError, getattr, self.launchpad.me, "name")
  125. def test_string_property(self):
  126. """
  127. Sample data can be created by setting L{FakeLaunchpad} attributes with
  128. dicts that represent objects. Plain string values can be represented
  129. as C{str} values.
  130. """
  131. self.launchpad.me = dict(name="foo")
  132. self.assertEqual("foo", self.launchpad.me.name)
  133. def test_unicode_property(self):
  134. """
  135. Sample data can be created by setting L{FakeLaunchpad} attributes with
  136. dicts that represent objects. Plain string values can be represented
  137. as C{unicode} strings.
  138. """
  139. self.launchpad.me = dict(name=u"foo")
  140. self.assertEqual(u"foo", self.launchpad.me.name)
  141. def test_datetime_property(self):
  142. """
  143. Attributes that represent dates are set with C{datetime} instances.
  144. """
  145. now = datetime.utcnow()
  146. self.launchpad.me = dict(date_created=now)
  147. self.assertEqual(now, self.launchpad.me.date_created)
  148. def test_invalid_datetime_property(self):
  149. """
  150. Only C{datetime} values can be set on L{FakeLaunchpad} instances for
  151. attributes that represent dates.
  152. """
  153. self.assertRaises(
  154. IntegrityError,
  155. setattr,
  156. self.launchpad,
  157. "me",
  158. dict(date_created="now"),
  159. )
  160. def test_multiple_string_properties(self):
  161. """
  162. Sample data can be created by setting L{FakeLaunchpad} attributes with
  163. dicts that represent objects.
  164. """
  165. self.launchpad.me = dict(name="foo", display_name="Foo")
  166. self.assertEqual("foo", self.launchpad.me.name)
  167. self.assertEqual("Foo", self.launchpad.me.display_name)
  168. def test_invalid_property_name(self):
  169. """
  170. Sample data set on a L{FakeLaunchpad} instance is validated against
  171. the WADL definition. If a key is defined on a resource that doesn't
  172. match a related parameter, an L{IntegrityError} is raised.
  173. """
  174. self.assertRaises(
  175. IntegrityError, setattr, self.launchpad, "me", dict(foo="bar")
  176. )
  177. def test_invalid_property_value(self):
  178. """
  179. The types of sample data values set on L{FakeLaunchpad} instances are
  180. validated against types defined in the WADL definition.
  181. """
  182. self.assertRaises(
  183. IntegrityError, setattr, self.launchpad, "me", dict(name=102)
  184. )
  185. def test_callable(self):
  186. """
  187. A callable set on a L{FakeLaunchpad} instance is validated against the
  188. WADL definition, to make sure a matching method exists.
  189. """
  190. branches = dict(total_size="test-branch")
  191. self.launchpad.me = dict(getBranches=lambda statuses: branches)
  192. self.assertNotEqual(None, self.launchpad.me.getBranches([]))
  193. def test_invalid_callable_name(self):
  194. """
  195. An L{IntegrityError} is raised if a method is defined on a resource
  196. that doesn't match a method defined in the WADL definition.
  197. """
  198. self.assertRaises(
  199. IntegrityError,
  200. setattr,
  201. self.launchpad,
  202. "me",
  203. dict(foo=lambda: None),
  204. )
  205. def test_callable_object_return_type(self):
  206. """
  207. The result of a fake method is a L{FakeResource}, automatically
  208. created from the object used to define the return object.
  209. """
  210. branches = dict(total_size="8")
  211. self.launchpad.me = dict(getBranches=lambda statuses: branches)
  212. branches = self.launchpad.me.getBranches([])
  213. self.assertTrue(isinstance(branches, FakeResource))
  214. self.assertEqual("8", branches.total_size)
  215. def test_invalid_callable_object_return_type(self):
  216. """
  217. An L{IntegrityError} is raised if a method returns an invalid result.
  218. """
  219. branches = dict(total_size=8)
  220. self.launchpad.me = dict(getBranches=lambda statuses: branches)
  221. self.assertRaises(IntegrityError, self.launchpad.me.getBranches, [])
  222. def test_callable_object_return_None(self):
  223. """
  224. A fake method passes through a return value of None rather than
  225. trying to create a L{FakeResource}.
  226. """
  227. self.launchpad.branches = dict(getByUniqueName=lambda name: None)
  228. self.assertIsNone(self.launchpad.branches.getByUniqueName("foo"))
  229. def test_callable_object_no_response_representation(self):
  230. """
  231. If the WADL definition of a method does not include a response
  232. representation, then fake versions of that method just pass through
  233. the return value.
  234. """
  235. branch = dict(canBeDeleted=lambda: True)
  236. self.launchpad.branches = dict(getByUniqueName=lambda name: branch)
  237. branch = self.launchpad.branches.getByUniqueName("foo")
  238. self.assertTrue(branch.canBeDeleted())
  239. def test_entry_property(self):
  240. """
  241. Attributes that represent links to other objects are set using a
  242. dict representing the object.
  243. """
  244. bug = dict(owner=dict(name="test-person"))
  245. self.launchpad.bugs = dict(entries=[bug])
  246. bug = self.launchpad.bugs[0]
  247. self.assertEqual("test-person", bug.owner.name)
  248. def test_invalid_entry_property(self):
  249. """
  250. Sample data for linked entries is validated.
  251. """
  252. bug = dict(owner=dict(foo="bar"))
  253. self.assertRaises(
  254. IntegrityError,
  255. setattr,
  256. self.launchpad,
  257. "bugs",
  258. dict(entries=[bug]),
  259. )
  260. def test_top_level_collection_property(self):
  261. """
  262. Sample top-level collections can be set on L{FakeLaunchpad}
  263. instances. They are validated the same way other sample data is
  264. validated.
  265. """
  266. branch = dict(name="foo")
  267. self.launchpad.branches = dict(getByUniqueName=lambda name: branch)
  268. branch = self.launchpad.branches.getByUniqueName("foo")
  269. self.assertEqual("foo", branch.name)
  270. def test_collection_property(self):
  271. """
  272. Attributes that represent links to collections of other objects are
  273. set using a dict representing the collection.
  274. """
  275. bug = dict(id="1")
  276. branch = dict(linked_bugs=dict(entries=[bug]))
  277. self.launchpad.branches = dict(getByUniqueName=lambda name: branch)
  278. branch = self.launchpad.branches.getByUniqueName("foo")
  279. [bug] = list(branch.linked_bugs)
  280. self.assertEqual("1", bug.id)
  281. def test_iterate_collection(self):
  282. """
  283. Data for a sample collection set on a L{FakeLaunchpad} instance can be
  284. iterated over if an C{entries} key is defined.
  285. """
  286. bug = dict(id="1", title="Bug #1")
  287. self.launchpad.bugs = dict(entries=[bug])
  288. bugs = list(self.launchpad.bugs)
  289. self.assertEqual(1, len(bugs))
  290. bug = bugs[0]
  291. self.assertEqual("1", bug.id)
  292. self.assertEqual("Bug #1", bug.title)
  293. def test_top_level_collection_with_invalid_entries(self):
  294. """
  295. Sample data for each entry in a collection is validated when it's set
  296. on a L{FakeLaunchpad} instance.
  297. """
  298. bug = dict(foo="bar")
  299. self.assertRaises(
  300. IntegrityError,
  301. setattr,
  302. self.launchpad,
  303. "bugs",
  304. dict(entries=[bug]),
  305. )
  306. def test_collection_with_invalid_entries(self):
  307. """
  308. Sample data for each entry in a collection is validated when it's set
  309. on an attribute representing a link to a collection of objects.
  310. """
  311. bug = dict(foo="bar")
  312. branch = dict(linked_bugs=dict(entries=[bug]))
  313. self.launchpad.branches = dict(getByUniqueName=lambda name: branch)
  314. self.assertRaises(
  315. IntegrityError,
  316. self.launchpad.branches.getByUniqueName,
  317. "foo",
  318. )
  319. def test_slice_collection(self):
  320. """
  321. Data for a sample collection set on a L{FakeLaunchpad} instance can be
  322. sliced if an C{entries} key is defined.
  323. """
  324. bug1 = dict(id="1", title="Bug #1")
  325. bug2 = dict(id="2", title="Bug #2")
  326. bug3 = dict(id="3", title="Bug #3")
  327. self.launchpad.bugs = dict(entries=[bug1, bug2, bug3])
  328. bugs = self.launchpad.bugs[1:3]
  329. self.assertEqual(2, len(bugs))
  330. self.assertEqual("2", bugs[0].id)
  331. self.assertEqual("3", bugs[1].id)
  332. def test_slice_collection_with_negative_start(self):
  333. """
  334. A C{ValueError} is raised if a negative start value is used when
  335. slicing a sample collection set on a L{FakeLaunchpad} instance.
  336. """
  337. bug1 = dict(id="1", title="Bug #1")
  338. bug2 = dict(id="2", title="Bug #2")
  339. self.launchpad.bugs = dict(entries=[bug1, bug2])
  340. self.assertRaises(ValueError, lambda: self.launchpad.bugs[-1:])
  341. self.assertRaises(ValueError, lambda: self.launchpad.bugs[-1:2])
  342. def test_slice_collection_with_negative_stop(self):
  343. """
  344. A C{ValueError} is raised if a negative stop value is used when
  345. slicing a sample collection set on a L{FakeLaunchpad} instance.
  346. """
  347. bug1 = dict(id="1", title="Bug #1")
  348. bug2 = dict(id="2", title="Bug #2")
  349. self.launchpad.bugs = dict(entries=[bug1, bug2])
  350. self.assertRaises(ValueError, lambda: self.launchpad.bugs[:-1])
  351. self.assertRaises(ValueError, lambda: self.launchpad.bugs[0:-1])
  352. def test_subscript_operator_out_of_range(self):
  353. """
  354. An C{IndexError} is raised if an invalid index is used when retrieving
  355. data from a sample collection.
  356. """
  357. bug1 = dict(id="1", title="Bug #1")
  358. self.launchpad.bugs = dict(entries=[bug1])
  359. self.assertRaises(IndexError, lambda: self.launchpad.bugs[2])
  360. def test_replace_property(self):
  361. """Values already set on fake resource objects can be replaced."""
  362. self.launchpad.me = dict(name="foo")
  363. person = self.launchpad.me
  364. self.assertEqual("foo", person.name)
  365. person.name = "bar"
  366. self.assertEqual("bar", person.name)
  367. self.assertEqual("bar", self.launchpad.me.name)
  368. def test_replace_method(self):
  369. """Methods already set on fake resource objects can be replaced."""
  370. branch1 = dict(name="foo", bzr_identity="lp:~user/project/branch1")
  371. branch2 = dict(name="foo", bzr_identity="lp:~user/project/branch2")
  372. self.launchpad.branches = dict(getByUniqueName=lambda name: branch1)
  373. self.launchpad.branches.getByUniqueName = lambda name: branch2
  374. branch = self.launchpad.branches.getByUniqueName("foo")
  375. self.assertEqual("lp:~user/project/branch2", branch.bzr_identity)
  376. def test_replace_property_with_invalid_value(self):
  377. """Values set on fake resource objects are validated."""
  378. self.launchpad.me = dict(name="foo")
  379. person = self.launchpad.me
  380. self.assertRaises(IntegrityError, setattr, person, "name", 1)
  381. def test_replace_resource(self):
  382. """Resources already set on L{FakeLaunchpad} can be replaced."""
  383. self.launchpad.me = dict(name="foo")
  384. self.assertEqual("foo", self.launchpad.me.name)
  385. self.launchpad.me = dict(name="bar")
  386. self.assertEqual("bar", self.launchpad.me.name)
  387. def test_add_property(self):
  388. """Sample data set on a L{FakeLaunchpad} instance can be added to."""
  389. self.launchpad.me = dict(name="foo")
  390. person = self.launchpad.me
  391. person.display_name = "Foo"
  392. self.assertEqual("foo", person.name)
  393. self.assertEqual("Foo", person.display_name)
  394. self.assertEqual("foo", self.launchpad.me.name)
  395. self.assertEqual("Foo", self.launchpad.me.display_name)
  396. def test_add_property_to_empty_object(self):
  397. """An empty object can be used when creating sample data."""
  398. self.launchpad.me = dict()
  399. self.assertRaises(AttributeError, getattr, self.launchpad.me, "name")
  400. self.launchpad.me.name = "foo"
  401. self.assertEqual("foo", self.launchpad.me.name)
  402. def test_login(self):
  403. """
  404. L{FakeLaunchpad.login} ignores all parameters and returns a new
  405. instance using the builtin WADL definition.
  406. """
  407. launchpad = FakeLaunchpad.login("name", "token", "secret")
  408. self.assertTrue(isinstance(launchpad, FakeLaunchpad))
  409. def test_get_token_and_login(self):
  410. """
  411. L{FakeLaunchpad.get_token_and_login} ignores all parameters and
  412. returns a new instance using the builtin WADL definition.
  413. """
  414. launchpad = FakeLaunchpad.get_token_and_login("name")
  415. self.assertTrue(isinstance(launchpad, FakeLaunchpad))
  416. def test_login_with(self):
  417. """
  418. L{FakeLaunchpad.login_with} ignores all parameters and returns a new
  419. instance using the builtin WADL definition.
  420. """
  421. launchpad = FakeLaunchpad.login_with("name")
  422. self.assertTrue(isinstance(launchpad, FakeLaunchpad))
  423. def test_lp_save(self):
  424. """
  425. Sample object have an C{lp_save} method that is a no-op by default.
  426. """
  427. self.launchpad.me = dict(name="foo")
  428. self.assertTrue(self.launchpad.me.lp_save())
  429. def test_custom_lp_save(self):
  430. """A custom C{lp_save} method can be set on a L{FakeResource}."""
  431. self.launchpad.me = dict(name="foo", lp_save=lambda: "custom")
  432. self.assertEqual("custom", self.launchpad.me.lp_save())
  433. def test_set_custom_lp_save(self):
  434. """
  435. A custom C{lp_save} method can be set on a L{FakeResource} after its
  436. been created.
  437. """
  438. self.launchpad.me = dict(name="foo")
  439. self.launchpad.me.lp_save = lambda: "custom"
  440. self.assertEqual("custom", self.launchpad.me.lp_save())