helpdesk_rating_five_stars/tests/test_rating_migration.py
2025-11-26 10:39:26 +07:00

483 lines
18 KiB
Python

# -*- 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"
)