From 610177efd111fd1d41ee532e01bccfaacf4b0757 Mon Sep 17 00:00:00 2001 From: azdle Date: Thu, 22 May 2025 11:57:37 -0500 Subject: [PATCH] add rough DB stuff --- .env | 1 + .gitignore | 1 + Cargo.lock | 978 +++++++++++++++++- Cargo.toml | 6 + conf/default.toml | 2 +- ...50514191325_create-subscriptions-table.sql | 7 + scripts/start_dev_db.sh | 50 + src/conf.rs | 4 +- src/main.rs | 2 +- src/server/mod.rs | 23 +- src/server/routes/subscriptions/mod.rs | 29 +- tests/fixture/mod.rs | 344 +++++- 12 files changed, 1425 insertions(+), 22 deletions(-) create mode 100644 .env create mode 100644 migrations/20250514191325_create-subscriptions-table.sql create mode 100755 scripts/start_dev_db.sh diff --git a/.env b/.env new file mode 100644 index 0000000..6beac97 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +DATABASE_URL=postgres://ztoa:0zpVXAVK20@localhost:5432/ztoa diff --git a/.gitignore b/.gitignore index ea8c4bf..2c4918c 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +*.db diff --git a/Cargo.lock b/Cargo.lock index b6f0f3c..ecbdbb9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -61,6 +61,27 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anyhow" version = "1.0.98" @@ -87,6 +108,15 @@ dependencies = [ "syn", ] +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -204,6 +234,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" + [[package]] name = "bitflags" version = "2.9.0" @@ -222,12 +258,62 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bollard" +version = "0.19.0-rc1" +source = "git+https://github.com/fussybeaver/bollard.git?rev=50a25a0#50a25a0d81bf7b9085a69e6b5bfb56a28855f158" +dependencies = [ + "base64 0.22.1", + "bollard-stubs", + "bytes", + "futures-core", + "futures-util", + "hex", + "http", + "http-body-util", + "hyper", + "hyper-named-pipe", + "hyper-util", + "hyperlocal", + "log", + "pin-project-lite", + "serde", + "serde_derive", + "serde_json", + "serde_repr", + "serde_urlencoded", + "thiserror", + "tokio", + "tokio-util", + "tower-service", + "url", + "winapi", +] + +[[package]] +name = "bollard-stubs" +version = "1.48.2-rc.28.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cdf0fccd5341b38ae0be74b74410bdd5eceeea8876dc149a13edfe57e3b259" +dependencies = [ + "serde", + "serde_json", + "serde_repr", + "serde_with", +] + [[package]] name = "bumpalo" version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.10.1" @@ -249,6 +335,19 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + [[package]] name = "cipher" version = "0.4.4" @@ -259,6 +358,15 @@ dependencies = [ "inout", ] +[[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 = "config" version = "0.15.11" @@ -278,6 +386,12 @@ dependencies = [ "yaml-rust2", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "const-random" version = "0.1.18" @@ -365,6 +479,36 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[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.3" @@ -391,6 +535,17 @@ dependencies = [ "cipher", ] +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "deranged" version = "0.4.0" @@ -398,6 +553,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ "powerfmt", + "serde", ] [[package]] @@ -407,7 +563,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", + "subtle", ] [[package]] @@ -439,6 +597,21 @@ dependencies = [ "litrs", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + [[package]] name = "encoding_rs" version = "0.8.35" @@ -464,12 +637,57 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "filetime" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.59.0", +] + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + [[package]] name = "fnv" version = "1.0.7" @@ -513,6 +731,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -521,6 +740,34 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + [[package]] name = "futures-macro" version = "0.3.31" @@ -551,8 +798,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", + "futures-io", "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", "slab", @@ -619,13 +869,19 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap", + "indexmap 2.9.0", "slab", "tokio", "tokio-util", "tracing", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -638,6 +894,8 @@ version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" dependencies = [ + "allocator-api2", + "equivalent", "foldhash", ] @@ -674,6 +932,45 @@ dependencies = [ "http", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "http" version = "1.3.1" @@ -741,6 +1038,21 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-named-pipe" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278" +dependencies = [ + "hex", + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", + "winapi", +] + [[package]] name = "hyper-rustls" version = "0.27.5" @@ -794,6 +1106,45 @@ dependencies = [ "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.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.0.0" @@ -901,6 +1252,17 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.9.0" @@ -909,6 +1271,7 @@ checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", "hashbrown 0.15.3", + "serde", ] [[package]] @@ -958,6 +1321,9 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "libc" @@ -965,6 +1331,33 @@ version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags", + "libc", + "redox_syscall", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.9.4" @@ -1014,6 +1407,16 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "memchr" version = "2.7.4" @@ -1073,12 +1476,59 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + [[package]] name = "object" version = "0.36.7" @@ -1160,6 +1610,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[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.3" @@ -1189,6 +1645,15 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1272,6 +1737,27 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -1509,6 +1995,26 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "rsa" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rust-ini" version = "0.21.1" @@ -1673,6 +2179,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", +] + [[package]] name = "serde_spanned" version = "0.6.8" @@ -1694,6 +2211,23 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.9.0", + "serde", + "serde_derive", + "serde_json", + "time", +] + [[package]] name = "sha1" version = "0.10.6" @@ -1740,6 +2274,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + [[package]] name = "slab" version = "0.4.9" @@ -1754,6 +2298,9 @@ name = "smallvec" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +dependencies = [ + "serde", +] [[package]] name = "socket2" @@ -1765,12 +2312,239 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3a85280daca669cfd3bcb68a337882a8bc57ec882f72c5d13a430613a738e" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f743f2a3cea30a58cd479013f75550e879009e3a02f616f18ca699335aa248c3" +dependencies = [ + "base64 0.22.1", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.3", + "hashlink", + "indexmap 2.9.0", + "log", + "memchr", + "once_cell", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4200e0fde19834956d4252347c12a083bdcb237d7a1a1446bffd8768417dce" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882ceaa29cade31beca7129b6beeb05737f44f82dbe2a9806ecea5a7093d00b7" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn", + "tempfile", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0afdd3aa7a629683c2d750c2df343025545087081ab5942593a5288855b1b7a7" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags", + "byteorder", + "bytes", + "chrono", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0bedbe1bbb5e2615ef347a5e9d8cd7680fb63e77d9dafc0f29be15e53f1ebe6" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c26083e9a520e8eb87a06b12347679b142dc2ea29e6e409f805644a7a979a5bc" +dependencies = [ + "atoi", + "chrono", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror", + "tracing", + "url", + "uuid", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + [[package]] name = "subtle" version = "2.6.1" @@ -1829,6 +2603,17 @@ dependencies = [ "libc", ] +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "tempfile" version = "3.20.0" @@ -1943,6 +2728,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.45.0" @@ -2043,7 +2843,7 @@ version = "0.22.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" dependencies = [ - "indexmap", + "indexmap 2.9.0", "serde", "serde_spanned", "toml_datetime", @@ -2164,12 +2964,33 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + [[package]] name = "unicode-segmentation" version = "1.12.0" @@ -2209,6 +3030,15 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "uuid" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" +dependencies = [ + "getrandom 0.3.3", +] + [[package]] name = "valuable" version = "0.1.1" @@ -2251,6 +3081,12 @@ dependencies = [ "wit-bindgen-rt", ] +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -2332,6 +3168,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "whoami" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7" +dependencies = [ + "redox_syscall", + "wasite", +] + [[package]] name = "winapi" version = "0.3.9" @@ -2354,6 +3200,41 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings 0.4.0", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.1.1" @@ -2367,7 +3248,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" dependencies = [ "windows-result", - "windows-strings", + "windows-strings 0.3.1", "windows-targets 0.53.0", ] @@ -2389,6 +3270,24 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-strings" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -2407,6 +3306,21 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -2439,6 +3353,12 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -2451,6 +3371,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -2463,6 +3389,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -2487,6 +3419,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -2499,6 +3437,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -2511,6 +3455,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -2523,6 +3473,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -2559,6 +3515,16 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +[[package]] +name = "xattr" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d65cbf2f12c15564212d48f4e3dfb87923d25d611f2aed18f4cb23f0413d89e" +dependencies = [ + "libc", + "rustix", +] + [[package]] name = "yaml-rust2" version = "0.10.1" @@ -2601,19 +3567,25 @@ dependencies = [ "anyhow", "axum", "axum-extra", + "bollard", "config", "futures-util", + "http-body-util", "hyper", "pin-project", "reqwest", "serde", "serde_json", + "sqlx", + "tar", "test-log", "thiserror", "tokio", "tokio-stream", + "tokio-util", "tracing", "tracing-subscriber", + "uuid", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index a2d9190..7ce6e17 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,12 +17,18 @@ hyper = "1.1" pin-project = "1.1.0" serde = { version = "1.0.164", features = ["derive"] } serde_json = "1.0.99" +sqlx = { version = "0.8.5", features = ["runtime-tokio", "macros", "postgres", "uuid", "chrono", "migrate"] } +tar = "0.4.44" thiserror = "2.0" tokio = { version = "1.28.2", features = ["full"] } tokio-stream = "0.1" +tokio-util = "0.7.15" tracing = "0.1.37" tracing-subscriber = { version = "0.3", features =["env-filter"] } +uuid = { version = "1.16.0", features = ["v4"] } [dev-dependencies] +bollard = { git = "https://github.com/fussybeaver/bollard.git", rev = "50a25a0" } +http-body-util = "0.1.3" reqwest = { version = "0.12", features = ["cookies"] } test-log = { version = "0.2.12", default-features = false, features = ["trace"] } diff --git a/conf/default.toml b/conf/default.toml index 75d0533..aa63ecf 100644 --- a/conf/default.toml +++ b/conf/default.toml @@ -3,4 +3,4 @@ debug = true listen = "[::]:3742" [database] -url = "sqlite://./zero-to-axum.db" +url = "postgres://ztoa:0zpVXAVK20@localhost:5432/ztoa" diff --git a/migrations/20250514191325_create-subscriptions-table.sql b/migrations/20250514191325_create-subscriptions-table.sql new file mode 100644 index 0000000..66402e8 --- /dev/null +++ b/migrations/20250514191325_create-subscriptions-table.sql @@ -0,0 +1,7 @@ +-- Create Subscriptions Table +CREATE TABLE subscriptions( + id uuid NOT NULL PRIMARY KEY, + email TEXT NOT NULL UNIQUE, + name TEXT, + subscribed_at timestamptz NOT NULL +) diff --git a/scripts/start_dev_db.sh b/scripts/start_dev_db.sh new file mode 100755 index 0000000..83870dc --- /dev/null +++ b/scripts/start_dev_db.sh @@ -0,0 +1,50 @@ +#!/bin/bash + +SUPERUSER_NAME="postgres" +SUPERUSER_PASS="nOMvDptXFk" + +USER_NAME="ztoa" +USER_PASS="0zpVXAVK20" + +APP_DB_NAME="ztoa" + +CONTAINER_NAME="postgres" + +DATABASE_URL="postgres://ztoa:0zpVXAVK20@localhost:5432/ztoa" + +# Launch postgres +podman run \ + --replace \ + --env POSTGRES_USER=${SUPERUSER_NAME} \ + --env POSTGRES_PASSWORD=${SUPERUSER_PASS} \ + --health-cmd="pg_isready -U ${SUPERUSER_NAME} || exit 1" \ + --health-interval=1s \ + --health-timeout=5s \ + --health-retries=5 \ + --publish 5432:5432 \ + --detach \ + --name "${CONTAINER_NAME}" \ + postgres -N 1000 + +# Wait for Postgres to be ready +until [ \ + "$(podman inspect -f "{{.State.Health.Status}}" ${CONTAINER_NAME})" == \ + "healthy" \ +]; do + >&2 echo "Postgres is still unavailable - sleeping" + sleep 1 +done + +>&2 echo "Postgres is up and running" + +# Create the application user +CREATE_QUERY="CREATE USER ${USER_NAME} WITH PASSWORD '${USER_PASS}';" +podman exec -it "${CONTAINER_NAME}" psql -U "${SUPERUSER_NAME}" -c "${CREATE_QUERY}" + +# Grant create db privileges to the app user +GRANT_QUERY="ALTER USER ${USER_NAME} CREATEDB;" +podman exec -it "${CONTAINER_NAME}" psql -U "${SUPERUSER_NAME}" -c "${GRANT_QUERY}" + +sqlx database create + +sqlx migrate run diff --git a/src/conf.rs b/src/conf.rs index 5ea95b3..764a961 100644 --- a/src/conf.rs +++ b/src/conf.rs @@ -3,12 +3,12 @@ use config::{Config, Environment, File}; use serde::Deserialize; use std::{env, net::SocketAddr}; -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Clone)] pub struct Database { pub url: String, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Clone)] #[allow(unused)] pub struct Conf { pub debug: bool, diff --git a/src/main.rs b/src/main.rs index a38cbfd..494acd7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,7 +16,7 @@ async fn main() -> Result<()> { init_tracing(); let conf = Conf::read()?; - let server = ZeroToAxum::serve(conf).await; + let server = ZeroToAxum::serve(conf).await?; server.await.context("run server") } diff --git a/src/server/mod.rs b/src/server/mod.rs index c67d737..191ae02 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -4,10 +4,13 @@ use anyhow::Result; use axum::extract::FromRef; use axum_extra::extract::cookie::Key; use pin_project::pin_project; +use sqlx::postgres::PgPoolOptions; +use sqlx::PgPool; use std::future::{Future, IntoFuture}; use std::net::SocketAddr; use std::pin::pin; use std::pin::Pin; +use std::sync::Arc; use tokio::signal; use tracing::info; @@ -36,13 +39,20 @@ impl ZeroToAxum { self.bound_addr } - pub async fn serve(conf: Conf) -> ZeroToAxum { - let state = AppState { + pub async fn serve(conf: Conf) -> Result { + let db = PgPoolOptions::new() + .max_connections(5) + .connect(&conf.database.url) + .await?; + + let app_state = AppState { + conf: Arc::new(conf.clone()), // TODO: pull from config key: Key::generate(), + db, }; - let app = routes::build().with_state(state); + let app = routes::build().with_state(app_state); let listener = tokio::net::TcpListener::bind(&conf.listen).await.unwrap(); let bound_addr = listener.local_addr().unwrap(); @@ -50,17 +60,20 @@ impl ZeroToAxum { info!("server started, listening on {bound_addr:?}"); - ZeroToAxum { + Ok(ZeroToAxum { server: Box::pin(server.into_future()), bound_addr, - } + }) } } #[derive(Clone)] pub struct AppState { + #[allow(unused)] + conf: Arc, // The key used to encrypt cookies. key: Key, + db: PgPool, } impl FromRef for Key { diff --git a/src/server/routes/subscriptions/mod.rs b/src/server/routes/subscriptions/mod.rs index 0331d7d..8d1198d 100644 --- a/src/server/routes/subscriptions/mod.rs +++ b/src/server/routes/subscriptions/mod.rs @@ -1,6 +1,9 @@ -use axum::{http::StatusCode, response::IntoResponse, routing::post, Form, Router}; +use anyhow::Context; +use axum::{extract::State, http::StatusCode, response::IntoResponse, routing::post, Form, Router}; use serde::Deserialize; +use sqlx::types::chrono::Utc; use tracing::info; +use uuid::Uuid; use crate::server::AppState; @@ -14,24 +17,46 @@ pub struct SubscribeForm { email: String, } -pub async fn subscribe(Form(form): Form) -> Result<(), SubscribeError> { +pub async fn subscribe( + State(AppState { db, .. }): State, + Form(form): Form, +) -> Result<(), SubscribeError> { info!(form.name, form.email, "subscribe attempt"); if form.email.is_empty() { return Err(SubscribeError::InvalidEmail); } + sqlx::query!( + r#" + INSERT INTO subscriptions (id, email, name, subscribed_at) + VALUES ($1, $2, $3, $4) + "#, + Uuid::new_v4(), + form.email, + form.name, + Utc::now() + ) + .execute(&db) + .await + .context("insert subscription into database")?; + Ok(()) } +#[derive(thiserror::Error, Debug)] pub enum SubscribeError { + #[error("Invalid Email Address")] InvalidEmail, + #[error("Unknown Error: {0}")] + Unknown(#[from] anyhow::Error), } impl IntoResponse for SubscribeError { fn into_response(self) -> axum::response::Response { match self { SubscribeError::InvalidEmail => (StatusCode::BAD_REQUEST, "Invalid Email Address"), + SubscribeError::Unknown(_e) => (StatusCode::INTERNAL_SERVER_ERROR, "Unknown Error"), } .into_response() } diff --git a/tests/fixture/mod.rs b/tests/fixture/mod.rs index fff3829..a17b953 100644 --- a/tests/fixture/mod.rs +++ b/tests/fixture/mod.rs @@ -1,33 +1,62 @@ use anyhow::Result; -use futures_util::FutureExt; +use bollard::query_parameters::CreateImageOptionsBuilder; +use bollard::secret::{ + ContainerCreateBody, ContainerInspectResponse, ContainerState, CreateImageInfo, Health, + HealthConfig, HealthStatusEnum, HostConfig, PortBinding, +}; +use bollard::Docker; +use futures_util::{FutureExt, StreamExt as _}; +use sqlx::migrate::MigrateDatabase; +use sqlx::{Connection, PgConnection}; use std::net::SocketAddr; +use std::path::Path; +use std::sync::Arc; +use std::time::Duration; +use tokio::sync::OnceCell; use tokio::task::JoinHandle; -use tracing::info; +use tokio::time::sleep; +use tracing::{debug, trace}; use zero_to_axum::{Conf, ZeroToAxum}; +static SHARED_DB: OnceCell> = OnceCell::const_new(); +async fn get_shared_db() -> Arc { + SHARED_DB + .get_or_init(|| async { Arc::new(TestDb::spawn().await) }) + .await + .clone() +} + pub struct TestServer { server_task_handle: JoinHandle<()>, addr: SocketAddr, + db: Arc, } impl TestServer { pub async fn spawn() -> TestServer { - info!("start server"); + debug!("start test server"); + + // TODO: allow per-test DBs in some cases + // let db = TestDb::spawn().await; + // TODO: share test server between test file, somehow? + let db = get_shared_db().await; + let url = dbg!(db.get_url()); + let server = ZeroToAxum::serve(Conf { listen: "[::]:0".parse().unwrap(), - database: zero_to_axum::conf::Database { - url: "memory:".into(), - }, + database: zero_to_axum::conf::Database { url }, debug: true, }) - .await; + .await + .unwrap(); let addr = server.local_addr(); let server_task_handle = tokio::spawn(server.map(|res| res.unwrap())); - info!("server spawned"); + debug!(?addr, "test server spawned"); TestServer { server_task_handle, addr, + db, } } @@ -41,6 +70,305 @@ impl TestServer { self.server_task_handle.abort(); let _ = self.server_task_handle.await; + self.db.stop().await; + Ok(()) } } + +const TEST_DB_IMAGE_NAME: &str = "postgres"; +const TEST_DB_SUPERUSER: &str = "postgres"; +const TEST_DB_SUPERUSER_PASS: &str = "password"; +const TEST_DB_APP_USER: &str = "app"; +const TEST_DB_APP_PASS: &str = "apppass"; +const TEST_DB_APP_NAME: &str = "ztoa"; + +pub struct TestDb { + docker: Docker, + // image_id: String, + container: bollard::secret::ContainerInspectResponse, +} + +impl TestDb { + pub async fn spawn() -> Self { + let docker = Docker::connect_with_local_defaults().expect("connect to docker daemon"); + + let docker = docker.negotiate_version().await.unwrap(); + + let version = docker.version().await.unwrap(); + + trace!("version: {version:?}"); + + let mut image_id = None; + + // check for image + if let Ok(image) = docker.inspect_image(TEST_DB_IMAGE_NAME).await { + image_id = Some(image.id.unwrap()); + } + + // build docker image from docker file + // let mut image_id = None; + // { + // let filename = "Dockerfile.db"; + // let image_options = bollard::query_parameters::BuildImageOptionsBuilder::default() + // .dockerfile(filename) + // .rm(true) + // .build(); + + // let archive_bytes = { + // let mut archive = tar::Builder::new(Vec::new()); + // archive.append_path(filename).unwrap(); + // archive.into_inner().unwrap() + // }; + + // let mut image_build_stream = docker.build_image( + // image_options, + // None, + // Some(http_body_util::Either::Left(http_body_util::Full::new( + // archive_bytes.into(), + // ))), + // ); + + // while let Some(msg) = image_build_stream.next().await { + // info!("Message: {msg:?}"); + + // if let Ok(BuildInfo { + // aux: Some(ImageId { id: Some(id) }), + // .. + // }) = msg + // { + // trace!("Image ID: {id}"); + // image_id = Some(id); + // } + // } + // } + // let image_id = image_id.expect("get image id for built docker image"); + + // pull image + if image_id.is_none() { + let image_opts = CreateImageOptionsBuilder::new() + .from_image(TEST_DB_IMAGE_NAME) + .build(); + + trace!(?image_opts, "pull image"); + let mut image_create_stream = docker.create_image(Some(image_opts), None, None); + + while let Some(msg) = image_create_stream.next().await { + trace!("Message: {msg:?}"); + + if let Ok(CreateImageInfo { id: Some(id), .. }) = msg { + trace!("Image ID: {id}"); + image_id = Some(id); + } + } + } + + let image_id = image_id.expect("get image id for built docker image"); + + // create and start docker container + let container_id; + { + let container_config = ContainerCreateBody { + image: Some(image_id.clone()), + exposed_ports: Some([("5432/tcp".to_string(), [].into())].into()), + host_config: Some(HostConfig { + port_bindings: Some( + [( + "5432/tcp".to_string(), + Some(vec![PortBinding { + host_ip: Some("127.0.0.1".to_string()), + host_port: None, // auto-assign + }]), + )] + .into(), + ), + ..Default::default() + }), + env: Some(vec![ + format!("POSTGRES_USER={TEST_DB_SUPERUSER}"), + format!("POSTGRES_PASSWORD={TEST_DB_SUPERUSER_PASS}"), + ]), + healthcheck: Some(HealthConfig { + test: Some(vec!["pg_isready -U postgres || exit 1".to_string()]), + // nano seconds + interval: Some(1 * 1000 * 1000 * 1000), + timeout: Some(5 * 1000 * 1000 * 1000), + retries: Some(5 * 1000 * 1000 * 1000), + ..Default::default() + }), + ..Default::default() + }; + + trace!("create container"); + bollard::secret::ContainerCreateResponse { + id: container_id, + .. + } = docker + .create_container( + None::, + container_config, + ) + .await + .unwrap(); + + trace!("start container"); + docker + .start_container( + &container_id, + None::, + ) + .await + .unwrap(); + } + + // wait for container to be started + let container = loop { + trace!("inspect container"); + let container = docker + .inspect_container( + &container_id, + None::, + ) + .await + .unwrap(); + + if let ContainerInspectResponse { + state: + Some(ContainerState { + health: + Some(Health { + status: Some(status), + .. + }), + .. + }), + .. + } = &container + { + trace!("status: {status:?}"); + if *status == HealthStatusEnum::HEALTHY { + break container; + } + } + + sleep(Duration::from_secs(2)).await; + }; + + let db = TestDb { + docker, + // image_id, + container, + }; + + // setup app db + { + let mut conn = PgConnection::connect(&db.get_superuser_url()) + .await + .unwrap(); + + // create application user + // Note: In general, string formtting a query is bad practice, but it's required here. + sqlx::query(&format!( + "CREATE USER {TEST_DB_APP_USER} WITH PASSWORD '{TEST_DB_APP_PASS}';" + )) + .execute(&mut conn) + .await + .unwrap(); + + // grant privs to app user + // Note: In general, string formtting a query is bad practice, but it's required here. + sqlx::query(&format!("ALTER USER {TEST_DB_APP_USER} CREATEDB;")) + .execute(&mut conn) + .await + .unwrap(); + } + + // create test db + sqlx::Postgres::create_database(&dbg!(db.get_url())) + .await + .unwrap(); + + let mut conn = PgConnection::connect(&db.get_url()).await.unwrap(); + + // run migrations on test db + let m = sqlx::migrate::Migrator::new(Path::new("./migrations")) + .await + .unwrap(); + m.run(&mut conn).await.unwrap(); + + db + } + + /// Get the authenticated URL for accessing the test DB from the host. + pub fn get_url(&self) -> String { + let binding = self + .container + .network_settings + .as_ref() + .unwrap() + .ports + .as_ref() + .unwrap() + .get("5432/tcp") + .as_ref() + .unwrap() + .as_ref() + .unwrap() + .first() + .unwrap(); + let host_ip = binding.host_ip.as_ref().unwrap().clone(); + let host_port = binding.host_port.as_ref().unwrap().clone(); + format!("postgres://{TEST_DB_APP_USER}:{TEST_DB_APP_PASS}@{host_ip}:{host_port}/{TEST_DB_APP_NAME}") + } + + /// Get the superuser-authenticated URL for accessing the `postgres` db from the host. + fn get_superuser_url(&self) -> String { + let binding = self + .container + .network_settings + .as_ref() + .unwrap() + .ports + .as_ref() + .unwrap() + .get("5432/tcp") + .as_ref() + .unwrap() + .as_ref() + .unwrap() + .first() + .unwrap(); + let host_ip = binding.host_ip.as_ref().unwrap().clone(); + let host_port = binding.host_port.as_ref().unwrap().clone(); + format!("postgres://{TEST_DB_SUPERUSER}:{TEST_DB_SUPERUSER_PASS}@{host_ip}:{host_port}/postgres") + } + + pub async fn stop(&self) { + self.docker + .stop_container( + &self.container.id.as_deref().unwrap(), + #[allow(deprecated)] // is deprecated, but also required + None::, + ) + .await + .unwrap(); + + self.docker + .remove_container( + &self.container.id.as_deref().unwrap(), + None::, + ) + .await + .unwrap(); + + // // TODO: images seem like they might be reused, this is probably a bad idea, but it seems to work? + // let _ = self + // .docker + // .remove_image( + // &self.image_id, + // None::, + // None, + // ) + // .await; + } +}