use axum::Router;
use axum::http::StatusCode;
use axum::response::Response;
use crate::test_support::{body_text, delete, get, post, put_json, router_desarrollo, router_oidc};
const UN_UUID: &str = "00000000-0000-4000-8000-000000000000";
fn las_cinco_rutas(recurso: &str) -> [(&'static str, String); 5] {
[
("GET", format!("/api/v1/{recurso}")),
("POST", format!("/api/v1/{recurso}")),
("GET", format!("/api/v1/{recurso}/{UN_UUID}")),
("PUT", format!("/api/v1/{recurso}/{UN_UUID}")),
("DELETE", format!("/api/v1/{recurso}/{UN_UUID}")),
]
}
const LOS_TRES_RECURSOS: [&str; 3] = ["repositories", "targets", "jobs"];
async fn sin_cuerpo(router: Router, metodo: &str, uri: &str) -> Response {
match metodo {
"GET" => get(router, uri).await,
"POST" => post(router, uri).await,
"PUT" => put_json(router, uri, &serde_json::json!({})).await,
"DELETE" => delete(router, uri).await,
otro => panic!("método de prueba no contemplado: {otro}"),
}
}
async fn es_un_401_con_envoltura(respuesta: Response, donde: &str) {
let status = respuesta.status();
let texto = body_text(respuesta).await;
assert_eq!(
status,
StatusCode::UNAUTHORIZED,
"{donde} tiene que exigir sesión y contestó {status} con {texto}"
);
let cuerpo: serde_json::Value =
serde_json::from_str(&texto).expect("el 401 tiene que traer JSON en el cuerpo");
assert_eq!(cuerpo["status"], 401, "la envoltura del 401: {texto}");
assert!(
cuerpo["message"]
.as_str()
.is_some_and(|mensaje| !mensaje.is_empty()),
"el 401 tiene que traer un mensaje: {texto}"
);
}
#[tokio::test]
async fn en_modo_oidc_cada_ruta_nueva_exige_sesion() {
let router = router_oidc().await;
for recurso in LOS_TRES_RECURSOS {
for (metodo, uri) in las_cinco_rutas(recurso) {
let respuesta = sin_cuerpo(router.clone(), metodo, &uri).await;
es_un_401_con_envoltura(respuesta, &format!("{metodo} {uri}")).await;
}
}
}
#[tokio::test]
async fn en_desarrollo_las_rutas_nuevas_estan_montadas_y_no_contesta_404() {
let router = router_desarrollo().await;
for recurso in LOS_TRES_RECURSOS {
let uri = format!("/api/v1/{recurso}");
let respuesta = get(router.clone(), &uri).await;
let status = respuesta.status();
let texto = body_text(respuesta).await;
assert_ne!(
status,
StatusCode::NOT_FOUND,
"GET {uri} no está montada en modo de desarrollo: contestó {status} con {texto}"
);
assert_eq!(
status,
StatusCode::OK,
"GET {uri} en modo de desarrollo contestó {status} con {texto}"
);
}
}
#[tokio::test]
async fn un_metodo_no_declarado_sobre_una_ruta_nueva_sin_sesion_contesta_401() {
let router = router_oidc().await;
for recurso in LOS_TRES_RECURSOS {
let uri = format!("/api/v1/{recurso}/{UN_UUID}");
let respuesta = post(router.clone(), &uri).await;
let status = respuesta.status();
let texto = body_text(respuesta).await;
assert_ne!(
status,
StatusCode::METHOD_NOT_ALLOWED,
"POST {uri} sin sesión contestó {status} con {texto}: un 405 revela qué métodos \
existen a quien no ha entrado"
);
assert_eq!(
status,
StatusCode::UNAUTHORIZED,
"POST {uri} sin sesión tiene que exigir sesión y contestó {status} con {texto}"
);
let cuerpo: serde_json::Value =
serde_json::from_str(&texto).expect("el 401 tiene que traer JSON en el cuerpo");
assert_eq!(cuerpo["status"], 401, "la envoltura del 401: {texto}");
assert!(
cuerpo["message"]
.as_str()
.is_some_and(|mensaje| !mensaje.is_empty()),
"el 401 tiene que traer un mensaje: {texto}"
);
}
}
#[tokio::test]
async fn un_identificador_mal_formado_contesta_400_en_los_tres_recursos() {
let router = router_desarrollo().await;
for recurso in LOS_TRES_RECURSOS {
let uri = format!("/api/v1/{recurso}/no-es-un-uuid");
let respuesta = get(router.clone(), &uri).await;
let status = respuesta.status();
let texto = body_text(respuesta).await;
assert_ne!(
status,
StatusCode::NOT_FOUND,
"GET {uri}: un identificador mal formado no es un recurso ausente ({texto})"
);
assert_ne!(
status,
StatusCode::INTERNAL_SERVER_ERROR,
"GET {uri}: un identificador mal formado no es un fallo del servidor ({texto})"
);
assert_eq!(
status,
StatusCode::BAD_REQUEST,
"GET {uri} contestó {status} con {texto}"
);
let cuerpo: serde_json::Value =
serde_json::from_str(&texto).expect("el 400 tiene que traer JSON en el cuerpo");
assert_eq!(cuerpo["status"], 400, "la envoltura del 400: {texto}");
assert!(
cuerpo["message"]
.as_str()
.is_some_and(|mensaje| !mensaje.is_empty()),
"el 400 tiene que traer un mensaje de texto: {texto}"
);
}
}
fn las_cinco_rutas_sueltas() -> [(&'static str, String); 5] {
[
("GET", "/api/v1/executions".to_owned()),
("POST", format!("/api/v1/jobs/{UN_UUID}/run")),
("GET", "/api/v1/events".to_owned()),
("GET", "/api/v1/snapshots".to_owned()),
("GET", "/api/v1/restorations".to_owned()),
]
}
#[tokio::test]
async fn las_rutas_del_srs_que_no_estan_en_la_tabla_de_las_quince_siguen_la_particion_del_resto() {
let router = router_oidc().await;
for (metodo, uri) in las_cinco_rutas_sueltas() {
let respuesta = sin_cuerpo(router.clone(), metodo, &uri).await;
es_un_401_con_envoltura(respuesta, &format!("{metodo} {uri} sin sesión")).await;
}
let respuesta = get(router_desarrollo().await, "/api/v1/executions").await;
let status = respuesta.status();
let texto = body_text(respuesta).await;
assert_eq!(
status,
StatusCode::OK,
"en el modo sin autenticación las rutas de recursos están montadas y contestan, \
no dan 404; contestó {status} con {texto}"
);
}