Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ pre_commit:

test:
@echo "🧪 Executando os testes..."
poetry run python src/manage.py test
poetry run python src/manage.py test src
@echo "✅ Testes concluídos."

worker:
Expand Down
1 change: 0 additions & 1 deletion src/accounts/tests.py

This file was deleted.

File renamed without changes.
38 changes: 38 additions & 0 deletions src/accounts/tests/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from typing import Unpack

from django.contrib.auth.hashers import make_password
from model_bakery import baker

from accounts.models import (
HumanAgent,
User,
)
from accounts.typing import (
AuthModel,
HumanAgentDTO,
UserDTO,
)
from config.faker import fake


def sample_user(
model: type[AuthModel] = User,
prepare: bool = False,
**kwargs: Unpack[UserDTO],
) -> AuthModel | list[AuthModel]:
kwargs.setdefault("name", fake.name)
kwargs.setdefault("email", fake.ascii_email)

kwargs["password"] = make_password(kwargs.get("password", "password"))

if prepare:
return baker.prepare(model, **kwargs)

return baker.make(model, **kwargs)


def sample_human_agent(
prepare=False,
**kwargs: Unpack[HumanAgentDTO],
) -> HumanAgent | list[HumanAgent]:
return sample_user(HumanAgent, prepare, **kwargs)
63 changes: 63 additions & 0 deletions src/accounts/tests/test_contact_view_set.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from assets.helpers import configure_api_client

from rest_framework.test import APITestCase
from django.urls import reverse
from rest_framework import status
from rest_framework.response import Response

from accounts.tests.helpers import sample_human_agent
from accounts.models import Contact
from channels.tests.helpers import sample_channel
from config.faker import fake


class TestContactViewSet(APITestCase):
def retrieve(self, contact_id: int = None, **kwargs) -> Response:
if contact_id is None:
contact = Contact.objects.create(
name=fake.name(),
channel=sample_channel(),
channel_identifier=str(fake.random_int()),
)
contact_id = contact.id
url = reverse("contact-detail", kwargs={"pk": contact_id})
return self.client.get(url, format="json", **kwargs)

def list(self, filters=None, **kwargs) -> Response:
Contact.objects.create(
name=fake.name(),
channel=sample_channel(),
channel_identifier=str(fake.random_int()),
)
Contact.objects.create(
name=fake.name(),
channel=sample_channel(),
channel_identifier=str(fake.random_int()),
)
url = reverse("contact-list")
return self.client.get(url, filters)


class TestContactViewSetUnauthenticated(TestContactViewSet):
def test_retrieve_unauthenticated(self):
response = self.retrieve()
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

def test_list_unauthenticated(self):
response = self.list()
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


class TestContactViewSetAuthenticated(TestContactViewSet):
def setUp(self) -> None:
self.user = sample_human_agent()
configure_api_client(self.client, user=self.user)

def test_retrieve_authenticated(self):
response = self.retrieve()
self.assertEqual(response.status_code, status.HTTP_200_OK)

def test_list_authenticated(self):
response = self.list()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data["results"]), 2)
127 changes: 127 additions & 0 deletions src/accounts/tests/test_human_agent_view_set.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
from assets.helpers import configure_api_client

from rest_framework.test import APITestCase
from django.urls import reverse
from rest_framework import status
from rest_framework.response import Response

from accounts.tests.helpers import sample_human_agent
from accounts.models import HumanAgent
from config.faker import fake


class TestHumanAgentViewSet(APITestCase):
def create(self, payload: dict = None, **args) -> tuple[Response, dict]:
url = reverse("human-agent-list")

if payload is None:
payload = {
"name": fake.name(),
"email": fake.ascii_email(),
}

response = self.client.post(url, payload, format="json", **args)
return response, payload

def retrieve(self, agent_id: int = None, **kwargs) -> Response:
if agent_id is None:
agent = sample_human_agent()
agent_id = agent.id
url = reverse("human-agent-detail", kwargs={"pk": agent_id})
return self.client.get(url, format="json", **kwargs)

def list(self, **kwargs) -> Response:
sample_human_agent(_quantity=3)
url = reverse("human-agent-list")
return self.client.get(url, **kwargs)

def update(
self, agent: HumanAgent = None, payload: dict = None, **kwargs
) -> tuple[Response, HumanAgent, dict]:
if agent is None:
agent = sample_human_agent()
url = reverse("human-agent-detail", kwargs={"pk": agent.id})

if payload is None:
payload = {
"name": fake.name(),
"email": fake.ascii_email(),
}

response = self.client.put(url, payload, format="json", **kwargs)
agent.refresh_from_db()
return response, agent, payload

def delete(self, agent: HumanAgent = None, **kwargs) -> tuple[Response, HumanAgent]:
if agent is None:
agent = sample_human_agent()
url = reverse("human-agent-detail", kwargs={"pk": agent.id})
response = self.client.delete(url, format="json", **kwargs)
agent.refresh_from_db()
return response, agent


class TestHumanAgentViewSetUnauthenticated(TestHumanAgentViewSet):
def test_create_unauthenticated(self):
response, _ = self.create()
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

def test_retrieve_unauthenticated(self):
response = self.retrieve()
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

def test_list_unauthenticated(self):
response = self.list()
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

def test_update_unauthenticated(self):
response, _, _ = self.update()
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

def test_delete_unauthenticated(self):
response, _ = self.delete()
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)


class TestHumanAgentViewSetAuthenticated(TestHumanAgentViewSet):
def setUp(self) -> None:
self.user = sample_human_agent()
configure_api_client(self.client, user=self.user)

