apollo-errors 0.7.0

Structured error handling with automatic format conversion
Documentation
//! Tests for HeapErrorExt trait on Box<dyn Error> and Arc<dyn Error>

mod common;

use apollo_errors::{CodeCase, FieldCase, FormatConfig, HeapErrorExt};
use common::{ErrorWithFields, ErrorWithStatus};
use std::sync::Arc;
use tower::BoxError;

#[test]
fn test_box_error_to_json() {
    let error: BoxError = Box::new(ErrorWithFields::InvalidPort {
        port: 8080,
        config_file: "/etc/config.toml".to_string(),
    });

    let json = error.to_json(FormatConfig::default()).unwrap();
    insta::assert_json_snapshot!(json, @r###"
    {
      "config_file": "/etc/config.toml",
      "error": "config::invalid_port",
      "message": "Invalid port",
      "port": 8080
    }
    "###);
}

#[test]
fn test_box_error_to_html() {
    let error: BoxError = Box::new(ErrorWithFields::InvalidPort {
        port: 9000,
        config_file: "/app/config.conf".to_string(),
    });

    let html = error.to_html(FormatConfig::default());
    insta::assert_snapshot!(html, @r###"
    <div class="error">
    <h3 class="error-code">config::invalid_port</h3>
    <p class="error-message">Invalid port</p>

    <div class="error-extensions">
    <div class="error-field"><span class="field-name">port:</span> <span class="field-value">9000</span></div>
    <div class="error-field"><span class="field-name">config_file:</span> <span class="field-value">/app/config.conf</span></div>
    </div>
    </div>
    "###);
}

#[test]
fn test_box_error_to_graphql() {
    let error: BoxError = Box::new(ErrorWithFields::MissingConfig {
        expected_path: "/etc/app.yaml".to_string(),
    });

    let json = error.to_graphql(FormatConfig::default()).unwrap();
    insta::assert_json_snapshot!(json, @r###"
    {
      "extensions": {
        "code": "config::missing",
        "expected_path": "/etc/app.yaml"
      },
      "message": "Missing configuration"
    }
    "###);
}

#[test]
fn test_box_error_to_text() {
    let error: BoxError = Box::new(ErrorWithFields::InvalidPort {
        port: 3000,
        config_file: "/config.toml".to_string(),
    });

    let text = error.to_text(FormatConfig::default());
    insta::assert_snapshot!(text, @"[config::invalid_port] Invalid port");
}

#[test]
fn test_arc_error_to_json() {
    let error: Arc<dyn std::error::Error + Send + Sync> = Arc::new(ErrorWithFields::InvalidPort {
        port: 8080,
        config_file: "/etc/config.toml".to_string(),
    });

    let json = error.to_json(FormatConfig::default()).unwrap();
    insta::assert_json_snapshot!(json, @r###"
    {
      "config_file": "/etc/config.toml",
      "error": "config::invalid_port",
      "message": "Invalid port",
      "port": 8080
    }
    "###);
}

#[test]
fn test_arc_error_to_html() {
    let error: Arc<dyn std::error::Error + Send + Sync> =
        Arc::new(ErrorWithFields::MissingConfig {
            expected_path: "/app/config.yaml".to_string(),
        });

    let html = error.to_html(FormatConfig::default());
    insta::assert_snapshot!(html, @r###"
    <div class="error">
    <h3 class="error-code">config::missing</h3>
    <p class="error-message">Missing configuration</p>

    <div class="error-extensions">
    <div class="error-field"><span class="field-name">expected_path:</span> <span class="field-value">/app/config.yaml</span></div>
    </div>
    </div>
    "###);
}

#[test]
fn test_arc_error_to_graphql() {
    let error: Arc<dyn std::error::Error + Send + Sync> = Arc::new(ErrorWithFields::InvalidPort {
        port: 5000,
        config_file: "/etc/server.conf".to_string(),
    });

    let json = error.to_graphql(FormatConfig::default()).unwrap();
    insta::assert_json_snapshot!(json, @r###"
    {
      "extensions": {
        "code": "config::invalid_port",
        "config_file": "/etc/server.conf",
        "port": 5000
      },
      "message": "Invalid port"
    }
    "###);
}

#[test]
fn test_arc_error_to_text() {
    let error: Arc<dyn std::error::Error + Send + Sync> =
        Arc::new(ErrorWithFields::MissingConfig {
            expected_path: "/config/app.yaml".to_string(),
        });

    let text = error.to_text(FormatConfig::default());
    insta::assert_snapshot!(text, @"[config::missing] Missing configuration");
}

#[test]
fn test_box_error_to_jsonrpc() {
    let error: BoxError = Box::new(ErrorWithFields::InvalidPort {
        port: 8080,
        config_file: "/etc/config.toml".to_string(),
    });

    let jsonrpc = error.to_jsonrpc(FormatConfig::default()).unwrap();
    insta::assert_json_snapshot!(jsonrpc, @r#"
    {
      "code": -32000,
      "data": {
        "config_file": "/etc/config.toml",
        "diagnostic_code": "config::invalid_port",
        "port": 8080
      },
      "message": "Invalid port"
    }
    "#);
}

#[test]
fn test_arc_error_to_jsonrpc() {
    let error: Arc<dyn std::error::Error + Send + Sync> = Arc::new(ErrorWithFields::InvalidPort {
        port: 9000,
        config_file: "/app/server.toml".to_string(),
    });

    let jsonrpc = error.to_jsonrpc(FormatConfig::default()).unwrap();
    insta::assert_json_snapshot!(jsonrpc, @r#"
    {
      "code": -32000,
      "data": {
        "config_file": "/app/server.toml",
        "diagnostic_code": "config::invalid_port",
        "port": 9000
      },
      "message": "Invalid port"
    }
    "#);
}

#[test]
fn test_format_config_threads_through_arc() {
    let error: Arc<dyn std::error::Error + Send + Sync> = Arc::new(ErrorWithFields::InvalidPort {
        port: 8080,
        config_file: "/etc/config.toml".to_string(),
    });
    let config = FormatConfig {
        field_case: FieldCase::CamelCase,
        code_case: CodeCase::ScreamingSnakeCase,
    };
    let json = error.to_json(config).unwrap();
    insta::assert_json_snapshot!(json, @r#"
    {
      "configFile": "/etc/config.toml",
      "error": "CONFIG_INVALID_PORT",
      "message": "Invalid port",
      "port": 8080
    }
    "#);
}

#[test]
fn test_box_with_nested_arc() {
    let arc_error: Arc<dyn std::error::Error + Send + Sync> =
        Arc::new(ErrorWithFields::InvalidPort {
            port: 7000,
            config_file: "/nested/config.toml".to_string(),
        });
    let box_error: BoxError = Box::new(arc_error);

    let json = box_error.to_json(FormatConfig::default()).unwrap();
    insta::assert_json_snapshot!(json, @r###"
    {
      "config_file": "/nested/config.toml",
      "error": "config::invalid_port",
      "message": "Invalid port",
      "port": 7000
    }
    "###);
}

#[test]
fn test_error_with_status_to_json() {
    let error: BoxError = Box::new(ErrorWithStatus::NotFound);

    let json = error.to_json(FormatConfig::default()).unwrap();
    insta::assert_json_snapshot!(json, @r###"
    {
      "error": "resource::not_found",
      "message": "Resource not found"
    }
    "###);
}

#[test]
fn test_unregistered_error_fallback() {
    #[derive(Debug)]
    struct UnregisteredError;

    impl std::fmt::Display for UnregisteredError {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            write!(f, "This error is not registered")
        }
    }

    impl std::error::Error for UnregisteredError {}

    let error: BoxError = Box::new(UnregisteredError);

    let json = error.to_json(FormatConfig::default()).unwrap();
    insta::assert_json_snapshot!(json, @r###"
    {
      "error": "UNKNOWN_ERROR",
      "message": "This error is not registered"
    }
    "###);

    let html = error.to_html(FormatConfig::default());
    insta::assert_snapshot!(html, @r###"
    <div class="error">
    <h3 class="error-code">UNKNOWN_ERROR</h3>
    <p class="error-message">This error is not registered</p>
    </div>
    "###);

    let graphql = error.to_graphql(FormatConfig::default()).unwrap();
    insta::assert_json_snapshot!(graphql, @r###"
    {
      "extensions": {
        "code": "UNKNOWN_ERROR"
      },
      "message": "This error is not registered"
    }
    "###);

    let text = error.to_text(FormatConfig::default());
    insta::assert_snapshot!(text, @"[UNKNOWN_ERROR] This error is not registered");

    let jsonrpc = error.to_jsonrpc(FormatConfig::default()).unwrap();
    insta::assert_json_snapshot!(jsonrpc, @r#"
    {
      "code": -32000,
      "data": {
        "diagnostic_code": "UNKNOWN_ERROR"
      },
      "message": "This error is not registered"
    }
    "#);
}