diff --git a/.gitignore b/.gitignore index fa54942..c82589e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ /config.toml Cargo.lock dreamhost-dns.code-workspace -build-bins.bat \ No newline at end of file +build-bins.bat +settings.json \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 59a482a..8b365e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -67,6 +67,195 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "ascii-canvas" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6" +dependencies = [ + "term", +] + +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "async-attributes" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener 2.5.3", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" +dependencies = [ + "async-channel 2.5.0", + "async-executor", + "async-io", + "async-lock", + "blocking", + "futures-lite", + "once_cell", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener 5.4.1", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-object-pool" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "333c456b97c3f2d50604e8b2624253b7f787208cb72eb75e64b0ad11b221652c" +dependencies = [ + "async-std", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel 2.5.0", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener 5.4.1", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-signal" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-std" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b" +dependencies = [ + "async-attributes", + "async-channel 1.9.0", + "async-global-executor", + "async-io", + "async-lock", + "async-process", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "async-trait" version = "0.1.89" @@ -75,15 +264,53 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + [[package]] name = "base64" version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "basic-cookies" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67bd8fd42c16bdb08688243dc5f0cc117a3ca9efeeaba3a345a18a6159ad96f7" +dependencies = [ + "lalrpop", + "lalrpop-util", + "regex", +] + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitflags" version = "1.3.2" @@ -96,6 +323,19 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel 2.5.0", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "bumpalo" version = "3.20.2" @@ -155,7 +395,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -170,6 +410,15 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -186,12 +435,45 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "data-encoding" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -200,7 +482,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -217,6 +499,7 @@ dependencies = [ "clap", "dotenvy", "env_logger", + "httpmock", "log", "rand", "reqwest", @@ -226,6 +509,21 @@ dependencies = [ "trust-dns-resolver", ] +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "ena" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabffdaee24bd1bf95c5ef7cec31260444317e72ea56c4c91750e8b7ee58d5f1" +dependencies = [ + "log", +] + [[package]] name = "encoding_rs" version = "0.8.35" @@ -244,7 +542,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -276,12 +574,61 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener 5.4.1", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "fnv" version = "1.0.7" @@ -318,6 +665,30 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "futures-sink" version = "0.3.32" @@ -338,6 +709,7 @@ checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", "futures-io", + "futures-macro", "futures-task", "memchr", "pin-project-lite", @@ -355,6 +727,18 @@ dependencies = [ "wasi", ] +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "h2" version = "0.3.27" @@ -386,6 +770,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "http" version = "0.2.12" @@ -420,6 +810,34 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "httpmock" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08ec9586ee0910472dec1a1f0f8acf52f0fdde93aea74d70d4a3107b4be0fd5b" +dependencies = [ + "assert-json-diff", + "async-object-pool", + "async-std", + "async-trait", + "base64", + "basic-cookies", + "crossbeam-utils", + "form_urlencoded", + "futures-util", + "hyper", + "lazy_static", + "levenshtein", + "log", + "regex", + "serde", + "serde_json", + "serde_regex", + "similar", + "tokio", + "url", +] + [[package]] name = "hyper" version = "0.14.32" @@ -604,6 +1022,15 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.17" @@ -631,7 +1058,7 @@ checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -644,18 +1071,85 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + +[[package]] +name = "lalrpop" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cb077ad656299f160924eb2912aa147d7339ea7d69e1b5517326fdcec3c1ca" +dependencies = [ + "ascii-canvas", + "bit-set", + "ena", + "itertools", + "lalrpop-util", + "petgraph", + "pico-args", + "regex", + "regex-syntax", + "string_cache", + "term", + "tiny-keccak", + "unicode-xid", + "walkdir", +] + +[[package]] +name = "lalrpop-util" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507460a910eb7b32ee961886ff48539633b788a36b65692b95f225b844c82553" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "levenshtein" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760" + [[package]] name = "libc" version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +[[package]] +name = "libredox" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +dependencies = [ + "libc", +] + [[package]] name = "linked-hash-map" version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "litemap" version = "0.8.1" @@ -676,6 +1170,9 @@ name = "log" version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +dependencies = [ + "value-bag", +] [[package]] name = "lru-cache" @@ -709,6 +1206,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + [[package]] name = "once_cell" version = "1.21.3" @@ -721,6 +1224,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -750,12 +1259,68 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + [[package]] name = "pin-project-lite" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "portable-atomic" version = "1.13.1" @@ -789,6 +1354,12 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "proc-macro2" version = "1.0.106" @@ -846,6 +1417,17 @@ dependencies = [ "bitflags 2.11.0", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + [[package]] name = "regex" version = "1.12.3" @@ -936,6 +1518,19 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + [[package]] name = "rustls" version = "0.21.12" @@ -979,6 +1574,15 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -1022,7 +1626,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1038,6 +1642,16 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_regex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8136f1a4ea815d7eac4101cfd0b16dc0cb5e1fe1b8609dfd728058656b7badf" +dependencies = [ + "regex", + "serde", +] + [[package]] name = "serde_spanned" version = "0.6.9" @@ -1065,6 +1679,28 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + [[package]] name = "slab" version = "0.4.12" @@ -1103,12 +1739,35 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.117" @@ -1134,7 +1793,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1158,6 +1817,17 @@ dependencies = [ "libc", ] +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -1175,7 +1845,16 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", ] [[package]] @@ -1213,10 +1892,23 @@ dependencies = [ "libc", "mio", "pin-project-lite", + "signal-hook-registry", "socket2 0.6.3", + "tokio-macros", "windows-sys 0.61.2", ] +[[package]] +name = "tokio-macros" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "tokio-rustls" version = "0.24.1" @@ -1306,7 +1998,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1391,6 +2083,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "untrusted" version = "0.9.0" @@ -1421,6 +2119,22 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "value-bag" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -1482,7 +2196,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wasm-bindgen-shared", ] @@ -1517,6 +2231,37 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-link" version = "0.2.1" @@ -1715,7 +2460,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -1736,7 +2481,7 @@ checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1756,7 +2501,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -1790,7 +2535,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index d221a1d..4926af8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dreamhost-ddns" -version = "1.1.0" +version = "1.1.1" edition = "2021" [dependencies] @@ -19,4 +19,7 @@ rand = "0.8" [profile.release] lto = true strip = true -codegen-units = 1 \ No newline at end of file +codegen-units = 1 + +[dev-dependencies] +httpmock = "0.7" \ No newline at end of file diff --git a/binaries/linux-aarch64/dreamhost-ddns b/binaries/linux-aarch64/dreamhost-ddns index 0039c0f..2343f0c 100644 Binary files a/binaries/linux-aarch64/dreamhost-ddns and b/binaries/linux-aarch64/dreamhost-ddns differ diff --git a/binaries/linux-rpi-armv7/dreamhost-ddns b/binaries/linux-rpi-armv7/dreamhost-ddns index 8933f52..1ab511a 100644 Binary files a/binaries/linux-rpi-armv7/dreamhost-ddns and b/binaries/linux-rpi-armv7/dreamhost-ddns differ diff --git a/binaries/linux-x86_64/dreamhost-ddns b/binaries/linux-x86_64/dreamhost-ddns index c2c6a57..4fa21cf 100644 Binary files a/binaries/linux-x86_64/dreamhost-ddns and b/binaries/linux-x86_64/dreamhost-ddns differ diff --git a/binaries/windows/dreamhost-ddns.exe b/binaries/windows/dreamhost-ddns.exe index e42378d..f686977 100644 Binary files a/binaries/windows/dreamhost-ddns.exe and b/binaries/windows/dreamhost-ddns.exe differ diff --git a/src/main.rs b/src/main.rs index 856b466..e462ba2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,16 @@ use anyhow::{anyhow, Result}; use clap::{Parser, ValueEnum}; -use log::{info, warn, debug, trace}; +use log::{debug, info, trace, warn}; use rand::seq::SliceRandom; use reqwest::blocking::Client; use serde::Deserialize; use std::net::IpAddr; use std::sync::mpsc; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, Mutex, +}; use std::thread; -use std::sync::{Arc, atomic::{AtomicBool, Ordering}, Mutex}; #[derive(Parser)] #[command( @@ -67,6 +70,7 @@ struct Config { struct DreamhostClient { client: Client, api_key: String, + base_url: String, record_cache: Mutex>>, } @@ -74,8 +78,8 @@ impl From for log::LevelFilter { fn from(level: LogLevel) -> Self { match level { LogLevel::Error => log::LevelFilter::Error, - LogLevel::Warn => log::LevelFilter::Warn, - LogLevel::Info => log::LevelFilter::Info, + LogLevel::Warn => log::LevelFilter::Warn, + LogLevel::Info => log::LevelFilter::Info, LogLevel::Debug => log::LevelFilter::Debug, LogLevel::Trace => log::LevelFilter::Trace, } @@ -83,47 +87,53 @@ impl From for log::LevelFilter { } impl DreamhostClient { - pub fn new(client: Client, api_key: String) -> Self { Self { client, api_key, + base_url: "https://api.dreamhost.com/".to_string(), record_cache: Mutex::new(None), } } - fn call(&self, params: &[(&str, &str)]) -> Result { + #[cfg(test)] + pub fn new_with_base(client: Client, api_key: String, base_url: String) -> Self { + Self { + client, + api_key, + base_url, + record_cache: Mutex::new(None), + } + } - let mut query = vec![ - ("key", self.api_key.as_str()), - ("format", "json"), - ]; + fn call(&self, params: &[(&str, &str)]) -> Result { + let mut query = vec![("key", self.api_key.as_str()), ("format", "json")]; query.extend_from_slice(params); - let mut request = self.client - .get("https://api.dreamhost.com/") - .query(&query) - .build()?; + let mut request = self.client.get(&self.base_url).query(&query).build()?; // ensure user-agent is visible in trace logs if !request.headers().contains_key(reqwest::header::USER_AGENT) { request.headers_mut().insert( reqwest::header::USER_AGENT, - reqwest::header::HeaderValue::from_str( - &format!("dreamhost-ddns/{}", env!("CARGO_PKG_VERSION")) - )?, + reqwest::header::HeaderValue::from_str(&format!( + "dreamhost-ddns/{}", + env!("CARGO_PKG_VERSION") + ))?, ); } // ---- TRACE REQUEST LOGGING ---- if log::log_enabled!(log::Level::Trace) { - let mut url = request.url().to_string(); // mask API key if let Some(start) = url.find("key=") { - let end = url[start..].find('&').map(|i| start + i).unwrap_or(url.len()); + let end = url[start..] + .find('&') + .map(|i| start + i) + .unwrap_or(url.len()); url.replace_range(start + 4..end, "***"); } @@ -143,7 +153,6 @@ impl DreamhostClient { // ---- TRACE RESPONSE LOGGING ---- if log::log_enabled!(log::Level::Trace) { - trace!("HTTP Status: {}", response.status()); for (name, value) in response.headers() { @@ -159,14 +168,13 @@ impl DreamhostClient { // ---- DREAMHOST API ERROR HANDLING ---- if resp["result"] != "success" { + let reason = resp["reason"].as_str(); + let data = resp["data"].as_str(); - let reason = resp["reason"] - .as_str() - .unwrap_or("Unknown DreamHost API error"); + let message = reason.or(data).unwrap_or("Unknown DreamHost API error"); - return Err(anyhow!("DreamHost API error: {}", reason)); + return Err(anyhow!("DreamHost API error: {}", message)); } - Ok(resp) } @@ -178,13 +186,17 @@ impl DreamhostClient { } let resp = self.call(&[("cmd", "dns-list_records")])?; - + // Ensure we have a JSON array for data; else, treat as error let records: Vec = match &resp["data"] { - serde_json::Value::Array(arr) => serde_json::from_value(serde_json::Value::Array(arr.clone()))?, + serde_json::Value::Array(arr) => { + serde_json::from_value(serde_json::Value::Array(arr.clone()))? + } _ => { // "data" might be a string like "slow_down_bucko" - let reason = resp["reason"].as_str().unwrap_or("Unknown DreamHost API error"); + let reason = resp["reason"] + .as_str() + .unwrap_or("Unknown DreamHost API error"); return Err(anyhow!("DreamHost API error: {}", reason)); } }; @@ -201,12 +213,20 @@ impl DreamhostClient { .into_iter() .find(|r| r.record == record_name && r.record_type == record_type) .map(|r| r.value) - .ok_or_else(|| anyhow!("DreamHost error: {} record '{}' not found", record_type, record_name)) + .ok_or_else(|| { + anyhow!( + "DreamHost error: {} record '{}' not found", + record_type, + record_name + ) + }) } fn record_exists(&self, record_name: &str, ip: &str, record_type: &str) -> Result { let records = self.list_records()?; // uses cache if available - Ok(records.iter().any(|r| r.record == record_name && r.record_type == record_type && r.value == ip)) + Ok(records + .iter() + .any(|r| r.record == record_name && r.record_type == record_type && r.value == ip)) } fn invalidate_cache(&self) { @@ -219,10 +239,12 @@ impl DreamhostClient { record: &str, old_ip: &str, new_ip: &str, - record_type: &str + record_type: &str, ) -> Result<()> { - - info!("Adding new {} DNS record {} -> {}", record_type, record, new_ip); + info!( + "Adding new {} DNS record {} -> {}", + record_type, record, new_ip + ); self.call(&[ ("cmd", "dns-add_record"), ("record", record), @@ -240,7 +262,10 @@ impl DreamhostClient { info!("New {} record verified", record_type); break; } - warn!("New {} record not visible yet (attempt {})", record_type, attempt); + warn!( + "New {} record not visible yet (attempt {})", + record_type, attempt + ); std::thread::sleep(std::time::Duration::from_secs(2)); if attempt == 5 { @@ -251,7 +276,10 @@ impl DreamhostClient { } } - info!("Removing old {} DNS record {} -> {}", record_type, record, old_ip); + info!( + "Removing old {} DNS record {} -> {}", + record_type, record, old_ip + ); self.call(&[ ("cmd", "dns-remove_record"), ("record", record), @@ -272,7 +300,6 @@ fn check_and_update( record_type: &str, dry_run: bool, ) -> Result<()> { - match dh.get_dns_ip(record, record_type) { Ok(current_ip) => { // Existing record exists, update if necessary @@ -286,7 +313,10 @@ fn check_and_update( warn!("{} record mismatch detected", record_type); if dry_run { - info!("DRY RUN: Would update {} record {} -> {}", record_type, current_ip, detected_ip); + info!( + "DRY RUN: Would update {} record {} -> {}", + record_type, current_ip, detected_ip + ); return Ok(()); } @@ -301,7 +331,10 @@ fn check_and_update( warn!("{} record does not exist, creating new one", record_type); if dry_run { - info!("DRY RUN: Would create {} record -> {}", record_type, detected_ip); + info!( + "DRY RUN: Would create {} record -> {}", + record_type, detected_ip + ); return Ok(()); } @@ -363,14 +396,19 @@ fn main() -> Result<()> { impl DetectionJob { fn run(self, dh: &Arc) -> Result<()> { - // helper to safely remove a DNS record if it exists let remove_stale_record = |record_type: &str, record_name: &str| -> Result<()> { match dh.get_dns_ip(record_name, record_type) { Ok(existing_ip) => { - warn!("{} record exists but no WAN IP detected: {}", record_type, existing_ip); + warn!( + "{} record exists but no WAN IP detected: {}", + record_type, existing_ip + ); if self.dry_run { - info!("DRY RUN: Would remove stale {} record {}", record_type, existing_ip); + info!( + "DRY RUN: Would remove stale {} record {}", + record_type, existing_ip + ); } else { dh.call(&[ ("cmd", "dns-remove_record"), @@ -388,7 +426,11 @@ fn main() -> Result<()> { match detect_ip(&self.client, self.services, self.require_ipv4) { Ok(ip) => { - let ipv = if self.record_type=="A" { "IPV4" } else { "IPV6" }; + let ipv = if self.record_type == "A" { + "IPV4" + } else { + "IPV6" + }; info!("Detected {} WAN: {}", ipv, ip); check_and_update(dh, &self.record_name, ip, self.record_type, self.dry_run)?; } @@ -433,7 +475,9 @@ fn main() -> Result<()> { } if jobs.is_empty() { - return Err(anyhow!("Both --ipv4-only and --ipv6-only flags cannot be used together; nothing to do")); + return Err(anyhow!( + "Both --ipv4-only and --ipv6-only flags cannot be used together; nothing to do" + )); } let handles: Vec<_> = jobs @@ -452,9 +496,7 @@ fn main() -> Result<()> { Ok(()) } - fn resolve_config(args: &Args) -> Result { - let mut api_key = args.api_key.clone(); let mut record = args.record.clone(); @@ -466,21 +508,21 @@ fn resolve_config(args: &Args) -> Result { record = std::env::var("DNS_RECORD").ok(); } - if (api_key.is_none() || record.is_none()) && args.config.is_some() { + if api_key.is_none() || record.is_none() { + if let Some(config_path) = &args.config { + let cfg = load_config(config_path)?; - let cfg = load_config(args.config.as_ref().unwrap())?; - - if api_key.is_none() { - api_key = Some(cfg.dreamhost_api_key); - } + if api_key.is_none() { + api_key = Some(cfg.dreamhost_api_key); + } - if record.is_none() { - record = Some(cfg.dns_record); + if record.is_none() { + record = Some(cfg.dns_record); + } } } if (api_key.is_none() || record.is_none()) && std::path::Path::new("config.toml").exists() { - let cfg = load_config("config.toml")?; if api_key.is_none() { @@ -507,7 +549,6 @@ fn load_config(path: &str) -> Result { } fn detect_ip(client: &Client, services: Vec<&str>, require_ipv4: bool) -> Result { - let mut services = services; services.shuffle(&mut rand::thread_rng()); @@ -515,14 +556,12 @@ fn detect_ip(client: &Client, services: Vec<&str>, require_ipv4: bool) -> Result let cancel = Arc::new(AtomicBool::new(false)); for url in services { - let tx = tx.clone(); let client = client.clone(); let cancel = cancel.clone(); let url = url.to_string(); thread::spawn(move || { - if cancel.load(Ordering::Relaxed) { return; } @@ -535,7 +574,6 @@ fn detect_ip(client: &Client, services: Vec<&str>, require_ipv4: bool) -> Result .and_then(|text| text.trim().parse::().ok()); if let Some(ip) = result { - if require_ipv4 && !ip.is_ipv4() { return; } @@ -563,7 +601,6 @@ fn detect_ip(client: &Client, services: Vec<&str>, require_ipv4: bool) -> Result } fn ipv4_services() -> Vec<&'static str> { - vec![ "https://icanhazip.com", "https://api.ipify.org", @@ -574,7 +611,6 @@ fn ipv4_services() -> Vec<&'static str> { } fn ipv6_services() -> Vec<&'static str> { - vec![ "https://api64.ipify.org", "https://ipv6.icanhazip.com", @@ -582,4 +618,136 @@ fn ipv6_services() -> Vec<&'static str> { "https://api-ipv6.ip.sb/ip", "https://ifconfig.co/ip", ] -} \ No newline at end of file +} + +#[cfg(test)] +mod tests { + use super::*; + use httpmock::Method::GET; + use httpmock::MockServer; + use std::net::IpAddr; + + #[test] + fn parses_ipv4_address() { + let ip: std::net::IpAddr = "8.8.8.8".parse().unwrap(); + assert!(ip.is_ipv4()); + } + + #[test] + fn parses_ipv6_address() { + let ip: IpAddr = "2001:db8::1".parse().unwrap(); + assert!(ip.is_ipv6()); + } + + #[test] + fn ipv6_is_not_ipv4() { + let ip: IpAddr = "2001:db8::1".parse().unwrap(); + assert!(!ip.is_ipv4()); + } + + #[test] + fn api_error_uses_data_when_reason_missing() { + let resp = serde_json::json!({ + "result": "error", + "data": "no_such_zone" + }); + + let msg = resp["reason"] + .as_str() + .or(resp["data"].as_str()) + .unwrap_or("Unknown DreamHost API error"); + + assert_eq!(msg, "no_such_zone"); + } + + #[test] + fn no_update_when_ips_match() { + let existing = "1.2.3.4".parse::().unwrap(); + let detected = "1.2.3.4".parse::().unwrap(); + + assert_eq!(existing, detected); + } + + #[test] + fn test_dns_list_records_success() { + let server = MockServer::start(); + + let mock = server.mock(|when, then| { + when.method(GET); + + then.status(200) + .header("content-type", "application/json") + .body( + r#"{ + "result":"success", + "data":[ + { + "record":"test.example.com", + "type":"A", + "value":"1.2.3.4" + } + ] + }"#, + ); + }); + + let client = reqwest::blocking::Client::new(); + + let dh = DreamhostClient::new_with_base(client, "fake_key".into(), server.url("/")); + + let records = dh.list_records().unwrap(); + + assert_eq!(records.len(), 1); + assert_eq!(records[0].value, "1.2.3.4"); + + mock.assert(); + } + + #[test] + fn test_rate_limit_error() { + let server = MockServer::start(); + + server.mock(|when, then| { + when.method(GET); + + then.status(200).body( + r#"{ + "result":"error", + "data":"slow_down_bucko" + }"#, + ); + }); + + let client = reqwest::blocking::Client::new(); + + let dh = DreamhostClient::new_with_base(client, "fake_key".into(), server.url("/")); + + let result = dh.list_records(); + + assert!(result.is_err()); + } + + #[test] + fn test_no_such_zone_error() { + let server = MockServer::start(); + + server.mock(|when, then| { + when.method(GET); + + then.status(200).body( + r#"{ + "result":"error", + "data":"no_such_zone" + }"#, + ); + }); + + let client = reqwest::blocking::Client::new(); + + let dh = DreamhostClient::new_with_base(client, "fake_key".into(), server.url("/")); + + let result = dh.list_records(); + + assert!(result.is_err()); + } +}