Skip to content

Testing

articwake has comprehensive unit and integration tests. This guide covers running tests locally.

Terminal window
cargo test --verbose

This runs:

  • Unit tests in each module
  • Integration tests in tests/
Terminal window
# Run tests matching a pattern
cargo test validate_mac
# Run tests in a specific module
cargo test auth::tests

Show stdout for passing tests:

Terminal window
cargo test -- --nocapture

Located within each source file using #[cfg(test)] modules:

ModuleTests
config.rsMAC validation (8 tests)
auth.rsToken generation, rate limiting, PIN verification (12 tests)
api/unlock.rsPassphrase validation (8 tests)
services/wol.rsMAC parsing, packet creation (13 tests)
services/network.rsHost status (5 tests)

Located in tests/:

FilePurpose
api_integration.rsEnd-to-end API endpoint tests
Terminal window
cargo clippy -- -D warnings

The CI pipeline treats all warnings as errors.

Terminal window
# Auto-fix what clippy can
cargo clippy --fix
Terminal window
cargo fmt -- --check
Terminal window
cargo fmt

The GitHub Actions workflow runs on every push and PR:

jobs:
test:
steps:
- cargo test --verbose
- cargo clippy -- -D warnings
- cargo fmt -- --check

All checks must pass before merging.

#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_mac_colon_separated() {
assert!(validate_mac("aa:bb:cc:dd:ee:ff").is_ok());
}
#[test]
fn test_validate_mac_invalid() {
assert!(validate_mac("invalid").is_err());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[actix_rt::test]
async fn test_async_function() {
let result = async_operation().await;
assert!(result.is_ok());
}
}
#[test]
fn test_with_temp_file() {
use tempfile::NamedTempFile;
use std::io::Write;
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "test content").unwrap();
// Use file.path() in your test
}

Tests often need a mock Config:

fn create_test_config() -> Config {
Config {
bind_host: "127.0.0.1".to_string(),
port: 8080,
homelab_mac: "aa:bb:cc:dd:ee:ff".to_string(),
homelab_ip: "192.168.1.100".to_string(),
homelab_broadcast: "255.255.255.255".to_string(),
ssh_port: 2222,
ssh_key_path: PathBuf::from("/tmp/test-key"),
pin_hash_path: PathBuf::from("/tmp/test-hash"),
}
}

Rate limit tests use the actual rate limiter:

#[test]
fn test_rate_limit_blocks_over_limit() {
let state = AppState::new(config);
let ip = "192.168.1.1".parse().unwrap();
// Exhaust the limit
for _ in 0..10 {
assert!(state.check_rate_limit(ip).is_ok());
}
// 11th attempt should fail
assert!(state.check_rate_limit(ip).is_err());
}

Some tests may timeout waiting for network:

  • Ensure you’re not testing against a real server
  • Check for hardcoded IPs that might be routable

Rate limit tests can be timing-sensitive:

  • Tests run in parallel by default
  • Use unique IPs for each test
  • Consider using cargo test -- --test-threads=1