wakezilla 0.2.12

A Wake-on-LAN proxy server written in Rust
use std::time::Duration;

use wakezilla::shutdown_auth::{
    generate_key, sign_request_at, ReplayGuard, SignedRequestHeaders, VerifyError,
};

const NOW: u64 = 1_700_000_000;

#[test]
fn valid_signature_is_accepted_once() {
    let key = generate_key();
    let headers = sign_request_at(&key, "POST", "/machines/turn-off", NOW).unwrap();
    let mut guard = ReplayGuard::new(Duration::from_secs(120), 16);

    guard
        .verify(&key, "POST", "/machines/turn-off", &headers, NOW)
        .expect("valid signature should be accepted");
}

#[test]
fn signature_from_another_key_is_rejected() {
    let expected_key = generate_key();
    let other_key = generate_key();
    let headers = sign_request_at(&other_key, "POST", "/machines/turn-off", NOW).unwrap();
    let mut guard = ReplayGuard::new(Duration::from_secs(120), 16);

    let error = guard
        .verify(&expected_key, "POST", "/machines/turn-off", &headers, NOW)
        .unwrap_err();

    assert_eq!(error, VerifyError::InvalidSignature);
}

#[test]
fn expired_timestamp_is_rejected() {
    let key = generate_key();
    let headers = sign_request_at(&key, "POST", "/machines/turn-off", NOW - 61).unwrap();
    let mut guard = ReplayGuard::new(Duration::from_secs(120), 16);

    let error = guard
        .verify(&key, "POST", "/machines/turn-off", &headers, NOW)
        .unwrap_err();

    assert_eq!(error, VerifyError::TimestampOutsideWindow);
}

#[test]
fn replayed_nonce_is_rejected() {
    let key = generate_key();
    let headers = sign_request_at(&key, "POST", "/machines/turn-off", NOW).unwrap();
    let mut guard = ReplayGuard::new(Duration::from_secs(120), 16);

    guard
        .verify(&key, "POST", "/machines/turn-off", &headers, NOW)
        .unwrap();
    let error = guard
        .verify(&key, "POST", "/machines/turn-off", &headers, NOW)
        .unwrap_err();

    assert_eq!(error, VerifyError::Replay);
}

#[test]
fn malformed_key_is_rejected_before_signing() {
    let error = sign_request_at("YWJj", "GET", "/health/secure", NOW).unwrap_err();
    assert!(error.to_string().contains("32 bytes"));
}

#[test]
fn generated_key_is_url_safe_and_256_bits() {
    let key = generate_key();
    assert_eq!(key.len(), 43);
    assert!(key
        .bytes()
        .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_')));
}

#[test]
fn headers_are_plain_serializable_values() {
    let headers = SignedRequestHeaders {
        timestamp: "1700000000".into(),
        nonce: "nonce".into(),
        signature: "signature".into(),
    };
    assert_eq!(headers.timestamp, "1700000000");
}