test_s3_gateway_multitenant.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. # -*- coding: utf-8 -*-
  2. # (c) Nelen & Schuurmans
  3. import io
  4. from datetime import datetime
  5. import boto3
  6. import pytest
  7. from botocore.exceptions import ClientError
  8. from clean_python import ctx
  9. from clean_python import DoesNotExist
  10. from clean_python import Filter
  11. from clean_python import PageOptions
  12. from clean_python import Tenant
  13. from clean_python.s3 import S3BucketOptions
  14. from clean_python.s3 import S3BucketProvider
  15. from clean_python.s3 import S3Gateway
  16. @pytest.fixture(scope="session")
  17. def s3_settings(s3_url):
  18. minio_settings = {
  19. "url": s3_url,
  20. "access_key": "cleanpython",
  21. "secret_key": "cleanpython",
  22. "bucket": "cleanpython-test",
  23. "region": None,
  24. }
  25. if not minio_settings["bucket"].endswith("-test"): # type: ignore
  26. pytest.exit("Not running against a test minio bucket?! 😱")
  27. return minio_settings.copy()
  28. @pytest.fixture(scope="session")
  29. def s3_bucket(s3_settings):
  30. s3 = boto3.resource(
  31. "s3",
  32. endpoint_url=s3_settings["url"],
  33. aws_access_key_id=s3_settings["access_key"],
  34. aws_secret_access_key=s3_settings["secret_key"],
  35. )
  36. bucket = s3.Bucket(s3_settings["bucket"])
  37. # ensure existence
  38. try:
  39. bucket.create()
  40. except ClientError as e:
  41. if "BucketAlreadyOwnedByYou" in str(e):
  42. pass
  43. return bucket
  44. @pytest.fixture
  45. def s3_provider(s3_bucket, s3_settings):
  46. # wipe contents before each test
  47. s3_bucket.objects.all().delete()
  48. # set up a tenant
  49. ctx.tenant = Tenant(id=22, name="foo")
  50. return S3BucketProvider(S3BucketOptions(**s3_settings))
  51. @pytest.fixture
  52. def s3_gateway(s3_provider):
  53. return S3Gateway(s3_provider, multitenant=True)
  54. @pytest.fixture
  55. def object_in_s3(s3_bucket):
  56. s3_bucket.upload_fileobj(io.BytesIO(b"foo"), "tenant-22/object-in-s3")
  57. return "object-in-s3"
  58. @pytest.fixture
  59. def object_in_s3_other_tenant(s3_bucket):
  60. s3_bucket.upload_fileobj(io.BytesIO(b"foo"), "tenant-222/object-in-s3")
  61. return "object-in-s3"
  62. @pytest.fixture
  63. def local_file(tmp_path):
  64. path = tmp_path / "test-upload.txt"
  65. path.write_bytes(b"foo")
  66. return path
  67. async def test_upload_file_uses_tenant(s3_gateway: S3Gateway, local_file, s3_bucket):
  68. object_name = "test-upload-file"
  69. await s3_gateway.upload_file(object_name, local_file)
  70. assert s3_bucket.Object("tenant-22/test-upload-file").content_length == 3
  71. async def test_download_file_uses_tenant(s3_gateway: S3Gateway, object_in_s3, tmp_path):
  72. path = tmp_path / "test-download.txt"
  73. await s3_gateway.download_file(object_in_s3, path)
  74. assert path.read_bytes() == b"foo"
  75. async def test_download_file_different_tenant(
  76. s3_gateway: S3Gateway, s3_bucket, tmp_path, object_in_s3_other_tenant
  77. ):
  78. path = tmp_path / "test-download.txt"
  79. with pytest.raises(DoesNotExist):
  80. await s3_gateway.download_file("object-in-s3", path)
  81. assert not path.exists()
  82. async def test_remove_uses_tenant(s3_gateway: S3Gateway, s3_bucket, object_in_s3):
  83. await s3_gateway.remove(object_in_s3)
  84. assert await s3_gateway.get(object_in_s3) is None
  85. async def test_remove_other_tenant(
  86. s3_gateway: S3Gateway, s3_bucket, object_in_s3_other_tenant
  87. ):
  88. await s3_gateway.remove(object_in_s3_other_tenant)
  89. # it is still there
  90. assert s3_bucket.Object("tenant-222/object-in-s3").content_length == 3
  91. @pytest.fixture
  92. def multiple_objects(s3_bucket):
  93. s3_bucket.upload_fileobj(io.BytesIO(b"a"), "tenant-22/raster-1/bla")
  94. s3_bucket.upload_fileobj(io.BytesIO(b"ab"), "tenant-222/raster-2/bla")
  95. s3_bucket.upload_fileobj(io.BytesIO(b"abc"), "tenant-22/raster-2/foo")
  96. s3_bucket.upload_fileobj(io.BytesIO(b"abcde"), "tenant-22/raster-2/bz")
  97. return ["raster-1/bla", "raster-2/bla", "raster-2/foo", "raster-2/bz"]
  98. async def test_remove_multiple_multitenant(
  99. s3_gateway: S3Gateway, multiple_objects, s3_bucket
  100. ):
  101. await s3_gateway.remove_multiple(multiple_objects[:2])
  102. assert await s3_gateway.get(multiple_objects[0]) is None
  103. # the other-tenant object is still there
  104. assert s3_bucket.Object("tenant-222/raster-2/bla").content_length == 2
  105. async def test_filter_multitenant(s3_gateway: S3Gateway, multiple_objects):
  106. actual = await s3_gateway.filter([], params=PageOptions(limit=10))
  107. assert len(actual) == 3
  108. assert actual[0]["id"] == "raster-1/bla"
  109. async def test_filter_with_prefix_multitenant(s3_gateway: S3Gateway, multiple_objects):
  110. actual = await s3_gateway.filter(
  111. [Filter(field="prefix", values=["raster-2/"])], params=PageOptions(limit=10)
  112. )
  113. assert len(actual) == 2
  114. assert actual[0]["id"] == "raster-2/bz"
  115. assert actual[1]["id"] == "raster-2/foo"
  116. async def test_filter_with_cursor_multitenant(s3_gateway: S3Gateway, multiple_objects):
  117. actual = await s3_gateway.filter(
  118. [], params=PageOptions(limit=3, cursor="raster-2/bz")
  119. )
  120. assert len(actual) == 1
  121. assert actual[0]["id"] == "raster-2/foo"
  122. async def test_get_multitenant(s3_gateway: S3Gateway, object_in_s3):
  123. actual = await s3_gateway.get(object_in_s3)
  124. assert actual["id"] == object_in_s3
  125. assert isinstance(actual["last_modified"], datetime)
  126. assert actual["etag"] == "acbd18db4cc2f85cedef654fccc4a4d8"
  127. assert actual["size"] == 3
  128. async def test_get_other_tenant(s3_gateway: S3Gateway, object_in_s3_other_tenant):
  129. actual = await s3_gateway.get(object_in_s3_other_tenant)
  130. assert actual is None
  131. async def test_remove_filtered_all(s3_gateway: S3Gateway, multiple_objects):
  132. await s3_gateway.remove_filtered([])
  133. # tenant 22 is completely wiped
  134. for i in (0, 2, 3):
  135. assert await s3_gateway.get(multiple_objects[i]) is None
  136. # object of tenant 222 is still there
  137. ctx.tenant = Tenant(id=222, name="other")
  138. await s3_gateway.get("raster-2/bla") is not None
  139. async def test_remove_filtered_prefix(s3_gateway: S3Gateway, multiple_objects):
  140. await s3_gateway.remove_filtered([Filter(field="prefix", values=["raster-2/"])])
  141. assert await s3_gateway.get("raster-1/bla") is not None
  142. assert await s3_gateway.get("raster-2/foo") is None
  143. assert await s3_gateway.get("raster-2/bz") is None
  144. # object of tenant 222 is still there
  145. ctx.tenant = Tenant(id=222, name="other")
  146. await s3_gateway.get("raster-2/bla") is not None