this repo has no description

Proxying requests

+1
.env.example
··· 15 JWT_SECRET=your-super-secret-jwt-key-please-change-me 16 PDS_HOSTNAME=localhost:3000 # The public-facing hostname of the PDS 17 PLC_URL=plc.directory
··· 15 JWT_SECRET=your-super-secret-jwt-key-please-change-me 16 PDS_HOSTNAME=localhost:3000 # The public-facing hostname of the PDS 17 PLC_URL=plc.directory 18 + APPVIEW_URL=https://api.bsky.app
+516 -2
Cargo.lock
··· 91 checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" 92 93 [[package]] 94 name = "async-compression" 95 version = "0.4.34" 96 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 104 ] 105 106 [[package]] 107 name = "async-trait" 108 version = "0.1.89" 109 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 217 218 [[package]] 219 name = "base64" 220 version = "0.22.1" 221 source = "registry+https://github.com/rust-lang/crates.io-index" 222 checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" ··· 269 ] 270 271 [[package]] 272 name = "bon" 273 version = "3.8.1" 274 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 345 "serde_json", 346 "sha2", 347 "sqlx", 348 "tokio", 349 "tracing", 350 "tracing-subscriber", ··· 935 ] 936 937 [[package]] 938 name = "dotenvy" 939 version = "0.15.7" 940 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1073 ] 1074 1075 [[package]] 1076 name = "event-listener" 1077 version = "5.4.1" 1078 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1088 version = "2.3.0" 1089 source = "registry+https://github.com/rust-lang/crates.io-index" 1090 checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 1091 1092 [[package]] 1093 name = "ff" ··· 1675 ] 1676 1677 [[package]] 1678 name = "hyper-rustls" 1679 version = "0.27.7" 1680 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1692 ] 1693 1694 [[package]] 1695 name = "hyper-tls" 1696 version = "0.6.0" 1697 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1734 ] 1735 1736 [[package]] 1737 name = "iana-time-zone" 1738 version = "0.1.64" 1739 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1968 "thiserror 1.0.69", 1969 "tokio", 1970 "unsigned-varint 0.7.2", 1971 ] 1972 1973 [[package]] ··· 2662 "openssl-probe", 2663 "openssl-sys", 2664 "schannel", 2665 - "security-framework", 2666 "security-framework-sys", 2667 "tempfile", 2668 ] ··· 2699 ] 2700 2701 [[package]] 2702 name = "num-bigint" 2703 version = "0.4.6" 2704 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2725 ] 2726 2727 [[package]] 2728 name = "num-conv" 2729 version = "0.1.0" 2730 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2746 checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" 2747 dependencies = [ 2748 "autocfg", 2749 "num-integer", 2750 "num-traits", 2751 ] ··· 2932 ] 2933 2934 [[package]] 2935 name = "pem" 2936 version = "3.0.6" 2937 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3054 checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 3055 3056 [[package]] 3057 name = "potential_utf" 3058 version = "0.1.4" 3059 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3146 "syn 2.0.111", 3147 "version_check", 3148 "yansi", 3149 ] 3150 3151 [[package]] ··· 3520 source = "registry+https://github.com/rust-lang/crates.io-index" 3521 checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" 3522 dependencies = [ 3523 "once_cell", 3524 "ring", 3525 "rustls-pki-types", ··· 3529 ] 3530 3531 [[package]] 3532 name = "rustls-pki-types" 3533 version = "1.13.1" 3534 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3643 dependencies = [ 3644 "bitflags", 3645 "core-foundation 0.9.4", 3646 "core-foundation-sys", 3647 "libc", 3648 "security-framework-sys", ··· 3861 checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 3862 3863 [[package]] 3864 name = "signature" 3865 version = "2.2.0" 3866 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4125 "chrono", 4126 "crc", 4127 "dotenvy", 4128 - "etcetera", 4129 "futures-channel", 4130 "futures-core", 4131 "futures-util", ··· 4252 checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 4253 4254 [[package]] 4255 name = "subtle" 4256 version = "2.6.1" 4257 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4351 ] 4352 4353 [[package]] 4354 name = "thiserror" 4355 version = "1.0.69" 4356 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4488 "libc", 4489 "mio", 4490 "pin-project-lite", 4491 "socket2 0.6.1", 4492 "tokio-macros", 4493 "windows-sys 0.61.2", ··· 4550 ] 4551 4552 [[package]] 4553 name = "tower" 4554 version = "0.5.2" 4555 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4557 dependencies = [ 4558 "futures-core", 4559 "futures-util", 4560 "pin-project-lite", 4561 "sync_wrapper", 4562 "tokio", 4563 "tower-layer", 4564 "tower-service", 4565 "tracing", ··· 4764 version = "0.9.0" 4765 source = "registry+https://github.com/rust-lang/crates.io-index" 4766 checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 4767 4768 [[package]] 4769 name = "url" ··· 5019 checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" 5020 5021 [[package]] 5022 name = "winapi-util" 5023 version = "0.1.11" 5024 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 5026 dependencies = [ 5027 "windows-sys 0.61.2", 5028 ] 5029 5030 [[package]] 5031 name = "windows" ··· 5495 version = "0.6.2" 5496 source = "registry+https://github.com/rust-lang/crates.io-index" 5497 checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" 5498 5499 [[package]] 5500 name = "xml5ever"
··· 91 checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" 92 93 [[package]] 94 + name = "astral-tokio-tar" 95 + version = "0.5.6" 96 + source = "registry+https://github.com/rust-lang/crates.io-index" 97 + checksum = "ec179a06c1769b1e42e1e2cbe74c7dcdb3d6383c838454d063eaac5bbb7ebbe5" 98 + dependencies = [ 99 + "filetime", 100 + "futures-core", 101 + "libc", 102 + "portable-atomic", 103 + "rustc-hash", 104 + "tokio", 105 + "tokio-stream", 106 + "xattr", 107 + ] 108 + 109 + [[package]] 110 name = "async-compression" 111 version = "0.4.34" 112 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 120 ] 121 122 [[package]] 123 + name = "async-stream" 124 + version = "0.3.6" 125 + source = "registry+https://github.com/rust-lang/crates.io-index" 126 + checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" 127 + dependencies = [ 128 + "async-stream-impl", 129 + "futures-core", 130 + "pin-project-lite", 131 + ] 132 + 133 + [[package]] 134 + name = "async-stream-impl" 135 + version = "0.3.6" 136 + source = "registry+https://github.com/rust-lang/crates.io-index" 137 + checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" 138 + dependencies = [ 139 + "proc-macro2", 140 + "quote", 141 + "syn 2.0.111", 142 + ] 143 + 144 + [[package]] 145 name = "async-trait" 146 version = "0.1.89" 147 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 255 256 [[package]] 257 name = "base64" 258 + version = "0.21.7" 259 + source = "registry+https://github.com/rust-lang/crates.io-index" 260 + checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" 261 + 262 + [[package]] 263 + name = "base64" 264 version = "0.22.1" 265 source = "registry+https://github.com/rust-lang/crates.io-index" 266 checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" ··· 313 ] 314 315 [[package]] 316 + name = "bollard" 317 + version = "0.19.4" 318 + source = "registry+https://github.com/rust-lang/crates.io-index" 319 + checksum = "87a52479c9237eb04047ddb94788c41ca0d26eaff8b697ecfbb4c32f7fdc3b1b" 320 + dependencies = [ 321 + "async-stream", 322 + "base64 0.22.1", 323 + "bitflags", 324 + "bollard-buildkit-proto", 325 + "bollard-stubs", 326 + "bytes", 327 + "chrono", 328 + "futures-core", 329 + "futures-util", 330 + "hex", 331 + "home", 332 + "http", 333 + "http-body-util", 334 + "hyper", 335 + "hyper-named-pipe", 336 + "hyper-rustls", 337 + "hyper-util", 338 + "hyperlocal", 339 + "log", 340 + "num", 341 + "pin-project-lite", 342 + "rand 0.9.2", 343 + "rustls", 344 + "rustls-native-certs", 345 + "rustls-pemfile", 346 + "rustls-pki-types", 347 + "serde", 348 + "serde_derive", 349 + "serde_json", 350 + "serde_repr", 351 + "serde_urlencoded", 352 + "thiserror 2.0.17", 353 + "tokio", 354 + "tokio-stream", 355 + "tokio-util", 356 + "tonic", 357 + "tower-service", 358 + "url", 359 + "winapi", 360 + ] 361 + 362 + [[package]] 363 + name = "bollard-buildkit-proto" 364 + version = "0.7.0" 365 + source = "registry+https://github.com/rust-lang/crates.io-index" 366 + checksum = "85a885520bf6249ab931a764ffdb87b0ceef48e6e7d807cfdb21b751e086e1ad" 367 + dependencies = [ 368 + "prost", 369 + "prost-types", 370 + "tonic", 371 + "tonic-prost", 372 + "ureq", 373 + ] 374 + 375 + [[package]] 376 + name = "bollard-stubs" 377 + version = "1.49.1-rc.28.4.0" 378 + source = "registry+https://github.com/rust-lang/crates.io-index" 379 + checksum = "5731fe885755e92beff1950774068e0cae67ea6ec7587381536fca84f1779623" 380 + dependencies = [ 381 + "base64 0.22.1", 382 + "bollard-buildkit-proto", 383 + "bytes", 384 + "chrono", 385 + "prost", 386 + "serde", 387 + "serde_json", 388 + "serde_repr", 389 + "serde_with", 390 + ] 391 + 392 + [[package]] 393 name = "bon" 394 version = "3.8.1" 395 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 466 "serde_json", 467 "sha2", 468 "sqlx", 469 + "testcontainers", 470 + "testcontainers-modules", 471 "tokio", 472 "tracing", 473 "tracing-subscriber", ··· 1058 ] 1059 1060 [[package]] 1061 + name = "docker_credential" 1062 + version = "1.3.2" 1063 + source = "registry+https://github.com/rust-lang/crates.io-index" 1064 + checksum = "1d89dfcba45b4afad7450a99b39e751590463e45c04728cf555d36bb66940de8" 1065 + dependencies = [ 1066 + "base64 0.21.7", 1067 + "serde", 1068 + "serde_json", 1069 + ] 1070 + 1071 + [[package]] 1072 name = "dotenvy" 1073 version = "0.15.7" 1074 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1207 ] 1208 1209 [[package]] 1210 + name = "etcetera" 1211 + version = "0.11.0" 1212 + source = "registry+https://github.com/rust-lang/crates.io-index" 1213 + checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" 1214 + dependencies = [ 1215 + "cfg-if", 1216 + "windows-sys 0.61.2", 1217 + ] 1218 + 1219 + [[package]] 1220 name = "event-listener" 1221 version = "5.4.1" 1222 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1232 version = "2.3.0" 1233 source = "registry+https://github.com/rust-lang/crates.io-index" 1234 checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 1235 + 1236 + [[package]] 1237 + name = "ferroid" 1238 + version = "0.8.7" 1239 + source = "registry+https://github.com/rust-lang/crates.io-index" 1240 + checksum = "e0e9414a6ae93ef993ce40a1e02944f13d4508e2bf6f1ced1580ce6910f08253" 1241 + dependencies = [ 1242 + "portable-atomic", 1243 + "rand 0.9.2", 1244 + "web-time", 1245 + ] 1246 1247 [[package]] 1248 name = "ff" ··· 1830 ] 1831 1832 [[package]] 1833 + name = "hyper-named-pipe" 1834 + version = "0.1.0" 1835 + source = "registry+https://github.com/rust-lang/crates.io-index" 1836 + checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278" 1837 + dependencies = [ 1838 + "hex", 1839 + "hyper", 1840 + "hyper-util", 1841 + "pin-project-lite", 1842 + "tokio", 1843 + "tower-service", 1844 + "winapi", 1845 + ] 1846 + 1847 + [[package]] 1848 name = "hyper-rustls" 1849 version = "0.27.7" 1850 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1862 ] 1863 1864 [[package]] 1865 + name = "hyper-timeout" 1866 + version = "0.5.2" 1867 + source = "registry+https://github.com/rust-lang/crates.io-index" 1868 + checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" 1869 + dependencies = [ 1870 + "hyper", 1871 + "hyper-util", 1872 + "pin-project-lite", 1873 + "tokio", 1874 + "tower-service", 1875 + ] 1876 + 1877 + [[package]] 1878 name = "hyper-tls" 1879 version = "0.6.0" 1880 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1917 ] 1918 1919 [[package]] 1920 + name = "hyperlocal" 1921 + version = "0.9.1" 1922 + source = "registry+https://github.com/rust-lang/crates.io-index" 1923 + checksum = "986c5ce3b994526b3cd75578e62554abd09f0899d6206de48b3e96ab34ccc8c7" 1924 + dependencies = [ 1925 + "hex", 1926 + "http-body-util", 1927 + "hyper", 1928 + "hyper-util", 1929 + "pin-project-lite", 1930 + "tokio", 1931 + "tower-service", 1932 + ] 1933 + 1934 + [[package]] 1935 name = "iana-time-zone" 1936 version = "0.1.64" 1937 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2166 "thiserror 1.0.69", 2167 "tokio", 2168 "unsigned-varint 0.7.2", 2169 + ] 2170 + 2171 + [[package]] 2172 + name = "itertools" 2173 + version = "0.14.0" 2174 + source = "registry+https://github.com/rust-lang/crates.io-index" 2175 + checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" 2176 + dependencies = [ 2177 + "either", 2178 ] 2179 2180 [[package]] ··· 2869 "openssl-probe", 2870 "openssl-sys", 2871 "schannel", 2872 + "security-framework 2.11.1", 2873 "security-framework-sys", 2874 "tempfile", 2875 ] ··· 2906 ] 2907 2908 [[package]] 2909 + name = "num" 2910 + version = "0.4.3" 2911 + source = "registry+https://github.com/rust-lang/crates.io-index" 2912 + checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" 2913 + dependencies = [ 2914 + "num-bigint", 2915 + "num-complex", 2916 + "num-integer", 2917 + "num-iter", 2918 + "num-rational", 2919 + "num-traits", 2920 + ] 2921 + 2922 + [[package]] 2923 name = "num-bigint" 2924 version = "0.4.6" 2925 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2946 ] 2947 2948 [[package]] 2949 + name = "num-complex" 2950 + version = "0.4.6" 2951 + source = "registry+https://github.com/rust-lang/crates.io-index" 2952 + checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" 2953 + dependencies = [ 2954 + "num-traits", 2955 + ] 2956 + 2957 + [[package]] 2958 name = "num-conv" 2959 version = "0.1.0" 2960 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2976 checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" 2977 dependencies = [ 2978 "autocfg", 2979 + "num-integer", 2980 + "num-traits", 2981 + ] 2982 + 2983 + [[package]] 2984 + name = "num-rational" 2985 + version = "0.4.2" 2986 + source = "registry+https://github.com/rust-lang/crates.io-index" 2987 + checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" 2988 + dependencies = [ 2989 + "num-bigint", 2990 "num-integer", 2991 "num-traits", 2992 ] ··· 3173 ] 3174 3175 [[package]] 3176 + name = "parse-display" 3177 + version = "0.9.1" 3178 + source = "registry+https://github.com/rust-lang/crates.io-index" 3179 + checksum = "914a1c2265c98e2446911282c6ac86d8524f495792c38c5bd884f80499c7538a" 3180 + dependencies = [ 3181 + "parse-display-derive", 3182 + "regex", 3183 + "regex-syntax", 3184 + ] 3185 + 3186 + [[package]] 3187 + name = "parse-display-derive" 3188 + version = "0.9.1" 3189 + source = "registry+https://github.com/rust-lang/crates.io-index" 3190 + checksum = "2ae7800a4c974efd12df917266338e79a7a74415173caf7e70aa0a0707345281" 3191 + dependencies = [ 3192 + "proc-macro2", 3193 + "quote", 3194 + "regex", 3195 + "regex-syntax", 3196 + "structmeta", 3197 + "syn 2.0.111", 3198 + ] 3199 + 3200 + [[package]] 3201 name = "pem" 3202 version = "3.0.6" 3203 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3320 checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 3321 3322 [[package]] 3323 + name = "portable-atomic" 3324 + version = "1.11.1" 3325 + source = "registry+https://github.com/rust-lang/crates.io-index" 3326 + checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" 3327 + 3328 + [[package]] 3329 name = "potential_utf" 3330 version = "0.1.4" 3331 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3418 "syn 2.0.111", 3419 "version_check", 3420 "yansi", 3421 + ] 3422 + 3423 + [[package]] 3424 + name = "prost" 3425 + version = "0.14.1" 3426 + source = "registry+https://github.com/rust-lang/crates.io-index" 3427 + checksum = "7231bd9b3d3d33c86b58adbac74b5ec0ad9f496b19d22801d773636feaa95f3d" 3428 + dependencies = [ 3429 + "bytes", 3430 + "prost-derive", 3431 + ] 3432 + 3433 + [[package]] 3434 + name = "prost-derive" 3435 + version = "0.14.1" 3436 + source = "registry+https://github.com/rust-lang/crates.io-index" 3437 + checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425" 3438 + dependencies = [ 3439 + "anyhow", 3440 + "itertools", 3441 + "proc-macro2", 3442 + "quote", 3443 + "syn 2.0.111", 3444 + ] 3445 + 3446 + [[package]] 3447 + name = "prost-types" 3448 + version = "0.14.1" 3449 + source = "registry+https://github.com/rust-lang/crates.io-index" 3450 + checksum = "b9b4db3d6da204ed77bb26ba83b6122a73aeb2e87e25fbf7ad2e84c4ccbf8f72" 3451 + dependencies = [ 3452 + "prost", 3453 ] 3454 3455 [[package]] ··· 3824 source = "registry+https://github.com/rust-lang/crates.io-index" 3825 checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" 3826 dependencies = [ 3827 + "log", 3828 "once_cell", 3829 "ring", 3830 "rustls-pki-types", ··· 3834 ] 3835 3836 [[package]] 3837 + name = "rustls-native-certs" 3838 + version = "0.8.2" 3839 + source = "registry+https://github.com/rust-lang/crates.io-index" 3840 + checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" 3841 + dependencies = [ 3842 + "openssl-probe", 3843 + "rustls-pki-types", 3844 + "schannel", 3845 + "security-framework 3.5.1", 3846 + ] 3847 + 3848 + [[package]] 3849 + name = "rustls-pemfile" 3850 + version = "2.2.0" 3851 + source = "registry+https://github.com/rust-lang/crates.io-index" 3852 + checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" 3853 + dependencies = [ 3854 + "rustls-pki-types", 3855 + ] 3856 + 3857 + [[package]] 3858 name = "rustls-pki-types" 3859 version = "1.13.1" 3860 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3969 dependencies = [ 3970 "bitflags", 3971 "core-foundation 0.9.4", 3972 + "core-foundation-sys", 3973 + "libc", 3974 + "security-framework-sys", 3975 + ] 3976 + 3977 + [[package]] 3978 + name = "security-framework" 3979 + version = "3.5.1" 3980 + source = "registry+https://github.com/rust-lang/crates.io-index" 3981 + checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" 3982 + dependencies = [ 3983 + "bitflags", 3984 + "core-foundation 0.10.1", 3985 "core-foundation-sys", 3986 "libc", 3987 "security-framework-sys", ··· 4200 checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 4201 4202 [[package]] 4203 + name = "signal-hook-registry" 4204 + version = "1.4.7" 4205 + source = "registry+https://github.com/rust-lang/crates.io-index" 4206 + checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" 4207 + dependencies = [ 4208 + "libc", 4209 + ] 4210 + 4211 + [[package]] 4212 name = "signature" 4213 version = "2.2.0" 4214 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4473 "chrono", 4474 "crc", 4475 "dotenvy", 4476 + "etcetera 0.8.0", 4477 "futures-channel", 4478 "futures-core", 4479 "futures-util", ··· 4600 checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 4601 4602 [[package]] 4603 + name = "structmeta" 4604 + version = "0.3.0" 4605 + source = "registry+https://github.com/rust-lang/crates.io-index" 4606 + checksum = "2e1575d8d40908d70f6fd05537266b90ae71b15dbbe7a8b7dffa2b759306d329" 4607 + dependencies = [ 4608 + "proc-macro2", 4609 + "quote", 4610 + "structmeta-derive", 4611 + "syn 2.0.111", 4612 + ] 4613 + 4614 + [[package]] 4615 + name = "structmeta-derive" 4616 + version = "0.3.0" 4617 + source = "registry+https://github.com/rust-lang/crates.io-index" 4618 + checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc" 4619 + dependencies = [ 4620 + "proc-macro2", 4621 + "quote", 4622 + "syn 2.0.111", 4623 + ] 4624 + 4625 + [[package]] 4626 name = "subtle" 4627 version = "2.6.1" 4628 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4722 ] 4723 4724 [[package]] 4725 + name = "testcontainers" 4726 + version = "0.26.0" 4727 + source = "registry+https://github.com/rust-lang/crates.io-index" 4728 + checksum = "a347cac4368ba4f1871743adb27dc14829024d26b1763572404726b0b9943eb8" 4729 + dependencies = [ 4730 + "astral-tokio-tar", 4731 + "async-trait", 4732 + "bollard", 4733 + "bytes", 4734 + "docker_credential", 4735 + "either", 4736 + "etcetera 0.11.0", 4737 + "ferroid", 4738 + "futures", 4739 + "itertools", 4740 + "log", 4741 + "memchr", 4742 + "parse-display", 4743 + "pin-project-lite", 4744 + "serde", 4745 + "serde_json", 4746 + "serde_with", 4747 + "thiserror 2.0.17", 4748 + "tokio", 4749 + "tokio-stream", 4750 + "tokio-util", 4751 + "url", 4752 + ] 4753 + 4754 + [[package]] 4755 + name = "testcontainers-modules" 4756 + version = "0.14.0" 4757 + source = "registry+https://github.com/rust-lang/crates.io-index" 4758 + checksum = "5e75e78ff453128a2c7da9a5d5a3325ea34ea214d4bf51eab3417de23a4e5147" 4759 + dependencies = [ 4760 + "testcontainers", 4761 + ] 4762 + 4763 + [[package]] 4764 name = "thiserror" 4765 version = "1.0.69" 4766 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4898 "libc", 4899 "mio", 4900 "pin-project-lite", 4901 + "signal-hook-registry", 4902 "socket2 0.6.1", 4903 "tokio-macros", 4904 "windows-sys 0.61.2", ··· 4961 ] 4962 4963 [[package]] 4964 + name = "tonic" 4965 + version = "0.14.2" 4966 + source = "registry+https://github.com/rust-lang/crates.io-index" 4967 + checksum = "eb7613188ce9f7df5bfe185db26c5814347d110db17920415cf2fbcad85e7203" 4968 + dependencies = [ 4969 + "async-trait", 4970 + "axum", 4971 + "base64 0.22.1", 4972 + "bytes", 4973 + "h2", 4974 + "http", 4975 + "http-body", 4976 + "http-body-util", 4977 + "hyper", 4978 + "hyper-timeout", 4979 + "hyper-util", 4980 + "percent-encoding", 4981 + "pin-project", 4982 + "socket2 0.6.1", 4983 + "sync_wrapper", 4984 + "tokio", 4985 + "tokio-stream", 4986 + "tower", 4987 + "tower-layer", 4988 + "tower-service", 4989 + "tracing", 4990 + ] 4991 + 4992 + [[package]] 4993 + name = "tonic-prost" 4994 + version = "0.14.2" 4995 + source = "registry+https://github.com/rust-lang/crates.io-index" 4996 + checksum = "66bd50ad6ce1252d87ef024b3d64fe4c3cf54a86fb9ef4c631fdd0ded7aeaa67" 4997 + dependencies = [ 4998 + "bytes", 4999 + "prost", 5000 + "tonic", 5001 + ] 5002 + 5003 + [[package]] 5004 name = "tower" 5005 version = "0.5.2" 5006 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 5008 dependencies = [ 5009 "futures-core", 5010 "futures-util", 5011 + "indexmap 2.12.1", 5012 "pin-project-lite", 5013 + "slab", 5014 "sync_wrapper", 5015 "tokio", 5016 + "tokio-util", 5017 "tower-layer", 5018 "tower-service", 5019 "tracing", ··· 5218 version = "0.9.0" 5219 source = "registry+https://github.com/rust-lang/crates.io-index" 5220 checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 5221 + 5222 + [[package]] 5223 + name = "ureq" 5224 + version = "3.1.4" 5225 + source = "registry+https://github.com/rust-lang/crates.io-index" 5226 + checksum = "d39cb1dbab692d82a977c0392ffac19e188bd9186a9f32806f0aaa859d75585a" 5227 + dependencies = [ 5228 + "base64 0.22.1", 5229 + "log", 5230 + "percent-encoding", 5231 + "rustls", 5232 + "rustls-pki-types", 5233 + "ureq-proto", 5234 + "utf-8", 5235 + "webpki-roots 1.0.4", 5236 + ] 5237 + 5238 + [[package]] 5239 + name = "ureq-proto" 5240 + version = "0.5.3" 5241 + source = "registry+https://github.com/rust-lang/crates.io-index" 5242 + checksum = "d81f9efa9df032be5934a46a068815a10a042b494b6a58cb0a1a97bb5467ed6f" 5243 + dependencies = [ 5244 + "base64 0.22.1", 5245 + "http", 5246 + "httparse", 5247 + "log", 5248 + ] 5249 5250 [[package]] 5251 name = "url" ··· 5501 checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" 5502 5503 [[package]] 5504 + name = "winapi" 5505 + version = "0.3.9" 5506 + source = "registry+https://github.com/rust-lang/crates.io-index" 5507 + checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 5508 + dependencies = [ 5509 + "winapi-i686-pc-windows-gnu", 5510 + "winapi-x86_64-pc-windows-gnu", 5511 + ] 5512 + 5513 + [[package]] 5514 + name = "winapi-i686-pc-windows-gnu" 5515 + version = "0.4.0" 5516 + source = "registry+https://github.com/rust-lang/crates.io-index" 5517 + checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 5518 + 5519 + [[package]] 5520 name = "winapi-util" 5521 version = "0.1.11" 5522 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 5524 dependencies = [ 5525 "windows-sys 0.61.2", 5526 ] 5527 + 5528 + [[package]] 5529 + name = "winapi-x86_64-pc-windows-gnu" 5530 + version = "0.4.0" 5531 + source = "registry+https://github.com/rust-lang/crates.io-index" 5532 + checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 5533 5534 [[package]] 5535 name = "windows" ··· 5999 version = "0.6.2" 6000 source = "registry+https://github.com/rust-lang/crates.io-index" 6001 checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" 6002 + 6003 + [[package]] 6004 + name = "xattr" 6005 + version = "1.6.1" 6006 + source = "registry+https://github.com/rust-lang/crates.io-index" 6007 + checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" 6008 + dependencies = [ 6009 + "libc", 6010 + "rustix", 6011 + ] 6012 6013 [[package]] 6014 name = "xml5ever"
+4
Cargo.toml
··· 26 tracing = "0.1.43" 27 tracing-subscriber = "0.3.22" 28 uuid = { version = "1.19.0", features = ["v4", "fast-rng"] }
··· 26 tracing = "0.1.43" 27 tracing-subscriber = "0.3.22" 28 uuid = { version = "1.19.0", features = ["v4", "fast-rng"] } 29 + 30 + [dev-dependencies] 31 + testcontainers = "0.26.0" 32 + testcontainers-modules = { version = "0.14.0", features = ["postgres"] }
+48 -75
TODO.md
··· 1 - # Implementation TODOs 2 3 - Lewis' special big boy todofile 4 5 - ## 1. Server Infrastructure & Health 6 - [x] Health Check 7 - [x] Implement `GET /health` endpoint (returns "OK"). 8 - [x] Server Description 9 - [x] Implement `com.atproto.server.describeServer` (returns available user domains). 10 11 ## 2. Authentication & Account Management (`com.atproto.server`) 12 - [x] Account Creation 13 - [x] Implement `com.atproto.server.createAccount`. 14 - [x] Validate handle format (reject invalid characters). 15 - - [x] Create DID for new user. 16 - - [x] Initialize user repository. 17 - [x] Return access JWT and DID. 18 - - [x] MST stuff I think... 19 - 20 - [x] Session Management 21 - [x] Implement `com.atproto.server.createSession` (Login). 22 - - [x] Validate identifier (handle/email) and password. 23 - - [x] Return access JWT, refresh JWT, and DID. 24 - [x] Implement `com.atproto.server.getSession`. 25 - - [x] Verify JWT validity. 26 - [x] Implement `com.atproto.server.refreshSession`. 27 - [x] Implement `com.atproto.server.deleteSession` (Logout). 28 - - [x] Invalidate current session/token. 29 30 ## 3. Repository Operations (`com.atproto.repo`) 31 - [ ] Record CRUD 32 - [ ] Implement `com.atproto.repo.createRecord`. 33 - - [ ] Generate `rkey` if not provided. 34 - - [ ] Validate schema against Lexicon. 35 - - [ ] Handle `swapCommit` for optimistic locking. 36 - [ ] Implement `com.atproto.repo.putRecord`. 37 - - [ ] Handle create vs update logic. 38 - - [ ] Validate `repo` matches authenticated user. 39 - - [ ] Validate record schema (e.g., missing required fields). 40 - [ ] Implement `com.atproto.repo.getRecord`. 41 - - [ ] Handle missing params (400 Bad Request). 42 - - [ ] Handle non-existent record (404 Not Found). 43 - [ ] Implement `com.atproto.repo.deleteRecord`. 44 - [ ] Implement `com.atproto.repo.listRecords`. 45 - - [ ] Support pagination (`limit`, `cursor`). 46 - [ ] Blob Management 47 - [ ] Implement `com.atproto.repo.uploadBlob`. 48 - - [ ] Enforce authentication. 49 - - [ ] Validate MIME types (reject unsupported). 50 - - [ ] Return blob reference (`$link`). 51 - - [ ] Repo Meta 52 - - [ ] Implement `com.atproto.repo.describeRepo`. 53 54 - ## 4. Actor & Profile (`app.bsky.actor`) 55 - - [ ] Profile Management 56 - - [ ] Implement `app.bsky.actor.getProfile`. 57 - - [ ] Resolve handle to DID. 58 - - [ ] Return profile record data. 59 - - [ ] Discovery 60 - - [ ] Implement `app.bsky.actor.searchActors`. 61 62 - ## 5. Feed & Timeline (`app.bsky.feed`) 63 - - [ ] Feed Retrieval 64 - - [ ] Implement `app.bsky.feed.getTimeline`. 65 - - [ ] Implement `app.bsky.feed.getAuthorFeed`. 66 - - [ ] Filter by actor. 67 - - [ ] Respect mutes (if viewer is authenticated). 68 - - [ ] Implement `app.bsky.feed.getPostThread`. 69 - - [ ] Construct thread tree (parents, replies). 70 - - [ ] Handle deleted posts (return `notFoundPost` view). 71 - - [ ] Record Types 72 - - [ ] Support `app.bsky.feed.post` record type. 73 - - [ ] Support `app.bsky.feed.like` record type. 74 - - [ ] Support `app.bsky.embed.images` in posts. 75 - 76 - ## 6. Social Graph (`app.bsky.graph`) 77 - - [ ] Relationships 78 - - [ ] Implement `app.bsky.graph.getFollows`. 79 - - [ ] Implement `app.bsky.graph.getFollowers`. 80 - - [ ] Implement `app.bsky.graph.getMutes`. 81 - - [ ] Implement `app.bsky.graph.getBlocks`. 82 - - [ ] Record Types 83 - - [ ] Support `app.bsky.graph.follow` record type. 84 - - [ ] Support `app.bsky.graph.mute` record type. 85 - 86 - ## 7. Notifications (`app.bsky.notification`) 87 - - [ ] Notification Management 88 - - [ ] Implement `app.bsky.notification.listNotifications`. 89 - - [ ] Aggregate notifications (likes, follows, replies). 90 - - [ ] Implement `app.bsky.notification.getUnreadCount`. 91 - - [ ] Track read state. 92 - - [ ] Reset count on list/read. 93 - 94 - ## 8. Identity (`com.atproto.identity`) 95 - [ ] Resolution 96 - - [ ] Implement `com.atproto.identity.resolveHandle`. 97 98 - ## 9. Sync & Federation (`com.atproto.sync`) 99 - - [ ] Data Export 100 - - [ ] Implement `com.atproto.sync.getRepo` (Export CAR file). 101 - - [ ] Implement `com.atproto.sync.getBlocks`. 102 103 - ## 10. General Requirements 104 - [ ] Validation 105 - - [ ] Ensure all endpoints validate input parameters. 106 - - [ ] Ensure proper error codes (400, 401, 404, 409). 107 - - [ ] Concurrency 108 - - [ ] Ensure thread safety for repo updates.
··· 1 + # PDS Implementation TODOs 2 3 + Lewis' corrected big boy todofile 4 5 + ## 1. Server Infrastructure & Proxying 6 - [x] Health Check 7 - [x] Implement `GET /health` endpoint (returns "OK"). 8 - [x] Server Description 9 - [x] Implement `com.atproto.server.describeServer` (returns available user domains). 10 + - [x] XRPC Proxying 11 + - [x] Implement strict forwarding for all `app.bsky.*` and `chat.bsky.*` requests to an appview. 12 + - [x] Forward Auth headers correctly. 13 + - [x] Handle AppView errors/timeouts gracefully. 14 15 ## 2. Authentication & Account Management (`com.atproto.server`) 16 - [x] Account Creation 17 - [x] Implement `com.atproto.server.createAccount`. 18 - [x] Validate handle format (reject invalid characters). 19 + - [x] Create DID for new user (PLC directory). 20 + - [x] Initialize user repository (Root commit). 21 - [x] Return access JWT and DID. 22 + - [ ] Create DID for new user (did:web). 23 - [x] Session Management 24 - [x] Implement `com.atproto.server.createSession` (Login). 25 - [x] Implement `com.atproto.server.getSession`. 26 - [x] Implement `com.atproto.server.refreshSession`. 27 - [x] Implement `com.atproto.server.deleteSession` (Logout). 28 29 ## 3. Repository Operations (`com.atproto.repo`) 30 - [ ] Record CRUD 31 - [ ] Implement `com.atproto.repo.createRecord`. 32 + - [ ] Validate schema against Lexicon (just structure, not complex logic). 33 + - [ ] Generate `rkey` (TID) if not provided. 34 + - [ ] Handle MST (Merkle Search Tree) insertion. 35 + - [ ] **Trigger Firehose Event**. 36 - [ ] Implement `com.atproto.repo.putRecord`. 37 - [ ] Implement `com.atproto.repo.getRecord`. 38 - [ ] Implement `com.atproto.repo.deleteRecord`. 39 - [ ] Implement `com.atproto.repo.listRecords`. 40 + - [ ] Implement `com.atproto.repo.describeRepo`. 41 - [ ] Blob Management 42 - [ ] Implement `com.atproto.repo.uploadBlob`. 43 + - [ ] Store blob (S3). 44 + - [ ] return `blob` ref (CID + MimeType). 45 46 + ## 4. Sync & Federation (`com.atproto.sync`) 47 + - [ ] The Firehose (WebSocket) 48 + - [ ] Implement `com.atproto.sync.subscribeRepos`. 49 + - [ ] Broadcast real-time commit events. 50 + - [ ] Handle cursor replay (backfill). 51 + - [ ] Bulk Export 52 + - [ ] Implement `com.atproto.sync.getRepo` (Return full CAR file of repo). 53 + - [ ] Implement `com.atproto.sync.getBlocks` (Return specific blocks via CIDs). 54 + - [ ] Implement `com.atproto.sync.getLatestCommit`. 55 + - [ ] Implement `com.atproto.sync.getRecord` (Sync version, distinct from repo.getRecord). 56 + - [ ] Blob Sync 57 + - [ ] Implement `com.atproto.sync.getBlob`. 58 + - [ ] Implement `com.atproto.sync.listBlobs`. 59 + - [ ] Crawler Interaction 60 + - [ ] Implement `com.atproto.sync.requestCrawl` (Notify relays to index us). 61 62 + ## 5. Identity (`com.atproto.identity`) 63 - [ ] Resolution 64 + - [ ] Implement `com.atproto.identity.resolveHandle` (Can be internal or proxy to PLC). 65 + - [ ] Implement `/.well-known/did.json` (Depends on supporting did:web). 66 67 + ## 6. Record Schema Validation 68 + - [ ] `app.bsky.feed.post` 69 + - [ ] `app.bsky.feed.like` 70 + - [ ] `app.bsky.feed.repost` 71 + - [ ] `app.bsky.graph.follow` 72 + - [ ] `app.bsky.graph.block` 73 + - [ ] `app.bsky.actor.profile` 74 + - [ ] Other app(view) validation too!!! 75 76 + ## 7. General Requirements 77 + - [ ] IPLD & MST 78 + - [ ] Implement Merkle Search Tree (MST) logic for repo signing. 79 + - [ ] Implement CAR (Content Addressable Archives) encoding/decoding. 80 - [ ] Validation 81 + - [ ] DID PLC Operations (Sign rotation keys).
+23
justfile
···
··· 1 + # Run all tests with correct threading models 2 + test: test-proxy test-lifecycle test-others 3 + 4 + # Proxy tests modify environment variables, so must run single-threaded 5 + # TODO: figure out how to run in parallel 6 + test-proxy: 7 + cargo test --test proxy -- --test-threads=1 8 + 9 + # Lifecycle tests involve complex state mutations, run single-threaded to be safe 10 + # TODO: figure out how to run in parallel 11 + test-lifecycle: 12 + cargo test --test lifecycle -- --test-threads=1 13 + 14 + test-others: 15 + cargo test --lib 16 + cargo test --test actor 17 + cargo test --test feed 18 + cargo test --test graph 19 + cargo test --test identity 20 + cargo test --test notification 21 + cargo test --test repo 22 + cargo test --test server 23 + cargo test --test sync
+1
src/api/mod.rs
··· 1 pub mod server; 2 pub mod repo;
··· 1 pub mod server; 2 pub mod repo; 3 + pub mod proxy;
+84
src/api/proxy.rs
···
··· 1 + use axum::{ 2 + extract::{Path, Query}, 3 + http::{HeaderMap, Method, StatusCode}, 4 + response::{IntoResponse, Response}, 5 + body::Bytes, 6 + }; 7 + use reqwest::Client; 8 + use tracing::{info, error}; 9 + use std::collections::HashMap; 10 + 11 + pub async fn proxy_handler( 12 + Path(method): Path<String>, 13 + method_verb: Method, 14 + headers: HeaderMap, 15 + Query(params): Query<HashMap<String, String>>, 16 + body: Bytes, 17 + ) -> Response { 18 + 19 + let proxy_header = headers.get("atproto-proxy") 20 + .and_then(|h| h.to_str().ok()) 21 + .map(|s| s.to_string()); 22 + 23 + let appview_url = match proxy_header { 24 + Some(url) => url, 25 + None => match std::env::var("APPVIEW_URL") { 26 + Ok(url) => url, 27 + Err(_) => return (StatusCode::BAD_GATEWAY, "No upstream AppView configured").into_response(), 28 + }, 29 + }; 30 + 31 + let target_url = format!("{}/xrpc/{}", appview_url, method); 32 + 33 + info!("Proxying {} request to {}", method_verb, target_url); 34 + 35 + let client = Client::new(); 36 + 37 + let mut request_builder = client 38 + .request(method_verb, &target_url) 39 + .query(&params); 40 + 41 + for (key, value) in headers.iter() { 42 + if key != "host" && key != "content-length" { 43 + request_builder = request_builder.header(key, value); 44 + } 45 + } 46 + 47 + request_builder = request_builder.body(body); 48 + 49 + match request_builder.send().await { 50 + Ok(resp) => { 51 + let status = resp.status(); 52 + let headers = resp.headers().clone(); 53 + let body = match resp.bytes().await { 54 + Ok(b) => b, 55 + Err(e) => { 56 + error!("Error reading proxy response body: {:?}", e); 57 + return (StatusCode::BAD_GATEWAY, "Error reading upstream response").into_response(); 58 + } 59 + }; 60 + 61 + let mut response_builder = Response::builder().status(status); 62 + 63 + for (key, value) in headers.iter() { 64 + response_builder = response_builder.header(key, value); 65 + } 66 + 67 + match response_builder.body(axum::body::Body::from(body)) { 68 + Ok(r) => r, 69 + Err(e) => { 70 + error!("Error building proxy response: {:?}", e); 71 + (StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error").into_response() 72 + } 73 + } 74 + }, 75 + Err(e) => { 76 + error!("Error sending proxy request: {:?}", e); 77 + if e.is_timeout() { 78 + (StatusCode::GATEWAY_TIMEOUT, "Upstream Timeout").into_response() 79 + } else { 80 + (StatusCode::BAD_GATEWAY, "Upstream Error").into_response() 81 + } 82 + } 83 + } 84 + }
+19
src/api/server.rs
··· 14 use jacquard::types::{string::Tid, did::Did, integer::LimitedU32}; 15 use std::sync::Arc; 16 17 #[derive(Deserialize)] 18 pub struct CreateAccountInput { 19 pub handle: String,
··· 14 use jacquard::types::{string::Tid, did::Did, integer::LimitedU32}; 15 use std::sync::Arc; 16 17 + pub async fn describe_server() -> impl IntoResponse { 18 + let domains_str = std::env::var("AVAILABLE_USER_DOMAINS").unwrap_or_else(|_| "example.com".to_string()); 19 + let domains: Vec<&str> = domains_str.split(',').map(|s| s.trim()).collect(); 20 + 21 + Json(json!({ 22 + "availableUserDomains": domains 23 + })) 24 + } 25 + 26 + pub async fn health(State(state): State<AppState>) -> impl IntoResponse { 27 + match sqlx::query("SELECT 1").execute(&state.db).await { 28 + Ok(_) => (StatusCode::OK, "OK"), 29 + Err(e) => { 30 + error!("Health check failed: {:?}", e); 31 + (StatusCode::SERVICE_UNAVAILABLE, "Service Unavailable") 32 + } 33 + } 34 + } 35 + 36 #[derive(Deserialize)] 37 pub struct CreateAccountInput { 38 pub handle: String,
+24
src/lib.rs
···
··· 1 + pub mod api; 2 + pub mod state; 3 + pub mod auth; 4 + pub mod repo; 5 + 6 + use axum::{ 7 + routing::{get, post, any}, 8 + Router, 9 + }; 10 + use state::AppState; 11 + 12 + pub fn app(state: AppState) -> Router { 13 + Router::new() 14 + .route("/health", get(api::server::health)) 15 + .route("/xrpc/com.atproto.server.describeServer", get(api::server::describe_server)) 16 + .route("/xrpc/com.atproto.server.createAccount", post(api::server::create_account)) 17 + .route("/xrpc/com.atproto.server.createSession", post(api::server::create_session)) 18 + .route("/xrpc/com.atproto.server.getSession", get(api::server::get_session)) 19 + .route("/xrpc/com.atproto.server.deleteSession", post(api::server::delete_session)) 20 + .route("/xrpc/com.atproto.server.refreshSession", post(api::server::refresh_session)) 21 + .route("/xrpc/com.atproto.repo.createRecord", post(api::repo::create_record)) 22 + .route("/xrpc/{*method}", any(api::proxy::proxy_handler)) 23 + .with_state(state) 24 + }
+3 -45
src/main.rs
··· 1 - mod api; 2 - mod state; 3 - mod auth; 4 - mod repo; 5 - 6 - use axum::{ 7 - extract::State, 8 - routing::{get, post}, 9 - Router, 10 - Json, 11 - response::IntoResponse, 12 - http::StatusCode, 13 - }; 14 - use serde_json::json; 15 use std::net::SocketAddr; 16 - use state::AppState; 17 - use tracing::{info, error}; 18 19 #[tokio::main] 20 async fn main() { ··· 36 37 let state = AppState::new(pool); 38 39 - let app = Router::new() 40 - .route("/health", get(health)) 41 - .route("/xrpc/com.atproto.server.describeServer", get(describe_server)) 42 - .route("/xrpc/com.atproto.server.createAccount", post(api::server::create_account)) 43 - .route("/xrpc/com.atproto.server.createSession", post(api::server::create_session)) 44 - .route("/xrpc/com.atproto.server.getSession", get(api::server::get_session)) 45 - .route("/xrpc/com.atproto.server.deleteSession", post(api::server::delete_session)) 46 - .route("/xrpc/com.atproto.server.refreshSession", post(api::server::refresh_session)) 47 - .route("/xrpc/com.atproto.repo.createRecord", post(api::repo::create_record)) 48 - .with_state(state); 49 50 let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); 51 info!("listening on {}", addr); 52 let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); 53 axum::serve(listener, app).await.unwrap(); 54 } 55 - 56 - async fn health(State(state): State<AppState>) -> impl IntoResponse { 57 - match sqlx::query("SELECT 1").execute(&state.db).await { 58 - Ok(_) => (StatusCode::OK, "OK"), 59 - Err(e) => { 60 - error!("Health check failed: {:?}", e); 61 - (StatusCode::SERVICE_UNAVAILABLE, "Service Unavailable") 62 - } 63 - } 64 - } 65 - 66 - async fn describe_server() -> impl IntoResponse { 67 - let domains_str = std::env::var("AVAILABLE_USER_DOMAINS").unwrap_or_else(|_| "example.com".to_string()); 68 - let domains: Vec<&str> = domains_str.split(',').map(|s| s.trim()).collect(); 69 - 70 - Json(json!({ 71 - "availableUserDomains": domains 72 - })) 73 - }
··· 1 use std::net::SocketAddr; 2 + use bspds::state::AppState; 3 + use tracing::info; 4 5 #[tokio::main] 6 async fn main() { ··· 22 23 let state = AppState::new(pool); 24 25 + let app = bspds::app(state); 26 27 let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); 28 info!("listening on {}", addr); 29 let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); 30 axum::serve(listener, app).await.unwrap(); 31 }
+2 -2
tests/actor.rs
··· 8 let params = [ 9 ("actor", AUTH_DID), 10 ]; 11 - let res = client.get(format!("{}/xrpc/app.bsky.actor.getProfile", BASE_URL)) 12 .query(&params) 13 .bearer_auth(AUTH_TOKEN) 14 .send() ··· 25 ("q", "test"), 26 ("limit", "10"), 27 ]; 28 - let res = client.get(format!("{}/xrpc/app.bsky.actor.searchActors", BASE_URL)) 29 .query(&params) 30 .bearer_auth(AUTH_TOKEN) 31 .send()
··· 8 let params = [ 9 ("actor", AUTH_DID), 10 ]; 11 + let res = client.get(format!("{}/xrpc/app.bsky.actor.getProfile", base_url().await)) 12 .query(&params) 13 .bearer_auth(AUTH_TOKEN) 14 .send() ··· 25 ("q", "test"), 26 ("limit", "10"), 27 ]; 28 + let res = client.get(format!("{}/xrpc/app.bsky.actor.searchActors", base_url().await)) 29 .query(&params) 30 .bearer_auth(AUTH_TOKEN) 31 .send()
+70 -4
tests/common/mod.rs
··· 5 use std::collections::HashMap; 6 #[allow(unused_imports)] 7 use std::time::Duration; 8 9 - pub const BASE_URL: &str = "http://127.0.0.1:3000"; 10 #[allow(dead_code)] 11 pub const AUTH_TOKEN: &str = "test-token"; 12 #[allow(dead_code)] ··· 20 Client::new() 21 } 22 23 #[allow(dead_code)] 24 pub async fn upload_test_blob(client: &Client, data: &'static str, mime: &'static str) -> Value { 25 - let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", BASE_URL)) 26 .header(header::CONTENT_TYPE, mime) 27 .bearer_auth(AUTH_TOKEN) 28 .body(data) ··· 59 "record": record 60 }); 61 62 - let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", BASE_URL)) 63 .bearer_auth(AUTH_TOKEN) 64 .json(&payload) 65 .send() ··· 84 "password": "password" 85 }); 86 87 - let res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", BASE_URL)) 88 .json(&payload) 89 .send() 90 .await
··· 5 use std::collections::HashMap; 6 #[allow(unused_imports)] 7 use std::time::Duration; 8 + use std::sync::OnceLock; 9 + use bspds::state::AppState; 10 + use sqlx::postgres::PgPoolOptions; 11 + use tokio::net::TcpListener; 12 + use testcontainers::{runners::AsyncRunner, ContainerAsync, ImageExt}; 13 + use testcontainers_modules::postgres::Postgres; 14 15 + static SERVER_URL: OnceLock<String> = OnceLock::new(); 16 + static DB_CONTAINER: OnceLock<ContainerAsync<Postgres>> = OnceLock::new(); 17 + 18 #[allow(dead_code)] 19 pub const AUTH_TOKEN: &str = "test-token"; 20 #[allow(dead_code)] ··· 28 Client::new() 29 } 30 31 + pub async fn base_url() -> &'static str { 32 + SERVER_URL.get_or_init(|| { 33 + let (tx, rx) = std::sync::mpsc::channel(); 34 + 35 + std::thread::spawn(move || { 36 + if std::env::var("DOCKER_HOST").is_err() { 37 + if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") { 38 + let podman_sock = std::path::Path::new(&runtime_dir).join("podman/podman.sock"); 39 + if podman_sock.exists() { 40 + unsafe { std::env::set_var("DOCKER_HOST", format!("unix://{}", podman_sock.display())); } 41 + } 42 + } 43 + } 44 + 45 + let rt = tokio::runtime::Runtime::new().unwrap(); 46 + rt.block_on(async move { 47 + let container = Postgres::default().with_tag("18-alpine").start().await.expect("Failed to start Postgres"); 48 + let connection_string = format!( 49 + "postgres://postgres:postgres@127.0.0.1:{}/postgres", 50 + container.get_host_port_ipv4(5432).await.expect("Failed to get port") 51 + ); 52 + 53 + DB_CONTAINER.set(container).ok(); 54 + 55 + let url = spawn_app(connection_string).await; 56 + tx.send(url).unwrap(); 57 + std::future::pending::<()>().await; 58 + }); 59 + }); 60 + 61 + rx.recv().expect("Failed to start test server") 62 + }) 63 + } 64 + 65 + async fn spawn_app(database_url: String) -> String { 66 + let pool = PgPoolOptions::new() 67 + .connect(&database_url) 68 + .await 69 + .expect("Failed to connect to Postgres. Make sure the database is running."); 70 + 71 + sqlx::migrate!("./migrations") 72 + .run(&pool) 73 + .await 74 + .expect("Failed to run migrations"); 75 + 76 + let state = AppState::new(pool); 77 + let app = bspds::app(state); 78 + 79 + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); 80 + let addr = listener.local_addr().unwrap(); 81 + 82 + tokio::spawn(async move { 83 + axum::serve(listener, app).await.unwrap(); 84 + }); 85 + 86 + format!("http://{}", addr) 87 + } 88 + 89 #[allow(dead_code)] 90 pub async fn upload_test_blob(client: &Client, data: &'static str, mime: &'static str) -> Value { 91 + let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", base_url().await)) 92 .header(header::CONTENT_TYPE, mime) 93 .bearer_auth(AUTH_TOKEN) 94 .body(data) ··· 125 "record": record 126 }); 127 128 + let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", base_url().await)) 129 .bearer_auth(AUTH_TOKEN) 130 .json(&payload) 131 .send() ··· 150 "password": "password" 151 }); 152 153 + let res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await)) 154 .json(&payload) 155 .send() 156 .await
+3 -3
tests/feed.rs
··· 8 async fn test_get_timeline() { 9 let client = client(); 10 let params = [("limit", "30")]; 11 - let res = client.get(format!("{}/xrpc/app.bsky.feed.getTimeline", BASE_URL)) 12 .query(&params) 13 .bearer_auth(AUTH_TOKEN) 14 .send() ··· 25 ("actor", AUTH_DID), 26 ("limit", "30") 27 ]; 28 - let res = client.get(format!("{}/xrpc/app.bsky.feed.getAuthorFeed", BASE_URL)) 29 .query(&params) 30 .bearer_auth(AUTH_TOKEN) 31 .send() ··· 42 params.insert("uri", "at://did:plc:other/app.bsky.feed.post/3k12345"); 43 params.insert("depth", "5"); 44 45 - let res = client.get(format!("{}/xrpc/app.bsky.feed.getPostThread", BASE_URL)) 46 .query(&params) 47 .bearer_auth(AUTH_TOKEN) 48 .send()
··· 8 async fn test_get_timeline() { 9 let client = client(); 10 let params = [("limit", "30")]; 11 + let res = client.get(format!("{}/xrpc/app.bsky.feed.getTimeline", base_url().await)) 12 .query(&params) 13 .bearer_auth(AUTH_TOKEN) 14 .send() ··· 25 ("actor", AUTH_DID), 26 ("limit", "30") 27 ]; 28 + let res = client.get(format!("{}/xrpc/app.bsky.feed.getAuthorFeed", base_url().await)) 29 .query(&params) 30 .bearer_auth(AUTH_TOKEN) 31 .send() ··· 42 params.insert("uri", "at://did:plc:other/app.bsky.feed.post/3k12345"); 43 params.insert("depth", "5"); 44 45 + let res = client.get(format!("{}/xrpc/app.bsky.feed.getPostThread", base_url().await)) 46 .query(&params) 47 .bearer_auth(AUTH_TOKEN) 48 .send()
+4 -4
tests/graph.rs
··· 8 let params = [ 9 ("actor", AUTH_DID), 10 ]; 11 - let res = client.get(format!("{}/xrpc/app.bsky.graph.getFollows", BASE_URL)) 12 .query(&params) 13 .bearer_auth(AUTH_TOKEN) 14 .send() ··· 24 let params = [ 25 ("actor", AUTH_DID), 26 ]; 27 - let res = client.get(format!("{}/xrpc/app.bsky.graph.getFollowers", BASE_URL)) 28 .query(&params) 29 .bearer_auth(AUTH_TOKEN) 30 .send() ··· 40 let params = [ 41 ("limit", "25"), 42 ]; 43 - let res = client.get(format!("{}/xrpc/app.bsky.graph.getMutes", BASE_URL)) 44 .query(&params) 45 .bearer_auth(AUTH_TOKEN) 46 .send() ··· 57 let params = [ 58 ("limit", "25"), 59 ]; 60 - let res = client.get(format!("{}/xrpc/app.bsky.graph.getBlocks", BASE_URL)) 61 .query(&params) 62 .bearer_auth(AUTH_TOKEN) 63 .send()
··· 8 let params = [ 9 ("actor", AUTH_DID), 10 ]; 11 + let res = client.get(format!("{}/xrpc/app.bsky.graph.getFollows", base_url().await)) 12 .query(&params) 13 .bearer_auth(AUTH_TOKEN) 14 .send() ··· 24 let params = [ 25 ("actor", AUTH_DID), 26 ]; 27 + let res = client.get(format!("{}/xrpc/app.bsky.graph.getFollowers", base_url().await)) 28 .query(&params) 29 .bearer_auth(AUTH_TOKEN) 30 .send() ··· 40 let params = [ 41 ("limit", "25"), 42 ]; 43 + let res = client.get(format!("{}/xrpc/app.bsky.graph.getMutes", base_url().await)) 44 .query(&params) 45 .bearer_auth(AUTH_TOKEN) 46 .send() ··· 57 let params = [ 58 ("limit", "25"), 59 ]; 60 + let res = client.get(format!("{}/xrpc/app.bsky.graph.getBlocks", base_url().await)) 61 .query(&params) 62 .bearer_auth(AUTH_TOKEN) 63 .send()
+1 -1
tests/identity.rs
··· 8 let params = [ 9 ("handle", "bsky.app"), 10 ]; 11 - let res = client.get(format!("{}/xrpc/com.atproto.identity.resolveHandle", BASE_URL)) 12 .query(&params) 13 .send() 14 .await
··· 8 let params = [ 9 ("handle", "bsky.app"), 10 ]; 11 + let res = client.get(format!("{}/xrpc/com.atproto.identity.resolveHandle", base_url().await)) 12 .query(&params) 13 .send() 14 .await
+42 -42
tests/lifecycle.rs
··· 30 } 31 }); 32 33 - let create_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL)) 34 .bearer_auth(AUTH_TOKEN) 35 .json(&create_payload) 36 .send() ··· 47 ("collection", collection), 48 ("rkey", &rkey), 49 ]; 50 - let get_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", BASE_URL)) 51 .query(&params) 52 .send() 53 .await ··· 71 } 72 }); 73 74 - let update_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL)) 75 .bearer_auth(AUTH_TOKEN) 76 .json(&update_payload) 77 .send() ··· 81 assert_eq!(update_res.status(), StatusCode::OK, "Failed to update record"); 82 83 84 - let get_updated_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", BASE_URL)) 85 .query(&params) 86 .send() 87 .await ··· 98 "rkey": rkey 99 }); 100 101 - let delete_res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", BASE_URL)) 102 .bearer_auth(AUTH_TOKEN) 103 .json(&delete_payload) 104 .send() ··· 108 assert_eq!(delete_res.status(), StatusCode::OK, "Failed to delete record"); 109 110 111 - let get_deleted_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", BASE_URL)) 112 .query(&params) 113 .send() 114 .await ··· 157 } 158 }); 159 160 - let create_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL)) 161 .bearer_auth(AUTH_TOKEN) 162 .json(&create_payload) 163 .send() ··· 172 ("collection", collection), 173 ("rkey", &rkey), 174 ]; 175 - let get_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", BASE_URL)) 176 .query(&params) 177 .send() 178 .await ··· 202 } 203 }); 204 205 - let create_res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", BASE_URL)) 206 .bearer_auth(AUTH_TOKEN) 207 .json(&create_payload) 208 .send() ··· 219 let params_get_follows = [ 220 ("actor", AUTH_DID), 221 ]; 222 - let get_follows_res = client.get(format!("{}/xrpc/app.bsky.graph.getFollows", BASE_URL)) 223 .query(&params_get_follows) 224 .bearer_auth(AUTH_TOKEN) 225 .send() ··· 243 "rkey": rkey 244 }); 245 246 - let delete_res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", BASE_URL)) 247 .bearer_auth(AUTH_TOKEN) 248 .json(&delete_payload) 249 .send() ··· 253 assert_eq!(delete_res.status(), StatusCode::OK, "Failed to delete follow record"); 254 255 256 - let get_unfollowed_res = client.get(format!("{}/xrpc/app.bsky.graph.getFollows", BASE_URL)) 257 .query(&params_get_follows) 258 .bearer_auth(AUTH_TOKEN) 259 .send() ··· 290 } 291 }); 292 293 - let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL)) 294 .bearer_auth(AUTH_TOKEN) 295 .json(&payload) 296 .send() ··· 308 ("limit", "2"), 309 ]; 310 311 - let page1_res = client.get(format!("{}/xrpc/com.atproto.repo.listRecords", BASE_URL)) 312 .query(&params_page1) 313 .send() 314 .await ··· 330 ("cursor", cursor), 331 ]; 332 333 - let page2_res = client.get(format!("{}/xrpc/com.atproto.repo.listRecords", BASE_URL)) 334 .query(&params_page2) 335 .send() 336 .await ··· 351 "collection": collection, 352 "rkey": rkey 353 }); 354 - client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", BASE_URL)) 355 .bearer_auth(AUTH_TOKEN) 356 .json(&delete_payload) 357 .send() ··· 386 let params = [ 387 ("uri", &root_uri), 388 ]; 389 - let res = client.get(format!("{}/xrpc/app.bsky.feed.getPostThread", BASE_URL)) 390 .query(&params) 391 .bearer_auth(AUTH_TOKEN) 392 .send() ··· 410 411 412 let collection = "app.bsky.feed.post"; 413 - client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", BASE_URL)) 414 .bearer_auth(AUTH_TOKEN) 415 .json(&json!({ "repo": AUTH_DID, "collection": collection, "rkey": reply_rkey })) 416 .send().await.expect("Failed to delete reply"); 417 418 - client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", BASE_URL)) 419 .bearer_auth(AUTH_TOKEN) 420 .json(&json!({ "repo": AUTH_DID, "collection": collection, "rkey": root_rkey })) 421 .send().await.expect("Failed to delete root post"); ··· 436 "password": password 437 }); 438 439 - let create_res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", BASE_URL)) 440 .json(&create_account_payload) 441 .send() 442 .await ··· 455 "password": password 456 }); 457 458 - let session_res = client.post(format!("{}/xrpc/com.atproto.server.createSession", BASE_URL)) 459 .json(&session_payload) 460 .send() 461 .await ··· 479 } 480 }); 481 482 - let profile_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL)) 483 .bearer_auth(&session_jwt) 484 .json(&profile_payload) 485 .send() ··· 492 let params_get_profile = [ 493 ("actor", &handle), 494 ]; 495 - let get_profile_res = client.get(format!("{}/xrpc/app.bsky.actor.getProfile", BASE_URL)) 496 .query(&params_get_profile) 497 .send() 498 .await ··· 506 assert_eq!(profile_body["displayName"], "E2E Test User"); 507 508 509 - let logout_res = client.post(format!("{}/xrpc/com.atproto.server.deleteSession", BASE_URL)) 510 .bearer_auth(&session_jwt) 511 .send() 512 .await ··· 515 assert_eq!(logout_res.status(), StatusCode::OK, "Failed to delete session"); 516 517 518 - let get_session_res = client.get(format!("{}/xrpc/com.atproto.server.getSession", BASE_URL)) 519 .bearer_auth(&session_jwt) 520 .send() 521 .await ··· 536 "email": email, 537 "password": password 538 }); 539 - let create_res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", BASE_URL)) 540 .json(&create_account_payload) 541 .send() 542 .await ··· 557 "description": "A user created by the e2e test suite." 558 } 559 }); 560 - let profile_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL)) 561 .bearer_auth(&new_jwt) 562 .json(&profile_payload) 563 .send() ··· 581 "record": record 582 }); 583 584 - let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", BASE_URL)) 585 .bearer_auth(jwt) 586 .json(&payload) 587 .send() ··· 609 "rkey": rkey 610 }); 611 612 - let res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", BASE_URL)) 613 .bearer_auth(jwt) 614 .json(&payload) 615 .send() ··· 640 ).await; 641 let post_ref = json!({ "uri": post_uri, "cid": post_cid }); 642 643 - let count_res_1 = client.get(format!("{}/xrpc/app.bsky.notification.getUnreadCount", BASE_URL)) 644 .bearer_auth(&user_a_jwt) 645 .send().await.expect("getUnreadCount 1 failed"); 646 let count_body_1: Value = count_res_1.json().await.expect("count 1 not json"); ··· 677 678 tokio::time::sleep(Duration::from_millis(500)).await; 679 680 - let count_res_2 = client.get(format!("{}/xrpc/app.bsky.notification.getUnreadCount", BASE_URL)) 681 .bearer_auth(&user_a_jwt) 682 .send().await.expect("getUnreadCount 2 failed"); 683 let count_body_2: Value = count_res_2.json().await.expect("count 2 not json"); 684 assert_eq!(count_body_2["count"], 3, "Unread count was not 3 after actions"); 685 686 - let list_res = client.get(format!("{}/xrpc/app.bsky.notification.listNotifications", BASE_URL)) 687 .bearer_auth(&user_a_jwt) 688 .send().await.expect("listNotifications failed"); 689 let list_body: Value = list_res.json().await.expect("list not json"); ··· 699 assert!(has_like, "Notification list missing 'like'"); 700 assert!(has_reply, "Notification list missing 'reply'"); 701 702 - let count_res_3 = client.get(format!("{}/xrpc/app.bsky.notification.getUnreadCount", BASE_URL)) 703 .bearer_auth(&user_a_jwt) 704 .send().await.expect("getUnreadCount 3 failed"); 705 let count_body_3: Value = count_res_3.json().await.expect("count 3 not json"); ··· 727 ).await; 728 729 let feed_params_1 = [("actor", &user_b_did)]; 730 - let feed_res_1 = client.get(format!("{}/xrpc/app.bsky.feed.getAuthorFeed", BASE_URL)) 731 .query(&feed_params_1) 732 .bearer_auth(&user_a_jwt) 733 .send().await.expect("getAuthorFeed 1 failed"); ··· 749 let mute_rkey = mute_uri.split('/').last().unwrap(); 750 751 let feed_params_2 = [("actor", &user_b_did)]; 752 - let feed_res_2 = client.get(format!("{}/xrpc/app.bsky.feed.getAuthorFeed", BASE_URL)) 753 .query(&feed_params_2) 754 .bearer_auth(&user_a_jwt) 755 .send().await.expect("getAuthorFeed 2 failed"); ··· 765 ).await; 766 767 let feed_params_3 = [("actor", &user_b_did)]; 768 - let feed_res_3 = client.get(format!("{}/xrpc/app.bsky.feed.getAuthorFeed", BASE_URL)) 769 .query(&feed_params_3) 770 .bearer_auth(&user_a_jwt) 771 .send().await.expect("getAuthorFeed 3 failed"); ··· 783 784 let (user_did, user_jwt) = setup_new_user("user-conflict").await; 785 786 - let get_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", BASE_URL)) 787 .query(&[ 788 ("repo", &user_did), 789 ("collection", &"app.bsky.actor.profile".to_string()), ··· 803 }, 804 "swapCommit": cid_v1 // <-- Correctly point to v1 805 }); 806 - let update_res_v2 = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL)) 807 .bearer_auth(&user_jwt) 808 .json(&update_payload_v2) 809 .send().await.expect("putRecord v2 failed"); ··· 821 }, 822 "swapCommit": cid_v1 823 }); 824 - let update_res_v3_stale = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL)) 825 .bearer_auth(&user_jwt) 826 .json(&update_payload_v3_stale) 827 .send().await.expect("putRecord v3 (stale) failed"); ··· 842 }, 843 "swapCommit": cid_v2 // <-- Correct 844 }); 845 - let update_res_v3_good = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL)) 846 .bearer_auth(&user_jwt) 847 .json(&update_payload_v3_good) 848 .send().await.expect("putRecord v3 (good) failed"); ··· 894 }), 895 ).await; 896 897 - let thread_res_1 = client.get(format!("{}/xrpc/app.bsky.feed.getPostThread", BASE_URL)) 898 .query(&[("uri", &p1_uri)]) 899 .bearer_auth(&user_a_jwt) 900 .send().await.expect("getThread 1 failed"); ··· 914 &p2_rkey, 915 ).await; 916 917 - let thread_res_2 = client.get(format!("{}/xrpc/app.bsky.feed.getPostThread", BASE_URL)) 918 .query(&[("uri", &p1_uri)]) 919 .bearer_auth(&user_a_jwt) 920 .send().await.expect("getThread 2 failed");
··· 30 } 31 }); 32 33 + let create_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 34 .bearer_auth(AUTH_TOKEN) 35 .json(&create_payload) 36 .send() ··· 47 ("collection", collection), 48 ("rkey", &rkey), 49 ]; 50 + let get_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await)) 51 .query(&params) 52 .send() 53 .await ··· 71 } 72 }); 73 74 + let update_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 75 .bearer_auth(AUTH_TOKEN) 76 .json(&update_payload) 77 .send() ··· 81 assert_eq!(update_res.status(), StatusCode::OK, "Failed to update record"); 82 83 84 + let get_updated_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await)) 85 .query(&params) 86 .send() 87 .await ··· 98 "rkey": rkey 99 }); 100 101 + let delete_res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await)) 102 .bearer_auth(AUTH_TOKEN) 103 .json(&delete_payload) 104 .send() ··· 108 assert_eq!(delete_res.status(), StatusCode::OK, "Failed to delete record"); 109 110 111 + let get_deleted_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await)) 112 .query(&params) 113 .send() 114 .await ··· 157 } 158 }); 159 160 + let create_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 161 .bearer_auth(AUTH_TOKEN) 162 .json(&create_payload) 163 .send() ··· 172 ("collection", collection), 173 ("rkey", &rkey), 174 ]; 175 + let get_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await)) 176 .query(&params) 177 .send() 178 .await ··· 202 } 203 }); 204 205 + let create_res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", base_url().await)) 206 .bearer_auth(AUTH_TOKEN) 207 .json(&create_payload) 208 .send() ··· 219 let params_get_follows = [ 220 ("actor", AUTH_DID), 221 ]; 222 + let get_follows_res = client.get(format!("{}/xrpc/app.bsky.graph.getFollows", base_url().await)) 223 .query(&params_get_follows) 224 .bearer_auth(AUTH_TOKEN) 225 .send() ··· 243 "rkey": rkey 244 }); 245 246 + let delete_res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await)) 247 .bearer_auth(AUTH_TOKEN) 248 .json(&delete_payload) 249 .send() ··· 253 assert_eq!(delete_res.status(), StatusCode::OK, "Failed to delete follow record"); 254 255 256 + let get_unfollowed_res = client.get(format!("{}/xrpc/app.bsky.graph.getFollows", base_url().await)) 257 .query(&params_get_follows) 258 .bearer_auth(AUTH_TOKEN) 259 .send() ··· 290 } 291 }); 292 293 + let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 294 .bearer_auth(AUTH_TOKEN) 295 .json(&payload) 296 .send() ··· 308 ("limit", "2"), 309 ]; 310 311 + let page1_res = client.get(format!("{}/xrpc/com.atproto.repo.listRecords", base_url().await)) 312 .query(&params_page1) 313 .send() 314 .await ··· 330 ("cursor", cursor), 331 ]; 332 333 + let page2_res = client.get(format!("{}/xrpc/com.atproto.repo.listRecords", base_url().await)) 334 .query(&params_page2) 335 .send() 336 .await ··· 351 "collection": collection, 352 "rkey": rkey 353 }); 354 + client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await)) 355 .bearer_auth(AUTH_TOKEN) 356 .json(&delete_payload) 357 .send() ··· 386 let params = [ 387 ("uri", &root_uri), 388 ]; 389 + let res = client.get(format!("{}/xrpc/app.bsky.feed.getPostThread", base_url().await)) 390 .query(&params) 391 .bearer_auth(AUTH_TOKEN) 392 .send() ··· 410 411 412 let collection = "app.bsky.feed.post"; 413 + client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await)) 414 .bearer_auth(AUTH_TOKEN) 415 .json(&json!({ "repo": AUTH_DID, "collection": collection, "rkey": reply_rkey })) 416 .send().await.expect("Failed to delete reply"); 417 418 + client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await)) 419 .bearer_auth(AUTH_TOKEN) 420 .json(&json!({ "repo": AUTH_DID, "collection": collection, "rkey": root_rkey })) 421 .send().await.expect("Failed to delete root post"); ··· 436 "password": password 437 }); 438 439 + let create_res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await)) 440 .json(&create_account_payload) 441 .send() 442 .await ··· 455 "password": password 456 }); 457 458 + let session_res = client.post(format!("{}/xrpc/com.atproto.server.createSession", base_url().await)) 459 .json(&session_payload) 460 .send() 461 .await ··· 479 } 480 }); 481 482 + let profile_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 483 .bearer_auth(&session_jwt) 484 .json(&profile_payload) 485 .send() ··· 492 let params_get_profile = [ 493 ("actor", &handle), 494 ]; 495 + let get_profile_res = client.get(format!("{}/xrpc/app.bsky.actor.getProfile", base_url().await)) 496 .query(&params_get_profile) 497 .send() 498 .await ··· 506 assert_eq!(profile_body["displayName"], "E2E Test User"); 507 508 509 + let logout_res = client.post(format!("{}/xrpc/com.atproto.server.deleteSession", base_url().await)) 510 .bearer_auth(&session_jwt) 511 .send() 512 .await ··· 515 assert_eq!(logout_res.status(), StatusCode::OK, "Failed to delete session"); 516 517 518 + let get_session_res = client.get(format!("{}/xrpc/com.atproto.server.getSession", base_url().await)) 519 .bearer_auth(&session_jwt) 520 .send() 521 .await ··· 536 "email": email, 537 "password": password 538 }); 539 + let create_res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await)) 540 .json(&create_account_payload) 541 .send() 542 .await ··· 557 "description": "A user created by the e2e test suite." 558 } 559 }); 560 + let profile_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 561 .bearer_auth(&new_jwt) 562 .json(&profile_payload) 563 .send() ··· 581 "record": record 582 }); 583 584 + let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", base_url().await)) 585 .bearer_auth(jwt) 586 .json(&payload) 587 .send() ··· 609 "rkey": rkey 610 }); 611 612 + let res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await)) 613 .bearer_auth(jwt) 614 .json(&payload) 615 .send() ··· 640 ).await; 641 let post_ref = json!({ "uri": post_uri, "cid": post_cid }); 642 643 + let count_res_1 = client.get(format!("{}/xrpc/app.bsky.notification.getUnreadCount", base_url().await)) 644 .bearer_auth(&user_a_jwt) 645 .send().await.expect("getUnreadCount 1 failed"); 646 let count_body_1: Value = count_res_1.json().await.expect("count 1 not json"); ··· 677 678 tokio::time::sleep(Duration::from_millis(500)).await; 679 680 + let count_res_2 = client.get(format!("{}/xrpc/app.bsky.notification.getUnreadCount", base_url().await)) 681 .bearer_auth(&user_a_jwt) 682 .send().await.expect("getUnreadCount 2 failed"); 683 let count_body_2: Value = count_res_2.json().await.expect("count 2 not json"); 684 assert_eq!(count_body_2["count"], 3, "Unread count was not 3 after actions"); 685 686 + let list_res = client.get(format!("{}/xrpc/app.bsky.notification.listNotifications", base_url().await)) 687 .bearer_auth(&user_a_jwt) 688 .send().await.expect("listNotifications failed"); 689 let list_body: Value = list_res.json().await.expect("list not json"); ··· 699 assert!(has_like, "Notification list missing 'like'"); 700 assert!(has_reply, "Notification list missing 'reply'"); 701 702 + let count_res_3 = client.get(format!("{}/xrpc/app.bsky.notification.getUnreadCount", base_url().await)) 703 .bearer_auth(&user_a_jwt) 704 .send().await.expect("getUnreadCount 3 failed"); 705 let count_body_3: Value = count_res_3.json().await.expect("count 3 not json"); ··· 727 ).await; 728 729 let feed_params_1 = [("actor", &user_b_did)]; 730 + let feed_res_1 = client.get(format!("{}/xrpc/app.bsky.feed.getAuthorFeed", base_url().await)) 731 .query(&feed_params_1) 732 .bearer_auth(&user_a_jwt) 733 .send().await.expect("getAuthorFeed 1 failed"); ··· 749 let mute_rkey = mute_uri.split('/').last().unwrap(); 750 751 let feed_params_2 = [("actor", &user_b_did)]; 752 + let feed_res_2 = client.get(format!("{}/xrpc/app.bsky.feed.getAuthorFeed", base_url().await)) 753 .query(&feed_params_2) 754 .bearer_auth(&user_a_jwt) 755 .send().await.expect("getAuthorFeed 2 failed"); ··· 765 ).await; 766 767 let feed_params_3 = [("actor", &user_b_did)]; 768 + let feed_res_3 = client.get(format!("{}/xrpc/app.bsky.feed.getAuthorFeed", base_url().await)) 769 .query(&feed_params_3) 770 .bearer_auth(&user_a_jwt) 771 .send().await.expect("getAuthorFeed 3 failed"); ··· 783 784 let (user_did, user_jwt) = setup_new_user("user-conflict").await; 785 786 + let get_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await)) 787 .query(&[ 788 ("repo", &user_did), 789 ("collection", &"app.bsky.actor.profile".to_string()), ··· 803 }, 804 "swapCommit": cid_v1 // <-- Correctly point to v1 805 }); 806 + let update_res_v2 = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 807 .bearer_auth(&user_jwt) 808 .json(&update_payload_v2) 809 .send().await.expect("putRecord v2 failed"); ··· 821 }, 822 "swapCommit": cid_v1 823 }); 824 + let update_res_v3_stale = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 825 .bearer_auth(&user_jwt) 826 .json(&update_payload_v3_stale) 827 .send().await.expect("putRecord v3 (stale) failed"); ··· 842 }, 843 "swapCommit": cid_v2 // <-- Correct 844 }); 845 + let update_res_v3_good = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 846 .bearer_auth(&user_jwt) 847 .json(&update_payload_v3_good) 848 .send().await.expect("putRecord v3 (good) failed"); ··· 894 }), 895 ).await; 896 897 + let thread_res_1 = client.get(format!("{}/xrpc/app.bsky.feed.getPostThread", base_url().await)) 898 .query(&[("uri", &p1_uri)]) 899 .bearer_auth(&user_a_jwt) 900 .send().await.expect("getThread 1 failed"); ··· 914 &p2_rkey, 915 ).await; 916 917 + let thread_res_2 = client.get(format!("{}/xrpc/app.bsky.feed.getPostThread", base_url().await)) 918 .query(&[("uri", &p1_uri)]) 919 .bearer_auth(&user_a_jwt) 920 .send().await.expect("getThread 2 failed");
+2 -2
tests/notification.rs
··· 8 let params = [ 9 ("limit", "30"), 10 ]; 11 - let res = client.get(format!("{}/xrpc/app.bsky.notification.listNotifications", BASE_URL)) 12 .query(&params) 13 .bearer_auth(AUTH_TOKEN) 14 .send() ··· 21 #[tokio::test] 22 async fn test_get_unread_count() { 23 let client = client(); 24 - let res = client.get(format!("{}/xrpc/app.bsky.notification.getUnreadCount", BASE_URL)) 25 .bearer_auth(AUTH_TOKEN) 26 .send() 27 .await
··· 8 let params = [ 9 ("limit", "30"), 10 ]; 11 + let res = client.get(format!("{}/xrpc/app.bsky.notification.listNotifications", base_url().await)) 12 .query(&params) 13 .bearer_auth(AUTH_TOKEN) 14 .send() ··· 21 #[tokio::test] 22 async fn test_get_unread_count() { 23 let client = client(); 24 + let res = client.get(format!("{}/xrpc/app.bsky.notification.getUnreadCount", base_url().await)) 25 .bearer_auth(AUTH_TOKEN) 26 .send() 27 .await
+96
tests/proxy.rs
···
··· 1 + mod common; 2 + 3 + use axum::{ 4 + routing::any, 5 + Router, 6 + extract::Request, 7 + http::StatusCode, 8 + }; 9 + use tokio::net::TcpListener; 10 + use reqwest::Client; 11 + use std::sync::Arc; 12 + 13 + async fn spawn_mock_upstream() -> (String, tokio::sync::mpsc::Receiver<(String, String, Option<String>)>) { 14 + let (tx, rx) = tokio::sync::mpsc::channel(10); 15 + let tx = Arc::new(tx); 16 + 17 + let app = Router::new().fallback(any(move |req: Request| { 18 + let tx = tx.clone(); 19 + async move { 20 + let method = req.method().to_string(); 21 + let uri = req.uri().to_string(); 22 + let auth = req.headers().get("Authorization") 23 + .and_then(|h| h.to_str().ok()) 24 + .map(|s| s.to_string()); 25 + 26 + let _ = tx.send((method, uri, auth)).await; 27 + (StatusCode::OK, "Mock Response") 28 + } 29 + })); 30 + 31 + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); 32 + let addr = listener.local_addr().unwrap(); 33 + 34 + tokio::spawn(async move { 35 + axum::serve(listener, app).await.unwrap(); 36 + }); 37 + 38 + (format!("http://{}", addr), rx) 39 + } 40 + 41 + #[tokio::test] 42 + async fn test_proxy_via_header() { 43 + let app_url = common::base_url().await; 44 + let (upstream_url, mut rx) = spawn_mock_upstream().await; 45 + let client = Client::new(); 46 + 47 + let res = client.get(format!("{}/xrpc/com.example.test", app_url)) 48 + .header("atproto-proxy", &upstream_url) 49 + .header("Authorization", "Bearer test-token") 50 + .send() 51 + .await 52 + .unwrap(); 53 + 54 + assert_eq!(res.status(), StatusCode::OK); 55 + 56 + let (method, uri, auth) = rx.recv().await.expect("Upstream should receive request"); 57 + assert_eq!(method, "GET"); 58 + assert_eq!(uri, "/xrpc/com.example.test"); 59 + assert_eq!(auth, Some("Bearer test-token".to_string())); 60 + } 61 + 62 + #[tokio::test] 63 + async fn test_proxy_via_env_var() { 64 + let (upstream_url, mut rx) = spawn_mock_upstream().await; 65 + 66 + unsafe { std::env::set_var("APPVIEW_URL", &upstream_url); } 67 + 68 + let app_url = common::base_url().await; 69 + let client = Client::new(); 70 + 71 + let res = client.get(format!("{}/xrpc/com.example.envtest", app_url)) 72 + .send() 73 + .await 74 + .unwrap(); 75 + 76 + assert_eq!(res.status(), StatusCode::OK); 77 + 78 + let (method, uri, _) = rx.recv().await.expect("Upstream should receive request"); 79 + assert_eq!(method, "GET"); 80 + assert_eq!(uri, "/xrpc/com.example.envtest"); 81 + } 82 + 83 + #[tokio::test] 84 + async fn test_proxy_missing_config() { 85 + unsafe { std::env::remove_var("APPVIEW_URL"); } 86 + 87 + let app_url = common::base_url().await; 88 + let client = Client::new(); 89 + 90 + let res = client.get(format!("{}/xrpc/com.example.fail", app_url)) 91 + .send() 92 + .await 93 + .unwrap(); 94 + 95 + assert_eq!(res.status(), StatusCode::BAD_GATEWAY); 96 + }
+16 -16
tests/repo.rs
··· 15 ("rkey", "self"), 16 ]; 17 18 - let res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", BASE_URL)) 19 .query(&params) 20 .send() 21 .await ··· 36 ("rkey", "nonexistent"), 37 ]; 38 39 - let res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", BASE_URL)) 40 .query(&params) 41 .send() 42 .await ··· 51 #[ignore] 52 async fn test_upload_blob_no_auth() { 53 let client = client(); 54 - let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", BASE_URL)) 55 .header(header::CONTENT_TYPE, "text/plain") 56 .body("no auth") 57 .send() ··· 68 async fn test_upload_blob_success() { 69 let client = client(); 70 let (token, _) = create_account_and_login(&client).await; 71 - let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", BASE_URL)) 72 .header(header::CONTENT_TYPE, "text/plain") 73 .bearer_auth(token) 74 .body("This is our blob data") ··· 92 "record": {} 93 }); 94 95 - let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL)) 96 .json(&payload) 97 .send() 98 .await ··· 120 } 121 }); 122 123 - let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL)) 124 .bearer_auth(token) 125 .json(&payload) 126 .send() ··· 142 ("repo", "did:plc:12345"), 143 ]; 144 145 - let res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", BASE_URL)) 146 .query(&params) 147 .send() 148 .await ··· 156 #[ignore] 157 async fn test_upload_blob_bad_token() { 158 let client = client(); 159 - let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", BASE_URL)) 160 .header(header::CONTENT_TYPE, "text/plain") 161 .bearer_auth(BAD_AUTH_TOKEN) 162 .body("This is our blob data") ··· 187 } 188 }); 189 190 - let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL)) 191 .bearer_auth(token) 192 .json(&payload) 193 .send() ··· 215 } 216 }); 217 218 - let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL)) 219 .bearer_auth(token) 220 .json(&payload) 221 .send() ··· 231 async fn test_upload_blob_unsupported_mime_type() { 232 let client = client(); 233 let (token, _) = create_account_and_login(&client).await; 234 - let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", BASE_URL)) 235 .header(header::CONTENT_TYPE, "application/xml") 236 .bearer_auth(token) 237 .body("<xml>not an image</xml>") ··· 252 ("collection", "app.bsky.feed.post"), 253 ("limit", "10"), 254 ]; 255 - let res = client.get(format!("{}/xrpc/com.atproto.repo.listRecords", BASE_URL)) 256 .query(&params) 257 .send() 258 .await ··· 270 "collection": "app.bsky.feed.post", 271 "rkey": "some_post_to_delete" 272 }); 273 - let res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", BASE_URL)) 274 .bearer_auth(token) 275 .json(&payload) 276 .send() ··· 287 let params = [ 288 ("repo", did.as_str()), 289 ]; 290 - let res = client.get(format!("{}/xrpc/com.atproto.repo.describeRepo", BASE_URL)) 291 .query(&params) 292 .send() 293 .await ··· 310 } 311 }); 312 313 - let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", BASE_URL)) 314 .json(&payload) 315 .bearer_auth(token) // Assuming auth is required 316 .send() ··· 340 } 341 }); 342 343 - let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", BASE_URL)) 344 .json(&payload) 345 .bearer_auth(token) // Assuming auth is required 346 .send()
··· 15 ("rkey", "self"), 16 ]; 17 18 + let res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await)) 19 .query(&params) 20 .send() 21 .await ··· 36 ("rkey", "nonexistent"), 37 ]; 38 39 + let res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await)) 40 .query(&params) 41 .send() 42 .await ··· 51 #[ignore] 52 async fn test_upload_blob_no_auth() { 53 let client = client(); 54 + let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", base_url().await)) 55 .header(header::CONTENT_TYPE, "text/plain") 56 .body("no auth") 57 .send() ··· 68 async fn test_upload_blob_success() { 69 let client = client(); 70 let (token, _) = create_account_and_login(&client).await; 71 + let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", base_url().await)) 72 .header(header::CONTENT_TYPE, "text/plain") 73 .bearer_auth(token) 74 .body("This is our blob data") ··· 92 "record": {} 93 }); 94 95 + let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 96 .json(&payload) 97 .send() 98 .await ··· 120 } 121 }); 122 123 + let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 124 .bearer_auth(token) 125 .json(&payload) 126 .send() ··· 142 ("repo", "did:plc:12345"), 143 ]; 144 145 + let res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await)) 146 .query(&params) 147 .send() 148 .await ··· 156 #[ignore] 157 async fn test_upload_blob_bad_token() { 158 let client = client(); 159 + let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", base_url().await)) 160 .header(header::CONTENT_TYPE, "text/plain") 161 .bearer_auth(BAD_AUTH_TOKEN) 162 .body("This is our blob data") ··· 187 } 188 }); 189 190 + let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 191 .bearer_auth(token) 192 .json(&payload) 193 .send() ··· 215 } 216 }); 217 218 + let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 219 .bearer_auth(token) 220 .json(&payload) 221 .send() ··· 231 async fn test_upload_blob_unsupported_mime_type() { 232 let client = client(); 233 let (token, _) = create_account_and_login(&client).await; 234 + let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", base_url().await)) 235 .header(header::CONTENT_TYPE, "application/xml") 236 .bearer_auth(token) 237 .body("<xml>not an image</xml>") ··· 252 ("collection", "app.bsky.feed.post"), 253 ("limit", "10"), 254 ]; 255 + let res = client.get(format!("{}/xrpc/com.atproto.repo.listRecords", base_url().await)) 256 .query(&params) 257 .send() 258 .await ··· 270 "collection": "app.bsky.feed.post", 271 "rkey": "some_post_to_delete" 272 }); 273 + let res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await)) 274 .bearer_auth(token) 275 .json(&payload) 276 .send() ··· 287 let params = [ 288 ("repo", did.as_str()), 289 ]; 290 + let res = client.get(format!("{}/xrpc/com.atproto.repo.describeRepo", base_url().await)) 291 .query(&params) 292 .send() 293 .await ··· 310 } 311 }); 312 313 + let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", base_url().await)) 314 .json(&payload) 315 .bearer_auth(token) // Assuming auth is required 316 .send() ··· 340 } 341 }); 342 343 + let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", base_url().await)) 344 .json(&payload) 345 .bearer_auth(token) // Assuming auth is required 346 .send()
+11 -11
tests/server.rs
··· 7 #[tokio::test] 8 async fn test_health() { 9 let client = client(); 10 - let res = client.get(format!("{}/health", BASE_URL)) 11 .send() 12 .await 13 .expect("Failed to send request"); ··· 19 #[tokio::test] 20 async fn test_describe_server() { 21 let client = client(); 22 - let res = client.get(format!("{}/xrpc/com.atproto.server.describeServer", BASE_URL)) 23 .send() 24 .await 25 .expect("Failed to send request"); ··· 39 "email": format!("{}@example.com", handle), 40 "password": "password" 41 }); 42 - let _ = client.post(format!("{}/xrpc/com.atproto.server.createAccount", BASE_URL)) 43 .json(&payload) 44 .send() 45 .await; ··· 49 "password": "password" 50 }); 51 52 - let res = client.post(format!("{}/xrpc/com.atproto.server.createSession", BASE_URL)) 53 .json(&payload) 54 .send() 55 .await ··· 67 "password": "password" 68 }); 69 70 - let res = client.post(format!("{}/xrpc/com.atproto.server.createSession", BASE_URL)) 71 .json(&payload) 72 .send() 73 .await ··· 86 "password": "password" 87 }); 88 89 - let res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", BASE_URL)) 90 .json(&payload) 91 .send() 92 .await ··· 98 #[tokio::test] 99 async fn test_get_session() { 100 let client = client(); 101 - let res = client.get(format!("{}/xrpc/com.atproto.server.getSession", BASE_URL)) 102 .bearer_auth(AUTH_TOKEN) 103 .send() 104 .await ··· 117 "email": format!("{}@example.com", handle), 118 "password": "password" 119 }); 120 - let _ = client.post(format!("{}/xrpc/com.atproto.server.createAccount", BASE_URL)) 121 .json(&payload) 122 .send() 123 .await; ··· 126 "identifier": handle, 127 "password": "password" 128 }); 129 - let res = client.post(format!("{}/xrpc/com.atproto.server.createSession", BASE_URL)) 130 .json(&login_payload) 131 .send() 132 .await ··· 137 let refresh_jwt = body["refreshJwt"].as_str().expect("No refreshJwt").to_string(); 138 let access_jwt = body["accessJwt"].as_str().expect("No accessJwt").to_string(); 139 140 - let res = client.post(format!("{}/xrpc/com.atproto.server.refreshSession", BASE_URL)) 141 .bearer_auth(&refresh_jwt) 142 .send() 143 .await ··· 154 #[tokio::test] 155 async fn test_delete_session() { 156 let client = client(); 157 - let res = client.post(format!("{}/xrpc/com.atproto.server.deleteSession", BASE_URL)) 158 .bearer_auth(AUTH_TOKEN) 159 .send() 160 .await
··· 7 #[tokio::test] 8 async fn test_health() { 9 let client = client(); 10 + let res = client.get(format!("{}/health", base_url().await)) 11 .send() 12 .await 13 .expect("Failed to send request"); ··· 19 #[tokio::test] 20 async fn test_describe_server() { 21 let client = client(); 22 + let res = client.get(format!("{}/xrpc/com.atproto.server.describeServer", base_url().await)) 23 .send() 24 .await 25 .expect("Failed to send request"); ··· 39 "email": format!("{}@example.com", handle), 40 "password": "password" 41 }); 42 + let _ = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await)) 43 .json(&payload) 44 .send() 45 .await; ··· 49 "password": "password" 50 }); 51 52 + let res = client.post(format!("{}/xrpc/com.atproto.server.createSession", base_url().await)) 53 .json(&payload) 54 .send() 55 .await ··· 67 "password": "password" 68 }); 69 70 + let res = client.post(format!("{}/xrpc/com.atproto.server.createSession", base_url().await)) 71 .json(&payload) 72 .send() 73 .await ··· 86 "password": "password" 87 }); 88 89 + let res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await)) 90 .json(&payload) 91 .send() 92 .await ··· 98 #[tokio::test] 99 async fn test_get_session() { 100 let client = client(); 101 + let res = client.get(format!("{}/xrpc/com.atproto.server.getSession", base_url().await)) 102 .bearer_auth(AUTH_TOKEN) 103 .send() 104 .await ··· 117 "email": format!("{}@example.com", handle), 118 "password": "password" 119 }); 120 + let _ = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await)) 121 .json(&payload) 122 .send() 123 .await; ··· 126 "identifier": handle, 127 "password": "password" 128 }); 129 + let res = client.post(format!("{}/xrpc/com.atproto.server.createSession", base_url().await)) 130 .json(&login_payload) 131 .send() 132 .await ··· 137 let refresh_jwt = body["refreshJwt"].as_str().expect("No refreshJwt").to_string(); 138 let access_jwt = body["accessJwt"].as_str().expect("No accessJwt").to_string(); 139 140 + let res = client.post(format!("{}/xrpc/com.atproto.server.refreshSession", base_url().await)) 141 .bearer_auth(&refresh_jwt) 142 .send() 143 .await ··· 154 #[tokio::test] 155 async fn test_delete_session() { 156 let client = client(); 157 + let res = client.post(format!("{}/xrpc/com.atproto.server.deleteSession", base_url().await)) 158 .bearer_auth(AUTH_TOKEN) 159 .send() 160 .await
+2 -2
tests/sync.rs
··· 8 let params = [ 9 ("did", AUTH_DID), 10 ]; 11 - let res = client.get(format!("{}/xrpc/com.atproto.sync.getRepo", BASE_URL)) 12 .query(&params) 13 .send() 14 .await ··· 24 ("did", AUTH_DID), 25 // "cids" would be a list of CIDs 26 ]; 27 - let res = client.get(format!("{}/xrpc/com.atproto.sync.getBlocks", BASE_URL)) 28 .query(&params) 29 .send() 30 .await
··· 8 let params = [ 9 ("did", AUTH_DID), 10 ]; 11 + let res = client.get(format!("{}/xrpc/com.atproto.sync.getRepo", base_url().await)) 12 .query(&params) 13 .send() 14 .await ··· 24 ("did", AUTH_DID), 25 // "cids" would be a list of CIDs 26 ]; 27 + let res = client.get(format!("{}/xrpc/com.atproto.sync.getBlocks", base_url().await)) 28 .query(&params) 29 .send() 30 .await