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:
parent
a72a00cc3f
commit
a0583c4966
80
docs/database.md
Normal file
80
docs/database.md
Normal 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 |
|
||||
201
tests/mock_server/test_monitoring_log_format.py
Normal file
201
tests/mock_server/test_monitoring_log_format.py
Normal 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()
|
||||
Loading…
Reference in New Issue
Block a user