coil-runtime 0.1.1

HTTP runtime and request handling for the Coil framework.
Documentation
use super::*;
use crate::RuntimeBuilder;
use coil_auth::DefaultAuthModelPackage;
use coil_config::{PlatformConfig, StorageClass};
use coil_storage::StoragePolicyOverride;
use coil_storage::execution::{ObjectStoreClientConfig, StorageDeliveryLocation};
use std::fs;
use std::path::PathBuf;

#[test]
fn publication_gate_reports_missing_conditions() {
    let gate = ManagedAssetPublicationGate {
        can_publish: true,
        can_replace: false,
        can_manage_storage: true,
        public_delivery_enabled: false,
    };

    assert!(!gate.can_publish_publicly());
    let error = gate
        .ensure_public_delivery_allowed("asset-hero")
        .unwrap_err();
    assert_eq!(
        error,
        RuntimeStorageError::PublicationAuthorizationDenied {
            asset_id: "asset-hero".to_string(),
            reason: "asset.replace, published public delivery state".to_string(),
        }
    );
}

fn test_config() -> PlatformConfig {
    PlatformConfig::from_toml_str(
        r#"
[app]
name = "coil-runtime-storage-tests"
environment = "development"

[server]
bind = "127.0.0.1:3000"
trusted_proxies = []

[http.session]
store = "redis"
idle_timeout_secs = 3600
absolute_timeout_secs = 86400

[http.session_cookie]
name = "coil_session"
path = "/"
same_site = "lax"
secure = true
http_only = true

[http.flash_cookie]
name = "coil_flash"
path = "/"
same_site = "lax"
secure = true
http_only = true

[http.csrf]
enabled = true
field_name = "_csrf"
header_name = "x-csrf-token"

[tls]
mode = "external"

[storage]
default_class = "public_upload"
single_node_escape_hatch = "explicit_single_node"
object_store = "s3"
local_root = "/tmp/coil-runtime-storage-tests"
deployment = "single_node"

[cache]
l1 = "moka"
l2 = "redis"

[i18n]
default_locale = "en"
supported_locales = ["en"]
fallback_locale = "en"
localized_routes = false

[seo]
canonical_host = "example.test"
emit_json_ld = false

[auth]
package = "coil-default-auth"
explain_api = false
tenant_id = 1

[modules]
enabled = ["cms"]

[wasm]
directory = "/tmp/coil-runtime-storage-tests"
default_time_limit_ms = 50
allow_network = false

[jobs]
backend = "redis"

[observability]
metrics = false
tracing = false

[assets]
publish_manifest = false
cdn_base_url = "https://cdn.example.test"
"#,
    )
    .unwrap()
}

fn unique_test_root() -> PathBuf {
    let nanos = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default()
        .as_nanos();
    std::env::temp_dir().join(format!("coil-runtime-storage-tests-{nanos}"))
}

#[test]
fn storage_host_plans_public_delivery_and_executes_local_escape_hatch_storage() {
    let root = unique_test_root();
    let _ = fs::remove_dir_all(&root);
    fs::create_dir_all(&root).unwrap();

    let mut config = test_config();
    config.storage.local_root = root.display().to_string();
    config.wasm.directory = root.display().to_string();
    let plan = RuntimeBuilder::new(config, DefaultAuthModelPackage::default())
        .build()
        .unwrap();
    let host = plan.storage_host();

    let object_plan = host
        .plan_write(
            coil_storage::StoragePlanRequest::new("uploads/catalog/item.jpg")
                .with_storage_class(StorageClass::PublicUpload),
        )
        .unwrap();
    assert!(matches!(
        host.delivery_location(&object_plan).unwrap(),
        StorageDeliveryLocation::PublicCdn { .. }
    ));

    let local_plan = host
        .plan_single_node_escape_hatch_write(
            coil_storage::StoragePlanRequest::new("secure/reports/march.csv")
                .with_storage_class(StorageClass::PrivateShared)
                .with_override(StoragePolicyOverride::force_single_node_escape_hatch()),
        )
        .unwrap();
    let local_write = host.execute_write(&local_plan, b"local-bytes").unwrap();
    assert_eq!(local_write.path, root.join("secure/reports/march.csv"));
    assert_eq!(
        host.execute_read(&local_plan).unwrap().bytes,
        b"local-bytes"
    );
    assert_eq!(
        host.delivery_location(&local_plan).unwrap(),
        StorageDeliveryLocation::LocalPath {
            path: root.join("secure/reports/march.csv"),
        }
    );

    let _ = fs::remove_dir_all(root);
}

#[test]
fn storage_host_generates_signed_urls_for_private_object_store_assets() {
    let plan = RuntimeBuilder::new(test_config(), DefaultAuthModelPackage::default())
        .build()
        .unwrap();
    let host = plan.storage_host_with_object_store(Some(
        ObjectStoreClientConfig::new("runtime", "us-east-1")
            .unwrap()
            .with_endpoint_url("https://storage.example.test")
            .unwrap()
            .with_static_credentials("runtime-access", "runtime-secret")
            .unwrap()
            .with_signed_url_ttl_secs(900),
    ));

    let private_plan = host
        .plan_write(
            coil_storage::StoragePlanRequest::new("secure/reports/april.csv")
                .with_storage_class(StorageClass::PrivateShared),
        )
        .unwrap();

    match host.delivery_location(&private_plan).unwrap() {
        StorageDeliveryLocation::SignedObject {
            object_key,
            signed_url,
            expires_at_unix_seconds,
        } => {
            assert_eq!(object_key, "secure/reports/april.csv");
            assert!(signed_url.contains("X-Amz-Algorithm=AWS4-HMAC-SHA256"));
            assert!(expires_at_unix_seconds > 0);
        }
        other => panic!("expected signed delivery, got {other:?}"),
    }
}