name: CI on: push: pull_request: jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.12" - name: Install dependencies run: pip install -r requirements.txt - name: Compile check all Python files run: python -m compileall -q . - name: Shell syntax check run: | bash -n install.sh bash -n mbs - name: Smoke test install-script rendering env: BOT_TOKEN: "x" BOT_USERNAME: "x" ADMIN_IDS: "1" ADMIN_PANEL_PASSWORD: "ci-test-password-not-real" PANEL_DOMAIN: "panel.test" SUB_DOMAIN: "sub.test" SITE_DOMAIN: "test" XRAY_PUBLIC_KEY: "x" XRAY_SHORT_ID_TCP: "x" XRAY_SHORT_ID_GRPC: "x" XRAY_SHORT_ID_XHTTP: "x" run: | python - << 'PYEOF' import json import nodeprov transports = nodeprov.build_transports("fi2.example.com", 443, "www.microsoft.com", "PUBKEY", include_ws=True) node = { "provision_token": "TESTTOKEN", "address": "fi2.example.com", "sni": "www.microsoft.com", "private_key": "PRIVKEY", "transports_json": json.dumps(transports), "hysteria_enabled": 1, "hysteria_port": 443, "hysteria_password": "hypass", "hysteria_obfs_password": "obfspass", } script = nodeprov.render_install_script(node) assert "PRIVKEY" in script assert len(script) > 500 print("node install script rendered OK,", len(script), "bytes") PYEOF - name: Smoke test app wiring + payments + HWID logic env: BOT_TOKEN: "123456789:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" BOT_USERNAME: "x" ADMIN_IDS: "1" ADMIN_PANEL_PASSWORD: "ci-test-password-not-real" PANEL_DOMAIN: "panel.test" SUB_DOMAIN: "sub.test" SITE_DOMAIN: "test" XRAY_PUBLIC_KEY: "x" XRAY_SHORT_ID_TCP: "x" XRAY_SHORT_ID_GRPC: "x" XRAY_SHORT_ID_XHTTP: "x" PAYMENTS_ENABLED: "true" YOOKASSA_ENABLED: "true" YOOKASSA_SHOP_ID: "123" YOOKASSA_SECRET_KEY: "xxx" PLATEGA_ENABLED: "true" PLATEGA_MERCHANT_ID: "abc" PLATEGA_SECRET: "yyy" HWID_LIMIT_ENABLED: "true" run: | python - << 'PYEOF' import hashlib import hmac import api import bot import payments import db assert set(payments.available_providers()) == {"yookassa", "platega"} good_sig = hmac.new(b"yyy", b'{"a":1}', hashlib.sha256).hexdigest() assert payments.verify_platega_signature(b'{"a":1}', good_sig) assert not payments.verify_platega_signature(b'{"a":1}', "wrong") db.init_db() db.create_payment("pid1", 1, "de1", "1m", "yookassa", 399) assert db.mark_payment_paid("pid1")["status"] == "paid" assert db.mark_payment_paid("pid1") is None db.get_or_create_user(1, "tester") db.add_device(1, "hwid-aaaaaaaaaa", "android", "Pixel", "ua") assert db.count_devices(1) == 1 assert db.get_device(1, "hwid-aaaaaaaaaa") is not None print("app wiring + payments + HWID logic OK") import legal import settings assert settings.get_brand_name() == "MBS Panel" legal.update_env_var("BRAND_NAME", "CI Test Brand") assert settings.get_brand_name() == "CI Test Brand" index_html = legal.render_site_page("index.html") assert "CI Test Brand" in index_html assert "MBS Panel" not in index_html assert "example.com" not in index_html assert "YourBot_robot" not in index_html assert "{{" not in index_html and "}}" not in index_html cabinet_html = legal.render_site_page("cabinet.html") assert "CI Test Brand" in cabinet_html assert "{{" not in cabinet_html and "}}" not in cabinet_html fake_request = type("FakeRequest", (), {"headers": {}})() root_resp = api.root(fake_request) assert "CI Test Brand" in root_resp plans_resp = api.public_plans() assert plans_resp["plans"][0]["code"] == "7d" branding_resp = api.public_branding() assert branding_resp["brand_name"] == "CI Test Brand" print("branding: site templates + public routes render live, no restart OK") PYEOF - name: Smoke test TOTP, backup/restore, node reorder, multi-admin, rate-limit env: BOT_TOKEN: "123456789:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" BOT_USERNAME: "x" ADMIN_IDS: "1" ADMIN_PANEL_PASSWORD: "ci-test-password-not-real" PANEL_DOMAIN: "panel.test" SUB_DOMAIN: "sub.test" SITE_DOMAIN: "test" XRAY_PUBLIC_KEY: "x" XRAY_SHORT_ID_TCP: "x" XRAY_SHORT_ID_GRPC: "x" XRAY_SHORT_ID_XHTTP: "x" run: | python - << 'PYEOF' import base64 import db import totp raw_key = b"12345678901234567890" secret = base64.b32encode(raw_key).decode("ascii").rstrip("=") expected = ["755224","287082","359152","969429","338314","254676","287922","162583","399871","520489"] for counter, exp in enumerate(expected): assert totp._hotp(secret, counter) == exp, f"RFC 4226 vector failed at counter={counter}" print("TOTP: all 10 RFC 4226 test vectors pass") db.init_db() db.create_node("n1", "Node One", "managed", "1.1.1.1", 443, "pub1", "sid1", "sni1", "xtls-rprx-vision") db.create_node("n2", "Node Two", "managed", "2.2.2.2", 443, "pub2", "sid2", "sni2", "xtls-rprx-vision") order = [n["code"] for n in db.list_nodes()] assert order == ["de1", "n1", "n2"], order db.reorder_nodes(["n2", "de1", "n1"]) assert [n["code"] for n in db.list_nodes()] == ["n2", "de1", "n1"] try: db.reorder_nodes(["n2", "de1"]) assert False, "should reject incomplete reorder list" except ValueError: pass print("node reorder OK") import backup data = backup.create_backup() db.create_node("n3", "Node Three", "managed", "3.3.3.3", 443, "pub3", "sid3", "sni3", "xtls-rprx-vision") assert len(db.list_nodes()) == 4 backup.restore_backup(data) assert len(db.list_nodes()) == 3, "restore should have reverted the extra node" print("backup/restore round-trip OK") admin = db.verify_admin_login("admin", "ci-test-password-not-real") assert admin is not None second = db.create_admin("second", "another-strong-password") assert len(db.list_admins()) == 2 try: db.delete_admin(admin["id"]) db.delete_admin(second["id"]) assert False, "should refuse deleting the last admin" except ValueError: pass print("multi-admin OK") ip = "203.0.113.9" for _ in range(10): db.record_login_attempt(ip, "password") assert db.count_recent_login_attempts(ip, "password", minutes=15) >= 10 db.clear_login_attempts(ip, "password") assert db.count_recent_login_attempts(ip, "password", minutes=15) == 0 print("rate-limit counters OK") import datetime as dt hold_sub = db.create_subscription(999, "n1", 30, "1m", source="bot") original_expires = dt.datetime.fromisoformat(hold_sub["expires_at"]) assert db.hold_subscription(hold_sub["uuid"]) assert db.hold_subscription(hold_sub["uuid"]) is False assert len(db.list_active_subscriptions(tg_id=999)) == 0, "held sub must not count as active" with db.get_conn() as conn: simulated = (dt.datetime.utcnow() - dt.timedelta(hours=5)).isoformat() conn.execute("UPDATE subscriptions SET held_at=? WHERE uuid=?", (simulated, hold_sub["uuid"])) resumed = db.resume_subscription(hold_sub["uuid"]) assert resumed["held_at"] is None shift_hours = (dt.datetime.fromisoformat(resumed["expires_at"]) - original_expires).total_seconds() / 3600 assert 4.9 <= shift_hours <= 5.1, f"expected ~5h shift, got {shift_hours}" assert len(db.list_active_subscriptions(tg_id=999)) == 1, "resumed sub must count as active again" assert db.resume_subscription(hold_sub["uuid"]) is None print("subscription hold/resume OK") print("all v1.1.0 feature smoke tests passed") PYEOF - name: Smoke test live settings (.env-backed plans/toggles/HWID/credentials, no restart) env: BOT_TOKEN: "123456789:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" BOT_USERNAME: "x" ADMIN_IDS: "1" ADMIN_PANEL_PASSWORD: "ci-test-password-not-real" PANEL_DOMAIN: "panel.test" SUB_DOMAIN: "sub.test" SITE_DOMAIN: "test" XRAY_PUBLIC_KEY: "x" XRAY_SHORT_ID_TCP: "x" XRAY_SHORT_ID_GRPC: "x" XRAY_SHORT_ID_XHTTP: "x" run: | python - << 'PYEOF' import hashlib import hmac with open(".env", "a", encoding="utf-8") as f: f.write("PLATEGA_SECRET=old_secret\n") f.write("PLATEGA_ENABLED=true\n") f.write("PLATEGA_MERCHANT_ID=m1\n") import legal import settings import payments plans = settings.get_plans_by_code() assert plans["1m"]["price"] > 0, "default price should come from config before any .env override" settings.set_plan_prices({"1m": 4242}) assert settings.get_plans_by_code()["1m"]["price"] == 4242, "price edit should apply live, no reimport" assert settings.get_plans_by_code()["7d"]["price"] != 4242, "unrelated plan must stay untouched" assert settings.get_hwid_settings()["enabled"] is False legal.update_env_var("HWID_LIMIT_ENABLED", "true") legal.update_env_var("HWID_FALLBACK_LIMIT", "9") hwid = settings.get_hwid_settings() assert hwid["enabled"] is True and hwid["fallback_limit"] == 9, "HWID settings should apply live" body = b'{"transactionId":"t1","status":"CONFIRMED"}' sig_old = hmac.new(b"old_secret", body, hashlib.sha256).hexdigest() assert payments.verify_platega_signature(body, sig_old), "signature must verify against the current secret" legal.update_env_var("PLATEGA_SECRET", "rotated_secret") assert not payments.verify_platega_signature(body, sig_old), "OLD signature must be rejected right after rotation, same process, no restart" sig_new = hmac.new(b"rotated_secret", body, hashlib.sha256).hexdigest() assert payments.verify_platega_signature(body, sig_new), "NEW signature must verify immediately after rotation, same process, no restart" for _ in range(5): legal.update_env_var("HWID_FALLBACK_LIMIT", "9") with open(".env", encoding="utf-8") as f: lines = [l for l in f.readlines() if l.startswith("HWID_FALLBACK_LIMIT=")] assert len(lines) == 1, "repeated writes to the same key must not duplicate .env lines" print("live settings: prices/HWID/credential-rotation all apply with zero reimport OK") PYEOF - name: Smoke test backup/restore round-trip covers branding + live settings + held subscriptions env: BOT_TOKEN: "123456789:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" BOT_USERNAME: "x" ADMIN_IDS: "1" ADMIN_PANEL_PASSWORD: "ci-test-password-not-real" PANEL_DOMAIN: "panel.test" SUB_DOMAIN: "sub.test" SITE_DOMAIN: "test" XRAY_PUBLIC_KEY: "x" XRAY_SHORT_ID_TCP: "x" XRAY_SHORT_ID_GRPC: "x" XRAY_SHORT_ID_XHTTP: "x" run: | python - << 'PYEOF' import backup import db import legal import settings db.init_db() db.create_node("n1", "Node One", "managed", "1.1.1.1", 443, "pub1", "sid1", "sni1", "xtls-rprx-vision") sub_a = db.create_subscription(111, "n1", 30, "1m", source="bot") sub_b = db.create_subscription(222, "n1", 30, "1m", source="bot") legal.update_env_var("BRAND_NAME", "SnapshotBrand") settings.set_plan_prices({"1m": 555}) legal.update_env_var("HWID_LIMIT_ENABLED", "true") legal.update_env_var("HWID_FALLBACK_LIMIT", "4") assert db.hold_subscription(sub_a["uuid"]) assert settings.get_brand_name() == "SnapshotBrand" assert settings.get_plans_by_code()["1m"]["price"] == 555 assert settings.get_hwid_settings() == {"enabled": True, "fallback_limit": 4} assert len(db.list_active_subscriptions(tg_id=111)) == 0, "held sub excluded pre-backup" assert len(db.list_active_subscriptions(tg_id=222)) == 1 snapshot = backup.create_backup() legal.update_env_var("BRAND_NAME", "MutatedAfterBackup") settings.set_plan_prices({"1m": 999}) legal.update_env_var("HWID_LIMIT_ENABLED", "false") assert db.resume_subscription(sub_a["uuid"])["held_at"] is None sub_c = db.create_subscription(333, "n1", 30, "1m", source="bot") assert settings.get_brand_name() == "MutatedAfterBackup" assert len(db.list_active_subscriptions(tg_id=111)) == 1 result = backup.restore_backup(snapshot) assert result["restored_env"] is True assert settings.get_brand_name() == "SnapshotBrand", "brand must revert to snapshot value" assert settings.get_plans_by_code()["1m"]["price"] == 555, "price override must revert" assert settings.get_hwid_settings() == {"enabled": True, "fallback_limit": 4}, "hwid settings must revert" restored_sub_a = db.get_subscription(sub_a["uuid"]) assert restored_sub_a["held_at"] is not None, "held_at must round-trip through backup/restore" assert len(db.list_active_subscriptions(tg_id=111)) == 0, "sub_a held again after restore" assert len(db.list_active_subscriptions(tg_id=222)) == 1, "sub_b untouched" assert db.get_subscription(sub_c["uuid"]) is None, "sub_c created after backup point must be gone" print("backup/restore correctly round-trips branding, live settings and held_at together OK") PYEOF - name: Smoke test custom ADMIN_PATH actually moves the login page, not just adds a copy env: BOT_TOKEN: "123456789:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" BOT_USERNAME: "x" ADMIN_IDS: "1" ADMIN_PANEL_PASSWORD: "ci-test-password-not-real" PANEL_DOMAIN: "panel.test" SUB_DOMAIN: "sub.test" SITE_DOMAIN: "test" XRAY_PUBLIC_KEY: "x" XRAY_SHORT_ID_TCP: "x" XRAY_SHORT_ID_GRPC: "x" XRAY_SHORT_ID_XHTTP: "x" ADMIN_PATH: "xyz123secret" run: | python - << 'PYEOF' import api paths = {r.path for r in api.app.routes} assert "/xyz123secret" in paths, "custom ADMIN_PATH must be registered as a route" assert "/admin" not in paths, "the default /admin page route must be GONE once a custom path is set, not just supplemented" assert "/admin/api/login" in paths, "the API namespace must stay fixed regardless of ADMIN_PATH" fake_request = type("FakeRequest", (), {"headers": {"host": "panel.test"}})() root_response = api.root(fake_request) assert isinstance(root_response, str), \ f"root() on PANEL_DOMAIN must return the rendered site page (a string), not admin.html, once ADMIN_PATH is customized — got {type(root_response)}" assert "admin.html" not in root_response print("custom ADMIN_PATH: old /admin route gone, new path registered, root() no longer leaks the panel OK") PYEOF