import pytest import requests import time import os from datetime import datetime, timezone, timedelta @pytest.mark.e2e class TestAuthentication: def test_invalid_api_key_returns_401(self, api_base_url): """Invalid API key should return 401 Unauthorized.""" headers = {"Authorization": "Bearer invalid-fake-key-123"} response = requests.get(f"{api_base_url}/schedules/recurring", headers=headers) assert response.status_code == 401 def test_no_auth_header_returns_403(self, api_base_url): """Missing auth header should return 403.""" response = requests.get(f"{api_base_url}/schedules/recurring") assert response.status_code == 403 def test_api_key_not_in_response(self, api_base_url, valid_letta_api_key, valid_letta_agent_id): """API key should never be returned in responses.""" headers = {"Authorization": f"Bearer {valid_letta_api_key}"} # Create schedule payload = { "agent_id": valid_letta_agent_id, "cron": "*/5 * * * *", "message": "Test message", "role": "user" } response = requests.post(f"{api_base_url}/schedules/recurring", json=payload, headers=headers) # API key should not be in response data = response.json() assert "api_key" not in data # Clean up schedule_id = data["id"] requests.delete(f"{api_base_url}/schedules/recurring/{schedule_id}", headers=headers) @pytest.mark.e2e class TestRecurringScheduleCRUD: def test_create_recurring_schedule(self, api_base_url, valid_letta_api_key, valid_letta_agent_id): """Should successfully create a recurring schedule.""" headers = {"Authorization": f"Bearer {valid_letta_api_key}"} payload = { "agent_id": valid_letta_agent_id, "cron": "0 9 * * *", "message": "Daily morning message", "role": "user" } response = requests.post(f"{api_base_url}/schedules/recurring", json=payload, headers=headers) assert response.status_code == 201 data = response.json() assert "id" in data assert data["cron"] == "0 9 * * *" assert data["message"] == "Daily morning message" # Clean up requests.delete(f"{api_base_url}/schedules/recurring/{data['id']}", headers=headers) def test_list_recurring_schedules(self, api_base_url, valid_letta_api_key, valid_letta_agent_id): """List should only return schedules for authenticated user.""" headers = {"Authorization": f"Bearer {valid_letta_api_key}"} # Create a schedule payload = { "agent_id": valid_letta_agent_id, "cron": "*/10 * * * *", "message": "Test", "role": "user" } create_response = requests.post(f"{api_base_url}/schedules/recurring", json=payload, headers=headers) schedule_id = create_response.json()["id"] # List schedules response = requests.get(f"{api_base_url}/schedules/recurring", headers=headers) assert response.status_code == 200 schedules = response.json() assert isinstance(schedules, list) # Find our schedule our_schedule = next((s for s in schedules if s["id"] == schedule_id), None) assert our_schedule is not None assert our_schedule["cron"] == "*/10 * * * *" # Clean up requests.delete(f"{api_base_url}/schedules/recurring/{schedule_id}", headers=headers) def test_get_recurring_schedule(self, api_base_url, valid_letta_api_key, valid_letta_agent_id): """Get should return specific schedule.""" headers = {"Authorization": f"Bearer {valid_letta_api_key}"} # Create schedule payload = { "agent_id": valid_letta_agent_id, "cron": "0 12 * * *", "message": "Noon message", "role": "user" } create_response = requests.post(f"{api_base_url}/schedules/recurring", json=payload, headers=headers) schedule_id = create_response.json()["id"] # Get schedule response = requests.get(f"{api_base_url}/schedules/recurring/{schedule_id}", headers=headers) assert response.status_code == 200 data = response.json() assert data["id"] == schedule_id assert data["cron"] == "0 12 * * *" # Clean up requests.delete(f"{api_base_url}/schedules/recurring/{schedule_id}", headers=headers) def test_delete_recurring_schedule(self, api_base_url, valid_letta_api_key, valid_letta_agent_id): """Delete should remove schedule.""" headers = {"Authorization": f"Bearer {valid_letta_api_key}"} # Create schedule payload = { "agent_id": valid_letta_agent_id, "cron": "0 0 * * *", "message": "To be deleted", "role": "user" } create_response = requests.post(f"{api_base_url}/schedules/recurring", json=payload, headers=headers) schedule_id = create_response.json()["id"] # Delete schedule delete_response = requests.delete(f"{api_base_url}/schedules/recurring/{schedule_id}", headers=headers) assert delete_response.status_code == 200 # Verify it's gone get_response = requests.get(f"{api_base_url}/schedules/recurring/{schedule_id}", headers=headers) assert get_response.status_code == 404 def test_delete_nonexistent_returns_404(self, api_base_url, valid_letta_api_key): """Deleting non-existent schedule should return 404.""" headers = {"Authorization": f"Bearer {valid_letta_api_key}"} response = requests.delete(f"{api_base_url}/schedules/recurring/fake-uuid-123", headers=headers) assert response.status_code == 404 @pytest.mark.e2e class TestOneTimeScheduleCRUD: def test_create_onetime_schedule(self, api_base_url, valid_letta_api_key, valid_letta_agent_id): """Should successfully create a one-time schedule.""" headers = {"Authorization": f"Bearer {valid_letta_api_key}"} future_time = datetime.now(timezone.utc) + timedelta(hours=1) payload = { "agent_id": valid_letta_agent_id, "execute_at": future_time.isoformat(), "message": "Future message", "role": "user" } response = requests.post(f"{api_base_url}/schedules/one-time", json=payload, headers=headers) assert response.status_code == 201 data = response.json() assert "id" in data assert data["execute_at"] == future_time.isoformat() # Clean up requests.delete(f"{api_base_url}/schedules/one-time/{data['id']}", headers=headers) def test_list_onetime_schedules(self, api_base_url, valid_letta_api_key, valid_letta_agent_id): """List should only return user's schedules.""" headers = {"Authorization": f"Bearer {valid_letta_api_key}"} future_time = datetime.now(timezone.utc) + timedelta(hours=2) payload = { "agent_id": valid_letta_agent_id, "execute_at": future_time.isoformat(), "message": "Test", "role": "user" } create_response = requests.post(f"{api_base_url}/schedules/one-time", json=payload, headers=headers) schedule_id = create_response.json()["id"] # List schedules response = requests.get(f"{api_base_url}/schedules/one-time", headers=headers) assert response.status_code == 200 schedules = response.json() our_schedule = next((s for s in schedules if s["id"] == schedule_id), None) assert our_schedule is not None # Clean up requests.delete(f"{api_base_url}/schedules/one-time/{schedule_id}", headers=headers) def test_delete_onetime_schedule(self, api_base_url, valid_letta_api_key, valid_letta_agent_id): """Delete should remove one-time schedule.""" headers = {"Authorization": f"Bearer {valid_letta_api_key}"} future_time = datetime.now(timezone.utc) + timedelta(hours=3) payload = { "agent_id": valid_letta_agent_id, "execute_at": future_time.isoformat(), "message": "To be deleted", "role": "user" } create_response = requests.post(f"{api_base_url}/schedules/one-time", json=payload, headers=headers) schedule_id = create_response.json()["id"] # Delete delete_response = requests.delete(f"{api_base_url}/schedules/one-time/{schedule_id}", headers=headers) assert delete_response.status_code == 200 # Verify gone get_response = requests.get(f"{api_base_url}/schedules/one-time/{schedule_id}", headers=headers) assert get_response.status_code == 404 @pytest.mark.e2e class TestResults: def test_list_results(self, api_base_url, valid_letta_api_key): """Should list execution results.""" headers = {"Authorization": f"Bearer {valid_letta_api_key}"} response = requests.get(f"{api_base_url}/results", headers=headers) assert response.status_code == 200 assert isinstance(response.json(), list) def test_get_result_nonexistent_returns_404(self, api_base_url, valid_letta_api_key): """Getting non-existent result should return 404.""" headers = {"Authorization": f"Bearer {valid_letta_api_key}"} response = requests.get(f"{api_base_url}/results/fake-uuid-123", headers=headers) assert response.status_code == 404 @pytest.mark.e2e @pytest.mark.slow class TestExecution: def test_past_onetime_executes_immediately(self, api_base_url, valid_letta_api_key, valid_letta_agent_id): """One-time schedule in the past should execute within 1 minute.""" headers = {"Authorization": f"Bearer {valid_letta_api_key}"} past_time = datetime.now(timezone.utc) - timedelta(seconds=30) payload = { "agent_id": valid_letta_agent_id, "execute_at": past_time.isoformat(), "message": "Should execute immediately", "role": "user" } create_response = requests.post(f"{api_base_url}/schedules/one-time", json=payload, headers=headers) schedule_id = create_response.json()["id"] # Wait up to 90 seconds for execution max_wait = 90 for i in range(max_wait): time.sleep(1) # Check if result exists result_response = requests.get(f"{api_base_url}/results/{schedule_id}", headers=headers) if result_response.status_code == 200: result = result_response.json() assert "run_id" in result assert result["schedule_type"] == "one-time" print(f"✓ Executed in {i+1} seconds, run_id: {result['run_id']}") return pytest.fail(f"Schedule did not execute within {max_wait} seconds") def test_onetime_schedule_deleted_after_execution(self, api_base_url, valid_letta_api_key, valid_letta_agent_id): """One-time schedule should be deleted from filesystem after execution.""" headers = {"Authorization": f"Bearer {valid_letta_api_key}"} past_time = datetime.now(timezone.utc) - timedelta(seconds=30) payload = { "agent_id": valid_letta_agent_id, "execute_at": past_time.isoformat(), "message": "Should be deleted after execution", "role": "user" } create_response = requests.post(f"{api_base_url}/schedules/one-time", json=payload, headers=headers) schedule_id = create_response.json()["id"] # Wait for execution for _ in range(90): time.sleep(1) result_response = requests.get(f"{api_base_url}/results/{schedule_id}", headers=headers) if result_response.status_code == 200: break # Schedule should be deleted schedule_response = requests.get(f"{api_base_url}/schedules/one-time/{schedule_id}", headers=headers) assert schedule_response.status_code == 404 def test_result_persists_after_schedule_deletion(self, api_base_url, valid_letta_api_key, valid_letta_agent_id): """Execution result should persist even after schedule is deleted.""" headers = {"Authorization": f"Bearer {valid_letta_api_key}"} past_time = datetime.now(timezone.utc) - timedelta(seconds=30) payload = { "agent_id": valid_letta_agent_id, "execute_at": past_time.isoformat(), "message": "Test result persistence", "role": "user" } create_response = requests.post(f"{api_base_url}/schedules/one-time", json=payload, headers=headers) schedule_id = create_response.json()["id"] # Wait for execution for _ in range(90): time.sleep(1) result_response = requests.get(f"{api_base_url}/results/{schedule_id}", headers=headers) if result_response.status_code == 200: result = result_response.json() break # Result should still exist final_result = requests.get(f"{api_base_url}/results/{schedule_id}", headers=headers) assert final_result.status_code == 200 assert "run_id" in final_result.json() def test_no_duplicate_execution(self, api_base_url, valid_letta_api_key, valid_letta_agent_id): """One-time schedule should execute exactly once, even if multiple executors spawn.""" headers = {"Authorization": f"Bearer {valid_letta_api_key}"} past_time = datetime.now(timezone.utc) - timedelta(seconds=30) payload = { "agent_id": valid_letta_agent_id, "execute_at": past_time.isoformat(), "message": "Should only execute once", "role": "user" } create_response = requests.post(f"{api_base_url}/schedules/one-time", json=payload, headers=headers) schedule_id = create_response.json()["id"] # Wait for execution time.sleep(90) # Check result result_response = requests.get(f"{api_base_url}/results/{schedule_id}", headers=headers) assert result_response.status_code == 200 # There should be exactly one result file, indicating one execution # (We can't directly verify this without filesystem access, but we can check result exists) result = result_response.json() assert result["schedule_id"] == schedule_id assert "run_id" in result @pytest.mark.e2e class TestAuthorization: def test_user_isolation_list(self, api_base_url, valid_letta_api_key): """Users should only see their own schedules in list.""" # This test requires a second valid API key # Skip if not available second_key = os.getenv("LETTA_API_KEY_2") if not second_key: pytest.skip("LETTA_API_KEY_2 not set for multi-user testing") headers1 = {"Authorization": f"Bearer {valid_letta_api_key}"} headers2 = {"Authorization": f"Bearer {second_key}"} # User 1's list should not contain User 2's schedules response1 = requests.get(f"{api_base_url}/schedules/recurring", headers=headers1) response2 = requests.get(f"{api_base_url}/schedules/recurring", headers=headers2) schedules1 = response1.json() schedules2 = response2.json() # Lists should be independent (no overlap) ids1 = {s["id"] for s in schedules1} ids2 = {s["id"] for s in schedules2} assert ids1.isdisjoint(ids2), "Users should not see each other's schedules"