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
12 changes: 10 additions & 2 deletions nylas/models/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,14 +110,22 @@ def from_dict(cls, resp: dict, generic_type, headers: Optional[CaseInsensitiveDi
headers: The headers returned from the API.
"""

raw_data = resp.get("data", [])
if isinstance(raw_data, dict):
next_cursor = resp.get("next_cursor", raw_data.get("next_cursor"))
data = raw_data.get("items", [])
else:
next_cursor = resp.get("next_cursor")
data = raw_data

converted_data = []
for item in resp["data"]:
for item in data:
converted_data.append(generic_type.from_dict(item, infer_missing=True))

return cls(
data=converted_data,
request_id=resp["request_id"],
next_cursor=resp.get("next_cursor", None),
next_cursor=next_cursor,
headers=headers,
)

Expand Down
6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,9 @@ version = {attr = "nylas._client_sdk_version.__VERSION__"}
[tool.setuptools.packages.find]
where = ["."]
include = ["nylas*"]

[tool.pytest.ini_options]
addopts = "-m 'not e2e'"
markers = [
"e2e: marks tests that call live Nylas APIs",
]
118 changes: 118 additions & 0 deletions tests/e2e/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import os
from typing import Dict, List
from uuid import uuid4

import pytest

from nylas import Client


_E2E_API_KEY_ENV = "NYLAS_E2E_API_KEY"
_E2E_API_URI_ENV = "NYLAS_E2E_API_URI"
_E2E_RUN_ENV = "NYLAS_E2E_RUN"


def _is_truthy(value: str) -> bool:
return value.lower() in {"1", "true", "yes", "on"}


def pytest_addoption(parser):
parser.addoption(
"--run-e2e",
action="store_true",
default=False,
help="Run live E2E tests that call Nylas APIs.",
)


def pytest_collection_modifyitems(config, items):
run_e2e = config.getoption("--run-e2e") or _is_truthy(os.getenv(_E2E_RUN_ENV, ""))
if run_e2e:
return

skip_e2e = pytest.mark.skip(
reason=(
"E2E tests are opt-in. Set NYLAS_E2E_RUN=1 or pass --run-e2e to execute."
)
)
for item in items:
if "e2e" in item.keywords:
item.add_marker(skip_e2e)


@pytest.fixture
def paginated_list_contains_id():
def _contains_id(list_method, resource_id: str, limit: int = 100, max_pages: int = 20) -> bool:
next_cursor = None
seen_cursors = set()

for _ in range(max_pages):
query_params = {"limit": limit}
if next_cursor:
query_params["page_token"] = next_cursor

response = list_method(query_params=query_params)
if any(item.id == resource_id for item in response.data if item and item.id):
return True

if not response.next_cursor or response.next_cursor in seen_cursors:
return False

seen_cursors.add(response.next_cursor)
next_cursor = response.next_cursor

return False

return _contains_id


@pytest.fixture(scope="session")
def e2e_client() -> Client:
api_key = os.getenv(_E2E_API_KEY_ENV, "")
if not api_key:
pytest.skip(
"E2E tests require NYLAS_E2E_API_KEY to be set."
)

api_uri = os.getenv(_E2E_API_URI_ENV, "")
timeout = int(os.getenv("NYLAS_E2E_TIMEOUT", "90"))
if api_uri:
return Client(api_key=api_key, api_uri=api_uri, timeout=timeout)
return Client(api_key=api_key, timeout=timeout)


@pytest.fixture
def unique_name():
def _build(prefix: str) -> str:
return f"{prefix}-{uuid4().hex[:10]}"

return _build


@pytest.fixture
def e2e_resource_registry(e2e_client):
registry: Dict[str, List[str]] = {
"policies": [],
"rules": [],
"lists": [],
}
yield registry

for policy_id in reversed(registry["policies"]):
try:
e2e_client.policies.destroy(policy_id)
except Exception:
pass

for rule_id in reversed(registry["rules"]):
try:
e2e_client.rules.destroy(rule_id)
except Exception:
pass

for list_id in reversed(registry["lists"]):
try:
e2e_client.lists.destroy(list_id)
except Exception:
pass

60 changes: 60 additions & 0 deletions tests/e2e/test_lists_e2e.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import pytest


@pytest.mark.e2e
def test_lists_lifecycle_e2e(e2e_client, e2e_resource_registry, unique_name):
create_response = e2e_client.lists.create(
{
"name": unique_name("e2e-list"),
"type": "domain",
"description": "Created by SDK e2e test",
}
)
created_list = create_response.data
assert created_list.id
assert created_list.type == "domain"
e2e_resource_registry["lists"].append(created_list.id)

found_response = e2e_client.lists.find(created_list.id)
assert found_response.data.id == created_list.id

updated_name = unique_name("e2e-list-updated")
update_response = e2e_client.lists.update(
created_list.id,
{"name": updated_name, "description": "Updated by SDK e2e test"},
)
assert update_response.data.id == created_list.id
assert update_response.data.name == updated_name

first_domain = f"{unique_name('allowed')}.example"
second_domain = f"{unique_name('blocked')}.example"
add_items_response = e2e_client.lists.add_items(
created_list.id, {"items": [first_domain, second_domain]}
)
assert add_items_response.data.id == created_list.id

list_items_response = e2e_client.lists.list_items(
created_list.id, query_params={"limit": 200}
)
item_values = {item.value for item in list_items_response.data if item.value}
assert first_domain in item_values
assert second_domain in item_values

remove_items_response = e2e_client.lists.remove_items(
created_list.id, {"items": [first_domain]}
)
assert remove_items_response.data.id == created_list.id

after_remove_response = e2e_client.lists.list_items(
created_list.id, query_params={"limit": 200}
)
item_values_after_remove = {
item.value for item in after_remove_response.data if item.value
}
assert first_domain not in item_values_after_remove
assert second_domain in item_values_after_remove

destroy_response = e2e_client.lists.destroy(created_list.id)
assert destroy_response.request_id
e2e_resource_registry["lists"].remove(created_list.id)

69 changes: 69 additions & 0 deletions tests/e2e/test_policies_e2e.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import pytest


@pytest.mark.e2e
def test_policies_lifecycle_with_rule_association_e2e(
e2e_client, e2e_resource_registry, unique_name, paginated_list_contains_id
):
rule_response = e2e_client.rules.create(
{
"name": unique_name("e2e-policy-rule"),
"trigger": "inbound",
"match": {
"operator": "any",
"conditions": [
{
"field": "from.domain",
"operator": "is",
"value": "example.com",
}
],
},
"actions": [{"type": "archive"}],
}
)
created_rule = rule_response.data
assert created_rule.id
e2e_resource_registry["rules"].append(created_rule.id)

policy_response = e2e_client.policies.create(
{"name": unique_name("e2e-policy"), "rules": [created_rule.id]}
)
created_policy = policy_response.data
assert created_policy.id
e2e_resource_registry["policies"].append(created_policy.id)

find_response = e2e_client.policies.find(created_policy.id)
assert find_response.data.id == created_policy.id

updated_name = unique_name("e2e-policy-updated")
update_response = e2e_client.policies.update(
created_policy.id,
{
"name": updated_name,
"rules": [created_rule.id],
"spam_detection": {
"use_list_dnsbl": True,
"use_header_anomaly_detection": True,
},
},
)
# Some policy update responses may omit id; verify canonical state by refetching.
assert update_response.data.name == updated_name

refetch_response = e2e_client.policies.find(created_policy.id)
assert refetch_response.data.id == created_policy.id
assert refetch_response.data.name == updated_name
assert refetch_response.data.rules is not None
assert created_rule.id in refetch_response.data.rules

assert paginated_list_contains_id(e2e_client.policies.list, created_policy.id)

destroy_policy_response = e2e_client.policies.destroy(created_policy.id)
assert destroy_policy_response.request_id
e2e_resource_registry["policies"].remove(created_policy.id)

destroy_rule_response = e2e_client.rules.destroy(created_rule.id)
assert destroy_rule_response.request_id
e2e_resource_registry["rules"].remove(created_rule.id)

50 changes: 50 additions & 0 deletions tests/e2e/test_rules_e2e.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import pytest


@pytest.mark.e2e
def test_rules_lifecycle_e2e(
e2e_client, e2e_resource_registry, unique_name, paginated_list_contains_id
):
create_response = e2e_client.rules.create(
{
"name": unique_name("e2e-rule"),
"description": "Created by SDK e2e test",
"trigger": "inbound",
"match": {
"operator": "any",
"conditions": [
{
"field": "from.domain",
"operator": "is",
"value": "example.com",
}
],
},
"actions": [{"type": "archive"}],
}
)
created_rule = create_response.data
assert created_rule.id
e2e_resource_registry["rules"].append(created_rule.id)

find_response = e2e_client.rules.find(created_rule.id)
assert find_response.data.id == created_rule.id

updated_name = unique_name("e2e-rule-updated")
update_response = e2e_client.rules.update(
created_rule.id,
{
"name": updated_name,
"enabled": False,
"actions": [{"type": "mark_as_spam"}],
},
)
assert update_response.data.id == created_rule.id
assert update_response.data.name == updated_name

assert paginated_list_contains_id(e2e_client.rules.list, created_rule.id)

destroy_response = e2e_client.rules.destroy(created_rule.id)
assert destroy_response.request_id
e2e_resource_registry["rules"].remove(created_rule.id)

34 changes: 34 additions & 0 deletions tests/test_response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from nylas.models.response import ListResponse
from nylas.models.rules import Rule


class TestListResponse:
def test_from_dict_with_list_data(self):
response = {
"request_id": "req-123",
"data": [{"id": "rule-1", "name": "Rule One"}],
"next_cursor": "cursor-1",
}

parsed = ListResponse.from_dict(response, Rule)

assert parsed.request_id == "req-123"
assert parsed.next_cursor == "cursor-1"
assert len(parsed.data) == 1
assert parsed.data[0].id == "rule-1"

def test_from_dict_with_items_wrapper(self):
response = {
"request_id": "req-456",
"data": {
"items": [{"id": "rule-2", "name": "Rule Two"}],
"next_cursor": "cursor-2",
},
}

parsed = ListResponse.from_dict(response, Rule)

assert parsed.request_id == "req-456"
assert parsed.next_cursor == "cursor-2"
assert len(parsed.data) == 1
assert parsed.data[0].id == "rule-2"
Loading