Skip to content

Commit afff2e1

Browse files
feat: Add unit tests for /create-newsletter endpoint
1 parent 1b7e4f5 commit afff2e1

File tree

5 files changed

+139
-4
lines changed

5 files changed

+139
-4
lines changed

docker/init-db.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-E
99
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
1010
1111
-- You can add other initialization SQL here
12+
CREATE DATABASE mxtoaitest;
1213
1314
GRANT ALL PRIVILEGES ON DATABASE $POSTGRES_DB TO $POSTGRES_USER;
1415
EOSQL

docker/worker.dockerfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ RUN poetry config virtualenvs.create false && poetry install --no-root --no-inte
2626
# Copy application code
2727
COPY mxgo ./mxgo
2828

29+
# Copy test code into the container
30+
COPY ./tests /app/tests
31+
2932
# Create directories
3033
RUN mkdir -p /app/attachments
3134

mxgo/tools/scheduled_tasks_tool.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
from mxgo.config import SCHEDULED_TASKS_MINIMUM_INTERVAL_HOURS
1111
from mxgo.crud import create_task, delete_task, update_task_status
1212
from mxgo.db import init_db_connection
13+
from mxgo.email_handles import DEFAULT_EMAIL_HANDLES
14+
from mxgo.instruction_resolver import ProcessingInstructionsResolver
1315
from mxgo.models import TaskStatus
1416
from mxgo.request_context import RequestContext
1517
from mxgo.scheduling.scheduled_task_executor import execute_scheduled_task
@@ -247,11 +249,23 @@ def forward(
247249
}
248250

249251
try:
252+
# Resolve the handle alias before validation
253+
resolved_handle_alias = future_handle_alias
254+
if future_handle_alias:
255+
try:
256+
# Resolve the provided alias (e.g., 'remind') to its canonical handle (e.g., 'schedule')
257+
resolver = ProcessingInstructionsResolver(DEFAULT_EMAIL_HANDLES)
258+
resolved_instructions = resolver(future_handle_alias)
259+
resolved_handle_alias = resolved_instructions.handle
260+
logger.info(f"Resolved future handle alias '{future_handle_alias}' to '{resolved_handle_alias}'")
261+
except Exception:
262+
logger.warning(f"Could not resolve handle alias '{future_handle_alias}'. Validation might fail.")
263+
250264
# Validate input using Pydantic
251265
input_data = ScheduledTaskInput(
252266
cron_expression=cron_expression,
253267
distilled_future_task_instructions=distilled_future_task_instructions,
254-
future_handle_alias=future_handle_alias,
268+
future_handle_alias=resolved_handle_alias,
255269
start_time=start_time,
256270
end_time=end_time,
257271
)

poetry.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/test_api.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import mxgo.validators
1616
from mxgo._logging import get_logger
1717
from mxgo.api import app
18+
from mxgo.config import NEWSLETTER_LIMITS_BY_PLAN
1819
from mxgo.schemas import EmailSuggestionResponse, SuggestionDetail, UserPlan
1920
from tests.generate_test_jwt import generate_test_jwt
2021

