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
54 changes: 54 additions & 0 deletions src/admin/blueprints/publisher_partners.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,13 +217,67 @@ def sync_publisher_partners(tenant_id: str) -> Response | tuple[Response, int]:

session.commit()

# Create mock properties for verified publishers (only for mock adapters - they don't have real adagents.json)
properties_created = 0
tags_created = 0
if is_mock and verified_domains:
from src.core.database.models import AuthorizedProperty, PropertyTag

# Ensure 'all_inventory' tag exists
tag_stmt = select(PropertyTag).where(
PropertyTag.tenant_id == tenant_id, PropertyTag.tag_id == "all_inventory"
)
all_inventory_tag = session.scalars(tag_stmt).first()
if not all_inventory_tag:
all_inventory_tag = PropertyTag(
tag_id="all_inventory",
tenant_id=tenant_id,
name="All Inventory",
description="Default tag that applies to all properties.",
created_at=datetime.now(UTC),
updated_at=datetime.now(UTC),
)
session.add(all_inventory_tag)
tags_created += 1
logger.info(f"Created 'all_inventory' tag for tenant {tenant_id}")

# Create a mock property for each publisher domain
for domain in verified_domains:
property_id = f"website_{domain.replace('.', '_').replace('-', '_')}"
prop_stmt = select(AuthorizedProperty).where(
AuthorizedProperty.tenant_id == tenant_id,
AuthorizedProperty.property_id == property_id,
)
existing = session.scalars(prop_stmt).first()
if not existing:
mock_property = AuthorizedProperty(
tenant_id=tenant_id,
property_id=property_id,
property_type="website",
name=domain,
publisher_domain=domain,
identifiers=[{"type": "domain", "value": domain}],
tags=["all_inventory"],
verification_status="verified",
verification_checked_at=datetime.now(UTC),
created_at=datetime.now(UTC),
updated_at=datetime.now(UTC),
)
session.add(mock_property)
properties_created += 1
logger.info(f"Created mock property for {domain}")

session.commit()

return jsonify(
{
"message": f"Sync completed ({reason_str} - auto-verified)",
"synced": len(partners),
"verified": len(partners),
"errors": 0,
"total": len(partners),
"properties_created": properties_created,
"tags_created": tags_created,
}
)

Expand Down
2 changes: 1 addition & 1 deletion src/services/delivery_webhook_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

# 1 hour because AdCP protocol has frequency options hourly, daily and monthly
# Configurable via env var for testing
SLEEP_INTERVAL_SECONDS = int(os.getenv("DELIVERY_WEBHOOK_INTERVAL", "3600"))
SLEEP_INTERVAL_SECONDS = int(os.getenv("DELIVERY_WEBHOOK_INTERVAL") or "3600")


class DeliveryWebhookScheduler:
Expand Down
2 changes: 1 addition & 1 deletion src/services/media_buy_status_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
logger = logging.getLogger(__name__)

# Configurable via env var - default 60 seconds
STATUS_CHECK_INTERVAL_SECONDS = int(os.getenv("MEDIA_BUY_STATUS_CHECK_INTERVAL", "60"))
STATUS_CHECK_INTERVAL_SECONDS = int(os.getenv("MEDIA_BUY_STATUS_CHECK_INTERVAL") or "60")


class MediaBuyStatusScheduler:
Expand Down
221 changes: 221 additions & 0 deletions tests/integration/test_mock_adapter_publisher_sync.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
"""Tests for mock adapter publisher sync property creation.

These tests ensure that when syncing publishers for mock/dev adapters,
AuthorizedProperty and PropertyTag records are created correctly.

This is a regression test for an issue where mock tenants would have
verified publishers but no properties, causing the product creation UI
to show empty property/tag lists.
"""

import pytest
from sqlalchemy import select

from src.core.database.database_session import get_db_session
from src.core.database.models import (
AdapterConfig,
AuthorizedProperty,
PropertyTag,
PublisherPartner,
Tenant,
)


@pytest.mark.requires_db
class TestMockAdapterPublisherSync:
"""Test that mock adapter publisher sync creates properties and tags."""

@pytest.fixture
def mock_tenant(self, integration_db):
"""Create a tenant with mock adapter configuration."""
from datetime import UTC, datetime

with get_db_session() as session:
# Create tenant
tenant = Tenant(
tenant_id="test_mock_sync",
name="Test Mock Sync Tenant",
subdomain="test-mock-sync",
ad_server="mock",
authorized_emails=["test@example.com"],
created_at=datetime.now(UTC),
updated_at=datetime.now(UTC),
)
session.add(tenant)
session.flush()

# Create adapter config for mock adapter
adapter_config = AdapterConfig(
tenant_id="test_mock_sync",
adapter_type="mock",
)
session.add(adapter_config)
session.commit()

yield "test_mock_sync"

# Cleanup
session.execute(select(AuthorizedProperty).where(AuthorizedProperty.tenant_id == "test_mock_sync"))
for prop in session.scalars(
select(AuthorizedProperty).where(AuthorizedProperty.tenant_id == "test_mock_sync")
).all():
session.delete(prop)
for tag in session.scalars(select(PropertyTag).where(PropertyTag.tenant_id == "test_mock_sync")).all():
session.delete(tag)
for partner in session.scalars(
select(PublisherPartner).where(PublisherPartner.tenant_id == "test_mock_sync")
).all():
session.delete(partner)
session.delete(adapter_config)
session.delete(tenant)
session.commit()

