1
0
forked from BRT/arc

Add tests for monitoring log format: validate task_success/task_error JSON structure and improve database documentation with schema and migration details.

This commit is contained in:
slava 2026-06-04 13:36:18 +05:00
parent a72a00cc3f
commit a0583c4966
2 changed files with 281 additions and 0 deletions

80
docs/database.md Normal file
View File

@ -0,0 +1,80 @@
# База данных — схема и миграции
SQLite-база, путь задаётся в `config.ini` (`db/dbPath`). Управляется через `DatabaseManager` (singleton) + DAO-слой.
---
## Инициализация схемы
При каждом запуске приложения вызывается `DatabaseManager::initSchema()` (`database/DatabaseManager.cpp`).
Именно здесь происходят все миграции — до открытия главного окна.
При **обновлении из меню** приложение скачивает новый `.exe`, применяет его и перезапускается, поэтому `initSchema()` тоже выполняется заново на новой версии.
---
## Что применяется автоматически при обновлении
| Изменение | Применяется? | Как |
|-----------|-------------|-----|
| Новая таблица | Да | `CREATE TABLE IF NOT EXISTS` |
| Новая колонка | Да | `ALTER TABLE ... ADD COLUMN` |
| Удаление таблицы | Да | `DROP TABLE IF EXISTS` |
| Удаление колонки | Нет | SQLite не поддерживает |
| Переименование колонки | Нет | Нужна пересборка таблицы |
| Изменение типа колонки | Нет | Нужна пересборка таблицы |
| Изменение DEFAULT у существующей колонки | Нет | `CREATE TABLE IF NOT EXISTS` не выполняется повторно |
---
## Паттерн добавления колонки
```cpp
// 1. Добавить колонку в CREATE TABLE IF NOT EXISTS (для новых установок)
q.exec(R"(CREATE TABLE IF NOT EXISTS my_table (
...
new_col TEXT DEFAULT ''
))");
// 2. Добавить ALTER TABLE для существующих БД (ошибка "column already exists" игнорируется)
q.exec("ALTER TABLE my_table ADD COLUMN new_col TEXT DEFAULT ''");
```
Оба шага обязательны: `CREATE TABLE` покрывает свежие установки, `ALTER TABLE` — обновление существующей БД.
---
## Сложные миграции (изменение типа, переименование)
Для изменений, которые SQLite не поддерживает напрямую, используется ручная миграция с пересозданием таблицы и `PRAGMA user_version` для версионирования:
```cpp
QSqlQuery q(db);
q.exec("PRAGMA user_version");
q.next();
int version = q.value(0).toInt();
if (version < 2) {
// пересоздаём таблицу
q.exec("ALTER TABLE my_table RENAME TO my_table_old");
q.exec("CREATE TABLE my_table (...)");
q.exec("INSERT INTO my_table SELECT ... FROM my_table_old");
q.exec("DROP TABLE my_table_old");
q.exec("PRAGMA user_version = 2");
}
```
---
## Таблицы
| Таблица | Описание |
|---------|----------|
| `devices` | Android-устройства, подключённые по ADB |
| `bank_profiles` | Банковские профили (приложение + устройство) |
| `materials` | Карты/счета профиля |
| `transactions` | Транзакции по картам |
| `events` | Задачи из очереди API |
| `banks` | Справочник банков (alias, name, nspk_name) |
| `general_logs` | Общий лог приложения |
| `settings` | Настройки key-value |

View File

