From 9a7981ac217e61fe57874cada33e76b18dd5f223 Mon Sep 17 00:00:00 2001 From: Kari Salminen Date: Mon, 9 Feb 2026 16:20:42 +0200 Subject: [PATCH 1/4] feat(ks,backend): index Company.name for search & ordering performance refs YJDH-805 --- .../migrations/0011_company_add_name_index.py | 16 ++++++++++++++++ backend/kesaseteli/companies/models.py | 3 +++ 2 files changed, 19 insertions(+) create mode 100644 backend/kesaseteli/companies/migrations/0011_company_add_name_index.py diff --git a/backend/kesaseteli/companies/migrations/0011_company_add_name_index.py b/backend/kesaseteli/companies/migrations/0011_company_add_name_index.py new file mode 100644 index 0000000000..54bd165c66 --- /dev/null +++ b/backend/kesaseteli/companies/migrations/0011_company_add_name_index.py @@ -0,0 +1,16 @@ +# Generated by Django 5.1.15 on 2026-02-09 14:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("companies", "0010_populate_company_timestamps"), + ] + + operations = [ + migrations.AddIndex( + model_name="company", + index=models.Index(fields=["name"], name="company_name_idx"), + ), + ] diff --git a/backend/kesaseteli/companies/models.py b/backend/kesaseteli/companies/models.py index 4390d97a0a..2984310845 100644 --- a/backend/kesaseteli/companies/models.py +++ b/backend/kesaseteli/companies/models.py @@ -40,6 +40,9 @@ class Meta: verbose_name = _("company") verbose_name_plural = _("companies") ordering = ["name"] + indexes = [ + models.Index(fields=["name"], name="company_name_idx"), + ] def __str__(self): return f"{self.name} ({self.business_id})" From 17079e146718a7117eabcc0630fbbbff10d5dfa6 Mon Sep 17 00:00:00 2001 From: Kari Salminen Date: Mon, 9 Feb 2026 16:22:15 +0200 Subject: [PATCH 2/4] feat(ks,backend): index EmployerApplication timestamps & status field Rationale: - For search & ordering performance refs YJDH-805 --- ...cation_add_timestamp_and_status_indexes.py | 34 +++++++++++++++++++ backend/kesaseteli/applications/models.py | 5 +++ 2 files changed, 39 insertions(+) create mode 100644 backend/kesaseteli/applications/migrations/0048_employerapplication_add_timestamp_and_status_indexes.py diff --git a/backend/kesaseteli/applications/migrations/0048_employerapplication_add_timestamp_and_status_indexes.py b/backend/kesaseteli/applications/migrations/0048_employerapplication_add_timestamp_and_status_indexes.py new file mode 100644 index 0000000000..d327eb80fb --- /dev/null +++ b/backend/kesaseteli/applications/migrations/0048_employerapplication_add_timestamp_and_status_indexes.py @@ -0,0 +1,34 @@ +# Generated by Django 5.1.15 on 2026-02-09 14:14 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ( + "applications", + "0047_alter_historicalyouthsummervoucher_target_group_and_more", + ), + ("companies", "0011_company_add_name_index"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddIndex( + model_name="employerapplication", + index=models.Index( + fields=["created_at"], name="employer_app_created_at_idx" + ), + ), + migrations.AddIndex( + model_name="employerapplication", + index=models.Index( + fields=["modified_at"], name="employer_app_modified_at_idx" + ), + ), + migrations.AddIndex( + model_name="employerapplication", + index=models.Index(fields=["status"], name="employer_app_status_idx"), + ), + ] diff --git a/backend/kesaseteli/applications/models.py b/backend/kesaseteli/applications/models.py index 2058b6e196..a86435a390 100644 --- a/backend/kesaseteli/applications/models.py +++ b/backend/kesaseteli/applications/models.py @@ -1258,6 +1258,11 @@ class Meta: verbose_name = _("employer application") verbose_name_plural = _("employer applications") ordering = ["-created_at", "id"] + indexes = [ + models.Index(fields=["created_at"], name="employer_app_created_at_idx"), + models.Index(fields=["modified_at"], name="employer_app_modified_at_idx"), + models.Index(fields=["status"], name="employer_app_status_idx"), + ] class EmployerSummerVoucher(HistoricalModel, TimeStampedModel, UUIDModel): From 0d85c7f041bab8118a8628effa38cabc5f2de544 Mon Sep 17 00:00:00 2001 From: Kari Salminen Date: Fri, 6 Feb 2026 16:03:53 +0200 Subject: [PATCH 3/4] feat(ks,backend): add django-filter package refs YJDH-805 --- backend/docker/kesaseteli.Dockerfile | 5 ++++- backend/kesaseteli/requirements.in | 1 + backend/kesaseteli/requirements.txt | 3 +++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/backend/docker/kesaseteli.Dockerfile b/backend/docker/kesaseteli.Dockerfile index 8a0e599da2..d9b3ed8b7a 100644 --- a/backend/docker/kesaseteli.Dockerfile +++ b/backend/docker/kesaseteli.Dockerfile @@ -12,6 +12,9 @@ COPY --chown=default:root kesaseteli/requirements-prod.txt /app/requirements-pro COPY --chown=default:root kesaseteli/.prod/escape_json.c /app/.prod/escape_json.c COPY --chown=default:root shared /shared/ +# WORKAROUND: pip 25.3 used until https://github.com/jazzband/pip-tools/issues/2319 really works. +# Otherwise "AttributeError: 'PackageFinder' object has no attribute 'allow_all_prereleases'" +# can happen when using pip-tools / pip-compile in development. RUN dnf update -y \ && dnf install -y \ git \ @@ -23,7 +26,7 @@ RUN dnf update -y \ xmlsec1-openssl \ cyrus-sasl-devel \ openssl-devel \ - && pip install -U pip setuptools wheel \ + && pip install -U pip==25.3 setuptools wheel \ && pip install --no-cache-dir -r /app/requirements.txt \ && pip install --no-cache-dir -r /app/requirements-prod.txt \ && uwsgi --build-plugin /app/.prod/escape_json.c \ diff --git a/backend/kesaseteli/requirements.in b/backend/kesaseteli/requirements.in index 67cdf84050..62a0126320 100644 --- a/backend/kesaseteli/requirements.in +++ b/backend/kesaseteli/requirements.in @@ -3,6 +3,7 @@ django-auth-adfs django-cors-headers django-environ django-extensions +django-filter django-localflavor django-searchable-encrypted-fields django-sequences diff --git a/backend/kesaseteli/requirements.txt b/backend/kesaseteli/requirements.txt index 0630335f92..26fa382b6a 100644 --- a/backend/kesaseteli/requirements.txt +++ b/backend/kesaseteli/requirements.txt @@ -46,6 +46,7 @@ django==5.1.15 # django-auth-adfs # django-cors-headers # django-extensions + # django-filter # django-localflavor # django-searchable-encrypted-fields # django-sequences @@ -67,6 +68,8 @@ django-extensions==4.1 # via # -r requirements.in # yjdh-backend-shared +django-filter==25.1 + # via -r requirements.in django-localflavor==5.0 # via -r requirements.in django-searchable-encrypted-fields==0.2.1 From 9751a7ba2c29b9fbc2c41465f350ca02c9936c44 Mon Sep 17 00:00:00 2001 From: Kari Salminen Date: Mon, 9 Feb 2026 18:00:08 +0200 Subject: [PATCH 4/4] feat(ks,backend): allow filtering & ordering EmployerApplicationViewSet e.g. `?only_mine=false` can be used to show all the user's company's EmployerApplications using EmployerApplicationViewSet. Currently it defaults to `?only_mine=true` i.e. the previous behavior. also: - allow all company's users to view all of the company's EmployerApplication objects, EmployerSummerVoucher objects, and their attachments - allow only the employer application's creator to: - update an existing EmployerApplication - post a new attachment to an EmployerSummerVoucher - delete an attachment from an EmployerSummerVoucher refs YJDH-805 --- .../applications/api/v1/permissions.py | 25 ++-- .../applications/api/v1/serializers.py | 4 +- .../kesaseteli/applications/api/v1/views.py | 119 +++++++++++++++--- .../test_security_with_non_staff_user.py | 119 +++++++++++++++--- 4 files changed, 215 insertions(+), 52 deletions(-) diff --git a/backend/kesaseteli/applications/api/v1/permissions.py b/backend/kesaseteli/applications/api/v1/permissions.py index 90abe0b04a..d991c54093 100644 --- a/backend/kesaseteli/applications/api/v1/permissions.py +++ b/backend/kesaseteli/applications/api/v1/permissions.py @@ -28,23 +28,16 @@ def has_employer_application_permission( request: HttpRequest, employer_application: EmployerApplication ) -> bool: """ - Allow access only for DRAFT status employer applications of the user & - company. + Allow access only to DRAFT status employer applications of the user's company. """ - user = request.user - - if user.is_staff or user.is_superuser: - return True - - user_company = get_user_company(request) - - if ( - employer_application.company == user_company - and employer_application.user == user - and employer_application.status in ALLOWED_APPLICATION_VIEW_STATUSES - ): - return True - return False + return ( + request.user.is_staff + or request.user.is_superuser + or ( + employer_application.company == get_user_company(request) + and employer_application.status in ALLOWED_APPLICATION_VIEW_STATUSES + ) + ) class EmployerApplicationPermission(BasePermission): diff --git a/backend/kesaseteli/applications/api/v1/serializers.py b/backend/kesaseteli/applications/api/v1/serializers.py index d2a651a0f8..215da9f3ef 100644 --- a/backend/kesaseteli/applications/api/v1/serializers.py +++ b/backend/kesaseteli/applications/api/v1/serializers.py @@ -351,6 +351,8 @@ class Meta: model = EmployerApplication fields = [ "id", + "created_at", + "modified_at", "status", "street_address", "bank_account_number", @@ -367,7 +369,7 @@ class Meta: "language", "submitted_at", ] - read_only_fields = ["user"] + read_only_fields = ["created_at", "modified_at", "user"] @transaction.atomic def update(self, instance, validated_data): diff --git a/backend/kesaseteli/applications/api/v1/views.py b/backend/kesaseteli/applications/api/v1/views.py index 4654071263..9a6f01b24f 100644 --- a/backend/kesaseteli/applications/api/v1/views.py +++ b/backend/kesaseteli/applications/api/v1/views.py @@ -4,6 +4,7 @@ from django.conf import settings from django.core import exceptions +from django.core.exceptions import PermissionDenied from django.db import transaction from django.db.models import F, Func from django.db.utils import ProgrammingError @@ -14,6 +15,7 @@ from django.utils.text import format_lazy from django.utils.translation import gettext_lazy as _ from django.views.decorators.csrf import csrf_protect +from django_filters import rest_framework as filters from rest_framework import status from rest_framework.decorators import action from rest_framework.exceptions import ValidationError @@ -654,23 +656,98 @@ def create_without_ssn(self, request, *args, **kwargs) -> HttpResponse: raise +class EmployerApplicationFilter(filters.FilterSet): + """ + Filtering & sorting support for EmployerApplicationViewSet + """ + + only_mine = filters.BooleanFilter(method="filter_only_mine") + status = filters.MultipleChoiceFilter(choices=EmployerApplicationStatus.choices) + created_at = filters.DateTimeFromToRangeFilter() + modified_at = filters.DateTimeFromToRangeFilter() + ordering = filters.OrderingFilter( + # (Model field name, parameter name) tuples: + fields=[ + # Present model field names directly as the query parameters: + (field_name, field_name) + for field_name in [ + "company__business_id", + "company__name", + "company_id", + "created_at", + "id", + "modified_at", + "status", + "user__first_name", + "user__last_name", + "user_id", + ] + ] + ) + + def _filter_by_user(self, queryset): + """ + Filter queryset by the current user, if they're authenticated, + otherwise return empty queryset. + """ + user = self.request.user + if user and user.is_authenticated: + return queryset.filter(user=user) + return queryset.none() + + def filter_only_mine(self, queryset, name, value): + """ + Filter to show only current user's employer applications + """ + if value: + return self._filter_by_user(queryset) + return queryset + + @property + def qs(self): + """ + Queryset property overridden to apply only_mine filter by default + when not provided or empty. + """ + parent_qs = super().qs + + # Apply only_mine filter by default when not provided or empty + if self.data.get("only_mine") in (None, ""): + return self._filter_by_user(parent_qs) + + return parent_qs + + class Meta: + model = EmployerApplication + _timestamp_field_lookups = [ + f"{prefix}{filter}" + for prefix in ["", "year__", "date__"] + for filter in ["gte", "lte", "gt", "lt", "exact"] + ] + fields = { + "company__business_id": ["exact", "in"], + "company_id": ["exact", "in"], + "created_at": _timestamp_field_lookups, + "modified_at": _timestamp_field_lookups, + "status": ["exact", "in"], + "user_id": ["exact", "in"], + } + + class EmployerApplicationViewSet(AuditLoggingModelViewSet): queryset = EmployerApplication.objects.all() serializer_class = EmployerApplicationSerializer permission_classes = [IsAuthenticated, EmployerApplicationPermission] pagination_class = LimitOffsetPagination + filter_backends = [filters.DjangoFilterBackend] + filterset_class = EmployerApplicationFilter def get_queryset(self): - """ - Fetch all DRAFT status applications of the user & company. - - Should inlcude only 1 application since we don't allow creation of - multiple DRAFT applications per user & company. - """ queryset = ( super() .get_queryset() .select_related("company") + .select_related("user") .prefetch_related("summer_vouchers") ) @@ -682,7 +759,6 @@ def get_queryset(self): return queryset.filter( company=user_company, - user=user, status__in=ALLOWED_APPLICATION_VIEW_STATUSES, ) @@ -690,8 +766,13 @@ def create(self, request, *args, **kwargs): """ Allow only 1 (DRAFT) application per user & company. """ - if self.get_queryset().filter(status=EmployerApplicationStatus.DRAFT).exists(): + if ( + self.get_queryset() + .filter(status=EmployerApplicationStatus.DRAFT, user=request.user) + .exists() + ): raise ValidationError("Company & user can have only one draft application") + return super().create(request, *args, **kwargs) def update(self, request, *args, **kwargs): @@ -701,6 +782,9 @@ def update(self, request, *args, **kwargs): instance = self.get_object() if instance.status not in ALLOWED_APPLICATION_UPDATE_STATUSES: raise ValidationError("Only DRAFT applications can be updated") + if request.user != instance.user: + raise PermissionDenied("Only application creator can update it") + return super().update(request, *args, **kwargs) def destroy(self, request, *args, **kwargs): @@ -717,8 +801,8 @@ class EmployerSummerVoucherViewSet(AuditLoggingModelViewSet): def get_queryset(self): """ - Fetch summer vouchers of DRAFT status applications of the user & - company. + Fetch summer vouchers of DRAFT/SUBMITTED status applications + of the user's company. """ queryset = ( super() @@ -728,7 +812,7 @@ def get_queryset(self): ) user = self.request.user - if user.is_staff: + if user.is_staff or user.is_superuser: return queryset elif user.is_anonymous: return queryset.none() @@ -737,7 +821,6 @@ def get_queryset(self): return queryset.filter( application__company=user_company, - application__user=user, application__status__in=ALLOWED_APPLICATION_VIEW_STATUSES, ) @@ -772,6 +855,8 @@ def post_attachment(self, request, *args, **kwargs): raise ValidationError( "Attachments can be uploaded only for DRAFT applications" ) + if obj.application.user != request.user: + raise PermissionDenied("Only application creator can post attachment to it") # Validate request data serializer = AttachmentSerializer( @@ -821,15 +906,19 @@ def handle_attachment(self, request, attachment_pk, *args, **kwargs): raise ValidationError( "Attachments can be deleted only for DRAFT applications" ) + if obj.application.user != request.user: + raise PermissionDenied( + "Only application creator can delete attachment from it" + ) if ( obj.application.status not in AttachmentSerializer.ATTACHMENT_MODIFICATION_ALLOWED_STATUSES ): - return Response( - {"detail": _("Operation not allowed for this application status.")}, - status=status.HTTP_403_FORBIDDEN, + raise PermissionDenied( + "Operation not allowed for this application status." ) + try: instance = obj.attachments.get(id=attachment_pk) except exceptions.ObjectDoesNotExist: diff --git a/backend/kesaseteli/applications/tests/test_security_with_non_staff_user.py b/backend/kesaseteli/applications/tests/test_security_with_non_staff_user.py index 08407ea77d..4e238d10ff 100644 --- a/backend/kesaseteli/applications/tests/test_security_with_non_staff_user.py +++ b/backend/kesaseteli/applications/tests/test_security_with_non_staff_user.py @@ -75,7 +75,7 @@ def test_employer_application_list_viewable_statuses( """ Test that the employer application list endpoint returns draft and submitted employer applications but only those that are the user's and use - the user's company. + the user's company by default. """ user1, user2 = UserFactory.create_batch(size=2) user1_client = force_login_user(user1) @@ -103,6 +103,63 @@ def test_employer_application_list_viewable_statuses( assert response.data[0]["company"]["id"] == str(company.id) +@override_settings(NEXT_PUBLIC_MOCK_FLAG=False) +@pytest.mark.parametrize( + "application_status", + [ + EmployerApplicationStatus.DRAFT, + EmployerApplicationStatus.SUBMITTED, + ], +) +@pytest.mark.django_db +def test_employer_application_list_viewable_statuses_not_only_mine( + application_status: EmployerApplicationStatus, +): + """ + Test that the employer application list endpoint returns draft and + submitted employer applications for the user's company, including + other users' employer applications for the company, if only_mine=false. + """ + user1, user2, extra_user = UserFactory.create_batch(size=3) + user1_client = force_login_user(user1) + user2_client = force_login_user(user2) + + company1, company2, extra_company = CompanyFactory.create_batch(size=3) + + company1_attachments = ( + create_attachment(user1, company1, application_status), + create_attachment(user2, company1, application_status), + ) + company2_attachments = ( + create_attachment(user1, company2, application_status), + create_attachment(user2, company2, application_status), + ) + # Attachments that should not be returned as they're not connected to company1 or company2: + _extra_company_attachments = ( + create_attachment(user1, extra_company, application_status), + create_attachment(user2, extra_company, application_status), + create_attachment(extra_user, extra_company, application_status), + ) + + for client, company, attachments in [ + (user1_client, company1, company1_attachments), + (user1_client, company2, company2_attachments), + (user2_client, company1, company1_attachments), + (user2_client, company2, company2_attachments), + ]: + set_company_business_id_to_client(company, client) + response = client.get( + reverse("v1:employerapplication-list"), query_params={"only_mine": "false"} + ) + assert response.status_code == status.HTTP_200_OK + assert len(response.data) == 2 + assert {x["user"] for x in response.data} == {user1.id, user2.id} + assert {x["company"]["id"] for x in response.data} == {str(company.id)} + assert {x["id"] for x in response.data} == { + str(attachment.summer_voucher.application.id) for attachment in attachments + } + + @override_settings(NEXT_PUBLIC_MOCK_FLAG=False) @pytest.mark.parametrize( "application_status", @@ -177,20 +234,26 @@ def test_employer_summer_voucher_handle_attachment_viewable_statuses( ): """ Test that the employer summer voucher's handle attachment endpoint returns - draft and submitted employer applications' attachments but only those that - are the user's and use the user's company. + draft and submitted employer applications' attachments only for the user's company. """ - user1, user2 = UserFactory.create_batch(size=2) + user1, user2, extra_user = UserFactory.create_batch(size=3) user1_client = force_login_user(user1) user2_client = force_login_user(user2) - company1, company2 = CompanyFactory.create_batch(size=2) + company1, company2, extra_company = CompanyFactory.create_batch(size=3) user1_company1_attachment = create_attachment(user1, company1, application_status) user1_company2_attachment = create_attachment(user1, company2, application_status) user2_company1_attachment = create_attachment(user2, company1, application_status) user2_company2_attachment = create_attachment(user2, company2, application_status) + # Attachments that should not be returned as they're not connected to company1 or company2: + extra_attachments = ( + create_attachment(user1, extra_company, application_status), + create_attachment(user2, extra_company, application_status), + create_attachment(extra_user, extra_company, application_status), + ) + def get_attachment(client: Client, attachment: Attachment): return client.get( reverse( @@ -202,30 +265,46 @@ def get_attachment(client: Client, attachment: Attachment): ) ) - # The status code 200s are the ones that match both the user and the company: + # The status code 200s are the ones that match the user's company: set_company_business_id_to_client(company1, user1_client) assert get_attachment(user1_client, user1_company1_attachment).status_code == 200 assert get_attachment(user1_client, user1_company2_attachment).status_code == 404 - assert get_attachment(user1_client, user2_company1_attachment).status_code == 404 + assert get_attachment(user1_client, user2_company1_attachment).status_code == 200 assert get_attachment(user1_client, user2_company2_attachment).status_code == 404 + assert all( + get_attachment(user1_client, attachment).status_code == 404 + for attachment in extra_attachments + ) set_company_business_id_to_client(company1, user2_client) - assert get_attachment(user2_client, user1_company1_attachment).status_code == 404 + assert get_attachment(user2_client, user1_company1_attachment).status_code == 200 assert get_attachment(user2_client, user1_company2_attachment).status_code == 404 assert get_attachment(user2_client, user2_company1_attachment).status_code == 200 assert get_attachment(user2_client, user2_company2_attachment).status_code == 404 + assert all( + get_attachment(user2_client, attachment).status_code == 404 + for attachment in extra_attachments + ) set_company_business_id_to_client(company2, user1_client) assert get_attachment(user1_client, user1_company1_attachment).status_code == 404 assert get_attachment(user1_client, user1_company2_attachment).status_code == 200 assert get_attachment(user1_client, user2_company1_attachment).status_code == 404 - assert get_attachment(user1_client, user2_company2_attachment).status_code == 404 + assert get_attachment(user1_client, user2_company2_attachment).status_code == 200 + assert all( + get_attachment(user1_client, attachment).status_code == 404 + for attachment in extra_attachments + ) set_company_business_id_to_client(company2, user2_client) assert get_attachment(user2_client, user1_company1_attachment).status_code == 404 - assert get_attachment(user2_client, user1_company2_attachment).status_code == 404 + assert get_attachment(user2_client, user1_company2_attachment).status_code == 200 assert get_attachment(user2_client, user2_company1_attachment).status_code == 404 assert get_attachment(user2_client, user2_company2_attachment).status_code == 200 + assert all( + get_attachment(user2_client, attachment).status_code == 404 + for attachment in extra_attachments + ) @override_settings(NEXT_PUBLIC_MOCK_FLAG=False) @@ -326,25 +405,25 @@ def del_attachment(client: Client, attachment: Attachment): ) ) - # Unallowed deletions + # Seen but unallowed deletions (=403), and not seen cases (=404): set_company_business_id_to_client(company1, user1_client) assert del_attachment(user1_client, user1_company2_attachment).status_code == 404 - assert del_attachment(user1_client, user2_company1_attachment).status_code == 404 + assert del_attachment(user1_client, user2_company1_attachment).status_code == 403 assert del_attachment(user1_client, user2_company2_attachment).status_code == 404 set_company_business_id_to_client(company1, user2_client) - assert del_attachment(user2_client, user1_company1_attachment).status_code == 404 + assert del_attachment(user2_client, user1_company1_attachment).status_code == 403 assert del_attachment(user2_client, user1_company2_attachment).status_code == 404 assert del_attachment(user2_client, user2_company2_attachment).status_code == 404 set_company_business_id_to_client(company2, user1_client) assert del_attachment(user1_client, user1_company1_attachment).status_code == 404 assert del_attachment(user1_client, user2_company1_attachment).status_code == 404 - assert del_attachment(user1_client, user2_company2_attachment).status_code == 404 + assert del_attachment(user1_client, user2_company2_attachment).status_code == 403 set_company_business_id_to_client(company2, user2_client) assert del_attachment(user2_client, user1_company1_attachment).status_code == 404 - assert del_attachment(user2_client, user1_company2_attachment).status_code == 404 + assert del_attachment(user2_client, user1_company2_attachment).status_code == 403 assert del_attachment(user2_client, user2_company1_attachment).status_code == 404 # Allowed deletions @@ -392,15 +471,15 @@ def del_attachment(client: Client, attachment: Attachment): ) ) - # The status code 400s are the ones that match both the user and the company: + # The status code 400s are the ones that match the user's company: set_company_business_id_to_client(company1, user1_client) assert del_attachment(user1_client, user1_company1_attachment).status_code == 400 assert del_attachment(user1_client, user1_company2_attachment).status_code == 404 - assert del_attachment(user1_client, user2_company1_attachment).status_code == 404 + assert del_attachment(user1_client, user2_company1_attachment).status_code == 400 assert del_attachment(user1_client, user2_company2_attachment).status_code == 404 set_company_business_id_to_client(company1, user2_client) - assert del_attachment(user2_client, user1_company1_attachment).status_code == 404 + assert del_attachment(user2_client, user1_company1_attachment).status_code == 400 assert del_attachment(user2_client, user1_company2_attachment).status_code == 404 assert del_attachment(user2_client, user2_company1_attachment).status_code == 400 assert del_attachment(user2_client, user2_company2_attachment).status_code == 404 @@ -409,11 +488,11 @@ def del_attachment(client: Client, attachment: Attachment): assert del_attachment(user1_client, user1_company1_attachment).status_code == 404 assert del_attachment(user1_client, user1_company2_attachment).status_code == 400 assert del_attachment(user1_client, user2_company1_attachment).status_code == 404 - assert del_attachment(user1_client, user2_company2_attachment).status_code == 404 + assert del_attachment(user1_client, user2_company2_attachment).status_code == 400 set_company_business_id_to_client(company2, user2_client) assert del_attachment(user2_client, user1_company1_attachment).status_code == 404 - assert del_attachment(user2_client, user1_company2_attachment).status_code == 404 + assert del_attachment(user2_client, user1_company2_attachment).status_code == 400 assert del_attachment(user2_client, user2_company1_attachment).status_code == 404 assert del_attachment(user2_client, user2_company2_attachment).status_code == 400