diff --git a/.github/workflows/code-check.yml b/.github/workflows/code-check.yml index 68c6004..558823c 100644 --- a/.github/workflows/code-check.yml +++ b/.github/workflows/code-check.yml @@ -38,4 +38,7 @@ jobs: uses: actions-rust-lang/rustfmt@v1 - name: Run Clippy - run: cargo clippy + run: SQLX_OFFLINE=true cargo clippy --workspace + + - name: Run Tests + run: SQLX_OFFLINE=true cargo test --workspace diff --git a/.github/workflows/publish-new-build.yml b/.github/workflows/publish-new-build.yml index 753c9f0..bc41b3d 100644 --- a/.github/workflows/publish-new-build.yml +++ b/.github/workflows/publish-new-build.yml @@ -45,7 +45,7 @@ jobs: run: cargo set-version ${{ inputs.version }} - name: Run cargo check - run: cargo check + run: SQLX_OFFLINE=true cargo check - name: Git config run: | diff --git a/Cargo.lock b/Cargo.lock index 56f5294..d9871a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -104,7 +104,7 @@ dependencies = [ "serde", "serde_json", "serde_with", - "thiserror 2.0.3", + "thiserror 2.0.17", ] [[package]] @@ -141,7 +141,7 @@ dependencies = [ "alloy-rlp", "crc", "serde", - "thiserror 2.0.3", + "thiserror 2.0.17", ] [[package]] @@ -166,7 +166,7 @@ dependencies = [ "alloy-rlp", "borsh", "serde", - "thiserror 2.0.3", + "thiserror 2.0.17", ] [[package]] @@ -189,7 +189,7 @@ dependencies = [ "serde", "serde_with", "sha2", - "thiserror 2.0.3", + "thiserror 2.0.17", ] [[package]] @@ -215,7 +215,7 @@ dependencies = [ "http", "serde", "serde_json", - "thiserror 2.0.3", + "thiserror 2.0.17", "tracing", ] @@ -242,7 +242,7 @@ dependencies = [ "futures-utils-wasm", "serde", "serde_json", - "thiserror 2.0.3", + "thiserror 2.0.17", ] [[package]] @@ -279,7 +279,7 @@ dependencies = [ "proptest", "rand 0.9.2", "ruint", - "rustc-hash 2.1.1", + "rustc-hash", "serde", "sha3", "tiny-keccak", @@ -317,7 +317,7 @@ dependencies = [ "reqwest", "serde", "serde_json", - "thiserror 2.0.3", + "thiserror 2.0.17", "tokio", "tracing", "url", @@ -410,7 +410,7 @@ dependencies = [ "serde", "serde_json", "serde_with", - "thiserror 2.0.3", + "thiserror 2.0.17", ] [[package]] @@ -436,7 +436,7 @@ dependencies = [ "either", "elliptic-curve", "k256", - "thiserror 2.0.3", + "thiserror 2.0.17", ] [[package]] @@ -517,14 +517,14 @@ checksum = "fe215a2f9b51d5f1aa5c8cf22c8be8cdb354934de09c9a4e37aefb79b77552fd" dependencies = [ "alloy-json-rpc", "auto_impl", - "base64", + "base64 0.22.1", "derive_more", "futures", "futures-utils-wasm", "parking_lot 0.12.3", "serde", "serde_json", - "thiserror 2.0.3", + "thiserror 2.0.17", "tokio", "tower", "tracing", @@ -575,12 +575,6 @@ dependencies = [ "syn 2.0.110", ] -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -819,6 +813,22 @@ dependencies = [ "serde_json", ] +[[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-lock" version = "3.4.0" @@ -897,40 +907,37 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "aws-lc-rs" -version = "1.11.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f47bb8cc16b669d267eeccf585aea077d0882f4777b1c1f740217885d6e6e5a3" +checksum = "5932a7d9d28b0d2ea34c6b3779d35e3dd6f6345317c34e73438c4f1f29144151" dependencies = [ "aws-lc-sys", - "paste", "zeroize", ] [[package]] name = "aws-lc-sys" -version = "0.23.1" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2101df3813227bbaaaa0b04cd61c534c7954b22bd68d399b440be937dc63ff7" +checksum = "1826f2e4cfc2cd19ee53c42fbf68e2f81ec21108e0b7ecf6a71cf062137360fc" dependencies = [ "bindgen", "cc", "cmake", "dunce", "fs_extra", - "libc", - "paste", ] [[package]] name = "axum" -version = "0.7.9" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425" dependencies = [ - "async-trait", "axum-core", "axum-macros", "bytes", + "form_urlencoded", "futures-util", "http", "http-body", @@ -943,8 +950,7 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rustversion", - "serde", + "serde_core", "serde_json", "serde_path_to_error", "serde_urlencoded", @@ -958,19 +964,17 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.4.5" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" dependencies = [ - "async-trait", "bytes", - "futures-util", + "futures-core", "http", "http-body", "http-body-util", "mime", "pin-project-lite", - "rustversion", "sync_wrapper", "tower-layer", "tower-service", @@ -979,38 +983,66 @@ dependencies = [ [[package]] name = "axum-extra" -version = "0.9.6" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c794b30c904f0a1c2fb7740f7df7f7972dfaa14ef6f57cb6178dc63e5dca2f04" +checksum = "9963ff19f40c6102c76756ef0a46004c0d58957d87259fc9208ff8441c12ab96" dependencies = [ "axum", "axum-core", "bytes", - "fastrand", "futures-util", "http", "http-body", "http-body-util", "mime", - "multer", "pin-project-lite", - "serde", - "tower", + "rustversion", + "serde_core", "tower-layer", "tower-service", + "tracing", ] [[package]] name = "axum-macros" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce" +checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" dependencies = [ "proc-macro2", "quote", "syn 2.0.110", ] +[[package]] +name = "axum-test" +version = "18.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d419a2aae56fdf2bca28b274fd3f57dbc5cb8f2143c1c8629c82dbc75992596" +dependencies = [ + "anyhow", + "axum", + "bytes", + "bytesize", + "cookie", + "expect-json", + "http", + "http-body-util", + "hyper", + "hyper-util", + "mime", + "pretty_assertions", + "reserve-port", + "rust-multipart-rfc7578_2", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "tokio", + "tower", + "url", +] + [[package]] name = "backtrace" version = "0.3.71" @@ -1038,6 +1070,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +[[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" @@ -1065,25 +1103,22 @@ dependencies = [ [[package]] name = "bindgen" -version = "0.69.5" +version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ "bitflags 2.10.0", "cexpr", "clang-sys", - "itertools 0.12.1", - "lazy_static", - "lazycell", + "itertools 0.13.0", "log", "prettyplease", "proc-macro2", "quote", "regex", - "rustc-hash 1.1.0", + "rustc-hash", "shlex", "syn 2.0.110", - "which", ] [[package]] @@ -1165,6 +1200,83 @@ dependencies = [ "zeroize", ] +[[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 2.10.0", + "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 = "borsh" version = "1.5.7" @@ -1217,13 +1329,19 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.8.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" dependencies = [ "serde", ] +[[package]] +name = "bytesize" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c99fa31e08a43eaa5913ef68d7e01c37a2bdce6ed648168239ad33b7d30a9cd8" + [[package]] name = "c-kzg" version = "2.1.5" @@ -1241,10 +1359,11 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.1" +version = "1.2.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" +checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07" dependencies = [ + "find-msvc-tools", "jobserver", "libc", "shlex", @@ -1273,17 +1392,16 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.38" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.52.6", + "windows-link 0.2.1", ] [[package]] @@ -1299,9 +1417,9 @@ dependencies = [ [[package]] name = "cmake" -version = "0.1.52" +version = "0.1.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c682c223677e0e5b6b7f63a64b9351844c3f1b1678a68b7ee617e30fb082620e" +checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" dependencies = [ "cc", ] @@ -1333,16 +1451,6 @@ dependencies = [ "tracing-error", ] -[[package]] -name = "common" -version = "1.0.3" -source = "git+https://github.com/fidlabs/bandwidth-measurement-system.git?rev=0ca8779#0ca87794de0ffb44c9b7f2fe354ef0a22ec703f6" -dependencies = [ - "axum", - "serde", - "utoipa", -] - [[package]] name = "concurrent-queue" version = "2.5.0" @@ -1370,6 +1478,16 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1380,6 +1498,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -1567,12 +1695,12 @@ dependencies = [ [[package]] name = "deadpool" -version = "0.10.0" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb84100978c1c7b37f09ed3ce3e5f843af02c2a2c431bae5b19230dad2c1b490" +checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b" dependencies = [ - "async-trait", "deadpool-runtime", + "lazy_static", "num_cpus", "tokio", ] @@ -1647,6 +1775,12 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "digest" version = "0.9.0" @@ -1679,6 +1813,17 @@ dependencies = [ "syn 2.0.110", ] +[[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" @@ -1753,6 +1898,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" +dependencies = [ + "serde", +] + [[package]] name = "encoding_rs" version = "0.8.35" @@ -1788,14 +1942,25 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "erased-serde" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + [[package]] name = "errno" -version = "0.3.9" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1809,6 +1974,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "etcetera" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c7b13d0780cb82722fd59f6f57f925e143427e4a75313a6c77243bf5326ae6" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.59.0", +] + [[package]] name = "event-listener" version = "5.3.1" @@ -1830,6 +2006,34 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "expect-json" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7519e78573c950576b89eb4f4fe82aedf3a80639245afa07e3ee3d199dcdb29e" +dependencies = [ + "chrono", + "email_address", + "expect-json-macros", + "num", + "serde", + "serde_json", + "thiserror 2.0.17", + "typetag", + "uuid", +] + +[[package]] +name = "expect-json-macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bf7f5979e98460a0eb412665514594f68f366a32b85fa8d7ffb65bb1edee6a0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + [[package]] name = "eyre" version = "0.6.12" @@ -1878,6 +2082,24 @@ dependencies = [ "subtle", ] +[[package]] +name = "filetime" +version = "0.2.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.60.2", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + [[package]] name = "fixed-hash" version = "0.8.0" @@ -1892,12 +2114,13 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.35" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" dependencies = [ "crc32fast", - "miniz_oxide 0.8.0", + "libz-rs-sys", + "miniz_oxide 0.8.9", ] [[package]] @@ -2132,9 +2355,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasip2", + "wasm-bindgen", ] [[package]] @@ -2151,23 +2376,25 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "governor" -version = "0.8.0" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "842dc78579ce01e6a1576ad896edc92fca002dd60c9c3746b7fc2bec6fb429d0" +checksum = "6e23d5986fd4364c2fb7498523540618b4b8d92eec6c36a02e565f66748e2f79" dependencies = [ "cfg-if", "dashmap", "futures-sink", "futures-timer", "futures-util", - "no-std-compat", + "getrandom 0.3.4", + "hashbrown 0.16.0", "nonzero_ext", "parking_lot 0.12.3", "portable-atomic", "quanta", - "rand 0.8.5", + "rand 0.9.2", "smallvec", "spinning_top", + "web-time", ] [[package]] @@ -2233,6 +2460,8 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" dependencies = [ + "allocator-api2", + "equivalent", "foldhash 0.2.0", "serde", ] @@ -2308,9 +2537,9 @@ dependencies = [ [[package]] name = "http" -version = "1.1.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", @@ -2354,13 +2583,14 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.5.1" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", + "futures-core", "h2", "http", "http-body", @@ -2368,11 +2598,27 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "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.3" @@ -2388,14 +2634,27 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots", + "webpki-roots 0.26.7", ] [[package]] -name = "hyper-tls" -version = "0.6.0" +name = "hyper-timeout" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", @@ -2420,12 +2679,27 @@ dependencies = [ "http-body", "hyper", "pin-project-lite", - "socket2", + "socket2 0.5.7", "tokio", "tower-service", "tracing", ] +[[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.61" @@ -2655,6 +2929,15 @@ dependencies = [ "web-sys", ] +[[package]] +name = "inventory" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e" +dependencies = [ + "rustversion", +] + [[package]] name = "ipnet" version = "2.10.1" @@ -2670,15 +2953,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.13.0" @@ -2714,10 +2988,11 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.72" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" dependencies = [ + "once_cell", "wasm-bindgen", ] @@ -2763,17 +3038,11 @@ dependencies = [ "spin", ] -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - [[package]] name = "libc" -version = "0.2.166" +version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2ccc108bbc0b1331bd061864e7cd823c0cab660bbe6970e66e2c0614decde36" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "libloading" @@ -2806,6 +3075,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "libredox" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4488594b9328dee448adb906d8b126d9b7deb7cf5c22161ee591610bb1be83c0" +dependencies = [ + "bitflags 2.10.0", + "libc", + "redox_syscall 0.5.7", +] + [[package]] name = "libsqlite3-sys" version = "0.30.1" @@ -2817,12 +3097,27 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libz-rs-sys" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "840db8cf39d9ec4dd794376f38acc40d0fc65eec2a8f484f7fd375b84602becd" +dependencies = [ + "zlib-rs", +] + [[package]] name = "linux-raw-sys" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + [[package]] name = "litemap" version = "0.7.4" @@ -2847,9 +3142,9 @@ checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" [[package]] name = "log" -version = "0.4.22" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "loom" @@ -2895,9 +3190,9 @@ dependencies = [ [[package]] name = "matchit" -version = "0.7.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "md-5" @@ -2948,11 +3243,12 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.8.0" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -2989,23 +3285,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "multer" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" -dependencies = [ - "bytes", - "encoding_rs", - "futures-util", - "http", - "httparse", - "memchr", - "mime", - "spin", - "version_check", -] - [[package]] name = "multiaddr" version = "0.18.2" @@ -3058,17 +3337,11 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "security-framework", + "security-framework 2.11.1", "security-framework-sys", "tempfile", ] -[[package]] -name = "no-std-compat" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" - [[package]] name = "nom" version = "7.1.3" @@ -3101,6 +3374,20 @@ dependencies = [ "winapi", ] +[[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" @@ -3128,6 +3415,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" @@ -3154,6 +3450,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" @@ -3252,9 +3559,9 @@ dependencies = [ [[package]] name = "openssl-probe" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" @@ -3360,6 +3667,31 @@ dependencies = [ "windows-targets 0.52.6", ] +[[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 0.8.5", +] + +[[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 0.8.5", + "structmeta", + "syn 2.0.110", +] + [[package]] name = "paste" version = "1.0.15" @@ -3471,6 +3803,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "prettyplease" version = "0.2.25" @@ -3525,9 +3867,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.92" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] @@ -3551,6 +3893,38 @@ dependencies = [ "unarray", ] +[[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 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "prost-types" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9b4db3d6da204ed77bb26ba83b6122a73aeb2e87e25fbf7ad2e84c4ccbf8f72" +dependencies = [ + "prost", +] + [[package]] name = "quanta" version = "0.12.5" @@ -3591,10 +3965,10 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash 2.1.1", + "rustc-hash", "rustls", - "socket2", - "thiserror 2.0.3", + "socket2 0.5.7", + "thiserror 2.0.17", "tokio", "tracing", ] @@ -3609,11 +3983,11 @@ dependencies = [ "getrandom 0.2.15", "rand 0.8.5", "ring", - "rustc-hash 2.1.1", + "rustc-hash", "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.3", + "thiserror 2.0.17", "tinyvec", "tracing", "web-time", @@ -3628,16 +4002,16 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2", + "socket2 0.5.7", "tracing", "windows-sys 0.59.0", ] [[package]] name = "quote" -version = "1.0.37" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -3822,7 +4196,7 @@ version = "0.12.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "encoding_rs", "futures-channel", @@ -3861,7 +4235,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots", + "webpki-roots 0.26.7", "windows-registry", ] @@ -3902,6 +4276,15 @@ dependencies = [ "wasm-timer", ] +[[package]] +name = "reserve-port" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21918d6644020c6f6ef1993242989bf6d4952d2e025617744f184c02df51c356" +dependencies = [ + "thiserror 2.0.17", +] + [[package]] name = "retry-policies" version = "0.4.0" @@ -4035,16 +4418,25 @@ dependencies = [ ] [[package]] -name = "rustc-demangle" -version = "0.1.24" +name = "rust-multipart-rfc7578_2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "c839d037155ebc06a571e305af66ff9fd9063a6e662447051737e1ac75beea41" +dependencies = [ + "bytes", + "futures-core", + "futures-util", + "http", + "mime", + "rand 0.9.2", + "thiserror 2.0.17", +] [[package]] -name = "rustc-hash" -version = "1.1.0" +name = "rustc-demangle" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" @@ -4085,17 +4477,31 @@ dependencies = [ "bitflags 2.10.0", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.4.14", "windows-sys 0.52.0", ] +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys 0.11.0", + "windows-sys 0.61.2", +] + [[package]] name = "rustls" -version = "0.23.18" +version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9cc1d47e243d655ace55ed38201c19ae02c148ae56412ab8750e8f0166ab7f" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ "aws-lc-rs", + "log", "once_cell", "ring", "rustls-pki-types", @@ -4104,6 +4510,18 @@ 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" @@ -4115,18 +4533,19 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.10.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" +checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" dependencies = [ "web-time", + "zeroize", ] [[package]] name = "rustls-webpki" -version = "0.102.8" +version = "0.103.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ "aws-lc-rs", "ring", @@ -4167,6 +4586,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scc" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" +dependencies = [ + "sdd", +] + [[package]] name = "schannel" version = "0.1.27" @@ -4212,6 +4640,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sdd" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" + [[package]] name = "sec1" version = "0.7.3" @@ -4255,7 +4689,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags 2.10.0", - "core-foundation", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "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 2.10.0", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -4263,9 +4710,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.12.1" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", @@ -4327,14 +4774,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.133" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] @@ -4347,6 +4795,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -4365,7 +4824,7 @@ version = "3.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c522100790450cf78eeac1507263d0a350d4d5b30df0c8e1fe051a10c22b376e" dependencies = [ - "base64", + "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", @@ -4401,6 +4860,31 @@ dependencies = [ "serde", ] +[[package]] +name = "serial_test" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9" +dependencies = [ + "futures", + "log", + "once_cell", + "parking_lot 0.12.3", + "scc", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + [[package]] name = "sha1" version = "0.10.6" @@ -4494,9 +4978,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.2" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" dependencies = [ "serde", ] @@ -4511,6 +4995,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + [[package]] name = "spin" version = "0.9.8" @@ -4604,7 +5098,7 @@ dependencies = [ "tracing", "url", "uuid", - "webpki-roots", + "webpki-roots 0.26.7", ] [[package]] @@ -4653,7 +5147,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64bb4714269afa44aef2755150a0fc19d756fb580a67db8885608cf02f47d06a" dependencies = [ "atoi", - "base64", + "base64 0.22.1", "bigdecimal", "bitflags 2.10.0", "byteorder", @@ -4698,14 +5192,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fa91a732d854c5d7726349bb4bb879bb9478993ceb764247660aee25f67c2f8" dependencies = [ "atoi", - "base64", + "base64 0.22.1", "bigdecimal", "bitflags 2.10.0", "byteorder", "chrono", "crc", "dotenvy", - "etcetera", + "etcetera 0.8.0", "futures-channel", "futures-core", "futures-io", @@ -4787,6 +5281,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.110", +] + +[[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.110", +] + [[package]] name = "strum" version = "0.27.2" @@ -4875,7 +5392,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ "bitflags 2.10.0", - "core-foundation", + "core-foundation 0.9.4", "system-configuration-sys", ] @@ -4910,10 +5427,48 @@ dependencies = [ "cfg-if", "fastrand", "once_cell", - "rustix", + "rustix 0.38.41", "windows-sys 0.59.0", ] +[[package]] +name = "testcontainers" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3ac71069f20ecfa60c396316c283fbf35e6833a53dff551a31b5458da05edc" +dependencies = [ + "astral-tokio-tar", + "async-trait", + "bollard", + "bytes", + "docker_credential", + "either", + "etcetera 0.10.0", + "futures", + "log", + "memchr", + "parse-display", + "pin-project-lite", + "serde", + "serde_json", + "serde_with", + "thiserror 2.0.17", + "tokio", + "tokio-stream", + "tokio-util", + "ulid", + "url", +] + +[[package]] +name = "testcontainers-modules" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1966329d5bb3f89d33602d2db2da971fb839f9297dad16527abf4564e2ae0a6d" +dependencies = [ + "testcontainers", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -4925,11 +5480,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.3" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.3", + "thiserror-impl 2.0.17", ] [[package]] @@ -4945,9 +5500,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.3" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", @@ -5040,27 +5595,26 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.41.1" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "backtrace", "bytes", "libc", "mio", "parking_lot 0.12.3", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.6.1", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.4.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", @@ -5100,6 +5654,19 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "tokio-test" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7" +dependencies = [ + "async-stream", + "bytes", + "futures-core", + "tokio", + "tokio-stream", +] + [[package]] name = "tokio-util" version = "0.7.12" @@ -5143,6 +5710,46 @@ dependencies = [ "winnow", ] +[[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" @@ -5151,7 +5758,9 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", + "indexmap 2.12.0", "pin-project-lite", + "slab", "sync_wrapper", "tokio", "tokio-util", @@ -5188,16 +5797,17 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tower_governor" -version = "0.5.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe9ce4d13ec8b7228fa4003e31ea6cfe27b113c9ba51d323aeb4bbe394ef06fe" +checksum = "44de9b94d849d3c46e06a883d72d408c2de6403367b39df2b1c9d9e7b6736fe6" dependencies = [ "axum", "forwarded-header-value", "governor", "http", "pin-project", - "thiserror 2.0.3", + "thiserror 2.0.17", + "tonic", "tower", "tracing", ] @@ -5280,12 +5890,42 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "typetag" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be2212c8a9b9bcfca32024de14998494cf9a5dfa59ea1b829de98bac374b86bf" +dependencies = [ + "erased-serde", + "inventory", + "once_cell", + "serde", + "typetag-impl", +] + +[[package]] +name = "typetag-impl" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27a7a9b72ba121f6f1f6c3632b85604cac41aedb5ddc70accbebb6cac83de846" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + [[package]] name = "ucd-trie" version = "0.1.7" @@ -5304,6 +5944,16 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "ulid" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe" +dependencies = [ + "rand 0.9.2", + "web-time", +] + [[package]] name = "unarray" version = "0.1.4" @@ -5367,6 +6017,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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b4531c118335662134346048ddb0e54cc86bd7e81866757873055f0e38f5d2" +dependencies = [ + "base64 0.22.1", + "http", + "httparse", + "log", +] + [[package]] name = "url" version = "2.5.4" @@ -5376,6 +6054,7 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] @@ -5383,18 +6062,20 @@ name = "url_finder" version = "0.4.0" dependencies = [ "alloy", + "assert-json-diff", "async-trait", "axum", "axum-extra", + "axum-test", "chrono", "color-eyre", - "common", "dotenvy", "futures", "http", "moka", "multiaddr", "once_cell", + "pretty_assertions", "regex", "reqwest", "reqwest-middleware", @@ -5402,8 +6083,12 @@ dependencies = [ "retry-policies", "serde", "serde_json", + "serial_test", "sqlx", + "testcontainers", + "testcontainers-modules", "tokio", + "tokio-test", "tower", "tower-http", "tower_governor", @@ -5422,6 +6107,12 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf16_iter" version = "1.0.5" @@ -5436,9 +6127,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "utoipa" -version = "5.2.0" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514a48569e4e21c86d0b84b5612b5e73c0b2cf09db63260134ba426d4e8ea714" +checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993" dependencies = [ "indexmap 2.12.0", "serde", @@ -5448,9 +6139,9 @@ dependencies = [ [[package]] name = "utoipa-gen" -version = "5.2.0" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5629efe65599d0ccd5d493688cbf6e03aa7c1da07fe59ff97cf5977ed0637f66" +checksum = "6d79d08d92ab8af4c5e8a6da20c47ae3f61a0f1dabc1997cdf2d082b757ca08b" dependencies = [ "proc-macro2", "quote", @@ -5462,11 +6153,12 @@ dependencies = [ [[package]] name = "utoipa-swagger-ui" -version = "8.0.3" +version = "9.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5c80b4dd79ea382e8374d67dcce22b5c6663fa13a82ad3886441d1bbede5e35" +checksum = "d047458f1b5b65237c2f6dc6db136945667f40a7668627b3490b9513a3d43a55" dependencies = [ "axum", + "base64 0.22.1", "mime_guess", "regex", "reqwest", @@ -5480,12 +6172,14 @@ dependencies = [ [[package]] name = "uuid" -version = "1.11.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.3.4", + "js-sys", "serde", + "wasm-bindgen", ] [[package]] @@ -5557,27 +6251,14 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.95" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" dependencies = [ "cfg-if", "once_cell", + "rustversion", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn 2.0.110", "wasm-bindgen-shared", ] @@ -5595,9 +6276,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.95" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5605,22 +6286,25 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.95" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn 2.0.110", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.95" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +dependencies = [ + "unicode-ident", +] [[package]] name = "wasm-timer" @@ -5681,15 +6365,12 @@ dependencies = [ ] [[package]] -name = "which" -version = "4.4.2" +name = "webpki-roots" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" dependencies = [ - "either", - "home", - "once_cell", - "rustix", + "rustls-pki-types", ] [[package]] @@ -5742,7 +6423,7 @@ dependencies = [ "windows-collections", "windows-core 0.61.1", "windows-future", - "windows-link", + "windows-link 0.1.1", "windows-numerics", ] @@ -5772,7 +6453,7 @@ checksum = "46ec44dc15085cea82cf9c78f85a9114c463a369786585ad2882d1ff0b0acf40" dependencies = [ "windows-implement", "windows-interface", - "windows-link", + "windows-link 0.1.1", "windows-result 0.3.3", "windows-strings 0.4.1", ] @@ -5784,7 +6465,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core 0.61.1", - "windows-link", + "windows-link 0.1.1", "windows-threading", ] @@ -5816,6 +6497,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-numerics" version = "0.2.0" @@ -5823,7 +6510,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ "windows-core 0.61.1", - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -5852,7 +6539,7 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b895b5356fc36103d0f64dd1e94dfa7ac5633f1c9dd6e80fe9ec4adef69e09d" dependencies = [ - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -5871,7 +6558,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a7ab927b2637c19b3dbe0965e75d8f2d30bdd697a1516191cad2ec4df8fb28a" dependencies = [ - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -5901,6 +6588,24 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -5925,20 +6630,37 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + [[package]] name = "windows-threading" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" dependencies = [ - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -5953,6 +6675,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -5965,6 +6693,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -5977,12 +6711,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -5995,6 +6741,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -6007,6 +6759,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -6019,6 +6777,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -6031,6 +6795,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" version = "0.7.13" @@ -6042,13 +6812,12 @@ dependencies = [ [[package]] name = "wiremock" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b8b99d4cdbf36b239a9532e31fe4fb8acc38d1897c1761e161550a7dc78e6a" +checksum = "08db1edfb05d9b3c1542e521aea074442088292f00b5f28e435c714a98f85031" dependencies = [ "assert-json-diff", - "async-trait", - "base64", + "base64 0.22.1", "deadpool", "futures", "http", @@ -6091,6 +6860,22 @@ dependencies = [ "tap", ] +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix 1.1.2", +] + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "yoke" version = "0.7.5" @@ -6201,21 +6986,24 @@ dependencies = [ [[package]] name = "zip" -version = "2.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d52293fc86ea7cf13971b3bb81eb21683636e7ae24c729cdaf1b7c4157a352" +checksum = "12598812502ed0105f607f941c386f43d441e00148fce9dec3ca5ffb0bde9308" dependencies = [ "arbitrary", "crc32fast", - "crossbeam-utils", - "displaydoc", "flate2", "indexmap 2.12.0", "memchr", - "thiserror 2.0.3", "zopfli", ] +[[package]] +name = "zlib-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2" + [[package]] name = "zopfli" version = "0.8.1" diff --git a/Makefile b/Makefile index 5aed0c9..a1f12e8 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ include .env export -.PHONY: check format lint build run stop logs prepare +.PHONY: check format lint build run stop logs prepare test .PHONY: run-db stop-db clean-db clear-db logs-db exec-db .PHONY: migration migrate-up migrate-down migrate-status init-dev init-dev-db @@ -16,6 +16,9 @@ format: lint: @cargo clippy +test: + @SQLX_OFFLINE=true cargo test --workspace + # Docker app commands build: @docker compose build diff --git a/url_finder/Cargo.toml b/url_finder/Cargo.toml index 5a0900c..520dedd 100644 --- a/url_finder/Cargo.toml +++ b/url_finder/Cargo.toml @@ -3,11 +3,19 @@ name = "url_finder" version = "0.4.0" edition = "2024" +[lib] +name = "url_finder" +path = "src/lib.rs" + +[[bin]] +name = "url_finder" +path = "src/main.rs" + [dependencies] tokio = { version = "1.40.0", features = ["full"] } color-eyre = "0.6.3" -axum = { version = "0.7.9", features = ["macros", "tokio"] } -axum-extra = { version = "0.9.3" } +axum = { version = "0.8", features = ["macros", "tokio"] } +axum-extra = { version = "0.10" } reqwest = { version = "0.12.7", features = ["json"] } reqwest-middleware = { version = "0.3.3", features = ["json"] } reqwest-retry = "0.7.0" @@ -17,7 +25,7 @@ http = "1.1" serde = { version = "1.0.209" } serde_json = "1.0.128" tracing = "0.1.40" -tracing-subscriber = { version = "0.3.18", features = ["env-filter", 'fmt'] } +tracing-subscriber = { version = "0.3.18", features = ["env-filter", "fmt"] } sqlx = { version = "0.8.2", features = [ "runtime-tokio", "postgres", @@ -29,18 +37,17 @@ sqlx = { version = "0.8.2", features = [ ] } futures = "0.3.31" once_cell = "1.20.2" -common = { git = "https://github.com/fidlabs/bandwidth-measurement-system.git", rev = "0ca8779" } tower = { version = "0.5.2", features = ["timeout", "limit"] } tower-http = { version = "0.6.2", features = ["cors"]} -utoipa = { version = "5.2.0", features = [ +utoipa = { version = "5.4.0", features = [ "axum_extras", "uuid", "url", "chrono", "debug", ] } -utoipa-swagger-ui = { version = "8.0.3", features = ["axum", "reqwest", "url"] } -tower_governor = { version = "0.5.0", features = ["axum"] } +utoipa-swagger-ui = { version = "9.0.2", features = ["axum", "reqwest", "url"] } +tower_governor = { version = "0.8.0", features = ["axum"] } uuid = { version = "1.10.0", features = ["v4", "serde"] } chrono = { version = "0.4.38", features = ["serde"] } regex = "1.11.1" @@ -51,4 +58,11 @@ multiaddr = "0.18.2" dotenvy = "0.15.7" [dev-dependencies] -wiremock = "0.6" +wiremock = "0.6.5" +assert-json-diff = "2.0" +pretty_assertions = "1.4.1" +tokio-test = "0.4" +axum-test = "18.2" +serial_test = "3.2" +testcontainers = "0.25.2" +testcontainers-modules = { version = "0.13.0", features = ["postgres"] } diff --git a/url_finder/src/api/api_doc.rs b/url_finder/src/api/api_doc.rs index a6ee305..de1d1ef 100644 --- a/url_finder/src/api/api_doc.rs +++ b/url_finder/src/api/api_doc.rs @@ -1,4 +1,4 @@ -use common::api_response::ErrorResponse; +use crate::api_response::ErrorResponse; use utoipa::OpenApi; use crate::api::*; diff --git a/url_finder/src/api/cache_middleware.rs b/url_finder/src/api/cache_middleware.rs index 85dec5f..6b0fc0e 100644 --- a/url_finder/src/api/cache_middleware.rs +++ b/url_finder/src/api/cache_middleware.rs @@ -1,3 +1,4 @@ +use crate::api_response::*; use axum::{ body::{self, Body}, extract::{Request, State}, @@ -5,7 +6,6 @@ use axum::{ middleware::Next, response::{IntoResponse, Response}, }; -use common::api_response::*; use serde_json::Value; use std::sync::Arc; use tokio::time::Instant; diff --git a/url_finder/src/api/create_job.rs b/url_finder/src/api/create_job.rs index 259016d..182d453 100644 --- a/url_finder/src/api/create_job.rs +++ b/url_finder/src/api/create_job.rs @@ -1,9 +1,9 @@ use std::sync::Arc; +use crate::api_response::*; use axum::{Json, extract::State}; use axum_extra::extract::WithRejection; use color_eyre::Result; -use common::api_response::*; use serde::{Deserialize, Serialize}; use tracing::{debug, error}; use utoipa::ToSchema; @@ -66,8 +66,8 @@ pub async fn handle_create_job( // Parse and validate client address if provided let client_address = if let Some(client) = &payload.client { Some(ClientAddress::new(client.clone()).map_err(|e| { - error!("Invalid client address: {}", e); - bad_request(format!("Invalid client address: {}", e)) + error!("Invalid client address: {e}"); + bad_request(format!("Invalid client address: {e}")) })?) } else { None @@ -76,22 +76,23 @@ pub async fn handle_create_job( // Parse and validate provider address if provided let provider_address = if let Some(provider) = &payload.provider { let provider_addr = ProviderAddress::new(provider.clone()).map_err(|e| { - error!("Invalid provider address: {}", e); - bad_request(format!("Invalid provider address: {}", e)) + error!("Invalid provider address: {e}"); + bad_request(format!("Invalid provider address: {e}")) })?; // Verify that we have http endpoint for the provider - let _ = match provider_endpoints::get_provider_endpoints(&provider_addr).await { - Ok((result_code, endpoints)) if endpoints.is_none() => { - debug!("No endpoints found"); - return Ok(ok_response(CreateJobResponse { - result: result_code, - id: None, - })); - } - Err(e) => return Err(internal_server_error(e.to_string())), - Ok(result) => result, - }; + let _ = + match provider_endpoints::get_provider_endpoints(&state.config, &provider_addr).await { + Ok((result_code, endpoints)) if endpoints.is_none() => { + debug!("No endpoints found"); + return Ok(ok_response(CreateJobResponse { + result: result_code, + id: None, + })); + } + Err(e) => return Err(internal_server_error(e.to_string())), + Ok(result) => result, + }; Some(provider_addr) } else { diff --git a/url_finder/src/api/find_client.rs b/url_finder/src/api/find_client.rs index be070d8..f6ea04a 100644 --- a/url_finder/src/api/find_client.rs +++ b/url_finder/src/api/find_client.rs @@ -1,12 +1,12 @@ use std::{sync::Arc, time::Duration}; +use crate::api_response::*; use axum::{ debug_handler, extract::{Path, State}, }; use axum_extra::extract::WithRejection; use color_eyre::Result; -use common::api_response::*; use serde::{Deserialize, Serialize}; use tokio::time::timeout; use tracing::debug; @@ -70,7 +70,7 @@ pub async fn handle_find_client( // Parse and validate client address let client_address = ClientAddress::new(path.client.clone()) - .map_err(|e| bad_request(format!("Invalid client address: {}", e)))?; + .map_err(|e| bad_request(format!("Invalid client address: {e}")))?; let providers = match deal_service::get_distinct_providers_by_client(&state.deal_repo, &client_address) @@ -83,9 +83,9 @@ pub async fn handle_find_client( &path.client, e ); + let client = &path.client; return Err(internal_server_error(format!( - "Failed to get providers for client {0}", - path.client + "Failed to get providers for client {client}" ))); } }; @@ -104,7 +104,7 @@ pub async fn handle_find_client( for provider in providers { let (result_code, endpoints) = - match provider_endpoints::get_provider_endpoints(&provider).await { + match provider_endpoints::get_provider_endpoints(&state.config, &provider).await { Ok(endpoints) => endpoints, Err(e) => return Err(internal_server_error(e.to_string())), }; diff --git a/url_finder/src/api/find_retri_sp.rs b/url_finder/src/api/find_retri_sp.rs index 9da9269..c66b2a6 100644 --- a/url_finder/src/api/find_retri_sp.rs +++ b/url_finder/src/api/find_retri_sp.rs @@ -1,12 +1,12 @@ use std::{sync::Arc, time::Duration}; +use crate::api_response::*; use axum::{ debug_handler, extract::{Path, State}, }; use axum_extra::extract::WithRejection; use color_eyre::Result; -use common::api_response::*; use serde::{Deserialize, Serialize}; use tokio::time::timeout; use tracing::debug; @@ -61,10 +61,10 @@ pub async fn handle_find_retri_by_sp( // Parse and validate provider address let provider_address = ProviderAddress::new(path.provider) - .map_err(|e| bad_request(format!("Invalid provider address: {}", e)))?; + .map_err(|e| bad_request(format!("Invalid provider address: {e}")))?; let (result_code, endpoints) = - match provider_endpoints::get_provider_endpoints(&provider_address).await { + match provider_endpoints::get_provider_endpoints(&state.config, &provider_address).await { Ok(endpoints) => endpoints, Err(e) => return Err(internal_server_error(e.to_string())), }; diff --git a/url_finder/src/api/find_retri_sp_client.rs b/url_finder/src/api/find_retri_sp_client.rs index 81ea843..b60761c 100644 --- a/url_finder/src/api/find_retri_sp_client.rs +++ b/url_finder/src/api/find_retri_sp_client.rs @@ -1,12 +1,12 @@ use std::{sync::Arc, time::Duration}; +use crate::api_response::*; use axum::{ debug_handler, extract::{Path, State}, }; use axum_extra::extract::WithRejection; use color_eyre::Result; -use common::api_response::*; use serde::{Deserialize, Serialize}; use tokio::time::timeout; use tracing::debug; @@ -65,12 +65,12 @@ pub async fn handle_find_retri_by_client_and_sp( // Parse and validate provider and client addresses let provider_address = ProviderAddress::new(path.provider) - .map_err(|e| bad_request(format!("Invalid provider address: {}", e)))?; + .map_err(|e| bad_request(format!("Invalid provider address: {e}")))?; let client_address = ClientAddress::new(path.client) - .map_err(|e| bad_request(format!("Invalid client address: {}", e)))?; + .map_err(|e| bad_request(format!("Invalid client address: {e}")))?; let (result_code, endpoints) = - match provider_endpoints::get_provider_endpoints(&provider_address).await { + match provider_endpoints::get_provider_endpoints(&state.config, &provider_address).await { Ok(endpoints) => endpoints, Err(e) => return Err(internal_server_error(e.to_string())), }; diff --git a/url_finder/src/api/find_url_sp.rs b/url_finder/src/api/find_url_sp.rs index fe9c427..711a0f1 100644 --- a/url_finder/src/api/find_url_sp.rs +++ b/url_finder/src/api/find_url_sp.rs @@ -1,12 +1,12 @@ use std::sync::Arc; +use crate::api_response::*; use axum::{ debug_handler, extract::{Path, State}, }; use axum_extra::extract::WithRejection; use color_eyre::Result; -use common::api_response::*; use serde::{Deserialize, Serialize}; use tracing::debug; use utoipa::{IntoParams, ToSchema}; @@ -54,10 +54,10 @@ pub async fn handle_find_url_sp( // Parse and validate provider address let provider_address = ProviderAddress::new(path.provider) - .map_err(|e| bad_request(format!("Invalid provider address: {}", e)))?; + .map_err(|e| bad_request(format!("Invalid provider address: {e}")))?; let (result_code, endpoints) = - match provider_endpoints::get_provider_endpoints(&provider_address).await { + match provider_endpoints::get_provider_endpoints(&state.config, &provider_address).await { Ok(endpoints) => endpoints, Err(e) => return Err(internal_server_error(e.to_string())), }; diff --git a/url_finder/src/api/find_url_sp_client.rs b/url_finder/src/api/find_url_sp_client.rs index 6736cd2..33fa032 100644 --- a/url_finder/src/api/find_url_sp_client.rs +++ b/url_finder/src/api/find_url_sp_client.rs @@ -1,12 +1,12 @@ use std::sync::Arc; +use crate::api_response::*; use axum::{ debug_handler, extract::{Path, State}, }; use axum_extra::extract::WithRejection; use color_eyre::Result; -use common::api_response::*; use serde::{Deserialize, Serialize}; use tracing::debug; use utoipa::{IntoParams, ToSchema}; @@ -58,12 +58,12 @@ pub async fn handle_find_url_sp_client( // Parse and validate provider and client addresses let provider_address = ProviderAddress::new(path.provider) - .map_err(|e| bad_request(format!("Invalid provider address: {}", e)))?; + .map_err(|e| bad_request(format!("Invalid provider address: {e}")))?; let client_address = ClientAddress::new(path.client) - .map_err(|e| bad_request(format!("Invalid client address: {}", e)))?; + .map_err(|e| bad_request(format!("Invalid client address: {e}")))?; let (result_code, endpoints) = - match provider_endpoints::get_provider_endpoints(&provider_address).await { + match provider_endpoints::get_provider_endpoints(&state.config, &provider_address).await { Ok(endpoints) => endpoints, Err(e) => return Err(internal_server_error(e.to_string())), }; diff --git a/url_finder/src/api/get_job.rs b/url_finder/src/api/get_job.rs index 72876e5..7ed5379 100644 --- a/url_finder/src/api/get_job.rs +++ b/url_finder/src/api/get_job.rs @@ -1,9 +1,9 @@ use std::sync::Arc; +use crate::api_response::*; use axum::extract::{Path, State}; use axum_extra::extract::WithRejection; use color_eyre::Result; -use common::api_response::*; use serde::{Deserialize, Serialize}; use tracing::error; diff --git a/url_finder/src/api/healthcheck.rs b/url_finder/src/api/healthcheck.rs index 76b102d..478397b 100644 --- a/url_finder/src/api/healthcheck.rs +++ b/url_finder/src/api/healthcheck.rs @@ -2,7 +2,7 @@ use axum::debug_handler; use serde::Serialize; use utoipa::ToSchema; -use common::api_response::*; +use crate::api_response::*; #[derive(Serialize, ToSchema)] pub struct HealthcheckResponse { diff --git a/url_finder/src/api_response.rs b/url_finder/src/api_response.rs new file mode 100644 index 0000000..6ae1019 --- /dev/null +++ b/url_finder/src/api_response.rs @@ -0,0 +1,92 @@ +// This is a temporary vendored copy from BMS to allow upgrading to axum 0.8 independently from BMS. + +use axum::{ + extract::rejection::{JsonRejection, PathRejection, QueryRejection}, + http::StatusCode, + response::{IntoResponse, Json, Response}, +}; +use serde::Serialize; +use utoipa::ToSchema; + +#[derive(Serialize, ToSchema, Clone, Debug)] +pub struct ErrorResponse { + error: String, +} + +#[derive(Clone, Debug)] +pub enum ApiResponse { + BadRequest(Json), + InternalServerError(Json), + NotFound(Json), + Unauthorized(Json), + TooManyRequests(Json), + OkResponse(Json), +} + +impl From for ApiResponse { + fn from(rejection: JsonRejection) -> ApiResponse { + ApiResponse::BadRequest(Json(ErrorResponse { + error: rejection.body_text(), + })) + } +} + +impl From for ApiResponse { + fn from(rejection: QueryRejection) -> ApiResponse { + ApiResponse::BadRequest(Json(ErrorResponse { + error: rejection.body_text(), + })) + } +} + +impl From for ApiResponse { + fn from(rejection: PathRejection) -> ApiResponse { + ApiResponse::BadRequest(Json(ErrorResponse { + error: rejection.body_text(), + })) + } +} + +impl IntoResponse for ApiResponse +where + T: Serialize, +{ + fn into_response(self) -> Response { + match self { + ApiResponse::BadRequest(json) => (StatusCode::BAD_REQUEST, json).into_response(), + ApiResponse::InternalServerError(json) => { + (StatusCode::INTERNAL_SERVER_ERROR, json).into_response() + } + ApiResponse::NotFound(json) => (StatusCode::NOT_FOUND, json).into_response(), + ApiResponse::OkResponse(json) => (StatusCode::OK, json).into_response(), + ApiResponse::Unauthorized(json) => (StatusCode::UNAUTHORIZED, json).into_response(), + ApiResponse::TooManyRequests(json) => { + (StatusCode::TOO_MANY_REQUESTS, json).into_response() + } + } + } +} + +pub fn bad_request>(msg: T) -> ApiResponse<()> { + ApiResponse::BadRequest(Json(ErrorResponse { error: msg.into() })) +} + +pub fn internal_server_error>(msg: T) -> ApiResponse<()> { + ApiResponse::InternalServerError(Json(ErrorResponse { error: msg.into() })) +} + +pub fn not_found>(msg: T) -> ApiResponse<()> { + ApiResponse::NotFound(Json(ErrorResponse { error: msg.into() })) +} + +pub fn ok_response(data: T) -> ApiResponse { + ApiResponse::OkResponse(Json(data)) +} + +pub fn unauthorized>(msg: T) -> ApiResponse<()> { + ApiResponse::Unauthorized(Json(ErrorResponse { error: msg.into() })) +} + +pub fn too_many_requests>(msg: T) -> ApiResponse<()> { + ApiResponse::TooManyRequests(Json(ErrorResponse { error: msg.into() })) +} diff --git a/url_finder/src/background/job_handler.rs b/url_finder/src/background/job_handler.rs index 5dd5bd1..4b39746 100644 --- a/url_finder/src/background/job_handler.rs +++ b/url_finder/src/background/job_handler.rs @@ -4,7 +4,9 @@ use tokio::time::sleep; use tracing::{debug, info}; use crate::{ - ErrorCode, Job, JobRepository, JobStatus, ResultCode, provider_endpoints, + ErrorCode, Job, JobRepository, JobStatus, ResultCode, + config::Config, + provider_endpoints, repository::DealRepository, services::deal_service, types::{ClientAddress, ClientId, ProviderAddress, ProviderId}, @@ -42,7 +44,11 @@ pub(super) enum JobHandlerResult { MultipleResults(Vec, Vec), } -pub async fn job_handler(job_repo: Arc, deal_repo: Arc) { +pub async fn job_handler( + config: Arc, + job_repo: Arc, + deal_repo: Arc, +) { info!("Starting job handler loop"); loop { @@ -57,7 +63,7 @@ pub async fn job_handler(job_repo: Arc, deal_repo: Arc { debug!("Skipping job: {}", reason); continue; @@ -140,14 +146,26 @@ pub async fn job_handler(job_repo: Arc, deal_repo: Arc JobHandlerResult { +async fn process_pending_job( + config: &Config, + deal_repo: &DealRepository, + job: &Job, +) -> JobHandlerResult { match (&job.provider, &job.client) { (Some(provider_address), None) => { - process_job_with_provider(deal_repo, provider_address).await + process_job_with_provider(config, deal_repo, provider_address).await + } + (None, Some(client_address)) => { + process_job_with_client(config, deal_repo, client_address).await } - (None, Some(client_address)) => process_job_with_client(deal_repo, client_address).await, (Some(provider_address), Some(client_address)) => { - process_job_with_provider_and_client(deal_repo, provider_address, client_address).await + process_job_with_provider_and_client( + config, + deal_repo, + provider_address, + client_address, + ) + .await } (None, None) => { // should not happen @@ -161,6 +179,7 @@ async fn process_pending_job(deal_repo: &DealRepository, job: &Job) -> JobHandle } async fn process_job_with_client( + config: &Config, deal_repo: &DealRepository, client_address: &ClientAddress, ) -> JobHandlerResult { @@ -192,7 +211,7 @@ async fn process_job_with_client( for provider_address in providers { debug!("Processing job with provider: {}", &provider_address); - match process_job(deal_repo, &provider_address, Some(client_address)).await { + match process_job(config, deal_repo, &provider_address, Some(client_address)).await { JobHandlerResult::SuccessResult(result) => success_results.push(result), JobHandlerResult::ErrorResult(result) => error_results.push(result), JobHandlerResult::Skip(reason) => return JobHandlerResult::Skip(reason), @@ -208,6 +227,7 @@ async fn process_job_with_client( } async fn process_job_with_provider_and_client( + config: &Config, deal_repo: &DealRepository, provider_address: &ProviderAddress, client_address: &ClientAddress, @@ -217,19 +237,21 @@ async fn process_job_with_provider_and_client( provider_address, client_address ); - process_job(deal_repo, provider_address, Some(client_address)).await + process_job(config, deal_repo, provider_address, Some(client_address)).await } async fn process_job_with_provider( + config: &Config, deal_repo: &DealRepository, provider_address: &ProviderAddress, ) -> JobHandlerResult { debug!("Processing job with provider: {}", provider_address); - process_job(deal_repo, provider_address, None).await + process_job(config, deal_repo, provider_address, None).await } async fn process_job( + config: &Config, deal_repo: &DealRepository, provider_address: &ProviderAddress, client_address: Option<&ClientAddress>, @@ -237,25 +259,26 @@ async fn process_job( let provider_id: ProviderId = provider_address.clone().into(); let client_id: Option = client_address.map(|c| c.clone().into()); - let (_, endpoints) = match provider_endpoints::get_provider_endpoints(provider_address).await { - Ok((result_code, _)) if result_code != ResultCode::Success => { - return JobHandlerResult::ErrorResult(JobErrorResult { - provider: provider_address.clone(), - client: client_address.cloned(), - result: Some(result_code), - error: None, - }); - } - Ok(result) => result, - Err(error_code) => { - return JobHandlerResult::ErrorResult(JobErrorResult { - provider: provider_address.clone(), - client: client_address.cloned(), - result: Some(ResultCode::Error), - error: Some(error_code), - }); - } - }; + let (_, endpoints) = + match provider_endpoints::get_provider_endpoints(config, provider_address).await { + Ok((result_code, _)) if result_code != ResultCode::Success => { + return JobHandlerResult::ErrorResult(JobErrorResult { + provider: provider_address.clone(), + client: client_address.cloned(), + result: Some(result_code), + error: None, + }); + } + Ok(result) => result, + Err(error_code) => { + return JobHandlerResult::ErrorResult(JobErrorResult { + provider: provider_address.clone(), + client: client_address.cloned(), + result: Some(ResultCode::Error), + error: Some(error_code), + }); + } + }; if endpoints.is_none() || endpoints.as_ref().unwrap().is_empty() { debug!("No endpoints found"); diff --git a/url_finder/src/background/url_discovery_scheduler.rs b/url_finder/src/background/url_discovery_scheduler.rs index 577042d..06bcbc8 100644 --- a/url_finder/src/background/url_discovery_scheduler.rs +++ b/url_finder/src/background/url_discovery_scheduler.rs @@ -1,4 +1,5 @@ use crate::{ + config::Config, repository::{DealRepository, StorageProviderRepository, UrlResult, UrlResultRepository}, services::url_discovery_service, types::{ClientAddress, ClientId, ProviderAddress, ProviderId}, @@ -17,6 +18,7 @@ const BATCH_SIZE: i64 = 100; const MAX_CONCURRENT_CLIENT_TESTS: usize = 5; pub async fn run_url_discovery_scheduler( + config: Arc, sp_repo: Arc, url_repo: Arc, deal_repo: Arc, @@ -24,26 +26,28 @@ pub async fn run_url_discovery_scheduler( info!("Starting URL discovery scheduler loop"); loop { - let interval = match schedule_url_discoveries(&sp_repo, &url_repo, &deal_repo).await { - Ok(0) => { - info!("No providers due for URL discovery, sleeping..."); - SCHEDULER_SLEEP_INTERVAL - } - Ok(count) => { - info!("URL discovery cycle completed: {} providers tested", count); - SCHEDULER_NEXT_INTERVAL - } - Err(e) => { - error!("URL discovery scheduler failed: {:?}", e); - SCHEDULER_SLEEP_INTERVAL - } - }; + let interval = + match schedule_url_discoveries(&config, &sp_repo, &url_repo, &deal_repo).await { + Ok(0) => { + info!("No providers due for URL discovery, sleeping..."); + SCHEDULER_SLEEP_INTERVAL + } + Ok(count) => { + info!("URL discovery cycle completed: {} providers tested", count); + SCHEDULER_NEXT_INTERVAL + } + Err(e) => { + error!("URL discovery scheduler failed: {:?}", e); + SCHEDULER_SLEEP_INTERVAL + } + }; sleep(interval).await; } } async fn schedule_url_discoveries( + config: &Config, sp_repo: &StorageProviderRepository, url_repo: &UrlResultRepository, deal_repo: &DealRepository, @@ -76,7 +80,8 @@ async fn schedule_url_discoveries( clients.len() ); - let results = test_provider_with_clients(&provider.provider_id, clients, deal_repo).await; + let results = + test_provider_with_clients(config, &provider.provider_id, clients, deal_repo).await; let url_results: Vec = results .iter() @@ -115,6 +120,7 @@ async fn schedule_url_discoveries( } async fn test_provider_with_clients( + config: &Config, provider_id: &ProviderId, client_ids: Vec, deal_repo: &DealRepository, @@ -124,9 +130,12 @@ async fn test_provider_with_clients( let provider_address: ProviderAddress = provider_id.clone().into(); let provider_task = { + let cfg = config.clone(); let addr = provider_address.clone(); let repo = deal_repo.clone(); - tokio::spawn(async move { url_discovery_service::discover_url(&addr, None, &repo).await }) + tokio::spawn( + async move { url_discovery_service::discover_url(&cfg, &addr, None, &repo).await }, + ) }; tasks.push(provider_task); @@ -136,13 +145,18 @@ async fn test_provider_with_clients( .acquire_owned() .await .expect("Semaphore should never be closed"); + let cfg = config.clone(); let provider_addr = provider_address.clone(); let client_address: ClientAddress = client_id.into(); let repo = deal_repo.clone(); tasks.push(tokio::spawn(async move { - let result = - url_discovery_service::discover_url(&provider_addr, Some(client_address), &repo) - .await; + let result = url_discovery_service::discover_url( + &cfg, + &provider_addr, + Some(client_address), + &repo, + ) + .await; drop(permit); // release semaphore result })); diff --git a/url_finder/src/cid_contact.rs b/url_finder/src/cid_contact.rs index 155fa60..3afc8ed 100644 --- a/url_finder/src/cid_contact.rs +++ b/url_finder/src/cid_contact.rs @@ -4,7 +4,7 @@ use color_eyre::Result; use tracing::debug; use urlencoding::decode; -use crate::utils::build_reqwest_retry_client; +use crate::{config::Config, utils::build_reqwest_retry_client}; const CID_CONTACT_MIN_RETRY_INTERVAL_MS: u64 = 2_000; const CID_CONTACT_MAX_RETRY_INTERVAL_MS: u64 = 30_000; @@ -24,12 +24,16 @@ impl fmt::Display for CidContactError { } } -pub async fn get_contact(peer_id: &str) -> Result { +pub async fn get_contact( + config: &Config, + peer_id: &str, +) -> Result { let client = build_reqwest_retry_client( CID_CONTACT_MIN_RETRY_INTERVAL_MS, CID_CONTACT_MAX_RETRY_INTERVAL_MS, ); - let url = format!("https://cid.contact/providers/{peer_id}"); + let base_url = config.cid_contact_url.trim_end_matches('/'); + let url = format!("{base_url}/providers/{peer_id}"); debug!("cid contact url: {:?}", url); @@ -99,9 +103,10 @@ pub fn get_all_addresses_from_response(json: serde_json::Value) -> Vec { None => &cleaned, }; - let final_addr = if trimmed.ends_with("/https") { + let has_tcp = trimmed.contains("/tcp/"); + let final_addr = if !has_tcp && trimmed.ends_with("/https") { trimmed.replace("/https", "/tcp/443/https") - } else if trimmed.ends_with("/http") { + } else if !has_tcp && trimmed.ends_with("/http") { trimmed.replace("/http", "/tcp/80/http") } else { trimmed.to_string() @@ -113,3 +118,60 @@ pub fn get_all_addresses_from_response(json: serde_json::Value) -> Vec { addresses } + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + // Real-world example: https://cid.contact/providers/12D3KooWRf7tJR2NfJYE3PQJKXGt1EFqmFBBfQCgPRBLwwR9XL15 + #[test] + fn transforms_publisher_https_with_http_path() { + let response = json!({ + "Publisher": { + "ID": "12D3KooWRf7tJR2NfJYE3PQJKXGt1EFqmFBBfQCgPRBLwwR9XL15", + "Addrs": [ + "/dns/adela.myfil.net/https/http-path/%2Fipni-provider%2F12D3KooWRf7tJR2NfJYE3PQJKXGt1EFqmFBBfQCgPRBLwwR9XL15" + ] + } + }); + + let addrs = get_all_addresses_from_response(response); + + assert_eq!(addrs.len(), 1); + assert_eq!(addrs[0], "/dns/adela.myfil.net/tcp/443/https"); + } + + #[test] + fn preserves_publisher_addr_with_explicit_tcp() { + let response = json!({ + "Publisher": { + "ID": "test-peer-id", + "Addrs": ["/ip4/1.2.3.4/tcp/8080/http"] + } + }); + + let addrs = get_all_addresses_from_response(response); + + assert_eq!(addrs.len(), 1); + assert_eq!(addrs[0], "/ip4/1.2.3.4/tcp/8080/http"); + } + + // ExtendedProviders path does NOT transform addresses (unlike Publisher path) + #[test] + fn extended_providers_not_transformed() { + let response = json!({ + "ExtendedProviders": { + "Providers": [{ + "ID": "test-peer-id", + "Addrs": ["/dns/example.com/https"] + }] + } + }); + + let addrs = get_all_addresses_from_response(response); + + assert_eq!(addrs.len(), 1); + assert_eq!(addrs[0], "/dns/example.com/https"); + } +} diff --git a/url_finder/src/config.rs b/url_finder/src/config.rs index b3f4ce7..90ea8de 100644 --- a/url_finder/src/config.rs +++ b/url_finder/src/config.rs @@ -1,19 +1,18 @@ use std::env; use color_eyre::Result; -use once_cell::sync::Lazy; use crate::types::DbConnectParams; -pub static CONFIG: Lazy = Lazy::new(|| Config::new_from_env().unwrap()); - -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Config { pub db_url: String, pub dmob_db_url: String, pub log_level: String, pub glif_url: String, + pub cid_contact_url: String, } + impl Config { pub fn new_from_env() -> Result { let db_url = env::var("DATABASE_URL").unwrap_or_else(|_| { @@ -31,6 +30,19 @@ impl Config { dmob_db_url: env::var("DMOB_DATABASE_URL").expect("DMOB_DATABASE_URL must be set"), log_level: env::var("LOG_LEVEL").unwrap_or("info".to_string()), glif_url: env::var("GLIF_URL").unwrap_or("https://api.node.glif.io/rpc/v1".to_string()), + cid_contact_url: env::var("CID_CONTACT_URL") + .unwrap_or("https://cid.contact".to_string()), }) } + + // Test helper + pub fn new_for_test(glif_url: String, cid_contact_url: String) -> Self { + Self { + db_url: "dummy".to_string(), + dmob_db_url: "dummy".to_string(), + log_level: "info".to_string(), + glif_url, + cid_contact_url, + } + } } diff --git a/url_finder/src/lib.rs b/url_finder/src/lib.rs new file mode 100644 index 0000000..8f4dac0 --- /dev/null +++ b/url_finder/src/lib.rs @@ -0,0 +1,32 @@ +/// lib exports for integration testing +/// separated to simulate real api call: http request -> api handler -> service -> repo -> db +pub use moka::future::Cache; +pub use std::sync::{Arc, atomic::AtomicUsize}; + +pub mod api; +pub mod api_response; +pub mod background; +mod cid_contact; +pub mod config; +mod lotus_rpc; +mod multiaddr_parser; +mod pix_filspark; +pub mod provider_endpoints; +pub mod repository; +pub mod routes; +pub mod services; +pub mod types; +mod url_tester; +mod utils; + +pub use repository::{Job, JobRepository, JobStatus}; +pub use types::{ErrorCode, ResultCode}; + +pub struct AppState { + pub deal_repo: Arc, + pub active_requests: Arc, + pub job_repo: Arc, + pub storage_provider_repo: Arc, + pub cache: Cache, + pub config: Arc, +} diff --git a/url_finder/src/lotus_rpc.rs b/url_finder/src/lotus_rpc.rs index 5c2d13e..c9d22b3 100644 --- a/url_finder/src/lotus_rpc.rs +++ b/url_finder/src/lotus_rpc.rs @@ -4,13 +4,13 @@ use color_eyre::{Result, eyre::eyre}; use serde_json::json; use tracing::debug; -use crate::{config::CONFIG, types::ProviderAddress, utils::build_reqwest_retry_client}; +use crate::{config::Config, types::ProviderAddress, utils::build_reqwest_retry_client}; const LOTUS_RPC_MIN_RETRY_INTERVAL_MS: u64 = 10_000; const LOTUS_RPC_MAX_RETRY_INTERVAL_MS: u64 = 180_000; const LOTUS_RPC_TOTAL_TIMEOUT_MS: u64 = 250_000; -pub async fn get_peer_id(address: &ProviderAddress) -> Result { +pub async fn get_peer_id(config: &Config, address: &ProviderAddress) -> Result { debug!("get_peer_id address: {}", address); let client = build_reqwest_retry_client( @@ -18,7 +18,7 @@ pub async fn get_peer_id(address: &ProviderAddress) -> Result { LOTUS_RPC_MAX_RETRY_INTERVAL_MS, ); let res = client - .post(&CONFIG.glif_url) + .post(&config.glif_url) .json(&json!({ "jsonrpc": "2.0", "id": 1, diff --git a/url_finder/src/main.rs b/url_finder/src/main.rs index fba4af0..0ff9f04 100644 --- a/url_finder/src/main.rs +++ b/url_finder/src/main.rs @@ -12,10 +12,7 @@ use axum::{ response::Response, }; use color_eyre::Result; -use config::CONFIG; use moka::future::Cache; -use repository::DealRepository; -use routes::create_routes; use tokio::{ net::TcpListener, signal::unix::{SignalKind, signal}, @@ -24,31 +21,10 @@ use tower_http::cors::{Any, CorsLayer}; use tracing::{debug, info}; use tracing_subscriber::EnvFilter; -use crate::api::*; -use crate::repository::*; - -mod api; -mod background; -mod cid_contact; -mod config; -mod lotus_rpc; -mod multiaddr_parser; -mod pix_filspark; -mod provider_endpoints; -mod repository; -mod routes; -mod services; -mod types; -mod url_tester; -mod utils; - -pub struct AppState { - pub deal_repo: Arc, - pub active_requests: Arc, - pub job_repo: Arc, - pub storage_provider_repo: Arc, - pub cache: Cache, -} +use url_finder::{ + AppState, api::cache_middleware, background, config::Config, repository::*, + routes::create_routes, +}; /// Active requests counter middleware. /// Keeps track of the number of active requests. @@ -73,14 +49,16 @@ async fn main() -> Result<()> { color_eyre::install()?; + let config = Arc::new(Config::new_from_env()?); + tracing_subscriber::fmt() .with_env_filter( - EnvFilter::try_from_default_env().unwrap_or(EnvFilter::new(CONFIG.log_level.clone())), + EnvFilter::try_from_default_env().unwrap_or(EnvFilter::new(config.log_level.clone())), ) .init(); - let pool = sqlx::PgPool::connect(&CONFIG.db_url).await?; - let dmob_pool = sqlx::PgPool::connect(&CONFIG.dmob_db_url).await?; + let pool = sqlx::PgPool::connect(&config.db_url).await?; + let dmob_pool = sqlx::PgPool::connect(&config.dmob_db_url).await?; info!("Running database migrations..."); sqlx::migrate!("../migrations") @@ -106,6 +84,7 @@ async fn main() -> Result<()> { job_repo: Arc::new(JobRepository::new()), storage_provider_repo: sp_repo.clone(), cache, + config: config.clone(), }); // Start the provider discovery in the background @@ -122,13 +101,15 @@ async fn main() -> Result<()> { let sp_repo = sp_repo.clone(); let url_repo = url_repo.clone(); let deal_repo = deal_repo.clone(); + let config = config.clone(); async move { - background::run_url_discovery_scheduler(sp_repo, url_repo, deal_repo).await; + background::run_url_discovery_scheduler(config, sp_repo, url_repo, deal_repo).await; } }); // Start the job handler in the background tokio::spawn(background::job_handler( + config.clone(), app_state.job_repo.clone(), app_state.deal_repo.clone(), )); @@ -140,7 +121,11 @@ async fn main() -> Result<()> { .allow_methods(Any) .allow_headers(Any); - let app = create_routes(app_state.clone()) + let app = create_routes() + .route_layer(middleware::from_fn_with_state( + app_state.clone(), + cache_middleware, + )) .layer(middleware::from_fn_with_state( app_state.clone(), request_counter, diff --git a/url_finder/src/multiaddr_parser.rs b/url_finder/src/multiaddr_parser.rs index b8b9d1b..f4dbe43 100644 --- a/url_finder/src/multiaddr_parser.rs +++ b/url_finder/src/multiaddr_parser.rs @@ -6,6 +6,7 @@ pub struct UrlParts { protocol: Option, host: Option, port: Option, + is_tcp: bool, } impl UrlParts { fn new() -> Self { @@ -13,6 +14,7 @@ impl UrlParts { protocol: None, host: None, port: None, + is_tcp: false, } } fn to_url(&self) -> Result { @@ -28,8 +30,8 @@ impl std::fmt::Display for UrlParts { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "protocol: {:?}, host: {:?}, port: {:?}", - self.protocol, self.host, self.port + "protocol: {:?}, host: {:?}, port: {:?}, is_tcp: {}", + self.protocol, self.host, self.port, self.is_tcp ) } } @@ -39,39 +41,36 @@ pub fn parse(addrs: Vec) -> Vec { } fn parse_addr(addr: String) -> Option { - debug!("parsing addr: {:?}", addr); + debug!("Parsing multiaddr: {}", addr); match addr.parse::() { Ok(multiaddr) => { let url_parts = parse_multiaddr(multiaddr); - debug!("url_parts: {}", url_parts); + debug!("Multiaddr parsed to url_parts: {}", url_parts); match url_parts.to_url() { Ok(endpoint) => { - debug!("parsed endpoint: {:?}", endpoint); + debug!("Multiaddr parsed to endpoint: {}", endpoint); Some(endpoint) } Err(e) => { - warn!( - "Failed to convert multiaddr: {:?} to URL: {:?}", - addr, - e.to_string() - ); + warn!("Failed to convert multiaddr {} to URL: {}", addr, e); None } } } Err(e) => { - info!("Failed to parse multiaddr: {:?} due to {:?}", addr, e); + info!("Failed to parse multiaddr {}: {}", addr, e); None } } } fn parse_multiaddr(multiaddr: Multiaddr) -> UrlParts { - multiaddr + let mut url_parts = multiaddr .into_iter() .fold(UrlParts::new(), |mut url_parts, protocol| { + debug!("Processing protocol component: {:?}", protocol); match protocol { Protocol::Dns(dns) | Protocol::Dns4(dns) | Protocol::Dns6(dns) => { url_parts.host = Some(dns.to_string()); @@ -82,8 +81,16 @@ fn parse_multiaddr(multiaddr: Multiaddr) -> UrlParts { Protocol::Ip6(ip) => { url_parts.host = Some(ip.to_string()); } - Protocol::Tcp(port) | Protocol::Udp(port) => { - url_parts.port = Some(port.to_string()); + Protocol::Tcp(port) => { + if url_parts.port.is_none() { + url_parts.port = Some(port.to_string()); + } + url_parts.is_tcp = true; + } + Protocol::Udp(port) => { + if url_parts.port.is_none() { + url_parts.port = Some(port.to_string()); + } } Protocol::Http => { url_parts.protocol = Some("http".to_string()); @@ -94,5 +101,69 @@ fn parse_multiaddr(multiaddr: Multiaddr) -> UrlParts { _ => {} } url_parts - }) + }); + + // Default to HTTP if we have host + TCP port but no explicit protocol. + // This handles multiaddrs like /ip4/x.x.x.x/tcp/8080 without explicit /http suffix. + if url_parts.protocol.is_none() + && url_parts.host.is_some() + && url_parts.port.is_some() + && url_parts.is_tcp + { + warn!( + "Inferring HTTP for TCP multiaddr without explicit protocol: {}:{}", + url_parts.host.as_ref().unwrap(), + url_parts.port.as_ref().unwrap() + ); + url_parts.protocol = Some("http".to_string()); + } + + url_parts +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_ip4_tcp_http() { + assert_eq!( + parse_addr("/ip4/1.2.3.4/tcp/8080/http".to_string()), + Some("http://1.2.3.4:8080".to_string()) + ); + } + + #[test] + fn infers_http_for_tcp_without_protocol() { + assert_eq!( + parse_addr("/ip4/1.2.3.4/tcp/8080".to_string()), + Some("http://1.2.3.4:8080".to_string()) + ); + } + + #[test] + fn fails_without_tcp_port() { + assert_eq!(parse_addr("/dns/example.com/https".to_string()), None); + } + + #[test] + fn fails_udp_without_protocol() { + // UDP should NOT auto-infer HTTP (only TCP does) + assert_eq!(parse_addr("/ip4/1.2.3.4/udp/8080".to_string()), None); + } + + #[test] + fn parse_filters_invalid_multiaddrs() { + let addrs = vec![ + "/ip4/1.2.3.4/tcp/8080/http".to_string(), + "/dns/host/https".to_string(), + "/dns/example.com/tcp/443/https".to_string(), + ]; + + let result = parse(addrs); + + assert_eq!(result.len(), 2); + assert_eq!(result[0], "http://1.2.3.4:8080"); + assert_eq!(result[1], "https://example.com:443"); + } } diff --git a/url_finder/src/provider_endpoints.rs b/url_finder/src/provider_endpoints.rs index c881a7a..6ac4776 100644 --- a/url_finder/src/provider_endpoints.rs +++ b/url_finder/src/provider_endpoints.rs @@ -11,12 +11,12 @@ use alloy::{ }; use color_eyre::{Result, eyre::eyre}; use tokio::time::sleep; -use tracing::{debug, error, info}; +use tracing::{debug, error, info, warn}; -use crate::config::CONFIG; use crate::{ ErrorCode, ResultCode, cid_contact::{self, CidContactError}, + config::Config, lotus_rpc, multiaddr_parser, types::ProviderAddress, }; @@ -30,8 +30,11 @@ sol! { function getPeerData(uint64 minerID) view returns (PeerData); } -pub async fn valid_curio_provider(address: &ProviderAddress) -> Result> { - let rpc_url = &CONFIG.glif_url; +pub async fn valid_curio_provider( + config: &Config, + address: &ProviderAddress, +) -> Result> { + let rpc_url = &config.glif_url; let rpc_provider = ProviderBuilder::new() .connect(rpc_url) @@ -68,7 +71,10 @@ pub async fn valid_curio_provider(address: &ProviderAddress) -> Result Result Result<(ResultCode, Option>), ErrorCode> { - let peer_id = if let Some(curio_provider) = - valid_curio_provider(address).await.map_err(|e| { - error!("Failed to get peer id from curio: {:?}", e); - ErrorCode::FailedToGetPeerIdFromCurio - })? { - curio_provider - } else { - // get peer_id from miner info from lotus rpc - lotus_rpc::get_peer_id(address).await.map_err(|e| { - error!("Failed to get peer id: {:?}", e); - ErrorCode::FailedToGetPeerId - })? + let peer_id = match valid_curio_provider(config, address).await { + Ok(Some(curio_provider)) => curio_provider, + _ => { + debug!("Falling back to lotus for peer_id lookup"); + lotus_rpc::get_peer_id(config, address).await.map_err(|e| { + error!("Failed to get peer id from lotus: {e:?}"); + ErrorCode::FailedToGetPeerId + })? + } }; // get cid contact response - let cid_contact_res = match cid_contact::get_contact(&peer_id).await { + let cid_contact_res = match cid_contact::get_contact(config, &peer_id).await { Ok(res) => res, Err(CidContactError::NoData) => { return Ok((ResultCode::NoCidContactData, None)); diff --git a/url_finder/src/routes.rs b/url_finder/src/routes.rs index df971ce..8f8002d 100644 --- a/url_finder/src/routes.rs +++ b/url_finder/src/routes.rs @@ -4,11 +4,9 @@ use axum::{ Router, body::Body, http::Response, - middleware, response::IntoResponse, routing::{get, post}, }; -use common::api_response::*; use tower_governor::{ GovernorError, GovernorLayer, governor::GovernorConfigBuilder, key_extractor::SmartIpKeyExtractor, @@ -16,44 +14,44 @@ use tower_governor::{ use utoipa::OpenApi; use utoipa_swagger_ui::SwaggerUi; -use crate::{AppState, api::*}; +use crate::{AppState, api::*, api_response::*}; fn too_many_requests_error_handler(error: GovernorError) -> Response { - tracing::error!("Rate limit error: {:?}", error); - match error { GovernorError::TooManyRequests { .. } => { + tracing::warn!("Rate limit hit: {:?}", error); too_many_requests("Rate limit exceeded").into_response() } - _ => internal_server_error("Rate limit error").into_response(), + _ => { + tracing::error!("Rate limit error: {:?}", error); + internal_server_error("Rate limit error").into_response() + } } } -pub fn create_routes(app_state: Arc) -> Router> { +pub fn create_routes() -> Router> { // more strict rate limiting for the sync routes - let governor_secure = Arc::new( + let governor_secure_config = Arc::new( GovernorConfigBuilder::default() .per_millisecond(30) .burst_size(30) .key_extractor(SmartIpKeyExtractor) - .error_handler(too_many_requests_error_handler) .finish() .unwrap(), ); // less strict rate limiting for the async routes that will have internal queue anyway - let governor_async = Arc::new( + let governor_async_config = Arc::new( GovernorConfigBuilder::default() .per_millisecond(30) .burst_size(30) .key_extractor(SmartIpKeyExtractor) - .error_handler(too_many_requests_error_handler) .finish() .unwrap(), ); - let governor_secure_limiter = governor_secure.limiter().clone(); - let governor_async_limiter = governor_async.limiter().clone(); + let governor_secure_limiter = governor_secure_config.limiter().clone(); + let governor_async_limiter = governor_async_config.limiter().clone(); let interval = Duration::from_secs(60); @@ -79,44 +77,43 @@ pub fn create_routes(app_state: Arc) -> Router> { let swagger_routes = SwaggerUi::new("/").url("/api-doc/openapi.json", ApiDoc::openapi()); let sync_routes = Router::new() - .route("/url/find/:provider", get(handle_find_url_sp)) + .route("/url/find/{provider}", get(handle_find_url_sp)) .route( - "/url/find/:provider/:client", + "/url/find/{provider}/{client}", get(handle_find_url_sp_client), ) .route( - "/url/retrievability/:provider/:client", + "/url/retrievability/{provider}/{client}", get(handle_find_retri_by_client_and_sp), ) .route( - "/url/retrievability/:provider", + "/url/retrievability/{provider}", get(handle_find_retri_by_sp), ) - .route("/url/client/:client", get(handle_find_client)) - .layer(middleware::from_fn_with_state( - app_state.clone(), - cache_middleware, - )) - .layer(GovernorLayer { - config: governor_secure.clone(), - }); + .route("/url/client/{client}", get(handle_find_client)) + .layer( + GovernorLayer::new(governor_secure_config.clone()) + .error_handler(too_many_requests_error_handler), + ); let async_routes = Router::new() - .route("/jobs/:id", get(handle_get_job)) + .route("/jobs/{id}", get(handle_get_job)) .route("/jobs", post(handle_create_job)) - .layer(GovernorLayer { - config: governor_async.clone(), - }); + .layer( + GovernorLayer::new(governor_async_config.clone()) + .error_handler(too_many_requests_error_handler), + ); let healthcheck_route = Router::new() .route("/healthcheck", get(handle_healthcheck)) - .layer(GovernorLayer { - config: governor_secure.clone(), - }); + .layer( + GovernorLayer::new(governor_secure_config.clone()) + .error_handler(too_many_requests_error_handler), + ); Router::new() .merge(swagger_routes) .merge(sync_routes) .merge(async_routes) - .merge(healthcheck_route.clone()) + .merge(healthcheck_route) } diff --git a/url_finder/src/services/deal_service.rs b/url_finder/src/services/deal_service.rs index 5bc0e9f..cdb76da 100644 --- a/url_finder/src/services/deal_service.rs +++ b/url_finder/src/services/deal_service.rs @@ -111,7 +111,7 @@ pub async fn get_piece_url(endpoints: Vec, piece_ids: Vec) -> Ve endpoints .iter() .flat_map(|endpoint| { - let endpoint = endpoint.clone(); + let endpoint = endpoint.trim_end_matches('/'); piece_ids .iter() .map(move |piece_id| format!("{endpoint}/piece/{piece_id}")) diff --git a/url_finder/src/services/url_discovery_service.rs b/url_finder/src/services/url_discovery_service.rs index dd10533..7a7be1e 100644 --- a/url_finder/src/services/url_discovery_service.rs +++ b/url_finder/src/services/url_discovery_service.rs @@ -1,4 +1,5 @@ use crate::{ + config::Config, provider_endpoints, repository::DealRepository, services::deal_service, @@ -7,7 +8,7 @@ use crate::{ }, url_tester, }; -use tracing::error; +use tracing::{debug, error}; use uuid::Uuid; #[derive(Debug, Clone)] @@ -51,6 +52,7 @@ impl UrlDiscoveryResult { } pub async fn discover_url( + config: &Config, provider_address: &ProviderAddress, client_address: Option, deal_repo: &DealRepository, @@ -58,13 +60,19 @@ pub async fn discover_url( let provider_id: ProviderId = provider_address.clone().into(); let client_id: Option = client_address.clone().map(|c| c.into()); + tracing::info!( + "discover_url called for provider={}, client={:?}", + provider_address, + client_address + ); + let mut result = match &client_id { Some(c) => UrlDiscoveryResult::new_provider_client(provider_id.clone(), c.clone()), None => UrlDiscoveryResult::new_provider_only(provider_id.clone()), }; let (result_code, endpoints) = - match provider_endpoints::get_provider_endpoints(provider_address).await { + match provider_endpoints::get_provider_endpoints(config, provider_address).await { Ok((code, eps)) => (code, eps), Err(e) => { error!( @@ -103,9 +111,19 @@ pub async fn discover_url( return result; } - let urls = deal_service::get_piece_url(endpoints, piece_ids).await; + let urls = deal_service::get_piece_url(endpoints.clone(), piece_ids).await; + debug!( + "Built {} URLs to test from endpoints: {:?}", + urls.len(), + endpoints + ); + debug!("Testing URLs: {:?}", urls); let (working_url, retrievability_percent) = url_tester::check_retrievability_with_get(urls, true).await; + debug!( + "URL test result - working_url: {:?}, retrievability: {:?}", + working_url, retrievability_percent + ); result.working_url = working_url.clone(); result.retrievability_percent = retrievability_percent.unwrap_or(0.0); diff --git a/url_finder/src/url_tester.rs b/url_finder/src/url_tester.rs index 4e640ef..209d9f0 100644 --- a/url_finder/src/url_tester.rs +++ b/url_finder/src/url_tester.rs @@ -141,15 +141,22 @@ pub async fn check_retrievability_with_get( async move { total_clone.fetch_add(1, Ordering::SeqCst); + debug!("Testing URL: {}", url); match client.get(&url).send().await { Ok(resp) => { + let status = resp.status(); let content_type = resp .headers() .get("content-type") .and_then(|v| v.to_str().ok()); let etag = resp.headers().get("etag"); - if resp.status().is_success() + debug!( + "Response for {}: status={}, content_type={:?}, etag={:?}", + url, status, content_type, etag + ); + + if status.is_success() && matches!( content_type, Some("application/octet-stream") | Some("application/piece") diff --git a/url_finder/tests/api_tests.rs b/url_finder/tests/api_tests.rs new file mode 100644 index 0000000..1b52936 --- /dev/null +++ b/url_finder/tests/api_tests.rs @@ -0,0 +1,2 @@ +mod common; +mod integration_tests; diff --git a/url_finder/tests/common/container.rs b/url_finder/tests/common/container.rs new file mode 100644 index 0000000..72f9991 --- /dev/null +++ b/url_finder/tests/common/container.rs @@ -0,0 +1,42 @@ +#![allow(dead_code)] + +use std::sync::{Arc, LazyLock, Weak}; +use testcontainers::{ContainerAsync, ImageExt, runners::AsyncRunner}; +use testcontainers_modules::postgres::Postgres; +use tokio::sync::Mutex; + +pub const POSTGRES_USER: &str = "postgres"; +pub const POSTGRES_PASSWORD: &str = "postgres"; + +pub struct ContainerState { + pub container: ContainerAsync, + pub port: u16, +} + +static CONTAINER: LazyLock>> = LazyLock::new(|| Mutex::new(Weak::new())); + +pub async fn get_or_create_container() -> Arc { + let mut weak_lock = CONTAINER.lock().await; + + // Try to upgrade Weak to Arc (reuse existing container) + if let Some(arc) = weak_lock.upgrade() { + return arc; + } + + // Weak failed to upgrade (Create new container) + let container = Postgres::default() + .with_tag("16-alpine") + .start() + .await + .expect("Failed to start Postgres container"); + + let port = container + .get_host_port_ipv4(5432) + .await + .expect("Failed to get container port"); + + let state = Arc::new(ContainerState { container, port }); + *weak_lock = Arc::downgrade(&state); + + state +} diff --git a/url_finder/tests/common/db_setup.rs b/url_finder/tests/common/db_setup.rs new file mode 100644 index 0000000..d02060e --- /dev/null +++ b/url_finder/tests/common/db_setup.rs @@ -0,0 +1,147 @@ +#![allow(dead_code)] + +use sqlx::{PgPool, Postgres, migrate::MigrateDatabase}; + +pub use super::container::{POSTGRES_PASSWORD, POSTGRES_USER}; + +pub struct TestDatabases { + pub app_pool: PgPool, + pub app_db_name: String, + pub postgres_host: String, +} + +pub async fn setup_test_db_with_port(port: u16) -> TestDatabases { + let postgres_host = format!("localhost:{port}"); + // test function name + let test_name = std::thread::current() + .name() + .unwrap_or("unknown") + .rsplit("::") + .next() + .unwrap_or("unknown") + .to_lowercase(); + + let app_db_name = format!("test_{test_name}"); + let app_db_url = + format!("postgres://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{postgres_host}/{app_db_name}"); + + if let Err(e) = Postgres::drop_database(&app_db_url).await { + let err_str = e.to_string(); + // "database does not exist" is expected on first run - not an error + if !err_str.contains("does not exist") { + tracing::warn!("Failed to drop test database {app_db_name}: {e}"); + } + } + + Postgres::create_database(&app_db_url) + .await + .expect("Failed to create test database"); + + let app_pool = PgPool::connect(&app_db_url) + .await + .expect("Failed to connect to test database"); + + sqlx::migrate!("../migrations") + .run(&app_pool) + .await + .expect("Failed to run migrations"); + + let test_schema = include_str!("test_schema.sql"); + sqlx::query(test_schema) + .execute(&app_pool) + .await + .expect("Failed to apply test schema"); + + TestDatabases { + app_pool, + app_db_name, + postgres_host, + } +} + +pub async fn seed_provider(app_pool: &PgPool, provider_id: &str) { + sqlx::query( + r#"INSERT INTO + storage_providers ( + provider_id + ) + VALUES + ($1) + ON CONFLICT (provider_id) DO NOTHING"#, + ) + .bind(provider_id) + .execute(app_pool) + .await + .expect("Failed to insert provider"); +} + +pub async fn seed_deals( + app_pool: &PgPool, + provider_id: &str, + client_id: Option<&str>, + piece_cids: Vec<&str>, +) { + for piece_cid in piece_cids { + sqlx::query( + r#"INSERT INTO + unified_verified_deal ( + "providerId", + "clientId", + "pieceCid" + ) + VALUES + ($1, $2, $3) + "#, + ) + .bind(provider_id) + .bind(client_id) + .bind(piece_cid) + .execute(app_pool) + .await + .expect("Failed to insert deal"); + } +} + +pub async fn seed_url_result( + app_pool: &PgPool, + provider_id: &str, + client_id: Option<&str>, + working_url: Option<&str>, + retrievability: f64, + result_code: &str, +) { + assert!( + (0.0..=100.0).contains(&retrievability), + "retrievability must be in range 0..=100, got {retrievability}" + ); + + let result_type = if client_id.is_some() { + "ProviderClient" + } else { + "Provider" + }; + + sqlx::query( + r#"INSERT INTO + url_results ( + provider_id, + client_id, + result_type, + working_url, + retrievability_percent, + result_code, + tested_at + ) + VALUES + ($1, $2, $3::discovery_type, $4, $5, $6::result_code, NOW())"#, + ) + .bind(provider_id) + .bind(client_id) + .bind(result_type) + .bind(working_url) + .bind(retrievability) + .bind(result_code) + .execute(app_pool) + .await + .expect("Failed to insert url_result"); +} diff --git a/url_finder/tests/common/mock_servers.rs b/url_finder/tests/common/mock_servers.rs new file mode 100644 index 0000000..ee57700 --- /dev/null +++ b/url_finder/tests/common/mock_servers.rs @@ -0,0 +1,111 @@ +#![allow(dead_code)] + +use serde_json::json; +use wiremock::matchers::{body_partial_json, method, path}; +use wiremock::{Mock, MockServer, ResponseTemplate}; + +pub struct MockExternalServices { + pub lotus: MockServer, + pub cid_contact: MockServer, + pub piece_server: MockServer, +} + +impl MockExternalServices { + pub async fn start() -> Self { + Self { + lotus: MockServer::start().await, + cid_contact: MockServer::start().await, + piece_server: MockServer::start().await, + } + } + + pub fn lotus_url(&self) -> String { + self.lotus.uri() + } + + pub fn cid_contact_url(&self) -> String { + self.cid_contact.uri() + } + + pub fn piece_server_url(&self) -> String { + self.piece_server.uri() + } + + pub async fn setup_lotus_peer_id_mock( + &self, + provider: &str, + peer_id: &str, + multiaddrs: Vec, + ) { + // force fallback to lotus + Mock::given(method("POST")) + .and(path("/rpc/v1")) + .and(body_partial_json(json!({ + "method": "eth_call" + }))) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "jsonrpc": "2.0", + "id": 1, + "result": "0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + }))) + .mount(&self.lotus) + .await; + + Mock::given(method("POST")) + .and(path("/rpc/v1")) + .and(body_partial_json(json!({ + "method": "Filecoin.StateMinerInfo", + "params": [provider, null] + }))) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "jsonrpc": "2.0", + "id": 1, + "result": { + "PeerId": peer_id, + "Multiaddrs": multiaddrs + } + }))) + .mount(&self.lotus) + .await; + } + + pub async fn setup_cid_contact_mock(&self, peer_id: &str, multiaddrs: Vec) { + let response_body = json!({ + "Publisher": { + "ID": peer_id, + "Addrs": multiaddrs + } + }); + + Mock::given(method("GET")) + .and(path(format!("/providers/{peer_id}"))) + .respond_with(ResponseTemplate::new(200).set_body_json(response_body)) + .mount(&self.cid_contact) + .await; + } + + pub async fn setup_piece_retrieval_mock(&self, piece_cid: &str, should_succeed: bool) { + if should_succeed { + for http_method in ["GET", "HEAD"] { + Mock::given(method(http_method)) + .and(path(format!("/piece/{piece_cid}"))) + .respond_with( + ResponseTemplate::new(200) + .insert_header("content-type", "application/piece") + .insert_header("etag", "\"mock-etag-12345\"") + .set_body_raw(vec![0u8; 100], "application/piece"), + ) + .mount(&self.piece_server) + .await; + } + } else { + for http_method in ["GET", "HEAD"] { + Mock::given(method(http_method)) + .and(path(format!("/piece/{piece_cid}"))) + .respond_with(ResponseTemplate::new(404)) + .mount(&self.piece_server) + .await; + } + } + } +} diff --git a/url_finder/tests/common/mod.rs b/url_finder/tests/common/mod.rs new file mode 100644 index 0000000..ed8f8bb --- /dev/null +++ b/url_finder/tests/common/mod.rs @@ -0,0 +1,14 @@ +#![allow(unused_imports)] + +pub mod container; +pub mod db_setup; +pub mod mock_servers; +pub mod test_app; +pub mod test_constants; +pub mod test_context; +pub mod validation_helpers; + +pub use db_setup::*; +pub use test_constants::*; +pub use test_context::{ProviderFixture, TestContext}; +pub use validation_helpers::*; diff --git a/url_finder/tests/common/test_app.rs b/url_finder/tests/common/test_app.rs new file mode 100644 index 0000000..2b4e835 --- /dev/null +++ b/url_finder/tests/common/test_app.rs @@ -0,0 +1,52 @@ +#![allow(dead_code)] + +use axum::{ + extract::Request, + middleware::{self, Next}, + response::Response, +}; +use axum_test::TestServer; +use moka::future::Cache; +use std::{ + net::SocketAddr, + sync::{Arc, atomic::AtomicUsize}, +}; +use url_finder::{AppState, config::Config, repository::*}; + +use super::{TestDatabases, mock_servers::MockExternalServices}; + +async fn inject_socket_addr(mut request: Request, next: Next) -> Response { + let mock_addr = SocketAddr::from(([127, 0, 0, 1], 8080)); + request.extensions_mut().insert(mock_addr); + next.run(request).await +} + +pub async fn create_test_app(dbs: &TestDatabases, mocks: &MockExternalServices) -> TestServer { + let active_requests = Arc::new(AtomicUsize::new(0)); + let cache: Cache = Cache::builder() + .max_capacity(1000) + .time_to_live(std::time::Duration::from_secs(3600)) + .build(); + + let lotus_url = mocks.lotus_url(); + let lotus_base = lotus_url.trim_end_matches('/'); + let config = Arc::new(Config::new_for_test( + format!("{lotus_base}/rpc/v1"), + mocks.cid_contact_url(), + )); + + let app_state = Arc::new(AppState { + deal_repo: Arc::new(DealRepository::new(dbs.app_pool.clone())), + active_requests, + job_repo: Arc::new(JobRepository::new()), + storage_provider_repo: Arc::new(StorageProviderRepository::new(dbs.app_pool.clone())), + cache, + config, + }); + + let app = url_finder::routes::create_routes() + .layer(middleware::from_fn(inject_socket_addr)) + .with_state(app_state.clone()); + + TestServer::new(app).expect("Failed to create test server") +} diff --git a/url_finder/tests/common/test_constants.rs b/url_finder/tests/common/test_constants.rs new file mode 100644 index 0000000..0438b09 --- /dev/null +++ b/url_finder/tests/common/test_constants.rs @@ -0,0 +1,58 @@ +//! Shared test constants. +//! Each test file is compiled as a separate binary, so some constants may appear unused in specific binaries. +#![allow(dead_code)] + +use url_finder::types::{ClientAddress, ClientId, ProviderAddress, ProviderId}; + +pub const TEST_PIECE_CID: &str = "baga6ea4seaqao7s73y24kcutaosvacpdjgfe5pw76ooefnyqw4ynr3d2y6x2mpq"; + +pub const TEST_PIECE_CID_2: &str = + "baga6ea4seaqao7s73y24kcutaosvacpdjgfe5pw76ooefnyqw4ynr3d2y6x2AAA"; + +// String constants for DB seeding functions +pub const TEST_CLIENT_ID_DB: &str = "1000"; +pub const TEST_CLIENT_ID_API: &str = "f01000"; +pub const TEST_PROVIDER_1_DB: &str = "88881000"; +pub const TEST_PROVIDER_1_API: &str = "f088881000"; +pub const TEST_PROVIDER_2_DB: &str = "88882000"; +pub const TEST_PROVIDER_2_API: &str = "f088882000"; + +// Typed helpers for tests +pub fn test_client_id() -> ClientId { + ClientId::new(TEST_CLIENT_ID_DB).unwrap() +} + +pub fn test_client_address() -> ClientAddress { + ClientAddress::new(TEST_CLIENT_ID_API).unwrap() +} + +pub fn test_provider_1_id() -> ProviderId { + ProviderId::new(TEST_PROVIDER_1_DB).unwrap() +} + +pub fn test_provider_1_address() -> ProviderAddress { + ProviderAddress::new(TEST_PROVIDER_1_API).unwrap() +} + +pub fn test_provider_2_id() -> ProviderId { + ProviderId::new(TEST_PROVIDER_2_DB).unwrap() +} + +pub fn test_provider_2_address() -> ProviderAddress { + ProviderAddress::new(TEST_PROVIDER_2_API).unwrap() +} + +pub const TEST_MULTIADDR_HTTP_80: &str = "/ip4/1.2.3.4/tcp/80/http"; +pub const TEST_MULTIADDR_HTTP_8080: &str = "/ip4/1.2.3.4/tcp/8080/http"; + +pub fn multiaddrs_http_80() -> Vec { + vec![TEST_MULTIADDR_HTTP_80.to_string()] +} + +pub fn multiaddrs_http_8080() -> Vec { + vec![TEST_MULTIADDR_HTTP_8080.to_string()] +} + +pub fn multiaddrs_empty() -> Vec { + vec![] +} diff --git a/url_finder/tests/common/test_context.rs b/url_finder/tests/common/test_context.rs new file mode 100644 index 0000000..66f9136 --- /dev/null +++ b/url_finder/tests/common/test_context.rs @@ -0,0 +1,198 @@ +#![allow(dead_code)] + +use axum_test::TestServer; +use sqlx::{Postgres, migrate::MigrateDatabase}; +use std::env; +use std::sync::Arc; +use tracing::{debug, warn}; +use url_finder::types::{ProviderAddress, ProviderId}; + +use super::container::{ContainerState, get_or_create_container}; +use super::db_setup::{ + POSTGRES_PASSWORD, POSTGRES_USER, TestDatabases, seed_deals, seed_provider, + setup_test_db_with_port, +}; +use super::mock_servers::MockExternalServices; +use super::test_app::create_test_app; +use super::test_constants::TEST_PIECE_CID; + +pub struct TestContext { + pub mocks: MockExternalServices, + pub dbs: TestDatabases, + pub app: TestServer, + _container_ref: Arc, +} + +pub struct ProviderFixture { + pub provider_id: ProviderId, + pub provider_address: ProviderAddress, + pub peer_id: String, +} + +impl TestContext { + pub async fn new() -> Self { + let container_ref = get_or_create_container().await; + let port = container_ref.port; + + let mocks = MockExternalServices::start().await; + let dbs = setup_test_db_with_port(port).await; + let app = create_test_app(&dbs, &mocks).await; + + Self { + mocks, + dbs, + app, + _container_ref: container_ref, + } + } + + pub async fn setup_provider_with_deals( + &self, + provider_id: &str, + client_id: Option<&str>, + multiaddrs: Vec, + ) -> ProviderFixture { + let peer_id = format!("12D3Koo{}", provider_id); + + seed_provider(&self.dbs.app_pool, provider_id).await; + seed_deals( + &self.dbs.app_pool, + provider_id, + client_id, + vec![TEST_PIECE_CID], + ) + .await; + + self.mocks + .setup_lotus_peer_id_mock(&format!("f0{provider_id}"), &peer_id, vec![]) + .await; + + self.mocks + .setup_cid_contact_mock(&peer_id, multiaddrs) + .await; + + ProviderFixture { + provider_id: ProviderId::new(provider_id).unwrap(), + provider_address: ProviderAddress::new(format!("f0{provider_id}")).unwrap(), + peer_id, + } + } + + pub async fn setup_provider_no_deals( + &self, + provider_id: &str, + multiaddrs: Vec, + ) -> ProviderFixture { + let peer_id = format!("12D3Koo{}", provider_id); + + seed_provider(&self.dbs.app_pool, provider_id).await; + + self.mocks + .setup_lotus_peer_id_mock(&format!("f0{provider_id}"), &peer_id, vec![]) + .await; + + self.mocks + .setup_cid_contact_mock(&peer_id, multiaddrs) + .await; + + ProviderFixture { + provider_id: ProviderId::new(provider_id).unwrap(), + provider_address: ProviderAddress::new(format!("f0{provider_id}")).unwrap(), + peer_id, + } + } + + pub async fn setup_provider_with_deals_and_mock_server( + &self, + provider_id: &str, + client_id: Option<&str>, + piece_cids: Vec<&str>, + success_rate: f64, + ) -> ProviderFixture { + let peer_id = format!("12D3Koo{}", provider_id); + + seed_provider(&self.dbs.app_pool, provider_id).await; + seed_deals( + &self.dbs.app_pool, + provider_id, + client_id, + piece_cids.clone(), + ) + .await; + + let success_count = (piece_cids.len() as f64 * success_rate).ceil() as usize; + for (idx, piece_cid) in piece_cids.iter().enumerate() { + let should_succeed = idx < success_count; + self.mocks + .setup_piece_retrieval_mock(piece_cid, should_succeed) + .await; + } + + let piece_server_url = self.mocks.piece_server_url(); + let host_and_port = piece_server_url.trim_start_matches("http://"); + let (host, port) = host_and_port + .split_once(':') + .expect("MockServer URL should be http://host:port"); + + // Build multiaddr pointing to mock server WITHOUT /http suffix + // The /http protocol auto-adds /tcp/80, so we can't use it for custom ports + // Instead, just use /ip4/{host}/tcp/{port} + let multiaddrs = vec![format!( + "/ip4/{}/tcp/{}", + if host == "localhost" { + "127.0.0.1" + } else { + host + }, + port + )]; + + debug!("Mock piece server URL: {}", piece_server_url); + debug!("Extracted host: {}, port: {}", host, port); + debug!("Generated multiaddrs: {:?}", multiaddrs); + + self.mocks + .setup_lotus_peer_id_mock(&format!("f0{provider_id}"), &peer_id, vec![]) + .await; + + self.mocks + .setup_cid_contact_mock(&peer_id, multiaddrs) + .await; + + ProviderFixture { + provider_id: ProviderId::new(provider_id).unwrap(), + provider_address: ProviderAddress::new(format!("f0{provider_id}")).unwrap(), + peer_id, + } + } +} + +impl Drop for TestContext { + fn drop(&mut self) { + if env::var("KEEP_TEST_DB").is_ok() { + println!("Keeping test database: {}", self.dbs.app_db_name); + return; + } + + let db_name = self.dbs.app_db_name.clone(); + let postgres_host = self.dbs.postgres_host.clone(); + let pool = self.dbs.app_pool.clone(); + + std::thread::spawn(move || { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("Failed to create cleanup runtime"); + + rt.block_on(async move { + pool.close().await; + let db_url = format!( + "postgres://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{postgres_host}/{db_name}" + ); + if let Err(e) = Postgres::drop_database(&db_url).await { + warn!("Failed to drop test database '{db_name}': {e}"); + } + }); + }); + } +} diff --git a/url_finder/tests/common/test_schema.sql b/url_finder/tests/common/test_schema.sql new file mode 100644 index 0000000..95c99e0 --- /dev/null +++ b/url_finder/tests/common/test_schema.sql @@ -0,0 +1,10 @@ +-- DMOB schema for testing purposes + +CREATE TABLE IF NOT EXISTS unified_verified_deal ( + id SERIAL PRIMARY KEY, + "dealId" INTEGER NOT NULL DEFAULT 0, + "claimId" INTEGER NOT NULL DEFAULT 0, + "clientId" TEXT, + "providerId" TEXT, + "pieceCid" TEXT +); diff --git a/url_finder/tests/common/validation_helpers.rs b/url_finder/tests/common/validation_helpers.rs new file mode 100644 index 0000000..9d05870 --- /dev/null +++ b/url_finder/tests/common/validation_helpers.rs @@ -0,0 +1,36 @@ +#![allow(dead_code)] + +use assert_json_diff::assert_json_include; +use axum::http::StatusCode; +use axum_test::TestResponse; +use axum_test::TestServer; +use pretty_assertions::assert_eq; + +pub async fn assert_bad_request_error(app: &TestServer, path: &str, error_contains: &str) { + let response = app.get(path).await; + assert_eq!(response.status_code(), StatusCode::BAD_REQUEST); + let body: serde_json::Value = response.json(); + assert!( + body["error"].as_str().unwrap().contains(error_contains), + "Expected error containing '{error_contains}', got: {:?}", + body["error"] + ); +} + +pub fn assert_json_response( + response: TestResponse, + expected_status: StatusCode, + expected_json: serde_json::Value, +) -> serde_json::Value { + assert_eq!(response.status_code(), expected_status); + let body: serde_json::Value = response.json(); + assert_json_include!(actual: body, expected: expected_json); + body +} + +pub fn assert_json_response_ok( + response: TestResponse, + expected_json: serde_json::Value, +) -> serde_json::Value { + assert_json_response(response, StatusCode::OK, expected_json) +} diff --git a/url_finder/tests/integration_tests/find_client.rs b/url_finder/tests/integration_tests/find_client.rs new file mode 100644 index 0000000..68c92f4 --- /dev/null +++ b/url_finder/tests/integration_tests/find_client.rs @@ -0,0 +1,50 @@ +use crate::common::*; +use serde_json::json; + +#[tokio::test] +async fn test_find_client_invalid_client() { + let ctx = TestContext::new().await; + assert_bad_request_error(&ctx.app, "/url/client/invalid", "Invalid client address").await; +} + +#[tokio::test] +async fn test_find_client_no_providers() { + let ctx = TestContext::new().await; + let client_id = "9998000"; + + let response = ctx.app.get(&format!("/url/client/f0{client_id}")).await; + + assert_json_response_ok( + response, + json!({ + "result": "Error", + "client": format!("f0{client_id}"), + "providers": [] + }), + ); +} + +#[tokio::test] +async fn test_find_client_providers_found() { + let ctx = TestContext::new().await; + let client_id = "9999000"; + + let fixture = ctx + .setup_provider_with_deals("99998000", Some(client_id), multiaddrs_http_8080()) + .await; + + let response = ctx.app.get(&format!("/url/client/f0{client_id}")).await; + + assert_json_response_ok( + response, + json!({ + "result": "Success", + "client": format!("f0{client_id}"), + "providers": [{ + "provider": fixture.provider_address, + "result": "Success", + "retrievability_percent": 0.0 + }] + }), + ); +} diff --git a/url_finder/tests/integration_tests/find_retri_sp.rs b/url_finder/tests/integration_tests/find_retri_sp.rs new file mode 100644 index 0000000..8c5408b --- /dev/null +++ b/url_finder/tests/integration_tests/find_retri_sp.rs @@ -0,0 +1,57 @@ +use crate::common::*; +use serde_json::json; + +#[tokio::test] +async fn test_find_retri_sp_invalid_provider() { + let ctx = TestContext::new().await; + assert_bad_request_error( + &ctx.app, + "/url/retrievability/invalid", + "Invalid provider address", + ) + .await; +} + +#[tokio::test] +async fn test_find_retri_sp_no_deals() { + let ctx = TestContext::new().await; + + let fixture = ctx + .setup_provider_no_deals("99994000", multiaddrs_http_80()) + .await; + + let response = ctx + .app + .get(&format!("/url/retrievability/{}", fixture.provider_address)) + .await; + + assert_json_response_ok( + response, + json!({ + "result": "NoDealsFound", + "retrievability_percent": 0.0 + }), + ); +} + +#[tokio::test] +async fn test_find_retri_sp_endpoints_discovered() { + let ctx = TestContext::new().await; + + let fixture = ctx + .setup_provider_with_deals("99995000", None, multiaddrs_http_8080()) + .await; + + let response = ctx + .app + .get(&format!("/url/retrievability/{}", fixture.provider_address)) + .await; + + assert_json_response_ok( + response, + json!({ + "result": "Success", + "retrievability_percent": 0.0 + }), + ); +} diff --git a/url_finder/tests/integration_tests/find_retri_sp_client.rs b/url_finder/tests/integration_tests/find_retri_sp_client.rs new file mode 100644 index 0000000..858125c --- /dev/null +++ b/url_finder/tests/integration_tests/find_retri_sp_client.rs @@ -0,0 +1,74 @@ +use crate::common::*; +use serde_json::json; + +#[tokio::test] +async fn test_find_retri_sp_client_invalid_provider() { + let ctx = TestContext::new().await; + assert_bad_request_error( + &ctx.app, + &format!("/url/retrievability/invalid/{TEST_CLIENT_ID_API}"), + "Invalid provider address", + ) + .await; +} + +#[tokio::test] +async fn test_find_retri_sp_client_invalid_client() { + let ctx = TestContext::new().await; + assert_bad_request_error( + &ctx.app, + "/url/retrievability/f01234/invalid", + "Invalid client address", + ) + .await; +} + +#[tokio::test] +async fn test_find_retri_sp_client_no_deals() { + let ctx = TestContext::new().await; + + let fixture = ctx + .setup_provider_no_deals("99996000", multiaddrs_http_80()) + .await; + + let response = ctx + .app + .get(&format!( + "/url/retrievability/{}/{}", + fixture.provider_address, TEST_CLIENT_ID_API + )) + .await; + + assert_json_response_ok( + response, + json!({ + "result": "NoDealsFound", + "retrievability_percent": 0.0 + }), + ); +} + +#[tokio::test] +async fn test_find_retri_sp_client_endpoints_discovered() { + let ctx = TestContext::new().await; + + let fixture = ctx + .setup_provider_with_deals("99997000", Some(TEST_CLIENT_ID_DB), multiaddrs_http_8080()) + .await; + + let response = ctx + .app + .get(&format!( + "/url/retrievability/{}/{}", + fixture.provider_address, TEST_CLIENT_ID_API + )) + .await; + + assert_json_response_ok( + response, + json!({ + "result": "Success", + "retrievability_percent": 0.0 + }), + ); +} diff --git a/url_finder/tests/integration_tests/find_url_sp.rs b/url_finder/tests/integration_tests/find_url_sp.rs new file mode 100644 index 0000000..49ef02d --- /dev/null +++ b/url_finder/tests/integration_tests/find_url_sp.rs @@ -0,0 +1,101 @@ +use crate::common::*; +use serde_json::json; + +#[tokio::test] +async fn test_find_url_sp_invalid_provider() { + let ctx = TestContext::new().await; + assert_bad_request_error(&ctx.app, "/url/find/invalid", "Invalid provider address").await; +} + +#[tokio::test] +async fn test_find_url_sp_no_deals() { + let ctx = TestContext::new().await; + + let fixture = ctx + .setup_provider_no_deals("99991234", multiaddrs_http_80()) + .await; + + let response = ctx + .app + .get(&format!("/url/find/{}", fixture.provider_address)) + .await; + + let body = assert_json_response_ok(response, json!({"result": "NoDealsFound"})); + assert!(body.get("url").is_none()); +} + +#[tokio::test] +async fn test_find_url_sp_unreachable_endpoints() { + let ctx = TestContext::new().await; + + let fixture = ctx + .setup_provider_with_deals("99995678", None, multiaddrs_http_8080()) + .await; + + let response = ctx + .app + .get(&format!("/url/find/{}", fixture.provider_address)) + .await; + + let body = assert_json_response_ok(response, json!({"result": "FailedToGetWorkingUrl"})); + assert!(body.get("url").is_none()); +} + +#[tokio::test] +async fn test_find_url_sp_success() { + let ctx = TestContext::new().await; + + let piece_cid = TEST_PIECE_CID; + ctx.mocks.setup_piece_retrieval_mock(piece_cid, true).await; + + let fixture = ctx + .setup_provider_with_deals_and_mock_server("88885000", None, vec![piece_cid], 1.0) + .await; + + let response = ctx + .app + .get(&format!("/url/find/{}", fixture.provider_address)) + .await; + + let body = assert_json_response_ok(response, json!({"result": "Success"})); + + let url = body["url"].as_str().expect("URL should be present"); + assert!( + url.contains(&ctx.mocks.piece_server_url()), + "URL should contain mock server address" + ); + assert!(url.contains(piece_cid), "URL should contain piece CID"); +} + +#[tokio::test] +async fn test_find_url_sp_partial_retrievability() { + let ctx = TestContext::new().await; + + let fixture = ctx + .setup_provider_with_deals_and_mock_server( + "88886000", + None, + vec![TEST_PIECE_CID, TEST_PIECE_CID_2], + 0.5, + ) + .await; + + let response = ctx + .app + .get(&format!("/url/find/{}", fixture.provider_address)) + .await; + + let body = assert_json_response_ok(response, json!({"result": "Success"})); + + let url = body["url"] + .as_str() + .expect("URL should be present even with partial retrievability"); + assert!( + url.contains(&ctx.mocks.piece_server_url()), + "URL should contain mock server address" + ); + assert!( + url.contains(TEST_PIECE_CID) || url.contains(TEST_PIECE_CID_2), + "URL should contain piece CID" + ); +} diff --git a/url_finder/tests/integration_tests/find_url_sp_client.rs b/url_finder/tests/integration_tests/find_url_sp_client.rs new file mode 100644 index 0000000..47fb923 --- /dev/null +++ b/url_finder/tests/integration_tests/find_url_sp_client.rs @@ -0,0 +1,130 @@ +use crate::common::*; +use serde_json::json; + +#[tokio::test] +async fn test_find_url_sp_client_invalid_provider() { + let ctx = TestContext::new().await; + assert_bad_request_error( + &ctx.app, + &format!("/url/find/invalid/{TEST_CLIENT_ID_API}"), + "Invalid provider address", + ) + .await; +} + +#[tokio::test] +async fn test_find_url_sp_client_invalid_client() { + let ctx = TestContext::new().await; + assert_bad_request_error( + &ctx.app, + "/url/find/f01234/invalid", + "Invalid client address", + ) + .await; +} + +#[tokio::test] +async fn test_find_url_sp_client_no_deals() { + let ctx = TestContext::new().await; + + let fixture = ctx + .setup_provider_no_deals("99992000", multiaddrs_http_80()) + .await; + + let response = ctx + .app + .get(&format!( + "/url/find/{}/{}", + fixture.provider_address, TEST_CLIENT_ID_API + )) + .await; + + let body = assert_json_response_ok(response, json!({"result": "NoDealsFound"})); + assert!(body.get("url").is_none()); +} + +#[tokio::test] +async fn test_find_url_sp_client_unreachable_endpoints() { + let ctx = TestContext::new().await; + + let fixture = ctx + .setup_provider_with_deals("99993000", Some(TEST_CLIENT_ID_DB), multiaddrs_http_8080()) + .await; + + let response = ctx + .app + .get(&format!( + "/url/find/{}/{}", + fixture.provider_address, TEST_CLIENT_ID_API + )) + .await; + + let body = assert_json_response_ok(response, json!({"result": "FailedToGetWorkingUrl"})); + assert!(body.get("url").is_none()); +} + +#[tokio::test] +async fn test_find_url_sp_client_success() { + let ctx = TestContext::new().await; + + let piece_cid = TEST_PIECE_CID; + ctx.mocks.setup_piece_retrieval_mock(piece_cid, true).await; + + let fixture = ctx + .setup_provider_with_deals_and_mock_server( + "88883000", + Some(TEST_CLIENT_ID_DB), + vec![piece_cid], + 1.0, + ) + .await; + + let response = ctx + .app + .get(&format!( + "/url/find/{}/{}", + fixture.provider_address, TEST_CLIENT_ID_API + )) + .await; + + let body = assert_json_response_ok(response, json!({"result": "Success"})); + + let url = body["url"].as_str().expect("URL should be present"); + assert!( + url.contains(&ctx.mocks.piece_server_url()), + "URL should contain mock server address" + ); + assert!(url.contains(piece_cid), "URL should contain piece CID"); +} + +#[tokio::test] +async fn test_find_url_sp_client_partial_retrievability() { + let ctx = TestContext::new().await; + + let fixture = ctx + .setup_provider_with_deals_and_mock_server( + "88884000", + Some(TEST_CLIENT_ID_DB), + vec![TEST_PIECE_CID, TEST_PIECE_CID_2], + 0.5, + ) + .await; + + let response = ctx + .app + .get(&format!( + "/url/find/{}/{}", + fixture.provider_address, TEST_CLIENT_ID_API + )) + .await; + + let body = assert_json_response_ok(response, json!({"result": "Success"})); + + let url = body["url"] + .as_str() + .expect("URL should be present even with partial retrievability"); + assert!( + url.contains(&ctx.mocks.piece_server_url()), + "URL should contain mock server address" + ); +} diff --git a/url_finder/tests/integration_tests/mod.rs b/url_finder/tests/integration_tests/mod.rs new file mode 100644 index 0000000..8afa14c --- /dev/null +++ b/url_finder/tests/integration_tests/mod.rs @@ -0,0 +1,7 @@ +pub mod find_client; +pub mod find_retri_sp; +pub mod find_retri_sp_client; +pub mod find_url_sp; +pub mod find_url_sp_client; +pub mod rate_limiting; +pub mod url_discovery_service; diff --git a/url_finder/tests/integration_tests/rate_limiting.rs b/url_finder/tests/integration_tests/rate_limiting.rs new file mode 100644 index 0000000..c28a5cb --- /dev/null +++ b/url_finder/tests/integration_tests/rate_limiting.rs @@ -0,0 +1,31 @@ +use crate::common::*; +use axum::http::StatusCode; +use serde_json::json; + +// Must match burst_size in routes.rs GovernorConfigBuilder +const BURST_LIMIT: usize = 30; + +#[tokio::test] +async fn test_rate_limit_429_after_burst_exceeded() { + let ctx = TestContext::new().await; + + // burst limit - all should succeed + for i in 1..=BURST_LIMIT { + let response = ctx.app.get("/healthcheck").await; + assert_eq!( + response.status_code(), + StatusCode::OK, + "Request {} should succeed within burst limit", + i + ); + } + + // should be rate limited + let response = ctx.app.get("/healthcheck").await; + + assert_json_response( + response, + StatusCode::TOO_MANY_REQUESTS, + json!({"error": "Rate limit exceeded"}), + ); +} diff --git a/url_finder/tests/integration_tests/url_discovery_service.rs b/url_finder/tests/integration_tests/url_discovery_service.rs new file mode 100644 index 0000000..548ad0c --- /dev/null +++ b/url_finder/tests/integration_tests/url_discovery_service.rs @@ -0,0 +1,102 @@ +use crate::common::*; +use url_finder::{ + config::Config, + repository::DealRepository, + services::url_discovery_service::discover_url, + types::{ClientAddress, ProviderAddress, ResultCode}, +}; + +fn setup_discovery_params( + ctx: &TestContext, + fixture: &ProviderFixture, +) -> (ProviderAddress, ClientAddress, DealRepository, Config) { + let provider_address = fixture.provider_address.clone(); + let client_address = test_client_address(); + let deal_repo = DealRepository::new(ctx.dbs.app_pool.clone()); + let lotus_url = ctx.mocks.lotus_url(); + let lotus_base = lotus_url.trim_end_matches('/'); + let config = Config::new_for_test(format!("{lotus_base}/rpc/v1"), ctx.mocks.cid_contact_url()); + (provider_address, client_address, deal_repo, config) +} + +#[tokio::test] +async fn test_url_discovery_success() { + let ctx = TestContext::new().await; + + let piece_cid = TEST_PIECE_CID; + ctx.mocks.setup_piece_retrieval_mock(piece_cid, true).await; + + let fixture = ctx + .setup_provider_with_deals_and_mock_server( + TEST_PROVIDER_1_DB, + Some(TEST_CLIENT_ID_DB), + vec![piece_cid], + 1.0, + ) + .await; + + let (provider_address, client_address, deal_repo, config) = + setup_discovery_params(&ctx, &fixture); + + let result = discover_url(&config, &provider_address, Some(client_address), &deal_repo).await; + + assert_eq!(result.result_code, ResultCode::Success, "Expected Success"); + + let url = result + .working_url + .as_ref() + .expect("Expected working URL to be present"); + assert!( + url.contains(&ctx.mocks.piece_server_url()), + "URL should contain mock server address" + ); + assert!(url.contains(piece_cid), "URL should contain piece CID"); + + assert_eq!( + result.retrievability_percent, 100.0, + "Should have 100% retrievability" + ); +} + +#[tokio::test] +async fn test_url_discovery_partial_retrievability() { + let ctx = TestContext::new().await; + + let fixture = ctx + .setup_provider_with_deals_and_mock_server( + TEST_PROVIDER_2_DB, + Some(TEST_CLIENT_ID_DB), + vec![TEST_PIECE_CID, TEST_PIECE_CID_2], + 0.5, + ) + .await; + + let (provider_address, client_address, deal_repo, config) = + setup_discovery_params(&ctx, &fixture); + + let result = discover_url(&config, &provider_address, Some(client_address), &deal_repo).await; + + assert_eq!( + result.result_code, + ResultCode::Success, + "Should succeed with partial retrievability" + ); + + let url = result + .working_url + .as_ref() + .expect("Expected working URL to be present"); + assert!( + url.contains(&ctx.mocks.piece_server_url()), + "URL should contain mock server address" + ); + assert!( + url.contains(TEST_PIECE_CID) || url.contains(TEST_PIECE_CID_2), + "URL should contain one of the piece CIDs" + ); + + assert_eq!( + result.retrievability_percent, 50.0, + "Should have 50% retrievability (1 of 2 pieces)" + ); +}