@@ -1207,3 +1208,119 @@ def test_user_info_api_different_user_emails(
12071208
# Verify the correct email was used for lookups
12081209
mock_get_user_plan.assert_called_once_with("[email protected]")
12091210
mock_get_customer_id.assert_called_once_with("[email protected]")
1211+
1212+
1213+
@patch("mxgo.auth.JWT_SECRET", "test_secret_key_for_development_only")
1214+
class TestCreateNewsletter:
1215+
@pytest.fixture
1216+
def mock_dependencies(self):
1217+
"""Mocks all external dependencies for the newsletter endpoint."""
1218+
with patch("mxgo.api.user.get_user_plan", new_callable=AsyncMock) as mock_get_plan, \
1219+
patch("mxgo.api.crud.count_active_tasks_for_user") as mock_count_tasks, \
1220+
patch("mxgo.api.whitelist.is_email_whitelisted", new_callable=AsyncMock) as mock_is_whitelisted, \
1221+
patch("mxgo.api.Scheduler.add_job") as mock_add_job, \
1222+
patch("mxgo.api.process_email_task.send") as mock_send_task:
1223+
1224+
# Default happy path mocks
1225+
mock_get_plan.return_value = UserPlan.BETA
1226+
mock_count_tasks.return_value = 0
1227+
mock_is_whitelisted.return_value = (True, True) # Assume user is whitelisted
1228+
1229+
yield {
1230+
"get_plan": mock_get_plan,
1231+
"count_tasks": mock_count_tasks,
1232+
"is_whitelisted": mock_is_whitelisted,
1233+
"add_job": mock_add_job,
1234+
"send_task": mock_send_task,
1235+
}
1236+
1237+
def test_create_newsletter_success_whitelisted(self, mock_dependencies):
1238+
"""Test successful newsletter creation for a whitelisted BETA user."""
1239+
jwt_token = generate_test_jwt(email="[email protected]", user_id="test_user_123")
1240+
1241+
response = client.post(
1242+
"/create-newsletter",
1243+
headers={"Authorization": f"Bearer {jwt_token}"},
1244+
json={
1245+
"prompt": "Weekly AI news",
1246+
"schedule": {
1247+
"type": "RECURRING_WEEKLY",
1248+
"recurring_weekly": {"days": ["friday"], "time": "10:00"},
1249+
},
1250+
},
1251+
)
1252+
assert response.status_code == 200
1253+
data = response.json()
1254+
assert data["is_scheduled"] is True
1255+
assert data["is_whitelisted"] is True
1256+
assert data["sample_email_sent"] is True
1257+
assert len(data["scheduled_task_ids"]) == 1
1258+
mock_dependencies["send_task"].assert_called_once()
1259+
1260+
def test_create_newsletter_task_limit_exceeded(self, mock_dependencies):
1261+
"""Test that newsletter creation fails if the task limit is reached."""
1262+
mock_dependencies["get_plan"].return_value = UserPlan.BETA
1263+
1264+
jwt_token = generate_test_jwt(email="[email protected]", user_id="test_user_123")
1265+
1266+
# Set current tasks to the max allowed for BETA plan using the config variable
1267+
mock_dependencies["count_tasks"].return_value = NEWSLETTER_LIMITS_BY_PLAN[UserPlan.BETA]["max_tasks"]
1268+
1269+
response = client.post(
1270+
"/create-newsletter",
1271+
headers={"Authorization": f"Bearer {jwt_token}"},
1272+
json={
1273+
"prompt": "Another newsletter",
1274+
"schedule": {
1275+
"type": "RECURRING_WEEKLY",
1276+
"recurring_weekly": {"days": ["monday"], "time": "08:00"},
1277+
},
1278+
},
1279+
)
1280+
assert response.status_code == 403
1281+
assert "Newsletter limit" in response.json()["detail"]
1282+
1283+
def test_create_newsletter_interval_too_frequent(self, mock_dependencies):
1284+
"""Test that creation fails if the cron interval is too short for the user's plan."""
1285+
mock_dependencies["get_plan"].return_value = UserPlan.BETA
1286+
1287+
jwt_token = generate_test_jwt(email="[email protected]", user_id="test_user_123")
1288+
1289+
response = client.post(
1290+
"/create-newsletter",
1291+
headers={"Authorization": f"Bearer {jwt_token}"},
1292+
json={
1293+
"prompt": "Daily newsletter",
1294+
"schedule": {
1295+
"type": "RECURRING_WEEKLY",
1296+
"recurring_weekly": {"days": ["monday", "tuesday"], "time": "07:00"},
1297+
},
1298+
},
1299+
)
1300+
assert response.status_code == 400
1301+
assert "Cron interval is too frequent" in response.json()["detail"]
1302+
1303+
def test_create_newsletter_not_whitelisted(self, mock_dependencies):
1304+
"""Test behavior for a non-whitelisted user."""
1305+
mock_dependencies["is_whitelisted"].return_value = (False, False)
1306+
jwt_token = generate_test_jwt(email="[email protected]", user_id="test_user_123")
1307+
1308+
with patch("mxgo.api.whitelist.trigger_automatic_verification", new_callable=AsyncMock) as mock_trigger_verify:
1309+
response = client.post(
1310+
"/create-newsletter",
1311+
headers={"Authorization": f"Bearer {jwt_token}"},
1312+
json={
1313+
"prompt": "A test newsletter",
1314+
"schedule": {
1315+
"type": "RECURRING_WEEKLY",
1316+
"recurring_weekly": {"days": ["saturday"], "time": "12:00"},
1317+
},
1318+
},
1319+
)
1320+
assert response.status_code == 200
1321+
data = response.json()
1322+
assert data["is_scheduled"] is True
1323+
assert data["is_whitelisted"] is False
1324+
assert data["sample_email_sent"] is False
1325+
mock_dependencies["send_task"].assert_not_called()
1326+
mock_trigger_verify.assert_called_once()

0 commit comments

Comments
 (0)