""" Integration tests for the PySwiss chart calculation API. These tests drive the TDD implementation of GET /api/chart/ and GET /api/tz/. They verify the HTTP contract using Django's test client. Run: pyswiss/.venv/Scripts/python pyswiss/manage.py test pyswiss/apps/charts """ from django.test import TestCase # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- # J2000.0 — a well-known reference point: Sun at ~280.37° (Capricorn 10°22') J2000 = '2000-01-01T12:00:00Z' LONDON = {'lat': 51.5074, 'lon': -0.1278} # Well-known coordinates with unambiguous timezone results NEW_YORK = {'lat': 40.7128, 'lon': -74.0060} # America/New_York TOKYO = {'lat': 35.6762, 'lon': 139.6503} # Asia/Tokyo REYKJAVIK = {'lat': 64.1355, 'lon': -21.8954} # Atlantic/Reykjavik class ChartApiTest(TestCase): """GET /api/chart/ — calculate a natal chart from datetime + coordinates.""" def _get(self, params): return self.client.get('/api/chart/', params) # ── guards ──────────────────────────────────────────────────────────── def test_chart_returns_400_if_dt_missing(self): response = self._get({'lat': 51.5074, 'lon': -0.1278}) self.assertEqual(response.status_code, 400) def test_chart_returns_400_if_lat_missing(self): response = self._get({'dt': J2000, 'lon': -0.1278}) self.assertEqual(response.status_code, 400) def test_chart_returns_400_if_lon_missing(self): response = self._get({'dt': J2000, 'lat': 51.5074}) self.assertEqual(response.status_code, 400) def test_chart_returns_400_for_invalid_dt_format(self): response = self._get({'dt': 'not-a-date', **LONDON}) self.assertEqual(response.status_code, 400) def test_chart_returns_400_for_out_of_range_lat(self): response = self._get({'dt': J2000, 'lat': 999, 'lon': -0.1278}) self.assertEqual(response.status_code, 400) # ── response shape ──────────────────────────────────────────────────── def test_chart_returns_200_for_valid_params(self): response = self._get({'dt': J2000, **LONDON}) self.assertEqual(response.status_code, 200) def test_chart_response_is_json(self): response = self._get({'dt': J2000, **LONDON}) self.assertIn('application/json', response['Content-Type']) def test_chart_returns_all_ten_planets(self): data = self._get({'dt': J2000, **LONDON}).json() expected = { 'Sun', 'Moon', 'Mercury', 'Venus', 'Mars', 'Jupiter', 'Saturn', 'Uranus', 'Neptune', 'Pluto', } self.assertEqual(set(data['planets'].keys()), expected) def test_each_planet_has_sign_degree_and_retrograde(self): data = self._get({'dt': J2000, **LONDON}).json() for name, planet in data['planets'].items(): with self.subTest(planet=name): self.assertIn('sign', planet) self.assertIn('degree', planet) self.assertIn('retrograde', planet) def test_chart_returns_houses(self): data = self._get({'dt': J2000, **LONDON}).json() houses = data['houses'] self.assertEqual(len(houses['cusps']), 12) self.assertIn('asc', houses) self.assertIn('mc', houses) def test_chart_returns_six_element_counts(self): """Fire/Water/Earth/Air are sign-based counts; Time/Space are emergent.""" data = self._get({'dt': J2000, **LONDON}).json() for key in ('Fire', 'Water', 'Earth', 'Air', 'Time', 'Space'): with self.subTest(element=key): self.assertIn(key, data['elements']) def test_chart_reports_active_house_system(self): data = self._get({'dt': J2000, **LONDON}).json() self.assertIn('house_system', data) # ── calculation correctness ─────────────────────────────────────────── def test_sun_is_in_capricorn_at_j2000(self): """Regression: Sun at J2000.0 is ~280.37° — Capricorn.""" data = self._get({'dt': J2000, **LONDON}).json() sun = data['planets']['Sun'] self.assertEqual(sun['sign'], 'Capricorn') self.assertAlmostEqual(sun['degree'], 280.37, delta=0.1) def test_sun_is_not_retrograde(self): """The Sun never goes retrograde.""" data = self._get({'dt': J2000, **LONDON}).json() self.assertFalse(data['planets']['Sun']['retrograde']) def test_element_counts_sum_to_ten(self): """All 10 planets are assigned to exactly one classical element.""" data = self._get({'dt': J2000, **LONDON}).json() classical = sum( data['elements'][e] for e in ('Fire', 'Water', 'Earth', 'Air') ) self.assertEqual(classical, 10) # ── house system ────────────────────────────────────────────────────── def test_default_house_system_is_porphyry(self): """Porphyry ('O') is the project default — no param needed.""" data = self._get({'dt': J2000, **LONDON}).json() self.assertEqual(data['house_system'], 'O') def test_non_superuser_cannot_override_house_system(self): """House system override is superuser-only; plain requests get 403.""" response = self._get({'dt': J2000, **LONDON, 'house_system': 'P'}) self.assertEqual(response.status_code, 403) # ── aspects ─────────────────────────────────────────────────────────── def test_chart_returns_aspects_list(self): data = self._get({'dt': J2000, **LONDON}).json() self.assertIn('aspects', data) self.assertIsInstance(data['aspects'], list) def test_each_aspect_has_required_fields(self): data = self._get({'dt': J2000, **LONDON}).json() for aspect in data['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_sun_saturn_trine_present_at_j2000(self): """Sun ~280.37° (Capricorn) and Saturn ~40.73° (Taurus) are ~120.36° apart — Trine.""" data = self._get({'dt': J2000, **LONDON}).json() pairs = {(a['planet1'], a['planet2'], a['type']) for a in data['aspects']} self.assertIn(('Sun', 'Saturn', 'Trine'), pairs) class TimezoneApiTest(TestCase): """GET /api/tz/ — resolve IANA timezone from lat/lon coordinates.""" def _get(self, params): return self.client.get('/api/tz/', params) # ── guards ──────────────────────────────────────────────────────────── def test_returns_400_if_lat_missing(self): response = self._get({'lon': -74.0060}) self.assertEqual(response.status_code, 400) def test_returns_400_if_lon_missing(self): response = self._get({'lat': 40.7128}) self.assertEqual(response.status_code, 400) def test_returns_400_for_invalid_lat(self): response = self._get({'lat': 'abc', 'lon': -74.0060}) self.assertEqual(response.status_code, 400) def test_returns_400_for_out_of_range_lat(self): response = self._get({'lat': 999, 'lon': -74.0060}) self.assertEqual(response.status_code, 400) def test_returns_400_for_out_of_range_lon(self): response = self._get({'lat': 40.7128, 'lon': 999}) self.assertEqual(response.status_code, 400) # ── response shape ──────────────────────────────────────────────────── def test_returns_200_for_valid_coords(self): response = self._get(NEW_YORK) self.assertEqual(response.status_code, 200) def test_response_is_json(self): response = self._get(NEW_YORK) self.assertIn('application/json', response['Content-Type']) def test_response_contains_timezone_key(self): data = self._get(NEW_YORK).json() self.assertIn('timezone', data) def test_timezone_is_a_string(self): data = self._get(NEW_YORK).json() self.assertIsInstance(data['timezone'], str) # ── correctness ─────────────────────────────────────────────────────── def test_new_york_timezone(self): data = self._get(NEW_YORK).json() self.assertEqual(data['timezone'], 'America/New_York') def test_tokyo_timezone(self): data = self._get(TOKYO).json() self.assertEqual(data['timezone'], 'Asia/Tokyo') def test_reykjavik_timezone(self): data = self._get(REYKJAVIK).json() self.assertEqual(data['timezone'], 'Atlantic/Reykjavik')