# -*- coding: utf-8 -*- from odoo.tests.common import TransactionCase from odoo import api, SUPERUSER_ID from hypothesis import given, strategies as st, settings class TestRatingMigration(TransactionCase): """Test cases for rating migration from 0-3 scale to 0-5 scale""" def setUp(self): super(TestRatingMigration, self).setUp() self.Rating = self.env['rating.rating'] self.Partner = self.env['res.partner'] self.User = self.env['res.users'] # Create test partner and user for rating context self.test_partner = self.Partner.create({ 'name': 'Test Customer', 'email': 'test@example.com', }) self.test_user = self.User.create({ 'name': 'Test User', 'login': 'testuser_migration', 'email': 'testuser_migration@example.com', }) def _create_rating_with_sql(self, rating_value): """ Helper method to create a rating using SQL to bypass constraints. This simulates old ratings that existed before the 5-star system. """ # First, temporarily disable the constraint self.env.cr.execute(""" ALTER TABLE rating_rating DROP CONSTRAINT IF EXISTS rating_rating_rating_range """) self.env.cr.execute(""" INSERT INTO rating_rating (rating, partner_id, rated_partner_id, res_model, res_id, create_date, write_date, create_uid, write_uid, access_token) VALUES (%s, %s, %s, %s, %s, NOW(), NOW(), %s, %s, %s) RETURNING id """, ( rating_value, self.test_partner.id, self.test_user.partner_id.id, 'res.partner', self.test_partner.id, SUPERUSER_ID, SUPERUSER_ID, 'test_token_' + str(rating_value) )) rating_id = self.env.cr.fetchone()[0] # Re-enable the constraint self.env.cr.execute(""" ALTER TABLE rating_rating ADD CONSTRAINT rating_rating_rating_range CHECK (rating = 0 OR (rating >= 1 AND rating <= 5)) """) return rating_id def _run_migration_logic(self): """ Helper method to run the migration logic without the hook wrapper. This avoids commit/rollback issues in tests. """ # Define the migration mapping migration_mapping = { 0: 0, # No rating stays 0 1: 3, # Poor (1) becomes 3 stars 2: 4, # Okay (2) becomes 4 stars 3: 5, # Good (3) becomes 5 stars } # Get all ratings that need migration (values 0-3) self.env.cr.execute(""" SELECT id, rating FROM rating_rating WHERE rating IN (0, 1, 2, 3) """) ratings_to_migrate = self.env.cr.fetchall() # Migrate each rating for rating_id, old_value in ratings_to_migrate: if old_value in migration_mapping: new_value = migration_mapping[old_value] self.env.cr.execute(""" UPDATE rating_rating SET rating = %s WHERE id = %s AND rating = %s """, (new_value, rating_id, old_value)) def test_migration_mapping_0_to_0(self): """ Test migration mapping: 0 → 0 Validates: Requirements 3.2 """ # Create a rating with value 0 using SQL rating_id = self._create_rating_with_sql(0) # Run migration logic self._run_migration_logic() # Verify the rating is still 0 rating = self.Rating.browse(rating_id) self.assertEqual(rating.rating, 0, "Rating value 0 should remain 0 after migration") def test_migration_mapping_1_to_3(self): """ Test migration mapping: 1 → 3 Validates: Requirements 3.3 """ # Create a rating with value 1 using SQL rating_id = self._create_rating_with_sql(1) # Run migration logic self._run_migration_logic() # Verify the rating is now 3 rating = self.Rating.browse(rating_id) self.assertEqual(rating.rating, 3, "Rating value 1 should be converted to 3") def test_migration_mapping_2_to_4(self): """ Test migration mapping: 2 → 4 Validates: Requirements 3.4 """ # Create a rating with value 2 using SQL rating_id = self._create_rating_with_sql(2) # Run migration logic self._run_migration_logic() # Verify the rating is now 4 rating = self.Rating.browse(rating_id) self.assertEqual(rating.rating, 4, "Rating value 2 should be converted to 4") def test_migration_mapping_3_to_5(self): """ Test migration mapping: 3 → 5 Validates: Requirements 3.5 """ # Create a rating with value 3 using SQL rating_id = self._create_rating_with_sql(3) # Run migration logic self._run_migration_logic() # Verify the rating is now 5 rating = self.Rating.browse(rating_id) self.assertEqual(rating.rating, 5, "Rating value 3 should be converted to 5") def test_migration_all_mappings(self): """ Test that all migration mappings work correctly in a single run Validates: Requirements 3.1, 3.2, 3.3, 3.4, 3.5 """ # Create ratings with all old scale values rating_ids = { 0: self._create_rating_with_sql(0), 1: self._create_rating_with_sql(1), 2: self._create_rating_with_sql(2), 3: self._create_rating_with_sql(3), } # Run migration logic self._run_migration_logic() # Verify all mappings expected_mappings = { 0: 0, 1: 3, 2: 4, 3: 5, } for old_value, rating_id in rating_ids.items(): rating = self.Rating.browse(rating_id) expected_value = expected_mappings[old_value] self.assertEqual(rating.rating, expected_value, f"Rating {old_value} should be converted to {expected_value}") def test_migration_preserves_other_fields(self): """ Test that migration preserves all other rating fields Validates: Requirements 3.6 """ # Create a rating with value 2 rating_id = self._create_rating_with_sql(2) # Get the rating and verify initial state rating = self.Rating.browse(rating_id) original_partner_id = rating.partner_id.id original_rated_partner_id = rating.rated_partner_id.id original_res_model = rating.res_model original_res_id = rating.res_id # Run migration logic self._run_migration_logic() # Invalidate cache and refresh the rating from database self.env.invalidate_all() rating = self.Rating.browse(rating_id) # Verify rating value changed self.assertEqual(rating.rating, 4, "Rating should be migrated to 4") # Verify other fields are preserved self.assertEqual(rating.partner_id.id, original_partner_id, "partner_id should be preserved") self.assertEqual(rating.rated_partner_id.id, original_rated_partner_id, "rated_partner_id should be preserved") self.assertEqual(rating.res_model, original_res_model, "res_model should be preserved") self.assertEqual(rating.res_id, original_res_id, "res_id should be preserved") def test_migration_idempotent(self): """ Test that running migration multiple times doesn't cause issues """ # Create ratings with old scale values rating_id_1 = self._create_rating_with_sql(1) rating_id_2 = self._create_rating_with_sql(2) # Run migration logic first time self._run_migration_logic() # Verify first migration rating_1 = self.Rating.browse(rating_id_1) rating_2 = self.Rating.browse(rating_id_2) self.assertEqual(rating_1.rating, 3) self.assertEqual(rating_2.rating, 4) # Run migration logic second time (should not change already migrated values) self._run_migration_logic() # Verify values are still correct rating_1 = self.Rating.browse(rating_id_1) rating_2 = self.Rating.browse(rating_id_2) self.assertEqual(rating_1.rating, 3, "Already migrated rating should not change") self.assertEqual(rating_2.rating, 4, "Already migrated rating should not change") def test_migration_with_no_ratings(self): """ Test that migration handles empty database gracefully """ # Ensure no ratings exist in old scale self.env.cr.execute("DELETE FROM rating_rating WHERE rating IN (0, 1, 2, 3)") # Run migration logic (should not raise any errors) try: self._run_migration_logic() except Exception as e: self.fail(f"Migration should handle empty database gracefully, but raised: {e}") def test_migration_batch_processing(self): """ Test that migration can handle large number of ratings """ # Create multiple ratings to test batch processing rating_ids = [] for i in range(50): # Create 50 ratings old_value = i % 4 # Cycle through 0, 1, 2, 3 rating_id = self._create_rating_with_sql(old_value) rating_ids.append((rating_id, old_value)) # Run migration logic self._run_migration_logic() # Verify all ratings were migrated correctly expected_mappings = {0: 0, 1: 3, 2: 4, 3: 5} for rating_id, old_value in rating_ids: rating = self.Rating.browse(rating_id) expected_value = expected_mappings[old_value] self.assertEqual(rating.rating, expected_value, f"Rating {old_value} should be converted to {expected_value}") @given(st.lists(st.integers(min_value=0, max_value=3), min_size=1, max_size=100)) @settings(max_examples=100, deadline=None) def test_property_migration_converts_all_ratings(self, old_ratings): """ Property Test: Migration converts all ratings Feature: helpdesk-rating-five-stars, Property 7: Migration converts all ratings Validates: Requirements 3.1 Property: For any list of old-scale ratings (0-3), the migration process should convert ALL of them to the new scale (0, 3, 4, 5) according to the mapping: - 0 → 0 - 1 → 3 - 2 → 4 - 3 → 5 """ # Define expected migration mapping migration_mapping = { 0: 0, 1: 3, 2: 4, 3: 5, } # Create ratings with the generated old scale values rating_ids = [] for old_value in old_ratings: rating_id = self._create_rating_with_sql(old_value) rating_ids.append((rating_id, old_value)) # Run migration logic self._run_migration_logic() # Property: ALL ratings should be converted according to the mapping for rating_id, old_value in rating_ids: rating = self.Rating.browse(rating_id) expected_value = migration_mapping[old_value] self.assertEqual( rating.rating, expected_value, f"Migration failed: rating with old value {old_value} should be " f"converted to {expected_value}, but got {rating.rating}" ) # Additional property: No ratings should remain in the old scale (except 0) # After migration, all non-zero ratings should be >= 3 for rating_id, old_value in rating_ids: rating = self.Rating.browse(rating_id) if rating.rating > 0: self.assertGreaterEqual( rating.rating, 3, f"After migration, non-zero ratings should be >= 3, but got {rating.rating}" ) self.assertLessEqual( rating.rating, 5, f"After migration, ratings should be <= 5, but got {rating.rating}" ) @given(st.lists( st.tuples( st.integers(min_value=0, max_value=3), # old rating value st.text(alphabet='abcdefghijklmnopqrstuvwxyz._', min_size=5, max_size=30), # res_model (valid model name format) st.integers(min_value=1, max_value=1000) # res_id ), min_size=1, max_size=50 )) @settings(max_examples=100, deadline=None) def test_property_migration_preserves_data_integrity(self, rating_data): """ Property Test: Migration preserves data integrity Feature: helpdesk-rating-five-stars, Property 8: Migration preserves data integrity Validates: Requirements 3.6 Property: For any ticket-rating relationship before migration, the same relationship should exist after migration with the converted rating value. All fields except the rating value should remain unchanged. """ # Define expected migration mapping migration_mapping = { 0: 0, 1: 3, 2: 4, 3: 5, } # Store pre-migration state: rating_id -> (old_value, partner_id, rated_partner_id, res_model, res_id, access_token) pre_migration_state = {} # Create ratings with the generated data for old_value, res_model, res_id in rating_data: # Create rating using SQL to bypass constraints self.env.cr.execute(""" ALTER TABLE rating_rating DROP CONSTRAINT IF EXISTS rating_rating_rating_range """) # Generate unique access token access_token = f'test_token_{old_value}_{res_model}_{res_id}_{len(pre_migration_state)}' self.env.cr.execute(""" INSERT INTO rating_rating (rating, partner_id, rated_partner_id, res_model, res_id, create_date, write_date, create_uid, write_uid, access_token) VALUES (%s, %s, %s, %s, %s, NOW(), NOW(), %s, %s, %s) RETURNING id """, ( old_value, self.test_partner.id, self.test_user.partner_id.id, res_model, res_id, SUPERUSER_ID, SUPERUSER_ID, access_token )) rating_id = self.env.cr.fetchone()[0] # Re-enable the constraint self.env.cr.execute(""" ALTER TABLE rating_rating ADD CONSTRAINT rating_rating_rating_range CHECK (rating = 0 OR (rating >= 1 AND rating <= 5)) """) # Store pre-migration state pre_migration_state[rating_id] = { 'old_rating': old_value, 'partner_id': self.test_partner.id, 'rated_partner_id': self.test_user.partner_id.id, 'res_model': res_model, 'res_id': res_id, 'access_token': access_token } # Run migration logic self._run_migration_logic() # Invalidate cache to ensure we read fresh data from database self.env.invalidate_all() # Property: ALL ticket-rating relationships should be preserved for rating_id, pre_state in pre_migration_state.items(): rating = self.Rating.browse(rating_id) # Verify rating exists self.assertTrue( rating.exists(), f"Rating {rating_id} should still exist after migration" ) # Verify rating value was converted correctly expected_rating = migration_mapping[pre_state['old_rating']] self.assertEqual( rating.rating, expected_rating, f"Rating value should be converted from {pre_state['old_rating']} to {expected_rating}, " f"but got {rating.rating}" ) # Property: ALL other fields should be preserved self.assertEqual( rating.partner_id.id, pre_state['partner_id'], f"partner_id should be preserved for rating {rating_id}" ) self.assertEqual( rating.rated_partner_id.id, pre_state['rated_partner_id'], f"rated_partner_id should be preserved for rating {rating_id}" ) self.assertEqual( rating.res_model, pre_state['res_model'], f"res_model should be preserved for rating {rating_id}" ) self.assertEqual( rating.res_id, pre_state['res_id'], f"res_id should be preserved for rating {rating_id}" ) self.assertEqual( rating.access_token, pre_state['access_token'], f"access_token should be preserved for rating {rating_id}" ) # Additional property: The number of ratings should remain the same self.assertEqual( len(pre_migration_state), len([r for r in self.Rating.browse(list(pre_migration_state.keys())) if r.exists()]), "The number of ratings should remain the same after migration" )