Backeup configs & cleanup of files and db

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-13 03:31:20 +00:00
parent b5787081e2
commit f46fbe4343
70 changed files with 26362 additions and 6502 deletions

View File

@@ -1,2 +0,0 @@
# Billing tests

View File

@@ -1,299 +0,0 @@
"""
Concurrency tests for payment approval
Tests race conditions and concurrent approval attempts
"""
import pytest
from django.test import TestCase, TransactionTestCase
from django.contrib.auth import get_user_model
from django.db import transaction
from concurrent.futures import ThreadPoolExecutor, as_completed
from decimal import Decimal
from igny8_core.business.billing.models import (
Invoice, Payment, Subscription, Plan, Account
)
from igny8_core.business.billing.views import approve_payment
from unittest.mock import Mock
import threading
User = get_user_model()
class PaymentApprovalConcurrencyTest(TransactionTestCase):
"""Test concurrent payment approval scenarios"""
def setUp(self):
"""Set up test data"""
# Create admin user
self.admin = User.objects.create_user(
email='admin@test.com',
password='testpass123',
is_staff=True
)
# Create account
self.account = Account.objects.create(
name='Test Account',
owner=self.admin,
credit_balance=0
)
# Create plan
self.plan = Plan.objects.create(
name='Test Plan',
slug='test-plan',
price=Decimal('100.00'),
currency='USD',
billing_period='monthly',
included_credits=1000
)
# Create subscription
self.subscription = Subscription.objects.create(
account=self.account,
plan=self.plan,
status='pending_payment'
)
# Create invoice
self.invoice = Invoice.objects.create(
account=self.account,
invoice_number='INV-TEST-001',
status='pending',
subtotal=Decimal('100.00'),
total_amount=Decimal('100.00'),
currency='USD',
invoice_type='subscription'
)
# Create payment
self.payment = Payment.objects.create(
account=self.account,
invoice=self.invoice,
amount=Decimal('100.00'),
currency='USD',
payment_method='bank_transfer',
status='pending_approval',
manual_reference='TEST-REF-001'
)
def test_concurrent_approval_attempts(self):
"""
Test that only one concurrent approval succeeds
Multiple admins trying to approve same payment simultaneously
"""
num_threads = 5
success_count = 0
failure_count = 0
results = []
def approve_payment_thread(payment_id, admin_user):
"""Thread worker to approve payment"""
try:
# Simulate approval logic with transaction
with transaction.atomic():
payment = Payment.objects.select_for_update().get(id=payment_id)
# Check if already approved
if payment.status == 'succeeded':
return {'success': False, 'reason': 'already_approved'}
# Approve payment
payment.status = 'succeeded'
payment.approved_by = admin_user
payment.save()
# Update invoice
invoice = payment.invoice
invoice.status = 'paid'
invoice.save()
return {'success': True}
except Exception as e:
return {'success': False, 'error': str(e)}
# Create multiple threads attempting approval
with ThreadPoolExecutor(max_workers=num_threads) as executor:
futures = []
for i in range(num_threads):
future = executor.submit(approve_payment_thread, self.payment.id, self.admin)
futures.append(future)
# Collect results
for future in as_completed(futures):
result = future.result()
results.append(result)
if result.get('success'):
success_count += 1
else:
failure_count += 1
# Verify only one approval succeeded
self.assertEqual(success_count, 1, "Only one approval should succeed")
self.assertEqual(failure_count, num_threads - 1, "Other attempts should fail")
# Verify final state
payment = Payment.objects.get(id=self.payment.id)
self.assertEqual(payment.status, 'succeeded')
invoice = Invoice.objects.get(id=self.invoice.id)
self.assertEqual(invoice.status, 'paid')
def test_payment_and_invoice_consistency(self):
"""
Test that payment and invoice remain consistent under concurrent operations
"""
def read_payment_invoice(payment_id):
"""Read payment and invoice status"""
payment = Payment.objects.get(id=payment_id)
invoice = Invoice.objects.get(id=payment.invoice_id)
return {
'payment_status': payment.status,
'invoice_status': invoice.status,
'consistent': (
(payment.status == 'succeeded' and invoice.status == 'paid') or
(payment.status == 'pending_approval' and invoice.status == 'pending')
)
}
# Approve payment in one thread
def approve():
with transaction.atomic():
payment = Payment.objects.select_for_update().get(id=self.payment.id)
payment.status = 'succeeded'
payment.save()
invoice = Invoice.objects.select_for_update().get(id=self.invoice.id)
invoice.status = 'paid'
invoice.save()
# Read state in parallel threads
results = []
with ThreadPoolExecutor(max_workers=10) as executor:
# Start approval
approval_future = executor.submit(approve)
# Multiple concurrent reads
read_futures = [
executor.submit(read_payment_invoice, self.payment.id)
for _ in range(20)
]
# Wait for approval
approval_future.result()
# Collect read results
for future in as_completed(read_futures):
results.append(future.result())
# All reads should show consistent state
for result in results:
self.assertTrue(
result['consistent'],
f"Inconsistent state: payment={result['payment_status']}, invoice={result['invoice_status']}"
)
def test_double_approval_prevention(self):
"""
Test that payment cannot be approved twice
"""
# First approval
with transaction.atomic():
payment = Payment.objects.select_for_update().get(id=self.payment.id)
payment.status = 'succeeded'
payment.approved_by = self.admin
payment.save()
invoice = payment.invoice
invoice.status = 'paid'
invoice.save()
# Attempt second approval
result = None
try:
with transaction.atomic():
payment = Payment.objects.select_for_update().get(id=self.payment.id)
# Should detect already approved
if payment.status == 'succeeded':
result = 'already_approved'
else:
payment.status = 'succeeded'
payment.save()
result = 'approved'
except Exception as e:
result = f'error: {str(e)}'
self.assertEqual(result, 'already_approved', "Second approval should be prevented")
class CreditTransactionConcurrencyTest(TransactionTestCase):
"""Test concurrent credit additions/deductions"""
def setUp(self):
self.admin = User.objects.create_user(
email='admin@test.com',
password='testpass123'
)
self.account = Account.objects.create(
name='Test Account',
owner=self.admin,
credit_balance=1000
)
def test_concurrent_credit_deductions(self):
"""
Test that concurrent credit deductions maintain correct balance
"""
initial_balance = self.account.credit_balance
deduction_amount = 10
num_operations = 20
def deduct_credits(account_id, amount):
"""Deduct credits atomically"""
from igny8_core.business.billing.models import CreditTransaction
with transaction.atomic():
account = Account.objects.select_for_update().get(id=account_id)
# Check sufficient balance
if account.credit_balance < amount:
return {'success': False, 'reason': 'insufficient_credits'}
# Deduct credits
account.credit_balance -= amount
new_balance = account.credit_balance
account.save()
# Record transaction
CreditTransaction.objects.create(
account=account,
transaction_type='deduction',
amount=-amount,
balance_after=new_balance,
description='Test deduction'
)
return {'success': True, 'new_balance': new_balance}
# Concurrent deductions
with ThreadPoolExecutor(max_workers=10) as executor:
futures = [
executor.submit(deduct_credits, self.account.id, deduction_amount)
for _ in range(num_operations)
]
results = [future.result() for future in as_completed(futures)]
# Verify all succeeded
success_count = sum(1 for r in results if r.get('success'))
self.assertEqual(success_count, num_operations, "All deductions should succeed")
# Verify final balance
self.account.refresh_from_db()
expected_balance = initial_balance - (deduction_amount * num_operations)
self.assertEqual(
self.account.credit_balance,
expected_balance,
f"Final balance should be {expected_balance}"
)

View File

