добавил систему миграций и связь одной записи Redmine с несколькими активностями Clockify и запретил отправку трудочасов при инициализации базы данных
This commit is contained in:
78
tests/test_migrations.py
Normal file
78
tests/test_migrations.py
Normal file
@@ -0,0 +1,78 @@
|
||||
"""Тесты применения миграций локальной базы данных."""
|
||||
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
from unittest import TestCase
|
||||
|
||||
from db.db import Database
|
||||
|
||||
|
||||
class MigrationTestCase(TestCase):
|
||||
"""Проверяет миграцию связи Redmine с активностями Clockify."""
|
||||
|
||||
def test_migration_creates_one_to_many_relation_and_runs_once(self):
|
||||
"""Миграция переносит связь и не выполняется повторно."""
|
||||
with TemporaryDirectory() as directory:
|
||||
db_path = Path(directory) / "tracker.db"
|
||||
db = Database(str(db_path), "config/RedmineTracker.sql")
|
||||
|
||||
db.run_migrations("migrations")
|
||||
db.run_migrations("migrations")
|
||||
|
||||
activity_columns = {
|
||||
row[1]
|
||||
for row in db.cursor.execute("PRAGMA table_info(activity)").fetchall()
|
||||
}
|
||||
time_entry_columns = {
|
||||
row[1]
|
||||
for row in db.cursor.execute("PRAGMA table_info(time_entry)").fetchall()
|
||||
}
|
||||
migration = db.cursor.execute(
|
||||
"SELECT name, executed_at FROM migration"
|
||||
).fetchall()
|
||||
activity_foreign_keys = {
|
||||
row[3]: row[2]
|
||||
for row in db.cursor.execute(
|
||||
"PRAGMA foreign_key_list(activity)"
|
||||
).fetchall()
|
||||
}
|
||||
|
||||
self.assertIn("time_entry_id", activity_columns)
|
||||
self.assertNotIn("activity_id", time_entry_columns)
|
||||
self.assertEqual(activity_foreign_keys["time_entry_id"], "time_entry")
|
||||
self.assertEqual(len(migration), 1)
|
||||
self.assertEqual(
|
||||
migration[0][0],
|
||||
"a7f3c9e1_redmine_clockify_relation.py",
|
||||
)
|
||||
self.assertIsNotNone(migration[0][1])
|
||||
db.con.close()
|
||||
|
||||
def test_multiple_activities_can_reference_one_time_entry(self):
|
||||
"""Несколько активностей Clockify могут ссылаться на одну запись Redmine."""
|
||||
with TemporaryDirectory() as directory:
|
||||
db_path = Path(directory) / "tracker.db"
|
||||
db = Database(str(db_path), "config/RedmineTracker.sql")
|
||||
db.run_migrations("migrations")
|
||||
db.cursor.execute("PRAGMA foreign_keys = OFF")
|
||||
db.cursor.execute(
|
||||
"""INSERT INTO time_entry(
|
||||
id, description, date, time_spent,
|
||||
task_id, activity_type_id, author_id
|
||||
) VALUES(42, 'Работа', '2026-07-14', 1, 51906, 9, 1)"""
|
||||
)
|
||||
for activity_id in ("first", "second"):
|
||||
db.cursor.execute(
|
||||
"""INSERT INTO activity(
|
||||
id, description, author_id, time_spent, date_start,
|
||||
tag_id, time_entry_id
|
||||
) VALUES(?, 'Работа', 1, 0.5, '2026-07-14', 'tag', 42)""",
|
||||
(activity_id,),
|
||||
)
|
||||
|
||||
count = db.cursor.execute(
|
||||
"SELECT COUNT(*) FROM activity WHERE time_entry_id = 42"
|
||||
).fetchone()[0]
|
||||
|
||||
self.assertEqual(count, 2)
|
||||
db.con.close()
|
||||
43
tests/test_redmine.py
Normal file
43
tests/test_redmine.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""Тесты выборочной загрузки задач Redmine."""
|
||||
|
||||
from unittest import TestCase
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import requests
|
||||
|
||||
from api.redmine import RedmineAPI
|
||||
|
||||
|
||||
class RedmineAPITestCase(TestCase):
|
||||
"""Проверяет запрос только необходимых приложению задач."""
|
||||
|
||||
def test_get_issues_requests_only_given_issue_ids(self):
|
||||
"""Задачи загружаются отдельно и только по номерам тегов Clockify."""
|
||||
redmine = RedmineAPI.__new__(RedmineAPI)
|
||||
redmine.get_issue = Mock(side_effect=lambda issue_id: issue_id)
|
||||
|
||||
result = redmine.get_issues({60130, 51906})
|
||||
|
||||
self.assertEqual(result, [51906, 60130])
|
||||
self.assertEqual(
|
||||
[call.args[0] for call in redmine.get_issue.call_args_list],
|
||||
[51906, 60130],
|
||||
)
|
||||
|
||||
def test_get_issues_skips_unavailable_issue(self):
|
||||
"""Недоступная задача не мешает загрузить остальные указанные задачи."""
|
||||
redmine = RedmineAPI.__new__(RedmineAPI)
|
||||
redmine.get_issue = Mock(side_effect=[51906, None])
|
||||
|
||||
self.assertEqual(redmine.get_issues({51906, 60130}), [51906])
|
||||
|
||||
@patch("api.redmine.requests.get")
|
||||
def test_get_issue_returns_none_on_timeout(self, get):
|
||||
"""Сетевой таймаут одной задачи не прерывает обновление БД."""
|
||||
get.side_effect = requests.ConnectTimeout("Redmine недоступен")
|
||||
redmine = RedmineAPI.__new__(RedmineAPI)
|
||||
redmine.base_url = "https://redmine.example/"
|
||||
redmine.issue = "issues/{id}.json"
|
||||
redmine.token = "token"
|
||||
|
||||
self.assertIsNone(redmine.get_issue(38243))
|
||||
@@ -92,11 +92,15 @@ class TrackingServiceTestCase(TestCase):
|
||||
is_debug=False,
|
||||
)
|
||||
|
||||
service._save_tracked_activity(aggregated_activity)
|
||||
service._save_tracked_activity(aggregated_activity, 42)
|
||||
|
||||
self.assertTrue(all(activity.is_tracked for activity in activities))
|
||||
db.insert_activities.assert_called_once_with(activities)
|
||||
db.insert_time_entries.assert_called_once_with(activities)
|
||||
db.insert_time_entry.assert_called_once_with(
|
||||
aggregated_activity,
|
||||
activities,
|
||||
42,
|
||||
)
|
||||
|
||||
def test_activities_for_different_tasks_are_not_merged(self):
|
||||
"""Активности разных задач остаются отдельными записями Redmine."""
|
||||
|
||||
Reference in New Issue
Block a user