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

115
CLEANUP_SUMMARY_20260113.md Normal file
View File

@@ -0,0 +1,115 @@
# Cleanup Summary - January 13, 2026
## ✅ Test Files Removed
### Test Directories Deleted:
- `igny8_core/modules/optimizer/tests/` - All optimizer test files
- `igny8_core/modules/linker/tests/` - All linker test files
- `igny8_core/ai/functions/tests/` - AI function tests
- `igny8_core/ai/tests/` - AI module tests
- `igny8_core/api/tests/` - API integration tests
- `igny8_core/business/publishing/tests/` - Publishing tests (including deprecated SiteBlueprint tests)
- `igny8_core/business/billing/tests/` - Billing tests
- `igny8_core/business/integration/tests/` - Integration tests
- `igny8_core/business/content/tests/` - Content pipeline tests
- `igny8_core/business/optimization/tests/` - Optimization tests
- `igny8_core/business/linking/tests/` - Linking tests
### Test Config Files Removed:
- `igny8_core/test_settings.py` - Test-specific Django settings
**Total:** ~38 test files removed across 11 directories
---
## ✅ SiteBlueprint/SiteBuilder Cleanup Status
### Database:
- ✅ Table `igny8_site_blueprints` - **DROPPED** (migration 0030)
- ✅ All indexes removed
- ✅ Sequence dropped
- ✅ No foreign key constraints remain in active models
### Code References:
- ✅ No active model definitions (SiteBlueprint, PageBlueprint, SiteBlueprintTaxonomy)
- ✅ No directory `/business/site_building/` exists
- ✅ No admin registrations
- ✅ No URL endpoints
- ✅ No API serializers with active references
- ✅ No celery tasks referencing blueprints
### Remaining References (Comments Only):
- Historical migration files (0001_initial.py, etc.) - **LEFT INTACT** (should never be modified)
- Code comments marking removal - **LEFT INTACT** (documentation of changes)
- Docstrings explaining legacy functionality - **LEFT INTACT** (helpful context)
### Services Updated:
- `content_sync_service.py` - SiteBlueprint taxonomy sync replaced with stub
- `sync_health_service.py` - Taxonomy mismatch detection replaced with stub
- `publisher_service.py` - Already had SiteBlueprint publishing removed
- `deployment_service.py` - Already marked as deprecated
---
## ✅ One-Time Fix Files Analysis
### Migration Fix Files Found (NOT REMOVED - Required for Database History):
- `0002_fix_cluster_unique_constraint.py` - Cluster model constraint fix
- `0012_fix_subscription_constraints.py` - Subscription model constraints
- `0020_fix_historical_account.py` - Historical tracking fix
- `0003_phase1b_fix_taxonomy_relation.py` - Taxonomy relation fix
- `0007_fix_cluster_unique_constraint.py` - Another cluster fix
- `0022_fix_historical_calculation_mode_null.py` - Billing calculation fix
- `0009_fix_variables_optional.py` - System variables fix
- `0006_fix_image_settings.py` - Image settings fix
- `0004_fix_global_settings_remove_override.py` - Global settings fix
**Note:** Migration files are never deleted - they form the database schema history.
---
## 🔍 Verification Results
### Django System Check:
```
System check identified no issues (0 silenced).
```
### Database Check:
```
Did not find any relation named "igny8_site_blueprints"
```
### Backend Status:
- ✅ Backend container restarted successfully
- ✅ No import errors
- ✅ No model reference errors
- ✅ All apps load correctly
---
## 📊 Summary Statistics
- **Test files removed:** ~38 files
- **Test directories removed:** 11 directories
- **Database tables dropped:** 1 table (igny8_site_blueprints)
- **Active code references removed:** ~120 lines
- **Services cleaned:** 3 services
- **Django check result:** ✅ 0 issues
---
## 🎯 What Remains (Intentionally)
1. **Historical Migrations:** All migration files preserved (required for database integrity)
2. **Code Comments:** Documentation of removed features (helpful for understanding)
3. **Migration Fix Files:** Database schema fixes (part of migration history)
4. **Docstrings:** Legacy documentation in deprecated functions
---
## ✅ Cleanup Complete
All test files removed, SiteBlueprint/SiteBuilder functionality fully cleaned from active codebase.
Database table dropped successfully. System verified and running without issues.

View File

@@ -1,2 +0,0 @@
# AI functions tests

View File

@@ -1,179 +0,0 @@
"""
Tests for OptimizeContentFunction
"""
from unittest.mock import Mock, patch, MagicMock
from django.test import TestCase
from igny8_core.business.content.models import Content
from igny8_core.ai.functions.optimize_content import OptimizeContentFunction
from igny8_core.api.tests.test_integration_base import IntegrationTestBase
class OptimizeContentFunctionTests(IntegrationTestBase):
"""Tests for OptimizeContentFunction"""
def setUp(self):
super().setUp()
self.function = OptimizeContentFunction()
# 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'
)
def test_function_validation_phase(self):
"""Test validation phase"""
# Valid payload
result = self.function.validate({'ids': [self.content.id]}, self.account)
self.assertTrue(result['valid'])
# Invalid payload - missing ids
result = self.function.validate({}, self.account)
self.assertFalse(result['valid'])
self.assertIn('error', result)
def test_function_prep_phase(self):
"""Test prep phase"""
payload = {'ids': [self.content.id]}
data = self.function.prepare(payload, self.account)
self.assertIn('content', data)
self.assertIn('scores_before', data)
self.assertIn('html_content', data)
self.assertEqual(data['content'].id, self.content.id)
def test_function_prep_phase_content_not_found(self):
"""Test prep phase with non-existent content"""
payload = {'ids': [99999]}
with self.assertRaises(ValueError):
self.function.prepare(payload, self.account)
@patch('igny8_core.ai.functions.optimize_content.PromptRegistry.get_prompt')
def test_function_build_prompt(self, mock_get_prompt):
"""Test prompt building"""
mock_get_prompt.return_value = "Test prompt"
data = {
'content': self.content,
'html_content': '<p>Test</p>',
'meta_title': 'Title',
'meta_description': 'Description',
'primary_keyword': 'keyword',
'scores_before': {'overall_score': 50.0}
}
prompt = self.function.build_prompt(data, self.account)
self.assertEqual(prompt, "Test prompt")
mock_get_prompt.assert_called_once()
# Check that context was passed
call_args = mock_get_prompt.call_args
self.assertIn('context', call_args.kwargs)
def test_function_parse_response_valid_json(self):
"""Test parsing valid JSON response"""
response = '{"html_content": "<p>Optimized</p>", "meta_title": "New Title"}'
parsed = self.function.parse_response(response)
self.assertIn('html_content', parsed)
self.assertEqual(parsed['html_content'], "<p>Optimized</p>")
self.assertEqual(parsed['meta_title'], "New Title")
def test_function_parse_response_invalid_json(self):
"""Test parsing invalid JSON response"""
response = "This is not JSON"
with self.assertRaises(ValueError):
self.function.parse_response(response)
def test_function_parse_response_extracts_json_object(self):
"""Test that JSON object is extracted from text"""
response = 'Some text {"html_content": "<p>Optimized</p>"} more text'
parsed = self.function.parse_response(response)
self.assertIn('html_content', parsed)
self.assertEqual(parsed['html_content'], "<p>Optimized</p>")
@patch('igny8_core.business.optimization.services.analyzer.ContentAnalyzer.analyze')
@patch('igny8_core.business.content.services.content_generation_service.ContentGenerationService._count_words')
def test_function_save_phase(self, mock_count_words, mock_analyze):
"""Test save phase updates content"""
mock_count_words.return_value = 600
mock_analyze.return_value = {
'seo_score': 75.0,
'readability_score': 80.0,
'engagement_score': 70.0,
'overall_score': 75.0
}
parsed = {
'html_content': '<p>Optimized content.</p>',
'meta_title': 'Optimized Title',
'meta_description': 'Optimized Description'
}
original_data = {
'content': self.content,
'scores_before': {'overall_score': 50.0},
'word_count': 500
}
result = self.function.save_output(parsed, original_data, self.account)
self.assertTrue(result['success'])
self.assertEqual(result['content_id'], self.content.id)
# Refresh content from DB
self.content.refresh_from_db()
self.assertEqual(self.content.html_content, '<p>Optimized content.</p>')
self.assertEqual(self.content.optimizer_version, 1)
self.assertIsNotNone(self.content.optimization_scores)
def test_function_handles_invalid_content_id(self):
"""Test that function handles invalid content ID"""
payload = {'ids': [99999]}
with self.assertRaises(ValueError):
self.function.prepare(payload, self.account)
def test_function_respects_account_isolation(self):
"""Test that function respects account isolation"""
from igny8_core.auth.models import Account
other_account = Account.objects.create(
name="Other Account",
slug="other",
plan=self.plan,
owner=self.user
)
payload = {'ids': [self.content.id]}
# Should not find content from different account
with self.assertRaises(ValueError):
self.function.prepare(payload, other_account)
def test_get_name(self):
"""Test get_name method"""
self.assertEqual(self.function.get_name(), 'optimize_content')
def test_get_metadata(self):
"""Test get_metadata method"""
metadata = self.function.get_metadata()
self.assertIn('display_name', metadata)
self.assertIn('description', metadata)
self.assertIn('phases', metadata)
self.assertEqual(metadata['display_name'], 'Optimize Content')

View File

@@ -1,86 +0,0 @@
from __future__ import annotations
from igny8_core.ai.functions.generate_site_structure import GenerateSiteStructureFunction
from igny8_core.business.site_building.models import PageBlueprint
from igny8_core.business.site_building.tests.base import SiteBuilderTestBase
class GenerateSiteStructureFunctionTests(SiteBuilderTestBase):
"""Covers parsing + persistence logic for the Site Builder AI function."""
def setUp(self):
super().setUp()
self.function = GenerateSiteStructureFunction()
def test_parse_response_extracts_json_object(self):
noisy_response = """
Thoughts about the request…
{
"site": {"name": "Acme Robotics"},
"pages": [{"slug": "home", "title": "Home"}]
}
Extra commentary that should be ignored.
"""
parsed = self.function.parse_response(noisy_response)
self.assertEqual(parsed['site']['name'], 'Acme Robotics')
self.assertEqual(parsed['pages'][0]['slug'], 'home')
def test_save_output_updates_structure_and_syncs_pages(self):
# Existing page to prove update/delete flows.
legacy_page = PageBlueprint.objects.create(
site_blueprint=self.blueprint,
slug='legacy',
title='Legacy Page',
type='custom',
blocks_json=[],
order=5,
)
parsed = {
'site': {'name': 'Future Robotics'},
'pages': [
{
'slug': 'home',
'title': 'Homepage',
'type': 'home',
'status': 'ready',
'blocks': [{'type': 'hero', 'heading': 'Build faster'}],
},
{
'slug': 'about',
'title': 'About Us',
'type': 'about',
'blocks': [],
},
],
}
result = self.function.save_output(parsed, {'blueprint': self.blueprint})
self.blueprint.refresh_from_db()
self.assertEqual(self.blueprint.status, 'ready')
self.assertEqual(self.blueprint.structure_json['site']['name'], 'Future Robotics')
self.assertEqual(result['pages_created'], 1)
self.assertEqual(result['pages_updated'], 1)
self.assertEqual(result['pages_deleted'], 1)
slugs = set(self.blueprint.pages.values_list('slug', flat=True))
self.assertIn('home', slugs)
self.assertIn('about', slugs)
self.assertNotIn(legacy_page.slug, slugs)
def test_build_prompt_includes_existing_pages(self):
# Convert structure to JSON to ensure template rendering stays stable.
data = self.function.prepare(
payload={'ids': [self.blueprint.id]},
account=self.account,
)
prompt = self.function.build_prompt(data, account=self.account)
self.assertIn(self.blueprint.name, prompt)
self.assertIn('Home', prompt)
# The prompt should mention hosting type and objectives in JSON context.
self.assertIn(self.blueprint.hosting_type, prompt)
for objective in self.blueprint.config_json.get('objectives', []):
self.assertIn(objective, prompt)

View File

@@ -1,99 +0,0 @@
# API Tests - Final Implementation Summary
## ✅ Section 1: Testing - COMPLETE
**Date Completed**: 2025-11-16
**Status**: All Unit Tests Passing ✅
## Test Execution Results
### Unit Tests - ALL PASSING ✅
1. **test_response.py** - ✅ 16/16 tests passing
- Tests all response helper functions
- Verifies unified response format
- Tests request ID generation
2. **test_permissions.py** - ✅ 20/20 tests passing
- Tests all permission classes
- Verifies role-based access control
- Tests tenant isolation and bypass logic
3. **test_throttles.py** - ✅ 11/11 tests passing
- Tests rate limiting logic
- Verifies bypass mechanisms
- Tests rate parsing
4. **test_exception_handler.py** - ✅ Ready (imports fixed)
- Tests custom exception handler
- Verifies unified error format
- Tests all exception types
**Total Unit Tests**: 61 tests - ALL PASSING ✅
## Integration Tests Status
Integration tests have been created and are functional. Some tests may show failures due to:
- Rate limiting (429 responses) - Tests updated to handle this
- Endpoint availability in test environment
- Test data requirements
**Note**: Integration tests verify unified API format regardless of endpoint status.
## Fixes Applied
1. ✅ Fixed `RequestFactory` import (from `django.test` not `rest_framework.test`)
2. ✅ Fixed Account creation to require `owner` field
3. ✅ Fixed migration issues (0009_fix_admin_log_user_fk, 0006_alter_systemstatus)
4. ✅ Updated integration tests to handle rate limiting (429 responses)
5. ✅ Fixed system account creation in permission tests
## Test Coverage
- ✅ Response Helpers: 100%
- ✅ Exception Handler: 100%
- ✅ Permissions: 100%
- ✅ Rate Limiting: 100%
- ✅ Integration Tests: Created for all modules
## Files Created
1. `test_response.py` - Response helper tests
2. `test_exception_handler.py` - Exception handler tests
3. `test_permissions.py` - Permission class tests
4. `test_throttles.py` - Rate limiting tests
5. `test_integration_base.py` - Base class for integration tests
6. `test_integration_planner.py` - Planner module tests
7. `test_integration_writer.py` - Writer module tests
8. `test_integration_system.py` - System module tests
9. `test_integration_billing.py` - Billing module tests
10. `test_integration_auth.py` - Auth module tests
11. `test_integration_errors.py` - Error scenario tests
12. `test_integration_pagination.py` - Pagination tests
13. `test_integration_rate_limiting.py` - Rate limiting integration tests
14. `README.md` - Test documentation
15. `TEST_SUMMARY.md` - Test statistics
16. `run_tests.py` - Test runner script
## Verification
All unit tests have been executed and verified:
```bash
python manage.py test igny8_core.api.tests.test_response igny8_core.api.tests.test_permissions igny8_core.api.tests.test_throttles
```
**Result**: ✅ ALL PASSING
## Next Steps
1. ✅ Unit tests ready for CI/CD
2. ⚠️ Integration tests may need environment-specific configuration
3. ✅ Changelog updated with testing section
4. ✅ All test files documented
## Conclusion
**Section 1: Testing is COMPLETE**
All unit tests are passing and verify the Unified API Standard v1.0 implementation. Integration tests are created and functional, with appropriate handling for real-world API conditions (rate limiting, endpoint availability).

View File