@@ -1,141 +0,0 @@
"""
Test payment method filtering by country
"""
from django.test import TestCase, Client
from django.contrib.auth import get_user_model
from igny8_core.business.billing.models import PaymentMethodConfig
User = get_user_model()
class PaymentMethodFilteringTest(TestCase):
"""Test payment method filtering by billing country"""
def setUp(self):
"""Create test payment method configs"""
# Global methods (available everywhere)
PaymentMethodConfig.objects.create(
country_code='*',
payment_method='stripe',
display_name='Credit/Debit Card',
is_enabled=True,
sort_order=1,
)
PaymentMethodConfig.objects.create(
country_code='*',
payment_method='paypal',
display_name='PayPal',
is_enabled=True,
sort_order=2,
)
# Country-specific methods
PaymentMethodConfig.objects.create(
country_code='GB',
payment_method='bank_transfer',
display_name='Bank Transfer (UK)',
is_enabled=True,
sort_order=3,
)
PaymentMethodConfig.objects.create(
country_code='IN',
payment_method='local_wallet',
display_name='UPI/Wallets',
is_enabled=True,
sort_order=4,
)
PaymentMethodConfig.objects.create(
country_code='PK',
payment_method='bank_transfer',
display_name='Bank Transfer (Pakistan)',
is_enabled=True,
sort_order=5,
)
# Disabled method (should not appear)
PaymentMethodConfig.objects.create(
country_code='*',
payment_method='manual',
display_name='Manual',
is_enabled=False,
sort_order=99,
)
self.client = Client()
def test_filter_payment_methods_by_us(self):
"""Test filtering for US country - should get only global methods"""
response = self.client.get('/api/v1/billing/admin/payment-methods/?country=US')
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertTrue(data['success'])
self.assertEqual(len(data['results']), 2) # Only stripe and paypal
methods = [m['type'] for m in data['results']]
self.assertIn('stripe', methods)
self.assertIn('paypal', methods)
def test_filter_payment_methods_by_gb(self):
"""Test filtering for GB - should get global + GB-specific"""
response = self.client.get('/api/v1/billing/admin/payment-methods/?country=GB')
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertTrue(data['success'])
self.assertEqual(len(data['results']), 3) # stripe, paypal, bank_transfer(GB)
methods = [m['type'] for m in data['results']]
self.assertIn('stripe', methods)
self.assertIn('paypal', methods)
self.assertIn('bank_transfer', methods)
def test_filter_payment_methods_by_in(self):
"""Test filtering for IN - should get global + IN-specific"""
response = self.client.get('/api/v1/billing/admin/payment-methods/?country=IN')
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertTrue(data['success'])
self.assertEqual(len(data['results']), 3) # stripe, paypal, local_wallet(IN)
methods = [m['type'] for m in data['results']]
self.assertIn('stripe', methods)
self.assertIn('paypal', methods)
self.assertIn('local_wallet', methods)
def test_disabled_methods_not_returned(self):
"""Test that disabled payment methods are not included"""
response = self.client.get('/api/v1/billing/admin/payment-methods/?country=*')
self.assertEqual(response.status_code, 200)
data = response.json()
methods = [m['type'] for m in data['results']]
self.assertNotIn('manual', methods) # Disabled method should not appear
def test_sort_order_respected(self):
\"\"\"Test that payment methods are returned in sort_order\"\"\"
response = self.client.get('/api/v1/billing/admin/payment-methods/?country=GB')
self.assertEqual(response.status_code, 200)
data = response.json()
# Verify first method has lowest sort_order
self.assertEqual(data['results'][0]['type'], 'stripe')
self.assertEqual(data['results'][0]['sort_order'], 1)
def test_default_country_fallback(self):
"""Test that missing country parameter defaults to global (*)\"\"\"\n response = self.client.get('/api/v1/billing/admin/payment-methods/')
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertTrue(data['success'])
# Should get at least global methods
methods = [m['type'] for m in data['results']]
self.assertIn('stripe', methods)
self.assertIn('paypal', methods)

View File

@@ -1,192 +0,0 @@
"""
Integration tests for payment workflow
"""
from django.test import TestCase
from django.contrib.auth import get_user_model
from django.utils import timezone
from decimal import Decimal
from datetime import timedelta
from igny8_core.auth.models import Account, Plan, Subscription
from igny8_core.business.billing.models import Invoice, Payment
from igny8_core.business.billing.services.invoice_service import InvoiceService
User = get_user_model()
class PaymentWorkflowIntegrationTest(TestCase):
"""Test complete payment workflow including invoice.subscription FK"""
def setUp(self):
"""Create test data"""
# Create plan
self.plan = Plan.objects.create(
name='Test Plan',
slug='test-plan',
price=Decimal('29.00'),
included_credits=1000,
max_sites=5,
)
# Create account
self.account = Account.objects.create(
name='Test Account',
slug='test-account',
status='pending_payment',
billing_country='US',
billing_email='test@example.com',
)
# Create user
self.user = User.objects.create_user(
username='testuser',
email='testuser@example.com',
password='testpass123',
account=self.account,
)
# Create subscription
billing_period_start = timezone.now()
billing_period_end = billing_period_start + timedelta(days=30)
self.subscription = Subscription.objects.create(
account=self.account,
plan=self.plan,
status='pending_payment',
current_period_start=billing_period_start,
current_period_end=billing_period_end,
)
def test_invoice_subscription_fk_relationship(self):
"""Test that invoice.subscription FK works correctly"""
# Create invoice via service
billing_period_start = timezone.now()
billing_period_end = billing_period_start + timedelta(days=30)
invoice = InvoiceService.create_subscription_invoice(
subscription=self.subscription,
billing_period_start=billing_period_start,
billing_period_end=billing_period_end,
)
# Verify FK relationship
self.assertIsNotNone(invoice.subscription)
self.assertEqual(invoice.subscription.id, self.subscription.id)
self.assertEqual(invoice.subscription.plan.id, self.plan.id)
# Verify can access subscription from invoice
self.assertEqual(invoice.subscription.account, self.account)
self.assertEqual(invoice.subscription.plan.name, 'Test Plan')
def test_payment_approval_with_subscription(self):
"""Test payment approval workflow uses invoice.subscription"""
# Create invoice
billing_period_start = timezone.now()
billing_period_end = billing_period_start + timedelta(days=30)
invoice = InvoiceService.create_subscription_invoice(
subscription=self.subscription,
billing_period_start=billing_period_start,
billing_period_end=billing_period_end,
)
# Create payment
payment = Payment.objects.create(
account=self.account,
invoice=invoice,
amount=invoice.total,
currency='USD',
status='pending_approval',
payment_method='bank_transfer',
manual_reference='TEST-REF-001',
)
# Verify payment links to invoice which links to subscription
self.assertIsNotNone(payment.invoice)
self.assertIsNotNone(payment.invoice.subscription)
self.assertEqual(payment.invoice.subscription.id, self.subscription.id)
# Simulate approval workflow
payment.status = 'succeeded'
payment.approved_by = self.user
payment.approved_at = timezone.now()
payment.save()
# Update related records
invoice.status = 'paid'
invoice.paid_at = timezone.now()
invoice.save()
subscription = invoice.subscription
subscription.status = 'active'
subscription.save()
# Verify workflow completed successfully
self.assertEqual(payment.status, 'succeeded')
self.assertEqual(invoice.status, 'paid')
self.assertEqual(subscription.status, 'active')
self.assertEqual(subscription.plan.included_credits, 1000)
def test_subscription_dates_not_null_for_paid_plans(self):
"""Test that subscription dates are set for paid plans"""
self.assertIsNotNone(self.subscription.current_period_start)
self.assertIsNotNone(self.subscription.current_period_end)
# Verify dates are in future
self.assertGreater(self.subscription.current_period_end, self.subscription.current_period_start)
def test_invoice_currency_based_on_country(self):
"""Test that invoice currency is set based on billing country"""
# Test US -> USD
self.account.billing_country = 'US'
self.account.save()
billing_period_start = timezone.now()
billing_period_end = billing_period_start + timedelta(days=30)
invoice_us = InvoiceService.create_subscription_invoice(
subscription=self.subscription,
billing_period_start=billing_period_start,
billing_period_end=billing_period_end,
)
self.assertEqual(invoice_us.currency, 'USD')
# Test GB -> GBP
self.account.billing_country = 'GB'
self.account.save()
invoice_gb = InvoiceService.create_subscription_invoice(
subscription=self.subscription,
billing_period_start=billing_period_start,
billing_period_end=billing_period_end,
)
self.assertEqual(invoice_gb.currency, 'GBP')
# Test IN -> INR
self.account.billing_country = 'IN'
self.account.save()
invoice_in = InvoiceService.create_subscription_invoice(
subscription=self.subscription,
billing_period_start=billing_period_start,
billing_period_end=billing_period_end,
)
self.assertEqual(invoice_in.currency, 'INR')
def test_invoice_due_date_grace_period(self):
"""Test that invoice due date uses grace period instead of billing_period_end"""
billing_period_start = timezone.now()
billing_period_end = billing_period_start + timedelta(days=30)
invoice = InvoiceService.create_subscription_invoice(
subscription=self.subscription,
billing_period_start=billing_period_start,
billing_period_end=billing_period_end,
)
# Verify due date is invoice_date + 7 days (grace period)
expected_due_date = invoice.invoice_date + timedelta(days=7)
self.assertEqual(invoice.due_date, expected_due_date)
# Verify it's NOT billing_period_end
self.assertNotEqual(invoice.due_date, billing_period_end.date())

View File

@@ -1,133 +0,0 @@
"""
Tests for Phase 4 credit deduction
"""
from unittest.mock import patch
from django.test import TestCase
from igny8_core.business.content.models import Content
from igny8_core.business.billing.services.credit_service import CreditService
from igny8_core.business.billing.constants import CREDIT_COSTS
from igny8_core.business.billing.exceptions import InsufficientCreditsError
from igny8_core.api.tests.test_integration_base import IntegrationTestBase
class Phase4CreditTests(IntegrationTestBase):
"""Tests for Phase 4 credit deduction"""
def setUp(self):
super().setUp()
# Set initial credits
self.account.credits = 1000
self.account.save()
def test_linking_deducts_correct_credits(self):
"""Test that linking deducts correct credits"""
cost = CreditService.get_credit_cost('linking')
expected_cost = CREDIT_COSTS.get('linking', 0)
self.assertEqual(cost, expected_cost)
self.assertEqual(cost, 8) # From constants
def test_optimization_deducts_correct_credits(self):
"""Test that optimization deducts correct credits based on word count"""
word_count = 500
cost = CreditService.get_credit_cost('optimization', word_count)
# Should be 1 credit per 200 words, so 500 words = 3 credits (max(1, 1 * 500/200) = 3)
expected = max(1, int(CREDIT_COSTS.get('optimization', 1) * (word_count / 200)))
self.assertEqual(cost, expected)
def test_optimization_credits_per_entry_point(self):
"""Test that optimization credits are same regardless of entry point"""
word_count = 400
# All entry points should use same credit calculation
cost = CreditService.get_credit_cost('optimization', word_count)
# 400 words = 2 credits (1 * 400/200)
self.assertEqual(cost, 2)
@patch('igny8_core.business.billing.services.credit_service.CreditService.deduct_credits')
def test_pipeline_deducts_credits_at_each_stage(self, mock_deduct):
"""Test that pipeline deducts credits at each stage"""
from igny8_core.business.content.services.content_pipeline_service import ContentPipelineService
from igny8_core.business.linking.services.linker_service import LinkerService
from igny8_core.business.optimization.services.optimizer_service import OptimizerService
content = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title="Test",
word_count=400,
source='igny8'
)
# Mock the services
with patch.object(LinkerService, 'process') as mock_link, \
patch.object(OptimizerService, 'optimize_from_writer') as mock_optimize:
mock_link.return_value = content
mock_optimize.return_value = content
service = ContentPipelineService()
service.process_writer_content(content.id)
# Should deduct credits for both linking and optimization
self.assertGreater(mock_deduct.call_count, 0)
def test_insufficient_credits_blocks_linking(self):
"""Test that insufficient credits blocks linking"""
self.account.credits = 5 # Less than linking cost (8)
self.account.save()
with self.assertRaises(InsufficientCreditsError):
CreditService.check_credits(self.account, 'linking')
def test_insufficient_credits_blocks_optimization(self):
"""Test that insufficient credits blocks optimization"""
self.account.credits = 1 # Less than optimization cost for 500 words
self.account.save()
with self.assertRaises(InsufficientCreditsError):
CreditService.check_credits(self.account, 'optimization', 500)
def test_credit_deduction_logged(self):
"""Test that credit deduction is logged"""
from igny8_core.business.billing.models import CreditUsageLog
initial_credits = self.account.credits
cost = CreditService.get_credit_cost('linking')
CreditService.deduct_credits_for_operation(
account=self.account,
operation_type='linking',
description="Test linking"
)
self.account.refresh_from_db()
self.assertEqual(self.account.credits, initial_credits - cost)
# Check that usage log was created
log = CreditUsageLog.objects.filter(
account=self.account,
operation_type='linking'
).first()
self.assertIsNotNone(log)
def test_batch_operations_deduct_multiple_credits(self):
"""Test that batch operations deduct multiple credits"""
initial_credits = self.account.credits
linking_cost = CreditService.get_credit_cost('linking')
# Deduct for 3 linking operations
for i in range(3):
CreditService.deduct_credits_for_operation(
account=self.account,
operation_type='linking',
description=f"Linking {i}"
)
self.account.refresh_from_db()
expected_credits = initial_credits - (linking_cost * 3)
self.assertEqual(self.account.credits, expected_credits)

View File

@@ -1,2 +0,0 @@
# Content tests

View File

@@ -1,185 +0,0 @@
"""
Tests for ContentPipelineService
"""
from unittest.mock import patch, MagicMock
from django.test import TestCase
from igny8_core.business.content.models import Content
from igny8_core.business.content.services.content_pipeline_service import ContentPipelineService
from igny8_core.api.tests.test_integration_base import IntegrationTestBase
class ContentPipelineServiceTests(IntegrationTestBase):
"""Tests for ContentPipelineService"""
def setUp(self):
super().setUp()
self.service = ContentPipelineService()
# Create writer content
self.writer_content = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title="Writer Content",
html_content="<p>Writer content.</p>",
word_count=500,
status='draft',
source='igny8'
)
# Create synced content
self.synced_content = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title="WordPress Content",
html_content="<p>WordPress content.</p>",
word_count=500,
status='draft',
source='wordpress'
)
@patch('igny8_core.business.content.services.content_pipeline_service.LinkerService.process')
@patch('igny8_core.business.content.services.content_pipeline_service.OptimizerService.optimize_from_writer')
def test_process_writer_content_full_pipeline(self, mock_optimize, mock_link):
"""Test full pipeline for writer content (linking + optimization)"""
mock_link.return_value = self.writer_content
mock_optimize.return_value = self.writer_content
result = self.service.process_writer_content(self.writer_content.id)
self.assertEqual(result.id, self.writer_content.id)
mock_link.assert_called_once()
mock_optimize.assert_called_once()
@patch('igny8_core.business.content.services.content_pipeline_service.OptimizerService.optimize_from_writer')
def test_process_writer_content_optimization_only(self, mock_optimize):
"""Test writer content with optimization only"""
mock_optimize.return_value = self.writer_content
result = self.service.process_writer_content(
self.writer_content.id,
stages=['optimization']
)
self.assertEqual(result.id, self.writer_content.id)
mock_optimize.assert_called_once()
@patch('igny8_core.business.content.services.content_pipeline_service.LinkerService.process')
def test_process_writer_content_linking_only(self, mock_link):
"""Test writer content with linking only"""
mock_link.return_value = self.writer_content
result = self.service.process_writer_content(
self.writer_content.id,
stages=['linking']
)
self.assertEqual(result.id, self.writer_content.id)
mock_link.assert_called_once()
@patch('igny8_core.business.content.services.content_pipeline_service.LinkerService.process')
@patch('igny8_core.business.content.services.content_pipeline_service.OptimizerService.optimize_from_writer')
def test_process_writer_content_handles_linker_failure(self, mock_optimize, mock_link):
"""Test that pipeline continues when linking fails"""
mock_link.side_effect = Exception("Linking failed")
mock_optimize.return_value = self.writer_content
# Should not raise exception, should continue to optimization
result = self.service.process_writer_content(self.writer_content.id)
self.assertEqual(result.id, self.writer_content.id)
mock_optimize.assert_called_once()
@patch('igny8_core.business.content.services.content_pipeline_service.OptimizerService.optimize_from_wordpress_sync')
def test_process_synced_content_wordpress(self, mock_optimize):
"""Test synced content pipeline for WordPress"""
mock_optimize.return_value = self.synced_content
result = self.service.process_synced_content(self.synced_content.id)
self.assertEqual(result.id, self.synced_content.id)
mock_optimize.assert_called_once()
@patch('igny8_core.business.content.services.content_pipeline_service.OptimizerService.optimize_from_external_sync')
def test_process_synced_content_shopify(self, mock_optimize):
"""Test synced content pipeline for Shopify"""
shopify_content = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title="Shopify Content",
word_count=100,
source='shopify'
)
mock_optimize.return_value = shopify_content
result = self.service.process_synced_content(shopify_content.id)
self.assertEqual(result.id, shopify_content.id)
mock_optimize.assert_called_once()
@patch('igny8_core.business.content.services.content_pipeline_service.OptimizerService.optimize_manual')
def test_process_synced_content_custom(self, mock_optimize):
"""Test synced content pipeline for custom source"""
custom_content = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title="Custom Content",
word_count=100,
source='custom'
)
mock_optimize.return_value = custom_content
result = self.service.process_synced_content(custom_content.id)
self.assertEqual(result.id, custom_content.id)
mock_optimize.assert_called_once()
@patch('igny8_core.business.content.services.content_pipeline_service.ContentPipelineService.process_writer_content')
def test_batch_process_writer_content(self, mock_process):
"""Test batch processing writer content"""
content2 = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title="Content 2",
word_count=100,
source='igny8'
)
mock_process.side_effect = [self.writer_content, content2]
results = self.service.batch_process_writer_content([
self.writer_content.id,
content2.id
])
self.assertEqual(len(results), 2)
self.assertEqual(mock_process.call_count, 2)
@patch('igny8_core.business.content.services.content_pipeline_service.ContentPipelineService.process_writer_content')
def test_batch_process_handles_partial_failure(self, mock_process):
"""Test batch processing handles partial failures"""
mock_process.side_effect = [self.writer_content, Exception("Failed")]
results = self.service.batch_process_writer_content([
self.writer_content.id,
99999
])
# Should continue processing and return successful results
self.assertEqual(len(results), 1)
self.assertEqual(results[0].id, self.writer_content.id)
def test_process_writer_content_invalid_content(self):
"""Test that ValueError is raised for invalid content"""
with self.assertRaises(ValueError):
self.service.process_writer_content(99999)
def test_process_synced_content_invalid_content(self):
"""Test that ValueError is raised for invalid synced content"""
with self.assertRaises(ValueError):
self.service.process_synced_content(99999)

View File

@@ -1,283 +0,0 @@
"""
Tests for Universal Content Types (Phase 8)
Tests for product, service, and taxonomy content generation
"""
from unittest.mock import patch, MagicMock
from django.test import TestCase
from igny8_core.business.content.models import Content
from igny8_core.business.content.services.content_generation_service import ContentGenerationService
from igny8_core.api.tests.test_integration_base import IntegrationTestBase
class UniversalContentTypesTests(IntegrationTestBase):
"""Tests for Phase 8: Universal Content Types"""
def setUp(self):
super().setUp()
# Add credits to account for testing
self.account.credits = 10000
self.account.save()
self.service = ContentGenerationService()
@patch('igny8_core.ai.tasks.run_ai_task')
def test_product_content_generates_correctly(self, mock_run_ai_task):
"""
Test: Product content generates correctly
Task 17: Verify product generation creates content with correct entity_type and structure
"""
# Mock AI task response
mock_task = MagicMock()
mock_task.id = 'test-task-123'
mock_run_ai_task.delay.return_value = mock_task
product_data = {
'name': 'Test Product',
'description': 'A test product description',
'features': ['Feature 1', 'Feature 2', 'Feature 3'],
'target_audience': 'Small businesses',
'primary_keyword': 'test product',
'word_count': 1500
}
# Generate product content
result = self.service.generate_product_content(
product_data=product_data,
account=self.account,
site=self.site,
sector=self.sector
)
# Verify result
self.assertTrue(result.get('success'))
self.assertIsNotNone(result.get('task_id'))
self.assertEqual(result.get('message'), 'Product content generation started')
# Verify AI task was called with correct function name
mock_run_ai_task.delay.assert_called_once()
call_args = mock_run_ai_task.delay.call_args
self.assertEqual(call_args[1]['function_name'], 'generate_product_content')
self.assertEqual(call_args[1]['payload']['product_name'], 'Test Product')
@patch('igny8_core.ai.tasks.run_ai_task')
def test_service_pages_work_correctly(self, mock_run_ai_task):
"""
Test: Service pages work correctly
Task 18: Verify service page generation creates content with correct entity_type
"""
# Mock AI task response
mock_task = MagicMock()
mock_task.id = 'test-task-456'
mock_run_ai_task.delay.return_value = mock_task
service_data = {
'name': 'Test Service',
'description': 'A test service description',
'benefits': ['Benefit 1', 'Benefit 2', 'Benefit 3'],
'target_audience': 'Enterprise clients',
'primary_keyword': 'test service',
'word_count': 1800
}
# Generate service page
result = self.service.generate_service_page(
service_data=service_data,
account=self.account,
site=self.site,
sector=self.sector
)
# Verify result
self.assertTrue(result.get('success'))
self.assertIsNotNone(result.get('task_id'))
self.assertEqual(result.get('message'), 'Service page generation started')
# Verify AI task was called with correct function name
mock_run_ai_task.delay.assert_called_once()
call_args = mock_run_ai_task.delay.call_args
self.assertEqual(call_args[1]['function_name'], 'generate_service_page')
self.assertEqual(call_args[1]['payload']['service_name'], 'Test Service')
@patch('igny8_core.ai.tasks.run_ai_task')
def test_taxonomy_pages_work_correctly(self, mock_run_ai_task):
"""
Test: Taxonomy pages work correctly
Task 19: Verify taxonomy generation creates content with correct entity_type
"""
# Mock AI task response
mock_task = MagicMock()
mock_task.id = 'test-task-789'
mock_run_ai_task.delay.return_value = mock_task
taxonomy_data = {
'name': 'Test Taxonomy',
'description': 'A test taxonomy description',
'items': ['Category 1', 'Category 2', 'Category 3'],
'primary_keyword': 'test taxonomy',
'word_count': 1200
}
# Generate taxonomy
result = self.service.generate_taxonomy(
taxonomy_data=taxonomy_data,
account=self.account,
site=self.site,
sector=self.sector
)
# Verify result
self.assertTrue(result.get('success'))
self.assertIsNotNone(result.get('task_id'))
self.assertEqual(result.get('message'), 'Taxonomy generation started')
# Verify AI task was called with correct function name
mock_run_ai_task.delay.assert_called_once()
call_args = mock_run_ai_task.delay.call_args
self.assertEqual(call_args[1]['function_name'], 'generate_taxonomy')
self.assertEqual(call_args[1]['payload']['taxonomy_name'], 'Test Taxonomy')
def test_product_content_has_correct_structure(self):
"""
Test: Product content generates correctly
Task 17: Verify product content has correct entity_type, json_blocks, and structure_data
"""
# Create product content manually to test structure
product_content = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title='Test Product',
html_content='<p>Product content</p>',
entity_type='product',
json_blocks=[
{
'type': 'product_overview',
'heading': 'Product Overview',
'content': 'Product description'
},
{
'type': 'features',
'heading': 'Key Features',
'items': ['Feature 1', 'Feature 2']
},
{
'type': 'specifications',
'heading': 'Specifications',
'data': {'Spec 1': 'Value 1'}
}
],
structure_data={
'product_type': 'software',
'price_range': '$99-$199',
'target_market': 'SMB'
},
word_count=1500,
status='draft'
)
# Verify structure
self.assertEqual(product_content.entity_type, 'product')
self.assertIsNotNone(product_content.json_blocks)
self.assertEqual(len(product_content.json_blocks), 3)
self.assertEqual(product_content.json_blocks[0]['type'], 'product_overview')
self.assertIsNotNone(product_content.structure_data)
self.assertEqual(product_content.structure_data['product_type'], 'software')
def test_service_content_has_correct_structure(self):
"""
Test: Service pages work correctly
Task 18: Verify service content has correct entity_type and json_blocks
"""
# Create service content manually to test structure
service_content = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title='Test Service',
html_content='<p>Service content</p>',
entity_type='service',
json_blocks=[
{
'type': 'service_overview',
'heading': 'Service Overview',
'content': 'Service description'
},
{
'type': 'benefits',
'heading': 'Benefits',
'items': ['Benefit 1', 'Benefit 2']
},
{
'type': 'process',
'heading': 'Our Process',
'steps': ['Step 1', 'Step 2']
}
],
structure_data={
'service_type': 'consulting',
'duration': '3-6 months',
'target_market': 'Enterprise'
},
word_count=1800,
status='draft'
)
# Verify structure
self.assertEqual(service_content.entity_type, 'service')
self.assertIsNotNone(service_content.json_blocks)
self.assertEqual(len(service_content.json_blocks), 3)
self.assertEqual(service_content.json_blocks[0]['type'], 'service_overview')
self.assertIsNotNone(service_content.structure_data)
self.assertEqual(service_content.structure_data['service_type'], 'consulting')
def test_taxonomy_content_has_correct_structure(self):
"""
Test: Taxonomy pages work correctly
Task 19: Verify taxonomy content has correct entity_type and json_blocks
"""
# Create taxonomy content manually to test structure
taxonomy_content = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title='Test Taxonomy',
html_content='<p>Taxonomy content</p>',
entity_type='taxonomy',
json_blocks=[
{
'type': 'taxonomy_overview',
'heading': 'Taxonomy Overview',
'content': 'Taxonomy description'
},
{
'type': 'categories',
'heading': 'Categories',
'items': [
{
'name': 'Category 1',
'description': 'Category description',
'subcategories': ['Subcat 1', 'Subcat 2']
}
]
},
{
'type': 'tags',
'heading': 'Tags',
'items': ['Tag 1', 'Tag 2', 'Tag 3']
}
],
structure_data={
'taxonomy_type': 'product_categories',
'item_count': 10,
'hierarchy_levels': 3
},
word_count=1200,
status='draft'
)
# Verify structure
self.assertEqual(taxonomy_content.entity_type, 'taxonomy')
self.assertIsNotNone(taxonomy_content.json_blocks)
self.assertEqual(len(taxonomy_content.json_blocks), 3)
self.assertEqual(taxonomy_content.json_blocks[0]['type'], 'taxonomy_overview')
self.assertIsNotNone(taxonomy_content.structure_data)
self.assertEqual(taxonomy_content.structure_data['taxonomy_type'], 'product_categories')

View File

@@ -374,100 +374,10 @@ class ContentSyncService:
Returns:
dict: Sync result with synced_count
"""
try:
from igny8_core.business.site_building.models import SiteBlueprint
from igny8_core.business.site_building.services.taxonomy_service import TaxonomyService
# Get or create site blueprint for this site
blueprint = SiteBlueprint.objects.filter(
account=integration.account,
site=integration.site
).first()
if not blueprint:
logger.warning(f"No blueprint found for site {integration.site.id}, skipping taxonomy sync")
return {'success': True, 'synced_count': 0}
taxonomy_service = TaxonomyService()
synced_count = 0
# Sync WordPress categories
categories = client.get_categories(per_page=100)
category_records = [
{
'name': cat['name'],
'slug': cat['slug'],
'description': cat.get('description', ''),
'taxonomy_type': 'blog_category',
'external_reference': str(cat['id']),
'metadata': {'parent': cat.get('parent', 0)}
}
for cat in categories
]
if category_records:
taxonomy_service.import_from_external(
blueprint,
category_records,
default_type='blog_category'
)
synced_count += len(category_records)
# Sync WordPress tags
tags = client.get_tags(per_page=100)
tag_records = [
{
'name': tag['name'],
'slug': tag['slug'],
'description': tag.get('description', ''),
'taxonomy_type': 'blog_tag',
'external_reference': str(tag['id'])
}
for tag in tags
]
if tag_records:
taxonomy_service.import_from_external(
blueprint,
tag_records,
default_type='blog_tag'
)
synced_count += len(tag_records)
# Sync WooCommerce product categories if available (401 is expected if WooCommerce not installed or credentials missing)
try:
product_categories = client.get_product_categories(per_page=100)
product_category_records = [
{
'name': cat['name'],
'slug': cat['slug'],
'description': cat.get('description', ''),
'taxonomy_type': 'product_category',
'external_reference': f"wc_cat_{cat['id']}",
'metadata': {'parent': cat.get('parent', 0)}
}
for cat in product_categories
]
if product_category_records:
taxonomy_service.import_from_external(
blueprint,
product_category_records,
default_type='product_category'
)
synced_count += len(product_category_records)
except Exception as e:
# Silently skip WooCommerce if not available (401 means no consumer key/secret configured or plugin not installed)
logger.debug(f"WooCommerce product categories not available: {e}")
return {
'success': True,
'synced_count': synced_count
}
except Exception as e:
logger.error(f"Error syncing taxonomies from WordPress: {e}", exc_info=True)
return {
'success': False,
'error': str(e),
'synced_count': 0
}
# REMOVED: Legacy SiteBlueprint taxonomy sync removed.
# Taxonomy management now uses ContentTaxonomy model.
logger.info(f"Skipping legacy taxonomy sync for site {integration.site.id}")
return {'success': True, 'synced_count': 0}
def _sync_taxonomies_to_wordpress(
self,

View File

@@ -308,11 +308,14 @@ class SyncHealthService:
"""
Detect mismatches between IGNY8 and WordPress.
DEPRECATED: Legacy SiteBlueprint taxonomy mismatch detection removed.
Taxonomy management now uses ContentTaxonomy model.
Args:
integration: SiteIntegration instance
Returns:
dict: Mismatch details
dict: Mismatch details (empty for now)
"""
mismatches = {
'taxonomies': {
@@ -330,116 +333,8 @@ class SyncHealthService:
}
}
try:
from igny8_core.utils.wordpress import WordPressClient
from igny8_core.business.site_building.models import SiteBlueprint, SiteBlueprintTaxonomy
from igny8_core.business.content.models import Content
credentials = integration.get_credentials()
client = WordPressClient(
site_url=integration.config_json.get('site_url', ''),
username=credentials.get('username'),
app_password=credentials.get('app_password')
)
# Get site blueprint
blueprint = SiteBlueprint.objects.filter(
account=integration.account,
site=integration.site
).first()
if not blueprint:
return mismatches
# Check taxonomy mismatches
# Get IGNY8 taxonomies
igny8_taxonomies = SiteBlueprintTaxonomy.objects.filter(
site_blueprint=blueprint
)
# Get WordPress categories
wp_categories = client.get_categories(per_page=100)
wp_category_ids = {str(cat['id']): cat for cat in wp_categories}
# Get WordPress tags
wp_tags = client.get_tags(per_page=100)
wp_tag_ids = {str(tag['id']): tag for tag in wp_tags}
for taxonomy in igny8_taxonomies:
if taxonomy.external_reference:
# Check if still exists in WordPress
if taxonomy.taxonomy_type in ['blog_category', 'product_category']:
if taxonomy.external_reference not in wp_category_ids:
mismatches['taxonomies']['missing_in_wordpress'].append({
'id': taxonomy.id,
'name': taxonomy.name,
'type': taxonomy.taxonomy_type,
'external_reference': taxonomy.external_reference
})
elif taxonomy.taxonomy_type in ['blog_tag', 'product_tag']:
if taxonomy.external_reference not in wp_tag_ids:
mismatches['taxonomies']['missing_in_wordpress'].append({
'id': taxonomy.id,
'name': taxonomy.name,
'type': taxonomy.taxonomy_type,
'external_reference': taxonomy.external_reference
})
else:
# Taxonomy exists in IGNY8 but not synced to WordPress
mismatches['taxonomies']['missing_in_wordpress'].append({
'id': taxonomy.id,
'name': taxonomy.name,
'type': taxonomy.taxonomy_type
})
# Check for WordPress taxonomies not in IGNY8
for cat in wp_categories:
if not SiteBlueprintTaxonomy.objects.filter(
site_blueprint=blueprint,
external_reference=str(cat['id'])
).exists():
mismatches['taxonomies']['missing_in_igny8'].append({
'name': cat['name'],
'slug': cat['slug'],
'type': 'blog_category',
'external_reference': str(cat['id'])
})
for tag in wp_tags:
if not SiteBlueprintTaxonomy.objects.filter(
site_blueprint=blueprint,
external_reference=str(tag['id'])
).exists():
mismatches['taxonomies']['missing_in_igny8'].append({
'name': tag['name'],
'slug': tag['slug'],
'type': 'blog_tag',
'external_reference': str(tag['id'])
})
# Check content mismatches (basic check)
igny8_content = Content.objects.filter(
account=integration.account,
site=integration.site,
source='igny8',
status='publish'
)
for content in igny8_content[:50]: # Limit check
if content.metadata and content.metadata.get('wordpress_id'):
# Content should exist in WordPress (would need to check)
# For now, just note if metadata exists
pass
else:
# Content not synced to WordPress
mismatches['posts']['missing_in_wordpress'].append({
'id': content.id,
'title': content.title,
'type': content.content_type
})
except Exception as e:
logger.warning(f"Error detecting WordPress mismatches: {e}")
# Legacy taxonomy detection removed - would need to be reimplemented with ContentTaxonomy
logger.info(f"Mismatch detection for integration {integration.id} - legacy code removed")
return mismatches

View File

@@ -1,5 +0,0 @@
"""
Integration Tests
Phase 6: Site Integration & Multi-Destination Publishing
"""

View File

@@ -1,155 +0,0 @@
"""
Tests for ContentSyncService
Phase 6: Site Integration & Multi-Destination Publishing
"""
from django.test import TestCase
from unittest.mock import patch, Mock
from igny8_core.auth.models import Account, Site, Sector, User, Plan, Industry, IndustrySector
from igny8_core.business.integration.models import SiteIntegration
from igny8_core.business.integration.services.content_sync_service import ContentSyncService
from igny8_core.business.content.models import Content
class ContentSyncServiceTestCase(TestCase):
"""Test cases for ContentSyncService"""
def setUp(self):
"""Set up test data"""
# Create plan first
self.plan = Plan.objects.create(
name="Test Plan",
slug="test-plan",
price=0,
credits_per_month=1000
)
# Create user first (Account needs owner)
self.user = User.objects.create_user(
username='testuser',
email='test@test.com',
password='testpass123',
role='owner'
)
# Create account with owner
self.account = Account.objects.create(
name="Test Account",
slug="test-account",
plan=self.plan,
owner=self.user
)
# Update user to have account
self.user.account = self.account
self.user.save()
# Create industry and sector
self.industry = Industry.objects.create(
name="Test Industry",
slug="test-industry"
)
self.industry_sector = IndustrySector.objects.create(
industry=self.industry,
name="Test Sector",
slug="test-sector"
)
self.site = Site.objects.create(
account=self.account,
name="Test Site",
slug="test-site",
industry=self.industry
)
self.sector = Sector.objects.create(
account=self.account,
site=self.site,
industry_sector=self.industry_sector,
name="Test Sector",
slug="test-sector"
)
self.integration = SiteIntegration.objects.create(
account=self.account,
site=self.site,
platform='wordpress',
platform_type='cms',
sync_enabled=True
)
self.service = ContentSyncService()
def test_sync_content_from_wordpress_creates_content(self):
"""Test: WordPress sync works (when plugin connected)"""
mock_posts = [
{
'id': 1,
'title': 'Test Post',
'content': '<p>Test content</p>',
'status': 'publish',
}
]
with patch.object(self.service, '_fetch_wordpress_posts') as mock_fetch:
mock_fetch.return_value = mock_posts
result = self.service.sync_from_wordpress(self.integration)
self.assertTrue(result.get('success'))
self.assertEqual(result.get('synced_count'), 1)
# Verify content was created
content = Content.objects.filter(site=self.site).first()
self.assertIsNotNone(content)
self.assertEqual(content.title, 'Test Post')
self.assertEqual(content.source, 'wordpress')
def test_sync_content_from_shopify_creates_content(self):
"""Test: Content sync works"""
mock_products = [
{
'id': 1,
'title': 'Test Product',
'body_html': '<p>Product description</p>',
}
]
with patch.object(self.service, '_fetch_shopify_products') as mock_fetch:
mock_fetch.return_value = mock_products
result = self.service.sync_from_shopify(self.integration)
self.assertTrue(result.get('success'))
self.assertEqual(result.get('synced_count'), 1)
def test_sync_handles_duplicate_content(self):
"""Test: Content sync works"""
# Create existing content
Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title="Test Post",
html_content="<p>Existing</p>",
source='wordpress'
)
mock_posts = [
{
'id': 1,
'title': 'Test Post',
'content': '<p>Updated content</p>',
}
]
with patch.object(self.service, '_fetch_wordpress_posts') as mock_fetch:
mock_fetch.return_value = mock_posts
result = self.service.sync_from_wordpress(self.integration)
# Should update existing, not create duplicate
content_count = Content.objects.filter(
site=self.site,
title='Test Post'
).count()
self.assertEqual(content_count, 1)

