use context7_cli::api::{
criar_cliente_http, executar_com_retry, RespostaDocumentacao, RespostaListaBibliotecas,
};
use context7_cli::errors::ErroContext7;
use wiremock::matchers::{method, path, query_param};
use wiremock::{Mock, MockServer, ResponseTemplate};
#[tokio::test]
async fn testa_buscar_biblioteca_retorna_resultados_200() {
let servidor = MockServer::start().await;
let resposta_json = serde_json::json!({
"results": [
{
"id": "/facebook/react",
"title": "React",
"description": "A JavaScript library for building user interfaces",
"trust_score": 95.0
}
]
});
Mock::given(method("GET"))
.and(path("/api/v1/search"))
.and(query_param("libraryName", "react"))
.respond_with(ResponseTemplate::new(200).set_body_json(&resposta_json))
.mount(&servidor)
.await;
let url_base = servidor.uri();
let cliente = criar_cliente_http().unwrap();
let url = format!("{url_base}/api/v1/search");
let resposta = cliente
.get(&url)
.bearer_auth("ctx7sk-mock-token-12345678")
.query(&[("libraryName", "react"), ("query", "react")])
.send()
.await
.expect("deve conectar ao mock server");
assert!(resposta.status().is_success());
let dados: RespostaListaBibliotecas = resposta.json().await.expect("deve deserializar");
assert_eq!(dados.results.len(), 1);
assert_eq!(dados.results[0].id, "/facebook/react");
assert_eq!(dados.results[0].title, "React");
assert!((dados.results[0].trust_score.unwrap() - 95.0).abs() < f64::EPSILON);
}
#[tokio::test]
async fn testa_buscar_biblioteca_retorna_lista_vazia() {
let servidor = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/v1/search"))
.respond_with(
ResponseTemplate::new(200).set_body_json(serde_json::json!({ "results": [] })),
)
.mount(&servidor)
.await;
let url_base = servidor.uri();
let cliente = criar_cliente_http().unwrap();
let url = format!("{url_base}/api/v1/search");
let resposta = cliente
.get(&url)
.bearer_auth("ctx7sk-mock-token-12345678")
.query(&[("libraryName", "inexistente"), ("query", "inexistente")])
.send()
.await
.expect("deve conectar ao mock server");
let dados: RespostaListaBibliotecas = resposta.json().await.expect("deve deserializar");
assert_eq!(dados.results.len(), 0, "lista deve estar vazia");
}
#[tokio::test]
async fn testa_buscar_biblioteca_401_mapeia_para_sem_chaves_api() {
let servidor = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/v1/search"))
.respond_with(ResponseTemplate::new(401))
.mount(&servidor)
.await;
let url_base = servidor.uri();
let cliente = criar_cliente_http().unwrap();
let url = format!("{url_base}/api/v1/search");
let resposta = cliente
.get(&url)
.bearer_auth("ctx7sk-invalida")
.query(&[("libraryName", "react"), ("query", "react")])
.send()
.await
.expect("deve conectar ao mock server");
assert_eq!(resposta.status().as_u16(), 401, "mock deve retornar 401");
}
#[tokio::test]
async fn testa_buscar_biblioteca_429_nao_causa_panic() {
let servidor = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/v1/search"))
.respond_with(ResponseTemplate::new(429))
.mount(&servidor)
.await;
let url_base = servidor.uri();
let cliente = criar_cliente_http().unwrap();
let url = format!("{url_base}/api/v1/search");
let resposta = cliente
.get(&url)
.bearer_auth("ctx7sk-mock-token-12345678")
.query(&[("libraryName", "react"), ("query", "react")])
.send()
.await
.expect("deve conectar ao mock server");
assert_eq!(resposta.status().as_u16(), 429, "mock deve retornar 429");
}
#[tokio::test]
async fn testa_buscar_documentacao_retorna_snippets_200() {
let servidor = MockServer::start().await;
let resposta_json = serde_json::json!({
"id": "/facebook/react",
"snippets": [
{
"content": "O useEffect é um hook para efeitos colaterais.",
"type": "text",
"source_urls": ["https://react.dev/reference/react/useEffect"]
}
]
});
Mock::given(method("GET"))
.and(path("/api/v1/facebook/react"))
.respond_with(ResponseTemplate::new(200).set_body_json(&resposta_json))
.mount(&servidor)
.await;
let url_base = servidor.uri();
let cliente = criar_cliente_http().unwrap();
let url = format!("{url_base}/api/v1/facebook/react");
let resposta = cliente
.get(&url)
.bearer_auth("ctx7sk-mock-token-12345678")
.query(&[("type", "json")])
.send()
.await
.expect("deve conectar ao mock server");
assert!(resposta.status().is_success());
let dados: RespostaDocumentacao = resposta.json().await.expect("deve deserializar");
let snippets = dados.snippets.expect("deve ter snippets");
assert_eq!(snippets.len(), 1);
assert!(snippets[0].content.contains("useEffect"));
}
#[tokio::test]
async fn testa_buscar_documentacao_aceita_conteudo_texto_plano() {
let servidor = MockServer::start().await;
let resposta_json = serde_json::json!({
"id": "/axum-rs/axum",
"content": "Axum é um framework web para Rust baseado em Tower e Tokio."
});
Mock::given(method("GET"))
.and(path("/api/v1/axum-rs/axum"))
.respond_with(ResponseTemplate::new(200).set_body_json(&resposta_json))
.mount(&servidor)
.await;
let url_base = servidor.uri();
let cliente = criar_cliente_http().unwrap();
let url = format!("{url_base}/api/v1/axum-rs/axum");
let resposta = cliente
.get(&url)
.bearer_auth("ctx7sk-mock-token-12345678")
.query(&[("type", "txt")])
.send()
.await
.expect("deve conectar ao mock server");
let dados: RespostaDocumentacao = resposta.json().await.expect("deve deserializar");
assert!(dados.snippets.is_none(), "modo txt não deve ter snippets");
let conteudo = dados.content.expect("modo txt deve ter content");
assert!(conteudo.contains("Axum"));
}
#[tokio::test]
async fn testa_buscar_documentacao_500_nao_causa_panic() {
let servidor = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/v1/rust-lang/rust"))
.respond_with(ResponseTemplate::new(500))
.mount(&servidor)
.await;
let url_base = servidor.uri();
let cliente = criar_cliente_http().unwrap();
let url = format!("{url_base}/api/v1/rust-lang/rust");
let resposta = cliente
.get(&url)
.bearer_auth("ctx7sk-mock-token-12345678")
.query(&[("type", "json")])
.send()
.await
.expect("deve conectar ao mock server");
assert_eq!(resposta.status().as_u16(), 500, "mock deve retornar 500");
}
#[tokio::test]
async fn testa_executar_com_retry_sucesso_na_primeira_tentativa() {
let chaves = vec!["ctx7sk-chave-valida-123456789012".to_string()];
let resultado = executar_com_retry(&chaves, |_chave| async move {
Ok::<String, ErroContext7>("sucesso".to_string())
})
.await;
assert!(resultado.is_ok(), "deve retornar Ok na primeira tentativa");
assert_eq!(resultado.unwrap(), "sucesso");
}
#[tokio::test]
async fn testa_executar_com_retry_todas_chaves_invalidas_retorna_sem_chaves() {
let chaves = vec![
"ctx7sk-invalida-01-123456789012".to_string(),
"ctx7sk-invalida-02-123456789012".to_string(),
"ctx7sk-invalida-03-123456789012".to_string(),
];
let resultado = executar_com_retry(&chaves, |_chave| async move {
Err::<String, ErroContext7>(ErroContext7::SemChavesApi)
})
.await;
assert!(
resultado.is_err(),
"deve falhar quando todas as chaves são inválidas"
);
}
#[tokio::test]
async fn testa_executar_com_retry_usa_segunda_chave_quando_primeira_falha() {
use std::sync::Arc;
use std::sync::Mutex;
let contador = Arc::new(Mutex::new(0usize));
let chaves = vec![
"ctx7sk-primeira-chave-123456789".to_string(),
"ctx7sk-segunda-chave-1234567890".to_string(),
];
let contador_clone = Arc::clone(&contador);
let resultado = executar_com_retry(&chaves, move |_chave| {
let cont = Arc::clone(&contador_clone);
async move {
let mut n = cont.lock().unwrap();
*n += 1;
let tentativa = *n;
drop(n);
if tentativa == 1 {
Err(ErroContext7::RespostaInvalida { status: 503 })
} else {
Ok::<String, ErroContext7>("sucesso na segunda".to_string())
}
}
})
.await;
assert!(resultado.is_ok(), "deve ter sucesso na segunda tentativa");
let tentativas = *contador.lock().unwrap();
assert!(
tentativas >= 2,
"deve ter feito pelo menos 2 tentativas, fez: {tentativas}"
);
}
#[tokio::test]
async fn testa_executar_com_retry_aborta_em_erro_400() {
let chaves = vec![
"ctx7sk-chave-01-123456789012345".to_string(),
"ctx7sk-chave-02-123456789012345".to_string(),
];
let resultado = executar_com_retry(&chaves, |_chave| async move {
Err::<String, ErroContext7>(ErroContext7::ApiRetornou400 {
mensagem: "bad request".to_string(),
})
})
.await;
assert!(resultado.is_err(), "deve falhar em 400");
let err = resultado.unwrap_err().to_string();
assert!(
err.contains("400") || err.contains("bad request") || err.contains("Bad request"),
"mensagem deve mencionar 400 ou bad request: {err}"
);
}