diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml deleted file mode 100644 index d5d0e0b147b46c30e985b78426eb7f86b470835a..0000000000000000000000000000000000000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -1,37 +0,0 @@ -on: - - push - - pull_request - -name: Lint check -jobs: - pre_job: - # continue-on-error: true # Uncomment once integration is finished - runs-on: ubuntu-latest - # Map a step output to a job output - outputs: - should_skip: ${{ steps.skip_check.outputs.should_skip }} - steps: - - id: skip_check - uses: fkirc/skip-duplicate-actions@master - with: - # All of these options are optional, so you can remove them if you are happy with the defaults - concurrent_skipping: 'same_content' - skip_after_successful_duplicate: 'true' - paths_ignore: '["**/README.md"]' - - clippy_check: - needs: pre_job - if: ${{ needs.pre_job.outputs.should_skip != 'true' }} - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - run: rustup component add clippy - - uses: actions-rs/clippy-check@v1 - with: - token: ${{ secrets.GITHUB_TOKEN }} - args: --all-features -p rustus -- -W clippy::all -W clippy::pedantic -D warnings - fmt_check: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - run: cargo fmt -- --check diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000000000000000000000000000000000000..300c4683fd57ea8bb0959bbd4fe80c06c0ddf438 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,97 @@ +on: + - pull_request + +name: Lint check +jobs: + pre_job: + # continue-on-error: true # Uncomment once integration is finished + runs-on: ubuntu-latest + # Map a step output to a job output + outputs: + should_skip: ${{ steps.skip_check.outputs.should_skip }} + steps: + - id: skip_check + uses: fkirc/skip-duplicate-actions@master + with: + # All of these options are optional, so you can remove them if you are happy with the defaults + concurrent_skipping: 'same_content' + skip_after_successful_duplicate: 'true' + paths_ignore: '["**/README.md"]' + + fmt_check: + needs: pre_job + if: ${{ needs.pre_job.outputs.should_skip != 'true' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Install stable toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + - name: Adding component + run: rustup component add rustfmt + - name: Checking code format + run: cargo fmt -- --check + + code_check: + needs: pre_job + if: ${{ needs.pre_job.outputs.should_skip != 'true' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Install stable toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + - run: rustup component add clippy + - uses: actions-rs/clippy-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + args: --all-features -p rustus -- -W clippy::all -W clippy::pedantic -D warnings + + tests: + needs: pre_job + if: ${{ needs.pre_job.outputs.should_skip != 'true' }} + runs-on: ubuntu-latest + services: + redis: + image: redis:6.2-alpine3.15 + ports: + - 6379/tcp + pg: + image: postgres:13.1 + ports: + - 5432/tcp + env: + POSTGRES_PASSWORD: "rustus" + POSTGRES_USER: "rustus" + POSTGRES_DB: "rustus" + rabbit: + image: rabbitmq:3.8.27-alpine + ports: + - 5672/tcp + env: + RABBITMQ_DEFAULT_USER: "guest" + RABBITMQ_DEFAULT_PASS: "guest" + RABBITMQ_DEFAULT_VHOST: "/" + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Install nightly toolchain + run: rustup toolchain install nightly --component llvm-tools-preview + - name: Install cargo-llvm-cov + uses: taiki-e/install-action@cargo-llvm-cov + - name: Generate code coverage + run: cargo llvm-cov --features=all,integration_tests --lcov --output-path lcov.info -- --test-threads 1 + env: + TEST_REDIS_URL: redis://localhost:${{ job.services.redis.ports['6379'] }}/0 + TEST_DB_URL: postgresql://rustus:rustus@localhost:${{ job.services.pg.ports['5432'] }}/rustus + TEST_AMQP_URL: amqp://guest:guest@localhost:${{ job.services.rabbit.ports['5672'] }} + - name: Coveralls + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + path-to-lcov: lcov.info diff --git a/.gitignore b/.gitignore index 155c48169534dff53ee2599d1b02916e9e22ef29..31bd9be85d7d6656702340c2683eb0deb978e48a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .idea/ /target -data/ \ No newline at end of file +data/ +tarpaulin-report.html +lcov.info \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 14f6fe7f711f8c720f58bc116270836810134763..ffc5f67b4cb07df1375a4ab92a51bcd55dc73b3d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,17 +6,6 @@ repos: - repo: local hooks: - - id: build - types: - - rust - name: cargo build - language: system - entry: cargo - pass_filenames: false - args: - - build - - --features=all - - id: fmt types: - rust @@ -46,4 +35,15 @@ repos: - -W - clippy::pedantic - -D - - warnings \ No newline at end of file + - warnings + + - id: build + types: + - rust + name: cargo build + language: system + entry: cargo + pass_filenames: false + args: + - build + - --features=all diff --git a/Cargo.lock b/Cargo.lock index b1636ea7842b39583f171daf3314318a0067f10a..602fd875db043d77e955d7e59a82cfe2136d0c30 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,9 +21,9 @@ dependencies = [ [[package]] name = "actix-files" -version = "0.6.0-beta.13" +version = "0.6.0-beta.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21889a4682d4886d81e3a32eef419519a62a7990bdfb8295110b94729884f8fd" +checksum = "0b49f1b48724a52605ba40b67ede24f5a6cbc246817f9278d280d393a28e8b0e" dependencies = [ "actix-http", "actix-service", @@ -44,9 +44,9 @@ dependencies = [ [[package]] name = "actix-http" -version = "3.0.0-beta.18" +version = "3.0.0-rc.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b95871724d27ac9a23d6db3246b23e4e42cd44a23145e1c6b04b78fb5271da" +checksum = "fb0185d65352deeea60d92231708068c04dc64f1ab307a1a307206a47d5a45d3" dependencies = [ "actix-codec", "actix-rt", @@ -55,7 +55,7 @@ dependencies = [ "ahash", "base64", "bitflags", - "brotli2", + "brotli", "bytes", "bytestring", "derive_more", @@ -73,7 +73,7 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rand", + "rand 0.8.5", "sha-1 0.10.0", "smallvec", "zstd", @@ -91,9 +91,9 @@ dependencies = [ [[package]] name = "actix-router" -version = "0.5.0-rc.2" +version = "0.5.0-rc.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e0b59ad08167ffbb686ddb495846707231e96908b829b1fc218198ec581e2ad" +checksum = "cb6506dbef336634ff35d994d58daa0a412ea23751f15f9b4dcac4d594b1ed1f" dependencies = [ "bytestring", "firestorm", @@ -155,9 +155,9 @@ dependencies = [ [[package]] name = "actix-web" -version = "4.0.0-beta.20" +version = "4.0.0-rc.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa8ba5081e9f8d0016cf34df516c699198158fd8c77990aa284115b055ead61b" +checksum = "83e3c85bc4116b69913b03f16cff8cade1212508fcd321847d9cfe3d3e41f991" dependencies = [ "actix-codec", "actix-http", @@ -188,15 +188,15 @@ dependencies = [ "serde_urlencoded", "smallvec", "socket2", - "time 0.3.5", + "time 0.3.7", "url", ] [[package]] name = "actix-web-codegen" -version = "0.5.0-rc.1" +version = "0.5.0-rc.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98a793e4a7bd059e06e1bc1bd9943b57a47f806de3599d2437441682292c333e" +checksum = "4d0976042e6ddc82c7d0dedd64d39959bc26d9bba098b2f6c32a73fbef784eaf" dependencies = [ "actix-router", "proc-macro2", @@ -230,6 +230,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ef4730490ad1c4eae5c4325b2a95f521d023e5c885853ff7aca0a6a1631db3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "697ed7edc0f1711de49ce108c541623a0af97c6c60b2f6e2b65229847ac843c2" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "amq-protocol" version = "6.1.0" @@ -376,23 +391,6 @@ dependencies = [ "event-listener", ] -[[package]] -name = "async-process" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83137067e3a2a6a06d67168e49e68a0957d215410473a740cea95a2425c0b7c6" -dependencies = [ - "async-io", - "blocking", - "cfg-if", - "event-listener", - "futures-lite", - "libc", - "once_cell", - "signal-hook", - "winapi", -] - [[package]] name = "async-std" version = "1.10.0" @@ -422,9 +420,9 @@ dependencies = [ [[package]] name = "async-task" -version = "4.0.3" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91831deabf0d6d7ec49552e489aed63b7456a7a3c46cff62adad428110b0af0" +checksum = "677d306121baf53310a3fd342d88dc0824f6bbeace68347593658525565abee8" [[package]] name = "async-trait" @@ -465,15 +463,18 @@ dependencies = [ [[package]] name = "autocfg" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" +checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78" +dependencies = [ + "autocfg 1.1.0", +] [[package]] name = "autocfg" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "base-x" @@ -543,9 +544,9 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.10.0" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1d36a02058e76b040de25a4464ba1c80935655595b661505c8b39b664828b95" +checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" dependencies = [ "generic-array 0.14.5", ] @@ -574,30 +575,42 @@ dependencies = [ ] [[package]] -name = "brotli-sys" -version = "0.3.2" +name = "brotli" +version = "3.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4445dea95f4c2b41cde57cc9fee236ae4dbae88d8fcbdb4750fc1bb5d86aaecd" +checksum = "f838e47a451d5a8fa552371f80024dd6ace9b7acdf25c4c3d0f9bc6816fb1c39" dependencies = [ - "cc", - "libc", + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", ] [[package]] -name = "brotli2" -version = "0.3.2" +name = "brotli-decompressor" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cb036c3eade309815c15ddbacec5b22c4d1f3983a774ab2eac2e3e9ea85568e" +checksum = "59ad2d4653bf5ca36ae797b1f4bb4dbddb60ce49ca4aed8a2ce4829f60425b80" dependencies = [ - "brotli-sys", - "libc", + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bstr" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "lazy_static", + "memchr", + "regex-automata", ] [[package]] name = "bumpalo" -version = "3.8.0" +version = "3.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c" +checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" [[package]] name = "byte-tools" @@ -689,9 +702,9 @@ dependencies = [ [[package]] name = "combine" -version = "4.6.2" +version = "4.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2b2f5d0ee456f3928812dfc8c6d9a1d592b98678f6d56db9b0cd2b7bc6c8db5" +checksum = "50b727aacc797f9fc28e355d21f34709ac4fc9adecfe470ad07b8f4464f53062" dependencies = [ "bytes", "futures-core", @@ -735,7 +748,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94d4706de1b0fa5b132270cddffa8585166037822e260a944fe161acd137ca05" dependencies = [ "percent-encoding", - "time 0.3.5", + "time 0.3.7", "version_check", ] @@ -747,9 +760,9 @@ checksum = "396de984970346b0d9e93d1415082923c679e5ae5c3ee3dcbd104f5610af126b" [[package]] name = "core-foundation" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6888e10551bb93e424d8df1d07f1a8b4fceb0001a3a4b048bfc47554946f47b3" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" dependencies = [ "core-foundation-sys", "libc", @@ -787,18 +800,18 @@ checksum = "ccaeedb56da03b09f598226e25e80088cb4cd25f316e6e4df7d695f0feeb1403" [[package]] name = "crc32fast" -version = "1.3.0" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "738c290dfaea84fc1ca15ad9c168d083b05a714e1efddd8edaab678dc28d2836" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam-channel" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4" +checksum = "e54ea8bc3fb1ee042f5aace6e3c6e025d3874866da222930f70ce62aceba0bfa" dependencies = [ "cfg-if", "crossbeam-utils", @@ -806,9 +819,9 @@ dependencies = [ [[package]] name = "crossbeam-queue" -version = "0.3.2" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b10ddc024425c88c2ad148c1b0fd53f4c6d38db9697c9f1588381212fa657c9" +checksum = "4dd435b205a4842da59efd07628f921c096bc1cc0a156835b4fa0bcb9a19bcce" dependencies = [ "cfg-if", "crossbeam-utils", @@ -816,9 +829,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.5" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db" +checksum = "b5e5bed1f1c269533fa816a0a5492b3545209a205ca1a54842be180eb63a16a6" dependencies = [ "cfg-if", "lazy_static", @@ -831,15 +844,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f83bd3bb4314701c568e340cd8cf78c975aa0ca79e03d3f6d1677d5b0c9c0c03" dependencies = [ "generic-array 0.14.5", - "rand_core", + "rand_core 0.6.3", "subtle", ] [[package]] name = "crypto-common" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d6b536309245c849479fba3da410962a43ed8e51c26b729208ec0ac2798d0" +checksum = "a4600d695eb3f6ce1cd44e6e291adceb2cc3ab12f20a33777ecd0bf6eba34e06" dependencies = [ "generic-array 0.14.5", ] @@ -917,13 +930,12 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b697d66081d42af4fba142d56918a3cb21dc8eb63372c6b85d14f44fb9c5979b" +checksum = "8cb780dce4f9a8f5c087362b3a4595936b2019e7c8b30f2c3e9a7e94e6ae9837" dependencies = [ - "block-buffer 0.10.0", + "block-buffer 0.10.2", "crypto-common", - "generic-array 0.14.5", ] [[package]] @@ -984,9 +996,9 @@ dependencies = [ [[package]] name = "event-listener" -version = "2.5.1" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7531096570974c3a9dcf9e4b8e1cede1ec26cf5046219fb3b9d897503b9be59" +checksum = "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71" [[package]] name = "fake-simd" @@ -996,9 +1008,9 @@ checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" [[package]] name = "fastrand" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "779d043b6a0b90cc4c0ed7ee380a6504394cee7efd7db050e3774eee387324b2" +checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" dependencies = [ "instant", ] @@ -1033,9 +1045,9 @@ dependencies = [ [[package]] name = "flume" -version = "0.10.9" +version = "0.10.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24c3fd473b3a903a62609e413ed7538f99e10b665ecb502b5e481a95283f8ab4" +checksum = "0b279436a715a9de95dcd26b151db590a71961cc06e54918b24fe0dd5b7d3fc4" dependencies = [ "futures-core", "futures-sink", @@ -1074,11 +1086,17 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + [[package]] name = "futures" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28560757fe2bb34e79f907794bb6b22ae8b0e5c669b638a1132f2592b19035b4" +checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" dependencies = [ "futures-channel", "futures-core", @@ -1091,9 +1109,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3dda0b6588335f360afc675d0564c17a77a2bda81ca178a4b6081bd86c7f0b" +checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" dependencies = [ "futures-core", "futures-sink", @@ -1101,15 +1119,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0c8ff0461b82559810cdccfde3215c3f373807f5e5232b71479bff7bb2583d7" +checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" [[package]] name = "futures-executor" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29d6d2ff5bb10fb95c85b8ce46538a2e5f5e7fdc755623a7d4529ab8a4ed9d2a" +checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" dependencies = [ "futures-core", "futures-task", @@ -1124,14 +1142,14 @@ checksum = "62007592ac46aa7c2b6416f7deb9a8a8f63a01e0f1d6e1787d5630170db2b63e" dependencies = [ "futures-core", "lock_api", - "parking_lot", + "parking_lot 0.11.2", ] [[package]] name = "futures-io" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f9d34af5a1aac6fb380f735fe510746c38067c5bf16c7fd250280503c971b2" +checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" [[package]] name = "futures-lite" @@ -1150,9 +1168,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbd947adfffb0efc70599b3ddcf7b5597bb5fa9e245eb99f62b3a5f7bb8bd3c" +checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" dependencies = [ "proc-macro2", "quote", @@ -1161,15 +1179,15 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3055baccb68d74ff6480350f8d6eb8fcfa3aa11bdc1a1ae3afdd0514617d508" +checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" [[package]] name = "futures-task" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ee7c6485c30167ce4dfb83ac568a849fe53274c831081476ee13e0dce1aad72" +checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" [[package]] name = "futures-timer" @@ -1179,9 +1197,9 @@ checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" [[package]] name = "futures-util" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b5cf40b47a271f77a8b1bec03ca09044d99d2372c0de244e66430761127164" +checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" dependencies = [ "futures-channel", "futures-core", @@ -1216,9 +1234,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c" dependencies = [ "cfg-if", "libc", @@ -1227,22 +1245,21 @@ dependencies = [ [[package]] name = "gloo-timers" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f16c88aa13d2656ef20d1c042086b8767bbe2bdb62526894275a1b062161b2e" +checksum = "4d12a7f4e95cfe710f1d624fb1210b7d961a5fb05c4fd942f4feab06e61f590e" dependencies = [ "futures-channel", "futures-core", "js-sys", "wasm-bindgen", - "web-sys", ] [[package]] name = "h2" -version = "0.3.9" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f072413d126e57991455e0a922b31e4c8ba7c2ffbebf6b78b4f8521397d65cd" +checksum = "d9f1f717ddc7b2ba36df7e871fd88db79326551d3d6f1fc406fbfd28b582ff8e" dependencies = [ "bytes", "fnv", @@ -1354,9 +1371,9 @@ checksum = "eee9694f83d9b7c09682fdb32213682939507884e5bcf227be9aff5d644b90dc" [[package]] name = "httparse" -version = "1.5.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503" +checksum = "9100414882e15fb7feccb4897e5f0ff0ff1ca7d1a86a23208ada4d7a18e6c6c4" [[package]] name = "httpdate" @@ -1364,11 +1381,33 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +[[package]] +name = "httptest" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f25cfb6def593d43fae1ead24861f217e93bc70768a45cc149a69b5f049df4" +dependencies = [ + "bstr", + "bytes", + "crossbeam-channel", + "form_urlencoded", + "futures", + "http", + "hyper", + "log", + "once_cell", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", +] + [[package]] name = "hyper" -version = "0.14.16" +version = "0.14.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7ec3e62bdc98a2f0393a5048e4c30ef659440ea6e0e572965103e72bd836f55" +checksum = "043f0e083e9901b6cc658a77d1eb86f4fc650bbb977a4337dd63192826aa85dd" dependencies = [ "bytes", "futures-channel", @@ -1379,7 +1418,7 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa 0.4.8", + "itoa 1.0.1", "pin-project-lite", "socket2", "tokio", @@ -1414,11 +1453,11 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" +checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" dependencies = [ - "autocfg 1.0.1", + "autocfg 1.1.0", "hashbrown", ] @@ -1478,9 +1517,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.55" +version = "0.3.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" +checksum = "a38fc24e30fd564ce974c02bf1d337caddff65be6cc4735a1f7eab22a7440f04" dependencies = [ "wasm-bindgen", ] @@ -1502,9 +1541,9 @@ checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" [[package]] name = "lapin" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef0a8c145a248b1536cfa06890d480cda7982add59f77973ac7b03db47f349b5" +checksum = "36c0eacc7b8880c2e73ab70e47c9f099ad81af6debde9353fdb11045c0ce716e" dependencies = [ "amq-protocol", "async-task", @@ -1512,7 +1551,7 @@ dependencies = [ "futures-core", "log", "mio 0.7.14", - "parking_lot", + "parking_lot 0.12.0", "pinky-swear", "serde", ] @@ -1528,15 +1567,15 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.112" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" +checksum = "e74d72e0f9b65b5b4ca49a346af3976df0f9c61d550727f349ecd559f251a26c" [[package]] name = "libm" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a" +checksum = "33a33a362ce288760ec6a508b94caaec573ae7d3bbbd91b87aa0bad4456839db" [[package]] name = "libsqlite3-sys" @@ -1569,9 +1608,9 @@ checksum = "902eb695eb0591864543cbfbf6d742510642a605a61fc5e97fe6ceb5a30ac4fb" [[package]] name = "lock_api" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" +checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b" dependencies = [ "scopeguard", ] @@ -1644,7 +1683,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" dependencies = [ "adler", - "autocfg 1.0.1", + "autocfg 1.1.0", ] [[package]] @@ -1748,9 +1787,9 @@ dependencies = [ [[package]] name = "ntapi" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" +checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" dependencies = [ "winapi", ] @@ -1761,7 +1800,7 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f6f7833f2cbf2360a6cfd58cd41a53aa7a90bd4c202f5b1c7dd2ed73c57b2c3" dependencies = [ - "autocfg 1.0.1", + "autocfg 1.1.0", "num-integer", "num-traits", ] @@ -1772,14 +1811,14 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4547ee5541c18742396ae2c895d0717d0f886d8823b8399cdaf7b07d63ad0480" dependencies = [ - "autocfg 0.1.7", + "autocfg 0.1.8", "byteorder", "lazy_static", "libm", "num-integer", "num-iter", "num-traits", - "rand", + "rand 0.8.5", "smallvec", "zeroize", ] @@ -1790,7 +1829,7 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" dependencies = [ - "autocfg 1.0.1", + "autocfg 1.1.0", "num-traits", ] @@ -1800,7 +1839,7 @@ version = "0.1.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" dependencies = [ - "autocfg 1.0.1", + "autocfg 1.1.0", "num-integer", "num-traits", ] @@ -1811,7 +1850,7 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" dependencies = [ - "autocfg 1.0.1", + "autocfg 1.1.0", "libm", ] @@ -1825,6 +1864,15 @@ dependencies = [ "libc", ] +[[package]] +name = "num_threads" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ba99ba6393e2c3734791401b66902d981cb03bf190af674ca69949b6d5fb15" +dependencies = [ + "libc", +] + [[package]] name = "once_cell" version = "1.9.0" @@ -1859,9 +1907,9 @@ dependencies = [ [[package]] name = "openssl-probe" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" @@ -1878,7 +1926,7 @@ version = "0.9.72" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb" dependencies = [ - "autocfg 1.0.1", + "autocfg 1.1.0", "cc", "libc", "openssl-src", @@ -1900,7 +1948,17 @@ checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" dependencies = [ "instant", "lock_api", - "parking_lot_core", + "parking_lot_core 0.8.5", +] + +[[package]] +name = "parking_lot" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.1", ] [[package]] @@ -1917,6 +1975,19 @@ dependencies = [ "winapi", ] +[[package]] +name = "parking_lot_core" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28141e0cc4143da2443301914478dc976a61ffdb3f043058310c70df2fed8954" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + [[package]] name = "paste" version = "1.0.6" @@ -2026,12 +2097,12 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pinky-swear" -version = "4.4.0" +version = "4.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bf8cda6f8e1500338634e4e3ce90ac59eb7929a1e088b6946c742be1cc44dc1" +checksum = "c5ade3d5e4fa85586b4795e097180fd48447d39fb56e351bd889f1c9664291a8" dependencies = [ "doc-comment", - "parking_lot", + "parking_lot 0.12.0", "tracing", ] @@ -2137,23 +2208,35 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47aa80447ce4daf1717500037052af176af5d38cc3e571d9ec1c7353fc10c87d" +checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" dependencies = [ "proc-macro2", ] [[package]] name = "rand" -version = "0.8.4" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi", +] + +[[package]] +name = "rand" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", - "rand_core", - "rand_hc", + "rand_core 0.6.3", ] [[package]] @@ -2163,32 +2246,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.3", ] [[package]] name = "rand_core" -version = "0.6.3" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" dependencies = [ - "getrandom", + "rand_core 0.4.2", ] [[package]] -name = "rand_hc" -version = "0.3.1" +name = "rand_core" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" dependencies = [ - "rand_core", + "getrandom", ] [[package]] name = "rbatis" -version = "3.0.30" +version = "3.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8c968bd15606738295d8b71dca35cfef3bb1a124af89dab45a18ed83b19975" +checksum = "c7d3ebffcbf93beb72bf3854c590b027e8590069d5cb0d9e614ed5adc9e349a5" dependencies = [ "async-trait", "chrono", @@ -2197,7 +2286,7 @@ dependencies = [ "hex", "log", "once_cell", - "rand", + "rand 0.8.5", "rbatis-core", "rbatis-macro-driver", "rbatis_sql", @@ -2208,9 +2297,9 @@ dependencies = [ [[package]] name = "rbatis-core" -version = "3.0.20" +version = "3.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cd0da1efe628da94506a15dc8e628a6c25b6d8de77fb2d51149ac9c7893153d" +checksum = "ac21224137c4c885210ab3fd659ecd440806de10ea49ce43461d83acf3d54c1a" dependencies = [ "base64", "bigdecimal", @@ -2284,13 +2373,22 @@ dependencies = [ "hex", "indexmap", "lazy_static", - "rand", + "rand 0.8.5", "serde", "serde_bytes", "serde_json", "uuid 0.8.2", ] +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + [[package]] name = "redis" version = "0.19.0" @@ -2341,6 +2439,12 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" + [[package]] name = "regex-syntax" version = "0.6.25" @@ -2358,15 +2462,16 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c4e0a76dc12a116108933f6301b95e83634e0c47b0afbed6abbaa0601e99258" +checksum = "87f242f1488a539a79bac6dbe7c8609ae43b7914b7736210f239a37cccb32525" dependencies = [ "base64", "bytes", "encoding_rs", "futures-core", "futures-util", + "h2", "http", "http-body", "hyper", @@ -2432,16 +2537,16 @@ dependencies = [ "num-traits", "pkcs1", "pkcs8", - "rand", + "rand 0.8.5", "subtle", "zeroize", ] [[package]] name = "rust_decimal" -version = "1.19.0" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c2d4912d369fb95a351c221475657970678d344d70c1a788223f6e74d1e3732" +checksum = "4214023b1223d02a4aad9f0bb9828317634a56530870a2eaf7200a99c0c10f68" dependencies = [ "arrayvec", "num-traits", @@ -2463,7 +2568,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 1.0.4", + "semver 1.0.5", ] [[package]] @@ -2481,11 +2586,11 @@ dependencies = [ [[package]] name = "rustus" -version = "0.4.0" +version = "0.4.1" dependencies = [ "actix-files", + "actix-rt", "actix-web", - "async-process", "async-std", "async-trait", "base64", @@ -2493,6 +2598,7 @@ dependencies = [ "derive_more", "fern", "futures", + "httptest", "lapin", "lazy_static", "log", @@ -2507,6 +2613,7 @@ dependencies = [ "strfmt", "structopt", "strum", + "tempdir", "thiserror", "tokio", "tokio-amqp", @@ -2554,9 +2661,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.4.2" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525bc1abfda2e1998d152c45cf13e696f76d0a4972310b22fac1658b05df7c87" +checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" dependencies = [ "bitflags", "core-foundation", @@ -2567,9 +2674,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.4.2" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9dd14d83160b528b7bfd66439110573efcfbe281b17fc2ca9f39f550d619c7e" +checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" dependencies = [ "core-foundation-sys", "libc", @@ -2586,9 +2693,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012" +checksum = "0486718e92ec9a68fbed73bb5ef687d71103b142595b406835649bebd33f72c7" [[package]] name = "semver-parser" @@ -2598,9 +2705,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.133" +version = "1.0.136" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97565067517b60e2d1ea8b268e59ce036de907ac523ad83a0475da04e818989a" +checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" dependencies = [ "serde_derive", ] @@ -2616,9 +2723,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.133" +version = "1.0.136" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed201699328568d8d08208fdd080e3ff594e6c422e438b6705905da01005d537" +checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" dependencies = [ "proc-macro2", "quote", @@ -2627,9 +2734,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.75" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c059c05b48c5c0067d4b4b2b4f0732dd65feb52daf7e0ea09cd87e7dadc1af79" +checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" dependencies = [ "indexmap", "itoa 1.0.1", @@ -2639,12 +2746,12 @@ dependencies = [ [[package]] name = "serde_urlencoded" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 0.4.8", + "itoa 1.0.1", "ryu", "serde", ] @@ -2682,20 +2789,29 @@ checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.1", + "digest 0.10.2", ] [[package]] name = "sha1" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" +checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770" +dependencies = [ + "sha1_smol", +] + +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" [[package]] name = "sha2" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b69f9a4c9740d74c5baa3fd2e547f9525fa8088a8a958e0ca2409a514e33f5fa" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ "block-buffer 0.9.0", "cfg-if", @@ -2704,16 +2820,6 @@ dependencies = [ "opaque-debug 0.3.0", ] -[[package]] -name = "signal-hook" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "647c97df271007dcea485bb74ffdb57f2e683f1306c854f468a0c244badabf2d" -dependencies = [ - "libc", - "signal-hook-registry", -] - [[package]] name = "signal-hook-registry" version = "1.4.0" @@ -2731,15 +2837,15 @@ checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" [[package]] name = "smallvec" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" +checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" [[package]] name = "socket2" -version = "0.4.2" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516" +checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" dependencies = [ "libc", "winapi", @@ -2822,9 +2928,9 @@ dependencies = [ "memchr", "num-bigint", "once_cell", - "parking_lot", + "parking_lot 0.11.2", "percent-encoding", - "rand", + "rand 0.8.5", "regex", "rsa", "rust_decimal", @@ -2941,9 +3047,9 @@ checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" [[package]] name = "structopt" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40b9788f4202aa75c240ecc9c15c65185e6a39ccdeb0fd5d008b98825464c87c" +checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" dependencies = [ "clap", "lazy_static", @@ -2993,9 +3099,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.84" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecb2e6da8ee5eb9a61068762a32fa9619cc591ceb055b3687f4cd4051ec2e06b" +checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" dependencies = [ "proc-macro2", "quote", @@ -3016,9 +3122,9 @@ dependencies = [ [[package]] name = "tcp-stream" -version = "0.20.9" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a77f06a7c6a1ecff1bbab0743a8beca305ca1ebcbe5d1bc32390e94117859af" +checksum = "c839b9cf24db4225fa445589e014e6ecc4c42ba6ecf5db3e9fe38fbe8ea2377a" dependencies = [ "cfg-if", "mio 0.7.14", @@ -3026,15 +3132,25 @@ dependencies = [ "pem", ] +[[package]] +name = "tempdir" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" +dependencies = [ + "rand 0.4.6", + "remove_dir_all", +] + [[package]] name = "tempfile" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" dependencies = [ "cfg-if", + "fastrand", "libc", - "rand", "redox_syscall", "remove_dir_all", "winapi", @@ -3098,12 +3214,13 @@ dependencies = [ [[package]] name = "time" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41effe7cfa8af36f439fac33861b66b049edc6f9a32331e2312660529c1c24ad" +checksum = "004cbc98f30fa233c61a38bc77e96a9106e65c88f2d3bef182ae952027e5753d" dependencies = [ - "itoa 0.4.8", + "itoa 1.0.1", "libc", + "num_threads", "time-macros 0.2.3", ] @@ -3153,9 +3270,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.15.0" +version = "1.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbbf1c778ec206785635ce8ad57fe52b3009ae9e0c9f574a728f3049d3e55838" +checksum = "0c27a64b625de6d309e8c57716ba93021dccf1b3b5c97edd6d3dd2d2135afc0a" dependencies = [ "bytes", "libc", @@ -3163,7 +3280,7 @@ dependencies = [ "mio 0.7.14", "num_cpus", "once_cell", - "parking_lot", + "parking_lot 0.11.2", "pin-project-lite", "signal-hook-registry", "winapi", @@ -3171,12 +3288,12 @@ dependencies = [ [[package]] name = "tokio-amqp" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2913044c3ce17203305876b2fbd615dd98e13a20b9a24c061bbb7abab88f19cb" +checksum = "85ac53b83d524e047f3d20ce43dc5851d932dcb187578a5b37f754ca61b909d1" dependencies = [ "lapin", - "parking_lot", + "parking_lot 0.12.0", "tokio", ] @@ -3234,9 +3351,9 @@ checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" [[package]] name = "tracing" -version = "0.1.29" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105" +checksum = "2d8d93354fe2a8e50d5953f5ae2e47a3fc2ef03292e7ea46e3cc38f549525fb9" dependencies = [ "cfg-if", "pin-project-lite", @@ -3245,9 +3362,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.21" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4" +checksum = "03cfcb51380632a72d3111cb8d3447a8d908e577d31beeac006f836383d29a23" dependencies = [ "lazy_static", ] @@ -3296,9 +3413,9 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" +checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" [[package]] name = "unicode-width" @@ -3407,9 +3524,9 @@ checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" [[package]] name = "wasm-bindgen" -version = "0.2.78" +version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" +checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -3417,9 +3534,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.78" +version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" +checksum = "8b21c0df030f5a177f3cba22e9bc4322695ec43e7257d865302900290bcdedca" dependencies = [ "bumpalo", "lazy_static", @@ -3432,9 +3549,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e8d7523cb1f2a4c96c1317ca690031b714a51cc14e05f712446691f413f5d39" +checksum = "2eb6ec270a31b1d3c7e266b999739109abce8b6c87e4b31fcfcd788b65267395" dependencies = [ "cfg-if", "js-sys", @@ -3444,9 +3561,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.78" +version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" +checksum = "2f4203d69e40a52ee523b2529a773d5ffc1dc0071801c87b3d270b471b80ed01" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3454,9 +3571,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.78" +version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" +checksum = "bfa8a30d46208db204854cadbb5d4baf5fcf8071ba5bf48190c3e59937962ebc" dependencies = [ "proc-macro2", "quote", @@ -3467,15 +3584,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.78" +version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" +checksum = "3d958d035c4438e28c70e4321a2911302f10135ce78a9c7834c0cab4123d06a2" [[package]] name = "web-sys" -version = "0.3.55" +version = "0.3.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" +checksum = "c060b319f29dd25724f09a2ba1418f142f539b2be99fbf4d2d5a8f7330afb8eb" dependencies = [ "js-sys", "wasm-bindgen", @@ -3541,6 +3658,49 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-sys" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df6e476185f92a12c072be4a189a0210dcdcf512a1891d6dff9edb874deadc6" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8e92753b1c443191654ec532f14c199742964a061be25d77d7a96f09db20bf5" + +[[package]] +name = "windows_i686_gnu" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a711c68811799e017b6038e0922cb27a5e2f43a2ddb609fe0b6f3eeda9de615" + +[[package]] +name = "windows_i686_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c11bb1a02615db74680b32a68e2d61f553cc24c4eb5b4ca10311740e44172" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c912b12f7454c6620635bbff3450962753834be2a594819bd5e945af18ec64bc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316" + [[package]] name = "winreg" version = "0.7.0" @@ -3567,9 +3727,9 @@ dependencies = [ [[package]] name = "zeroize_derive" -version = "1.2.2" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65f1a51723ec88c66d5d1fe80c841f17f63587d6691901d66be9bec6c3b51f73" +checksum = "81e8f13fef10b63c06356d65d416b070798ddabcadc10d3ece0c5be9b3c7eddb" dependencies = [ "proc-macro2", "quote", @@ -3579,18 +3739,18 @@ dependencies = [ [[package]] name = "zstd" -version = "0.9.1+zstd.1.5.1" +version = "0.10.0+zstd.1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "538b8347df9257b7fbce37677ef7535c00a3c7bf1f81023cc328ed7fe4b41de8" +checksum = "3b1365becbe415f3f0fcd024e2f7b45bacfb5bdd055f0dc113571394114e7bdd" dependencies = [ "zstd-safe", ] [[package]] name = "zstd-safe" -version = "4.1.2+zstd.1.5.1" +version = "4.1.4+zstd.1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb4cfe2f6e6d35c5d27ecd9d256c4b6f7933c4895654917460ec56c29336cc1" +checksum = "2f7cd17c9af1a4d6c24beb1cc54b17e2ef7b593dc92f19e9d9acad8b182bbaee" dependencies = [ "libc", "zstd-sys", @@ -3598,9 +3758,9 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "1.6.2+zstd.1.5.1" +version = "1.6.3+zstd.1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2daf2f248d9ea44454bfcb2516534e8b8ad2fc91bf818a1885495fc42bc8ac9f" +checksum = "fc49afa5c8d634e75761feda8c592051e7eeb4683ba827211eb0d731d3402ea8" dependencies = [ "cc", "libc", diff --git a/Cargo.toml b/Cargo.toml index fcbb70db8a7f69011745f059c874044d7254d681..a083d0debca07ad7770de7127d361a8a2172702c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustus" -version = "0.4.0" +version = "0.4.1" edition = "2021" description = "TUS protocol implementation written in Rust." @@ -12,12 +12,15 @@ async-trait = "0.1.52" base64 = "^0.13.0" lazy_static = "1.4.0" log = "^0.4.14" -serde = "1" serde_json = "1" strfmt = "^0.1.6" thiserror = "^1.0" url = "2.2.2" +[dependencies.serde] +version = "1" +features = ["derive"] + [dependencies.openssl] version = "0.10.38" features = ["vendored"] @@ -71,10 +74,6 @@ version = "^3.0" optional = true version = "2.0" -[dependencies.async-process] -version = "1.3.0" -optional = true - [dependencies.reqwest] features = ["json"] optional = true @@ -88,7 +87,7 @@ features = ["derive"] version = "0.23" [dependencies.tokio] -features = ["time"] +features = ["time", "process"] version = "1.4.0" [dependencies.tokio-amqp] @@ -100,13 +99,23 @@ features = ["v4"] version = "^1.0.0-alpha.1" [features] -all = ["redis_info_storage", "db_info_storage", "http_notifier", "amqp_notifier", "file_notifiers"] +all = ["redis_info_storage", "db_info_storage", "http_notifier", "amqp_notifier"] amqp_notifier = ["lapin", "tokio-amqp", "mobc-lapin"] db_info_storage = ["rbatis", "rbson"] default = [] http_notifier = ["reqwest"] redis_info_storage = ["mobc-redis"] -file_notifiers = ["async-process"] + +### For testing +test_redis = [] +test_db = [] +test_rmq = [] +integration_tests = ["test_redis", "test_db", "test_rmq"] + +[dev-dependencies] +tempdir = "0.3.7" +actix-rt = "2.6.0" +httptest = "0.15.4" [profile] [profile.release] diff --git a/README.md b/README.md index 7cb97ee7f5db3f4e896bf0e0e0535b30325e103c..ea1bb822d14015f290251c6204c4cdf1accadfdf 100644 --- a/README.md +++ b/README.md @@ -226,7 +226,8 @@ Example of a single file hook: # Hook name would be "pre-create", "post-create" and so on. HOOK_NAME="$1" -MEME="$(cat /dev/stdin | jq ".upload .metadata .meme" | xargs)" +HOOK_INFO="$2" +MEME="$(echo "$HOOK_INFO" | jq ".upload .metadata .meme" | xargs)" # Here we check if name in metadata is equal to pepe. if [[ $MEME = "pepe" ]]; then @@ -271,7 +272,10 @@ rustus --hooks-dir "hooks" In this case rustus will append a hook name to the directory you pointed at and call it as an executable. -Information about hook can be found in stdin. +Information about hook is passed as a first parameter, as if you call script by running: +```bash +./hooks/pre-create '{"id": "someid", ...}' +``` ### Http Hooks diff --git a/src/config.rs b/src/config.rs index 2adf14dadb70f49dcb5001534f8445dc1c0e77a0..f772c6fab56dad52945036b7e7831a8752f93430 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,10 +1,6 @@ -use std::collections::HashMap; -use std::env; +use std::ffi::OsString; use std::path::PathBuf; -use chrono::{Datelike, Timelike}; -use lazy_static::lazy_static; -use log::error; use structopt::StructOpt; use crate::info_storages::AvailableInfoStores; @@ -13,17 +9,6 @@ use crate::protocol::extensions::Extensions; use crate::storages::AvailableStores; -lazy_static! { - /// Freezing ENVS on startup. - static ref ENV_MAP: HashMap<String, String> = { - let mut m = HashMap::new(); - for (key, value) in env::vars() { - m.insert(format!("env[{}]", key), value); - } - m - }; -} - #[derive(StructOpt, Debug, Clone)] pub struct StorageOptions { /// Rustus storage type. @@ -124,11 +109,17 @@ pub struct NotificationsOptions { #[structopt(long, env = "RUSTUS_HOOKS_AMQP_EXCHANGE", default_value = "rustus")] pub hooks_amqp_exchange: String, - #[cfg(feature = "file_notifiers")] + #[cfg(feature = "amqp_notifier")] + #[structopt( + long, + env = "RUSTUS_HOOKS_AMQP_QUEUES_PREFIX", + default_value = "rustus" + )] + pub hooks_amqp_queues_prefix: String, + #[structopt(long, env = "RUSTUS_HOOKS_DIR")] pub hooks_dir: Option<PathBuf>, - #[cfg(feature = "file_notifiers")] #[structopt(long, env = "RUSTUS_HOOKS_FILE")] pub hooks_file: Option<String>, } @@ -200,6 +191,7 @@ pub struct RustusConf { pub notification_opts: NotificationsOptions, } +#[cfg_attr(coverage, no_coverage)] impl RustusConf { /// Function to parse CLI parametes. /// @@ -209,6 +201,14 @@ impl RustusConf { <RustusConf as StructOpt>::from_args() } + pub fn from_iter<I>(iter: I) -> RustusConf + where + I: IntoIterator, + I::Item: Into<OsString> + Clone, + { + <RustusConf as StructOpt>::from_iter(iter) + } + /// Base API url. pub fn base_url(&self) -> String { format!( @@ -219,14 +219,16 @@ impl RustusConf { ) } - /// URL for a particular file. - pub fn file_url(&self) -> String { + /// Helper for generating URI for test files. + #[cfg(test)] + pub fn file_url(&self, file_id: &str) -> String { let base_url = self.base_url(); format!( - "{}/{{file_id}}", + "{}/{}", base_url .strip_suffix('/') - .unwrap_or_else(|| base_url.as_str()) + .unwrap_or_else(|| base_url.as_str()), + file_id ) } @@ -235,21 +237,6 @@ impl RustusConf { self.notification_opts.hooks.contains(&hook) } - /// Generate directory name with user template. - pub fn dir_struct(&self) -> String { - let now = chrono::Utc::now(); - let mut vars: HashMap<String, String> = ENV_MAP.clone(); - vars.insert("day".into(), now.day().to_string()); - vars.insert("month".into(), now.month().to_string()); - vars.insert("year".into(), now.year().to_string()); - vars.insert("hour".into(), now.hour().to_string()); - vars.insert("minute".into(), now.minute().to_string()); - strfmt::strfmt(self.storage_opts.dir_structure.as_str(), &vars).unwrap_or_else(|err| { - error!("{}", err); - "".into() - }) - } - /// List of extensions. /// /// This function will parse list of extensions from CLI diff --git a/src/errors.rs b/src/errors.rs index 19be4062cb8e175be17b2094c0b2c237626bed2c..942fe39fdef95c98a0623950f8ed02a20dc13258 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -61,6 +61,7 @@ pub enum RustusError { } /// This conversion allows us to use `RustusError` in the `main` function. +#[cfg_attr(coverage, no_coverage)] impl From<RustusError> for Error { fn from(err: RustusError) -> Self { Error::new(ErrorKind::Other, err) @@ -68,6 +69,7 @@ impl From<RustusError> for Error { } /// Trait to convert errors to http-responses. +#[cfg_attr(coverage, no_coverage)] impl ResponseError for RustusError { fn error_response(&self) -> HttpResponse { error!("{}", self); diff --git a/src/info_storages/db_info_storage.rs b/src/info_storages/db_info_storage.rs index e96f053bc715eac8bf597fbb317c00f60004cb20..164f2bcf82a9bea0a9d2ed645cdd8429c7a8316e 100644 --- a/src/info_storages/db_info_storage.rs +++ b/src/info_storages/db_info_storage.rs @@ -9,7 +9,6 @@ use rbatis::rbatis::Rbatis; use crate::errors::{RustusError, RustusResult}; use crate::info_storages::{FileInfo, InfoStorage}; -use crate::RustusConf; #[crud_table] struct DbModel { @@ -33,15 +32,11 @@ pub struct DBInfoStorage { } impl DBInfoStorage { - pub async fn new(app_conf: RustusConf) -> RustusResult<Self> { + pub async fn new(dsn: &str) -> RustusResult<Self> { let db = Rbatis::new(); let mut opts = DBPoolOptions::new(); opts.connect_timeout = Duration::new(2, 0); - db.link_opt( - app_conf.info_storage_opts.info_db_dsn.unwrap().as_str(), - opts, - ) - .await?; + db.link_opt(dsn, opts).await?; Ok(Self { db }) } } @@ -84,3 +79,68 @@ impl InfoStorage for DBInfoStorage { Ok(()) } } + +#[cfg(feature = "test_db")] +#[cfg(test)] +mod tests { + use super::{DBInfoStorage, DbModel}; + use crate::info_storages::FileInfo; + use crate::InfoStorage; + use rbatis::crud::CRUD; + + async fn get_info_storage() -> DBInfoStorage { + let db_url = std::env::var("TEST_DB_URL").unwrap(); + let mut storage = DBInfoStorage::new(db_url.as_str()).await.unwrap(); + storage.prepare().await.unwrap(); + storage + } + + #[actix_rt::test] + async fn success() { + let info_storage = get_info_storage().await; + let file_info = FileInfo::new_test(); + info_storage.set_info(&file_info, true).await.unwrap(); + let info = info_storage + .db + .fetch_by_column::<Option<DbModel>, &str>("id", file_info.id.as_str()) + .await + .unwrap(); + assert!(info.is_some()); + let info = info_storage.get_info(file_info.id.as_str()).await.unwrap(); + assert_eq!(file_info.id, info.id); + assert_eq!(file_info.storage, info.storage); + assert_eq!(file_info.length, info.length); + } + + #[actix_rt::test] + async fn success_deletion() { + let info_storage = get_info_storage().await; + let file_info = FileInfo::new_test(); + info_storage.set_info(&file_info, true).await.unwrap(); + info_storage + .remove_info(file_info.id.as_str()) + .await + .unwrap(); + let info = info_storage + .db + .fetch_by_column::<Option<DbModel>, &str>("id", file_info.id.as_str()) + .await + .unwrap(); + assert!(info.is_none()); + } + + #[actix_rt::test] + async fn deletion_not_found() { + let info_storage = get_info_storage().await; + let res = info_storage.remove_info("unknown").await; + // We don't care if it doesn't exist. + assert!(res.is_ok()); + } + + #[actix_rt::test] + async fn getting_not_found() { + let info_storage = get_info_storage().await; + let res = info_storage.get_info("unknown").await; + assert!(res.is_err()); + } +} diff --git a/src/info_storages/file_info_storage.rs b/src/info_storages/file_info_storage.rs index 338ac6800406dfe8166fab78f8028eb3fe8105eb..65cd3cac2dab9699b52f982cfaad99fb33de96c4 100644 --- a/src/info_storages/file_info_storage.rs +++ b/src/info_storages/file_info_storage.rs @@ -7,31 +7,27 @@ use log::error; use crate::errors::{RustusError, RustusResult}; use crate::info_storages::{FileInfo, InfoStorage}; -use crate::RustusConf; pub struct FileInfoStorage { - app_conf: RustusConf, + info_dir: PathBuf, } impl FileInfoStorage { - pub fn new(app_conf: RustusConf) -> Self { - Self { app_conf } + pub fn new(info_dir: PathBuf) -> Self { + Self { info_dir } } pub fn info_file_path(&self, file_id: &str) -> PathBuf { - self.app_conf - .info_storage_opts - .info_dir - .join(format!("{}.info", file_id)) + self.info_dir.join(format!("{}.info", file_id)) } } #[async_trait] impl InfoStorage for FileInfoStorage { async fn prepare(&mut self) -> RustusResult<()> { - if !self.app_conf.info_storage_opts.info_dir.exists() { + if !self.info_dir.exists() { DirBuilder::new() - .create(self.app_conf.info_storage_opts.info_dir.as_path()) + .create(self.info_dir.as_path()) .await .map_err(|err| RustusError::UnableToPrepareInfoStorage(err.to_string()))?; } @@ -42,6 +38,7 @@ impl InfoStorage for FileInfoStorage { let mut file = OpenOptions::new() .write(true) .create(create) + .truncate(true) .open(self.info_file_path(file_info.id.as_str()).as_path()) .await .map_err(|err| { @@ -86,3 +83,78 @@ impl InfoStorage for FileInfoStorage { }) } } + +#[cfg(test)] +mod tests { + use super::FileInfoStorage; + use crate::info_storages::FileInfo; + use crate::InfoStorage; + use std::collections::HashMap; + use std::fs::File; + use std::io::{Read, Write}; + + #[actix_rt::test] + async fn preparation() { + let dir = tempdir::TempDir::new("file_info").unwrap(); + let target_path = dir.into_path().join("not_exist"); + let mut storage = FileInfoStorage::new(target_path.clone()); + assert!(!target_path.exists()); + storage.prepare().await.unwrap(); + assert!(target_path.exists()); + } + + #[actix_rt::test] + async fn setting_info() { + let dir = tempdir::TempDir::new("file_info").unwrap(); + let storage = FileInfoStorage::new(dir.into_path()); + let file_info = FileInfo::new( + uuid::Uuid::new_v4().to_string().as_str(), + Some(10), + Some("random_path".into()), + "random_storage".into(), + None, + ); + storage.set_info(&file_info, true).await.unwrap(); + let info_path = storage.info_file_path(file_info.id.as_str()); + let mut buffer = String::new(); + File::open(info_path) + .unwrap() + .read_to_string(&mut buffer) + .unwrap(); + assert!(buffer.len() > 0); + } + + #[actix_rt::test] + async fn set_get_info() { + let dir = tempdir::TempDir::new("file_info").unwrap(); + let storage = FileInfoStorage::new(dir.into_path()); + let file_info = FileInfo::new( + uuid::Uuid::new_v4().to_string().as_str(), + Some(10), + Some("random_path".into()), + "random_storage".into(), + { + let mut a = HashMap::new(); + a.insert("test".into(), "pest".into()); + Some(a) + }, + ); + storage.set_info(&file_info, true).await.unwrap(); + let read_info = storage.get_info(file_info.id.as_str()).await.unwrap(); + assert_eq!(read_info.id, read_info.id); + assert_eq!(read_info.length, read_info.length); + assert_eq!(read_info.path, read_info.path); + assert_eq!(read_info.metadata, read_info.metadata); + } + + #[actix_rt::test] + async fn get_broken_info() { + let dir = tempdir::TempDir::new("file_info").unwrap(); + let storage = FileInfoStorage::new(dir.into_path()); + let file_id = "random_file"; + let mut file = File::create(storage.info_file_path(file_id)).unwrap(); + file.write_all("{not a json}".as_bytes()).unwrap(); + let read_info = storage.get_info(file_id).await; + assert!(read_info.is_err()); + } +} diff --git a/src/info_storages/models/available_info_storages.rs b/src/info_storages/models/available_info_storages.rs index 66a84cd717781da8b919b6afe8c145c5140cb877..e5e481fb556dc20e606b3745689d36b7a644447c 100644 --- a/src/info_storages/models/available_info_storages.rs +++ b/src/info_storages/models/available_info_storages.rs @@ -32,21 +32,38 @@ impl AvailableInfoStores { /// # Params /// `config` - Rustus configuration. /// + #[cfg_attr(coverage, no_coverage)] pub async fn get( &self, config: &RustusConf, ) -> RustusResult<Box<dyn InfoStorage + Sync + Send>> { match self { Self::Files => Ok(Box::new(file_info_storage::FileInfoStorage::new( - config.clone(), + config.info_storage_opts.info_dir.clone(), ))), #[cfg(feature = "db_info_storage")] Self::DB => Ok(Box::new( - db_info_storage::DBInfoStorage::new(config.clone()).await?, + db_info_storage::DBInfoStorage::new( + config + .info_storage_opts + .info_db_dsn + .clone() + .unwrap() + .as_str(), + ) + .await?, )), #[cfg(feature = "redis_info_storage")] AvailableInfoStores::Redis => Ok(Box::new( - redis_info_storage::RedisStorage::new(config.clone()).await?, + redis_info_storage::RedisStorage::new( + config + .info_storage_opts + .info_db_dsn + .clone() + .unwrap() + .as_str(), + ) + .await?, )), } } diff --git a/src/info_storages/models/file_info.rs b/src/info_storages/models/file_info.rs index 44c55a161202d14598e01374bf5dc02043830cc8..bace48bd72d78095b72a280a05396a0e0d2e5c09 100644 --- a/src/info_storages/models/file_info.rs +++ b/src/info_storages/models/file_info.rs @@ -87,4 +87,15 @@ impl FileInfo { Some(result.join(",")) } } + + #[cfg(test)] + pub fn new_test() -> Self { + FileInfo::new( + uuid::Uuid::new_v4().to_string().as_str(), + Some(10), + Some("random_path".into()), + "random_storage".into(), + None, + ) + } } diff --git a/src/info_storages/redis_info_storage.rs b/src/info_storages/redis_info_storage.rs index 584407df2900bd1d0a3ffcb8959aff6f17150c9a..b9e0b3b11b4686f9e454b593f38188ea589f8dfd 100644 --- a/src/info_storages/redis_info_storage.rs +++ b/src/info_storages/redis_info_storage.rs @@ -6,15 +6,14 @@ use redis::aio::Connection; use crate::errors::{RustusError, RustusResult}; use crate::info_storages::{FileInfo, InfoStorage}; -use crate::RustusConf; pub struct RedisStorage { pool: Pool<RedisConnectionManager>, } impl RedisStorage { - pub async fn new(app_conf: RustusConf) -> RustusResult<Self> { - let client = redis::Client::open(app_conf.info_storage_opts.info_db_dsn.unwrap().as_str())?; + pub async fn new(db_dsn: &str) -> RustusResult<Self> { + let client = redis::Client::open(db_dsn)?; let manager = RedisConnectionManager::new(client); let pool = Pool::builder().max_open(100).build(manager); Ok(Self { pool }) @@ -62,3 +61,81 @@ impl InfoStorage for RedisStorage { } } } + +#[cfg(test)] +#[cfg(feature = "test_redis")] +mod tests { + use super::RedisStorage; + use crate::info_storages::FileInfo; + use crate::InfoStorage; + use mobc_redis::redis; + use mobc_redis::redis::AsyncCommands; + + async fn get_storage() -> RedisStorage { + let redis_url = std::env::var("TEST_REDIS_URL").unwrap(); + RedisStorage::new(redis_url.as_str()).await.unwrap() + } + + async fn get_redis() -> redis::aio::Connection { + let redis_url = std::env::var("TEST_REDIS_URL").unwrap(); + let redis = redis::Client::open(redis_url).unwrap(); + redis.get_async_connection().await.unwrap() + } + + #[actix_rt::test] + async fn success() { + let info_storage = get_storage().await; + let file_info = FileInfo::new_test(); + info_storage.set_info(&file_info, true).await.unwrap(); + let mut redis = get_redis().await; + let value: Option<String> = redis.get(file_info.id.as_str()).await.unwrap(); + assert!(value.is_some()); + + let file_info_from_storage = info_storage.get_info(file_info.id.as_str()).await.unwrap(); + + assert_eq!(file_info.id, file_info_from_storage.id); + assert_eq!(file_info.path, file_info_from_storage.path); + assert_eq!(file_info.storage, file_info_from_storage.storage); + } + + #[actix_rt::test] + async fn no_connection() { + let info_storage = RedisStorage::new("redis://unknonwn_url/0").await.unwrap(); + let file_info = FileInfo::new_test(); + let res = info_storage.set_info(&file_info, true).await; + assert!(res.is_err()); + } + + #[actix_rt::test] + async fn unknown_id() { + let info_storage = get_storage().await; + let res = info_storage + .get_info(uuid::Uuid::new_v4().to_string().as_str()) + .await; + assert!(res.is_err()); + } + + #[actix_rt::test] + async fn deletion_success() { + let info_storage = get_storage().await; + let mut redis = get_redis().await; + let res = info_storage.remove_info("unknown").await; + assert!(res.is_err()); + let file_info = FileInfo::new_test(); + info_storage.set_info(&file_info, true).await.unwrap(); + assert!(redis + .get::<&str, Option<String>>(file_info.id.as_str()) + .await + .unwrap() + .is_some()); + info_storage + .remove_info(file_info.id.as_str()) + .await + .unwrap(); + assert!(redis + .get::<&str, Option<String>>(file_info.id.as_str()) + .await + .unwrap() + .is_none()); + } +} diff --git a/src/main.rs b/src/main.rs index b05a51bf48a5110c010348f297fb62b0cc0e5214..93aacc19b89975f8e51d66314f381374d2c21764 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +#![cfg_attr(coverage, feature(no_coverage))] use std::str::FromStr; use std::sync::Arc; @@ -10,12 +11,13 @@ use fern::colors::{Color, ColoredLevelConfig}; use fern::Dispatch; use log::LevelFilter; +use config::RustusConf; + use crate::errors::RustusResult; use crate::info_storages::InfoStorage; use crate::notifiers::models::notification_manager::NotificationManager; +use crate::server::rustus_service; use crate::state::State; -use config::RustusConf; - use crate::storages::Storage; mod config; @@ -24,10 +26,12 @@ mod info_storages; mod notifiers; mod protocol; mod routes; +mod server; mod state; mod storages; mod utils; +#[cfg_attr(coverage, no_coverage)] fn greeting(app_conf: &RustusConf) { let extensions = app_conf .extensions_vec() @@ -66,24 +70,15 @@ fn greeting(app_conf: &RustusConf) { /// This function may throw an error /// if the server can't be bound to the /// given address. +#[cfg_attr(coverage, no_coverage)] pub fn create_server(state: State) -> Result<Server, std::io::Error> { let host = state.config.host.clone(); let port = state.config.port; - let config = state.config.clone(); let workers = state.config.workers; let state_data: web::Data<State> = web::Data::from(Arc::new(state)); let mut server = HttpServer::new(move || { App::new() - .app_data(state_data.clone()) - // Adds all routes. - .configure(protocol::setup(config.clone())) - // Main middleware that appends TUS headers. - .wrap( - middleware::DefaultHeaders::new() - .add(("Tus-Resumable", "1.0.0")) - .add(("Tus-Max-Size", config.max_body_size.to_string())) - .add(("Tus-Version", "1.0.0")), - ) + .configure(rustus_service(state_data.clone())) .wrap(middleware::Logger::new("\"%r\" \"-\" \"%s\" \"%a\" \"%D\"")) // Middleware that overrides method of a request if // "X-HTTP-Method-Override" header is provided. @@ -111,6 +106,7 @@ pub fn create_server(state: State) -> Result<Server, std::io::Error> { Ok(server.run()) } +#[cfg_attr(coverage, no_coverage)] fn setup_logging(app_config: &RustusConf) -> RustusResult<()> { let colors = ColoredLevelConfig::new() // use builder methods @@ -137,6 +133,7 @@ fn setup_logging(app_config: &RustusConf) -> RustusResult<()> { } /// Main program entrypoint. +#[cfg_attr(coverage, no_coverage)] #[actix_web::main] async fn main() -> std::io::Result<()> { let app_conf = RustusConf::from_args(); diff --git a/src/notifiers/amqp_notifier.rs b/src/notifiers/amqp_notifier.rs index 4177c61be55400c20d2c925f9fe798e76be7095e..a7088f96161c65163754ff60cbc62949ca80b7fa 100644 --- a/src/notifiers/amqp_notifier.rs +++ b/src/notifiers/amqp_notifier.rs @@ -1,5 +1,5 @@ use crate::notifiers::{Hook, Notifier}; -use crate::{RustusConf, RustusResult}; +use crate::RustusResult; use actix_web::http::header::HeaderMap; use async_trait::async_trait; use lapin::options::{ @@ -15,23 +15,25 @@ use tokio_amqp::LapinTokioExt; pub struct AMQPNotifier { exchange_name: String, pool: Pool<RMQConnectionManager>, + queues_prefix: String, } impl AMQPNotifier { - pub fn new(app_conf: RustusConf) -> Self { + pub fn new(amqp_url: &str, exchange: &str, queues_prefix: &str) -> Self { let manager = RMQConnectionManager::new( - app_conf.notification_opts.hooks_amqp_url.unwrap(), + amqp_url.into(), ConnectionProperties::default().with_tokio(), ); let pool = Pool::<RMQConnectionManager>::builder().build(manager); Self { pool, - exchange_name: app_conf.notification_opts.hooks_amqp_exchange, + exchange_name: exchange.into(), + queues_prefix: queues_prefix.into(), } } - pub fn get_queue_name(hook: Hook) -> String { - format!("rustus.{}", hook) + pub fn get_queue_name(&self, hook: Hook) -> String { + format!("{}.{}", self.queues_prefix.as_str(), hook) } } @@ -47,7 +49,7 @@ impl Notifier for AMQPNotifier { ) .await?; for hook in Hook::iter() { - let queue_name = Self::get_queue_name(hook); + let queue_name = self.get_queue_name(hook); chan.queue_declare( queue_name.as_str(), QueueDeclareOptions::default(), @@ -73,7 +75,7 @@ impl Notifier for AMQPNotifier { _header_map: &HeaderMap, ) -> RustusResult<()> { let chan = self.pool.get().await?.create_channel().await?; - let queue = Self::get_queue_name(hook); + let queue = self.get_queue_name(hook); chan.basic_publish( self.exchange_name.as_str(), queue.as_str(), @@ -85,3 +87,68 @@ impl Notifier for AMQPNotifier { Ok(()) } } + +#[cfg(feature = "test_rmq")] +#[cfg(test)] +mod tests { + use super::AMQPNotifier; + use crate::notifiers::{Hook, Notifier}; + use actix_web::http::header::HeaderMap; + use lapin::options::{BasicAckOptions, BasicGetOptions}; + + async fn get_notifier() -> AMQPNotifier { + let amqp_url = std::env::var("TEST_AMQP_URL").unwrap(); + let mut notifier = AMQPNotifier::new( + amqp_url.as_str(), + uuid::Uuid::new_v4().to_string().as_str(), + uuid::Uuid::new_v4().to_string().as_str(), + ); + notifier.prepare().await.unwrap(); + notifier + } + + #[actix_rt::test] + async fn success() { + let notifier = get_notifier().await; + let hook = Hook::PostCreate; + let test_msg = String::from("Test Message"); + notifier + .send_message(test_msg.clone(), hook.clone(), &HeaderMap::new()) + .await + .unwrap(); + let chan = notifier + .pool + .get() + .await + .unwrap() + .create_channel() + .await + .unwrap(); + let message = chan + .basic_get( + format!("{}.{}", notifier.queues_prefix.as_str(), hook).as_str(), + BasicGetOptions::default(), + ) + .await + .unwrap(); + assert!(message.is_some()); + assert_eq!( + String::from_utf8(message.clone().unwrap().data.clone()).unwrap(), + test_msg + ); + message + .unwrap() + .ack(BasicAckOptions::default()) + .await + .unwrap(); + } + + #[actix_rt::test] + async fn unknown_url() { + let notifier = AMQPNotifier::new("http://unknown", "test", "test"); + let res = notifier + .send_message("Test Message".into(), Hook::PostCreate, &HeaderMap::new()) + .await; + assert!(res.is_err()); + } +} diff --git a/src/notifiers/dir_notifier.rs b/src/notifiers/dir_notifier.rs index 670d1e1bd04aeb9839ce3691932d89b7f0474d50..008d79ca07ca42244619e1aef9d4eddc65a5e32b 100644 --- a/src/notifiers/dir_notifier.rs +++ b/src/notifiers/dir_notifier.rs @@ -2,11 +2,10 @@ use crate::errors::RustusError; use crate::notifiers::{Hook, Notifier}; use crate::RustusResult; use actix_web::http::header::HeaderMap; -use async_process::{Command, Stdio}; use async_trait::async_trait; -use futures::AsyncWriteExt; use log::debug; use std::path::PathBuf; +use tokio::process::Command; pub struct DirNotifier { pub dir: PathBuf, @@ -31,18 +30,71 @@ impl Notifier for DirNotifier { _headers_map: &HeaderMap, ) -> RustusResult<()> { let hook_path = self.dir.join(hook.to_string()); + if !hook_path.exists() { + debug!("Hook {} not found.", hook.to_string()); + return Err(RustusError::HookError(format!( + "Hook file {} not found.", + hook + ))); + } debug!("Running hook: {}", hook_path.as_path().display()); - let mut command = Command::new(hook_path).stdin(Stdio::piped()).spawn()?; - command - .stdin - .as_mut() - .unwrap() - .write_all(message.as_bytes()) - .await?; - let stat = command.status().await?; + let mut command = Command::new(hook_path).arg(message).spawn()?; + let stat = command.wait().await?; if !stat.success() { return Err(RustusError::HookError("Returned wrong status code".into())); } Ok(()) } } + +#[cfg(test)] +mod tests { + use super::DirNotifier; + use crate::notifiers::{Hook, Notifier}; + use actix_web::http::header::HeaderMap; + use std::fs::File; + use std::io::{Read, Write}; + #[cfg(unix)] + use std::os::unix::fs::PermissionsExt; + use tempdir::TempDir; + + #[actix_rt::test] + async fn no_such_hook_file() { + let hook_dir = TempDir::new("dir_notifier").unwrap().into_path(); + let notifier = DirNotifier::new(hook_dir); + let res = notifier + .send_message("test".into(), Hook::PostCreate, &HeaderMap::new()) + .await; + assert!(res.is_err()); + } + + #[cfg(unix)] + #[actix_rt::test] + async fn success() { + let hook = Hook::PostCreate; + let dir = tempdir::TempDir::new("dir_notifier").unwrap().into_path(); + let hook_path = dir.join(hook.to_string()); + { + let mut file = File::create(hook_path.clone()).unwrap(); + let mut permissions = file.metadata().unwrap().permissions(); + permissions.set_mode(0o755); + file.set_permissions(permissions).unwrap(); + let script = r#"#!/bin/sh + echo "$1" > "$(dirname $0)/output""#; + file.write_all(script.as_bytes()).unwrap(); + file.sync_all().unwrap(); + } + let notifier = DirNotifier::new(dir.to_path_buf()); + let test_message = uuid::Uuid::new_v4().to_string(); + notifier + .send_message(test_message.clone(), hook.clone(), &HeaderMap::new()) + .await + .unwrap(); + let output_path = dir.join("output"); + assert!(output_path.exists()); + let mut buffer = String::new(); + let mut out_file = File::open(output_path).unwrap(); + out_file.read_to_string(&mut buffer).unwrap(); + assert_eq!(buffer, format!("{}\n", test_message)); + } +} diff --git a/src/notifiers/file_notifier.rs b/src/notifiers/file_notifier.rs index a502592c24fdc68fc3ef2db125598e3f1b1f2f75..5ac7e31484df5bf543483947075878968ce474fc 100644 --- a/src/notifiers/file_notifier.rs +++ b/src/notifiers/file_notifier.rs @@ -2,10 +2,9 @@ use crate::errors::RustusError; use crate::notifiers::{Hook, Notifier}; use crate::RustusResult; use actix_web::http::header::HeaderMap; -use async_process::{Command, Stdio}; use async_trait::async_trait; -use futures::AsyncWriteExt; use log::debug; +use tokio::process::Command; pub struct FileNotifier { pub command: String, @@ -19,6 +18,7 @@ impl FileNotifier { #[async_trait] impl Notifier for FileNotifier { + #[cfg_attr(coverage, no_coverage)] async fn prepare(&mut self) -> RustusResult<()> { Ok(()) } @@ -32,18 +32,87 @@ impl Notifier for FileNotifier { debug!("Running command: {}", self.command.as_str()); let mut command = Command::new(self.command.as_str()) .arg(hook.to_string()) - .stdin(Stdio::piped()) + .arg(message) .spawn()?; - command - .stdin - .as_mut() - .unwrap() - .write_all(message.as_bytes()) - .await?; - let stat = command.status().await?; + let stat = command.wait().await?; if !stat.success() { return Err(RustusError::HookError("Returned wrong status code".into())); } Ok(()) } } + +#[cfg(test)] +mod tests { + use super::FileNotifier; + use crate::notifiers::{Hook, Notifier}; + use actix_web::http::header::HeaderMap; + use std::fs::File; + use std::io::{Read, Write}; + #[cfg(unix)] + use std::os::unix::fs::PermissionsExt; + + #[cfg(unix)] + #[actix_rt::test] + async fn success() { + let dir = tempdir::TempDir::new("file_notifier").unwrap().into_path(); + let hook_path = dir.join("executable.sh"); + { + let mut file = File::create(hook_path.clone()).unwrap(); + let mut permissions = file.metadata().unwrap().permissions(); + permissions.set_mode(0o755); + file.set_permissions(permissions).unwrap(); + let script = r#"#!/bin/sh + HOOK_NAME="$1"; + MESSAGE="$2"; + echo "$HOOK_NAME $MESSAGE" > "$(dirname $0)/output""#; + file.write_all(script.as_bytes()).unwrap(); + file.sync_all().unwrap(); + } + let notifier = FileNotifier::new(hook_path.display().to_string()); + let hook = Hook::PostCreate; + let test_message = uuid::Uuid::new_v4().to_string(); + notifier + .send_message(test_message.clone(), hook.clone(), &HeaderMap::new()) + .await + .unwrap(); + let output_path = dir.join("output"); + assert!(output_path.exists()); + let mut buffer = String::new(); + let mut out_file = File::open(output_path).unwrap(); + out_file.read_to_string(&mut buffer).unwrap(); + assert_eq!(buffer, format!("{} {}\n", hook.to_string(), test_message)); + } + + #[cfg(unix)] + #[actix_rt::test] + async fn error_status() { + let dir = tempdir::TempDir::new("file_notifier").unwrap().into_path(); + let hook_path = dir.join("error_executable.sh"); + { + let mut file = File::create(hook_path.clone()).unwrap(); + let mut permissions = file.metadata().unwrap().permissions(); + permissions.set_mode(0o755); + file.set_permissions(permissions).unwrap(); + let script = r#"#!/bin/sh + read -t 0.1 MESSAGE + exit 1"#; + file.write_all(script.as_bytes()).unwrap(); + file.sync_all().unwrap(); + } + let notifier = FileNotifier::new(hook_path.display().to_string()); + let res = notifier + .send_message("test".into(), Hook::PostCreate, &HeaderMap::new()) + .await; + assert!(res.is_err()); + } + + #[actix_rt::test] + async fn no_such_file() { + let notifier = FileNotifier::new(format!("/{}.sh", uuid::Uuid::new_v4())); + let res = notifier + .send_message("test".into(), Hook::PreCreate, &HeaderMap::new()) + .await; + assert!(res.is_err()); + } +} diff --git a/src/notifiers/http_notifier.rs b/src/notifiers/http_notifier.rs index d8ea9ac300881b0924622c02100434331e254612..2e710047d16bf3a3099c6ab20008dd5ed36c75dd 100644 --- a/src/notifiers/http_notifier.rs +++ b/src/notifiers/http_notifier.rs @@ -28,6 +28,7 @@ impl HttpNotifier { #[async_trait] impl Notifier for HttpNotifier { + #[cfg_attr(coverage, no_coverage)] async fn prepare(&mut self) -> RustusResult<()> { Ok(()) } @@ -62,3 +63,94 @@ impl Notifier for HttpNotifier { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::HttpNotifier; + use crate::notifiers::{Hook, Notifier}; + use actix_web::http::header::{HeaderMap, HeaderName, HeaderValue}; + use httptest::matchers::contains; + use httptest::responders::status_code; + use std::str::FromStr; + use std::time::Duration; + + #[actix_rt::test] + async fn success_request() { + let server = httptest::Server::run(); + server.expect( + httptest::Expectation::matching(httptest::matchers::request::method_path( + "POST", "/hook", + )) + .respond_with(httptest::responders::status_code(200)), + ); + let hook_url = server.url_str("/hook"); + + let notifier = HttpNotifier::new(vec![hook_url], vec![]); + notifier + .send_message("test_message".into(), Hook::PostCreate, &HeaderMap::new()) + .await + .unwrap(); + } + + #[actix_rt::test] + async fn timeout_request() { + let server = httptest::Server::run(); + server.expect( + httptest::Expectation::matching(httptest::matchers::request::method_path( + "POST", "/hook", + )) + .respond_with(httptest::responders::delay_and_then( + Duration::from_secs(3), + status_code(200), + )), + ); + let hook_url = server.url_str("/hook"); + + let notifier = HttpNotifier::new(vec![hook_url], vec![]); + let result = notifier + .send_message("test_message".into(), Hook::PostCreate, &HeaderMap::new()) + .await; + assert!(result.is_err()); + } + + #[actix_rt::test] + async fn unknown_url() { + let server = httptest::Server::run(); + server.expect( + httptest::Expectation::matching(httptest::matchers::request::method_path( + "POST", "/hook", + )) + .respond_with(httptest::responders::status_code(404)), + ); + let hook_url = server.url_str("/hook"); + + let notifier = HttpNotifier::new(vec![hook_url], vec![]); + let result = notifier + .send_message("test_message".into(), Hook::PostCreate, &HeaderMap::new()) + .await; + assert!(result.is_err()); + } + + #[actix_rt::test] + async fn forwarded_header() { + let server = httptest::Server::run(); + server.expect( + httptest::Expectation::matching(httptest::matchers::all_of![ + httptest::matchers::request::method_path("POST", "/hook",), + httptest::matchers::request::headers(contains(("x-test-header", "meme-value"))) + ]) + .respond_with(httptest::responders::status_code(200)), + ); + let hook_url = server.url_str("/hook"); + let notifier = HttpNotifier::new(vec![hook_url], vec!["X-TEST-HEADER".into()]); + let mut header_map = HeaderMap::new(); + header_map.insert( + HeaderName::from_str("X-TEST-HEADER").unwrap(), + HeaderValue::from_str("meme-value").unwrap(), + ); + notifier + .send_message("test_message".into(), Hook::PostCreate, &header_map) + .await + .unwrap(); + } +} diff --git a/src/notifiers/mod.rs b/src/notifiers/mod.rs index 86ac9c692379c510f4fb15ee8cc7012f21f84131..03595103b447fc29583de41caaf80e1ac392c031 100644 --- a/src/notifiers/mod.rs +++ b/src/notifiers/mod.rs @@ -1,8 +1,6 @@ #[cfg(feature = "amqp_notifier")] pub mod amqp_notifier; -#[cfg(feature = "file_notifiers")] pub mod dir_notifier; -#[cfg(feature = "file_notifiers")] mod file_notifier; #[cfg(feature = "http_notifier")] pub mod http_notifier; diff --git a/src/notifiers/models/notification_manager.rs b/src/notifiers/models/notification_manager.rs index 84509da22954ee0e5ae5241896001713bb86b39e..68c2ae9dfa8e7278ac7736bd736472ecd8a217fb 100644 --- a/src/notifiers/models/notification_manager.rs +++ b/src/notifiers/models/notification_manager.rs @@ -1,9 +1,7 @@ use crate::errors::RustusResult; #[cfg(feature = "amqp_notifier")] use crate::notifiers::amqp_notifier; -#[cfg(feature = "file_notifiers")] use crate::notifiers::dir_notifier::DirNotifier; -#[cfg(feature = "file_notifiers")] use crate::notifiers::file_notifier::FileNotifier; #[cfg(feature = "http_notifier")] use crate::notifiers::http_notifier; @@ -17,45 +15,52 @@ pub struct NotificationManager { } impl NotificationManager { - pub async fn new(tus_config: &RustusConf) -> RustusResult<Self> { + pub async fn new(rustus_config: &RustusConf) -> RustusResult<Self> { let mut manager = Self { notifiers: Vec::new(), }; debug!("Initializing notification manager."); - #[cfg(feature = "file_notifiers")] - if tus_config.notification_opts.hooks_file.is_some() { + if rustus_config.notification_opts.hooks_file.is_some() { debug!("Found hooks file"); manager.notifiers.push(Box::new(FileNotifier::new( - tus_config.notification_opts.hooks_file.clone().unwrap(), + rustus_config.notification_opts.hooks_file.clone().unwrap(), ))); } - #[cfg(feature = "file_notifiers")] - if tus_config.notification_opts.hooks_dir.is_some() { + if rustus_config.notification_opts.hooks_dir.is_some() { debug!("Found hooks directory"); manager.notifiers.push(Box::new(DirNotifier::new( - tus_config.notification_opts.hooks_dir.clone().unwrap(), + rustus_config.notification_opts.hooks_dir.clone().unwrap(), ))); } #[cfg(feature = "http_notifier")] - if !tus_config.notification_opts.hooks_http_urls.is_empty() { + if !rustus_config.notification_opts.hooks_http_urls.is_empty() { debug!("Found http hook urls."); manager .notifiers .push(Box::new(http_notifier::HttpNotifier::new( - tus_config.notification_opts.hooks_http_urls.clone(), - tus_config + rustus_config.notification_opts.hooks_http_urls.clone(), + rustus_config .notification_opts .hooks_http_proxy_headers .clone(), ))); } #[cfg(feature = "amqp_notifier")] - if tus_config.notification_opts.hooks_amqp_url.is_some() { + if rustus_config.notification_opts.hooks_amqp_url.is_some() { debug!("Found AMQP notifier."); manager .notifiers .push(Box::new(amqp_notifier::AMQPNotifier::new( - tus_config.clone(), + rustus_config + .notification_opts + .hooks_amqp_url + .as_ref() + .unwrap(), + rustus_config.notification_opts.hooks_amqp_exchange.as_str(), + rustus_config + .notification_opts + .hooks_amqp_queues_prefix + .as_str(), ))); } for notifier in &mut manager.notifiers.iter_mut() { diff --git a/src/protocol/core/get_info.rs b/src/protocol/core/get_info.rs new file mode 100644 index 0000000000000000000000000000000000000000..bc8cc8ad64572a5e2358a3467f9f15ca8c02095f --- /dev/null +++ b/src/protocol/core/get_info.rs @@ -0,0 +1,263 @@ +use actix_web::{web, HttpRequest, HttpResponse}; + +use crate::errors::RustusError; + +use crate::{RustusResult, State}; + +pub async fn get_file_info( + state: web::Data<State>, + request: HttpRequest, +) -> RustusResult<HttpResponse> { + // Getting file id from URL. + if request.match_info().get("file_id").is_none() { + return Err(RustusError::FileNotFound); + } + let file_id = request.match_info().get("file_id").unwrap(); + + // Getting file info from info_storage. + let file_info = state.info_storage.get_info(file_id).await?; + if file_info.storage != state.data_storage.to_string() { + return Err(RustusError::FileNotFound); + } + let mut builder = HttpResponse::Ok(); + if file_info.is_partial { + builder.insert_header(("Upload-Concat", "partial")); + } + if file_info.is_final && file_info.parts.is_some() { + #[allow(clippy::or_fun_call)] + let parts = file_info + .parts + .clone() + .unwrap() + .iter() + .map(|file| { + format!( + "{}/{}", + state + .config + .base_url() + .strip_suffix('/') + .unwrap_or(state.config.base_url().as_str()), + file.as_str() + ) + }) + .collect::<Vec<String>>() + .join(" "); + builder.insert_header(("Upload-Concat", format!("final; {}", parts))); + } + builder + .no_chunking(file_info.offset as u64) + .insert_header(("Upload-Offset", file_info.offset.to_string())) + .insert_header(("Content-Length", file_info.offset.to_string())); + // Upload length is known. + if let Some(upload_len) = file_info.length { + builder.insert_header(("Upload-Length", upload_len.to_string())); + } else { + builder.insert_header(("Upload-Defer-Length", "1")); + } + if let Some(meta) = file_info.get_metadata_string() { + builder.insert_header(("Upload-Metadata", meta)); + } + Ok(builder.finish()) +} + +#[cfg(test)] +mod tests { + use actix_web::http::{Method, StatusCode}; + + use crate::{rustus_service, State}; + use actix_web::test::{call_service, init_service, TestRequest}; + use actix_web::{web, App}; + + #[actix_rt::test] + async fn success() { + let state = State::test_new().await; + let mut rustus = init_service( + App::new().configure(rustus_service(web::Data::new(state.test_clone().await))), + ) + .await; + let mut file_info = state.create_test_file().await; + file_info.offset = 100; + file_info.length = Some(100); + state + .info_storage + .set_info(&file_info, false) + .await + .unwrap(); + let request = TestRequest::with_uri(state.config.file_url(file_info.id.as_str()).as_str()) + .method(Method::HEAD) + .to_request(); + let response = call_service(&mut rustus, request).await; + let offset = response + .headers() + .get("Upload-Offset") + .unwrap() + .to_str() + .unwrap() + .parse::<usize>() + .unwrap(); + assert_eq!(file_info.offset, offset) + } + + #[actix_rt::test] + async fn success_metadata() { + let state = State::test_new().await; + let mut rustus = init_service( + App::new().configure(rustus_service(web::Data::new(state.test_clone().await))), + ) + .await; + let mut file_info = state.create_test_file().await; + file_info.offset = 100; + file_info.length = Some(100); + file_info.metadata.insert("test".into(), "value".into()); + state + .info_storage + .set_info(&file_info, false) + .await + .unwrap(); + let request = TestRequest::with_uri(state.config.file_url(file_info.id.as_str()).as_str()) + .method(Method::HEAD) + .to_request(); + let response = call_service(&mut rustus, request).await; + let metadata = response + .headers() + .get("Upload-Metadata") + .unwrap() + .to_str() + .unwrap(); + assert_eq!( + String::from(metadata), + format!("{} {}", "test", base64::encode("value")) + ) + } + + #[actix_rt::test] + async fn success_defer_len() { + let state = State::test_new().await; + let mut rustus = init_service( + App::new().configure(rustus_service(web::Data::new(state.test_clone().await))), + ) + .await; + let mut file_info = state.create_test_file().await; + file_info.deferred_size = true; + file_info.length = None; + state + .info_storage + .set_info(&file_info, false) + .await + .unwrap(); + let request = TestRequest::with_uri(state.config.file_url(file_info.id.as_str()).as_str()) + .method(Method::HEAD) + .to_request(); + let response = call_service(&mut rustus, request).await; + assert_eq!( + response + .headers() + .get("Upload-Defer-Length") + .unwrap() + .to_str() + .unwrap(), + "1" + ); + } + + #[actix_rt::test] + async fn test_get_file_info_partial() { + let state = State::test_new().await; + let mut rustus = init_service( + App::new().configure(rustus_service(web::Data::new(state.test_clone().await))), + ) + .await; + let mut file_info = state.create_test_file().await; + file_info.is_partial = true; + state + .info_storage + .set_info(&file_info, false) + .await + .unwrap(); + let request = TestRequest::with_uri(state.config.file_url(file_info.id.as_str()).as_str()) + .method(Method::HEAD) + .to_request(); + let response = call_service(&mut rustus, request).await; + assert_eq!( + response + .headers() + .get("Upload-Concat") + .unwrap() + .to_str() + .unwrap(), + "partial" + ); + } + + #[actix_rt::test] + async fn success_final() { + let state = State::test_new().await; + let mut rustus = init_service( + App::new().configure(rustus_service(web::Data::new(state.test_clone().await))), + ) + .await; + let mut file_info = state.create_test_file().await; + file_info.is_partial = false; + file_info.is_final = true; + file_info.parts = Some(vec!["test1".into(), "test2".into()]); + state + .info_storage + .set_info(&file_info, false) + .await + .unwrap(); + let request = TestRequest::with_uri(state.config.file_url(file_info.id.as_str()).as_str()) + .method(Method::HEAD) + .to_request(); + let response = call_service(&mut rustus, request).await; + assert_eq!( + response + .headers() + .get("Upload-Concat") + .unwrap() + .to_str() + .unwrap(), + format!( + "final; {} {}", + state.config.file_url("test1"), + state.config.file_url("test2") + ) + .as_str() + ); + } + + #[actix_rt::test] + async fn no_file() { + let state = State::test_new().await; + let mut rustus = init_service( + App::new().configure(rustus_service(web::Data::new(state.test_clone().await))), + ) + .await; + let request = TestRequest::with_uri(state.config.file_url("unknknown").as_str()) + .method(Method::HEAD) + .to_request(); + let response = call_service(&mut rustus, request).await; + assert_eq!(response.status(), StatusCode::NOT_FOUND); + } + + #[actix_rt::test] + async fn test_get_file_info_wrong_storage() { + let state = State::test_new().await; + let mut rustus = init_service( + App::new().configure(rustus_service(web::Data::new(state.test_clone().await))), + ) + .await; + let mut file_info = state.create_test_file().await; + file_info.storage = String::from("unknown"); + state + .info_storage + .set_info(&file_info, false) + .await + .unwrap(); + let request = TestRequest::with_uri(state.config.file_url(file_info.id.as_str()).as_str()) + .method(Method::HEAD) + .to_request(); + let response = call_service(&mut rustus, request).await; + assert_eq!(response.status(), StatusCode::NOT_FOUND); + } +} diff --git a/src/protocol/core/mod.rs b/src/protocol/core/mod.rs index df00d5cab53b70e640e582c47d2cc7abcbbded15..dc29f98b35c65ee0b12ebee5a6e0c2b40c1c9622 100644 --- a/src/protocol/core/mod.rs +++ b/src/protocol/core/mod.rs @@ -1,9 +1,8 @@ -use actix_web::web::PayloadConfig; use actix_web::{guard, middleware, web}; -use crate::RustusConf; - -mod routes; +mod get_info; +mod server_info; +mod write_bytes; /// Add core TUS protocol endpoints. /// @@ -13,34 +12,33 @@ mod routes; /// OPTIONS /api - to get info about the app. /// HEAD /api/file - to get info about the file. /// PATCH /api/file - to add bytes to file. -pub fn add_extension(web_app: &mut web::ServiceConfig, app_conf: &RustusConf) { +#[cfg_attr(coverage, no_coverage)] +pub fn add_extension(web_app: &mut web::ServiceConfig) { web_app .service( // PATCH /base/{file_id} // Main URL for uploading files. - web::resource(app_conf.base_url().as_str()) + web::resource("") .name("core:server_info") .guard(guard::Options()) - .to(routes::server_info), + .to(server_info::server_info), ) .service( // PATCH /base/{file_id} // Main URL for uploading files. - web::resource(app_conf.file_url().as_str()) - // 10 MB chunks - .app_data(PayloadConfig::new(app_conf.max_body_size)) + web::resource("{file_id}") .name("core:write_bytes") .guard(guard::Patch()) - .to(routes::write_bytes), + .to(write_bytes::write_bytes), ) .service( // HEAD /base/{file_id} // Main URL for getting info about files. - web::resource(app_conf.file_url().as_str()) + web::resource("{file_id}") .name("core:file_info") .guard(guard::Head()) // Header to prevent the client and/or proxies from caching the response. .wrap(middleware::DefaultHeaders::new().add(("Cache-Control", "no-store"))) - .to(routes::get_file_info), + .to(get_info::get_file_info), ); } diff --git a/src/protocol/core/routes.rs b/src/protocol/core/routes.rs deleted file mode 100644 index d910f67c2be7bb6a7b18fa562cd9a266ab1f3fb6..0000000000000000000000000000000000000000 --- a/src/protocol/core/routes.rs +++ /dev/null @@ -1,187 +0,0 @@ -use actix_web::{web, web::Bytes, HttpRequest, HttpResponse}; - -use crate::errors::RustusError; -use crate::notifiers::Hook; -use crate::protocol::extensions::Extensions; -use crate::utils::headers::{check_header, parse_header}; -use crate::{RustusConf, State}; - -#[allow(clippy::needless_pass_by_value)] -pub fn server_info(app_conf: web::Data<RustusConf>) -> HttpResponse { - let ext_str = app_conf - .extensions_vec() - .into_iter() - .map(|x| x.to_string()) - .collect::<Vec<String>>() - .join(","); - HttpResponse::Ok() - .insert_header(("Tus-Extension", ext_str.as_str())) - .finish() -} - -pub async fn get_file_info( - state: web::Data<State>, - request: HttpRequest, -) -> actix_web::Result<HttpResponse> { - // Getting file id from URL. - if request.match_info().get("file_id").is_none() { - return Ok(HttpResponse::NotFound().body("No file id provided.")); - } - let file_id = request.match_info().get("file_id").unwrap(); - - // Getting file info from info_storage. - let file_info = state.info_storage.get_info(file_id).await?; - if file_info.storage != state.data_storage.to_string() { - return Ok(HttpResponse::NotFound().body("File not found.")); - } - let mut builder = HttpResponse::Ok(); - if file_info.is_partial { - builder.insert_header(("Upload-Concat", "partial")); - } - if file_info.is_final && file_info.parts.is_some() { - #[allow(clippy::or_fun_call)] - let parts = file_info - .parts - .clone() - .unwrap() - .iter() - .map(|file| { - format!( - "{}/{}", - state - .config - .base_url() - .strip_suffix('/') - .unwrap_or(state.config.base_url().as_str()), - file.as_str() - ) - }) - .collect::<Vec<String>>() - .join(" "); - builder.insert_header(("Upload-Concat", format!("final; {}", parts))); - } - builder - .no_chunking(file_info.offset as u64) - .insert_header(("Upload-Offset", file_info.offset.to_string())) - .insert_header(("Content-Length", file_info.offset.to_string())); - // Upload length is known. - if let Some(upload_len) = file_info.length { - builder.insert_header(("Upload-Length", upload_len.to_string())); - } else { - builder.insert_header(("Upload-Defer-Length", "1")); - } - if let Some(meta) = file_info.get_metadata_string() { - builder.insert_header(("Upload-Metadata", meta)); - } - Ok(builder.finish()) -} - -pub async fn write_bytes( - request: HttpRequest, - bytes: Bytes, - state: web::Data<State>, -) -> actix_web::Result<HttpResponse> { - // Checking if request has required headers. - let check_content_type = |val: &str| val == "application/offset+octet-stream"; - if !check_header(&request, "Content-Type", check_content_type) { - return Ok(HttpResponse::UnsupportedMediaType().body("Unknown content-type.")); - } - // Getting current offset. - let offset: Option<usize> = parse_header(&request, "Upload-Offset"); - - if offset.is_none() { - return Ok(HttpResponse::UnsupportedMediaType().body("No offset provided.")); - } - - if request.match_info().get("file_id").is_none() { - return Ok(HttpResponse::NotFound().body("No file id provided.")); - } - - // New upload length. - // Parses header `Upload-Length` only if the creation-defer-length extension is enabled. - let updated_len = if state - .config - .extensions_vec() - .contains(&Extensions::CreationDeferLength) - { - parse_header(&request, "Upload-Length") - } else { - None - }; - - let file_id = request.match_info().get("file_id").unwrap(); - // Getting file info. - let mut file_info = state.info_storage.get_info(file_id).await?; - - // According to TUS protocol you can't update final uploads. - if file_info.is_final { - return Ok(HttpResponse::Forbidden().finish()); - } - - // Checking if file was stored in the same storage. - if file_info.storage != state.data_storage.to_string() { - return Ok(HttpResponse::NotFound().finish()); - } - // Checking if offset from request is the same as the real offset. - if offset.unwrap() != file_info.offset { - return Ok(HttpResponse::Conflict().finish()); - } - - // If someone want to update file length. - // This required by Upload-Defer-Length extension. - if let Some(new_len) = updated_len { - // Whoop, someone gave us total file length - // less that he had already uploaded. - if new_len < file_info.offset { - return Err(RustusError::WrongOffset.into()); - } - // We already know the exact size of a file. - // Someone want to update it. - // Anyway, it's not allowed, heh. - if file_info.length.is_some() { - return Err(RustusError::SizeAlreadyKnown.into()); - } - - // All checks are ok. Now our file will have exact size. - file_info.deferred_size = false; - file_info.length = Some(new_len); - } - - // Checking if the size of the upload is already equals - // to calculated offset. It means that all bytes were already written. - if Some(file_info.offset) == file_info.length { - return Err(RustusError::FrozenFile.into()); - } - - // Appending bytes to file. - state - .data_storage - .add_bytes(&file_info, bytes.as_ref()) - .await?; - // Updating offset. - file_info.offset += bytes.len(); - // Saving info to info storage. - state.info_storage.set_info(&file_info, false).await?; - - let mut hook = Hook::PostReceive; - if file_info.length == Some(file_info.offset) { - hook = Hook::PostFinish; - } - if state.config.hook_is_active(hook) { - let message = state - .config - .notification_opts - .hooks_format - .format(&request, &file_info)?; - let headers = request.headers().clone(); - tokio::spawn(async move { - state - .notification_manager - .send_message(message, hook, &headers) - .await - }); - } - Ok(HttpResponse::NoContent() - .insert_header(("Upload-Offset", file_info.offset.to_string())) - .finish()) -} diff --git a/src/protocol/core/server_info.rs b/src/protocol/core/server_info.rs new file mode 100644 index 0000000000000000000000000000000000000000..f5217cd49d43f88ff6ea25fde34e15a3a12c468b --- /dev/null +++ b/src/protocol/core/server_info.rs @@ -0,0 +1,56 @@ +use actix_web::{web, HttpResponse}; + +use crate::State; + +#[allow(clippy::needless_pass_by_value)] +#[allow(clippy::unused_async)] +pub async fn server_info(state: web::Data<State>) -> HttpResponse { + let ext_str = state + .config + .extensions_vec() + .into_iter() + .map(|x| x.to_string()) + .collect::<Vec<String>>() + .join(","); + HttpResponse::Ok() + .insert_header(("Tus-Extension", ext_str.as_str())) + .finish() +} + +#[cfg(test)] +mod tests { + use crate::protocol::extensions::Extensions; + use crate::{rustus_service, State}; + use actix_web::test::{call_service, init_service, TestRequest}; + + use actix_web::http::Method; + use actix_web::{web, App}; + + #[actix_rt::test] + async fn test_server_info() { + let mut state = State::test_new().await; + let mut rustus = init_service( + App::new().configure(rustus_service(web::Data::new(state.test_clone().await))), + ) + .await; + state.config.tus_extensions = vec![ + Extensions::Creation, + Extensions::Concatenation, + Extensions::Termination, + ]; + let request = TestRequest::with_uri(state.config.base_url().as_str()) + .method(Method::OPTIONS) + .to_request(); + let response = call_service(&mut rustus, request).await; + let extensions = response + .headers() + .get("Tus-Extension") + .unwrap() + .to_str() + .unwrap() + .clone(); + assert!(extensions.contains(Extensions::Creation.to_string().as_str())); + assert!(extensions.contains(Extensions::Concatenation.to_string().as_str())); + assert!(extensions.contains(Extensions::Termination.to_string().as_str())); + } +} diff --git a/src/protocol/core/write_bytes.rs b/src/protocol/core/write_bytes.rs new file mode 100644 index 0000000000000000000000000000000000000000..322b2191a6864e7e0ccf27fc2b6628c5fad2540a --- /dev/null +++ b/src/protocol/core/write_bytes.rs @@ -0,0 +1,411 @@ +use actix_web::{web, web::Bytes, HttpRequest, HttpResponse}; + +use crate::errors::RustusError; +use crate::notifiers::Hook; +use crate::protocol::extensions::Extensions; +use crate::utils::headers::{check_header, parse_header}; +use crate::{RustusResult, State}; + +pub async fn write_bytes( + request: HttpRequest, + bytes: Bytes, + state: web::Data<State>, +) -> RustusResult<HttpResponse> { + // Checking if request has required headers. + let check_content_type = |val: &str| val == "application/offset+octet-stream"; + if !check_header(&request, "Content-Type", check_content_type) { + return Ok(HttpResponse::UnsupportedMediaType().body("Unknown content-type.")); + } + // Getting current offset. + let offset: Option<usize> = parse_header(&request, "Upload-Offset"); + + if offset.is_none() { + return Ok(HttpResponse::UnsupportedMediaType().body("No offset provided.")); + } + + if request.match_info().get("file_id").is_none() { + return Err(RustusError::FileNotFound); + } + + // New upload length. + // Parses header `Upload-Length` only if the creation-defer-length extension is enabled. + let updated_len = if state + .config + .extensions_vec() + .contains(&Extensions::CreationDeferLength) + { + parse_header(&request, "Upload-Length") + } else { + None + }; + + let file_id = request.match_info().get("file_id").unwrap(); + // Getting file info. + let mut file_info = state.info_storage.get_info(file_id).await?; + + // According to TUS protocol you can't update final uploads. + if file_info.is_final { + return Ok(HttpResponse::Forbidden().finish()); + } + + // Checking if file was stored in the same storage. + if file_info.storage != state.data_storage.to_string() { + return Err(RustusError::FileNotFound); + } + // Checking if offset from request is the same as the real offset. + if offset.unwrap() != file_info.offset { + return Ok(HttpResponse::Conflict().finish()); + } + + // If someone want to update file length. + // This required by Upload-Defer-Length extension. + if let Some(new_len) = updated_len { + // Whoop, someone gave us total file length + // less that he had already uploaded. + if new_len < file_info.offset { + return Err(RustusError::WrongOffset); + } + // We already know the exact size of a file. + // Someone want to update it. + // Anyway, it's not allowed, heh. + if file_info.length.is_some() { + return Err(RustusError::SizeAlreadyKnown); + } + + // All checks are ok. Now our file will have exact size. + file_info.deferred_size = false; + file_info.length = Some(new_len); + } + + // Checking if the size of the upload is already equals + // to calculated offset. It means that all bytes were already written. + if Some(file_info.offset) == file_info.length { + return Err(RustusError::FrozenFile); + } + + // Appending bytes to file. + state + .data_storage + .add_bytes(&file_info, bytes.as_ref()) + .await?; + // Updating offset. + file_info.offset += bytes.len(); + // Saving info to info storage. + state.info_storage.set_info(&file_info, false).await?; + + let mut hook = Hook::PostReceive; + if file_info.length == Some(file_info.offset) { + hook = Hook::PostFinish; + } + if state.config.hook_is_active(hook) { + let message = state + .config + .notification_opts + .hooks_format + .format(&request, &file_info)?; + let headers = request.headers().clone(); + tokio::spawn(async move { + state + .notification_manager + .send_message(message, hook, &headers) + .await + }); + } + Ok(HttpResponse::NoContent() + .insert_header(("Upload-Offset", file_info.offset.to_string())) + .finish()) +} + +#[cfg(test)] +mod tests { + use crate::{rustus_service, State}; + use actix_web::http::StatusCode; + use actix_web::test::{call_service, init_service, TestRequest}; + use actix_web::{web, App}; + + #[actix_rt::test] + /// Success test for writing bytes. + /// + /// This test creates file and writes bytes to it. + async fn success() { + let state = State::test_new().await; + let mut rustus = init_service( + App::new().configure(rustus_service(web::Data::new(state.test_clone().await))), + ) + .await; + let mut file = state.create_test_file().await; + file.length = Some(100); + file.offset = 0; + state.info_storage.set_info(&file, false).await.unwrap(); + let test_data = "memes"; + let request = TestRequest::patch() + .uri(state.config.file_url(file.id.as_str()).as_str()) + .insert_header(("Content-Type", "application/offset+octet-stream")) + .insert_header(("Upload-Offset", file.offset)) + .set_payload(test_data) + .to_request(); + let resp = call_service(&mut rustus, request).await; + assert_eq!(resp.status(), StatusCode::NO_CONTENT); + assert_eq!( + resp.headers() + .get("Upload-Offset") + .unwrap() + .to_str() + .unwrap(), + test_data.len().to_string().as_str() + ); + let new_info = state + .info_storage + .get_info(file.id.clone().as_str()) + .await + .unwrap(); + assert_eq!(new_info.offset, test_data.len()); + } + + #[actix_rt::test] + /// Testing defer-length extension. + /// + /// During this test we'll try to update + /// file's length while writing bytes to it. + async fn success_update_file_length() { + let state = State::test_new().await; + let mut rustus = init_service( + App::new().configure(rustus_service(web::Data::new(state.test_clone().await))), + ) + .await; + let mut file = state.create_test_file().await; + file.length = None; + file.deferred_size = true; + file.offset = 0; + state.info_storage.set_info(&file, false).await.unwrap(); + let test_data = "memes"; + let request = TestRequest::patch() + .uri(state.config.file_url(file.id.as_str()).as_str()) + .param("file_id", file.id.clone()) + .insert_header(("Content-Type", "application/offset+octet-stream")) + .insert_header(("Upload-Offset", file.offset)) + .insert_header(("Upload-Length", "20")) + .set_payload(test_data) + .to_request(); + let resp = call_service(&mut rustus, request).await; + assert_eq!(resp.status(), StatusCode::NO_CONTENT); + assert_eq!( + resp.headers() + .get("Upload-Offset") + .unwrap() + .to_str() + .unwrap(), + test_data.len().to_string().as_str() + ); + let new_info = state + .info_storage + .get_info(file.id.clone().as_str()) + .await + .unwrap(); + assert_eq!(new_info.offset, test_data.len()); + assert_eq!(new_info.deferred_size, false); + assert_eq!(new_info.length, Some(20)); + } + + #[actix_rt::test] + /// Tests that if new file length + /// is less than current offset, error is thrown. + async fn new_file_length_lt_offset() { + let state = State::test_new().await; + let mut rustus = init_service( + App::new().configure(rustus_service(web::Data::new(state.test_clone().await))), + ) + .await; + let mut file = state.create_test_file().await; + file.length = None; + file.deferred_size = true; + file.offset = 30; + state.info_storage.set_info(&file, false).await.unwrap(); + let test_data = "memes"; + let request = TestRequest::patch() + .uri(state.config.file_url(file.id.as_str()).as_str()) + .insert_header(("Content-Type", "application/offset+octet-stream")) + .insert_header(("Upload-Offset", file.offset)) + .insert_header(("Upload-Length", "20")) + .set_payload(test_data) + .to_request(); + let resp = call_service(&mut rustus, request).await; + assert_eq!(resp.status(), StatusCode::CONFLICT); + } + + #[actix_rt::test] + /// Tests if user tries to update + /// file length with known length, + /// error is thrown. + async fn new_file_length_size_already_known() { + let state = State::test_new().await; + let mut rustus = init_service( + App::new().configure(rustus_service(web::Data::new(state.test_clone().await))), + ) + .await; + let mut file = state.create_test_file().await; + file.length = Some(100); + file.deferred_size = false; + file.offset = 0; + state.info_storage.set_info(&file, false).await.unwrap(); + let test_data = "memes"; + let request = TestRequest::patch() + .uri(state.config.file_url(file.id.as_str()).as_str()) + .insert_header(("Content-Type", "application/offset+octet-stream")) + .insert_header(("Upload-Offset", file.offset)) + .insert_header(("Upload-Length", "120")) + .set_payload(test_data) + .to_request(); + let resp = call_service(&mut rustus, request).await; + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + } + + #[actix_rt::test] + /// Checks that if Content-Type header missing, + /// wrong status code is returned. + async fn no_content_header() { + let state = State::test_new().await; + let mut rustus = init_service( + App::new().configure(rustus_service(web::Data::new(state.test_clone().await))), + ) + .await; + let mut file = state.create_test_file().await; + file.length = Some(100); + file.offset = 0; + state.info_storage.set_info(&file, false).await.unwrap(); + let request = TestRequest::patch() + .uri(state.config.file_url(file.id.as_str()).as_str()) + .insert_header(("Upload-Offset", "0")) + .set_payload("memes") + .to_request(); + let resp = call_service(&mut rustus, request).await; + assert_eq!(resp.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE); + } + + #[actix_rt::test] + /// Tests that method will return error if no offset header specified. + async fn no_offset_header() { + let state = State::test_new().await; + let mut rustus = init_service( + App::new().configure(rustus_service(web::Data::new(state.test_clone().await))), + ) + .await; + let mut file = state.create_test_file().await; + file.length = Some(100); + file.offset = 0; + state.info_storage.set_info(&file, false).await.unwrap(); + let request = TestRequest::patch() + .uri(state.config.file_url(file.id.as_str()).as_str()) + .insert_header(("Content-Type", "application/offset+octet-stream")) + .set_payload("memes") + .to_request(); + let resp = call_service(&mut rustus, request).await; + assert_eq!(resp.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE); + } + + #[actix_rt::test] + /// Tests that method will return error if wrong offset is passed. + async fn wrong_offset_header() { + let state = State::test_new().await; + let mut rustus = init_service( + App::new().configure(rustus_service(web::Data::new(state.test_clone().await))), + ) + .await; + let mut file = state.create_test_file().await; + file.length = Some(100); + file.offset = 0; + state.info_storage.set_info(&file, false).await.unwrap(); + let request = TestRequest::patch() + .uri(state.config.file_url(file.id.as_str()).as_str()) + .insert_header(("Upload-Offset", "1")) + .insert_header(("Content-Type", "application/offset+octet-stream")) + .set_payload("memes") + .to_request(); + let resp = call_service(&mut rustus, request).await; + assert_eq!(resp.status(), StatusCode::CONFLICT); + } + + #[actix_rt::test] + /// Tests that method would return error if file was already uploaded. + async fn final_upload() { + let state = State::test_new().await; + let mut rustus = init_service( + App::new().configure(rustus_service(web::Data::new(state.test_clone().await))), + ) + .await; + let mut file = state.create_test_file().await; + file.is_final = true; + state.info_storage.set_info(&file, false).await.unwrap(); + let request = TestRequest::patch() + .uri(state.config.file_url(file.id.as_str()).as_str()) + .insert_header(("Upload-Offset", file.offset)) + .insert_header(("Content-Type", "application/offset+octet-stream")) + .set_payload("memes") + .to_request(); + let resp = call_service(&mut rustus, request).await; + assert_eq!(resp.status(), StatusCode::FORBIDDEN); + } + + #[actix_rt::test] + /// Tests that method would return 404 if file was saved in other storage. + async fn wrong_storage() { + let state = State::test_new().await; + let mut rustus = init_service( + App::new().configure(rustus_service(web::Data::new(state.test_clone().await))), + ) + .await; + let mut file = state.create_test_file().await; + file.storage = "unknown".into(); + state.info_storage.set_info(&file, false).await.unwrap(); + let request = TestRequest::patch() + .uri(state.config.file_url(file.id.as_str()).as_str()) + .insert_header(("Upload-Offset", file.offset)) + .insert_header(("Content-Type", "application/offset+octet-stream")) + .set_payload("memes") + .to_request(); + let resp = call_service(&mut rustus, request).await; + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + } + + #[actix_rt::test] + /// Tests that method won't allow you to update + /// file if it's offset already equal to length. + async fn frozen_file() { + let state = State::test_new().await; + let mut rustus = init_service( + App::new().configure(rustus_service(web::Data::new(state.test_clone().await))), + ) + .await; + let mut file = state.create_test_file().await; + file.offset = 10; + file.length = Some(10); + state.info_storage.set_info(&file, false).await.unwrap(); + let request = TestRequest::patch() + .uri(state.config.file_url(file.id.as_str()).as_str()) + .insert_header(("Upload-Offset", file.offset)) + .insert_header(("Content-Type", "application/offset+octet-stream")) + .set_payload("memes") + .to_request(); + let resp = call_service(&mut rustus, request).await; + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + } + + #[actix_rt::test] + /// Tests that method will return 404 if + /// unknown file_id is passed. + async fn unknown_file_id() { + let state = State::test_new().await; + let mut rustus = init_service( + App::new().configure(rustus_service(web::Data::new(state.test_clone().await))), + ) + .await; + let request = TestRequest::patch() + .uri(state.config.file_url("unknown").as_str()) + .insert_header(("Upload-Offset", "0")) + .insert_header(("Content-Type", "application/offset+octet-stream")) + .set_payload("memes") + .to_request(); + let resp = call_service(&mut rustus, request).await; + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + } +} diff --git a/src/protocol/creation/mod.rs b/src/protocol/creation/mod.rs index 2ecaf127e9de83ce0806fd89374eb44ffd704614..eede35da9d24fcdbea6fba7bd08fb482669496a3 100644 --- a/src/protocol/creation/mod.rs +++ b/src/protocol/creation/mod.rs @@ -1,18 +1,16 @@ use actix_web::{guard, web}; - -use crate::RustusConf; - mod routes; /// Add creation extensions. /// /// This extension allows you /// to create file before sending data. -pub fn add_extension(web_app: &mut web::ServiceConfig, app_conf: &RustusConf) { +#[cfg_attr(coverage, no_coverage)] +pub fn add_extension(web_app: &mut web::ServiceConfig) { web_app.service( // Post /base // URL for creating files. - web::resource(app_conf.base_url().as_str()) + web::resource("") .name("creation:create_file") .guard(guard::Post()) .to(routes::create_file), diff --git a/src/protocol/creation/routes.rs b/src/protocol/creation/routes.rs index 21c710d8e45f515339ff33185218ffca42e5d8b4..20df718620cc9b1a40aa2ddba47c3896cd462dff 100644 --- a/src/protocol/creation/routes.rs +++ b/src/protocol/creation/routes.rs @@ -30,7 +30,7 @@ fn get_metadata(request: &HttpRequest) -> Option<HashMap<String, String>> { .map(|header_string| { let mut meta_map = HashMap::new(); for meta_pair in header_string.split(',') { - let mut split = meta_pair.split(' '); + let mut split = meta_pair.trim().split(' '); let key = split.next(); let b64val = split.next(); if key.is_none() || b64val.is_none() { @@ -55,7 +55,7 @@ fn get_upload_parts(request: &HttpRequest) -> Vec<String> { let urls = header_str.strip_prefix("final;").unwrap(); urls.split(' ') - .filter_map(|val: &str| val.split('/').last().map(String::from)) + .filter_map(|val: &str| val.trim().split('/').last().map(String::from)) .filter(|val| val.trim() != "") .collect() } @@ -171,9 +171,6 @@ pub async fn create_file( } } - // Create upload URL for this file. - let upload_url = request.url_for("core:write_bytes", &[file_info.id.clone()])?; - // Checking if creation-with-upload extension is enabled. let with_upload = state .config @@ -181,15 +178,14 @@ pub async fn create_file( .contains(&Extensions::CreationWithUpload); if with_upload && !bytes.is_empty() && !(concat_ext && is_final) { let octet_stream = |val: &str| val == "application/offset+octet-stream"; - if !check_header(&request, "Content-Type", octet_stream) { - return Ok(HttpResponse::BadRequest().finish()); + if check_header(&request, "Content-Type", octet_stream) { + // Writing first bytes. + state + .data_storage + .add_bytes(&file_info, bytes.as_ref()) + .await?; + file_info.offset += bytes.len(); } - // Writing first bytes. - state - .data_storage - .add_bytes(&file_info, bytes.as_ref()) - .await?; - file_info.offset += bytes.len(); } state.info_storage.set_info(&file_info, true).await?; @@ -211,8 +207,299 @@ pub async fn create_file( }); } + // Create upload URL for this file. + let upload_url = request.url_for("core:write_bytes", &[file_info.id.clone()])?; + Ok(HttpResponse::Created() .insert_header(("Location", upload_url.as_str())) .insert_header(("Upload-Offset", file_info.offset.to_string())) .finish()) } + +#[cfg(test)] +mod tests { + use crate::server::rustus_service; + use crate::State; + use actix_web::http::StatusCode; + use actix_web::test::{call_service, init_service, TestRequest}; + use actix_web::{web, App}; + + #[actix_rt::test] + async fn success() { + let state = State::test_new().await; + let mut rustus = init_service( + App::new().configure(rustus_service(web::Data::new(state.test_clone().await))), + ) + .await; + let request = TestRequest::post() + .uri(state.config.base_url().as_str()) + .insert_header(("Upload-Length", 100)) + .to_request(); + let resp = call_service(&mut rustus, request).await; + assert_eq!(resp.status(), StatusCode::CREATED); + // Getting file from location header. + let item_id = resp + .headers() + .get("Location") + .unwrap() + .to_str() + .unwrap() + .split('/') + .last() + .unwrap(); + let file_info = state.info_storage.get_info(item_id).await.unwrap(); + assert_eq!(file_info.length, Some(100)); + assert_eq!(file_info.offset, 0); + } + + #[actix_rt::test] + async fn success_with_bytes() { + let state = State::test_new().await; + let mut rustus = init_service( + App::new().configure(rustus_service(web::Data::new(state.test_clone().await))), + ) + .await; + let test_data = "memes"; + let request = TestRequest::post() + .uri(state.config.base_url().as_str()) + .insert_header(("Upload-Length", 100)) + .insert_header(("Content-Type", "application/offset+octet-stream")) + .set_payload(web::Bytes::from(test_data)) + .to_request(); + let resp = call_service(&mut rustus, request).await; + assert_eq!(resp.status(), StatusCode::CREATED); + // Getting file from location header. + let item_id = resp + .headers() + .get("Location") + .unwrap() + .to_str() + .unwrap() + .split('/') + .last() + .unwrap(); + let file_info = state.info_storage.get_info(item_id).await.unwrap(); + assert_eq!(file_info.length, Some(100)); + assert_eq!(file_info.offset, test_data.len()); + } + + #[actix_rt::test] + async fn with_bytes_wrong_content_type() { + let state = State::test_new().await; + let mut rustus = init_service( + App::new().configure(rustus_service(web::Data::new(state.test_clone().await))), + ) + .await; + let test_data = "memes"; + let request = TestRequest::post() + .uri(state.config.base_url().as_str()) + .insert_header(("Upload-Length", 100)) + .insert_header(("Content-Type", "random")) + .set_payload(web::Bytes::from(test_data)) + .to_request(); + let resp = call_service(&mut rustus, request).await; + assert_eq!(resp.status(), StatusCode::CREATED); + // Getting file from location header. + let item_id = resp + .headers() + .get("Location") + .unwrap() + .to_str() + .unwrap() + .split('/') + .last() + .unwrap(); + let file_info = state.info_storage.get_info(item_id).await.unwrap(); + assert_eq!(file_info.length, Some(100)); + assert_eq!(file_info.offset, 0); + } + + #[actix_rt::test] + async fn success_defer_size() { + let state = State::test_new().await; + let mut rustus = init_service( + App::new().configure(rustus_service(web::Data::new(state.test_clone().await))), + ) + .await; + let request = TestRequest::post() + .uri(state.config.base_url().as_str()) + .insert_header(("Upload-Defer-Length", "1")) + .to_request(); + let resp = call_service(&mut rustus, request).await; + assert_eq!(resp.status(), StatusCode::CREATED); + // Getting file from location header. + let item_id = resp + .headers() + .get("Location") + .unwrap() + .to_str() + .unwrap() + .split('/') + .last() + .unwrap(); + let file_info = state.info_storage.get_info(item_id).await.unwrap(); + assert_eq!(file_info.length, None); + assert!(file_info.deferred_size); + } + + #[actix_rt::test] + async fn success_partial_upload() { + let state = State::test_new().await; + let mut rustus = init_service( + App::new().configure(rustus_service(web::Data::new(state.test_clone().await))), + ) + .await; + let request = TestRequest::post() + .uri(state.config.base_url().as_str()) + .insert_header(("Upload-Length", 100)) + .insert_header(("Upload-Concat", "partial")) + .to_request(); + let resp = call_service(&mut rustus, request).await; + assert_eq!(resp.status(), StatusCode::CREATED); + // Getting file from location header. + let item_id = resp + .headers() + .get("Location") + .unwrap() + .to_str() + .unwrap() + .split('/') + .last() + .unwrap(); + let file_info = state.info_storage.get_info(item_id).await.unwrap(); + assert_eq!(file_info.length, Some(100)); + assert!(file_info.is_partial); + assert_eq!(file_info.is_final, false); + } + + #[actix_rt::test] + async fn success_final_upload() { + let state = State::test_new().await; + let mut rustus = init_service( + App::new().configure(rustus_service(web::Data::new(state.test_clone().await))), + ) + .await; + let mut part1 = state.create_test_file().await; + let mut part2 = state.create_test_file().await; + part1.is_partial = true; + part1.length = Some(100); + part1.offset = 100; + + part2.is_partial = true; + part2.length = Some(100); + part2.offset = 100; + + state.info_storage.set_info(&part1, false).await.unwrap(); + state.info_storage.set_info(&part2, false).await.unwrap(); + + let request = TestRequest::post() + .uri(state.config.base_url().as_str()) + .insert_header(("Upload-Length", 100)) + .insert_header(( + "Upload-Concat", + format!("final;/files/{} /files/{}", part1.id, part2.id), + )) + .to_request(); + let resp = call_service(&mut rustus, request).await; + assert_eq!(resp.status(), StatusCode::CREATED); + // Getting file from location header. + let item_id = resp + .headers() + .get("Location") + .unwrap() + .to_str() + .unwrap() + .split('/') + .last() + .unwrap(); + let file_info = state.info_storage.get_info(item_id).await.unwrap(); + assert_eq!(file_info.length, Some(200)); + assert!(file_info.is_final); + } + + #[actix_rt::test] + async fn success_with_metadata() { + let state = State::test_new().await; + let mut rustus = init_service( + App::new().configure(rustus_service(web::Data::new(state.test_clone().await))), + ) + .await; + let request = TestRequest::post() + .uri(state.config.base_url().as_str()) + .insert_header(("Upload-Length", 100)) + .insert_header(( + "Upload-Metadata", + format!( + "test {}, pest {}", + base64::encode("data1"), + base64::encode("data2") + ), + )) + .to_request(); + let resp = call_service(&mut rustus, request).await; + assert_eq!(resp.status(), StatusCode::CREATED); + // Getting file from location header. + let item_id = resp + .headers() + .get("Location") + .unwrap() + .to_str() + .unwrap() + .split('/') + .last() + .unwrap(); + let file_info = state.info_storage.get_info(item_id).await.unwrap(); + assert_eq!(file_info.length, Some(100)); + assert_eq!(file_info.metadata.get("test").unwrap(), "data1"); + assert_eq!(file_info.metadata.get("pest").unwrap(), "data2"); + assert_eq!(file_info.offset, 0); + } + + #[actix_rt::test] + async fn success_with_metadata_wrong_encoding() { + let state = State::test_new().await; + let mut rustus = init_service( + App::new().configure(rustus_service(web::Data::new(state.test_clone().await))), + ) + .await; + let request = TestRequest::post() + .uri(state.config.base_url().as_str()) + .insert_header(("Upload-Length", 100)) + .insert_header(( + "Upload-Metadata", + format!("test data1, pest {}", base64::encode("data")), + )) + .to_request(); + let resp = call_service(&mut rustus, request).await; + assert_eq!(resp.status(), StatusCode::CREATED); + // Getting file from location header. + let item_id = resp + .headers() + .get("Location") + .unwrap() + .to_str() + .unwrap() + .split('/') + .last() + .unwrap(); + let file_info = state.info_storage.get_info(item_id).await.unwrap(); + assert_eq!(file_info.length, Some(100)); + assert!(file_info.metadata.get("test").is_none()); + assert_eq!(file_info.metadata.get("pest").unwrap(), "data"); + assert_eq!(file_info.offset, 0); + } + + #[actix_rt::test] + async fn no_length_header() { + let state = State::test_new().await; + let mut rustus = init_service( + App::new().configure(rustus_service(web::Data::new(state.test_clone().await))), + ) + .await; + let request = TestRequest::post() + .uri(state.config.base_url().as_str()) + .to_request(); + let resp = call_service(&mut rustus, request).await; + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + } +} diff --git a/src/protocol/getting/mod.rs b/src/protocol/getting/mod.rs index e80e4485069e0aff5dddb0669711877ed9c77644..7b9184768e9612264c47cfff7efe252b364f9e3b 100644 --- a/src/protocol/getting/mod.rs +++ b/src/protocol/getting/mod.rs @@ -1,7 +1,5 @@ use actix_web::{guard, web}; -use crate::RustusConf; - mod routes; /// Add getting extension. @@ -10,10 +8,11 @@ mod routes; /// to get uploaded file. /// /// This is unofficial extension. -pub fn add_extension(web_app: &mut web::ServiceConfig, app_conf: &RustusConf) { +#[cfg_attr(coverage, no_coverage)] +pub fn add_extension(web_app: &mut web::ServiceConfig) { web_app.service( // GET /base/file - web::resource(app_conf.file_url().as_str()) + web::resource("{file_id}") .name("getting:get") .guard(guard::Get()) .to(routes::get_file), diff --git a/src/protocol/getting/routes.rs b/src/protocol/getting/routes.rs index 97af31a183dba4a927d2b064c5abb9098d75aad8..e3e7c0a59973d727c828186fe153830593f59491 100644 --- a/src/protocol/getting/routes.rs +++ b/src/protocol/getting/routes.rs @@ -1,12 +1,13 @@ -use actix_web::{web, HttpRequest, Responder}; +use actix_files::NamedFile; +use actix_web::{web, HttpRequest}; use crate::errors::RustusError; -use crate::State; +use crate::{RustusResult, State}; /// Retrieve actual file. /// /// This method allows you to download files directly from storage. -pub async fn get_file(request: HttpRequest, state: web::Data<State>) -> impl Responder { +pub async fn get_file(request: HttpRequest, state: web::Data<State>) -> RustusResult<NamedFile> { let file_id_opt = request.match_info().get("file_id").map(String::from); if let Some(file_id) = file_id_opt { let file_info = state.info_storage.get_info(file_id.as_str()).await?; @@ -18,3 +19,67 @@ pub async fn get_file(request: HttpRequest, state: web::Data<State>) -> impl Res Err(RustusError::FileNotFound) } } + +#[cfg(test)] +#[cfg_attr(coverage, no_coverage)] +mod test { + use crate::{rustus_service, State}; + use actix_web::http::StatusCode; + use actix_web::test::{call_service, init_service, TestRequest}; + use actix_web::{web, App}; + + #[actix_rt::test] + async fn success() { + let state = State::test_new().await; + let mut rustus = init_service( + App::new().configure(rustus_service(web::Data::new(state.test_clone().await))), + ) + .await; + let file_info = state.create_test_file().await; + state + .data_storage + .add_bytes(&file_info, "data".as_bytes()) + .await + .unwrap(); + let request = TestRequest::get() + .uri(state.config.file_url(file_info.id.as_str()).as_str()) + .to_request(); + let resp = call_service(&mut rustus, request).await; + assert!(resp.status().is_success()); + } + + #[actix_rt::test] + async fn unknown_file_id() { + let state = State::test_new().await; + let mut rustus = init_service( + App::new().configure(rustus_service(web::Data::new(state.test_clone().await))), + ) + .await; + let request = TestRequest::get() + .uri(state.config.file_url("random_str").as_str()) + .to_request(); + let resp = call_service(&mut rustus, request).await; + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + } + + #[actix_rt::test] + async fn unknown_storage() { + let state = State::test_new().await; + let mut rustus = init_service( + App::new().configure(rustus_service(web::Data::new(state.test_clone().await))), + ) + .await; + let mut file_info = state.create_test_file().await; + file_info.storage = "unknown_storage".into(); + state + .info_storage + .set_info(&file_info, false) + .await + .unwrap(); + let request = TestRequest::get() + .uri(state.config.file_url(file_info.id.as_str()).as_str()) + .to_request(); + let resp = call_service(&mut rustus, request).await; + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + } +} diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs index ecd6dba3bf8798e97d9337c86210cc8fe72c848c..6ac9a7b2739dbe2708a550f0ea09f3ebd008b814 100644 --- a/src/protocol/mod.rs +++ b/src/protocol/mod.rs @@ -12,20 +12,21 @@ mod termination; /// /// This function resolves all protocol extensions /// provided by CLI into services and adds it to the application. +#[cfg_attr(coverage, no_coverage)] pub fn setup(app_conf: RustusConf) -> Box<dyn Fn(&mut web::ServiceConfig)> { Box::new(move |web_app| { for extension in app_conf.extensions_vec() { match extension { - extensions::Extensions::Creation => creation::add_extension(web_app, &app_conf), + extensions::Extensions::Creation => creation::add_extension(web_app), extensions::Extensions::Termination => { - termination::add_extension(web_app, &app_conf); + termination::add_extension(web_app); } extensions::Extensions::Getting => { - getting::add_extension(web_app, &app_conf); + getting::add_extension(web_app); } _ => {} } } - core::add_extension(web_app, &app_conf); + core::add_extension(web_app); }) } diff --git a/src/protocol/termination/mod.rs b/src/protocol/termination/mod.rs index 3774fff828fd9835234041dfa8767e15bbc1a85f..bc3e23b2744b5351849126d371fcf1728dccad0a 100644 --- a/src/protocol/termination/mod.rs +++ b/src/protocol/termination/mod.rs @@ -1,17 +1,16 @@ use actix_web::{guard, web}; -use crate::RustusConf; - mod routes; /// Add termination extension. /// /// This extension allows you /// to terminate file upload. -pub fn add_extension(web_app: &mut web::ServiceConfig, app_conf: &RustusConf) { +#[cfg_attr(coverage, no_coverage)] +pub fn add_extension(web_app: &mut web::ServiceConfig) { web_app.service( // DELETE /base/file - web::resource(app_conf.file_url().as_str()) + web::resource("{file_id}") .name("termination:terminate") .guard(guard::Delete()) .to(routes::terminate), diff --git a/src/protocol/termination/routes.rs b/src/protocol/termination/routes.rs index 8e70f9d7539d46aadf33beac11697518412a683d..82ae88e6c3ca51aede0518ed8e31c91f7f71b2fc 100644 --- a/src/protocol/termination/routes.rs +++ b/src/protocol/termination/routes.rs @@ -1,6 +1,6 @@ use actix_web::{web, HttpRequest, HttpResponse}; -use crate::errors::RustusResult; +use crate::errors::{RustusError, RustusResult}; use crate::notifiers::Hook; use crate::State; @@ -16,7 +16,7 @@ pub async fn terminate( if let Some(file_id) = file_id_opt { let file_info = state.info_storage.get_info(file_id.as_str()).await?; if file_info.storage != state.data_storage.to_string() { - return Ok(HttpResponse::NotFound().finish()); + return Err(RustusError::FileNotFound); } state.info_storage.remove_info(file_id.as_str()).await?; state.data_storage.remove_file(&file_info).await?; @@ -37,3 +37,68 @@ pub async fn terminate( } Ok(HttpResponse::NoContent().finish()) } + +#[cfg(test)] +mod tests { + use crate::{rustus_service, State}; + use actix_web::http::StatusCode; + use actix_web::test::{call_service, init_service, TestRequest}; + use actix_web::{web, App}; + use std::path::PathBuf; + + #[actix_rt::test] + async fn success() { + let state = State::test_new().await; + let mut rustus = init_service( + App::new().configure(rustus_service(web::Data::new(state.test_clone().await))), + ) + .await; + let file_info = state.create_test_file().await; + let request = TestRequest::delete() + .uri(state.config.file_url(file_info.id.as_str()).as_str()) + .to_request(); + let response = call_service(&mut rustus, request).await; + assert_eq!(response.status(), StatusCode::NO_CONTENT); + assert!(state + .info_storage + .get_info(file_info.id.as_str()) + .await + .is_err()); + assert!(!PathBuf::from(file_info.path.unwrap()).exists()); + } + + #[actix_rt::test] + async fn unknown_file_id() { + let state = State::test_new().await; + let mut rustus = init_service( + App::new().configure(rustus_service(web::Data::new(state.test_clone().await))), + ) + .await; + let request = TestRequest::delete() + .param("file_id", "not_exists") + .to_request(); + let result = call_service(&mut rustus, request).await; + assert_eq!(result.status(), StatusCode::NOT_FOUND); + } + + #[actix_rt::test] + async fn wrong_storage() { + let state = State::test_new().await; + let mut rustus = init_service( + App::new().configure(rustus_service(web::Data::new(state.test_clone().await))), + ) + .await; + let mut file_info = state.create_test_file().await; + file_info.storage = "unknown_storage".into(); + state + .info_storage + .set_info(&file_info, false) + .await + .unwrap(); + let request = TestRequest::delete() + .uri(state.config.file_url(file_info.id.as_str()).as_str()) + .to_request(); + let response = call_service(&mut rustus, request).await; + assert_eq!(response.status(), StatusCode::NOT_FOUND); + } +} diff --git a/src/routes.rs b/src/routes.rs index d7310952a278edaff27b0d6e626dfa5332cdab7e..9dca6f6662765707b129ff720e2589c130d1a711 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -6,6 +6,7 @@ use crate::errors::{RustusError, RustusResult}; /// All protocol urls can be found /// at `crate::protocol::*`. #[allow(clippy::unused_async)] +#[cfg_attr(coverage, no_coverage)] pub async fn not_found() -> RustusResult<HttpResponse> { Err(RustusError::FileNotFound) } diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 0000000000000000000000000000000000000000..b688e88e56efa212bab91fa12605aa40f01b13a6 --- /dev/null +++ b/src/server.rs @@ -0,0 +1,20 @@ +use crate::{protocol, State}; +use actix_web::web::PayloadConfig; +use actix_web::{middleware, web}; + +pub fn rustus_service(state: web::Data<State>) -> Box<dyn Fn(&mut web::ServiceConfig)> { + Box::new(move |web_app| { + web_app.service( + web::scope(state.config.base_url().as_str()) + .app_data(state.clone()) + .app_data(PayloadConfig::new(state.config.max_body_size)) + // Main middleware that appends TUS headers. + .wrap( + middleware::DefaultHeaders::new() + .add(("Tus-Resumable", "1.0.0")) + .add(("Tus-Version", "1.0.0")), + ) + .configure(protocol::setup(state.config.clone())), + ); + }) +} diff --git a/src/state.rs b/src/state.rs index 16bc4f24ab2bab10450226a85b8e6c2d8c7faef8..07de07387dbf81c457e8f9ecb202ae7a45981c3d 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,3 +1,5 @@ +#[cfg(test)] +use crate::info_storages::FileInfo; use crate::{InfoStorage, NotificationManager, RustusConf, Storage}; pub struct State { @@ -21,4 +23,58 @@ impl State { notification_manager, } } + + #[cfg(test)] + pub async fn from_config(config: RustusConf) -> Self { + Self { + config: config.clone(), + data_storage: Box::new(crate::storages::file_storage::FileStorage::new( + config.storage_opts.data_dir.clone(), + config.storage_opts.dir_structure.clone(), + )), + info_storage: Box::new( + crate::info_storages::file_info_storage::FileInfoStorage::new( + config.info_storage_opts.info_dir.clone(), + ), + ), + notification_manager: NotificationManager::new(&config).await.unwrap(), + } + } + + #[cfg(test)] + pub async fn test_new() -> Self { + let data_dir = tempdir::TempDir::new("data_dir").unwrap(); + let info_dir = tempdir::TempDir::new("info_dir").unwrap(); + let config = RustusConf::from_iter( + vec![ + "rustus", + "--data-dir", + data_dir.into_path().to_str().unwrap(), + "--info-dir", + info_dir.into_path().to_str().unwrap(), + ] + .into_iter(), + ); + Self::from_config(config).await + } + + #[cfg(test)] + pub async fn test_clone(&self) -> Self { + let config = self.config.clone(); + Self::from_config(config).await + } + + #[cfg(test)] + pub async fn create_test_file(&self) -> FileInfo { + let mut new_file = FileInfo::new( + uuid::Uuid::new_v4().to_string().as_str(), + Some(10), + None, + self.data_storage.to_string(), + None, + ); + new_file.path = Some(self.data_storage.create_file(&new_file).await.unwrap()); + self.info_storage.set_info(&new_file, true).await.unwrap(); + new_file + } } diff --git a/src/storages/file_storage.rs b/src/storages/file_storage.rs index 12d8b15ec0c5f4ce95c2c618bac85c97fc8ea863..ccc699c0c70ab37c155b340a412352224972f053 100644 --- a/src/storages/file_storage.rs +++ b/src/storages/file_storage.rs @@ -10,24 +10,26 @@ use log::error; use crate::errors::{RustusError, RustusResult}; use crate::info_storages::FileInfo; use crate::storages::Storage; -use crate::RustusConf; +use crate::utils::dir_struct::dir_struct; use derive_more::Display; #[derive(Display)] #[display(fmt = "file_storage")] pub struct FileStorage { - app_conf: RustusConf, + data_dir: PathBuf, + dir_struct: String, } impl FileStorage { - pub fn new(app_conf: RustusConf) -> FileStorage { - FileStorage { app_conf } + pub fn new(data_dir: PathBuf, dir_struct: String) -> FileStorage { + FileStorage { + data_dir, + dir_struct, + } } pub async fn data_file_path(&self, file_id: &str) -> RustusResult<PathBuf> { let dir = self - .app_conf - .storage_opts .data_dir // We're working wit absolute paths, because tus.io says so. .canonicalize() @@ -35,7 +37,7 @@ impl FileStorage { error!("{}", err); RustusError::UnableToWrite(err.to_string()) })? - .join(self.app_conf.dir_struct().as_str()); + .join(dir_struct(self.dir_struct.as_str())); DirBuilder::new() .recursive(true) .create(dir.as_path()) @@ -44,7 +46,7 @@ impl FileStorage { error!("{}", err); RustusError::UnableToWrite(err.to_string()) })?; - Ok(dir.join(file_id.to_string())) + Ok(dir.join(file_id)) } } @@ -53,10 +55,10 @@ impl Storage for FileStorage { async fn prepare(&mut self) -> RustusResult<()> { // We're creating directory for new files // if it doesn't already exist. - if !self.app_conf.storage_opts.data_dir.exists() { + if !self.data_dir.exists() { DirBuilder::new() .recursive(true) - .create(self.app_conf.storage_opts.data_dir.as_path()) + .create(self.data_dir.as_path()) .await .map_err(|err| RustusError::UnableToPrepareStorage(err.to_string()))?; } @@ -137,8 +139,7 @@ impl Storage for FileStorage { let mut file = OpenOptions::new() .write(true) .append(true) - .create(false) - .create_new(false) + .create(true) .open(file_info.path.as_ref().unwrap().clone()) .await .map_err(|err| { @@ -169,3 +170,151 @@ impl Storage for FileStorage { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::FileStorage; + use crate::info_storages::FileInfo; + use crate::Storage; + use std::fs::File; + use std::io::{Read, Write}; + use std::path::PathBuf; + + #[actix_rt::test] + async fn preparation() { + let dir = tempdir::TempDir::new("file_storage").unwrap(); + let target_path = dir.into_path().join("not_exist"); + let mut storage = FileStorage::new(target_path.clone(), "".into()); + assert_eq!(target_path.exists(), false); + storage.prepare().await.unwrap(); + assert_eq!(target_path.exists(), true); + } + + #[actix_rt::test] + async fn create_file() { + let dir = tempdir::TempDir::new("file_storage").unwrap(); + let storage = FileStorage::new(dir.into_path().clone(), "".into()); + let file_info = FileInfo::new("test_id", Some(5), None, storage.to_string(), None); + let new_path = storage.create_file(&file_info).await.unwrap(); + assert!(PathBuf::from(new_path).exists()); + } + + #[actix_rt::test] + async fn create_file_but_it_exists() { + let dir = tempdir::TempDir::new("file_storage").unwrap(); + let base_path = dir.into_path().clone(); + let storage = FileStorage::new(base_path.clone(), "".into()); + let file_info = FileInfo::new("test_id", Some(5), None, storage.to_string(), None); + File::create(base_path.join("test_id")).unwrap(); + let result = storage.create_file(&file_info).await; + assert!(result.is_err()); + } + + #[actix_rt::test] + async fn adding_bytes() { + let dir = tempdir::TempDir::new("file_storage").unwrap(); + let storage = FileStorage::new(dir.into_path().clone(), "".into()); + let mut file_info = FileInfo::new("test_id", Some(5), None, storage.to_string(), None); + let new_path = storage.create_file(&file_info).await.unwrap(); + let test_data = "MyTestData"; + file_info.path = Some(new_path.clone()); + storage + .add_bytes(&file_info, test_data.as_bytes()) + .await + .unwrap(); + let mut file = File::open(new_path).unwrap(); + let mut contents = String::new(); + file.read_to_string(&mut contents).unwrap(); + assert_eq!(contents, String::from(test_data)) + } + + #[actix_rt::test] + async fn adding_bytes_to_unknown_file() { + let dir = tempdir::TempDir::new("file_storage").unwrap(); + let storage = FileStorage::new(dir.into_path().clone(), "".into()); + let file_info = FileInfo::new( + "test_id", + Some(5), + Some(String::from("some_file")), + storage.to_string(), + None, + ); + let test_data = "MyTestData"; + let result = storage.add_bytes(&file_info, test_data.as_bytes()).await; + assert!(result.is_err()) + } + + #[actix_rt::test] + async fn get_contents_of_unknown_file() { + let dir = tempdir::TempDir::new("file_storage").unwrap(); + let storage = FileStorage::new(dir.into_path().clone(), "".into()); + let file_info = FileInfo::new( + "test_id", + Some(5), + Some(storage.data_dir.join("unknown").display().to_string()), + storage.to_string(), + None, + ); + let file_info = storage.get_contents(&file_info).await; + assert!(file_info.is_err()); + } + + #[actix_rt::test] + async fn remove_unknown_file() { + let dir = tempdir::TempDir::new("file_storage").unwrap(); + let storage = FileStorage::new(dir.into_path().clone(), "".into()); + let file_info = FileInfo::new( + "test_id", + Some(5), + Some(storage.data_dir.join("unknown").display().to_string()), + storage.to_string(), + None, + ); + let file_info = storage.remove_file(&file_info).await; + assert!(file_info.is_err()); + } + + #[actix_rt::test] + async fn success_concatenation() { + let dir = tempdir::TempDir::new("file_storage").unwrap(); + let storage = FileStorage::new(dir.into_path().clone(), "".into()); + + let mut parts = Vec::new(); + let part1_path = storage.data_dir.as_path().join("part1"); + let mut part1 = File::create(part1_path.clone()).unwrap(); + let size1 = part1.write("hello ".as_bytes()).unwrap(); + + parts.push(FileInfo::new( + "part_id1", + Some(size1), + Some(part1_path.display().to_string()), + storage.to_string(), + None, + )); + + let part2_path = storage.data_dir.as_path().join("part2"); + let mut part2 = File::create(part2_path.clone()).unwrap(); + let size2 = part2.write("world".as_bytes()).unwrap(); + parts.push(FileInfo::new( + "part_id2", + Some(size2), + Some(part2_path.display().to_string()), + storage.to_string(), + None, + )); + + let final_info = FileInfo::new( + "final_id", + None, + Some(storage.data_dir.join("final_info").display().to_string()), + storage.to_string(), + None, + ); + storage.concat_files(&final_info, parts).await.unwrap(); + let mut final_file = File::open(final_info.path.unwrap()).unwrap(); + let mut buffer = String::new(); + final_file.read_to_string(&mut buffer).unwrap(); + + assert_eq!(buffer.as_str(), "hello world"); + } +} diff --git a/src/storages/models/available_stores.rs b/src/storages/models/available_stores.rs index c4f8d1b788a84fecb0bf29cd00bd89944a2c5761..7bde0987c00c335302068b56c9194631606b82ca 100644 --- a/src/storages/models/available_stores.rs +++ b/src/storages/models/available_stores.rs @@ -19,10 +19,14 @@ impl AvailableStores { /// `config` - Rustus configuration. /// `info_storage` - Storage for information about files. /// + #[cfg_attr(coverage, no_coverage)] pub fn get(&self, config: &RustusConf) -> Box<dyn Storage + Send + Sync> { #[allow(clippy::single_match)] match self { - Self::FileStorage => Box::new(file_storage::FileStorage::new(config.clone())), + Self::FileStorage => Box::new(file_storage::FileStorage::new( + config.storage_opts.data_dir.clone(), + config.storage_opts.dir_structure.clone(), + )), } } } diff --git a/src/utils/dir_struct.rs b/src/utils/dir_struct.rs new file mode 100644 index 0000000000000000000000000000000000000000..9c1875729d0e960a24f4dc4de6f23405c83bc578 --- /dev/null +++ b/src/utils/dir_struct.rs @@ -0,0 +1,50 @@ +use chrono::{Datelike, Timelike}; +use lazy_static::lazy_static; +use log::error; +use std::collections::HashMap; +use std::env; + +lazy_static! { + /// Freezing ENVS on startup. + static ref ENV_MAP: HashMap<String, String> = { + let mut m = HashMap::new(); + for (key, value) in env::vars() { + m.insert(format!("env[{}]", key), value); + } + m + }; +} + +/// Generate directory name with user template. +pub fn dir_struct(dir_structure: &str) -> String { + let now = chrono::Utc::now(); + let mut vars: HashMap<String, String> = ENV_MAP.clone(); + vars.insert("day".into(), now.day().to_string()); + vars.insert("month".into(), now.month().to_string()); + vars.insert("year".into(), now.year().to_string()); + vars.insert("hour".into(), now.hour().to_string()); + vars.insert("minute".into(), now.minute().to_string()); + strfmt::strfmt(dir_structure, &vars).unwrap_or_else(|err| { + error!("{}", err); + "".into() + }) +} + +#[cfg(test)] +mod tests { + use super::dir_struct; + use chrono::Datelike; + + #[test] + pub fn test_time() { + let now = chrono::Utc::now(); + let dir = dir_struct("{day}/{month}"); + assert_eq!(dir, format!("{}/{}", now.day(), now.month())); + } + + #[test] + pub fn test_unknown_var() { + let dir = dir_struct("test/{quake}"); + assert_eq!(dir, String::from("")); + } +} diff --git a/src/utils/enums.rs b/src/utils/enums.rs index 5109aeef353f609c345e4b70dde0a42ea0477ad3..4d90861881afddf73fe6c605627da289d9e1f3f1 100644 --- a/src/utils/enums.rs +++ b/src/utils/enums.rs @@ -27,3 +27,32 @@ macro_rules! from_str { } }; } + +#[cfg(test)] +mod tests { + use crate::from_str; + use derive_more::{Display, From}; + use strum::EnumIter; + + #[derive(PartialEq, Debug, Display, EnumIter, From, Clone, Eq)] + pub enum TestEnum { + #[display(fmt = "test-val-1")] + TestVal1, + #[display(fmt = "test-val-2")] + TestVal2, + } + + from_str!(TestEnum, "test-vals"); + + #[test] + fn test_from_str_unknown_val() { + let result = TestEnum::from_str("unknown"); + assert!(result.is_err()) + } + + #[test] + fn test_from_str() { + let result = TestEnum::from_str("test-val-1"); + assert_eq!(result.unwrap(), TestEnum::TestVal1) + } +} diff --git a/src/utils/headers.rs b/src/utils/headers.rs index f2b0c42aaa214275c9b845b6bf1dde4b9b118f02..493b09ab79571000e5661ef8ad9db44346c3c4ba 100644 --- a/src/utils/headers.rs +++ b/src/utils/headers.rs @@ -41,3 +41,52 @@ pub fn check_header(request: &HttpRequest, header_name: &str, expr: fn(&str) -> }) .unwrap_or(false) } + +#[cfg(test)] +mod tests { + use super::{check_header, parse_header}; + use actix_web::test::TestRequest; + + #[actix_rt::test] + async fn test_parse_header_unknown_header() { + let request = TestRequest::get().to_http_request(); + let header = parse_header::<String>(&request, "unknown"); + assert!(header.is_none()); + } + + #[actix_rt::test] + async fn test_parse_header_wrong_type() { + let request = TestRequest::get() + .insert_header(("test_header", String::from("test").as_bytes())) + .to_http_request(); + let header = parse_header::<i32>(&request, "test_header"); + assert!(header.is_none()); + } + + #[actix_rt::test] + async fn test_parse_header() { + let request = TestRequest::get() + .insert_header(("test_header", String::from("123").as_bytes())) + .to_http_request(); + let header = parse_header::<usize>(&request, "test_header"); + assert_eq!(header.unwrap(), 123); + } + + #[actix_rt::test] + async fn test_check_header_unknown_header() { + let request = TestRequest::get().to_http_request(); + let check = check_header(&request, "unknown", |value| value == "1"); + assert_eq!(check, false); + } + + #[actix_rt::test] + async fn test_check_header() { + let request = TestRequest::get() + .insert_header(("test_header", "1")) + .to_http_request(); + let check = check_header(&request, "test_header", |value| value == "1"); + assert!(check); + let check = check_header(&request, "test_header", |value| value == "2"); + assert!(!check); + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 0f6b3005ddba8a142e59aa63b83f8ba61de6af87..0bef5e255fa47a6f53bdba0c498862fee1325665 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,2 +1,3 @@ +pub mod dir_struct; pub mod enums; pub mod headers;