View File

@@ -1,141 +0,0 @@
"""
Tests for IntegrationService
Phase 6: Site Integration & Multi-Destination Publishing
"""
from django.test import TestCase
from igny8_core.auth.models import Account, Site, Sector, User, Plan, Industry, IndustrySector
from igny8_core.business.integration.models import SiteIntegration
from igny8_core.business.integration.services.integration_service import IntegrationService
class IntegrationServiceTestCase(TestCase):
"""Test cases for IntegrationService"""
def setUp(self):
"""Set up test data"""
# Create plan first
self.plan = Plan.objects.create(
name="Test Plan",
slug="test-plan",
price=0,
credits_per_month=1000
)
# Create user first (Account needs owner)
self.user = User.objects.create_user(
username='testuser',
email='test@test.com',
password='testpass123',
role='owner'
)
# Create account with owner
self.account = Account.objects.create(
name="Test Account",
slug="test-account",
plan=self.plan,
owner=self.user
)
# Update user to have account
self.user.account = self.account
self.user.save()
# Create industry and sector
self.industry = Industry.objects.create(
name="Test Industry",
slug="test-industry"
)
self.industry_sector = IndustrySector.objects.create(
industry=self.industry,
name="Test Sector",
slug="test-sector"
)
self.site = Site.objects.create(
account=self.account,
name="Test Site",
slug="test-site",
industry=self.industry
)
self.sector = Sector.objects.create(
account=self.account,
site=self.site,
industry_sector=self.industry_sector,
name="Test Sector",
slug="test-sector"
)
self.service = IntegrationService()
def test_create_integration_stores_config(self):
"""Test: Site integrations work correctly"""
integration = self.service.create_integration(
site=self.site,
platform='wordpress',
config={'url': 'https://example.com'},
credentials={'api_key': 'test-key'},
platform_type='cms'
)
self.assertIsNotNone(integration)
self.assertEqual(integration.platform, 'wordpress')
self.assertEqual(integration.platform_type, 'cms')
self.assertEqual(integration.config_json.get('url'), 'https://example.com')
self.assertTrue(integration.is_active)
def test_get_integrations_for_site_returns_all(self):
"""Test: Site integrations work correctly"""
self.service.create_integration(
site=self.site,
platform='wordpress',
config={},
credentials={}
)
self.service.create_integration(
site=self.site,
platform='shopify',
config={},
credentials={}
)
integrations = self.service.get_integrations_for_site(self.site)
self.assertEqual(integrations.count(), 2)
platforms = [i.platform for i in integrations]
self.assertIn('wordpress', platforms)
self.assertIn('shopify', platforms)
def test_test_connection_validates_credentials(self):
"""Test: Site integrations work correctly"""
# Test with unsupported platform to verify NotImplementedError is raised
integration = self.service.create_integration(
site=self.site,
platform='unsupported_platform',
config={'url': 'https://example.com'},
credentials={'api_key': 'test-key'}
)
with self.assertRaises(NotImplementedError):
# Connection testing should raise NotImplementedError for unsupported platforms
self.service.test_connection(integration)
def test_update_integration_updates_fields(self):
"""Test: Site integrations work correctly"""
integration = self.service.create_integration(
site=self.site,
platform='wordpress',
config={'url': 'https://old.com'},
credentials={}
)
updated = self.service.update_integration(
integration,
config={'url': 'https://new.com'},
is_active=False
)
self.assertEqual(updated.config_json.get('url'), 'https://new.com')
self.assertFalse(updated.is_active)

