shardline-server 1.0.0

HTTP server boundary, runtime, and operator workflows for Shardline.
Documentation
use std::{fmt::Write, num::NonZeroUsize};

use axum::http::{
    HeaderMap,
    header::{AUTHORIZATION, HeaderValue},
};
use shardline_index::ProviderRepositoryState;
use shardline_protocol::RepositoryProvider;

use super::{
    MAX_BATCH_RECONSTRUCTION_FILE_IDS, MAX_BATCH_RECONSTRUCTION_QUERY_BYTES,
    MAX_PROVIDER_BASIC_AUTH_HEADER_BYTES, MAX_PROVIDER_NAME_BYTES, MAX_PROVIDER_SUBJECT_BYTES,
    MAX_PROVIDER_WEBHOOK_BODY_BYTES, bounded_api_body_limit, extract_provider_subject,
    latest_lifecycle_signal_at, parse_batch_reconstruction_query,
    reconciled_provider_repository_state, validate_provider_name_path,
};
use crate::ServerError;

#[test]
fn provider_subject_extraction_rejects_oversized_query_subject() {
    let oversized = "s".repeat(MAX_PROVIDER_SUBJECT_BYTES + 1);
    let result = extract_provider_subject(&HeaderMap::new(), Some(&oversized));

    assert!(matches!(
        result,
        Err(ServerError::InvalidProviderTokenRequest)
    ));
}

#[test]
fn provider_subject_extraction_rejects_oversized_basic_auth_header_before_decode() {
    let oversized = "a".repeat(MAX_PROVIDER_BASIC_AUTH_HEADER_BYTES + 1);
    let header_value = HeaderValue::from_str(&format!("Basic {oversized}"));
    assert!(header_value.is_ok());
    let Ok(header_value) = header_value else {
        return;
    };
    let mut headers = HeaderMap::new();
    headers.insert(AUTHORIZATION, header_value);

    let result = extract_provider_subject(&headers, None);

    assert!(matches!(
        result,
        Err(ServerError::InvalidAuthorizationHeader)
    ));
}

#[test]
fn provider_api_body_limit_uses_stricter_configured_or_endpoint_ceiling() {
    let tighter = NonZeroUsize::new(32).unwrap_or(NonZeroUsize::MIN);
    let looser =
        NonZeroUsize::new(MAX_PROVIDER_WEBHOOK_BODY_BYTES + 1).unwrap_or(NonZeroUsize::MIN);

    assert_eq!(
        bounded_api_body_limit(tighter, MAX_PROVIDER_WEBHOOK_BODY_BYTES),
        tighter.get()
    );
    assert_eq!(
        bounded_api_body_limit(looser, MAX_PROVIDER_WEBHOOK_BODY_BYTES),
        MAX_PROVIDER_WEBHOOK_BODY_BYTES
    );
}

#[test]
fn provider_repository_reconciliation_marks_pending_lifecycle_signals() {
    let state = ProviderRepositoryState::new(
        RepositoryProvider::GitHub,
        "team".to_owned(),
        "assets".to_owned(),
        Some(10),
        Some(12),
        Some("refs/heads/main".to_owned()),
    )
    .with_reconciliation(Some(11), None, None);

    assert_eq!(latest_lifecycle_signal_at(&state), Some(12));
    let reconciled = reconciled_provider_repository_state(&state, 20);

    assert_eq!(
        reconciled.last_cache_invalidated_at_unix_seconds(),
        Some(20)
    );
    assert_eq!(
        reconciled.last_authorization_rechecked_at_unix_seconds(),
        Some(20)
    );
    assert_eq!(reconciled.last_drift_checked_at_unix_seconds(), Some(20));
}

#[test]
fn batch_reconstruction_parser_deduplicates_file_ids() {
    let parsed = parse_batch_reconstruction_query(
        "file_id=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&file_id=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&ignored=value",
    );

    assert!(parsed.is_ok());
    let Ok(parsed) = parsed else {
        return;
    };
    assert_eq!(
        parsed,
        vec!["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_owned()]
    );
}

#[test]
fn batch_reconstruction_parser_rejects_excessive_file_ids() {
    let mut query = String::new();
    for index in 0..=MAX_BATCH_RECONSTRUCTION_FILE_IDS {
        if !query.is_empty() {
            query.push('&');
        }
        query.push_str("file_id=");
        let written = write!(&mut query, "{index:064x}");
        assert!(written.is_ok());
    }

    let parsed = parse_batch_reconstruction_query(&query);

    assert!(matches!(
        parsed,
        Err(ServerError::TooManyBatchReconstructionFileIds)
    ));
}

#[test]
fn batch_reconstruction_parser_rejects_oversized_query_before_scanning() {
    let mut query = String::from("ignored=");
    query.push_str(&"a".repeat(MAX_BATCH_RECONSTRUCTION_QUERY_BYTES + 1));

    let parsed = parse_batch_reconstruction_query(&query);

    assert!(matches!(parsed, Err(ServerError::RequestQueryTooLarge)));
}

#[test]
fn provider_path_name_rejects_empty_or_oversized_values() {
    let empty = validate_provider_name_path("");
    let oversized = validate_provider_name_path(&"p".repeat(MAX_PROVIDER_NAME_BYTES + 1));
    let valid = validate_provider_name_path("github");

    assert!(matches!(
        empty,
        Err(ServerError::InvalidProviderTokenRequest)
    ));
    assert!(matches!(
        oversized,
        Err(ServerError::InvalidProviderTokenRequest)
    ));
    assert!(valid.is_ok());
}