Files
upstream-mirror da934b693c
Some checks failed
Build / ubuntu-latest (push) Failing after 5s
Build / Android (push) Failing after 2s
Build / Code coverage (push) Failing after 2s
Lint / format (push) Failing after 3s
Lint / clippy (push) Failing after 3s
Build / macOS-latest (push) Has been cancelled
Build / windows-latest (push) Has been cancelled
snapshot of https://github.com/ActivityWatch/aw-server-rust.git (master, 2026-09-16; blobs > 131072B omitted)
2026-09-16 00:19:17 +05:00

304 lines
10 KiB
Rust

extern crate aw_client_rust;
extern crate aw_datastore;
extern crate aw_server;
extern crate chrono;
extern crate rocket;
extern crate serde_json;
extern crate tokio_test;
#[cfg(test)]
mod test {
use aw_client_rust::blocking::AwClient;
use aw_client_rust::Event;
use chrono::{DateTime, Duration, Utc};
use serde_json::Map;
use std::cell::RefCell;
use std::fs;
use std::net::TcpListener;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use std::thread;
use std::time::{SystemTime, UNIX_EPOCH};
use tokio_test::block_on;
// Config-reading helpers — only needed in tests; production code passes API keys explicitly.
#[derive(serde::Deserialize, Default)]
struct LocalAuthConfig {
#[serde(default)]
api_key: Option<String>,
}
#[derive(serde::Deserialize, Default)]
struct LocalServerConfig {
#[serde(default)]
port: Option<u16>,
#[serde(default)]
auth: LocalAuthConfig,
}
#[derive(Clone, Copy)]
struct ConfigCandidate {
filename: &'static str,
default_port: u16,
}
fn get_server_config_dir() -> Option<PathBuf> {
Some(
dirs::config_dir()?
.join("activitywatch")
.join("aw-server-rust"),
)
}
fn load_local_api_key(host: &str, port: u16) -> Option<String> {
if host != "127.0.0.1" && host != "localhost" {
return None;
}
let config_dir = get_server_config_dir()?;
let candidates = [
ConfigCandidate {
filename: "config.toml",
default_port: 5600,
},
ConfigCandidate {
filename: "config-testing.toml",
default_port: 5666,
},
];
for candidate in candidates {
let path = config_dir.join(candidate.filename);
let content = match fs::read_to_string(path) {
Ok(content) => content,
Err(_) => continue,
};
let config: LocalServerConfig = match toml::from_str(&content) {
Ok(config) => config,
Err(_) => continue,
};
let configured_port = config.port.unwrap_or(candidate.default_port);
if configured_port == port {
return config.auth.api_key.filter(|k| !k.is_empty());
}
}
None
}
// A random port, but still not guaranteed to not be bound
// FIXME: Bind to a port that is free for certain and use that for the client instead
static PORT: u16 = 41293;
static ENV_LOCK: Mutex<()> = Mutex::new(());
// Keep the listener alive until the server binds — prevents TOCTOU race in reserve_port
thread_local! {
static RESERVED_PORT: RefCell<Option<TcpListener>> = const { RefCell::new(None) };
}
fn wait_for_server(timeout_s: u32, client: &AwClient) {
for i in 0.. {
match client.get_info() {
Ok(_) => break,
Err(err) => {
if i == timeout_s - 1 {
panic!("Timed out starting aw-server after {timeout_s}s: {err:?}");
}
}
}
use std::time;
let duration = time::Duration::from_secs(1);
thread::sleep(duration);
}
}
fn setup_testserver(port: u16, api_key: Option<&str>) -> rocket::Shutdown {
use aw_server::endpoints::AssetResolver;
use aw_server::endpoints::ServerState;
let state = ServerState {
datastore: aw_datastore::Datastore::new_in_memory(false),
asset_resolver: AssetResolver::new(None),
device_id: "test_id".to_string(),
};
let aw_config = aw_server::config::AWConfig {
port,
testing: true,
auth: aw_server::config::AWAuthConfig {
api_key: api_key.map(str::to_owned),
},
..Default::default()
};
let server = aw_server::endpoints::build_rocket(state, aw_config);
let server = block_on(server.ignite()).unwrap();
let shutdown_handler = server.shutdown();
thread::spawn(move || {
let _ = block_on(server.launch()).unwrap();
});
shutdown_handler
}
fn reserve_port() -> u16 {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
// Keep the listener alive until the server binds — prevents TOCTOU race
RESERVED_PORT.with(|cell| *cell.borrow_mut() = Some(listener));
port
}
fn write_server_config(port: u16, api_key: Option<&str>) -> PathBuf {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let config_home = std::env::temp_dir().join(format!(
"aw-client-rust-config-{}-{}",
std::process::id(),
unique
));
let config_dir = config_home.join("activitywatch").join("aw-server-rust");
fs::create_dir_all(&config_dir).unwrap();
let mut content = format!("port = {port}\n");
if let Some(api_key) = api_key {
content.push_str("\n[auth]\n");
content.push_str(&format!("api_key = \"{api_key}\"\n"));
}
fs::write(config_dir.join("config.toml"), content).unwrap();
config_home
}
fn with_config_home<T>(config_home: &Path, f: impl FnOnce() -> T) -> T {
let _guard = ENV_LOCK.lock().unwrap();
let old_value = std::env::var_os("XDG_CONFIG_HOME");
std::env::set_var("XDG_CONFIG_HOME", config_home);
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
if let Some(old_value) = old_value {
std::env::set_var("XDG_CONFIG_HOME", old_value);
} else {
std::env::remove_var("XDG_CONFIG_HOME");
}
let _ = fs::remove_dir_all(config_home);
result.unwrap()
}
#[test]
fn test_full() {
let clientname = "aw-client-rust-test";
// Hold ENV_LOCK during client creation to prevent parallel-test interference
// with test_reads_api_key_from_matching_server_config (which holds the lock
// via with_config_home for the entire client+server lifetime).
let client: AwClient = {
let _guard = ENV_LOCK.lock().unwrap();
AwClient::new("127.0.0.1", PORT, clientname).expect("Client creation failed")
};
let shutdown_handler = setup_testserver(PORT, None);
wait_for_server(20, &client);
let info = client.get_info().unwrap();
assert!(info.testing);
let bucketname = format!("aw-client-rust-test_{}", client.hostname);
let buckettype = "test-type";
client
.create_bucket_simple(&bucketname, buckettype)
.unwrap();
let bucket = client.get_bucket(&bucketname).unwrap();
assert!(bucket.id == bucketname);
println!("{}", bucket.id);
let buckets = client.get_buckets().unwrap();
println!("Buckets: {buckets:?}");
let mut event = Event {
id: None,
timestamp: DateTime::from_utc(
DateTime::parse_from_rfc3339("2017-12-30T01:00:00+00:00")
.unwrap()
.naive_utc(),
Utc,
),
duration: Duration::seconds(0),
data: Map::new(),
};
println!("{event:?}");
client.insert_event(&bucketname, &event).unwrap();
// Ugly way to create a UTC from timestamp, see https://github.com/chronotope/chrono/issues/263
event.timestamp = DateTime::from_utc(
DateTime::parse_from_rfc3339("2017-12-30T01:00:01+00:00")
.unwrap()
.naive_utc(),
Utc,
);
client.heartbeat(&bucketname, &event, 10.0).unwrap();
let events = client.get_events(&bucketname, None, None, None).unwrap();
println!("Events: {events:?}");
assert!(events[0].duration == Duration::seconds(1));
// Query
let query = format!(
"events = query_bucket(\"{}\");
RETURN = events;",
bucket.id
);
let start: DateTime<Utc> = DateTime::parse_from_rfc3339("1996-12-19T00:00:00-08:00")
.unwrap()
.into();
let end: DateTime<Utc> = DateTime::parse_from_rfc3339("2020-12-19T00:00:00-08:00")
.unwrap()
.into();
let timeperiods = (start, end);
let query_result = client.query(&query, vec![timeperiods]).unwrap();
println!("Query result: {query_result:?}");
client
.delete_event(&bucketname, events[0].id.unwrap())
.unwrap();
let count = client.get_event_count(&bucketname).unwrap();
assert_eq!(count, 0);
client.delete_bucket(&bucketname).unwrap();
shutdown_handler.notify();
}
// XDG_CONFIG_HOME is only respected by dirs::config_dir() on Linux.
// On macOS it returns $HOME/Library/Application Support (ignoring XDG_CONFIG_HOME),
// so this test would fail — gate it on Linux only.
#[test]
#[cfg(target_os = "linux")]
fn test_reads_api_key_from_matching_server_config() {
let clientname = "aw-client-rust-auth-test";
let port = reserve_port();
let config_home = write_server_config(port, Some("secret123"));
with_config_home(&config_home, || {
let api_key = load_local_api_key("127.0.0.1", port);
let client: AwClient =
AwClient::new_with_api_key("127.0.0.1", port, clientname, api_key)
.expect("Client creation failed");
// Drop the reserved listener before Rocket tries to bind the same port.
RESERVED_PORT.with(|cell| *cell.borrow_mut() = None);
let shutdown_handler = setup_testserver(port, Some("secret123"));
wait_for_server(20, &client);
let bucketname = format!("aw-client-rust-auth-test_{}", client.hostname);
client
.create_bucket_simple(&bucketname, "test-type")
.unwrap();
let bucket = client.get_bucket(&bucketname).unwrap();
assert_eq!(bucket.id, bucketname);
shutdown_handler.notify();
});
}
}