fix: harden admin access, repair ORM joins, and add migration/tests
This commit is contained in:
12
tests/test_model_mappings.py
Normal file
12
tests/test_model_mappings.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from sqlalchemy.orm import configure_mappers
|
||||
|
||||
|
||||
def test_sqlalchemy_mappers_configure_without_join_errors():
|
||||
"""
|
||||
Ensure relationship joins are fully resolvable.
|
||||
|
||||
This catches missing FK/primaryjoin regressions early.
|
||||
"""
|
||||
import app.models # noqa: F401
|
||||
|
||||
configure_mappers()
|
||||
@@ -1,275 +1,77 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for normalization calculations.
|
||||
|
||||
This script tests the normalization functions to ensure they work correctly
|
||||
without requiring database connections.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Add the project root to the path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||
import pytest
|
||||
|
||||
# Ensure project root is importable when tests run in isolated environments.
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
from app.services.normalization import apply_normalization
|
||||
|
||||
|
||||
def test_apply_normalization():
|
||||
"""Test the apply_normalization function."""
|
||||
print("Testing apply_normalization function...")
|
||||
print("=" * 60)
|
||||
|
||||
# Test case 1: Normal normalization (NM=500, rataan=500, sb=100)
|
||||
nm1 = 500
|
||||
rataan1 = 500
|
||||
sb1 = 100
|
||||
nn1 = apply_normalization(nm1, rataan1, sb1)
|
||||
expected1 = 500
|
||||
print(f"Test 1: NM={nm1}, rataan={rataan1}, sb={sb1}")
|
||||
print(f" Expected NN: {expected1}")
|
||||
print(f" Actual NN: {nn1}")
|
||||
print(f" Status: {'PASS' if nn1 == expected1 else 'FAIL'}")
|
||||
print()
|
||||
|
||||
# Test case 2: High score (NM=600, rataan=500, sb=100)
|
||||
nm2 = 600
|
||||
rataan2 = 500
|
||||
sb2 = 100
|
||||
nn2 = apply_normalization(nm2, rataan2, sb2)
|
||||
expected2 = 600
|
||||
print(f"Test 2: NM={nm2}, rataan={rataan2}, sb={sb2}")
|
||||
print(f" Expected NN: {expected2}")
|
||||
print(f" Actual NN: {nn2}")
|
||||
print(f" Status: {'PASS' if nn2 == expected2 else 'FAIL'}")
|
||||
print()
|
||||
|
||||
# Test case 3: Low score (NM=400, rataan=500, sb=100)
|
||||
nm3 = 400
|
||||
rataan3 = 500
|
||||
sb3 = 100
|
||||
nn3 = apply_normalization(nm3, rataan3, sb3)
|
||||
expected3 = 400
|
||||
print(f"Test 3: NM={nm3}, rataan={rataan3}, sb={sb3}")
|
||||
print(f" Expected NN: {expected3}")
|
||||
print(f" Actual NN: {nn3}")
|
||||
print(f" Status: {'PASS' if nn3 == expected3 else 'FAIL'}")
|
||||
print()
|
||||
|
||||
# Test case 4: Edge case - maximum NM
|
||||
nm4 = 1000
|
||||
rataan4 = 500
|
||||
sb4 = 100
|
||||
nn4 = apply_normalization(nm4, rataan4, sb4)
|
||||
expected4 = 1000
|
||||
print(f"Test 4: NM={nm4}, rataan={rataan4}, sb={sb4}")
|
||||
print(f" Expected NN: {expected4}")
|
||||
print(f" Actual NN: {nn4}")
|
||||
print(f" Status: {'PASS' if nn4 == expected4 else 'FAIL'}")
|
||||
print()
|
||||
|
||||
# Test case 5: Edge case - minimum NM
|
||||
nm5 = 0
|
||||
rataan5 = 500
|
||||
sb5 = 100
|
||||
nn5 = apply_normalization(nm5, rataan5, sb5)
|
||||
expected5 = 0
|
||||
print(f"Test 5: NM={nm5}, rataan={rataan5}, sb={sb5}")
|
||||
print(f" Expected NN: {expected5}")
|
||||
print(f" Actual NN: {nn5}")
|
||||
print(f" Status: {'PASS' if nn5 == expected5 else 'FAIL'}")
|
||||
print()
|
||||
|
||||
# Test case 6: Error case - invalid NM (above max)
|
||||
try:
|
||||
nm6 = 1200 # Above valid range
|
||||
rataan6 = 500
|
||||
sb6 = 100
|
||||
nn6 = apply_normalization(nm6, rataan6, sb6)
|
||||
print(f"Test 6: NM={nm6}, rataan={rataan6}, sb={sb6} (should raise ValueError)")
|
||||
print(f" Status: FAIL - Should have raised ValueError")
|
||||
except ValueError as e:
|
||||
print(f"Test 6: NM={nm6}, rataan={rataan6}, sb={sb6} (should raise ValueError)")
|
||||
print(f" Error: {e}")
|
||||
print(f" Status: PASS - Correctly raised ValueError")
|
||||
print()
|
||||
|
||||
# Test case 7: Error case - invalid NM (below min)
|
||||
try:
|
||||
nm7 = -100 # Below valid range
|
||||
rataan7 = 500
|
||||
sb7 = 100
|
||||
nn7 = apply_normalization(nm7, rataan7, sb7)
|
||||
print(f"Test 7: NM={nm7}, rataan={rataan7}, sb={sb7} (should raise ValueError)")
|
||||
print(f" Status: FAIL - Should have raised ValueError")
|
||||
except ValueError as e:
|
||||
print(f"Test 7: NM={nm7}, rataan={rataan7}, sb={sb7} (should raise ValueError)")
|
||||
print(f" Error: {e}")
|
||||
print(f" Status: PASS - Correctly raised ValueError")
|
||||
print()
|
||||
|
||||
# Test case 8: Different rataan/sb (NM=500, rataan=600, sb=80)
|
||||
nm8 = 500
|
||||
rataan8 = 600
|
||||
sb8 = 80
|
||||
nn8 = apply_normalization(nm8, rataan8, sb8)
|
||||
# z_score = (500 - 600) / 80 = -1.25
|
||||
# nn = 500 + 100 * (-1.25) = 500 - 125 = 375
|
||||
expected8 = 375
|
||||
print(f"Test 8: NM={nm8}, rataan={rataan8}, sb={sb8}")
|
||||
print(f" Expected NN: {expected8}")
|
||||
print(f" Actual NN: {nn8}")
|
||||
print(f" Status: {'PASS' if nn8 == expected8 else 'FAIL'}")
|
||||
print()
|
||||
|
||||
# Test case 9: Error case - invalid NM
|
||||
try:
|
||||
nm9 = 1500 # Above valid range
|
||||
rataan9 = 500
|
||||
sb9 = 100
|
||||
nn9 = apply_normalization(nm9, rataan9, sb9)
|
||||
print(f"Test 9: NM={nm9}, rataan={rataan9}, sb={sb9} (should raise ValueError)")
|
||||
print(f" Status: FAIL - Should have raised ValueError")
|
||||
except ValueError as e:
|
||||
print(f"Test 9: NM=1500, rataan=500, sb=100 (should raise ValueError)")
|
||||
print(f" Error: {e}")
|
||||
print(f" Status: PASS - Correctly raised ValueError")
|
||||
print()
|
||||
|
||||
# Test case 10: Error case - invalid sb
|
||||
try:
|
||||
nm10 = 500
|
||||
rataan10 = 500
|
||||
sb10 = 0 # Invalid SD
|
||||
nn10 = apply_normalization(nm10, rataan10, sb10)
|
||||
expected10 = 500 # Should return default when sb <= 0
|
||||
print(f"Test 10: NM={nm10}, rataan={rataan10}, sb={sb10} (should return default)")
|
||||
print(f" Expected NN: {expected10}")
|
||||
print(f" Actual NN: {nn10}")
|
||||
print(f" Status: {'PASS' if nn10 == expected10 else 'FAIL'}")
|
||||
except Exception as e:
|
||||
print(f"Test 10: NM=500, rataan=500, sb=0 (should return default)")
|
||||
print(f" Error: {e}")
|
||||
print(f" Status: FAIL - Should have returned default value")
|
||||
print()
|
||||
|
||||
print("=" * 60)
|
||||
print("All tests completed!")
|
||||
print("=" * 60)
|
||||
@pytest.mark.parametrize(
|
||||
("nm", "rataan", "sb", "expected"),
|
||||
[
|
||||
(500, 500, 100, 500),
|
||||
(600, 500, 100, 600),
|
||||
(400, 500, 100, 400),
|
||||
(1000, 500, 100, 1000),
|
||||
(0, 500, 100, 0),
|
||||
(500, 600, 80, 375),
|
||||
],
|
||||
)
|
||||
def test_apply_normalization_nominal_cases(nm: int, rataan: float, sb: float, expected: int):
|
||||
assert apply_normalization(nm, rataan, sb) == expected
|
||||
|
||||
|
||||
def calculate_dynamic_mean_and_std(nm_values):
|
||||
"""
|
||||
Calculate mean and standard deviation from a list of NM values.
|
||||
This simulates what update_dynamic_normalization does.
|
||||
"""
|
||||
n = len(nm_values)
|
||||
if n == 0:
|
||||
return None, None
|
||||
|
||||
# Calculate mean
|
||||
mean = sum(nm_values) / n
|
||||
|
||||
# Calculate variance (population variance)
|
||||
if n > 1:
|
||||
variance = sum((x - mean) ** 2 for x in nm_values) / n
|
||||
std = variance ** 0.5
|
||||
else:
|
||||
std = 0.0
|
||||
|
||||
return mean, std
|
||||
@pytest.mark.parametrize("nm", [-1, 1001, 1500, -100])
|
||||
def test_apply_normalization_rejects_invalid_nm(nm: int):
|
||||
with pytest.raises(ValueError):
|
||||
apply_normalization(nm, 500, 100)
|
||||
|
||||
|
||||
def test_dynamic_normalization_simulation():
|
||||
"""Test dynamic normalization with simulated participant scores."""
|
||||
print("\nTesting dynamic normalization simulation...")
|
||||
print("=" * 60)
|
||||
@pytest.mark.parametrize("sb", [0, -1, -100.0])
|
||||
def test_apply_normalization_returns_default_when_sd_non_positive(sb: float):
|
||||
assert apply_normalization(500, 500, sb) == 500
|
||||
|
||||
# Simulate 10 participant NM scores
|
||||
|
||||
def test_dynamic_normalization_distribution_behaves_as_expected():
|
||||
nm_scores = [450, 480, 500, 520, 550, 480, 510, 490, 530, 470]
|
||||
print(f"Simulated NM scores: {nm_scores}")
|
||||
print()
|
||||
|
||||
# Calculate mean and SD
|
||||
mean, std = calculate_dynamic_mean_and_std(nm_scores)
|
||||
print(f"Calculated mean (rataan): {mean:.2f}")
|
||||
print(f"Calculated SD (sb): {std:.2f}")
|
||||
print()
|
||||
mean = sum(nm_scores) / len(nm_scores)
|
||||
variance = sum((x - mean) ** 2 for x in nm_scores) / len(nm_scores)
|
||||
std = math.sqrt(variance)
|
||||
|
||||
# Normalize each score
|
||||
print("Normalized scores:")
|
||||
for i, nm in enumerate(nm_scores):
|
||||
nn = apply_normalization(nm, mean, std)
|
||||
print(f" Participant {i+1}: NM={nm:3d} -> NN={nn:3d}")
|
||||
print()
|
||||
|
||||
# Check if normalized distribution is close to mean=500, SD=100
|
||||
nn_scores = [apply_normalization(nm, mean, std) for nm in nm_scores]
|
||||
nn_mean, nn_std = calculate_dynamic_mean_and_std(nn_scores)
|
||||
nn_mean = sum(nn_scores) / len(nn_scores)
|
||||
nn_variance = sum((x - nn_mean) ** 2 for x in nn_scores) / len(nn_scores)
|
||||
nn_std = math.sqrt(nn_variance)
|
||||
|
||||
print(f"Normalized distribution:")
|
||||
print(f" Mean: {nn_mean:.2f} (target: 500 ± 5)")
|
||||
print(f" SD: {nn_std:.2f} (target: 100 ± 5)")
|
||||
print(f" Status: {'PASS' if abs(nn_mean - 500) <= 5 and abs(nn_std - 100) <= 5 else 'NEAR PASS'}")
|
||||
print()
|
||||
|
||||
print("=" * 60)
|
||||
# Rounding in apply_normalization introduces small drift; these bounds are tight.
|
||||
assert abs(nn_mean - 500) <= 5
|
||||
assert abs(nn_std - 100) <= 5
|
||||
|
||||
|
||||
def test_incremental_update():
|
||||
"""Test incremental update of dynamic normalization."""
|
||||
print("\nTesting incremental update simulation...")
|
||||
print("=" * 60)
|
||||
def test_incremental_population_stats_match_batch_stats():
|
||||
scores = [500, 550, 450, 600, 400]
|
||||
|
||||
# Simulate adding scores incrementally
|
||||
nm_scores = []
|
||||
participant_count = 0
|
||||
total_nm_sum = 0.0
|
||||
total_nm_sq_sum = 0.0
|
||||
|
||||
new_scores = [500, 550, 450, 600, 400]
|
||||
|
||||
for i, nm in enumerate(new_scores):
|
||||
# Update running statistics
|
||||
for score in scores:
|
||||
participant_count += 1
|
||||
total_nm_sum += nm
|
||||
total_nm_sq_sum += nm * nm
|
||||
total_nm_sum += score
|
||||
total_nm_sq_sum += score * score
|
||||
|
||||
# Calculate mean and SD
|
||||
mean = total_nm_sum / participant_count
|
||||
if participant_count > 1:
|
||||
variance = (total_nm_sq_sum / participant_count) - (mean ** 2)
|
||||
std = variance ** 0.5
|
||||
else:
|
||||
std = 0.0
|
||||
incremental_mean = total_nm_sum / participant_count
|
||||
incremental_variance = (total_nm_sq_sum / participant_count) - (incremental_mean**2)
|
||||
incremental_std = math.sqrt(max(0.0, incremental_variance))
|
||||
|
||||
nm_scores.append(nm)
|
||||
batch_mean = sum(scores) / len(scores)
|
||||
batch_variance = sum((x - batch_mean) ** 2 for x in scores) / len(scores)
|
||||
batch_std = math.sqrt(batch_variance)
|
||||
|
||||
print(f"After adding participant {i+1}:")
|
||||
print(f" NM: {nm}")
|
||||
print(f" Participant count: {participant_count}")
|
||||
print(f" Mean (rataan): {mean:.2f}")
|
||||
print(f" SD (sb): {std:.2f}")
|
||||
print()
|
||||
|
||||
# Final calculation
|
||||
final_mean, final_std = calculate_dynamic_mean_and_std(nm_scores)
|
||||
print(f"Final statistics:")
|
||||
print(f" All scores: {nm_scores}")
|
||||
print(f" Mean: {final_mean:.2f}")
|
||||
print(f" SD: {final_std:.2f}")
|
||||
print()
|
||||
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Normalization Calculation Tests")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
test_apply_normalization()
|
||||
test_dynamic_normalization_simulation()
|
||||
test_incremental_update()
|
||||
|
||||
print("\nAll test simulations completed successfully!")
|
||||
assert incremental_mean == pytest.approx(batch_mean, rel=0, abs=1e-10)
|
||||
assert incremental_std == pytest.approx(batch_std, rel=0, abs=1e-10)
|
||||
|
||||
Reference in New Issue
Block a user