Compare commits

..

12 commits

13 changed files with 1284 additions and 18 deletions

View file

@ -0,0 +1,67 @@
name: Publish Cargo Package
on:
push:
tags:
- "v*"
workflow_dispatch:
jobs:
publish:
runs-on: docker
env:
CARGO_TERM_COLOR: always
CARGO_REGISTRIES_FORGEJO_TOKEN: Bearer ${{ secrets.FORGEJO_CARGO_TOKEN }}
FORGEJO_CARGO_INDEX: ${{ github.server_url }}/${{ github.repository_owner }}/_cargo-index.git
steps:
- name: Check out repository
uses: https://data.forgejo.org/actions/checkout@v4
- name: Install Rust
run: |
if ! command -v cargo >/dev/null 2>&1; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal
fi
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
. "$HOME/.cargo/env"
rustup toolchain install stable --profile minimal
rustup default stable
cargo --version
- name: Configure Cargo registry
run: |
mkdir -p "$HOME/.cargo"
cat > "$HOME/.cargo/config.toml" <<EOF
[registries.forgejo]
index = "${FORGEJO_CARGO_INDEX}"
[net]
git-fetch-with-cli = true
EOF
- name: Verify tag matches package version
if: startsWith(github.ref, 'refs/tags/v')
run: |
version=$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -n1)
tag="${GITHUB_REF_NAME#v}"
if [ -z "$version" ]; then
echo "Could not determine package version from Cargo.toml"
exit 1
fi
if [ "$version" != "$tag" ]; then
echo "Tag version ($tag) does not match Cargo.toml version ($version)"
exit 1
fi
- name: Check package can be published
run: |
. "$HOME/.cargo/env"
cargo publish --dry-run --locked --registry forgejo
- name: Publish package
run: |
. "$HOME/.cargo/env"
cargo publish --locked --registry forgejo

1
.gitignore vendored
View file

@ -1 +1,2 @@
/target /target
.env

518
Cargo.lock generated
View file

