# Tests Phase 1f: Woo-Bridge Perioden ohne /wc/v3/subscriptions from __future__ import annotations import gc import os import sqlite3 import unittest from unittest.mock import patch from fastapi import FastAPI from fastapi.testclient import TestClient import stripe_routes import wc_routes from wc_period_payload import coerce_next_payment_unix, resolve_wc_period_fields def _safe_unlink(path: str) -> None: gc.collect() try: if os.path.isfile(path): os.unlink(path) except PermissionError: pass class TestResolveWcPeriodFields(unittest.TestCase): def test_explicit_pair(self) -> None: ps, pe = resolve_wc_period_fields(current_period_start=1700000000, current_period_end=1702592000) self.assertEqual(ps, 1700000000) self.assertEqual(pe, 1702592000) def test_reject_start_ge_end(self) -> None: ps, pe = resolve_wc_period_fields(current_period_start=1702592000, current_period_end=1700000000) self.assertIsNone(ps) self.assertIsNone(pe) def test_next_payment_flexible_billing_m(self) -> None: ps, pe = resolve_wc_period_fields( next_payment_date="2030-06-15T00:00:00+00:00", billing_period="M", billing_interval=1, ) self.assertIsNotNone(ps) self.assertIsNotNone(pe) assert ps is not None and pe is not None self.assertLess(ps, pe) def test_next_payment_monthly_word(self) -> None: ps, pe = resolve_wc_period_fields( next_payment_date="2030-06-15T00:00:00+00:00", billing_period="month", billing_interval=1, ) self.assertIsNotNone(ps) self.assertIsNotNone(pe) assert ps is not None and pe is not None self.assertLess(ps, pe) def test_next_payment_unix_int(self) -> None: ps, pe = resolve_wc_period_fields( next_payment_date=1893456000, billing_period="month", billing_interval=1, ) self.assertIsNotNone(ps) self.assertIsNotNone(pe) def test_coerce_ms(self) -> None: u = coerce_next_payment_unix(1893456000000) self.assertEqual(u, 1893456000) def test_only_end_with_billing(self) -> None: ps, pe = resolve_wc_period_fields( current_period_end=2000000000, billing_period="month", billing_interval=1, ) self.assertIsNotNone(ps) self.assertIsNotNone(pe) assert ps is not None and pe is not None def test_only_end_with_billing_m(self) -> None: ps, pe = resolve_wc_period_fields( current_period_end=2000000000, billing_period="M", billing_interval=1, ) self.assertIsNotNone(ps) self.assertIsNotNone(pe) assert ps is not None and pe is not None self.assertLess(ps, pe) def _make_client(tmp_db: str, events_log: str) -> TestClient: stripe_routes.DB_PATH = __import__("pathlib").Path(tmp_db) stripe_routes.EVENTS_LOG = __import__("pathlib").Path(events_log) wc_routes.DB_PATH = tmp_db stripe_routes._ensure_storage() app = FastAPI() app.include_router(wc_routes.router, prefix="/wc") return TestClient(app) class TestWcProvisionPeriods(unittest.TestCase): def setUp(self) -> None: self._old_secret = os.environ.get("WC_PROVISION_SECRET") os.environ["WC_PROVISION_SECRET"] = "unit_test_wc_secret_1f" def tearDown(self) -> None: if self._old_secret is None: os.environ.pop("WC_PROVISION_SECRET", None) else: os.environ["WC_PROVISION_SECRET"] = self._old_secret wc_routes.DB_PATH = None def test_provision_without_period_backward_compat(self) -> None: import tempfile with tempfile.NamedTemporaryFile(suffix=".sqlite", delete=False) as f: dbp = f.name elog = dbp + ".jsonl" try: c = _make_client(dbp, elog) body = { "customer_email": "buyer@example.com", "wc_order_id": 9001, "wc_subscription_id": 8001, "lookup_key": "aza_basic_monthly", } with patch("stripe_routes.send_license_email", return_value=True): r = c.post("/wc/provision", json=body, headers={"X-WC-Secret": "unit_test_wc_secret_1f"}) self.assertEqual(r.status_code, 200, r.text) data = r.json() self.assertEqual(data["status"], "provisioned") self.assertNotIn("period_synced", data) con = sqlite3.connect(dbp) row = con.execute( "SELECT current_period_start, current_period_end FROM licenses WHERE subscription_id=?", ("wc_sub_8001",), ).fetchone() con.close() self.assertIsNotNone(row) self.assertIsNone(row[0]) self.assertIsNone(row[1]) del c finally: _safe_unlink(dbp) _safe_unlink(elog) def test_provision_with_period_fields(self) -> None: import tempfile with tempfile.NamedTemporaryFile(suffix=".sqlite", delete=False) as f: dbp = f.name elog = dbp + ".jsonl" try: c = _make_client(dbp, elog) body = { "customer_email": "buyer2@example.com", "wc_order_id": 9002, "wc_subscription_id": 8002, "current_period_start": 1700000000, "current_period_end": 1702592000, } with patch("stripe_routes.send_license_email", return_value=True): r = c.post("/wc/provision", json=body, headers={"X-WC-Secret": "unit_test_wc_secret_1f"}) self.assertEqual(r.status_code, 200, r.text) self.assertTrue(r.json().get("period_synced")) con = sqlite3.connect(dbp) row = con.execute( "SELECT current_period_start, current_period_end, customer_email FROM licenses WHERE subscription_id=?", ("wc_sub_8002",), ).fetchone() con.close() self.assertEqual(row[0], 1700000000) self.assertEqual(row[1], 1702592000) del c finally: _safe_unlink(dbp) _safe_unlink(elog) def test_idempotent_call_updates_period(self) -> None: import tempfile with tempfile.NamedTemporaryFile(suffix=".sqlite", delete=False) as f: dbp = f.name elog = dbp + ".jsonl" try: c = _make_client(dbp, elog) base = { "customer_email": "seq@example.com", "wc_order_id": 9100, "wc_subscription_id": 8100, "lookup_key": "aza_basic_monthly", } with patch("stripe_routes.send_license_email", return_value=True): r1 = c.post("/wc/provision", json=dict(base), headers={"X-WC-Secret": "unit_test_wc_secret_1f"}) self.assertEqual(r1.status_code, 200) self.assertEqual(r1.json()["status"], "provisioned") body2 = dict(base) body2["current_period_start"] = 1700000100 body2["current_period_end"] = 1702592100 r2 = c.post("/wc/provision", json=body2, headers={"X-WC-Secret": "unit_test_wc_secret_1f"}) self.assertEqual(r2.status_code, 200) self.assertEqual(r2.json()["status"], "already_provisioned") self.assertTrue(r2.json().get("period_synced")) con = sqlite3.connect(dbp) row = con.execute( "SELECT current_period_start, current_period_end FROM licenses WHERE subscription_id=?", ("wc_sub_8100",), ).fetchone() con.close() self.assertEqual(row, (1700000100, 1702592100)) del c finally: _safe_unlink(dbp) _safe_unlink(elog) class TestSubscriptionPeriodEndpoint(unittest.TestCase): def setUp(self) -> None: self._old_secret = os.environ.get("WC_PROVISION_SECRET") os.environ["WC_PROVISION_SECRET"] = "unit_test_wc_secret_1f" def tearDown(self) -> None: if self._old_secret is None: os.environ.pop("WC_PROVISION_SECRET", None) else: os.environ["WC_PROVISION_SECRET"] = self._old_secret wc_routes.DB_PATH = None def _seed_license(self, dbp: str) -> None: stripe_routes.DB_PATH = __import__("pathlib").Path(dbp) stripe_routes._ensure_storage() with sqlite3.connect(dbp) as con: con.execute( """ INSERT INTO licenses ( subscription_id, customer_id, status, lookup_key, allowed_users, devices_per_user, customer_email, client_reference_id, current_period_start, current_period_end, updated_at, license_key ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?) """, ( "wc_sub_7001", "wc_order_6001", "active", "aza_basic_monthly", 1, 2, "u@example.com", "wc_order_6001", None, None, 1, "LIC_TEST", ), ) con.commit() def test_no_secret_401(self) -> None: import tempfile with tempfile.NamedTemporaryFile(suffix=".sqlite", delete=False) as f: dbp = f.name elog = dbp + ".jsonl" try: self._seed_license(dbp) wc_routes.DB_PATH = dbp app = FastAPI() app.include_router(wc_routes.router, prefix="/wc") cl = TestClient(app) r = cl.post("/wc/subscription_period", json={"wc_subscription_id": 7001, "current_period_start": 100, "current_period_end": 200}) self.assertEqual(r.status_code, 401) del cl finally: _safe_unlink(dbp) _safe_unlink(elog) def test_wrong_secret_401(self) -> None: import tempfile with tempfile.NamedTemporaryFile(suffix=".sqlite", delete=False) as f: dbp = f.name elog = dbp + ".jsonl" try: self._seed_license(dbp) wc_routes.DB_PATH = dbp app = FastAPI() app.include_router(wc_routes.router, prefix="/wc") cl = TestClient(app) r = cl.post( "/wc/subscription_period", json={"wc_subscription_id": 7001, "current_period_start": 1000, "current_period_end": 2000}, headers={"X-WC-Secret": "wrong"}, ) self.assertEqual(r.status_code, 401) del cl finally: _safe_unlink(dbp) _safe_unlink(elog) def test_updates_only_periods(self) -> None: import tempfile with tempfile.NamedTemporaryFile(suffix=".sqlite", delete=False) as f: dbp = f.name elog = dbp + ".jsonl" try: self._seed_license(dbp) c = _make_client(dbp, elog) r = c.post( "/wc/subscription_period", json={ "wc_subscription_id": 7001, "wc_order_id": 6001, "current_period_start": 1800000000, "current_period_end": 1802000000, }, headers={"X-WC-Secret": "unit_test_wc_secret_1f"}, ) self.assertEqual(r.status_code, 200, r.text) self.assertEqual(r.json(), {"ok": True, "rows_updated": 1}) con = sqlite3.connect(dbp) row = con.execute( "SELECT current_period_start, current_period_end, customer_email, status FROM licenses WHERE subscription_id=?", ("wc_sub_7001",), ).fetchone() con.close() self.assertEqual(row[0], 1800000000) self.assertEqual(row[1], 1802000000) self.assertEqual(row[2], "u@example.com") self.assertEqual(row[3], "active") del c finally: _safe_unlink(dbp) _safe_unlink(elog) def test_response_has_no_secret(self) -> None: import tempfile with tempfile.NamedTemporaryFile(suffix=".sqlite", delete=False) as f: dbp = f.name elog = dbp + ".jsonl" try: self._seed_license(dbp) c = _make_client(dbp, elog) r = c.post( "/wc/subscription_period", json={"wc_subscription_id": 7001, "current_period_start": 1800000000, "current_period_end": 1802000000}, headers={"X-WC-Secret": "unit_test_wc_secret_1f"}, ) body = r.json() self.assertNotIn("secret", str(body).lower()) del c finally: _safe_unlink(dbp) _safe_unlink(elog) def test_unknown_subscription_404(self) -> None: import tempfile with tempfile.NamedTemporaryFile(suffix=".sqlite", delete=False) as f: dbp = f.name elog = dbp + ".jsonl" try: self._seed_license(dbp) c = _make_client(dbp, elog) r = c.post( "/wc/subscription_period", json={ "wc_subscription_id": 99999, "current_period_start": 1800000000, "current_period_end": 1802000000, }, headers={"X-WC-Secret": "unit_test_wc_secret_1f"}, ) self.assertEqual(r.status_code, 404) del c finally: _safe_unlink(dbp) _safe_unlink(elog) if __name__ == "__main__": unittest.main()