mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
2388 lines
95 KiB
Python
2388 lines
95 KiB
Python
from functools import cached_property
|
|
from io import BytesIO
|
|
from pathlib import Path
|
|
from tempfile import TemporaryDirectory
|
|
from types import SimpleNamespace
|
|
from unittest.mock import MagicMock, Mock, patch
|
|
from uuid import uuid4
|
|
|
|
import pytest
|
|
import yaml
|
|
from django.core.files.storage import Storage
|
|
from google.cloud.devtools.cloudbuild_v1 import Build
|
|
from google_crc32c import value as crc32c
|
|
|
|
from sentry.backup.crypto import (
|
|
LocalFileDecryptor,
|
|
LocalFileEncryptor,
|
|
create_encrypted_export_tarball,
|
|
decrypt_encrypted_tarball,
|
|
unwrap_encrypted_export_tarball,
|
|
)
|
|
from sentry.backup.dependencies import NormalizedModelName, get_model_name
|
|
from sentry.backup.helpers import ImportFlags, Printer
|
|
from sentry.backup.imports import import_in_organization_scope
|
|
from sentry.models.files.file import File
|
|
from sentry.models.files.utils import get_relocation_storage, get_storage
|
|
from sentry.models.importchunk import (
|
|
ControlImportChunk,
|
|
ControlImportChunkReplica,
|
|
RegionImportChunk,
|
|
)
|
|
from sentry.models.organization import Organization
|
|
from sentry.models.organizationmember import OrganizationMember
|
|
from sentry.models.relocation import (
|
|
Relocation,
|
|
RelocationFile,
|
|
RelocationValidation,
|
|
RelocationValidationAttempt,
|
|
ValidationStatus,
|
|
)
|
|
from sentry.models.user import User
|
|
from sentry.silo.base import SiloMode
|
|
from sentry.tasks.relocation import (
|
|
ERR_NOTIFYING_INTERNAL,
|
|
ERR_POSTPROCESSING_INTERNAL,
|
|
ERR_PREPROCESSING_DECRYPTION,
|
|
ERR_PREPROCESSING_INTERNAL,
|
|
ERR_PREPROCESSING_INVALID_JSON,
|
|
ERR_PREPROCESSING_INVALID_TARBALL,
|
|
ERR_PREPROCESSING_MISSING_ORGS,
|
|
ERR_PREPROCESSING_NO_ORGS,
|
|
ERR_PREPROCESSING_NO_USERS,
|
|
ERR_PREPROCESSING_TOO_MANY_ORGS,
|
|
ERR_PREPROCESSING_TOO_MANY_USERS,
|
|
ERR_UPLOADING_FAILED,
|
|
ERR_VALIDATING_INTERNAL,
|
|
ERR_VALIDATING_MAX_RUNS,
|
|
MAX_FAST_TASK_ATTEMPTS,
|
|
MAX_FAST_TASK_RETRIES,
|
|
MAX_VALIDATION_POLL_ATTEMPTS,
|
|
MAX_VALIDATION_POLLS,
|
|
LostPasswordHash,
|
|
completed,
|
|
importing,
|
|
notifying_owner,
|
|
notifying_users,
|
|
postprocessing,
|
|
preprocessing_baseline_config,
|
|
preprocessing_colliding_users,
|
|
preprocessing_complete,
|
|
preprocessing_scan,
|
|
preprocessing_transfer,
|
|
uploading_complete,
|
|
validating_complete,
|
|
validating_poll,
|
|
validating_start,
|
|
)
|
|
from sentry.testutils.cases import TestCase, TransactionTestCase
|
|
from sentry.testutils.factories import get_fixture_path
|
|
from sentry.testutils.helpers.backups import FakeKeyManagementServiceClient, generate_rsa_key_pair
|
|
from sentry.testutils.helpers.task_runner import BurstTaskRunner, BustTaskRunnerRetryError
|
|
from sentry.testutils.silo import assume_test_silo_mode, region_silo_test
|
|
from sentry.utils import json
|
|
from sentry.utils.relocation import RELOCATION_BLOB_SIZE, RELOCATION_FILE_TYPE, OrderedTask
|
|
|
|
IMPORT_JSON_FILE_PATH = get_fixture_path("backup", "fresh-install.json")
|
|
|
|
|
|
class FakeCloudBuildClient:
|
|
"""
|
|
Fake version of `CloudBuildClient` that removes the two network calls we rely on.
|
|
"""
|
|
|
|
create_build = MagicMock()
|
|
get_build = MagicMock()
|
|
|
|
|
|
class RelocationTaskTestCase(TestCase):
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.owner = self.create_user(
|
|
email="[email protected]", is_superuser=False, is_staff=False, is_active=True
|
|
)
|
|
self.superuser = self.create_user(
|
|
email="[email protected]", is_superuser=True, is_staff=True, is_active=True
|
|
)
|
|
self.login_as(user=self.superuser, superuser=True)
|
|
self.relocation: Relocation = Relocation.objects.create(
|
|
creator_id=self.superuser.id,
|
|
owner_id=self.owner.id,
|
|
want_org_slugs=["testing"],
|
|
step=Relocation.Step.UPLOADING.value,
|
|
)
|
|
self.relocation_file = RelocationFile.objects.create(
|
|
relocation=self.relocation,
|
|
file=self.file,
|
|
kind=RelocationFile.Kind.RAW_USER_DATA.value,
|
|
)
|
|
self.uuid = str(self.relocation.uuid)
|
|
|
|
@cached_property
|
|
def file(self):
|
|
with TemporaryDirectory() as tmp_dir:
|
|
(priv_key_pem, pub_key_pem) = generate_rsa_key_pair()
|
|
tmp_priv_key_path = Path(tmp_dir).joinpath("key")
|
|
self.priv_key_pem = priv_key_pem
|
|
with open(tmp_priv_key_path, "wb") as f:
|
|
f.write(priv_key_pem)
|
|
|
|
tmp_pub_key_path = Path(tmp_dir).joinpath("key.pub")
|
|
self.pub_key_pem = pub_key_pem
|
|
with open(tmp_pub_key_path, "wb") as f:
|
|
f.write(pub_key_pem)
|
|
|
|
with open(IMPORT_JSON_FILE_PATH, "rb") as f:
|
|
data = json.load(f)
|
|
with open(tmp_pub_key_path, "rb") as p:
|
|
file = File.objects.create(name="export.tar", type=RELOCATION_FILE_TYPE)
|
|
self.tarball = create_encrypted_export_tarball(
|
|
data, LocalFileEncryptor(p)
|
|
).getvalue()
|
|
file.putfile(BytesIO(self.tarball))
|
|
|
|
return file
|
|
|
|
def swap_file(
|
|
self, file: File, fixture_name: str, blob_size: int = RELOCATION_BLOB_SIZE
|
|
) -> None:
|
|
with TemporaryDirectory() as tmp_dir:
|
|
tmp_priv_key_path = Path(tmp_dir).joinpath("key")
|
|
tmp_pub_key_path = Path(tmp_dir).joinpath("key.pub")
|
|
with open(tmp_priv_key_path, "wb") as f:
|
|
f.write(self.priv_key_pem)
|
|
with open(tmp_pub_key_path, "wb") as f:
|
|
f.write(self.pub_key_pem)
|
|
with open(get_fixture_path("backup", fixture_name)) as f:
|
|
data = json.load(f)
|
|
with open(tmp_pub_key_path, "rb") as p:
|
|
self.tarball = create_encrypted_export_tarball(
|
|
data, LocalFileEncryptor(p)
|
|
).getvalue()
|
|
file.putfile(BytesIO(self.tarball), blob_size=blob_size)
|
|
|
|
def mock_kms_client(self, fake_kms_client: FakeKeyManagementServiceClient):
|
|
fake_kms_client.asymmetric_decrypt.call_count = 0
|
|
fake_kms_client.get_public_key.call_count = 0
|
|
|
|
unwrapped = unwrap_encrypted_export_tarball(BytesIO(self.tarball))
|
|
plaintext_dek = LocalFileDecryptor.from_bytes(
|
|
self.priv_key_pem
|
|
).decrypt_data_encryption_key(unwrapped)
|
|
|
|
fake_kms_client.asymmetric_decrypt.return_value = SimpleNamespace(
|
|
plaintext=plaintext_dek,
|
|
plaintext_crc32c=crc32c(plaintext_dek),
|
|
)
|
|
fake_kms_client.asymmetric_decrypt.side_effect = None
|
|
|
|
fake_kms_client.get_public_key.return_value = SimpleNamespace(
|
|
pem=self.pub_key_pem.decode("utf-8")
|
|
)
|
|
fake_kms_client.get_public_key.side_effect = None
|
|
|
|
def mock_cloudbuild_client(
|
|
self, fake_cloudbuild_client: FakeCloudBuildClient, status: Build.Status
|
|
):
|
|
fake_cloudbuild_client.create_build.call_count = 0
|
|
fake_cloudbuild_client.get_build.call_count = 0
|
|
|
|
fake_cloudbuild_client.create_build.return_value = SimpleNamespace(
|
|
metadata=SimpleNamespace(build=SimpleNamespace(id=uuid4().hex))
|
|
)
|
|
fake_cloudbuild_client.create_build.side_effect = None
|
|
|
|
fake_cloudbuild_client.get_build.return_value = SimpleNamespace(status=status)
|
|
fake_cloudbuild_client.get_build.side_effect = None
|
|
|
|
def mock_message_builder(self, fake_message_builder: Mock):
|
|
fake_message_builder.return_value.send_async.return_value = Mock()
|
|
|
|
|
|
@region_silo_test
|
|
@patch("sentry.utils.relocation.MessageBuilder")
|
|
@patch("sentry.tasks.relocation.preprocessing_scan.apply_async")
|
|
class UploadingCompleteTest(RelocationTaskTestCase):
|
|
def test_success(
|
|
self,
|
|
preprocessing_scan_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
):
|
|
self.mock_message_builder(fake_message_builder)
|
|
|
|
uploading_complete(self.uuid)
|
|
|
|
assert fake_message_builder.call_count == 0
|
|
assert preprocessing_scan_mock.call_count == 1
|
|
|
|
def test_retry_if_attempts_left(
|
|
self,
|
|
preprocessing_scan_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
):
|
|
RelocationFile.objects.filter(relocation=self.relocation).delete()
|
|
self.mock_message_builder(fake_message_builder)
|
|
|
|
# An exception being raised will trigger a retry in celery.
|
|
with pytest.raises(Exception):
|
|
uploading_complete(self.uuid)
|
|
|
|
assert fake_message_builder.call_count == 0
|
|
assert preprocessing_scan_mock.call_count == 0
|
|
|
|
relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.status == Relocation.Status.IN_PROGRESS.value
|
|
assert not relocation.failure_reason
|
|
|
|
def test_fail_if_no_attempts_left(
|
|
self,
|
|
preprocessing_scan_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
):
|
|
self.relocation.latest_task = OrderedTask.UPLOADING_COMPLETE.name
|
|
self.relocation.latest_task_attempts = MAX_FAST_TASK_RETRIES
|
|
self.relocation.save()
|
|
RelocationFile.objects.filter(relocation=self.relocation).delete()
|
|
self.mock_message_builder(fake_message_builder)
|
|
|
|
with pytest.raises(Exception):
|
|
uploading_complete(self.uuid)
|
|
|
|
assert fake_message_builder.call_count == 1
|
|
assert fake_message_builder.call_args.kwargs["type"] == "relocation.failed"
|
|
fake_message_builder.return_value.send_async.assert_called_once_with(
|
|
to=[self.owner.email, self.superuser.email]
|
|
)
|
|
|
|
assert preprocessing_scan_mock.call_count == 0
|
|
|
|
relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.status == Relocation.Status.FAILURE.value
|
|
assert relocation.failure_reason == ERR_UPLOADING_FAILED
|
|
|
|
|
|
@region_silo_test
|
|
@patch(
|
|
"sentry.backup.crypto.KeyManagementServiceClient",
|
|
new_callable=lambda: FakeKeyManagementServiceClient,
|
|
)
|
|
@patch("sentry.utils.relocation.MessageBuilder")
|
|
@patch("sentry.tasks.relocation.preprocessing_transfer.apply_async")
|
|
class PreprocessingScanTest(RelocationTaskTestCase):
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.relocation.step = Relocation.Step.UPLOADING.value
|
|
self.relocation.latest_task = OrderedTask.UPLOADING_COMPLETE.name
|
|
self.relocation.save()
|
|
|
|
def test_success_admin_assisted_relocation(
|
|
self,
|
|
preprocessing_transfer_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
fake_kms_client: FakeKeyManagementServiceClient,
|
|
):
|
|
self.mock_message_builder(fake_message_builder)
|
|
self.mock_kms_client(fake_kms_client)
|
|
|
|
preprocessing_scan(self.uuid)
|
|
|
|
assert fake_kms_client.asymmetric_decrypt.call_count == 1
|
|
assert fake_kms_client.get_public_key.call_count == 0
|
|
|
|
assert fake_message_builder.call_count == 1
|
|
assert fake_message_builder.call_args.kwargs["type"] == "relocation.started"
|
|
fake_message_builder.return_value.send_async.assert_called_once_with(
|
|
to=[self.owner.email, self.superuser.email]
|
|
)
|
|
|
|
assert preprocessing_transfer_mock.call_count == 1
|
|
|
|
relocation: Relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.want_usernames == [
|
|
"[email protected]",
|
|
"[email protected]",
|
|
]
|
|
assert relocation.latest_notified == Relocation.EmailKind.STARTED.value
|
|
|
|
def test_success_self_service_relocation(
|
|
self,
|
|
preprocessing_transfer_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
fake_kms_client: FakeKeyManagementServiceClient,
|
|
):
|
|
self.mock_kms_client(fake_kms_client)
|
|
self.relocation.creator_id = self.relocation.owner_id
|
|
self.relocation.save()
|
|
|
|
preprocessing_scan(self.uuid)
|
|
|
|
assert fake_kms_client.asymmetric_decrypt.call_count == 1
|
|
assert fake_kms_client.get_public_key.call_count == 0
|
|
|
|
assert fake_message_builder.call_count == 1
|
|
assert fake_message_builder.call_args.kwargs["type"] == "relocation.started"
|
|
fake_message_builder.return_value.send_async.assert_called_once_with(to=[self.owner.email])
|
|
|
|
assert preprocessing_transfer_mock.call_count == 1
|
|
|
|
relocation: Relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.want_usernames == [
|
|
"[email protected]",
|
|
"[email protected]",
|
|
]
|
|
assert relocation.latest_notified == Relocation.EmailKind.STARTED.value
|
|
|
|
def test_pause(
|
|
self,
|
|
preprocessing_transfer_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
fake_kms_client: FakeKeyManagementServiceClient,
|
|
):
|
|
self.mock_message_builder(fake_message_builder)
|
|
self.mock_kms_client(fake_kms_client)
|
|
self.relocation.scheduled_pause_at_step = Relocation.Step.PREPROCESSING.value
|
|
self.relocation.save()
|
|
|
|
preprocessing_scan(self.uuid)
|
|
|
|
assert fake_kms_client.asymmetric_decrypt.call_count == 0
|
|
assert fake_kms_client.get_public_key.call_count == 0
|
|
assert fake_message_builder.call_count == 0
|
|
assert preprocessing_transfer_mock.call_count == 0
|
|
|
|
relocation: Relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.status == Relocation.Status.PAUSE.value
|
|
assert relocation.step == Relocation.Step.PREPROCESSING.value
|
|
assert relocation.scheduled_pause_at_step is None
|
|
assert relocation.latest_task == OrderedTask.PREPROCESSING_SCAN.name
|
|
|
|
def test_retry_if_attempts_left(
|
|
self,
|
|
preprocessing_transfer_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
fake_kms_client: FakeKeyManagementServiceClient,
|
|
):
|
|
RelocationFile.objects.filter(relocation=self.relocation).delete()
|
|
self.mock_message_builder(fake_message_builder)
|
|
self.mock_kms_client(fake_kms_client)
|
|
|
|
# An exception being raised will trigger a retry in celery.
|
|
with pytest.raises(Exception):
|
|
preprocessing_scan(self.uuid)
|
|
|
|
assert fake_kms_client.asymmetric_decrypt.call_count == 0
|
|
assert fake_kms_client.get_public_key.call_count == 0
|
|
assert fake_message_builder.call_count == 0
|
|
assert preprocessing_transfer_mock.call_count == 0
|
|
|
|
relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.status == Relocation.Status.IN_PROGRESS.value
|
|
assert relocation.latest_notified != Relocation.EmailKind.FAILED.value
|
|
assert not relocation.failure_reason
|
|
|
|
def test_fail_if_no_attempts_left(
|
|
self,
|
|
preprocessing_transfer_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
fake_kms_client: FakeKeyManagementServiceClient,
|
|
):
|
|
self.relocation.latest_task = OrderedTask.PREPROCESSING_SCAN.name
|
|
self.relocation.latest_task_attempts = MAX_FAST_TASK_RETRIES
|
|
self.relocation.save()
|
|
RelocationFile.objects.filter(relocation=self.relocation).delete()
|
|
self.mock_message_builder(fake_message_builder)
|
|
self.mock_kms_client(fake_kms_client)
|
|
|
|
with pytest.raises(Exception):
|
|
preprocessing_scan(self.uuid)
|
|
|
|
assert fake_kms_client.asymmetric_decrypt.call_count == 0
|
|
assert fake_kms_client.get_public_key.call_count == 0
|
|
|
|
assert fake_message_builder.call_count == 1
|
|
assert fake_message_builder.call_args.kwargs["type"] == "relocation.failed"
|
|
fake_message_builder.return_value.send_async.assert_called_once_with(
|
|
to=[self.owner.email, self.superuser.email]
|
|
)
|
|
|
|
assert preprocessing_transfer_mock.call_count == 0
|
|
|
|
relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.status == Relocation.Status.FAILURE.value
|
|
assert relocation.latest_notified == Relocation.EmailKind.FAILED.value
|
|
assert relocation.failure_reason == ERR_PREPROCESSING_INTERNAL
|
|
|
|
def test_fail_invalid_tarball(
|
|
self,
|
|
preprocessing_transfer_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
fake_kms_client: FakeKeyManagementServiceClient,
|
|
):
|
|
file = RelocationFile.objects.get(relocation=self.relocation).file
|
|
corrupted_tarball_bytes = bytearray(file.getfile().read())[9:]
|
|
file.putfile(BytesIO(bytes(corrupted_tarball_bytes)))
|
|
self.mock_message_builder(fake_message_builder)
|
|
self.mock_kms_client(fake_kms_client)
|
|
|
|
preprocessing_scan(self.uuid)
|
|
|
|
assert fake_message_builder.call_count == 1
|
|
assert fake_message_builder.call_args.kwargs["type"] == "relocation.failed"
|
|
fake_message_builder.return_value.send_async.assert_called_once_with(
|
|
to=[self.owner.email, self.superuser.email]
|
|
)
|
|
|
|
assert preprocessing_transfer_mock.call_count == 0
|
|
|
|
relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.status == Relocation.Status.FAILURE.value
|
|
assert relocation.latest_notified == Relocation.EmailKind.FAILED.value
|
|
assert relocation.failure_reason == ERR_PREPROCESSING_INVALID_TARBALL
|
|
|
|
def test_fail_decryption_failure(
|
|
self,
|
|
preprocessing_transfer_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
fake_kms_client: FakeKeyManagementServiceClient,
|
|
):
|
|
# Add invalid 2-octet UTF-8 sequence to the returned plaintext.
|
|
self.mock_message_builder(fake_message_builder)
|
|
self.mock_kms_client(fake_kms_client)
|
|
fake_kms_client.asymmetric_decrypt.return_value.plaintext += b"\xc3\x28"
|
|
|
|
# We retry on decryption failures, just to account for flakiness on the KMS server's side.
|
|
# Try this as the last attempt to see the actual error.
|
|
self.relocation.latest_task = OrderedTask.PREPROCESSING_SCAN.name
|
|
self.relocation.latest_task_attempts = MAX_FAST_TASK_RETRIES
|
|
self.relocation.save()
|
|
|
|
with pytest.raises(Exception):
|
|
preprocessing_scan(self.uuid)
|
|
|
|
assert fake_message_builder.call_count == 1
|
|
assert fake_message_builder.call_args.kwargs["type"] == "relocation.failed"
|
|
fake_message_builder.return_value.send_async.assert_called_once_with(
|
|
to=[self.owner.email, self.superuser.email]
|
|
)
|
|
|
|
assert fake_kms_client.asymmetric_decrypt.call_count == 1
|
|
assert preprocessing_transfer_mock.call_count == 0
|
|
|
|
relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.status == Relocation.Status.FAILURE.value
|
|
assert relocation.latest_notified == Relocation.EmailKind.FAILED.value
|
|
assert relocation.failure_reason == ERR_PREPROCESSING_DECRYPTION
|
|
|
|
def test_fail_invalid_json(
|
|
self,
|
|
preprocessing_transfer_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
fake_kms_client: FakeKeyManagementServiceClient,
|
|
):
|
|
file = RelocationFile.objects.get(relocation=self.relocation).file
|
|
self.swap_file(file, "invalid-user.json")
|
|
self.mock_message_builder(fake_message_builder)
|
|
self.mock_kms_client(fake_kms_client)
|
|
|
|
preprocessing_scan(self.uuid)
|
|
|
|
assert fake_message_builder.call_count == 1
|
|
assert fake_message_builder.call_args.kwargs["type"] == "relocation.failed"
|
|
fake_message_builder.return_value.send_async.assert_called_once_with(
|
|
to=[self.owner.email, self.superuser.email]
|
|
)
|
|
|
|
assert preprocessing_transfer_mock.call_count == 0
|
|
|
|
relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.status == Relocation.Status.FAILURE.value
|
|
assert relocation.latest_notified == Relocation.EmailKind.FAILED.value
|
|
assert relocation.failure_reason == ERR_PREPROCESSING_INVALID_JSON
|
|
|
|
def test_fail_no_users(
|
|
self,
|
|
preprocessing_transfer_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
fake_kms_client: FakeKeyManagementServiceClient,
|
|
):
|
|
file = RelocationFile.objects.get(relocation=self.relocation).file
|
|
self.swap_file(file, "single-option.json")
|
|
self.mock_message_builder(fake_message_builder)
|
|
self.mock_kms_client(fake_kms_client)
|
|
|
|
preprocessing_scan(self.uuid)
|
|
|
|
assert fake_message_builder.call_count == 1
|
|
assert fake_message_builder.call_args.kwargs["type"] == "relocation.failed"
|
|
fake_message_builder.return_value.send_async.assert_called_once_with(
|
|
to=[self.owner.email, self.superuser.email]
|
|
)
|
|
|
|
assert preprocessing_transfer_mock.call_count == 0
|
|
|
|
relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.status == Relocation.Status.FAILURE.value
|
|
assert relocation.latest_notified == Relocation.EmailKind.FAILED.value
|
|
assert relocation.failure_reason == ERR_PREPROCESSING_NO_USERS
|
|
|
|
@patch("sentry.tasks.relocation.MAX_USERS_PER_RELOCATION", 0)
|
|
def test_fail_too_many_users(
|
|
self,
|
|
preprocessing_transfer_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
fake_kms_client: FakeKeyManagementServiceClient,
|
|
):
|
|
self.mock_message_builder(fake_message_builder)
|
|
self.mock_kms_client(fake_kms_client)
|
|
|
|
preprocessing_scan(self.uuid)
|
|
|
|
assert fake_message_builder.call_count == 1
|
|
assert fake_message_builder.call_args.kwargs["type"] == "relocation.failed"
|
|
fake_message_builder.return_value.send_async.assert_called_once_with(
|
|
to=[self.owner.email, self.superuser.email]
|
|
)
|
|
|
|
assert preprocessing_transfer_mock.call_count == 0
|
|
|
|
relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.status == Relocation.Status.FAILURE.value
|
|
assert relocation.latest_notified == Relocation.EmailKind.FAILED.value
|
|
assert relocation.failure_reason == ERR_PREPROCESSING_TOO_MANY_USERS.substitute(count=2)
|
|
|
|
def test_fail_no_orgs(
|
|
self,
|
|
preprocessing_transfer_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
fake_kms_client: FakeKeyManagementServiceClient,
|
|
):
|
|
file = RelocationFile.objects.get(relocation=self.relocation).file
|
|
self.swap_file(file, "user-with-minimum-privileges.json")
|
|
self.mock_message_builder(fake_message_builder)
|
|
self.mock_kms_client(fake_kms_client)
|
|
|
|
preprocessing_scan(self.uuid)
|
|
|
|
assert fake_message_builder.call_count == 1
|
|
assert fake_message_builder.call_args.kwargs["type"] == "relocation.failed"
|
|
fake_message_builder.return_value.send_async.assert_called_once_with(
|
|
to=[self.owner.email, self.superuser.email]
|
|
)
|
|
|
|
assert preprocessing_transfer_mock.call_count == 0
|
|
|
|
relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.status == Relocation.Status.FAILURE.value
|
|
assert relocation.latest_notified == Relocation.EmailKind.FAILED.value
|
|
assert relocation.failure_reason == ERR_PREPROCESSING_NO_ORGS
|
|
|
|
@patch("sentry.tasks.relocation.MAX_ORGS_PER_RELOCATION", 0)
|
|
def test_fail_too_many_orgs(
|
|
self,
|
|
preprocessing_transfer_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
fake_kms_client: FakeKeyManagementServiceClient,
|
|
):
|
|
self.mock_message_builder(fake_message_builder)
|
|
self.mock_kms_client(fake_kms_client)
|
|
|
|
preprocessing_scan(self.uuid)
|
|
|
|
assert fake_message_builder.call_count == 1
|
|
assert fake_message_builder.call_args.kwargs["type"] == "relocation.failed"
|
|
fake_message_builder.return_value.send_async.assert_called_once_with(
|
|
to=[self.owner.email, self.superuser.email]
|
|
)
|
|
|
|
assert preprocessing_transfer_mock.call_count == 0
|
|
|
|
relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.status == Relocation.Status.FAILURE.value
|
|
assert relocation.latest_notified == Relocation.EmailKind.FAILED.value
|
|
assert relocation.failure_reason == ERR_PREPROCESSING_TOO_MANY_ORGS.substitute(count=1)
|
|
|
|
def test_fail_missing_orgs(
|
|
self,
|
|
preprocessing_transfer_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
fake_kms_client: FakeKeyManagementServiceClient,
|
|
):
|
|
orgs = ["does-not-exist"]
|
|
relocation = Relocation.objects.get(uuid=self.uuid)
|
|
relocation.want_org_slugs = orgs
|
|
relocation.save()
|
|
self.mock_message_builder(fake_message_builder)
|
|
self.mock_kms_client(fake_kms_client)
|
|
|
|
preprocessing_scan(self.uuid)
|
|
|
|
assert fake_message_builder.call_count == 1
|
|
assert fake_message_builder.call_args.kwargs["type"] == "relocation.failed"
|
|
fake_message_builder.return_value.send_async.assert_called_once_with(
|
|
to=[self.owner.email, self.superuser.email]
|
|
)
|
|
|
|
assert preprocessing_transfer_mock.call_count == 0
|
|
|
|
relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.status == Relocation.Status.FAILURE.value
|
|
assert relocation.latest_notified == Relocation.EmailKind.FAILED.value
|
|
assert relocation.failure_reason == ERR_PREPROCESSING_MISSING_ORGS.substitute(
|
|
orgs=",".join(orgs)
|
|
)
|
|
|
|
|
|
@region_silo_test
|
|
@patch("sentry.utils.relocation.MessageBuilder")
|
|
@patch("sentry.tasks.relocation.preprocessing_baseline_config.apply_async")
|
|
class PreprocessingTransferTest(RelocationTaskTestCase):
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.relocation.step = Relocation.Step.PREPROCESSING.value
|
|
self.relocation.latest_task = OrderedTask.PREPROCESSING_SCAN.name
|
|
self.relocation.want_usernames = ["importing"]
|
|
self.relocation.save()
|
|
self.create_user("importing")
|
|
self.relocation_storage = get_relocation_storage()
|
|
|
|
def test_success(
|
|
self,
|
|
preprocessing_baseline_config_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
):
|
|
self.mock_message_builder(fake_message_builder)
|
|
assert not self.relocation_storage.exists(f"runs/{self.uuid}")
|
|
|
|
preprocessing_transfer(self.uuid)
|
|
|
|
assert fake_message_builder.call_count == 0
|
|
assert preprocessing_baseline_config_mock.call_count == 1
|
|
|
|
(_, files) = self.relocation_storage.listdir(f"runs/{self.uuid}/conf")
|
|
assert len(files) == 2
|
|
assert "cloudbuild.yaml" in files
|
|
assert "cloudbuild.zip" in files
|
|
|
|
cb_yaml_file = self.relocation_storage.open(f"runs/{self.uuid}/conf/cloudbuild.yaml")
|
|
with cb_yaml_file:
|
|
cb_conf = yaml.safe_load(cb_yaml_file)
|
|
assert cb_conf is not None
|
|
|
|
# These entries in the generated `cloudbuild.yaml` depend on the UUID, so check them
|
|
# separately then replace them for snapshotting.
|
|
in_path = cb_conf["steps"][0]["args"][2]
|
|
findings_path = cb_conf["artifacts"]["objects"]["location"]
|
|
assert in_path == f"gs://default/runs/{self.uuid}/in"
|
|
assert findings_path == f"gs://default/runs/{self.uuid}/findings/"
|
|
|
|
# Do a snapshot test of the cloudbuild config.
|
|
cb_conf["steps"][0]["args"][2] = "gs://<BUCKET>/runs/<UUID>/in"
|
|
cb_conf["artifacts"]["objects"]["location"] = "gs://<BUCKET>/runs/<UUID>/findings/"
|
|
cb_conf["steps"][12]["args"][3] = "gs://<BUCKET>/runs/<UUID>/out"
|
|
self.insta_snapshot(cb_conf)
|
|
|
|
(_, files) = self.relocation_storage.listdir(f"runs/{self.uuid}/in")
|
|
assert len(files) == 2
|
|
assert "kms-config.json" in files
|
|
assert "raw-relocation-data.tar" in files
|
|
|
|
kms_file = self.relocation_storage.open(f"runs/{self.uuid}/in/kms-config.json")
|
|
with kms_file:
|
|
json.load(kms_file)
|
|
|
|
def test_retry_if_attempts_left(
|
|
self,
|
|
preprocessing_baseline_config_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
):
|
|
RelocationFile.objects.filter(relocation=self.relocation).delete()
|
|
self.mock_message_builder(fake_message_builder)
|
|
|
|
# An exception being raised will trigger a retry in celery.
|
|
with pytest.raises(Exception):
|
|
preprocessing_transfer(self.uuid)
|
|
|
|
assert fake_message_builder.call_count == 0
|
|
assert preprocessing_baseline_config_mock.call_count == 0
|
|
|
|
relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.status == Relocation.Status.IN_PROGRESS.value
|
|
assert relocation.latest_notified != Relocation.EmailKind.FAILED.value
|
|
assert not relocation.failure_reason
|
|
|
|
def test_fail_if_no_attempts_left(
|
|
self,
|
|
preprocessing_baseline_config_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
):
|
|
self.relocation.latest_task = OrderedTask.PREPROCESSING_TRANSFER.name
|
|
self.relocation.latest_task_attempts = MAX_FAST_TASK_RETRIES
|
|
self.relocation.save()
|
|
RelocationFile.objects.filter(relocation=self.relocation).delete()
|
|
self.mock_message_builder(fake_message_builder)
|
|
|
|
with pytest.raises(Exception):
|
|
preprocessing_transfer(self.uuid)
|
|
|
|
assert fake_message_builder.call_count == 1
|
|
assert fake_message_builder.call_args.kwargs["type"] == "relocation.failed"
|
|
fake_message_builder.return_value.send_async.assert_called_once_with(
|
|
to=[self.owner.email, self.superuser.email]
|
|
)
|
|
|
|
assert preprocessing_baseline_config_mock.call_count == 0
|
|
|
|
relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.status == Relocation.Status.FAILURE.value
|
|
assert relocation.latest_notified == Relocation.EmailKind.FAILED.value
|
|
assert relocation.failure_reason == ERR_PREPROCESSING_INTERNAL
|
|
|
|
|
|
@region_silo_test
|
|
@patch(
|
|
"sentry.backup.crypto.KeyManagementServiceClient",
|
|
new_callable=lambda: FakeKeyManagementServiceClient,
|
|
)
|
|
@patch("sentry.utils.relocation.MessageBuilder")
|
|
@patch("sentry.tasks.relocation.preprocessing_colliding_users.apply_async")
|
|
class PreprocessingBaselineConfigTest(RelocationTaskTestCase):
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.relocation.step = Relocation.Step.PREPROCESSING.value
|
|
self.relocation.latest_task = OrderedTask.PREPROCESSING_TRANSFER.name
|
|
self.relocation.save()
|
|
self.relocation_storage = get_relocation_storage()
|
|
|
|
def test_success(
|
|
self,
|
|
preprocessing_colliding_users_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
fake_kms_client: FakeKeyManagementServiceClient,
|
|
):
|
|
self.mock_message_builder(fake_message_builder)
|
|
self.mock_kms_client(fake_kms_client)
|
|
|
|
preprocessing_baseline_config(self.uuid)
|
|
|
|
assert fake_kms_client.asymmetric_decrypt.call_count == 0
|
|
assert fake_kms_client.get_public_key.call_count == 1
|
|
assert fake_message_builder.call_count == 0
|
|
assert preprocessing_colliding_users_mock.call_count == 1
|
|
|
|
(_, files) = self.relocation_storage.listdir(f"runs/{self.uuid}/in")
|
|
assert len(files) == 1
|
|
assert "baseline-config.tar" in files
|
|
|
|
with self.relocation_storage.open(f"runs/{self.uuid}/in/baseline-config.tar") as fp:
|
|
json_models = json.loads(
|
|
decrypt_encrypted_tarball(fp, LocalFileDecryptor.from_bytes(self.priv_key_pem))
|
|
)
|
|
assert len(json_models) > 0
|
|
|
|
# Only user `superuser` is an admin, so only they should be exported.
|
|
for json_model in json_models:
|
|
if NormalizedModelName(json_model["model"]) == get_model_name(User):
|
|
assert json_model["fields"]["username"] in "[email protected]"
|
|
|
|
def test_retry_if_attempts_left(
|
|
self,
|
|
preprocessing_colliding_users_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
fake_kms_client: FakeKeyManagementServiceClient,
|
|
):
|
|
self.mock_message_builder(fake_message_builder)
|
|
self.mock_kms_client(fake_kms_client)
|
|
RelocationFile.objects.filter(relocation=self.relocation).delete()
|
|
|
|
# An exception being raised will trigger a retry in celery.
|
|
with pytest.raises(Exception):
|
|
fake_kms_client.get_public_key.side_effect = Exception("Test")
|
|
|
|
preprocessing_baseline_config(self.uuid)
|
|
|
|
assert fake_kms_client.asymmetric_decrypt.call_count == 0
|
|
assert fake_kms_client.get_public_key.call_count == 1
|
|
assert fake_message_builder.call_count == 0
|
|
assert preprocessing_colliding_users_mock.call_count == 0
|
|
|
|
relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.status == Relocation.Status.IN_PROGRESS.value
|
|
assert relocation.latest_notified != Relocation.EmailKind.FAILED.value
|
|
assert not relocation.failure_reason
|
|
|
|
def test_fail_if_no_attempts_left(
|
|
self,
|
|
preprocessing_colliding_users_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
fake_kms_client: FakeKeyManagementServiceClient,
|
|
):
|
|
self.relocation.latest_task = OrderedTask.PREPROCESSING_BASELINE_CONFIG.name
|
|
self.relocation.latest_task_attempts = MAX_FAST_TASK_RETRIES
|
|
self.relocation.save()
|
|
RelocationFile.objects.filter(relocation=self.relocation).delete()
|
|
|
|
self.mock_message_builder(fake_message_builder)
|
|
self.mock_kms_client(fake_kms_client)
|
|
fake_kms_client.get_public_key.side_effect = Exception("Test")
|
|
|
|
with pytest.raises(Exception):
|
|
preprocessing_baseline_config(self.uuid)
|
|
|
|
assert fake_kms_client.asymmetric_decrypt.call_count == 0
|
|
assert fake_kms_client.get_public_key.call_count == 1
|
|
|
|
assert fake_message_builder.call_count == 1
|
|
assert fake_message_builder.call_args.kwargs["type"] == "relocation.failed"
|
|
fake_message_builder.return_value.send_async.assert_called_once_with(
|
|
to=[self.owner.email, self.superuser.email]
|
|
)
|
|
|
|
assert preprocessing_colliding_users_mock.call_count == 0
|
|
|
|
relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.status == Relocation.Status.FAILURE.value
|
|
assert relocation.latest_notified == Relocation.EmailKind.FAILED.value
|
|
assert relocation.failure_reason == ERR_PREPROCESSING_INTERNAL
|
|
|
|
|
|
@region_silo_test
|
|
@patch(
|
|
"sentry.backup.crypto.KeyManagementServiceClient",
|
|
new_callable=lambda: FakeKeyManagementServiceClient,
|
|
)
|
|
@patch("sentry.utils.relocation.MessageBuilder")
|
|
@patch("sentry.tasks.relocation.preprocessing_complete.apply_async")
|
|
class PreprocessingCollidingUsersTest(RelocationTaskTestCase):
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.relocation.step = Relocation.Step.PREPROCESSING.value
|
|
self.relocation.latest_task = OrderedTask.PREPROCESSING_BASELINE_CONFIG.name
|
|
self.relocation.want_usernames = ["a", "b", "c"]
|
|
self.relocation.save()
|
|
|
|
self.create_user("c")
|
|
self.create_user("d")
|
|
self.create_user("e")
|
|
|
|
self.relocation_storage = get_relocation_storage()
|
|
|
|
def test_success(
|
|
self,
|
|
preprocessing_complete_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
fake_kms_client: FakeKeyManagementServiceClient,
|
|
):
|
|
self.mock_message_builder(fake_message_builder)
|
|
self.mock_kms_client(fake_kms_client)
|
|
|
|
preprocessing_colliding_users(self.uuid)
|
|
|
|
assert fake_kms_client.asymmetric_decrypt.call_count == 0
|
|
assert fake_kms_client.get_public_key.call_count == 1
|
|
assert fake_message_builder.call_count == 0
|
|
assert preprocessing_complete_mock.call_count == 1
|
|
|
|
(_, files) = self.relocation_storage.listdir(f"runs/{self.uuid}/in")
|
|
assert len(files) == 1
|
|
assert "colliding-users.tar" in files
|
|
|
|
with self.relocation_storage.open(f"runs/{self.uuid}/in/colliding-users.tar") as fp:
|
|
json_models = json.loads(
|
|
decrypt_encrypted_tarball(fp, LocalFileDecryptor.from_bytes(self.priv_key_pem))
|
|
)
|
|
assert len(json_models) > 0
|
|
|
|
# Only user `c` was colliding, so only they should be exported.
|
|
for json_model in json_models:
|
|
if NormalizedModelName(json_model["model"]) == get_model_name(User):
|
|
assert json_model["fields"]["username"] == "c"
|
|
|
|
def test_retry_if_attempts_left(
|
|
self,
|
|
preprocessing_complete_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
fake_kms_client: FakeKeyManagementServiceClient,
|
|
):
|
|
RelocationFile.objects.filter(relocation=self.relocation).delete()
|
|
self.mock_message_builder(fake_message_builder)
|
|
self.mock_kms_client(fake_kms_client)
|
|
|
|
# An exception being raised will trigger a retry in celery.
|
|
with pytest.raises(Exception):
|
|
fake_kms_client.get_public_key.side_effect = Exception("Test")
|
|
|
|
preprocessing_colliding_users(self.uuid)
|
|
|
|
assert fake_kms_client.asymmetric_decrypt.call_count == 0
|
|
assert fake_kms_client.get_public_key.call_count == 1
|
|
assert fake_message_builder.call_count == 0
|
|
assert preprocessing_complete_mock.call_count == 0
|
|
|
|
relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.status == Relocation.Status.IN_PROGRESS.value
|
|
assert relocation.latest_notified != Relocation.EmailKind.FAILED.value
|
|
assert not relocation.failure_reason
|
|
|
|
def test_fail_if_no_attempts_left(
|
|
self,
|
|
preprocessing_complete_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
fake_kms_client: FakeKeyManagementServiceClient,
|
|
):
|
|
self.relocation.latest_task = OrderedTask.PREPROCESSING_COLLIDING_USERS.name
|
|
self.relocation.latest_task_attempts = MAX_FAST_TASK_RETRIES
|
|
self.relocation.save()
|
|
RelocationFile.objects.filter(relocation=self.relocation).delete()
|
|
|
|
self.mock_message_builder(fake_message_builder)
|
|
self.mock_kms_client(fake_kms_client)
|
|
fake_kms_client.get_public_key.side_effect = Exception("Test")
|
|
|
|
with pytest.raises(Exception):
|
|
preprocessing_colliding_users(self.uuid)
|
|
|
|
assert fake_kms_client.asymmetric_decrypt.call_count == 0
|
|
assert fake_kms_client.get_public_key.call_count == 1
|
|
|
|
assert fake_message_builder.call_count == 1
|
|
assert fake_message_builder.call_args.kwargs["type"] == "relocation.failed"
|
|
fake_message_builder.return_value.send_async.assert_called_once_with(
|
|
to=[self.owner.email, self.superuser.email]
|
|
)
|
|
|
|
assert preprocessing_complete_mock.call_count == 0
|
|
|
|
relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.status == Relocation.Status.FAILURE.value
|
|
assert relocation.latest_notified == Relocation.EmailKind.FAILED.value
|
|
assert relocation.failure_reason == ERR_PREPROCESSING_INTERNAL
|
|
|
|
|
|
@region_silo_test
|
|
@patch("sentry.utils.relocation.MessageBuilder")
|
|
@patch("sentry.tasks.relocation.validating_start.apply_async")
|
|
class PreprocessingCompleteTest(RelocationTaskTestCase):
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.relocation.step = Relocation.Step.PREPROCESSING.value
|
|
self.relocation.latest_task = OrderedTask.PREPROCESSING_COLLIDING_USERS.name
|
|
self.relocation.want_usernames = ["importing"]
|
|
self.relocation.save()
|
|
self.create_user("importing")
|
|
self.relocation_storage = get_relocation_storage()
|
|
|
|
self.relocation_storage.save(f"runs/{self.uuid}/conf/cloudbuild.yaml", BytesIO())
|
|
self.relocation_storage.save(f"runs/{self.uuid}/conf/cloudbuild.zip", BytesIO())
|
|
self.relocation_storage.save(f"runs/{self.uuid}/in/kms-config.json", BytesIO())
|
|
self.relocation_storage.save(f"runs/{self.uuid}/in/raw-relocation-data.tar", BytesIO())
|
|
self.relocation_storage.save(f"runs/{self.uuid}/in/baseline-config.tar", BytesIO())
|
|
self.relocation_storage.save(f"runs/{self.uuid}/in/colliding-users.tar", BytesIO())
|
|
|
|
def test_success(
|
|
self,
|
|
validating_start_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
):
|
|
self.mock_message_builder(fake_message_builder)
|
|
|
|
preprocessing_complete(self.uuid)
|
|
|
|
assert fake_message_builder.call_count == 0
|
|
assert validating_start_mock.call_count == 1
|
|
|
|
self.relocation.refresh_from_db()
|
|
assert self.relocation.step == Relocation.Step.VALIDATING.value
|
|
assert RelocationValidation.objects.filter(relocation=self.relocation).count() == 1
|
|
|
|
def test_retry_if_attempts_left(
|
|
self,
|
|
validating_start_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
):
|
|
self.relocation_storage.delete(f"runs/{self.uuid}/conf/cloudbuild.yaml")
|
|
self.mock_message_builder(fake_message_builder)
|
|
|
|
# An exception being raised will trigger a retry in celery.
|
|
with pytest.raises(Exception):
|
|
preprocessing_complete(self.uuid)
|
|
|
|
assert fake_message_builder.call_count == 0
|
|
assert validating_start_mock.call_count == 0
|
|
|
|
relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.status == Relocation.Status.IN_PROGRESS.value
|
|
assert relocation.latest_notified != Relocation.EmailKind.FAILED.value
|
|
assert not relocation.failure_reason
|
|
|
|
def test_fail_if_no_attempts_left(
|
|
self,
|
|
validating_start_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
):
|
|
self.relocation.latest_task = OrderedTask.PREPROCESSING_COMPLETE.name
|
|
self.relocation.latest_task_attempts = MAX_FAST_TASK_RETRIES
|
|
self.relocation.save()
|
|
self.relocation_storage.delete(f"runs/{self.uuid}/in/raw-relocation-data.tar")
|
|
self.mock_message_builder(fake_message_builder)
|
|
|
|
with pytest.raises(Exception):
|
|
preprocessing_complete(self.uuid)
|
|
|
|
assert fake_message_builder.call_count == 1
|
|
assert fake_message_builder.call_args.kwargs["type"] == "relocation.failed"
|
|
fake_message_builder.return_value.send_async.assert_called_once_with(
|
|
to=[self.owner.email, self.superuser.email]
|
|
)
|
|
|
|
assert validating_start_mock.call_count == 0
|
|
|
|
relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.status == Relocation.Status.FAILURE.value
|
|
assert relocation.latest_notified == Relocation.EmailKind.FAILED.value
|
|
assert relocation.failure_reason == ERR_PREPROCESSING_INTERNAL
|
|
|
|
def test_fail_missing_cloudbuild_zip(
|
|
self,
|
|
validating_start_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
):
|
|
self.relocation_storage.delete(f"runs/{self.uuid}/conf/cloudbuild.zip")
|
|
self.mock_message_builder(fake_message_builder)
|
|
|
|
# An exception being raised will trigger a retry in celery.
|
|
with pytest.raises(Exception):
|
|
preprocessing_complete(self.uuid)
|
|
|
|
assert fake_message_builder.call_count == 0
|
|
assert validating_start_mock.call_count == 0
|
|
|
|
relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.status == Relocation.Status.IN_PROGRESS.value
|
|
assert relocation.latest_notified != Relocation.EmailKind.FAILED.value
|
|
assert not relocation.failure_reason
|
|
|
|
def test_fail_missing_kms_config(
|
|
self,
|
|
validating_start_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
):
|
|
self.relocation_storage.delete(f"runs/{self.uuid}/in/kms-config.json")
|
|
self.mock_message_builder(fake_message_builder)
|
|
|
|
# An exception being raised will trigger a retry in celery.
|
|
with pytest.raises(Exception):
|
|
preprocessing_complete(self.uuid)
|
|
|
|
assert fake_message_builder.call_count == 0
|
|
assert validating_start_mock.call_count == 0
|
|
|
|
relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.status == Relocation.Status.IN_PROGRESS.value
|
|
assert relocation.latest_notified != Relocation.EmailKind.FAILED.value
|
|
assert not relocation.failure_reason
|
|
|
|
|
|
@region_silo_test
|
|
@patch(
|
|
"sentry.tasks.relocation.CloudBuildClient",
|
|
new_callable=lambda: FakeCloudBuildClient,
|
|
)
|
|
@patch("sentry.utils.relocation.MessageBuilder")
|
|
@patch("sentry.tasks.relocation.validating_poll.apply_async")
|
|
class ValidatingStartTest(RelocationTaskTestCase):
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.relocation.step = Relocation.Step.PREPROCESSING.value
|
|
self.relocation.latest_task = OrderedTask.PREPROCESSING_COMPLETE.name
|
|
self.relocation.want_usernames = ["testuser"]
|
|
self.relocation.want_org_slugs = ["test-slug"]
|
|
self.relocation.save()
|
|
|
|
self.relocation_validation: RelocationValidation = RelocationValidation.objects.create(
|
|
relocation=self.relocation
|
|
)
|
|
|
|
def test_success(
|
|
self,
|
|
validating_poll_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
fake_cloudbuild_client: FakeCloudBuildClient,
|
|
):
|
|
self.mock_cloudbuild_client(fake_cloudbuild_client, Build.Status(Build.Status.QUEUED))
|
|
self.mock_message_builder(fake_message_builder)
|
|
|
|
validating_start(self.uuid)
|
|
|
|
assert validating_poll_mock.call_count == 1
|
|
assert fake_cloudbuild_client.create_build.call_count == 1
|
|
|
|
self.relocation.refresh_from_db()
|
|
self.relocation_validation.refresh_from_db()
|
|
assert self.relocation_validation.status == ValidationStatus.IN_PROGRESS.value
|
|
assert self.relocation_validation.attempts == 1
|
|
|
|
relocation_validation_attempt = RelocationValidationAttempt.objects.get(
|
|
relocation_validation=self.relocation_validation
|
|
)
|
|
assert relocation_validation_attempt.status == ValidationStatus.IN_PROGRESS.value
|
|
|
|
def test_pause(
|
|
self,
|
|
validating_poll_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
fake_cloudbuild_client: FakeCloudBuildClient,
|
|
):
|
|
self.mock_cloudbuild_client(fake_cloudbuild_client, Build.Status(Build.Status.QUEUED))
|
|
self.mock_message_builder(fake_message_builder)
|
|
self.relocation.scheduled_pause_at_step = Relocation.Step.VALIDATING.value
|
|
self.relocation.save()
|
|
|
|
validating_start(self.uuid)
|
|
|
|
assert fake_cloudbuild_client.create_build.call_count == 0
|
|
assert fake_cloudbuild_client.get_build.call_count == 0
|
|
assert fake_message_builder.call_count == 0
|
|
assert validating_poll_mock.call_count == 0
|
|
|
|
relocation: Relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.status == Relocation.Status.PAUSE.value
|
|
assert relocation.step == Relocation.Step.VALIDATING.value
|
|
assert relocation.scheduled_pause_at_step is None
|
|
assert relocation.latest_task == OrderedTask.VALIDATING_START.name
|
|
|
|
def test_retry_if_attempts_left(
|
|
self,
|
|
validating_poll_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
fake_cloudbuild_client: FakeCloudBuildClient,
|
|
):
|
|
self.mock_cloudbuild_client(fake_cloudbuild_client, Build.Status(Build.Status.QUEUED))
|
|
self.mock_message_builder(fake_message_builder)
|
|
|
|
# An exception being raised will trigger a retry in celery.
|
|
with pytest.raises(Exception):
|
|
fake_cloudbuild_client.create_build.side_effect = Exception("Test")
|
|
|
|
validating_start(self.uuid)
|
|
|
|
assert fake_cloudbuild_client.create_build.call_count == 1
|
|
assert fake_message_builder.call_count == 0
|
|
assert validating_poll_mock.call_count == 0
|
|
|
|
relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.status == Relocation.Status.IN_PROGRESS.value
|
|
assert relocation.latest_notified != Relocation.EmailKind.FAILED.value
|
|
assert not relocation.failure_reason
|
|
|
|
def test_fail_if_no_attempts_left(
|
|
self,
|
|
validating_poll_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
fake_cloudbuild_client: FakeCloudBuildClient,
|
|
):
|
|
self.relocation.latest_task = OrderedTask.VALIDATING_START.name
|
|
self.relocation.latest_task_attempts = MAX_FAST_TASK_RETRIES
|
|
self.relocation.save()
|
|
|
|
self.mock_cloudbuild_client(fake_cloudbuild_client, Build.Status(Build.Status.QUEUED))
|
|
fake_cloudbuild_client.create_build.side_effect = Exception("Test")
|
|
self.mock_message_builder(fake_message_builder)
|
|
|
|
with pytest.raises(Exception):
|
|
validating_start(self.uuid)
|
|
|
|
assert fake_cloudbuild_client.create_build.call_count == 1
|
|
assert fake_message_builder.call_count == 1
|
|
assert validating_poll_mock.call_count == 0
|
|
|
|
relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.status == Relocation.Status.FAILURE.value
|
|
assert relocation.latest_notified == Relocation.EmailKind.FAILED.value
|
|
assert relocation.failure_reason == ERR_VALIDATING_INTERNAL
|
|
|
|
def test_fail_if_max_runs_attempted(
|
|
self,
|
|
validating_poll_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
fake_cloudbuild_client: FakeCloudBuildClient,
|
|
):
|
|
for _ in range(3):
|
|
RelocationValidationAttempt.objects.create(
|
|
relocation=self.relocation,
|
|
relocation_validation=self.relocation_validation,
|
|
build_id=uuid4().hex,
|
|
)
|
|
|
|
self.relocation_validation.attempts = 3
|
|
self.relocation_validation.save()
|
|
|
|
self.relocation.latest_task_attempts = MAX_FAST_TASK_RETRIES
|
|
self.relocation.save()
|
|
|
|
self.mock_cloudbuild_client(fake_cloudbuild_client, Build.Status(Build.Status.QUEUED))
|
|
self.mock_message_builder(fake_message_builder)
|
|
|
|
validating_start(self.uuid)
|
|
|
|
assert fake_cloudbuild_client.create_build.call_count == 0
|
|
assert fake_message_builder.call_count == 1
|
|
assert validating_poll_mock.call_count == 0
|
|
|
|
relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.status == Relocation.Status.FAILURE.value
|
|
assert relocation.latest_notified == Relocation.EmailKind.FAILED.value
|
|
assert relocation.failure_reason == ERR_VALIDATING_MAX_RUNS
|
|
|
|
|
|
@region_silo_test
|
|
@patch(
|
|
"sentry.tasks.relocation.CloudBuildClient",
|
|
new_callable=lambda: FakeCloudBuildClient,
|
|
)
|
|
@patch("sentry.utils.relocation.MessageBuilder")
|
|
class ValidatingPollTest(RelocationTaskTestCase):
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.relocation.step = Relocation.Step.VALIDATING.value
|
|
self.relocation.latest_task = OrderedTask.VALIDATING_START.name
|
|
self.relocation.want_usernames = ["testuser"]
|
|
self.relocation.want_org_slugs = ["test-slug"]
|
|
self.relocation.save()
|
|
|
|
self.relocation_validation: RelocationValidation = RelocationValidation.objects.create(
|
|
relocation=self.relocation, attempts=1
|
|
)
|
|
|
|
self.relocation_validation_attempt: RelocationValidationAttempt = (
|
|
RelocationValidationAttempt.objects.create(
|
|
relocation=self.relocation,
|
|
relocation_validation=self.relocation_validation,
|
|
build_id=uuid4().hex,
|
|
)
|
|
)
|
|
|
|
@patch("sentry.tasks.relocation.validating_complete.apply_async")
|
|
def test_success(
|
|
self,
|
|
validating_complete_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
fake_cloudbuild_client: FakeCloudBuildClient,
|
|
):
|
|
self.mock_cloudbuild_client(fake_cloudbuild_client, Build.Status(Build.Status.SUCCESS))
|
|
self.mock_message_builder(fake_message_builder)
|
|
|
|
validating_poll(self.uuid, self.relocation_validation_attempt.build_id)
|
|
|
|
assert fake_cloudbuild_client.get_build.call_count == 1
|
|
assert fake_message_builder.call_count == 0
|
|
assert validating_complete_mock.call_count == 1
|
|
|
|
self.relocation.refresh_from_db()
|
|
self.relocation_validation.refresh_from_db()
|
|
self.relocation_validation_attempt.refresh_from_db()
|
|
assert self.relocation_validation.status == ValidationStatus.IN_PROGRESS.value
|
|
assert self.relocation.latest_task == "VALIDATING_POLL"
|
|
|
|
@patch("sentry.tasks.relocation.validating_start.apply_async")
|
|
def test_timeout_starts_new_validation_attempt(
|
|
self,
|
|
validating_start_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
fake_cloudbuild_client: FakeCloudBuildClient,
|
|
):
|
|
for stat in {Build.Status.TIMEOUT, Build.Status.EXPIRED}:
|
|
self.mock_message_builder(fake_message_builder)
|
|
self.mock_cloudbuild_client(fake_cloudbuild_client, Build.Status(stat))
|
|
validating_start_mock.call_count = 0
|
|
|
|
validating_poll(self.uuid, self.relocation_validation_attempt.build_id)
|
|
|
|
assert fake_cloudbuild_client.get_build.call_count == 1
|
|
assert fake_message_builder.call_count == 0
|
|
assert validating_start_mock.call_count == 1
|
|
|
|
self.relocation.refresh_from_db()
|
|
self.relocation_validation.refresh_from_db()
|
|
self.relocation_validation_attempt.refresh_from_db()
|
|
|
|
assert self.relocation.latest_task == "VALIDATING_START"
|
|
assert self.relocation_validation.status == ValidationStatus.IN_PROGRESS.value
|
|
assert self.relocation_validation_attempt.status == ValidationStatus.TIMEOUT.value
|
|
|
|
@patch("sentry.tasks.relocation.validating_start.apply_async")
|
|
def test_failure_starts_new_validation_attempt(
|
|
self,
|
|
validating_start_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
fake_cloudbuild_client: FakeCloudBuildClient,
|
|
):
|
|
for stat in {
|
|
Build.Status.FAILURE,
|
|
Build.Status.INTERNAL_ERROR,
|
|
Build.Status.CANCELLED,
|
|
}:
|
|
self.mock_cloudbuild_client(fake_cloudbuild_client, Build.Status(stat))
|
|
self.mock_message_builder(fake_message_builder)
|
|
validating_start_mock.call_count = 0
|
|
|
|
validating_poll(self.uuid, self.relocation_validation_attempt.build_id)
|
|
|
|
assert fake_cloudbuild_client.get_build.call_count == 1
|
|
assert fake_message_builder.call_count == 0
|
|
assert validating_start_mock.call_count == 1
|
|
|
|
self.relocation.refresh_from_db()
|
|
self.relocation_validation.refresh_from_db()
|
|
self.relocation_validation_attempt.refresh_from_db()
|
|
assert self.relocation.latest_task == "VALIDATING_START"
|
|
assert self.relocation_validation.status == ValidationStatus.IN_PROGRESS.value
|
|
assert self.relocation_validation_attempt.status == ValidationStatus.FAILURE.value
|
|
|
|
@patch("sentry.tasks.relocation.validating_poll.apply_async")
|
|
def test_in_progress_retries_poll(
|
|
self,
|
|
validating_poll_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
fake_cloudbuild_client: FakeCloudBuildClient,
|
|
):
|
|
for stat in {
|
|
Build.Status.QUEUED,
|
|
Build.Status.PENDING,
|
|
Build.Status.WORKING,
|
|
}:
|
|
self.mock_cloudbuild_client(fake_cloudbuild_client, Build.Status(stat))
|
|
self.mock_message_builder(fake_message_builder)
|
|
validating_poll_mock.call_count = 0
|
|
|
|
validating_poll(self.uuid, self.relocation_validation_attempt.build_id)
|
|
|
|
assert fake_cloudbuild_client.get_build.call_count == 1
|
|
assert fake_message_builder.call_count == 0
|
|
assert validating_poll_mock.call_count == 1
|
|
|
|
self.relocation.refresh_from_db()
|
|
self.relocation_validation.refresh_from_db()
|
|
self.relocation_validation_attempt.refresh_from_db()
|
|
assert self.relocation.latest_task == "VALIDATING_POLL"
|
|
assert self.relocation_validation.status == ValidationStatus.IN_PROGRESS.value
|
|
assert self.relocation_validation_attempt.status == ValidationStatus.IN_PROGRESS.value
|
|
assert (
|
|
RelocationValidationAttempt.objects.filter(
|
|
relocation_validation=self.relocation_validation
|
|
).count()
|
|
== 1
|
|
)
|
|
|
|
@patch("sentry.tasks.relocation.validating_poll.apply_async")
|
|
def test_retry_if_attempts_left(
|
|
self,
|
|
validating_poll_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
fake_cloudbuild_client: FakeCloudBuildClient,
|
|
):
|
|
self.mock_cloudbuild_client(fake_cloudbuild_client, Build.Status(Build.Status.QUEUED))
|
|
self.mock_message_builder(fake_message_builder)
|
|
fake_cloudbuild_client.get_build.side_effect = Exception("Test")
|
|
|
|
# An exception being raised will trigger a retry in celery.
|
|
with pytest.raises(Exception):
|
|
validating_poll(self.uuid, self.relocation_validation_attempt.build_id)
|
|
|
|
assert fake_cloudbuild_client.get_build.call_count == 1
|
|
assert fake_message_builder.call_count == 0
|
|
assert validating_poll_mock.call_count == 0
|
|
|
|
relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.status == Relocation.Status.IN_PROGRESS.value
|
|
assert relocation.latest_notified != Relocation.EmailKind.FAILED.value
|
|
assert not relocation.failure_reason
|
|
|
|
@patch("sentry.tasks.relocation.validating_poll.apply_async")
|
|
def test_fail_if_no_attempts_left(
|
|
self,
|
|
validating_poll_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
fake_cloudbuild_client: FakeCloudBuildClient,
|
|
):
|
|
self.relocation.latest_task = OrderedTask.VALIDATING_POLL.name
|
|
self.relocation.latest_task_attempts = MAX_VALIDATION_POLLS
|
|
self.relocation.save()
|
|
|
|
self.mock_cloudbuild_client(fake_cloudbuild_client, Build.Status(Build.Status.QUEUED))
|
|
fake_cloudbuild_client.get_build.side_effect = Exception("Test")
|
|
self.mock_message_builder(fake_message_builder)
|
|
|
|
with pytest.raises(Exception):
|
|
validating_poll(self.uuid, self.relocation_validation_attempt.build_id)
|
|
|
|
assert fake_cloudbuild_client.get_build.call_count == 1
|
|
|
|
assert fake_message_builder.call_count == 1
|
|
assert fake_message_builder.call_args.kwargs["type"] == "relocation.failed"
|
|
fake_message_builder.return_value.send_async.assert_called_once_with(
|
|
to=[self.owner.email, self.superuser.email]
|
|
)
|
|
|
|
assert validating_poll_mock.call_count == 0
|
|
|
|
relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.status == Relocation.Status.FAILURE.value
|
|
assert relocation.latest_notified == Relocation.EmailKind.FAILED.value
|
|
assert relocation.failure_reason == ERR_VALIDATING_INTERNAL
|
|
|
|
|
|
def mock_invalid_finding(storage: Storage, uuid: str):
|
|
storage.save(
|
|
f"runs/{uuid}/findings/import-baseline-config.json",
|
|
BytesIO(
|
|
b"""
|
|
[
|
|
{
|
|
"finding": "RpcImportError",
|
|
"kind": "Unknown",
|
|
"left_pk": 2,
|
|
"on": {
|
|
"model": "sentry.email",
|
|
"ordinal": 1
|
|
},
|
|
"reason": "test reason",
|
|
"right_pk": 3
|
|
}
|
|
]
|
|
"""
|
|
),
|
|
)
|
|
|
|
|
|
@region_silo_test
|
|
@patch("sentry.utils.relocation.MessageBuilder")
|
|
@patch("sentry.tasks.relocation.importing.apply_async")
|
|
class ValidatingCompleteTest(RelocationTaskTestCase):
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.relocation.step = Relocation.Step.VALIDATING.value
|
|
self.relocation.latest_task = OrderedTask.VALIDATING_POLL.name
|
|
self.relocation.want_usernames = ["testuser"]
|
|
self.relocation.want_org_slugs = ["test-slug"]
|
|
self.relocation.save()
|
|
|
|
self.relocation_validation: RelocationValidation = RelocationValidation.objects.create(
|
|
relocation=self.relocation, attempts=1
|
|
)
|
|
|
|
self.relocation_validation_attempt: RelocationValidationAttempt = (
|
|
RelocationValidationAttempt.objects.create(
|
|
relocation=self.relocation,
|
|
relocation_validation=self.relocation_validation,
|
|
build_id=uuid4().hex,
|
|
)
|
|
)
|
|
|
|
self.storage = get_storage()
|
|
self.storage.save(
|
|
f"runs/{self.uuid}/findings/artifacts-prefixes-are-ignored.json",
|
|
BytesIO(b"invalid-json"),
|
|
)
|
|
files = [
|
|
"null.json",
|
|
"import-baseline-config.json",
|
|
"import-colliding-users.json",
|
|
"import-raw-relocation-data.json",
|
|
"export-baseline-config.json",
|
|
"export-colliding-users.json",
|
|
"export-raw-relocation-data.json",
|
|
"compare-baseline-config.json",
|
|
"compare-colliding-users.json",
|
|
]
|
|
for file in files:
|
|
self.storage.save(f"runs/{self.uuid}/findings/{file}", BytesIO(b"[]"))
|
|
|
|
def test_valid(
|
|
self,
|
|
importing_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
):
|
|
self.mock_message_builder(fake_message_builder)
|
|
|
|
validating_complete(self.uuid, self.relocation_validation_attempt.build_id)
|
|
|
|
assert fake_message_builder.call_count == 0
|
|
assert importing_mock.call_count == 1
|
|
|
|
self.relocation.refresh_from_db()
|
|
self.relocation_validation.refresh_from_db()
|
|
self.relocation_validation_attempt.refresh_from_db()
|
|
assert self.relocation.latest_task == "VALIDATING_COMPLETE"
|
|
assert self.relocation.step == Relocation.Step.IMPORTING.value
|
|
assert self.relocation_validation.status == ValidationStatus.VALID.value
|
|
assert self.relocation_validation_attempt.status == ValidationStatus.VALID.value
|
|
|
|
def test_invalid(
|
|
self,
|
|
importing_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
):
|
|
self.mock_message_builder(fake_message_builder)
|
|
mock_invalid_finding(self.storage, self.uuid)
|
|
|
|
validating_complete(self.uuid, self.relocation_validation_attempt.build_id)
|
|
|
|
assert fake_message_builder.call_count == 1
|
|
assert fake_message_builder.call_args.kwargs["type"] == "relocation.failed"
|
|
fake_message_builder.return_value.send_async.assert_called_once_with(
|
|
to=[self.owner.email, self.superuser.email]
|
|
)
|
|
|
|
assert importing_mock.call_count == 0
|
|
|
|
self.relocation.refresh_from_db()
|
|
self.relocation_validation.refresh_from_db()
|
|
self.relocation_validation_attempt.refresh_from_db()
|
|
assert self.relocation.latest_task == "VALIDATING_COMPLETE"
|
|
assert self.relocation.step == Relocation.Step.VALIDATING.value
|
|
assert self.relocation.failure_reason is not None
|
|
assert self.relocation.latest_notified == Relocation.EmailKind.FAILED.value
|
|
assert self.relocation_validation.status == ValidationStatus.INVALID.value
|
|
assert self.relocation_validation_attempt.status == ValidationStatus.INVALID.value
|
|
|
|
def test_retry_if_attempts_left(
|
|
self,
|
|
importing_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
):
|
|
self.mock_message_builder(fake_message_builder)
|
|
self.storage.save(
|
|
f"runs/{self.uuid}/findings/null.json",
|
|
BytesIO(b"invalid-json"),
|
|
)
|
|
|
|
assert fake_message_builder.call_count == 0
|
|
assert importing_mock.call_count == 0
|
|
|
|
# An exception being raised will trigger a retry in celery.
|
|
with pytest.raises(Exception):
|
|
validating_complete(self.uuid, self.relocation_validation_attempt.build_id)
|
|
|
|
relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.status == Relocation.Status.IN_PROGRESS.value
|
|
assert relocation.latest_notified != Relocation.EmailKind.FAILED.value
|
|
assert not relocation.failure_reason
|
|
|
|
def test_fail_if_no_attempts_left(
|
|
self,
|
|
importing_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
):
|
|
self.mock_message_builder(fake_message_builder)
|
|
self.relocation.latest_task = OrderedTask.VALIDATING_COMPLETE.name
|
|
self.relocation.latest_task_attempts = MAX_FAST_TASK_RETRIES
|
|
self.relocation.save()
|
|
self.storage.save(f"runs/{self.uuid}/findings/null.json", BytesIO(b"invalid-json"))
|
|
|
|
with pytest.raises(Exception):
|
|
validating_complete(self.uuid, self.relocation_validation_attempt.build_id)
|
|
|
|
assert fake_message_builder.call_count == 1
|
|
assert fake_message_builder.call_args.kwargs["type"] == "relocation.failed"
|
|
fake_message_builder.return_value.send_async.assert_called_once_with(
|
|
to=[self.owner.email, self.superuser.email]
|
|
)
|
|
|
|
assert importing_mock.call_count == 0
|
|
|
|
relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.status == Relocation.Status.FAILURE.value
|
|
assert relocation.latest_notified == Relocation.EmailKind.FAILED.value
|
|
assert relocation.failure_reason == ERR_VALIDATING_INTERNAL
|
|
|
|
|
|
@region_silo_test
|
|
@patch(
|
|
"sentry.backup.crypto.KeyManagementServiceClient",
|
|
new_callable=lambda: FakeKeyManagementServiceClient,
|
|
)
|
|
@patch("sentry.tasks.relocation.postprocessing.apply_async")
|
|
class ImportingTest(RelocationTaskTestCase, TransactionTestCase):
|
|
def setUp(self):
|
|
RelocationTaskTestCase.setUp(self)
|
|
TransactionTestCase.setUp(self)
|
|
self.relocation.step = Relocation.Step.VALIDATING.value
|
|
self.relocation.latest_task = OrderedTask.VALIDATING_COMPLETE.name
|
|
self.relocation.save()
|
|
|
|
def test_success(
|
|
self, postprocessing_mock: Mock, fake_kms_client: FakeKeyManagementServiceClient
|
|
):
|
|
self.mock_kms_client(fake_kms_client)
|
|
org_count = Organization.objects.filter(slug__startswith="testing").count()
|
|
|
|
importing(self.uuid)
|
|
|
|
assert postprocessing_mock.call_count == 1
|
|
assert Organization.objects.filter(slug__startswith="testing").count() == org_count + 1
|
|
|
|
assert RegionImportChunk.objects.filter(import_uuid=self.uuid).count() == 9
|
|
assert sorted(RegionImportChunk.objects.values_list("model", flat=True)) == [
|
|
"sentry.organization",
|
|
"sentry.organizationmember",
|
|
"sentry.organizationmemberteam",
|
|
"sentry.project",
|
|
"sentry.projectkey",
|
|
"sentry.projectoption",
|
|
"sentry.projectteam",
|
|
"sentry.rule",
|
|
"sentry.team",
|
|
]
|
|
|
|
with assume_test_silo_mode(SiloMode.CONTROL):
|
|
assert ControlImportChunk.objects.filter(import_uuid=self.uuid).count() == 2
|
|
assert sorted(ControlImportChunk.objects.values_list("model", flat=True)) == [
|
|
"sentry.user",
|
|
"sentry.useremail",
|
|
]
|
|
|
|
def test_pause(
|
|
self,
|
|
postprocessing_mock: Mock,
|
|
fake_kms_client: FakeKeyManagementServiceClient,
|
|
):
|
|
self.mock_kms_client(fake_kms_client)
|
|
self.relocation.scheduled_pause_at_step = Relocation.Step.IMPORTING.value
|
|
self.relocation.save()
|
|
|
|
importing(self.uuid)
|
|
|
|
assert fake_kms_client.asymmetric_decrypt.call_count == 0
|
|
assert fake_kms_client.get_public_key.call_count == 0
|
|
assert postprocessing_mock.call_count == 0
|
|
|
|
relocation: Relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.status == Relocation.Status.PAUSE.value
|
|
assert relocation.step == Relocation.Step.IMPORTING.value
|
|
assert relocation.scheduled_pause_at_step is None
|
|
assert relocation.latest_task == OrderedTask.IMPORTING.name
|
|
|
|
|
|
@region_silo_test
|
|
@patch("sentry.utils.relocation.MessageBuilder")
|
|
@patch("sentry.signals.relocated.send_robust")
|
|
@patch("sentry.signals.relocation_redeem_promo_code.send_robust")
|
|
@patch("sentry.tasks.relocation.notifying_users.apply_async")
|
|
@patch("sentry.analytics.record")
|
|
class PostprocessingTest(RelocationTaskTestCase):
|
|
def setUp(self):
|
|
RelocationTaskTestCase.setUp(self)
|
|
TransactionTestCase.setUp(self)
|
|
self.relocation.step = Relocation.Step.IMPORTING.value
|
|
self.relocation.latest_task = OrderedTask.IMPORTING.name
|
|
self.relocation.save()
|
|
|
|
with open(IMPORT_JSON_FILE_PATH, "rb") as fp:
|
|
import_in_organization_scope(
|
|
fp,
|
|
flags=ImportFlags(
|
|
merge_users=False, overwrite_configs=False, import_uuid=str(self.uuid)
|
|
),
|
|
org_filter=set(self.relocation.want_org_slugs),
|
|
printer=Printer(),
|
|
)
|
|
|
|
imported_orgs = RegionImportChunk.objects.get(
|
|
import_uuid=self.uuid, model="sentry.organization"
|
|
)
|
|
assert len(imported_orgs.inserted_map) == 1
|
|
assert len(imported_orgs.inserted_identifiers) == 1
|
|
|
|
self.imported_org_id: int = next(iter(imported_orgs.inserted_map.values()))
|
|
self.imported_org_slug: str = next(iter(imported_orgs.inserted_identifiers.values()))
|
|
|
|
@staticmethod
|
|
def noop_relocated_signal_receiver(sender, **kwargs) -> None:
|
|
pass
|
|
|
|
def test_success(
|
|
self,
|
|
analytics_record_mock: Mock,
|
|
notifying_users_mock: Mock,
|
|
relocation_redeem_promo_code_signal_mock: Mock,
|
|
relocated_signal_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
):
|
|
self.mock_message_builder(fake_message_builder)
|
|
assert (
|
|
OrganizationMember.objects.filter(
|
|
organization_id=self.imported_org_id, role="owner", has_global_access=True
|
|
).count()
|
|
== 1
|
|
)
|
|
assert not OrganizationMember.objects.filter(
|
|
organization_id=self.imported_org_id, user_id=self.owner.id
|
|
).exists()
|
|
|
|
postprocessing(self.uuid)
|
|
|
|
assert relocated_signal_mock.call_count == 1
|
|
assert relocation_redeem_promo_code_signal_mock.call_count == 1
|
|
assert notifying_users_mock.call_count == 1
|
|
|
|
assert (
|
|
OrganizationMember.objects.filter(
|
|
organization_id=self.imported_org_id, role="owner", has_global_access=True
|
|
).count()
|
|
== 2
|
|
)
|
|
assert OrganizationMember.objects.filter(
|
|
organization_id=self.imported_org_id, user_id=self.owner.id
|
|
).exists()
|
|
|
|
relocation = Relocation.objects.get(uuid=self.uuid)
|
|
|
|
analytics_record_mock.assert_called_with(
|
|
"relocation.organization_imported",
|
|
organization_id=self.imported_org_id,
|
|
relocation_uuid=str(relocation.uuid),
|
|
slug=self.imported_org_slug,
|
|
owner_id=self.owner.id,
|
|
)
|
|
|
|
imported_org = Organization.objects.get(slug=self.imported_org_slug)
|
|
|
|
relocation_redeem_promo_code_signal_mock.assert_called_with(
|
|
sender=postprocessing,
|
|
relocation_uuid=str(relocation.uuid),
|
|
user_id=self.owner.id,
|
|
orgs=[imported_org],
|
|
)
|
|
|
|
def test_pause(
|
|
self,
|
|
analytics_record_mock: Mock,
|
|
notifying_users_mock: Mock,
|
|
relocation_redeem_promo_code_signal_mock: Mock,
|
|
relocated_signal_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
):
|
|
self.relocation.scheduled_pause_at_step = Relocation.Step.POSTPROCESSING.value
|
|
self.relocation.save()
|
|
|
|
postprocessing(self.uuid)
|
|
|
|
assert fake_message_builder.call_count == 0
|
|
assert relocated_signal_mock.call_count == 0
|
|
assert notifying_users_mock.call_count == 0
|
|
|
|
relocation: Relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.status == Relocation.Status.PAUSE.value
|
|
assert relocation.step == Relocation.Step.POSTPROCESSING.value
|
|
assert relocation.scheduled_pause_at_step is None
|
|
assert relocation.latest_task == OrderedTask.POSTPROCESSING.name
|
|
|
|
analytics_record_mock.assert_not_called()
|
|
relocation_redeem_promo_code_signal_mock.assert_not_called()
|
|
|
|
def test_retry_if_attempts_left(
|
|
self,
|
|
analytics_record_mock: Mock,
|
|
notifying_users_mock: Mock,
|
|
relocation_redeem_promo_code_signal_mock: Mock,
|
|
relocated_signal_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
):
|
|
self.mock_message_builder(fake_message_builder)
|
|
relocated_signal_mock.side_effect = [
|
|
(self.noop_relocated_signal_receiver, None),
|
|
(self.noop_relocated_signal_receiver, Exception("receiver failure")),
|
|
]
|
|
self.relocation.save()
|
|
|
|
# An exception being raised will trigger a retry in celery.
|
|
with pytest.raises(Exception):
|
|
postprocessing(self.uuid)
|
|
|
|
assert fake_message_builder.call_count == 0
|
|
assert relocated_signal_mock.call_count == 1
|
|
assert notifying_users_mock.call_count == 0
|
|
|
|
relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.status == Relocation.Status.IN_PROGRESS.value
|
|
assert relocation.latest_notified != Relocation.EmailKind.FAILED.value
|
|
assert not relocation.failure_reason
|
|
|
|
# Technically this should be called, but since we're mocking out the `send_robust` function, it won't
|
|
analytics_record_mock.assert_not_called()
|
|
relocation_redeem_promo_code_signal_mock.assert_not_called()
|
|
|
|
def test_fail_if_no_attempts_left(
|
|
self,
|
|
analytics_record_mock: Mock,
|
|
notifying_users_mock: Mock,
|
|
relocation_redeem_promo_code_signal_mock: Mock,
|
|
relocated_signal_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
):
|
|
self.mock_message_builder(fake_message_builder)
|
|
self.relocation.latest_task = OrderedTask.POSTPROCESSING.name
|
|
self.relocation.latest_task_attempts = MAX_FAST_TASK_RETRIES
|
|
relocated_signal_mock.side_effect = [
|
|
(self.noop_relocated_signal_receiver, None),
|
|
(self.noop_relocated_signal_receiver, Exception("receiver failure")),
|
|
]
|
|
self.relocation.save()
|
|
|
|
with pytest.raises(Exception):
|
|
postprocessing(self.uuid)
|
|
|
|
assert fake_message_builder.call_count == 1
|
|
assert fake_message_builder.call_args.kwargs["type"] == "relocation.failed"
|
|
fake_message_builder.return_value.send_async.assert_called_once_with(
|
|
to=[self.owner.email, self.superuser.email]
|
|
)
|
|
|
|
assert relocated_signal_mock.call_count == 1
|
|
assert notifying_users_mock.call_count == 0
|
|
|
|
relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.status == Relocation.Status.FAILURE.value
|
|
assert relocation.latest_notified == Relocation.EmailKind.FAILED.value
|
|
assert relocation.failure_reason == ERR_POSTPROCESSING_INTERNAL
|
|
analytics_record_mock.assert_not_called()
|
|
relocation_redeem_promo_code_signal_mock.assert_not_called()
|
|
|
|
|
|
@region_silo_test
|
|
@patch("sentry.utils.relocation.MessageBuilder")
|
|
@patch("sentry.tasks.relocation.notifying_owner.apply_async")
|
|
class NotifyingUsersTest(RelocationTaskTestCase):
|
|
def setUp(self):
|
|
RelocationTaskTestCase.setUp(self)
|
|
TransactionTestCase.setUp(self)
|
|
self.relocation.step = Relocation.Step.POSTPROCESSING.value
|
|
self.relocation.latest_task = OrderedTask.POSTPROCESSING.name
|
|
self.relocation.want_usernames = ["[email protected]", "[email protected]"]
|
|
self.relocation.save()
|
|
|
|
with open(IMPORT_JSON_FILE_PATH, "rb") as fp:
|
|
import_in_organization_scope(
|
|
fp,
|
|
flags=ImportFlags(
|
|
merge_users=False, overwrite_configs=False, import_uuid=str(self.uuid)
|
|
),
|
|
org_filter=set(self.relocation.want_org_slugs),
|
|
printer=Printer(),
|
|
)
|
|
|
|
self.imported_users = ControlImportChunkReplica.objects.get(
|
|
import_uuid=self.uuid, model="sentry.user"
|
|
)
|
|
|
|
assert len(self.imported_users.inserted_map) == 2
|
|
|
|
def test_success(
|
|
self,
|
|
notifying_owner_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
):
|
|
self.mock_message_builder(fake_message_builder)
|
|
|
|
with patch.object(LostPasswordHash, "send_relocate_account_email") as mock_relocation_email:
|
|
notifying_users(self.uuid)
|
|
|
|
# Called once for each user imported, which is 2 for `fresh-install.json`
|
|
assert mock_relocation_email.call_count == 2
|
|
email_targets = [
|
|
mock_relocation_email.call_args_list[0][0][0].username,
|
|
mock_relocation_email.call_args_list[1][0][0].username,
|
|
]
|
|
assert mock_relocation_email.call_args_list[0][0][2] == ["testing"]
|
|
assert mock_relocation_email.call_args_list[1][0][2] == ["testing"]
|
|
assert "[email protected]" in email_targets
|
|
assert "[email protected]" in email_targets
|
|
|
|
assert fake_message_builder.call_count == 0
|
|
assert notifying_owner_mock.call_count == 1
|
|
|
|
relocation: Relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.latest_unclaimed_emails_sent_at is not None
|
|
|
|
def test_pause(
|
|
self,
|
|
notifying_owner_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
):
|
|
self.relocation.scheduled_pause_at_step = Relocation.Step.NOTIFYING.value
|
|
self.relocation.save()
|
|
|
|
notifying_users(self.uuid)
|
|
|
|
assert fake_message_builder.call_count == 0
|
|
assert notifying_owner_mock.call_count == 0
|
|
|
|
relocation: Relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.status == Relocation.Status.PAUSE.value
|
|
assert relocation.step == Relocation.Step.NOTIFYING.value
|
|
assert relocation.scheduled_pause_at_step is None
|
|
assert relocation.latest_task == OrderedTask.NOTIFYING_USERS.name
|
|
|
|
def test_retry_if_attempts_left(
|
|
self,
|
|
notifying_owner_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
):
|
|
self.mock_message_builder(fake_message_builder)
|
|
self.relocation.want_usernames = ["doesnotexist"]
|
|
self.relocation.save()
|
|
|
|
# An exception being raised will trigger a retry in celery.
|
|
with pytest.raises(Exception):
|
|
notifying_users(self.uuid)
|
|
|
|
assert fake_message_builder.call_count == 0
|
|
assert notifying_owner_mock.call_count == 0
|
|
|
|
relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.status == Relocation.Status.IN_PROGRESS.value
|
|
assert relocation.latest_notified != Relocation.EmailKind.FAILED.value
|
|
assert not relocation.failure_reason
|
|
|
|
def test_fail_if_no_attempts_left(
|
|
self,
|
|
notifying_owner_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
):
|
|
self.mock_message_builder(fake_message_builder)
|
|
self.relocation.latest_task = OrderedTask.NOTIFYING_USERS.name
|
|
self.relocation.latest_task_attempts = MAX_FAST_TASK_RETRIES
|
|
self.relocation.want_usernames = ["doesnotexist"]
|
|
self.relocation.save()
|
|
|
|
with pytest.raises(Exception):
|
|
notifying_users(self.uuid)
|
|
|
|
assert fake_message_builder.call_count == 1
|
|
assert fake_message_builder.call_args.kwargs["type"] == "relocation.failed"
|
|
fake_message_builder.return_value.send_async.assert_called_once_with(
|
|
to=[self.owner.email, self.superuser.email]
|
|
)
|
|
assert notifying_owner_mock.call_count == 0
|
|
|
|
relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.status == Relocation.Status.FAILURE.value
|
|
assert relocation.latest_notified == Relocation.EmailKind.FAILED.value
|
|
assert relocation.failure_reason == ERR_NOTIFYING_INTERNAL
|
|
|
|
|
|
@region_silo_test
|
|
@patch("sentry.utils.relocation.MessageBuilder")
|
|
@patch("sentry.tasks.relocation.completed.apply_async")
|
|
class NotifyingOwnerTest(RelocationTaskTestCase):
|
|
def setUp(self):
|
|
RelocationTaskTestCase.setUp(self)
|
|
TransactionTestCase.setUp(self)
|
|
self.relocation.step = Relocation.Step.NOTIFYING.value
|
|
self.relocation.latest_task = OrderedTask.NOTIFYING_USERS.name
|
|
self.relocation.save()
|
|
|
|
def test_success_admin_assisted_relocation(
|
|
self,
|
|
completed_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
):
|
|
self.mock_message_builder(fake_message_builder)
|
|
|
|
notifying_owner(self.uuid)
|
|
|
|
assert fake_message_builder.call_count == 1
|
|
assert fake_message_builder.call_args.kwargs["type"] == "relocation.succeeded"
|
|
fake_message_builder.return_value.send_async.assert_called_once_with(
|
|
to=[self.owner.email, self.superuser.email]
|
|
)
|
|
|
|
assert completed_mock.call_count == 1
|
|
|
|
relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.latest_notified == Relocation.EmailKind.SUCCEEDED.value
|
|
|
|
def test_success_self_serve_relocation(
|
|
self,
|
|
completed_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
):
|
|
self.mock_message_builder(fake_message_builder)
|
|
self.relocation.creator_id = self.relocation.owner_id
|
|
self.relocation.save()
|
|
|
|
notifying_owner(self.uuid)
|
|
|
|
assert fake_message_builder.call_count == 1
|
|
assert fake_message_builder.call_args.kwargs["type"] == "relocation.succeeded"
|
|
fake_message_builder.return_value.send_async.assert_called_once_with(to=[self.owner.email])
|
|
|
|
assert completed_mock.call_count == 1
|
|
|
|
relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.latest_notified == Relocation.EmailKind.SUCCEEDED.value
|
|
|
|
def test_retry_if_attempts_left(
|
|
self,
|
|
completed_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
):
|
|
self.mock_message_builder(fake_message_builder)
|
|
fake_message_builder.return_value.send_async.side_effect = Exception("Test")
|
|
|
|
# An exception being raised will trigger a retry in celery.
|
|
with pytest.raises(Exception):
|
|
notifying_owner(self.uuid)
|
|
|
|
assert fake_message_builder.call_count == 1
|
|
assert completed_mock.call_count == 0
|
|
|
|
relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.status == Relocation.Status.IN_PROGRESS.value
|
|
assert relocation.latest_notified != Relocation.EmailKind.FAILED.value
|
|
assert not relocation.failure_reason
|
|
|
|
def test_fail_if_no_attempts_left(
|
|
self,
|
|
completed_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
):
|
|
self.relocation.latest_task = OrderedTask.NOTIFYING_OWNER.name
|
|
self.relocation.latest_task_attempts = MAX_FAST_TASK_RETRIES
|
|
self.relocation.save()
|
|
|
|
self.mock_message_builder(fake_message_builder)
|
|
fake_message_builder.return_value.send_async.side_effect = [Exception("Test"), None]
|
|
|
|
with pytest.raises(Exception):
|
|
notifying_owner(self.uuid)
|
|
|
|
# Oh, the irony: sending the "relocation success" email failed, so we send a "relocation
|
|
# failed" email instead...
|
|
assert fake_message_builder.call_count == 2
|
|
email_types = [args.kwargs["type"] for args in fake_message_builder.call_args_list]
|
|
assert "relocation.failed" in email_types
|
|
assert "relocation.succeeded" in email_types
|
|
|
|
assert completed_mock.call_count == 0
|
|
|
|
relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.status == Relocation.Status.FAILURE.value
|
|
assert relocation.latest_notified == Relocation.EmailKind.FAILED.value
|
|
assert relocation.failure_reason == ERR_NOTIFYING_INTERNAL
|
|
|
|
|
|
@region_silo_test
|
|
class CompletedTest(RelocationTaskTestCase):
|
|
def setUp(self):
|
|
RelocationTaskTestCase.setUp(self)
|
|
TransactionTestCase.setUp(self)
|
|
self.relocation.step = Relocation.Step.NOTIFYING.value
|
|
self.relocation.latest_task = OrderedTask.NOTIFYING_OWNER.name
|
|
self.relocation.save()
|
|
|
|
def test_success(self):
|
|
completed(self.uuid)
|
|
|
|
relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.status == Relocation.Status.SUCCESS.value
|
|
assert not relocation.failure_reason
|
|
|
|
|
|
@region_silo_test
|
|
@patch(
|
|
"sentry.backup.crypto.KeyManagementServiceClient",
|
|
new_callable=lambda: FakeKeyManagementServiceClient,
|
|
)
|
|
@patch(
|
|
"sentry.tasks.relocation.CloudBuildClient",
|
|
new_callable=lambda: FakeCloudBuildClient,
|
|
)
|
|
@patch("sentry.utils.relocation.MessageBuilder")
|
|
@patch("sentry.signals.relocated.send_robust")
|
|
@patch("sentry.signals.relocation_redeem_promo_code.send_robust")
|
|
@patch("sentry.analytics.record")
|
|
class EndToEndTest(RelocationTaskTestCase, TransactionTestCase):
|
|
def setUp(self):
|
|
RelocationTaskTestCase.setUp(self)
|
|
TransactionTestCase.setUp(self)
|
|
|
|
self.storage = get_storage()
|
|
files = [
|
|
"null.json",
|
|
"import-baseline-config.json",
|
|
"import-colliding-users.json",
|
|
"import-raw-relocation-data.json",
|
|
"export-baseline-config.json",
|
|
"export-colliding-users.json",
|
|
"export-raw-relocation-data.json",
|
|
"compare-baseline-config.json",
|
|
"compare-colliding-users.json",
|
|
]
|
|
for file in files:
|
|
self.storage.save(f"runs/{self.relocation.uuid}/findings/{file}", BytesIO(b"[]"))
|
|
|
|
def mock_max_retries(
|
|
self,
|
|
fake_cloudbuild_client: FakeCloudBuildClient,
|
|
fake_kms_client: FakeKeyManagementServiceClient,
|
|
):
|
|
fake_cloudbuild_client.create_build.side_effect = (
|
|
[BustTaskRunnerRetryError("Retry")] * MAX_FAST_TASK_RETRIES
|
|
) + [fake_cloudbuild_client.create_build.return_value]
|
|
|
|
fake_cloudbuild_client.get_build.side_effect = (
|
|
[BustTaskRunnerRetryError("Retry")] * MAX_VALIDATION_POLLS
|
|
) + [fake_cloudbuild_client.get_build.return_value]
|
|
|
|
fake_kms_client.asymmetric_decrypt.side_effect = (
|
|
[BustTaskRunnerRetryError("Retry")] * MAX_FAST_TASK_RETRIES
|
|
) + [
|
|
fake_kms_client.asymmetric_decrypt.return_value,
|
|
# The second call to `asymmetric_decrypt` occurs from inside the `importing` task, which
|
|
# is not retried.
|
|
fake_kms_client.asymmetric_decrypt.return_value,
|
|
]
|
|
|
|
fake_kms_client.get_public_key.side_effect = (
|
|
[BustTaskRunnerRetryError("Retry")] * MAX_FAST_TASK_RETRIES
|
|
) + [fake_kms_client.get_public_key.return_value]
|
|
# Used by two tasks, so repeat the pattern (fail, fail, fail, succeed) twice.
|
|
fake_kms_client.get_public_key.side_effect = (
|
|
list(fake_kms_client.get_public_key.side_effect) * 2
|
|
)
|
|
|
|
def assert_success_database_state(self, org_count: int):
|
|
assert Organization.objects.filter(slug__startswith="testing").count() == org_count + 1
|
|
|
|
assert RegionImportChunk.objects.filter(import_uuid=self.uuid).count() == 9
|
|
assert sorted(RegionImportChunk.objects.values_list("model", flat=True)) == [
|
|
"sentry.organization",
|
|
"sentry.organizationmember",
|
|
"sentry.organizationmemberteam",
|
|
"sentry.project",
|
|
"sentry.projectkey",
|
|
"sentry.projectoption",
|
|
"sentry.projectteam",
|
|
"sentry.rule",
|
|
"sentry.team",
|
|
]
|
|
|
|
assert ControlImportChunkReplica.objects.filter(import_uuid=self.uuid).count() == 2
|
|
with assume_test_silo_mode(SiloMode.CONTROL):
|
|
assert ControlImportChunk.objects.filter(import_uuid=self.uuid).count() == 2
|
|
assert sorted(ControlImportChunk.objects.values_list("model", flat=True)) == [
|
|
"sentry.user",
|
|
"sentry.useremail",
|
|
]
|
|
|
|
def assert_failure_database_state(self, org_count: int):
|
|
assert Organization.objects.filter(slug__startswith="testing").count() == org_count
|
|
assert RegionImportChunk.objects.filter(import_uuid=self.uuid).count() == 0
|
|
|
|
assert ControlImportChunkReplica.objects.filter(import_uuid=self.uuid).count() == 0
|
|
with assume_test_silo_mode(SiloMode.CONTROL):
|
|
assert ControlImportChunk.objects.filter(import_uuid=self.uuid).count() == 0
|
|
|
|
def assert_success_analytics_record(self, analytics_record_mock: Mock):
|
|
imported_orgs = RegionImportChunk.objects.get(
|
|
import_uuid=self.uuid, model="sentry.organization"
|
|
)
|
|
|
|
imported_org_id: int = next(iter(imported_orgs.inserted_map.values()))
|
|
imported_org_slug: str = next(iter(imported_orgs.inserted_identifiers.values()))
|
|
|
|
analytics_record_mock.assert_called_with(
|
|
"relocation.organization_imported",
|
|
organization_id=imported_org_id,
|
|
relocation_uuid=str(self.uuid),
|
|
slug=imported_org_slug,
|
|
owner_id=self.owner.id,
|
|
)
|
|
|
|
def test_valid_no_retries(
|
|
self,
|
|
analytics_record_mock: Mock,
|
|
relocation_redeem_promo_code_signal_mock: Mock,
|
|
relocated_signal_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
fake_cloudbuild_client: FakeCloudBuildClient,
|
|
fake_kms_client: FakeKeyManagementServiceClient,
|
|
):
|
|
self.mock_cloudbuild_client(fake_cloudbuild_client, Build.Status(Build.Status.SUCCESS))
|
|
self.mock_kms_client(fake_kms_client)
|
|
self.mock_message_builder(fake_message_builder)
|
|
org_count = Organization.objects.filter(slug__startswith="testing").count()
|
|
|
|
with BurstTaskRunner() as burst:
|
|
uploading_complete(self.relocation.uuid)
|
|
|
|
with patch.object(LostPasswordHash, "send_relocate_account_email") as mock_relocation_email:
|
|
burst()
|
|
|
|
assert mock_relocation_email.call_count == 2
|
|
|
|
assert fake_cloudbuild_client.create_build.call_count == 1
|
|
assert fake_cloudbuild_client.get_build.call_count == 1
|
|
|
|
assert fake_kms_client.asymmetric_decrypt.call_count == 2
|
|
assert fake_kms_client.get_public_key.call_count == 2
|
|
|
|
assert fake_message_builder.call_count == 2
|
|
email_types = [args.kwargs["type"] for args in fake_message_builder.call_args_list]
|
|
assert "relocation.started" in email_types
|
|
assert "relocation.succeeded" in email_types
|
|
assert "relocation.failed" not in email_types
|
|
|
|
assert relocated_signal_mock.call_count == 1
|
|
assert relocation_redeem_promo_code_signal_mock.call_count == 1
|
|
|
|
relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.status == Relocation.Status.SUCCESS.value
|
|
assert relocation.latest_notified == Relocation.EmailKind.SUCCEEDED.value
|
|
assert not relocation.failure_reason
|
|
|
|
self.assert_success_database_state(org_count)
|
|
self.assert_success_analytics_record(analytics_record_mock)
|
|
|
|
def test_valid_max_retries(
|
|
self,
|
|
analytics_record_mock: Mock,
|
|
relocation_redeem_promo_code_signal_mock: Mock,
|
|
relocated_signal_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
fake_cloudbuild_client: FakeCloudBuildClient,
|
|
fake_kms_client: FakeKeyManagementServiceClient,
|
|
):
|
|
self.mock_cloudbuild_client(fake_cloudbuild_client, Build.Status(Build.Status.SUCCESS))
|
|
self.mock_kms_client(fake_kms_client)
|
|
self.mock_max_retries(fake_cloudbuild_client, fake_kms_client)
|
|
|
|
self.mock_message_builder(fake_message_builder)
|
|
org_count = Organization.objects.filter(slug__startswith="testing").count()
|
|
|
|
with BurstTaskRunner() as burst:
|
|
uploading_complete(self.relocation.uuid)
|
|
|
|
with patch.object(LostPasswordHash, "send_relocate_account_email") as mock_relocation_email:
|
|
burst()
|
|
|
|
assert mock_relocation_email.call_count == 2
|
|
|
|
assert fake_cloudbuild_client.create_build.call_count == MAX_FAST_TASK_ATTEMPTS
|
|
assert fake_cloudbuild_client.get_build.call_count == MAX_VALIDATION_POLL_ATTEMPTS
|
|
|
|
assert fake_kms_client.asymmetric_decrypt.call_count == MAX_FAST_TASK_ATTEMPTS + 1
|
|
assert fake_kms_client.get_public_key.call_count == 2 * MAX_FAST_TASK_ATTEMPTS
|
|
|
|
assert fake_message_builder.call_count == 2
|
|
email_types = [args.kwargs["type"] for args in fake_message_builder.call_args_list]
|
|
assert "relocation.started" in email_types
|
|
assert "relocation.succeeded" in email_types
|
|
assert "relocation.failed" not in email_types
|
|
|
|
assert relocated_signal_mock.call_count == 1
|
|
assert relocation_redeem_promo_code_signal_mock.call_count == 1
|
|
|
|
relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.status == Relocation.Status.SUCCESS.value
|
|
assert relocation.latest_notified == Relocation.EmailKind.SUCCEEDED.value
|
|
assert not relocation.failure_reason
|
|
|
|
self.assert_success_database_state(org_count)
|
|
self.assert_success_analytics_record(analytics_record_mock)
|
|
|
|
def test_invalid_no_retries(
|
|
self,
|
|
analytics_record_mock: Mock,
|
|
relocation_redeem_promo_code_signal_mock: Mock,
|
|
relocated_signal_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
fake_cloudbuild_client: FakeCloudBuildClient,
|
|
fake_kms_client: FakeKeyManagementServiceClient,
|
|
):
|
|
self.mock_cloudbuild_client(fake_cloudbuild_client, Build.Status(Build.Status.SUCCESS))
|
|
self.mock_kms_client(fake_kms_client)
|
|
self.mock_message_builder(fake_message_builder)
|
|
mock_invalid_finding(self.storage, self.uuid)
|
|
org_count = Organization.objects.filter(slug__startswith="testing").count()
|
|
|
|
with BurstTaskRunner() as burst:
|
|
uploading_complete(self.relocation.uuid)
|
|
|
|
with patch.object(LostPasswordHash, "send_relocate_account_email") as mock_relocation_email:
|
|
burst()
|
|
|
|
assert mock_relocation_email.call_count == 0
|
|
|
|
assert fake_cloudbuild_client.create_build.call_count == 1
|
|
assert fake_cloudbuild_client.get_build.call_count == 1
|
|
|
|
assert fake_kms_client.asymmetric_decrypt.call_count == 1
|
|
assert fake_kms_client.get_public_key.call_count == 2
|
|
|
|
assert fake_message_builder.call_count == 2
|
|
email_types = [args.kwargs["type"] for args in fake_message_builder.call_args_list]
|
|
assert "relocation.started" in email_types
|
|
assert "relocation.failed" in email_types
|
|
assert "relocation.succeeded" not in email_types
|
|
|
|
assert relocated_signal_mock.call_count == 0
|
|
assert relocation_redeem_promo_code_signal_mock.call_count == 0
|
|
|
|
relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.status == Relocation.Status.FAILURE.value
|
|
assert relocation.latest_notified == Relocation.EmailKind.FAILED.value
|
|
assert relocation.failure_reason
|
|
|
|
self.assert_failure_database_state(org_count)
|
|
analytics_record_mock.assert_not_called()
|
|
|
|
def test_invalid_max_retries(
|
|
self,
|
|
analytics_record_mock: Mock,
|
|
relocation_redeem_promo_code_signal_mock: Mock,
|
|
relocated_signal_mock: Mock,
|
|
fake_message_builder: Mock,
|
|
fake_cloudbuild_client: FakeCloudBuildClient,
|
|
fake_kms_client: FakeKeyManagementServiceClient,
|
|
):
|
|
self.mock_cloudbuild_client(fake_cloudbuild_client, Build.Status(Build.Status.SUCCESS))
|
|
self.mock_kms_client(fake_kms_client)
|
|
self.mock_max_retries(fake_cloudbuild_client, fake_kms_client)
|
|
|
|
self.mock_message_builder(fake_message_builder)
|
|
mock_invalid_finding(self.storage, self.uuid)
|
|
org_count = Organization.objects.filter(slug__startswith="testing").count()
|
|
|
|
with BurstTaskRunner() as burst:
|
|
uploading_complete(self.relocation.uuid)
|
|
|
|
with patch.object(LostPasswordHash, "send_relocate_account_email") as mock_relocation_email:
|
|
burst()
|
|
|
|
assert mock_relocation_email.call_count == 0
|
|
|
|
assert fake_cloudbuild_client.create_build.call_count == MAX_FAST_TASK_ATTEMPTS
|
|
assert fake_cloudbuild_client.get_build.call_count == MAX_VALIDATION_POLL_ATTEMPTS
|
|
|
|
assert fake_kms_client.asymmetric_decrypt.call_count == MAX_FAST_TASK_ATTEMPTS
|
|
assert fake_kms_client.get_public_key.call_count == 2 * MAX_FAST_TASK_ATTEMPTS
|
|
|
|
assert fake_message_builder.call_count == 2
|
|
email_types = [args.kwargs["type"] for args in fake_message_builder.call_args_list]
|
|
assert "relocation.started" in email_types
|
|
assert "relocation.failed" in email_types
|
|
assert "relocation.succeeded" not in email_types
|
|
|
|
assert relocated_signal_mock.call_count == 0
|
|
assert relocation_redeem_promo_code_signal_mock.call_count == 0
|
|
|
|
relocation = Relocation.objects.get(uuid=self.uuid)
|
|
assert relocation.status == Relocation.Status.FAILURE.value
|
|
assert relocation.latest_notified == Relocation.EmailKind.FAILED.value
|
|
assert relocation.failure_reason
|
|
|
|
self.assert_failure_database_state(org_count)
|
|
analytics_record_mock.assert_not_called()
|