test_swift.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500
  1. # test_swift.py -- Unittests for the Swift backend.
  2. # Copyright (C) 2013 eNovance SAS <licensing@enovance.com>
  3. #
  4. # Author: Fabien Boucher <fabien.boucher@enovance.com>
  5. #
  6. # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
  7. # General Public License as public by the Free Software Foundation; version 2.0
  8. # or (at your option) any later version. You can redistribute it and/or
  9. # modify it under the terms of either of these two licenses.
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. #
  17. # You should have received a copy of the licenses; if not, see
  18. # <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
  19. # and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
  20. # License, Version 2.0.
  21. #
  22. """Tests for dulwich.contrib.swift."""
  23. import posixpath
  24. from time import time
  25. from io import BytesIO, StringIO
  26. from unittest import skipIf
  27. from dulwich.tests import (
  28. TestCase,
  29. )
  30. from dulwich.tests.test_object_store import (
  31. ObjectStoreTests,
  32. )
  33. from dulwich.objects import (
  34. Blob,
  35. Commit,
  36. Tree,
  37. Tag,
  38. parse_timezone,
  39. )
  40. import json
  41. missing_libs = []
  42. try:
  43. import gevent # noqa:F401
  44. except ImportError:
  45. missing_libs.append("gevent")
  46. try:
  47. import geventhttpclient # noqa:F401
  48. except ImportError:
  49. missing_libs.append("geventhttpclient")
  50. try:
  51. from unittest.mock import patch
  52. except ImportError:
  53. missing_libs.append("mock")
  54. skipmsg = "Required libraries are not installed (%r)" % missing_libs
  55. if not missing_libs:
  56. from dulwich.contrib import swift
  57. config_file = """[swift]
  58. auth_url = http://127.0.0.1:8080/auth/%(version_str)s
  59. auth_ver = %(version_int)s
  60. username = test;tester
  61. password = testing
  62. region_name = %(region_name)s
  63. endpoint_type = %(endpoint_type)s
  64. concurrency = %(concurrency)s
  65. chunk_length = %(chunk_length)s
  66. cache_length = %(cache_length)s
  67. http_pool_length = %(http_pool_length)s
  68. http_timeout = %(http_timeout)s
  69. """
  70. def_config_file = {
  71. "version_str": "v1.0",
  72. "version_int": 1,
  73. "concurrency": 1,
  74. "chunk_length": 12228,
  75. "cache_length": 1,
  76. "region_name": "test",
  77. "endpoint_type": "internalURL",
  78. "http_pool_length": 1,
  79. "http_timeout": 1,
  80. }
  81. def create_swift_connector(store={}):
  82. return lambda root, conf: FakeSwiftConnector(root, conf=conf, store=store)
  83. class Response(object):
  84. def __init__(self, headers={}, status=200, content=None):
  85. self.headers = headers
  86. self.status_code = status
  87. self.content = content
  88. def __getitem__(self, key):
  89. return self.headers[key]
  90. def items(self):
  91. return self.headers.items()
  92. def read(self):
  93. return self.content
  94. def fake_auth_request_v1(*args, **kwargs):
  95. ret = Response(
  96. {
  97. "X-Storage-Url": "http://127.0.0.1:8080/v1.0/AUTH_fakeuser",
  98. "X-Auth-Token": "12" * 10,
  99. },
  100. 200,
  101. )
  102. return ret
  103. def fake_auth_request_v1_error(*args, **kwargs):
  104. ret = Response({}, 401)
  105. return ret
  106. def fake_auth_request_v2(*args, **kwargs):
  107. s_url = "http://127.0.0.1:8080/v1.0/AUTH_fakeuser"
  108. resp = {
  109. "access": {
  110. "token": {"id": "12" * 10},
  111. "serviceCatalog": [
  112. {
  113. "type": "object-store",
  114. "endpoints": [
  115. {
  116. "region": "test",
  117. "internalURL": s_url,
  118. },
  119. ],
  120. },
  121. ],
  122. }
  123. }
  124. ret = Response(status=200, content=json.dumps(resp))
  125. return ret
  126. def create_commit(data, marker=b"Default", blob=None):
  127. if not blob:
  128. blob = Blob.from_string(b"The blob content " + marker)
  129. tree = Tree()
  130. tree.add(b"thefile_" + marker, 0o100644, blob.id)
  131. cmt = Commit()
  132. if data:
  133. assert isinstance(data[-1], Commit)
  134. cmt.parents = [data[-1].id]
  135. cmt.tree = tree.id
  136. author = b"John Doe " + marker + b" <john@doe.net>"
  137. cmt.author = cmt.committer = author
  138. tz = parse_timezone(b"-0200")[0]
  139. cmt.commit_time = cmt.author_time = int(time())
  140. cmt.commit_timezone = cmt.author_timezone = tz
  141. cmt.encoding = b"UTF-8"
  142. cmt.message = b"The commit message " + marker
  143. tag = Tag()
  144. tag.tagger = b"john@doe.net"
  145. tag.message = b"Annotated tag"
  146. tag.tag_timezone = parse_timezone(b"-0200")[0]
  147. tag.tag_time = cmt.author_time
  148. tag.object = (Commit, cmt.id)
  149. tag.name = b"v_" + marker + b"_0.1"
  150. return blob, tree, tag, cmt
  151. def create_commits(length=1, marker=b"Default"):
  152. data = []
  153. for i in range(0, length):
  154. _marker = ("%s_%s" % (marker, i)).encode()
  155. blob, tree, tag, cmt = create_commit(data, _marker)
  156. data.extend([blob, tree, tag, cmt])
  157. return data
  158. @skipIf(missing_libs, skipmsg)
  159. class FakeSwiftConnector(object):
  160. def __init__(self, root, conf, store=None):
  161. if store:
  162. self.store = store
  163. else:
  164. self.store = {}
  165. self.conf = conf
  166. self.root = root
  167. self.concurrency = 1
  168. self.chunk_length = 12228
  169. self.cache_length = 1
  170. def put_object(self, name, content):
  171. name = posixpath.join(self.root, name)
  172. if hasattr(content, "seek"):
  173. content.seek(0)
  174. content = content.read()
  175. self.store[name] = content
  176. def get_object(self, name, range=None):
  177. name = posixpath.join(self.root, name)
  178. if not range:
  179. try:
  180. return BytesIO(self.store[name])
  181. except KeyError:
  182. return None
  183. else:
  184. l, r = range.split("-")
  185. try:
  186. if not l:
  187. r = -int(r)
  188. return self.store[name][r:]
  189. else:
  190. return self.store[name][int(l) : int(r)]
  191. except KeyError:
  192. return None
  193. def get_container_objects(self):
  194. return [{"name": k.replace(self.root + "/", "")} for k in self.store]
  195. def create_root(self):
  196. if self.root in self.store.keys():
  197. pass
  198. else:
  199. self.store[self.root] = ""
  200. def get_object_stat(self, name):
  201. name = posixpath.join(self.root, name)
  202. if name not in self.store:
  203. return None
  204. return {"content-length": len(self.store[name])}
  205. @skipIf(missing_libs, skipmsg)
  206. class TestSwiftRepo(TestCase):
  207. def setUp(self):
  208. super(TestSwiftRepo, self).setUp()
  209. self.conf = swift.load_conf(file=StringIO(config_file % def_config_file))
  210. def test_init(self):
  211. store = {"fakerepo/objects/pack": ""}
  212. with patch(
  213. "dulwich.contrib.swift.SwiftConnector",
  214. new_callable=create_swift_connector,
  215. store=store,
  216. ):
  217. swift.SwiftRepo("fakerepo", conf=self.conf)
  218. def test_init_no_data(self):
  219. with patch(
  220. "dulwich.contrib.swift.SwiftConnector",
  221. new_callable=create_swift_connector,
  222. ):
  223. self.assertRaises(Exception, swift.SwiftRepo, "fakerepo", self.conf)
  224. def test_init_bad_data(self):
  225. store = {"fakerepo/.git/objects/pack": ""}
  226. with patch(
  227. "dulwich.contrib.swift.SwiftConnector",
  228. new_callable=create_swift_connector,
  229. store=store,
  230. ):
  231. self.assertRaises(Exception, swift.SwiftRepo, "fakerepo", self.conf)
  232. def test_put_named_file(self):
  233. store = {"fakerepo/objects/pack": ""}
  234. with patch(
  235. "dulwich.contrib.swift.SwiftConnector",
  236. new_callable=create_swift_connector,
  237. store=store,
  238. ):
  239. repo = swift.SwiftRepo("fakerepo", conf=self.conf)
  240. desc = b"Fake repo"
  241. repo._put_named_file("description", desc)
  242. self.assertEqual(repo.scon.store["fakerepo/description"], desc)
  243. def test_init_bare(self):
  244. fsc = FakeSwiftConnector("fakeroot", conf=self.conf)
  245. with patch(
  246. "dulwich.contrib.swift.SwiftConnector",
  247. new_callable=create_swift_connector,
  248. store=fsc.store,
  249. ):
  250. swift.SwiftRepo.init_bare(fsc, conf=self.conf)
  251. self.assertIn("fakeroot/objects/pack", fsc.store)
  252. self.assertIn("fakeroot/info/refs", fsc.store)
  253. self.assertIn("fakeroot/description", fsc.store)
  254. @skipIf(missing_libs, skipmsg)
  255. class TestSwiftInfoRefsContainer(TestCase):
  256. def setUp(self):
  257. super(TestSwiftInfoRefsContainer, self).setUp()
  258. content = (
  259. b"22effb216e3a82f97da599b8885a6cadb488b4c5\trefs/heads/master\n"
  260. b"cca703b0e1399008b53a1a236d6b4584737649e4\trefs/heads/dev"
  261. )
  262. self.store = {"fakerepo/info/refs": content}
  263. self.conf = swift.load_conf(file=StringIO(config_file % def_config_file))
  264. self.fsc = FakeSwiftConnector("fakerepo", conf=self.conf)
  265. self.object_store = {}
  266. def test_init(self):
  267. """info/refs does not exists"""
  268. irc = swift.SwiftInfoRefsContainer(self.fsc, self.object_store)
  269. self.assertEqual(len(irc._refs), 0)
  270. self.fsc.store = self.store
  271. irc = swift.SwiftInfoRefsContainer(self.fsc, self.object_store)
  272. self.assertIn(b"refs/heads/dev", irc.allkeys())
  273. self.assertIn(b"refs/heads/master", irc.allkeys())
  274. def test_set_if_equals(self):
  275. self.fsc.store = self.store
  276. irc = swift.SwiftInfoRefsContainer(self.fsc, self.object_store)
  277. irc.set_if_equals(
  278. b"refs/heads/dev",
  279. b"cca703b0e1399008b53a1a236d6b4584737649e4",
  280. b"1" * 40,
  281. )
  282. self.assertEqual(irc[b"refs/heads/dev"], b"1" * 40)
  283. def test_remove_if_equals(self):
  284. self.fsc.store = self.store
  285. irc = swift.SwiftInfoRefsContainer(self.fsc, self.object_store)
  286. irc.remove_if_equals(
  287. b"refs/heads/dev", b"cca703b0e1399008b53a1a236d6b4584737649e4"
  288. )
  289. self.assertNotIn(b"refs/heads/dev", irc.allkeys())
  290. @skipIf(missing_libs, skipmsg)
  291. class TestSwiftConnector(TestCase):
  292. def setUp(self):
  293. super(TestSwiftConnector, self).setUp()
  294. self.conf = swift.load_conf(file=StringIO(config_file % def_config_file))
  295. with patch("geventhttpclient.HTTPClient.request", fake_auth_request_v1):
  296. self.conn = swift.SwiftConnector("fakerepo", conf=self.conf)
  297. def test_init_connector(self):
  298. self.assertEqual(self.conn.auth_ver, "1")
  299. self.assertEqual(self.conn.auth_url, "http://127.0.0.1:8080/auth/v1.0")
  300. self.assertEqual(self.conn.user, "test:tester")
  301. self.assertEqual(self.conn.password, "testing")
  302. self.assertEqual(self.conn.root, "fakerepo")
  303. self.assertEqual(
  304. self.conn.storage_url, "http://127.0.0.1:8080/v1.0/AUTH_fakeuser"
  305. )
  306. self.assertEqual(self.conn.token, "12" * 10)
  307. self.assertEqual(self.conn.http_timeout, 1)
  308. self.assertEqual(self.conn.http_pool_length, 1)
  309. self.assertEqual(self.conn.concurrency, 1)
  310. self.conf.set("swift", "auth_ver", "2")
  311. self.conf.set("swift", "auth_url", "http://127.0.0.1:8080/auth/v2.0")
  312. with patch("geventhttpclient.HTTPClient.request", fake_auth_request_v2):
  313. conn = swift.SwiftConnector("fakerepo", conf=self.conf)
  314. self.assertEqual(conn.user, "tester")
  315. self.assertEqual(conn.tenant, "test")
  316. self.conf.set("swift", "auth_ver", "1")
  317. self.conf.set("swift", "auth_url", "http://127.0.0.1:8080/auth/v1.0")
  318. with patch("geventhttpclient.HTTPClient.request", fake_auth_request_v1_error):
  319. self.assertRaises(
  320. swift.SwiftException,
  321. lambda: swift.SwiftConnector("fakerepo", conf=self.conf),
  322. )
  323. def test_root_exists(self):
  324. with patch("geventhttpclient.HTTPClient.request", lambda *args: Response()):
  325. self.assertEqual(self.conn.test_root_exists(), True)
  326. def test_root_not_exists(self):
  327. with patch(
  328. "geventhttpclient.HTTPClient.request",
  329. lambda *args: Response(status=404),
  330. ):
  331. self.assertEqual(self.conn.test_root_exists(), None)
  332. def test_create_root(self):
  333. with patch(
  334. "dulwich.contrib.swift.SwiftConnector.test_root_exists",
  335. lambda *args: None,
  336. ):
  337. with patch("geventhttpclient.HTTPClient.request", lambda *args: Response()):
  338. self.assertEqual(self.conn.create_root(), None)
  339. def test_create_root_fails(self):
  340. with patch(
  341. "dulwich.contrib.swift.SwiftConnector.test_root_exists",
  342. lambda *args: None,
  343. ):
  344. with patch(
  345. "geventhttpclient.HTTPClient.request",
  346. lambda *args: Response(status=404),
  347. ):
  348. self.assertRaises(swift.SwiftException, lambda: self.conn.create_root())
  349. def test_get_container_objects(self):
  350. with patch(
  351. "geventhttpclient.HTTPClient.request",
  352. lambda *args: Response(
  353. content=json.dumps((({"name": "a"}, {"name": "b"})))
  354. ),
  355. ):
  356. self.assertEqual(len(self.conn.get_container_objects()), 2)
  357. def test_get_container_objects_fails(self):
  358. with patch(
  359. "geventhttpclient.HTTPClient.request",
  360. lambda *args: Response(status=404),
  361. ):
  362. self.assertEqual(self.conn.get_container_objects(), None)
  363. def test_get_object_stat(self):
  364. with patch(
  365. "geventhttpclient.HTTPClient.request",
  366. lambda *args: Response(headers={"content-length": "10"}),
  367. ):
  368. self.assertEqual(self.conn.get_object_stat("a")["content-length"], "10")
  369. def test_get_object_stat_fails(self):
  370. with patch(
  371. "geventhttpclient.HTTPClient.request",
  372. lambda *args: Response(status=404),
  373. ):
  374. self.assertEqual(self.conn.get_object_stat("a"), None)
  375. def test_put_object(self):
  376. with patch(
  377. "geventhttpclient.HTTPClient.request",
  378. lambda *args, **kwargs: Response(),
  379. ):
  380. self.assertEqual(self.conn.put_object("a", BytesIO(b"content")), None)
  381. def test_put_object_fails(self):
  382. with patch(
  383. "geventhttpclient.HTTPClient.request",
  384. lambda *args, **kwargs: Response(status=400),
  385. ):
  386. self.assertRaises(
  387. swift.SwiftException,
  388. lambda: self.conn.put_object("a", BytesIO(b"content")),
  389. )
  390. def test_get_object(self):
  391. with patch(
  392. "geventhttpclient.HTTPClient.request",
  393. lambda *args, **kwargs: Response(content=b"content"),
  394. ):
  395. self.assertEqual(self.conn.get_object("a").read(), b"content")
  396. with patch(
  397. "geventhttpclient.HTTPClient.request",
  398. lambda *args, **kwargs: Response(content=b"content"),
  399. ):
  400. self.assertEqual(self.conn.get_object("a", range="0-6"), b"content")
  401. def test_get_object_fails(self):
  402. with patch(
  403. "geventhttpclient.HTTPClient.request",
  404. lambda *args, **kwargs: Response(status=404),
  405. ):
  406. self.assertEqual(self.conn.get_object("a"), None)
  407. def test_del_object(self):
  408. with patch("geventhttpclient.HTTPClient.request", lambda *args: Response()):
  409. self.assertEqual(self.conn.del_object("a"), None)
  410. def test_del_root(self):
  411. with patch(
  412. "dulwich.contrib.swift.SwiftConnector.del_object",
  413. lambda *args: None,
  414. ):
  415. with patch(
  416. "dulwich.contrib.swift.SwiftConnector." "get_container_objects",
  417. lambda *args: ({"name": "a"}, {"name": "b"}),
  418. ):
  419. with patch(
  420. "geventhttpclient.HTTPClient.request",
  421. lambda *args: Response(),
  422. ):
  423. self.assertEqual(self.conn.del_root(), None)
  424. @skipIf(missing_libs, skipmsg)
  425. class SwiftObjectStoreTests(ObjectStoreTests, TestCase):
  426. def setUp(self):
  427. TestCase.setUp(self)
  428. conf = swift.load_conf(file=StringIO(config_file % def_config_file))
  429. fsc = FakeSwiftConnector("fakerepo", conf=conf)
  430. self.store = swift.SwiftObjectStore(fsc)