From 71b97a30c130d3d6332dada340f6d243390f6bc2 Mon Sep 17 00:00:00 2001
From: Pavel Kirilin <win10@list.ru>
Date: Tue, 8 Feb 2022 14:38:43 +0400
Subject: [PATCH] Added tests.

Signed-off-by: Pavel Kirilin <win10@list.ru>
---
 .github/workflows/lint.yml                    |  37 --
 .github/workflows/test.yml                    |  70 +++
 .gitignore                                    |   4 +-
 Cargo.lock                                    | 143 ++++--
 Cargo.toml                                    |  19 +-
 README.md                                     |   8 +-
 src/config.rs                                 |  53 +--
 src/errors.rs                                 |   2 +
 src/info_storages/file_info_storage.rs        |  92 +++-
 .../models/available_info_storages.rs         |   3 +-
 src/main.rs                                   |  23 +-
 src/notifiers/dir_notifier.rs                 |  72 ++-
 src/notifiers/file_notifier.rs                |  89 +++-
 src/notifiers/http_notifier.rs                |  92 ++++
 src/notifiers/mod.rs                          |   2 -
 src/notifiers/models/notification_manager.rs  |   4 -
 src/protocol/core/get_info.rs                 | 263 +++++++++++
 src/protocol/core/mod.rs                      |  24 +-
 src/protocol/core/routes.rs                   | 187 --------
 src/protocol/core/server_info.rs              |  56 +++
 src/protocol/core/write_bytes.rs              | 411 ++++++++++++++++++
 src/protocol/creation/mod.rs                  |   8 +-
 src/protocol/creation/routes.rs               | 313 ++++++++++++-
 src/protocol/getting/mod.rs                   |   7 +-
 src/protocol/getting/routes.rs                |  71 ++-
 src/protocol/mod.rs                           |   9 +-
 src/protocol/termination/mod.rs               |   7 +-
 src/protocol/termination/routes.rs            |  69 ++-
 src/routes.rs                                 |   1 +
 src/server.rs                                 |  20 +
 src/state.rs                                  |  56 +++
 src/storages/file_storage.rs                  | 173 +++++++-
 src/storages/models/available_stores.rs       |   6 +-
 src/utils/dir_struct.rs                       |  50 +++
 src/utils/enums.rs                            |  29 ++
 src/utils/headers.rs                          |  49 +++
 src/utils/mod.rs                              |   1 +
 37 files changed, 2102 insertions(+), 421 deletions(-)
 delete mode 100644 .github/workflows/lint.yml
 create mode 100644 .github/workflows/test.yml
 create mode 100644 src/protocol/core/get_info.rs
 delete mode 100644 src/protocol/core/routes.rs
 create mode 100644 src/protocol/core/server_info.rs
 create mode 100644 src/protocol/core/write_bytes.rs
 create mode 100644 src/server.rs
 create mode 100644 src/utils/dir_struct.rs

diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
deleted file mode 100644
index d5d0e0b..0000000
--- 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 0000000..c8b52b1
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,70 @@
+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
+    steps:
+      - uses: actions/checkout@v1
+      - 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 --lcov --output-path lcov.info
+      - name: Coveralls
+        uses: coverallsapp/github-action@master
+        with:
+          github-token: ${{ secrets.GITHUB_TOKEN }}
+          path-to-lcov: lcov.info
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 155c481..31bd9be 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/Cargo.lock b/Cargo.lock
index 333bba1..101c5bf 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -73,7 +73,7 @@ dependencies = [
  "mime",
  "percent-encoding",
  "pin-project-lite",
- "rand",
+ "rand 0.8.4",
  "sha-1 0.10.0",
  "smallvec",
  "zstd",
@@ -391,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"
@@ -609,6 +592,17 @@ dependencies = [
  "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.9.1"
@@ -847,7 +841,7 @@ 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",
 ]
 
@@ -1090,6 +1084,12 @@ 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"
@@ -1380,6 +1380,28 @@ 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"
@@ -1795,7 +1817,7 @@ dependencies = [
  "num-integer",
  "num-iter",
  "num-traits",
- "rand",
+ "rand 0.8.4",
  "smallvec",
  "zeroize",
 ]
@@ -2169,6 +2191,19 @@ dependencies = [
  "proc-macro2",
 ]
 
+[[package]]
+name = "rand"
+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.4"
@@ -2177,7 +2212,7 @@ checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8"
 dependencies = [
  "libc",
  "rand_chacha",
- "rand_core",
+ "rand_core 0.6.3",
  "rand_hc",
 ]
 
@@ -2188,9 +2223,24 @@ 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.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
+dependencies = [
+ "rand_core 0.4.2",
 ]
 
+[[package]]
+name = "rand_core"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
+
 [[package]]
 name = "rand_core"
 version = "0.6.3"
@@ -2206,7 +2256,7 @@ version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7"
 dependencies = [
- "rand_core",
+ "rand_core 0.6.3",
 ]
 
 [[package]]
@@ -2222,7 +2272,7 @@ dependencies = [
  "hex",
  "log",
  "once_cell",
- "rand",
+ "rand 0.8.4",
  "rbatis-core",
  "rbatis-macro-driver",
  "rbatis_sql",
@@ -2309,13 +2359,22 @@ dependencies = [
  "hex",
  "indexmap",
  "lazy_static",
- "rand",
+ "rand 0.8.4",
  "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"
@@ -2366,6 +2425,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"
@@ -2458,7 +2523,7 @@ dependencies = [
  "num-traits",
  "pkcs1",
  "pkcs8",
- "rand",
+ "rand 0.8.4",
  "subtle",
  "zeroize",
 ]
@@ -2510,8 +2575,8 @@ name = "rustus"
 version = "0.4.1"
 dependencies = [
  "actix-files",
+ "actix-rt",
  "actix-web",
- "async-process",
  "async-std",
  "async-trait",
  "base64",
@@ -2519,6 +2584,7 @@ dependencies = [
  "derive_more",
  "fern",
  "futures",
+ "httptest",
  "lapin",
  "lazy_static",
  "log",
@@ -2533,6 +2599,7 @@ dependencies = [
  "strfmt",
  "structopt",
  "strum",
+ "tempdir",
  "thiserror",
  "tokio",
  "tokio-amqp",
@@ -2739,16 +2806,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"
@@ -2859,7 +2916,7 @@ dependencies = [
  "once_cell",
  "parking_lot",
  "percent-encoding",
- "rand",
+ "rand 0.8.4",
  "regex",
  "rsa",
  "rust_decimal",
@@ -3061,6 +3118,16 @@ 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.3.0"
diff --git a/Cargo.toml b/Cargo.toml
index 0dfcee7..579f2fe 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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,17 @@ 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"]
+
+[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 7cb97ee..ea1bb82 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 2adf14d..32e44d9 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,9 @@ pub struct NotificationsOptions {
     #[structopt(long, env = "RUSTUS_HOOKS_AMQP_EXCHANGE", default_value = "rustus")]
     pub hooks_amqp_exchange: String,
 
-    #[cfg(feature = "file_notifiers")]
     #[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 +183,7 @@ pub struct RustusConf {
     pub notification_opts: NotificationsOptions,
 }
 
+#[cfg_attr(coverage, no_coverage)]
 impl RustusConf {
     /// Function to parse CLI parametes.
     ///
@@ -209,6 +193,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 +211,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 +229,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 19be406..942fe39 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/file_info_storage.rs b/src/info_storages/file_info_storage.rs
index 338ac68..65cd3ca 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 66a84cd..befef8d 100644
--- a/src/info_storages/models/available_info_storages.rs
+++ b/src/info_storages/models/available_info_storages.rs
@@ -32,13 +32,14 @@ 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(
diff --git a/src/main.rs b/src/main.rs
index b05a51b..93aacc1 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/dir_notifier.rs b/src/notifiers/dir_notifier.rs
index 670d1e1..008d79c 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 a502592..5ac7e31 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 d8ea9ac..2e71004 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 86ac9c6..0359510 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 84509da..b01cc72 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;
@@ -22,14 +20,12 @@ impl NotificationManager {
             notifiers: Vec::new(),
         };
         debug!("Initializing notification manager.");
-        #[cfg(feature = "file_notifiers")]
         if tus_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(),
             )));
         }
-        #[cfg(feature = "file_notifiers")]
         if tus_config.notification_opts.hooks_dir.is_some() {
             debug!("Found hooks directory");
             manager.notifiers.push(Box::new(DirNotifier::new(
diff --git a/src/protocol/core/get_info.rs b/src/protocol/core/get_info.rs
new file mode 100644
index 0000000..bc8cc8a
--- /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 df00d5c..dc29f98 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 d910f67..0000000
--- 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 0000000..f5217cd
--- /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 0000000..322b219
--- /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 2ecaf12..eede35d 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 21c710d..20df718 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 e80e448..7b91847 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 97af31a..e3e7c0a 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 ecd6dba..6ac9a7b 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 3774fff..bc3e23b 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 8e70f9d..82ae88e 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 d731095..9dca6f6 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 0000000..b688e88
--- /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 16bc4f2..07de073 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 12d8b15..ccc699c 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 c4f8d1b..7bde098 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 0000000..9c18757
--- /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 5109aee..4d90861 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 f2b0c42..493b09a 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 0f6b300..0bef5e2 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;
-- 
GitLab