@pytest.fixture
def publisher_partner(self, mock_tenant):
"""Create a publisher partner for the mock tenant."""
from datetime import UTC, datetime

with get_db_session() as session:
partner = PublisherPartner(
tenant_id=mock_tenant,
publisher_domain="example.com",
display_name="Example Publisher",
sync_status="pending",
is_verified=False,
created_at=datetime.now(UTC),
updated_at=datetime.now(UTC),
)
session.add(partner)
session.commit()

return partner.id

def test_sync_creates_property_tag(self, mock_tenant, publisher_partner):
"""Test that sync creates 'all_inventory' PropertyTag for mock tenants."""
from unittest.mock import patch

from src.admin.app import create_app
from src.admin.blueprints.publisher_partners import sync_publisher_partners
from src.core.config import AppConfig

# Mock config to return development environment (triggers auto-verify)
mock_config = AppConfig()
mock_config.environment = "development"

app, _ = create_app()

with app.test_request_context():
with patch("src.admin.blueprints.publisher_partners.get_config", return_value=mock_config):
response = sync_publisher_partners(mock_tenant)
data = response.get_json()

assert data["verified"] == 1
assert data.get("tags_created", 0) >= 0 # May be 0 if tag already exists

# Verify PropertyTag was created
with get_db_session() as session:
tag = session.scalars(
select(PropertyTag).where(PropertyTag.tenant_id == mock_tenant, PropertyTag.tag_id == "all_inventory")
).first()

assert tag is not None, "all_inventory PropertyTag should be created"
assert tag.name == "All Inventory"

def test_sync_creates_authorized_property(self, mock_tenant, publisher_partner):
"""Test that sync creates AuthorizedProperty for each publisher domain."""
from unittest.mock import patch

from src.admin.app import create_app
from src.admin.blueprints.publisher_partners import sync_publisher_partners
from src.core.config import AppConfig

mock_config = AppConfig()
mock_config.environment = "development"

app, _ = create_app()

with app.test_request_context():
with patch("src.admin.blueprints.publisher_partners.get_config", return_value=mock_config):
response = sync_publisher_partners(mock_tenant)
data = response.get_json()

assert data["verified"] == 1
assert data.get("properties_created", 0) >= 1

# Verify AuthorizedProperty was created
with get_db_session() as session:
props = session.scalars(select(AuthorizedProperty).where(AuthorizedProperty.tenant_id == mock_tenant)).all()

assert len(props) >= 1, "At least one AuthorizedProperty should be created"

# Find the property for example.com
example_prop = next((p for p in props if "example" in p.property_id), None)
assert example_prop is not None, "Property for example.com should exist"
assert example_prop.verification_status == "verified"
assert example_prop.property_type == "website"
assert "all_inventory" in (example_prop.tags or [])

def test_sync_property_has_verified_status(self, mock_tenant, publisher_partner):
"""Test that created properties have verification_status='verified'.

This is critical because the product creation UI only shows properties
with verification_status='verified'.
"""
from unittest.mock import patch

from src.admin.app import create_app
from src.admin.blueprints.publisher_partners import sync_publisher_partners
from src.core.config import AppConfig

mock_config = AppConfig()
mock_config.environment = "development"

app, _ = create_app()

with app.test_request_context():
with patch("src.admin.blueprints.publisher_partners.get_config", return_value=mock_config):
sync_publisher_partners(mock_tenant)

# Verify all properties have verified status
with get_db_session() as session:
props = session.scalars(select(AuthorizedProperty).where(AuthorizedProperty.tenant_id == mock_tenant)).all()

for prop in props:
assert prop.verification_status == "verified", (
f"Property {prop.property_id} should have verification_status='verified', "
f"got '{prop.verification_status}'"
)

def test_sync_is_idempotent(self, mock_tenant, publisher_partner):
"""Test that running sync multiple times doesn't create duplicate records."""
from unittest.mock import patch

from src.admin.app import create_app
from src.admin.blueprints.publisher_partners import sync_publisher_partners
from src.core.config import AppConfig

mock_config = AppConfig()
mock_config.environment = "development"

app, _ = create_app()

# Run sync twice
with app.test_request_context():
with patch("src.admin.blueprints.publisher_partners.get_config", return_value=mock_config):
sync_publisher_partners(mock_tenant)
sync_publisher_partners(mock_tenant)

# Verify no duplicates
with get_db_session() as session:
tags = session.scalars(
select(PropertyTag).where(PropertyTag.tenant_id == mock_tenant, PropertyTag.tag_id == "all_inventory")
).all()
assert len(tags) == 1, "Should have exactly one all_inventory tag"

props = session.scalars(
select(AuthorizedProperty).where(
AuthorizedProperty.tenant_id == mock_tenant,
AuthorizedProperty.publisher_domain == "example.com",
)
).all()
assert len(props) == 1, "Should have exactly one property per domain"
Loading