@ -17,6 +17,21 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[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.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]] [[package]]
name = "atoi" name = "atoi"
version = "2.0.0" version = "2.0.0"
@ -45,6 +60,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8"
dependencies = [ dependencies = [
"axum-core", "axum-core",
"axum-macros",
"bytes", "bytes",
"form_urlencoded", "form_urlencoded",
"futures-util", "futures-util",
@ -90,6 +106,39 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "axum-extra"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fef252edff26ddba56bbcdf2ee3307b8129acb86f5749b68990c168a6fcc9c76"
dependencies = [
"axum",
"axum-core",
"bytes",
"cookie",
"futures-core",
"futures-util",
"http",
"http-body",
"http-body-util",
"mime",
"pin-project-lite",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "axum-macros"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.22.1" version = "0.22.1"
@ -120,6 +169,12 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "bumpalo"
version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]] [[package]]
name = "byteorder" name = "byteorder"
version = "1.5.0" version = "1.5.0"
@ -132,12 +187,35 @@ version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]]
name = "cc"
version = "1.2.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.4" version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "chrono"
version = "0.4.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-link",
]
[[package]] [[package]]
name = "concurrent-queue" name = "concurrent-queue"
version = "2.5.0" version = "2.5.0"
@ -153,6 +231,23 @@ version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "cookie"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
dependencies = [
"percent-encoding",
"time",
"version_check",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]] [[package]]
name = "cpufeatures" name = "cpufeatures"
version = "0.2.17" version = "0.2.17"
@ -213,6 +308,15 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "deranged"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
dependencies = [
"powerfmt",
]
[[package]] [[package]]
name = "digest" name = "digest"
version = "0.10.7" version = "0.10.7"
@ -289,6 +393,12 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]] [[package]]
name = "flume" name = "flume"
version = "0.11.1" version = "0.11.1"
@ -407,6 +517,19 @@ dependencies = [
"wasi", "wasi",
] ]
[[package]]
name = "getrandom"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasip2",
"wasip3",
]
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.15.5" version = "0.15.5"
@ -553,6 +676,30 @@ dependencies = [
"tower-service", "tower-service",
] ]
[[package]]
name = "iana-time-zone"
version = "0.1.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
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]] [[package]]
name = "icu_collections" name = "icu_collections"
version = "2.1.1" version = "2.1.1"
@ -634,6 +781,12 @@ dependencies = [
"zerovec", "zerovec",
] ]
[[package]]
name = "id-arena"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
[[package]] [[package]]
name = "idna" name = "idna"
version = "1.1.0" version = "1.1.0"
@ -663,6 +816,8 @@ checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.16.1", "hashbrown 0.16.1",
"serde",
"serde_core",
] ]
[[package]] [[package]]
@ -671,6 +826,16 @@ version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "js-sys"
version = "0.3.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.5.0" version = "1.5.0"
@ -680,6 +845,12 @@ dependencies = [
"spin", "spin",
] ]
[[package]]
name = "leb128fmt"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.183" version = "0.2.183"
@ -808,6 +979,12 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "num-conv"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
[[package]] [[package]]
name = "num-integer" name = "num-integer"
version = "0.1.46" version = "0.1.46"
@ -942,6 +1119,12 @@ dependencies = [
"zerovec", "zerovec",
] ]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.21" version = "0.2.21"
@ -951,6 +1134,16 @@ dependencies = [
"zerocopy", "zerocopy",
] ]
[[package]]
name = "prettyplease"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn",
]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.106" version = "1.0.106"
@ -969,6 +1162,12 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "r-efi"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.8.5" version = "0.8.5"
@ -996,7 +1195,7 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [ dependencies = [
"getrandom", "getrandom 0.2.17",
] ]
[[package]] [[package]]
@ -1054,6 +1253,12 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.23" version = "1.0.23"
@ -1066,6 +1271,12 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "semver"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.228" version = "1.0.228"
@ -1163,6 +1374,12 @@ dependencies = [
"lazy_static", "lazy_static",
] ]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]] [[package]]
name = "signal-hook-registry" name = "signal-hook-registry"
version = "1.4.8" version = "1.4.8"
@ -1248,6 +1465,7 @@ checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
dependencies = [ dependencies = [
"base64", "base64",
"bytes", "bytes",
"chrono",
"crc", "crc",
"crossbeam-queue", "crossbeam-queue",
"either", "either",
@ -1272,6 +1490,7 @@ dependencies = [
"tokio-stream", "tokio-stream",
"tracing", "tracing",
"url", "url",
"uuid",
] ]
[[package]] [[package]]
@ -1323,6 +1542,7 @@ dependencies = [
"bitflags", "bitflags",
"byteorder", "byteorder",
"bytes", "bytes",
"chrono",
"crc", "crc",
"digest", "digest",
"dotenvy", "dotenvy",
@ -1351,6 +1571,7 @@ dependencies = [
"stringprep", "stringprep",
"thiserror", "thiserror",
"tracing", "tracing",
"uuid",
"whoami", "whoami",
] ]
@ -1364,6 +1585,7 @@ dependencies = [
"base64", "base64",
"bitflags", "bitflags",
"byteorder", "byteorder",
"chrono",
"crc", "crc",
"dotenvy", "dotenvy",
"etcetera", "etcetera",
@ -1388,6 +1610,7 @@ dependencies = [
"stringprep", "stringprep",
"thiserror", "thiserror",
"tracing", "tracing",
"uuid",
"whoami", "whoami",
] ]
@ -1398,6 +1621,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea"
dependencies = [ dependencies = [
"atoi", "atoi",
"chrono",
"flume", "flume",
"futures-channel", "futures-channel",
"futures-core", "futures-core",
@ -1413,6 +1637,7 @@ dependencies = [
"thiserror", "thiserror",
"tracing", "tracing",
"url", "url",
"uuid",
] ]
[[package]] [[package]]
@ -1495,6 +1720,37 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "time"
version = "0.3.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde_core",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
[[package]]
name = "time-macros"
version = "0.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
dependencies = [
"num-conv",
"time-core",
]
[[package]] [[package]]
name = "tinystr" name = "tinystr"
version = "0.8.2" version = "0.8.2"
@ -1698,18 +1954,29 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
[[package]]
name = "unicode-xid"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]] [[package]]
name = "uprs" name = "uprs"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow",
"axum", "axum",
"axum-extra",
"chrono",
"serde", "serde",
"serde_json",
"sqlx", "sqlx",
"tokio", "tokio",
"tower", "tower",
"tower-http", "tower-http",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"uuid",
] ]
[[package]] [[package]]
@ -1730,6 +1997,17 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "uuid"
version = "1.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37"
dependencies = [
"getrandom 0.4.2",
"js-sys",
"wasm-bindgen",
]
[[package]] [[package]]
name = "valuable" name = "valuable"
version = "0.1.1" version = "0.1.1"
@ -1754,12 +2032,109 @@ version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasip2"
version = "1.0.2+wasi-0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasip3"
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
dependencies = [
"wit-bindgen",
]
[[package]] [[package]]
name = "wasite" name = "wasite"
version = "0.1.0" version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
[[package]]
name = "wasm-bindgen"
version = "0.2.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16"
dependencies = [
"unicode-ident",
]
[[package]]
name = "wasm-encoder"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
dependencies = [
"leb128fmt",
"wasmparser",
]
[[package]]
name = "wasm-metadata"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
dependencies = [
"anyhow",
"indexmap",
"wasm-encoder",
"wasmparser",
]
[[package]]
name = "wasmparser"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
dependencies = [
"bitflags",
"hashbrown 0.15.5",
"indexmap",
"semver",
]
[[package]] [[package]]
name = "whoami" name = "whoami"
version = "1.6.1" version = "1.6.1"
@ -1770,12 +2145,65 @@ dependencies = [
"wasite", "wasite",
] ]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "windows-link" name = "windows-link"
version = "0.2.1" version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.48.0" version = "0.48.0"
@ -1851,6 +2279,94 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "wit-bindgen"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
dependencies = [
"wit-bindgen-rust-macro",
]
[[package]]
name = "wit-bindgen-core"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
dependencies = [
"anyhow",
"heck",
"wit-parser",
]
[[package]]
name = "wit-bindgen-rust"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
dependencies = [
"anyhow",
"heck",
"indexmap",
"prettyplease",
"syn",
"wasm-metadata",
"wit-bindgen-core",
"wit-component",
]
[[package]]
name = "wit-bindgen-rust-macro"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
dependencies = [
"anyhow",
"prettyplease",
"proc-macro2",
"quote",
"syn",
"wit-bindgen-core",
"wit-bindgen-rust",
]
[[package]]
name = "wit-component"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
dependencies = [
"anyhow",
"bitflags",
"indexmap",
"log",
"serde",
"serde_derive",
"serde_json",
"wasm-encoder",
"wasm-metadata",
"wasmparser",
"wit-parser",
]
[[package]]
name = "wit-parser"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
dependencies = [
"anyhow",
"id-arena",
"indexmap",
"log",
"semver",
"serde",
"serde_derive",
"serde_json",
"unicode-xid",
"wasmparser",
]
[[package]] [[package]]
name = "writeable" name = "writeable"
version = "0.6.2" version = "0.6.2"

