test_swift.py 16 KB

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