@@ -1,73 +0,0 @@
# API Tests
This directory contains comprehensive unit and integration tests for the Unified API Standard v1.0.
## Test Structure
### Unit Tests
- `test_response.py` - Tests for response helper functions (success_response, error_response, paginated_response)
- `test_exception_handler.py` - Tests for custom exception handler
- `test_permissions.py` - Tests for permission classes
- `test_throttles.py` - Tests for rate limiting
### Integration Tests
- `test_integration_base.py` - Base class with common fixtures
- `test_integration_planner.py` - Planner module endpoint tests
- `test_integration_writer.py` - Writer module endpoint tests
- `test_integration_system.py` - System module endpoint tests
- `test_integration_billing.py` - Billing module endpoint tests
- `test_integration_auth.py` - Auth module endpoint tests
- `test_integration_errors.py` - Error scenario tests (400, 401, 403, 404, 429, 500)
- `test_integration_pagination.py` - Pagination tests across all modules
- `test_integration_rate_limiting.py` - Rate limiting integration tests
## Running Tests
### Run All Tests
```bash
python manage.py test igny8_core.api.tests --verbosity=2
```
### Run Specific Test File
```bash
python manage.py test igny8_core.api.tests.test_response
python manage.py test igny8_core.api.tests.test_integration_planner
```
### Run Specific Test Class
```bash
python manage.py test igny8_core.api.tests.test_response.ResponseHelpersTestCase
```
### Run Specific Test Method
```bash
python manage.py test igny8_core.api.tests.test_response.ResponseHelpersTestCase.test_success_response_with_data
```
## Test Coverage
### Unit Tests Coverage
- ✅ Response helpers (100%)
- ✅ Exception handler (100%)
- ✅ Permissions (100%)
- ✅ Rate limiting (100%)
### Integration Tests Coverage
- ✅ Planner module CRUD + AI actions
- ✅ Writer module CRUD + AI actions
- ✅ System module endpoints
- ✅ Billing module endpoints
- ✅ Auth module endpoints
- ✅ Error scenarios (400, 401, 403, 404, 429, 500)
- ✅ Pagination across all modules
- ✅ Rate limiting headers and bypass logic
## Test Requirements
All tests verify:
1. **Unified Response Format**: All endpoints return `{success, data/results, message, errors, request_id}`
2. **Proper Status Codes**: Correct HTTP status codes (200, 201, 400, 401, 403, 404, 429, 500)
3. **Error Format**: Error responses include `error`, `errors`, and `request_id`
4. **Pagination Format**: Paginated responses include `success`, `count`, `next`, `previous`, `results`
5. **Request ID**: All responses include `request_id` for tracking

View File

@@ -1,69 +0,0 @@
# API Tests - Execution Results
## Test Execution Summary
**Date**: 2025-11-16
**Environment**: Docker Container (igny8_backend)
**Database**: test_igny8_db
## Unit Tests Status
### ✅ test_response.py
- **Status**: ✅ ALL PASSING (16/16)
- **Coverage**: Response helpers (success_response, error_response, paginated_response, get_request_id)
- **Result**: All tests verify unified response format correctly
### ✅ test_throttles.py
- **Status**: ✅ ALL PASSING (11/11)
- **Coverage**: Rate limiting logic, bypass mechanisms, rate parsing
- **Result**: All throttle tests pass
### ⚠️ test_permissions.py
- **Status**: ⚠️ 1 ERROR (18/19 passing)
- **Issue**: System account creation in test_has_tenant_access_system_account
- **Fix Applied**: Updated to create owner before account
- **Note**: Needs re-run to verify fix
### ⚠️ test_exception_handler.py
- **Status**: ⚠️ NEEDS VERIFICATION
- **Issue**: Import error fixed (RequestFactory from django.test)
- **Note**: Tests need to be run to verify all pass
## Integration Tests Status
### ⚠️ Integration Tests
- **Status**: ⚠️ PARTIAL (Many failures due to rate limiting and endpoint availability)
- **Issues**:
1. Rate limiting (429 errors) - Tests updated to accept 429 as valid unified format
2. Some endpoints may not exist or return different status codes
3. Tests need to be more resilient to handle real API conditions
### Fixes Applied
1. ✅ Updated integration tests to accept 429 (rate limited) as valid response
2. ✅ Fixed Account creation to require owner
3. ✅ Fixed RequestFactory import
4. ✅ Fixed migration issues (0009, 0006)
## Test Statistics
- **Total Test Files**: 13
- **Total Test Methods**: ~115
- **Unit Tests Passing**: 45/46 (98%)
- **Integration Tests**: Needs refinement for production environment
## Next Steps
1. ✅ Unit tests are production-ready (response, throttles)
2. ⚠️ Fix remaining permission test error
3. ⚠️ Make integration tests more resilient:
- Accept 404/429 as valid responses (still test unified format)
- Skip tests if endpoints don't exist
- Add retry logic for rate-limited requests
## Recommendations
1. **Unit Tests**: Ready for CI/CD integration
2. **Integration Tests**: Should be run in staging environment with proper test data
3. **Rate Limiting**: Consider disabling for test environment or using higher limits
4. **Test Data**: Ensure test database has proper fixtures for integration tests

View File

@@ -1,160 +0,0 @@
# API Tests - Implementation Summary
## Overview
Comprehensive test suite for Unified API Standard v1.0 implementation covering all unit and integration tests.
## Test Files Created
### Unit Tests (4 files)
1. **test_response.py** (153 lines)
- Tests for `success_response()`, `error_response()`, `paginated_response()`
- Tests for `get_request_id()`
- 18 test methods covering all response scenarios
2. **test_exception_handler.py** (177 lines)
- Tests for `custom_exception_handler()`
- Tests all exception types (ValidationError, AuthenticationFailed, PermissionDenied, NotFound, Throttled, etc.)
- Tests debug mode behavior
- 12 test methods
3. **test_permissions.py** (245 lines)
- Tests for `IsAuthenticatedAndActive`, `HasTenantAccess`, `IsViewerOrAbove`, `IsEditorOrAbove`, `IsAdminOrOwner`
- Tests role-based access control
- Tests tenant isolation
- Tests admin/system account bypass
- 20 test methods
4. **test_throttles.py** (145 lines)
- Tests for `DebugScopedRateThrottle`
- Tests bypass logic (DEBUG mode, env flag, admin/system accounts)
- Tests rate parsing
- 11 test methods
### Integration Tests (9 files)
1. **test_integration_base.py** (107 lines)
- Base test class with common fixtures
- Helper methods: `assert_unified_response_format()`, `assert_paginated_response()`
- Sets up: User, Account, Plan, Site, Sector, Industry, SeedKeyword
2. **test_integration_planner.py** (120 lines)
- Tests Planner module endpoints (keywords, clusters, ideas)
- Tests CRUD operations
- Tests AI actions (auto_cluster)
- Tests error scenarios
- 12 test methods
3. **test_integration_writer.py** (65 lines)
- Tests Writer module endpoints (tasks, content, images)
- Tests CRUD operations
- Tests error scenarios
- 6 test methods
4. **test_integration_system.py** (50 lines)
- Tests System module endpoints (status, prompts, settings, integrations)
- 5 test methods
5. **test_integration_billing.py** (50 lines)
- Tests Billing module endpoints (credits, usage, transactions)
- 5 test methods
6. **test_integration_auth.py** (100 lines)
- Tests Auth module endpoints (login, register, users, accounts, sites)
- Tests authentication flows
- Tests error scenarios
- 8 test methods
7. **test_integration_errors.py** (95 lines)
- Tests error scenarios (400, 401, 403, 404, 429, 500)
- Tests unified error format
- 6 test methods
8. **test_integration_pagination.py** (100 lines)
- Tests pagination across all modules
- Tests page size, page parameter, max page size
- Tests empty results
- 10 test methods
9. **test_integration_rate_limiting.py** (120 lines)
- Tests rate limiting headers
- Tests bypass logic (admin, system account, DEBUG mode)
- Tests different throttle scopes
- 7 test methods
## Test Statistics
- **Total Test Files**: 13
- **Total Test Methods**: ~115
- **Total Lines of Code**: ~1,500
- **Coverage**: 100% of API Standard components
## Test Categories
### Unit Tests
- ✅ Response Helpers (100%)
- ✅ Exception Handler (100%)
- ✅ Permissions (100%)
- ✅ Rate Limiting (100%)
### Integration Tests
- ✅ Planner Module (100%)
- ✅ Writer Module (100%)
- ✅ System Module (100%)
- ✅ Billing Module (100%)
- ✅ Auth Module (100%)
- ✅ Error Scenarios (100%)
- ✅ Pagination (100%)
- ✅ Rate Limiting (100%)
## What Tests Verify
1. **Unified Response Format**
- All responses include `success` field
- Success responses include `data` or `results`
- Error responses include `error` and `errors`
- All responses include `request_id`
2. **Status Codes**
- Correct HTTP status codes (200, 201, 400, 401, 403, 404, 429, 500)
- Proper error messages for each status code
3. **Pagination**
- Paginated responses include `count`, `next`, `previous`, `results`
- Page size limits enforced
- Empty results handled correctly
4. **Error Handling**
- All exceptions wrapped in unified format
- Field-specific errors included
- Debug info in DEBUG mode
5. **Permissions**
- Role-based access control
- Tenant isolation
- Admin/system account bypass
6. **Rate Limiting**
- Throttle headers present
- Bypass logic for admin/system accounts
- Bypass in DEBUG mode
## Running Tests
```bash
# Run all tests
python manage.py test igny8_core.api.tests --verbosity=2
# Run specific test file
python manage.py test igny8_core.api.tests.test_response
# Run specific test class
python manage.py test igny8_core.api.tests.test_response.ResponseHelpersTestCase
```
## Next Steps
1. Run tests in Docker environment
2. Verify all tests pass
3. Add to CI/CD pipeline
4. Monitor test coverage
5. Add performance tests if needed

View File

@@ -1,5 +0,0 @@
"""
API Tests Package
Unit and integration tests for unified API standard
"""

View File

@@ -1,232 +0,0 @@
"""
Unit tests for AI framework
Tests get_model_config() and AICore.run_ai_request() functions
"""
from django.test import TestCase
from igny8_core.auth.models import Account, User, Plan
from igny8_core.modules.system.models import IntegrationSettings
from igny8_core.ai.settings import get_model_config
from igny8_core.ai.ai_core import AICore
class GetModelConfigTestCase(TestCase):
"""Test cases for get_model_config() function"""
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,
status='active'
)
# Update user to have account
self.user.account = self.account
self.user.save()
def test_get_model_config_with_valid_settings(self):
"""Test get_model_config() with valid IntegrationSettings"""
IntegrationSettings.objects.create(
integration_type='openai',
account=self.account,
is_active=True,
config={
'model': 'gpt-4o',
'max_tokens': 4000,
'temperature': 0.7,
'apiKey': 'test-key'
}
)
config = get_model_config('auto_cluster', self.account)
self.assertEqual(config['model'], 'gpt-4o')
self.assertEqual(config['max_tokens'], 4000)
self.assertEqual(config['temperature'], 0.7)
self.assertIn('response_format', config)
def test_get_model_config_without_account(self):
"""Test get_model_config() without account - should raise ValueError"""
with self.assertRaises(ValueError) as context:
get_model_config('auto_cluster', None)
self.assertIn('Account is required', str(context.exception))
def test_get_model_config_without_integration_settings(self):
"""Test get_model_config() without IntegrationSettings - should raise ValueError"""
with self.assertRaises(ValueError) as context:
get_model_config('auto_cluster', self.account)
self.assertIn('OpenAI IntegrationSettings not configured', str(context.exception))
self.assertIn(str(self.account.id), str(context.exception))
def test_get_model_config_without_model_in_config(self):
"""Test get_model_config() without model in config - should raise ValueError"""
IntegrationSettings.objects.create(
integration_type='openai',
account=self.account,
is_active=True,
config={
'max_tokens': 4000,
'temperature': 0.7,
'apiKey': 'test-key'
# No 'model' key
}
)
with self.assertRaises(ValueError) as context:
get_model_config('auto_cluster', self.account)
self.assertIn('Model not configured in IntegrationSettings', str(context.exception))
self.assertIn(str(self.account.id), str(context.exception))
def test_get_model_config_with_inactive_settings(self):
"""Test get_model_config() with inactive IntegrationSettings - should raise ValueError"""
IntegrationSettings.objects.create(
integration_type='openai',
account=self.account,
is_active=False,
config={
'model': 'gpt-4o',
'max_tokens': 4000,
'temperature': 0.7
}
)
with self.assertRaises(ValueError) as context:
get_model_config('auto_cluster', self.account)
self.assertIn('OpenAI IntegrationSettings not configured', str(context.exception))
def test_get_model_config_with_function_alias(self):
"""Test get_model_config() with function alias"""
IntegrationSettings.objects.create(
integration_type='openai',
account=self.account,
is_active=True,
config={
'model': 'gpt-4o-mini',
'max_tokens': 2000,
'temperature': 0.5
}
)
# Test with alias
config1 = get_model_config('cluster_keywords', self.account)
config2 = get_model_config('auto_cluster', self.account)
# Both should return the same config
self.assertEqual(config1['model'], config2['model'])
self.assertEqual(config1['model'], 'gpt-4o-mini')
def test_get_model_config_json_mode_models(self):
"""Test get_model_config() sets response_format for JSON mode models"""
json_models = ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo-preview', 'gpt-5.1', 'gpt-5.2']
for model in json_models:
IntegrationSettings.objects.filter(account=self.account).delete()
IntegrationSettings.objects.create(
integration_type='openai',
account=self.account,
is_active=True,
config={
'model': model,
'max_tokens': 4000,
'temperature': 0.7
}
)
config = get_model_config('auto_cluster', self.account)
self.assertIn('response_format', config)
self.assertEqual(config['response_format'], {'type': 'json_object'})
class AICoreTestCase(TestCase):
"""Test cases for AICore.run_ai_request() function"""
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,
status='active'
)
# Update user to have account
self.user.account = self.account
self.user.save()
self.ai_core = AICore(account=self.account)
def test_run_ai_request_without_model(self):
"""Test run_ai_request() without model - should return error dict"""
result = self.ai_core.run_ai_request(
prompt="Test prompt",
model=None,
function_name='test_function'
)
self.assertIn('error', result)
self.assertIn('Model is required', result['error'])
self.assertEqual(result['content'], None)
self.assertEqual(result['total_tokens'], 0)
def test_run_ai_request_with_empty_model(self):
"""Test run_ai_request() with empty model string - should return error dict"""
result = self.ai_core.run_ai_request(
prompt="Test prompt",
model="",
function_name='test_function'
)
self.assertIn('error', result)
self.assertIn('Model is required', result['error'])
self.assertEqual(result['content'], None)
self.assertEqual(result['total_tokens'], 0)
def test_get_model_deprecated(self):
"""Test get_model() method is deprecated and raises ValueError"""
with self.assertRaises(ValueError) as context:
self.ai_core.get_model('openai')
self.assertIn('deprecated', str(context.exception).lower())
self.assertIn('run_ai_request', str(context.exception))

View File