@ -0,0 +1,201 @@
#!/usr/bin/env python3
"""
Test: monitoring log format for task_success / task_error.
Starts the mock server in a subprocess, sends POST /monitoring/log
requests that replicate what ARC C++ sends, then fetches the logs via
/_admin/monitoring_logs and asserts the JSON format.
Usage:
python3 test_monitoring_log_format.py
Requirements: flask (same as mock_server.py), requests
pip install flask requests
"""
import json
import os
import subprocess
import sys
import time
import requests
PORT = 18889
BASE = f"http://127.0.0.1:{PORT}"
TX = {
"amount": 5000,
"bank_participator_id": "test-participator",
"bank_transaction_id": "dushanbe_2026-06-03T18:34:18Z",
"created_at": "2026-06-03T18:34:18Z",
"currency": 972,
"extra": {"fee": 0},
"real_created_at": "2026-06-03T18:34:34Z",
"status": "completed",
"transaction_type": "bank",
}
def build_task_success_message(tx: dict, *, pretty: bool) -> str:
"""Build the monitoring message the way C++ sendTaskSuccess does.
pretty=True new format (after the fix): 'JSON: ' + indented JSON
pretty=False old format (before the fix): compact JSON on one line
"""
lines = [
"<b>[TASK_SUCCESS] [FETCH_TRANSACTIONS] [DUSHANBE_CITY_BANK]</b>",
"Desktop: f7ff2724-78bd-4783-876d-faece1106c6a",
"DesktopVersion: 1.0.118",
"Profile: 4b9a2bbd-7201-49cb-beb3-7839f21acc2e",
"Material: bb537b06-155b-4e39-82ff-4b14f5b7c327",
"Phone: +992919275251",
"AppVersion: 3.2.41",
]
if pretty:
lines.append("JSON: " + json.dumps(tx, indent=2, ensure_ascii=False))
else:
lines.append(json.dumps(tx, ensure_ascii=False, separators=(",", ":")))
return "\n".join(lines)
def build_task_error_message(tx: dict) -> str:
"""Build the monitoring message the way C++ sendTaskError does."""
lines = [
"<b>[TASK_ERROR] [FETCH_TRANSACTIONS] [DUSHANBE_CITY_BANK]</b>",
"Desktop: f7ff2724-78bd-4783-876d-faece1106c6a",
"DesktopVersion: 1.0.118",
"Profile: 4b9a2bbd-7201-49cb-beb3-7839f21acc2e",
"Material: bb537b06-155b-4e39-82ff-4b14f5b7c327",
"Phone: +992919275251",
"AppVersion: 3.2.41",
"Error: some error description",
"JSON: " + json.dumps(tx, indent=2, ensure_ascii=False),
]
return "\n".join(lines)
def post_log(msg: str, event: str = "task_success") -> None:
resp = requests.post(
f"{BASE}/monitoring/log",
data={"type": "info", "event": event, "message": msg},
timeout=5,
)
resp.raise_for_status()
def get_logs(event: str) -> list:
resp = requests.get(f"{BASE}/_admin/monitoring_logs", params={"event": event}, timeout=5)
resp.raise_for_status()
return resp.json()
# ------------------------------------------------------------------ tests
PASS = "\033[32mPASS\033[0m"
FAIL = "\033[31mFAIL\033[0m"
def assert_eq(a, b, label):
if a == b:
print(f" {PASS}: {label}")
else:
print(f" {FAIL}: {label}")
print(f" expected: {b!r}")
print(f" got: {a!r}")
sys.exit(1)
def assert_true(cond, label):
assert_eq(bool(cond), True, label)
def test_new_format_is_pretty():
msg = build_task_success_message(TX, pretty=True)
post_log(msg, event="task_success")
logs = get_logs("task_success")
assert_true(len(logs) >= 1, "at least one task_success log received")
latest = logs[-1]["message"]
assert_true("JSON: " in latest, "message contains 'JSON: ' prefix")
json_part = latest.split("JSON: ", 1)[1]
parsed = json.loads(json_part)
assert_eq(parsed["amount"], 5000, "JSON 'amount' parsed correctly")
assert_eq(parsed["status"], "completed", "JSON 'status' parsed correctly")
assert_true("\n" in json_part, "JSON is multi-line (indented)")
assert_true(" " in json_part, "JSON has two-space indentation")
compact = json.dumps(TX, ensure_ascii=False, separators=(",", ":"))
assert_true(compact not in latest, "compact JSON not present (was replaced by pretty)")
def test_new_format_matches_error_style():
success_msg = build_task_success_message(TX, pretty=True)
error_msg = build_task_error_message(TX)
success_json = success_msg.split("JSON: ", 1)[1]
error_json = error_msg.split("JSON: ", 1)[1]
assert_eq(success_json, error_json, "task_success JSON block equals task_error JSON block")
def test_old_format_is_compact():
"""Sanity check: confirm old format had no 'JSON: ' prefix and was compact."""
msg = build_task_success_message(TX, pretty=False)
assert_true("JSON: " not in msg, "old format has no 'JSON: ' prefix")
assert_true(
json.dumps(TX, ensure_ascii=False, separators=(",", ":")) in msg,
"old format contains compact JSON",
)
# ------------------------------------------------------------------ runner
def wait_for_server(timeout=10):
deadline = time.time() + timeout
while time.time() < deadline:
try:
requests.get(f"{BASE}/_admin/state", timeout=1)
return True
except Exception:
time.sleep(0.2)
return False
def main():
here = os.path.dirname(os.path.abspath(__file__))
server = subprocess.Popen(
[sys.executable, os.path.join(here, "mock_server.py"), "--port", str(PORT)],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
try:
if not wait_for_server():
print("ERROR: mock server did not start in time")
sys.exit(1)
print("Running tests against mock server...")
print()
print("test_old_format_is_compact")
test_old_format_is_compact()
print("test_new_format_is_pretty")
test_new_format_is_pretty()
print("test_new_format_matches_error_style")
test_new_format_matches_error_style()
print()
print("All tests passed.")
finally:
server.terminate()
server.wait()
if __name__ == "__main__":
main()