test_swift.py 16 KB

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