From eedb0ceef9b71711446655264fb4f3700ca42ed2 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 18 Apr 2023 17:23:06 +0100 Subject: [PATCH] tests: e2e tests scaffolding --- .cargo/config.toml | 7 ++++ .env.local | 1 + .github/workflows/develop.yml | 35 +++++++----------- .github/workflows/release.yml | 3 ++ .gitignore | 2 ++ Cargo.toml | 3 ++ bin/install.sh | 3 ++ compose.yaml | 27 ++++++++++++++ config-idx-back.toml.local | 4 +-- docker/README.md | 8 +++-- docker/bin/e2e-env-down.sh | 3 ++ docker/bin/e2e-env-up.sh | 10 ++++++ docker/bin/run-e2e-tests.sh | 57 +++++++++++++++++++++++++++++ tests/e2e/client.rs | 67 +++++++++++++++++++++++++++++++++++ tests/e2e/connection_info.rs | 16 +++++++++ tests/e2e/contexts/about.rs | 21 +++++++++++ tests/e2e/contexts/mod.rs | 1 + tests/e2e/http.rs | 54 ++++++++++++++++++++++++++++ tests/e2e/mod.rs | 9 +++++ tests/mod.rs | 1 + 20 files changed, 304 insertions(+), 28 deletions(-) create mode 100644 .cargo/config.toml create mode 100755 docker/bin/e2e-env-down.sh create mode 100755 docker/bin/e2e-env-up.sh create mode 100755 docker/bin/run-e2e-tests.sh create mode 100644 tests/e2e/client.rs create mode 100644 tests/e2e/connection_info.rs create mode 100644 tests/e2e/contexts/about.rs create mode 100644 tests/e2e/contexts/mod.rs create mode 100644 tests/e2e/http.rs create mode 100644 tests/e2e/mod.rs diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..e67234cb --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,7 @@ +[alias] +cov = "llvm-cov" +cov-lcov = "llvm-cov --lcov --output-path=./.coverage/lcov.info" +cov-html = "llvm-cov --html" +time = "build --timings --all-targets" +e2e = "test --features e2e-tests" + diff --git a/.env.local b/.env.local index ef58fd2e..90b3e4b3 100644 --- a/.env.local +++ b/.env.local @@ -3,3 +3,4 @@ TORRUST_IDX_BACK_CONFIG= TORRUST_IDX_BACK_USER_UID=1000 TORRUST_TRACKER_CONFIG= TORRUST_TRACKER_USER_UID=1000 +TORRUST_TRACKER_API_TOKEN=MyAccessToken diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml index 1b27a4e7..dca7e7e1 100644 --- a/.github/workflows/develop.yml +++ b/.github/workflows/develop.yml @@ -1,6 +1,6 @@ name: Development Checks -on: [push,pull_request] +on: [push, pull_request] jobs: run: @@ -14,27 +14,16 @@ jobs: components: rustfmt, clippy - uses: Swatinem/rust-cache@v2 - name: Format - uses: ClementTsang/cargo-action@main - with: - command: fmt - args: --all --check + run: cargo fmt --all --check - name: Check - uses: ClementTsang/cargo-action@main - with: - command: check - args: --all-targets + run: cargo check --all-targets - name: Clippy - uses: ClementTsang/cargo-action@main - with: - command: clippy - args: --all-targets - - name: Build - uses: ClementTsang/cargo-action@main - with: - command: build - args: --all-targets - - name: Test - uses: ClementTsang/cargo-action@main - with: - command: test - args: --all-targets \ No newline at end of file + run: cargo clippy --all-targets + - name: Unit and integration tests + run: cargo test --all-targets + - uses: taiki-e/install-action@cargo-llvm-cov + - uses: taiki-e/install-action@nextest + - name: Test Coverage + run: cargo llvm-cov nextest + - name: E2E Tests + run: ./docker/bin/run-e2e-tests.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 38d427f6..10c62fb8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,6 +25,9 @@ jobs: - uses: Swatinem/rust-cache@v1 - name: Run tests run: cargo test + - name: Stop databases + working-directory: ./tests + run: docker-compose down tag: needs: test diff --git a/.gitignore b/.gitignore index 282b85e3..eb90c276 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +/.coverage/ /.env /config.toml /data_v2.db* @@ -5,3 +6,4 @@ /storage/ /target /uploads/ + diff --git a/Cargo.toml b/Cargo.toml index 1eb837e9..ab3fd7ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,9 @@ default-run = "main" [profile.dev.package.sqlx-macros] opt-level = 3 +[features] +e2e-tests = [] + [dependencies] actix-web = "4.0.0-beta.8" actix-multipart = "0.4.0-beta.5" diff --git a/bin/install.sh b/bin/install.sh index b8df7f7a..041863b2 100755 --- a/bin/install.sh +++ b/bin/install.sh @@ -5,6 +5,9 @@ if ! [ -f "./config.toml" ]; then cp ./config.toml.local ./config.toml fi +# Generate storage directory if it does not exist +mkdir -p "./storage/database" + # Generate the sqlite database for the index baclend if it does not exist if ! [ -f "./storage/database/data.db" ]; then # todo: it should get the path from config.toml and only do it when we use sqlite diff --git a/compose.yaml b/compose.yaml index 82658156..35447943 100644 --- a/compose.yaml +++ b/compose.yaml @@ -4,15 +4,30 @@ services: idx-back: build: context: . + args: + RUN_AS_USER: appuser + UID: ${TORRUST_IDX_BACK_USER_UID:-1000} target: development user: ${TORRUST_IDX_BACK_USER_UID:-1000}:${TORRUST_IDX_BACK_USER_UID:-1000} tty: true environment: - TORRUST_IDX_BACK_CONFIG=${TORRUST_IDX_BACK_CONFIG} + - CARGO_HOME=/home/appuser/.cargo networks: - server_side ports: - 3000:3000 + # todo: implement healthcheck + #healthcheck: + # test: + # [ + # "CMD-SHELL", + # "cargo run healthcheck" + # ] + # interval: 10s + # retries: 5 + # start_period: 10s + # timeout: 3s volumes: - ./:/app - ~/.cargo:/home/appuser/.cargo @@ -25,11 +40,23 @@ services: tty: true environment: - TORRUST_TRACKER_CONFIG=${TORRUST_TRACKER_CONFIG} + - TORRUST_TRACKER_API_TOKEN=${TORRUST_TRACKER_API_TOKEN:-MyAccessToken} networks: - server_side ports: - 6969:6969/udp - 1212:1212/tcp + # todo: implement healthcheck + #healthcheck: + # test: + # [ + # "CMD-SHELL", + # "/app/main healthcheck" + # ] + # interval: 10s + # retries: 5 + # start_period: 10s + # timeout: 3s volumes: - ./storage:/app/storage depends_on: diff --git a/config-idx-back.toml.local b/config-idx-back.toml.local index 2257c16f..1051dcb9 100644 --- a/config-idx-back.toml.local +++ b/config-idx-back.toml.local @@ -18,8 +18,8 @@ max_password_length = 64 secret_key = "MaxVerstappenWC2021" [database] -#connect_url = "sqlite://storage/database/data.db?mode=rwc" # SQLite -connect_url = "mysql://root:root_secret_password@mysql:3306/torrust_index_backend" # MySQL +connect_url = "sqlite://storage/database/data.db?mode=rwc" # SQLite +#connect_url = "mysql://root:root_secret_password@mysql:3306/torrust_index_backend" # MySQL torrent_info_update_interval = 3600 [mail] diff --git a/docker/README.md b/docker/README.md index 02be6219..664e58b8 100644 --- a/docker/README.md +++ b/docker/README.md @@ -63,9 +63,11 @@ If you want to inject an environment variable into docker-compose you can use th Build and run it locally: ```s -TORRUST_TRACKER_CONFIG=$(cat config-tracker.toml.local) \ -TORRUST_IDX_BACK_CONFIG=$(cat config-idx-back.toml.local) \ - docker compose up --build +TORRUST_IDX_BACK_USER_UID=${TORRUST_IDX_BACK_USER_UID:-1000} \ + TORRUST_IDX_BACK_CONFIG=$(cat config-idx-back.toml.local) \ + TORRUST_TRACKER_CONFIG=$(cat config-tracker.toml.local) \ + TORRUST_TRACKER_API_TOKEN=${TORRUST_TRACKER_API_TOKEN:-MyAccessToken} \ + docker compose up -d --build ``` After running the "up" command you will have three running containers: diff --git a/docker/bin/e2e-env-down.sh b/docker/bin/e2e-env-down.sh new file mode 100755 index 00000000..5e50d101 --- /dev/null +++ b/docker/bin/e2e-env-down.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +docker compose down diff --git a/docker/bin/e2e-env-up.sh b/docker/bin/e2e-env-up.sh new file mode 100755 index 00000000..a5de770c --- /dev/null +++ b/docker/bin/e2e-env-up.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +TORRUST_IDX_BACK_USER_UID=${TORRUST_IDX_BACK_USER_UID:-1000} \ + docker compose build + +TORRUST_IDX_BACK_USER_UID=${TORRUST_IDX_BACK_USER_UID:-1000} \ + TORRUST_IDX_BACK_CONFIG=$(cat config-idx-back.toml.local) \ + TORRUST_TRACKER_CONFIG=$(cat config-tracker.toml.local) \ + TORRUST_TRACKER_API_TOKEN=${TORRUST_TRACKER_API_TOKEN:-MyAccessToken} \ + docker compose up -d diff --git a/docker/bin/run-e2e-tests.sh b/docker/bin/run-e2e-tests.sh new file mode 100755 index 00000000..5eb63c33 --- /dev/null +++ b/docker/bin/run-e2e-tests.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +CURRENT_USER_NAME=$(whoami) +CURRENT_USER_ID=$(id -u) +echo "User name: $CURRENT_USER_NAME" +echo "User id: $CURRENT_USER_ID" + +TORRUST_IDX_BACK_USER_UID=$CURRENT_USER_ID +TORRUST_TRACKER_USER_UID=$CURRENT_USER_ID +export TORRUST_IDX_BACK_USER_UID +export TORRUST_TRACKER_USER_UID + +wait_for_container_to_be_healthy() { + local container_name="$1" + local max_retries="$2" + local retry_interval="$3" + local retry_count=0 + + while [ $retry_count -lt "$max_retries" ]; do + container_health="$(docker inspect --format='{{json .State.Health}}' "$container_name")" + if [ "$container_health" != "{}" ]; then + container_status="$(echo "$container_health" | jq -r '.Status')" + if [ "$container_status" == "healthy" ]; then + echo "Container $container_name is healthy" + return 0 + fi + fi + + retry_count=$((retry_count + 1)) + echo "Waiting for container $container_name to become healthy (attempt $retry_count of $max_retries)..." + sleep "$retry_interval" + done + + echo "Timeout reached, container $container_name is not healthy" + return 1 +} + +cp .env.local .env +./bin/install.sh + +# Start E2E testing environment +./docker/bin/e2e-env-up.sh + +wait_for_container_to_be_healthy torrust-mysql-1 10 3 +# todo: implement healthchecks for tracker and backend and wait until they are healthy +#wait_for_container torrust-tracker-1 10 3 +#wait_for_container torrust-idx-back-1 10 3 +sleep 20s + +# Just to make sure that everything is up and running +docker ps + +# Run E2E tests +cargo test --features e2e-tests + +# Stop E2E testing environment +./docker/bin/e2e-env-down.sh diff --git a/tests/e2e/client.rs b/tests/e2e/client.rs new file mode 100644 index 00000000..3d249d66 --- /dev/null +++ b/tests/e2e/client.rs @@ -0,0 +1,67 @@ +use reqwest::Response; + +use crate::e2e::connection_info::ConnectionInfo; +use crate::e2e::http::{Query, ReqwestQuery}; + +/// API Client +pub struct Client { + connection_info: ConnectionInfo, + base_path: String, +} + +impl Client { + pub fn new(connection_info: ConnectionInfo) -> Self { + Self { + connection_info, + base_path: "/".to_string(), + } + } + + pub async fn entrypoint(&self) -> Response { + self.get("", Query::default()).await + } + + pub async fn get(&self, path: &str, params: Query) -> Response { + self.get_request_with_query(path, params).await + } + + /* + pub async fn post(&self, path: &str) -> Response { + reqwest::Client::new().post(self.base_url(path).clone()).send().await.unwrap() + } + + async fn delete(&self, path: &str) -> Response { + reqwest::Client::new() + .delete(self.base_url(path).clone()) + .send() + .await + .unwrap() + } + + pub async fn get_request(&self, path: &str) -> Response { + get(&self.base_url(path), None).await + } + */ + + pub async fn get_request_with_query(&self, path: &str, params: Query) -> Response { + get(&self.base_url(path), Some(params)).await + } + + fn base_url(&self, path: &str) -> String { + format!("http://{}{}{path}", &self.connection_info.bind_address, &self.base_path) + } +} + +async fn get(path: &str, query: Option) -> Response { + match query { + Some(params) => reqwest::Client::builder() + .build() + .unwrap() + .get(path) + .query(&ReqwestQuery::from(params)) + .send() + .await + .unwrap(), + None => reqwest::Client::builder().build().unwrap().get(path).send().await.unwrap(), + } +} diff --git a/tests/e2e/connection_info.rs b/tests/e2e/connection_info.rs new file mode 100644 index 00000000..f70dae6f --- /dev/null +++ b/tests/e2e/connection_info.rs @@ -0,0 +1,16 @@ +pub fn connection_with_no_token(bind_address: &str) -> ConnectionInfo { + ConnectionInfo::anonymous(bind_address) +} + +#[derive(Clone)] +pub struct ConnectionInfo { + pub bind_address: String, +} + +impl ConnectionInfo { + pub fn anonymous(bind_address: &str) -> Self { + Self { + bind_address: bind_address.to_string(), + } + } +} diff --git a/tests/e2e/contexts/about.rs b/tests/e2e/contexts/about.rs new file mode 100644 index 00000000..99bcb276 --- /dev/null +++ b/tests/e2e/contexts/about.rs @@ -0,0 +1,21 @@ +use crate::e2e::client::Client; +use crate::e2e::connection_info::connection_with_no_token; + +#[tokio::test] +#[cfg_attr(not(feature = "e2e-tests"), ignore)] +async fn it_should_load_the_about_page_at_the_api_entrypoint() { + let client = Client::new(connection_with_no_token("localhost:3000")); + + let response = client.entrypoint().await; + + assert_eq!(response.status(), 200); + assert_eq!(response.headers().get("content-type").unwrap(), "text/html; charset=utf-8"); + + let title = format!("About"); + let response_text = response.text().await.unwrap(); + + assert!( + response_text.contains(&title), + ":\n response: `\"{response_text}\"`\n does not contain: `\"{title}\"`." + ); +} diff --git a/tests/e2e/contexts/mod.rs b/tests/e2e/contexts/mod.rs new file mode 100644 index 00000000..ced75210 --- /dev/null +++ b/tests/e2e/contexts/mod.rs @@ -0,0 +1 @@ +pub mod about; diff --git a/tests/e2e/http.rs b/tests/e2e/http.rs new file mode 100644 index 00000000..d682027f --- /dev/null +++ b/tests/e2e/http.rs @@ -0,0 +1,54 @@ +pub type ReqwestQuery = Vec; +pub type ReqwestQueryParam = (String, String); + +/// URL Query component +#[derive(Default, Debug)] +pub struct Query { + params: Vec, +} + +impl Query { + pub fn empty() -> Self { + Self { params: vec![] } + } + + pub fn params(params: Vec) -> Self { + Self { params } + } + + pub fn add_param(&mut self, param: QueryParam) { + self.params.push(param); + } +} + +impl From for ReqwestQuery { + fn from(url_search_params: Query) -> Self { + url_search_params + .params + .iter() + .map(|param| ReqwestQueryParam::from((*param).clone())) + .collect() + } +} + +/// URL query param +#[derive(Clone, Debug)] +pub struct QueryParam { + name: String, + value: String, +} + +impl QueryParam { + pub fn new(name: &str, value: &str) -> Self { + Self { + name: name.to_string(), + value: value.to_string(), + } + } +} + +impl From for ReqwestQueryParam { + fn from(param: QueryParam) -> Self { + (param.name, param.value) + } +} diff --git a/tests/e2e/mod.rs b/tests/e2e/mod.rs new file mode 100644 index 00000000..35de4dcf --- /dev/null +++ b/tests/e2e/mod.rs @@ -0,0 +1,9 @@ +//! End-to-end tests. +//! +//! ``` +//! cargo test -- --ignored +//! ``` +mod client; +mod connection_info; +mod contexts; +mod http; diff --git a/tests/mod.rs b/tests/mod.rs index 27bea3bd..f90fa4f2 100644 --- a/tests/mod.rs +++ b/tests/mod.rs @@ -1,2 +1,3 @@ mod databases; +mod e2e; pub mod upgrades;