use async_trait::async_trait;
use super::*;
fn creds() -> AtlassianCreds {
AtlassianCreds {
email: "bob@acme.com".to_string(),
token: "tok".to_string(), base_url: "https://acme.atlassian.net".to_string(),
}
}
struct FakeJira {
body: Result<String, ()>,
}
#[async_trait]
impl JiraTransport for FakeJira {
async fn search_jql(
&self,
_creds: &AtlassianCreds,
_jql: &str,
_max: u32,
) -> Result<String, ContextSourceError> {
self.body.clone().map_err(|_| ContextSourceError::Api {
src: SOURCE_NAME,
status: 500,
body: "boom".to_string(),
})
}
}
fn subject() -> ReviewSubject {
ReviewSubject {
owner: "acme".to_string(),
repo: "backend".to_string(),
title: "Add token refresh".to_string(),
identifiers: vec!["TokenStore".to_string()],
..Default::default()
}
}
#[test]
fn query_builds_jql_keyword() {
let jql = JiraSource::build_jql(&subject()).expect("has signal");
assert!(jql.contains("text ~ \"Add token refresh TokenStore\""));
assert!(jql.contains("ORDER BY updated DESC"));
}
#[test]
fn query_builds_jql_ticket_ids() {
let subj = ReviewSubject {
title: "PROJ-42 add token refresh".to_string(),
identifiers: vec!["TokenStore".to_string()],
..Default::default()
};
let jql = JiraSource::build_jql(&subj).expect("has signal");
assert_eq!(jql, "issueKey in (PROJ-42) ORDER BY updated DESC");
assert!(!jql.contains("text ~"));
}
#[test]
fn query_ticket_ids_scan_body_too() {
let subj = ReviewSubject {
title: "Add token refresh".to_string(),
body: "Implements PROJ-7 and PROJ-8.".to_string(),
..Default::default()
};
let jql = JiraSource::build_jql(&subj).expect("has signal");
assert_eq!(jql, "issueKey in (PROJ-7, PROJ-8) ORDER BY updated DESC");
}
#[test]
fn query_ticket_ids_dedup_and_capped() {
let ids: Vec<String> = (1..=10).map(|n| format!("PROJ-{n}")).collect();
let subj = ReviewSubject {
title: format!("{} PROJ-1", ids.join(" ")),
..Default::default()
};
let jql = JiraSource::build_jql(&subj).expect("has signal");
let inner = jql
.trim_start_matches("issueKey in (")
.split(')')
.next()
.unwrap();
assert_eq!(inner.split(", ").count(), MAX_RESULTS as usize);
}
#[test]
fn query_strips_quotes() {
let subj = ReviewSubject {
title: "Add \"quoted\" thing".to_string(),
..Default::default()
};
let jql = JiraSource::build_jql(&subj).unwrap();
assert!(!jql.contains("\"quoted\""));
}
#[test]
fn query_none_without_signal() {
let subj = ReviewSubject::default();
assert!(JiraSource::build_jql(&subj).is_none());
}
#[tokio::test]
async fn disabled_when_no_creds() {
let src = JiraSource::new(
true,
RetrievalMode::Live,
None,
Box::new(FakeJira {
body: Ok("{}".into()),
}),
);
let r = src.gather(&subject()).await;
assert!(matches!(r, Err(ContextSourceError::NotConfigured { .. })));
}
#[tokio::test]
async fn semantic_mode_errors() {
let src = JiraSource::new(
true,
RetrievalMode::Semantic,
Some(creds()),
Box::new(FakeJira {
body: Ok("{}".into()),
}),
);
let r = src.gather(&subject()).await;
assert!(matches!(
r,
Err(ContextSourceError::SemanticNotImplemented { src: "jira" })
));
}
#[tokio::test]
async fn gather_with_fake_transport() {
let body =
r#"{"issues":[{"key":"PROJ-7","fields":{"summary":"Fix","status":{"name":"Open"}}}]}"#;
let src = JiraSource::new(
true,
RetrievalMode::Live,
Some(creds()),
Box::new(FakeJira {
body: Ok(body.to_string()),
}),
);
let section = src.gather(&subject()).await.expect("ok");
assert_eq!(section.snippets.len(), 1);
assert_eq!(section.snippets[0].title, "PROJ-7 — Fix");
}
#[tokio::test]
async fn gather_propagates_api_error_for_logging() {
let src = JiraSource::new(
true,
RetrievalMode::Live,
Some(creds()),
Box::new(FakeJira { body: Err(()) }),
);
let r = src.gather(&subject()).await;
assert!(matches!(
r,
Err(ContextSourceError::Api { status: 500, .. })
));
}