mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
* [e530aa5e] Diagnose and fix roboco-api CI failure (run 29629255153) (#561) (#562) * [e530aa5e] fix(tests): narrow None before indexing validate_init_data() result in telegram_initdata self-check CI run 29629255153 failed on mypy, not the historical pydantic-settings issue (uv.lock already pins 2.14.2). The __main__ self-check block in test_telegram_initdata.py indexed the dict[str, object] | None return of validate_init_data() without narrowing away None first. * [e530aa5e] docs(qa): document CI fix for mypy type narrowing in telegram_initdata test Explains the root cause (mypy type error in __main__ block), the solution (None narrowing before indexing), and the safe pattern for future test self-checks that call functions returning optional types. --------- Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech> Co-authored-by: Backend Documenter <be-doc@roboco.tech> * [0884b737] Diagnose and fix Python quality gate + e2e lifecycle smoke CI failures on PR #563 (#564) (#565) * [0884b737] fix(tests): isolate ROBOCO_SDK_URL for scripted e2e-smoke agents tests/e2e_smoke/harness.py already isolates ROBOCO_AGENT_TOKEN from the host environment (the #503/#504 fix) but left ROBOCO_SDK_URL leaking through. flow_server/do_server both default it to http://localhost:9000 and forward every rejection there for the per-verb circuit breaker; inside a real spawned agent container that port is a live SDK loopback, so the breaker records genuine attempts for the ephemeral test-agent IDs and trips circuit_open mid-test (test_sandbox_on_demand.py::test_request_sandbox_guard_chain_over_real_api, which deliberately causes 3 rejections in a row). Point it at a guaranteed-refused loopback address so every environment gets the same fail-open bypass a bare CI runner already gets by having nothing listening on 9000 at all. * [0884b737] docs(changelog): document e2e-smoke harness ROBOCO_SDK_URL isolation fix Document the fix that isolates ROBOCO_SDK_URL in the ScriptedAgent harness to prevent the per-verb circuit breaker from leaking state into ephemeral test-agent identities when the e2e-smoke suite runs inside a live agent container. This ensures the suite passes consistently regardless of whether it runs on bare CI or inside a spawned agent. --------- Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech> Co-authored-by: Backend Documenter <be-doc@roboco.tech> * [3b9a1771] Diagnose and fix ALL make quality + e2e-smoke stage failures on PR #563; confirm real CI green (round 3) (#566) (#567) * [3b9a1771] fix(e2e-smoke): match real embedding dimension when seeding fake journal chunk test_c3_deleted_journal_unindexed inserted a 4-dim placeholder vector into chunks_journals, but the e2e stack's app lifespan eagerly creates that table with the real settings.embedding_dimensions (1024) before the test runs, so the insert failed with "expected 1024 dimensions, not 4". Derive _SMOKE_DIM from settings.embedding_dimensions instead of a hardcoded constant so the seeded vector always matches the table's actual column width. * [3b9a1771] docs(qa): document e2e-smoke embedding dimension fix in round 3 CI diagnosis Recorded the root cause, solution, and pattern for the final e2e-smoke test failure found in comprehensive sandbox testing: the test seeded a 4-dim placeholder vector but the app's eager lifespan init created chunks_journals with the real 1024-dim embedding column. Updated _SMOKE_DIM to derive from settings.embedding_dimensions instead of a hardcoded constant. --------- Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech> Co-authored-by: Backend Documenter <be-doc@roboco.tech> --------- Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech> Co-authored-by: Backend Documenter <be-doc@roboco.tech>
127 lines
4.8 KiB
Python
127 lines
4.8 KiB
Python
"""``validate_init_data`` coverage — hand-computed HMAC vectors pin the exact
|
|
algorithm shape (HMAC key=b"WebAppData"/msg=bot_token for the secret, then
|
|
HMAC key=secret/msg=data_check_string for the hash), plus tamper/expiry/
|
|
missing-field cases. Pure function, no I/O — no DB/network fixtures needed.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import hmac
|
|
import json
|
|
import time
|
|
from urllib.parse import urlencode
|
|
|
|
from roboco.utils.telegram_initdata import validate_init_data
|
|
|
|
_BOT_TOKEN = "123456:TEST-bot-token-for-unit-tests"
|
|
|
|
|
|
def _sign(fields: dict[str, str], bot_token: str = _BOT_TOKEN) -> str:
|
|
"""Reference HMAC computation, independent of the module under test."""
|
|
data_check_string = "\n".join(f"{k}={v}" for k, v in sorted(fields.items()))
|
|
secret_key = hmac.new(b"WebAppData", bot_token.encode(), hashlib.sha256).digest()
|
|
return hmac.new(secret_key, data_check_string.encode(), hashlib.sha256).hexdigest()
|
|
|
|
|
|
def _init_data(fields: dict[str, str], bot_token: str = _BOT_TOKEN) -> str:
|
|
signed = dict(fields)
|
|
signed["hash"] = _sign(fields, bot_token)
|
|
return urlencode(signed)
|
|
|
|
|
|
def test_valid_init_data_returns_parsed_fields_with_user_decoded() -> None:
|
|
user = {"id": 987654321, "first_name": "Renzo"}
|
|
fields = {
|
|
"auth_date": str(int(time.time())),
|
|
"user": json.dumps(user),
|
|
"query_id": "AAH_abc123",
|
|
}
|
|
result = validate_init_data(_init_data(fields), _BOT_TOKEN, max_age_seconds=600)
|
|
assert result is not None
|
|
assert result["user"] == user
|
|
assert result["query_id"] == "AAH_abc123"
|
|
|
|
|
|
def test_missing_hash_rejected() -> None:
|
|
fields = {"auth_date": str(int(time.time()))}
|
|
init_data = urlencode(fields) # no hash field at all
|
|
assert validate_init_data(init_data, _BOT_TOKEN, max_age_seconds=600) is None
|
|
|
|
|
|
def test_tampered_field_after_signing_rejected() -> None:
|
|
fields = {"auth_date": str(int(time.time())), "user": json.dumps({"id": 1})}
|
|
signed_hash = _sign(fields)
|
|
tampered = dict(fields)
|
|
tampered["auth_date"] = str(int(time.time()) + 999) # changed post-signing
|
|
tampered["hash"] = signed_hash
|
|
init_data = urlencode(tampered)
|
|
assert validate_init_data(init_data, _BOT_TOKEN, max_age_seconds=600) is None
|
|
|
|
|
|
def test_wrong_bot_token_rejected() -> None:
|
|
fields = {"auth_date": str(int(time.time()))}
|
|
init_data = _init_data(fields, bot_token=_BOT_TOKEN)
|
|
assert validate_init_data(init_data, "wrong-token", max_age_seconds=600) is None
|
|
|
|
|
|
def test_expired_auth_date_rejected() -> None:
|
|
stale = int(time.time()) - 3600
|
|
fields = {"auth_date": str(stale)}
|
|
init_data = _init_data(fields)
|
|
assert validate_init_data(init_data, _BOT_TOKEN, max_age_seconds=600) is None
|
|
|
|
|
|
def test_fresh_auth_date_within_window_accepted() -> None:
|
|
fields = {"auth_date": str(int(time.time()) - 10)}
|
|
init_data = _init_data(fields)
|
|
assert validate_init_data(init_data, _BOT_TOKEN, max_age_seconds=600) is not None
|
|
|
|
|
|
def test_far_future_auth_date_rejected() -> None:
|
|
# A far-future auth_date is nonsense from a server-stamped field; without
|
|
# an upper bound it would count as eternally fresh.
|
|
fields = {"auth_date": str(int(time.time()) + 100_000)}
|
|
init_data = _init_data(fields)
|
|
assert validate_init_data(init_data, _BOT_TOKEN, max_age_seconds=600) is None
|
|
|
|
|
|
def test_slightly_future_auth_date_within_skew_tolerance_accepted() -> None:
|
|
# Local clock lagging Telegram's by a few seconds must not break login.
|
|
fields = {"auth_date": str(int(time.time()) + 30)}
|
|
init_data = _init_data(fields)
|
|
assert validate_init_data(init_data, _BOT_TOKEN, max_age_seconds=600) is not None
|
|
|
|
|
|
def test_missing_auth_date_rejected() -> None:
|
|
fields = {"query_id": "abc"}
|
|
init_data = _init_data(fields)
|
|
assert validate_init_data(init_data, _BOT_TOKEN, max_age_seconds=600) is None
|
|
|
|
|
|
def test_malformed_user_json_rejected() -> None:
|
|
fields = {"auth_date": str(int(time.time())), "user": "not-json"}
|
|
init_data = _init_data(fields)
|
|
assert validate_init_data(init_data, _BOT_TOKEN, max_age_seconds=600) is None
|
|
|
|
|
|
def test_empty_init_data_rejected() -> None:
|
|
assert validate_init_data("", _BOT_TOKEN, max_age_seconds=600) is None
|
|
|
|
|
|
def test_empty_bot_token_rejected() -> None:
|
|
fields = {"auth_date": str(int(time.time()))}
|
|
init_data = _init_data(fields)
|
|
assert validate_init_data(init_data, "", max_age_seconds=600) is None
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# ponytail: smallest runnable self-check; pytest is the real suite above.
|
|
user = {"id": 42}
|
|
fields = {"auth_date": str(int(time.time())), "user": json.dumps(user)}
|
|
valid_result = validate_init_data(_init_data(fields), _BOT_TOKEN, 600)
|
|
assert valid_result is not None
|
|
assert valid_result["user"] == user
|
|
assert validate_init_data(_init_data(fields), "wrong", 600) is None
|
|
print("telegram_initdata self-check OK")
|