{ pkgs, self, ... }: pkgs.testers.nixosTest { name = "tranquil-pds"; nodes.server = { config, pkgs, ... }: { imports = [self.nixosModules.default]; services.tranquil-pds = { enable = true; database.createLocally = true; settings = { server.hostname = "pds.test"; server.host = "0.0.0.0"; server.disable_rate_limiting = true; server.invite_code_required = false; server.enable_pds_hosted_did_web = true; secrets.jwt_secret = "test-jwt-secret-must-be-32-chars-long"; secrets.dpop_secret = "test-dpop-secret-must-be-32-chars-long"; secrets.master_key = "test-master-key-must-be-32-chars-long"; secrets.allow_insecure = true; }; }; services.nginx = let vhost = { locations."/" = { proxyPass = "http://127.0.0.1:3000"; proxyWebsockets = true; extraConfig = '' proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 120s; ''; }; }; in { enable = true; recommendedProxySettings = true; virtualHosts."pds.test" = vhost; virtualHosts."*.pds.test" = vhost; }; }; testScript = '' import json server.wait_for_unit("postgresql.service") server.wait_for_unit("tranquil-pds.service") server.wait_for_unit("nginx.service") server.wait_for_open_port(3000) server.wait_for_open_port(80) def xrpc(method, endpoint, *, headers=None, data=None, raw_body=None, via="nginx"): host_header = "-H 'Host: pds.test'" if via == "nginx" else "" base = "http://localhost" if via == "nginx" else "http://localhost:3000" url = f"{base}/xrpc/{endpoint}" parts = ["curl", "-s", "-w", r"'\n%{http_code}'", "-X", method, host_header] if headers: parts.extend(f"-H '{k}: {v}'" for k, v in headers.items()) if data is not None: parts.append("-H 'Content-Type: application/json'") parts.append(f"-d '{json.dumps(data)}'") if raw_body: parts.append(f"--data-binary @{raw_body}") parts.append(f"'{url}'") result = server.succeed(" ".join(parts)) lines = result.rsplit("\n", 1) body = lines[0] if len(lines) > 1 else result status = lines[1].strip() if len(lines) > 1 else "000" assert status.startswith("2"), f"xrpc {endpoint} returned HTTP {status}: {body}" return body def xrpc_json(method, endpoint, **kwargs): return json.loads(xrpc(method, endpoint, **kwargs)) def xrpc_status(endpoint, *, headers=None, via="nginx"): host_header = "-H 'Host: pds.test'" if via == "nginx" else "" base = "http://localhost" if via == "nginx" else "http://localhost:3000" url = f"{base}/xrpc/{endpoint}" parts = ["curl", "-s", "-o", "/dev/null", "-w", "'%{http_code}'", host_header] if headers: parts.extend(f"-H '{k}: {v}'" for k, v in headers.items()) parts.append(f"'{url}'") return server.succeed(" ".join(parts)).strip() def http_status(path, *, host="pds.test", via="nginx"): base = "http://localhost" if via == "nginx" else "http://localhost:3000" return server.succeed( f"curl -s -o /dev/null -w '%{{http_code}}' -H 'Host: {host}' '{base}{path}'" ).strip() def http_get(path, *, host="pds.test"): return server.succeed( f"curl -sf -H 'Host: {host}' 'http://localhost{path}'" ) def http_header(path, header, *, host="pds.test"): return server.succeed( f"curl -sI -H 'Host: {host}' 'http://localhost{path}'" f" | grep -i '^{header}:'" ).strip() # --- testing that stuff is up in general --- with subtest("service is running"): status = server.succeed("systemctl is-active tranquil-pds") assert "active" in status with subtest("data directories exist"): server.succeed("test -d /var/lib/tranquil-pds/blobs") with subtest("postgres database created"): server.succeed("sudo -u tranquil-pds psql -d tranquil-pds -c 'SELECT 1'") with subtest("healthcheck via backend"): xrpc("GET", "_health", via="backend") with subtest("healthcheck via nginx"): xrpc("GET", "_health") with subtest("describeServer"): desc = xrpc_json("GET", "com.atproto.server.describeServer") assert "availableUserDomains" in desc assert "did" in desc assert desc.get("inviteCodeRequired") == False with subtest("nginx serves frontend"): result = server.succeed("curl -sf -H 'Host: pds.test' http://localhost/") assert "/dev/null") blob_resp = xrpc_json( "POST", "com.atproto.repo.uploadBlob", headers={**auth, "Content-Type": "application/octet-stream"}, raw_body="/tmp/testblob.bin", ) assert "blob" in blob_resp, f"no blob: {blob_resp}" blob_ref = blob_resp["blob"] assert blob_ref["size"] == 4096 with subtest("export repo as car"): server.succeed( f"curl -sf -H 'Host: pds.test' " f"-o /tmp/repo.car " f"'http://localhost/xrpc/com.atproto.sync.getRepo?did={did}'" ) size = int(server.succeed("stat -c%s /tmp/repo.car").strip()) assert size > 0, "exported car is empty" with subtest("delete record"): xrpc_json("POST", "com.atproto.repo.deleteRecord", headers=auth, data={ "repo": did, "collection": "app.bsky.feed.post", "rkey": rkey, }) with subtest("deleted record gone"): code = xrpc_status( f"com.atproto.repo.getRecord?repo={did}&collection=app.bsky.feed.post&rkey={rkey}", ) assert code != "200", f"expected non-200 for deleted record, got {code}" with subtest("service still healthy after lifecycle"): xrpc("GET", "_health") ''; }