View File

@@ -1,127 +0,0 @@
"""
Tests for SyncService
Phase 6: Site Integration & Multi-Destination Publishing
"""
from django.test import TestCase
from django.utils import timezone
from unittest.mock import patch, Mock
from igny8_core.auth.models import Account, Site, Sector, User, Plan, Industry, IndustrySector
from igny8_core.business.integration.models import SiteIntegration
from igny8_core.business.integration.services.sync_service import SyncService
class SyncServiceTestCase(TestCase):
"""Test cases for SyncService"""
def setUp(self):
"""Set up test data"""
# Create plan first
self.plan = Plan.objects.create(
name="Test Plan",
slug="test-plan",
price=0,
credits_per_month=1000
)
# Create user first (Account needs owner)
self.user = User.objects.create_user(
username='testuser',
email='test@test.com',
password='testpass123',
role='owner'
)
# Create account with owner
self.account = Account.objects.create(
name="Test Account",
slug="test-account",
plan=self.plan,
owner=self.user
)
# Update user to have account
self.user.account = self.account
self.user.save()
# Create industry and sector
self.industry = Industry.objects.create(
name="Test Industry",
slug="test-industry"
)
self.industry_sector = IndustrySector.objects.create(
industry=self.industry,
name="Test Sector",
slug="test-sector"
)
self.site = Site.objects.create(
account=self.account,
name="Test Site",
slug="test-site",
industry=self.industry
)
self.sector = Sector.objects.create(
account=self.account,
site=self.site,
industry_sector=self.industry_sector,
name="Test Sector",
slug="test-sector"
)
self.integration = SiteIntegration.objects.create(
account=self.account,
site=self.site,
platform='wordpress',
platform_type='cms',
sync_enabled=True,
sync_status='pending'
)
self.service = SyncService()
def test_sync_updates_status(self):
"""Test: Two-way sync functions properly"""
with patch.object(self.service, '_sync_to_external') as mock_sync_to, \
patch.object(self.service, '_sync_from_external') as mock_sync_from:
mock_sync_to.return_value = {'success': True, 'synced': 5}
mock_sync_from.return_value = {'success': True, 'synced': 3}
result = self.service.sync(self.integration, direction='both')
self.assertTrue(result.get('success'))
self.integration.refresh_from_db()
self.assertEqual(self.integration.sync_status, 'success')
self.assertIsNotNone(self.integration.last_sync_at)
def test_sync_to_external_only(self):
"""Test: Two-way sync functions properly"""
with patch.object(self.service, '_sync_to_external') as mock_sync_to:
mock_sync_to.return_value = {'success': True, 'synced': 5}
result = self.service.sync(self.integration, direction='to_external')
self.assertTrue(result.get('success'))
mock_sync_to.assert_called_once()
def test_sync_from_external_only(self):
"""Test: WordPress sync works (when plugin connected)"""
with patch.object(self.service, '_sync_from_external') as mock_sync_from:
mock_sync_from.return_value = {'success': True, 'synced': 3}
result = self.service.sync(self.integration, direction='from_external')
self.assertTrue(result.get('success'))
mock_sync_from.assert_called_once()
def test_sync_handles_errors(self):
"""Test: Two-way sync functions properly"""
with patch.object(self.service, '_sync_to_external') as mock_sync_to:
mock_sync_to.side_effect = Exception("Sync failed")
result = self.service.sync(self.integration, direction='to_external')
self.assertFalse(result.get('success'))
self.integration.refresh_from_db()
self.assertEqual(self.integration.sync_status, 'failed')
self.assertIsNotNone(self.integration.sync_error)

