import base64 import hashlib import io import json import os import tempfile import time import unittest from contextlib import redirect_stdout from datetime import datetime, timezone from _common import SETTINGS, agent, eva_cfg, no_sleep from fake_hub import FakeHub from fake_relay import FakeRelay class FolderTransportTest(unittest.TestCase): def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.root = os.path.join(self.tmp.name, "My Shared Files", "jira-bridge") # пробелы в пути os.makedirs(self.root) self.hub = FakeHub(SETTINGS) self.relay = None def tearDown(self): if self.relay: self.relay.stop() self.hub.stop() self.tmp.cleanup() def client(self, timeout=5): cfg = {"hub_url": "folder:" + self.root, "hub_token": self.hub.token, "timeout": timeout} return agent.HubClient(cfg) def run_relay(self, mode="forward", delay=0.0): self.relay = FakeRelay(self.root, self.hub.base_url, mode, delay).start() return self.relay def listing(self, sub): return sorted(os.listdir(os.path.join(self.root, sub))) def test_settings_through_folder(self): self.run_relay() hub = self.client() self.assertIsNotNone(hub.folder) self.assertEqual(hub.settings()["settings_version"], 1) self.assertEqual(self.listing("resp"), []) self.assertEqual(self.listing("req"), []) req = self.relay.requests[0] self.assertEqual(req["method"], "GET") self.assertEqual(req["path"], "/agent/settings") self.assertEqual(req["authorization"], "Bearer " + self.hub.token) self.assertEqual(len(req["id"]), 32) def test_error_status_raises_http_error(self): self.run_relay() with self.assertRaises(agent.HttpError) as e: self.client().result(999, {"status": "done"}) self.assertEqual(e.exception.code, 404) def test_claim_deadline_at_least_45_seconds(self): self.run_relay() before = time.time() self.client(timeout=5).claim(2, 1, None) deadline = datetime.fromisoformat(self.relay.requests[0]["deadline"]) self.assertIsNotNone(deadline.tzinfo) self.assertGreaterEqual(deadline.timestamp() - before, 44.5) def test_no_answer_gives_connection_error_and_removes_request(self): hub = self.client(timeout=0.5) t0 = time.monotonic() with self.assertRaises(ConnectionError) as e: hub.settings() self.assertLess(time.monotonic() - t0, 2) self.assertIn("не ответил через папку", str(e.exception)) self.assertEqual([n for n in self.listing("req") if not n.startswith(".")], []) def test_missing_folder_gives_connection_error(self): cfg = {"hub_url": "folder:" + os.path.join(self.tmp.name, "нет"), "hub_token": "t", "timeout": 5} with self.assertRaises(ConnectionError) as e: agent.HubClient(cfg).settings() self.assertIn("нет папки", str(e.exception)) def test_bad_json_response_gives_connection_error(self): self.run_relay("garbage") hub = self.client() hub.folder.BAD_JSON_WAIT = 0.3 with self.assertRaises(ConnectionError) as e: hub.settings() self.assertIn("нечитаемый ответ", str(e.exception)) def _bad_response(self, payload): path = os.path.join(self.root, "resp-bad.json") with open(path, "w", encoding="utf-8") as f: json.dump(payload, f) ft = agent.FolderTransport(self.root, "Bearer x") ft.BAD_JSON_WAIT = 0.3 return ft, path def test_wrong_status_type_is_unreadable(self): for payload in ({"id": "x", "status": "ok", "body": "{}"}, {"id": "x", "status": True, "body": "{}"}, {"id": "x", "status": 200, "body": 5}): ft, path = self._bad_response(payload) with self.assertRaises(ConnectionError) as e: ft._take(path) self.assertIn("нечитаемый ответ", str(e.exception)) def test_no_answer_message_has_no_hub_prefix(self): hub = self.client(timeout=0.3) with self.assertRaises(ConnectionError) as e: hub.settings() self.assertFalse(str(e.exception).startswith("хаб")) # цикл сам пишет «хаб: …» (O-3 agent#8) def test_late_response_is_not_reused(self): self.run_relay("late", delay=1.0) with self.assertRaises(ConnectionError): self.client(timeout=0.3).settings() time.sleep(1.3) # опоздавший ответ лёг в resp/ late = self.listing("resp") self.assertEqual(len(late), 1) self.relay.mode = "forward" self.assertEqual(self.client().settings()["settings_version"], 1) self.assertEqual(self.listing("resp"), late) # чужой (опоздавший) ответ не тронут def test_foreign_files_untouched(self): resp_dir = os.path.join(self.root, "resp") os.makedirs(resp_dir, exist_ok=True) foreign = os.path.join(resp_dir, "0" * 32 + ".json") with open(foreign, "w", encoding="utf-8") as f: json.dump({"id": "0" * 32, "status": 200, "body": "{}"}, f) self.run_relay() self.client().settings() self.assertTrue(os.path.exists(foreign)) def test_http_url_keeps_http_transport(self): cfg = {"hub_url": self.hub.base_url, "hub_token": self.hub.token, "timeout": 5} hub = agent.HubClient(cfg) self.assertIsNone(hub.folder) self.assertEqual(hub.settings()["settings_version"], 1) def test_wiki_pages_through_folder(self): self.run_relay() pages = [{"code": "DOC-1", "name": "Корень", "html": "
" + "я" * 200000 + "
"}] got = self.client().wiki_pages(5, pages) self.assertEqual(got, {"accepted": 1}) self.assertEqual(self.hub.pages, [{"command_id": 5, "pages": pages}]) self.assertEqual(self.relay.requests[0]["path"], "/agent/wiki/pages") def test_wiki_pages_over_http(self): hub = agent.HubClient({"hub_url": self.hub.base_url, "hub_token": self.hub.token, "timeout": 5}) self.assertEqual(hub.wiki_pages(5, [{"code": "DOC-1"}]), {"accepted": 1}) self.assertEqual(self.hub.pages[0]["command_id"], 5) def test_wiki_pages_rejected_is_http_error(self): self.hub.reject_pages = (422, {"error": "bad_page"}) hub = agent.HubClient({"hub_url": self.hub.base_url, "hub_token": self.hub.token, "timeout": 5}) with self.assertRaises(agent.HttpError) as e: hub.wiki_pages(5, [{}]) self.assertEqual(e.exception.code, 422) def test_upload_file_through_folder(self): self.run_relay() data = os.urandom(300 * 1024) got = self.client().upload_file(data) self.assertEqual(got, {"file": hashlib.sha256(data).hexdigest(), "size": len(data)}) self.assertEqual(self.hub.uploaded, [data]) self.assertEqual(self.relay.requests[0]["path"], "/agent/files") def test_upload_file_over_http(self): hub = agent.HubClient({"hub_url": self.hub.base_url, "hub_token": self.hub.token, "timeout": 5}) self.assertEqual(hub.upload_file(b""), {"file": hashlib.sha256(b"").hexdigest(), "size": 0}) self.assertEqual(self.hub.uploaded, [b""]) class FolderIntegrationTest(unittest.TestCase): def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.root = os.path.join(self.tmp.name, "My Shared Files", "jira-bridge") os.makedirs(self.root) self.hub = FakeHub(SETTINGS) self.relay = FakeRelay(self.root, self.hub.base_url).start() def tearDown(self): self.relay.stop() self.hub.stop() self.tmp.cleanup() def cfg(self): return eva_cfg(hub_url="folder:" + self.root, hub_token=self.hub.token) def test_version(self): self.assertEqual(agent.AGENT_VERSION, 1) def test_runner_through_folder(self): cid = self.hub.enqueue("wiki_fetch", {"url": "https://eva.example.ru/files/a.png", "name": "a.png"}) cfg = self.cfg() journal = agent.Journal(os.path.join(self.tmp.name, "journal.jsonl")) runner = agent.Runner(cfg, agent.HubClient(cfg), agent.EvaClient(cfg), journal) runner.sleep = lambda s: None runner.eva.sleep = no_sleep runner.idle_limit = 1 runner.loop() self.assertEqual(self.hub.results[cid]["status"], "failed") # заглушка Executor self.assertEqual(self.hub.results[cid]["error"], "wiki_fetch: не реализовано") self.assertEqual(self.hub.results[cid]["agent_version"], agent.AGENT_VERSION) self.assertIn(("acked", cid), [(r["kind"], r["command_id"]) for r in journal.records()]) def test_check_reports_folder(self): out = io.StringIO() with redirect_stdout(out): code = agent.cmd_check(self.cfg()) self.assertEqual(code, 0) self.assertIn("хаб: доступен через папку %s, настройки v1" % self.root, out.getvalue()) def test_check_folder_silent_relay(self): self.relay.stop() self.relay = FakeRelay(self.root, self.hub.base_url, "silent").start() out = io.StringIO() t0 = time.monotonic() with redirect_stdout(out): code = agent.cmd_check(self.cfg()) self.assertEqual(code, 1) self.assertLess(time.monotonic() - t0, 13) self.assertIn("хаб: НЕДОСТУПЕН", out.getvalue()) self.assertIn("не ответил через папку", out.getvalue()) def test_relative_folder_url_rejected(self): path = os.path.join(self.tmp.name, "agent.json") for url in ("folder:relative/path", "folder:"): cfg = self.cfg() cfg["hub_url"] = url with open(path, "w", encoding="utf-8") as f: json.dump(cfg, f) with self.assertRaises(SystemExit) as e: agent.load_config(path) self.assertIn("абсолютный путь", str(e.exception.code)) if __name__ == "__main__": unittest.main()