Vecna 4 тижнів тому
батько
коміт
72103f95af
4 змінених файлів з 209 додано та 2 видалено
  1. 3 1
      Cargo.toml
  2. 52 1
      src/lib.rs
  3. 1 0
      src/tests.rs
  4. 153 0
      src/tests/simulated_time.rs

+ 3 - 1
Cargo.toml

@@ -13,6 +13,7 @@ chrono = "0.4"
 clap = { version = "4.4.14", features = ["derive"] }
 curve25519-dalek = { version = "4", default-features = false, features = ["serde", "rand_core", "digest"] }
 ed25519-dalek = { version = "2", features = ["serde", "rand_core"] }
+faketime = { version = "0.2", optional = true }
 futures = "0.3.30"
 hkdf = "0.12"
 http = "1"
@@ -41,6 +42,7 @@ x25519-dalek = { version = "2", features = ["serde", "static_secrets"] }
 
 [dev-dependencies]
 base64 = "0.21.7"
+faketime = "0.2"
 
 [features]
-simulation = ["lox_cli"]
+simulation = ["faketime", "lox_cli"]

+ 52 - 1
src/lib.rs

@@ -12,6 +12,13 @@ use std::{
 };
 use x25519_dalek::{PublicKey, StaticSecret};
 
+#[cfg(any(feature = "simulation", test))]
+use {
+    chrono::{DateTime, Utc},
+    julianday::JulianDay,
+    std::{path::Path, time::UNIX_EPOCH},
+};
+
 pub mod analysis;
 pub mod bridge_verification_info;
 pub mod crypto;
