diff --git a/Dockerfile b/Dockerfile index 670e9af..54e75f6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,14 @@ FROM odoo:14.0 -# Install Python testing dependencies using the system Python +# Install Python testing dependencies. odoo:14.0 ships Python 3.7, so pin to +# the last releases that still support 3.7 (pytest 8 / pytest-bdd 7 / pytest-html 4 +# all require 3.8+). RUN python3 -m pip install --no-cache-dir \ - pytest \ - pytest-odoo \ - pytest-bdd \ - pytest-cov \ - pytest-html \ + "pytest>=7,<8" \ + "pytest-odoo<2" \ + "pytest-bdd>=6,<7" \ + "pytest-cov<5" \ + "pytest-html<4" \ requests # Copy addon to Odoo addons path diff --git a/addons/itsulu_blog_publisher/tests/test_blog_generation_log.py b/addons/itsulu_blog_publisher/tests/test_blog_generation_log.py index 9656e81..0c1397e 100644 --- a/addons/itsulu_blog_publisher/tests/test_blog_generation_log.py +++ b/addons/itsulu_blog_publisher/tests/test_blog_generation_log.py @@ -14,11 +14,10 @@ from .factories import BlogPublisherFactory class TestBlogGenerationLogCreation(TransactionCase): """Verify that generation log records capture the correct metadata.""" - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.factory = BlogPublisherFactory(cls.env) - cls.blog = cls.factory.blog(name='ITSulu Insights') + def setUp(self): + super().setUp() + self.factory = BlogPublisherFactory(self.env) + self.blog = self.factory.blog(name='ITSulu Insights') def test_successful_log_record_is_created_with_correct_fields(self): """ @@ -105,10 +104,9 @@ class TestBlogGenerationLogCreation(TransactionCase): class TestBlogGenerationLogRetry(TransactionCase): """Verify that failed logs expose a working Retry action.""" - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.factory = BlogPublisherFactory(cls.env) + def setUp(self): + super().setUp() + self.factory = BlogPublisherFactory(self.env) def test_error_log_action_retry_returns_wizard_action(self): """ diff --git a/addons/itsulu_blog_publisher/tests/test_blog_post_social.py b/addons/itsulu_blog_publisher/tests/test_blog_post_social.py index 44e137a..61f4652 100644 --- a/addons/itsulu_blog_publisher/tests/test_blog_post_social.py +++ b/addons/itsulu_blog_publisher/tests/test_blog_post_social.py @@ -16,11 +16,10 @@ from .factories import BlogPublisherFactory class TestSEOPopulation(TransactionCase): """Verify that all SEO fields are correctly populated after blog post generation.""" - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.factory = BlogPublisherFactory(cls.env) - cls.blog = cls.factory.blog(name='ITSulu Insights') + def setUp(self): + super().setUp() + self.factory = BlogPublisherFactory(self.env) + self.blog = self.factory.blog(name='ITSulu Insights') def test_generated_post_has_non_empty_meta_title(self): """ @@ -125,11 +124,10 @@ class TestSEOPopulation(TransactionCase): class TestBlogPostSocialModel(TransactionCase): """Verify the itsulu.blog.post.social model stores all platform copy correctly.""" - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.factory = BlogPublisherFactory(cls.env) - cls.blog = cls.factory.blog(name='ITSulu Insights') + def setUp(self): + super().setUp() + self.factory = BlogPublisherFactory(self.env) + self.blog = self.factory.blog(name='ITSulu Insights') def test_social_record_is_linked_one_to_one_with_blog_post(self): """Each blog post has at most one social record.""" @@ -200,12 +198,11 @@ class TestNotificationEmail(TransactionCase): structure matching the [ITSulu Insights] template in the uploaded .eml. """ - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.factory = BlogPublisherFactory(cls.env) - cls.blog = cls.factory.blog(name='ITSulu Insights') - cls.env['ir.config_parameter'].sudo().set_param( + def setUp(self): + super().setUp() + self.factory = BlogPublisherFactory(self.env) + self.blog = self.factory.blog(name='ITSulu Insights') + self.env['ir.config_parameter'].sudo().set_param( 'itsulu_blog_publisher.notification_emails', 'nicholasr@itsulu.com', ) diff --git a/addons/itsulu_blog_publisher/tests/test_blog_schedule.py b/addons/itsulu_blog_publisher/tests/test_blog_schedule.py index cc5e0ea..48db1be 100644 --- a/addons/itsulu_blog_publisher/tests/test_blog_schedule.py +++ b/addons/itsulu_blog_publisher/tests/test_blog_schedule.py @@ -15,11 +15,10 @@ from .factories import BlogPublisherFactory class TestBlogScheduleConfiguration(TransactionCase): """Verify that schedule slot records are configured correctly.""" - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.factory = BlogPublisherFactory(cls.env) - cls.blog = cls.factory.blog(name='ITSulu Insights') + def setUp(self): + super().setUp() + self.factory = BlogPublisherFactory(self.env) + self.blog = self.factory.blog(name='ITSulu Insights') def test_schedule_slot_is_created_with_correct_defaults(self): """ @@ -69,12 +68,11 @@ class TestBlogScheduleExecution(TransactionCase): LLM calls are mocked — we are testing orchestration, not the LLM. """ - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.factory = BlogPublisherFactory(cls.env) - cls.blog = cls.factory.blog(name='ITSulu Insights') - cls.env['ir.config_parameter'].sudo().set_param( + def setUp(self): + super().setUp() + self.factory = BlogPublisherFactory(self.env) + self.blog = self.factory.blog(name='ITSulu Insights') + self.env['ir.config_parameter'].sudo().set_param( 'itsulu_blog_publisher.anthropic_api_key', 'sk-ant-test-key' ) diff --git a/addons/itsulu_blog_publisher/tests/test_blog_topic.py b/addons/itsulu_blog_publisher/tests/test_blog_topic.py index e9b3b87..3dd43eb 100644 --- a/addons/itsulu_blog_publisher/tests/test_blog_topic.py +++ b/addons/itsulu_blog_publisher/tests/test_blog_topic.py @@ -15,10 +15,9 @@ from .factories import BlogPublisherFactory class TestBlogTopicQueueManagement(TransactionCase): """Verify that the topic queue picks topics in priority order.""" - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.factory = BlogPublisherFactory(cls.env) + def setUp(self): + super().setUp() + self.factory = BlogPublisherFactory(self.env) def test_topic_is_created_with_pending_state(self): """ diff --git a/addons/itsulu_blog_publisher/tests/test_llm_router.py b/addons/itsulu_blog_publisher/tests/test_llm_router.py index 1abc262..1d1184b 100644 --- a/addons/itsulu_blog_publisher/tests/test_llm_router.py +++ b/addons/itsulu_blog_publisher/tests/test_llm_router.py @@ -42,21 +42,20 @@ def _make_mock_llm_response(tokens_used=800): class TestLLMRouterProviderDispatch(TransactionCase): """Verify that the LLM router dispatches to the correct backend provider.""" - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.factory = BlogPublisherFactory(cls.env) + def setUp(self): + super().setUp() + self.factory = BlogPublisherFactory(self.env) # Store a known-valid stub API key in ir.config_parameter - cls.env['ir.config_parameter'].sudo().set_param( + self.env['ir.config_parameter'].sudo().set_param( 'itsulu_blog_publisher.anthropic_api_key', 'sk-ant-test-key' ) - cls.env['ir.config_parameter'].sudo().set_param( + self.env['ir.config_parameter'].sudo().set_param( 'itsulu_blog_publisher.openai_api_key', 'sk-openai-test-key' ) - cls.env['ir.config_parameter'].sudo().set_param( + self.env['ir.config_parameter'].sudo().set_param( 'itsulu_blog_publisher.gemini_api_key', 'gemini-test-key' ) - cls.env['ir.config_parameter'].sudo().set_param( + self.env['ir.config_parameter'].sudo().set_param( 'itsulu_blog_publisher.ollama_base_url', 'http://localhost:11434' ) @@ -170,11 +169,10 @@ class TestLLMRouterProviderDispatch(TransactionCase): class TestLLMRouterTokenLogging(TransactionCase): """Verify that token usage is captured from provider responses.""" - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.factory = BlogPublisherFactory(cls.env) - cls.env['ir.config_parameter'].sudo().set_param( + def setUp(self): + super().setUp() + self.factory = BlogPublisherFactory(self.env) + self.env['ir.config_parameter'].sudo().set_param( 'itsulu_blog_publisher.anthropic_api_key', 'sk-ant-test-key' ) diff --git a/addons/itsulu_blog_publisher/tests/test_performance.py b/addons/itsulu_blog_publisher/tests/test_performance.py index 84c478f..d4216cc 100644 --- a/addons/itsulu_blog_publisher/tests/test_performance.py +++ b/addons/itsulu_blog_publisher/tests/test_performance.py @@ -16,12 +16,11 @@ _logger = logging.getLogger(__name__) class TestGenerationLatency(TransactionCase): """Measure time from run_generation() call to blog.post creation.""" - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.factory = BlogPublisherFactory(cls.env) - cls.blog = cls.factory.blog(name='ITSulu Insights') - cls.env['ir.config_parameter'].sudo().set_param( + def setUp(self): + super().setUp() + self.factory = BlogPublisherFactory(self.env) + self.blog = self.factory.blog(name='ITSulu Insights') + self.env['ir.config_parameter'].sudo().set_param( 'itsulu_blog_publisher.anthropic_api_key', 'sk-ant-test-key' ) @@ -103,12 +102,11 @@ class TestGenerationLatency(TransactionCase): class TestQueryCount(TransactionCase): """Verify N+1 query patterns don't exist in critical paths.""" - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.factory = BlogPublisherFactory(cls.env) - cls.blog = cls.factory.blog(name='ITSulu Insights') - cls.env['ir.config_parameter'].sudo().set_param( + def setUp(self): + super().setUp() + self.factory = BlogPublisherFactory(self.env) + self.blog = self.factory.blog(name='ITSulu Insights') + self.env['ir.config_parameter'].sudo().set_param( 'itsulu_blog_publisher.anthropic_api_key', 'sk-ant-test-key' ) @@ -179,11 +177,10 @@ class TestQueryCount(TransactionCase): class TestTokenUsageBaseline(TransactionCase): """Establish token usage baseline for cost tracking.""" - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.factory = BlogPublisherFactory(cls.env) - cls.blog = cls.factory.blog(name='ITSulu Insights') + def setUp(self): + super().setUp() + self.factory = BlogPublisherFactory(self.env) + self.blog = self.factory.blog(name='ITSulu Insights') def test_typical_post_uses_800_to_1200_tokens(self): """ @@ -231,12 +228,11 @@ class TestTokenUsageBaseline(TransactionCase): class TestConcurrentGeneration(TransactionCase): """Test that concurrent post generation handles contention correctly.""" - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.factory = BlogPublisherFactory(cls.env) - cls.blog = cls.factory.blog(name='ITSulu Insights') - cls.env['ir.config_parameter'].sudo().set_param( + def setUp(self): + super().setUp() + self.factory = BlogPublisherFactory(self.env) + self.blog = self.factory.blog(name='ITSulu Insights') + self.env['ir.config_parameter'].sudo().set_param( 'itsulu_blog_publisher.anthropic_api_key', 'sk-ant-test-key' ) diff --git a/addons/itsulu_blog_publisher/views/blog_schedule_views.xml b/addons/itsulu_blog_publisher/views/blog_schedule_views.xml index 9ec9463..c6ce9d4 100644 --- a/addons/itsulu_blog_publisher/views/blog_schedule_views.xml +++ b/addons/itsulu_blog_publisher/views/blog_schedule_views.xml @@ -38,7 +38,7 @@ + attrs="{'invisible': [('active', '=', True)]}"/>