n8n 連携: 受け口を呼ぶクライアントと設定の保持を追加

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-13 03:13:01 +09:00
co-authored by Claude Opus 5.5
parent 38a3307cd4
commit c1d1d046b6
2 changed files with 220 additions and 0 deletions
+106
View File
@@ -170,5 +170,111 @@ class CollectAndMarkTest(unittest.TestCase):
self.assertEqual(sorted(o["kind"] for o in b3["overlaps"]), ["block", "row"]) # 進行中の行 (終了 = 今) とも重なる
class FakeN8n:
"""wt-import の受け口を真似る偽のサーバー。受け取った要求を記録する。"""
TOKEN = "test-token"
def __init__(self):
self.requests = []
fake = self
class Handler(BaseHTTPRequestHandler):
def log_message(self, *args):
pass
def reply(self, status, payload):
body = json.dumps(payload).encode()
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_GET(self):
fake.requests.append(("GET", self.path, self.headers.get("X-WT-Token")))
if self.headers.get("X-WT-Token") != FakeN8n.TOKEN:
return self.reply(403, {"message": "Authorization data is wrong!"})
if "from=2099" in self.path:
return self.reply(200, {"ok": False, "error": "from が to より後です"})
self.reply(200, {"ok": True, "rows": [{"id": 9, "source": "chktime:s1:1", "start_time": None}], "deletedSources": []})
def do_POST(self):
length = int(self.headers.get("Content-Length") or 0)
body = json.loads(self.rfile.read(length))
fake.requests.append(("POST", self.path, body))
if self.headers.get("X-WT-Token") != FakeN8n.TOKEN:
return self.reply(403, {})
inserted = [{"source": r["source"], "id": 100 + i} for i, r in enumerate(body["rows"])]
self.reply(200, {"ok": True, "inserted": inserted, "skipped": [], "errors": []})
self.server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
self.url = f"http://127.0.0.1:{self.server.server_address[1]}"
threading.Thread(target=self.server.serve_forever, daemon=True).start()
def close(self):
self.server.shutdown()
self.server.server_close()
class N8nClientTest(unittest.TestCase):
def setUp(self):
self.fake = FakeN8n()
def tearDown(self):
self.fake.close()
def test_rows_sends_token_and_range(self):
client = chktime.N8n(self.fake.url, FakeN8n.TOKEN)
data = client.rows(date(2026, 1, 1), date(2026, 1, 7))
self.assertEqual(data["rows"][0]["id"], 9)
method, path, token = self.fake.requests[-1]
self.assertEqual(path, "/webhook/wt-import-rows?from=2026-01-01&to=2026-01-07")
self.assertEqual(token, FakeN8n.TOKEN)
def test_wrong_token_is_403_message(self):
client = chktime.N8n(self.fake.url, "wrong")
with self.assertRaisesRegex(chktime.N8nError, "合言葉が違います"):
client.rows(date(2026, 1, 1), date(2026, 1, 1))
def test_error_payload_raises(self):
client = chktime.N8n(self.fake.url, FakeN8n.TOKEN)
with self.assertRaisesRegex(chktime.N8nError, "from が to より後です"):
client.rows(date(2099, 1, 2), date(2099, 1, 1))
def test_unreachable(self):
client = chktime.N8n("http://127.0.0.1:9", FakeN8n.TOKEN)
with self.assertRaisesRegex(chktime.N8nError, "接続できません"):
client.rows(date(2026, 1, 1), date(2026, 1, 1))
def test_missing_settings(self):
with self.assertRaisesRegex(chktime.N8nError, "URL が未設定"):
chktime.N8n("", "x").rows(date(2026, 1, 1), date(2026, 1, 1))
with self.assertRaisesRegex(chktime.N8nError, "合言葉が未設定"):
chktime.N8n(self.fake.url, "").rows(date(2026, 1, 1), date(2026, 1, 1))
class N8nHolderTest(unittest.TestCase):
def test_public_hides_token_and_update_keeps_it(self):
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "n8n.json"
holder = chktime.N8nHolder(path)
self.assertEqual(holder.settings(), (30, 5))
pub = holder.update({"url": "https://n8n.example/", "token": "secret", "gapMinutes": 20, "minMinutes": 3})
self.assertTrue(pub["hasToken"])
self.assertNotIn("secret", json.dumps(pub))
self.assertEqual(holder.settings(), (20, 3))
holder.update({"token": ""}) # 空なら既存を残す
self.assertEqual(json.loads(path.read_text(encoding="utf-8"))["token"], "secret")
self.assertEqual(holder.client.url, "https://n8n.example")
def test_rejects_bad_minutes(self):
with tempfile.TemporaryDirectory() as tmp:
holder = chktime.N8nHolder(Path(tmp) / "n8n.json")
for patch in ({"gapMinutes": 0}, {"gapMinutes": 241}, {"minMinutes": -1}, {"gapMinutes": "abc"}):
with self.assertRaises(chktime.N8nError):
holder.update(patch)
if __name__ == "__main__":
unittest.main()