clickup_v2 0.1.1

A comprehensive Rust client library and CLI for ClickUp API v2 with OAuth2 authentication, task management, and custom fields support
Documentation
use clickup_v2::error::{AuthError, AuthResult};
use clickup_v2::client::api::ClickUpClient;
use mockito::Server;
use serde_json::json;

#[test]
fn test_auth_error_creation_and_display() {
    // Testa criação e mensagens de erro
    let token_error = AuthError::TokenExpired;
    assert_eq!(token_error.to_string(), "Token de acesso expirado ou inválido");

    let invalid_code = AuthError::InvalidCode("XYZ123".to_string());
    assert_eq!(invalid_code.to_string(), "Código de autorização inválido: XYZ123");

    let access_denied = AuthError::AccessDenied;
    assert_eq!(access_denied.to_string(), "Acesso negado pelo usuário");

    let invalid_state = AuthError::InvalidState;
    assert_eq!(invalid_state.to_string(), "Estado OAuth2 inválido");

    let timeout = AuthError::Timeout;
    assert_eq!(timeout.to_string(), "Timeout durante autenticação");
}

#[test]
fn test_custom_error_constructors() {
    let env_error = AuthError::env_error("ENV_VAR not found");
    assert!(env_error.to_string().contains("ENV_VAR not found"));

    let config_error = AuthError::config_error("Invalid config");
    assert!(config_error.to_string().contains("Invalid config"));

    let callback_error = AuthError::callback_error("Server error");
    assert!(callback_error.to_string().contains("Server error"));

    let browser_error = AuthError::browser_error("Cannot open browser");
    assert!(browser_error.to_string().contains("Cannot open browser"));

    let token_error = AuthError::token_error("Invalid token");
    assert!(token_error.to_string().contains("Invalid token"));

    let api_error = AuthError::api_error("API failed");
    assert!(api_error.to_string().contains("API failed"));

    let parse_error = AuthError::parse_error("Parse failed");
    assert!(parse_error.to_string().contains("Parse failed"));

    let generic_error = AuthError::generic("Something went wrong");
    assert!(generic_error.to_string().contains("Something went wrong"));
}

#[test]
fn test_error_from_conversions() {
    // Teste conversão de erro de URL
    let url_error = url::Url::parse("invalid-url").unwrap_err();
    let auth_error = AuthError::from(url_error);
    assert!(auth_error.to_string().contains("Erro de parsing de URL"));

    // Teste conversão de erro de IO
    use std::io::{Error as IoError, ErrorKind};
    let io_error = IoError::new(ErrorKind::NotFound, "File not found");
    let auth_error = AuthError::from(io_error);
    assert!(auth_error.to_string().contains("Erro de IO"));

    // Teste conversão de erro de serialização JSON
    let invalid_json = "{invalid json}";
    let json_error = serde_json::from_str::<serde_json::Value>(invalid_json).unwrap_err();
    let auth_error = AuthError::from(json_error);
    assert!(auth_error.to_string().contains("Erro de serialização"));
}

#[test]
fn test_auth_result_type() {
    // Teste função que retorna sucesso
    fn success_function() -> AuthResult<String> {
        Ok("Success".to_string())
    }

    // Teste função que retorna erro
    fn error_function() -> AuthResult<String> {
        Err(AuthError::TokenExpired)
    }

    let success = success_function();
    assert!(success.is_ok());
    assert_eq!(success.unwrap(), "Success");

    let error = error_function();
    assert!(error.is_err());
    assert!(matches!(error.unwrap_err(), AuthError::TokenExpired));
}

#[tokio::test]
async fn test_api_error_handling_401() {
    let mut server = Server::new_async().await;

    let _mock = server.mock("GET", "/user")
        .with_status(401)
        .with_header("content-type", "application/json")
        .with_body(json!({
            "err": "Token is invalid",
            "ECODE": "OAUTH_021"
        }).to_string())
        .create_async().await;

    let client = ClickUpClient::new(
        "invalid_token".to_string(),
        server.url(),
    );

    let result = client.get_authorized_user().await;
    assert!(result.is_err());

    let error = result.unwrap_err();
    assert!(error.to_string().contains("401"));
}

#[tokio::test]
async fn test_api_error_handling_404() {
    let mut server = Server::new_async().await;

    // Mock a 404 response for the user endpoint
    let _mock = server.mock("GET", "/user")
        .with_status(404)
        .with_header("content-type", "application/json")
        .with_body(json!({
            "err": "User not found",
            "ECODE": "NOT_FOUND"
        }).to_string())
        .create_async()
        .await;

    let client = ClickUpClient::new(
        "test_token".to_string(),
        server.url(),
    );

    // Use public method get_authorized_user() instead of private get()
    let result = client.get_authorized_user().await;
    assert!(result.is_err());

    let error = result.unwrap_err();
    assert!(error.to_string().contains("não encontrado"));
}

#[tokio::test]
async fn test_api_error_handling_429_rate_limit() {
    let mut server = Server::new_async().await;

    let _mock = server.mock("GET", "/user")
        .with_status(429)
        .with_header("content-type", "application/json")
        .with_header("x-ratelimit-limit", "100")
        .with_header("x-ratelimit-remaining", "0")
        .with_header("x-ratelimit-reset", "1567780450")
        .with_body(json!({
            "err": "Rate limit exceeded",
            "ECODE": "RATE_LIMIT"
        }).to_string())
        .create_async().await;

    let client = ClickUpClient::new(
        "test_token".to_string(),
        server.url(),
    );

    let result = client.get_authorized_user().await;
    assert!(result.is_err());

    let error = result.unwrap_err();
    assert!(error.to_string().contains("429"));
}