View File

@@ -1,2 +0,0 @@
# Linking tests

View File

@@ -1,139 +0,0 @@
"""
Tests for CandidateEngine
"""
from django.test import TestCase
from igny8_core.business.content.models import Content
from igny8_core.business.linking.services.candidate_engine import CandidateEngine
from igny8_core.api.tests.test_integration_base import IntegrationTestBase
class CandidateEngineTests(IntegrationTestBase):
"""Tests for CandidateEngine"""
def setUp(self):
super().setUp()
self.engine = CandidateEngine()
# Create source content
self.source_content = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title="Source Content",
html_content="<p>Source content about test keyword.</p>",
primary_keyword="test keyword",
secondary_keywords=["keyword1", "keyword2"],
categories=["category1"],
tags=["tag1", "tag2"],
word_count=100,
status='draft'
)
# Create relevant content (same keyword)
self.relevant_content = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title="Relevant Content",
html_content="<p>Relevant content about test keyword.</p>",
primary_keyword="test keyword",
secondary_keywords=["keyword1"],
categories=["category1"],
tags=["tag1"],
word_count=150,
status='draft'
)
# Create less relevant content (different keyword)
self.less_relevant = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title="Less Relevant",
html_content="<p>Different content.</p>",
primary_keyword="different keyword",
word_count=100,
status='draft'
)
def test_find_candidates_returns_relevant_content(self):
"""Test that find_candidates returns relevant content"""
candidates = self.engine.find_candidates(self.source_content, max_candidates=10)
# Should find relevant content
candidate_ids = [c['content_id'] for c in candidates]
self.assertIn(self.relevant_content.id, candidate_ids)
def test_find_candidates_scores_by_relevance(self):
"""Test that candidates are scored by relevance"""
candidates = self.engine.find_candidates(self.source_content, max_candidates=10)
# Relevant content should have higher score
relevant_candidate = next((c for c in candidates if c['content_id'] == self.relevant_content.id), None)
self.assertIsNotNone(relevant_candidate)
self.assertGreater(relevant_candidate['relevance_score'], 0)
def test_find_candidates_excludes_self(self):
"""Test that source content is excluded from candidates"""
candidates = self.engine.find_candidates(self.source_content, max_candidates=10)
candidate_ids = [c['content_id'] for c in candidates]
self.assertNotIn(self.source_content.id, candidate_ids)
def test_find_candidates_respects_account_isolation(self):
"""Test that candidates are only from same account"""
# Create content from different account
from igny8_core.auth.models import Account
other_account = Account.objects.create(
name="Other Account",
slug="other-account",
plan=self.plan,
owner=self.user
)
other_content = Content.objects.create(
account=other_account,
site=self.site,
sector=self.sector,
title="Other Account Content",
primary_keyword="test keyword",
word_count=100,
status='draft'
)
candidates = self.engine.find_candidates(self.source_content, max_candidates=10)
candidate_ids = [c['content_id'] for c in candidates]
self.assertNotIn(other_content.id, candidate_ids)
def test_find_candidates_returns_empty_for_no_content(self):
"""Test that empty list is returned when no content"""
empty_content = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title="Empty",
html_content="",
word_count=0,
status='draft'
)
candidates = self.engine.find_candidates(empty_content, max_candidates=10)
self.assertEqual(len(candidates), 0)
def test_find_candidates_respects_max_candidates(self):
"""Test that max_candidates limit is respected"""
# Create multiple relevant content items
for i in range(15):
Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title=f"Content {i}",
primary_keyword="test keyword",
word_count=100,
status='draft'
)
candidates = self.engine.find_candidates(self.source_content, max_candidates=5)
self.assertLessEqual(len(candidates), 5)

View File

@@ -1,136 +0,0 @@
"""
Tests for InjectionEngine
"""
from django.test import TestCase
from igny8_core.business.content.models import Content
from igny8_core.business.linking.services.injection_engine import InjectionEngine
from igny8_core.api.tests.test_integration_base import IntegrationTestBase
class InjectionEngineTests(IntegrationTestBase):
"""Tests for InjectionEngine"""
def setUp(self):
super().setUp()
self.engine = InjectionEngine()
# Create content with HTML
self.content = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title="Test Content",
html_content="<p>This is test content with some keywords and text.</p>",
word_count=100,
status='draft'
)
def test_inject_links_adds_links_to_html(self):
"""Test that links are injected into HTML content"""
candidates = [{
'content_id': 1,
'title': 'Target Content',
'url': '/content/1/',
'relevance_score': 50,
'anchor_text': 'keywords'
}]
result = self.engine.inject_links(self.content, candidates, max_links=5)
# Check that link was added
self.assertIn('<a href="/content/1/" class="internal-link">keywords</a>', result['html_content'])
self.assertEqual(result['links_added'], 1)
self.assertEqual(len(result['links']), 1)
def test_inject_links_respects_max_links(self):
"""Test that max_links limit is respected"""
candidates = [
{'content_id': i, 'title': f'Content {i}', 'url': f'/content/{i}/',
'relevance_score': 50, 'anchor_text': f'keyword{i}'}
for i in range(10)
]
# Update HTML to include all anchor texts
self.content.html_content = "<p>" + " ".join([f'keyword{i}' for i in range(10)]) + "</p>"
self.content.save()
result = self.engine.inject_links(self.content, candidates, max_links=3)
self.assertLessEqual(result['links_added'], 3)
self.assertLessEqual(len(result['links']), 3)
def test_inject_links_returns_unchanged_when_no_candidates(self):
"""Test that content is unchanged when no candidates"""
original_html = self.content.html_content
result = self.engine.inject_links(self.content, [], max_links=5)
self.assertEqual(result['html_content'], original_html)
self.assertEqual(result['links_added'], 0)
self.assertEqual(len(result['links']), 0)
def test_inject_links_returns_unchanged_when_no_html(self):
"""Test that empty HTML returns unchanged"""
self.content.html_content = ""
self.content.save()
candidates = [{
'content_id': 1,
'title': 'Target',
'url': '/content/1/',
'relevance_score': 50,
'anchor_text': 'test'
}]
result = self.engine.inject_links(self.content, candidates, max_links=5)
self.assertEqual(result['html_content'], "")
self.assertEqual(result['links_added'], 0)
def test_inject_links_case_insensitive_matching(self):
"""Test that anchor text matching is case-insensitive"""
self.content.html_content = "<p>This is TEST content.</p>"
self.content.save()
candidates = [{
'content_id': 1,
'title': 'Target',
'url': '/content/1/',
'relevance_score': 50,
'anchor_text': 'test'
}]
result = self.engine.inject_links(self.content, candidates, max_links=5)
# Should find and replace despite case difference
self.assertIn('internal-link', result['html_content'])
self.assertEqual(result['links_added'], 1)
def test_inject_links_prevents_duplicate_links(self):
"""Test that same candidate is not linked twice"""
candidates = [
{
'content_id': 1,
'title': 'Target',
'url': '/content/1/',
'relevance_score': 50,
'anchor_text': 'test'
},
{
'content_id': 1, # Same content_id
'title': 'Target',
'url': '/content/1/',
'relevance_score': 40,
'anchor_text': 'test'
}
]
self.content.html_content = "<p>This is test content with test keywords.</p>"
self.content.save()
result = self.engine.inject_links(self.content, candidates, max_links=5)
# Should only add one link despite two candidates
self.assertEqual(result['links_added'], 1)
self.assertEqual(result['html_content'].count('internal-link'), 1)

View File

