Skip to content

Commit

Permalink
Merge #129: Integration tests scaffolding
Browse files Browse the repository at this point in the history
6d5e002 test: isolated environments for integration testing (Jose Celano)
2211871 refactor: extract app from main (Jose Celano)
89e544e feat: server port assigned by OS with port 0 (Jose Celano)

Pull request description:

  Changes needed to run tests with independent app instances running on different ports.

ACKs for top commit:
  josecelano:
    ACK 6d5e002

Tree-SHA512: 3f3731cc7b27042198f7dfeec6c30f7374481005bd4eeb70a45e1405576641234f7dc3b96bf8811b220867ad6e7b6292049be5c6e7807b1803925297a98ca3ba
  • Loading branch information
josecelano committed Apr 29, 2023
2 parents e78a5bf + 6d5e002 commit d0015d5
Show file tree
Hide file tree
Showing 16 changed files with 415 additions and 121 deletions.
2 changes: 2 additions & 0 deletions project-words.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
actix
addrs
AUTOINCREMENT
bencode
bencoded
Expand Down Expand Up @@ -31,6 +32,7 @@ NCCA
nilm
nocapture
Oberhachingerstr
oneshot
ppassword
reqwest
Roadmap
Expand Down
107 changes: 107 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
use std::net::SocketAddr;
use std::sync::Arc;

use actix_cors::Cors;
use actix_web::dev::Server;
use actix_web::{middleware, web, App, HttpServer};
use log::info;

use crate::auth::AuthorizationService;
use crate::bootstrap::logging;
use crate::common::AppData;
use crate::config::Configuration;
use crate::databases::database::connect_database;
use crate::mailer::MailerService;
use crate::routes;
use crate::tracker::TrackerService;

pub struct Running {
pub api_server: Server,
pub socket_address: SocketAddr,
pub tracker_data_importer_handle: tokio::task::JoinHandle<()>,
}

pub async fn run(configuration: Configuration) -> Running {
logging::setup();

let cfg = Arc::new(configuration);

// Get configuration settings needed to build the app dependencies and
// services: main API server and tracker torrents importer.

let settings = cfg.settings.read().await;

let database_connect_url = settings.database.connect_url.clone();
let database_torrent_info_update_interval = settings.database.torrent_info_update_interval;
let net_port = settings.net.port;

// IMPORTANT: drop settings before starting server to avoid read locks that
// leads to requests hanging.
drop(settings);

// Build app dependencies

let database = Arc::new(connect_database(&database_connect_url).await.expect("Database error."));
let auth = Arc::new(AuthorizationService::new(cfg.clone(), database.clone()));
let tracker_service = Arc::new(TrackerService::new(cfg.clone(), database.clone()));
let mailer_service = Arc::new(MailerService::new(cfg.clone()).await);

// Build app container

let app_data = Arc::new(AppData::new(
cfg.clone(),
database.clone(),
auth.clone(),
tracker_service.clone(),
mailer_service,
));

// Start repeating task to import tracker torrent data and updating
// seeders and leechers info.

let weak_tracker_service = Arc::downgrade(&tracker_service);

let tracker_data_importer_handle = tokio::spawn(async move {
let interval = std::time::Duration::from_secs(database_torrent_info_update_interval);
let mut interval = tokio::time::interval(interval);
interval.tick().await; // first tick is immediate...
loop {
interval.tick().await;
if let Some(tracker) = weak_tracker_service.upgrade() {
let _ = tracker.update_torrents().await;
} else {
break;
}
}
});

// Start main API server

// todo: get IP from settings
let ip = "0.0.0.0".to_string();

let server = HttpServer::new(move || {
App::new()
.wrap(Cors::permissive())
.app_data(web::Data::new(app_data.clone()))
.wrap(middleware::Logger::default())
.configure(routes::init_routes)
})
.bind((ip, net_port))
.expect("can't bind server to socket address");

let socket_address = server.addrs()[0];

let running_server = server.run();

let starting_message = format!("Listening on http://{}", socket_address);
info!("{}", starting_message);
// Logging could be disabled or redirected to file. So print to stdout too.
println!("{}", starting_message);

Running {
api_server: running_server,
socket_address,
tracker_data_importer_handle,
}
}
92 changes: 5 additions & 87 deletions src/bin/main.rs
Original file line number Diff line number Diff line change
@@ -1,93 +1,11 @@
use std::env;
use std::sync::Arc;

