diff --git a/.env.example b/.env.example index 9620138..c13ad07 100644 --- a/.env.example +++ b/.env.example @@ -15,3 +15,4 @@ OBJECT_STORAGE_FORCE_PATH_STYLE=false JWT_SECRET=your-super-secret-jwt-key-please-change-me PDS_HOSTNAME=localhost:3000 # The public-facing hostname of the PDS PLC_URL=plc.directory +APPVIEW_URL=https://api.bsky.app diff --git a/Cargo.lock b/Cargo.lock index b5a9d89..a5564a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -90,6 +90,22 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" +[[package]] +name = "astral-tokio-tar" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec179a06c1769b1e42e1e2cbe74c7dcdb3d6383c838454d063eaac5bbb7ebbe5" +dependencies = [ + "filetime", + "futures-core", + "libc", + "portable-atomic", + "rustc-hash", + "tokio", + "tokio-stream", + "xattr", +] + [[package]] name = "async-compression" version = "0.4.34" @@ -103,6 +119,28 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -215,6 +253,12 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" @@ -268,6 +312,83 @@ dependencies = [ "cipher", ] +[[package]] +name = "bollard" +version = "0.19.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87a52479c9237eb04047ddb94788c41ca0d26eaff8b697ecfbb4c32f7fdc3b1b" +dependencies = [ + "async-stream", + "base64 0.22.1", + "bitflags", + "bollard-buildkit-proto", + "bollard-stubs", + "bytes", + "chrono", + "futures-core", + "futures-util", + "hex", + "home", + "http", + "http-body-util", + "hyper", + "hyper-named-pipe", + "hyper-rustls", + "hyper-util", + "hyperlocal", + "log", + "num", + "pin-project-lite", + "rand 0.9.2", + "rustls", + "rustls-native-certs", + "rustls-pemfile", + "rustls-pki-types", + "serde", + "serde_derive", + "serde_json", + "serde_repr", + "serde_urlencoded", + "thiserror 2.0.17", + "tokio", + "tokio-stream", + "tokio-util", + "tonic", + "tower-service", + "url", + "winapi", +] + +[[package]] +name = "bollard-buildkit-proto" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a885520bf6249ab931a764ffdb87b0ceef48e6e7d807cfdb21b751e086e1ad" +dependencies = [ + "prost", + "prost-types", + "tonic", + "tonic-prost", + "ureq", +] + +[[package]] +name = "bollard-stubs" +version = "1.49.1-rc.28.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5731fe885755e92beff1950774068e0cae67ea6ec7587381536fca84f1779623" +dependencies = [ + "base64 0.22.1", + "bollard-buildkit-proto", + "bytes", + "chrono", + "prost", + "serde", + "serde_json", + "serde_repr", + "serde_with", +] + [[package]] name = "bon" version = "3.8.1" @@ -345,6 +466,8 @@ dependencies = [ "serde_json", "sha2", "sqlx", + "testcontainers", + "testcontainers-modules", "tokio", "tracing", "tracing-subscriber", @@ -934,6 +1057,17 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "docker_credential" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d89dfcba45b4afad7450a99b39e751590463e45c04728cf555d36bb66940de8" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -1072,6 +1206,16 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "etcetera" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" +dependencies = [ + "cfg-if", + "windows-sys 0.61.2", +] + [[package]] name = "event-listener" version = "5.4.1" @@ -1089,6 +1233,17 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "ferroid" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0e9414a6ae93ef993ce40a1e02944f13d4508e2bf6f1ced1580ce6910f08253" +dependencies = [ + "portable-atomic", + "rand 0.9.2", + "web-time", +] + [[package]] name = "ff" version = "0.13.1" @@ -1674,6 +1829,21 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-named-pipe" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278" +dependencies = [ + "hex", + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", + "winapi", +] + [[package]] name = "hyper-rustls" version = "0.27.7" @@ -1691,6 +1861,19 @@ dependencies = [ "webpki-roots 1.0.4", ] +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "hyper-tls" version = "0.6.0" @@ -1733,6 +1916,21 @@ dependencies = [ "windows-registry", ] +[[package]] +name = "hyperlocal" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "986c5ce3b994526b3cd75578e62554abd09f0899d6206de48b3e96ab34ccc8c7" +dependencies = [ + "hex", + "http-body-util", + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "iana-time-zone" version = "0.1.64" @@ -1970,6 +2168,15 @@ dependencies = [ "unsigned-varint 0.7.2", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -2662,7 +2869,7 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "security-framework", + "security-framework 2.11.1", "security-framework-sys", "tempfile", ] @@ -2698,6 +2905,20 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -2724,6 +2945,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -2750,6 +2980,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2931,6 +3172,31 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "parse-display" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914a1c2265c98e2446911282c6ac86d8524f495792c38c5bd884f80499c7538a" +dependencies = [ + "parse-display-derive", + "regex", + "regex-syntax", +] + +[[package]] +name = "parse-display-derive" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae7800a4c974efd12df917266338e79a7a74415173caf7e70aa0a0707345281" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "regex-syntax", + "structmeta", + "syn 2.0.111", +] + [[package]] name = "pem" version = "3.0.6" @@ -3053,6 +3319,12 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + [[package]] name = "potential_utf" version = "0.1.4" @@ -3148,6 +3420,38 @@ dependencies = [ "yansi", ] +[[package]] +name = "prost" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7231bd9b3d3d33c86b58adbac74b5ec0ad9f496b19d22801d773636feaa95f3d" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "prost-types" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9b4db3d6da204ed77bb26ba83b6122a73aeb2e87e25fbf7ad2e84c4ccbf8f72" +dependencies = [ + "prost", +] + [[package]] name = "pulldown-cmark" version = "0.9.6" @@ -3520,6 +3824,7 @@ version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ + "log", "once_cell", "ring", "rustls-pki-types", @@ -3528,6 +3833,27 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework 3.5.1", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "rustls-pki-types" version = "1.13.1" @@ -3648,6 +3974,19 @@ dependencies = [ "security-framework-sys", ] +[[package]] +name = "security-framework" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + [[package]] name = "security-framework-sys" version = "2.15.0" @@ -3860,6 +4199,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +dependencies = [ + "libc", +] + [[package]] name = "signature" version = "2.2.0" @@ -4125,7 +4473,7 @@ dependencies = [ "chrono", "crc", "dotenvy", - "etcetera", + "etcetera 0.8.0", "futures-channel", "futures-core", "futures-util", @@ -4251,6 +4599,29 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "structmeta" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e1575d8d40908d70f6fd05537266b90ae71b15dbbe7a8b7dffa2b759306d329" +dependencies = [ + "proc-macro2", + "quote", + "structmeta-derive", + "syn 2.0.111", +] + +[[package]] +name = "structmeta-derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "subtle" version = "2.6.1" @@ -4350,6 +4721,45 @@ dependencies = [ "utf-8", ] +[[package]] +name = "testcontainers" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a347cac4368ba4f1871743adb27dc14829024d26b1763572404726b0b9943eb8" +dependencies = [ + "astral-tokio-tar", + "async-trait", + "bollard", + "bytes", + "docker_credential", + "either", + "etcetera 0.11.0", + "ferroid", + "futures", + "itertools", + "log", + "memchr", + "parse-display", + "pin-project-lite", + "serde", + "serde_json", + "serde_with", + "thiserror 2.0.17", + "tokio", + "tokio-stream", + "tokio-util", + "url", +] + +[[package]] +name = "testcontainers-modules" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e75e78ff453128a2c7da9a5d5a3325ea34ea214d4bf51eab3417de23a4e5147" +dependencies = [ + "testcontainers", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -4488,6 +4898,7 @@ dependencies = [ "libc", "mio", "pin-project-lite", + "signal-hook-registry", "socket2 0.6.1", "tokio-macros", "windows-sys 0.61.2", @@ -4549,6 +4960,46 @@ dependencies = [ "tokio", ] +[[package]] +name = "tonic" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb7613188ce9f7df5bfe185db26c5814347d110db17920415cf2fbcad85e7203" +dependencies = [ + "async-trait", + "axum", + "base64 0.22.1", + "bytes", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "socket2 0.6.1", + "sync_wrapper", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-prost" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66bd50ad6ce1252d87ef024b3d64fe4c3cf54a86fb9ef4c631fdd0ded7aeaa67" +dependencies = [ + "bytes", + "prost", + "tonic", +] + [[package]] name = "tower" version = "0.5.2" @@ -4557,9 +5008,12 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", + "indexmap 2.12.1", "pin-project-lite", + "slab", "sync_wrapper", "tokio", + "tokio-util", "tower-layer", "tower-service", "tracing", @@ -4765,6 +5219,34 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "ureq" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d39cb1dbab692d82a977c0392ffac19e188bd9186a9f32806f0aaa859d75585a" +dependencies = [ + "base64 0.22.1", + "log", + "percent-encoding", + "rustls", + "rustls-pki-types", + "ureq-proto", + "utf-8", + "webpki-roots 1.0.4", +] + +[[package]] +name = "ureq-proto" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d81f9efa9df032be5934a46a068815a10a042b494b6a58cb0a1a97bb5467ed6f" +dependencies = [ + "base64 0.22.1", + "http", + "httparse", + "log", +] + [[package]] name = "url" version = "2.5.7" @@ -5018,6 +5500,22 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.11" @@ -5027,6 +5525,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows" version = "0.61.3" @@ -5496,6 +6000,16 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + [[package]] name = "xml5ever" version = "0.18.1" diff --git a/Cargo.toml b/Cargo.toml index d27d937..aad6195 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,3 +26,7 @@ tokio = { version = "1.48.0", features = ["macros", "rt-multi-thread", "time"] } tracing = "0.1.43" tracing-subscriber = "0.3.22" uuid = { version = "1.19.0", features = ["v4", "fast-rng"] } + +[dev-dependencies] +testcontainers = "0.26.0" +testcontainers-modules = { version = "0.14.0", features = ["postgres"] } diff --git a/TODO.md b/TODO.md index 3f49a9d..430aa5f 100644 --- a/TODO.md +++ b/TODO.md @@ -1,108 +1,81 @@ -# Implementation TODOs +# PDS Implementation TODOs -Lewis' special big boy todofile +Lewis' corrected big boy todofile -## 1. Server Infrastructure & Health +## 1. Server Infrastructure & Proxying - [x] Health Check - [x] Implement `GET /health` endpoint (returns "OK"). - [x] Server Description - [x] Implement `com.atproto.server.describeServer` (returns available user domains). +- [x] XRPC Proxying + - [x] Implement strict forwarding for all `app.bsky.*` and `chat.bsky.*` requests to an appview. + - [x] Forward Auth headers correctly. + - [x] Handle AppView errors/timeouts gracefully. ## 2. Authentication & Account Management (`com.atproto.server`) - [x] Account Creation - [x] Implement `com.atproto.server.createAccount`. - [x] Validate handle format (reject invalid characters). - - [x] Create DID for new user. - - [x] Initialize user repository. + - [x] Create DID for new user (PLC directory). + - [x] Initialize user repository (Root commit). - [x] Return access JWT and DID. - - [x] MST stuff I think... - + - [ ] Create DID for new user (did:web). - [x] Session Management - [x] Implement `com.atproto.server.createSession` (Login). - - [x] Validate identifier (handle/email) and password. - - [x] Return access JWT, refresh JWT, and DID. - [x] Implement `com.atproto.server.getSession`. - - [x] Verify JWT validity. - [x] Implement `com.atproto.server.refreshSession`. - [x] Implement `com.atproto.server.deleteSession` (Logout). - - [x] Invalidate current session/token. ## 3. Repository Operations (`com.atproto.repo`) - [ ] Record CRUD - [ ] Implement `com.atproto.repo.createRecord`. - - [ ] Generate `rkey` if not provided. - - [ ] Validate schema against Lexicon. - - [ ] Handle `swapCommit` for optimistic locking. + - [ ] Validate schema against Lexicon (just structure, not complex logic). + - [ ] Generate `rkey` (TID) if not provided. + - [ ] Handle MST (Merkle Search Tree) insertion. + - [ ] **Trigger Firehose Event**. - [ ] Implement `com.atproto.repo.putRecord`. - - [ ] Handle create vs update logic. - - [ ] Validate `repo` matches authenticated user. - - [ ] Validate record schema (e.g., missing required fields). - [ ] Implement `com.atproto.repo.getRecord`. - - [ ] Handle missing params (400 Bad Request). - - [ ] Handle non-existent record (404 Not Found). - [ ] Implement `com.atproto.repo.deleteRecord`. - [ ] Implement `com.atproto.repo.listRecords`. - - [ ] Support pagination (`limit`, `cursor`). + - [ ] Implement `com.atproto.repo.describeRepo`. - [ ] Blob Management - [ ] Implement `com.atproto.repo.uploadBlob`. - - [ ] Enforce authentication. - - [ ] Validate MIME types (reject unsupported). - - [ ] Return blob reference (`$link`). -- [ ] Repo Meta - - [ ] Implement `com.atproto.repo.describeRepo`. + - [ ] Store blob (S3). + - [ ] return `blob` ref (CID + MimeType). -## 4. Actor & Profile (`app.bsky.actor`) -- [ ] Profile Management - - [ ] Implement `app.bsky.actor.getProfile`. - - [ ] Resolve handle to DID. - - [ ] Return profile record data. -- [ ] Discovery - - [ ] Implement `app.bsky.actor.searchActors`. +## 4. Sync & Federation (`com.atproto.sync`) +- [ ] The Firehose (WebSocket) + - [ ] Implement `com.atproto.sync.subscribeRepos`. + - [ ] Broadcast real-time commit events. + - [ ] Handle cursor replay (backfill). +- [ ] Bulk Export + - [ ] Implement `com.atproto.sync.getRepo` (Return full CAR file of repo). + - [ ] Implement `com.atproto.sync.getBlocks` (Return specific blocks via CIDs). + - [ ] Implement `com.atproto.sync.getLatestCommit`. + - [ ] Implement `com.atproto.sync.getRecord` (Sync version, distinct from repo.getRecord). +- [ ] Blob Sync + - [ ] Implement `com.atproto.sync.getBlob`. + - [ ] Implement `com.atproto.sync.listBlobs`. +- [ ] Crawler Interaction + - [ ] Implement `com.atproto.sync.requestCrawl` (Notify relays to index us). -## 5. Feed & Timeline (`app.bsky.feed`) -- [ ] Feed Retrieval - - [ ] Implement `app.bsky.feed.getTimeline`. - - [ ] Implement `app.bsky.feed.getAuthorFeed`. - - [ ] Filter by actor. - - [ ] Respect mutes (if viewer is authenticated). - - [ ] Implement `app.bsky.feed.getPostThread`. - - [ ] Construct thread tree (parents, replies). - - [ ] Handle deleted posts (return `notFoundPost` view). -- [ ] Record Types - - [ ] Support `app.bsky.feed.post` record type. - - [ ] Support `app.bsky.feed.like` record type. - - [ ] Support `app.bsky.embed.images` in posts. - -## 6. Social Graph (`app.bsky.graph`) -- [ ] Relationships - - [ ] Implement `app.bsky.graph.getFollows`. - - [ ] Implement `app.bsky.graph.getFollowers`. - - [ ] Implement `app.bsky.graph.getMutes`. - - [ ] Implement `app.bsky.graph.getBlocks`. -- [ ] Record Types - - [ ] Support `app.bsky.graph.follow` record type. - - [ ] Support `app.bsky.graph.mute` record type. - -## 7. Notifications (`app.bsky.notification`) -- [ ] Notification Management - - [ ] Implement `app.bsky.notification.listNotifications`. - - [ ] Aggregate notifications (likes, follows, replies). - - [ ] Implement `app.bsky.notification.getUnreadCount`. - - [ ] Track read state. - - [ ] Reset count on list/read. - -## 8. Identity (`com.atproto.identity`) +## 5. Identity (`com.atproto.identity`) - [ ] Resolution - - [ ] Implement `com.atproto.identity.resolveHandle`. + - [ ] Implement `com.atproto.identity.resolveHandle` (Can be internal or proxy to PLC). + - [ ] Implement `/.well-known/did.json` (Depends on supporting did:web). -## 9. Sync & Federation (`com.atproto.sync`) -- [ ] Data Export - - [ ] Implement `com.atproto.sync.getRepo` (Export CAR file). - - [ ] Implement `com.atproto.sync.getBlocks`. +## 6. Record Schema Validation +- [ ] `app.bsky.feed.post` +- [ ] `app.bsky.feed.like` +- [ ] `app.bsky.feed.repost` +- [ ] `app.bsky.graph.follow` +- [ ] `app.bsky.graph.block` +- [ ] `app.bsky.actor.profile` +- [ ] Other app(view) validation too!!! -## 10. General Requirements +## 7. General Requirements +- [ ] IPLD & MST + - [ ] Implement Merkle Search Tree (MST) logic for repo signing. + - [ ] Implement CAR (Content Addressable Archives) encoding/decoding. - [ ] Validation - - [ ] Ensure all endpoints validate input parameters. - - [ ] Ensure proper error codes (400, 401, 404, 409). -- [ ] Concurrency - - [ ] Ensure thread safety for repo updates. + - [ ] DID PLC Operations (Sign rotation keys). diff --git a/justfile b/justfile new file mode 100644 index 0000000..43276c3 --- /dev/null +++ b/justfile @@ -0,0 +1,23 @@ +# Run all tests with correct threading models +test: test-proxy test-lifecycle test-others + +# Proxy tests modify environment variables, so must run single-threaded +# TODO: figure out how to run in parallel +test-proxy: + cargo test --test proxy -- --test-threads=1 + +# Lifecycle tests involve complex state mutations, run single-threaded to be safe +# TODO: figure out how to run in parallel +test-lifecycle: + cargo test --test lifecycle -- --test-threads=1 + +test-others: + cargo test --lib + cargo test --test actor + cargo test --test feed + cargo test --test graph + cargo test --test identity + cargo test --test notification + cargo test --test repo + cargo test --test server + cargo test --test sync diff --git a/src/api/mod.rs b/src/api/mod.rs index 1c8487a..eff39ef 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,2 +1,3 @@ pub mod server; pub mod repo; +pub mod proxy; diff --git a/src/api/proxy.rs b/src/api/proxy.rs new file mode 100644 index 0000000..01b8ccb --- /dev/null +++ b/src/api/proxy.rs @@ -0,0 +1,84 @@ +use axum::{ + extract::{Path, Query}, + http::{HeaderMap, Method, StatusCode}, + response::{IntoResponse, Response}, + body::Bytes, +}; +use reqwest::Client; +use tracing::{info, error}; +use std::collections::HashMap; + +pub async fn proxy_handler( + Path(method): Path, + method_verb: Method, + headers: HeaderMap, + Query(params): Query>, + body: Bytes, +) -> Response { + + let proxy_header = headers.get("atproto-proxy") + .and_then(|h| h.to_str().ok()) + .map(|s| s.to_string()); + + let appview_url = match proxy_header { + Some(url) => url, + None => match std::env::var("APPVIEW_URL") { + Ok(url) => url, + Err(_) => return (StatusCode::BAD_GATEWAY, "No upstream AppView configured").into_response(), + }, + }; + + let target_url = format!("{}/xrpc/{}", appview_url, method); + + info!("Proxying {} request to {}", method_verb, target_url); + + let client = Client::new(); + + let mut request_builder = client + .request(method_verb, &target_url) + .query(¶ms); + + for (key, value) in headers.iter() { + if key != "host" && key != "content-length" { + request_builder = request_builder.header(key, value); + } + } + + request_builder = request_builder.body(body); + + match request_builder.send().await { + Ok(resp) => { + let status = resp.status(); + let headers = resp.headers().clone(); + let body = match resp.bytes().await { + Ok(b) => b, + Err(e) => { + error!("Error reading proxy response body: {:?}", e); + return (StatusCode::BAD_GATEWAY, "Error reading upstream response").into_response(); + } + }; + + let mut response_builder = Response::builder().status(status); + + for (key, value) in headers.iter() { + response_builder = response_builder.header(key, value); + } + + match response_builder.body(axum::body::Body::from(body)) { + Ok(r) => r, + Err(e) => { + error!("Error building proxy response: {:?}", e); + (StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error").into_response() + } + } + }, + Err(e) => { + error!("Error sending proxy request: {:?}", e); + if e.is_timeout() { + (StatusCode::GATEWAY_TIMEOUT, "Upstream Timeout").into_response() + } else { + (StatusCode::BAD_GATEWAY, "Upstream Error").into_response() + } + } + } +} diff --git a/src/api/server.rs b/src/api/server.rs index 72b2d6f..b9d8a00 100644 --- a/src/api/server.rs +++ b/src/api/server.rs @@ -14,6 +14,25 @@ use jacquard_repo::{mst::Mst, commit::Commit, storage::BlockStore}; use jacquard::types::{string::Tid, did::Did, integer::LimitedU32}; use std::sync::Arc; +pub async fn describe_server() -> impl IntoResponse { + let domains_str = std::env::var("AVAILABLE_USER_DOMAINS").unwrap_or_else(|_| "example.com".to_string()); + let domains: Vec<&str> = domains_str.split(',').map(|s| s.trim()).collect(); + + Json(json!({ + "availableUserDomains": domains + })) +} + +pub async fn health(State(state): State) -> impl IntoResponse { + match sqlx::query("SELECT 1").execute(&state.db).await { + Ok(_) => (StatusCode::OK, "OK"), + Err(e) => { + error!("Health check failed: {:?}", e); + (StatusCode::SERVICE_UNAVAILABLE, "Service Unavailable") + } + } +} + #[derive(Deserialize)] pub struct CreateAccountInput { pub handle: String, diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..f7e23eb --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,24 @@ +pub mod api; +pub mod state; +pub mod auth; +pub mod repo; + +use axum::{ + routing::{get, post, any}, + Router, +}; +use state::AppState; + +pub fn app(state: AppState) -> Router { + Router::new() + .route("/health", get(api::server::health)) + .route("/xrpc/com.atproto.server.describeServer", get(api::server::describe_server)) + .route("/xrpc/com.atproto.server.createAccount", post(api::server::create_account)) + .route("/xrpc/com.atproto.server.createSession", post(api::server::create_session)) + .route("/xrpc/com.atproto.server.getSession", get(api::server::get_session)) + .route("/xrpc/com.atproto.server.deleteSession", post(api::server::delete_session)) + .route("/xrpc/com.atproto.server.refreshSession", post(api::server::refresh_session)) + .route("/xrpc/com.atproto.repo.createRecord", post(api::repo::create_record)) + .route("/xrpc/{*method}", any(api::proxy::proxy_handler)) + .with_state(state) +} diff --git a/src/main.rs b/src/main.rs index 627b446..5701657 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,20 +1,6 @@ -mod api; -mod state; -mod auth; -mod repo; - -use axum::{ - extract::State, - routing::{get, post}, - Router, - Json, - response::IntoResponse, - http::StatusCode, -}; -use serde_json::json; use std::net::SocketAddr; -use state::AppState; -use tracing::{info, error}; +use bspds::state::AppState; +use tracing::info; #[tokio::main] async fn main() { @@ -36,38 +22,10 @@ async fn main() { let state = AppState::new(pool); - let app = Router::new() - .route("/health", get(health)) - .route("/xrpc/com.atproto.server.describeServer", get(describe_server)) - .route("/xrpc/com.atproto.server.createAccount", post(api::server::create_account)) - .route("/xrpc/com.atproto.server.createSession", post(api::server::create_session)) - .route("/xrpc/com.atproto.server.getSession", get(api::server::get_session)) - .route("/xrpc/com.atproto.server.deleteSession", post(api::server::delete_session)) - .route("/xrpc/com.atproto.server.refreshSession", post(api::server::refresh_session)) - .route("/xrpc/com.atproto.repo.createRecord", post(api::repo::create_record)) - .with_state(state); + let app = bspds::app(state); let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); info!("listening on {}", addr); let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); axum::serve(listener, app).await.unwrap(); } - -async fn health(State(state): State) -> impl IntoResponse { - match sqlx::query("SELECT 1").execute(&state.db).await { - Ok(_) => (StatusCode::OK, "OK"), - Err(e) => { - error!("Health check failed: {:?}", e); - (StatusCode::SERVICE_UNAVAILABLE, "Service Unavailable") - } - } -} - -async fn describe_server() -> impl IntoResponse { - let domains_str = std::env::var("AVAILABLE_USER_DOMAINS").unwrap_or_else(|_| "example.com".to_string()); - let domains: Vec<&str> = domains_str.split(',').map(|s| s.trim()).collect(); - - Json(json!({ - "availableUserDomains": domains - })) -} diff --git a/tests/actor.rs b/tests/actor.rs index b0a9b80..94d8d58 100644 --- a/tests/actor.rs +++ b/tests/actor.rs @@ -8,7 +8,7 @@ async fn test_get_profile() { let params = [ ("actor", AUTH_DID), ]; - let res = client.get(format!("{}/xrpc/app.bsky.actor.getProfile", BASE_URL)) + let res = client.get(format!("{}/xrpc/app.bsky.actor.getProfile", base_url().await)) .query(¶ms) .bearer_auth(AUTH_TOKEN) .send() @@ -25,7 +25,7 @@ async fn test_search_actors() { ("q", "test"), ("limit", "10"), ]; - let res = client.get(format!("{}/xrpc/app.bsky.actor.searchActors", BASE_URL)) + let res = client.get(format!("{}/xrpc/app.bsky.actor.searchActors", base_url().await)) .query(¶ms) .bearer_auth(AUTH_TOKEN) .send() diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 2778057..df171bc 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -5,8 +5,16 @@ use chrono::Utc; use std::collections::HashMap; #[allow(unused_imports)] use std::time::Duration; +use std::sync::OnceLock; +use bspds::state::AppState; +use sqlx::postgres::PgPoolOptions; +use tokio::net::TcpListener; +use testcontainers::{runners::AsyncRunner, ContainerAsync, ImageExt}; +use testcontainers_modules::postgres::Postgres; + +static SERVER_URL: OnceLock = OnceLock::new(); +static DB_CONTAINER: OnceLock> = OnceLock::new(); -pub const BASE_URL: &str = "http://127.0.0.1:3000"; #[allow(dead_code)] pub const AUTH_TOKEN: &str = "test-token"; #[allow(dead_code)] @@ -20,9 +28,67 @@ pub fn client() -> Client { Client::new() } +pub async fn base_url() -> &'static str { + SERVER_URL.get_or_init(|| { + let (tx, rx) = std::sync::mpsc::channel(); + + std::thread::spawn(move || { + if std::env::var("DOCKER_HOST").is_err() { + if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") { + let podman_sock = std::path::Path::new(&runtime_dir).join("podman/podman.sock"); + if podman_sock.exists() { + unsafe { std::env::set_var("DOCKER_HOST", format!("unix://{}", podman_sock.display())); } + } + } + } + + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async move { + let container = Postgres::default().with_tag("18-alpine").start().await.expect("Failed to start Postgres"); + let connection_string = format!( + "postgres://postgres:postgres@127.0.0.1:{}/postgres", + container.get_host_port_ipv4(5432).await.expect("Failed to get port") + ); + + DB_CONTAINER.set(container).ok(); + + let url = spawn_app(connection_string).await; + tx.send(url).unwrap(); + std::future::pending::<()>().await; + }); + }); + + rx.recv().expect("Failed to start test server") + }) +} + +async fn spawn_app(database_url: String) -> String { + let pool = PgPoolOptions::new() + .connect(&database_url) + .await + .expect("Failed to connect to Postgres. Make sure the database is running."); + + sqlx::migrate!("./migrations") + .run(&pool) + .await + .expect("Failed to run migrations"); + + let state = AppState::new(pool); + let app = bspds::app(state); + + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + + tokio::spawn(async move { + axum::serve(listener, app).await.unwrap(); + }); + + format!("http://{}", addr) +} + #[allow(dead_code)] pub async fn upload_test_blob(client: &Client, data: &'static str, mime: &'static str) -> Value { - let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", BASE_URL)) + let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", base_url().await)) .header(header::CONTENT_TYPE, mime) .bearer_auth(AUTH_TOKEN) .body(data) @@ -59,7 +125,7 @@ pub async fn create_test_post( "record": record }); - let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", BASE_URL)) + let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", base_url().await)) .bearer_auth(AUTH_TOKEN) .json(&payload) .send() @@ -84,7 +150,7 @@ pub async fn create_account_and_login(client: &Client) -> (String, String) { "password": "password" }); - let res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", BASE_URL)) + let res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await)) .json(&payload) .send() .await diff --git a/tests/feed.rs b/tests/feed.rs index c0de0f5..f507fa6 100644 --- a/tests/feed.rs +++ b/tests/feed.rs @@ -8,7 +8,7 @@ use std::collections::HashMap; async fn test_get_timeline() { let client = client(); let params = [("limit", "30")]; - let res = client.get(format!("{}/xrpc/app.bsky.feed.getTimeline", BASE_URL)) + let res = client.get(format!("{}/xrpc/app.bsky.feed.getTimeline", base_url().await)) .query(¶ms) .bearer_auth(AUTH_TOKEN) .send() @@ -25,7 +25,7 @@ async fn test_get_author_feed() { ("actor", AUTH_DID), ("limit", "30") ]; - let res = client.get(format!("{}/xrpc/app.bsky.feed.getAuthorFeed", BASE_URL)) + let res = client.get(format!("{}/xrpc/app.bsky.feed.getAuthorFeed", base_url().await)) .query(¶ms) .bearer_auth(AUTH_TOKEN) .send() @@ -42,7 +42,7 @@ async fn test_get_post_thread() { params.insert("uri", "at://did:plc:other/app.bsky.feed.post/3k12345"); params.insert("depth", "5"); - let res = client.get(format!("{}/xrpc/app.bsky.feed.getPostThread", BASE_URL)) + let res = client.get(format!("{}/xrpc/app.bsky.feed.getPostThread", base_url().await)) .query(¶ms) .bearer_auth(AUTH_TOKEN) .send() diff --git a/tests/graph.rs b/tests/graph.rs index 57d34c6..b9cf422 100644 --- a/tests/graph.rs +++ b/tests/graph.rs @@ -8,7 +8,7 @@ async fn test_get_follows() { let params = [ ("actor", AUTH_DID), ]; - let res = client.get(format!("{}/xrpc/app.bsky.graph.getFollows", BASE_URL)) + let res = client.get(format!("{}/xrpc/app.bsky.graph.getFollows", base_url().await)) .query(¶ms) .bearer_auth(AUTH_TOKEN) .send() @@ -24,7 +24,7 @@ async fn test_get_followers() { let params = [ ("actor", AUTH_DID), ]; - let res = client.get(format!("{}/xrpc/app.bsky.graph.getFollowers", BASE_URL)) + let res = client.get(format!("{}/xrpc/app.bsky.graph.getFollowers", base_url().await)) .query(¶ms) .bearer_auth(AUTH_TOKEN) .send() @@ -40,7 +40,7 @@ async fn test_get_mutes() { let params = [ ("limit", "25"), ]; - let res = client.get(format!("{}/xrpc/app.bsky.graph.getMutes", BASE_URL)) + let res = client.get(format!("{}/xrpc/app.bsky.graph.getMutes", base_url().await)) .query(¶ms) .bearer_auth(AUTH_TOKEN) .send() @@ -57,7 +57,7 @@ async fn test_get_user_blocks() { let params = [ ("limit", "25"), ]; - let res = client.get(format!("{}/xrpc/app.bsky.graph.getBlocks", BASE_URL)) + let res = client.get(format!("{}/xrpc/app.bsky.graph.getBlocks", base_url().await)) .query(¶ms) .bearer_auth(AUTH_TOKEN) .send() diff --git a/tests/identity.rs b/tests/identity.rs index 9766ed9..423b98c 100644 --- a/tests/identity.rs +++ b/tests/identity.rs @@ -8,7 +8,7 @@ async fn test_resolve_handle() { let params = [ ("handle", "bsky.app"), ]; - let res = client.get(format!("{}/xrpc/com.atproto.identity.resolveHandle", BASE_URL)) + let res = client.get(format!("{}/xrpc/com.atproto.identity.resolveHandle", base_url().await)) .query(¶ms) .send() .await diff --git a/tests/lifecycle.rs b/tests/lifecycle.rs index 8380e77..d00af12 100644 --- a/tests/lifecycle.rs +++ b/tests/lifecycle.rs @@ -30,7 +30,7 @@ async fn test_post_crud_lifecycle() { } }); - let create_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL)) + let create_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) .bearer_auth(AUTH_TOKEN) .json(&create_payload) .send() @@ -47,7 +47,7 @@ async fn test_post_crud_lifecycle() { ("collection", collection), ("rkey", &rkey), ]; - let get_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", BASE_URL)) + let get_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await)) .query(¶ms) .send() .await @@ -71,7 +71,7 @@ async fn test_post_crud_lifecycle() { } }); - let update_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL)) + let update_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) .bearer_auth(AUTH_TOKEN) .json(&update_payload) .send() @@ -81,7 +81,7 @@ async fn test_post_crud_lifecycle() { assert_eq!(update_res.status(), StatusCode::OK, "Failed to update record"); - let get_updated_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", BASE_URL)) + let get_updated_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await)) .query(¶ms) .send() .await @@ -98,7 +98,7 @@ async fn test_post_crud_lifecycle() { "rkey": rkey }); - let delete_res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", BASE_URL)) + let delete_res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await)) .bearer_auth(AUTH_TOKEN) .json(&delete_payload) .send() @@ -108,7 +108,7 @@ async fn test_post_crud_lifecycle() { assert_eq!(delete_res.status(), StatusCode::OK, "Failed to delete record"); - let get_deleted_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", BASE_URL)) + let get_deleted_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await)) .query(¶ms) .send() .await @@ -157,7 +157,7 @@ async fn test_post_with_image_lifecycle() { } }); - let create_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL)) + let create_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) .bearer_auth(AUTH_TOKEN) .json(&create_payload) .send() @@ -172,7 +172,7 @@ async fn test_post_with_image_lifecycle() { ("collection", collection), ("rkey", &rkey), ]; - let get_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", BASE_URL)) + let get_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await)) .query(¶ms) .send() .await @@ -202,7 +202,7 @@ async fn test_graph_lifecycle_follow_unfollow() { } }); - let create_res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", BASE_URL)) + let create_res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", base_url().await)) .bearer_auth(AUTH_TOKEN) .json(&create_payload) .send() @@ -219,7 +219,7 @@ async fn test_graph_lifecycle_follow_unfollow() { let params_get_follows = [ ("actor", AUTH_DID), ]; - let get_follows_res = client.get(format!("{}/xrpc/app.bsky.graph.getFollows", BASE_URL)) + let get_follows_res = client.get(format!("{}/xrpc/app.bsky.graph.getFollows", base_url().await)) .query(¶ms_get_follows) .bearer_auth(AUTH_TOKEN) .send() @@ -243,7 +243,7 @@ async fn test_graph_lifecycle_follow_unfollow() { "rkey": rkey }); - let delete_res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", BASE_URL)) + let delete_res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await)) .bearer_auth(AUTH_TOKEN) .json(&delete_payload) .send() @@ -253,7 +253,7 @@ async fn test_graph_lifecycle_follow_unfollow() { assert_eq!(delete_res.status(), StatusCode::OK, "Failed to delete follow record"); - let get_unfollowed_res = client.get(format!("{}/xrpc/app.bsky.graph.getFollows", BASE_URL)) + let get_unfollowed_res = client.get(format!("{}/xrpc/app.bsky.graph.getFollows", base_url().await)) .query(¶ms_get_follows) .bearer_auth(AUTH_TOKEN) .send() @@ -290,7 +290,7 @@ async fn test_list_records_pagination() { } }); - let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL)) + let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) .bearer_auth(AUTH_TOKEN) .json(&payload) .send() @@ -308,7 +308,7 @@ async fn test_list_records_pagination() { ("limit", "2"), ]; - let page1_res = client.get(format!("{}/xrpc/com.atproto.repo.listRecords", BASE_URL)) + let page1_res = client.get(format!("{}/xrpc/com.atproto.repo.listRecords", base_url().await)) .query(¶ms_page1) .send() .await @@ -330,7 +330,7 @@ async fn test_list_records_pagination() { ("cursor", cursor), ]; - let page2_res = client.get(format!("{}/xrpc/com.atproto.repo.listRecords", BASE_URL)) + let page2_res = client.get(format!("{}/xrpc/com.atproto.repo.listRecords", base_url().await)) .query(¶ms_page2) .send() .await @@ -351,7 +351,7 @@ async fn test_list_records_pagination() { "collection": collection, "rkey": rkey }); - client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", BASE_URL)) + client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await)) .bearer_auth(AUTH_TOKEN) .json(&delete_payload) .send() @@ -386,7 +386,7 @@ async fn test_reply_thread_lifecycle() { let params = [ ("uri", &root_uri), ]; - let res = client.get(format!("{}/xrpc/app.bsky.feed.getPostThread", BASE_URL)) + let res = client.get(format!("{}/xrpc/app.bsky.feed.getPostThread", base_url().await)) .query(¶ms) .bearer_auth(AUTH_TOKEN) .send() @@ -410,12 +410,12 @@ async fn test_reply_thread_lifecycle() { let collection = "app.bsky.feed.post"; - client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", BASE_URL)) + client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await)) .bearer_auth(AUTH_TOKEN) .json(&json!({ "repo": AUTH_DID, "collection": collection, "rkey": reply_rkey })) .send().await.expect("Failed to delete reply"); - client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", BASE_URL)) + client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await)) .bearer_auth(AUTH_TOKEN) .json(&json!({ "repo": AUTH_DID, "collection": collection, "rkey": root_rkey })) .send().await.expect("Failed to delete root post"); @@ -436,7 +436,7 @@ async fn test_account_journey_lifecycle() { "password": password }); - let create_res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", BASE_URL)) + let create_res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await)) .json(&create_account_payload) .send() .await @@ -455,7 +455,7 @@ async fn test_account_journey_lifecycle() { "password": password }); - let session_res = client.post(format!("{}/xrpc/com.atproto.server.createSession", BASE_URL)) + let session_res = client.post(format!("{}/xrpc/com.atproto.server.createSession", base_url().await)) .json(&session_payload) .send() .await @@ -479,7 +479,7 @@ async fn test_account_journey_lifecycle() { } }); - let profile_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL)) + let profile_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) .bearer_auth(&session_jwt) .json(&profile_payload) .send() @@ -492,7 +492,7 @@ async fn test_account_journey_lifecycle() { let params_get_profile = [ ("actor", &handle), ]; - let get_profile_res = client.get(format!("{}/xrpc/app.bsky.actor.getProfile", BASE_URL)) + let get_profile_res = client.get(format!("{}/xrpc/app.bsky.actor.getProfile", base_url().await)) .query(¶ms_get_profile) .send() .await @@ -506,7 +506,7 @@ async fn test_account_journey_lifecycle() { assert_eq!(profile_body["displayName"], "E2E Test User"); - let logout_res = client.post(format!("{}/xrpc/com.atproto.server.deleteSession", BASE_URL)) + let logout_res = client.post(format!("{}/xrpc/com.atproto.server.deleteSession", base_url().await)) .bearer_auth(&session_jwt) .send() .await @@ -515,7 +515,7 @@ async fn test_account_journey_lifecycle() { assert_eq!(logout_res.status(), StatusCode::OK, "Failed to delete session"); - let get_session_res = client.get(format!("{}/xrpc/com.atproto.server.getSession", BASE_URL)) + let get_session_res = client.get(format!("{}/xrpc/com.atproto.server.getSession", base_url().await)) .bearer_auth(&session_jwt) .send() .await @@ -536,7 +536,7 @@ async fn setup_new_user(handle_prefix: &str) -> (String, String) { "email": email, "password": password }); - let create_res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", BASE_URL)) + let create_res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await)) .json(&create_account_payload) .send() .await @@ -557,7 +557,7 @@ async fn setup_new_user(handle_prefix: &str) -> (String, String) { "description": "A user created by the e2e test suite." } }); - let profile_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL)) + let profile_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) .bearer_auth(&new_jwt) .json(&profile_payload) .send() @@ -581,7 +581,7 @@ async fn create_record_as( "record": record }); - let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", BASE_URL)) + let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", base_url().await)) .bearer_auth(jwt) .json(&payload) .send() @@ -609,7 +609,7 @@ async fn delete_record_as( "rkey": rkey }); - let res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", BASE_URL)) + let res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await)) .bearer_auth(jwt) .json(&payload) .send() @@ -640,7 +640,7 @@ async fn test_notification_lifecycle() { ).await; let post_ref = json!({ "uri": post_uri, "cid": post_cid }); - let count_res_1 = client.get(format!("{}/xrpc/app.bsky.notification.getUnreadCount", BASE_URL)) + let count_res_1 = client.get(format!("{}/xrpc/app.bsky.notification.getUnreadCount", base_url().await)) .bearer_auth(&user_a_jwt) .send().await.expect("getUnreadCount 1 failed"); let count_body_1: Value = count_res_1.json().await.expect("count 1 not json"); @@ -677,13 +677,13 @@ async fn test_notification_lifecycle() { tokio::time::sleep(Duration::from_millis(500)).await; - let count_res_2 = client.get(format!("{}/xrpc/app.bsky.notification.getUnreadCount", BASE_URL)) + let count_res_2 = client.get(format!("{}/xrpc/app.bsky.notification.getUnreadCount", base_url().await)) .bearer_auth(&user_a_jwt) .send().await.expect("getUnreadCount 2 failed"); let count_body_2: Value = count_res_2.json().await.expect("count 2 not json"); assert_eq!(count_body_2["count"], 3, "Unread count was not 3 after actions"); - let list_res = client.get(format!("{}/xrpc/app.bsky.notification.listNotifications", BASE_URL)) + let list_res = client.get(format!("{}/xrpc/app.bsky.notification.listNotifications", base_url().await)) .bearer_auth(&user_a_jwt) .send().await.expect("listNotifications failed"); let list_body: Value = list_res.json().await.expect("list not json"); @@ -699,7 +699,7 @@ async fn test_notification_lifecycle() { assert!(has_like, "Notification list missing 'like'"); assert!(has_reply, "Notification list missing 'reply'"); - let count_res_3 = client.get(format!("{}/xrpc/app.bsky.notification.getUnreadCount", BASE_URL)) + let count_res_3 = client.get(format!("{}/xrpc/app.bsky.notification.getUnreadCount", base_url().await)) .bearer_auth(&user_a_jwt) .send().await.expect("getUnreadCount 3 failed"); let count_body_3: Value = count_res_3.json().await.expect("count 3 not json"); @@ -727,7 +727,7 @@ async fn test_mute_lifecycle_filters_feed() { ).await; let feed_params_1 = [("actor", &user_b_did)]; - let feed_res_1 = client.get(format!("{}/xrpc/app.bsky.feed.getAuthorFeed", BASE_URL)) + let feed_res_1 = client.get(format!("{}/xrpc/app.bsky.feed.getAuthorFeed", base_url().await)) .query(&feed_params_1) .bearer_auth(&user_a_jwt) .send().await.expect("getAuthorFeed 1 failed"); @@ -749,7 +749,7 @@ async fn test_mute_lifecycle_filters_feed() { let mute_rkey = mute_uri.split('/').last().unwrap(); let feed_params_2 = [("actor", &user_b_did)]; - let feed_res_2 = client.get(format!("{}/xrpc/app.bsky.feed.getAuthorFeed", BASE_URL)) + let feed_res_2 = client.get(format!("{}/xrpc/app.bsky.feed.getAuthorFeed", base_url().await)) .query(&feed_params_2) .bearer_auth(&user_a_jwt) .send().await.expect("getAuthorFeed 2 failed"); @@ -765,7 +765,7 @@ async fn test_mute_lifecycle_filters_feed() { ).await; let feed_params_3 = [("actor", &user_b_did)]; - let feed_res_3 = client.get(format!("{}/xrpc/app.bsky.feed.getAuthorFeed", BASE_URL)) + let feed_res_3 = client.get(format!("{}/xrpc/app.bsky.feed.getAuthorFeed", base_url().await)) .query(&feed_params_3) .bearer_auth(&user_a_jwt) .send().await.expect("getAuthorFeed 3 failed"); @@ -783,7 +783,7 @@ async fn test_record_update_conflict_lifecycle() { let (user_did, user_jwt) = setup_new_user("user-conflict").await; - let get_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", BASE_URL)) + let get_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await)) .query(&[ ("repo", &user_did), ("collection", &"app.bsky.actor.profile".to_string()), @@ -803,7 +803,7 @@ async fn test_record_update_conflict_lifecycle() { }, "swapCommit": cid_v1 // <-- Correctly point to v1 }); - let update_res_v2 = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL)) + let update_res_v2 = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) .bearer_auth(&user_jwt) .json(&update_payload_v2) .send().await.expect("putRecord v2 failed"); @@ -821,7 +821,7 @@ async fn test_record_update_conflict_lifecycle() { }, "swapCommit": cid_v1 }); - let update_res_v3_stale = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL)) + let update_res_v3_stale = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) .bearer_auth(&user_jwt) .json(&update_payload_v3_stale) .send().await.expect("putRecord v3 (stale) failed"); @@ -842,7 +842,7 @@ async fn test_record_update_conflict_lifecycle() { }, "swapCommit": cid_v2 // <-- Correct }); - let update_res_v3_good = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL)) + let update_res_v3_good = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) .bearer_auth(&user_jwt) .json(&update_payload_v3_good) .send().await.expect("putRecord v3 (good) failed"); @@ -894,7 +894,7 @@ async fn test_complex_thread_deletion_lifecycle() { }), ).await; - let thread_res_1 = client.get(format!("{}/xrpc/app.bsky.feed.getPostThread", BASE_URL)) + let thread_res_1 = client.get(format!("{}/xrpc/app.bsky.feed.getPostThread", base_url().await)) .query(&[("uri", &p1_uri)]) .bearer_auth(&user_a_jwt) .send().await.expect("getThread 1 failed"); @@ -914,7 +914,7 @@ async fn test_complex_thread_deletion_lifecycle() { &p2_rkey, ).await; - let thread_res_2 = client.get(format!("{}/xrpc/app.bsky.feed.getPostThread", BASE_URL)) + let thread_res_2 = client.get(format!("{}/xrpc/app.bsky.feed.getPostThread", base_url().await)) .query(&[("uri", &p1_uri)]) .bearer_auth(&user_a_jwt) .send().await.expect("getThread 2 failed"); diff --git a/tests/notification.rs b/tests/notification.rs index ced79a4..8c191fb 100644 --- a/tests/notification.rs +++ b/tests/notification.rs @@ -8,7 +8,7 @@ async fn test_list_notifications() { let params = [ ("limit", "30"), ]; - let res = client.get(format!("{}/xrpc/app.bsky.notification.listNotifications", BASE_URL)) + let res = client.get(format!("{}/xrpc/app.bsky.notification.listNotifications", base_url().await)) .query(¶ms) .bearer_auth(AUTH_TOKEN) .send() @@ -21,7 +21,7 @@ async fn test_list_notifications() { #[tokio::test] async fn test_get_unread_count() { let client = client(); - let res = client.get(format!("{}/xrpc/app.bsky.notification.getUnreadCount", BASE_URL)) + let res = client.get(format!("{}/xrpc/app.bsky.notification.getUnreadCount", base_url().await)) .bearer_auth(AUTH_TOKEN) .send() .await diff --git a/tests/proxy.rs b/tests/proxy.rs new file mode 100644 index 0000000..554c45f --- /dev/null +++ b/tests/proxy.rs @@ -0,0 +1,96 @@ +mod common; + +use axum::{ + routing::any, + Router, + extract::Request, + http::StatusCode, +}; +use tokio::net::TcpListener; +use reqwest::Client; +use std::sync::Arc; + +async fn spawn_mock_upstream() -> (String, tokio::sync::mpsc::Receiver<(String, String, Option)>) { + let (tx, rx) = tokio::sync::mpsc::channel(10); + let tx = Arc::new(tx); + + let app = Router::new().fallback(any(move |req: Request| { + let tx = tx.clone(); + async move { + let method = req.method().to_string(); + let uri = req.uri().to_string(); + let auth = req.headers().get("Authorization") + .and_then(|h| h.to_str().ok()) + .map(|s| s.to_string()); + + let _ = tx.send((method, uri, auth)).await; + (StatusCode::OK, "Mock Response") + } + })); + + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + + tokio::spawn(async move { + axum::serve(listener, app).await.unwrap(); + }); + + (format!("http://{}", addr), rx) +} + +#[tokio::test] +async fn test_proxy_via_header() { + let app_url = common::base_url().await; + let (upstream_url, mut rx) = spawn_mock_upstream().await; + let client = Client::new(); + + let res = client.get(format!("{}/xrpc/com.example.test", app_url)) + .header("atproto-proxy", &upstream_url) + .header("Authorization", "Bearer test-token") + .send() + .await + .unwrap(); + + assert_eq!(res.status(), StatusCode::OK); + + let (method, uri, auth) = rx.recv().await.expect("Upstream should receive request"); + assert_eq!(method, "GET"); + assert_eq!(uri, "/xrpc/com.example.test"); + assert_eq!(auth, Some("Bearer test-token".to_string())); +} + +#[tokio::test] +async fn test_proxy_via_env_var() { + let (upstream_url, mut rx) = spawn_mock_upstream().await; + + unsafe { std::env::set_var("APPVIEW_URL", &upstream_url); } + + let app_url = common::base_url().await; + let client = Client::new(); + + let res = client.get(format!("{}/xrpc/com.example.envtest", app_url)) + .send() + .await + .unwrap(); + + assert_eq!(res.status(), StatusCode::OK); + + let (method, uri, _) = rx.recv().await.expect("Upstream should receive request"); + assert_eq!(method, "GET"); + assert_eq!(uri, "/xrpc/com.example.envtest"); +} + +#[tokio::test] +async fn test_proxy_missing_config() { + unsafe { std::env::remove_var("APPVIEW_URL"); } + + let app_url = common::base_url().await; + let client = Client::new(); + + let res = client.get(format!("{}/xrpc/com.example.fail", app_url)) + .send() + .await + .unwrap(); + + assert_eq!(res.status(), StatusCode::BAD_GATEWAY); +} diff --git a/tests/repo.rs b/tests/repo.rs index 512cf32..9db159c 100644 --- a/tests/repo.rs +++ b/tests/repo.rs @@ -15,7 +15,7 @@ async fn test_get_record() { ("rkey", "self"), ]; - let res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", BASE_URL)) + let res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await)) .query(¶ms) .send() .await @@ -36,7 +36,7 @@ async fn test_get_record_not_found() { ("rkey", "nonexistent"), ]; - let res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", BASE_URL)) + let res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await)) .query(¶ms) .send() .await @@ -51,7 +51,7 @@ async fn test_get_record_not_found() { #[ignore] async fn test_upload_blob_no_auth() { let client = client(); - let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", BASE_URL)) + let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", base_url().await)) .header(header::CONTENT_TYPE, "text/plain") .body("no auth") .send() @@ -68,7 +68,7 @@ async fn test_upload_blob_no_auth() { async fn test_upload_blob_success() { let client = client(); let (token, _) = create_account_and_login(&client).await; - let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", BASE_URL)) + let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", base_url().await)) .header(header::CONTENT_TYPE, "text/plain") .bearer_auth(token) .body("This is our blob data") @@ -92,7 +92,7 @@ async fn test_put_record_no_auth() { "record": {} }); - let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL)) + let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) .json(&payload) .send() .await @@ -120,7 +120,7 @@ async fn test_put_record_success() { } }); - let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL)) + let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) .bearer_auth(token) .json(&payload) .send() @@ -142,7 +142,7 @@ async fn test_get_record_missing_params() { ("repo", "did:plc:12345"), ]; - let res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", BASE_URL)) + let res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await)) .query(¶ms) .send() .await @@ -156,7 +156,7 @@ async fn test_get_record_missing_params() { #[ignore] async fn test_upload_blob_bad_token() { let client = client(); - let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", BASE_URL)) + let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", base_url().await)) .header(header::CONTENT_TYPE, "text/plain") .bearer_auth(BAD_AUTH_TOKEN) .body("This is our blob data") @@ -187,7 +187,7 @@ async fn test_put_record_mismatched_repo() { } }); - let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL)) + let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) .bearer_auth(token) .json(&payload) .send() @@ -215,7 +215,7 @@ async fn test_put_record_invalid_schema() { } }); - let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL)) + let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) .bearer_auth(token) .json(&payload) .send() @@ -231,7 +231,7 @@ async fn test_put_record_invalid_schema() { async fn test_upload_blob_unsupported_mime_type() { let client = client(); let (token, _) = create_account_and_login(&client).await; - let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", BASE_URL)) + let res = client.post(format!("{}/xrpc/com.atproto.repo.uploadBlob", base_url().await)) .header(header::CONTENT_TYPE, "application/xml") .bearer_auth(token) .body("not an image") @@ -252,7 +252,7 @@ async fn test_list_records() { ("collection", "app.bsky.feed.post"), ("limit", "10"), ]; - let res = client.get(format!("{}/xrpc/com.atproto.repo.listRecords", BASE_URL)) + let res = client.get(format!("{}/xrpc/com.atproto.repo.listRecords", base_url().await)) .query(¶ms) .send() .await @@ -270,7 +270,7 @@ async fn test_delete_record() { "collection": "app.bsky.feed.post", "rkey": "some_post_to_delete" }); - let res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", BASE_URL)) + let res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await)) .bearer_auth(token) .json(&payload) .send() @@ -287,7 +287,7 @@ async fn test_describe_repo() { let params = [ ("repo", did.as_str()), ]; - let res = client.get(format!("{}/xrpc/com.atproto.repo.describeRepo", BASE_URL)) + let res = client.get(format!("{}/xrpc/com.atproto.repo.describeRepo", base_url().await)) .query(¶ms) .send() .await @@ -310,7 +310,7 @@ async fn test_create_record_success_with_generated_rkey() { } }); - let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", BASE_URL)) + let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", base_url().await)) .json(&payload) .bearer_auth(token) // Assuming auth is required .send() @@ -340,7 +340,7 @@ async fn test_create_record_success_with_provided_rkey() { } }); - let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", BASE_URL)) + let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", base_url().await)) .json(&payload) .bearer_auth(token) // Assuming auth is required .send() diff --git a/tests/server.rs b/tests/server.rs index e2bb839..e7fbca3 100644 --- a/tests/server.rs +++ b/tests/server.rs @@ -7,7 +7,7 @@ use serde_json::{json, Value}; #[tokio::test] async fn test_health() { let client = client(); - let res = client.get(format!("{}/health", BASE_URL)) + let res = client.get(format!("{}/health", base_url().await)) .send() .await .expect("Failed to send request"); @@ -19,7 +19,7 @@ async fn test_health() { #[tokio::test] async fn test_describe_server() { let client = client(); - let res = client.get(format!("{}/xrpc/com.atproto.server.describeServer", BASE_URL)) + let res = client.get(format!("{}/xrpc/com.atproto.server.describeServer", base_url().await)) .send() .await .expect("Failed to send request"); @@ -39,7 +39,7 @@ async fn test_create_session() { "email": format!("{}@example.com", handle), "password": "password" }); - let _ = client.post(format!("{}/xrpc/com.atproto.server.createAccount", BASE_URL)) + let _ = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await)) .json(&payload) .send() .await; @@ -49,7 +49,7 @@ async fn test_create_session() { "password": "password" }); - let res = client.post(format!("{}/xrpc/com.atproto.server.createSession", BASE_URL)) + let res = client.post(format!("{}/xrpc/com.atproto.server.createSession", base_url().await)) .json(&payload) .send() .await @@ -67,7 +67,7 @@ async fn test_create_session_missing_identifier() { "password": "password" }); - let res = client.post(format!("{}/xrpc/com.atproto.server.createSession", BASE_URL)) + let res = client.post(format!("{}/xrpc/com.atproto.server.createSession", base_url().await)) .json(&payload) .send() .await @@ -86,7 +86,7 @@ async fn test_create_account_invalid_handle() { "password": "password" }); - let res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", BASE_URL)) + let res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await)) .json(&payload) .send() .await @@ -98,7 +98,7 @@ async fn test_create_account_invalid_handle() { #[tokio::test] async fn test_get_session() { let client = client(); - let res = client.get(format!("{}/xrpc/com.atproto.server.getSession", BASE_URL)) + let res = client.get(format!("{}/xrpc/com.atproto.server.getSession", base_url().await)) .bearer_auth(AUTH_TOKEN) .send() .await @@ -117,7 +117,7 @@ async fn test_refresh_session() { "email": format!("{}@example.com", handle), "password": "password" }); - let _ = client.post(format!("{}/xrpc/com.atproto.server.createAccount", BASE_URL)) + let _ = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await)) .json(&payload) .send() .await; @@ -126,7 +126,7 @@ async fn test_refresh_session() { "identifier": handle, "password": "password" }); - let res = client.post(format!("{}/xrpc/com.atproto.server.createSession", BASE_URL)) + let res = client.post(format!("{}/xrpc/com.atproto.server.createSession", base_url().await)) .json(&login_payload) .send() .await @@ -137,7 +137,7 @@ async fn test_refresh_session() { let refresh_jwt = body["refreshJwt"].as_str().expect("No refreshJwt").to_string(); let access_jwt = body["accessJwt"].as_str().expect("No accessJwt").to_string(); - let res = client.post(format!("{}/xrpc/com.atproto.server.refreshSession", BASE_URL)) + let res = client.post(format!("{}/xrpc/com.atproto.server.refreshSession", base_url().await)) .bearer_auth(&refresh_jwt) .send() .await @@ -154,7 +154,7 @@ async fn test_refresh_session() { #[tokio::test] async fn test_delete_session() { let client = client(); - let res = client.post(format!("{}/xrpc/com.atproto.server.deleteSession", BASE_URL)) + let res = client.post(format!("{}/xrpc/com.atproto.server.deleteSession", base_url().await)) .bearer_auth(AUTH_TOKEN) .send() .await diff --git a/tests/sync.rs b/tests/sync.rs index 30095eb..78d2421 100644 --- a/tests/sync.rs +++ b/tests/sync.rs @@ -8,7 +8,7 @@ async fn test_get_repo() { let params = [ ("did", AUTH_DID), ]; - let res = client.get(format!("{}/xrpc/com.atproto.sync.getRepo", BASE_URL)) + let res = client.get(format!("{}/xrpc/com.atproto.sync.getRepo", base_url().await)) .query(¶ms) .send() .await @@ -24,7 +24,7 @@ async fn test_get_blocks() { ("did", AUTH_DID), // "cids" would be a list of CIDs ]; - let res = client.get(format!("{}/xrpc/com.atproto.sync.getBlocks", BASE_URL)) + let res = client.get(format!("{}/xrpc/com.atproto.sync.getBlocks", base_url().await)) .query(¶ms) .send() .await