/home/user/openspp-modules-v2/spp_cel_widget/tests/test_symbol_provider.py/home/user/openspp-modules-v2/spp_cel_widget/tests/test_controller.py
- Profile retrieval - Tests for both individuals and groups profiles
- Symbol structure - Verifies variables, functions, operators, keywords are returned
- Field metadata - Checks that fields have type information
- Function signatures - Validates functions have proper documentation
- Validation - Basic valid/invalid/empty expression validation
- Error handling - Unknown profile returns error gracefully
-
No test for field relationship traversal
# Missing: Test that you can access related model fields # e.g., me.partner_id.parent_id.name def test_field_relationship_navigation(self): """Test that related fields are properly exposed.""" result = self.provider.get_symbols_for_profile("registry_individuals") me_var = next(v for v in result["variables"] if v["name"] == "me") # Find a many2one field m2o_fields = [f for f in me_var["fields"] if f["type"] == "many2one"] self.assertGreater(len(m2o_fields), 0, "Should have many2one fields") # Check that many2one field has a model attribute for traversal m2o_field = m2o_fields[0] self.assertIn("model", m2o_field, "many2one should expose related model")
-
No test for iterable field operations
# Missing: Test members variable in groups profile is properly iterable def test_iterable_variable_structure(self): """Test that iterable variables have correct metadata.""" result = self.provider.get_symbols_for_profile("registry_groups") members = next(v for v in result["variables"] if v["name"] == "members") self.assertTrue(members.get("iterable")) self.assertIn("element_type", members) # What type does iteration yield? self.assertIn("element_fields", members) # What fields on each element?
-
No test for function parameter validation
# Missing: Verify functions have correct parameter count and types def test_function_parameter_metadata(self): """Test that function parameters are well-documented.""" result = self.provider.get_symbols_for_profile("registry_individuals") age_years = next(f for f in result["functions"] if f["name"] == "age_years") self.assertEqual(len(age_years["params"]), 1) param = age_years["params"][0] self.assertIn("name", param) self.assertIn("type", param) self.assertIn("doc", param)
-
No test for complex expression validation
# Missing: Test validation of complex expressions with functions and operators def test_validate_complex_expression(self): """Test validation of expression with functions and operators.""" result = self.provider.validate_expression( 'age_years(me.birthdate) >= 18 and me.gender == "female"', "registry_individuals", ) self.assertTrue(result["valid"]) def test_validate_nested_function_calls(self): """Test validation of nested function calls.""" result = self.provider.validate_expression( 'exists(me.child_ids.filter(c, c.age < 5))', "registry_individuals", ) self.assertTrue(result["valid"])
-
Special characters in expressions
def test_validate_special_characters_in_strings(self): """Test that special characters in strings are handled.""" result = self.provider.validate_expression( r'me.name == "O\'Brien"', # Single quote in string "registry_individuals", ) self.assertTrue(result["valid"])
-
Whitespace handling
def test_validate_whitespace_expression(self): """Test that whitespace-only expression is invalid.""" result = self.provider.validate_expression(" \n\t ", "registry_individuals") self.assertFalse(result["valid"])
-
Very long expressions
def test_validate_long_expression(self): """Test handling of very long expressions.""" long_expr = " and ".join([f'me.field_{i} == true' for i in range(100)]) result = self.provider.validate_expression(long_expr, "registry_individuals") # Should either validate or return meaningful error, not crash self.assertIn("valid", result)
-
Field type edge cases
def test_field_types_complete(self): """Test that all Odoo field types are handled.""" result = self.provider.get_symbols_for_profile("registry_individuals") me_var = next(v for v in result["variables"] if v["name"] == "me") field_types = {f["type"] for f in me_var["fields"]} # Should handle all major types expected_types = {"char", "text", "boolean", "integer", "float", "date", "datetime", "many2one", "one2many", "many2many"} # At least some of these should be present self.assertTrue(field_types & expected_types, "Should have common field types")
-
Malformed expressions
def test_validate_malformed_parentheses(self): """Test validation of mismatched parentheses.""" result = self.provider.validate_expression( "age_years(me.birthdate >= 18", # Missing closing paren "registry_individuals", ) self.assertFalse(result["valid"]) self.assertTrue(any("parenthes" in e["message"].lower() for e in result["errors"])) def test_validate_invalid_operator(self): """Test validation of unknown operator.""" result = self.provider.validate_expression( "me.age <> 18", # Invalid operator "registry_individuals", ) self.assertFalse(result["valid"]) def test_validate_undefined_variable(self): """Test validation catches undefined variables.""" result = self.provider.validate_expression( "undefined_var.name == 'test'", "registry_individuals", ) self.assertFalse(result["valid"]) def test_validate_undefined_field(self): """Test validation catches undefined fields.""" result = self.provider.validate_expression( "me.nonexistent_field == true", "registry_individuals", ) self.assertFalse(result["valid"])
-
Profile edge cases
def test_get_symbols_empty_profile_name(self): """Test handling of empty profile name.""" result = self.provider.get_symbols_for_profile("") self.assertIn("error", result) def test_get_symbols_none_profile(self): """Test handling of None as profile.""" result = self.provider.get_symbols_for_profile(None) self.assertIn("error", result)
- Authentication - Tests that endpoints require auth
- Basic success cases - Happy path for all endpoints
- Error responses - Invalid expression returns proper error structure
- JSON structure - Verifies response format
-
No CSRF token validation test
def test_symbols_endpoint_csrf_protection(self): """Test that endpoints have CSRF protection.""" self.authenticate("admin", "admin") # Try without CSRF token response = self.url_open( "/spp_cel/symbols/registry_individuals", data=json.dumps({}), headers={ "Content-Type": "application/json", "X-CSRF-Token": "invalid", }, ) # Should be rejected self.assertEqual(response.status_code, 400)
-
No test for malformed JSON
def test_validate_endpoint_malformed_json(self): """Test handling of malformed JSON payload.""" self.authenticate("admin", "admin") response = self.url_open( "/spp_cel/validate", data="not valid json{", headers={"Content-Type": "application/json"}, ) # Should return error, not crash self.assertIn(response.status_code, [400, 500])
-
No test for missing parameters
def test_validate_endpoint_missing_expression(self): """Test validate endpoint without expression parameter.""" self.authenticate("admin", "admin") response = self.url_open( "/spp_cel/validate", data=json.dumps({ "params": { "profile": "registry_individuals", # Missing "expression" } }), headers={"Content-Type": "application/json"}, ) self.assertEqual(response.status_code, 400) def test_validate_endpoint_missing_profile(self): """Test validate endpoint without profile parameter.""" self.authenticate("admin", "admin") response = self.url_open( "/spp_cel/validate", data=json.dumps({ "params": { "expression": "me.name == 'test'", # Missing "profile" } }), headers={"Content-Type": "application/json"}, ) self.assertEqual(response.status_code, 400)
-
No test for rate limiting / abuse
def test_validate_endpoint_rate_limiting(self): """Test that rapid requests don't cause issues.""" self.authenticate("admin", "admin") # Make many rapid requests for i in range(20): response = self.url_open( "/spp_cel/validate", data=json.dumps({ "params": { "expression": f"me.field_{i} == true", "profile": "registry_individuals", } }), headers={"Content-Type": "application/json"}, ) self.assertEqual(response.status_code, 200)
-
No test for different HTTP methods
def test_symbols_endpoint_get_method_rejected(self): """Test that GET requests are rejected (should be POST).""" self.authenticate("admin", "admin") response = self.url_open( "/spp_cel/symbols/registry_individuals", method="GET", ) # Should reject GET self.assertNotEqual(response.status_code, 200)
-
No permission/group tests
def test_symbols_endpoint_permissions(self): """Test that appropriate user groups can access endpoints.""" # Create a user with limited permissions user = self.env["res.users"].create({ "name": "Test User", "login": "testuser", "password": "testuser", "groups_id": [(6, 0, [self.env.ref("base.group_user").id])], }) self.authenticate("testuser", "testuser") response = self.url_open( "/spp_cel/symbols/registry_individuals", data=json.dumps({}), headers={"Content-Type": "application/json"}, ) # Verify response based on expected permission model # Either 200 (allowed) or 403 (forbidden) self.assertIn(response.status_code, [200, 403])
-
Large payloads
def test_validate_endpoint_large_expression(self): """Test handling of very large expressions.""" self.authenticate("admin", "admin") # Create a very large but valid expression large_expr = " or ".join([f'me.name == "test{i}"' for i in range(1000)]) response = self.url_open( "/spp_cel/validate", data=json.dumps({ "params": { "expression": large_expr, "profile": "registry_individuals", } }), headers={"Content-Type": "application/json"}, ) # Should handle gracefully (validate or return error, not crash) self.assertIn(response.status_code, [200, 413]) # 413 = Payload Too Large
-
Unicode and special characters
def test_validate_endpoint_unicode_expression(self): """Test handling of unicode characters in expressions.""" self.authenticate("admin", "admin") response = self.url_open( "/spp_cel/validate", data=json.dumps({ "params": { "expression": 'me.name == "José García 中文"', "profile": "registry_individuals", } }), headers={"Content-Type": "application/json"}, ) self.assertEqual(response.status_code, 200) data = response.json() result = data.get("result", data) self.assertTrue(result["valid"])
-
Widget in actual form context
- No test that creates a record using the widget in a real form
- No test of widget interactions with Odoo's form view lifecycle
- No test of save/reload cycle preserving expression
-
Cross-module integration
- No test with spp_eligibility_cel integration
- No test with spp_entitlement_amount_cel
- No test with spp_compliance_cel
-
Performance tests
- No test of symbol loading time
- No test of validation performance
- No benchmark for autocomplete response time
-
Browser compatibility tests
- Tests don't cover actual browser rendering (need E2E for this)
/home/user/openspp-modules-v2/spp_cel_widget/static/tests/tours/cel_widget_tour.js
-
cel_widget_basic_rendering - Tests widget initialization and UI components
- Verifies toolbar appears
- Checks Symbols button exists
- Confirms autocomplete trigger button present
- Validates help text displays
-
cel_widget_autocomplete - Tests autocomplete functionality
- Types "me." to trigger autocomplete
- Waits for suggestion menu
- Selects a field from suggestions
- Verifies insertion into editor
-
cel_widget_validation_success - Tests valid expression validation
- Enters valid expression
me.name == "Test" - Waits for validation (debounced)
- Checks for success icon (fa-check-circle)
- Verifies success message "Valid"
- Enters valid expression
-
cel_widget_validation_error - Tests invalid expression handling
- Enters incomplete expression
me.name == - Waits for validation
- Checks for error icon (fa-times-circle)
- Verifies error message displayed
- Enters incomplete expression
-
cel_widget_symbol_browser - Tests symbol browser navigation
- Opens symbol browser
- Expands "me" variable
- Views and selects fields
- Switches to Functions tab
- Inserts function
- Closes browser
-
cel_widget_symbol_search - Tests search/filter functionality
- Opens browser
- Searches for "birth" in fields
- Verifies filtering works
- Searches in Functions tab
- Tests case-insensitive search
-
cel_widget_manual_autocomplete - Tests manual autocomplete trigger
- Types "me" (without dot)
- Clicks autocomplete button
- Verifies menu appears
-
cel_widget_readonly - Tests readonly mode
- Opens existing record in readonly
- Verifies editor has readonly class
- Ensures editing is disabled
-
cel_widget_empty_validation - Tests empty expression handling
- Verifies no validation on empty
- Types then clears expression
- Confirms validation clears
-
cel_widget_complex_expression - Tests complex expressions
- Enters
age_years(me.birthdate) >= 18 and me.gender == "female" - Validates successfully
- Verifies all parts rendered correctly
- Enters
# Run a specific tour
odoo-bin -c odoo.conf -d test_db --test-enable --test-tags=/cel_widget_basic_rendering --stop-after-init
# Run all CEL widget tours
odoo-bin -c odoo.conf -d test_db --test-enable --test-tags=/cel_widget --stop-after-init
# Run in headless mode with Chrome
odoo-bin -c odoo.conf -d test_db --test-enable --test-tags=/cel_widget --stop-after-init --screencastsPriority: HIGH Create
/home/user/openspp-modules-v2/spp_cel_widget/tests/test_symbol_provider_extended.py:
"""Extended tests for CEL Symbol Provider covering edge cases."""
from odoo.tests.common import TransactionCase
class TestCelSymbolProviderExtended(TransactionCase):
"""Extended test cases for edge cases and complex scenarios."""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.provider = cls.env["spp.cel.symbol.provider"]
def test_complex_expression_with_functions(self):
"""Test validation of expression with multiple functions."""
result = self.provider.validate_expression(
'age_years(me.birthdate) >= 18 and me.gender == "female"',
"registry_individuals",
)
self.assertTrue(result["valid"])
def test_undefined_field_validation(self):
"""Test that undefined fields are caught."""
result = self.provider.validate_expression(
"me.nonexistent_field_xyz == true",
"registry_individuals",
)
self.assertFalse(result["valid"])
self.assertGreater(len(result["errors"]), 0)
def test_malformed_expression_parentheses(self):
"""Test mismatched parentheses are detected."""
result = self.provider.validate_expression(
"age_years(me.birthdate >= 18",
"registry_individuals",
)
self.assertFalse(result["valid"])
def test_whitespace_only_expression(self):
"""Test whitespace-only expression is invalid."""
result = self.provider.validate_expression(
" \n\t ",
"registry_individuals",
)
self.assertFalse(result["valid"])
def test_special_characters_in_strings(self):
"""Test special characters in string literals."""
result = self.provider.validate_expression(
r'me.name == "O\'Brien"',
"registry_individuals",
)
# Should either validate or handle gracefully
self.assertIn("valid", result)
def test_unicode_in_expressions(self):
"""Test unicode characters are handled."""
result = self.provider.validate_expression(
'me.name == "José García 中文"',
"registry_individuals",
)
# Should handle unicode without crashing
self.assertIn("valid", result)
def test_empty_profile_name(self):
"""Test empty profile name returns error."""
result = self.provider.get_symbols_for_profile("")
self.assertIn("error", result)
def test_function_parameter_metadata(self):
"""Test that functions have proper parameter metadata."""
result = self.provider.get_symbols_for_profile("registry_individuals")
age_years = next(
(f for f in result["functions"] if f["name"] == "age_years"),
None
)
self.assertIsNotNone(age_years, "age_years function should exist")
self.assertIn("params", age_years)
self.assertGreater(len(age_years["params"]), 0)
param = age_years["params"][0]
self.assertIn("name", param)
self.assertIn("type", param)
def test_many2one_field_metadata(self):
"""Test that many2one fields expose related model info."""
result = self.provider.get_symbols_for_profile("registry_individuals")
me_var = next(v for v in result["variables"] if v["name"] == "me")
m2o_fields = [f for f in me_var["fields"] if f.get("type") == "many2one"]
if m2o_fields:
# If there are m2o fields, they should have model info
m2o_field = m2o_fields[0]
self.assertIn("model", m2o_field)
def test_iterable_variable_metadata(self):
"""Test that iterable variables (like members) have proper metadata."""
result = self.provider.get_symbols_for_profile("registry_groups")
members = next(
(v for v in result["variables"] if v["name"] == "members"),
None
)
if members:
self.assertTrue(members.get("iterable"))Priority: MEDIUM Create
/home/user/openspp-modules-v2/spp_cel_widget/tests/test_controller_extended.py:
"""Extended tests for CEL Widget HTTP Controller."""
import json
from odoo.tests.common import HttpCase
class TestCelWidgetControllerExtended(HttpCase):
"""Extended test cases for controller edge cases."""
def test_validate_missing_expression_param(self):
"""Test validation endpoint with missing expression."""
self.authenticate("admin", "admin")
response = self.url_open(
"/spp_cel/validate",
data=json.dumps({
"params": {
"profile": "registry_individuals",
# Missing "expression"
}
}),
headers={"Content-Type": "application/json"},
)
# Should return error
self.assertIn(response.status_code, [400, 500])
def test_validate_missing_profile_param(self):
"""Test validation endpoint with missing profile."""
self.authenticate("admin", "admin")
response = self.url_open(
"/spp_cel/validate",
data=json.dumps({
"params": {
"expression": "me.name == 'test'",
# Missing "profile"
}
}),
headers={"Content-Type": "application/json"},
)
# Should return error
self.assertIn(response.status_code, [400, 500])
def test_symbols_invalid_profile(self):
"""Test symbols endpoint with invalid profile."""
self.authenticate("admin", "admin")
response = self.url_open(
"/spp_cel/symbols/invalid_profile_xyz",
data=json.dumps({}),
headers={"Content-Type": "application/json"},
)
self.assertEqual(response.status_code, 200)
data = response.json()
result = data.get("result", data)
# Should return error in result
self.assertIn("error", result)
def test_validate_unicode_expression(self):
"""Test validation with unicode characters."""
self.authenticate("admin", "admin")
response = self.url_open(
"/spp_cel/validate",
data=json.dumps({
"params": {
"expression": 'me.name == "José García 中文"',
"profile": "registry_individuals",
}
}),
headers={"Content-Type": "application/json"},
)
self.assertEqual(response.status_code, 200)
def test_validate_very_long_expression(self):
"""Test handling of very long expressions."""
self.authenticate("admin", "admin")
# Create large expression
large_expr = " or ".join([f'me.name == "test{i}"' for i in range(100)])
response = self.url_open(
"/spp_cel/validate",
data=json.dumps({
"params": {
"expression": large_expr,
"profile": "registry_individuals",
}
}),
headers={"Content-Type": "application/json"},
)
# Should handle gracefully (not timeout or crash)
self.assertIn(response.status_code, [200, 413, 500])
def test_concurrent_validation_requests(self):
"""Test that concurrent requests don't cause issues."""
self.authenticate("admin", "admin")
# Make several rapid requests
for i in range(10):
response = self.url_open(
"/spp_cel/validate",
data=json.dumps({
"params": {
"expression": f'me.field_{i} == true',
"profile": "registry_individuals",
}
}),
headers={"Content-Type": "application/json"},
)
self.assertEqual(response.status_code, 200)Priority: MEDIUM Create
/home/user/openspp-modules-v2/spp_cel_widget/tests/test_widget_integration.py:
"""Integration tests for CEL widget in form views."""
from odoo.tests.common import TransactionCase
class TestCelWidgetIntegration(TransactionCase):
"""Test CEL widget integration with Odoo models."""
def test_widget_with_eligibility_manager(self):
"""Test CEL widget in eligibility manager form."""
# Create eligibility manager with CEL expression
manager = self.env["spp.program.membership.manager.default"].create({
"name": "Test CEL Manager",
"eligibility_mode": "cel",
"cel_expression": 'age_years(me.birthdate) >= 18',
})
self.assertEqual(manager.eligibility_mode, "cel")
self.assertEqual(manager.cel_expression, 'age_years(me.birthdate) >= 18')
def test_widget_expression_persistence(self):
"""Test that expressions are properly saved and loaded."""
manager = self.env["spp.program.membership.manager.default"].create({
"name": "Test Persistence",
"eligibility_mode": "cel",
"cel_expression": 'me.name == "Test" and me.active == true',
})
# Reload from database
manager.invalidate_recordset()
manager_reloaded = self.env["spp.program.membership.manager.default"].browse(manager.id)
self.assertEqual(
manager_reloaded.cel_expression,
'me.name == "Test" and me.active == true'
)
def test_widget_empty_expression(self):
"""Test widget handles empty expressions correctly."""
manager = self.env["spp.program.membership.manager.default"].create({
"name": "Test Empty",
"eligibility_mode": "cel",
"cel_expression": "",
})
self.assertEqual(manager.cel_expression, "")
# Should not crash or cause validation errorsPriority: LOW Create
/home/user/openspp-modules-v2/spp_cel_widget/tests/test_performance.py:
"""Performance tests for CEL widget operations."""
import time
from odoo.tests.common import TransactionCase
class TestCelWidgetPerformance(TransactionCase):
"""Performance benchmarks for CEL widget."""
def setUp(self):
super().setUp()
self.provider = self.env["spp.cel.symbol.provider"]
def test_symbol_loading_performance(self):
"""Test that symbol loading completes in reasonable time."""
start_time = time.time()
result = self.provider.get_symbols_for_profile("registry_individuals")
elapsed = time.time() - start_time
# Should complete in less than 2 seconds
self.assertLess(elapsed, 2.0, f"Symbol loading took {elapsed:.2f}s")
self.assertGreater(len(result["variables"]), 0)
def test_validation_performance(self):
"""Test validation completes in reasonable time."""
expression = 'age_years(me.birthdate) >= 18 and me.gender == "female"'
start_time = time.time()
result = self.provider.validate_expression(expression, "registry_individuals")
elapsed = time.time() - start_time
# Should complete in less than 1 second
self.assertLess(elapsed, 1.0, f"Validation took {elapsed:.2f}s")
self.assertIn("valid", result)
def test_multiple_validations_performance(self):
"""Test performance of multiple sequential validations."""
expressions = [
'me.name == "Test"',
'age_years(me.birthdate) >= 18',
'me.active == true',
'me.gender == "female"',
'age_years(me.birthdate) < 65',
]
start_time = time.time()
for expr in expressions:
self.provider.validate_expression(expr, "registry_individuals")
elapsed = time.time() - start_time
# Should complete all in less than 3 seconds
self.assertLess(elapsed, 3.0, f"Multiple validations took {elapsed:.2f}s")Strengths:
- Good coverage of happy paths
- Proper use of Odoo test base classes
- Clear test names and documentation
- Both unit and HTTP tests present
Weaknesses:
- Missing edge case coverage
- No negative test cases for malformed input
- No integration tests with actual form views
- No performance benchmarks
- Limited error path testing
-
CRITICAL (Do First)
- Add edge case tests for malformed expressions (parentheses, operators)
- Add tests for undefined fields/variables
- Add controller tests for missing parameters
- Add tests for special characters and unicode
-
HIGH (Do Soon)
- Add integration tests with actual models
- Add readonly mode tests
- Add field relationship traversal tests
- Extend E2E tours with error recovery scenarios
-
MEDIUM (Do When Time Permits)
- Add performance benchmarks
- Add concurrency tests
- Add large payload tests
- Add cross-module integration tests
-
LOW (Nice to Have)
- Add browser compatibility tests
- Add accessibility tests
- Add load testing
- Add security penetration tests
The 10 E2E tours provide comprehensive coverage of:
- Widget initialization and rendering
- User interactions (typing, clicking, navigation)
- Autocomplete flow
- Validation feedback (success and error)
- Symbol browser functionality
- Search and filtering
- Readonly mode
- Complex expressions
These tours can be run with:
./scripts/test_single_module.sh spp_cel_widgetOr individually:
odoo-bin --test-enable --test-tags=/cel_widget_basic_rendering --stop-after-init-
Created:
/home/user/openspp-modules-v2/spp_cel_widget/static/tests/tours/cel_widget_tour.js- 10 comprehensive E2E tour tests
- ~650 lines of test code
- Covers all major user flows
-
Modified:
/home/user/openspp-modules-v2/spp_cel_widget/__manifest__.py- Added
web.assets_testsbundle - Included tour file in test assets
- Added
-
Created:
/home/user/openspp-modules-v2/spp_cel_widget/TEST_RECOMMENDATIONS.md- This comprehensive test review document