Email client, application startup logic and tests
This commit is contained in:
431
Cargo.lock
generated
431
Cargo.lock
generated
@@ -66,6 +66,16 @@ version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
|
||||
|
||||
[[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-trait"
|
||||
version = "0.1.89"
|
||||
@@ -236,6 +246,12 @@ version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.41"
|
||||
@@ -318,16 +334,6 @@ dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
@@ -424,6 +430,24 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deadpool"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb84100978c1c7b37f09ed3ce3e5f843af02c2a2c431bae5b19230dad2c1b490"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"deadpool-runtime",
|
||||
"num_cpus",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deadpool-runtime"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b"
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.7.10"
|
||||
@@ -532,16 +556,6 @@ dependencies = [
|
||||
"typeid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "etcetera"
|
||||
version = "0.8.0"
|
||||
@@ -575,12 +589,6 @@ dependencies = [
|
||||
"rand 0.9.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "flume"
|
||||
version = "0.11.1"
|
||||
@@ -604,21 +612,6 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
|
||||
dependencies = [
|
||||
"foreign-types-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.1"
|
||||
@@ -628,6 +621,21 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.31"
|
||||
@@ -672,6 +680,17 @@ version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.31"
|
||||
@@ -690,8 +709,10 @@ version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
@@ -727,8 +748,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -738,9 +761,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasi 0.14.2+wasi-0.2.4",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -800,6 +825,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 = "hex"
|
||||
version = "0.4.3"
|
||||
@@ -916,22 +947,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"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",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tower-service",
|
||||
"webpki-roots 1.0.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -952,12 +968,10 @@ dependencies = [
|
||||
"libc",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"system-configuration",
|
||||
"socket2 0.6.0",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1203,12 +1217,6 @@ dependencies = [
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
version = "0.8.0"
|
||||
@@ -1231,6 +1239,12 @@ version = "0.4.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
||||
|
||||
[[package]]
|
||||
name = "lru-slab"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.1.0"
|
||||
@@ -1288,23 +1302,6 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"openssl",
|
||||
"openssl-probe",
|
||||
"openssl-sys",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.46.0"
|
||||
@@ -1368,6 +1365,16 @@ dependencies = [
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_cpus"
|
||||
version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.36.7"
|
||||
@@ -1383,50 +1390,6 @@ version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.73"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"openssl-macros",
|
||||
"openssl-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.109"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ordered-float"
|
||||
version = "2.10.1"
|
||||
@@ -1662,6 +1625,61 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"cfg_aliases",
|
||||
"pin-project-lite",
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2 0.5.10",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"getrandom 0.3.3",
|
||||
"lru-slab",
|
||||
"rand 0.9.2",
|
||||
"ring",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-udp"
|
||||
version = "0.5.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970"
|
||||
dependencies = [
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2 0.5.10",
|
||||
"tracing",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.40"
|
||||
@@ -1797,29 +1815,26 @@ checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-tls",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"native-tls",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tower-service",
|
||||
@@ -1827,6 +1842,7 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"webpki-roots 1.0.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1893,17 +1909,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.0.8"
|
||||
name = "rustc-hash"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
@@ -1925,6 +1934,7 @@ version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
|
||||
dependencies = [
|
||||
"web-time",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@@ -1951,15 +1961,6 @@ version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
@@ -1976,29 +1977,6 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"core-foundation",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework-sys"
|
||||
version = "2.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.219"
|
||||
@@ -2157,6 +2135,16 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.0"
|
||||
@@ -2444,40 +2432,6 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"core-foundation",
|
||||
"system-configuration-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration-sys"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.3.3",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.16"
|
||||
@@ -2585,7 +2539,7 @@ dependencies = [
|
||||
"mio",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
"socket2",
|
||||
"socket2 0.6.0",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
@@ -2601,16 +2555,6 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-native-tls"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
|
||||
dependencies = [
|
||||
"native-tls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.2"
|
||||
@@ -3070,6 +3014,16 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-time"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.26.11"
|
||||
@@ -3161,17 +3115,6 @@ version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.3.4"
|
||||
@@ -3347,6 +3290,30 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wiremock"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2b8b99d4cdbf36b239a9532e31fe4fb8acc38d1897c1761e161550a7dc78e6a"
|
||||
dependencies = [
|
||||
"assert-json-diff",
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
"deadpool",
|
||||
"futures",
|
||||
"http",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"log",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rt"
|
||||
version = "0.39.0"
|
||||
@@ -3413,6 +3380,7 @@ dependencies = [
|
||||
"secrecy",
|
||||
"serde",
|
||||
"serde-aux",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
"tokio",
|
||||
"tower-http",
|
||||
@@ -3422,6 +3390,7 @@ dependencies = [
|
||||
"unicode-segmentation",
|
||||
"uuid",
|
||||
"validator",
|
||||
"wiremock",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -14,6 +14,7 @@ name = "zero2prod"
|
||||
axum = "0.8.4"
|
||||
chrono = { version = "0.4.41", default-features = false, features = ["clock"] }
|
||||
config = "0.15.14"
|
||||
reqwest = { version = "0.12.23", default-features = false, features = ["rustls-tls", "json"] }
|
||||
secrecy = { version = "0.10.3", features = ["serde"] }
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde-aux = "4.7.0"
|
||||
@@ -33,4 +34,5 @@ fake = "4.4.0"
|
||||
once_cell = "1.21.3"
|
||||
quickcheck = "1.0.3"
|
||||
quickcheck_macros = "1.1.0"
|
||||
reqwest = "0.12.23"
|
||||
serde_json = "1.0.143"
|
||||
wiremock = "0.6.4"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::domain::SubscriberEmail;
|
||||
use secrecy::{ExposeSecret, SecretString};
|
||||
use serde::Deserialize;
|
||||
use serde_aux::field_attributes::deserialize_number_from_string;
|
||||
@@ -58,6 +59,7 @@ impl TryFrom<String> for Environment {
|
||||
pub struct Settings {
|
||||
pub application: ApplicationSettings,
|
||||
pub database: DatabaseSettings,
|
||||
pub email_client: EmailClientSettings,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -67,6 +69,35 @@ pub struct ApplicationSettings {
|
||||
pub host: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct EmailClientSettings {
|
||||
pub base_url: String,
|
||||
sender_email: String,
|
||||
pub authorization_token: SecretString,
|
||||
pub timeout_milliseconds: u64,
|
||||
}
|
||||
|
||||
impl EmailClientSettings {
|
||||
pub fn sender(&self) -> Result<SubscriberEmail, String> {
|
||||
SubscriberEmail::parse(self.sender_email.clone())
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
base_url: String,
|
||||
sender_email: String,
|
||||
authorization_token: String,
|
||||
timeout_milliseconds: u64,
|
||||
) -> Self {
|
||||
let authorization_token = SecretString::from(authorization_token);
|
||||
Self {
|
||||
base_url,
|
||||
sender_email,
|
||||
authorization_token,
|
||||
timeout_milliseconds,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct DatabaseSettings {
|
||||
pub username: String,
|
||||
|
||||
199
src/email_client.rs
Normal file
199
src/email_client.rs
Normal file
@@ -0,0 +1,199 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use reqwest::Client;
|
||||
use secrecy::{ExposeSecret, SecretString};
|
||||
|
||||
use crate::{configuration::EmailClientSettings, domain::SubscriberEmail};
|
||||
|
||||
pub struct EmailClient {
|
||||
http_client: Client,
|
||||
base_url: reqwest::Url,
|
||||
sender: SubscriberEmail,
|
||||
authorization_token: SecretString,
|
||||
}
|
||||
|
||||
impl EmailClient {
|
||||
pub fn new(config: EmailClientSettings) -> Self {
|
||||
Self {
|
||||
http_client: Client::builder()
|
||||
.timeout(Duration::from_millis(config.timeout_milliseconds))
|
||||
.build()
|
||||
.unwrap(),
|
||||
base_url: reqwest::Url::parse(&config.base_url).unwrap(),
|
||||
sender: config.sender().unwrap(),
|
||||
authorization_token: config.authorization_token,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_email(
|
||||
&self,
|
||||
recipient: &SubscriberEmail,
|
||||
subject: &str,
|
||||
html_content: &str,
|
||||
text_content: &str,
|
||||
) -> Result<(), reqwest::Error> {
|
||||
let url = self.base_url.join("v1/email").unwrap();
|
||||
let request_body = SendEmailRequest {
|
||||
from: self.sender.as_ref(),
|
||||
to: vec![recipient.as_ref()],
|
||||
subject,
|
||||
text: text_content,
|
||||
html: html_content,
|
||||
};
|
||||
|
||||
self.http_client
|
||||
.post(url)
|
||||
.header("X-Requested-With", "XMLHttpRequest")
|
||||
.header(
|
||||
"Authorization",
|
||||
format!("Bearer {}", self.authorization_token.expose_secret()),
|
||||
)
|
||||
.json(&request_body)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct SendEmailRequest<'a> {
|
||||
from: &'a str,
|
||||
to: Vec<&'a str>,
|
||||
subject: &'a str,
|
||||
text: &'a str,
|
||||
html: &'a str,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::{
|
||||
configuration::EmailClientSettings, domain::SubscriberEmail, email_client::EmailClient,
|
||||
};
|
||||
use claims::{assert_err, assert_ok};
|
||||
use fake::{
|
||||
Fake, Faker,
|
||||
faker::{
|
||||
internet::en::SafeEmail,
|
||||
lorem::en::{Paragraph, Sentence},
|
||||
},
|
||||
};
|
||||
use wiremock::{
|
||||
Mock, MockServer, ResponseTemplate,
|
||||
matchers::{any, header, header_exists, method, path},
|
||||
};
|
||||
|
||||
struct SendEmailBodyMatcher;
|
||||
|
||||
impl wiremock::Match for SendEmailBodyMatcher {
|
||||
fn matches(&self, request: &wiremock::Request) -> bool {
|
||||
let result: Result<serde_json::Value, _> = serde_json::from_slice(&request.body);
|
||||
if let Ok(body) = result {
|
||||
body.get("from").is_some()
|
||||
&& body.get("to").is_some()
|
||||
&& body.get("subject").is_some()
|
||||
&& body.get("html").is_some()
|
||||
&& body.get("text").is_some()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn subject() -> String {
|
||||
Sentence(1..2).fake()
|
||||
}
|
||||
|
||||
fn content() -> String {
|
||||
Paragraph(1..10).fake()
|
||||
}
|
||||
|
||||
fn email() -> SubscriberEmail {
|
||||
SubscriberEmail::parse(SafeEmail().fake()).unwrap()
|
||||
}
|
||||
|
||||
fn email_client(base_url: String) -> EmailClient {
|
||||
let sender_email = SafeEmail().fake();
|
||||
let token: String = Faker.fake();
|
||||
let settings = EmailClientSettings::new(base_url, sender_email, token, 200);
|
||||
EmailClient::new(settings)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn send_email_sends_the_expected_request() {
|
||||
let mock_server = MockServer::start().await;
|
||||
let email_client = email_client(mock_server.uri());
|
||||
|
||||
Mock::given(header_exists("Authorization"))
|
||||
.and(header("Content-Type", "application/json"))
|
||||
.and(header("X-Requested-With", "XMLHttpRequest"))
|
||||
.and(path("v1/email"))
|
||||
.and(method("POST"))
|
||||
.and(SendEmailBodyMatcher)
|
||||
.respond_with(ResponseTemplate::new(200))
|
||||
.expect(1)
|
||||
.mount(&mock_server)
|
||||
.await;
|
||||
|
||||
email_client
|
||||
.send_email(&email(), &subject(), &content(), &content())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn send_email_succeeds_if_the_server_returns_200() {
|
||||
let mock_server = MockServer::start().await;
|
||||
let email_client = email_client(mock_server.uri());
|
||||
|
||||
Mock::given(any())
|
||||
.respond_with(ResponseTemplate::new(200))
|
||||
.expect(1)
|
||||
.mount(&mock_server)
|
||||
.await;
|
||||
|
||||
let response = email_client
|
||||
.send_email(&email(), &subject(), &content(), &content())
|
||||
.await;
|
||||
|
||||
assert_ok!(response);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn send_email_fails_if_the_server_retuns_500() {
|
||||
let mock_server = MockServer::start().await;
|
||||
let email_client = email_client(mock_server.uri());
|
||||
|
||||
Mock::given(any())
|
||||
.respond_with(ResponseTemplate::new(500))
|
||||
.expect(1)
|
||||
.mount(&mock_server)
|
||||
.await;
|
||||
|
||||
let response = email_client
|
||||
.send_email(&email(), &subject(), &content(), &content())
|
||||
.await;
|
||||
|
||||
assert_err!(response);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn send_email_times_out_if_the_server_takes_too_long() {
|
||||
let mock_server = MockServer::start().await;
|
||||
let email_client = email_client(mock_server.uri());
|
||||
|
||||
Mock::given(any())
|
||||
.respond_with(ResponseTemplate::new(200).set_delay(Duration::from_secs(180)))
|
||||
.expect(1)
|
||||
.mount(&mock_server)
|
||||
.await;
|
||||
|
||||
let response = email_client
|
||||
.send_email(&email(), &subject(), &content(), &content())
|
||||
.await;
|
||||
|
||||
assert_err!(response);
|
||||
}
|
||||
}
|
||||
@@ -3,3 +3,4 @@ pub mod domain;
|
||||
pub mod routes;
|
||||
pub mod startup;
|
||||
pub mod telemetry;
|
||||
pub mod email_client;
|
||||
|
||||
22
src/main.rs
22
src/main.rs
@@ -1,19 +1,13 @@
|
||||
use sqlx::postgres::PgPoolOptions;
|
||||
use tokio::net::TcpListener;
|
||||
use zero2prod::{configuration::get_configuration, startup::run, telemetry::init_subscriber};
|
||||
use zero2prod::{
|
||||
configuration::get_configuration, startup::Application, telemetry::init_subscriber,
|
||||
};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
async fn main() -> Result<(), std::io::Error> {
|
||||
init_subscriber(std::io::stdout);
|
||||
let configuration = get_configuration().expect("Failed to read configuration");
|
||||
let listener = TcpListener::bind(format!(
|
||||
"{}:{}",
|
||||
configuration.application.host, configuration.application.port
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
tracing::debug!("listening on {}", listener.local_addr().unwrap());
|
||||
let connection_pool = PgPoolOptions::new().connect_lazy_with(configuration.database.with_db());
|
||||
|
||||
run(listener, connection_pool).await
|
||||
let configuration = get_configuration().expect("Failed to read configuration");
|
||||
let application = Application::build(configuration).await?;
|
||||
application.run_until_stopped().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,20 +1,46 @@
|
||||
use crate::routes::*;
|
||||
use crate::{configuration::Settings, email_client::EmailClient, routes::*};
|
||||
use axum::{
|
||||
Router,
|
||||
extract::MatchedPath,
|
||||
http::Request,
|
||||
routing::{get, post},
|
||||
};
|
||||
use sqlx::PgPool;
|
||||
use sqlx::{PgPool, postgres::PgPoolOptions};
|
||||
use std::sync::Arc;
|
||||
use tokio::net::TcpListener;
|
||||
use tower_http::trace::TraceLayer;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub async fn run(listener: TcpListener, connection_pool: PgPool) {
|
||||
axum::serve(listener, app(connection_pool)).await.unwrap();
|
||||
pub struct Application {
|
||||
listener: TcpListener,
|
||||
router: Router,
|
||||
}
|
||||
|
||||
pub fn app(connection_pool: PgPool) -> Router {
|
||||
impl Application {
|
||||
pub async fn build(configuration: Settings) -> Result<Self, std::io::Error> {
|
||||
let address = format!(
|
||||
"{}:{}",
|
||||
configuration.application.host, configuration.application.port
|
||||
);
|
||||
let listener = TcpListener::bind(address).await?;
|
||||
let connection_pool =
|
||||
PgPoolOptions::new().connect_lazy_with(configuration.database.with_db());
|
||||
let email_client = EmailClient::new(configuration.email_client);
|
||||
let router = app(connection_pool, email_client);
|
||||
Ok(Self { listener, router })
|
||||
}
|
||||
|
||||
pub async fn run_until_stopped(self) -> Result<(), std::io::Error> {
|
||||
tracing::debug!("listening on {}", self.listener.local_addr().unwrap());
|
||||
axum::serve(self.listener, self.router).await
|
||||
}
|
||||
|
||||
pub fn address(&self) -> String {
|
||||
self.listener.local_addr().unwrap().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn app(connection_pool: PgPool, email_client: EmailClient) -> Router {
|
||||
Router::new()
|
||||
.route("/health_check", get(health_check))
|
||||
.route("/subscriptions", post(subscribe))
|
||||
@@ -36,4 +62,5 @@ pub fn app(connection_pool: PgPool) -> Router {
|
||||
}),
|
||||
)
|
||||
.with_state(connection_pool)
|
||||
.with_state(Arc::new(email_client))
|
||||
}
|
||||
|
||||
16
tests/api/health_check.rs
Normal file
16
tests/api/health_check.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use crate::helpers::TestApp;
|
||||
|
||||
#[tokio::test]
|
||||
async fn health_check_works() {
|
||||
let app = TestApp::spawn().await;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let response = client
|
||||
.get(format!("http://{}/health_check", app.address))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(response.status().is_success());
|
||||
assert_eq!(Some(0), response.content_length());
|
||||
}
|
||||
74
tests/api/helpers.rs
Normal file
74
tests/api/helpers.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use sqlx::{Connection, Executor, PgConnection, PgPool};
|
||||
use uuid::Uuid;
|
||||
use zero2prod::{
|
||||
configuration::{DatabaseSettings, get_configuration},
|
||||
startup::Application,
|
||||
telemetry::init_subscriber,
|
||||
};
|
||||
|
||||
static TRACING: Lazy<()> = Lazy::new(|| {
|
||||
if std::env::var("TEST_LOG").is_ok() {
|
||||
init_subscriber(std::io::stdout);
|
||||
} else {
|
||||
init_subscriber(std::io::sink);
|
||||
}
|
||||
});
|
||||
|
||||
pub struct TestApp {
|
||||
pub address: String,
|
||||
pub connection_pool: PgPool,
|
||||
}
|
||||
|
||||
impl TestApp {
|
||||
pub async fn spawn() -> Self {
|
||||
Lazy::force(&TRACING);
|
||||
|
||||
let mut configuration = get_configuration().expect("Failed to read configuration");
|
||||
configuration.database.database_name = Uuid::new_v4().to_string();
|
||||
configuration.application.port = 0;
|
||||
let connection_pool = configure_database(&configuration.database).await;
|
||||
let application = Application::build(configuration)
|
||||
.await
|
||||
.expect("Failed to build application");
|
||||
let address = application.address();
|
||||
let app = TestApp {
|
||||
address,
|
||||
connection_pool,
|
||||
};
|
||||
|
||||
tokio::spawn(application.run_until_stopped());
|
||||
|
||||
app
|
||||
}
|
||||
|
||||
pub async fn post_subscriptions(&self, body: String) -> reqwest::Response {
|
||||
reqwest::Client::new()
|
||||
.post(format!("http://{}/subscriptions", self.address))
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.body(body)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request")
|
||||
}
|
||||
}
|
||||
|
||||
async fn configure_database(config: &DatabaseSettings) -> PgPool {
|
||||
let mut connection = PgConnection::connect_with(&config.without_db())
|
||||
.await
|
||||
.expect("Failed to connect to Postgres");
|
||||
connection
|
||||
.execute(format!(r#"CREATE DATABASE "{}";"#, config.database_name).as_ref())
|
||||
.await
|
||||
.expect("Failed to create the database");
|
||||
|
||||
let connection_pool = PgPool::connect_with(config.with_db())
|
||||
.await
|
||||
.expect("Failed to connect to Postgres");
|
||||
sqlx::migrate!("./migrations")
|
||||
.run(&connection_pool)
|
||||
.await
|
||||
.expect("Failed to migrate the database");
|
||||
|
||||
connection_pool
|
||||
}
|
||||
3
tests/api/main.rs
Normal file
3
tests/api/main.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod health_check;
|
||||
mod helpers;
|
||||
mod subscriptions;
|
||||
61
tests/api/subscriptions.rs
Normal file
61
tests/api/subscriptions.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
use crate::helpers::TestApp;
|
||||
|
||||
#[tokio::test]
|
||||
async fn subscribe_returns_a_200_for_valid_form_data() {
|
||||
let app = TestApp::spawn().await;
|
||||
|
||||
let body = "name=alphonse&email=alphonse.paix%40outlook.com";
|
||||
let response = app.post_subscriptions(body.into()).await;
|
||||
|
||||
assert_eq!(200, response.status().as_u16());
|
||||
|
||||
let saved = sqlx::query!("SELECT email, name FROM subscriptions")
|
||||
.fetch_one(&app.connection_pool)
|
||||
.await
|
||||
.expect("Failed to fetch saved subscription");
|
||||
|
||||
assert_eq!(saved.email, "alphonse.paix@outlook.com");
|
||||
assert_eq!(saved.name, "alphonse");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn subscribe_returns_a_422_when_data_is_missing() {
|
||||
let app = TestApp::spawn().await;
|
||||
|
||||
let test_cases = [
|
||||
("name=Alphonse", "missing the email"),
|
||||
("email=alphonse.paix%40outlook.com", "missing the name"),
|
||||
("", "missing both name and email"),
|
||||
];
|
||||
for (invalid_body, error_message) in test_cases {
|
||||
let response = app.post_subscriptions(invalid_body.into()).await;
|
||||
|
||||
assert_eq!(
|
||||
422,
|
||||
response.status().as_u16(),
|
||||
"the API did not fail with 422 Unprocessable Entity when the payload was {}.",
|
||||
error_message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn subscribe_returns_a_400_when_fields_are_present_but_invalid() {
|
||||
let app = TestApp::spawn().await;
|
||||
|
||||
let test_cases = [
|
||||
("name=&email=alphonse.paix%40outlook.com", "empty name"),
|
||||
("name=Alphonse&email=", "empty email"),
|
||||
("name=Alphonse&email=not-an-email", "invalid email"),
|
||||
];
|
||||
for (body, description) in test_cases {
|
||||
let response = app.post_subscriptions(body.into()).await;
|
||||
|
||||
assert_eq!(
|
||||
400,
|
||||
response.status().as_u16(),
|
||||
"the API did not return a 400 Bad Request when the payload had an {}.",
|
||||
description
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use sqlx::{Connection, Executor, PgConnection, PgPool};
|
||||
use tokio::net::TcpListener;
|
||||
use uuid::Uuid;
|
||||
use zero2prod::{
|
||||
configuration::{DatabaseSettings, get_configuration},
|
||||
telemetry::init_subscriber,
|
||||
};
|
||||
|
||||
static TRACING: Lazy<()> = Lazy::new(|| {
|
||||
if std::env::var("TEST_LOG").is_ok() {
|
||||
init_subscriber(std::io::stdout);
|
||||
} else {
|
||||
init_subscriber(std::io::sink);
|
||||
}
|
||||
});
|
||||
|
||||
pub struct TestApp {
|
||||
pub address: String,
|
||||
pub connection_pool: PgPool,
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn health_check_works() {
|
||||
let app = spawn_app().await;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let response = client
|
||||
.get(format!("http://{}/health_check", app.address))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(response.status().is_success());
|
||||
assert_eq!(Some(0), response.content_length());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn subscribe_returns_a_200_for_valid_form_data() {
|
||||
let app = spawn_app().await;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let body = "name=alphonse&email=alphonse.paix%40outlook.com";
|
||||
let response = client
|
||||
.post(format!("http://{}/subscriptions", app.address))
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.body(body)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(200, response.status().as_u16());
|
||||
|
||||
let saved = sqlx::query!("SELECT email, name FROM subscriptions")
|
||||
.fetch_one(&app.connection_pool)
|
||||
.await
|
||||
.expect("Failed to fetch saved subscription");
|
||||
|
||||
assert_eq!(saved.email, "alphonse.paix@outlook.com");
|
||||
assert_eq!(saved.name, "alphonse");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn subscribe_returns_a_422_when_data_is_missing() {
|
||||
let app = spawn_app().await;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let test_cases = [
|
||||
("name=Alphonse", "missing the email"),
|
||||
("email=alphonse.paix%40outlook.com", "missing the name"),
|
||||
("", "missing both name and email"),
|
||||
];
|
||||
for (invalid_body, error_message) in test_cases {
|
||||
let response = client
|
||||
.post(format!("http://{}/subscriptions", app.address))
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.body(invalid_body)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
422,
|
||||
response.status().as_u16(),
|
||||
"the API did not fail with 422 Unprocessable Entity when the payload was {}.",
|
||||
error_message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn subscribe_returns_a_400_when_fields_are_present_but_invalid() {
|
||||
let app = spawn_app().await;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let test_cases = [
|
||||
("name=&email=alphonse.paix%40outlook.com", "empty name"),
|
||||
("name=Alphonse&email=", "empty email"),
|
||||
("name=Alphonse&email=not-an-email", "invalid email"),
|
||||
];
|
||||
for (body, description) in test_cases {
|
||||
let response = client
|
||||
.post(format!("http://{}/subscriptions", app.address))
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.body(body)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
400,
|
||||
response.status().as_u16(),
|
||||
"the API did not return a 400 Bad Request when the payload had an {}.",
|
||||
description
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async fn spawn_app() -> TestApp {
|
||||
Lazy::force(&TRACING);
|
||||
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let address = listener.local_addr().unwrap().to_string();
|
||||
|
||||
let mut configuration = get_configuration().expect("Failed to read configuration");
|
||||
configuration.database.database_name = Uuid::new_v4().to_string();
|
||||
let connection_pool = configure_database(&configuration.database).await;
|
||||
|
||||
let app = TestApp {
|
||||
address,
|
||||
connection_pool: connection_pool.clone(),
|
||||
};
|
||||
|
||||
tokio::spawn(async move {
|
||||
zero2prod::startup::run(listener, connection_pool).await;
|
||||
});
|
||||
|
||||
app
|
||||
}
|
||||
|
||||
async fn configure_database(config: &DatabaseSettings) -> PgPool {
|
||||
let mut connection = PgConnection::connect_with(&config.without_db())
|
||||
.await
|
||||
.expect("Failed to connect to Postgres");
|
||||
connection
|
||||
.execute(format!(r#"CREATE DATABASE "{}";"#, config.database_name).as_ref())
|
||||
.await
|
||||
.expect("Failed to create the database");
|
||||
|
||||
let connection_pool = PgPool::connect_with(config.with_db())
|
||||
.await
|
||||
.expect("Failed to connect to Postgres");
|
||||
sqlx::migrate!("./migrations")
|
||||
.run(&connection_pool)
|
||||
.await
|
||||
.expect("Failed to migrate the database");
|
||||
|
||||
connection_pool
|
||||
}
|
||||
Reference in New Issue
Block a user