@@ -1,141 +0,0 @@
"""
Tests for LinkerService
"""
from unittest.mock import Mock, patch, MagicMock
from django.test import TestCase
from igny8_core.business.content.models import Content
from igny8_core.business.linking.services.linker_service import LinkerService
from igny8_core.business.billing.exceptions import InsufficientCreditsError
from igny8_core.api.tests.test_integration_base import IntegrationTestBase
class LinkerServiceTests(IntegrationTestBase):
"""Tests for LinkerService"""
def setUp(self):
super().setUp()
self.service = LinkerService()
# Create test content
self.content = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title="Test Content",
html_content="<p>This is test content with some keywords.</p>",
primary_keyword="test keyword",
word_count=100,
status='draft'
)
# Create another content for linking
self.target_content = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title="Target Content",
html_content="<p>Target content for linking.</p>",
primary_keyword="test keyword",
word_count=150,
status='draft'
)
@patch('igny8_core.business.linking.services.linker_service.CreditService.check_credits')
@patch('igny8_core.business.linking.services.linker_service.CandidateEngine.find_candidates')
@patch('igny8_core.business.linking.services.linker_service.InjectionEngine.inject_links')
@patch('igny8_core.business.linking.services.linker_service.CreditService.deduct_credits_for_operation')
def test_process_single_content(self, mock_deduct, mock_inject, mock_find, mock_check):
"""Test processing single content for linking"""
# Setup mocks
mock_check.return_value = True
mock_find.return_value = [{
'content_id': self.target_content.id,
'title': 'Target Content',
'url': '/content/2/',
'relevance_score': 50,
'anchor_text': 'test keyword'
}]
mock_inject.return_value = {
'html_content': '<p>This is test content with <a href="/content/2/">test keyword</a>.</p>',
'links': [{
'content_id': self.target_content.id,
'anchor_text': 'test keyword',
'url': '/content/2/'
}],
'links_added': 1
}
# Execute
result = self.service.process(self.content.id)
# Assertions
self.assertEqual(result.id, self.content.id)
self.assertEqual(result.linker_version, 1)
self.assertEqual(len(result.internal_links), 1)
mock_check.assert_called_once_with(self.account, 'linking')
mock_deduct.assert_called_once()
@patch('igny8_core.business.linking.services.linker_service.CreditService.check_credits')
def test_process_insufficient_credits(self, mock_check):
"""Test that InsufficientCreditsError is raised when credits are insufficient"""
mock_check.side_effect = InsufficientCreditsError("Insufficient credits")
with self.assertRaises(InsufficientCreditsError):
self.service.process(self.content.id)
def test_process_content_not_found(self):
"""Test that ValueError is raised when content doesn't exist"""
with self.assertRaises(ValueError):
self.service.process(99999)
@patch('igny8_core.business.linking.services.linker_service.LinkerService.process')
def test_batch_process_multiple_content(self, mock_process):
"""Test batch processing multiple content items"""
# Create additional content
content2 = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title="Content 2",
html_content="<p>Content 2</p>",
word_count=100,
status='draft'
)
# Setup mock
mock_process.side_effect = [self.content, content2]
# Execute
results = self.service.batch_process([self.content.id, content2.id])
# Assertions
self.assertEqual(len(results), 2)
self.assertEqual(mock_process.call_count, 2)
@patch('igny8_core.business.linking.services.linker_service.LinkerService.process')
def test_batch_process_handles_partial_failure(self, mock_process):
"""Test batch processing handles partial failures"""
# Setup mock to fail on second item
mock_process.side_effect = [self.content, Exception("Processing failed")]
# Execute
results = self.service.batch_process([self.content.id, 99999])
# Assertions - should continue processing other items
self.assertEqual(len(results), 1)
self.assertEqual(results[0].id, self.content.id)
@patch('igny8_core.business.linking.services.linker_service.CreditService.check_credits')
@patch('igny8_core.business.linking.services.linker_service.CandidateEngine.find_candidates')
def test_process_no_candidates_found(self, mock_find, mock_check):
"""Test processing when no candidates are found"""
mock_check.return_value = True
mock_find.return_value = []
# Execute
result = self.service.process(self.content.id)
# Assertions - should return content unchanged
self.assertEqual(result.id, self.content.id)
self.assertEqual(result.linker_version, 0) # Not incremented

View File

@@ -1,193 +0,0 @@
"""
Tests for Universal Content Types Linking (Phase 8)
Tests for product and taxonomy linking
"""
from unittest.mock import patch, MagicMock
from django.test import TestCase
from igny8_core.business.content.models import Content
from igny8_core.business.linking.services.linker_service import LinkerService
from igny8_core.api.tests.test_integration_base import IntegrationTestBase
class UniversalContentLinkingTests(IntegrationTestBase):
"""Tests for Phase 8: Universal Content Types Linking"""
def setUp(self):
super().setUp()
# Add credits to account for testing
self.account.credits = 10000
self.account.save()
self.linker_service = LinkerService()
# Create product content
self.product_content = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title='Test Product',
html_content='<p>Product content with features and specifications.</p>',
entity_type='product',
json_blocks=[
{'type': 'features', 'heading': 'Features', 'items': ['Feature 1', 'Feature 2']}
],
structure_data={'product_type': 'software'},
word_count=1500,
status='draft'
)
# Create related product
self.related_product = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title='Related Product',
html_content='<p>Related product content.</p>',
entity_type='product',
structure_data={'product_type': 'software'},
word_count=1500,
status='draft'
)
# Create service content
self.service_content = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title='Related Service',
html_content='<p>Service content.</p>',
entity_type='service',
word_count=1800,
status='draft'
)
# Create taxonomy content
self.taxonomy_content = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title='Test Taxonomy',
html_content='<p>Taxonomy content with categories.</p>',
entity_type='taxonomy',
json_blocks=[
{
'type': 'categories',
'heading': 'Categories',
'items': [
{'name': 'Category 1', 'description': 'Desc 1', 'subcategories': []}
]
}
],
word_count=1200,
status='draft'
)
# Create related taxonomy
self.related_taxonomy = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title='Related Taxonomy',
html_content='<p>Related taxonomy content.</p>',
entity_type='taxonomy',
word_count=1200,
status='draft'
)
@patch('igny8_core.business.linking.services.linker_service.InjectionEngine.inject_links')
@patch('igny8_core.business.linking.services.linker_service.CreditService.check_credits')
@patch('igny8_core.business.linking.services.linker_service.CreditService.deduct_credits_for_operation')
def test_linking_works_for_products(self, mock_deduct, mock_check_credits, mock_inject_links):
"""
Test: Linking works for all content types (products, taxonomies)
Task 20: Verify product linking finds related products and services
"""
# Mock injection engine
mock_inject_links.return_value = {
'html_content': '<p>Product content with links.</p>',
'links': [
{'content_id': self.related_product.id, 'anchor_text': 'Related Product'},
{'content_id': self.service_content.id, 'anchor_text': 'Related Service'}
],
'links_added': 2
}
# Process product linking
result = self.linker_service.process_product(self.product_content.id)
# Verify result
self.assertIsNotNone(result)
self.assertEqual(result.entity_type, 'product')
self.assertIsNotNone(result.internal_links)
self.assertEqual(len(result.internal_links), 2)
self.assertEqual(result.linker_version, 1)
# Verify injection was called
mock_inject_links.assert_called_once()
candidates = mock_inject_links.call_args[0][1]
self.assertGreater(len(candidates), 0)
# Verify product candidates were found
product_candidates = [c for c in candidates if c.get('content_id') == self.related_product.id]
self.assertGreater(len(product_candidates), 0)
@patch('igny8_core.business.linking.services.linker_service.InjectionEngine.inject_links')
@patch('igny8_core.business.linking.services.linker_service.CreditService.check_credits')
@patch('igny8_core.business.linking.services.linker_service.CreditService.deduct_credits_for_operation')
def test_linking_works_for_taxonomies(self, mock_deduct, mock_check_credits, mock_inject_links):
"""
Test: Linking works for all content types (products, taxonomies)
Task 20: Verify taxonomy linking finds related taxonomies and content
"""
# Mock injection engine
mock_inject_links.return_value = {
'html_content': '<p>Taxonomy content with links.</p>',
'links': [
{'content_id': self.related_taxonomy.id, 'anchor_text': 'Related Taxonomy'}
],
'links_added': 1
}
# Process taxonomy linking
result = self.linker_service.process_taxonomy(self.taxonomy_content.id)
# Verify result
self.assertIsNotNone(result)
self.assertEqual(result.entity_type, 'taxonomy')
self.assertIsNotNone(result.internal_links)
self.assertEqual(len(result.internal_links), 1)
self.assertEqual(result.linker_version, 1)
# Verify injection was called
mock_inject_links.assert_called_once()
candidates = mock_inject_links.call_args[0][1]
self.assertGreater(len(candidates), 0)
# Verify taxonomy candidates were found
taxonomy_candidates = [c for c in candidates if c.get('content_id') == self.related_taxonomy.id]
self.assertGreater(len(taxonomy_candidates), 0)
def test_product_linking_finds_related_products(self):
"""
Test: Linking works for all content types (products, taxonomies)
Task 20: Verify _find_product_candidates finds related products
"""
candidates = self.linker_service._find_product_candidates(self.product_content)
# Should find related product
product_ids = [c['content_id'] for c in candidates]
self.assertIn(self.related_product.id, product_ids)
# Should find related service
self.assertIn(self.service_content.id, product_ids)
def test_taxonomy_linking_finds_related_taxonomies(self):
"""
Test: Linking works for all content types (products, taxonomies)
Task 20: Verify _find_taxonomy_candidates finds related taxonomies
"""
candidates = self.linker_service._find_taxonomy_candidates(self.taxonomy_content)
# Should find related taxonomy
taxonomy_ids = [c['content_id'] for c in candidates]
self.assertIn(self.related_taxonomy.id, taxonomy_ids)

View File

@@ -1,2 +0,0 @@
# Optimization tests

View File

@@ -1,177 +0,0 @@
"""
Tests for ContentAnalyzer
"""
from django.test import TestCase
from igny8_core.business.content.models import Content
from igny8_core.business.optimization.services.analyzer import ContentAnalyzer
from igny8_core.api.tests.test_integration_base import IntegrationTestBase
class ContentAnalyzerTests(IntegrationTestBase):
"""Tests for ContentAnalyzer"""
def setUp(self):
super().setUp()
self.analyzer = ContentAnalyzer()
def test_analyze_returns_all_scores(self):
"""Test that analyze returns all required scores"""
content = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title="Test Content",
html_content="<p>This is test content.</p>",
meta_title="Test Title",
meta_description="Test description",
primary_keyword="test keyword",
word_count=1500,
status='draft'
)
scores = self.analyzer.analyze(content)
self.assertIn('seo_score', scores)
self.assertIn('readability_score', scores)
self.assertIn('engagement_score', scores)
self.assertIn('overall_score', scores)
self.assertIn('word_count', scores)
self.assertIn('has_meta_title', scores)
self.assertIn('has_meta_description', scores)
self.assertIn('has_primary_keyword', scores)
self.assertIn('internal_links_count', scores)
def test_analyze_returns_zero_scores_for_empty_content(self):
"""Test that empty content returns zero scores"""
content = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title="Empty",
html_content="",
word_count=0,
status='draft'
)
scores = self.analyzer.analyze(content)
self.assertEqual(scores['seo_score'], 0)
self.assertEqual(scores['readability_score'], 0)
self.assertEqual(scores['engagement_score'], 0)
self.assertEqual(scores['overall_score'], 0)
def test_calculate_seo_score_with_meta_title(self):
"""Test SEO score calculation with meta title"""
content = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title="Test",
meta_title="Test Title" * 5, # 50 chars - optimal length
word_count=1500,
status='draft'
)
scores = self.analyzer.analyze(content)
self.assertGreater(scores['seo_score'], 0)
def test_calculate_seo_score_with_primary_keyword(self):
"""Test SEO score calculation with primary keyword"""
content = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title="Test",
primary_keyword="test keyword",
word_count=1500,
status='draft'
)
scores = self.analyzer.analyze(content)
self.assertGreater(scores['seo_score'], 0)
def test_calculate_readability_score(self):
"""Test readability score calculation"""
# Create content with good readability (short sentences, paragraphs)
html = "<p>This is a sentence.</p><p>This is another sentence.</p><p>And one more.</p>"
content = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title="Test",
html_content=html,
word_count=20,
status='draft'
)
scores = self.analyzer.analyze(content)
self.assertGreater(scores['readability_score'], 0)
def test_calculate_engagement_score_with_headings(self):
"""Test engagement score calculation with headings"""
html = "<h1>Main Heading</h1><h2>Subheading 1</h2><h2>Subheading 2</h2>"
content = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title="Test",
html_content=html,
word_count=100,
status='draft'
)
scores = self.analyzer.analyze(content)
self.assertGreater(scores['engagement_score'], 0)
def test_calculate_engagement_score_with_internal_links(self):
"""Test engagement score calculation with internal links"""
content = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title="Test",
html_content="<p>Test content.</p>",
internal_links=[
{'content_id': 1, 'anchor_text': 'link1'},
{'content_id': 2, 'anchor_text': 'link2'},
{'content_id': 3, 'anchor_text': 'link3'}
],
word_count=100,
status='draft'
)
scores = self.analyzer.analyze(content)
self.assertGreater(scores['engagement_score'], 0)
self.assertEqual(scores['internal_links_count'], 3)
def test_overall_score_is_weighted_average(self):
"""Test that overall score is weighted average"""
content = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title="Test",
html_content="<p>Test content.</p>",
meta_title="Test Title",
meta_description="Test description",
primary_keyword="test",
word_count=1500,
status='draft'
)
scores = self.analyzer.analyze(content)
# Overall should be weighted: SEO (40%) + Readability (30%) + Engagement (30%)
expected = (
scores['seo_score'] * 0.4 +
scores['readability_score'] * 0.3 +
scores['engagement_score'] * 0.3
)
self.assertAlmostEqual(scores['overall_score'], expected, places=1)

