""" Unit tests for calc.py helper functions. These tests verify pure calculation logic without hitting the database or the Swiss Ephemeris — all inputs are fixed synthetic data. Run: pyswiss/.venv/Scripts/python pyswiss/manage.py test pyswiss/apps/charts """ from django.test import SimpleTestCase from apps.charts.calc import calculate_aspects # --------------------------------------------------------------------------- # Synthetic planet data — degrees chosen for predictable aspects # Matches FAKE_PLANETS in test_populate_ephemeris.py # --------------------------------------------------------------------------- FAKE_PLANETS = { 'Sun': {'degree': 10.0}, # Aries 'Moon': {'degree': 130.0}, # Leo — 120° from Sun → Trine 'Mercury': {'degree': 250.0}, # Sagittarius — 120° from Sun → Trine 'Venus': {'degree': 40.0}, # Taurus — 90° from Moon → Square 'Mars': {'degree': 160.0}, # Virgo — 60° from Neptune → Sextile 'Jupiter': {'degree': 280.0}, # Capricorn — 120° from Mars → Trine 'Saturn': {'degree': 70.0}, # Gemini — 120° from Uranus → Trine 'Uranus': {'degree': 310.0}, # Aquarius — 60° from Sun (wrap) → Sextile 'Neptune': {'degree': 100.0}, # Cancer 'Pluto': {'degree': 340.0}, # Pisces } def _aspect_pairs(aspects): """Return a set of (planet1, planet2, type) tuples for easy assertion.""" return {(a['planet1'], a['planet2'], a['type']) for a in aspects} class CalculateAspectsTest(SimpleTestCase): def setUp(self): self.aspects = calculate_aspects(FAKE_PLANETS) # ── return shape ────────────────────────────────────────────────────── def test_returns_a_list(self): self.assertIsInstance(self.aspects, list) def test_each_aspect_has_required_keys(self): for aspect in self.aspects: with self.subTest(aspect=aspect): self.assertIn('planet1', aspect) self.assertIn('planet2', aspect) self.assertIn('type', aspect) self.assertIn('angle', aspect) self.assertIn('orb', aspect) def test_each_aspect_type_is_a_known_name(self): known = {'Conjunction', 'Sextile', 'Square', 'Trine', 'Opposition'} for aspect in self.aspects: with self.subTest(aspect=aspect): self.assertIn(aspect['type'], known) def test_angle_and_orb_are_floats(self): for aspect in self.aspects: with self.subTest(aspect=aspect): self.assertIsInstance(aspect['angle'], float) self.assertIsInstance(aspect['orb'], float) def test_no_self_aspects(self): for aspect in self.aspects: self.assertNotEqual(aspect['planet1'], aspect['planet2']) def test_no_duplicate_pairs(self): pairs = [(a['planet1'], a['planet2']) for a in self.aspects] self.assertEqual(len(pairs), len(set(pairs))) # ── known aspects in FAKE_PLANETS ──────────────────────────────────── def test_sun_moon_trine(self): """Moon at 130° is exactly 120° from Sun at 10°.""" pairs = _aspect_pairs(self.aspects) self.assertIn(('Sun', 'Moon', 'Trine'), pairs) def test_sun_mercury_trine(self): """Mercury at 250° wraps to 120° from Sun at 10° (360-250+10=120).""" pairs = _aspect_pairs(self.aspects) self.assertIn(('Sun', 'Mercury', 'Trine'), pairs) def test_moon_mercury_trine(self): """Moon 130° → Mercury 250° = 120°.""" pairs = _aspect_pairs(self.aspects) self.assertIn(('Moon', 'Mercury', 'Trine'), pairs) def test_moon_venus_square(self): """Moon 130° → Venus 40° = 90°.""" pairs = _aspect_pairs(self.aspects) self.assertIn(('Moon', 'Venus', 'Square'), pairs) def test_venus_neptune_sextile(self): """Venus 40° → Neptune 100° = 60°.""" pairs = _aspect_pairs(self.aspects) self.assertIn(('Venus', 'Neptune', 'Sextile'), pairs) def test_mars_neptune_sextile(self): """Mars 160° → Neptune 100° = 60°.""" pairs = _aspect_pairs(self.aspects) self.assertIn(('Mars', 'Neptune', 'Sextile'), pairs) def test_sun_uranus_sextile(self): """Sun 10° → Uranus 310° — angle = |10-310| = 300° → 360-300 = 60°.""" pairs = _aspect_pairs(self.aspects) self.assertIn(('Sun', 'Uranus', 'Sextile'), pairs) def test_mars_jupiter_trine(self): """Mars 160° → Jupiter 280° = 120°.""" pairs = _aspect_pairs(self.aspects) self.assertIn(('Mars', 'Jupiter', 'Trine'), pairs) def test_saturn_uranus_trine(self): """Saturn 70° → Uranus 310° = |70-310| = 240° → 360-240 = 120°.""" pairs = _aspect_pairs(self.aspects) self.assertIn(('Saturn', 'Uranus', 'Trine'), pairs) # ── orb bounds ──────────────────────────────────────────────────────── def test_orb_is_within_allowed_maximum(self): max_orbs = { 'Conjunction': 8.0, 'Sextile': 6.0, 'Square': 8.0, 'Trine': 8.0, 'Opposition': 10.0, } for aspect in self.aspects: with self.subTest(aspect=aspect): self.assertLessEqual( aspect['orb'], max_orbs[aspect['type']], msg=f"{aspect['planet1']}-{aspect['planet2']} orb exceeds maximum", ) def test_exact_trine_has_zero_orb(self): """Sun-Moon at exactly 120° should report orb of 0.0.""" sun_moon = next( a for a in self.aspects if a['planet1'] == 'Sun' and a['planet2'] == 'Moon' ) self.assertAlmostEqual(sun_moon['orb'], 0.0, places=5)