use actix_cors::Cors;
use actix_web::{middleware, web, App, HttpServer};
use torrust_index_backend::auth::AuthorizationService;
use torrust_index_backend::bootstrap::logging;
use torrust_index_backend::common::AppData;
use torrust_index_backend::config::{Configuration, CONFIG_ENV_VAR_NAME, CONFIG_PATH};
use torrust_index_backend::databases::database::connect_database;
use torrust_index_backend::mailer::MailerService;
use torrust_index_backend::routes;
use torrust_index_backend::tracker::TrackerService;
use torrust_index_backend::app;
use torrust_index_backend::bootstrap::config::init_configuration;

#[actix_web::main]
async fn main() -> std::io::Result<()> {
async fn main() -> Result<(), std::io::Error> {
let configuration = init_configuration().await;

logging::setup();

let cfg = Arc::new(configuration);

let settings = cfg.settings.read().await;

let database = Arc::new(
connect_database(&settings.database.connect_url)
.await
.expect("Database error."),
);

let auth = Arc::new(AuthorizationService::new(cfg.clone(), database.clone()));
let tracker_service = Arc::new(TrackerService::new(cfg.clone(), database.clone()));
let mailer_service = Arc::new(MailerService::new(cfg.clone()).await);
let app_data = Arc::new(AppData::new(
cfg.clone(),
database.clone(),
auth.clone(),
tracker_service.clone(),
mailer_service.clone(),
));

let interval = settings.database.torrent_info_update_interval;
let weak_tracker_service = Arc::downgrade(&tracker_service);

// repeating task, update all seeders and leechers info
tokio::spawn(async move {
let interval = std::time::Duration::from_secs(interval);
let mut interval = tokio::time::interval(interval);
interval.tick().await; // first tick is immediate...
loop {
interval.tick().await;
if let Some(tracker) = weak_tracker_service.upgrade() {
let _ = tracker.update_torrents().await;
} else {
break;
}
}
});

let port = settings.net.port;

drop(settings);

println!("Listening on http://0.0.0.0:{}", port);

HttpServer::new(move || {
App::new()
.wrap(Cors::permissive())
.app_data(web::Data::new(app_data.clone()))
.wrap(middleware::Logger::default())
.configure(routes::init_routes)
})
.bind(("0.0.0.0", port))?
.run()
.await
}

async fn init_configuration() -> Configuration {
if env::var(CONFIG_ENV_VAR_NAME).is_ok() {
println!("Loading configuration from env var `{}`", CONFIG_ENV_VAR_NAME);

Configuration::load_from_env_var(CONFIG_ENV_VAR_NAME).unwrap()
} else {
println!("Loading configuration from config file `{}`", CONFIG_PATH);
let app = app::run(configuration).await;

match Configuration::load_from_file().await {
Ok(config) => config,
Err(error) => {
panic!("{}", error)
}
}
}
app.api_server.await
}
28 changes: 28 additions & 0 deletions src/bootstrap/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
use std::env;

pub const CONFIG_PATH: &str = "./config.toml";
pub const CONFIG_ENV_VAR_NAME: &str = "TORRUST_IDX_BACK_CONFIG";

use crate::config::Configuration;

/// Initialize configuration from file or env var.
///
/// # Panics
///
/// Will panic if configuration is not found or cannot be parsed
pub async fn init_configuration() -> Configuration {
if env::var(CONFIG_ENV_VAR_NAME).is_ok() {
println!("Loading configuration from env var `{}`", CONFIG_ENV_VAR_NAME);

Configuration::load_from_env_var(CONFIG_ENV_VAR_NAME).unwrap()
} else {
println!("Loading configuration from config file `{}`", CONFIG_PATH);

match Configuration::load_from_file(CONFIG_PATH).await {
Ok(config) => config,
Err(error) => {
panic!("{}", error)
}
}
}
}
1 change: 1 addition & 0 deletions src/bootstrap/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pub mod config;
pub mod logging;
48 changes: 26 additions & 22 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ use log::warn;
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;

pub const CONFIG_PATH: &str = "./config.toml";
pub const CONFIG_ENV_VAR_NAME: &str = "TORRUST_IDX_BACK_CONFIG";

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Website {
pub name: String,
Expand All @@ -32,6 +29,9 @@ pub struct Tracker {
pub token_valid_seconds: u64,
}

/// Port 0 means that the OS will choose a random free port.
pub const FREE_PORT: u16 = 0;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Network {
pub port: u16,
Expand Down Expand Up @@ -80,14 +80,9 @@ pub struct TorrustConfig {
pub mail: Mail,
}

#[derive(Debug)]
pub struct Configuration {
pub settings: RwLock<TorrustConfig>,
}

impl Configuration {
pub fn default() -> Configuration {
let torrust_config = TorrustConfig {
impl TorrustConfig {
pub fn default() -> Self {
Self {
website: Website {
name: "Torrust".to_string(),
},
Expand All @@ -96,7 +91,7 @@ impl Configuration {
mode: TrackerMode::Public,
api_url: "http://localhost:1212".to_string(),
token: "MyAccessToken".to_string(),
token_valid_seconds: 7257600,
token_valid_seconds: 7_257_600,
},
net: Network {
port: 3000,
Expand All @@ -121,27 +116,36 @@ impl Configuration {
server: "".to_string(),
port: 25,
},
};
}
}
}

#[derive(Debug)]
pub struct Configuration {
pub settings: RwLock<TorrustConfig>,
}

impl Configuration {
pub fn default() -> Configuration {
Configuration {
settings: RwLock::new(torrust_config),
settings: RwLock::new(TorrustConfig::default()),
}
}

/// Loads the configuration from the configuration file.
pub async fn load_from_file() -> Result<Configuration, ConfigError> {
pub async fn load_from_file(config_path: &str) -> Result<Configuration, ConfigError> {
let config_builder = Config::builder();

#[allow(unused_assignments)]
let mut config = Config::default();

if Path::new(CONFIG_PATH).exists() {
config = config_builder.add_source(File::with_name(CONFIG_PATH)).build()?;
if Path::new(config_path).exists() {
config = config_builder.add_source(File::with_name(config_path)).build()?;
} else {
warn!("No config file found.");
warn!("Creating config file..");
let config = Configuration::default();
let _ = config.save_to_file().await;
let _ = config.save_to_file(config_path).await;
return Err(ConfigError::Message(
"Please edit the config.TOML in the root folder and restart the tracker.".to_string(),
));
Expand Down Expand Up @@ -183,24 +187,24 @@ impl Configuration {
}
}

pub async fn save_to_file(&self) -> Result<(), ()> {
pub async fn save_to_file(&self, config_path: &str) -> Result<(), ()> {
let settings = self.settings.read().await;

let toml_string = toml::to_string(&*settings).expect("Could not encode TOML value");

drop(settings);

fs::write(CONFIG_PATH, toml_string).expect("Could not write to file!");
fs::write(config_path, toml_string).expect("Could not write to file!");
Ok(())
}

pub async fn update_settings(&self, new_settings: TorrustConfig) -> Result<(), ()> {
pub async fn update_settings(&self, new_settings: TorrustConfig, config_path: &str) -> Result<(), ()> {
let mut settings = self.settings.write().await;
*settings = new_settings;

drop(settings);

let _ = self.save_to_file().await;
let _ = self.save_to_file(config_path).await;

Ok(())
}
Expand Down
14 changes: 7 additions & 7 deletions src/console/commands/import_tracker_statistics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ use std::sync::Arc;
use derive_more::{Display, Error};
use text_colorizer::*;

use crate::config::Configuration;
use crate::bootstrap::config::init_configuration;
use crate::bootstrap::logging;
use crate::databases::database::connect_database;
use crate::tracker::TrackerService;

Expand Down Expand Up @@ -57,12 +58,11 @@ pub async fn run_importer() {
pub async fn import(_args: &Arguments) {
println!("Importing statistics from linked tracker ...");

let cfg = match Configuration::load_from_file().await {
Ok(config) => Arc::new(config),
Err(error) => {
panic!("{}", error)
}
};
let configuration = init_configuration().await;

logging::setup();

let cfg = Arc::new(configuration);

let settings = cfg.settings.read().await;

Expand Down
Loading

0 comments on commit d0015d5

Please sign in to comment.