View File

@@ -1,189 +0,0 @@
"""
Tests for OptimizerService
"""
from unittest.mock import Mock, patch, MagicMock
from django.test import TestCase
from igny8_core.business.content.models import Content
from igny8_core.business.optimization.models import OptimizationTask
from igny8_core.business.optimization.services.optimizer_service import OptimizerService
from igny8_core.business.billing.exceptions import InsufficientCreditsError
from igny8_core.api.tests.test_integration_base import IntegrationTestBase
class OptimizerServiceTests(IntegrationTestBase):
"""Tests for OptimizerService"""
def setUp(self):
super().setUp()
self.service = OptimizerService()
# Create test content
self.content = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title="Test Content",
html_content="<p>This is test content.</p>",
meta_title="Test Title",
meta_description="Test description",
primary_keyword="test keyword",
word_count=500,
status='draft',
source='igny8'
)
@patch('igny8_core.business.optimization.services.optimizer_service.CreditService.check_credits')
@patch('igny8_core.business.optimization.services.optimizer_service.ContentAnalyzer.analyze')
@patch('igny8_core.business.optimization.services.optimizer_service.OptimizerService._optimize_content')
@patch('igny8_core.business.optimization.services.optimizer_service.CreditService.deduct_credits_for_operation')
def test_optimize_from_writer(self, mock_deduct, mock_optimize, mock_analyze, mock_check):
"""Test optimize_from_writer entry point"""
mock_check.return_value = True
mock_analyze.return_value = {
'seo_score': 50.0,
'readability_score': 60.0,
'engagement_score': 55.0,
'overall_score': 55.0
}
optimized_content = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title="Optimized Content",
html_content="<p>Optimized content.</p>",
word_count=500,
status='draft',
source='igny8'
)
mock_optimize.return_value = optimized_content
result = self.service.optimize_from_writer(self.content.id)
self.assertEqual(result.id, self.content.id)
mock_check.assert_called_once()
mock_deduct.assert_called_once()
def test_optimize_from_writer_invalid_content(self):
"""Test that ValueError is raised for invalid content"""
with self.assertRaises(ValueError):
self.service.optimize_from_writer(99999)
def test_optimize_from_writer_wrong_source(self):
"""Test that ValueError is raised for wrong source"""
content = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title="WordPress Content",
word_count=100,
source='wordpress'
)
with self.assertRaises(ValueError):
self.service.optimize_from_writer(content.id)
@patch('igny8_core.business.optimization.services.optimizer_service.CreditService.check_credits')
def test_optimize_insufficient_credits(self, mock_check):
"""Test that InsufficientCreditsError is raised when credits are insufficient"""
mock_check.side_effect = InsufficientCreditsError("Insufficient credits")
with self.assertRaises(InsufficientCreditsError):
self.service.optimize(self.content)
@patch('igny8_core.business.optimization.services.optimizer_service.CreditService.check_credits')
@patch('igny8_core.business.optimization.services.optimizer_service.ContentAnalyzer.analyze')
@patch('igny8_core.business.optimization.services.optimizer_service.OptimizerService._optimize_content')
@patch('igny8_core.business.optimization.services.optimizer_service.CreditService.deduct_credits_for_operation')
def test_optimize_creates_optimization_task(self, mock_deduct, mock_optimize, mock_analyze, mock_check):
"""Test that optimization creates OptimizationTask"""
mock_check.return_value = True
scores = {
'seo_score': 50.0,
'readability_score': 60.0,
'engagement_score': 55.0,
'overall_score': 55.0
}
mock_analyze.return_value = scores
optimized_content = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title="Optimized",
html_content="<p>Optimized.</p>",
word_count=500,
status='draft'
)
mock_optimize.return_value = optimized_content
result = self.service.optimize(self.content)
# Check that task was created
task = OptimizationTask.objects.filter(content=self.content).first()
self.assertIsNotNone(task)
self.assertEqual(task.status, 'completed')
self.assertEqual(task.scores_before, scores)
@patch('igny8_core.business.optimization.services.optimizer_service.CreditService.check_credits')
@patch('igny8_core.business.optimization.services.optimizer_service.ContentAnalyzer.analyze')
def test_analyze_only_returns_scores(self, mock_analyze, mock_check):
"""Test analyze_only method returns scores without optimizing"""
scores = {
'seo_score': 50.0,
'readability_score': 60.0,
'engagement_score': 55.0,
'overall_score': 55.0
}
mock_analyze.return_value = scores
result = self.service.analyze_only(self.content.id)
self.assertEqual(result, scores)
mock_analyze.assert_called_once()
def test_optimize_from_wordpress_sync(self):
"""Test optimize_from_wordpress_sync entry point"""
content = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title="WordPress Content",
word_count=100,
source='wordpress'
)
with patch.object(self.service, 'optimize') as mock_optimize:
mock_optimize.return_value = content
result = self.service.optimize_from_wordpress_sync(content.id)
self.assertEqual(result.id, content.id)
mock_optimize.assert_called_once()
def test_optimize_from_external_sync(self):
"""Test optimize_from_external_sync entry point"""
content = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title="Shopify Content",
word_count=100,
source='shopify'
)
with patch.object(self.service, 'optimize') as mock_optimize:
mock_optimize.return_value = content
result = self.service.optimize_from_external_sync(content.id)
self.assertEqual(result.id, content.id)
mock_optimize.assert_called_once()
def test_optimize_manual(self):
"""Test optimize_manual entry point"""
with patch.object(self.service, 'optimize') as mock_optimize:
mock_optimize.return_value = self.content
result = self.service.optimize_manual(self.content.id)
self.assertEqual(result.id, self.content.id)
mock_optimize.assert_called_once()

View File

@@ -1,184 +0,0 @@
"""
Tests for Universal Content Types Optimization (Phase 8)
Tests for product and taxonomy optimization
"""
from unittest.mock import patch, MagicMock
from django.test import TestCase
from igny8_core.business.content.models import Content
from igny8_core.business.optimization.services.optimizer_service import OptimizerService
from igny8_core.business.optimization.models import OptimizationTask
from igny8_core.api.tests.test_integration_base import IntegrationTestBase
class UniversalContentOptimizationTests(IntegrationTestBase):
"""Tests for Phase 8: Universal Content Types Optimization"""
def setUp(self):
super().setUp()
# Add credits to account for testing
self.account.credits = 10000
self.account.save()
self.optimizer_service = OptimizerService()
# Create product content
self.product_content = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title='Test Product',
html_content='<p>Product content that needs optimization.</p>',
entity_type='product',
json_blocks=[
{'type': 'features', 'heading': 'Features', 'items': ['Feature 1']},
{'type': 'specifications', 'heading': 'Specs', 'data': {'Spec': 'Value'}}
],
structure_data={'product_type': 'software', 'price_range': '$99-$199'},
word_count=1500,
status='draft'
)
# Create taxonomy content
self.taxonomy_content = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title='Test Taxonomy',
html_content='<p>Taxonomy content that needs optimization.</p>',
entity_type='taxonomy',
json_blocks=[
{'type': 'categories', 'heading': 'Categories', 'items': [{'name': 'Cat 1'}]},
{'type': 'tags', 'heading': 'Tags', 'items': ['Tag 1']},
{'type': 'hierarchy', 'heading': 'Hierarchy', 'structure': {}}
],
word_count=1200,
status='draft'
)
@patch('igny8_core.business.optimization.services.optimizer_service.OptimizerService._optimize_content')
@patch('igny8_core.business.optimization.services.optimizer_service.ContentAnalyzer.analyze')
@patch('igny8_core.business.optimization.services.optimizer_service.CreditService.check_credits')
@patch('igny8_core.business.optimization.services.optimizer_service.CreditService.get_credit_cost')
@patch('igny8_core.business.optimization.services.optimizer_service.CreditService.deduct_credits_for_operation')
def test_optimization_works_for_products(self, mock_deduct, mock_get_cost, mock_check_credits, mock_analyze, mock_optimize):
"""
Test: Optimization works for all content types (products, taxonomies)
Task 21: Verify product optimization includes product-specific metrics
"""
# Mock analyzer
mock_analyze.return_value = {
'seo_score': 75,
'readability_score': 80,
'engagement_score': 70,
'overall_score': 75
}
# Mock credit cost
mock_get_cost.return_value = 10
# Mock optimization
optimized_content = Content.objects.get(id=self.product_content.id)
optimized_content.html_content = '<p>Optimized product content.</p>'
mock_optimize.return_value = optimized_content
# Optimize product
result = self.optimizer_service.optimize_product(self.product_content.id)
# Verify result
self.assertIsNotNone(result)
self.assertEqual(result.entity_type, 'product')
self.assertEqual(result.optimizer_version, 1)
self.assertIsNotNone(result.optimization_scores)
# Verify product-specific scores were enhanced
scores = result.optimization_scores
self.assertIn('product_completeness', scores)
self.assertGreaterEqual(scores['product_completeness'], 0)
self.assertLessEqual(scores['product_completeness'], 1)
# Verify optimization task was created
task = OptimizationTask.objects.filter(content=result).first()
self.assertIsNotNone(task)
self.assertEqual(task.status, 'completed')
self.assertIn('product_completeness', task.scores_after)
@patch('igny8_core.business.optimization.services.optimizer_service.OptimizerService._optimize_content')
@patch('igny8_core.business.optimization.services.optimizer_service.ContentAnalyzer.analyze')
@patch('igny8_core.business.optimization.services.optimizer_service.CreditService.check_credits')
@patch('igny8_core.business.optimization.services.optimizer_service.CreditService.get_credit_cost')
@patch('igny8_core.business.optimization.services.optimizer_service.CreditService.deduct_credits_for_operation')
def test_optimization_works_for_taxonomies(self, mock_deduct, mock_get_cost, mock_check_credits, mock_analyze, mock_optimize):
"""
Test: Optimization works for all content types (products, taxonomies)
Task 21: Verify taxonomy optimization includes taxonomy-specific metrics
"""
# Mock analyzer
mock_analyze.return_value = {
'seo_score': 70,
'readability_score': 75,
'engagement_score': 65,
'overall_score': 70
}
# Mock credit cost
mock_get_cost.return_value = 8
# Mock optimization
optimized_content = Content.objects.get(id=self.taxonomy_content.id)
optimized_content.html_content = '<p>Optimized taxonomy content.</p>'
mock_optimize.return_value = optimized_content
# Optimize taxonomy
result = self.optimizer_service.optimize_taxonomy(self.taxonomy_content.id)
# Verify result
self.assertIsNotNone(result)
self.assertEqual(result.entity_type, 'taxonomy')
self.assertEqual(result.optimizer_version, 1)
self.assertIsNotNone(result.optimization_scores)
# Verify taxonomy-specific scores were enhanced
scores = result.optimization_scores
self.assertIn('taxonomy_organization', scores)
self.assertGreaterEqual(scores['taxonomy_organization'], 0)
self.assertLessEqual(scores['taxonomy_organization'], 1)
# Verify optimization task was created
task = OptimizationTask.objects.filter(content=result).first()
self.assertIsNotNone(task)
self.assertEqual(task.status, 'completed')
self.assertIn('taxonomy_organization', task.scores_after)
def test_enhance_product_scores_includes_completeness(self):
"""
Test: Optimization works for all content types (products, taxonomies)
Task 21: Verify _enhance_product_scores adds product_completeness
"""
base_scores = {
'seo_score': 75,
'readability_score': 80,
'overall_score': 75
}
enhanced = self.optimizer_service._enhance_product_scores(base_scores, self.product_content)
self.assertIn('product_completeness', enhanced)
self.assertGreaterEqual(enhanced['product_completeness'], 0)
self.assertLessEqual(enhanced['product_completeness'], 1)
def test_enhance_taxonomy_scores_includes_organization(self):
"""
Test: Optimization works for all content types (products, taxonomies)
Task 21: Verify _enhance_taxonomy_scores adds taxonomy_organization
"""
base_scores = {
'seo_score': 70,
'readability_score': 75,
'overall_score': 70
}
enhanced = self.optimizer_service._enhance_taxonomy_scores(base_scores, self.taxonomy_content)
self.assertIn('taxonomy_organization', enhanced)
self.assertGreaterEqual(enhanced['taxonomy_organization'], 0)
self.assertLessEqual(enhanced['taxonomy_organization'], 1)

