"""Exercise the stdlib Python client and both workloads against a temporary server.""" import os from pathlib import Path import queue import secrets import socket import struct import subprocess import sys import tempfile import threading import unittest from unittest.mock import patch ROOT = Path(__file__).resolve().parents[1] sys.path[:0] = [str(ROOT / "clients/python"), str(ROOT / "examples/python")] from ratdb import Client, ProtocolError, RatDBError from workloads import crm, website class FramingTests(unittest.TestCase): def test_response_deadline_spans_header_and_body(self): local, peer = socket.socketpair() with local, peer: peer.sendall(struct.pack(">I", 100) + b"{") with patch("ratdb.socket.create_connection", return_value=local), \ patch("ratdb.time.monotonic", side_effect=[0.0, 0.01, 0.11]): with self.assertRaises(TimeoutError): Client("admin", "fixture", timeout=0.1) self.assertEqual(local.fileno(), -1) def test_bad_or_truncated_response_closes_session(self): for response in ( struct.pack(">I", 1048577), struct.pack(">I", 12) + b"short", struct.pack(">I", 1) + b"\xff", struct.pack(">I", 2) + b"{}", ): with self.subTest(response=response): local, peer = socket.socketpair() with local, peer: peer.sendall(response) peer.shutdown(socket.SHUT_WR) with patch("ratdb.socket.create_connection", return_value=local): with self.assertRaises(ProtocolError): Client("admin", "fixture") self.assertEqual(local.fileno(), -1) class ClientTests(unittest.TestCase): def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.addCleanup(self.tmp.cleanup) self.db = Path(self.tmp.name) / "instance.ratdb" self.password = secrets.token_urlsafe(24) self.proc = None self.addCleanup(self.stop) for args in (["init", self.db], ["setup", self.db, "admin"]): subprocess.run([EXE, *args], check=True, capture_output=True, timeout=20, env={**os.environ, "RATDB_PASSWORD": self.password}) self.start() def start(self): self.proc = subprocess.Popen( [EXE, "serve", self.db, "--bind", "127.0.0.1:0"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, ) ready = queue.Queue() threading.Thread(target=lambda: ready.put(self.proc.stdout.readline()), daemon=True).start() line = ready.get(timeout=15) addr = line.split("listening on ")[1].split()[0] self.port = int(addr.rsplit(":", 1)[1]) def stop(self): if self.proc is not None: self.proc.kill() self.proc.communicate(timeout=10) self.proc = None def connect(self): return Client("admin", self.password, port=self.port) def test_workloads_and_restart(self): with self.connect() as db: session_id = website(db) crm(db) self.stop() self.start() with self.connect() as db: db.select_warren("crm") db.select_den("contacts") self.assertEqual(db.get("contact:1")["stage"], "qualified") db.select_den("notes") self.assertEqual(db.get("note:1")["contact_id"], "contact:1") db.select_den("activities") self.assertIsNone(db.get("act:1")) db.select_warren("website") db.select_den("sessions") self.assertEqual(db.get(session_id)["user_id"], "sample-user") db.select_den("users") with self.assertRaises(RatDBError): db.insert("sample-user", {"username": "duplicate"}) def test_rejections_and_session_reuse(self): with self.assertRaises(RatDBError) as failure: Client("admin", "incorrect", port=self.port) self.assertEqual(failure.exception.code, "authentication") with self.assertRaises(ValueError): Client("admin", self.password, host="192.0.2.1", port=self.port) with self.connect() as db: with self.assertRaises(RatDBError): db.get("no-den") db.select_warren("garage") db.select_den("cars") with self.assertRaises(ValueError): db.select_den("/wrong") with self.assertRaises(ValueError): db.put("too-large", {"text": "a" * 1048576}) db.put("id", {"text": "café"}) self.assertEqual(db.get("id")["text"], "café") if __name__ == "__main__": EXE = Path(sys.argv.pop(1)).resolve() unittest.main()