import numpy as np import pytest from sigverify.evaluation.metrics import verification_metrics from sigverify.features import extract_features, global_features from sigverify.io.schema import Signature from sigverify.io.loaders import load_mobisig from sigverify.matching.dtw import dtw_distance from sigverify.matching.models import DTWVerifier from sigverify.preprocessing import preprocess_signature from sigverify.security import AttemptLockout, TemplateCipher def sample(label="genuine", scale=1.0): t = np.array([2.0, 0.0, 1.0, 1.0, 3.0]) x = scale * np.array([2.0, 0.0, 1.0, 99.0, 3.0]) y = scale * np.array([0.0, 0.0, 1.0, 99.0, 1.0]) return Signature("person-1", label, t, x, y, p=np.ones(5)) def test_preprocessing_sorts_deduplicates_and_retains_original_size(): cleaned = preprocess_signature(sample(), points=20, smoothing_sigma=0) assert len(cleaned.t) == 20 assert np.all(np.diff(cleaned.t) > 0) assert cleaned.size_normalized assert cleaned.original_size == pytest.approx(np.hypot(3, 1)) assert np.max(np.abs(cleaned.x)) == pytest.approx(0.5) assert cleaned.p is not None and len(cleaned.p) == 20 def test_pen_up_points_are_removed_before_resampling(): raw = Signature( "person-1", "genuine", [0, 1, 2], [0, 100, 2], [0, 100, 2], pen_down=[True, False, True], ) cleaned = preprocess_signature(raw, points=3, smoothing_sigma=0, normalize_position=False) assert cleaned.x.tolist() == pytest.approx([0, 1, 2]) assert cleaned.pen_down is not None and cleaned.pen_down.all() def test_features_support_optional_pressure_and_global_descriptors(): clean = preprocess_signature(sample(), points=10, smoothing_sigma=0) base = extract_features(clean, include_acceleration=False, include_curvature=False) with_pressure = extract_features( clean, include_pressure=True, include_acceleration=False, include_curvature=False ) assert base.shape == (10, 5) assert with_pressure.shape == (10, 6) assert global_features(clean)["original_size"] > 0 def test_dtw_is_zero_for_identical_sequences_and_handles_band(): sequence = np.array([[0.0], [1.0], [2.0]]) assert dtw_distance(sequence, sequence) == 0 assert dtw_distance(sequence, sequence[::-1]) > 0 assert np.isfinite(dtw_distance(sequence, sequence[:2])) def test_dtw_verifier_enrollment_and_normalized_score(): first = preprocess_signature(sample(), points=20, smoothing_sigma=0) second = preprocess_signature(sample(scale=1.02), points=20, smoothing_sigma=0) verifier = DTWVerifier() verifier.enroll([first, second]) assert verifier.score(first) < 0.1 with pytest.raises(ValueError, match="genuine"): verifier.enroll([first, first.copy(label="forgery")]) def test_metrics_detect_perfect_separation(): result = verification_metrics([1, 1, 0, 0], [0.1, 0.2, 0.8, 0.9]) assert result["eer"] == 0 def test_template_cipher_round_trip_and_lockout(): from cryptography.fernet import Fernet signature = preprocess_signature(sample(), points=8) cipher = TemplateCipher(Fernet.generate_key()) restored = cipher.decrypt(cipher.encrypt(signature)) np.testing.assert_allclose(restored.x, signature.x) lockout = AttemptLockout(max_failures=2, lockout_seconds=1) lockout.record_failure("person-1") assert lockout.remaining_lockout("person-1") == 0 lockout.record_failure("person-1") assert lockout.remaining_lockout("person-1") > 0 def test_mobisig_loader_maps_columns_and_filename_metadata(tmp_path): user_dir = tmp_path / "USER1" user_dir.mkdir() (user_dir / "SIGN_GEN_USER1_USER1_1.csv").write_text( "x,y,timestamp,pressure,fingerarea\n1,2,10,0.5,3\n2,4,20,0.6,4\n", encoding="utf-8", ) (user_dir / "SIGN_FOR_USER1_USER2_1.csv").write_text( "x,y,timestamp,pressure,fingerarea\n1,2,10,0.5,3\n2,3,20,0.6,4\n", encoding="utf-8", ) signatures = load_mobisig(tmp_path) assert [(item.user_id, item.label) for item in signatures] == [ ("USER1", "forgery"), ("USER1", "genuine"), ] assert signatures[0].p is not None and signatures[0].area is not None