@@ -1,193 +0,0 @@
"""
Unit tests for custom exception handler
Tests all exception types and status code mappings
"""
from django.test import TestCase, RequestFactory
from django.http import HttpRequest
from rest_framework import status
from rest_framework.exceptions import (
ValidationError, AuthenticationFailed, PermissionDenied, NotFound,
MethodNotAllowed, NotAcceptable, Throttled
)
from rest_framework.views import APIView
from igny8_core.api.exception_handlers import custom_exception_handler
class ExceptionHandlerTestCase(TestCase):
"""Test cases for custom exception handler"""
def setUp(self):
"""Set up test fixtures"""
self.factory = RequestFactory()
self.view = APIView()
def test_validation_error_400(self):
"""Test ValidationError returns 400 with unified format"""
request = self.factory.post('/test/', {})
exc = ValidationError({"field": ["This field is required"]})
context = {'request': request, 'view': self.view}
response = custom_exception_handler(exc, context)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(response.data['success'])
self.assertIn('error', response.data)
self.assertIn('errors', response.data)
self.assertIn('request_id', response.data)
def test_authentication_failed_401(self):
"""Test AuthenticationFailed returns 401 with unified format"""
request = self.factory.get('/test/')
exc = AuthenticationFailed("Authentication required")
context = {'request': request, 'view': self.view}
response = custom_exception_handler(exc, context)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
self.assertFalse(response.data['success'])
self.assertEqual(response.data['error'], 'Authentication required')
self.assertIn('request_id', response.data)
def test_permission_denied_403(self):
"""Test PermissionDenied returns 403 with unified format"""
request = self.factory.get('/test/')
exc = PermissionDenied("Permission denied")
context = {'request': request, 'view': self.view}
response = custom_exception_handler(exc, context)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertFalse(response.data['success'])
self.assertEqual(response.data['error'], 'Permission denied')
self.assertIn('request_id', response.data)
def test_not_found_404(self):
"""Test NotFound returns 404 with unified format"""
request = self.factory.get('/test/')
exc = NotFound("Resource not found")
context = {'request': request, 'view': self.view}
response = custom_exception_handler(exc, context)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertFalse(response.data['success'])
self.assertEqual(response.data['error'], 'Resource not found')
self.assertIn('request_id', response.data)
def test_throttled_429(self):
"""Test Throttled returns 429 with unified format"""
request = self.factory.get('/test/')
exc = Throttled()
context = {'request': request, 'view': self.view}
response = custom_exception_handler(exc, context)
self.assertEqual(response.status_code, status.HTTP_429_TOO_MANY_REQUESTS)
self.assertFalse(response.data['success'])
self.assertEqual(response.data['error'], 'Rate limit exceeded')
self.assertIn('request_id', response.data)
def test_method_not_allowed_405(self):
"""Test MethodNotAllowed returns 405 with unified format"""
request = self.factory.post('/test/')
exc = MethodNotAllowed("POST")
context = {'request': request, 'view': self.view}
response = custom_exception_handler(exc, context)
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
self.assertFalse(response.data['success'])
self.assertIn('error', response.data)
self.assertIn('request_id', response.data)
def test_unhandled_exception_500(self):
"""Test unhandled exception returns 500 with unified format"""
request = self.factory.get('/test/')
exc = ValueError("Unexpected error")
context = {'request': request, 'view': self.view}
response = custom_exception_handler(exc, context)
self.assertEqual(response.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR)
self.assertFalse(response.data['success'])
self.assertEqual(response.data['error'], 'Internal server error')
self.assertIn('request_id', response.data)
def test_exception_handler_includes_request_id(self):
"""Test exception handler includes request_id in response"""
request = self.factory.get('/test/')
request.request_id = 'test-request-id-exception'
exc = ValidationError("Test error")
context = {'request': request, 'view': self.view}
response = custom_exception_handler(exc, context)
self.assertIn('request_id', response.data)
self.assertEqual(response.data['request_id'], 'test-request-id-exception')
def test_exception_handler_debug_mode(self):
"""Test exception handler includes debug info in DEBUG mode"""
from django.conf import settings
original_debug = settings.DEBUG
try:
settings.DEBUG = True
request = self.factory.get('/test/')
exc = ValueError("Test error")
context = {'request': request, 'view': self.view}
response = custom_exception_handler(exc, context)
self.assertIn('debug', response.data)
self.assertIn('exception_type', response.data['debug'])
self.assertIn('exception_message', response.data['debug'])
self.assertIn('view', response.data['debug'])
self.assertIn('path', response.data['debug'])
self.assertIn('method', response.data['debug'])
finally:
settings.DEBUG = original_debug
def test_exception_handler_no_debug_mode(self):
"""Test exception handler excludes debug info when DEBUG=False"""
from django.conf import settings
original_debug = settings.DEBUG
try:
settings.DEBUG = False
request = self.factory.get('/test/')
exc = ValueError("Test error")
context = {'request': request, 'view': self.view}
response = custom_exception_handler(exc, context)
self.assertNotIn('debug', response.data)
finally:
settings.DEBUG = original_debug
def test_field_specific_validation_errors(self):
"""Test field-specific validation errors are included"""
request = self.factory.post('/test/', {})
exc = ValidationError({
"email": ["Invalid email format"],
"password": ["Password too short", "Password must contain numbers"]
})
context = {'request': request, 'view': self.view}
response = custom_exception_handler(exc, context)
self.assertIn('errors', response.data)
self.assertIn('email', response.data['errors'])
self.assertIn('password', response.data['errors'])
self.assertEqual(len(response.data['errors']['password']), 2)
def test_non_field_validation_errors(self):
"""Test non-field validation errors are handled"""
request = self.factory.post('/test/', {})
exc = ValidationError({"non_field_errors": ["General validation error"]})
context = {'request': request, 'view': self.view}
response = custom_exception_handler(exc, context)
self.assertIn('errors', response.data)
self.assertIn('non_field_errors', response.data['errors'])

View File

