at protocol indexer with flexible filtering, xrpc queries, and a cursor-backed event stream, built on fjall
at-protocol
atproto
indexer
rust
fjall
1#!/usr/bin/env nu
2
3def test-repos [url: string] {
4 print "verifying /repos pagination and filtering..."
5
6 # 1. Test limit
7 print " testing limit=1..."
8 let items = (http get $"($url)/repos?limit=1" | from json -o)
9 let count = ($items | length)
10 print $" count: ($count)"
11 if $count != 1 {
12 print " FAILED: expected 1 item"
13 exit 1
14 }
15
16 # 2. Test partition=all
17 print " testing partition=all..."
18 let all_items = (http get $"($url)/repos?partition=all" | from json -o)
19 print $" count: ($all_items | length)"
20
21 # 3. Test cursor (if we have items)
22 if ($all_items | length) > 1 {
23 let first_did = ($all_items | get 0).did
24 print $" testing cursor with did ($first_did)..."
25 let cursor_items = (http get $"($url)/repos?cursor=($first_did)&limit=1" | from json -o)
26 if ($cursor_items | length) > 0 {
27 let next_did = ($cursor_items | get 0).did
28 if $first_did == $next_did {
29 print " FAILED: cursor did should be excluded"
30 exit 1
31 }
32 print $" next did: ($next_did)"
33 }
34 }
35
36 # 4. Test partition=pending
37 print " testing partition=pending..."
38 let pending_items = (http get $"($url)/repos?partition=pending" | from json -o)
39 print $" pending count: ($pending_items | length)"
40
41 # 5. Test partition=resync
42 print " testing partition=resync..."
43 let resync_items = (http get $"($url)/repos?partition=resync" | from json -o)
44 print $" resync count: ($resync_items | length)"
45
46 print "all /repos pagination and filtering tests passed!"
47}
48
49def test-errors [url: string] {
50 print "verifying /repos error handling..."
51
52 # 1. Invalid DID in PUT
53 print " testing PUT /repos with invalid DID..."
54 let resp_put = (http put -f -e -t application/json $"($url)/repos" { did: "invalid" })
55 if $resp_put.status != 400 {
56 print $" FAILED: expected 400, got ($resp_put.status)"
57 exit 1
58 }
59
60 # 2. Invalid DID in DELETE
61 print " testing DELETE /repos with invalid DID..."
62 let resp_del = (http delete -f -e -t application/json $"($url)/repos" --data { did: "invalid" })
63 if $resp_del.status != 400 {
64 print $" FAILED: expected 400, got ($resp_del.status)"
65 exit 1
66 }
67
68 # 3. Invalid cursor in GET
69 print " testing GET /repos with invalid cursor..."
70 let resp_get_cursor = (http get -f -e $"($url)/repos?cursor=invalid")
71 if $resp_get_cursor.status != 400 {
72 print $" FAILED: expected 400, got ($resp_get_cursor.status)"
73 exit 1
74 }
75
76 # 4. Invalid partition in GET
77 print " testing GET /repos with invalid partition..."
78 let resp_get_part = (http get -f -e $"($url)/repos?partition=invalid")
79 if $resp_get_part.status != 400 {
80 print $" FAILED: expected 400, got ($resp_get_part.status)"
81 exit 1
82 }
83
84 print "all /repos error handling tests passed!"
85}
86
87def main [] {
88 let port = 3001
89 let url = $"http://localhost:($port)"
90 let db_path = (mktemp -d -t hydrant_api_test.XXXXXX)
91
92 print $"starting hydrant for API verification..."
93 let binary = (build-hydrant)
94 let instance = (start-hydrant $binary $db_path $port)
95
96 if (wait-for-api $url) {
97 # add a few repos
98 let dids = [
99 "did:plc:dfl62fgb7wtjj3fcbb72naae"
100 "did:plc:q6gjnv26m4ay3m42ojvzx2m4"
101 ]
102 http put -t application/json $"($url)/repos" ($dids | each { |d| { did: $d } })
103
104 test-repos $url
105 test-errors $url
106 }
107
108 kill $instance.pid
109}
110
111# Helper to build hydrant
112def build-hydrant [] {
113 cargo build --release --quiet
114 "./target/release/hydrant"
115}
116
117# Helper to start hydrant
118def start-hydrant [binary: string, db_path: string, port: int] {
119 let log_file = $"($db_path)/hydrant.log"
120 let pid = (with-env {
121 HYDRANT_DATABASE_PATH: $db_path,
122 HYDRANT_API_PORT: ($port | into string),
123 HYDRANT_DEBUG_PORT: (($port + 1) | into string),
124 HYDRANT_MODE: "filter",
125 HYDRANT_LOG_LEVEL: "info"
126 } {
127 sh -c $"($binary) >($log_file) 2>&1 & echo $!" | str trim | into int
128 })
129 { pid: $pid, log: $log_file }
130}
131
132# Helper to wait for api
133def wait-for-api [url: string] {
134 for i in 1..20 {
135 if (try { (http get $"($url)/health") == "OK" } catch { false }) {
136 return true
137 }
138 sleep 500ms
139 }
140 false
141}