diff --git a/api/applications/tests/factories.py b/api/applications/tests/factories.py index acfc2d0f86..ca9c76d042 100644 --- a/api/applications/tests/factories.py +++ b/api/applications/tests/factories.py @@ -1,7 +1,10 @@ import factory import factory.fuzzy + from faker import Faker +from django.utils import timezone + from api.applications.enums import ApplicationExportType, ApplicationExportLicenceOfficialType from api.applications.models import ( ApplicationDocument, @@ -18,9 +21,16 @@ from api.cases.models import Advice from api.external_data.models import Denial, DenialEntity, SanctionMatch from api.documents.tests.factories import DocumentFactory +from api.flags.enums import FlagLevels from api.goods.tests.factories import GoodFactory from api.organisations.tests.factories import OrganisationFactory, SiteFactory, ExternalLocationFactory -from api.parties.tests.factories import ConsigneeFactory, EndUserFactory, PartyFactory, ThirdPartyFactory +from api.parties.tests.factories import ( + ConsigneeFactory, + EndUserFactory, + PartyFactory, + PartyDocumentFactory, + ThirdPartyFactory, +) from api.users.tests.factories import ExporterUserFactory, GovUserFactory from api.staticdata.units.enums import Units from api.staticdata.control_list_entries.helpers import get_control_list_entry @@ -62,6 +72,7 @@ def _create(cls, model_class, *args, **kwargs): obj = model_class(*args, **kwargs) if "status" not in kwargs: obj.status = get_case_status_by_status(CaseStatusEnum.SUBMITTED) + obj.save() return obj @@ -228,6 +239,11 @@ def _create(cls, model_class, *args, **kwargs): GoodOnApplicationFactory(application=obj, good=GoodFactory(organisation=obj.organisation)) PartyOnApplicationFactory(application=obj, party=EndUserFactory(organisation=obj.organisation)) + PartyDocumentFactory( + party=obj.end_user.party, + s3_key="party-document", + safe=True, + ) if kwargs["goods_recipients"] in [ StandardApplication.VIA_CONSIGNEE, @@ -262,3 +278,30 @@ def _create(cls, model_class, *args, **kwargs): AdviceFactory(case=obj, level=AdviceLevel.FINAL) return obj + + +class StandardSubmittedApplicationFactory(DraftStandardApplicationFactory): + + @classmethod + def _create(cls, model_class, *args, **kwargs): + flags = kwargs.pop("flags", {}) + application = super()._create(model_class, *args, **kwargs) + application.status = get_case_status_by_status(CaseStatusEnum.SUBMITTED) + application.submitted_at = timezone.now() + application.save() + + if flags: + case_flags = flags.get(FlagLevels.CASE, []) + application.case_ptr.flags.add(*case_flags) + + good_flags = flags.get(FlagLevels.GOOD, []) + for good_on_application in application.goods.all(): + good_on_application.good.flags.add(*good_flags) + + destination_flags = flags.get(FlagLevels.DESTINATION, []) + party_on_application_flags = flags.get(FlagLevels.PARTY_ON_APPLICATION, []) + for party_on_application in application.parties.all(): + party_on_application.party.flags.add(*destination_flags) + party_on_application.flags.add(*party_on_application_flags) + + return application diff --git a/api/cases/libraries/get_flags.py b/api/cases/libraries/get_flags.py index ccdfe7ac65..a1169daf24 100644 --- a/api/cases/libraries/get_flags.py +++ b/api/cases/libraries/get_flags.py @@ -1,4 +1,4 @@ -from django.db.models import QuerySet, When, Case as DB_Case, IntegerField, BinaryField +from django.db.models import Q, QuerySet, When, Case as DB_Case, IntegerField, BinaryField from api.cases.enums import CaseTypeSubTypeEnum from api.cases.models import Case @@ -27,10 +27,16 @@ def get_destination_flags(case, case_type): if case_type == CaseTypeSubTypeEnum.EUA: return Flag.objects.filter(parties__parties_on_application__application_id=case.id) elif case_type == CaseTypeSubTypeEnum.STANDARD: - return Flag.objects.filter( - parties__parties_on_application__application_id=case.id, - parties__parties_on_application__deleted_at__isnull=True, + # Usually destination level flags are attached to `Party` but some of the flags + # are attached to `PartyOnApplication` so gather all of them + flags = Flag.objects.filter( + ( + Q(parties__parties_on_application__application_id=case.id) + & Q(parties__parties_on_application__deleted_at__isnull=True) + ) + | (Q(parties_on_application__application__pk=case.id) & Q(parties_on_application__deleted_at__isnull=True)) ) + return flags return Flag.objects.none() diff --git a/api/cases/tests/conftest.py b/api/cases/tests/conftest.py index 70b1362335..ae2fe655ad 100644 --- a/api/cases/tests/conftest.py +++ b/api/cases/tests/conftest.py @@ -4,16 +4,17 @@ from rest_framework.test import APIClient from api.applications.tests.factories import DraftStandardApplicationFactory -from api.core.constants import ExporterPermissions, GovPermissions +from api.core.constants import ExporterPermissions, GovPermissions, Roles from api.organisations.tests.factories import OrganisationFactory from api.parties.tests.factories import PartyDocumentFactory from api.users.libraries.user_to_token import user_to_token -from api.users.models import Permission +from api.users.models import Permission, Role from api.users.enums import UserType from api.users.tests.factories import ( ExporterUserFactory, GovUserFactory, RoleFactory, + SystemUserFactory, UserOrganisationRelationshipFactory, ) @@ -52,6 +53,11 @@ def exporter_headers(exporter_user, organisation): } +@pytest.fixture(autouse=True) +def system_user(): + return SystemUserFactory() + + @pytest.fixture() def gov_headers(gov_user): return {"HTTP_GOV_USER_TOKEN": user_to_token(gov_user.baseuser_ptr)} @@ -59,7 +65,16 @@ def gov_headers(gov_user): @pytest.fixture() def gov_user(): - return GovUserFactory() + gov_user = GovUserFactory() + if Role.objects.filter(id=Roles.INTERNAL_DEFAULT_ROLE_ID, type=UserType.INTERNAL.value).exists(): + return gov_user + + gov_user.role = RoleFactory( + id=Roles.INTERNAL_DEFAULT_ROLE_ID, type=UserType.INTERNAL.value, name=Roles.INTERNAL_DEFAULT_ROLE_NAME + ) + gov_user.save() + + return gov_user @pytest.fixture() diff --git a/api/cases/tests/test_case_flags.py b/api/cases/tests/test_case_flags.py new file mode 100644 index 0000000000..718d79a9b9 --- /dev/null +++ b/api/cases/tests/test_case_flags.py @@ -0,0 +1,92 @@ +import pytest + +from django.urls import reverse +from urllib import parse + +from api.applications.tests.factories import StandardSubmittedApplicationFactory +from api.flags.enums import FlagLevels +from api.queues.constants import ALL_CASES_QUEUE_ID + +from lite_routing.routing_rules_internal.enums import FlagsEnum + +pytestmark = pytest.mark.django_db + + +@pytest.fixture +def all_cases_queue_url(): + query_params = {"queue_id": ALL_CASES_QUEUE_ID} + return f"{reverse('cases:search')}?{parse.urlencode(query_params, doseq=True)}" + + +@pytest.mark.parametrize( + "flags_data", + ( + {FlagLevels.CASE: [FlagsEnum.UNSCR_OFSI_SANCTIONS]}, + {FlagLevels.GOOD: [FlagsEnum.SMALL_ARMS, FlagsEnum.UK_DUAL_USE_SCH3]}, + {FlagLevels.DESTINATION: [FlagsEnum.OIL_AND_GAS_ID]}, + {FlagLevels.PARTY_ON_APPLICATION: [FlagsEnum.SANCTION_UK_MATCH, FlagsEnum.SANCTION_OFSI_MATCH]}, + ), +) +def test_queue_view_case_flags( + api_client, + all_cases_queue_url, + gov_headers, + mocker, + flags_data, +): + # When changes are saved in factory then post_save() signal can trigger flagging rules + # and alter flags applied which is not desirable in these tests hence mock that function. + mocker.patch("api.cases.signals.apply_flagging_rules_to_case", return_value=None) + StandardSubmittedApplicationFactory(flags=flags_data) + + response = api_client.get(all_cases_queue_url, **gov_headers) + assert response.status_code == 200 + + for case in response.json()["results"]["cases"]: + all_flags = [ + item["id"] for flag_level in ["flags", "goods_flags", "destinations_flags"] for item in case[flag_level] + ] + + expected_flags = [] + for flags in flags_data.values(): + expected_flags.extend(flags) + + assert sorted(all_flags) == sorted(expected_flags) + + +@pytest.mark.parametrize( + "flags_data", + ( + { + FlagLevels.CASE: [FlagsEnum.UNSCR_OFSI_SANCTIONS], + FlagLevels.GOOD: [FlagsEnum.DUAL_USE_ANNEX_1], + }, + {FlagLevels.GOOD: [FlagsEnum.SMALL_ARMS, FlagsEnum.UK_DUAL_USE_SCH3]}, + {FlagLevels.DESTINATION: [FlagsEnum.OIL_AND_GAS_ID]}, + { + FlagLevels.CASE: [FlagsEnum.GOODS_NOT_LISTED], + FlagLevels.PARTY_ON_APPLICATION: [FlagsEnum.SANCTION_UK_MATCH, FlagsEnum.SANCTION_OFSI_MATCH], + }, + ), +) +def test_case_detail_flags( + api_client, + gov_headers, + mocker, + flags_data, +): + mocker.patch("api.cases.signals.apply_flagging_rules_to_case", return_value=None) + case = StandardSubmittedApplicationFactory(flags=flags_data) + + url = reverse("cases:case", kwargs={"pk": case.id}) + response = api_client.get(url, **gov_headers) + assert response.status_code == 200 + + response = response.json() + all_flags = [item["id"] for item in response["case"]["all_flags"]] + + expected_flags = [] + for flags in flags_data.values(): + expected_flags.extend(flags) + + assert sorted(all_flags) == sorted(expected_flags) diff --git a/api/cases/tests/test_helpers.py b/api/cases/tests/test_helpers.py index f85b5e651a..f28b824952 100644 --- a/api/cases/tests/test_helpers.py +++ b/api/cases/tests/test_helpers.py @@ -1,10 +1,14 @@ # TODO; test notify_ecju_query in total isolation import datetime +import pytest + from parameterized import parameterized from api.cases.helpers import working_days_in_range +pytestmark = pytest.mark.django_db + @parameterized.expand( [ diff --git a/api/staticdata/denial_reasons/migrations/0007_criterion_1_description_update.py b/api/staticdata/denial_reasons/migrations/0007_criterion_1_description_update.py new file mode 100644 index 0000000000..b317a3f709 --- /dev/null +++ b/api/staticdata/denial_reasons/migrations/0007_criterion_1_description_update.py @@ -0,0 +1,19 @@ +from django.db import migrations + + +def update_denial_reason(apps, schema_editor): + DenialReason = apps.get_model("denial_reasons", "DenialReason") + denial_reason = DenialReason.objects.get(id=1) + if denial_reason: + denial_reason.description = "Respect for the UK's international obligations and commitments, in particular sanctions adopted by the UN Security Council, agreements on non-proliferation and other subjects, as well as other international obligations." + denial_reason.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("denial_reasons", "0006_populate_uuid_field"), + ] + + operations = [ + migrations.RunPython(update_denial_reason, migrations.RunPython.noop), + ] diff --git a/api/staticdata/denial_reasons/migrations/tests/test_0007_criterion_1_description_update.py b/api/staticdata/denial_reasons/migrations/tests/test_0007_criterion_1_description_update.py new file mode 100644 index 0000000000..319b2848d4 --- /dev/null +++ b/api/staticdata/denial_reasons/migrations/tests/test_0007_criterion_1_description_update.py @@ -0,0 +1,32 @@ +import pytest + +from django_test_migrations.migrator import Migrator + +from api.staticdata.denial_reasons.constants import DENIAL_REASON_ID_TO_UUID_MAP + + +@pytest.mark.django_db() +def test_populate_uuid_field(): + migrator = Migrator(database="default") + + old_state = migrator.apply_initial_migration(("denial_reasons", "0006_populate_uuid_field")) + DenialReason = old_state.apps.get_model("denial_reasons", "DenialReason") + denial_reason = DenialReason.objects.get(id=1) + assert ( + denial_reason.description + == """Respect for the UK's international obligations and commitments, in particular sanctions adopted by the UN Security Council, agreements on non-proliferation and other subjects, as well as other international obligations. + +Military End Use Control.""" + ) + + new_state = migrator.apply_tested_migration(("denial_reasons", "0007_criterion_1_description_update")) + DenialReason = new_state.apps.get_model("denial_reasons", "DenialReason") + denial_reason = DenialReason.objects.get(id=1) + assert ( + denial_reason.description + == "Respect for the UK's international obligations and commitments, in particular sanctions adopted by the UN Security Council, agreements on non-proliferation and other subjects, as well as other international obligations." + ) + + expected_uuids = set(DENIAL_REASON_ID_TO_UUID_MAP.values()) + actual_uuids = set([str(denial_reason.uuid) for denial_reason in DenialReason.objects.all()]) + assert expected_uuids == actual_uuids diff --git a/api/users/tests/factories.py b/api/users/tests/factories.py index 05d319ff1a..589c6ef1ee 100644 --- a/api/users/tests/factories.py +++ b/api/users/tests/factories.py @@ -3,7 +3,7 @@ from api.organisations.tests.factories import OrganisationFactory from api.users import models -from api.users.enums import UserType, UserStatuses +from api.users.enums import SystemUser, UserType, UserStatuses from api.users.models import Role, UserOrganisationRelationship from api.teams.tests.factories import TeamFactory @@ -19,6 +19,22 @@ class Meta: model = models.BaseUser +class SystemUserFactory(factory.django.DjangoModelFactory): + first_name = factory.Faker("first_name") + last_name = factory.Faker("last_name") + email = factory.LazyAttribute(lambda n: faker.unique.email()) + + class Meta: + model = models.BaseUser + django_get_or_create = ( + "id", + "type", + ) + + id = SystemUser.id + type = UserType.SYSTEM + + class GovUserFactory(factory.django.DjangoModelFactory): baseuser_ptr = factory.SubFactory(BaseUserFactory, type=UserType.INTERNAL) team = factory.SubFactory(TeamFactory)