From 5b07e8b0d8f62d3b8e08ef4cf53aa34e94880c57 Mon Sep 17 00:00:00 2001 From: pengfeiye Date: Fri, 24 Apr 2026 15:24:03 -0400 Subject: [PATCH] Add live E2E coverage for policies, rules, and lists --- tests/e2e/conftest.py | 104 +++++++++++++++++++++++++++++++++ tests/e2e/test_lists_e2e.py | 60 +++++++++++++++++++ tests/e2e/test_policies_e2e.py | 70 ++++++++++++++++++++++ tests/e2e/test_rules_e2e.py | 51 ++++++++++++++++ 4 files changed, 285 insertions(+) create mode 100644 tests/e2e/conftest.py create mode 100644 tests/e2e/test_lists_e2e.py create mode 100644 tests/e2e/test_policies_e2e.py create mode 100644 tests/e2e/test_rules_e2e.py diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py new file mode 100644 index 0000000..d8a5e2c --- /dev/null +++ b/tests/e2e/conftest.py @@ -0,0 +1,104 @@ +import os +from typing import Dict, List +from uuid import uuid4 + +import pytest + +from nylas import Client + + +_API_KEY_ENV_VARS = ("NYLAS_E2E_API_KEY", "NYLAS_API_KEY") +_API_URI_ENV_VARS = ("NYLAS_E2E_API_URI", "NYLAS_API_URI") + + +def _first_env_value(keys: tuple) -> str: + for key in keys: + value = os.getenv(key) + if value: + return value + return "" + + +def extract_list_items(response_data): + """ + Normalize list endpoint response payloads across API shapes. + """ + if isinstance(response_data, list): + return response_data + + if isinstance(response_data, dict): + items = response_data.get("items") + if isinstance(items, list): + return items + + return [] + + +def raw_list_ids(client: Client, path: str, id_key: str = "id", query_params=None): + json_response, _headers = client.http_client._execute( + "GET", + path, + None, + query_params or {"limit": 200}, + None, + ) + response_data = json_response.get("data") + items = extract_list_items(response_data) + return {item.get(id_key) for item in items if isinstance(item, dict) and item.get(id_key)} + + +@pytest.fixture +def raw_list_ids_helper(): + return raw_list_ids + + +@pytest.fixture(scope="session") +def e2e_client() -> Client: + api_key = _first_env_value(_API_KEY_ENV_VARS) + if not api_key: + pytest.skip( + "E2E tests require NYLAS_E2E_API_KEY (or NYLAS_API_KEY) to be set." + ) + + api_uri = _first_env_value(_API_URI_ENV_VARS) + 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 + diff --git a/tests/e2e/test_lists_e2e.py b/tests/e2e/test_lists_e2e.py new file mode 100644 index 0000000..a9778d1 --- /dev/null +++ b/tests/e2e/test_lists_e2e.py @@ -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) + diff --git a/tests/e2e/test_policies_e2e.py b/tests/e2e/test_policies_e2e.py new file mode 100644 index 0000000..0122608 --- /dev/null +++ b/tests/e2e/test_policies_e2e.py @@ -0,0 +1,70 @@ +import pytest + + +@pytest.mark.e2e +def test_policies_lifecycle_with_rule_association_e2e( + e2e_client, e2e_resource_registry, unique_name, raw_list_ids_helper +): + 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 + + returned_policy_ids = raw_list_ids_helper(e2e_client, "/v3/policies") + assert created_policy.id in returned_policy_ids + + 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) + diff --git a/tests/e2e/test_rules_e2e.py b/tests/e2e/test_rules_e2e.py new file mode 100644 index 0000000..4b7d81d --- /dev/null +++ b/tests/e2e/test_rules_e2e.py @@ -0,0 +1,51 @@ +import pytest + + +@pytest.mark.e2e +def test_rules_lifecycle_e2e( + e2e_client, e2e_resource_registry, unique_name, raw_list_ids_helper +): + 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 + + returned_rule_ids = raw_list_ids_helper(e2e_client, "/v3/rules") + assert created_rule.id in returned_rule_ids + + destroy_response = e2e_client.rules.destroy(created_rule.id) + assert destroy_response.request_id + e2e_resource_registry["rules"].remove(created_rule.id) +