483 lines
18 KiB
Python
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"
|
|
)
|