use aurabase::types::{ServerErrorMsg, ServerMessage};
const ERREUR_CANAL_REELLE: &str = r#"{"type":"error","message":"Non abonné au canal '0c4fb174-ff01-468a-b623-fe75ac679d75:canal-x' — Subscribe requis avant Broadcast","channel":"0c4fb174-ff01-468a-b623-fe75ac679d75:canal-x"}"#;
const ERREUR_GLOBALE_REELLE: &str =
r#"{"type":"error","message":"Trop de violations rate limit — déconnexion"}"#;
fn erreur(json: &str) -> ServerErrorMsg {
match serde_json::from_str::<ServerMessage>(json).expect("le SDK doit décoder ce message") {
ServerMessage::Error(e) => e,
autre => panic!("attendu ServerMessage::Error, obtenu {autre:?}"),
}
}
fn retenu(json: &str) -> serde_json::Value {
serde_json::to_value(erreur(json)).expect("ServerErrorMsg est Serialize")
}
#[test]
fn le_champ_channel_du_serveur_est_conserve() {
let v = retenu(ERREUR_CANAL_REELLE);
assert_eq!(
v.get("channel").and_then(|c| c.as_str()),
Some("0c4fb174-ff01-468a-b623-fe75ac679d75:canal-x"),
"le SDK a PERDU le champ `channel` du serveur. Sans lui il ne peut pas distinguer un \
refus PROPRE À UN CANAL d'une panne globale de connexion — c'est le contrat B9/N23 \
émis par le serveur. Ce que le SDK a retenu : {v}"
);
assert!(
v.get("message")
.and_then(|m| m.as_str())
.unwrap_or_default()
.contains("Non abonné"),
"le message doit être conservé tel quel : {v}"
);
}
#[test]
fn une_erreur_globale_na_pas_de_channel() {
let v = retenu(ERREUR_GLOBALE_REELLE);
assert!(
v.get("channel").map(|c| c.is_null()).unwrap_or(true),
"une erreur sans `channel` est GLOBALE : le champ doit rester optionnel, sinon le \
décodage d'un message de déconnexion échouerait. Reçu : {v}"
);
}
use futures::{SinkExt, StreamExt};
async fn serveur_ws_qui_repond(reponse: String) -> u16 {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind");
let port = listener.local_addr().unwrap().port();
tokio::spawn(async move {
let (stream, _) = listener.accept().await.expect("accept");
let mut ws = tokio_tungstenite::accept_async(stream)
.await
.expect("handshake");
let _ = ws.next().await;
let _ = ws
.send(tokio_tungstenite::tungstenite::Message::Text(reponse))
.await;
tokio::time::sleep(std::time::Duration::from_secs(20)).await;
});
port
}
fn client_sur(port: u16) -> aurabase::AuraClient {
aurabase::AuraClient::new(&format!("http://127.0.0.1:{port}"), "aura_anon_test", None)
}
#[tokio::test]
async fn une_erreur_de_canal_est_remontee_a_lappelant() {
let port = serveur_ws_qui_repond(ERREUR_CANAL_REELLE.to_string()).await;
let client = client_sur(port);
let rt = client.realtime();
let mut erreurs = rt.on_error();
let ch = rt.channel("canal-x", None);
rt.subscribe_channel(ch).await.expect("subscribe_channel");
let recu = tokio::time::timeout(std::time::Duration::from_secs(5), erreurs.recv())
.await
.expect(
"AUCUNE erreur remontée en 5 s — le SDK a avalé `ServerMessage::Error` \
(`_ => {}` dans dispatch_message) : un refus RLS d'abonnement est invisible \
pour l'appelant",
)
.expect("canal d'erreurs fermé");
assert!(
recu.message.contains("Non abonné"),
"le message du serveur doit être transmis tel quel, reçu: {recu:?}"
);
assert_eq!(
recu.channel.as_deref(),
Some("canal-x"),
"l'erreur doit être attribuée au canal concerné, en nom COURT (celui donné à \
`channel()`) — sinon l'appelant ne peut pas savoir lequel de ses canaux est tombé. \
Reçu: {recu:?}"
);
}
#[tokio::test]
async fn une_erreur_globale_est_remontee_sans_canal() {
let port = serveur_ws_qui_repond(ERREUR_GLOBALE_REELLE.to_string()).await;
let client = client_sur(port);
let rt = client.realtime();
let mut erreurs = rt.on_error();
let ch = rt.channel("canal-x", None);
rt.subscribe_channel(ch).await.expect("subscribe_channel");
let recu = tokio::time::timeout(std::time::Duration::from_secs(5), erreurs.recv())
.await
.expect("une erreur GLOBALE doit elle aussi être remontée")
.expect("canal d'erreurs fermé");
assert!(recu.message.contains("Trop de violations"), "{recu:?}");
assert_eq!(
recu.channel, None,
"sans `channel` émis par le serveur, l'erreur est globale — l'attribuer à un canal \
serait faux. Reçu: {recu:?}"
);
}