diff --git a/examples/testing/inboxes.py b/examples/testing/inboxes.py new file mode 100644 index 0000000..7291a7e --- /dev/null +++ b/examples/testing/inboxes.py @@ -0,0 +1,63 @@ +from typing import Optional + +import mailtrap as mt +from mailtrap.models.inboxes import Inbox + +API_TOKEN = "YOU_API_TOKEN" +ACCOUNT_ID = "YOU_ACCOUNT_ID" + +client = mt.MailtrapClient(token=API_TOKEN, account_id=ACCOUNT_ID) +inboxes_api = client.testing_api.inboxes + + +def list_inboxes() -> list[Inbox]: + return inboxes_api.get_list() + + +def create_inbox(project_id: int, inbox_name: str) -> Inbox: + return inboxes_api.create( + project_id=project_id, inbox_params=mt.CreateInboxParams(name=inbox_name) + ) + + +def get_inbox_by_id(inbox_id: int) -> Inbox: + return inboxes_api.get_by_id(inbox_id) + + +def update_inbox( + inbox_id: int, + new_name: Optional[str] = None, + new_email_username: Optional[str] = None, +) -> Inbox: + return inboxes_api.update( + inbox_id, mt.UpdateInboxParams(name=new_name, email_username=new_email_username) + ) + + +def clean_inbox(inbox_id: int) -> Inbox: + return inboxes_api.clean(inbox_id) + + +def mark_inbox_as_read(inbox_id: int) -> Inbox: + return inboxes_api.mark_as_read(inbox_id) + + +def reset_inbox_credentials(inbox_id: int) -> Inbox: + return inboxes_api.reset_credentials(inbox_id) + + +def enable_inbox_email_address(inbox_id: int) -> Inbox: + return inboxes_api.enable_email_address(inbox_id) + + +def reset_inbox_email_username(inbox_id: int) -> Inbox: + return inboxes_api.reset_email_username(inbox_id) + + +def delete_inbox(inbox_id: int): + return inboxes_api.delete(inbox_id) + + +if __name__ == "__main__": + inboxes = list_inboxes() + print(inboxes) diff --git a/examples/testing/projects.py b/examples/testing/projects.py index c7d192d..7620d2b 100644 --- a/examples/testing/projects.py +++ b/examples/testing/projects.py @@ -1,10 +1,10 @@ -from mailtrap import MailtrapClient +import mailtrap as mt from mailtrap.models.projects import Project API_TOKEN = "YOU_API_TOKEN" ACCOUNT_ID = "YOU_ACCOUNT_ID" -client = MailtrapClient(token=API_TOKEN, account_id=ACCOUNT_ID) +client = mt.MailtrapClient(token=API_TOKEN, account_id=ACCOUNT_ID) projects_api = client.testing_api.projects @@ -12,15 +12,19 @@ def list_projects() -> list[Project]: return projects_api.get_list() -def create_project(project_name: str) -> Project: - return projects_api.create(project_name=project_name) +def get_project(project_id: int) -> Project: + return projects_api.get_by_id(project_id) -def update_project(project_id: str, new_name: str) -> Project: - return projects_api.update(project_id, new_name) +def create_project(name: str) -> Project: + return projects_api.create(project_params=mt.ProjectParams(name=name)) -def delete_project(project_id: str): +def update_project(project_id: int, new_name: str) -> Project: + return projects_api.update(project_id, mt.ProjectParams(name=new_name)) + + +def delete_project(project_id: int): return projects_api.delete(project_id) diff --git a/mailtrap/__init__.py b/mailtrap/__init__.py index 3409a1f..6c3670a 100644 --- a/mailtrap/__init__.py +++ b/mailtrap/__init__.py @@ -10,11 +10,14 @@ from .models.contacts import ImportContactParams from .models.contacts import UpdateContactFieldParams from .models.contacts import UpdateContactParams +from .models.inboxes import CreateInboxParams +from .models.inboxes import UpdateInboxParams from .models.mail import Address from .models.mail import Attachment from .models.mail import BaseMail from .models.mail import Disposition from .models.mail import Mail from .models.mail import MailFromTemplate +from .models.projects import ProjectParams from .models.templates import CreateEmailTemplateParams from .models.templates import UpdateEmailTemplateParams diff --git a/mailtrap/api/resources/contact_fields.py b/mailtrap/api/resources/contact_fields.py index 06521cd..f73eb34 100644 --- a/mailtrap/api/resources/contact_fields.py +++ b/mailtrap/api/resources/contact_fields.py @@ -13,16 +13,19 @@ def __init__(self, client: HttpClient, account_id: str) -> None: self._client = client def get_list(self) -> list[ContactField]: + """Get all Contact Fields existing in your account.""" response = self._client.get(self._api_path()) return [ContactField(**field) for field in response] def get_by_id(self, field_id: int) -> ContactField: + """Get a contact Field by ID.""" response = self._client.get( self._api_path(field_id), ) return ContactField(**response) def create(self, field_params: CreateContactFieldParams) -> ContactField: + """Create new Contact Fields. Please note, you can have up to 40 fields.""" response = self._client.post( self._api_path(), json=field_params.api_data, @@ -32,6 +35,10 @@ def create(self, field_params: CreateContactFieldParams) -> ContactField: def update( self, field_id: int, field_params: UpdateContactFieldParams ) -> ContactField: + """ + Update existing Contact Field. Please note, + you cannot change data_type of the field. + """ response = self._client.patch( self._api_path(field_id), json=field_params.api_data, @@ -39,6 +46,11 @@ def update( return ContactField(**response) def delete(self, field_id: int) -> DeletedObject: + """ + Delete existing Contact Field Please, note, you cannot delete a Contact Field + which is used in Automations, Email Campaigns (started or scheduled), and in + conditions of Contact Segments (you'll see the corresponding error) + """ self._client.delete(self._api_path(field_id)) return DeletedObject(field_id) diff --git a/mailtrap/api/resources/contact_imports.py b/mailtrap/api/resources/contact_imports.py index b20be6b..53e1f44 100644 --- a/mailtrap/api/resources/contact_imports.py +++ b/mailtrap/api/resources/contact_imports.py @@ -11,6 +11,12 @@ def __init__(self, client: HttpClient, account_id: str) -> None: self._client = client def import_contacts(self, contacts: list[ImportContactParams]) -> ContactImport: + """ + Import contacts in bulk with support for custom fields and list management. + Existing contacts with matching email addresses will be updated automatically. + You can import up to 50,000 contacts per request. The import process runs + asynchronously - use the returned import ID to check the status and results. + """ response = self._client.post( self._api_path(), json={"contacts": [contact.api_data for contact in contacts]}, @@ -18,6 +24,7 @@ def import_contacts(self, contacts: list[ImportContactParams]) -> ContactImport: return ContactImport(**response) def get_by_id(self, import_id: int) -> ContactImport: + """Get Contact Import by ID.""" response = self._client.get(self._api_path(import_id)) return ContactImport(**response) diff --git a/mailtrap/api/resources/contact_lists.py b/mailtrap/api/resources/contact_lists.py index 28ced08..cfd873b 100644 --- a/mailtrap/api/resources/contact_lists.py +++ b/mailtrap/api/resources/contact_lists.py @@ -12,14 +12,17 @@ def __init__(self, client: HttpClient, account_id: str) -> None: self._client = client def get_list(self) -> list[ContactList]: + """Get all contact lists existing in your account.""" response = self._client.get(self._api_path()) return [ContactList(**field) for field in response] def get_by_id(self, list_id: int) -> ContactList: + """Get a contact list by ID.""" response = self._client.get(self._api_path(list_id)) return ContactList(**response) def create(self, list_params: ContactListParams) -> ContactList: + """Create new Contact Lists.""" response = self._client.post( self._api_path(), json=list_params.api_data, @@ -27,6 +30,7 @@ def create(self, list_params: ContactListParams) -> ContactList: return ContactList(**response) def update(self, list_id: int, list_params: ContactListParams) -> ContactList: + """Update existing Contact List.""" response = self._client.patch( self._api_path(list_id), json=list_params.api_data, @@ -34,6 +38,7 @@ def update(self, list_id: int, list_params: ContactListParams) -> ContactList: return ContactList(**response) def delete(self, list_id: int) -> DeletedObject: + """Delete existing Contact List.""" self._client.delete(self._api_path(list_id)) return DeletedObject(list_id) diff --git a/mailtrap/api/resources/contacts.py b/mailtrap/api/resources/contacts.py index e160d66..f7d641d 100644 --- a/mailtrap/api/resources/contacts.py +++ b/mailtrap/api/resources/contacts.py @@ -15,10 +15,12 @@ def __init__(self, client: HttpClient, account_id: str) -> None: self._client = client def get_by_id(self, contact_id_or_email: str) -> Contact: + """Get contact using id or email (URL encoded).""" response = self._client.get(self._api_path(contact_id_or_email)) return ContactResponse(**response).data def create(self, contact_params: CreateContactParams) -> Contact: + """Create a new contact.""" response = self._client.post( self._api_path(), json={"contact": contact_params.api_data}, @@ -28,6 +30,7 @@ def create(self, contact_params: CreateContactParams) -> Contact: def update( self, contact_id_or_email: str, contact_params: UpdateContactParams ) -> Contact: + """Update contact using id or email (URL encoded).""" response = self._client.patch( self._api_path(contact_id_or_email), json={"contact": contact_params.api_data}, @@ -35,6 +38,7 @@ def update( return ContactResponse(**response).data def delete(self, contact_id_or_email: str) -> DeletedObject: + """Delete contact using id or email (URL encoded).""" self._client.delete(self._api_path(contact_id_or_email)) return DeletedObject(contact_id_or_email) diff --git a/mailtrap/api/resources/inboxes.py b/mailtrap/api/resources/inboxes.py new file mode 100644 index 0000000..5ca92e9 --- /dev/null +++ b/mailtrap/api/resources/inboxes.py @@ -0,0 +1,74 @@ +from typing import Optional + +from mailtrap.http import HttpClient +from mailtrap.models.inboxes import CreateInboxParams +from mailtrap.models.inboxes import Inbox +from mailtrap.models.inboxes import UpdateInboxParams + + +class InboxesApi: + def __init__(self, client: HttpClient, account_id: str) -> None: + self._account_id = account_id + self._client = client + + def get_list(self) -> list[Inbox]: + """Get a list of inboxes.""" + response = self._client.get(self._api_path()) + return [Inbox(**inbox) for inbox in response] + + def get_by_id(self, inbox_id: int) -> Inbox: + """Get inbox attributes by inbox id.""" + response = self._client.get(self._api_path(inbox_id)) + return Inbox(**response) + + def create(self, project_id: int, inbox_params: CreateInboxParams) -> Inbox: + """Create an inbox in a project.""" + response = self._client.post( + f"/api/accounts/{self._account_id}/projects/{project_id}/inboxes", + json={"inbox": inbox_params.api_data}, + ) + return Inbox(**response) + + def update(self, inbox_id: int, inbox_params: UpdateInboxParams) -> Inbox: + """Update inbox name, inbox email username.""" + response = self._client.patch( + self._api_path(inbox_id), + json={"inbox": inbox_params.api_data}, + ) + return Inbox(**response) + + def delete(self, inbox_id: int) -> Inbox: + """Delete an inbox with all its emails.""" + response = self._client.delete(self._api_path(inbox_id)) + return Inbox(**response) + + def clean(self, inbox_id: int) -> Inbox: + """Delete all messages (emails) from inbox.""" + response = self._client.patch(f"{self._api_path(inbox_id)}/clean") + return Inbox(**response) + + def mark_as_read(self, inbox_id: int) -> Inbox: + """Mark all messages in the inbox as read.""" + response = self._client.patch(f"{self._api_path(inbox_id)}/all_read") + return Inbox(**response) + + def reset_credentials(self, inbox_id: int) -> Inbox: + """Reset SMTP credentials of the inbox.""" + response = self._client.patch(f"{self._api_path(inbox_id)}/reset_credentials") + return Inbox(**response) + + def enable_email_address(self, inbox_id: int) -> Inbox: + """Turn the email address of the inbox on/off.""" + response = self._client.patch(f"{self._api_path(inbox_id)}/toggle_email_username") + return Inbox(**response) + + def reset_email_username(self, inbox_id: int) -> Inbox: + """Reset username of email address per inbox.""" + response = self._client.patch(f"{self._api_path(inbox_id)}/reset_email_username") + return Inbox(**response) + + def _api_path(self, inbox_id: Optional[int] = None) -> str: + path = f"/api/accounts/{self._account_id}/inboxes" + if inbox_id: + return f"{path}/{inbox_id}" + return path diff --git a/mailtrap/api/resources/projects.py b/mailtrap/api/resources/projects.py index 95aca4d..34df6d5 100644 --- a/mailtrap/api/resources/projects.py +++ b/mailtrap/api/resources/projects.py @@ -3,6 +3,7 @@ from mailtrap.http import HttpClient from mailtrap.models.common import DeletedObject from mailtrap.models.projects import Project +from mailtrap.models.projects import ProjectParams class ProjectsApi: @@ -11,28 +12,39 @@ def __init__(self, client: HttpClient, account_id: str) -> None: self._client = client def get_list(self) -> list[Project]: + """List projects and their inboxes to which the API token has access.""" response = self._client.get(self._api_path()) return [Project(**project) for project in response] def get_by_id(self, project_id: int) -> Project: + """Get the project and its inboxes.""" response = self._client.get(self._api_path(project_id)) return Project(**response) - def create(self, project_name: str) -> Project: + def create(self, project_params: ProjectParams) -> Project: + """ + Create a new project. + The project name is min 2 characters and max 100 characters long. + """ response = self._client.post( self._api_path(), - json={"project": {"name": project_name}}, + json={"project": project_params.api_data}, ) return Project(**response) - def update(self, project_id: int, project_name: str) -> Project: + def update(self, project_id: int, project_params: ProjectParams) -> Project: + """ + Update project name. + The project name is min 2 characters and max 100 characters long. + """ response = self._client.patch( self._api_path(project_id), - json={"project": {"name": project_name}}, + json={"project": project_params.api_data}, ) return Project(**response) def delete(self, project_id: int) -> DeletedObject: + """Delete project and its inboxes.""" response = self._client.delete(self._api_path(project_id)) return DeletedObject(**response) diff --git a/mailtrap/api/resources/templates.py b/mailtrap/api/resources/templates.py index 1fd012b..667a4a0 100644 --- a/mailtrap/api/resources/templates.py +++ b/mailtrap/api/resources/templates.py @@ -13,14 +13,17 @@ def __init__(self, client: HttpClient, account_id: str) -> None: self._client = client def get_list(self) -> list[EmailTemplate]: + """Get all email templates existing in your account.""" response = self._client.get(self._api_path()) return [EmailTemplate(**template) for template in response] def get_by_id(self, template_id: int) -> EmailTemplate: + """Get an email template by ID.""" response = self._client.get(self._api_path(template_id)) return EmailTemplate(**response) def create(self, template_params: CreateEmailTemplateParams) -> EmailTemplate: + """Create a new email template.""" response = self._client.post( self._api_path(), json={"email_template": template_params.api_data}, @@ -30,6 +33,7 @@ def create(self, template_params: CreateEmailTemplateParams) -> EmailTemplate: def update( self, template_id: int, template_params: UpdateEmailTemplateParams ) -> EmailTemplate: + """Update an email template.""" response = self._client.patch( self._api_path(template_id), json={"email_template": template_params.api_data}, @@ -37,6 +41,7 @@ def update( return EmailTemplate(**response) def delete(self, template_id: int) -> DeletedObject: + """Delete an email template.""" self._client.delete(self._api_path(template_id)) return DeletedObject(template_id) diff --git a/mailtrap/api/sending.py b/mailtrap/api/sending.py index 08a5cde..06c0f58 100644 --- a/mailtrap/api/sending.py +++ b/mailtrap/api/sending.py @@ -18,5 +18,6 @@ def _api_url(self) -> str: return url def send(self, mail: BaseMail) -> SendingMailResponse: + """Send email (text, html, text&html, templates).""" response = self._client.post(self._api_url, json=mail.api_data) return SendingMailResponse(**response) diff --git a/mailtrap/api/testing.py b/mailtrap/api/testing.py index 1f6fda1..e545df4 100644 --- a/mailtrap/api/testing.py +++ b/mailtrap/api/testing.py @@ -1,5 +1,6 @@ from typing import Optional +from mailtrap.api.resources.inboxes import InboxesApi from mailtrap.api.resources.projects import ProjectsApi from mailtrap.http import HttpClient @@ -15,3 +16,7 @@ def __init__( @property def projects(self) -> ProjectsApi: return ProjectsApi(account_id=self._account_id, client=self._client) + + @property + def inboxes(self) -> InboxesApi: + return InboxesApi(account_id=self._account_id, client=self._client) diff --git a/mailtrap/models/inboxes.py b/mailtrap/models/inboxes.py index 06d5374..c62b1ea 100644 --- a/mailtrap/models/inboxes.py +++ b/mailtrap/models/inboxes.py @@ -2,6 +2,7 @@ from pydantic.dataclasses import dataclass +from mailtrap.models.common import RequestParams from mailtrap.models.permissions import Permissions @@ -22,6 +23,7 @@ class Inbox: domain: str pop3_domain: str email_domain: str + api_domain: str emails_count: int emails_unread_count: int smtp_ports: list[int] @@ -32,3 +34,24 @@ class Inbox: None # Password is only available if you have admin permissions for the inbox. ) last_message_sent_at: Optional[str] = None + + +@dataclass +class CreateInboxParams(RequestParams): + name: str + + +@dataclass +class UpdateInboxParams(RequestParams): + name: Optional[str] = None + email_username: Optional[str] = None + + def __post_init__(self) -> None: + if all( + value is None + for value in [ + self.name, + self.email_username, + ] + ): + raise ValueError("At least one field must be provided for update action") diff --git a/mailtrap/models/projects.py b/mailtrap/models/projects.py index 20ecec6..1e7364d 100644 --- a/mailtrap/models/projects.py +++ b/mailtrap/models/projects.py @@ -2,6 +2,7 @@ from pydantic.dataclasses import dataclass +from mailtrap.models.common import RequestParams from mailtrap.models.inboxes import Inbox from mailtrap.models.permissions import Permissions @@ -19,3 +20,8 @@ class Project: inboxes: list[Inbox] permissions: Permissions share_links: Optional[ShareLinks] = None + + +@dataclass +class ProjectParams(RequestParams): + name: str diff --git a/tests/unit/api/contacts/__init__.py b/tests/unit/api/contacts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/api/test_contact_fields.py b/tests/unit/api/contacts/test_contact_fields.py similarity index 100% rename from tests/unit/api/test_contact_fields.py rename to tests/unit/api/contacts/test_contact_fields.py diff --git a/tests/unit/api/test_contact_imports.py b/tests/unit/api/contacts/test_contact_imports.py similarity index 100% rename from tests/unit/api/test_contact_imports.py rename to tests/unit/api/contacts/test_contact_imports.py diff --git a/tests/unit/api/test_contact_lists.py b/tests/unit/api/contacts/test_contact_lists.py similarity index 100% rename from tests/unit/api/test_contact_lists.py rename to tests/unit/api/contacts/test_contact_lists.py diff --git a/tests/unit/api/test_contacts.py b/tests/unit/api/contacts/test_contacts.py similarity index 100% rename from tests/unit/api/test_contacts.py rename to tests/unit/api/contacts/test_contacts.py diff --git a/tests/unit/api/email_templates/__init__.py b/tests/unit/api/email_templates/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/api/test_email_templates.py b/tests/unit/api/email_templates/test_email_templates.py similarity index 100% rename from tests/unit/api/test_email_templates.py rename to tests/unit/api/email_templates/test_email_templates.py diff --git a/tests/unit/api/testing/__init__.py b/tests/unit/api/testing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/api/testing/test_inboxes.py b/tests/unit/api/testing/test_inboxes.py new file mode 100644 index 0000000..9f8b56b --- /dev/null +++ b/tests/unit/api/testing/test_inboxes.py @@ -0,0 +1,626 @@ +from typing import Any + +import pytest +import responses + +from mailtrap.api.resources.inboxes import InboxesApi +from mailtrap.config import GENERAL_HOST +from mailtrap.exceptions import APIError +from mailtrap.http import HttpClient +from mailtrap.models.inboxes import CreateInboxParams +from mailtrap.models.inboxes import Inbox +from mailtrap.models.inboxes import UpdateInboxParams +from tests import conftest + +ACCOUNT_ID = "321" +INBOX_ID = 3538 +PROJECT_ID = 2293 +BASE_INBOXES_URL = f"https://{GENERAL_HOST}/api/accounts/{ACCOUNT_ID}/inboxes" +BASE_PROJECT_INBOXES_URL = ( + f"https://{GENERAL_HOST}/api/accounts/{ACCOUNT_ID}/projects/{PROJECT_ID}/inboxes" +) + + +@pytest.fixture +def client() -> InboxesApi: + return InboxesApi(account_id=ACCOUNT_ID, client=HttpClient(GENERAL_HOST)) + + +@pytest.fixture +def sample_inbox_dict() -> dict[str, Any]: + return { + "id": INBOX_ID, + "name": "Admin Inbox", + "username": "b3a87978452ae1", + "password": "6be9fcfc613a7c", + "max_size": 0, + "status": "active", + "email_username": "b7eae548c3-54c542", + "email_username_enabled": False, + "sent_messages_count": 52, + "forwarded_messages_count": 0, + "used": False, + "forward_from_email_address": "a3538-i4088@forward.mailtrap.info", + "project_id": PROJECT_ID, + "domain": "localhost", + "pop3_domain": "localhost", + "email_domain": "localhost", + "api_domain": "localhost", + "emails_count": 0, + "emails_unread_count": 0, + "last_message_sent_at": None, + "smtp_ports": [25, 465, 587, 2525], + "pop3_ports": [1100, 9950], + "max_message_size": 5242880, + "permissions": { + "can_read": True, + "can_update": True, + "can_destroy": True, + "can_leave": True, + }, + } + + +class TestInboxesApi: + + @pytest.mark.parametrize( + "status_code,response_json,expected_error_message", + [ + ( + conftest.UNAUTHORIZED_STATUS_CODE, + conftest.UNAUTHORIZED_RESPONSE, + conftest.UNAUTHORIZED_ERROR_MESSAGE, + ), + ( + conftest.FORBIDDEN_STATUS_CODE, + conftest.FORBIDDEN_RESPONSE, + conftest.FORBIDDEN_ERROR_MESSAGE, + ), + ], + ) + @responses.activate + def test_get_list_should_raise_api_errors( + self, + client: InboxesApi, + status_code: int, + response_json: dict, + expected_error_message: str, + ) -> None: + responses.get( + BASE_INBOXES_URL, + status=status_code, + json=response_json, + ) + + with pytest.raises(APIError) as exc_info: + client.get_list() + + assert expected_error_message in str(exc_info.value) + + @responses.activate + def test_get_list_should_return_inbox_list( + self, client: InboxesApi, sample_inbox_dict: dict + ) -> None: + responses.get( + BASE_INBOXES_URL, + json=[sample_inbox_dict], + status=200, + ) + + inboxes = client.get_list() + + assert isinstance(inboxes, list) + assert all(isinstance(i, Inbox) for i in inboxes) + assert inboxes[0].id == INBOX_ID + + @pytest.mark.parametrize( + "status_code,response_json,expected_error_message", + [ + ( + conftest.UNAUTHORIZED_STATUS_CODE, + conftest.UNAUTHORIZED_RESPONSE, + conftest.UNAUTHORIZED_ERROR_MESSAGE, + ), + ( + conftest.FORBIDDEN_STATUS_CODE, + conftest.FORBIDDEN_RESPONSE, + conftest.FORBIDDEN_ERROR_MESSAGE, + ), + ( + conftest.NOT_FOUND_STATUS_CODE, + conftest.NOT_FOUND_RESPONSE, + conftest.NOT_FOUND_ERROR_MESSAGE, + ), + ], + ) + @responses.activate + def test_get_by_id_should_raise_api_errors( + self, + client: InboxesApi, + status_code: int, + response_json: dict, + expected_error_message: str, + ) -> None: + url = f"{BASE_INBOXES_URL}/{INBOX_ID}" + responses.get( + url, + status=status_code, + json=response_json, + ) + + with pytest.raises(APIError) as exc_info: + client.get_by_id(INBOX_ID) + + assert expected_error_message in str(exc_info.value) + + @responses.activate + def test_get_by_id_should_return_single_inbox( + self, client: InboxesApi, sample_inbox_dict: dict + ) -> None: + url = f"{BASE_INBOXES_URL}/{INBOX_ID}" + responses.get( + url, + json=sample_inbox_dict, + status=200, + ) + + inbox = client.get_by_id(INBOX_ID) + + assert isinstance(inbox, Inbox) + assert inbox.id == INBOX_ID + + @pytest.mark.parametrize( + "status_code,response_json,expected_error_message", + [ + ( + conftest.UNAUTHORIZED_STATUS_CODE, + conftest.UNAUTHORIZED_RESPONSE, + conftest.UNAUTHORIZED_ERROR_MESSAGE, + ), + ( + conftest.FORBIDDEN_STATUS_CODE, + conftest.FORBIDDEN_RESPONSE, + conftest.FORBIDDEN_ERROR_MESSAGE, + ), + ( + conftest.NOT_FOUND_STATUS_CODE, + conftest.NOT_FOUND_RESPONSE, + conftest.NOT_FOUND_ERROR_MESSAGE, + ), + ], + ) + @responses.activate + def test_create_should_raise_api_errors( + self, + client: InboxesApi, + status_code: int, + response_json: dict, + expected_error_message: str, + ) -> None: + responses.post( + BASE_PROJECT_INBOXES_URL, + status=status_code, + json=response_json, + ) + + with pytest.raises(APIError) as exc_info: + client.create( + project_id=PROJECT_ID, inbox_params=CreateInboxParams(name="New Inbox") + ) + + assert expected_error_message in str(exc_info.value) + + @responses.activate + def test_create_should_return_new_inbox( + self, client: InboxesApi, sample_inbox_dict: dict + ) -> None: + responses.post( + BASE_PROJECT_INBOXES_URL, + json=sample_inbox_dict, + status=201, + ) + + inbox = client.create( + project_id=PROJECT_ID, inbox_params=CreateInboxParams(name="New Inbox") + ) + + assert isinstance(inbox, Inbox) + assert inbox.name == "Admin Inbox" + + @pytest.mark.parametrize( + "status_code,response_json,expected_error_message", + [ + ( + conftest.UNAUTHORIZED_STATUS_CODE, + conftest.UNAUTHORIZED_RESPONSE, + conftest.UNAUTHORIZED_ERROR_MESSAGE, + ), + ( + conftest.FORBIDDEN_STATUS_CODE, + conftest.FORBIDDEN_RESPONSE, + conftest.FORBIDDEN_ERROR_MESSAGE, + ), + ( + conftest.NOT_FOUND_STATUS_CODE, + conftest.NOT_FOUND_RESPONSE, + conftest.NOT_FOUND_ERROR_MESSAGE, + ), + ], + ) + @responses.activate + def test_update_should_raise_api_errors( + self, + client: InboxesApi, + status_code: int, + response_json: dict, + expected_error_message: str, + ) -> None: + url = f"{BASE_INBOXES_URL}/{INBOX_ID}" + responses.patch( + url, + status=status_code, + json=response_json, + ) + + with pytest.raises(APIError) as exc_info: + _ = client.update( + INBOX_ID, inbox_params=UpdateInboxParams(name="Updated Inbox Name") + ) + + assert expected_error_message in str(exc_info.value) + + @responses.activate + def test_update_should_return_updated_inbox( + self, client: InboxesApi, sample_inbox_dict: dict + ) -> None: + url = f"{BASE_INBOXES_URL}/{INBOX_ID}" + updated_name = "Updated Inbox" + updated_inbox_dict = sample_inbox_dict.copy() + updated_inbox_dict["name"] = updated_name + + responses.patch( + url, + json=updated_inbox_dict, + status=200, + ) + + inbox = client.update(INBOX_ID, inbox_params=UpdateInboxParams(name=updated_name)) + + assert isinstance(inbox, Inbox) + assert inbox.name == updated_name + + @pytest.mark.parametrize( + "status_code,response_json,expected_error_message", + [ + ( + conftest.UNAUTHORIZED_STATUS_CODE, + conftest.UNAUTHORIZED_RESPONSE, + conftest.UNAUTHORIZED_ERROR_MESSAGE, + ), + ( + conftest.FORBIDDEN_STATUS_CODE, + conftest.FORBIDDEN_RESPONSE, + conftest.FORBIDDEN_ERROR_MESSAGE, + ), + ( + conftest.NOT_FOUND_STATUS_CODE, + conftest.NOT_FOUND_RESPONSE, + conftest.NOT_FOUND_ERROR_MESSAGE, + ), + ], + ) + @responses.activate + def test_delete_should_raise_api_errors( + self, + client: InboxesApi, + status_code: int, + response_json: dict, + expected_error_message: str, + ) -> None: + url = f"{BASE_INBOXES_URL}/{INBOX_ID}" + responses.delete( + url, + status=status_code, + json=response_json, + ) + + with pytest.raises(APIError) as exc_info: + client.delete(INBOX_ID) + + assert expected_error_message in str(exc_info.value) + + @responses.activate + def test_delete_should_return_deleted_inbox( + self, client: InboxesApi, sample_inbox_dict: dict + ) -> None: + url = f"{BASE_INBOXES_URL}/{INBOX_ID}" + responses.delete( + url, + json=sample_inbox_dict, + status=200, + ) + + result = client.delete(INBOX_ID) + + assert isinstance(result, Inbox) + assert result.id == INBOX_ID + + @pytest.mark.parametrize( + "status_code,response_json,expected_error_message", + [ + ( + conftest.UNAUTHORIZED_STATUS_CODE, + conftest.UNAUTHORIZED_RESPONSE, + conftest.UNAUTHORIZED_ERROR_MESSAGE, + ), + ( + conftest.FORBIDDEN_STATUS_CODE, + conftest.FORBIDDEN_RESPONSE, + conftest.FORBIDDEN_ERROR_MESSAGE, + ), + ( + conftest.NOT_FOUND_STATUS_CODE, + conftest.NOT_FOUND_RESPONSE, + conftest.NOT_FOUND_ERROR_MESSAGE, + ), + ], + ) + @responses.activate + def test_clean_should_raise_api_errors( + self, + client: InboxesApi, + status_code: int, + response_json: dict, + expected_error_message: str, + ) -> None: + url = f"{BASE_INBOXES_URL}/{INBOX_ID}/clean" + responses.patch( + url, + status=status_code, + json=response_json, + ) + + with pytest.raises(APIError) as exc_info: + client.clean(INBOX_ID) + + assert expected_error_message in str(exc_info.value) + + @responses.activate + def test_clean_should_return_cleaned_inbox( + self, client: InboxesApi, sample_inbox_dict: dict + ) -> None: + url = f"{BASE_INBOXES_URL}/{INBOX_ID}/clean" + responses.patch( + url, + json=sample_inbox_dict, + status=200, + ) + + result = client.clean(INBOX_ID) + + assert isinstance(result, Inbox) + assert result.id == INBOX_ID + + @pytest.mark.parametrize( + "status_code,response_json,expected_error_message", + [ + ( + conftest.UNAUTHORIZED_STATUS_CODE, + conftest.UNAUTHORIZED_RESPONSE, + conftest.UNAUTHORIZED_ERROR_MESSAGE, + ), + ( + conftest.FORBIDDEN_STATUS_CODE, + conftest.FORBIDDEN_RESPONSE, + conftest.FORBIDDEN_ERROR_MESSAGE, + ), + ( + conftest.NOT_FOUND_STATUS_CODE, + conftest.NOT_FOUND_RESPONSE, + conftest.NOT_FOUND_ERROR_MESSAGE, + ), + ], + ) + @responses.activate + def test_mark_as_read_should_raise_api_errors( + self, + client: InboxesApi, + status_code: int, + response_json: dict, + expected_error_message: str, + ) -> None: + url = f"{BASE_INBOXES_URL}/{INBOX_ID}/all_read" + responses.patch( + url, + status=status_code, + json=response_json, + ) + + with pytest.raises(APIError) as exc_info: + client.mark_as_read(INBOX_ID) + + assert expected_error_message in str(exc_info.value) + + @responses.activate + def test_mark_as_read_should_return_inbox( + self, client: InboxesApi, sample_inbox_dict: dict + ) -> None: + url = f"{BASE_INBOXES_URL}/{INBOX_ID}/all_read" + responses.patch( + url, + json=sample_inbox_dict, + status=200, + ) + + result = client.mark_as_read(INBOX_ID) + + assert isinstance(result, Inbox) + assert result.id == INBOX_ID + + @pytest.mark.parametrize( + "status_code,response_json,expected_error_message", + [ + ( + conftest.UNAUTHORIZED_STATUS_CODE, + conftest.UNAUTHORIZED_RESPONSE, + conftest.UNAUTHORIZED_ERROR_MESSAGE, + ), + ( + conftest.FORBIDDEN_STATUS_CODE, + conftest.FORBIDDEN_RESPONSE, + conftest.FORBIDDEN_ERROR_MESSAGE, + ), + ( + conftest.NOT_FOUND_STATUS_CODE, + conftest.NOT_FOUND_RESPONSE, + conftest.NOT_FOUND_ERROR_MESSAGE, + ), + ], + ) + @responses.activate + def test_reset_credentials_should_raise_api_errors( + self, + client: InboxesApi, + status_code: int, + response_json: dict, + expected_error_message: str, + ) -> None: + url = f"{BASE_INBOXES_URL}/{INBOX_ID}/reset_credentials" + responses.patch( + url, + status=status_code, + json=response_json, + ) + + with pytest.raises(APIError) as exc_info: + client.reset_credentials(INBOX_ID) + + assert expected_error_message in str(exc_info.value) + + @responses.activate + def test_reset_credentials_should_return_inbox( + self, client: InboxesApi, sample_inbox_dict: dict + ) -> None: + url = f"{BASE_INBOXES_URL}/{INBOX_ID}/reset_credentials" + responses.patch( + url, + json=sample_inbox_dict, + status=200, + ) + + result = client.reset_credentials(INBOX_ID) + + assert isinstance(result, Inbox) + assert result.id == INBOX_ID + + @pytest.mark.parametrize( + "status_code,response_json,expected_error_message", + [ + ( + conftest.UNAUTHORIZED_STATUS_CODE, + conftest.UNAUTHORIZED_RESPONSE, + conftest.UNAUTHORIZED_ERROR_MESSAGE, + ), + ( + conftest.FORBIDDEN_STATUS_CODE, + conftest.FORBIDDEN_RESPONSE, + conftest.FORBIDDEN_ERROR_MESSAGE, + ), + ( + conftest.NOT_FOUND_STATUS_CODE, + conftest.NOT_FOUND_RESPONSE, + conftest.NOT_FOUND_ERROR_MESSAGE, + ), + ], + ) + @responses.activate + def test_enable_email_address_should_raise_api_errors( + self, + client: InboxesApi, + status_code: int, + response_json: dict, + expected_error_message: str, + ) -> None: + url = f"{BASE_INBOXES_URL}/{INBOX_ID}/toggle_email_username" + responses.patch( + url, + status=status_code, + json=response_json, + ) + + with pytest.raises(APIError) as exc_info: + client.enable_email_address(INBOX_ID) + + assert expected_error_message in str(exc_info.value) + + @responses.activate + def test_enable_email_address_should_return_inbox( + self, client: InboxesApi, sample_inbox_dict: dict + ) -> None: + url = f"{BASE_INBOXES_URL}/{INBOX_ID}/toggle_email_username" + responses.patch( + url, + json=sample_inbox_dict, + status=200, + ) + + result = client.enable_email_address(INBOX_ID) + + assert isinstance(result, Inbox) + assert result.id == INBOX_ID + + @pytest.mark.parametrize( + "status_code,response_json,expected_error_message", + [ + ( + conftest.UNAUTHORIZED_STATUS_CODE, + conftest.UNAUTHORIZED_RESPONSE, + conftest.UNAUTHORIZED_ERROR_MESSAGE, + ), + ( + conftest.FORBIDDEN_STATUS_CODE, + conftest.FORBIDDEN_RESPONSE, + conftest.FORBIDDEN_ERROR_MESSAGE, + ), + ( + conftest.NOT_FOUND_STATUS_CODE, + conftest.NOT_FOUND_RESPONSE, + conftest.NOT_FOUND_ERROR_MESSAGE, + ), + ], + ) + @responses.activate + def test_reset_email_username_should_raise_api_errors( + self, + client: InboxesApi, + status_code: int, + response_json: dict, + expected_error_message: str, + ) -> None: + url = f"{BASE_INBOXES_URL}/{INBOX_ID}/reset_email_username" + responses.patch( + url, + status=status_code, + json=response_json, + ) + + with pytest.raises(APIError) as exc_info: + client.reset_email_username(INBOX_ID) + + assert expected_error_message in str(exc_info.value) + + @responses.activate + def test_reset_email_username_should_return_inbox( + self, client: InboxesApi, sample_inbox_dict: dict + ) -> None: + url = f"{BASE_INBOXES_URL}/{INBOX_ID}/reset_email_username" + responses.patch( + url, + json=sample_inbox_dict, + status=200, + ) + + result = client.reset_email_username(INBOX_ID) + + assert isinstance(result, Inbox) + assert result.id == INBOX_ID diff --git a/tests/unit/api/test_projects.py b/tests/unit/api/testing/test_projects.py similarity index 95% rename from tests/unit/api/test_projects.py rename to tests/unit/api/testing/test_projects.py index 6ff42c0..91e6b6a 100644 --- a/tests/unit/api/test_projects.py +++ b/tests/unit/api/testing/test_projects.py @@ -9,6 +9,7 @@ from mailtrap.http import HttpClient from mailtrap.models.common import DeletedObject from mailtrap.models.projects import Project +from mailtrap.models.projects import ProjectParams from tests import conftest ACCOUNT_ID = "321" @@ -178,7 +179,7 @@ def test_create_should_raise_api_errors( ) with pytest.raises(APIError) as exc_info: - client.create(project_name="New Project") + client.create(project_params=ProjectParams(name="New Project")) assert expected_error_message in str(exc_info.value) @@ -192,7 +193,7 @@ def test_create_should_return_new_project( status=201, ) - project = client.create(project_name="New Project") + project = client.create(project_params=ProjectParams(name="New Project")) assert isinstance(project, Project) assert project.name == "Test Project" @@ -233,7 +234,9 @@ def test_update_should_raise_api_errors( ) with pytest.raises(APIError) as exc_info: - client.update(PROJECT_ID, project_name="Update Project Name") + _ = client.update( + PROJECT_ID, project_params=ProjectParams(name="Update Project Namet") + ) assert expected_error_message in str(exc_info.value) @@ -252,7 +255,9 @@ def test_update_should_return_updated_project( status=200, ) - project = client.update(PROJECT_ID, project_name=updated_name) + project = client.update( + PROJECT_ID, project_params=ProjectParams(name=updated_name) + ) assert isinstance(project, Project) assert project.name == updated_name diff --git a/tests/unit/models/test_inboxes.py b/tests/unit/models/test_inboxes.py new file mode 100644 index 0000000..483c30b --- /dev/null +++ b/tests/unit/models/test_inboxes.py @@ -0,0 +1,29 @@ +import pytest + +from mailtrap.models.inboxes import CreateInboxParams +from mailtrap.models.inboxes import UpdateInboxParams + + +class TestCreateInboxParams: + def test_api_data_should_return_dict_with_all_props(self) -> None: + entity = CreateInboxParams(name="test") + assert entity.api_data == {"name": "test"} + + +class TestUpdateInboxParams: + def test_raise_error_when_all_fields_are_missing(self) -> None: + with pytest.raises( + ValueError, match="At least one field must be provided for update action" + ): + _ = UpdateInboxParams() + + def test_api_data_should_return_dict_with_all_props(self) -> None: + entity = UpdateInboxParams(name="test", email_username="test_username") + assert entity.api_data == {"name": "test", "email_username": "test_username"} + + def test_api_data_should_return_dict_with_required_props_only(self) -> None: + entity = UpdateInboxParams(name="test") + assert entity.api_data == {"name": "test"} + + entity = UpdateInboxParams(email_username="test_username") + assert entity.api_data == {"email_username": "test_username"} diff --git a/tests/unit/models/test_projects.py b/tests/unit/models/test_projects.py new file mode 100644 index 0000000..3c81772 --- /dev/null +++ b/tests/unit/models/test_projects.py @@ -0,0 +1,7 @@ +from mailtrap.models.projects import ProjectParams + + +class TestProjectParams: + def test_api_data_should_return_dict_with_all_props(self) -> None: + entity = ProjectParams(name="test") + assert entity.api_data == {"name": "test"} diff --git a/tests/unit/models/test_templates.py b/tests/unit/models/test_templates.py index a021aef..91e28a9 100644 --- a/tests/unit/models/test_templates.py +++ b/tests/unit/models/test_templates.py @@ -1,5 +1,4 @@ import pytest -from pydantic import ValidationError from mailtrap.models.templates import CreateEmailTemplateParams from mailtrap.models.templates import UpdateEmailTemplateParams @@ -33,7 +32,7 @@ def test_api_data_should_return_dict_with_all_props(self) -> None: class TestUpdateEmailTemplateParams: def test_raise_error_when_all_fields_are_missing(self) -> None: - with pytest.raises(ValidationError) as exc: + with pytest.raises(ValueError) as exc: _ = UpdateEmailTemplateParams() assert "At least one field must be provided for update actio" in str(exc)