fraiseql-functions 2.14.1

Serverless functions runtime for FraiseQL — WASM and Deno backends
Documentation
#![allow(clippy::panic)] // Reason: test code, panics acceptable
use super::*;

#[test]
fn test_parse_after_mutation_trigger() {
    let parsed = ParsedTrigger::parse("after:mutation:createUser").expect("parse");
    match parsed {
        ParsedTrigger::AfterMutation {
            entity_type,
            operation,
        } => {
            assert_eq!(entity_type, "createUser");
            assert_eq!(operation, None);
        },
        _ => panic!("Wrong trigger type"),
    }
}

#[test]
fn test_parse_before_mutation_trigger() {
    let parsed = ParsedTrigger::parse("before:mutation:validateUser").expect("parse");
    match parsed {
        ParsedTrigger::BeforeMutation { mutation_name } => {
            assert_eq!(mutation_name, "validateUser");
        },
        _ => panic!("Wrong trigger type"),
    }
}

#[test]
fn test_parse_http_trigger() {
    let parsed = ParsedTrigger::parse("http:GET:/users/:id").expect("parse");
    match parsed {
        ParsedTrigger::Http { method, path } => {
            assert_eq!(method, "GET");
            assert_eq!(path, "/users/:id");
        },
        _ => panic!("Wrong trigger type"),
    }
}

#[test]
fn test_parse_cron_trigger() {
    let parsed = ParsedTrigger::parse("cron:0 2 * * *").expect("parse");
    match parsed {
        ParsedTrigger::Cron { expression } => {
            assert_eq!(expression, "0 2 * * *");
        },
        _ => panic!("Wrong trigger type"),
    }
}

#[test]
fn test_parse_invalid_trigger() {
    let result = ParsedTrigger::parse("invalid:format:here");
    assert!(result.is_err());
}

#[test]
fn test_parse_after_ingest_triggers() {
    // Bare: matches every source.
    match ParsedTrigger::parse("after:ingest").expect("parse") {
        ParsedTrigger::AfterIngest { source } => assert_eq!(source, None),
        _ => panic!("Wrong trigger type"),
    }
    // Simple source.
    match ParsedTrigger::parse("after:ingest:email").expect("parse") {
        ParsedTrigger::AfterIngest { source } => assert_eq!(source.as_deref(), Some("email")),
        _ => panic!("Wrong trigger type"),
    }
    // Colon-bearing source (webhook:<provider>) is rejoined intact.
    match ParsedTrigger::parse("after:ingest:webhook:stripe").expect("parse") {
        ParsedTrigger::AfterIngest { source } => {
            assert_eq!(source.as_deref(), Some("webhook:stripe"));
        },
        _ => panic!("Wrong trigger type"),
    }
    assert_eq!(
        ParsedTrigger::parse("after:ingest").expect("parse").trigger_type(),
        "after:ingest"
    );
}

#[test]
fn test_registry_registers_ingest_triggers() {
    use crate::{FunctionDefinition, InboundMessage, IngestSource, RuntimeType};

    let functions = vec![
        FunctionDefinition::new("onAnyInbound", "after:ingest", RuntimeType::Deno),
        FunctionDefinition::new("onStripe", "after:ingest:webhook:stripe", RuntimeType::Deno),
        FunctionDefinition::new("onEmail", "after:ingest:email", RuntimeType::Deno),
    ];
    let registry = TriggerRegistry::load_from_definitions(&functions).expect("load registry");
    assert_eq!(registry.ingest_trigger_count(), 3);

    let stripe_msg = InboundMessage::new(
        IngestSource::Webhook {
            provider: "stripe".to_string(),
        },
        "evt_1",
        chrono::Utc::now(),
    );
    // The stripe message fires the source-agnostic trigger and the stripe-specific
    // one, but not the email trigger.
    let matched: Vec<_> = registry
        .find_ingest_triggers(&stripe_msg)
        .into_iter()
        .map(|t| t.function_name)
        .collect();
    assert_eq!(matched.len(), 2);
    assert!(matched.contains(&"onAnyInbound".to_string()));
    assert!(matched.contains(&"onStripe".to_string()));
    assert!(!matched.contains(&"onEmail".to_string()));
}

#[test]
fn test_registry_rejects_unknown_ingest_source() {
    use crate::{FunctionDefinition, RuntimeType};

    let functions = vec![FunctionDefinition::new(
        "onBad",
        "after:ingest:carrier-pigeon",
        RuntimeType::Deno,
    )];
    let error = TriggerRegistry::load_from_definitions(&functions)
        .expect_err("unknown source must fail loud");
    assert!(error.message.contains("carrier-pigeon"));
}