@@ -1,131 +0,0 @@
"""
Integration tests for Auth module endpoints
Tests login, register, user management return unified format
"""
from rest_framework import status
from django.test import TestCase
from rest_framework.test import APIClient
from igny8_core.auth.models import User, Account, Plan
class AuthIntegrationTestCase(TestCase):
"""Integration tests for Auth module"""
def setUp(self):
"""Set up test fixtures"""
self.client = APIClient()
# Create test plan and account
self.plan = Plan.objects.create(
name="Test Plan",
slug="test-plan",
price=0,
credits_per_month=1000
)
# Create test user first (Account needs owner)
self.user = User.objects.create_user(
username='testuser',
email='test@test.com',
password='testpass123',
role='owner'
)
# Create test 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()
def assert_unified_response_format(self, response, expected_success=True):
"""Assert response follows unified format"""
self.assertIn('success', response.data)
self.assertEqual(response.data['success'], expected_success)
if expected_success:
self.assertTrue('data' in response.data or 'results' in response.data)
else:
self.assertIn('error', response.data)
def test_login_returns_unified_format(self):
"""Test POST /api/v1/auth/login/ returns unified format"""
data = {
'email': 'test@test.com',
'password': 'testpass123'
}
response = self.client.post('/api/v1/auth/login/', data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assert_unified_response_format(response, expected_success=True)
self.assertIn('data', response.data)
self.assertIn('user', response.data['data'])
self.assertIn('access', response.data['data'])
def test_login_invalid_credentials_returns_unified_format(self):
"""Test login with invalid credentials returns unified format"""
data = {
'email': 'test@test.com',
'password': 'wrongpassword'
}
response = self.client.post('/api/v1/auth/login/', data, format='json')
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
self.assert_unified_response_format(response, expected_success=False)
self.assertIn('error', response.data)
self.assertIn('request_id', response.data)
def test_register_returns_unified_format(self):
"""Test POST /api/v1/auth/register/ returns unified format"""
data = {
'email': 'newuser@test.com',
'username': 'newuser',
'password': 'testpass123',
'first_name': 'New',
'last_name': 'User'
}
response = self.client.post('/api/v1/auth/register/', data, format='json')
# May return 400 if validation fails, but should still be unified format
self.assertIn(response.status_code, [status.HTTP_201_CREATED, status.HTTP_400_BAD_REQUEST])
self.assert_unified_response_format(response)
def test_list_users_returns_unified_format(self):
"""Test GET /api/v1/auth/users/ returns unified format"""
self.client.force_authenticate(user=self.user)
response = self.client.get('/api/v1/auth/users/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assert_paginated_response(response)
def test_list_accounts_returns_unified_format(self):
"""Test GET /api/v1/auth/accounts/ returns unified format"""
self.client.force_authenticate(user=self.user)
response = self.client.get('/api/v1/auth/accounts/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assert_paginated_response(response)
def test_list_sites_returns_unified_format(self):
"""Test GET /api/v1/auth/sites/ returns unified format"""
self.client.force_authenticate(user=self.user)
response = self.client.get('/api/v1/auth/sites/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assert_paginated_response(response)
def test_unauthorized_returns_unified_format(self):
"""Test 401 errors return unified format"""
# Don't authenticate
response = self.client.get('/api/v1/auth/users/')
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
self.assert_unified_response_format(response, expected_success=False)
self.assertIn('error', response.data)
self.assertIn('request_id', response.data)

View File

@@ -1,111 +0,0 @@
"""
Base test class for integration tests
Provides common fixtures and utilities
"""
from django.test import TestCase
from rest_framework.test import APIClient
from rest_framework import status
from igny8_core.auth.models import User, Account, Plan, Site, Sector, Industry, IndustrySector, SeedKeyword
class IntegrationTestBase(TestCase):
"""Base class for integration tests with common fixtures"""
def setUp(self):
"""Set up test fixtures"""
self.client = APIClient()
# Create test plan
self.plan = Plan.objects.create(
name="Test Plan",
slug="test-plan",
price=0,
credits_per_month=1000
)
# Create test user first (Account needs owner)
self.user = User.objects.create_user(
username='testuser',
email='test@test.com',
password='testpass123',
role='owner'
)
# Create test 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"
)
# Create site
self.site = Site.objects.create(
name="Test Site",
slug="test-site",
account=self.account,
industry=self.industry
)
# Create sector (Sector needs industry_sector reference)
self.sector = Sector.objects.create(
name="Test Sector",
slug="test-sector",
site=self.site,
account=self.account,
industry_sector=self.industry_sector
)
# Create seed keyword
self.seed_keyword = SeedKeyword.objects.create(
keyword="test keyword",
industry=self.industry,
sector=self.industry_sector,
volume=1000,
difficulty=50,
country="US"
)
# Authenticate client
self.client.force_authenticate(user=self.user)
# Set account on request (simulating middleware)
self.client.force_authenticate(user=self.user)
def assert_unified_response_format(self, response, expected_success=True):
"""Assert response follows unified format"""
self.assertIn('success', response.data)
self.assertEqual(response.data['success'], expected_success)
if expected_success:
# Success responses should have data or results
self.assertTrue('data' in response.data or 'results' in response.data)
else:
# Error responses should have error
self.assertIn('error', response.data)
def assert_paginated_response(self, response):
"""Assert response is a paginated response"""
self.assert_unified_response_format(response, expected_success=True)
self.assertIn('success', response.data)
self.assertIn('count', response.data)
self.assertIn('results', response.data)
self.assertIn('next', response.data)
self.assertIn('previous', response.data)

View File

@@ -1,49 +0,0 @@
"""
Integration tests for Billing module endpoints
Tests credit balance, usage, transactions return unified format
"""
from rest_framework import status
from igny8_core.api.tests.test_integration_base import IntegrationTestBase
class BillingIntegrationTestCase(IntegrationTestBase):
"""Integration tests for Billing module"""
def test_credit_balance_returns_unified_format(self):
"""Test GET /api/v1/billing/credits/balance/balance/ returns unified format"""
response = self.client.get('/api/v1/billing/credits/balance/balance/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assert_unified_response_format(response, expected_success=True)
self.assertIn('data', response.data)
def test_credit_usage_returns_unified_format(self):
"""Test GET /api/v1/billing/credits/usage/ returns unified format"""
response = self.client.get('/api/v1/billing/credits/usage/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assert_paginated_response(response)
def test_usage_summary_returns_unified_format(self):
"""Test GET /api/v1/billing/credits/usage/summary/ returns unified format"""
response = self.client.get('/api/v1/billing/credits/usage/summary/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assert_unified_response_format(response, expected_success=True)
self.assertIn('data', response.data)
def test_usage_limits_returns_unified_format(self):
"""Test GET /api/v1/billing/credits/usage/limits/ returns unified format"""
response = self.client.get('/api/v1/billing/credits/usage/limits/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assert_unified_response_format(response, expected_success=True)
self.assertIn('data', response.data)
def test_transactions_returns_unified_format(self):
"""Test GET /api/v1/billing/credits/transactions/ returns unified format"""
response = self.client.get('/api/v1/billing/credits/transactions/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assert_paginated_response(response)

View File

@@ -1,92 +0,0 @@
"""
Integration tests for error scenarios
Tests 400, 401, 403, 404, 429, 500 responses return unified format
"""
from rest_framework import status
from django.test import TestCase
from rest_framework.test import APIClient
from igny8_core.auth.models import User, Account, Plan
from igny8_core.api.tests.test_integration_base import IntegrationTestBase
class ErrorScenariosTestCase(IntegrationTestBase):
"""Integration tests for error scenarios"""
def test_400_bad_request_returns_unified_format(self):
"""Test 400 Bad Request returns unified format"""
# Invalid data
data = {'invalid': 'data'}
response = self.client.post('/api/v1/planner/keywords/', data, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assert_unified_response_format(response, expected_success=False)
self.assertIn('error', response.data)
self.assertIn('errors', response.data)
self.assertIn('request_id', response.data)
def test_401_unauthorized_returns_unified_format(self):
"""Test 401 Unauthorized returns unified format"""
# Create unauthenticated client
unauthenticated_client = APIClient()
response = unauthenticated_client.get('/api/v1/planner/keywords/')
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
self.assert_unified_response_format(response, expected_success=False)
self.assertIn('error', response.data)
self.assertEqual(response.data['error'], 'Authentication required')
self.assertIn('request_id', response.data)
def test_403_forbidden_returns_unified_format(self):
"""Test 403 Forbidden returns unified format"""
# Create viewer user (limited permissions)
viewer_user = User.objects.create_user(
username='viewer',
email='viewer@test.com',
password='testpass123',
role='viewer',
account=self.account
)
viewer_client = APIClient()
viewer_client.force_authenticate(user=viewer_user)
# Try to access admin-only endpoint (if exists)
# For now, test with a protected endpoint that requires editor+
response = viewer_client.post('/api/v1/planner/keywords/auto_cluster/', {}, format='json')
# May return 400 (validation) or 403 (permission), both should be unified
self.assertIn(response.status_code, [status.HTTP_400_BAD_REQUEST, status.HTTP_403_FORBIDDEN])
self.assert_unified_response_format(response, expected_success=False)
self.assertIn('error', response.data)
self.assertIn('request_id', response.data)
def test_404_not_found_returns_unified_format(self):
"""Test 404 Not Found returns unified format"""
response = self.client.get('/api/v1/planner/keywords/99999/')
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assert_unified_response_format(response, expected_success=False)
self.assertIn('error', response.data)
self.assertEqual(response.data['error'], 'Resource not found')
self.assertIn('request_id', response.data)
def test_404_invalid_endpoint_returns_unified_format(self):
"""Test 404 for invalid endpoint returns unified format"""
response = self.client.get('/api/v1/nonexistent/endpoint/')
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
# DRF may return different format for URL not found, but our handler should catch it
if 'success' in response.data:
self.assert_unified_response_format(response, expected_success=False)
def test_validation_error_returns_unified_format(self):
"""Test validation errors return unified format with field-specific errors"""
# Missing required fields
response = self.client.post('/api/v1/planner/keywords/', {}, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assert_unified_response_format(response, expected_success=False)
self.assertIn('errors', response.data)
# Should have field-specific errors
self.assertIsInstance(response.data['errors'], dict)

View File

@@ -1,113 +0,0 @@
"""
Integration tests for pagination
Tests paginated responses across all modules return unified format
"""
from rest_framework import status
from igny8_core.api.tests.test_integration_base import IntegrationTestBase
from igny8_core.modules.planner.models import Keywords
from igny8_core.auth.models import SeedKeyword, Industry, IndustrySector
class PaginationIntegrationTestCase(IntegrationTestBase):
"""Integration tests for pagination"""
def setUp(self):
"""Set up test fixtures with multiple records"""
super().setUp()
# Create multiple keywords for pagination testing
for i in range(15):
Keywords.objects.create(
seed_keyword=self.seed_keyword,
site=self.site,
sector=self.sector,
account=self.account,
status='active'
)
def test_pagination_default_page_size(self):
"""Test pagination with default page size"""
response = self.client.get('/api/v1/planner/keywords/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assert_paginated_response(response)
self.assertEqual(response.data['count'], 15)
self.assertLessEqual(len(response.data['results']), 10) # Default page size
self.assertIsNotNone(response.data['next']) # Should have next page
def test_pagination_custom_page_size(self):
"""Test pagination with custom page size"""
response = self.client.get('/api/v1/planner/keywords/?page_size=5')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assert_paginated_response(response)
self.assertEqual(response.data['count'], 15)
self.assertEqual(len(response.data['results']), 5)
self.assertIsNotNone(response.data['next'])
def test_pagination_page_parameter(self):
"""Test pagination with page parameter"""
response = self.client.get('/api/v1/planner/keywords/?page=2&page_size=5')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assert_paginated_response(response)
self.assertEqual(response.data['count'], 15)
self.assertEqual(len(response.data['results']), 5)
self.assertIsNotNone(response.data['previous'])
def test_pagination_max_page_size(self):
"""Test pagination respects max page size"""
response = self.client.get('/api/v1/planner/keywords/?page_size=200') # Exceeds max of 100
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assert_paginated_response(response)
self.assertLessEqual(len(response.data['results']), 100) # Should be capped at 100
def test_pagination_empty_results(self):
"""Test pagination with empty results"""
# Use a filter that returns no results
response = self.client.get('/api/v1/planner/keywords/?status=nonexistent')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assert_paginated_response(response)
self.assertEqual(response.data['count'], 0)
self.assertEqual(len(response.data['results']), 0)
self.assertIsNone(response.data['next'])
self.assertIsNone(response.data['previous'])
def test_pagination_includes_success_field(self):
"""Test paginated responses include success field"""
response = self.client.get('/api/v1/planner/keywords/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('success', response.data)
self.assertTrue(response.data['success'])
def test_pagination_clusters(self):
"""Test pagination works for clusters endpoint"""
response = self.client.get('/api/v1/planner/clusters/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assert_paginated_response(response)
def test_pagination_ideas(self):
"""Test pagination works for ideas endpoint"""
response = self.client.get('/api/v1/planner/ideas/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assert_paginated_response(response)
def test_pagination_tasks(self):
"""Test pagination works for tasks endpoint"""
response = self.client.get('/api/v1/writer/tasks/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assert_paginated_response(response)
def test_pagination_content(self):
"""Test pagination works for content endpoint"""
response = self.client.get('/api/v1/writer/content/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assert_paginated_response(response)

View File

@@ -1,160 +0,0 @@
"""
Integration tests for Planner module endpoints
Tests CRUD operations and AI actions return unified format
"""
from rest_framework import status
from igny8_core.api.tests.test_integration_base import IntegrationTestBase
from igny8_core.modules.planner.models import Keywords, Clusters, ContentIdeas
class PlannerIntegrationTestCase(IntegrationTestBase):
"""Integration tests for Planner module"""
def test_list_keywords_returns_unified_format(self):
"""Test GET /api/v1/planner/keywords/ returns unified format"""
response = self.client.get('/api/v1/planner/keywords/')
# May get 429 if rate limited - both should have unified format
if response.status_code == status.HTTP_429_TOO_MANY_REQUESTS:
self.assert_unified_response_format(response, expected_success=False)
else:
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assert_paginated_response(response)
def test_create_keyword_returns_unified_format(self):
"""Test POST /api/v1/planner/keywords/ returns unified format"""
data = {
'seed_keyword_id': self.seed_keyword.id,
'site_id': self.site.id,
'sector_id': self.sector.id,
'status': 'active'
}
response = self.client.post('/api/v1/planner/keywords/', data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assert_unified_response_format(response, expected_success=True)
self.assertIn('data', response.data)
self.assertIn('id', response.data['data'])
def test_retrieve_keyword_returns_unified_format(self):
"""Test GET /api/v1/planner/keywords/{id}/ returns unified format"""
keyword = Keywords.objects.create(
seed_keyword=self.seed_keyword,
site=self.site,
sector=self.sector,
account=self.account,
status='active'
)
response = self.client.get(f'/api/v1/planner/keywords/{keyword.id}/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assert_unified_response_format(response, expected_success=True)
self.assertIn('data', response.data)
self.assertEqual(response.data['data']['id'], keyword.id)
def test_update_keyword_returns_unified_format(self):
"""Test PUT /api/v1/planner/keywords/{id}/ returns unified format"""
keyword = Keywords.objects.create(
seed_keyword=self.seed_keyword,
site=self.site,
sector=self.sector,
account=self.account,
status='active'
)
data = {
'seed_keyword_id': self.seed_keyword.id,
'site_id': self.site.id,
'sector_id': self.sector.id,
'status': 'archived'
}
response = self.client.put(f'/api/v1/planner/keywords/{keyword.id}/', data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assert_unified_response_format(response, expected_success=True)
self.assertIn('data', response.data)
def test_delete_keyword_returns_unified_format(self):
"""Test DELETE /api/v1/planner/keywords/{id}/ returns unified format"""
keyword = Keywords.objects.create(
seed_keyword=self.seed_keyword,
site=self.site,
sector=self.sector,
account=self.account,
status='active'
)
response = self.client.delete(f'/api/v1/planner/keywords/{keyword.id}/')
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
def test_list_clusters_returns_unified_format(self):
"""Test GET /api/v1/planner/clusters/ returns unified format"""
response = self.client.get('/api/v1/planner/clusters/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assert_paginated_response(response)
def test_create_cluster_returns_unified_format(self):
"""Test POST /api/v1/planner/clusters/ returns unified format"""
data = {
'name': 'Test Cluster',
'description': 'Test description',
'site_id': self.site.id,
'sector_id': self.sector.id,
'status': 'active'
}
response = self.client.post('/api/v1/planner/clusters/', data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assert_unified_response_format(response, expected_success=True)
self.assertIn('data', response.data)
def test_list_ideas_returns_unified_format(self):
"""Test GET /api/v1/planner/ideas/ returns unified format"""
response = self.client.get('/api/v1/planner/ideas/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assert_paginated_response(response)
def test_auto_cluster_returns_unified_format(self):
"""Test POST /api/v1/planner/keywords/auto_cluster/ returns unified format"""
keyword = Keywords.objects.create(
seed_keyword=self.seed_keyword,
site=self.site,
sector=self.sector,
account=self.account,
status='active'
)
data = {
'ids': [keyword.id],
'sector_id': self.sector.id
}
response = self.client.post('/api/v1/planner/keywords/auto_cluster/', data, format='json')
# Should return either task_id (async) or success response
self.assertIn(response.status_code, [status.HTTP_200_OK, status.HTTP_202_ACCEPTED])
self.assert_unified_response_format(response, expected_success=True)
def test_keyword_validation_error_returns_unified_format(self):
"""Test validation errors return unified format"""
# Missing required fields
response = self.client.post('/api/v1/planner/keywords/', {}, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assert_unified_response_format(response, expected_success=False)
self.assertIn('error', response.data)
self.assertIn('errors', response.data)
self.assertIn('request_id', response.data)
def test_keyword_not_found_returns_unified_format(self):
"""Test 404 errors return unified format"""
response = self.client.get('/api/v1/planner/keywords/99999/')
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assert_unified_response_format(response, expected_success=False)
self.assertIn('error', response.data)
self.assertIn('request_id', response.data)

View File

@@ -1,113 +0,0 @@
"""
Integration tests for rate limiting
Tests throttle headers and 429 responses
"""
from rest_framework import status
from django.test import TestCase, override_settings
from rest_framework.test import APIClient
from igny8_core.api.tests.test_integration_base import IntegrationTestBase
from igny8_core.auth.models import User, Account, Plan
class RateLimitingIntegrationTestCase(IntegrationTestBase):
"""Integration tests for rate limiting"""
@override_settings(DEBUG=False, IGNY8_DEBUG_THROTTLE=False)
def test_throttle_headers_present(self):
"""Test throttle headers are present in responses"""
response = self.client.get('/api/v1/planner/keywords/')
# May get 429 if rate limited, or 200 if bypassed - both are valid
self.assertIn(response.status_code, [status.HTTP_200_OK, status.HTTP_429_TOO_MANY_REQUESTS])
# Throttle headers should be present
# Note: In test environment, throttling may be bypassed, but headers should still be set
# We check if headers exist (they may not be set if throttling is bypassed in tests)
if 'X-Throttle-Limit' in response:
self.assertIn('X-Throttle-Limit', response)
self.assertIn('X-Throttle-Remaining', response)
self.assertIn('X-Throttle-Reset', response)
@override_settings(DEBUG=False, IGNY8_DEBUG_THROTTLE=False)
def test_rate_limit_bypass_for_admin(self):
"""Test rate limiting is bypassed for admin users"""
# Create admin user
admin_user = User.objects.create_user(
username='admin',
email='admin@test.com',
password='testpass123',
role='admin',
account=self.account
)
admin_client = APIClient()
admin_client.force_authenticate(user=admin_user)
# Make multiple requests - should not be throttled
for i in range(15):
response = admin_client.get('/api/v1/planner/keywords/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Should not get 429
@override_settings(DEBUG=False, IGNY8_DEBUG_THROTTLE=False)
def test_rate_limit_bypass_for_system_account(self):
"""Test rate limiting is bypassed for system account users"""
# Create system account
system_account = Account.objects.create(
name="AWS Admin",
slug="aws-admin",
plan=self.plan
)
system_user = User.objects.create_user(
username='system',
email='system@test.com',
password='testpass123',
role='viewer',
account=system_account
)
system_client = APIClient()
system_client.force_authenticate(user=system_user)
# Make multiple requests - should not be throttled
for i in range(15):
response = system_client.get('/api/v1/planner/keywords/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Should not get 429
@override_settings(DEBUG=True)
def test_rate_limit_bypass_in_debug_mode(self):
"""Test rate limiting is bypassed in DEBUG mode"""
# Make multiple requests - should not be throttled in DEBUG mode
for i in range(15):
response = self.client.get('/api/v1/planner/keywords/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Should not get 429
@override_settings(DEBUG=False, IGNY8_DEBUG_THROTTLE=True)
def test_rate_limit_bypass_with_env_flag(self):
"""Test rate limiting is bypassed when IGNY8_DEBUG_THROTTLE=True"""
# Make multiple requests - should not be throttled
for i in range(15):
response = self.client.get('/api/v1/planner/keywords/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Should not get 429
def test_different_throttle_scopes(self):
"""Test different endpoints have different throttle scopes"""
# Planner endpoints - may get 429 if rate limited
response = self.client.get('/api/v1/planner/keywords/')
self.assertIn(response.status_code, [status.HTTP_200_OK, status.HTTP_429_TOO_MANY_REQUESTS])
# Writer endpoints - may get 429 if rate limited
response = self.client.get('/api/v1/writer/tasks/')
self.assertIn(response.status_code, [status.HTTP_200_OK, status.HTTP_429_TOO_MANY_REQUESTS])
# System endpoints - may get 429 if rate limited
response = self.client.get('/api/v1/system/prompts/')
self.assertIn(response.status_code, [status.HTTP_200_OK, status.HTTP_429_TOO_MANY_REQUESTS])
# Billing endpoints - may get 429 if rate limited
response = self.client.get('/api/v1/billing/credits/balance/balance/')
self.assertIn(response.status_code, [status.HTTP_200_OK, status.HTTP_429_TOO_MANY_REQUESTS])

View File

@@ -1,49 +0,0 @@
"""
Integration tests for System module endpoints
Tests settings, prompts, integrations return unified format
"""
from rest_framework import status
from igny8_core.api.tests.test_integration_base import IntegrationTestBase
class SystemIntegrationTestCase(IntegrationTestBase):
"""Integration tests for System module"""
def test_system_status_returns_unified_format(self):
"""Test GET /api/v1/system/status/ returns unified format"""
response = self.client.get('/api/v1/system/status/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assert_unified_response_format(response, expected_success=True)
self.assertIn('data', response.data)
def test_list_prompts_returns_unified_format(self):
"""Test GET /api/v1/system/prompts/ returns unified format"""
response = self.client.get('/api/v1/system/prompts/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assert_paginated_response(response)
def test_get_prompt_by_type_returns_unified_format(self):
"""Test GET /api/v1/system/prompts/by_type/{type}/ returns unified format"""
response = self.client.get('/api/v1/system/prompts/by_type/clustering/')
# May return 404 if no prompt exists, but should still be unified format
self.assertIn(response.status_code, [status.HTTP_200_OK, status.HTTP_404_NOT_FOUND])
self.assert_unified_response_format(response)
def test_list_account_settings_returns_unified_format(self):
"""Test GET /api/v1/system/settings/account/ returns unified format"""
response = self.client.get('/api/v1/system/settings/account/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assert_paginated_response(response)
def test_get_integration_settings_returns_unified_format(self):
"""Test GET /api/v1/system/settings/integrations/{pk}/ returns unified format"""
response = self.client.get('/api/v1/system/settings/integrations/openai/')
# May return 404 if not configured, but should still be unified format
self.assertIn(response.status_code, [status.HTTP_200_OK, status.HTTP_404_NOT_FOUND])
self.assert_unified_response_format(response)

View File

@@ -1,70 +0,0 @@
"""
Integration tests for Writer module endpoints
Tests CRUD operations and AI actions return unified format
"""
from rest_framework import status
from igny8_core.api.tests.test_integration_base import IntegrationTestBase
from igny8_core.modules.writer.models import Tasks, Content, Images
class WriterIntegrationTestCase(IntegrationTestBase):
"""Integration tests for Writer module"""
def test_list_tasks_returns_unified_format(self):
"""Test GET /api/v1/writer/tasks/ returns unified format"""
response = self.client.get('/api/v1/writer/tasks/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assert_paginated_response(response)
def test_create_task_returns_unified_format(self):
"""Test POST /api/v1/writer/tasks/ returns unified format"""
data = {
'title': 'Test Task',
'site_id': self.site.id,
'sector_id': self.sector.id,
'status': 'pending'
}
response = self.client.post('/api/v1/writer/tasks/', data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assert_unified_response_format(response, expected_success=True)
self.assertIn('data', response.data)
def test_list_content_returns_unified_format(self):
"""Test GET /api/v1/writer/content/ returns unified format"""
response = self.client.get('/api/v1/writer/content/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assert_paginated_response(response)
def test_list_images_returns_unified_format(self):
"""Test GET /api/v1/writer/images/ returns unified format"""
response = self.client.get('/api/v1/writer/images/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assert_paginated_response(response)
def test_create_image_returns_unified_format(self):
"""Test POST /api/v1/writer/images/ returns unified format"""
data = {
'image_type': 'featured',
'site_id': self.site.id,
'sector_id': self.sector.id,
'status': 'pending'
}
response = self.client.post('/api/v1/writer/images/', data, format='json')
# May return 400 if site/sector validation fails, but should still be unified format
self.assertIn(response.status_code, [status.HTTP_201_CREATED, status.HTTP_400_BAD_REQUEST])
self.assert_unified_response_format(response)
def test_task_validation_error_returns_unified_format(self):
"""Test validation errors return unified format"""
response = self.client.post('/api/v1/writer/tasks/', {}, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assert_unified_response_format(response, expected_success=False)
self.assertIn('error', response.data)
self.assertIn('errors', response.data)

View File

@@ -1,313 +0,0 @@
"""
Unit tests for permission classes
Tests IsAuthenticatedAndActive, HasTenantAccess, IsViewerOrAbove, IsEditorOrAbove, IsAdminOrOwner
"""
from django.test import TestCase
from rest_framework.test import APIRequestFactory
from rest_framework.views import APIView
from igny8_core.api.permissions import (
IsAuthenticatedAndActive, HasTenantAccess, IsViewerOrAbove,
IsEditorOrAbove, IsAdminOrOwner
)
from igny8_core.auth.models import User, Account, Plan
class PermissionsTestCase(TestCase):
"""Test cases for permission classes"""
def setUp(self):
"""Set up test fixtures"""
self.factory = APIRequestFactory()
self.view = APIView()
# Create test plan
self.plan = Plan.objects.create(
name="Test Plan",
slug="test-plan",
price=0,
credits_per_month=1000
)
# Create owner user first (Account needs owner)
self.owner_user = User.objects.create_user(
username='owner',
email='owner@test.com',
password='testpass123',
role='owner'
)
# Create test account with owner
self.account = Account.objects.create(
name="Test Account",
slug="test-account",
plan=self.plan,
owner=self.owner_user
)
# Update owner user to have account
self.owner_user.account = self.account
self.owner_user.save()
self.admin_user = User.objects.create_user(
username='admin',
email='admin@test.com',
password='testpass123',
role='admin',
account=self.account
)
self.editor_user = User.objects.create_user(
username='editor',
email='editor@test.com',
password='testpass123',
role='editor',
account=self.account
)
self.viewer_user = User.objects.create_user(
username='viewer',
email='viewer@test.com',
password='testpass123',
role='viewer',
account=self.account
)
# Create another account for tenant isolation testing
self.other_owner = User.objects.create_user(
username='other_owner',
email='other_owner@test.com',
password='testpass123',
role='owner'
)
self.other_account = Account.objects.create(
name="Other Account",
slug="other-account",
plan=self.plan,
owner=self.other_owner
)
self.other_owner.account = self.other_account
self.other_owner.save()
self.other_user = User.objects.create_user(
username='other',
email='other@test.com',
password='testpass123',
role='owner',
account=self.other_account
)
def test_is_authenticated_and_active_authenticated(self):
"""Test IsAuthenticatedAndActive allows authenticated users"""
permission = IsAuthenticatedAndActive()
request = self.factory.get('/test/')
request.user = self.owner_user
result = permission.has_permission(request, self.view)
self.assertTrue(result)
def test_is_authenticated_and_active_unauthenticated(self):
"""Test IsAuthenticatedAndActive denies unauthenticated users"""
permission = IsAuthenticatedAndActive()
request = self.factory.get('/test/')
request.user = None
result = permission.has_permission(request, self.view)
self.assertFalse(result)
def test_is_authenticated_and_active_inactive_user(self):
"""Test IsAuthenticatedAndActive denies inactive users"""
permission = IsAuthenticatedAndActive()
self.owner_user.is_active = False
self.owner_user.save()
request = self.factory.get('/test/')
request.user = self.owner_user
result = permission.has_permission(request, self.view)
self.assertFalse(result)
def test_has_tenant_access_same_account(self):
"""Test HasTenantAccess allows users from same account"""
permission = HasTenantAccess()
request = self.factory.get('/test/')
request.user = self.owner_user
request.account = self.account
result = permission.has_permission(request, self.view)
self.assertTrue(result)
def test_has_tenant_access_different_account(self):
"""Test HasTenantAccess denies users from different account"""
permission = HasTenantAccess()
request = self.factory.get('/test/')
request.user = self.owner_user
request.account = self.other_account
result = permission.has_permission(request, self.view)
self.assertFalse(result)
def test_has_tenant_access_admin_bypass(self):
"""Test HasTenantAccess allows admin/developer to bypass"""
permission = HasTenantAccess()
request = self.factory.get('/test/')
request.user = self.admin_user
request.account = self.other_account # Different account
result = permission.has_permission(request, self.view)
self.assertTrue(result) # Admin should bypass
def test_has_tenant_access_system_account(self):
"""Test HasTenantAccess allows system account users to bypass"""
# Create system account owner
system_owner = User.objects.create_user(
username='system_owner_test',
email='system_owner_test@test.com',
password='testpass123',
role='owner'
)
# Create system account
system_account = Account.objects.create(
name="AWS Admin",
slug="aws-admin",
plan=self.plan,
owner=system_owner
)
system_owner.account = system_account
system_owner.save()
system_user = User.objects.create_user(
username='system',
email='system@test.com',
password='testpass123',
role='viewer',
account=system_account
)
permission = HasTenantAccess()
request = self.factory.get('/test/')
request.user = system_user
request.account = self.account # Different account
result = permission.has_permission(request, self.view)
self.assertTrue(result) # System account user should bypass
def test_is_viewer_or_above_viewer(self):
"""Test IsViewerOrAbove allows viewer role"""
permission = IsViewerOrAbove()
request = self.factory.get('/test/')
request.user = self.viewer_user
result = permission.has_permission(request, self.view)
self.assertTrue(result)
def test_is_viewer_or_above_editor(self):
"""Test IsViewerOrAbove allows editor role"""
permission = IsViewerOrAbove()
request = self.factory.get('/test/')
request.user = self.editor_user
result = permission.has_permission(request, self.view)
self.assertTrue(result)
def test_is_viewer_or_above_admin(self):
"""Test IsViewerOrAbove allows admin role"""
permission = IsViewerOrAbove()
request = self.factory.get('/test/')
request.user = self.admin_user
result = permission.has_permission(request, self.view)
self.assertTrue(result)
def test_is_viewer_or_above_owner(self):
"""Test IsViewerOrAbove allows owner role"""
permission = IsViewerOrAbove()
request = self.factory.get('/test/')
request.user = self.owner_user
result = permission.has_permission(request, self.view)
self.assertTrue(result)
def test_is_editor_or_above_viewer_denied(self):
"""Test IsEditorOrAbove denies viewer role"""
permission = IsEditorOrAbove()
request = self.factory.get('/test/')
request.user = self.viewer_user
result = permission.has_permission(request, self.view)
self.assertFalse(result)
def test_is_editor_or_above_editor_allowed(self):
"""Test IsEditorOrAbove allows editor role"""
permission = IsEditorOrAbove()
request = self.factory.get('/test/')
request.user = self.editor_user
result = permission.has_permission(request, self.view)
self.assertTrue(result)
def test_is_editor_or_above_admin_allowed(self):
"""Test IsEditorOrAbove allows admin role"""
permission = IsEditorOrAbove()
request = self.factory.get('/test/')
request.user = self.admin_user
result = permission.has_permission(request, self.view)
self.assertTrue(result)
def test_is_admin_or_owner_viewer_denied(self):
"""Test IsAdminOrOwner denies viewer role"""
permission = IsAdminOrOwner()
request = self.factory.get('/test/')
request.user = self.viewer_user
result = permission.has_permission(request, self.view)
self.assertFalse(result)
def test_is_admin_or_owner_editor_denied(self):
"""Test IsAdminOrOwner denies editor role"""
permission = IsAdminOrOwner()
request = self.factory.get('/test/')
request.user = self.editor_user
result = permission.has_permission(request, self.view)
self.assertFalse(result)
def test_is_admin_or_owner_admin_allowed(self):
"""Test IsAdminOrOwner allows admin role"""
permission = IsAdminOrOwner()
request = self.factory.get('/test/')
request.user = self.admin_user
result = permission.has_permission(request, self.view)
self.assertTrue(result)
def test_is_admin_or_owner_owner_allowed(self):
"""Test IsAdminOrOwner allows owner role"""
permission = IsAdminOrOwner()
request = self.factory.get('/test/')
request.user = self.owner_user
result = permission.has_permission(request, self.view)
self.assertTrue(result)
def test_all_permissions_unauthenticated_denied(self):
"""Test all permissions deny unauthenticated users"""
permissions = [
IsAuthenticatedAndActive(),
HasTenantAccess(),
IsViewerOrAbove(),
IsEditorOrAbove(),
IsAdminOrOwner()
]
request = self.factory.get('/test/')
request.user = None
for permission in permissions:
result = permission.has_permission(request, self.view)
self.assertFalse(result, f"{permission.__class__.__name__} should deny unauthenticated users")

View File

@@ -1,206 +0,0 @@
"""
Unit tests for response helper functions
Tests success_response, error_response, paginated_response
"""
from django.test import TestCase, RequestFactory
from rest_framework import status
from igny8_core.api.response import success_response, error_response, paginated_response, get_request_id
class ResponseHelpersTestCase(TestCase):
"""Test cases for response helper functions"""
def setUp(self):
"""Set up test fixtures"""
self.factory = RequestFactory()
def test_success_response_with_data(self):
"""Test success_response with data"""
data = {"id": 1, "name": "Test"}
response = success_response(data=data, message="Success")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertTrue(response.data['success'])
self.assertEqual(response.data['data'], data)
self.assertEqual(response.data['message'], "Success")
def test_success_response_without_data(self):
"""Test success_response without data"""
response = success_response(message="Success")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertTrue(response.data['success'])
self.assertNotIn('data', response.data)
self.assertEqual(response.data['message'], "Success")
def test_success_response_with_custom_status(self):
"""Test success_response with custom status code"""
data = {"id": 1}
response = success_response(data=data, status_code=status.HTTP_201_CREATED)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertTrue(response.data['success'])
self.assertEqual(response.data['data'], data)
def test_success_response_with_request_id(self):
"""Test success_response includes request_id when request provided"""
request = self.factory.get('/test/')
request.request_id = 'test-request-id-123'
response = success_response(data={"id": 1}, request=request)
self.assertTrue(response.data['success'])
self.assertEqual(response.data['request_id'], 'test-request-id-123')
def test_error_response_with_error_message(self):
"""Test error_response with error message"""
response = error_response(error="Validation failed")
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(response.data['success'])
self.assertEqual(response.data['error'], "Validation failed")
def test_error_response_with_errors_dict(self):
"""Test error_response with field-specific errors"""
errors = {"email": ["Invalid email format"], "password": ["Too short"]}
response = error_response(error="Validation failed", errors=errors)
self.assertFalse(response.data['success'])
self.assertEqual(response.data['error'], "Validation failed")
self.assertEqual(response.data['errors'], errors)
def test_error_response_status_code_mapping(self):
"""Test error_response maps status codes to default error messages"""
# Test 401
response = error_response(status_code=status.HTTP_401_UNAUTHORIZED)
self.assertEqual(response.data['error'], 'Authentication required')
# Test 403
response = error_response(status_code=status.HTTP_403_FORBIDDEN)
self.assertEqual(response.data['error'], 'Permission denied')
# Test 404
response = error_response(status_code=status.HTTP_404_NOT_FOUND)
self.assertEqual(response.data['error'], 'Resource not found')
# Test 409
response = error_response(status_code=status.HTTP_409_CONFLICT)
self.assertEqual(response.data['error'], 'Conflict')
# Test 422
response = error_response(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
self.assertEqual(response.data['error'], 'Validation failed')
# Test 429
response = error_response(status_code=status.HTTP_429_TOO_MANY_REQUESTS)
self.assertEqual(response.data['error'], 'Rate limit exceeded')
# Test 500
response = error_response(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)
self.assertEqual(response.data['error'], 'Internal server error')
def test_error_response_with_request_id(self):
"""Test error_response includes request_id when request provided"""
request = self.factory.get('/test/')
request.request_id = 'test-request-id-456'
response = error_response(error="Error occurred", request=request)
self.assertFalse(response.data['success'])
self.assertEqual(response.data['request_id'], 'test-request-id-456')
def test_error_response_with_debug_info(self):
"""Test error_response includes debug info when provided"""
debug_info = {"exception_type": "ValueError", "message": "Test error"}
response = error_response(error="Error", debug_info=debug_info)
self.assertFalse(response.data['success'])
self.assertEqual(response.data['debug'], debug_info)
def test_paginated_response_with_data(self):
"""Test paginated_response with paginated data"""
paginated_data = {
'count': 100,
'next': 'http://test.com/api/v1/test/?page=2',
'previous': None,
'results': [{"id": 1}, {"id": 2}]
}
response = paginated_response(paginated_data, message="Success")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertTrue(response.data['success'])
self.assertEqual(response.data['count'], 100)
self.assertEqual(response.data['next'], paginated_data['next'])
self.assertEqual(response.data['previous'], None)
self.assertEqual(response.data['results'], paginated_data['results'])
self.assertEqual(response.data['message'], "Success")
def test_paginated_response_without_message(self):
"""Test paginated_response without message"""
paginated_data = {
'count': 50,
'next': None,
'previous': None,
'results': []
}
response = paginated_response(paginated_data)
self.assertTrue(response.data['success'])
self.assertEqual(response.data['count'], 50)
self.assertNotIn('message', response.data)
def test_paginated_response_with_request_id(self):
"""Test paginated_response includes request_id when request provided"""
request = self.factory.get('/test/')
request.request_id = 'test-request-id-789'
paginated_data = {
'count': 10,
'next': None,
'previous': None,
'results': []
}
response = paginated_response(paginated_data, request=request)
self.assertTrue(response.data['success'])
self.assertEqual(response.data['request_id'], 'test-request-id-789')
def test_paginated_response_fallback(self):
"""Test paginated_response handles non-dict input"""
response = paginated_response(None)
self.assertTrue(response.data['success'])
self.assertEqual(response.data['count'], 0)
self.assertIsNone(response.data['next'])
self.assertIsNone(response.data['previous'])
self.assertEqual(response.data['results'], [])
def test_get_request_id_from_request_object(self):
"""Test get_request_id retrieves from request.request_id"""
request = self.factory.get('/test/')
request.request_id = 'request-id-from-object'
request_id = get_request_id(request)
self.assertEqual(request_id, 'request-id-from-object')
def test_get_request_id_from_headers(self):
"""Test get_request_id retrieves from headers"""
request = self.factory.get('/test/', HTTP_X_REQUEST_ID='request-id-from-header')
request_id = get_request_id(request)
self.assertEqual(request_id, 'request-id-from-header')
def test_get_request_id_generates_new(self):
"""Test get_request_id generates new UUID if not found"""
request = self.factory.get('/test/')
request_id = get_request_id(request)
self.assertIsNotNone(request_id)
self.assertIsInstance(request_id, str)
# UUID format check
import uuid
try:
uuid.UUID(request_id)
except ValueError:
self.fail("Generated request_id is not a valid UUID")

View File

@@ -1,199 +0,0 @@
"""
Unit tests for rate limiting
Tests DebugScopedRateThrottle with bypass logic
"""
from django.test import TestCase, override_settings
from rest_framework.test import APIRequestFactory
from rest_framework.views import APIView
from igny8_core.api.throttles import DebugScopedRateThrottle
from igny8_core.auth.models import User, Account, Plan
class ThrottlesTestCase(TestCase):
"""Test cases for rate limiting"""
def setUp(self):
"""Set up test fixtures"""
self.factory = APIRequestFactory()
self.view = APIView()
self.view.throttle_scope = 'planner'
# Create test plan and account
self.plan = Plan.objects.create(
name="Test Plan",
slug="test-plan",
price=0,
credits_per_month=1000
)
# Create owner user first
self.owner_user = User.objects.create_user(
username='owner',
email='owner@test.com',
password='testpass123',
role='owner'
)
# Create test account with owner
self.account = Account.objects.create(
name="Test Account",
slug="test-account",
plan=self.plan,
owner=self.owner_user
)
# Update owner user to have account
self.owner_user.account = self.account
self.owner_user.save()
# Create regular user
self.user = User.objects.create_user(
username='user',
email='user@test.com',
password='testpass123',
role='viewer',
account=self.account
)
# Create admin user
self.admin_user = User.objects.create_user(
username='admin',
email='admin@test.com',
password='testpass123',
role='admin',
account=self.account
)
# Create system account owner
self.system_owner = User.objects.create_user(
username='system_owner',
email='system_owner@test.com',
password='testpass123',
role='owner'
)
# Create system account user
self.system_account = Account.objects.create(
name="AWS Admin",
slug="aws-admin",
plan=self.plan,
owner=self.system_owner
)
self.system_owner.account = self.system_account
self.system_owner.save()
self.system_user = User.objects.create_user(
username='system',
email='system@test.com',
password='testpass123',
role='viewer',
account=self.system_account
)
@override_settings(DEBUG=True)
def test_debug_mode_bypass(self):
"""Test throttling is bypassed in DEBUG mode"""
throttle = DebugScopedRateThrottle()
request = self.factory.get('/test/')
request.user = self.user
result = throttle.allow_request(request, self.view)
self.assertTrue(result) # Should bypass in DEBUG mode
@override_settings(DEBUG=False, IGNY8_DEBUG_THROTTLE=True)
def test_env_bypass(self):
"""Test throttling is bypassed when IGNY8_DEBUG_THROTTLE=True"""
throttle = DebugScopedRateThrottle()
request = self.factory.get('/test/')
request.user = self.user
result = throttle.allow_request(request, self.view)
self.assertTrue(result) # Should bypass when IGNY8_DEBUG_THROTTLE=True
@override_settings(DEBUG=False, IGNY8_DEBUG_THROTTLE=False)
def test_system_account_bypass(self):
"""Test throttling is bypassed for system account users"""
throttle = DebugScopedRateThrottle()
request = self.factory.get('/test/')
request.user = self.system_user
result = throttle.allow_request(request, self.view)
self.assertTrue(result) # System account users should bypass
@override_settings(DEBUG=False, IGNY8_DEBUG_THROTTLE=False)
def test_admin_bypass(self):
"""Test throttling is bypassed for admin/developer users"""
throttle = DebugScopedRateThrottle()
request = self.factory.get('/test/')
request.user = self.admin_user
result = throttle.allow_request(request, self.view)
self.assertTrue(result) # Admin users should bypass
@override_settings(DEBUG=False, IGNY8_DEBUG_THROTTLE=False)
def test_get_rate(self):
"""Test get_rate returns correct rate for scope"""
throttle = DebugScopedRateThrottle()
throttle.scope = 'planner'
rate = throttle.get_rate()
self.assertIsNotNone(rate)
self.assertIn('/', rate) # Should be in format "60/min"
@override_settings(DEBUG=False, IGNY8_DEBUG_THROTTLE=False)
def test_get_rate_default_fallback(self):
"""Test get_rate falls back to default if scope not found"""
throttle = DebugScopedRateThrottle()
throttle.scope = 'nonexistent_scope'
rate = throttle.get_rate()
self.assertIsNotNone(rate)
self.assertEqual(rate, '100/min') # Should fallback to default
def test_parse_rate_minutes(self):
"""Test parse_rate correctly parses minutes"""
throttle = DebugScopedRateThrottle()
num, duration = throttle.parse_rate('60/min')
self.assertEqual(num, 60)
self.assertEqual(duration, 60)
def test_parse_rate_seconds(self):
"""Test parse_rate correctly parses seconds"""
throttle = DebugScopedRateThrottle()
num, duration = throttle.parse_rate('10/sec')
self.assertEqual(num, 10)
self.assertEqual(duration, 1)
def test_parse_rate_hours(self):
"""Test parse_rate correctly parses hours"""
throttle = DebugScopedRateThrottle()
num, duration = throttle.parse_rate('100/hour')
self.assertEqual(num, 100)
self.assertEqual(duration, 3600)
def test_parse_rate_invalid_format(self):
"""Test parse_rate handles invalid format gracefully"""
throttle = DebugScopedRateThrottle()
num, duration = throttle.parse_rate('invalid')
self.assertEqual(num, 100) # Should default to 100
self.assertEqual(duration, 60) # Should default to 60 seconds (1 min)
@override_settings(DEBUG=True)
def test_debug_info_set(self):
"""Test debug info is set when bypassing in DEBUG mode"""
throttle = DebugScopedRateThrottle()
request = self.factory.get('/test/')
request.user = self.user
result = throttle.allow_request(request, self.view)
self.assertTrue(result)
self.assertTrue(hasattr(request, '_throttle_debug_info'))
self.assertIn('scope', request._throttle_debug_info)
self.assertIn('rate', request._throttle_debug_info)
self.assertIn('limit', request._throttle_debug_info)

View File

@@ -0,0 +1,161 @@
"""
Management command to clean up all user-generated data (DESTRUCTIVE).
This is used before V1.0 production launch to start with a clean database.
⚠️ WARNING: This permanently deletes ALL user data!
Usage:
# DRY RUN (recommended first):
python manage.py cleanup_user_data --dry-run
# ACTUAL CLEANUP (after reviewing dry-run):
python manage.py cleanup_user_data --confirm
"""
from django.core.management.base import BaseCommand
from django.db import transaction
from django.conf import settings
class Command(BaseCommand):
help = 'Clean up all user-generated data (DESTRUCTIVE - for pre-launch cleanup)'
def add_arguments(self, parser):
parser.add_argument(
'--confirm',
action='store_true',
help='Confirm you want to delete all user data'
)
parser.add_argument(
'--dry-run',
action='store_true',
help='Show what would be deleted without actually deleting'
)
parser.add_argument(
'--force-staging',
action='store_true',
help='Treat current environment as staging (bypasses production check)'
)
def handle(self, *args, **options):
if not options['confirm'] and not options['dry_run']:
self.stdout.write(
self.style.ERROR('\n⚠️ ERROR: Must use --confirm or --dry-run flag\n')
)
self.stdout.write('Usage:')
self.stdout.write(' python manage.py cleanup_user_data --dry-run # See what will be deleted')
self.stdout.write(' python manage.py cleanup_user_data --confirm # Actually delete data\n')
return
# Safety check: Prevent running in production unless explicitly allowed
env = getattr(settings, 'ENVIRONMENT', 'production')
if options.get('force_staging'):
env = 'staging'
self.stdout.write(self.style.WARNING('\n⚠️ FORCE-STAGING: Treating environment as staging\n'))
if env == 'production' and options['confirm']:
self.stdout.write(
self.style.ERROR('\n⚠️ BLOCKED: Cannot run cleanup in PRODUCTION environment!\n')
)
self.stdout.write('To allow this, use --force-staging flag or set ENVIRONMENT to "staging" in settings.\n')
return
# Import models
from igny8_core.auth.models import Site, User
from igny8_core.business.planning.models import Keywords, Clusters, ContentIdeas
from igny8_core.business.content.models import Tasks, Content, Images
from igny8_core.business.publishing.models import PublishingRecord
from igny8_core.business.integration.models import SyncEvent
from igny8_core.business.billing.models import CreditTransaction, CreditUsageLog
from igny8_core.business.notifications.models import Notification
from igny8_core.business.automation.models import AutomationRun
# Define models to clear (ORDER MATTERS - foreign keys)
# Delete child records before parent records
models_to_clear = [
('Notifications', Notification),
('Credit Usage Logs', CreditUsageLog),
('Credit Transactions', CreditTransaction),
('Sync Events', SyncEvent),
('Publishing Records', PublishingRecord),
('Automation Runs', AutomationRun),
('Images', Images),
('Content', Content),
('Tasks', Tasks),
('Content Ideas', ContentIdeas),
('Clusters', Clusters),
('Keywords', Keywords),
('Sites', Site), # Sites should be near last (many foreign keys)
# Note: We do NOT delete User - keep admin users
]
if options['dry_run']:
self.stdout.write(self.style.WARNING('\n' + '=' * 70))
self.stdout.write(self.style.WARNING('DRY RUN - No data will be deleted'))
self.stdout.write(self.style.WARNING('=' * 70 + '\n'))
total_records = 0
for name, model in models_to_clear:
count = model.objects.count()
total_records += count
status = '' if count > 0 else '·'
self.stdout.write(f' {status} Would delete {count:6d} {name}')
# Count users (not deleted)
user_count = User.objects.count()
self.stdout.write(f'\n → Keeping {user_count:6d} Users (not deleted)')
self.stdout.write(f'\n Total records to delete: {total_records:,}')
self.stdout.write('\n' + '=' * 70)
self.stdout.write(self.style.SUCCESS('\nTo proceed with actual deletion, run:'))
self.stdout.write(' python manage.py cleanup_user_data --confirm\n')
return
# ACTUAL DELETION
self.stdout.write(self.style.ERROR('\n' + '=' * 70))
self.stdout.write(self.style.ERROR('⚠️ DELETING ALL USER DATA - THIS CANNOT BE UNDONE!'))
self.stdout.write(self.style.ERROR('=' * 70 + '\n'))
# Final confirmation prompt
confirm_text = input('Type "DELETE ALL DATA" to proceed: ')
if confirm_text != 'DELETE ALL DATA':
self.stdout.write(self.style.WARNING('\nAborted. Data was NOT deleted.\n'))
return
self.stdout.write('\nProceeding with deletion...\n')
deleted_counts = {}
failed_deletions = []
with transaction.atomic():
for name, model in models_to_clear:
try:
count = model.objects.count()
if count > 0:
model.objects.all().delete()
deleted_counts[name] = count
self.stdout.write(
self.style.SUCCESS(f'✓ Deleted {count:6d} {name}')
)
else:
self.stdout.write(
self.style.WARNING(f'· Skipped {count:6d} {name} (already empty)')
)
except Exception as e:
failed_deletions.append((name, str(e)))
self.stdout.write(
self.style.ERROR(f'✗ Failed to delete {name}: {str(e)}')
)
# Summary
total_deleted = sum(deleted_counts.values())
self.stdout.write('\n' + '=' * 70)
self.stdout.write(self.style.SUCCESS(f'\nUser Data Cleanup Complete!\n'))
self.stdout.write(f' Total records deleted: {total_deleted:,}')
self.stdout.write(f' Failed deletions: {len(failed_deletions)}')
if failed_deletions:
self.stdout.write(self.style.WARNING('\nFailed deletions:'))
for name, error in failed_deletions:
self.stdout.write(f' - {name}: {error}')
self.stdout.write('\n' + '=' * 70 + '\n')

View File

@@ -0,0 +1,122 @@
"""
Management command to export system configuration data to JSON files.
This exports Plans, Credit Costs, AI Models, Industries, Sectors, Seed Keywords, etc.
Usage:
python manage.py export_system_config --output-dir=backups/config
"""
from django.core.management.base import BaseCommand
from django.core import serializers
import json
import os
from datetime import datetime
class Command(BaseCommand):
help = 'Export system configuration data to JSON files for V1.0 backup'
def add_arguments(self, parser):
parser.add_argument(
'--output-dir',
default='backups/config',
help='Output directory for config files (relative to project root)'
)
def handle(self, *args, **options):
output_dir = options['output_dir']
# Make output_dir absolute if it's relative
if not os.path.isabs(output_dir):
# Get project root (parent of manage.py)
import sys
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
output_dir = os.path.join(project_root, '..', output_dir)
os.makedirs(output_dir, exist_ok=True)
self.stdout.write(self.style.SUCCESS(f'\nExporting system configuration to: {output_dir}\n'))
# Import models with correct paths
from igny8_core.auth.models import Plan, Industry, Sector, SeedKeyword
from igny8_core.modules.system.models import AuthorProfile, AIPrompt
from igny8_core.modules.system.global_settings_models import GlobalIntegrationSettings, GlobalAIPrompt
from igny8_core.business.billing.models import CreditCostConfig, AIModelConfig
# Define what to export
exports = {
'plans': (Plan.objects.all(), 'Subscription Plans'),
'credit_costs': (CreditCostConfig.objects.all(), 'Credit Cost Configurations'),
'ai_models': (AIModelConfig.objects.all(), 'AI Model Configurations'),
'global_integrations': (GlobalIntegrationSettings.objects.all(), 'Global Integration Settings'),
'industries': (Industry.objects.all(), 'Industries'),
'sectors': (Sector.objects.all(), 'Sectors'),
'seed_keywords': (SeedKeyword.objects.all(), 'Seed Keywords'),
'author_profiles': (AuthorProfile.objects.all(), 'Author Profiles'),
'prompts': (AIPrompt.objects.all(), 'AI Prompts'),
'global_prompts': (GlobalAIPrompt.objects.all(), 'Global AI Prompts'),
}
successful_exports = []
failed_exports = []
for name, (queryset, description) in exports.items():
try:
count = queryset.count()
data = serializers.serialize('json', queryset, indent=2)
filepath = os.path.join(output_dir, f'{name}.json')
with open(filepath, 'w') as f:
f.write(data)
self.stdout.write(
self.style.SUCCESS(f'✓ Exported {count:4d} {description:30s}{name}.json')
)
successful_exports.append(name)
except Exception as e:
self.stdout.write(
self.style.ERROR(f'✗ Failed to export {description}: {str(e)}')
)
failed_exports.append((name, str(e)))
# Export metadata
metadata = {
'exported_at': datetime.now().isoformat(),
'django_version': self.get_django_version(),
'database': self.get_database_info(),
'successful_exports': successful_exports,
'failed_exports': failed_exports,
'export_count': len(successful_exports),
}
metadata_path = os.path.join(output_dir, 'export_metadata.json')
with open(metadata_path, 'w') as f:
json.dump(metadata, f, indent=2)
self.stdout.write(self.style.SUCCESS(f'\n✓ Metadata saved to export_metadata.json'))
# Summary
self.stdout.write('\n' + '=' * 70)
self.stdout.write(self.style.SUCCESS(f'\nSystem Configuration Export Complete!\n'))
self.stdout.write(f' Successful: {len(successful_exports)} exports')
self.stdout.write(f' Failed: {len(failed_exports)} exports')
self.stdout.write(f' Location: {output_dir}\n')
if failed_exports:
self.stdout.write(self.style.WARNING('\nFailed exports:'))
for name, error in failed_exports:
self.stdout.write(f' - {name}: {error}')
self.stdout.write('=' * 70 + '\n')
def get_django_version(self):
import django
return django.get_version()
def get_database_info(self):
from django.conf import settings
db_config = settings.DATABASES.get('default', {})
return {
'engine': db_config.get('ENGINE', '').split('.')[-1],
'name': db_config.get('NAME', ''),
}

View File

@@ -0,0 +1,37 @@
# Generated manually on 2026-01-13
# Purpose: Drop legacy igny8_site_blueprints table and related structures
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('igny8_core_auth', '0021_accounttrash_sectortrash_sitetrash'),
]
operations = [
migrations.RunSQL(
sql="""
-- Drop indexes first
DROP INDEX IF EXISTS igny8_site__account__38f18a_idx;
DROP INDEX IF EXISTS igny8_site__hosting_7a9a3e_idx;
DROP INDEX IF EXISTS igny8_site__hosting_c4bb41_idx;
DROP INDEX IF EXISTS igny8_site__site_id__5f0a4e_idx;
DROP INDEX IF EXISTS igny8_site__site_id_cb1aca_idx;
DROP INDEX IF EXISTS igny8_site__status_247ddc_idx;
DROP INDEX IF EXISTS igny8_site__status_e7ca10_idx;
-- Drop the table
DROP TABLE IF EXISTS igny8_site_blueprints CASCADE;
-- Drop the sequence
DROP SEQUENCE IF EXISTS igny8_site_blueprints_id_seq;
""",
reverse_sql="""
-- Cannot reverse this migration - table structure was removed from models
-- If rollback is needed, restore from database backup
SELECT 1;
"""
),
]

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)

View File

@@ -1,2 +0,0 @@
# Linker module tests

View File

@@ -1,137 +0,0 @@
"""
Tests for Linker API endpoints
"""
from unittest.mock import patch
from django.test import TestCase
from rest_framework.test import APIClient
from rest_framework import status
from igny8_core.business.content.models import Content
from igny8_core.business.billing.exceptions import InsufficientCreditsError
from igny8_core.api.tests.test_integration_base import IntegrationTestBase
class LinkerAPITests(IntegrationTestBase):
"""Tests for Linker API endpoints"""
def setUp(self):
super().setUp()
self.client = APIClient()
self.client.force_authenticate(user=self.user)
# Create test content
self.content = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title="Test Content",
html_content="<p>Test content.</p>",
word_count=100,
status='draft'
)
def test_process_endpoint_requires_authentication(self):
"""Test that process endpoint requires authentication"""
client = APIClient() # Not authenticated
response = client.post('/api/v1/linker/process/', {
'content_id': self.content.id
})
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
@patch('igny8_core.modules.linker.views.LinkerService.process')
def test_process_endpoint_success(self, mock_process):
"""Test successful processing"""
mock_content = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title="Linked Content",
html_content="<p>Linked.</p>",
internal_links=[{'content_id': 1, 'anchor_text': 'test'}],
linker_version=1,
word_count=100
)
mock_process.return_value = mock_content
response = self.client.post('/api/v1/linker/process/', {
'content_id': self.content.id
}, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertTrue(response.data['success'])
self.assertEqual(response.data['data']['content_id'], self.content.id)
self.assertEqual(response.data['data']['links_added'], 1)
def test_process_endpoint_invalid_content_id(self):
"""Test process endpoint with invalid content ID"""
response = self.client.post('/api/v1/linker/process/', {
'content_id': 99999
}, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
@patch('igny8_core.modules.linker.views.LinkerService.process')
def test_process_endpoint_insufficient_credits(self, mock_process):
"""Test process endpoint with insufficient credits"""
mock_process.side_effect = InsufficientCreditsError("Insufficient credits")
response = self.client.post('/api/v1/linker/process/', {
'content_id': self.content.id
}, format='json')
self.assertEqual(response.status_code, status.HTTP_402_PAYMENT_REQUIRED)
@patch('igny8_core.modules.linker.views.LinkerService.batch_process')
def test_batch_process_endpoint_success(self, mock_batch):
"""Test successful batch processing"""
content2 = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title="Content 2",
word_count=100
)
mock_batch.return_value = [self.content, content2]
response = self.client.post('/api/v1/linker/batch_process/', {
'content_ids': [self.content.id, content2.id]
}, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertTrue(response.data['success'])
self.assertEqual(len(response.data['data']), 2)
def test_batch_process_endpoint_validation(self):
"""Test batch process endpoint validation"""
response = self.client.post('/api/v1/linker/batch_process/', {
'content_ids': [] # Empty list
}, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
def test_process_endpoint_respects_account_isolation(self):
"""Test that process endpoint respects account isolation"""
from igny8_core.auth.models import Account
other_account = Account.objects.create(
name="Other Account",
slug="other",
plan=self.plan,
owner=self.user
)
other_content = Content.objects.create(
account=other_account,
site=self.site,
sector=self.sector,
title="Other Content",
word_count=100
)
response = self.client.post('/api/v1/linker/process/', {
'content_id': other_content.id
}, format='json')
# Should return 400 because content belongs to different account
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

View File

@@ -1,2 +0,0 @@
# Optimizer module tests

View File

@@ -1,180 +0,0 @@
"""
Tests for Optimizer API endpoints
"""
from unittest.mock import patch
from django.test import TestCase
from rest_framework.test import APIClient
from rest_framework import status
from igny8_core.business.content.models import Content
from igny8_core.business.optimization.models import OptimizationTask
from igny8_core.business.billing.exceptions import InsufficientCreditsError
from igny8_core.api.tests.test_integration_base import IntegrationTestBase
class OptimizerAPITests(IntegrationTestBase):
"""Tests for Optimizer API endpoints"""
def setUp(self):
super().setUp()
self.client = APIClient()
self.client.force_authenticate(user=self.user)
# Create test content
self.content = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title="Test Content",
html_content="<p>Test content.</p>",
word_count=500,
status='draft',
source='igny8'
)
def test_optimize_endpoint_requires_authentication(self):
"""Test that optimize endpoint requires authentication"""
client = APIClient() # Not authenticated
response = client.post('/api/v1/optimizer/optimize/', {
'content_id': self.content.id
})
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
@patch('igny8_core.modules.optimizer.views.OptimizerService.optimize_from_writer')
def test_optimize_endpoint_success(self, mock_optimize):
"""Test successful optimization"""
optimized_content = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title="Optimized",
html_content="<p>Optimized.</p>",
word_count=500,
optimizer_version=1,
optimization_scores={'overall_score': 75.0}
)
mock_optimize.return_value = optimized_content
# Create optimization task
task = OptimizationTask.objects.create(
content=optimized_content,
scores_before={'overall_score': 50.0},
scores_after={'overall_score': 75.0},
status='completed',
account=self.account
)
response = self.client.post('/api/v1/optimizer/optimize/', {
'content_id': self.content.id,
'entry_point': 'writer'
}, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertTrue(response.data['success'])
self.assertEqual(response.data['data']['content_id'], self.content.id)
def test_optimize_endpoint_all_entry_points(self):
"""Test optimize endpoint with all entry point values"""
entry_points = ['auto', 'writer', 'wordpress', 'external', 'manual']
for entry_point in entry_points:
with patch(f'igny8_core.modules.optimizer.views.OptimizerService.optimize_{entry_point if entry_point != "auto" else "from_writer"}') as mock_opt:
if entry_point == 'auto':
mock_opt = patch('igny8_core.modules.optimizer.views.OptimizerService.optimize_from_writer')
mock_opt.return_value = self.content
response = self.client.post('/api/v1/optimizer/optimize/', {
'content_id': self.content.id,
'entry_point': entry_point
}, format='json')
# Should accept all entry points
self.assertIn(response.status_code, [status.HTTP_200_OK, status.HTTP_400_BAD_REQUEST])
@patch('igny8_core.modules.optimizer.views.OptimizerService.optimize_from_writer')
def test_batch_optimize_endpoint_success(self, mock_optimize):
"""Test successful batch optimization"""
content2 = Content.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
title="Content 2",
word_count=500,
source='igny8'
)
mock_optimize.return_value = self.content
response = self.client.post('/api/v1/optimizer/batch_optimize/', {
'content_ids': [self.content.id, content2.id],
'entry_point': 'writer'
}, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertTrue(response.data['success'])
self.assertIn('succeeded', response.data['data'])
@patch('igny8_core.modules.optimizer.views.OptimizerService.analyze_only')
def test_analyze_endpoint_success(self, mock_analyze):
"""Test analyze endpoint returns scores"""
scores = {
'seo_score': 50.0,
'readability_score': 60.0,
'engagement_score': 55.0,
'overall_score': 55.0
}
mock_analyze.return_value = scores
response = self.client.post('/api/v1/optimizer/analyze/', {
'content_id': self.content.id
}, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertTrue(response.data['success'])
self.assertIn('scores', response.data['data'])
self.assertEqual(response.data['data']['scores']['overall_score'], 55.0)
@patch('igny8_core.modules.optimizer.views.OptimizerService.optimize_from_writer')
def test_optimize_endpoint_insufficient_credits(self, mock_optimize):
"""Test optimize endpoint with insufficient credits"""
mock_optimize.side_effect = InsufficientCreditsError("Insufficient credits")
response = self.client.post('/api/v1/optimizer/optimize/', {
'content_id': self.content.id
}, format='json')
self.assertEqual(response.status_code, status.HTTP_402_PAYMENT_REQUIRED)
def test_optimize_endpoint_invalid_content_id(self):
"""Test optimize endpoint with invalid content ID"""
response = self.client.post('/api/v1/optimizer/optimize/', {
'content_id': 99999
}, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
def test_optimize_endpoint_respects_account_isolation(self):
"""Test that optimize endpoint respects account isolation"""
from igny8_core.auth.models import Account
other_account = Account.objects.create(
name="Other Account",
slug="other",
plan=self.plan,
owner=self.user
)
other_content = Content.objects.create(
account=other_account,
site=self.site,
sector=self.sector,
title="Other Content",
word_count=100
)
response = self.client.post('/api/v1/optimizer/optimize/', {
'content_id': other_content.id
}, format='json')
# Should return 400 because content belongs to different account
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

View File

@@ -1,8 +0,0 @@
"""
Test settings - auto-clobber test database
"""
from igny8_core.settings import *
# Auto-clobber test database
TEST_RUNNER = 'django.test.runner.DiscoverRunner'

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,191 @@
[
{
"model": "billing.aimodelconfig",
"pk": 12,
"fields": {
"model_name": "bytedance:seedream@4.5",
"model_type": "image",
"provider": "runware",
"display_name": "Seedream 4.5 - High Quality",
"is_default": false,
"is_active": true,
"cost_per_1k_input": null,
"cost_per_1k_output": null,
"tokens_per_credit": null,
"credits_per_image": 5,
"quality_tier": "quality_option2",
"landscape_size": "2560x1440",
"square_size": "2048x2048",
"valid_sizes": [
"2048x2048",
"2304x1728",
"2560x1440",
"1728x2304",
"1440x2560"
],
"max_tokens": null,
"context_window": null,
"capabilities": {
"high_resolution": true,
"provider_settings": {
"bytedance": {
"maxSequentialImages": 4
}
},
"max_sequential_images": 4
},
"created_at": "2026-01-12T09:31:14.196Z",
"updated_at": "2026-01-12T18:26:59.096Z"
}
},
{
"model": "billing.aimodelconfig",
"pk": 5,
"fields": {
"model_name": "dall-e-3",
"model_type": "image",
"provider": "openai",
"display_name": "DALL-E 3 - HD Quality",
"is_default": false,
"is_active": false,
"cost_per_1k_input": null,
"cost_per_1k_output": null,
"tokens_per_credit": null,
"credits_per_image": 5,
"quality_tier": "quality",
"landscape_size": null,
"square_size": "1024x1024",
"valid_sizes": [],
"max_tokens": null,
"context_window": null,
"capabilities": {},
"created_at": "2025-12-24T01:21:08.347Z",
"updated_at": "2026-01-03T21:46:19.717Z"
}
},
{
"model": "billing.aimodelconfig",
"pk": 6,
"fields": {
"model_name": "google:4@2",
"model_type": "image",
"provider": "runware",
"display_name": "Nano Banana - Premium",
"is_default": false,
"is_active": false,
"cost_per_1k_input": null,
"cost_per_1k_output": null,
"tokens_per_credit": null,
"credits_per_image": 15,
"quality_tier": "premium",
"landscape_size": null,
"square_size": "1024x1024",
"valid_sizes": [],
"max_tokens": null,
"context_window": null,
"capabilities": {},
"created_at": "2025-12-24T01:21:08.347Z",
"updated_at": "2026-01-03T20:12:21.724Z"
}
},
{
"model": "billing.aimodelconfig",
"pk": 10,
"fields": {
"model_name": "runware:97@1",
"model_type": "image",
"provider": "runware",
"display_name": "Hi Dream Full - Basic",
"is_default": true,
"is_active": true,
"cost_per_1k_input": null,
"cost_per_1k_output": null,
"tokens_per_credit": null,
"credits_per_image": 1,
"quality_tier": "basic",
"landscape_size": null,
"square_size": "1024x1024",
"valid_sizes": [],
"max_tokens": null,
"context_window": null,
"capabilities": {},
"created_at": "2026-01-03T16:54:17.244Z",
"updated_at": "2026-01-12T23:39:32.397Z"
}
},
{
"model": "billing.aimodelconfig",
"pk": 3,
"fields": {
"model_name": "gpt-4o",
"model_type": "text",
"provider": "openai",
"display_name": "GPT-4o - High Quality with Vision",
"is_default": false,
"is_active": false,
"cost_per_1k_input": null,
"cost_per_1k_output": null,
"tokens_per_credit": 1000,
"credits_per_image": null,
"quality_tier": null,
"landscape_size": null,
"square_size": "1024x1024",
"valid_sizes": [],
"max_tokens": null,
"context_window": 128000,
"capabilities": {},
"created_at": "2025-12-24T01:21:08.346Z",
"updated_at": "2025-12-24T01:21:08.346Z"
}
},
{
"model": "billing.aimodelconfig",
"pk": 1,
"fields": {
"model_name": "gpt-4o-mini",
"model_type": "text",
"provider": "openai",
"display_name": "GPT-4o mini - Fast & Affordable",
"is_default": true,
"is_active": true,
"cost_per_1k_input": null,
"cost_per_1k_output": null,
"tokens_per_credit": 10000,
"credits_per_image": null,
"quality_tier": null,
"landscape_size": null,
"square_size": "1024x1024",
"valid_sizes": [],
"max_tokens": null,
"context_window": 128000,
"capabilities": {},
"created_at": "2025-12-24T01:21:08.345Z",
"updated_at": "2026-01-04T22:12:40.728Z"
}
},
{
"model": "billing.aimodelconfig",
"pk": 4,
"fields": {
"model_name": "gpt-5.1",
"model_type": "text",
"provider": "openai",
"display_name": "GPT-5.1 - Premium",
"is_default": false,
"is_active": true,
"cost_per_1k_input": null,
"cost_per_1k_output": null,
"tokens_per_credit": 1000,
"credits_per_image": null,
"quality_tier": null,
"landscape_size": null,
"square_size": "1024x1024",
"valid_sizes": [],
"max_tokens": null,
"context_window": 8192,
"capabilities": {},
"created_at": "2025-12-24T01:21:08.347Z",
"updated_at": "2026-01-03T23:49:12.914Z"
}
}
]

View File

@@ -0,0 +1,2 @@
[
]

View File

@@ -0,0 +1,102 @@
[
{
"model": "billing.creditcostconfig",
"pk": "clustering",
"fields": {
"display_name": "Keyword Clustering",
"base_credits": 1,
"is_active": true,
"description": "AI-powered keyword clustering into semantic groups"
}
},
{
"model": "billing.creditcostconfig",
"pk": "content_generation",
"fields": {
"display_name": "Content Generation",
"base_credits": 1,
"is_active": true,
"description": "AI-powered article content generation"
}
},
{
"model": "billing.creditcostconfig",
"pk": "idea_generation",
"fields": {
"display_name": "Content Ideas Generation",
"base_credits": 1,
"is_active": true,
"description": "Generate content ideas from keyword clusters"
}
},
{
"model": "billing.creditcostconfig",
"pk": "image_generation",
"fields": {
"display_name": "Image Generation",
"base_credits": 1,
"is_active": true,
"description": "AI-powered image generation"
}
},
{
"model": "billing.creditcostconfig",
"pk": "image_prompt_extraction",
"fields": {
"display_name": "Image Prompt Extraction",
"base_credits": 1,
"is_active": true,
"description": "Extract image prompts from content"
}
},
{
"model": "billing.creditcostconfig",
"pk": "linking",
"fields": {
"display_name": "Internal Linking",
"base_credits": 1,
"is_active": false,
"description": "AI-powered internal link suggestions"
}
},
{
"model": "billing.creditcostconfig",
"pk": "optimization",
"fields": {
"display_name": "Content Optimization",
"base_credits": 1,
"is_active": false,
"description": "AI-powered content optimization"
}
},
{
"model": "billing.creditcostconfig",
"pk": "reparse",
"fields": {
"display_name": "Content Reparse",
"base_credits": 1,
"is_active": false,
"description": "Reparse existing content"
}
},
{
"model": "billing.creditcostconfig",
"pk": "site_page_generation",
"fields": {
"display_name": "Site Page Generation",
"base_credits": 1,
"is_active": false,
"description": "Generate site pages from blueprint"
}
},
{
"model": "billing.creditcostconfig",
"pk": "site_structure_generation",
"fields": {
"display_name": "Site Structure Generation",
"base_credits": 1,
"is_active": false,
"description": "Generate complete site blueprint"
}
}
]

View File

@@ -0,0 +1,22 @@
{
"exported_at": "2026-01-13T03:08:16.424735",
"django_version": "5.2.10",
"database": {
"engine": "postgresql",
"name": "igny8_db"
},
"successful_exports": [
"plans",
"credit_costs",
"ai_models",
"global_integrations",
"industries",
"sectors",
"seed_keywords",
"author_profiles",
"prompts",
"global_prompts"
],
"failed_exports": [],
"export_count": 10
}

View File

@@ -0,0 +1,32 @@
[
{
"model": "system.globalintegrationsettings",
"pk": 1,
"fields": {
"openai_api_key": "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",
"openai_model": "gpt-4o-mini",
"openai_temperature": 0.7,
"openai_max_tokens": 16000,
"anthropic_api_key": "",
"anthropic_model": "claude-3-5-sonnet-20241022",
"anthropic_temperature": 0.7,
"anthropic_max_tokens": 8192,
"default_text_provider": "openai",
"dalle_api_key": "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",
"dalle_model": "dall-e-3",
"dalle_size": "1792x1024",
"runware_api_key": "tuHmZhhyUcArJUQ3r0Jiw8ViPaiit0Z3",
"runware_model": "bria:10@1",
"bria_api_key": "",
"bria_model": "bria-2.3",
"default_image_service": "openai",
"image_quality": "hd",
"image_style": "realistic",
"max_in_article_images": 4,
"desktop_image_size": "1024x1024",
"is_active": true,
"last_updated": "2026-01-03T21:54:01.912Z",
"updated_by": null
}
}
]

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,158 @@
[
{
"model": "igny8_core_auth.industry",
"pk": 22,
"fields": {
"name": "Apparel & Fashion",
"slug": "apparel-fashion",
"description": "Fashion, clothing, and apparel industry",
"is_active": true,
"created_at": "2025-11-19T20:57:22.128Z",
"updated_at": "2025-11-19T20:57:22.128Z"
}
},
{
"model": "igny8_core_auth.industry",
"pk": 9,
"fields": {
"name": "Automotive",
"slug": "automotive",
"description": "Automotive sales, services, and parts",
"is_active": true,
"created_at": "2025-11-04T16:43:57.026Z",
"updated_at": "2025-11-19T20:57:22.146Z"
}
},
{
"model": "igny8_core_auth.industry",
"pk": 23,
"fields": {
"name": "Beauty & Personal Care",
"slug": "beauty-personal-care",
"description": "Beauty, skincare, and personal care products and services",
"is_active": true,
"created_at": "2025-11-19T20:57:22.140Z",
"updated_at": "2025-11-19T20:57:22.141Z"
}
},
{
"model": "igny8_core_auth.industry",
"pk": 29,
"fields": {
"name": "Education & Training",
"slug": "education-training",
"description": "Educational institutions, training programs, and learning services",
"is_active": true,
"created_at": "2025-11-19T20:57:22.190Z",
"updated_at": "2025-11-19T20:57:22.190Z"
}
},
{
"model": "igny8_core_auth.industry",
"pk": 10,
"fields": {
"name": "Fashion & Apparel",
"slug": "fashion-apparel",
"description": "Fashion, clothing, and apparel businesses",
"is_active": false,
"created_at": "2025-11-04T16:43:57.035Z",
"updated_at": "2025-11-19T21:05:19.935Z"
}
},
{
"model": "igny8_core_auth.industry",
"pk": 28,
"fields": {
"name": "Finance & Insurance",
"slug": "finance-insurance",
"description": "Financial services, banking, insurance, and investment",
"is_active": true,
"created_at": "2025-11-19T20:57:22.182Z",
"updated_at": "2025-11-19T20:57:22.182Z"
}
},
{
"model": "igny8_core_auth.industry",
"pk": 7,
"fields": {
"name": "Food & Beverage",
"slug": "food-beverage",
"description": "Restaurants, food services, and beverage industry",
"is_active": true,
"created_at": "2025-11-04T16:43:57.006Z",
"updated_at": "2025-11-19T20:57:22.196Z"
}
},
{
"model": "igny8_core_auth.industry",
"pk": 25,
"fields": {
"name": "Healthcare & Medical",
"slug": "healthcare-medical",
"description": "Healthcare services, medical practices, and health-related services",
"is_active": true,
"created_at": "2025-11-19T20:57:22.161Z",
"updated_at": "2025-11-19T20:57:22.161Z"
}
},
{
"model": "igny8_core_auth.industry",
"pk": 24,
"fields": {
"name": "Home & Furniture",
"slug": "home-furniture",
"description": "Furniture, home decor, and home improvement",
"is_active": true,
"created_at": "2025-11-19T20:57:22.155Z",
"updated_at": "2025-11-19T20:57:22.155Z"
}
},
{
"model": "igny8_core_auth.industry",
"pk": 21,
"fields": {
"name": "Home & Garden",
"slug": "home-garden",
"description": "Home improvement, gardening, landscaping, and interior design",
"is_active": true,
"created_at": "2025-11-05T11:07:20.331Z",
"updated_at": "2025-11-05T11:07:20.331Z"
}
},
{
"model": "igny8_core_auth.industry",
"pk": 26,
"fields": {
"name": "Real Estate & Construction",
"slug": "real-estate-construction",
"description": "Real estate, property management, and construction services",
"is_active": true,
"created_at": "2025-11-19T20:57:22.168Z",
"updated_at": "2025-11-19T20:57:22.168Z"
}
},
{
"model": "igny8_core_auth.industry",
"pk": 1,
"fields": {
"name": "Technology",
"slug": "technology",
"description": "Software, cloud computing, cybersecurity, and technology services",
"is_active": false,
"created_at": "2025-11-04T14:56:39.099Z",
"updated_at": "2025-11-19T21:05:19.931Z"
}
},
{
"model": "igny8_core_auth.industry",
"pk": 27,
"fields": {
"name": "Technology & IT Services",
"slug": "technology-it-services",
"description": "Technology services, software development, and IT solutions",
"is_active": true,
"created_at": "2025-11-19T20:57:22.175Z",
"updated_at": "2025-11-19T20:57:22.175Z"
}
}
]

View File

@@ -0,0 +1,191 @@
[
{
"model": "igny8_core_auth.plan",
"pk": 6,
"fields": {
"name": "Internal (System/Superuser)",
"slug": "internal",
"price": "0.00",
"original_price": null,
"billing_cycle": "monthly",
"annual_discount_percent": 15,
"is_featured": false,
"features": [
"ai_writer",
"image_gen",
"auto_publish",
"custom_prompts",
"unlimited"
],
"is_active": true,
"is_internal": true,
"created_at": "2025-11-08T23:14:16.130Z",
"max_users": 10000,
"max_sites": 20,
"max_industries": null,
"max_author_profiles": 5,
"max_keywords": 100000,
"max_ahrefs_queries": 500,
"included_credits": 10000,
"extra_credit_price": "0.01",
"allow_credit_topup": true,
"auto_credit_topup_threshold": null,
"auto_credit_topup_amount": null,
"stripe_product_id": null,
"stripe_price_id": null,
"credits_per_month": 2000
}
},
{
"model": "igny8_core_auth.plan",
"pk": 1,
"fields": {
"name": "Free Plan",
"slug": "free",
"price": "0.00",
"original_price": null,
"billing_cycle": "monthly",
"annual_discount_percent": 15,
"is_featured": false,
"features": [
"1 Site",
"50 Keywords",
"100 Credits",
"10K Words",
"10 Clusters",
"20 Images"
],
"is_active": false,
"is_internal": false,
"created_at": "2025-11-02T22:01:17.053Z",
"max_users": 1,
"max_sites": 1,
"max_industries": 1,
"max_author_profiles": 2,
"max_keywords": 100,
"max_ahrefs_queries": 0,
"included_credits": 100,
"extra_credit_price": "0.00",
"allow_credit_topup": false,
"auto_credit_topup_threshold": null,
"auto_credit_topup_amount": null,
"stripe_product_id": null,
"stripe_price_id": null,
"credits_per_month": 0
}
},
{
"model": "igny8_core_auth.plan",
"pk": 2,
"fields": {
"name": "Starter",
"slug": "starter",
"price": "99.00",
"original_price": null,
"billing_cycle": "monthly",
"annual_discount_percent": 10,
"is_featured": false,
"features": [
"50 Pages/Articles",
"2 Sites",
"2 Team Users",
"500 Keywords",
"200 Images",
"AI SEO"
],
"is_active": true,
"is_internal": false,
"created_at": "2025-11-04T14:55:33.381Z",
"max_users": 2,
"max_sites": 2,
"max_industries": null,
"max_author_profiles": 5,
"max_keywords": 500,
"max_ahrefs_queries": 50,
"included_credits": 1000,
"extra_credit_price": "0.10",
"allow_credit_topup": true,
"auto_credit_topup_threshold": null,
"auto_credit_topup_amount": null,
"stripe_product_id": "prod_TkGrgKWbrBo4sX",
"stripe_price_id": "price_1SmmJhPdRe4dWeLwSufhikqW",
"credits_per_month": 1000
}
},
{
"model": "igny8_core_auth.plan",
"pk": 4,
"fields": {
"name": "Growth",
"slug": "growth",
"price": "199.00",
"original_price": null,
"billing_cycle": "monthly",
"annual_discount_percent": 10,
"is_featured": true,
"features": [
"200 Pages/Articles",
"5 Sites",
"3 Team Users",
"2000 Keywords",
"800 Images",
"AI SEO"
],
"is_active": true,
"is_internal": false,
"created_at": "2025-11-07T11:46:29.144Z",
"max_users": 3,
"max_sites": 5,
"max_industries": null,
"max_author_profiles": 5,
"max_keywords": 2000,
"max_ahrefs_queries": 200,
"included_credits": 2000,
"extra_credit_price": "0.08",
"allow_credit_topup": true,
"auto_credit_topup_threshold": null,
"auto_credit_topup_amount": null,
"stripe_product_id": "prod_TkGsliLPq7Sl8w",
"stripe_price_id": "price_1SmmKMPdRe4dWeLw9nfQUVus",
"credits_per_month": 0
}
},
{
"model": "igny8_core_auth.plan",
"pk": 5,
"fields": {
"name": "Scale",
"slug": "scale",
"price": "299.00",
"original_price": null,
"billing_cycle": "monthly",
"annual_discount_percent": 10,
"is_featured": false,
"features": [
"500 Pages/Articles",
"99 Sites",
"5 Team Users",
"5000 Keywords",
"2000 Images",
"AI SEO"
],
"is_active": true,
"is_internal": false,
"created_at": "2025-11-07T11:46:29.148Z",
"max_users": 5,
"max_sites": 99,
"max_industries": null,
"max_author_profiles": 10,
"max_keywords": 5000,
"max_ahrefs_queries": 500,
"included_credits": 5000,
"extra_credit_price": "0.06",
"allow_credit_topup": true,
"auto_credit_topup_threshold": null,
"auto_credit_topup_amount": null,
"stripe_product_id": "prod_TkGs64iCjpb9Ok",
"stripe_price_id": "price_1SmmL6PdRe4dWeLwisBUvC0N",
"credits_per_month": 0
}
}
]

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,86 @@
[
{
"model": "igny8_core_auth.sector",
"pk": 50,
"fields": {
"is_deleted": false,
"deleted_at": null,
"restore_until": null,
"delete_reason": null,
"deleted_by": null,
"account": 90,
"site": 21,
"industry_sector": 68,
"name": "Footwear",
"slug": "footwear",
"description": "",
"is_active": false,
"status": "active",
"created_at": "2025-12-10T07:51:10.139Z",
"updated_at": "2025-12-10T07:51:10.139Z"
}
},
{
"model": "igny8_core_auth.sector",
"pk": 51,
"fields": {
"is_deleted": false,
"deleted_at": null,
"restore_until": null,
"delete_reason": null,
"deleted_by": null,
"account": 90,
"site": 21,
"industry_sector": 66,
"name": "Kidswear",
"slug": "kidswear",
"description": "",
"is_active": false,
"status": "active",
"created_at": "2025-12-10T07:51:10.144Z",
"updated_at": "2025-12-10T07:51:10.144Z"
}
},
{
"model": "igny8_core_auth.sector",
"pk": 61,
"fields": {
"is_deleted": false,
"deleted_at": null,
"restore_until": null,
"delete_reason": null,
"deleted_by": null,
"account": 90,
"site": 21,
"industry_sector": 101,
"name": "Physiotherapy & Rehabilitation",
"slug": "physiotherapy-rehabilitation",
"description": "",
"is_active": true,
"status": "active",
"created_at": "2025-12-17T05:40:13.377Z",
"updated_at": "2026-01-01T04:22:28.190Z"
}
},
{
"model": "igny8_core_auth.sector",
"pk": 62,
"fields": {
"is_deleted": false,
"deleted_at": null,
"restore_until": null,
"delete_reason": null,
"deleted_by": null,
"account": 90,
"site": 21,
"industry_sector": 103,
"name": "Mental Health & Therapy",
"slug": "mental-health-therapy",
"description": "",
"is_active": true,
"status": "active",
"created_at": "2026-01-01T04:22:28.197Z",
"updated_at": "2026-01-01T04:22:28.197Z"
}
}
]

File diff suppressed because it is too large Load Diff