fraiseql-core 2.12.0

Core execution engine for FraiseQL v2 - Compiled GraphQL over SQL
Documentation
//! Mutation error scenarios.

#![allow(clippy::unwrap_used, clippy::panic, clippy::print_stderr)] // Reason: test code, panics + skip notes acceptable
use fraiseql_core::federation::{
    mutation_query_builder::{build_insert_query, build_update_query},
    types::FederationMetadata,
};
use serde_json::json;

use super::common;

#[test]
fn test_mutation_entity_not_found() {
    let metadata = common::metadata_single_key("User", "id");

    let variables = json!({
        "id": "nonexistent_user",
        "name": "Updated"
    });

    // Query builds successfully but returns error when executed against DB
    let query = build_update_query("User", &variables, &metadata).unwrap();
    assert!(
        query.contains("WHERE \"id\" = 'nonexistent_user'"),
        "Expected quoted column in: {query}"
    );
}

#[test]
fn test_mutation_invalid_field_value() {
    let metadata = FederationMetadata::default();

    // Invalid field values like objects should be rejected
    let invalid_variables = json!({
        "id": "user1",
        "metadata": { "nested": "object" }  // Invalid for SQL
    });

    let result = build_insert_query("User", &invalid_variables, &metadata);
    // Should error because objects cannot convert to SQL literals
    assert!(result.is_err(), "expected Err for nested object variable, got: {result:?}");
}

#[test]
fn test_mutation_missing_required_fields() {
    let metadata = common::metadata_single_key("User", "id");

    // Missing id key field should error
    let missing_key = json!({
        "name": "Updated Name"
    });

    let result = build_update_query("User", &missing_key, &metadata);
    assert!(result.is_err(), "expected Err for missing key field, got: {result:?}");
    assert!(result.unwrap_err().to_string().contains("missing"));
}

#[tokio::test]
async fn test_mutation_authorization_error() {
    let metadata = common::metadata_single_key("User", "id");
    let Some((_pg, executor)) =
        common::pg_mutation_executor(metadata, &[("user", &["id text", "role text"])]).await
    else {
        eprintln!("SKIP test_mutation_authorization_error: no postgres");
        return;
    };

    let variables = json!({
        "id": "admin_user",
        "role": "superadmin"
    });

    executor
        .execute_local_mutation("User", "createUser", &variables)
        .await
        .unwrap_or_else(|e| panic!("seed createUser failed: {e}"));
    // Execute mutation (authorization would be checked at application level)
    let result = executor.execute_local_mutation("User", "updateUser", &variables).await;

    // Query builds and executes successfully
    result.unwrap_or_else(|e| {
        panic!("execute_local_mutation(User/updateUser) authorization check failed: {e}")
    });
}

#[tokio::test]
async fn test_mutation_duplicate_key_error() {
    let metadata = common::metadata_single_key("User", "id");
    let Some((_pg, executor)) =
        common::pg_mutation_executor(metadata, &[("user", &["id text", "email text"])]).await
    else {
        eprintln!("SKIP test_mutation_duplicate_key_error: no postgres");
        return;
    };

    let variables = json!({
        "id": "existing_id",
        "email": "test@example.com"
    });

    executor
        .execute_local_mutation("User", "createUser", &variables)
        .await
        .unwrap_or_else(|e| panic!("seed createUser failed: {e}"));
    // Execute mutation (duplicate key would be caught at DB level if constrained)
    let result = executor.execute_local_mutation("User", "updateUser", &variables).await;

    // Query builds and executes successfully
    result.unwrap_or_else(|e| {
        panic!("execute_local_mutation(User/updateUser) duplicate key check failed: {e}")
    });
}

#[tokio::test]
async fn test_mutation_latency_single_entity() {
    // The original sub-10ms assertion measured the in-memory mock; against real
    // Postgres over a service binding a micro-benchmark is meaningless, so this
    // now just asserts the single mutation completes.
    let metadata = common::metadata_single_key("User", "id");
    let Some((_pg, executor)) =
        common::pg_mutation_executor(metadata, &[("user", &["id text", "name text"])]).await
    else {
        eprintln!("SKIP test_mutation_latency_single_entity: no postgres");
        return;
    };

    let variables = json!({
        "id": "user1",
        "name": "Updated User"
    });

    executor
        .execute_local_mutation("User", "createUser", &variables)
        .await
        .unwrap_or_else(|e| panic!("seed createUser failed: {e}"));
    let result = executor.execute_local_mutation("User", "updateUser", &variables).await;
    result.unwrap_or_else(|e| {
        panic!("execute_local_mutation(User/updateUser) single entity failed: {e}")
    });
}

#[tokio::test]
async fn test_mutation_latency_batch_updates() {
    // The original sub-100ms assertion measured the in-memory mock; against real
    // Postgres a batch-timing micro-benchmark is meaningless, so this now just
    // asserts all batch mutations complete.
    let metadata = common::metadata_single_key("User", "id");
    let Some((_pg, executor)) =
        common::pg_mutation_executor(metadata, &[("user", &["id text", "name text"])]).await
    else {
        eprintln!("SKIP test_mutation_latency_batch_updates: no postgres");
        return;
    };

    // Execute 10 mutations
    for i in 0..10 {
        let variables = json!({
            "id": format!("user{}", i),
            "name": format!("Updated User {}", i)
        });

        executor
            .execute_local_mutation("User", "createUser", &variables)
            .await
            .unwrap_or_else(|e| panic!("seed createUser batch iteration {i} failed: {e}"));
        let result = executor.execute_local_mutation("User", "updateUser", &variables).await;
        result.unwrap_or_else(|e| {
            panic!("execute_local_mutation(User/updateUser) batch iteration {i} failed: {e}")
        });
    }
}