View File

@@ -1,5 +0,0 @@
"""
Publishing Tests
Phase 5: Sites Renderer & Publishing
"""

View File

@@ -1,139 +0,0 @@
"""
Tests for Publishing Adapters
Phase 6: Site Integration & Multi-Destination Publishing
"""
from django.test import TestCase
from unittest.mock import Mock, patch
from igny8_core.auth.models import Account, Site, Sector, User, Plan, Industry, IndustrySector
from igny8_core.business.publishing.services.adapters.base_adapter import BaseAdapter
from igny8_core.business.publishing.services.adapters.sites_renderer_adapter import SitesRendererAdapter
from igny8_core.business.publishing.services.adapters.wordpress_adapter import WordPressAdapter
from igny8_core.business.site_building.models import SiteBlueprint
class AdapterPatternTestCase(TestCase):
"""Test cases for adapter pattern"""
def setUp(self):
"""Set up test data"""
# Create plan first
self.plan = Plan.objects.create(
name="Test Plan",
slug="test-plan",
price=0,
credits_per_month=1000
)
# Create user first (Account needs owner)
self.user = User.objects.create_user(
username='testuser',
email='test@test.com',
password='testpass123',
role='owner'
)
# Create account with owner
self.account = Account.objects.create(
name="Test Account",
slug="test-account",
plan=self.plan,
owner=self.user
)
# Update user to have account
self.user.account = self.account
self.user.save()
# Create industry and sector
self.industry = Industry.objects.create(
name="Test Industry",
slug="test-industry"
)
self.industry_sector = IndustrySector.objects.create(
industry=self.industry,
name="Test Sector",
slug="test-sector"
)
self.site = Site.objects.create(
account=self.account,
name="Test Site",
slug="test-site",
industry=self.industry
)
self.sector = Sector.objects.create(
account=self.account,
site=self.site,
industry_sector=self.industry_sector,
name="Test Sector",
slug="test-sector"
)
self.blueprint = SiteBlueprint.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
name="Test Blueprint",
status='ready'
)
def test_sites_renderer_adapter_implements_base_interface(self):
"""Test: Adapter pattern works correctly"""
adapter = SitesRendererAdapter()
self.assertIsInstance(adapter, BaseAdapter)
self.assertTrue(hasattr(adapter, 'publish'))
self.assertTrue(hasattr(adapter, 'test_connection'))
self.assertTrue(hasattr(adapter, 'get_status'))
def test_wordpress_adapter_implements_base_interface(self):
"""Test: Adapter pattern works correctly"""
adapter = WordPressAdapter()
self.assertIsInstance(adapter, BaseAdapter)
self.assertTrue(hasattr(adapter, 'publish'))
self.assertTrue(hasattr(adapter, 'test_connection'))
self.assertTrue(hasattr(adapter, 'get_status'))
def test_sites_renderer_adapter_deploys_site(self):
"""Test: Multi-destination publishing works"""
adapter = SitesRendererAdapter()
result = adapter.deploy(self.blueprint)
self.assertTrue(result.get('success'))
self.assertIsNotNone(result.get('deployment_url'))
self.assertIsNotNone(result.get('version'))
def test_wordpress_adapter_publishes_content(self):
"""Test: Multi-destination publishing works"""
from igny8_core.business.content.models import Content
content = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title="Test Content",
html_content="<p>Test</p>"
)
adapter = WordPressAdapter()
config = {
'site_url': 'https://example.com',
'username': 'test',
'app_password': 'test'
}
# Patch WordPressClient at the point where it's used in the adapter
with patch('igny8_core.business.publishing.services.adapters.wordpress_adapter.WordPressClient') as mock_client_class:
mock_instance = Mock()
mock_instance.create_post.return_value = {'id': 123, 'link': 'https://example.com/post/123'}
mock_client_class.return_value = mock_instance
result = adapter.publish(content, config)
self.assertTrue(result.get('success'))
self.assertIsNotNone(result.get('external_id'))
self.assertIsNotNone(result.get('url'))

View File

@@ -1,141 +0,0 @@
"""
DEPRECATED: Tests for DeploymentService - SiteBlueprint models removed
Phase 5: Sites Renderer & Publishing
"""
from django.test import TestCase
from django.utils import timezone
from igny8_core.auth.models import Account, Site, Sector, User, Plan, Industry, IndustrySector
from igny8_core.business.publishing.models import DeploymentRecord
from igny8_core.business.publishing.services.deployment_service import DeploymentService
class DeploymentServiceTestCase(TestCase):
"""DEPRECATED: Test cases for DeploymentService"""
def setUp(self):
"""Set up test data"""
# Create plan first
self.plan = Plan.objects.create(
name="Test Plan",
slug="test-plan",
price=0,
credits_per_month=1000
)
# Create user first (Account needs owner)
self.user = User.objects.create_user(
username='testuser',
email='test@test.com',
password='testpass123',
role='owner'
)
# Create account with owner
self.account = Account.objects.create(
name="Test Account",
slug="test-account",
plan=self.plan,
owner=self.user
)
# Update user to have account
self.user.account = self.account
self.user.save()
# Create industry and sector
self.industry = Industry.objects.create(
name="Test Industry",
slug="test-industry"
)
self.industry_sector = IndustrySector.objects.create(
industry=self.industry,
name="Test Sector",
slug="test-sector"
)
self.site = Site.objects.create(
account=self.account,
name="Test Site",
slug="test-site",
industry=self.industry
)
self.sector = Sector.objects.create(
account=self.account,
site=self.site,
industry_sector=self.industry_sector,
name="Test Sector",
slug="test-sector"
)
# DEPRECATED: SiteBlueprint model removed
self.blueprint = None
self.service = DeploymentService()
def test_get_status_returns_deployed_record(self):
"""Test: Sites are accessible publicly"""
DeploymentRecord.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
site_blueprint=self.blueprint,
version=1,
status='deployed',
deployment_url='https://test-site.igny8.com',
deployed_at=timezone.now()
)
status = self.service.get_status(self.blueprint)
self.assertIsNotNone(status)
self.assertEqual(status.status, 'deployed')
self.assertEqual(status.deployment_url, 'https://test-site.igny8.com')
def test_get_latest_deployment_returns_most_recent(self):
"""Test: Deployment works end-to-end"""
DeploymentRecord.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
site_blueprint=self.blueprint,
version=1,
status='failed',
created_at=timezone.now()
)
latest = DeploymentRecord.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
site_blueprint=self.blueprint,
version=2,
status='deployed',
deployment_url='https://test-site.igny8.com',
deployed_at=timezone.now()
)
result = self.service.get_latest_deployment(self.blueprint)
self.assertIsNotNone(result)
self.assertEqual(result.version, 2)
self.assertEqual(result.status, 'deployed')
def test_rollback_reverts_to_previous_version(self):
"""Test: Deployment works end-to-end"""
DeploymentRecord.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
site_blueprint=self.blueprint,
version=1,
status='deployed',
deployment_url='https://test-site.igny8.com',
deployed_at=timezone.now()
)
result = self.service.rollback(self.blueprint, target_version=1)
self.assertTrue(result.get('success'))
self.blueprint.refresh_from_db()
self.assertEqual(self.blueprint.deployed_version, 1)

View File

@@ -1,151 +0,0 @@
"""
Tests for PublisherService
Phase 5: Sites Renderer & Publishing
"""
from django.test import TestCase
from django.utils import timezone
from unittest.mock import Mock, patch
from igny8_core.auth.models import Account, Site, Sector, User, Plan, Industry, IndustrySector
from igny8_core.business.site_building.models import SiteBlueprint
from igny8_core.business.publishing.models import PublishingRecord, DeploymentRecord
from igny8_core.business.publishing.services.publisher_service import PublisherService
class PublisherServiceTestCase(TestCase):
"""Test cases for PublisherService"""
def setUp(self):
"""Set up test data"""
# Create plan first
self.plan = Plan.objects.create(
name="Test Plan",
slug="test-plan",
price=0,
credits_per_month=1000
)
# Create user first (Account needs owner)
self.user = User.objects.create_user(
username='testuser',
email='test@test.com',
password='testpass123',
role='owner'
)
# Create account with owner
self.account = Account.objects.create(
name="Test Account",
slug="test-account",
plan=self.plan,
owner=self.user
)
# Update user to have account
self.user.account = self.account
self.user.save()
# Create industry and sector
self.industry = Industry.objects.create(
name="Test Industry",
slug="test-industry"
)
self.industry_sector = IndustrySector.objects.create(
industry=self.industry,
name="Test Sector",
slug="test-sector"
)
self.site = Site.objects.create(
account=self.account,
name="Test Site",
slug="test-site",
industry=self.industry
)
self.sector = Sector.objects.create(
account=self.account,
site=self.site,
industry_sector=self.industry_sector,
name="Test Sector",
slug="test-sector"
)
self.blueprint = SiteBlueprint.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
name="Test Blueprint",
status='ready'
)
self.service = PublisherService()
def test_publish_to_sites_creates_deployment_record(self):
"""Test: Deployment works end-to-end"""
# Don't mock deploy - let it run to create the deployment record
# But mock the filesystem operations to avoid actual file writes
with patch('igny8_core.business.publishing.services.adapters.sites_renderer_adapter.Path.mkdir'), \
patch('igny8_core.business.publishing.services.adapters.sites_renderer_adapter.open', create=True) as mock_open:
mock_file = mock_open.return_value.__enter__.return_value
result = self.service.publish_to_sites(self.blueprint)
self.assertTrue(result.get('success'))
self.assertIsNotNone(result.get('deployment_url'))
# Verify deployment record was created
deployment = DeploymentRecord.objects.filter(site_blueprint=self.blueprint).first()
self.assertIsNotNone(deployment)
def test_get_deployment_status_returns_latest(self):
"""Test: Sites are accessible publicly"""
DeploymentRecord.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
site_blueprint=self.blueprint,
version=1,
status='deployed',
deployment_url='https://test-site.igny8.com',
deployed_at=timezone.now()
)
status = self.service.get_deployment_status(self.blueprint)
self.assertIsNotNone(status)
self.assertEqual(status.status, 'deployed')
self.assertIsNotNone(status.deployment_url)
def test_publish_content_to_multiple_destinations(self):
"""Test: Multi-destination publishing works"""
from igny8_core.business.content.models import Content
content = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title="Test Content",
html_content="<p>Test</p>"
)
with patch.object(self.service, '_get_adapter') as mock_get_adapter:
mock_adapter = Mock()
mock_adapter.publish.return_value = {
'success': True,
'external_id': '123',
'url': 'https://example.com/post/123'
}
mock_get_adapter.return_value = mock_adapter
result = self.service.publish_content(
content_id=content.id,
destinations=['wordpress', 'sites'],
account=self.account
)
self.assertTrue(result.get('success'))
self.assertEqual(len(result.get('results', [])), 2)
# Verify publishing records were created
records = PublishingRecord.objects.filter(content=content)
self.assertEqual(records.count(), 2)