#[tokio::test]
async fn test_api_error_handling_500() {
    let mut server = Server::new_async().await;

    let _mock = server.mock("GET", "/user")
        .with_status(500)
        .with_header("content-type", "application/json")
        .with_body(json!({
            "err": "Internal server error",
            "ECODE": "SERVER_ERROR"
        }).to_string())
        .create_async().await;

    let client = ClickUpClient::new(
        "test_token".to_string(),
        server.url(),
    );

    let result = client.get_authorized_user().await;
    assert!(result.is_err());

    let error = result.unwrap_err();
    assert!(error.to_string().contains("500"));
}

#[tokio::test]
async fn test_api_error_handling_503() {
    let mut server = Server::new_async().await;

    let _mock = server.mock("GET", "/user")
        .with_status(503)
        .with_header("content-type", "application/json")
        .with_body(json!({
            "err": "Service temporarily unavailable",
            "ECODE": "SERVICE_UNAVAILABLE"
        }).to_string())
        .create_async().await;

    let client = ClickUpClient::new(
        "test_token".to_string(),
        server.url(),
    );

    let result = client.get_authorized_user().await;
    assert!(result.is_err());

    let error = result.unwrap_err();
    assert!(error.to_string().contains("503"));
}

#[tokio::test]
async fn test_network_error_handling() {
    // Usa uma URL inválida para simular erro de rede
    let client = ClickUpClient::new(
        "test_token".to_string(),
        "http://invalid-domain-that-does-not-exist-12345.com".to_string(),
    );

    let result = client.get_authorized_user().await;
    assert!(result.is_err());
}

#[tokio::test]
async fn test_malformed_json_response() {
    let mut server = Server::new_async().await;

    let _mock = server.mock("GET", "/user")
        .with_status(200)
        .with_header("content-type", "application/json")
        .with_body("not valid json")
        .create_async().await;

    let client = ClickUpClient::new(
        "test_token".to_string(),
        server.url(),
    );

    let result = client.get_authorized_user().await;
    // Dependendo da implementação, isso pode ou não falhar
    // Se o cliente tenta parsear JSON, deve falhar
    if result.is_err() {
        let error = result.unwrap_err();
        assert!(error.to_string().contains("JSON") || error.to_string().contains("parse"));
    }
}

#[test]
fn test_error_debug_format() {
    let error = AuthError::InvalidCode("ABC123".to_string());
    let debug_str = format!("{:?}", error);
    assert!(debug_str.contains("InvalidCode"));
    assert!(debug_str.contains("ABC123"));

    let error = AuthError::TokenExpired;
    let debug_str = format!("{:?}", error);
    assert!(debug_str.contains("TokenExpired"));
}

#[test]
fn test_error_chaining() {
    fn inner_function() -> AuthResult<String> {
        Err(AuthError::token_error("Inner error"))
    }

    fn outer_function() -> AuthResult<String> {
        inner_function().map_err(|e| AuthError::generic(format!("Outer error: {}", e)))
    }

    let result = outer_function();
    assert!(result.is_err());
    let error = result.unwrap_err();
    assert!(error.to_string().contains("Outer error"));
    assert!(error.to_string().contains("Inner error"));
}

#[test]
fn test_error_pattern_matching() {
    let errors = vec![
        AuthError::TokenExpired,
        AuthError::AccessDenied,
        AuthError::InvalidState,
        AuthError::Timeout,
    ];

    for error in errors {
        match error {
            AuthError::TokenExpired => assert!(true),
            AuthError::AccessDenied => assert!(true),
            AuthError::InvalidState => assert!(true),
            AuthError::Timeout => assert!(true),
            _ => panic!("Unexpected error type"),
        }
    }
}

// Commented out because it uses private method `get`
// TODO: Rewrite to use public API methods
// #[tokio::test]
// async fn test_multiple_error_scenarios() {
//     // Simula múltiplos cenários de erro em sequência
//     let mut server = Server::new_async().await;
//
//     let scenarios = vec![
//         (401, "OAUTH_021", "Token is invalid"),
//         (403, "FORBIDDEN", "Access forbidden"),
//         (404, "NOT_FOUND", "Resource not found"),
//         (429, "RATE_LIMIT", "Rate limit exceeded"),
//         (500, "SERVER_ERROR", "Internal server error"),
//     ];
//
//     for (status, code, message) in scenarios {
//         let _mock = server.mock("GET", format!("/test/{}", status).as_str())
//             .with_status(status)
//             .with_header("content-type", "application/json")
//             .with_body(json!({
//                 "err": message,
//                 "ECODE": code
//             }).to_string())
//             .create_async().await;
//
//         let client = ClickUpClient::new(
//             "test_token".to_string(),
//             server.url(),
//         );
//
//         let result = client.get(format!("/test/{}", status).as_str()).await;
//         assert!(result.is_err());
//
//         let error = result.unwrap_err();
//         assert!(error.to_string().contains(&status.to_string()));
//     }
// }