use std::sync::Arc;
use async_trait::async_trait;
use serde_json::json;
use crate::host::HostState;
pub use car_proto::BrowserSignInSnapshot;
pub const BROWSER_SIGNIN_NEEDED: &str = "browser.signin_needed";
pub const BROWSER_SIGNIN_RESOLVED: &str = "browser.signin_resolved";
#[async_trait]
pub trait SignInAttention: Send + Sync {
async fn signin_needed(&self, conversation_id: Option<&str>, message: &str);
async fn signin_resolved(&self, conversation_id: Option<&str>);
}
pub struct HostSignInAttention {
host: Arc<HostState>,
}
impl HostSignInAttention {
pub fn new(host: Arc<HostState>) -> Self {
Self { host }
}
}
fn wire_conversation_id(conversation_id: Option<&str>) -> &str {
conversation_id.unwrap_or("")
}
#[async_trait]
impl SignInAttention for HostSignInAttention {
async fn signin_needed(&self, conversation_id: Option<&str>, message: &str) {
self.host
.record_event(
BROWSER_SIGNIN_NEEDED,
None,
message.to_string(),
json!({
"conversation_id": wire_conversation_id(conversation_id),
"standing_session": conversation_id.is_none(),
"message": message,
}),
)
.await;
}
async fn signin_resolved(&self, conversation_id: Option<&str>) {
self.host
.record_event(
BROWSER_SIGNIN_RESOLVED,
None,
"The browser sign-in wait ended".to_string(),
json!({
"conversation_id": wire_conversation_id(conversation_id),
"standing_session": conversation_id.is_none(),
}),
)
.await;
}
}
pub async fn notify_signin_transition(
attention: &Arc<dyn SignInAttention>,
conversation_id: Option<&str>,
before: Option<&str>,
after: Option<&str>,
) {
match (before, after) {
(None, Some(message)) => attention.signin_needed(conversation_id, message).await,
(Some(_), None) => attention.signin_resolved(conversation_id).await,
(Some(before), Some(after)) if before != after => {
attention.signin_needed(conversation_id, after).await
}
_ => {}
}
}
#[cfg(test)]
pub(crate) type RecordedAttention = (String, Option<String>, Option<String>);
#[cfg(test)]
#[derive(Default)]
pub(crate) struct RecordingAttention {
calls: std::sync::Mutex<Vec<RecordedAttention>>,
}
#[cfg(test)]
impl RecordingAttention {
pub(crate) fn calls(&self) -> Vec<RecordedAttention> {
self.calls.lock().unwrap().clone()
}
pub(crate) fn kinds(&self) -> Vec<String> {
self.calls().into_iter().map(|(kind, _, _)| kind).collect()
}
}
#[cfg(test)]
#[async_trait]
impl SignInAttention for RecordingAttention {
async fn signin_needed(&self, conversation_id: Option<&str>, message: &str) {
self.calls.lock().unwrap().push((
BROWSER_SIGNIN_NEEDED.to_string(),
conversation_id.map(str::to_string),
Some(message.to_string()),
));
}
async fn signin_resolved(&self, conversation_id: Option<&str>) {
self.calls.lock().unwrap().push((
BROWSER_SIGNIN_RESOLVED.to_string(),
conversation_id.map(str::to_string),
None,
));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn only_the_two_real_transitions_notify() {
let recorder = Arc::new(RecordingAttention::default());
let attention: Arc<dyn SignInAttention> = recorder.clone();
notify_signin_transition(&attention, Some("conv-1"), None, Some("Sign in at x")).await;
notify_signin_transition(
&attention,
Some("conv-1"),
Some("Sign in at x"),
Some("other"),
)
.await;
notify_signin_transition(&attention, Some("conv-1"), Some("other"), Some("other")).await;
notify_signin_transition(&attention, Some("conv-1"), None, None).await;
notify_signin_transition(&attention, Some("conv-1"), Some("Sign in at x"), None).await;
assert_eq!(
recorder.kinds(),
vec![
BROWSER_SIGNIN_NEEDED,
BROWSER_SIGNIN_NEEDED,
BROWSER_SIGNIN_RESOLVED
],
"a start, a changed prompt, and a resolution are news"
);
}
#[tokio::test]
async fn the_host_emitter_puts_both_kinds_on_the_host_event_channel() {
let host = Arc::new(HostState::new());
let attention: Arc<dyn SignInAttention> =
Arc::new(HostSignInAttention::new(Arc::clone(&host)));
attention
.signin_needed(Some("conv-1"), "Sign in at https://example.com/login")
.await;
attention.signin_resolved(Some("conv-1")).await;
attention.signin_needed(None, "Sign in to continue").await;
let events = host.events(10).await;
let kinds: Vec<&str> = events.iter().map(|e| e.kind.as_str()).rev().collect();
assert_eq!(
kinds,
vec![
BROWSER_SIGNIN_NEEDED,
BROWSER_SIGNIN_RESOLVED,
BROWSER_SIGNIN_NEEDED
]
);
let needed = events
.iter()
.rev()
.find(|e| e.kind == BROWSER_SIGNIN_NEEDED)
.expect("the needed event is recorded");
assert_eq!(needed.payload["conversation_id"], "conv-1");
assert_eq!(needed.payload["standing_session"], false);
assert_eq!(
needed.payload["message"],
"Sign in at https://example.com/login"
);
assert_eq!(needed.message, "Sign in at https://example.com/login");
let standing = events
.iter()
.find(|e| e.kind == BROWSER_SIGNIN_NEEDED)
.expect("the standing-session event is recorded");
assert_eq!(standing.payload["conversation_id"], "");
assert_eq!(standing.payload["standing_session"], true);
}
}