BTHLABS-61: Service layer refactoring

A journey to fix `ValidationError` in Pocket imports turned service
layer refactoring :D
This commit is contained in:
2025-10-12 18:37:32 +00:00
parent ac7a8dd90e
commit 8b86145519
45 changed files with 1023 additions and 337 deletions

View File

@@ -6,6 +6,7 @@ import hmac
import logging
import uuid
from django.core.exceptions import ValidationError
from django.db import models
import uuid6
@@ -15,6 +16,10 @@ from hotpocket_soa.dto.accounts import (
AccessTokenMetaUpdateIn,
AccessTokensQuery,
)
from hotpocket_soa.exceptions.backend import (
Invalid as InvalidError,
NotFound as NotFoundError,
)
LOGGER = logging.getLogger(__name__)
@@ -23,7 +28,10 @@ class AccessTokensService:
class AccessTokensServiceError(Exception):
pass
class AccessTokenNotFound(AccessTokensServiceError):
class Invalid(InvalidError, AccessTokensServiceError):
pass
class NotFound(NotFoundError, AccessTokensServiceError):
pass
def create(self,
@@ -32,20 +40,23 @@ class AccessTokensService:
origin: str,
meta: dict,
) -> AccessToken:
pk = uuid6.uuid7()
key = hmac.new(
settings.SECRET_KEY.encode('ascii'),
msg=pk.bytes,
digestmod=hashlib.sha256,
)
try:
pk = uuid6.uuid7()
key = hmac.new(
settings.SECRET_KEY.encode('ascii'),
msg=pk.bytes,
digestmod=hashlib.sha256,
)
return AccessToken.objects.create(
pk=pk,
account_uuid=account_uuid,
key=key.hexdigest(),
origin=origin,
meta=meta,
)
return AccessToken.objects.create(
pk=pk,
account_uuid=account_uuid,
key=key.hexdigest(),
origin=origin,
meta=meta,
)
except ValidationError as exception:
raise self.Invalid.from_django_validation_error(exception)
def get(self, *, pk: uuid.UUID) -> AccessToken:
try:
@@ -53,7 +64,7 @@ class AccessTokensService:
return query_set.get(pk=pk)
except AccessToken.DoesNotExist as exception:
raise self.AccessTokenNotFound(
raise self.NotFound(
f'Access Token not found: pk=`{pk}`',
) from exception
@@ -63,7 +74,7 @@ class AccessTokensService:
return query_set.get(key=key)
except AccessToken.DoesNotExist as exception:
raise self.AccessTokenNotFound(
raise self.NotFound(
f'Access Token not found: key=`{key}`',
) from exception
@@ -98,7 +109,7 @@ class AccessTokensService:
pk: uuid.UUID,
update: AccessTokenMetaUpdateIn,
) -> AccessToken:
access_token = AccessToken.active_objects.get(pk=pk)
access_token = self.get(pk=pk)
next_meta = {
**(access_token.meta or {}),

View File

@@ -5,6 +5,7 @@ import logging
import uuid
from hotpocket_backend.apps.accounts.models import Account
from hotpocket_soa.exceptions.backend import NotFound as NotFoundError
LOGGER = logging.getLogger(__name__)
@@ -13,7 +14,7 @@ class AccountsService:
class AccountsServiceError(Exception):
pass
class AccountNotFound(AccountsServiceError):
class NotFound(NotFoundError, AccountsServiceError):
pass
def get(self, *, pk: uuid.UUID) -> Account:
@@ -22,6 +23,6 @@ class AccountsService:
return query_set.get(pk=pk)
except Account.DoesNotExist as exception:
raise self.AccountNotFound(
raise self.NotFound(
f'Account not found: pk=`{pk}`',
) from exception

View File

@@ -5,11 +5,17 @@ import datetime
import logging
import uuid
from django.core.exceptions import ValidationError
from django.utils.timezone import now
import uuid6
from hotpocket_backend.apps.accounts.models import AuthKey
from hotpocket_backend.apps.core.conf import settings
from hotpocket_soa.exceptions.backend import (
InternalError,
Invalid as InvalidError,
NotFound as NotFoundError,
)
LOGGER = logging.getLogger(__name__)
@@ -18,22 +24,25 @@ class AuthKeysService:
class AuthKeysServiceError(Exception):
pass
class AuthKeyNotFound(AuthKeysServiceError):
class Invalid(InvalidError, AuthKeysServiceError):
pass
class AuthKeyExpired(AuthKeysServiceError):
class NotFound(NotFoundError, AuthKeysServiceError):
pass
class AuthKeyAccessDenied(AuthKeysServiceError):
class Expired(InternalError, AuthKeysServiceError):
pass
def create(self, *, account_uuid: uuid.UUID) -> AuthKey:
key = str(uuid6.uuid7())
try:
key = str(uuid6.uuid7())
return AuthKey.objects.create(
account_uuid=account_uuid,
key=key,
)
return AuthKey.objects.create(
account_uuid=account_uuid,
key=key,
)
except ValidationError as exception:
raise self.Invalid.from_django_validation_error(exception)
def get(self, *, pk: uuid.UUID) -> AuthKey:
try:
@@ -41,7 +50,7 @@ class AuthKeysService:
return query_set.get(pk=pk)
except AuthKey.DoesNotExist as exception:
raise self.AuthKeyNotFound(
raise self.NotFound(
f'Auth Key not found: pk=`{pk}`',
) from exception
@@ -56,17 +65,17 @@ class AuthKeysService:
if ttl > 0:
if result.created_at < now() - datetime.timedelta(seconds=ttl):
raise self.AuthKeyExpired(
raise self.Expired(
f'Auth Key expired: pk=`{key}`',
)
if result.consumed_at is not None:
raise self.AuthKeyExpired(
raise self.Expired(
f'Auth Key already consumed: pk=`{key}`',
)
return result
except AuthKey.DoesNotExist as exception:
raise self.AuthKeyNotFound(
raise self.NotFound(
f'Auth Key not found: key=`{key}`',
) from exception

View File

@@ -1,16 +1,39 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
import functools
import typing
from bthlabs_jsonrpc_core.exceptions import BaseJSONRPCError
from bthlabs_jsonrpc_django import (
DjangoExecutor,
DjangoJSONRPCSerializer,
JSONRPCView as BaseJSONRPCView,
)
from django.core.exceptions import ValidationError
import uuid6
from hotpocket_soa.exceptions.frontend import SOAError
class SOAJSONRPCError(BaseJSONRPCError):
ERROR_CODE = -32000
ERROR_MESSAGE = 'SOA Error'
def to_rpc(self) -> dict:
exception = typing.cast(SOAError, self.data)
code = (
exception.code
if exception.code is not None
else self.ERROR_CODE
)
return {
'code': code,
'message': exception.message or self.ERROR_MESSAGE,
'data': exception.data,
}
class JSONRPCSerializer(DjangoJSONRPCSerializer):
STRING_COERCIBLE_TYPES: typing.Any = (
@@ -18,30 +41,6 @@ class JSONRPCSerializer(DjangoJSONRPCSerializer):
uuid6.UUID,
)
def serialize_value(self, value: typing.Any) -> typing.Any:
if isinstance(value, ValidationError):
result: typing.Any = None
if hasattr(value, 'error_dict') is True:
result = {}
for field, errors in value.error_dict.items():
result[field] = [
error.code
for error
in errors
]
elif hasattr(value, 'error_list') is True:
result = [
error.code
for error in value.error_list
]
else:
result = value.code
return self.serialize_value(result)
return super().serialize_value(value)
class Executor(DjangoExecutor):
serializer = JSONRPCSerializer
@@ -49,3 +48,14 @@ class Executor(DjangoExecutor):
class JSONRPCView(BaseJSONRPCView):
executor = Executor
def wrap_soa_errors(func: typing.Callable) -> typing.Callable:
@functools.wraps(func)
def decorator(*args, **kwargs):
try:
return func(*args, **kwargs)
except SOAError as exception:
raise SOAJSONRPCError(exception)
return decorator

View File

@@ -5,6 +5,7 @@ import datetime
import logging
import uuid
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.timezone import now
@@ -15,6 +16,10 @@ from hotpocket_soa.dto.associations import (
AssociationsQuery,
AssociationUpdateIn,
)
from hotpocket_soa.exceptions.backend import (
Invalid as InvalidError,
NotFound as NotFoundError,
)
from .saves import SavesService
@@ -25,7 +30,10 @@ class AssociationsService:
class AssociationsServiceError(Exception):
pass
class AssociationNotFound(AssociationsServiceError):
class Invalid(InvalidError, AssociationsServiceError):
pass
class NotFound(NotFoundError, AssociationsServiceError):
pass
@property
@@ -46,30 +54,33 @@ class AssociationsService:
pk: uuid.UUID | None = None,
created_at: datetime.datetime | None = None,
) -> Association:
save = SavesService().get(pk=save_uuid)
try:
save = SavesService().get(pk=save_uuid)
defaults = dict(
account_uuid=account_uuid,
target=save,
)
defaults = dict(
account_uuid=account_uuid,
target=save,
)
if pk is not None:
defaults['id'] = pk
if pk is not None:
defaults['id'] = pk
result, created = Association.objects.get_or_create(
account_uuid=account_uuid,
deleted_at__isnull=True,
target=save,
archived_at__isnull=True,
defaults=defaults,
)
result, created = Association.objects.get_or_create(
account_uuid=account_uuid,
deleted_at__isnull=True,
target=save,
archived_at__isnull=True,
defaults=defaults,
)
if created is True:
if created_at is not None:
result.created_at = created_at
result.save()
if created is True:
if created_at is not None:
result.created_at = created_at
result.save()
return result
return result
except ValidationError as exception:
raise self.Invalid.from_django_validation_error(exception)
def get(self,
*,
@@ -87,7 +98,7 @@ class AssociationsService:
return query_set.get(pk=pk)
except Association.DoesNotExist as exception:
raise self.AssociationNotFound(
raise self.NotFound(
f'Association not found: pk=`{pk}`',
) from exception
@@ -112,21 +123,24 @@ class AssociationsService:
pk: uuid.UUID,
update: AssociationUpdateIn,
) -> Association:
association = self.get(pk=pk)
association.target_title = update.target_title
association.target_description = update.target_description
try:
association = self.get(pk=pk)
association.target_title = update.target_title
association.target_description = update.target_description
next_target_meta = {
**(association.target_meta or {}),
}
next_target_meta = {
**(association.target_meta or {}),
}
next_target_meta.pop('title', None)
next_target_meta.pop('description', None)
association.target_meta = next_target_meta
next_target_meta.pop('title', None)
next_target_meta.pop('description', None)
association.target_meta = next_target_meta
association.save()
association.save()
return association
return association
except ValidationError as exception:
raise self.Invalid.from_django_validation_error(exception)
def archive(self, *, pk: uuid.UUID) -> bool:
association = self.get(pk=pk)

View File

@@ -5,19 +5,27 @@ import hashlib
import typing
import uuid
from django.core.exceptions import ValidationError
from django.db import models
from hotpocket_backend.apps.core.services import get_adapter
from hotpocket_backend.apps.saves.models import Save
from hotpocket_backend.apps.saves.types import PSaveAdapter
from hotpocket_soa.dto.saves import ImportedSaveIn, SaveIn, SavesQuery
from hotpocket_soa.exceptions.backend import (
Invalid as InvalidError,
NotFound as NotFoundError,
)
class SavesService:
class SavesServiceError(Exception):
pass
class SaveNotFound(SavesServiceError):
class Invalid(InvalidError, SavesServiceError):
pass
class NotFound(NotFoundError, SavesServiceError):
pass
@property
@@ -36,35 +44,38 @@ class SavesService:
account_uuid: uuid.UUID,
save: SaveIn | ImportedSaveIn,
) -> Save:
key = hashlib.sha256(save.url.encode('utf-8')).hexdigest()
try:
key = hashlib.sha256(save.url.encode('utf-8')).hexdigest()
defaults = dict(
account_uuid=account_uuid,
key=key,
url=save.url,
)
defaults = dict(
account_uuid=account_uuid,
key=key,
url=save.url,
)
save_object, created = Save.objects.get_or_create(
key=key,
deleted_at__isnull=True,
defaults=defaults,
)
save_object, created = Save.objects.get_or_create(
key=key,
deleted_at__isnull=True,
defaults=defaults,
)
if created is True:
save_object.is_netloc_banned = save.is_netloc_banned
if created is True:
save_object.is_netloc_banned = save.is_netloc_banned
if isinstance(save, ImportedSaveIn) is True:
save_object.title = save.title # type: ignore[union-attr]
if isinstance(save, ImportedSaveIn) is True:
save_object.title = save.title # type: ignore[union-attr]
save_object.save()
save_object.save()
return save_object
return save_object
except ValidationError as exception:
raise self.Invalid.from_django_validation_error(exception)
def get(self, *, pk: uuid.UUID) -> Save:
try:
return Save.active_objects.get(pk=pk)
except Save.DoesNotExist as exception:
raise self.SaveNotFound(
raise self.NotFound(
f'Save not found: pk=`{pk}`',
) from exception

View File

@@ -7,6 +7,7 @@ from bthlabs_jsonrpc_core import register_method
from django import db
from django.http import HttpRequest
from hotpocket_backend.apps.core.rpc import wrap_soa_errors
from hotpocket_soa.services import (
AccessTokensService,
AccountsService,
@@ -17,6 +18,7 @@ LOGGER = logging.getLogger(__name__)
@register_method('accounts.access_tokens.create', namespace='accounts')
@wrap_soa_errors
def create(request: HttpRequest,
auth_key: str,
meta: dict,
@@ -27,7 +29,7 @@ def create(request: HttpRequest,
account_uuid=None,
key=auth_key,
)
except AuthKeysService.AuthKeyNotFound as exception:
except AuthKeysService.NotFound as exception:
LOGGER.error(
'Unable to issue access token: %s',
exception,
@@ -37,7 +39,7 @@ def create(request: HttpRequest,
try:
account = AccountsService().get(pk=auth_key_object.account_uuid)
except AccountsService.AccountNotFound as exception:
except AccountsService.NotFound as exception:
LOGGER.error(
'Unable to issue access token: %s',
exception,

View File

@@ -44,7 +44,7 @@ def check_access_token(request: HttpRequest,
access_token=access_token_object,
update=meta_update,
)
except AccessTokensService.AccessTokenNotFound as exception:
except AccessTokensService.NotFound as exception:
LOGGER.error(
'Access Token not found: account_uuid=`%s` key=`%s`',
request.user.pk,
@@ -52,7 +52,7 @@ def check_access_token(request: HttpRequest,
exc_info=exception,
)
result = False
except AccessTokensService.AccessTokenAccessDenied as exception:
except AccessTokensService.AccessDenied as exception:
LOGGER.error(
'Access Token access denied: account_uuid=`%s` key=`%s`',
request.user.pk,

View File

@@ -4,11 +4,13 @@ from __future__ import annotations
from bthlabs_jsonrpc_core import register_method
from django.http import HttpRequest
from hotpocket_backend.apps.core.rpc import wrap_soa_errors
from hotpocket_backend.apps.ui.services.workflows import CreateSaveWorkflow
from hotpocket_soa.dto.associations import AssociationOut
@register_method(method='saves.create')
@wrap_soa_errors
def create(request: HttpRequest, url: str) -> AssociationOut:
association = CreateSaveWorkflow().run_rpc(
request=request,

View File

@@ -27,7 +27,7 @@ class UIAccessTokensService:
account_uuid=account_uuid,
pk=pk,
)
except AccessTokensService.AccessTokenNotFound as exception:
except AccessTokensService.NotFound as exception:
LOGGER.error(
'Access Token not found: account_uuid=`%s` pk=`%s`',
account_uuid,
@@ -35,7 +35,7 @@ class UIAccessTokensService:
exc_info=exception,
)
raise Http404()
except AccessTokensService.AccessTokenAccessDenied as exception:
except AccessTokensService.AccessDenied as exception:
LOGGER.error(
'Access Token access denied: account_uuid=`%s` pk=`%s`',
account_uuid,

View File

@@ -34,7 +34,7 @@ class UIAssociationsService:
with_target=True,
allow_archived=allow_archived,
)
except AssociationsService.AssociationNotFound as exception:
except AssociationsService.NotFound as exception:
LOGGER.error(
'Association not found: account_uuid=`%s` pk=`%s`',
account_uuid,
@@ -42,7 +42,7 @@ class UIAssociationsService:
exc_info=exception,
)
raise Http404()
except AssociationsService.AssociationAccessDenied as exception:
except AssociationsService.AccessDenied as exception:
LOGGER.error(
'Association access denied: account_uuid=`%s` pk=`%s`',
account_uuid,

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
import csv
import datetime
import logging
import os
import uuid
@@ -13,16 +14,28 @@ from django.utils.timezone import get_current_timezone
from hotpocket_backend.apps.ui.services.workflows import ImportSaveWorkflow
from hotpocket_backend.apps.ui.tasks import import_from_pocket
from hotpocket_common.uuid import uuid7_from_timestamp
from hotpocket_soa.services import SavesService
LOGGER = logging.getLogger(__name__)
class UIImportsService:
def import_from_pocket(self,
*,
job: str,
account_uuid: uuid.UUID,
csv_path: str,
) -> list[tuple[uuid.UUID, uuid.UUID]]:
result = []
LOGGER.info(
'Starting import job: job=`%s` account_uuid=`%s`',
job,
account_uuid,
extra={
'job': job,
},
)
result = []
with db.transaction.atomic():
try:
with open(csv_path, 'r', encoding='utf-8') as csv_file:
@@ -34,22 +47,35 @@ class UIImportsService:
current_timezone = get_current_timezone()
is_header = False
for row in csv_reader:
for row_number, row in enumerate(csv_reader, start=1):
if is_header is False:
is_header = True
continue
timestamp = int(row['time_added'])
save, association = ImportSaveWorkflow().run(
account_uuid=account_uuid,
url=row['url'],
title=row['title'],
pk=uuid7_from_timestamp(timestamp),
created_at=datetime.datetime.fromtimestamp(
timestamp, tz=current_timezone,
),
)
try:
save, association = ImportSaveWorkflow().run(
account_uuid=account_uuid,
url=row['url'],
title=row['title'],
pk=uuid7_from_timestamp(timestamp),
created_at=datetime.datetime.fromtimestamp(
timestamp, tz=current_timezone,
),
)
except SavesService.Invalid as exception:
LOGGER.error(
'Import error: row_number=`%d` url=`%s` exception=`%s`',
row_number,
row['url'],
exception,
exc_info=exception,
extra={
'job': job,
},
)
continue
result.append((save.pk, association.pk))
finally:
@@ -64,6 +90,7 @@ class UIImportsService:
) -> AsyncResult:
return import_from_pocket.apply_async(
kwargs={
'job': str(uuid.uuid4()),
'account_uuid': account_uuid,
'csv_path': csv_path,
},

View File

@@ -19,7 +19,7 @@ class UISavesService:
def get_or_404(self, *, pk: uuid.UUID) -> SaveOut:
try:
return SavesService().get(pk=pk)
except SavesService.SaveNotFound as exception:
except SavesService.NotFound as exception:
LOGGER.error(
'Save not found: pk=`%s`', pk, exc_info=exception,
)

View File

@@ -1,9 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
from bthlabs_jsonrpc_core import JSONRPCInternalError
from django.contrib import messages
from django.core.exceptions import ValidationError
import django.db
from django.http import HttpRequest, HttpResponse
from django.shortcuts import redirect
@@ -14,7 +12,6 @@ from hotpocket_backend.apps.accounts.types import PAccount
from hotpocket_soa.dto.associations import AssociationOut
from hotpocket_soa.dto.celery import AsyncResultOut
from hotpocket_soa.dto.saves import SaveIn, SaveOut
from hotpocket_soa.services import SavesService
from .base import SaveWorkflow
@@ -73,14 +70,8 @@ class CreateSaveWorkflow(SaveWorkflow):
account: PAccount,
url: str,
) -> AssociationOut:
try:
save, association, processing_result = self.create_associate_and_process(
account, url,
)
save, association, processing_result = self.create_associate_and_process(
account, url,
)
return association
except SavesService.SavesServiceError as exception:
if isinstance(exception.__cause__, ValidationError) is True:
raise JSONRPCInternalError(data=exception.__cause__)
raise
return association

View File

@@ -11,6 +11,7 @@ LOGGER = logging.getLogger(__name__)
@shared_task
def import_from_pocket(*,
job: str,
account_uuid: uuid.UUID,
csv_path: str,
) -> list[tuple[uuid.UUID, uuid.UUID]]:
@@ -18,6 +19,7 @@ def import_from_pocket(*,
try:
return UIImportsService().import_from_pocket(
job=job,
account_uuid=account_uuid,
csv_path=csv_path,
)

View File

@@ -76,6 +76,18 @@ def starred_association_out(starred_association):
)
@pytest.fixture
def deleted_save_association(association_factory, deleted_save):
return association_factory(target=deleted_save)
@pytest.fixture
def deleted_save_association_out(deleted_save_association):
return AssociationWithTargetOut.model_validate(
deleted_save_association, from_attributes=True,
)
@pytest.fixture
def other_account_association(association_factory, other_account):
return association_factory(account=other_account)
@@ -124,6 +136,18 @@ def other_account_starred_association_out(other_account_starred_association):
)
@pytest.fixture
def other_account_deleted_save_association(association_factory, other_account, deleted_save):
return association_factory(account=other_account, target=deleted_save)
@pytest.fixture
def other_account_deleted_save_association_out(other_account_deleted_save_association):
return AssociationWithTargetOut.model_validate(
other_account_deleted_save_association, from_attributes=True,
)
@pytest.fixture
def browsable_associations(association,
deleted_association,

View File

@@ -64,11 +64,25 @@ def pocket_import_banned_netloc_save_spec():
})
@pytest.fixture
def pocket_import_invalid_url_spec():
return PocketImportSaveSpec.model_validate({
'title': "This isn't right",
'url': 'thisisntright',
'time_added': datetime.datetime(
2021, 1, 18, 8, 0, 0, 0, tzinfo=datetime.UTC,
),
'tags': '',
'status': 'unread',
})
@pytest.fixture
def pocket_csv_content(pocket_import_created_save_spec,
pocket_import_reused_save_spec,
pocket_import_other_account_save_spec,
pocket_import_banned_netloc_save_spec,
pocket_import_invalid_url_spec,
):
with io.StringIO() as csv_f:
field_names = [
@@ -82,6 +96,7 @@ def pocket_csv_content(pocket_import_created_save_spec,
pocket_import_reused_save_spec.dict(),
pocket_import_other_account_save_spec.dict(),
pocket_import_banned_netloc_save_spec.dict(),
pocket_import_invalid_url_spec.dict(),
])
csv_f.seek(0)

View File

@@ -29,6 +29,20 @@ class AssociationsTestingService:
if reference is not None:
assert association.updated_at > reference.updated_at
def assert_deleted(self, *, pk: uuid.UUID, reference: typing.Any = None):
association = Association.objects.get(pk=pk)
assert association.deleted_at is not None
if reference is not None:
assert association.updated_at > reference.updated_at
def assert_not_deleted(self, *, pk: uuid.UUID, reference: typing.Any = None):
association = Association.objects.get(pk=pk)
assert association.deleted_at is None
if reference is not None:
assert association.updated_at == reference.updated_at
def assert_starred(self, *, pk: uuid.UUID, reference: typing.Any = None):
association = Association.objects.get(pk=pk)
assert association.starred_at is not None

View File

@@ -45,10 +45,12 @@ def test_ok(account,
other_account_save_out,
pocket_import_other_account_save_spec: PocketImportSaveSpec,
pocket_import_banned_netloc_save_spec: PocketImportSaveSpec,
pocket_import_invalid_url_spec: PocketImportSaveSpec,
mock_saves_process_save_task_apply_async: mock.Mock,
):
# When
result = tasks_module.import_from_pocket(
job='test',
account_uuid=account.pk,
csv_path=str(pocket_csv_file_path),
)

View File

@@ -100,6 +100,54 @@ def test_invalid_all_empty(authenticated_client: Client,
assert 'canhazconfirm' in result.context['form'].errors
@pytest.mark.django_db
def test_archived(authenticated_client: Client,
archived_association_out,
):
# When
result = authenticated_client.post(
reverse('ui.associations.archive', args=(archived_association_out.pk,)),
data={
'canhazconfirm': 'hai',
},
)
# Then
assert result.status_code == http.HTTPStatus.NOT_FOUND
@pytest.mark.django_db
def test_deleted(authenticated_client: Client,
deleted_association_out,
):
# When
result = authenticated_client.post(
reverse('ui.associations.archive', args=(deleted_association_out.pk,)),
data={
'canhazconfirm': 'hai',
},
)
# Then
assert result.status_code == http.HTTPStatus.NOT_FOUND
@pytest.mark.django_db
def test_not_found(authenticated_client: Client,
null_uuid,
):
# When
result = authenticated_client.post(
reverse('ui.associations.archive', args=(null_uuid,)),
data={
'canhazconfirm': 'hai',
},
)
# Then
assert result.status_code == http.HTTPStatus.NOT_FOUND
@pytest.mark.django_db
def test_other_account_association(authenticated_client: Client,
other_account_association_out,

View File

@@ -9,7 +9,7 @@ from django.urls import reverse
import pytest
from pytest_django import asserts
from hotpocket_backend.apps.saves.models import Association
from hotpocket_backend_testing.services.saves import AssociationsTestingService
from hotpocket_common.constants import AssociationsSearchMode
@@ -35,9 +35,10 @@ def test_ok(authenticated_client: Client,
fetch_redirect_response=False,
)
association_object = Association.objects.get(pk=association_out.pk)
assert association_object.updated_at > association_out.updated_at
assert association_object.deleted_at is not None
AssociationsTestingService().assert_deleted(
pk=association_out.pk,
reference=association_out,
)
@pytest.mark.django_db
@@ -65,6 +66,34 @@ def test_ok_htmx(authenticated_client: Client,
assert result.json() == expected_payload
@pytest.mark.django_db
def test_ok_archived(authenticated_client: Client,
archived_association_out,
):
# When
result = authenticated_client.post(
reverse('ui.associations.delete', args=(archived_association_out.pk,)),
data={
'canhazconfirm': 'hai',
},
)
# Then
asserts.assertRedirects(
result,
reverse(
'ui.associations.browse',
query=[('mode', AssociationsSearchMode.ARCHIVED.value)],
),
fetch_redirect_response=False,
)
AssociationsTestingService().assert_deleted(
pk=archived_association_out.pk,
reference=archived_association_out,
)
@pytest.mark.django_db
def test_invalid_all_missing(authenticated_client: Client,
association_out,
@@ -78,13 +107,13 @@ def test_invalid_all_missing(authenticated_client: Client,
# Then
assert result.status_code == http.HTTPStatus.OK
association_object = Association.objects.get(pk=association_out.pk)
assert association_object.updated_at == association_out.updated_at
assert association_object.deleted_at is None
assert 'canhazconfirm' in result.context['form'].errors
AssociationsTestingService().assert_not_deleted(
pk=association_out.pk,
reference=association_out,
)
@pytest.mark.django_db
def test_invalid_all_empty(authenticated_client: Client,
@@ -100,13 +129,45 @@ def test_invalid_all_empty(authenticated_client: Client,
# Then
assert result.status_code == http.HTTPStatus.OK
association_object = Association.objects.get(pk=association_out.pk)
assert association_object.updated_at == association_out.updated_at
assert association_object.deleted_at is None
assert 'canhazconfirm' in result.context['form'].errors
AssociationsTestingService().assert_not_deleted(
pk=association_out.pk,
reference=association_out,
)
@pytest.mark.django_db
def test_deleted(authenticated_client: Client,
deleted_association_out,
):
# When
result = authenticated_client.post(
reverse('ui.associations.delete', args=(deleted_association_out.pk,)),
data={
'canhazconfirm': 'hai',
},
)
# Then
assert result.status_code == http.HTTPStatus.NOT_FOUND
@pytest.mark.django_db
def test_not_found(authenticated_client: Client,
null_uuid,
):
# When
result = authenticated_client.post(
reverse('ui.associations.delete', args=(null_uuid,)),
data={
'canhazconfirm': 'hai',
},
)
# Then
assert result.status_code == http.HTTPStatus.NOT_FOUND
@pytest.mark.django_db
def test_other_account_association(authenticated_client: Client,

View File

@@ -47,10 +47,10 @@ def test_ok(authenticated_client: Client,
@pytest.mark.django_db
def test_invalid_all_empty(authenticated_client: Client,
association_out,
payload,
):
def test_all_empty(authenticated_client: Client,
association_out,
payload,
):
# Given
effective_payload = {
key: ''
@@ -79,9 +79,9 @@ def test_invalid_all_empty(authenticated_client: Client,
@pytest.mark.django_db
def test_invalid_all_missing(authenticated_client: Client,
association_out,
):
def test_all_missing(authenticated_client: Client,
association_out,
):
# Given
effective_payload = {}
@@ -105,6 +105,45 @@ def test_invalid_all_missing(authenticated_client: Client,
)
@pytest.mark.django_db
def test_archived(authenticated_client: Client,
archived_association_out,
):
# When
result = authenticated_client.post(
reverse('ui.associations.edit', args=(archived_association_out.pk,)),
)
# Then
assert result.status_code == http.HTTPStatus.NOT_FOUND
@pytest.mark.django_db
def test_deleted(authenticated_client: Client,
deleted_association_out,
):
# When
result = authenticated_client.post(
reverse('ui.associations.edit', args=(deleted_association_out.pk,)),
)
# Then
assert result.status_code == http.HTTPStatus.NOT_FOUND
@pytest.mark.django_db
def test_not_found(authenticated_client: Client,
null_uuid,
):
# When
result = authenticated_client.post(
reverse('ui.associations.edit', args=(null_uuid,)),
)
# Then
assert result.status_code == http.HTTPStatus.NOT_FOUND
@pytest.mark.django_db
def test_other_account_association(authenticated_client: Client,
other_account_association_out,

View File

@@ -128,6 +128,54 @@ def test_invalid_all_empty(authenticated_client: Client,
assert 'canhazconfirm' in result.context['form'].errors
@pytest.mark.django_db
def test_archived(authenticated_client: Client,
archived_association_out,
):
# When
result = authenticated_client.post(
reverse('ui.associations.refresh', args=(archived_association_out.pk,)),
data={
'canhazconfirm': 'hai',
},
)
# Then
assert result.status_code == http.HTTPStatus.NOT_FOUND
@pytest.mark.django_db
def test_deleted(authenticated_client: Client,
deleted_association_out,
):
# When
result = authenticated_client.post(
reverse('ui.associations.refresh', args=(deleted_association_out.pk,)),
data={
'canhazconfirm': 'hai',
},
)
# Then
assert result.status_code == http.HTTPStatus.NOT_FOUND
@pytest.mark.django_db
def test_not_found(authenticated_client: Client,
null_uuid,
):
# When
result = authenticated_client.post(
reverse('ui.associations.refresh', args=(null_uuid,)),
data={
'canhazconfirm': 'hai',
},
)
# Then
assert result.status_code == http.HTTPStatus.NOT_FOUND
@pytest.mark.django_db
def test_other_account_association(authenticated_client: Client,
other_account_association_out,

View File

@@ -54,6 +54,45 @@ def test_ok_htmx(authenticated_client: Client,
assert result.context['association'].target.pk == association_out.target.pk
@pytest.mark.django_db
def test_archived(authenticated_client: Client,
archived_association_out,
):
# When
result = authenticated_client.get(
reverse('ui.associations.star', args=(archived_association_out.pk,)),
)
# Then
assert result.status_code == http.HTTPStatus.NOT_FOUND
@pytest.mark.django_db
def test_deleted(authenticated_client: Client,
deleted_association_out,
):
# When
result = authenticated_client.get(
reverse('ui.associations.star', args=(deleted_association_out.pk,)),
)
# Then
assert result.status_code == http.HTTPStatus.NOT_FOUND
@pytest.mark.django_db
def test_not_found(authenticated_client: Client,
null_uuid,
):
# When
result = authenticated_client.get(
reverse('ui.associations.star', args=(null_uuid,)),
)
# Then
assert result.status_code == http.HTTPStatus.NOT_FOUND
@pytest.mark.django_db
def test_other_account_association(authenticated_client: Client,
other_account_association_out,

View File

@@ -54,6 +54,45 @@ def test_ok_htmx(authenticated_client: Client,
assert result.context['association'].target.pk == starred_association_out.target.pk
@pytest.mark.django_db
def test_archived(authenticated_client: Client,
archived_association_out,
):
# When
result = authenticated_client.get(
reverse('ui.associations.unstar', args=(archived_association_out.pk,)),
)
# Then
assert result.status_code == http.HTTPStatus.NOT_FOUND
@pytest.mark.django_db
def test_deleted(authenticated_client: Client,
deleted_association_out,
):
# When
result = authenticated_client.get(
reverse('ui.associations.unstar', args=(deleted_association_out.pk,)),
)
# Then
assert result.status_code == http.HTTPStatus.NOT_FOUND
@pytest.mark.django_db
def test_not_found(authenticated_client: Client,
null_uuid,
):
# When
result = authenticated_client.get(
reverse('ui.associations.unstar', args=(null_uuid,)),
)
# Then
assert result.status_code == http.HTTPStatus.NOT_FOUND
@pytest.mark.django_db
def test_other_account_association(authenticated_client: Client,
other_account_starred_association_out,

View File

@@ -66,6 +66,19 @@ def test_authenticated_deleted(authenticated_client: Client,
assert result.status_code == http.HTTPStatus.NOT_FOUND
@pytest.mark.django_db
def test_authenticated_deleted_save(authenticated_client: Client,
deleted_save_association_out,
):
# When
result = authenticated_client.get(
reverse('ui.associations.view', args=(deleted_save_association_out.pk,)),
)
# Then
assert result.status_code == http.HTTPStatus.NOT_FOUND
@pytest.mark.django_db
def test_authenticated_not_found(authenticated_client: Client,
null_uuid,
@@ -169,6 +182,23 @@ def test_authenticated_share_deleted(authenticated_client: Client,
assert result.status_code == http.HTTPStatus.NOT_FOUND
@pytest.mark.django_db
def test_authenticated_share_deleted_save(authenticated_client: Client,
other_account_deleted_save_association_out,
):
# When
result = authenticated_client.get(
reverse(
'ui.associations.view',
args=(other_account_deleted_save_association_out.pk,),
query=[('share', 'true')],
),
)
# Then
assert result.status_code == http.HTTPStatus.NOT_FOUND
@pytest.mark.django_db
def test_authenticated_share_not_found(authenticated_client: Client,
null_uuid,
@@ -240,6 +270,23 @@ def test_anonymous_share_deleted(client: Client,
assert result.status_code == http.HTTPStatus.NOT_FOUND
@pytest.mark.django_db
def test_anonymous_share_deleted_save(client: Client,
deleted_save_association_out,
):
# When
result = client.get(
reverse(
'ui.associations.view',
args=(deleted_save_association_out.pk,),
query=[('share', 'true')],
),
)
# Then
assert result.status_code == http.HTTPStatus.NOT_FOUND
@pytest.mark.django_db
def test_anonymous_share_not_found(client: Client,
null_uuid,

View File

@@ -76,6 +76,7 @@ def test_ok(override_settings_upload_path,
mock_ui_import_from_pocket_task_apply_async.assert_called_once_with(
kwargs={
'job': mock.ANY,
'account_uuid': account.pk,
'csv_path': str(uploaded_file_path),
},

View File

@@ -77,10 +77,11 @@ def test_auth_key_not_found(null_uuid,
call_result = result.json()
assert 'error' in call_result
assert call_result['error']['data'].startswith(
assert call_result['error']['code'] == -32001
assert call_result['error']['message'].startswith(
'Auth Key not found',
)
assert call_auth_key in call_result['error']['data']
assert call_auth_key in call_result['error']['message']
@pytest.mark.django_db
@@ -108,10 +109,11 @@ def test_deleted_auth_key(deleted_auth_key_out,
call_result = result.json()
assert 'error' in call_result
assert call_result['error']['data'].startswith(
assert call_result['error']['code'] == -32001
assert call_result['error']['message'].startswith(
'Auth Key not found',
)
assert call_auth_key in call_result['error']['data']
assert call_auth_key in call_result['error']['message']
@pytest.mark.django_db
@@ -139,10 +141,11 @@ def test_expired_auth_key(expired_auth_key_out,
call_result = result.json()
assert 'error' in call_result
assert call_result['error']['data'].startswith(
assert call_result['error']['code'] == -32000
assert call_result['error']['message'].startswith(
'Auth Key expired',
)
assert call_auth_key in call_result['error']['data']
assert call_auth_key in call_result['error']['message']
@pytest.mark.django_db
@@ -170,10 +173,11 @@ def test_consumed_auth_key(consumed_auth_key,
call_result = result.json()
assert 'error' in call_result
assert call_result['error']['data'].startswith(
assert call_result['error']['code'] == -32000
assert call_result['error']['message'].startswith(
'Auth Key already consumed',
)
assert call_auth_key in call_result['error']['data']
assert call_auth_key in call_result['error']['message']
@pytest.mark.django_db
@@ -201,4 +205,5 @@ def test_inactive_account(inactive_account_auth_key,
call_result = result.json()
assert 'error' in call_result
assert str(inactive_account.pk) in call_result['error']['data']
assert call_result['error']['code'] == -32001
assert str(inactive_account.pk) in call_result['error']['message']