#[test]
fn test_registry_loads_multiple_triggers() {
    use crate::{FunctionDefinition, RuntimeType};

    let functions = vec![
        FunctionDefinition::new("onUserCreated", "after:mutation:createUser", RuntimeType::Deno),
        FunctionDefinition::new("validateInput", "before:mutation:createUser", RuntimeType::Deno),
        FunctionDefinition::new("getUser", "http:GET:/users/:id", RuntimeType::Deno),
    ];

    let registry = TriggerRegistry::load_from_definitions(&functions).expect("load registry");

    assert_eq!(registry.function_count, 3);
    assert_eq!(registry.before_mutation_count(), 1);
    assert_eq!(registry.http_route_count(), 1);
}

#[test]
fn test_registry_finds_http_route() {
    use crate::{FunctionDefinition, RuntimeType};

    let functions = vec![
        FunctionDefinition::new("getUser", "http:GET:/users/:id", RuntimeType::Deno),
        FunctionDefinition::new("listUsers", "http:GET:/users", RuntimeType::Deno),
    ];

    let registry = TriggerRegistry::load_from_definitions(&functions).expect("load registry");

    let route = registry.http_routes.find("GET", "/users/123");
    assert!(route.is_some());
    assert_eq!(route.expect("route found").function_name, "getUser");
}

#[test]
fn test_parsed_trigger_type_detection() {
    let after_mut = ParsedTrigger::parse("after:mutation:createUser").expect("parse");
    assert!(after_mut.is_after_mutation());
    assert_eq!(after_mut.trigger_type(), "after:mutation");

    let http = ParsedTrigger::parse("http:POST:/data").expect("parse");
    assert!(http.is_http());
    assert_eq!(http.trigger_type(), "http");
}

#[test]
fn test_registry_before_mutation_lookup() {
    use crate::{FunctionDefinition, RuntimeType};

    let functions = vec![
        FunctionDefinition::new("validate1", "before:mutation:createUser", RuntimeType::Deno),
        FunctionDefinition::new("validate2", "before:mutation:createUser", RuntimeType::Deno),
        FunctionDefinition::new("validate3", "before:mutation:deleteUser", RuntimeType::Deno),
    ];

    let registry = TriggerRegistry::load_from_definitions(&functions).expect("load registry");

    assert_eq!(registry.before_mutation_count(), 3);
    assert!(registry.has_before_mutation_triggers("createUser"));
    assert!(registry.has_before_mutation_triggers("deleteUser"));
    assert!(!registry.has_before_mutation_triggers("updateUser"));

    let create_user_triggers = registry.before_mutation_triggers_for("createUser");
    assert_eq!(create_user_triggers.len(), 2);
}

#[test]
fn test_registry_before_chain_returns_none_for_unknown_mutation() {
    use crate::{FunctionDefinition, RuntimeType};

    let functions = vec![FunctionDefinition::new(
        "validate",
        "before:mutation:createUser",
        RuntimeType::Deno,
    )];
    let registry = TriggerRegistry::load_from_definitions(&functions).expect("load");

    // Unknown mutation → None (zero overhead fast path)
    assert!(registry.before_chain("updateUser").is_none());
    assert!(registry.before_chain("deleteUser").is_none());
}

#[test]
fn test_registry_before_chain_returns_chain_for_known_mutation() {
    use crate::{FunctionDefinition, RuntimeType};

    let functions = vec![
        FunctionDefinition::new("validate1", "before:mutation:createUser", RuntimeType::Deno),
        FunctionDefinition::new("validate2", "before:mutation:createUser", RuntimeType::Deno),
        FunctionDefinition::new("other", "before:mutation:deleteUser", RuntimeType::Deno),
    ];
    let registry = TriggerRegistry::load_from_definitions(&functions).expect("load");

    let chain = registry.before_chain("createUser").expect("chain present");
    assert_eq!(chain.triggers.len(), 2);
    assert_eq!(chain.triggers[0].function_name, "validate1");
    assert_eq!(chain.triggers[1].function_name, "validate2");

    // deleteUser chain has only 1 trigger
    let del_chain = registry.before_chain("deleteUser").expect("chain present");
    assert_eq!(del_chain.triggers.len(), 1);
}