336 lines
14 KiB
Python
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
|
|
)
|