def test_create_authenticated(self):
response, payload = self.create()
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertIn("id", response.data)
self.assertEqual(response.data["name"], payload["name"])
self.assertEqual(response.data["email"], payload["email"])

def test_retrieve_authenticated(self):
response = self.retrieve()
self.assertEqual(response.status_code, status.HTTP_200_OK)

def test_list_authenticated(self):
response = self.list()
self.assertEqual(response.status_code, status.HTTP_200_OK)
# 3 created + 1 authenticated user in setUp
self.assertEqual(len(response.data["results"]), 4)

def test_update_self_authenticated(self):
response, agent, payload = self.update(agent=self.user)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(agent.name, payload["name"])
self.assertEqual(agent.email, payload["email"])

def test_update_other_returns_404(self):
other = sample_human_agent()
response, _, _ = self.update(agent=other)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)

def test_delete_self_authenticated(self):
response, agent = self.delete(agent=self.user)
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
self.assertTrue(agent.is_removed)

def test_delete_other_returns_404(self):
other = sample_human_agent()
response, _ = self.delete(agent=other)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
33 changes: 33 additions & 0 deletions src/accounts/tests/test_register_view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from rest_framework.test import APITestCase
from django.urls import reverse
from rest_framework import status

from accounts.models import HumanAgent
from config.faker import fake


class TestRegisterHumanAgentView(APITestCase):
def url(self) -> str:
return reverse("register-human-agent")

def payload(self, **overrides) -> dict:
data = {
"name": fake.name(),
"email": fake.ascii_email(),
"password": "password-strong",
}
data.update(overrides)
return data

def test_register_success(self):
response = self.client.post(self.url(), self.payload(), format="json")
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertIn("id", response.data)
self.assertIn("email", response.data)
self.assertTrue(HumanAgent.objects.filter(id=response.data["id"]).exists())

def test_register_missing_password(self):
data = self.payload()
data.pop("password")
response = self.client.post(self.url(), data, format="json")
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
17 changes: 17 additions & 0 deletions src/accounts/typing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from typing import TypedDict, TypeVar
from django.contrib.auth.models import User

AuthModel = TypeVar("AuthModel", bound=User)


class UserDTO(TypedDict, total=False):
email: str
name: str
is_active: bool
is_superuser: bool
is_staff: bool
password: str


class HumanAgentDTO(UserDTO, total=False):
pass
2 changes: 0 additions & 2 deletions src/accounts/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@

from .views import ContactViewSet, HumanAgentViewSet, RegisterHumanAgentView

app_name = "accounts"

router = DefaultRouter()

router.register("human-agents", HumanAgentViewSet, basename="human-agent")
Expand Down
4 changes: 2 additions & 2 deletions src/accounts/views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

from rest_framework.filters import OrderingFilter
from rest_framework import status
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.response import Response
Expand Down Expand Up @@ -76,8 +77,7 @@ class ContactViewSet(GetCacheMixin, ReadOnlyModelViewSet):
queryset = Contact.objects.select_related("channel").all()
serializer_class = ContactSerializer
permission_classes = [IsAuthenticated]
filter_backends = [DjangoFilterBackend]
filter_backends = [DjangoFilterBackend, OrderingFilter]
filterset_fields = ["channel"]
ordering = ["-created"]
ordering_fields = ["created", "modified"]
search_fields = ["name", "channel_identifier"]
35 changes: 35 additions & 0 deletions src/assets/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import base64

from rest_framework.test import APIClient as _APIClient
from rest_framework_simplejwt.serializers import (
TokenObtainPairSerializer as _TokenObtainPairSerializer,
)


def configure_api_client( # noqa
client: _APIClient,
set_header_version=True,
basic_auth=False,
access_token=None,
user=None,
password=None,
):
headers = {}
if set_header_version:
headers["HTTP_ACCEPT"] = "application/json; version=v1_web"

if access_token is not None and not basic_auth:
headers["HTTP_AUTHORIZATION"] = f"Bearer {access_token}"

if user is not None and not basic_auth:
access = str(_TokenObtainPairSerializer.get_token(user).access_token)
headers["HTTP_AUTHORIZATION"] = f"Bearer {access}"
client.force_authenticate(user=user)

if basic_auth and password:
access = base64.b64encode(str.encode(f"{user.email}:{password}")).decode(
"iso-8859-1",
)
headers["HTTP_AUTHORIZATION"] = f"Basic {access}"

client.credentials(**headers)
25 changes: 25 additions & 0 deletions src/channels/migrations/0002_alter_channel_name.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Generated by Django 5.2.7 on 2025-10-09 21:56

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("channels", "0001_initial"),
]

operations = [
migrations.AlterField(
model_name="channel",
name="name",
field=models.CharField(
choices=[
("whatsapp", "WhatsApp"),
("telegram", "Telegram"),
("mock", "Mock Channel"),
],
max_length=20,
),
),
]
2 changes: 1 addition & 1 deletion src/channels/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class ChannelType(models.TextChoices):
TELEGRAM = "telegram", "Telegram"
MOCK = "mock", "Mock Channel"

name = models.CharField(max_length=20, choices=ChannelType.choices, unique=True)
name = models.CharField(max_length=20, choices=ChannelType.choices)
config = models.JSONField(
default=dict, help_text="Configurações específicas do canal em formato JSON"
)
Expand Down
2 changes: 1 addition & 1 deletion src/channels/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def validate(self, data):
if getattr(self, "instance", None) is not None:
qs = qs.exclude(pk=self.instance.pk)

duplicate_exists = qs.filter(_config__token=token).exists()
duplicate_exists = qs.filter(config__token=token).exists()

if duplicate_exists:
raise serializers.ValidationError(
Expand Down
Loading