View file

@ -4,11 +4,22 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
axum = "0.8" anyhow = "1"
axum = { version = "0.8", features = ["macros"] }
axum-extra = { version = "0.12.5", features = ["cookie"] }
chrono = "0.4.44"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
sqlx = { version = "0.8.6", features = ["runtime-tokio", "postgres", "macros"] } serde_json = "1"
sqlx = { version = "0.8.6", features = [
"runtime-tokio",
"postgres",
"macros",
"uuid",
"chrono",
] }
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
tower = "0.5" tower = "0.5"
tower-http = { version = "0.6", features = ["trace"] } tower-http = { version = "0.6", features = ["trace"] }
tracing = "0.1" tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
uuid = { version = "1.22.0", features = ["v4"] }

View file

@ -0,0 +1,8 @@
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE TABLE IF NOT EXISTS votes (
slug text not null,
voter_id uuid not null,
created_at timestamptz NOT NULL DEFAULT timezone('utc'::text, now()),
primary key (slug, voter_id)
);

16
docker-compose.yml Normal file
View file

@ -0,0 +1,16 @@
services:
db:
image: postgres:16
container_name: uprs_db
restart: always
environment:
POSTGRES_USER: uprs
POSTGRES_PASSWORD: password123
POSTGRES_DB: uprs
volumes:
- pgdata:/var/lib/postgresql/data
- ./db/migrations:/docker-entrypoint-initdb.d # run initial schema
ports:
- "5432:5432"
volumes:
pgdata:

View file

@ -1,6 +1,7 @@
pub mod env; pub mod env;
pub mod routes; pub mod routes;
pub mod state; pub mod state;
pub mod test_helpers;
pub mod votes; pub mod votes;
use axum::Router; use axum::Router;

16
src/test_helpers.rs Normal file
View file

@ -0,0 +1,16 @@
#[cfg(test)]
pub mod db {
use sqlx::PgPool;
const DEFAULT_CONNECTION_STRING: &str =
"postgres://uprs:password123@localhost:5432/uprs";
pub async fn test_pool() -> PgPool {
let conn = std::env::var("POSTGRES_CONNECTION_STRING")
.unwrap_or_else(|_| DEFAULT_CONNECTION_STRING.to_string());
PgPool::connect(&conn)
.await
.expect("failed to connect to test database")
}
}

View file

@ -1,7 +1,7 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Deserialize)] #[derive(Serialize, Deserialize)]
pub struct CreateVoteRequest {} pub struct VotesAndDidVote {
pub vote_count: i64,
#[derive(Serialize)] pub voted: bool,
pub struct VoteResponse {} }

View file

@ -1,13 +1,334 @@
use axum::{ use axum::{
Router, Json, Router,
extract::{Path, State}, extract::{Path, State},
routing::{get, post}, http::StatusCode,
response::IntoResponse,
routing::{delete, get, post},
}; };
use axum_extra::extract::{CookieJar, cookie::Cookie};
use tracing::{error, info};
use uuid::Uuid;
use crate::state::AppState; use crate::{
state::AppState,
use super::dto::CreateVoteRequest; votes::{
repository::{delete_vote, insert_new_vote},
service::get_votes_and_voted,
},
};
pub fn router() -> Router<AppState> { pub fn router() -> Router<AppState> {
Router::new() Router::new()
.route("/posts/{slug}/vote", post(upvote_handler))
.route("/posts/{slug}/vote", delete(delete_vote_handler))
.route("/posts/{slug}/votes", get(get_votes_and_voted_handler))
}
fn get_or_init_voter_id(jar: CookieJar) -> (CookieJar, Uuid) {
match jar.get("voter_id") {
Some(cookie) => match Uuid::parse_str(cookie.value()) {
Ok(voter_id) => (jar, voter_id),
Err(_) => {
let jar = jar.remove("voter_id");
get_or_init_voter_id(jar)
}
},
None => {
let voter_id = Uuid::new_v4();
let cookie = Cookie::build(("voter_id", voter_id.to_string()))
.path("/")
.http_only(true)
.build();
(jar.add(cookie), voter_id)
}
}
}
async fn upvote_handler(
jar: CookieJar,
Path(slug): Path<String>,
State(state): State<AppState>,
) -> impl IntoResponse {
let (jar, voter_id) = get_or_init_voter_id(jar);
match insert_new_vote(&slug, &voter_id, &state.db).await {
Ok(()) => {
info!(slug = %slug, voter_id = %voter_id, "upvoted successfully");
(StatusCode::OK, jar, "Successfully upvoted")
}
Err(err) => {
error!(error = %err, slug = %slug, voter_id = %voter_id, "failed to insert vote");
(StatusCode::INTERNAL_SERVER_ERROR, jar, "Failed to upvote")
}
}
}
async fn delete_vote_handler(
jar: CookieJar,
Path(slug): Path<String>,
State(state): State<AppState>,
) -> impl IntoResponse {
let (jar, voter_id) = get_or_init_voter_id(jar);
match delete_vote(&slug, &voter_id, &state.db).await {
Ok(()) => {
info!(slug = %slug, voter_id = %voter_id, "vote deleted successfully");
(StatusCode::OK, jar, "Successfully deleted vote")
}
Err(err) => {
error!(error = %err, slug = %slug, voter_id = %voter_id, "failed to delete vote");
(
StatusCode::INTERNAL_SERVER_ERROR,
jar,
"Failed to delete vote",
)
}
}
}
async fn get_votes_and_voted_handler(
jar: CookieJar,
Path(slug): Path<String>,
State(state): State<AppState>,
) -> impl IntoResponse {
let (jar, voter_id) = get_or_init_voter_id(jar);
match get_votes_and_voted(&slug, &voter_id, &state.db).await {
Ok(votes_and_voted) => {
info!(slug = %slug, voter_id = %voter_id, "fetched vote count and voter status successfully");
(StatusCode::OK, jar, Json(votes_and_voted)).into_response()
}
Err(err) => {
error!(
error = %err,
slug = %slug,
voter_id = %voter_id,
"failed to fetch vote count and voter status"
);
(StatusCode::INTERNAL_SERVER_ERROR, jar, "Failed to upvote").into_response()
}
}
}
#[cfg(test)]
mod tests {
use axum::{
body::{Body, to_bytes},
http::{Request, StatusCode, header},
};
use serde_json::Value;
use sqlx::{PgPool, query_scalar};
use tower::ServiceExt;
use uuid::Uuid;
use crate::{
env::Env,
state::AppState,
test_helpers::db::test_pool,
votes::repository::{delete_votes_for_slug, insert_new_vote},
};
async fn get_vote_count_for_slug(slug: &str, db: &PgPool) -> i64 {
query_scalar("select count(*) from votes where slug = $1")
.bind(slug)
.fetch_one(db)
.await
.unwrap()
}
fn test_state(db: PgPool) -> AppState {
AppState {
db,
env: Env {
postgres_connection_string: String::new(),
},
}
}
#[tokio::test]
async fn upvote_sets_cookie_and_inserts_vote() {
let db = test_pool().await;
let slug = "upvote_handler_sets_cookie_and_inserts_vote";
delete_votes_for_slug(slug, &db).await.unwrap();
let app = crate::app(test_state(db.clone()));
let request = Request::builder()
.method("POST")
.uri(format!("/posts/{slug}/vote"))
.header(header::CONTENT_TYPE, "application/json")
.body(Body::empty())
.unwrap();
let response = app.oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::OK);
assert!(response.headers().contains_key(header::SET_COOKIE));
let body = to_bytes(response.into_body(), usize::MAX).await.unwrap();
assert_eq!(&body[..], b"Successfully upvoted");
assert_eq!(get_vote_count_for_slug(slug, &db).await, 1);
delete_votes_for_slug(slug, &db).await.unwrap();
}
#[tokio::test]
async fn upvote_is_idempotent_with_same_cookie() {
let db = test_pool().await;
let slug = "upvote_handler_idempotent_with_same_cookie";
let voter_id = Uuid::from_u128(0xabc);
delete_votes_for_slug(slug, &db).await.unwrap();
let cookie = format!("voter_id={voter_id}");
for _ in 0..2 {
let app = crate::app(test_state(db.clone()));
let request = Request::builder()
.method("POST")
.uri(format!("/posts/{slug}/vote"))
.header(header::CONTENT_TYPE, "application/json")
.header(header::COOKIE, &cookie)
.body(Body::empty())
.unwrap();
let response = app.oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
assert_eq!(get_vote_count_for_slug(slug, &db).await, 1);
delete_votes_for_slug(slug, &db).await.unwrap();
}
#[tokio::test]
async fn upvote_replaces_malformed_cookie() {
let db = test_pool().await;
let slug = "upvote_handler_replaces_malformed_cookie";
delete_votes_for_slug(slug, &db).await.unwrap();
let app = crate::app(test_state(db.clone()));
let request = Request::builder()
.method("POST")
.uri(format!("/posts/{slug}/vote"))
.header(header::CONTENT_TYPE, "application/json")
.header(header::COOKIE, "voter_id=not-a-uuid")
.body(Body::empty())
.unwrap();
let response = app.oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let set_cookie = response.headers().get(header::SET_COOKIE).unwrap();
let set_cookie = set_cookie.to_str().unwrap();
assert!(set_cookie.contains("voter_id="));
assert!(!set_cookie.contains("not-a-uuid"));
assert_eq!(get_vote_count_for_slug(slug, &db).await, 1);
delete_votes_for_slug(slug, &db).await.unwrap();
}
#[tokio::test]
async fn delete_vote_removes_vote_for_cookie_voter() {
let db = test_pool().await;
let slug = "delete_vote_handler_removes_vote_for_cookie_voter";
let voter_id = Uuid::from_u128(0xdef);
delete_votes_for_slug(slug, &db).await.unwrap();
insert_new_vote(slug, &voter_id, &db).await.unwrap();
let app = crate::app(test_state(db.clone()));
let request = Request::builder()
.method("DELETE")
.uri(format!("/posts/{slug}/vote"))
.header(header::COOKIE, format!("voter_id={voter_id}"))
.body(Body::empty())
.unwrap();
let response = app.oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = to_bytes(response.into_body(), usize::MAX).await.unwrap();
assert_eq!(&body[..], b"Successfully deleted vote");
assert_eq!(get_vote_count_for_slug(slug, &db).await, 0);
delete_votes_for_slug(slug, &db).await.unwrap();
}
#[tokio::test]
async fn delete_vote_initializes_cookie_when_missing() {
let db = test_pool().await;
let slug = "delete_vote_handler_initializes_cookie_when_missing";
delete_votes_for_slug(slug, &db).await.unwrap();
let app = crate::app(test_state(db.clone()));
let request = Request::builder()
.method("DELETE")
.uri(format!("/posts/{slug}/vote"))
.body(Body::empty())
.unwrap();
let response = app.oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::OK);
assert!(response.headers().contains_key(header::SET_COOKIE));
assert_eq!(get_vote_count_for_slug(slug, &db).await, 0);
delete_votes_for_slug(slug, &db).await.unwrap();
}
#[tokio::test]
async fn get_votes_and_voted_returns_count_and_voted_true_for_existing_vote() {
let db = test_pool().await;
let slug = "get_votes_and_voted_handler_returns_existing_vote";
let voter_id = Uuid::from_u128(0x123);
let other_voter_id = Uuid::from_u128(0x456);
delete_votes_for_slug(slug, &db).await.unwrap();
insert_new_vote(slug, &voter_id, &db).await.unwrap();
insert_new_vote(slug, &other_voter_id, &db).await.unwrap();
let app = crate::app(test_state(db.clone()));
let request = Request::builder()
.method("GET")
.uri(format!("/posts/{slug}/votes"))
.header(header::COOKIE, format!("voter_id={voter_id}"))
.body(Body::empty())
.unwrap();
let response = app.oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = to_bytes(response.into_body(), usize::MAX).await.unwrap();
let json: Value = serde_json::from_slice(&body).unwrap();
assert_eq!(json["vote_count"], 2);
assert_eq!(json["voted"], true);
delete_votes_for_slug(slug, &db).await.unwrap();
}
#[tokio::test]
async fn get_votes_and_voted_sets_cookie_and_returns_voted_false_for_new_voter() {
let db = test_pool().await;
let slug = "get_votes_and_voted_handler_sets_cookie_for_new_voter";
let existing_voter_id = Uuid::from_u128(0x789);
delete_votes_for_slug(slug, &db).await.unwrap();
insert_new_vote(slug, &existing_voter_id, &db)
.await
.unwrap();
let app = crate::app(test_state(db.clone()));
let request = Request::builder()
.method("GET")
.uri(format!("/posts/{slug}/votes"))
.body(Body::empty())
.unwrap();
let response = app.oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::OK);
assert!(response.headers().contains_key(header::SET_COOKIE));
let body = to_bytes(response.into_body(), usize::MAX).await.unwrap();
let json: Value = serde_json::from_slice(&body).unwrap();
assert_eq!(json["vote_count"], 1);
assert_eq!(json["voted"], false);
delete_votes_for_slug(slug, &db).await.unwrap();
}
} }