@@ -46,8 +53,20 @@ lazy_static! {
 /// We will accept reports up to this many days old.
 pub const MAX_BACKDATE: u32 = 3;
 
-/// Get Julian date
+#[cfg(any(feature = "simulation", test))]
+const FAKETIME_FILE: &str = "/tmp/troll-patrol-faketime";
+
+/// Get real or simulated Julian date
 pub fn get_date() -> u32 {
+    // If this is a simulation, get the simulated date
+    #[cfg(any(feature = "simulation", test))]
+    return get_simulated_date();
+
+    // If we're not running a simulation, return today's date
+    get_real_date()
+}
+
+fn get_real_date() -> u32 {
     time::OffsetDateTime::now_utc()
         .date()
         .to_julian_day()
@@ -55,6 +74,38 @@ pub fn get_date() -> u32 {
         .unwrap()
 }
 
+#[cfg(any(feature = "simulation", test))]
+fn get_simulated_date() -> u32 {
+    faketime::enable(Path::new(FAKETIME_FILE));
+    JulianDay::from(DateTime::<Utc>::from(UNIX_EPOCH + faketime::unix_time()).date_naive())
+        .inner()
+        .try_into()
+        .unwrap()
+}
+
+#[cfg(any(feature = "simulation", test))]
+pub fn set_simulated_date(date: u32) {
+    faketime::enable(Path::new(FAKETIME_FILE));
+    let unix_date_ms = DateTime::<Utc>::from_naive_utc_and_offset(
+        JulianDay::new(date.try_into().unwrap()).to_date().into(),
+        Utc,
+    )
+    .timestamp_millis();
+    //str.push_str(format!("\nbridge-stats-end {} 23:59:59 (86400 s)", date).as_str());
+    faketime::write_millis(Path::new(FAKETIME_FILE), unix_date_ms.try_into().unwrap()).unwrap();
+}
+
+#[cfg(any(feature = "simulation", test))]
+pub fn increment_simulated_date() {
+    let date = get_date();
+    set_simulated_date(date + 1);
+}
+
+#[cfg(any(feature = "simulation", test))]
+pub fn reset_simulated_date() {
+    set_simulated_date(get_real_date());
+}
+
 #[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
 pub enum BridgeDistributor {
     Lox,

+ 1 - 0
src/tests.rs

@@ -8,3 +8,4 @@ mod reports {
     mod negative_reports;
     mod positive_reports;
 }
+mod simulated_time;

+ 153 - 0
src/tests/simulated_time.rs

@@ -0,0 +1,153 @@
+use crate::*;
+
+use hyper::{
+    body,
+    header::HeaderValue,
+    service::{make_service_fn, service_fn},
+    Body, Client, Method, Request, Response, Server,
+};
+use std::{convert::Infallible, net::SocketAddr, time::Duration};
+use tokio::{spawn, time::sleep};
+
+use lox_library::bridge_table::BridgeLine;
+use rand::RngCore;
+
+// Prepare HTTP Response for successful Server Request
+fn prepare_header(response: String) -> Response<Body> {
+    let mut resp = Response::new(Body::from(response));
+    resp.headers_mut()
+        .insert("Access-Control-Allow-Origin", HeaderValue::from_static("*"));
+    resp
+}
+
+// Lets the client get or set the simulated date
+async fn date_server(req: Request<Body>) -> Result<Response<Body>, Infallible> {
+    match req.method() {
+        &Method::OPTIONS => Ok(Response::builder()
+            .header("Access-Control-Allow-Origin", HeaderValue::from_static("*"))
+            .header("Access-Control-Allow-Headers", "accept, content-type")
+            .header("Access-Control-Allow-Methods", "POST")
+            .status(200)
+            .body(Body::from("Allow POST"))
+            .unwrap()),
+        _ => match req.uri().path() {
+            "/getdate" => Ok::<_, Infallible>(prepare_header(get_date().to_string())),
+            "/setdate" => Ok::<_, Infallible>({
+                let bytes = body::to_bytes(req.into_body()).await.unwrap();
+                let date: u32 = std::str::from_utf8(&bytes).unwrap().parse().unwrap();
+                set_simulated_date(date);
+                prepare_header("OK".to_string())
+            }),
+            _ => Ok::<_, Infallible>(prepare_header("Wrong path".to_string())),
+        },
+    }
+}
+
+// Serves the function to let the client get or set the simulated date
+async fn server() {
+    let addr = SocketAddr::from(([127, 0, 0, 1], 9999));
+
+    let make_svc = make_service_fn(|_conn| async { Ok::<_, Infallible>(service_fn(date_server)) });
+
+    let server = Server::bind(&addr).serve(make_svc);
+
+    if let Err(e) = server.await {
+        eprintln!("server error: {}", e);
+    }
+}
+
+async fn get_date_from_server() -> u32 {
+    download("http://localhost:9999/getdate")
+        .await
+        .unwrap()
+        .parse()
+        .unwrap()
+}
+
+async fn set_date_on_server(date: u32) {
+    let client = Client::new();
+    let req = Request::builder()
+        .method(Method::POST)
+        .uri(
+            "http://localhost:9999/setdate"
+                .parse::<hyper::Uri>()
+                .unwrap(),
+        )
+        .body(Body::from(date.to_string()))
+        .unwrap();
+    client.request(req).await.unwrap();
+}
+
+#[tokio::test]
+async fn test_simulated_time() {
+    // Reset date in case we had a previous simulated date
+    reset_simulated_date();
+
+    // Get date
+    let date = get_date();
+
+    // Check that simulated date matches real date
+    let real_date: u32 = time::OffsetDateTime::now_utc()
+        .date()
+        .to_julian_day()
+        .try_into()
+        .unwrap();
+
+    assert_eq!(date, real_date);
+
+    // Check that incrementing simulated date works
+    increment_simulated_date();
+    assert_eq!(date + 1, get_date());
+
+    // Create dummy bridge
+    let mut rng = rand::thread_rng();
+    let mut bl = BridgeLine::default();
+    rng.fill_bytes(&mut bl.fingerprint);
+    let negative_report =
+        NegativeReport::from_bridgeline(bl, "ru".to_string(), BridgeDistributor::Lox);
+
+    // No issue
+    let negative_report = negative_report
+        .to_serializable_report()
+        .to_report()
+        .unwrap();
+
+    // Advance time so the report is no longer valid
+    set_simulated_date(get_date() + MAX_BACKDATE + 1);
+
+    // Report fails to deserialize
+    let negative_report_result = negative_report.to_serializable_report().to_report();
+    assert!(negative_report_result.is_err());
+
+    // Ensure one thread CAN influence the time for other threads
+    spawn(async move {
+        server().await;
+    });
+
+    // Give server time to start
+    sleep(Duration::new(1, 0)).await;
+
+    // Increment date
+    increment_simulated_date();
+    let date = get_date();
+
+    // Get date from server
+    let remote_date = get_date_from_server().await;
+    assert_eq!(date, remote_date);
+
+    // Increase date a lot
+    set_simulated_date(get_date() + 100);
+    let date = get_date();
+
+    // Check that date from server matches
+    let remote_date = get_date_from_server().await;
+    assert_eq!(date, remote_date);
+
+    // Have server increase date
+    let old_date = get_date();
+    let new_date = old_date + 500;
+    set_date_on_server(new_date).await;
+
+    // Check that we have the date as changed in the other thread
+    assert_eq!(get_date(), new_date);
+}