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

336 lines
14 KiB
Python

# -*- coding: utf-8 -*-
from odoo.tests import TransactionCase, tagged
from hypothesis import given, strategies as st, settings
import logging
_logger = logging.getLogger(__name__)
@tagged('post_install', '-at_install', 'helpdesk_rating_five_stars')
class TestDuplicateRatingProperty(TransactionCase):
"""Property-based test for duplicate rating handling (Task 14.1)"""
def setUp(self):
super(TestDuplicateRatingProperty, self).setUp()
# Create a test helpdesk team
self.helpdesk_team = self.env['helpdesk.team'].create({
'name': 'Test Support Team',
'use_rating': True,
})
# Create a test helpdesk ticket
self.ticket = self.env['helpdesk.ticket'].create({
'name': 'Test Ticket for Duplicate Rating',
'team_id': self.helpdesk_team.id,
'partner_id': self.env.ref('base.partner_demo').id,
})
def _create_rating_with_token(self):
"""Helper to create a fresh rating record with token"""
rating = self.env['rating.rating'].create({
'res_model': 'helpdesk.ticket',
'res_id': self.ticket.id,
'parent_res_model': 'helpdesk.team',
'parent_res_id': self.helpdesk_team.id,
'rated_partner_id': self.env.ref('base.partner_admin').id,
'partner_id': self.env.ref('base.partner_demo').id,
'rating': 0, # Not yet rated
'consumed': False,
})
return rating
# Feature: helpdesk-rating-five-stars, Property 17: Multiple ratings update existing record
@given(
first_rating=st.integers(min_value=1, max_value=5),
second_rating=st.integers(min_value=1, max_value=5)
)
@settings(max_examples=100, deadline=None)
def test_property_multiple_ratings_update_existing_record(self, first_rating, second_rating):
"""
Property 17: Multiple ratings update existing record
For any ticket, multiple rating attempts should result in updating the
existing rating record rather than creating duplicates.
Validates: Requirements 7.2
This test verifies that:
1. The first rating submission creates a rating record
2. The second rating submission updates the same record (no duplicate)
3. The rating value is updated to the new value
4. The same token is used for both submissions
5. All relationships (ticket, team, partners) are preserved
6. Only one rating record exists for the ticket after multiple submissions
The test simulates the complete duplicate handling flow:
1. Customer submits first rating via email link or web form
2. Rating is saved and marked as consumed
3. Customer submits second rating (duplicate attempt)
4. System detects duplicate (consumed=True, rating>0)
5. System updates existing record instead of creating new one
6. Latest rating value replaces previous value
"""
# Create a fresh rating for this test iteration
rating = self._create_rating_with_token()
token = rating.access_token
rating_id = rating.id
# Verify initial state - no rating yet
self.assertEqual(rating.rating, 0, "Rating should be 0 initially")
self.assertFalse(rating.consumed, "Rating should not be consumed initially")
# Count initial ratings for this ticket
initial_rating_count = self.env['rating.rating'].search_count([
('res_model', '=', 'helpdesk.ticket'),
('res_id', '=', self.ticket.id),
])
# FIRST RATING SUBMISSION
# =======================
# Step 1: Find rating by token (as controller does)
rating_found = self.env['rating.rating'].sudo().search([
('access_token', '=', token)
], limit=1)
self.assertTrue(rating_found, "Rating should be found by token")
self.assertEqual(rating_found.id, rating_id, "Should find the correct rating record")
# Step 2: Validate first rating value is in valid range
self.assertGreaterEqual(first_rating, 1, "First rating should be >= 1")
self.assertLessEqual(first_rating, 5, "First rating should be <= 5")
# Step 3: Check if this is a duplicate (it's not - first submission)
is_duplicate_first = rating_found.consumed and rating_found.rating > 0
self.assertFalse(is_duplicate_first, "First submission should not be detected as duplicate")
# Step 4: Save the first rating
rating_found.write({
'rating': float(first_rating),
'consumed': True,
})
# Step 5: Verify first rating was saved correctly
self.assertEqual(
rating_found.rating, float(first_rating),
f"First rating should be saved as {first_rating}"
)
self.assertTrue(
rating_found.consumed,
"Rating should be marked as consumed after first submission"
)
# Step 6: Verify no duplicate record was created for first submission
rating_count_after_first = self.env['rating.rating'].search_count([
('res_model', '=', 'helpdesk.ticket'),
('res_id', '=', self.ticket.id),
])
self.assertEqual(
initial_rating_count, rating_count_after_first,
"First submission should not create duplicate records"
)
# SECOND RATING SUBMISSION (DUPLICATE ATTEMPT)
# =============================================
# Step 7: Customer attempts to rate again with the same token
# Find rating by token again (simulating second submission)
rating_found_second = self.env['rating.rating'].sudo().search([
('access_token', '=', token)
], limit=1)
self.assertTrue(rating_found_second, "Rating should still be found by token")
self.assertEqual(
rating_found_second.id, rating_id,
"Should find the SAME rating record (not a new one)"
)
# Step 8: Validate second rating value is in valid range
self.assertGreaterEqual(second_rating, 1, "Second rating should be >= 1")
self.assertLessEqual(second_rating, 5, "Second rating should be <= 5")
# Step 9: Check if this is a duplicate (it IS - second submission)
# This is the key duplicate detection logic from the controller
is_duplicate_second = rating_found_second.consumed and rating_found_second.rating > 0
self.assertTrue(
is_duplicate_second,
"Second submission should be detected as duplicate (consumed=True, rating>0)"
)
# Step 10: Update the existing rating (not create new one)
# This is what the controller does for duplicate submissions
old_rating_value = rating_found_second.rating
rating_found_second.write({
'rating': float(second_rating),
'consumed': True,
})
# Step 11: Verify the rating value was UPDATED (not duplicated)
self.assertEqual(
rating_found_second.rating, float(second_rating),
f"Rating should be updated to {second_rating} (not {old_rating_value})"
)
# Step 12: Verify NO duplicate record was created
# This is the core property: multiple submissions should update, not duplicate
rating_count_after_second = self.env['rating.rating'].search_count([
('res_model', '=', 'helpdesk.ticket'),
('res_id', '=', self.ticket.id),
])
self.assertEqual(
initial_rating_count, rating_count_after_second,
"Second submission should NOT create a duplicate record - should update existing"
)
# Step 13: Verify the same rating ID is used (no new record)
self.assertEqual(
rating_found_second.id, rating_id,
"Rating ID should remain the same - proving update, not create"
)
# Step 14: Verify the token is preserved
self.assertEqual(
rating_found_second.access_token, token,
"Token should remain the same after update"
)
# Step 15: Verify all relationships are preserved
self.assertEqual(
rating_found_second.res_model, 'helpdesk.ticket',
"Resource model should be preserved"
)
self.assertEqual(
rating_found_second.res_id, self.ticket.id,
"Resource ID (ticket) should be preserved"
)
self.assertEqual(
rating_found_second.parent_res_model, 'helpdesk.team',
"Parent resource model should be preserved"
)
self.assertEqual(
rating_found_second.parent_res_id, self.helpdesk_team.id,
"Parent resource ID (team) should be preserved"
)
# Step 16: Verify only ONE rating exists for this ticket
# This is the ultimate proof that duplicates are not created
all_ratings_for_ticket = self.env['rating.rating'].search([
('res_model', '=', 'helpdesk.ticket'),
('res_id', '=', self.ticket.id),
])
self.assertEqual(
len(all_ratings_for_ticket), initial_rating_count,
f"Should have exactly {initial_rating_count} rating(s) for ticket, not more"
)
# Step 17: Verify the latest rating value is what's stored
# The second rating should have replaced the first rating
final_rating = self.env['rating.rating'].sudo().browse(rating_id)
self.assertEqual(
final_rating.rating, float(second_rating),
f"Final rating should be {second_rating} (latest submission), not {first_rating}"
)
# Step 18: Verify consumed flag is still True
self.assertTrue(
final_rating.consumed,
"Rating should still be marked as consumed after update"
)
# Step 19: Verify the rating is immediately queryable with new value
# This ensures the update was persisted correctly
updated_rating = self.env['rating.rating'].sudo().search([
('id', '=', rating_id),
('rating', '=', float(second_rating)),
('consumed', '=', True),
], limit=1)
self.assertTrue(
updated_rating,
f"Updated rating with value {second_rating} should be immediately queryable"
)
self.assertEqual(
updated_rating.id, rating_id,
"Queried rating should be the same record (proving update, not create)"
)
# Step 20: Verify no orphaned or duplicate ratings exist
# Search for any ratings with the same token
ratings_with_token = self.env['rating.rating'].sudo().search([
('access_token', '=', token)
])
self.assertEqual(
len(ratings_with_token), 1,
"Should have exactly 1 rating with this token (no duplicates)"
)
self.assertEqual(
ratings_with_token[0].id, rating_id,
"The rating with this token should be our original rating (updated)"
)
# Step 21: Verify the update behavior is consistent
# If we were to submit a third rating, it should also update (not create)
# This proves the duplicate handling is consistent across multiple attempts
# Generate a third rating value for consistency check
third_rating = (second_rating % 5) + 1 # Ensure it's different and in range 1-5
# Find rating by token for third submission
rating_found_third = self.env['rating.rating'].sudo().search([
('access_token', '=', token)
], limit=1)
# Verify it's still the same record
self.assertEqual(
rating_found_third.id, rating_id,
"Third submission should still find the same rating record"
)
# Check duplicate detection for third submission
is_duplicate_third = rating_found_third.consumed and rating_found_third.rating > 0
self.assertTrue(
is_duplicate_third,
"Third submission should also be detected as duplicate"
)
# Update with third rating
rating_found_third.write({
'rating': float(third_rating),
'consumed': True,
})
# Verify still no duplicates after third submission
rating_count_after_third = self.env['rating.rating'].search_count([
('res_model', '=', 'helpdesk.ticket'),
('res_id', '=', self.ticket.id),
])
self.assertEqual(
initial_rating_count, rating_count_after_third,
"Third submission should also NOT create duplicate - consistent behavior"
)
# Verify the rating value was updated to third value
self.assertEqual(
rating_found_third.rating, float(third_rating),
f"Rating should be updated to {third_rating} after third submission"
)
# Final verification: Only one rating record exists with the latest value
final_check_rating = self.env['rating.rating'].sudo().browse(rating_id)
self.assertEqual(
final_check_rating.rating, float(third_rating),
f"Final rating should be {third_rating} (latest of three submissions)"
)
_logger.info(
"Property 17 verified: Multiple ratings (%s, %s, %s) updated existing record %s "
"without creating duplicates. Final value: %s",
first_rating, second_rating, third_rating, rating_id, third_rating
)