View file

@ -1 +1,18 @@
pub struct Vote {} use chrono::{DateTime, Utc};
use sqlx::prelude::FromRow;
use uuid::Uuid;
/// Struct representing a single vote (row) in the votes table
#[derive(Debug, FromRow)]
pub struct Vote {
pub slug: String,
pub voter_id: Uuid,
pub created_at: DateTime<Utc>,
}
/// Struct representing the best slug posts
#[derive(Debug, FromRow)]
pub struct BestSlugs {
pub slug: String,
pub vote_count: i64,
}

View file

@ -1,3 +1,279 @@
use sqlx::PgPool; use anyhow::Result;
use sqlx::{PgPool, query, query_as, query_scalar};
use uuid::Uuid;
use super::model::Vote; use crate::votes::model::BestSlugs;
pub async fn delete_votes_for_slug(slug: &str, db: &PgPool) -> Result<()> {
query("delete from votes where slug = $1")
.bind(slug)
.execute(db)
.await?;
Ok(())
}
pub async fn vote_exists(slug: &str, voter_id: &Uuid, db: &PgPool) -> Result<bool> {
let count: i64 = query_scalar("select count(*) from votes where slug=$1 and voter_id=$2")
.bind(slug)
.bind(voter_id)
.fetch_one(db)
.await?;
Ok(count > 0)
}
pub async fn insert_new_vote(slug: &str, voter_id: &Uuid, db: &PgPool) -> Result<()> {
query(
r#"insert into votes (slug, voter_id)
values ($1, $2)
on conflict (slug, voter_id) do nothing"#,
)
.bind(slug)
.bind(voter_id)
.execute(db)
.await?;
Ok(())
}
pub async fn get_vote_count_for_slug(slug: &str, db: &PgPool) -> Result<i64> {
let count: i64 = query_scalar("select count(*) from votes where slug=$1")
.bind(slug)
.fetch_one(db)
.await?;
Ok(count)
}
pub async fn get_top_n_slugs(n: i64, db: &PgPool) -> Result<Vec<BestSlugs>> {
if n > 0 {
let top_slugs = query_as::<_, BestSlugs>(
r#"select slug, count(*) AS vote_count
from votes
group by slug
order by vote_count desc
limit $1
"#,
)
.bind(n)
.fetch_all(db)
.await?;
Ok(top_slugs)
} else {
Ok(vec![])
}
}
pub async fn delete_vote(slug: &str, voter_id: &Uuid, db: &PgPool) -> Result<()> {
query("delete from votes where slug=$1 and voter_id=$2")
.bind(slug)
.bind(voter_id)
.execute(db)
.await?;
Ok(())
}
#[cfg(test)]
mod postgres_tests {
use sqlx::PgPool;
use uuid::Uuid;
use crate::{
test_helpers::db::test_pool,
votes::repository::{
delete_vote, get_top_n_slugs, get_vote_count_for_slug, insert_new_vote, vote_exists,
},
};
async fn cleanup(db: &PgPool, votes: &[(String, Uuid)]) {
for (slug, voter_id) in votes {
delete_vote(slug, voter_id, db).await.unwrap()
}
}
#[tokio::test]
pub async fn postgres_tests() {
let db = test_pool().await;
let votes = vec![
(
"postgres_tests_blog_post1".to_string(),
Uuid::from_u128(0x1),
),
(
"postgres_tests_blog_post1".to_string(),
Uuid::from_u128(0x2),
),
(
"postgres_tests_blog_post2".to_string(),
Uuid::from_u128(0x3),
),
(
"postgres_tests_blog_post2".to_string(),
Uuid::from_u128(0x4),
),
(
"postgres_tests_blog_post3".to_string(),
Uuid::from_u128(0x5),
),
(
"postgres_tests_blog_post3".to_string(),
Uuid::from_u128(0x6),
),
(
"postgres_tests_blog_post1".to_string(),
Uuid::from_u128(0x7),
),
(
"postgres_tests_blog_post1".to_string(),
Uuid::from_u128(0x8),
),
(
"postgres_tests_blog_post3".to_string(),
Uuid::from_u128(0x9),
),
];
cleanup(&db, &votes).await;
for (slug, voter_id) in votes.iter() {
insert_new_vote(slug, voter_id, &db)
.await
.expect("Insertions to db failed");
}
assert_eq!(
get_vote_count_for_slug("postgres_tests_blog_post1", &db)
.await
.unwrap(),
4
);
assert_eq!(
get_vote_count_for_slug("postgres_tests_blog_post2", &db)
.await
.unwrap(),
2
);
assert_eq!(
get_vote_count_for_slug("postgres_tests_blog_post3", &db)
.await
.unwrap(),
3
);
let top_2 = get_top_n_slugs(2, &db).await.unwrap();
assert_eq!(top_2[0].slug, "postgres_tests_blog_post1");
assert_eq!(top_2[1].slug, "postgres_tests_blog_post3");
delete_vote(&votes[4].0, &votes[4].1, &db).await.unwrap();
delete_vote(&votes[5].0, &votes[5].1, &db).await.unwrap();
assert_eq!(
get_vote_count_for_slug("postgres_tests_blog_post1", &db)
.await
.unwrap(),
4
);
assert_eq!(
get_vote_count_for_slug("postgres_tests_blog_post2", &db)
.await
.unwrap(),
2
);
assert_eq!(
get_vote_count_for_slug("postgres_tests_blog_post3", &db)
.await
.unwrap(),
1
);
let top_2 = get_top_n_slugs(2, &db).await.unwrap();
assert_eq!(top_2[0].slug, "postgres_tests_blog_post1");
assert_eq!(top_2[1].slug, "postgres_tests_blog_post2");
cleanup(&db, &votes).await;
}
#[tokio::test]
pub async fn insert_idempotency_test() {
let db = test_pool().await;
let votes = vec![(
"insert_idempotency_test_blog_post1".to_string(),
Uuid::from_u128(0x1),
)];
cleanup(&db, &votes).await;
insert_new_vote(&votes[0].0, &votes[0].1, &db)
.await
.unwrap();
insert_new_vote(&votes[0].0, &votes[0].1, &db)
.await
.unwrap();
insert_new_vote(&votes[0].0, &votes[0].1, &db)
.await
.unwrap();
insert_new_vote(&votes[0].0, &votes[0].1, &db)
.await
.unwrap();
insert_new_vote(&votes[0].0, &votes[0].1, &db)
.await
.unwrap();
insert_new_vote(&votes[0].0, &votes[0].1, &db)
.await
.unwrap();
let votes_count = get_vote_count_for_slug(&votes[0].0, &db).await.unwrap();
assert_eq!(votes_count, 1);
cleanup(&db, &votes).await;
}
#[tokio::test]
pub async fn delete_idempotency_test() {
let db = test_pool().await;
let votes = vec![(
"delete_idempotency_test_blog_post1".to_string(),
Uuid::from_u128(0x1),
)];
cleanup(&db, &votes).await;
insert_new_vote(&votes[0].0, &votes[0].1, &db)
.await
.unwrap();
delete_vote(&votes[0].0, &votes[0].1, &db).await.unwrap();
delete_vote(&votes[0].0, &votes[0].1, &db).await.unwrap();
delete_vote(&votes[0].0, &votes[0].1, &db).await.unwrap();
let votes_count = get_vote_count_for_slug(&votes[0].0, &db).await.unwrap();
assert_eq!(votes_count, 0);
cleanup(&db, &votes).await;
}
#[tokio::test]
pub async fn vote_exists_test() {
let db = test_pool().await;
let votes = vec![
(
"vote_exists_test_blog_post1".to_string(),
Uuid::from_u128(0x1),
),
(
"vote_exists_test_blog_post2".to_string(),
Uuid::from_u128(0x2),
),
(
"vote_exists_test_blog_post3".to_string(),
Uuid::from_u128(0x3),
),
];
cleanup(&db, &votes).await;
insert_new_vote(&votes[0].0, &votes[0].1, &db)
.await
.unwrap();
insert_new_vote(&votes[1].0, &votes[1].1, &db)
.await
.unwrap();
assert!(vote_exists(&votes[0].0, &votes[0].1, &db).await.unwrap());
assert!(vote_exists(&votes[1].0, &votes[1].1, &db).await.unwrap());
assert!(!vote_exists(&votes[2].0, &votes[2].1, &db).await.unwrap());
}
}

View file

@ -1,3 +1,19 @@
use crate::state::AppState; use anyhow::Result;
use sqlx::PgPool;
use uuid::Uuid;
use super::dto::{CreateVoteRequest, VoteResponse}; use crate::votes::{
dto::VotesAndDidVote,
repository::{get_vote_count_for_slug, vote_exists},
};
pub async fn get_votes_and_voted(
slug: &str,
voter_id: &Uuid,
db: &PgPool,
) -> Result<VotesAndDidVote> {
let vote_count = get_vote_count_for_slug(slug, db).await?;
let voted = vote_exists(slug, voter_id, db).await?;
Ok(VotesAndDidVote { vote_count, voted })
}