"""常驻监听服务端单测:POST/GET /projects/{id}/resident-watches、 PATCH、POST pause / resume / end。不跑真实模型、不联网。 骨架与 test_automations_server.py 同款(本地临时端口 + 临时 SQLite)。""" import http.client import json import os import sqlite3 import sys import tempfile import threading import unittest from pathlib import Path try: from cloud.auth import AuthStore from cloud.server import create_server except ImportError: # discover -s cloud/tests 时把项目根目录补进 sys.path sys.path.insert(0, str(Path(__file__).resolve().parents[2])) from cloud.auth import AuthStore from cloud.server import create_server URL = "https://example.com/data" class ResidentServerTestCase(unittest.TestCase): def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.db_path = os.path.join(self.tmp.name, "control.db") self.auth = AuthStore(self.db_path) self.owner_id, self.owner_key = self.auth.create_user("owner") self.outsider_id, self.outsider_key = self.auth.create_user( "outsider") self.server = create_server(self.db_path, "127.0.0.1", 0) self.port = self.server.server_address[1] self.thread = threading.Thread( target=self.server.serve_forever, daemon=True) self.thread.start() status, self.project = self._req( "POST", "/projects", {"name": "demo"}, self.owner_key) self.assertEqual(status, 201) def tearDown(self): self.server.shutdown() self.server.server_close() self.thread.join(timeout=5) try: self.auth.conn.close() except Exception: pass self.tmp.cleanup() def _req(self, method, path, body=None, key=None): conn = http.client.HTTPConnection( "127.0.0.1", self.port, timeout=10) hdrs = {} payload = None if body is not None: payload = json.dumps(body).encode("utf-8") hdrs["Content-Type"] = "application/json" if key is not None: hdrs["X-API-Key"] = key try: conn.request(method, path, body=payload, headers=hdrs) resp = conn.getresponse() data = resp.read() finally: conn.close() try: parsed = json.loads(data.decode("utf-8")) if data else None except ValueError: parsed = None return resp.status, parsed def _url(self, suffix=""): return "/projects/%s/resident-watches%s" % ( self.project["id"], suffix) def _create(self, **extra): body = {"name": "watch1", "target_url": URL, "prompt": "变化时汇报"} body.update(extra) return self._req("POST", self._url(), body, self.owner_key) def _audit_actions(self): conn = sqlite3.connect(self.db_path) try: rows = conn.execute( "SELECT action FROM audit_log WHERE project_id = ?", (self.project["id"],)).fetchall() finally: conn.close() return [r[0] for r in rows] def test_create_list_get_and_validation(self): status, watch = self._create(interval_seconds=3600) self.assertEqual(status, 201, watch) self.assertEqual(watch["status"], "running") self.assertEqual(watch["target_url"], URL) self.assertTrue(watch["due"]) self.assertIsNotNone(watch["next_poll_at"]) self.assertIn("resident_watch_created", self._audit_actions()) status, body = self._req("GET", self._url(), None, self.owner_key) self.assertEqual(status, 200) self.assertEqual(len(body["resident_watches"]), 1) self.assertEqual(body["due_count"], 1) status, body = self._req( "GET", self._url("/" + watch["id"]), None, self.owner_key) self.assertEqual(status, 200) self.assertEqual(body["id"], watch["id"]) self.assertEqual(body["polls"], []) for bad in ( {"name": "x", "target_url": "ftp://example.com/x", "prompt": "p"}, {"name": "x", "target_url": "https://localhost/x", "prompt": "p"}, {"name": "x", "target_url": URL, "prompt": "p", "interval_seconds": 30}, {"name": "watch1", "target_url": URL, "prompt": "p"}, {"target_url": URL, "prompt": "p"}, {"name": "x", "prompt": "p"}): status, _ = self._req("POST", self._url(), bad, self.owner_key) self.assertEqual(status, 400, bad) status, _ = self._req( "GET", self._url("/nope"), None, self.owner_key) self.assertEqual(status, 404) def test_pause_resume_end_lifecycle_and_terminal_409(self): status, watch = self._create() self.assertEqual(status, 201) wid = watch["id"] status, body = self._req( "POST", self._url("/%s/pause" % wid), {}, self.owner_key) self.assertEqual(status, 200, body) self.assertEqual(body["status"], "paused") self.assertEqual(body["paused_reason"], "user_paused") self.assertIsNone(body["next_poll_at"]) self.assertFalse(body["due"]) status, body = self._req( "POST", self._url("/%s/pause" % wid), {}, self.owner_key) self.assertEqual(status, 409) status, body = self._req( "POST", self._url("/%s/resume" % wid), {}, self.owner_key) self.assertEqual(status, 200, body) self.assertEqual(body["status"], "running") self.assertIsNone(body["paused_reason"]) self.assertTrue(body["due"]) status, body = self._req( "POST", self._url("/%s/end" % wid), {}, self.owner_key) self.assertEqual(status, 200, body) self.assertEqual(body["status"], "ended") for action in ("pause", "resume", "end"): status, body = self._req( "POST", self._url("/%s/%s" % (wid, action)), {}, self.owner_key) self.assertEqual(status, 409, action) self.assertIn("terminal", body["error"]) actions = self._audit_actions() self.assertIn("resident_watch_paused", actions) self.assertIn("resident_watch_resumed", actions) self.assertIn("resident_watch_ended", actions) def test_update_and_outsider_forbidden(self): status, watch = self._create() self.assertEqual(status, 201) wid = watch["id"] status, body = self._req( "PATCH", self._url("/" + wid), {"interval_seconds": 7200}, self.owner_key) self.assertEqual(status, 200, body) self.assertEqual(body["interval_seconds"], 7200) status, _ = self._req( "POST", self._url(), {"name": "x", "target_url": URL, "prompt": "p"}, self.outsider_key) self.assertIn(status, (401, 403, 404)) if __name__ == "__main__": unittest.main()