262 lines
9.0 KiB
Python
262 lines
9.0 KiB
Python
|
|
# -*- coding: utf-8 -*-
|
||
|
|
"""Phase 1d WooCommerce Perioden-Sync (ohne Netzwerk gegen echtes Woo)."""
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import os
|
||
|
|
import sqlite3
|
||
|
|
import tempfile
|
||
|
|
import unittest
|
||
|
|
from pathlib import Path
|
||
|
|
from unittest.mock import patch
|
||
|
|
|
||
|
|
from aza_wc_period_sync import (
|
||
|
|
compute_period_unix_from_wc_subscription,
|
||
|
|
extract_wc_subscription_numeric_id,
|
||
|
|
sync_active_license_periods_from_woocommerce_only,
|
||
|
|
_subtract_billing_period,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
class TestExtractWcSub(unittest.TestCase):
|
||
|
|
def test_numeric(self):
|
||
|
|
self.assertEqual(extract_wc_subscription_numeric_id("wc_sub_1234"), 1234)
|
||
|
|
|
||
|
|
def test_reject_non_wc(self):
|
||
|
|
self.assertIsNone(extract_wc_subscription_numeric_id("sub_abc"))
|
||
|
|
self.assertIsNone(extract_wc_subscription_numeric_id("wc_sub_"))
|
||
|
|
|
||
|
|
def test_case_insensitive_prefix(self):
|
||
|
|
self.assertEqual(extract_wc_subscription_numeric_id("WC_SUB_99"), 99)
|
||
|
|
|
||
|
|
|
||
|
|
class TestPeriodCompute(unittest.TestCase):
|
||
|
|
def test_monthly_next_payment(self):
|
||
|
|
d = {
|
||
|
|
"status": "active",
|
||
|
|
"next_payment_date": "2026-07-01T12:00:00",
|
||
|
|
"billing_period": "month",
|
||
|
|
"billing_interval": 1,
|
||
|
|
}
|
||
|
|
ps, pe = compute_period_unix_from_wc_subscription(d)
|
||
|
|
self.assertIsNotNone(ps)
|
||
|
|
self.assertIsNotNone(pe)
|
||
|
|
self.assertLess(ps, pe)
|
||
|
|
|
||
|
|
def test_yearly(self):
|
||
|
|
d = {
|
||
|
|
"next_payment_date": "2027-05-18T00:00:00+00:00",
|
||
|
|
"billing_period": "year",
|
||
|
|
"billing_interval": 1,
|
||
|
|
}
|
||
|
|
ps, pe = compute_period_unix_from_wc_subscription(d)
|
||
|
|
self.assertLess(ps, pe)
|
||
|
|
self.assertGreaterEqual(pe - ps, 3600 * 24 * 300)
|
||
|
|
|
||
|
|
def test_meta_schedule_next_payment(self):
|
||
|
|
d = {
|
||
|
|
"billing_period": "month",
|
||
|
|
"billing_interval": 1,
|
||
|
|
"meta_data": [{"key": "_schedule_next_payment", "value": "2026-08-10T10:00:00"}],
|
||
|
|
}
|
||
|
|
ps, pe = compute_period_unix_from_wc_subscription(d)
|
||
|
|
self.assertIsNotNone(pe)
|
||
|
|
self.assertIsNotNone(ps)
|
||
|
|
self.assertLess(ps, int(pe))
|
||
|
|
|
||
|
|
def test_subtract_month_consistency(self):
|
||
|
|
end = 1_700_000_000
|
||
|
|
s = _subtract_billing_period(end, "month", 1)
|
||
|
|
self.assertLess(s, end)
|
||
|
|
|
||
|
|
|
||
|
|
class TestSyncNoCredentials(unittest.TestCase):
|
||
|
|
def test_no_env_no_touch_db(self):
|
||
|
|
env_keys = (
|
||
|
|
"AZA_WOOCOMMERCE_URL",
|
||
|
|
"AZA_WOOCOMMERCE_CONSUMER_KEY",
|
||
|
|
"AZA_WOOCOMMERCE_CONSUMER_SECRET",
|
||
|
|
"WOOCOMMERCE_URL",
|
||
|
|
"WOOCOMMERCE_CONSUMER_KEY",
|
||
|
|
"WOOCOMMERCE_CONSUMER_SECRET",
|
||
|
|
)
|
||
|
|
saved = {k: os.environ.pop(k, None) for k in env_keys}
|
||
|
|
try:
|
||
|
|
tmp = tempfile.NamedTemporaryFile(suffix=".sqlite", delete=False)
|
||
|
|
tmp.close()
|
||
|
|
db = Path(tmp.name)
|
||
|
|
con = sqlite3.connect(str(db))
|
||
|
|
con.execute(
|
||
|
|
"""
|
||
|
|
CREATE TABLE licenses (
|
||
|
|
subscription_id TEXT PRIMARY KEY,
|
||
|
|
customer_id TEXT,
|
||
|
|
status TEXT,
|
||
|
|
current_period_start INTEGER,
|
||
|
|
current_period_end INTEGER,
|
||
|
|
updated_at INTEGER
|
||
|
|
)
|
||
|
|
"""
|
||
|
|
)
|
||
|
|
con.execute(
|
||
|
|
"INSERT INTO licenses VALUES ('wc_sub_1','wc_order_1','active',NULL,NULL,0)"
|
||
|
|
)
|
||
|
|
con.commit()
|
||
|
|
con.close()
|
||
|
|
with patch("aza_wc_period_sync._stripe_db_path", return_value=db):
|
||
|
|
out = sync_active_license_periods_from_woocommerce_only()
|
||
|
|
self.assertFalse(out.get("ok"))
|
||
|
|
self.assertEqual(out.get("error_code"), "WOO_CREDENTIALS_MISSING")
|
||
|
|
con = sqlite3.connect(str(db))
|
||
|
|
row = con.execute(
|
||
|
|
"SELECT current_period_start, current_period_end FROM licenses"
|
||
|
|
).fetchone()
|
||
|
|
con.close()
|
||
|
|
self.assertIsNone(row[0])
|
||
|
|
self.assertIsNone(row[1])
|
||
|
|
finally:
|
||
|
|
for k, v in saved.items():
|
||
|
|
if v is not None:
|
||
|
|
os.environ[k] = v
|
||
|
|
try:
|
||
|
|
db.unlink()
|
||
|
|
except OSError:
|
||
|
|
pass
|
||
|
|
|
||
|
|
|
||
|
|
class TestSyncWithMockFetch(unittest.TestCase):
|
||
|
|
def test_updates_period_columns_only(self):
|
||
|
|
env_keys = (
|
||
|
|
"AZA_WOOCOMMERCE_URL",
|
||
|
|
"AZA_WOOCOMMERCE_CONSUMER_KEY",
|
||
|
|
"AZA_WOOCOMMERCE_CONSUMER_SECRET",
|
||
|
|
)
|
||
|
|
for k in env_keys:
|
||
|
|
os.environ[k] = "x" if "URL" not in k else "https://example.test"
|
||
|
|
tmp = tempfile.NamedTemporaryFile(suffix=".sqlite", delete=False)
|
||
|
|
tmp.close()
|
||
|
|
db = Path(tmp.name)
|
||
|
|
try:
|
||
|
|
con = sqlite3.connect(str(db))
|
||
|
|
con.execute(
|
||
|
|
"""
|
||
|
|
CREATE TABLE licenses (
|
||
|
|
subscription_id TEXT PRIMARY KEY,
|
||
|
|
customer_id TEXT,
|
||
|
|
status TEXT,
|
||
|
|
lookup_key TEXT,
|
||
|
|
current_period_start INTEGER,
|
||
|
|
current_period_end INTEGER,
|
||
|
|
updated_at INTEGER
|
||
|
|
)
|
||
|
|
"""
|
||
|
|
)
|
||
|
|
con.execute(
|
||
|
|
"""INSERT INTO licenses VALUES (
|
||
|
|
'wc_sub_42','wc_order_41','active','aza_basic_monthly',NULL,NULL,0
|
||
|
|
)"""
|
||
|
|
)
|
||
|
|
con.commit()
|
||
|
|
con.close()
|
||
|
|
fake_sub = {
|
||
|
|
"status": "active",
|
||
|
|
"next_payment_date": "2026-07-15T08:00:00",
|
||
|
|
"billing_period": "month",
|
||
|
|
"billing_interval": 1,
|
||
|
|
}
|
||
|
|
|
||
|
|
def fake_fetch(wid: int):
|
||
|
|
self.assertEqual(wid, 42)
|
||
|
|
return fake_sub
|
||
|
|
|
||
|
|
with patch("aza_wc_period_sync._stripe_db_path", return_value=db):
|
||
|
|
with patch("aza_wc_period_sync._fetch_wc_subscription", fake_fetch):
|
||
|
|
out = sync_active_license_periods_from_woocommerce_only()
|
||
|
|
self.assertTrue(out.get("ok"))
|
||
|
|
self.assertEqual(out.get("updated"), 1)
|
||
|
|
con = sqlite3.connect(str(db))
|
||
|
|
row = con.execute(
|
||
|
|
"SELECT status, lookup_key, current_period_start, current_period_end FROM licenses"
|
||
|
|
).fetchone()
|
||
|
|
con.close()
|
||
|
|
self.assertEqual(row[0], "active")
|
||
|
|
self.assertEqual(row[1], "aza_basic_monthly")
|
||
|
|
self.assertIsNotNone(row[2])
|
||
|
|
self.assertIsNotNone(row[3])
|
||
|
|
self.assertLess(row[2], row[3])
|
||
|
|
finally:
|
||
|
|
for k in env_keys:
|
||
|
|
os.environ.pop(k, None)
|
||
|
|
try:
|
||
|
|
db.unlink()
|
||
|
|
except OSError:
|
||
|
|
pass
|
||
|
|
|
||
|
|
|
||
|
|
class TestAdminWooEndpointAuth(unittest.TestCase):
|
||
|
|
def test_requires_admin_header(self):
|
||
|
|
os.environ["AZA_ADMIN_TOKEN"] = "adm_test_secret_unit"
|
||
|
|
try:
|
||
|
|
from fastapi import FastAPI
|
||
|
|
from fastapi.testclient import TestClient
|
||
|
|
|
||
|
|
from admin_routes import router as admin_router
|
||
|
|
|
||
|
|
app = FastAPI()
|
||
|
|
app.include_router(admin_router, prefix="/admin")
|
||
|
|
c = TestClient(app)
|
||
|
|
with patch(
|
||
|
|
"aza_wc_period_sync.sync_active_license_periods_from_woocommerce_only",
|
||
|
|
return_value={"ok": True, "updated": 0},
|
||
|
|
):
|
||
|
|
r0 = c.post("/admin/woocommerce_sync_periods")
|
||
|
|
r1 = c.post(
|
||
|
|
"/admin/woocommerce_sync_periods",
|
||
|
|
headers={"X-Admin-Token": "wrong"},
|
||
|
|
)
|
||
|
|
r2 = c.post(
|
||
|
|
"/admin/woocommerce_sync_periods",
|
||
|
|
headers={"X-Admin-Token": "adm_test_secret_unit"},
|
||
|
|
)
|
||
|
|
self.assertEqual(r0.status_code, 401)
|
||
|
|
self.assertEqual(r1.status_code, 401)
|
||
|
|
self.assertEqual(r2.status_code, 200)
|
||
|
|
self.assertNotIn("secret", (r2.text or "").lower())
|
||
|
|
finally:
|
||
|
|
os.environ.pop("AZA_ADMIN_TOKEN", None)
|
||
|
|
|
||
|
|
|
||
|
|
class TestCredentials503Response(unittest.TestCase):
|
||
|
|
def test_woo_missing_returns_503_json(self):
|
||
|
|
env_keys = (
|
||
|
|
"AZA_WOOCOMMERCE_URL",
|
||
|
|
"AZA_WOOCOMMERCE_CONSUMER_KEY",
|
||
|
|
"AZA_WOOCOMMERCE_CONSUMER_SECRET",
|
||
|
|
)
|
||
|
|
saved = {k: os.environ.pop(k, None) for k in env_keys}
|
||
|
|
os.environ["AZA_ADMIN_TOKEN"] = "adm_x"
|
||
|
|
try:
|
||
|
|
from fastapi import FastAPI
|
||
|
|
from fastapi.testclient import TestClient
|
||
|
|
|
||
|
|
from admin_routes import router as admin_router
|
||
|
|
|
||
|
|
app = FastAPI()
|
||
|
|
app.include_router(admin_router, prefix="/admin")
|
||
|
|
c = TestClient(app)
|
||
|
|
r = c.post(
|
||
|
|
"/admin/woocommerce_sync_periods",
|
||
|
|
headers={"X-Admin-Token": "adm_x"},
|
||
|
|
)
|
||
|
|
self.assertEqual(r.status_code, 503)
|
||
|
|
body = r.json()
|
||
|
|
self.assertEqual(body.get("error_code"), "WOO_CREDENTIALS_MISSING")
|
||
|
|
finally:
|
||
|
|
os.environ.pop("AZA_ADMIN_TOKEN", None)
|
||
|
|
for k, v in saved.items():
|
||
|
|
if v is not None:
|
||
|
|
os.environ[k] = v
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
unittest.main()
|