diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 36aad501..050039d1 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -25,7 +25,7 @@ jobs: - name: Install build dependencies run: | sudo apt-get update - sudo apt-get install -y libssl-dev pkg-config protobuf-compiler + sudo apt-get install -y libssl-dev pkg-config protobuf-compiler docker-compose - name: Check code formating uses: mbrobbel/rustfmt-check@master @@ -44,10 +44,7 @@ jobs: ignore: RUSTSEC-2023-0071 # patch not available att - name: Setup and Run Tests - run: | - cargo install cargo-make - cargo install dotenvy --features=cli - cargo make full-test + run: make ci-test publish: if: github.event_name == 'push' diff --git a/.gitignore b/.gitignore index ca23bcbe..59a52478 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .vscode .idea /target +mock_server/target .env .env_test .DS_STORE diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 69314d78..5814ea79 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -70,78 +70,71 @@ Once merged to the main branch, `po` files and any documentation change will be ### Setup on Ubuntu -```shell +```bash # Install the Rust toolchain curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal source "$HOME/.cargo/env" # Install build-time dependencies -sudo apt update -sudo apt install -y git gcc libssl-dev pkg-config protobuf-compiler +sudo apt-get update +sudo apt-get install -y git gcc libssl-dev pkg-config protobuf-compiler -# (Optional) Install Docker for running the OCI image +# Install Docker for running the OCI image and integration tests +sudo apt-get docker-compose +sudo usermod -aG docker $USER +newgrp docker + +# Alternatively, if you wish to install docker using the script provided by +# Docker themselves: curl -fsSL https://get.docker.com -o /tmp/get-docker.sh sh /tmp/get-docker.sh - -# For running the dotenv files -cargo install dotenvy --features=cli ``` -### Building and running the binaries - -To build and run the binary during development: - -```shell -dotenvy -f .env_files/example.env docker compose up -d #or podman-compose up -# Build and run -dotenvy -f .env_files/example.env cargo run --release -``` +### Building and running the server -To _just_ build the binary you can run `cargo build --release`. The result will be placed at -`./target/release/ratings`. +For local development there is a docker-compose file that can be used to run +the server alongside a local Postgres database for manual tesing and execution +of the integration test suite. The `Makefile` in the root of the repo has a +number of targets for running common actions within the project: -### About the testsuite +```bash +# To build the release artifact (located in ./target/release/ratings) +make build -The project includes a comprehensive testsuite made of unit and integration tests. All the tests must pass before the review is considered. If you have troubles with the testsuite, feel free to mention it on your PR description. +# To start the local stack: +make up -Currently (but to be changed) this test suite makes use of `cargo-make` and `docker` to coordinate tests. +# To stop the local stack +make down -To install these dependencies: +# To run only the unit tests +make test -``` -# Install cargo-make -cargo install cargo-make +# To run only the integration tests (requires the local stack to be up) +make integration-test -# Install docker -curl -fsSL https://get.docker.com -o /tmp/get-docker.sh -sh /tmp/get-docker.sh +# To run all tests (requires the local stack to be up) +make test-all ``` -Tests are located under the `tests/` folder and the coordination scripts are located in the `Makefile.toml` file. - -These tests require a database to run against. The easiest way to set up the database, run the tests and clean up is via the following commands: - -``` -# Run the tests -cargo make full-test +### About the testsuite -# Clean up docker images and build artifacts -cargo make full-clean -``` +The project includes a comprehensive testsuite made of unit and integration +tests. All the tests must pass before the review is considered. If you have +troubles with the testsuite, feel free to mention it on your PR description. -The test suite must pass before merging the PR to our main branch. Any new feature, change or fix must be covered by corresponding tests. -Also please note that the `category` suite will take *quite a while* to finish, so be patient with it or skip it by manually running the tests you need with `cargo test --test ` if you're not touching the category feature. +Unit tests are located within the files containing the code they are testing. -Note that the above won't work if you use `podman` (unless you've put in effort to alias docker commands to `podman` and `podman-compose`), -alternatively you can use: +Integration tests located in `./tests/` and run against the local docker-compose +stack (see the [Building and running the server](#building-and-running-the-server) +section above for details). -``` -dotenvy -f .env_files/test.env podman-compose up -dotenvy -f .env_files/test-server.env cargo run -dotenvy -f .env_files/test.env cargo test -``` +The test suite must pass before merging the PR to our main branch. Any new +feature, change or fix must be covered by corresponding tests. -In separate tabs (or `tmux` sessions etc), so long as you have the Docker repositories added as a `podman` source. +For more information on writing Rust tests, the following likes are useful: + - + - ### Code style @@ -149,7 +142,8 @@ This project follow the [rust style-guide](https://doc.rust-lang.org/1.0.0/style ## Contributor License Agreement -It is required to sign the [Contributor License Agreement](https://ubuntu.com/legal/contributors) in order to contribute to this project. +It is required to sign the [Contributor License Agreement](https://ubuntu.com/legal/contributors) +in order to contribute to this project. An automated test is executed on PRs to check if it has been accepted. @@ -157,4 +151,5 @@ This project is covered by [THIS LICENSE](LICENSE). ## Getting Help -Join us in the [Ubuntu Community](https://discourse.ubuntu.com/c/desktop/8) and post your question there with a descriptive tag. +Join us in the [Ubuntu Community](https://discourse.ubuntu.com/c/desktop/8) and +post your question there with a descriptive tag. diff --git a/Cargo.lock b/Cargo.lock index ae2dff3c..12bd2801 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -59,60 +59,11 @@ dependencies = [ "libc", ] -[[package]] -name = "anstream" -version = "0.6.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" - -[[package]] -name = "anstyle-parse" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" -dependencies = [ - "windows-sys 0.52.0", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" -dependencies = [ - "anstyle", - "windows-sys 0.52.0", -] - [[package]] name = "anyhow" -version = "1.0.86" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" [[package]] name = "argon2" @@ -295,28 +246,12 @@ dependencies = [ "generic-array", ] -[[package]] -name = "bstr" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "bumpalo" version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" -[[package]] -name = "bytecount" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce" - [[package]] name = "byteorder" version = "1.5.0" @@ -359,53 +294,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "clap" -version = "4.5.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", - "terminal_size", -] - -[[package]] -name = "clap_derive" -version = "4.5.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" - -[[package]] -name = "colorchoice" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" - [[package]] name = "concurrent-queue" version = "2.5.0" @@ -415,19 +303,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "console" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" -dependencies = [ - "encode_unicode", - "lazy_static", - "libc", - "unicode-width", - "windows-sys 0.52.0", -] - [[package]] name = "const-oid" version = "0.9.6" @@ -474,25 +349,6 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" -[[package]] -name = "crossbeam-deque" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "crossbeam-queue" version = "0.3.11" @@ -518,92 +374,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "cucumber" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5063d8cf24f4998ad01cac265da468a15ca682a8f4f826d50e661964e8d9b8" -dependencies = [ - "anyhow", - "async-trait", - "clap", - "console", - "crossbeam-utils", - "cucumber-codegen", - "cucumber-expressions", - "derive_more", - "drain_filter_polyfill", - "either", - "futures", - "gherkin", - "globwalk", - "humantime", - "inventory", - "itertools", - "lazy-regex", - "linked-hash-map", - "once_cell", - "pin-project", - "regex", - "sealed", - "serde", - "serde_json", - "smart-default", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "cucumber-codegen" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01091e28d1f566c8b31b67948399d2efd6c0a8f6228a9785519ed7b73f7f0aef" -dependencies = [ - "cucumber-expressions", - "inflections", - "itertools", - "proc-macro2", - "quote", - "regex", - "syn", - "synthez", -] - -[[package]] -name = "cucumber-expressions" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d794fed319eea24246fb5f57632f7ae38d61195817b7eb659455aa5bdd7c1810" -dependencies = [ - "derive_more", - "either", - "nom", - "nom_locate", - "regex", - "regex-syntax 0.7.5", -] - -[[package]] -name = "deadpool" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb84100978c1c7b37f09ed3ce3e5f843af02c2a2c431bae5b19230dad2c1b490" -dependencies = [ - "async-trait", - "deadpool-runtime", - "num_cpus", - "tokio", -] - -[[package]] -name = "deadpool-runtime" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" -dependencies = [ - "tokio", -] - [[package]] name = "der" version = "0.7.9" @@ -624,17 +394,6 @@ dependencies = [ "powerfmt", ] -[[package]] -name = "derive_more" -version = "0.99.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "digest" version = "0.10.7" @@ -653,12 +412,6 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" -[[package]] -name = "drain_filter_polyfill" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "669a445ee724c5c69b1b06fe0b63e70a1c84bc9bb7d9696cd4f4e3ec45050408" - [[package]] name = "either" version = "1.13.0" @@ -668,12 +421,6 @@ dependencies = [ "serde", ] -[[package]] -name = "encode_unicode" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" - [[package]] name = "encoding_rs" version = "0.8.34" @@ -912,23 +659,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "gherkin" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20b79820c0df536d1f3a089a2fa958f61cb96ce9e0f3f8f507f5a31179567755" -dependencies = [ - "heck 0.4.1", - "peg", - "quote", - "serde", - "serde_json", - "syn", - "textwrap", - "thiserror", - "typed-builder", -] - [[package]] name = "gimli" version = "0.29.0" @@ -948,30 +678,6 @@ dependencies = [ "url", ] -[[package]] -name = "globset" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" -dependencies = [ - "aho-corasick", - "bstr", - "log", - "regex-automata 0.4.7", - "regex-syntax 0.8.4", -] - -[[package]] -name = "globwalk" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" -dependencies = [ - "bitflags 1.3.2", - "ignore", - "walkdir", -] - [[package]] name = "h2" version = "0.3.26" @@ -1035,12 +741,6 @@ dependencies = [ "hashbrown 0.14.5", ] -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - [[package]] name = "heck" version = "0.5.0" @@ -1154,12 +854,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - [[package]] name = "hyper" version = "0.14.30" @@ -1302,22 +996,6 @@ dependencies = [ "unicode-normalization", ] -[[package]] -name = "ignore" -version = "0.4.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1" -dependencies = [ - "crossbeam-deque", - "globset", - "log", - "memchr", - "regex-automata 0.4.7", - "same-file", - "walkdir", - "winapi-util", -] - [[package]] name = "indexmap" version = "1.9.3" @@ -1338,30 +1016,12 @@ dependencies = [ "hashbrown 0.14.5", ] -[[package]] -name = "inflections" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a257582fdcde896fd96463bf2d40eefea0580021c0712a0e2b028b60b47a837a" - -[[package]] -name = "inventory" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f958d3d68f4167080a18141e10381e7634563984a537f2a49a30fd8e53ac5767" - [[package]] name = "ipnet" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" -[[package]] -name = "is_terminal_polyfill" -version = "1.70.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" - [[package]] name = "itertools" version = "0.12.1" @@ -1410,29 +1070,6 @@ dependencies = [ "simple_asn1", ] -[[package]] -name = "lazy-regex" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d8e41c97e6bc7ecb552016274b99fbb5d035e8de288c582d9b933af6677bfda" -dependencies = [ - "lazy-regex-proc_macros", - "once_cell", - "regex", -] - -[[package]] -name = "lazy-regex-proc_macros" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76e1d8b05d672c53cb9c7b920bbba8783845ae4f0b076e02a3db1d02c81b4163" -dependencies = [ - "proc-macro2", - "quote", - "regex", - "syn", -] - [[package]] name = "lazy_static" version = "1.5.0" @@ -1489,12 +1126,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "linked-hash-map" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" - [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -1617,17 +1248,6 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "nom_locate" -version = "4.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e3c83c053b0713da60c5b8de47fe8e494fe3ece5267b2f23090a07a053ba8f3" -dependencies = [ - "bytecount", - "memchr", - "nom", -] - [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1701,16 +1321,6 @@ dependencies = [ "libm", ] -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "libc", -] - [[package]] name = "object" version = "0.36.3" @@ -1822,33 +1432,6 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" -[[package]] -name = "peg" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f76678828272f177ac33b7e2ac2e3e73cc6c1cd1e3e387928aa69562fa51367" -dependencies = [ - "peg-macros", - "peg-runtime", -] - -[[package]] -name = "peg-macros" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "636d60acf97633e48d266d7415a9355d4389cea327a193f87df395d88cd2b14d" -dependencies = [ - "peg-runtime", - "proc-macro2", - "quote", -] - -[[package]] -name = "peg-runtime" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555b1514d2d99d78150d3c799d4c357a3e2c2a8062cd108e93a06d9057629c5" - [[package]] name = "pem" version = "3.0.4" @@ -2041,7 +1624,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" dependencies = [ "bytes", - "heck 0.5.0", + "heck", "itertools", "log", "multimap", @@ -2120,11 +1703,11 @@ dependencies = [ name = "ratings" version = "0.0.3" dependencies = [ + "anyhow", "argon2", "axum", "base64 0.22.1", "chrono", - "cucumber", "dotenvy", "envy", "futures", @@ -2145,7 +1728,7 @@ dependencies = [ "serde", "serde_json", "sha2", - "snapd", + "simple_test_case", "sqlx", "strum", "thiserror", @@ -2216,12 +1799,6 @@ version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" -[[package]] -name = "regex-syntax" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" - [[package]] name = "regex-syntax" version = "0.8.4" @@ -2378,15 +1955,6 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - [[package]] name = "schannel" version = "0.1.23" @@ -2402,18 +1970,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sealed" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a8caec23b7800fb97971a1c6ae365b6239aaeddfb934d6265f8505e795699d" -dependencies = [ - "heck 0.4.1", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "secrecy" version = "0.8.0" @@ -2569,6 +2125,17 @@ dependencies = [ "time", ] +[[package]] +name = "simple_test_case" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0649fa40b80dcacda1cabd018fd47b6b0c7fbbda6e1c3f658a6c4d5926500a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "siphasher" version = "0.3.11" @@ -2593,41 +2160,6 @@ dependencies = [ "serde", ] -[[package]] -name = "smart-default" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eb01866308440fc64d6c44d9e86c5cc17adfe33c4d6eed55da9145044d0ffc1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "smawk" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" - -[[package]] -name = "snapd" -version = "0.1.0" -source = "git+https://github.com/ZoopOTheGoop/snapd-rs?branch=framework#f4b67567f59dddfd014cb526f27e2dbc0d37db50" -dependencies = [ - "async-trait", - "deadpool", - "http 1.1.0", - "http-body-util", - "hyper 1.4.1", - "pin-project", - "serde", - "serde_json", - "thiserror", - "tokio", - "url", -] - [[package]] name = "socket2" version = "0.5.7" @@ -2744,7 +2276,7 @@ checksum = "3d100558134176a2629d46cec0c8891ba0be8910f7896abfdb75ef4ab6f4e7ce" dependencies = [ "dotenvy", "either", - "heck 0.5.0", + "heck", "hex", "once_cell", "proc-macro2", @@ -2879,12 +2411,6 @@ dependencies = [ "unicode-properties", ] -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - [[package]] name = "strum" version = "0.26.3" @@ -2900,7 +2426,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", "rustversion", @@ -2939,39 +2465,6 @@ dependencies = [ "futures-core", ] -[[package]] -name = "synthez" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3d2c2202510a1e186e63e596d9318c91a8cbe85cd1a56a7be0c333e5f59ec8d" -dependencies = [ - "syn", - "synthez-codegen", - "synthez-core", -] - -[[package]] -name = "synthez-codegen" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f724aa6d44b7162f3158a57bccd871a77b39a4aef737e01bcdff41f4772c7746" -dependencies = [ - "syn", - "synthez-core", -] - -[[package]] -name = "synthez-core" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bfa6ec52465e2425fd43ce5bbbe0f0b623964f7c63feb6b10980e816c654ea" -dependencies = [ - "proc-macro2", - "quote", - "sealed", - "syn", -] - [[package]] name = "system-configuration" version = "0.6.1" @@ -3006,27 +2499,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "terminal_size" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" -dependencies = [ - "rustix", - "windows-sys 0.48.0", -] - -[[package]] -name = "textwrap" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" -dependencies = [ - "smawk", - "unicode-linebreak", - "unicode-width", -] - [[package]] name = "thiserror" version = "1.0.63" @@ -3366,26 +2838,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "typed-builder" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe83c85a85875e8c4cb9ce4a890f05b23d38cd0d47647db7895d3d2a79566d2" -dependencies = [ - "typed-builder-macro", -] - -[[package]] -name = "typed-builder-macro" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29a3151c41d0b13e3d011f98adc24434560ef06673a155a6c7f66b9879eecce2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "typenum" version = "1.17.0" @@ -3404,12 +2856,6 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" -[[package]] -name = "unicode-linebreak" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" - [[package]] name = "unicode-normalization" version = "0.1.23" @@ -3425,12 +2871,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52ea75f83c0137a9b98608359a5f1af8144876eb67bcb1ce837368e906a9f524" -[[package]] -name = "unicode-width" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" - [[package]] name = "unicode_categories" version = "0.1.1" @@ -3454,12 +2894,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - [[package]] name = "valuable" version = "0.1.0" @@ -3478,16 +2912,6 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - [[package]] name = "want" version = "0.3.1" @@ -3622,15 +3046,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -[[package]] -name = "winapi-util" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" -dependencies = [ - "windows-sys 0.59.0", -] - [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index e2084972..c56eb9fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,8 +30,8 @@ rand = "0.8" reqwest = "0.12" secrecy = { version = "0.8.0", features = ["serde"] } serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0.114" sha2 = "0.10" -snapd = { git = "https://github.com/ZoopOTheGoop/snapd-rs", branch = "framework" } sqlx = { version = "0.8", features = [ "runtime-tokio-rustls", "postgres", @@ -47,33 +47,13 @@ tonic-reflection = "0.10" tower = "0.4" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } + [build-dependencies] git2 = { version = "0.18.2", default-features = false } tonic-build = { version = "0.11", features = ["prost"] } [dev-dependencies] -cucumber = { version = "0.20.2", features = ["libtest", "tracing"] } +anyhow = "1.0.89" lazy_static = "1.4.0" regex = "1.10.3" -serde_json = "1.0.114" - - -[[test]] -name = "voting" -harness = false - -[[test]] -name = "authentication" -harness = false - -[[test]] -name = "chart" -harness = false - -[[test]] -name = "log_level" -harness = false - -[[test]] -name = "api_info" -harness = false +simple_test_case = "1.2.0" diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..5e2c9703 --- /dev/null +++ b/Makefile @@ -0,0 +1,67 @@ +.PHONY: build +build: + @cargo build --release + +.PHONY: up +up: + @docker-compose up + +.PHONY: up-detached +up-detached: + @docker-compose up --detach + +.PHONY: down +down: + @docker-compose down + +.PHONY: test +test: + @cargo test --lib + +.PHONY: integration-test +integration-test: clear-db-data + @APP_JWT_SECRET='deadbeef' \ + MOCK_ADMIN_URL='http://127.0.0.1:11111/__admin__/register-snap' \ + HOST='0.0.0.0' \ + PORT='8080' \ + cargo test --test '*' + +.PHONY: test-all +test-all: test integration-test + +.PHONY: wait-for-server +wait-for-server: + @echo "Waiting for ratings server to start on port 8080..." + @until docker-compose ps | grep -E '^ratings\s' | grep healthy; do \ + echo "..."; \ + sleep 1; \ + done + +.PHONY: ci-test +ci-test: up-detached wait-for-server test-all down + +.PHONY: clear-db-data +clear-db-data: + @docker-compose exec -T db psql -U postgres ratings < tests/clear-db.sql + +.PHONY: rebuild-local +rebuild-local: + @docker-compose build + +.PHONY: rm-db +rm-db: + @docker-compose rm db + @docker volume rm -f postgres_data + +.PHONY: rm-volumes +rm-volumes: + @docker volume rm -f target-cache + @docker volume rm -f cargo-cache + +.PHONY: db-shell +db-shell: + @docker exec -it ratings-db psql -U postgres ratings + +.PHONY: ratings-shell +ratings-shell: + @docker exec -it ratings bash diff --git a/Makefile.toml b/Makefile.toml deleted file mode 100644 index 7e4a4f7d..00000000 --- a/Makefile.toml +++ /dev/null @@ -1,46 +0,0 @@ -[tasks.db-up] -script = ["docker compose up --detach", "sleep 3"] - -[tasks.db-up-test] -script = [ - "dotenvy -f .env_files/test.env docker compose up --detach", - "sleep 3", -] - -[tasks.run-server] -script = ["cargo run &", "echo $! > server.pid"] - -[tasks.run-server-test] -script = [ - "dotenvy -f .env_files/test-server.env cargo run &", - "echo $! > server.pid", -] - -[tasks.run-tests] -script = ["cargo test", "sleep 1"] - -[tasks.wait-for-server] -script = [ - "until nc -z -v -w5 localhost 8080; do", - " echo 'Waiting for server to start on port 8080...'", - " sleep 1", - "done", -] - -[tasks.kill-server] -script = ["kill $(cat server.pid)", "rm server.pid"] - -[tasks.db-down] -script = ["docker compose down"] - -[tasks.full-test] -dependencies = [ - "db-up-test", - "run-server-test", - "wait-for-server", - "run-tests", - "kill-server", -] - -[tasks.full-clean] -script = ["docker rmi ratings-postgres --force", "cargo clean"] diff --git a/README.md b/README.md index 031353dd..8bdc6054 100644 --- a/README.md +++ b/README.md @@ -12,23 +12,32 @@ [![License][license-image]](LICENSE) -This is the code repository for **Ratings**, the backend service responsible for managing the new vote and rating system used in [App Center](https://github.com/ubuntu/app-center) +This is the code repository for **Ratings**, the backend service responsible +for managing the new vote and rating system used in [App Center](https://github.com/ubuntu/app-center) -For general details, including installation, getting started and setting up a development environment, head over to our section on [Contributing to the code](CONTRIBUTING.md#contributing-to-the-code). +For general details, including installation, getting started and setting up a +development environment, please see [CONTRIBUTING.md](CONTRIBUTING.md). ## Dependencies -In order to run, this needs you to install at minimum `libssl-dev` (for OpenSSL) and `protobuf-compiler` (for `prost`) via `apt`. +In order to run, this needs you to install at minimum `libssl-dev` (for +OpenSSL) and `protobuf-compiler` (for `prost`) via `apt`. + +Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details. ## Get involved -This is an [open source](LICENSE) project and we warmly welcome community contributions, suggestions, and constructive feedback. If you're interested in contributing, please take a look at our [Contribution guidelines](CONTRIBUTING.md) first. +This is an [open source](LICENSE) project and we warmly welcome community +contributions, suggestions, and constructive feedback. If you're interested in +contributing, please take a look at our [Contribution guidelines](CONTRIBUTING.md) first. - To report an issue, please file a bug report against our repository. - For suggestions and constructive feedback, please file a feature request or a bug report. ## Get in touch -We're friendly! We have a community forum at [https://discourse.ubuntu.com](https://discourse.ubuntu.com) where we discuss feature plans, development news, issues, updates and troubleshooting. +We're friendly! We have a community forum at [https://discourse.ubuntu.com](https://discourse.ubuntu.com) +where we discuss feature plans, development news, issues, updates and troubleshooting. -For news and updates, follow the [Ubuntu twitter account](https://twitter.com/ubuntu) and on [Facebook](https://www.facebook.com/ubuntu). +For news and updates, follow the [Ubuntu twitter account](https://twitter.com/ubuntu) +and on [Facebook](https://www.facebook.com/ubuntu). diff --git a/build.rs b/build.rs index e44a24c7..8c7a94a5 100644 --- a/build.rs +++ b/build.rs @@ -1,4 +1,4 @@ -use git2::Repository; +// use git2::Repository; use std::path::Path; fn init_proto() -> Result<(), Box> { @@ -23,7 +23,10 @@ fn init_proto() -> Result<(), Box> { .build_server(true) .file_descriptor_set_path(descriptor_set_path) .out_dir(out_dir) - .type_attribute("Category", "#[derive(sqlx::Type, strum::EnumString)]") + .type_attribute( + "Category", + "#[derive(sqlx::Type, strum::EnumString, strum::Display)]", + ) .type_attribute( "Category", r#"#[strum(serialize_all = "kebab_case", ascii_case_insensitive)]"#, @@ -33,25 +36,29 @@ fn init_proto() -> Result<(), Box> { Ok(()) } -fn include_build_info() -> Result<(), Box> { - let repo = Repository::open(std::env::current_dir()?)?; - let head = repo.head()?; - let branch = head - .name() - .unwrap() - .strip_prefix("refs/heads/") - .unwrap_or("no-branch"); - println!("cargo:rustc-env=GIT_BRANCH={}", branch); - - let commit_sha = repo.head()?.target().unwrap(); - println!("cargo:rustc-env=GIT_HASH={}", commit_sha); - - Ok(()) -} +// fn include_build_info() -> Result<(), Box> { +// let repo = Repository::open(std::env::current_dir()?)?; +// let head = repo.head()?; +// let branch = head +// .name() +// .unwrap() +// .strip_prefix("refs/heads/") +// .unwrap_or("no-branch"); +// println!("cargo:rustc-env=GIT_BRANCH={}", branch); +// +// let commit_sha = repo.head()?.target().unwrap(); +// println!("cargo:rustc-env=GIT_HASH={}", commit_sha); +// +// Ok(()) +// } fn main() -> Result<(), Box> { init_proto()?; - include_build_info()?; + // Running this in docker compose breaks the build and we only need it for + // the admin API we are planning on ripping out anyway + // include_build_info()?; + println!("cargo:rustc-env=GIT_BRANCH=branch"); + println!("cargo:rustc-env=GIT_HASH=hash"); Ok(()) } diff --git a/docker-compose.yml b/docker-compose.yml index 76b53dbf..ab327e77 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,16 +1,65 @@ -version: "3" +version: "3.5" services: - postgres: + db: + container_name: ratings-db build: ./docker/database restart: always ports: - - "5433:5432" + - "5432:5432" environment: - POSTGRES_USER: ${DOCKER_POSTGRES_USER} - POSTGRES_PASSWORD: ${DOCKER_POSTGRES_PASSWORD} - MIGRATION_USER: ${DOCKER_MIGRATION_USER} - MIGRATION_PASSWORD: ${DOCKER_MIGRATION_PASSWORD} - SERVICE_USER: ${DOCKER_SERVICE_USER} - SERVICE_PASSWORD: ${DOCKER_SERVICE_PASSWORD} - RATINGS_DB: ${DOCKER_RATINGS_DB} + POSTGRES_USER: "postgres" + POSTGRES_PASSWORD: "@1234" + PGPASSWORD: "@1234" # Used by "sudo make db-shell" + MIGRATION_USER: "migration_user" + MIGRATION_PASSWORD: "strongpassword" + SERVICE_USER: "service" + SERVICE_PASSWORD: "covfefe!1" + RATINGS_DB: "ratings" + + ratings: + container_name: ratings + build: ./docker/server + ports: + - 8080:8080 + environment: + RUST_LOG: "info,hyper=error" + APP_LOG_LEVEL: "info" + APP_ENV: "dev" + APP_HOST: "0.0.0.0" + APP_JWT_SECRET: "deadbeef" + APP_NAME: "ratings" + APP_PORT: "8080" + APP_POSTGRES_URI: "postgresql://migration_user:strongpassword@db:5432/ratings" + APP_SNAPCRAFT_IO_URI: "http://snapcraft-mock:11111/" + #APP_SNAPCRAFT_IO_URI: "https://api.snapcraft.io/v2/" + APP_ADMIN_USER: "shadow" + APP_ADMIN_PASSWORD: "maria" + volumes: + - .:/app + - cargo-cache:/usr/local/cargo/registry + - target-cache:/app/target + entrypoint: "cargo watch -i 'tests/**' -x run" + depends_on: + - db + healthcheck: + test: [ "CMD-SHELL", "nc -z -w5 localhost 8080" ] + interval: 5s + retries: 50 + + snapcraft-mock: + container_name: snapcraft-mock + build: ./docker/server + ports: + - 11111:11111 + volumes: + - ./mock_server/:/app + - cargo-cache:/usr/local/cargo/registry + - target-cache:/app/target + environment: + RUST_LOG: "info,hyper=error" + entrypoint: "cargo watch -i 'tests/**' -x run" + +volumes: + cargo-cache: null + target-cache: null diff --git a/docker/ratings/Dockerfile b/docker/ratings/Dockerfile deleted file mode 100644 index de6f6a3c..00000000 --- a/docker/ratings/Dockerfile +++ /dev/null @@ -1,10 +0,0 @@ -FROM ubuntu:latest - -RUN DEBIAN_FRONTEND=noninteractive apt-get update && apt-get upgrade -y && rm -rf /var/lib/apt/lists/* - -WORKDIR /app -COPY target/release/ratings /app/ratings -COPY sql/migrations /app/sql/migrations - -EXPOSE 8080 -ENTRYPOINT ["/app/ratings"] diff --git a/docker/server/Dockerfile b/docker/server/Dockerfile new file mode 100644 index 00000000..18361c97 --- /dev/null +++ b/docker/server/Dockerfile @@ -0,0 +1,8 @@ +FROM rust:1.81.0 + +RUN apt-get update && apt-get install -y protobuf-compiler netcat-openbsd +RUN cargo install cargo-watch + +WORKDIR /app + +ENTRYPOINT ["cargo watch -i 'tests/**' -x run"] diff --git a/mock_server/Cargo.lock b/mock_server/Cargo.lock new file mode 100644 index 00000000..46cd5efd --- /dev/null +++ b/mock_server/Cargo.lock @@ -0,0 +1,843 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" + +[[package]] +name = "async-trait" +version = "0.1.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "axum" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "504e3947307ac8326a5437504c517c4b56716c9d98fac0028c2acc7ca47d70ae" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 1.0.1", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "bytes" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.159" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi", + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "mock_server" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum", + "serde", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", + "uuid", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "proc-macro2" +version = "1.0.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.8", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustversion" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "serde" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "syn" +version = "2.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tokio" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +dependencies = [ + "backtrace", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tower" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 0.1.2", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "uuid" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +dependencies = [ + "getrandom", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/mock_server/Cargo.toml b/mock_server/Cargo.toml new file mode 100644 index 00000000..84d59241 --- /dev/null +++ b/mock_server/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "mock_server" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1.0.89" +axum = "0.7.7" +serde = "1.0.210" +serde_json = "1.0.128" +tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread"] } +tracing = "0.1.40" +tracing-subscriber = { version = "0.3.18", features = ["env-filter", "fmt"] } +uuid = { version = "1.10.0", features = ["v4"] } diff --git a/mock_server/src/main.rs b/mock_server/src/main.rs new file mode 100644 index 00000000..64ebeb4d --- /dev/null +++ b/mock_server/src/main.rs @@ -0,0 +1,116 @@ +use axum::{ + extract::Path, + http::StatusCode, + response::IntoResponse, + routing::{get, post}, + Extension, Router, +}; +use serde_json::json; +use std::{ + collections::HashMap, + sync::{Arc, RwLock}, +}; +use tracing::{info, warn}; +use tracing_subscriber::EnvFilter; +use uuid::Uuid; + +const PORT: u16 = 11111; + +type State = Arc>; + +#[derive(Default, Debug)] +pub struct StateInner { + id_map: HashMap, // id -> name + categories: HashMap>, +} + +#[tokio::main] +async fn main() { + tracing_subscriber::fmt() + .compact() + .with_env_filter(EnvFilter::from_default_env()) + .init(); + + let app = Router::new() + // mocked snapcraft.io endpoints + .route( + "/assertions/snap-declaration/16/:snap_id", + get(snap_assertions), + ) + .route("/snaps/info/:snap_name", get(snap_info)) + // admin endpoint + .route("/__admin__/register-snap/:snap_id", post(register_snap)) + .layer(Extension(State::default())); + + info!("Starting mock-server"); + let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{PORT}")) + .await + .unwrap(); + + axum::serve(listener, app).await.unwrap(); +} + +async fn register_snap( + Path(snap_id): Path, + Extension(state): Extension, + categories: String, +) -> impl IntoResponse { + info!("registering snap: {snap_id} -> {categories:?}"); + let categories: Vec = categories.split(',').map(|c| c.to_string()).collect(); + let snap_name = Uuid::new_v4().to_string(); + + let mut guard = state.write().unwrap(); + guard.id_map.insert(snap_id, snap_name.clone()); + guard.categories.insert(snap_name, categories); + + (StatusCode::OK, "registered") +} + +async fn snap_assertions( + Path(snap_id): Path, + Extension(state): Extension, +) -> impl IntoResponse { + info!("getting snap assertions for {snap_id}"); + let guard = state.read().unwrap(); + + match guard.id_map.get(&snap_id) { + Some(name) => ( + StatusCode::OK, + json!({ "headers": { "snap-name": name } }).to_string(), + ), + + None => { + warn!("attempt to pull snap name for unknown id: {snap_id}"); + ( + StatusCode::NOT_FOUND, + json!({ "error": "not found" }).to_string(), + ) + } + } +} + +async fn snap_info( + Path(snap_name): Path, + Extension(state): Extension, +) -> impl IntoResponse { + info!("getting categories for {snap_name}"); + let guard = state.read().unwrap(); + + match guard.categories.get(&snap_name) { + Some(cats) => { + let categories: Vec<_> = cats.iter().map(|c| json!({ "name": c })).collect(); + ( + StatusCode::OK, + json!({ "snap": { "categories": categories } }).to_string(), + ) + } + + None => { + warn!("attempt to pull snap categories for unknown snap: {snap_name}"); + ( + StatusCode::NOT_FOUND, + json!({ "error": "not found" }).to_string(), + ) + } + } +} diff --git a/src/app/context.rs b/src/app/context.rs index 4d85c8de..dafa4209 100644 --- a/src/app/context.rs +++ b/src/app/context.rs @@ -17,7 +17,9 @@ impl AppContext { let inner = AppContextInner { infra, config: config.clone(), + http_client: reqwest::Client::new(), }; + Self(Arc::new(inner)) } @@ -30,6 +32,11 @@ impl AppContext { pub fn config(&self) -> &Config { &self.0.config } + + /// A reference to the shared HTTP client + pub fn http_client(&self) -> &reqwest::Client { + &self.0.http_client + } } /// Contains the overall state and configuration of the app, only meant to be used @@ -40,4 +47,6 @@ struct AppContextInner { infra: Infrastructure, /// App configuration settings. config: Config, + /// An HTTP client for pulling data from snapcraft.io + http_client: reqwest::Client, } diff --git a/src/features/user/errors.rs b/src/features/user/errors.rs index 85fa4d5e..1895b004 100644 --- a/src/features/user/errors.rs +++ b/src/features/user/errors.rs @@ -1,5 +1,4 @@ //! Errors related to user voting -use snapd::SnapdClientError; use thiserror::Error; /// Errors that can occur when a user votes. @@ -8,21 +7,31 @@ pub enum UserError { /// A record could not be created for the user #[error("failed to create user record")] FailedToCreateUserRecord, + /// We were unable to delete a user with the given instance ID #[error("failed to delete user by instance id")] FailedToDeleteUserRecord, + /// We could not get a vote by a given user #[error("failed to get user vote")] FailedToGetUserVote, + /// The user was unable to cast a vote #[error("failed to cast vote")] FailedToCastVote, - /// Errors from `snapd-rs` - #[error("an error occurred when calling snapd: {0}")] - SnapdError(#[from] SnapdClientError), + /// An error that occurred in category updating #[error("an error occurred with the DB when getting categories: {0}")] CategoryDBError(#[from] sqlx::Error), + + /// An error that occurred while trying to pull data from snapcraft.io + #[error(transparent)] + SnapcraftIo(#[from] reqwest::Error), + + /// An error that occurred while trying to convert JSON data + #[error(transparent)] + Json(#[from] serde_json::Error), + /// Anything else that can go wrong #[error("unknown user error")] Unknown, diff --git a/src/features/user/infrastructure.rs b/src/features/user/infrastructure.rs index a50c7769..4db0e321 100644 --- a/src/features/user/infrastructure.rs +++ b/src/features/user/infrastructure.rs @@ -1,8 +1,5 @@ //! Infrastructure for user handling -use snapd::{ - api::{convenience::SnapNameFromId, find::FindSnapByName}, - SnapdClient, -}; +use serde::{de::DeserializeOwned, Deserialize}; use sqlx::{Acquire, Executor, Row}; use tracing::error; @@ -186,18 +183,81 @@ pub(crate) async fn save_vote_to_db(app_ctx: &AppContext, vote: Vote) -> Result< Ok(result.rows_affected()) } -/// Convenience function for getting categories by their snap ID, since it takes multiple API calls -async fn snapd_categories_by_snap_id( - client: &SnapdClient, +async fn get_json( + client: &reqwest::Client, + url: reqwest::Url, + query: &[(&str, &str)], +) -> Result { + let s = client + .get(url) + .header("User-Agent", "ratings-service") + .header("Snap-Device-Series", 16) + .query(query) + .send() + .await? + .error_for_status()? + .text() + .await?; + + Ok(serde_json::from_str(&s)?) +} + +/// Pull snap categories by for a given snapd_id from the snapcraft.io rest API +async fn get_snap_categories( snap_id: &str, + base: &str, + client: &reqwest::Client, ) -> Result, UserError> { - let snap_name = SnapNameFromId::get_name(snap_id.into(), client).await?; - - Ok(FindSnapByName::get_categories(snap_name, client) - .await? + let base_url = reqwest::Url::parse(base).map_err(|_| UserError::Unknown)?; + + let assertions_url = base_url + .join(&format!("assertions/snap-declaration/16/{snap_id}")) + .map_err(|_| UserError::Unknown)?; + let AssertionsResp { + headers: Headers { snap_name }, + } = get_json(client, assertions_url, &[]).await?; + + let info_url = base_url + .join(&format!("snaps/info/{snap_name}")) + .map_err(|_| UserError::Unknown)?; + let FindResp { + snap: SnapInfo { categories }, + } = get_json(client, info_url, &[("fields", "categories")]).await?; + + let res: Result, UserError> = categories .into_iter() - .map(|v| Category::try_from(v.name.as_ref()).expect("got unknown category?")) - .collect()) + .map(|c| Category::try_from(c.name.as_str()).map_err(|_| UserError::Unknown)) + .collect(); + + return res; + + // serde structs + + #[derive(Debug, Deserialize)] + struct AssertionsResp { + headers: Headers, + } + + #[derive(Debug, Deserialize)] + #[serde(rename_all = "kebab-case")] + struct Headers { + snap_name: String, + } + + #[derive(Debug, Deserialize)] + struct FindResp { + snap: SnapInfo, + } + + #[derive(Debug, Deserialize)] + struct SnapInfo { + categories: Vec, + } + + #[derive(Debug, Deserialize)] + struct RawCategory { + name: String, + } } /// Update the category (we do this every time we get a vote for the time being) @@ -211,9 +271,9 @@ pub(crate) async fn update_category(app_ctx: &AppContext, snap_id: &str) -> Resu UserError::Unknown })?; - let snapd_client = &app_ctx.infrastructure().snapd_client; - - let categories = snapd_categories_by_snap_id(snapd_client, snap_id).await?; + let client = app_ctx.http_client(); + let base = &app_ctx.config().snapcraft_io_uri; + let categories = get_snap_categories(snap_id, base, client).await?; // Do a transaction because bulk querying doesn't seem to work cleanly let mut tx = pool.begin().await?; @@ -299,26 +359,19 @@ pub(crate) async fn find_user_votes( } #[cfg(test)] -mod test { - use std::collections::HashSet; - - use snapd::SnapdClient; - - use crate::features::pb::chart::Category; - - use super::snapd_categories_by_snap_id; - const TESTING_SNAP_ID: &str = "3Iwi803Tk3KQwyD6jFiAJdlq8MLgBIoD"; - const TESTING_SNAP_CATEGORIES: [Category; 2] = [Category::Utilities, Category::Development]; +mod tests { + use super::*; + // Can be run explicitly to validate the behaviour of the API calls we make against + // snapcraft.io but we don't want to do this in local testing or CI by default. + #[ignore = "hits snapcraft.io"] #[tokio::test] - async fn get_categories() { - let categories = snapd_categories_by_snap_id(&SnapdClient::default(), TESTING_SNAP_ID) - .await - .unwrap(); - - assert_eq!( - TESTING_SNAP_CATEGORIES.into_iter().collect::>(), - categories.into_iter().collect::>() - ) + async fn get_snap_categories_works() { + let client = reqwest::Client::new(); + let base = "https://api.snapcraft.io/v2/"; + let snap_id = "NeoQngJVBf2wKC48bxnF2xqmfEFGdVnx"; // steam + let categories = get_snap_categories(snap_id, base, &client).await.unwrap(); + + assert_eq!(categories, vec![Category::Games]); } } diff --git a/src/lib.rs b/src/lib.rs index 07003105..f65c3c77 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,7 +5,6 @@ #![deny(rustdoc::broken_intra_doc_links)] #![warn(missing_docs)] -#![warn(clippy::missing_docs_in_private_items)] pub mod app; pub mod features; diff --git a/src/utils/config.rs b/src/utils/config.rs index ccb6fcd6..2273149d 100644 --- a/src/utils/config.rs +++ b/src/utils/config.rs @@ -20,6 +20,8 @@ pub struct Config { pub port: u16, /// The URI of the postgres database pub postgres_uri: String, + /// The base URI for snapcraft.io + pub snapcraft_io_uri: String, } impl Config { diff --git a/src/utils/infrastructure.rs b/src/utils/infrastructure.rs index e76c7d46..32a3933a 100644 --- a/src/utils/infrastructure.rs +++ b/src/utils/infrastructure.rs @@ -5,7 +5,6 @@ use std::{ sync::Arc, }; -use snapd::SnapdClient; use sqlx::{pool::PoolConnection, postgres::PgPoolOptions, PgPool, Postgres}; use tokio::sync::OnceCell; use tracing::level_filters::LevelFilter; @@ -23,8 +22,6 @@ static RELOAD_HANDLE: tokio::sync::OnceCell> = Onc pub struct Infrastructure { /// The postgres DB pub postgres: Arc, - /// The client for making snapd requests - pub snapd_client: SnapdClient, /// The reload handle for the logger pub log_reload_handle: &'static Handle, /// The utility which lets us encode user tokens with our JWT credentials @@ -50,7 +47,6 @@ impl Infrastructure { Ok(Infrastructure { postgres, jwt_encoder, - snapd_client: Default::default(), log_reload_handle: reload_handle, }) } diff --git a/src/utils/migrator.rs b/src/utils/migrator.rs index f62c0e2a..5385bded 100644 --- a/src/utils/migrator.rs +++ b/src/utils/migrator.rs @@ -24,6 +24,7 @@ impl Migrator { pub async fn new(uri: &str) -> Result> { let pool = PgPoolOptions::new().max_connections(1).connect(uri).await?; let pool = Arc::new(pool); + Ok(Migrator { pool }) } @@ -43,6 +44,7 @@ impl Migrator { sqlx::migrate::Migrator::new(std::path::Path::new(&Self::migrations_path())).await?; m.run(&mut self.pool.acquire().await?).await?; + Ok(()) } diff --git a/tests/api_info.rs b/tests/api_info.rs deleted file mode 100644 index d55cb5b7..00000000 --- a/tests/api_info.rs +++ /dev/null @@ -1,73 +0,0 @@ -use cucumber::{given, then, when, World}; -use lazy_static::lazy_static; -use ratings::{features::admin::api_version::ApiVersion, utils::Config}; -use regex::Regex; - -use helpers::client::*; - -mod helpers; - -#[derive(Clone, Debug, World)] -#[world(init = Self::new)] -struct LogWorld { - client: TestClient, - returned_info: Option>, -} - -impl LogWorld { - fn new() -> Self { - let config = Config::load().expect("could not load config"); - let client = TestClient::new(config.socket()); - Self { - client, - returned_info: None, - } - } -} - -#[given(expr = "Big doesn't know the API build info")] -fn unknown_level(world: &mut LogWorld) { - world.returned_info = None -} - -#[when(expr = "Big asks for the API info")] -async fn get_api_info(world: &mut LogWorld) { - world.returned_info = Some( - world - .client - .get_api_info() - .await - .expect("could not get API info") - .0, - ) -} - -lazy_static! { - static ref VALID_SHA: Regex = Regex::new(r"/^([a-f0-9]{64})$/").unwrap(); - static ref VALID_SEMVER: Regex = Regex::new(r"((\d+).?){0,3}").unwrap(); -} - -#[then(expr = "Big gets an answer")] -fn got_info(world: &mut LogWorld) { - assert!( - world.returned_info.is_some(), - "did not get a valid level from the endpoint" - ); - - let info = world.returned_info.as_ref().unwrap(); - VALID_SHA.is_match(&info.commit); - VALID_SEMVER.is_match(&info.version); - // The regex for a valid git branch is too absurd to even bother testing, - // and also may not even be present (e.g. build from a detached HEAD). -} - -#[tokio::main] -async fn main() { - dotenvy::from_filename(".env_files/test.env").ok(); - - LogWorld::cucumber() - .repeat_skipped() - .max_concurrent_scenarios(1) - .run_and_exit("tests/features/admin/api-info.feature") - .await -} diff --git a/tests/authentication.rs b/tests/authentication.rs index c4a44d6e..94f282aa 100644 --- a/tests/authentication.rs +++ b/tests/authentication.rs @@ -1,101 +1,32 @@ -use cucumber::{given, then, when, World}; +pub mod common; -use helpers::client::*; -use ratings::utils::{Config, Infrastructure}; -use sqlx::Row; -use tonic::{Code, Status}; +use common::TestHelper; +use simple_test_case::test_case; -mod helpers; +#[test_case("notarealhash"; "short")] +#[test_case("abcdefghijkabcdefghijkabcdefghijkabcdefghijkabcdefghijkabcdefgh"; "one char too short")] +#[test_case("abcdefghijkabcdefghijkabcdefghijkabcdefghijkabcdefghijkabcdefghijk"; "one char too long")] +#[tokio::test] +async fn invalid_client_hashes_are_rejected(bad_hash: &str) -> anyhow::Result<()> { + let t = TestHelper::new(); -#[derive(Clone, Debug, Default, World)] -struct AuthenticationWorld { - client_hash: String, - client: Option, - tokens: Vec, - auth_error: Option, -} - -#[given(expr = "a valid client hash")] -fn generate_hash(world: &mut AuthenticationWorld) { - world.client_hash = helpers::data_faker::rnd_sha_256(); -} + let res = t.authenticate(bad_hash.to_string()).await; + assert!(res.is_err(), "{res:?}"); -#[given(expr = "a bad client with the hash {word}")] -fn with_hash(world: &mut AuthenticationWorld, hash: String) { - world.client_hash = hash; + Ok(()) } -#[when(expr = "the client attempts to authenticate")] -#[when(expr = "that client authenticates a second time")] -#[given(expr = "an authenticated client")] -async fn authenticate(world: &mut AuthenticationWorld) { - let config = Config::load().expect("Could not load config"); +#[tokio::test] +async fn valid_client_hashes_can_authenticate_multiple_times() -> anyhow::Result<()> { + let t = TestHelper::new(); + let client_hash = t.random_sha_256(); - world.client = Some(TestClient::new(config.socket())); - - match world - .client - .as_ref() - .unwrap() - .authenticate(&world.client_hash) - .await - { - Ok(resp) => world.tokens.push(resp.into_inner().token), - Err(err) => world.auth_error = Some(err), - } -} - -#[then(expr = "the authentication is rejected")] -fn check_rejected(world: &mut AuthenticationWorld) { - assert!(world.auth_error.is_some()); - - let err = world.auth_error.as_ref().unwrap(); - - assert_eq!(err.code(), Code::InvalidArgument); -} - -#[then(expr = "the returned token is valid")] -#[then(expr = "both tokens are valid")] -fn verify_token(world: &mut AuthenticationWorld) { - assert!( - world.auth_error.is_none(), - "needed clean exit, instead got status {:?}", - world.auth_error - ); - - for token in world.tokens.iter() { - helpers::assert::assert_token_is_valid(token); - } -} - -#[then(expr = "the hash is only in the database once")] -async fn no_double_auth(world: &mut AuthenticationWorld) { - // In other test scenarios we might do this when we init the world, but - // given authentication only needs this once this is fine - let config = Config::load().expect("Could not load config"); - let infra = Infrastructure::new(&config) - .await - .expect("Could not init DB"); - - // User still registered - let row = sqlx::query("SELECT COUNT(*) FROM users WHERE client_hash = $1") - .bind(&world.client_hash) - .fetch_one(&mut *infra.repository().await.expect("could not connect to DB")) - .await - .unwrap(); - - let count: i64 = row.try_get("count").expect("Failed to get count"); - - // Only appears in db once - assert_eq!(count, 1); -} + let token1 = t.authenticate(client_hash.clone()).await?; + let token2 = t.authenticate(client_hash.clone()).await?; -#[tokio::main] -async fn main() { - dotenvy::from_filename(".env_files/test.env").ok(); + assert_eq!(token1, token2); + t.assert_valid_jwt(&token1); + t.assert_valid_jwt(&token2); - AuthenticationWorld::cucumber() - .repeat_skipped() - .run_and_exit("tests/features/user/authentication.feature") - .await + Ok(()) } diff --git a/tests/chart.rs b/tests/chart.rs index 3496cc02..c181d008 100644 --- a/tests/chart.rs +++ b/tests/chart.rs @@ -1,251 +1,52 @@ -#![cfg(test)] - -use cucumber::{given, then, when, Parameter, World}; -use futures::FutureExt; -use helpers::client::*; +// NOTE: this is not at all ideal, but in order to get tests around charts to work we need +// to be able to control the set of snaps that are in a given category. If tests start +// failing when you are adding new test cases then double check that you are not +// making use of any of the Categories that the tests in this file rely on. +pub mod common; + +use common::{Category, TestHelper}; +use futures::future::join_all; use rand::{thread_rng, Rng}; -use ratings::{ - features::{ - common::entities::{calculate_band, VoteSummary}, - pb::chart::{Category, ChartData, Timeframe}, - }, - utils::{Config, Infrastructure}, -}; -use sqlx::Connection; -use strum::EnumString; - -mod helpers; - -#[derive(Copy, Clone, Debug, PartialEq, Eq, Parameter, EnumString)] -#[param(name = "category", regex = "Utilities|Development")] -pub enum TestCategory { - Utilities, - Development, -} - -impl From for Category { - fn from(value: TestCategory) -> Self { - match value { - TestCategory::Development => Self::Development, - TestCategory::Utilities => Self::Utilities, - } - } -} - -#[derive(Debug, World)] -#[world(init = Self::new)] -struct ChartWorld { - token: String, - snap_ids: Vec, - test_snap: String, - client: TestClient, - chart_data: Vec, -} - -impl ChartWorld { - async fn new() -> Self { - let config = Config::load().expect("could not load config"); - let client = TestClient::new(config.socket()); - - let token = client - .authenticate(&helpers::data_faker::rnd_sha_256()) - .await - .expect("could not authenticate test client") - .into_inner() - .token; - - Self { - snap_ids: Vec::with_capacity(25), - test_snap: Default::default(), - chart_data: Vec::new(), - client, - token, - } - } -} - -#[given(expr = "a snap with id {string} gets {int} votes where {int} are upvotes")] -async fn set_test_snap(world: &mut ChartWorld, snap_id: String, votes: usize, upvotes: usize) { - world.test_snap = snap_id; - - helpers::vote_generator::generate_votes( - &world.test_snap, - 1, - true, - upvotes as u64, - &world.client, - ) - .await - .expect("could not generate votes"); - - tracing::debug!("done generating upvotes"); - - helpers::vote_generator::generate_votes( - &world.test_snap, - 1, - false, - (votes - upvotes) as u64, - &world.client, - ) - .await - .expect("could not generate votes"); - - tracing::debug!("done generating downvotes"); -} - -#[given( - expr = "{int} test snaps gets between {int} and {int} votes, where {int} to {int} are upvotes" -)] -async fn generate_snaps( - world: &mut ChartWorld, - num_snaps: usize, - min_vote: usize, - max_vote: usize, - min_upvote: usize, - max_upvote: usize, -) { - let mut expected = Vec::with_capacity(num_snaps); - - for i in 1..=num_snaps { - tracing::debug!("starting snap {i} / {num_snaps}"); - - let (upvotes, votes) = { - let mut rng = thread_rng(); - - let upvotes = rng.gen_range(min_upvote..max_upvote); - let min_vote = Ord::max(upvotes, min_vote); - let votes = rng.gen_range(min_vote..=max_vote); - (upvotes, votes) - }; - let id = helpers::data_faker::rnd_id(); - - helpers::vote_generator::generate_votes(&id, 1, true, upvotes as u64, &world.client) - .await - .expect("could not generate votes"); - - tracing::debug!("done generating upvotes ({i} / {num_snaps})"); - - helpers::vote_generator::generate_votes( - &id, - 1, - false, - (votes - upvotes) as u64, - &world.client, - ) - .await - .expect("could not generate votes"); - - tracing::debug!("done generating downvotes ({i} / {num_snaps})"); - - let summary = VoteSummary { - snap_id: id, - total_votes: votes as i64, - positive_votes: upvotes as i64, - }; - - expected.push((calculate_band(&summary).0.unwrap(), summary.snap_id)); +// !! This test expects to be the only one making use of the "Development" category +#[tokio::test] +async fn category_chart_returns_expected_top_snap() -> anyhow::Result<()> { + let t = TestHelper::new(); + + // Generate a random set of snaps within the given category + let mut tasks = Vec::with_capacity(25); + for _ in 0..25 { + let client = t.clone(); + tasks.push(tokio::spawn(async move { + let (upvotes, downvotes) = random_votes(50, 100, 25, 75); + client + .test_snap_with_initial_votes(1, upvotes, downvotes, &[Category::Development]) + .await + })); } + join_all(tasks).await; - expected.sort_unstable_by(|(band1, _), (band2, _)| band1.partial_cmp(band2).unwrap().reverse()); - world.snap_ids.extend(expected.drain(..).map(|(band, id)| { - tracing::debug!("id: {id}; band: {band}"); - id - })); -} - -#[when(expr = "the client fetches the top snaps")] -async fn get_chart(world: &mut ChartWorld) { - get_chart_internal(world, None).await; -} - -#[when(expr = "the client fetches the top snaps for {category}")] -async fn get_chart_of_category(world: &mut ChartWorld, category: TestCategory) { - get_chart_internal(world, Some(category.into())).await; -} - -async fn get_chart_internal(world: &mut ChartWorld, category: Option) { - world.chart_data = world - .client - .get_chart_of_category(Timeframe::Unspecified, category, &world.token) - .await - .expect("couldn't get chart") - .into_inner() - .ordered_chart_data; -} - -#[then(expr = "the top {int} snaps are returned in the proper order")] -async fn chart_order(world: &mut ChartWorld, top: usize) { - assert_eq!(world.chart_data.len(), top); - - assert!(world - .chart_data - .iter() - .zip(world.snap_ids.iter()) - .all(|(data, id)| { - let left = &data - .rating - .as_ref() - .expect("no rating in chart data?") - .snap_id; - - tracing::debug!("chart data: {data:?}, expected: {id}"); - - left == id - })) -} - -#[then(expr = "the top snap returned is the one with the ID {string}")] -async fn check_test_snap(world: &mut ChartWorld, snap_id: String) { - assert_eq!( - world.test_snap, snap_id, - "feature file and test snap definition got out of sync" - ); - - assert_eq!( - &world.chart_data[0].rating.as_ref().unwrap().snap_id, - &snap_id, - "top chart result is not test snap" - ); -} - -/// Automatically clears and snaps with >= TO_CLEAR votes, preventing them from interfering with tests -/// Being independent, while also not affecting other tests that require lower vote counts -async fn clear_db() { - const TO_CLEAR: usize = 3; - - let config = Config::load().unwrap(); - let infra = Infrastructure::new(&config).await.unwrap(); - let mut conn = infra.repository().await.unwrap(); - - let mut tx = conn.begin().await.unwrap(); + // A snap that should be returned as the top snap for the category + let snap_id = t + .test_snap_with_initial_votes(1, 100, 0, &[Category::Development]) + .await?; - sqlx::query( - r#"DELETE FROM votes WHERE snap_id IN - (SELECT snap_id FROM votes GROUP BY snap_id HAVING COUNT(*) >= $1) - "#, - ) - .bind(TO_CLEAR as i64) - .execute(&mut *tx) - .await - .unwrap(); + let user_token = t.authenticate(t.random_sha_256()).await?; + let mut data = t + .get_chart(Some(Category::Development), &user_token) + .await?; - sqlx::query("TRUNCATE TABLE snap_categories") - .execute(&mut *tx) - .await - .unwrap(); + let top_snap = data[0].rating.take().expect("to have rating for top snap"); + assert_eq!(top_snap.snap_id, snap_id, "{top_snap:?}"); - tx.commit().await.unwrap(); + Ok(()) } -#[tokio::main] -async fn main() { - dotenvy::from_filename(".env_files/test.env").ok(); +fn random_votes(min_vote: usize, max_vote: usize, min_up: usize, max_up: usize) -> (u64, u64) { + let mut rng = thread_rng(); + let upvotes = rng.gen_range(min_up..max_up); + let min_vote = Ord::max(upvotes, min_vote); + let votes = rng.gen_range(min_vote..=max_vote); - ChartWorld::cucumber() - .before(|_, _, _, _| clear_db().boxed_local()) - .repeat_failed() - .max_concurrent_scenarios(1) - .run_and_exit("tests/features/chart.feature") - .await + (upvotes as u64, (votes - upvotes) as u64) } diff --git a/tests/clear-db.sql b/tests/clear-db.sql new file mode 100644 index 00000000..58264efa --- /dev/null +++ b/tests/clear-db.sql @@ -0,0 +1,3 @@ +DELETE FROM snap_categories; +DELETE FROM users; +DELETE FROM votes; diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 00000000..5bdcf221 --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,250 @@ +use anyhow::anyhow; +use futures::future::join_all; +use rand::{distributions::Alphanumeric, Rng}; +use ratings::{ + app::interfaces::authentication::jwt::JwtVerifier, + features::{ + common::entities::Rating, + pb::{ + app::{app_client::AppClient, GetRatingRequest}, + chart::{chart_client::ChartClient, ChartData, GetChartRequest, Timeframe}, + user::{ + user_client::UserClient, AuthenticateRequest, GetSnapVotesRequest, Vote, + VoteRequest, + }, + }, + }, +}; +use reqwest::Client; +use sha2::{Digest, Sha256}; +use std::fmt::Write; +use tonic::{ + metadata::MetadataValue, + transport::{Channel, Endpoint}, + Request, +}; + +// re-export to simplify setting up test data in the test files +pub use ratings::features::pb::chart::Category; + +// NOTE: these are set by the 'tests' Makefile target +const MOCK_ADMIN_URL: Option<&str> = option_env!("MOCK_ADMIN_URL"); +const HOST: Option<&str> = option_env!("HOST"); +const PORT: Option<&str> = option_env!("PORT"); + +macro_rules! client { + ($client:ident, $channel:expr, $token:expr) => { + $client::with_interceptor($channel, move |mut req: Request<()>| { + let header: MetadataValue<_> = format!("Bearer {}", $token).parse().unwrap(); + req.metadata_mut().insert("authorization", header); + + Ok(req) + }) + }; +} + +fn rnd_string(len: usize) -> String { + let rng = rand::thread_rng(); + rng.sample_iter(&Alphanumeric) + .take(len) + .map(char::from) + .collect() +} + +#[derive(Debug, Default, Clone)] +pub struct TestHelper { + server_url: String, + mock_admin_url: &'static str, + client: Client, +} + +impl TestHelper { + pub fn new() -> Self { + Self { + server_url: format!( + "http://{}:{}/", + HOST.expect("the integration tests need to be run using make integration-test"), + PORT.expect("the integration tests need to be run using make integration-test") + ), + mock_admin_url: MOCK_ADMIN_URL.unwrap(), + client: Client::new(), + } + } + + pub fn assert_valid_jwt(&self, value: &str) { + let jwt = JwtVerifier::from_env().expect("unable to init JwtVerifier"); + assert!(jwt.decode(value).is_ok(), "value should be a valid jwt"); + } + + /// NOTE: total needs to be above 25 in order to generate a rating + pub async fn test_snap_with_initial_votes( + &self, + revision: i32, + upvotes: u64, + downvotes: u64, + categories: &[Category], + ) -> anyhow::Result { + let snap_id = self.random_id(); + let str_categories: Vec = categories.iter().map(|c| c.to_string()).collect(); + self.client + .post(format!("{}/{snap_id}", self.mock_admin_url)) + .body(str_categories.join(",")) + .send() + .await?; + + if upvotes > 0 { + self.generate_votes(&snap_id, revision, true, upvotes) + .await?; + } + if downvotes > 0 { + self.generate_votes(&snap_id, revision, false, downvotes) + .await?; + } + + Ok(snap_id) + } + + pub fn random_sha_256(&self) -> String { + let data = rnd_string(100); + let mut hasher = Sha256::new(); + hasher.update(data); + + hasher + .finalize() + .iter() + .fold(String::new(), |mut output, b| { + // This ignores the error without the overhead of unwrap/expect, + // This is okay because writing to a string can't fail (barring OOM which won't happen) + let _ = write!(output, "{b:02x}"); + output + }) + } + + pub fn random_id(&self) -> String { + rnd_string(32) + } + + async fn register_and_vote( + &self, + snap_id: &str, + snap_revision: i32, + vote_up: bool, + ) -> anyhow::Result<()> { + let id: String = self.random_sha_256(); + // The first call registers and the second authenticates + let token = self.authenticate(id.clone()).await?; + self.authenticate(id).await?; + self.vote(snap_id, snap_revision, vote_up, &token).await?; + + Ok(()) + } + + pub async fn generate_votes( + &self, + snap_id: &str, + snap_revision: i32, + vote_up: bool, + count: u64, + ) -> anyhow::Result<()> { + let mut tasks = Vec::with_capacity(count as usize); + + for _ in 0..count { + let snap_id = snap_id.to_string(); + let client = self.clone(); + + tasks.push(tokio::spawn(async move { + client + .register_and_vote(&snap_id, snap_revision, vote_up) + .await + })); + } + + for res in join_all(tasks).await { + // Unwrapping twice as the join itself can error as well as the + // underlying call to register_and_vote. + // This is here so that tests panic in test generation if there + // are any issues rather than carrying on with malformed data + res.unwrap().unwrap(); + } + + Ok(()) + } + + async fn channel(&self) -> Channel { + Endpoint::from_shared(self.server_url.clone()) + .expect("failed to create Endpoint") + .connect() + .await + .expect("failed to connect") + } + + pub async fn get_rating(&self, id: &str, token: &str) -> anyhow::Result { + let resp = client!(AppClient, self.channel().await, token) + .get_rating(GetRatingRequest { + snap_id: id.to_string(), + }) + .await? + .into_inner(); + + resp.rating + .map(Into::into) + .ok_or(anyhow!("no rating for {id}")) + } + + pub async fn get_chart( + &self, + category: Option, + token: &str, + ) -> anyhow::Result> { + let resp = client!(ChartClient, self.channel().await, token) + .get_chart(GetChartRequest { + timeframe: Timeframe::Unspecified.into(), + category: category.map(|v| v.into()), + }) + .await? + .into_inner(); + + Ok(resp.ordered_chart_data) + } + + pub async fn vote( + &self, + snap_id: &str, + snap_revision: i32, + vote_up: bool, + token: &str, + ) -> anyhow::Result<()> { + client!(UserClient, self.channel().await, token) + .vote(VoteRequest { + snap_id: snap_id.to_string(), + snap_revision, + vote_up, + }) + .await?; + + Ok(()) + } + + pub async fn get_snap_votes( + &self, + token: &str, + request: GetSnapVotesRequest, + ) -> anyhow::Result> { + let resp = client!(UserClient, self.channel().await, token) + .get_snap_votes(request) + .await? + .into_inner(); + + Ok(resp.votes) + } + + pub async fn authenticate(&self, id: String) -> anyhow::Result { + let resp = UserClient::connect(self.server_url.clone()) + .await? + .authenticate(AuthenticateRequest { id }) + .await? + .into_inner(); + + Ok(resp.token) + } +} diff --git a/tests/features/admin/api-info.feature b/tests/features/admin/api-info.feature deleted file mode 100644 index 28505428..00000000 --- a/tests/features/admin/api-info.feature +++ /dev/null @@ -1,7 +0,0 @@ -Feature: Can retrieve API version info via REST requests - - Scenario: Big wants to find out the build information for the service - Given Big doesn't know the API build info - When Big asks for the API info - Then Big gets an answer - diff --git a/tests/features/admin/log-level.feature b/tests/features/admin/log-level.feature deleted file mode 100644 index 7f5b597a..00000000 --- a/tests/features/admin/log-level.feature +++ /dev/null @@ -1,19 +0,0 @@ -Feature: Can retrieve and set the internal log level via the REST endpoint - - Scenario: Espio wants to find out the current log level - Given Espio doesn't know the log level - When Espio asks for the log level - Then Espio gets an answer - - Scenario Outline: Espio wants to set the log level - Given the service's current log level - When Espio requests it changes to - Then the log level is set to - - Examples: - | level | - | error | - | info | - | debug | - | warn | - | trace | diff --git a/tests/features/chart.feature b/tests/features/chart.feature deleted file mode 100644 index 3f0daaf4..00000000 --- a/tests/features/chart.feature +++ /dev/null @@ -1,17 +0,0 @@ -Feature: List of top 20 snaps - Background: - Given a snap with id "3Iwi803Tk3KQwyD6jFiAJdlq8MLgBIoD" gets 100 votes where 75 are upvotes - Given 25 test snaps gets between 150 and 200 votes, where 125 to 175 are upvotes - - Scenario: Tails opens the store homepage, seeing the top snaps - When the client fetches the top snaps - Then the top 20 snaps are returned in the proper order - - Scenario Outline: Tails opens a few store categories, retrieving the top chart for those snaps - When the client fetches the top snaps for - Then the top snap returned is the one with the ID "3Iwi803Tk3KQwyD6jFiAJdlq8MLgBIoD" - - Examples: - | category | - | Utilities | - | Development | \ No newline at end of file diff --git a/tests/features/user/authentication.feature b/tests/features/user/authentication.feature deleted file mode 100644 index 9aa6ee7d..00000000 --- a/tests/features/user/authentication.feature +++ /dev/null @@ -1,24 +0,0 @@ -Feature: User authentication - - Scenario: The Snap Store tries to authenticate - Given a valid client hash - When the client attempts to authenticate - Then the returned token is valid - - Rule: Client hashes must be exactly 64 characters long - Scenario Outline: Eggman tries to directly rate a Snap with an unofficial client with an improper hardcoded "hash" - Given a bad client with the hash - When the client attempts to authenticate - Then the authentication is rejected - - Examples: - | hash | - | notarealhash | - | abcdefghijkabcdefghijkabcdefghijkabcdefghijkabcdefghijkabcdefghijk | - - Scenario: Charmy's client authenticates twice - Given a valid client hash - Given an authenticated client - When that client authenticates a second time - Then both tokens are valid - And the hash is only in the database once diff --git a/tests/features/user/voting.feature b/tests/features/user/voting.feature deleted file mode 100644 index 85434f59..00000000 --- a/tests/features/user/voting.feature +++ /dev/null @@ -1,22 +0,0 @@ -Feature: User voting - Background: - Given a Snap named "chu-chu-garden" has already accumulated 5 votes and 3 upvotes - - Scenario: Amy upvotes a snap she hasn't voted for in the past - When Amy casts an upvote - Then the total number of votes strictly increases - And the ratings band monotonically increases - - Rule: Votes that a user updates do not change the total vote count - - Scenario Outline: Sonic changes his vote between downvote and upvote because "chu-chu-garden" got better/worse - Given Sonic originally voted - When Sonic changes his vote to - Then the ratings band - But the total number of votes stays constant - - Examples: - | original | after | direction | - | upvote | downvote | monotonically increases | - | downvote | upvote | monotonically decreases | - diff --git a/tests/helpers/assert.rs b/tests/helpers/assert.rs deleted file mode 100644 index 00d95db1..00000000 --- a/tests/helpers/assert.rs +++ /dev/null @@ -1,16 +0,0 @@ -use ratings::app::interfaces::authentication::jwt::JwtVerifier; - -#[allow(dead_code)] -pub fn assert_token_is_valid(value: &str) { - let jwt = JwtVerifier::from_env(); - assert!( - jwt.unwrap().decode(value).is_ok(), - "value should be a valid jwt" - ); -} - -#[allow(dead_code)] -pub fn assert_token_is_not_valid(value: &str) { - let jwt = JwtVerifier::from_env(); - assert!(jwt.unwrap().decode(value).is_err(), "expected invalid jwt"); -} diff --git a/tests/helpers/client/api_info.rs b/tests/helpers/client/api_info.rs deleted file mode 100644 index 57c3280c..00000000 --- a/tests/helpers/client/api_info.rs +++ /dev/null @@ -1,40 +0,0 @@ -use std::str::FromStr; - -use axum::async_trait; -use ratings::{ - app::interfaces::authentication::admin::AdminAuthConfig, - features::admin::api_version::interface::ApiVersionResponse, -}; -use reqwest::Url; -use secrecy::ExposeSecret; - -use super::Client; - -#[async_trait] -pub trait ApiInfoClient: Client { - fn rest_url(&self) -> Url { - Url::from_str(self.url()) - .unwrap() - .join("/v1/admin/api-version") - .unwrap() - } - - async fn get_api_info( - &self, - ) -> Result> { - let (un, pass) = AdminAuthConfig::from_env() - .expect("could not decode admin secrets from env") - .into_inner(); - - let text_response = reqwest::Client::new() - .get(self.rest_url()) - .basic_auth(un.expose_secret(), Some(pass.expose_secret())) - .send() - .await? - .error_for_status()? - .text() - .await?; - - Ok(serde_json::from_str(&text_response)?) - } -} diff --git a/tests/helpers/client/app.rs b/tests/helpers/client/app.rs deleted file mode 100644 index 0c27bdaf..00000000 --- a/tests/helpers/client/app.rs +++ /dev/null @@ -1,33 +0,0 @@ -use tonic::async_trait; -use tonic::{metadata::MetadataValue, transport::Endpoint, Request, Response, Status}; - -use ratings::features::pb::app::{GetRatingRequest, GetRatingResponse}; - -use ratings::features::pb::app::app_client as pb; - -use super::Client; - -#[async_trait] -pub trait AppClient: Client { - async fn get_rating( - &self, - token: &str, - id: &str, - ) -> Result, Status> { - let channel = Endpoint::from_shared(self.url().to_string()) - .unwrap() - .connect() - .await - .unwrap(); - let mut client = pb::AppClient::with_interceptor(channel, move |mut req: Request<()>| { - let header: MetadataValue<_> = format!("Bearer {token}").parse().unwrap(); - req.metadata_mut().insert("authorization", header); - Ok(req) - }); - client - .get_rating(GetRatingRequest { - snap_id: id.to_string(), - }) - .await - } -} diff --git a/tests/helpers/client/chart.rs b/tests/helpers/client/chart.rs deleted file mode 100644 index 70cb20f4..00000000 --- a/tests/helpers/client/chart.rs +++ /dev/null @@ -1,58 +0,0 @@ -use tonic::metadata::MetadataValue; -use tonic::transport::Endpoint; -use tonic::{async_trait, Request, Response, Status}; - -use ratings::features::pb::chart::{chart_client as pb, Category, Timeframe}; -use ratings::features::pb::chart::{GetChartRequest, GetChartResponse}; - -use super::Client; - -#[async_trait] -pub trait ChartClient: Client { - async fn get_chart( - &self, - timeframe: Timeframe, - token: &str, - ) -> Result, Status> { - let channel = Endpoint::from_shared(self.url().to_string()) - .unwrap() - .connect() - .await - .unwrap(); - let mut client = pb::ChartClient::with_interceptor(channel, move |mut req: Request<()>| { - let header: MetadataValue<_> = format!("Bearer {token}").parse().unwrap(); - req.metadata_mut().insert("authorization", header); - Ok(req) - }); - client - .get_chart(GetChartRequest { - timeframe: timeframe.into(), - category: None, - }) - .await - } - - async fn get_chart_of_category( - &self, - timeframe: Timeframe, - category: Option, - token: &str, - ) -> Result, Status> { - let channel = Endpoint::from_shared(self.url().to_string()) - .unwrap() - .connect() - .await - .unwrap(); - let mut client = pb::ChartClient::with_interceptor(channel, move |mut req: Request<()>| { - let header: MetadataValue<_> = format!("Bearer {token}").parse().unwrap(); - req.metadata_mut().insert("authorization", header); - Ok(req) - }); - client - .get_chart(GetChartRequest { - timeframe: timeframe.into(), - category: category.map(|v| v.into()), - }) - .await - } -} diff --git a/tests/helpers/client/log_level.rs b/tests/helpers/client/log_level.rs deleted file mode 100644 index 3ad26e0d..00000000 --- a/tests/helpers/client/log_level.rs +++ /dev/null @@ -1,61 +0,0 @@ -use std::str::FromStr; - -use axum::async_trait; -use log::Level; -use ratings::{ - app::interfaces::authentication::admin::AdminAuthConfig, - features::admin::log_level::interface::{GetLogLevelResponse, SetLogLevelRequest}, -}; -use reqwest::Url; -use secrecy::ExposeSecret; - -use super::Client; - -#[async_trait] -pub trait LogClient: Client { - fn rest_url(&self) -> Url { - Url::from_str(self.url()) - .unwrap() - .join("/v1/admin/log-level") - .unwrap() - } - - async fn get_log_level( - &self, - ) -> Result> { - let (un, pass) = AdminAuthConfig::from_env() - .expect("could not decode admin secrets from env") - .into_inner(); - - let text_response = reqwest::Client::new() - .get(self.rest_url()) - .header("Content-Type", "application/json") - .basic_auth(un.expose_secret(), Some(pass.expose_secret())) - .send() - .await? - .error_for_status()? - .text() - .await?; - - Ok(serde_json::from_str(&text_response)?) - } - - async fn set_log_level( - &self, - level: Level, - ) -> Result<(), Box> { - let (un, pass) = AdminAuthConfig::from_env() - .expect("could not decode admin secrets from env") - .into_inner(); - reqwest::Client::new() - .post(self.rest_url()) - .header("Content-Type", "application/json") - .basic_auth(un.expose_secret(), Some(pass.expose_secret())) - .body(serde_json::to_string(&SetLogLevelRequest { level }).unwrap()) - .send() - .await? - .error_for_status_ref()?; - - Ok(()) - } -} diff --git a/tests/helpers/client/mod.rs b/tests/helpers/client/mod.rs deleted file mode 100644 index 6d5dbe84..00000000 --- a/tests/helpers/client/mod.rs +++ /dev/null @@ -1,42 +0,0 @@ -pub mod api_info; -pub mod app; -pub mod chart; -pub mod log_level; -pub mod user; - -use std::fmt::Display; - -pub use self::{ - api_info::ApiInfoClient, app::AppClient, chart::ChartClient, log_level::LogClient, - user::UserClient, -}; - -pub trait Client { - fn url(&self) -> &str; -} - -#[derive(Debug, Clone)] -pub struct TestClient { - url: String, -} - -impl TestClient { - pub fn new(url: D) -> Self { - Self { - url: format!("http://{}/", url), - } - } -} - -impl Client for TestClient { - #[inline(always)] - fn url(&self) -> &str { - &self.url - } -} - -impl AppClient for TestClient {} -impl ChartClient for TestClient {} -impl UserClient for TestClient {} -impl LogClient for TestClient {} -impl ApiInfoClient for TestClient {} diff --git a/tests/helpers/client/user.rs b/tests/helpers/client/user.rs deleted file mode 100644 index 940cef91..00000000 --- a/tests/helpers/client/user.rs +++ /dev/null @@ -1,70 +0,0 @@ -use tonic::metadata::MetadataValue; -use tonic::transport::Endpoint; -use tonic::{async_trait, Request, Response, Status}; - -use ratings::features::pb::user::user_client as pb; -use ratings::features::pb::user::{ - AuthenticateRequest, AuthenticateResponse, GetSnapVotesRequest, GetSnapVotesResponse, - VoteRequest, -}; - -use super::Client; - -#[async_trait] -pub trait UserClient: Client { - async fn vote(&self, token: &str, ballet: VoteRequest) -> Result, Status> { - let channel = Endpoint::from_shared(self.url().to_string()) - .unwrap() - .connect() - .await - .unwrap(); - let mut client = pb::UserClient::with_interceptor(channel, move |mut req: Request<()>| { - let header: MetadataValue<_> = format!("Bearer {token}").parse().unwrap(); - req.metadata_mut().insert("authorization", header); - Ok(req) - }); - client.vote(ballet).await - } - - async fn get_snap_votes( - &self, - token: &str, - request: GetSnapVotesRequest, - ) -> Result, Status> { - let channel = Endpoint::from_shared(self.url().to_string()) - .unwrap() - .connect() - .await - .unwrap(); - let mut client = pb::UserClient::with_interceptor(channel, move |mut req: Request<()>| { - let header: MetadataValue<_> = format!("Bearer {token}").parse().unwrap(); - req.metadata_mut().insert("authorization", header); - Ok(req) - }); - client.get_snap_votes(request).await - } - - async fn delete(&self, token: &str) -> Result, Status> { - let channel = Endpoint::from_shared(self.url().to_string()) - .unwrap() - .connect() - .await - .unwrap(); - let mut client = pb::UserClient::with_interceptor(channel, move |mut req: Request<()>| { - let header: MetadataValue<_> = format!("Bearer {token}").parse().unwrap(); - req.metadata_mut().insert("authorization", header); - Ok(req) - }); - - client.delete(()).await - } - - async fn authenticate(&self, id: &str) -> Result, Status> { - let mut client = pb::UserClient::connect(self.url().to_string()) - .await - .unwrap(); - client - .authenticate(AuthenticateRequest { id: id.to_string() }) - .await - } -} diff --git a/tests/helpers/data_faker.rs b/tests/helpers/data_faker.rs deleted file mode 100644 index cfea8a01..00000000 --- a/tests/helpers/data_faker.rs +++ /dev/null @@ -1,34 +0,0 @@ -use std::fmt::Write; - -use rand::distributions::Alphanumeric; -use rand::Rng; -use sha2::{Digest, Sha256}; - -#[allow(dead_code)] -pub fn rnd_sha_256() -> String { - let data = rnd_string(100); - let mut hasher = Sha256::new(); - hasher.update(data); - - hasher - .finalize() - .iter() - .fold(String::new(), |mut output, b| { - // This ignores the error without the overhead of unwrap/expect, - // This is okay because writing to a string can't fail (barring OOM which won't happen) - let _ = write!(output, "{b:02x}"); - output - }) -} - -pub fn rnd_id() -> String { - rnd_string(32) -} - -fn rnd_string(len: usize) -> String { - let rng = rand::thread_rng(); - rng.sample_iter(&Alphanumeric) - .take(len) - .map(char::from) - .collect() -} diff --git a/tests/helpers/mod.rs b/tests/helpers/mod.rs deleted file mode 100644 index 54fe8144..00000000 --- a/tests/helpers/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -#![allow(dead_code)] -#![cfg(test)] - -pub mod assert; -pub mod client; -pub mod data_faker; -pub mod vote_generator; diff --git a/tests/helpers/vote_generator.rs b/tests/helpers/vote_generator.rs deleted file mode 100644 index 001e973f..00000000 --- a/tests/helpers/vote_generator.rs +++ /dev/null @@ -1,91 +0,0 @@ -use std::sync::Arc; - -use super::client::*; -use crate::helpers; -use futures::future::join_all; -use ratings::features::pb::user::{AuthenticateResponse, VoteRequest}; -use thiserror::Error; -use tonic::Status; - -#[derive(Clone, Debug, Error)] -pub enum GenerateVoteError { - #[error("there was a panic while attempting to authenticate the votes: {0}")] - Panic(String), - #[error("there was a negative response from the server: {0}")] - Status(#[from] Status), -} - -impl From for GenerateVoteError { - fn from(value: String) -> Self { - Self::Panic(value) - } -} - -pub async fn generate_votes( - snap_id: &str, - snap_revision: i32, - vote_up: bool, - count: u64, - client: &TestClient, -) -> Result<(), GenerateVoteError> { - let mut joins = Vec::with_capacity(count as usize); - - let snap_id = Arc::new(snap_id.to_string()); - let client = Arc::new(client.clone()); - for _ in 0..count { - let snap_id = snap_id.clone(); - let client = client.clone(); - joins.push(tokio::spawn(async move { - register_and_vote(&snap_id, snap_revision, vote_up, &client).await - })); - } - - for join in join_all(joins).await { - join.map_err(|e| { - if e.is_panic() { - e.into_panic() - .downcast::<&'static str>() - .unwrap() - .to_string() - } else { - format!("other error: {}", e) - } - })?? - } - - Ok(()) -} - -async fn register_and_vote( - snap_id: &str, - snap_revision: i32, - vote_up: bool, - client: &TestClient, -) -> Result<(), Status> { - let id: String = helpers::data_faker::rnd_sha_256(); - let response: AuthenticateResponse = client - .authenticate(&id) - .await - .expect("register request should succeed") - .into_inner(); - let token: String = response.token; - - let _: AuthenticateResponse = client - .authenticate(&id) - .await - .expect("authenticate should succeed") - .into_inner(); - - let ballet = VoteRequest { - snap_id: snap_id.to_string(), - snap_revision, - vote_up, - }; - - client - .vote(&token, ballet) - .await - .expect("vote should succeed") - .into_inner(); - Ok(()) -} diff --git a/tests/log_level.rs b/tests/log_level.rs deleted file mode 100644 index 0b8b9457..00000000 --- a/tests/log_level.rs +++ /dev/null @@ -1,109 +0,0 @@ -use std::str::FromStr; - -use cucumber::{given, then, when, Parameter, World}; - -use helpers::client::*; -use ratings::utils::Config; - -mod helpers; - -#[derive(Copy, Clone, Eq, PartialEq, Parameter, Debug)] -#[param(name = "level", regex = "info|warn|debug|trace|error")] -pub struct Level(log::Level); - -impl FromStr for Level { - type Err = ::Err; - - fn from_str(s: &str) -> Result { - Ok(Level(log::Level::from_str(s)?)) - } -} - -impl From for Level { - fn from(value: log::Level) -> Self { - Self(value) - } -} - -impl From for log::Level { - fn from(value: Level) -> Self { - value.0 - } -} - -#[derive(Clone, Debug, World)] -#[world(init = Self::new)] -struct LogWorld { - client: TestClient, - current_level: Option, -} - -impl LogWorld { - fn new() -> Self { - let config = Config::load().expect("could not load config"); - let client = TestClient::new(config.socket()); - Self { - client, - current_level: None, - } - } -} - -#[given(expr = "Espio doesn't know the log level")] -fn unknown_level(world: &mut LogWorld) { - world.current_level = None -} - -#[when(expr = "Espio asks for the log level")] -#[given(expr = "the service's current log level")] -async fn get_log_level(world: &mut LogWorld) { - world.current_level = Some( - world - .client - .get_log_level() - .await - .expect("could not get log level") - .level - .into(), - ) -} - -#[when(expr = "Espio requests it changes to {level}")] -async fn set_log_level(world: &mut LogWorld, level: Level) { - world - .client - .set_log_level(level.into()) - .await - .expect("problem setting log level"); -} - -#[then(expr = "Espio gets an answer")] -fn got_any_level(world: &mut LogWorld) { - assert!( - world.current_level.is_some(), - "did not get a valid level from the endpoint" - ); -} - -#[then(expr = "the log level is set to {level}")] -async fn got_expected_level(world: &mut LogWorld, level: Level) { - let post_set_level = world - .client - .get_log_level() - .await - .expect("could not get log level") - .level; - - assert_eq!(level.0, post_set_level) -} - -#[tokio::main] -async fn main() { - dotenvy::from_filename(".env_files/test.env").ok(); - - LogWorld::cucumber() - .repeat_skipped() - .max_concurrent_scenarios(1) - .run_and_exit("tests/features/admin/log-level.feature") - .await -} diff --git a/tests/voting.rs b/tests/voting.rs index a9c58d66..239c1bf3 100644 --- a/tests/voting.rs +++ b/tests/voting.rs @@ -1,262 +1,100 @@ -use std::str::FromStr; - -use cucumber::{given, then, when, Parameter, World}; -use helpers::client::*; -use ratings::{ - features::{ - common::entities::{Rating, RatingsBand, VoteSummary}, - pb::user::VoteRequest, - }, - utils::Config, -}; -mod helpers; - -#[derive(Debug, Default)] -struct AuthenticatedUser { - token: String, -} - -#[derive(Debug, Default, Copy, Clone, Parameter, strum::EnumString)] -#[param(name = "vote-type", regex = "upvote|downvote")] -#[strum(ascii_case_insensitive)] -enum VoteType { - #[default] - Upvote, - Downvote, -} -impl From for bool { - fn from(value: VoteType) -> Self { - match value { - VoteType::Upvote => true, - VoteType::Downvote => false, - } - } -} - -impl From for u64 { - fn from(value: VoteType) -> Self { - bool::from(value) as u64 - } -} - -#[derive(Debug, Default, Copy, Clone, Parameter)] -#[param( - name = "direction", - regex = "strictly increases|strictly decreases|stays constant|monotonically increases|monotonically decreases" -)] -enum Direction { - #[default] - StrictlyIncrease, - StrictlyDecrease, - StaysConstant, - MonotonicallyIncrease, - MonotonicallyDecrease, -} - -impl FromStr for Direction { - type Err = String; - - fn from_str(s: &str) -> Result { - Ok(match s { - "strictly increases" => Self::StrictlyIncrease, - "strictly decreases" => Self::StrictlyDecrease, - "stays constant" => Self::StaysConstant, - "monotonically increases" => Self::MonotonicallyIncrease, - "monotonically decreases" => Self::MonotonicallyDecrease, - _ => return Err(format!("invalid vote count direction {s}")), - }) - } -} - -impl Direction { - fn check_and_apply(&self, current: &mut u64, new: u64) { - match self { - Direction::StrictlyDecrease => assert_eq!(new, *current - 1), - - Direction::StrictlyIncrease => assert_eq!(new, *current + 1), - - Direction::StaysConstant => assert_eq!(new, *current), - Direction::MonotonicallyIncrease => assert!( - new == *current || new == *current + 1, - "value is not montonically increasing, got {new}; current was {current}" - ), - Direction::MonotonicallyDecrease => assert!( - new == *current || new == *current - 1, - "value is not montonically decreasing, got {new}; current was {current}" - ), - }; - - *current = new - } - - fn check_and_apply_band(&self, current: &mut RatingsBand, new: RatingsBand) { - let comparison = (*current).partial_cmp(&new); - - if comparison.is_none() { - *current = new; - // Unable to conclude anything if there isn't enough information - return; - } - - let comparison = comparison.unwrap(); - - match (*self, comparison) { - (Direction::StrictlyIncrease, std::cmp::Ordering::Less) => {} - (Direction::StrictlyDecrease, std::cmp::Ordering::Greater) => {} - (Direction::StaysConstant, std::cmp::Ordering::Equal) => {} - (Direction::MonotonicallyIncrease, std::cmp::Ordering::Equal) - | (Direction::MonotonicallyIncrease, std::cmp::Ordering::Greater) => {} - (Direction::MonotonicallyDecrease, std::cmp::Ordering::Less) - | (Direction::MonotonicallyDecrease, std::cmp::Ordering::Equal) => {} - _ => { - panic!("Ratings band did not properly {self:?}; current: {current:?}; new: {new:?}") - } - } - - *current = new - } -} - -#[derive(Debug, PartialEq, Eq)] -struct Snap(String); - -impl Default for Snap { - fn default() -> Self { - Snap("93jv9vhsfbb8f7".to_string()) - } -} - -#[derive(Debug, World)] -#[world(init = Self::new)] -struct VotingWorld { - user: AuthenticatedUser, - client: TestClient, - snap: Snap, - rating: Rating, -} - -impl VotingWorld { - async fn new() -> Self { - let config = Config::load().expect("Could not load config"); - let client = TestClient::new(config.socket()); - - let id = helpers::data_faker::rnd_sha_256(); - tracing::debug!("User ID for this test: {id}"); - let user = AuthenticatedUser { - token: client - .authenticate(&id) - .await - .expect("could not authenticate user") - .into_inner() - .token, - }; - - VotingWorld { - user, - client, - snap: Default::default(), - rating: Default::default(), - } - } -} - -#[given(expr = "a Snap named {string} has already accumulated {int} votes and {int} upvotes")] -async fn seed_snap(world: &mut VotingWorld, _snap_name: String, votes: i64, upvotes: i64) { - world.snap.0 = helpers::data_faker::rnd_id(); - tracing::debug!("Snap ID for this test: {}", world.snap.0); - - helpers::vote_generator::generate_votes(&world.snap.0, 1, true, upvotes as u64, &world.client) - .await - .expect("could not generate votes"); - helpers::vote_generator::generate_votes( - &world.snap.0, - 1, - false, - (votes - upvotes) as u64, - &world.client, - ) - .await - .expect("could not generate votes"); - - let summary = VoteSummary { - snap_id: world.snap.0.clone(), - total_votes: votes, - positive_votes: upvotes, - }; - - world.rating = Rating::new(summary); -} - -#[when(expr = "{word} casts a(n) {vote-type}")] -#[when(expr = "{word} changes his/her/their vote to {vote-type}")] -async fn vote(world: &mut VotingWorld, _user_name: String, vote_type: VoteType) { - let request = VoteRequest { - snap_id: world.snap.0.clone(), - snap_revision: 1, - vote_up: vote_type.into(), - }; - - world - .client - .vote(&world.user.token, request) - .await - .expect("could not cast vote"); -} - -#[given(expr = "{word} originally voted {vote-type}")] -async fn originally_voted(world: &mut VotingWorld, _user_name: String, vote_type: VoteType) { - vote(world, _user_name, vote_type).await; - - world.rating = world - .client - .get_rating(&world.user.token, &world.snap.0) - .await - .expect("could not get snap rating") - .into_inner() - .rating - .expect("expected an actual rating") - .into(); -} - -#[then(expr = "the total number of votes {direction}")] -async fn check_vote(world: &mut VotingWorld, direction: Direction) { - let votes = world - .client - .get_rating(&world.user.token, &world.snap.0) - .await - .expect("could not get snap rating") - .into_inner() - .rating - .expect("Rating response was empty") - .total_votes; - - direction.check_and_apply(&mut world.rating.total_votes, votes); -} - -#[then(expr = "the ratings band {direction}")] -async fn check_upvote(world: &mut VotingWorld, direction: Direction) { - let band = world - .client - .get_rating(&world.user.token, &world.snap.0) - .await - .expect("could not get snap rating") - .into_inner() - .rating - .expect("Rating response was empty") - .ratings_band; - - let band = - ratings::features::pb::common::RatingsBand::try_from(band).expect("Unknown ratings band"); - - direction.check_and_apply_band(&mut world.rating.ratings_band, band.into()); -} - -#[tokio::main] -async fn main() { - dotenvy::from_filename(".env_files/test.env").ok(); - - VotingWorld::cucumber() - .repeat_skipped() - .run_and_exit("tests/features/user/voting.feature") - .await +pub mod common; + +use common::{Category, TestHelper}; +use ratings::features::common::entities::RatingsBand::{self, *}; +use simple_test_case::test_case; + +#[test_case(true; "up vote")] +#[test_case(false; "down vote")] +#[tokio::test] +async fn voting_increases_vote_count(vote_up: bool) -> anyhow::Result<()> { + let t = TestHelper::new(); + + let user_token = t.authenticate(t.random_sha_256()).await?; + let snap_revision = 1; + let snap_id = t + .test_snap_with_initial_votes(snap_revision, 3, 2, &[Category::Social]) + .await?; + + let initial_rating = t.get_rating(&snap_id, &user_token).await?; + assert_eq!(initial_rating.total_votes, 5, "initial total votes"); + + // Vote with a user who has not previously voted for this snap + t.vote(&snap_id, snap_revision, vote_up, &user_token) + .await?; + + let rating = t.get_rating(&snap_id, &user_token).await?; + assert_eq!(rating.total_votes, 6, "total votes: vote_up={vote_up}"); + + Ok(()) +} + +#[test_case(true; "up to down vote")] +#[test_case(false; "down to up vote")] +#[tokio::test] +async fn changing_your_vote_doesnt_alter_total(initial_up: bool) -> anyhow::Result<()> { + let t = TestHelper::new(); + + let user_token = t.authenticate(t.random_sha_256()).await?; + let snap_revision = 1; + let snap_id = t + .test_snap_with_initial_votes(snap_revision, 3, 2, &[Category::Social]) + .await?; + + let initial_rating = t.get_rating(&snap_id, &user_token).await?; + assert_eq!(initial_rating.total_votes, 5, "initial total votes"); + + // Vote with a user who has not previously voted for this snap + t.vote(&snap_id, snap_revision, initial_up, &user_token) + .await?; + + let rating = t.get_rating(&snap_id, &user_token).await?; + assert_eq!(rating.total_votes, 6, "total votes"); + + // That user changing their vote shouldn't alter the total + t.vote(&snap_id, snap_revision, !initial_up, &user_token) + .await?; + + let rating = t.get_rating(&snap_id, &user_token).await?; + assert_eq!(rating.total_votes, 6, "total votes"); + + Ok(()) +} + +// The ratings bands details are found in ../src/features/common/entities.rs and the following +// test expects the break points for the value of the confidence interval: +// +// 0.80 < r - VeryGood +// 0.55 < r <= 0.80 - Good +// 0.45 < r <= 0.55 - Neutral +// 0.20 < r <= 0.45 - Poor +// r <= 0.20 - VeryPoor +// +// NOTE: In order to generate a rating we need to have at least 25 votes +#[test_case(true, Neutral, Good; "neutral to good")] +#[test_case(false, Neutral, Poor; "neutral to poor")] +#[tokio::test] +async fn voting_updates_ratings_band( + vote_up: bool, + initial_band: RatingsBand, + new_band: RatingsBand, +) -> anyhow::Result<()> { + let t = TestHelper::new(); + + let user_token = t.authenticate(t.random_sha_256()).await?; + let snap_revision = 1; + let snap_id = t + .test_snap_with_initial_votes(snap_revision, 60, 40, &[Category::Games]) + .await?; + + let r = t.get_rating(&snap_id, &user_token).await?; + assert_eq!(r.ratings_band, initial_band, "initial band"); + + t.generate_votes(&snap_id, snap_revision, vote_up, 50) + .await?; + + let r = t.get_rating(&snap_id, &user_token).await?; + assert_eq!(r.ratings_band, new_band, "new band"); + + Ok(()) }