mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-11 22:53:00 +00:00
Replace mockito with httpmock in fabro-hooks, fabro-openai-oauth, and fabro-tracker
mockito's Server::new_async() triggers macOS SCDynamicStoreCreateWithOptions via hyper-util (~300ms per test), which serializes on configd under workspace concurrency and causes 4s+ timeouts. httpmock avoids this path entirely. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6eb0c50d75
commit
1b9a6285e4
9 changed files with 555 additions and 546 deletions
41
Cargo.lock
generated
41
Cargo.lock
generated
|
|
@ -688,15 +688,6 @@ version = "1.0.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||
|
||||
[[package]]
|
||||
name = "colored"
|
||||
version = "3.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.5.0"
|
||||
|
|
@ -1703,7 +1694,7 @@ dependencies = [
|
|||
"fabro-model",
|
||||
"fabro-types",
|
||||
"fabro-util",
|
||||
"mockito",
|
||||
"httpmock",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"serde",
|
||||
|
|
@ -1791,7 +1782,7 @@ dependencies = [
|
|||
"axum",
|
||||
"base64",
|
||||
"hex",
|
||||
"mockito",
|
||||
"httpmock",
|
||||
"open",
|
||||
"rand 0.8.5",
|
||||
"reqwest",
|
||||
|
|
@ -1927,7 +1918,7 @@ version = "0.176.2"
|
|||
dependencies = [
|
||||
"async-trait",
|
||||
"fabro-github",
|
||||
"mockito",
|
||||
"httpmock",
|
||||
"reqwest",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
|
|
@ -2017,7 +2008,6 @@ dependencies = [
|
|||
"futures",
|
||||
"git2",
|
||||
"hex",
|
||||
"mockito",
|
||||
"predicates",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
|
|
@ -3579,31 +3569,6 @@ dependencies = [
|
|||
"parking_lot",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mockito"
|
||||
version = "1.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90820618712cab19cfc46b274c6c22546a82affcb3c3bdf0f29e3db8e1bb92c0"
|
||||
dependencies = [
|
||||
"assert-json-diff",
|
||||
"bytes",
|
||||
"colored",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"log",
|
||||
"pin-project-lite",
|
||||
"rand 0.9.2",
|
||||
"regex",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"similar",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "naive-timer"
|
||||
version = "0.2.0"
|
||||
|
|
|
|||
|
|
@ -28,6 +28,6 @@ tracing.workspace = true
|
|||
tokio-util.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
mockito = "1"
|
||||
httpmock = "0.8"
|
||||
tokio = { workspace = true, features = ["test-util", "macros"] }
|
||||
toml.workspace = true
|
||||
|
|
|
|||
|
|
@ -908,19 +908,21 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn http_hook_posts_json_and_parses_decision() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let server = httpmock::MockServer::start_async().await;
|
||||
let mock = server
|
||||
.mock("POST", "/hook")
|
||||
.match_header("content-type", "application/json")
|
||||
.with_status(200)
|
||||
.with_body(r#"{"decision": "skip", "reason": "not needed"}"#)
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST")
|
||||
.path("/hook")
|
||||
.header("content-type", "application/json");
|
||||
then.status(200)
|
||||
.body(r#"{"decision": "skip", "reason": "not needed"}"#);
|
||||
})
|
||||
.await;
|
||||
|
||||
let client = test_http_client();
|
||||
let decision = HookExecutorImpl::execute_http(
|
||||
&client,
|
||||
&format!("{}/hook", server.url()),
|
||||
&server.url("/hook"),
|
||||
None,
|
||||
&[],
|
||||
&TlsMode::Off,
|
||||
|
|
@ -941,18 +943,18 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn http_hook_empty_2xx_returns_proceed() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let server = httpmock::MockServer::start_async().await;
|
||||
let mock = server
|
||||
.mock("POST", "/hook")
|
||||
.with_status(200)
|
||||
.with_body("")
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST").path("/hook");
|
||||
then.status(200).body("");
|
||||
})
|
||||
.await;
|
||||
|
||||
let client = test_http_client();
|
||||
let decision = HookExecutorImpl::execute_http(
|
||||
&client,
|
||||
&format!("{}/hook", server.url()),
|
||||
&server.url("/hook"),
|
||||
None,
|
||||
&[],
|
||||
&TlsMode::Off,
|
||||
|
|
@ -968,18 +970,18 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn http_hook_non_2xx_returns_proceed() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let server = httpmock::MockServer::start_async().await;
|
||||
let mock = server
|
||||
.mock("POST", "/hook")
|
||||
.with_status(500)
|
||||
.with_body("Internal Server Error")
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST").path("/hook");
|
||||
then.status(500).body("Internal Server Error");
|
||||
})
|
||||
.await;
|
||||
|
||||
let client = test_http_client();
|
||||
let decision = HookExecutorImpl::execute_http(
|
||||
&client,
|
||||
&format!("{}/hook", server.url()),
|
||||
&server.url("/hook"),
|
||||
None,
|
||||
&[],
|
||||
&TlsMode::Off,
|
||||
|
|
@ -1015,13 +1017,14 @@ mod tests {
|
|||
async fn http_hook_sends_interpolated_headers() {
|
||||
let env = test_env(&[("FABRO_TEST_TOKEN", "my-secret")]);
|
||||
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let server = httpmock::MockServer::start_async().await;
|
||||
let mock = server
|
||||
.mock("POST", "/hook")
|
||||
.match_header("authorization", "Bearer my-secret")
|
||||
.with_status(200)
|
||||
.with_body("")
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST")
|
||||
.path("/hook")
|
||||
.header("authorization", "Bearer my-secret");
|
||||
then.status(200).body("");
|
||||
})
|
||||
.await;
|
||||
|
||||
let headers = HashMap::from([(
|
||||
|
|
@ -1032,7 +1035,7 @@ mod tests {
|
|||
let client = test_http_client();
|
||||
let decision = HookExecutorImpl::execute_http(
|
||||
&client,
|
||||
&format!("{}/hook", server.url()),
|
||||
&server.url("/hook"),
|
||||
Some(&headers),
|
||||
&["FABRO_TEST_TOKEN".to_string()],
|
||||
&TlsMode::Off,
|
||||
|
|
@ -1086,18 +1089,18 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn http_hook_allows_http_url_when_tls_off() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let server = httpmock::MockServer::start_async().await;
|
||||
let mock = server
|
||||
.mock("POST", "/hook")
|
||||
.with_status(200)
|
||||
.with_body("")
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST").path("/hook");
|
||||
then.status(200).body("");
|
||||
})
|
||||
.await;
|
||||
|
||||
let client = test_http_client();
|
||||
let decision = HookExecutorImpl::execute_http(
|
||||
&client,
|
||||
&format!("{}/hook", server.url()),
|
||||
&server.url("/hook"),
|
||||
None,
|
||||
&[],
|
||||
&TlsMode::Off,
|
||||
|
|
@ -1113,12 +1116,12 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn executor_dispatches_http_hook() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let server = httpmock::MockServer::start_async().await;
|
||||
let mock = server
|
||||
.mock("POST", "/hook")
|
||||
.with_status(200)
|
||||
.with_body(r#"{"decision": "proceed"}"#)
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST").path("/hook");
|
||||
then.status(200).body(r#"{"decision": "proceed"}"#);
|
||||
})
|
||||
.await;
|
||||
|
||||
let executor = HookExecutorImpl;
|
||||
|
|
@ -1127,7 +1130,7 @@ mod tests {
|
|||
event: HookEvent::StageStart,
|
||||
command: None,
|
||||
hook_type: Some(HookType::Http {
|
||||
url: format!("{}/hook", server.url()),
|
||||
url: server.url("/hook"),
|
||||
headers: None,
|
||||
allowed_env_vars: vec![],
|
||||
tls: TlsMode::Off,
|
||||
|
|
|
|||
|
|
@ -25,5 +25,5 @@ axum = "0.8"
|
|||
open = "5"
|
||||
|
||||
[dev-dependencies]
|
||||
mockito = "1"
|
||||
httpmock = "0.8"
|
||||
tokio = { workspace = true, features = ["test-util", "macros"] }
|
||||
|
|
|
|||
|
|
@ -777,45 +777,36 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn exchange_code_success() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let server = httpmock::MockServer::start_async().await;
|
||||
|
||||
let mock = server
|
||||
.mock("POST", "/oauth/token")
|
||||
.match_header("content-type", "application/x-www-form-urlencoded")
|
||||
.match_body(mockito::Matcher::AllOf(vec![
|
||||
mockito::Matcher::UrlEncoded(
|
||||
"grant_type".to_string(),
|
||||
"authorization_code".to_string(),
|
||||
),
|
||||
mockito::Matcher::UrlEncoded("client_id".to_string(), "test-client".to_string()),
|
||||
mockito::Matcher::UrlEncoded("code".to_string(), "test-code".to_string()),
|
||||
mockito::Matcher::UrlEncoded(
|
||||
"redirect_uri".to_string(),
|
||||
"http://localhost/cb".to_string(),
|
||||
),
|
||||
mockito::Matcher::UrlEncoded(
|
||||
"code_verifier".to_string(),
|
||||
"test-verifier".to_string(),
|
||||
),
|
||||
]))
|
||||
.with_status(200)
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body(
|
||||
serde_json::json!({
|
||||
"id_token": "id-tok",
|
||||
"access_token": "access-tok",
|
||||
"refresh_token": "refresh-tok",
|
||||
"expires_in": 3600
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST")
|
||||
.path("/oauth/token")
|
||||
.header("content-type", "application/x-www-form-urlencoded")
|
||||
.form_urlencoded_tuple("grant_type", "authorization_code")
|
||||
.form_urlencoded_tuple("client_id", "test-client")
|
||||
.form_urlencoded_tuple("code", "test-code")
|
||||
.form_urlencoded_tuple("redirect_uri", "http://localhost/cb")
|
||||
.form_urlencoded_tuple("code_verifier", "test-verifier");
|
||||
then.status(200)
|
||||
.header("content-type", "application/json")
|
||||
.body(
|
||||
serde_json::json!({
|
||||
"id_token": "id-tok",
|
||||
"access_token": "access-tok",
|
||||
"refresh_token": "refresh-tok",
|
||||
"expires_in": 3600
|
||||
})
|
||||
.to_string(),
|
||||
);
|
||||
})
|
||||
.await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let tokens = exchange_code_for_tokens(
|
||||
&client,
|
||||
&server.url(),
|
||||
&server.url(""),
|
||||
"test-client",
|
||||
"test-code",
|
||||
"http://localhost/cb",
|
||||
|
|
@ -834,19 +825,19 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn exchange_code_error_response() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let server = httpmock::MockServer::start_async().await;
|
||||
|
||||
server
|
||||
.mock("POST", "/oauth/token")
|
||||
.with_status(400)
|
||||
.with_body(r#"{"error": "invalid_grant"}"#)
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST").path("/oauth/token");
|
||||
then.status(400).body(r#"{"error": "invalid_grant"}"#);
|
||||
})
|
||||
.await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let err = exchange_code_for_tokens(
|
||||
&client,
|
||||
&server.url(),
|
||||
&server.url(""),
|
||||
"test-client",
|
||||
"bad-code",
|
||||
"http://localhost/cb",
|
||||
|
|
@ -864,36 +855,34 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn refresh_token_success() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let server = httpmock::MockServer::start_async().await;
|
||||
|
||||
let mock = server
|
||||
.mock("POST", "/oauth/token")
|
||||
.match_body(mockito::Matcher::AllOf(vec![
|
||||
mockito::Matcher::UrlEncoded("grant_type".to_string(), "refresh_token".to_string()),
|
||||
mockito::Matcher::UrlEncoded("client_id".to_string(), "test-client".to_string()),
|
||||
mockito::Matcher::UrlEncoded(
|
||||
"refresh_token".to_string(),
|
||||
"old-refresh-tok".to_string(),
|
||||
),
|
||||
]))
|
||||
.with_status(200)
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body(
|
||||
serde_json::json!({
|
||||
"id_token": "new-id",
|
||||
"access_token": "new-access",
|
||||
"refresh_token": "new-refresh",
|
||||
"expires_in": 7200
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST")
|
||||
.path("/oauth/token")
|
||||
.form_urlencoded_tuple("grant_type", "refresh_token")
|
||||
.form_urlencoded_tuple("client_id", "test-client")
|
||||
.form_urlencoded_tuple("refresh_token", "old-refresh-tok");
|
||||
then.status(200)
|
||||
.header("content-type", "application/json")
|
||||
.body(
|
||||
serde_json::json!({
|
||||
"id_token": "new-id",
|
||||
"access_token": "new-access",
|
||||
"refresh_token": "new-refresh",
|
||||
"expires_in": 7200
|
||||
})
|
||||
.to_string(),
|
||||
);
|
||||
})
|
||||
.await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let tokens = refresh_access_token(&client, &server.url(), "test-client", "old-refresh-tok")
|
||||
.await
|
||||
.unwrap();
|
||||
let tokens =
|
||||
refresh_access_token(&client, &server.url(""), "test-client", "old-refresh-tok")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(tokens.access_token, "new-access");
|
||||
assert_eq!(tokens.refresh_token, "new-refresh");
|
||||
|
|
@ -904,17 +893,17 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn refresh_token_error() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let server = httpmock::MockServer::start_async().await;
|
||||
|
||||
server
|
||||
.mock("POST", "/oauth/token")
|
||||
.with_status(401)
|
||||
.with_body(r#"{"error": "invalid_token"}"#)
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST").path("/oauth/token");
|
||||
then.status(401).body(r#"{"error": "invalid_token"}"#);
|
||||
})
|
||||
.await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let err = refresh_access_token(&client, &server.url(), "test-client", "expired-tok")
|
||||
let err = refresh_access_token(&client, &server.url(""), "test-client", "expired-tok")
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
|
|
@ -927,25 +916,27 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn initiate_device_flow_success() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let server = httpmock::MockServer::start_async().await;
|
||||
|
||||
let mock = server
|
||||
.mock("POST", "/api/accounts/deviceauth/usercode")
|
||||
.with_status(200)
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body(
|
||||
serde_json::json!({
|
||||
"device_auth_id": "dev-123",
|
||||
"user_code": "ABCD-1234",
|
||||
"interval": 5
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST")
|
||||
.path("/api/accounts/deviceauth/usercode");
|
||||
then.status(200)
|
||||
.header("content-type", "application/json")
|
||||
.body(
|
||||
serde_json::json!({
|
||||
"device_auth_id": "dev-123",
|
||||
"user_code": "ABCD-1234",
|
||||
"interval": 5
|
||||
})
|
||||
.to_string(),
|
||||
);
|
||||
})
|
||||
.await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let device = initiate_device_flow(&client, &server.url(), "test-client")
|
||||
let device = initiate_device_flow(&client, &server.url(""), "test-client")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
|
@ -958,17 +949,18 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn initiate_device_flow_error() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let server = httpmock::MockServer::start_async().await;
|
||||
|
||||
server
|
||||
.mock("POST", "/api/accounts/deviceauth/usercode")
|
||||
.with_status(500)
|
||||
.with_body("Internal Server Error")
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST")
|
||||
.path("/api/accounts/deviceauth/usercode");
|
||||
then.status(500).body("Internal Server Error");
|
||||
})
|
||||
.await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let err = initiate_device_flow(&client, &server.url(), "test-client")
|
||||
let err = initiate_device_flow(&client, &server.url(""), "test-client")
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
|
|
@ -977,30 +969,32 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn poll_device_flow_success() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let server = httpmock::MockServer::start_async().await;
|
||||
|
||||
server
|
||||
.mock("POST", "/api/accounts/deviceauth/token")
|
||||
.with_status(200)
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body(serde_json::json!({"code": "auth-code-123"}).to_string())
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST").path("/api/accounts/deviceauth/token");
|
||||
then.status(200)
|
||||
.header("content-type", "application/json")
|
||||
.body(serde_json::json!({"code": "auth-code-123"}).to_string());
|
||||
})
|
||||
.await;
|
||||
|
||||
server
|
||||
.mock("POST", "/oauth/token")
|
||||
.with_status(200)
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body(
|
||||
serde_json::json!({
|
||||
"id_token": "dev-id",
|
||||
"access_token": "dev-access",
|
||||
"refresh_token": "dev-refresh",
|
||||
"expires_in": 3600
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST").path("/oauth/token");
|
||||
then.status(200)
|
||||
.header("content-type", "application/json")
|
||||
.body(
|
||||
serde_json::json!({
|
||||
"id_token": "dev-id",
|
||||
"access_token": "dev-access",
|
||||
"refresh_token": "dev-refresh",
|
||||
"expires_in": 3600
|
||||
})
|
||||
.to_string(),
|
||||
);
|
||||
})
|
||||
.await;
|
||||
|
||||
let device = DeviceAuthResponse {
|
||||
|
|
@ -1010,7 +1004,7 @@ mod tests {
|
|||
};
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let tokens = poll_device_flow(&client, &server.url(), "test-client", &device)
|
||||
let tokens = poll_device_flow(&client, &server.url(""), "test-client", &device)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
|
@ -1019,14 +1013,15 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn poll_device_flow_expired() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let server = httpmock::MockServer::start_async().await;
|
||||
|
||||
server
|
||||
.mock("POST", "/api/accounts/deviceauth/token")
|
||||
.with_status(200)
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body(serde_json::json!({"error": "expired_token"}).to_string())
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST").path("/api/accounts/deviceauth/token");
|
||||
then.status(200)
|
||||
.header("content-type", "application/json")
|
||||
.body(serde_json::json!({"error": "expired_token"}).to_string());
|
||||
})
|
||||
.await;
|
||||
|
||||
let device = DeviceAuthResponse {
|
||||
|
|
@ -1036,7 +1031,7 @@ mod tests {
|
|||
};
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let err = poll_device_flow(&client, &server.url(), "test-client", &device)
|
||||
let err = poll_device_flow(&client, &server.url(""), "test-client", &device)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
|
|
|
|||
|
|
@ -20,5 +20,5 @@ tracing.workspace = true
|
|||
tokio = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
mockito = "1"
|
||||
httpmock = "0.8"
|
||||
tokio = { workspace = true, features = ["test-util", "macros"] }
|
||||
|
|
@ -521,23 +521,25 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn execute_github_graphql_success() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let server = httpmock::MockServer::start_async().await;
|
||||
|
||||
let mock = server
|
||||
.mock("POST", "/graphql")
|
||||
.match_header("Authorization", "Bearer test-token")
|
||||
.match_header("Content-Type", "application/json")
|
||||
.match_header("User-Agent", "fabro")
|
||||
.with_status(200)
|
||||
.with_body(r#"{"data": {"viewer": {"id": "U_abc"}}}"#)
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST")
|
||||
.path("/graphql")
|
||||
.header("Authorization", "Bearer test-token")
|
||||
.header("Content-Type", "application/json")
|
||||
.header("User-Agent", "fabro");
|
||||
then.status(200)
|
||||
.body(r#"{"data": {"viewer": {"id": "U_abc"}}}"#);
|
||||
})
|
||||
.await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let result = execute_github_graphql(
|
||||
&client,
|
||||
"test-token",
|
||||
&format!("{}/graphql", server.url()),
|
||||
&server.url("/graphql"),
|
||||
"query { viewer { id } }",
|
||||
serde_json::json!({}),
|
||||
)
|
||||
|
|
@ -550,20 +552,20 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn execute_github_graphql_http_error() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let server = httpmock::MockServer::start_async().await;
|
||||
|
||||
server
|
||||
.mock("POST", "/graphql")
|
||||
.with_status(401)
|
||||
.with_body("Unauthorized")
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST").path("/graphql");
|
||||
then.status(401).body("Unauthorized");
|
||||
})
|
||||
.await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let err = execute_github_graphql(
|
||||
&client,
|
||||
"bad-token",
|
||||
&format!("{}/graphql", server.url()),
|
||||
&server.url("/graphql"),
|
||||
"query { viewer { id } }",
|
||||
serde_json::json!({}),
|
||||
)
|
||||
|
|
@ -575,20 +577,21 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn execute_github_graphql_errors_array() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let server = httpmock::MockServer::start_async().await;
|
||||
|
||||
server
|
||||
.mock("POST", "/graphql")
|
||||
.with_status(200)
|
||||
.with_body(r#"{"data": null, "errors": [{"message": "Not found"}]}"#)
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST").path("/graphql");
|
||||
then.status(200)
|
||||
.body(r#"{"data": null, "errors": [{"message": "Not found"}]}"#);
|
||||
})
|
||||
.await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let err = execute_github_graphql(
|
||||
&client,
|
||||
"token",
|
||||
&format!("{}/graphql", server.url()),
|
||||
&server.url("/graphql"),
|
||||
"query { bad }",
|
||||
serde_json::json!({}),
|
||||
)
|
||||
|
|
@ -600,23 +603,24 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn execute_github_graphql_correct_headers() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let server = httpmock::MockServer::start_async().await;
|
||||
|
||||
let mock = server
|
||||
.mock("POST", "/graphql")
|
||||
.match_header("Authorization", "Bearer my-token")
|
||||
.match_header("Content-Type", "application/json")
|
||||
.match_header("User-Agent", "fabro")
|
||||
.with_status(200)
|
||||
.with_body(r#"{"data": {}}"#)
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST")
|
||||
.path("/graphql")
|
||||
.header("Authorization", "Bearer my-token")
|
||||
.header("Content-Type", "application/json")
|
||||
.header("User-Agent", "fabro");
|
||||
then.status(200).body(r#"{"data": {}}"#);
|
||||
})
|
||||
.await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
execute_github_graphql(
|
||||
&client,
|
||||
"my-token",
|
||||
&format!("{}/graphql", server.url()),
|
||||
&server.url("/graphql"),
|
||||
"query { viewer { id } }",
|
||||
serde_json::json!({}),
|
||||
)
|
||||
|
|
@ -707,33 +711,38 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn project_node_id_resolved_via_org() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let server = httpmock::MockServer::start_async().await;
|
||||
let pem = test_rsa_key();
|
||||
let tracker = mock_github_tracker(&server.url(), pem);
|
||||
let tracker = mock_github_tracker(&server.url(""), pem);
|
||||
|
||||
server
|
||||
.mock("GET", "/repos/owner/repo/installation")
|
||||
.with_status(200)
|
||||
.with_body(r#"{"id": 1}"#)
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("GET").path("/repos/owner/repo/installation");
|
||||
then.status(200).body(r#"{"id": 1}"#);
|
||||
})
|
||||
.await;
|
||||
server
|
||||
.mock("POST", "/app/installations/1/access_tokens")
|
||||
.with_status(201)
|
||||
.with_body(r#"{"token": "ghs_test"}"#)
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST")
|
||||
.path("/app/installations/1/access_tokens");
|
||||
then.status(201).body(r#"{"token": "ghs_test"}"#);
|
||||
})
|
||||
.await;
|
||||
server
|
||||
.mock("POST", "/graphql")
|
||||
.with_status(200)
|
||||
.with_body(org_project_node_id_response())
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST")
|
||||
.path("/graphql")
|
||||
.body_includes("organization(login:");
|
||||
then.status(200).body(org_project_node_id_response());
|
||||
})
|
||||
.await;
|
||||
server
|
||||
.mock("POST", "/graphql")
|
||||
.with_status(200)
|
||||
.with_body(empty_items_response())
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST")
|
||||
.path("/graphql")
|
||||
.body_includes("items(first:");
|
||||
then.status(200).body(empty_items_response());
|
||||
})
|
||||
.await;
|
||||
|
||||
let issues = tracker.fetch_candidate_issues(&["Todo"]).await.unwrap();
|
||||
|
|
@ -742,42 +751,50 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn project_node_id_falls_back_to_user() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let server = httpmock::MockServer::start_async().await;
|
||||
let pem = test_rsa_key();
|
||||
let tracker = mock_github_tracker(&server.url(), pem);
|
||||
let tracker = mock_github_tracker(&server.url(""), pem);
|
||||
|
||||
server
|
||||
.mock("GET", "/repos/owner/repo/installation")
|
||||
.with_status(200)
|
||||
.with_body(r#"{"id": 1}"#)
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("GET").path("/repos/owner/repo/installation");
|
||||
then.status(200).body(r#"{"id": 1}"#);
|
||||
})
|
||||
.await;
|
||||
server
|
||||
.mock("POST", "/app/installations/1/access_tokens")
|
||||
.with_status(201)
|
||||
.with_body(r#"{"token": "ghs_test"}"#)
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST")
|
||||
.path("/app/installations/1/access_tokens");
|
||||
then.status(201).body(r#"{"token": "ghs_test"}"#);
|
||||
})
|
||||
.await;
|
||||
// Org query returns null → fall back to user
|
||||
server
|
||||
.mock("POST", "/graphql")
|
||||
.with_status(200)
|
||||
.with_body(r#"{"data": {"organization": null}}"#)
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST")
|
||||
.path("/graphql")
|
||||
.body_includes("organization(login:");
|
||||
then.status(200).body(r#"{"data": {"organization": null}}"#);
|
||||
})
|
||||
.await;
|
||||
// User query succeeds
|
||||
server
|
||||
.mock("POST", "/graphql")
|
||||
.with_status(200)
|
||||
.with_body(r#"{"data": {"user": {"projectV2": {"id": "PVT_user1"}}}}"#)
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST")
|
||||
.path("/graphql")
|
||||
.body_includes("user(login:");
|
||||
then.status(200)
|
||||
.body(r#"{"data": {"user": {"projectV2": {"id": "PVT_user1"}}}}"#);
|
||||
})
|
||||
.await;
|
||||
// Items page (empty)
|
||||
server
|
||||
.mock("POST", "/graphql")
|
||||
.with_status(200)
|
||||
.with_body(empty_items_response())
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST")
|
||||
.path("/graphql")
|
||||
.body_includes("items(first:");
|
||||
then.status(200).body(empty_items_response());
|
||||
})
|
||||
.await;
|
||||
|
||||
let issues = tracker.fetch_candidate_issues(&["Todo"]).await.unwrap();
|
||||
|
|
@ -790,27 +807,29 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn github_tracker_fetch_viewer_id_success() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let server = httpmock::MockServer::start_async().await;
|
||||
let pem = test_rsa_key();
|
||||
let tracker = mock_github_tracker(&server.url(), pem);
|
||||
let tracker = mock_github_tracker(&server.url(""), pem);
|
||||
|
||||
server
|
||||
.mock("GET", "/repos/owner/repo/installation")
|
||||
.with_status(200)
|
||||
.with_body(r#"{"id": 1}"#)
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("GET").path("/repos/owner/repo/installation");
|
||||
then.status(200).body(r#"{"id": 1}"#);
|
||||
})
|
||||
.await;
|
||||
server
|
||||
.mock("POST", "/app/installations/1/access_tokens")
|
||||
.with_status(201)
|
||||
.with_body(r#"{"token": "ghs_test"}"#)
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST")
|
||||
.path("/app/installations/1/access_tokens");
|
||||
then.status(201).body(r#"{"token": "ghs_test"}"#);
|
||||
})
|
||||
.await;
|
||||
server
|
||||
.mock("POST", "/graphql")
|
||||
.with_status(200)
|
||||
.with_body(r#"{"data": {"viewer": {"id": "U_xyz"}}}"#)
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST").path("/graphql");
|
||||
then.status(200)
|
||||
.body(r#"{"data": {"viewer": {"id": "U_xyz"}}}"#);
|
||||
})
|
||||
.await;
|
||||
|
||||
let id = tracker.fetch_viewer_id().await.unwrap();
|
||||
|
|
@ -823,27 +842,29 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn github_tracker_create_comment_success() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let server = httpmock::MockServer::start_async().await;
|
||||
let pem = test_rsa_key();
|
||||
let tracker = mock_github_tracker(&server.url(), pem);
|
||||
let tracker = mock_github_tracker(&server.url(""), pem);
|
||||
|
||||
server
|
||||
.mock("GET", "/repos/owner/repo/installation")
|
||||
.with_status(200)
|
||||
.with_body(r#"{"id": 1}"#)
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("GET").path("/repos/owner/repo/installation");
|
||||
then.status(200).body(r#"{"id": 1}"#);
|
||||
})
|
||||
.await;
|
||||
server
|
||||
.mock("POST", "/app/installations/1/access_tokens")
|
||||
.with_status(201)
|
||||
.with_body(r#"{"token": "ghs_test"}"#)
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST")
|
||||
.path("/app/installations/1/access_tokens");
|
||||
then.status(201).body(r#"{"token": "ghs_test"}"#);
|
||||
})
|
||||
.await;
|
||||
server
|
||||
.mock("POST", "/graphql")
|
||||
.with_status(200)
|
||||
.with_body(r#"{"data": {"addComment": {"clientMutationId": null}}}"#)
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST").path("/graphql");
|
||||
then.status(200)
|
||||
.body(r#"{"data": {"addComment": {"clientMutationId": null}}}"#);
|
||||
})
|
||||
.await;
|
||||
|
||||
let issue = make_test_issue("In Progress");
|
||||
|
|
@ -856,43 +877,42 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn github_tracker_update_issue_state_success() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let server = httpmock::MockServer::start_async().await;
|
||||
let pem = test_rsa_key();
|
||||
let tracker = mock_github_tracker(&server.url(), pem);
|
||||
let tracker = mock_github_tracker(&server.url(""), pem);
|
||||
|
||||
server
|
||||
.mock("GET", "/repos/owner/repo/installation")
|
||||
.with_status(200)
|
||||
.with_body(r#"{"id": 1}"#)
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("GET").path("/repos/owner/repo/installation");
|
||||
then.status(200).body(r#"{"id": 1}"#);
|
||||
})
|
||||
.await;
|
||||
server
|
||||
.mock("POST", "/app/installations/1/access_tokens")
|
||||
.with_status(201)
|
||||
.with_body(r#"{"token": "ghs_test"}"#)
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST")
|
||||
.path("/app/installations/1/access_tokens");
|
||||
then.status(201).body(r#"{"token": "ghs_test"}"#);
|
||||
})
|
||||
.await;
|
||||
// Resolve project node ID (org path)
|
||||
server
|
||||
.mock("POST", "/graphql")
|
||||
.with_status(200)
|
||||
.with_body(org_project_node_id_response())
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST")
|
||||
.path("/graphql")
|
||||
.body_includes("organization(login:");
|
||||
then.status(200).body(org_project_node_id_response());
|
||||
})
|
||||
.await;
|
||||
// Field query
|
||||
server
|
||||
.mock("POST", "/graphql")
|
||||
.with_status(200)
|
||||
.with_body(r#"{"data": {"node": {"field": {"id": "FLD_1", "options": [{"id": "opt-done", "name": "Done"}, {"id": "opt-todo", "name": "Todo"}]}}}}"#)
|
||||
.create_async()
|
||||
.await;
|
||||
server.mock_async(|when, then| {
|
||||
when.method("POST").path("/graphql").body_includes("field(name:");
|
||||
then.status(200).body(r#"{"data": {"node": {"field": {"id": "FLD_1", "options": [{"id": "opt-done", "name": "Done"}, {"id": "opt-todo", "name": "Todo"}]}}}}"#);
|
||||
}).await;
|
||||
// Update mutation
|
||||
server
|
||||
.mock("POST", "/graphql")
|
||||
.with_status(200)
|
||||
.with_body(r#"{"data": {"updateProjectV2ItemFieldValue": {"projectV2Item": {"id": "PVTI_item1"}}}}"#)
|
||||
.create_async()
|
||||
.await;
|
||||
server.mock_async(|when, then| {
|
||||
when.method("POST").path("/graphql").body_includes("updateProjectV2ItemFieldValue");
|
||||
then.status(200).body(r#"{"data": {"updateProjectV2ItemFieldValue": {"projectV2Item": {"id": "PVTI_item1"}}}}"#);
|
||||
}).await;
|
||||
|
||||
let issue = make_test_issue("In Progress");
|
||||
tracker.update_issue_state(&issue, "Done").await.unwrap();
|
||||
|
|
@ -900,36 +920,37 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn github_tracker_update_issue_state_status_not_found() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let server = httpmock::MockServer::start_async().await;
|
||||
let pem = test_rsa_key();
|
||||
let tracker = mock_github_tracker(&server.url(), pem);
|
||||
let tracker = mock_github_tracker(&server.url(""), pem);
|
||||
|
||||
server
|
||||
.mock("GET", "/repos/owner/repo/installation")
|
||||
.with_status(200)
|
||||
.with_body(r#"{"id": 1}"#)
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("GET").path("/repos/owner/repo/installation");
|
||||
then.status(200).body(r#"{"id": 1}"#);
|
||||
})
|
||||
.await;
|
||||
server
|
||||
.mock("POST", "/app/installations/1/access_tokens")
|
||||
.with_status(201)
|
||||
.with_body(r#"{"token": "ghs_test"}"#)
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST")
|
||||
.path("/app/installations/1/access_tokens");
|
||||
then.status(201).body(r#"{"token": "ghs_test"}"#);
|
||||
})
|
||||
.await;
|
||||
// Resolve project node ID
|
||||
server
|
||||
.mock("POST", "/graphql")
|
||||
.with_status(200)
|
||||
.with_body(org_project_node_id_response())
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST")
|
||||
.path("/graphql")
|
||||
.body_includes("organization(login:");
|
||||
then.status(200).body(org_project_node_id_response());
|
||||
})
|
||||
.await;
|
||||
// Field query — options don't include "Nonexistent"
|
||||
server
|
||||
.mock("POST", "/graphql")
|
||||
.with_status(200)
|
||||
.with_body(r#"{"data": {"node": {"field": {"id": "FLD_1", "options": [{"id": "opt-done", "name": "Done"}]}}}}"#)
|
||||
.create_async()
|
||||
.await;
|
||||
server.mock_async(|when, then| {
|
||||
when.method("POST").path("/graphql").body_includes("field(name:");
|
||||
then.status(200).body(r#"{"data": {"node": {"field": {"id": "FLD_1", "options": [{"id": "opt-done", "name": "Done"}]}}}}"#);
|
||||
}).await;
|
||||
|
||||
let issue = make_test_issue("Todo");
|
||||
let err = tracker
|
||||
|
|
@ -946,33 +967,38 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn github_tracker_fetch_candidate_issues_single_page() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let server = httpmock::MockServer::start_async().await;
|
||||
let pem = test_rsa_key();
|
||||
let tracker = mock_github_tracker(&server.url(), pem);
|
||||
let tracker = mock_github_tracker(&server.url(""), pem);
|
||||
|
||||
server
|
||||
.mock("GET", "/repos/owner/repo/installation")
|
||||
.with_status(200)
|
||||
.with_body(r#"{"id": 1}"#)
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("GET").path("/repos/owner/repo/installation");
|
||||
then.status(200).body(r#"{"id": 1}"#);
|
||||
})
|
||||
.await;
|
||||
server
|
||||
.mock("POST", "/app/installations/1/access_tokens")
|
||||
.with_status(201)
|
||||
.with_body(r#"{"token": "ghs_test"}"#)
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST")
|
||||
.path("/app/installations/1/access_tokens");
|
||||
then.status(201).body(r#"{"token": "ghs_test"}"#);
|
||||
})
|
||||
.await;
|
||||
server
|
||||
.mock("POST", "/graphql")
|
||||
.with_status(200)
|
||||
.with_body(org_project_node_id_response())
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST")
|
||||
.path("/graphql")
|
||||
.body_includes("organization(login:");
|
||||
then.status(200).body(org_project_node_id_response());
|
||||
})
|
||||
.await;
|
||||
server
|
||||
.mock("POST", "/graphql")
|
||||
.with_status(200)
|
||||
.with_body(single_item_response("In Progress"))
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST")
|
||||
.path("/graphql")
|
||||
.body_includes("items(first:");
|
||||
then.status(200).body(single_item_response("In Progress"));
|
||||
})
|
||||
.await;
|
||||
|
||||
let issues = tracker
|
||||
|
|
@ -992,33 +1018,38 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn github_tracker_fetch_candidate_issues_empty() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let server = httpmock::MockServer::start_async().await;
|
||||
let pem = test_rsa_key();
|
||||
let tracker = mock_github_tracker(&server.url(), pem);
|
||||
let tracker = mock_github_tracker(&server.url(""), pem);
|
||||
|
||||
server
|
||||
.mock("GET", "/repos/owner/repo/installation")
|
||||
.with_status(200)
|
||||
.with_body(r#"{"id": 1}"#)
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("GET").path("/repos/owner/repo/installation");
|
||||
then.status(200).body(r#"{"id": 1}"#);
|
||||
})
|
||||
.await;
|
||||
server
|
||||
.mock("POST", "/app/installations/1/access_tokens")
|
||||
.with_status(201)
|
||||
.with_body(r#"{"token": "ghs_test"}"#)
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST")
|
||||
.path("/app/installations/1/access_tokens");
|
||||
then.status(201).body(r#"{"token": "ghs_test"}"#);
|
||||
})
|
||||
.await;
|
||||
server
|
||||
.mock("POST", "/graphql")
|
||||
.with_status(200)
|
||||
.with_body(org_project_node_id_response())
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST")
|
||||
.path("/graphql")
|
||||
.body_includes("organization(login:");
|
||||
then.status(200).body(org_project_node_id_response());
|
||||
})
|
||||
.await;
|
||||
server
|
||||
.mock("POST", "/graphql")
|
||||
.with_status(200)
|
||||
.with_body(empty_items_response())
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST")
|
||||
.path("/graphql")
|
||||
.body_includes("items(first:");
|
||||
then.status(200).body(empty_items_response());
|
||||
})
|
||||
.await;
|
||||
|
||||
let issues = tracker.fetch_candidate_issues(&["Todo"]).await.unwrap();
|
||||
|
|
@ -1027,9 +1058,9 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn github_tracker_fetch_candidate_issues_status_filtering() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let server = httpmock::MockServer::start_async().await;
|
||||
let pem = test_rsa_key();
|
||||
let tracker = mock_github_tracker(&server.url(), pem);
|
||||
let tracker = mock_github_tracker(&server.url(""), pem);
|
||||
|
||||
let items_body = serde_json::json!({
|
||||
"data": {
|
||||
|
|
@ -1065,28 +1096,33 @@ mod tests {
|
|||
.to_string();
|
||||
|
||||
server
|
||||
.mock("GET", "/repos/owner/repo/installation")
|
||||
.with_status(200)
|
||||
.with_body(r#"{"id": 1}"#)
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("GET").path("/repos/owner/repo/installation");
|
||||
then.status(200).body(r#"{"id": 1}"#);
|
||||
})
|
||||
.await;
|
||||
server
|
||||
.mock("POST", "/app/installations/1/access_tokens")
|
||||
.with_status(201)
|
||||
.with_body(r#"{"token": "ghs_test"}"#)
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST")
|
||||
.path("/app/installations/1/access_tokens");
|
||||
then.status(201).body(r#"{"token": "ghs_test"}"#);
|
||||
})
|
||||
.await;
|
||||
server
|
||||
.mock("POST", "/graphql")
|
||||
.with_status(200)
|
||||
.with_body(org_project_node_id_response())
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST")
|
||||
.path("/graphql")
|
||||
.body_includes("organization(login:");
|
||||
then.status(200).body(org_project_node_id_response());
|
||||
})
|
||||
.await;
|
||||
server
|
||||
.mock("POST", "/graphql")
|
||||
.with_status(200)
|
||||
.with_body(items_body)
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST")
|
||||
.path("/graphql")
|
||||
.body_includes("items(first:");
|
||||
then.status(200).body(items_body);
|
||||
})
|
||||
.await;
|
||||
|
||||
let issues = tracker
|
||||
|
|
@ -1104,9 +1140,9 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn github_tracker_fetch_issues_by_ids_ordering() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let server = httpmock::MockServer::start_async().await;
|
||||
let pem = test_rsa_key();
|
||||
let tracker = mock_github_tracker(&server.url(), pem);
|
||||
let tracker = mock_github_tracker(&server.url(""), pem);
|
||||
|
||||
// Page returns issues in reverse order of what we request
|
||||
let items_body = serde_json::json!({
|
||||
|
|
@ -1143,28 +1179,33 @@ mod tests {
|
|||
.to_string();
|
||||
|
||||
server
|
||||
.mock("GET", "/repos/owner/repo/installation")
|
||||
.with_status(200)
|
||||
.with_body(r#"{"id": 1}"#)
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("GET").path("/repos/owner/repo/installation");
|
||||
then.status(200).body(r#"{"id": 1}"#);
|
||||
})
|
||||
.await;
|
||||
server
|
||||
.mock("POST", "/app/installations/1/access_tokens")
|
||||
.with_status(201)
|
||||
.with_body(r#"{"token": "ghs_test"}"#)
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST")
|
||||
.path("/app/installations/1/access_tokens");
|
||||
then.status(201).body(r#"{"token": "ghs_test"}"#);
|
||||
})
|
||||
.await;
|
||||
server
|
||||
.mock("POST", "/graphql")
|
||||
.with_status(200)
|
||||
.with_body(org_project_node_id_response())
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST")
|
||||
.path("/graphql")
|
||||
.body_includes("organization(login:");
|
||||
then.status(200).body(org_project_node_id_response());
|
||||
})
|
||||
.await;
|
||||
server
|
||||
.mock("POST", "/graphql")
|
||||
.with_status(200)
|
||||
.with_body(items_body)
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST")
|
||||
.path("/graphql")
|
||||
.body_includes("items(first:");
|
||||
then.status(200).body(items_body);
|
||||
})
|
||||
.await;
|
||||
|
||||
// Request in A, B order — should get back in A, B order despite page returning B, A
|
||||
|
|
|
|||
|
|
@ -577,16 +577,18 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn execute_graphql_success() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let config = mock_config(&server.url());
|
||||
let server = httpmock::MockServer::start_async().await;
|
||||
let config = mock_config(&server.url(""));
|
||||
|
||||
let mock = server
|
||||
.mock("POST", "/graphql")
|
||||
.match_header("Authorization", "lin_api_test123")
|
||||
.match_header("Content-Type", "application/json")
|
||||
.with_status(200)
|
||||
.with_body(r#"{"data": {"viewer": {"id": "user-1"}}}"#)
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST")
|
||||
.path("/graphql")
|
||||
.header("Authorization", "lin_api_test123")
|
||||
.header("Content-Type", "application/json");
|
||||
then.status(200)
|
||||
.body(r#"{"data": {"viewer": {"id": "user-1"}}}"#);
|
||||
})
|
||||
.await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
|
|
@ -605,14 +607,14 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn execute_graphql_http_401() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let config = mock_config(&server.url());
|
||||
let server = httpmock::MockServer::start_async().await;
|
||||
let config = mock_config(&server.url(""));
|
||||
|
||||
server
|
||||
.mock("POST", "/graphql")
|
||||
.with_status(401)
|
||||
.with_body("Unauthorized")
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST").path("/graphql");
|
||||
then.status(401).body("Unauthorized");
|
||||
})
|
||||
.await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
|
|
@ -630,14 +632,14 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn execute_graphql_http_500() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let config = mock_config(&server.url());
|
||||
let server = httpmock::MockServer::start_async().await;
|
||||
let config = mock_config(&server.url(""));
|
||||
|
||||
server
|
||||
.mock("POST", "/graphql")
|
||||
.with_status(500)
|
||||
.with_body("Internal Server Error")
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST").path("/graphql");
|
||||
then.status(500).body("Internal Server Error");
|
||||
})
|
||||
.await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
|
|
@ -655,14 +657,15 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn execute_graphql_errors_array() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let config = mock_config(&server.url());
|
||||
let server = httpmock::MockServer::start_async().await;
|
||||
let config = mock_config(&server.url(""));
|
||||
|
||||
server
|
||||
.mock("POST", "/graphql")
|
||||
.with_status(200)
|
||||
.with_body(r#"{"data": null, "errors": [{"message": "Variable not found"}]}"#)
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST").path("/graphql");
|
||||
then.status(200)
|
||||
.body(r#"{"data": null, "errors": [{"message": "Variable not found"}]}"#);
|
||||
})
|
||||
.await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
|
|
@ -675,16 +678,17 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn execute_graphql_correct_headers() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let config = mock_config(&server.url());
|
||||
let server = httpmock::MockServer::start_async().await;
|
||||
let config = mock_config(&server.url(""));
|
||||
|
||||
let mock = server
|
||||
.mock("POST", "/graphql")
|
||||
.match_header("Authorization", "lin_api_test123")
|
||||
.match_header("Content-Type", "application/json")
|
||||
.with_status(200)
|
||||
.with_body(r#"{"data": {}}"#)
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST")
|
||||
.path("/graphql")
|
||||
.header("Authorization", "lin_api_test123")
|
||||
.header("Content-Type", "application/json");
|
||||
then.status(200).body(r#"{"data": {}}"#);
|
||||
})
|
||||
.await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
|
|
@ -706,14 +710,15 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn fetch_viewer_id_success() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let config = mock_config(&server.url());
|
||||
let server = httpmock::MockServer::start_async().await;
|
||||
let config = mock_config(&server.url(""));
|
||||
|
||||
server
|
||||
.mock("POST", "/graphql")
|
||||
.with_status(200)
|
||||
.with_body(r#"{"data": {"viewer": {"id": "user-abc"}}}"#)
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST").path("/graphql");
|
||||
then.status(200)
|
||||
.body(r#"{"data": {"viewer": {"id": "user-abc"}}}"#);
|
||||
})
|
||||
.await;
|
||||
|
||||
let tracker = LinearTracker::new(config, reqwest::Client::new(), "proj".to_string());
|
||||
|
|
@ -723,14 +728,14 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn fetch_viewer_id_error() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let config = mock_config(&server.url());
|
||||
let server = httpmock::MockServer::start_async().await;
|
||||
let config = mock_config(&server.url(""));
|
||||
|
||||
server
|
||||
.mock("POST", "/graphql")
|
||||
.with_status(401)
|
||||
.with_body("Unauthorized")
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST").path("/graphql");
|
||||
then.status(401).body("Unauthorized");
|
||||
})
|
||||
.await;
|
||||
|
||||
let tracker = LinearTracker::new(config, reqwest::Client::new(), "proj".to_string());
|
||||
|
|
@ -744,14 +749,15 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn create_comment_success() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let config = mock_config(&server.url());
|
||||
let server = httpmock::MockServer::start_async().await;
|
||||
let config = mock_config(&server.url(""));
|
||||
|
||||
server
|
||||
.mock("POST", "/graphql")
|
||||
.with_status(200)
|
||||
.with_body(r#"{"data": {"commentCreate": {"success": true}}}"#)
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST").path("/graphql");
|
||||
then.status(200)
|
||||
.body(r#"{"data": {"commentCreate": {"success": true}}}"#);
|
||||
})
|
||||
.await;
|
||||
|
||||
let tracker = LinearTracker::new(config, reqwest::Client::new(), "proj".to_string());
|
||||
|
|
@ -761,14 +767,15 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn create_comment_returns_false() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let config = mock_config(&server.url());
|
||||
let server = httpmock::MockServer::start_async().await;
|
||||
let config = mock_config(&server.url(""));
|
||||
|
||||
server
|
||||
.mock("POST", "/graphql")
|
||||
.with_status(200)
|
||||
.with_body(r#"{"data": {"commentCreate": {"success": false}}}"#)
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST").path("/graphql");
|
||||
then.status(200)
|
||||
.body(r#"{"data": {"commentCreate": {"success": false}}}"#);
|
||||
})
|
||||
.await;
|
||||
|
||||
let tracker = LinearTracker::new(config, reqwest::Client::new(), "proj".to_string());
|
||||
|
|
@ -783,25 +790,25 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn update_issue_state_success() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let config = mock_config(&server.url());
|
||||
let server = httpmock::MockServer::start_async().await;
|
||||
let config = mock_config(&server.url(""));
|
||||
|
||||
// First call: resolve state name to ID
|
||||
let resolve_mock = server
|
||||
.mock("POST", "/graphql")
|
||||
.with_status(200)
|
||||
.with_body(
|
||||
r#"{"data": {"issue": {"team": {"states": {"nodes": [{"id": "state-done"}]}}}}}"#,
|
||||
)
|
||||
.create_async()
|
||||
.await;
|
||||
let resolve_mock = server.mock_async(|when, then| {
|
||||
when.method("POST").path("/graphql").body_includes("team");
|
||||
then.status(200)
|
||||
.body(r#"{"data": {"issue": {"team": {"states": {"nodes": [{"id": "state-done"}]}}}}}"#);
|
||||
}).await;
|
||||
|
||||
// Second call: update issue
|
||||
let update_mock = server
|
||||
.mock("POST", "/graphql")
|
||||
.with_status(200)
|
||||
.with_body(r#"{"data": {"issueUpdate": {"success": true}}}"#)
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST")
|
||||
.path("/graphql")
|
||||
.body_includes("issueUpdate");
|
||||
then.status(200)
|
||||
.body(r#"{"data": {"issueUpdate": {"success": true}}}"#);
|
||||
})
|
||||
.await;
|
||||
|
||||
let tracker = LinearTracker::new(config, reqwest::Client::new(), "proj".to_string());
|
||||
|
|
@ -814,14 +821,15 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn update_issue_state_not_found() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let config = mock_config(&server.url());
|
||||
let server = httpmock::MockServer::start_async().await;
|
||||
let config = mock_config(&server.url(""));
|
||||
|
||||
server
|
||||
.mock("POST", "/graphql")
|
||||
.with_status(200)
|
||||
.with_body(r#"{"data": {"issue": {"team": {"states": {"nodes": []}}}}}"#)
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST").path("/graphql");
|
||||
then.status(200)
|
||||
.body(r#"{"data": {"issue": {"team": {"states": {"nodes": []}}}}}"#);
|
||||
})
|
||||
.await;
|
||||
|
||||
let tracker = LinearTracker::new(config, reqwest::Client::new(), "proj".to_string());
|
||||
|
|
@ -840,8 +848,8 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn fetch_candidate_issues_single_page() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let config = mock_config(&server.url());
|
||||
let server = httpmock::MockServer::start_async().await;
|
||||
let config = mock_config(&server.url(""));
|
||||
|
||||
let issue = complete_issue_json();
|
||||
let body = serde_json::json!({
|
||||
|
|
@ -854,10 +862,10 @@ mod tests {
|
|||
});
|
||||
|
||||
server
|
||||
.mock("POST", "/graphql")
|
||||
.with_status(200)
|
||||
.with_body(body.to_string())
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST").path("/graphql");
|
||||
then.status(200).body(body.to_string());
|
||||
})
|
||||
.await;
|
||||
|
||||
let tracker = LinearTracker::new(config, reqwest::Client::new(), "my-project".to_string());
|
||||
|
|
@ -873,8 +881,8 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn fetch_candidate_issues_two_pages() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let config = mock_config(&server.url());
|
||||
let server = httpmock::MockServer::start_async().await;
|
||||
let config = mock_config(&server.url(""));
|
||||
|
||||
let issue1 = serde_json::json!({
|
||||
"id": "id-1", "identifier": "T-1", "title": "First",
|
||||
|
|
@ -905,22 +913,20 @@ mod tests {
|
|||
});
|
||||
|
||||
server
|
||||
.mock("POST", "/graphql")
|
||||
.match_body(mockito::Matcher::PartialJsonString(
|
||||
r#"{"variables":{"cursor":null}}"#.to_string(),
|
||||
))
|
||||
.with_status(200)
|
||||
.with_body(page1.to_string())
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST")
|
||||
.path("/graphql")
|
||||
.body_includes(r#""cursor":null"#);
|
||||
then.status(200).body(page1.to_string());
|
||||
})
|
||||
.await;
|
||||
server
|
||||
.mock("POST", "/graphql")
|
||||
.match_body(mockito::Matcher::PartialJsonString(
|
||||
r#"{"variables":{"cursor":"cursor-1"}}"#.to_string(),
|
||||
))
|
||||
.with_status(200)
|
||||
.with_body(page2.to_string())
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST")
|
||||
.path("/graphql")
|
||||
.body_includes(r#""cursor":"cursor-1""#);
|
||||
then.status(200).body(page2.to_string());
|
||||
})
|
||||
.await;
|
||||
|
||||
let tracker = LinearTracker::new(config, reqwest::Client::new(), "proj".to_string());
|
||||
|
|
@ -933,8 +939,8 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn fetch_candidate_issues_empty() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let config = mock_config(&server.url());
|
||||
let server = httpmock::MockServer::start_async().await;
|
||||
let config = mock_config(&server.url(""));
|
||||
|
||||
let body = serde_json::json!({
|
||||
"data": {
|
||||
|
|
@ -946,10 +952,10 @@ mod tests {
|
|||
});
|
||||
|
||||
server
|
||||
.mock("POST", "/graphql")
|
||||
.with_status(200)
|
||||
.with_body(body.to_string())
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST").path("/graphql");
|
||||
then.status(200).body(body.to_string());
|
||||
})
|
||||
.await;
|
||||
|
||||
let tracker = LinearTracker::new(config, reqwest::Client::new(), "proj".to_string());
|
||||
|
|
@ -964,8 +970,8 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn fetch_issues_by_ids_ordering() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let config = mock_config(&server.url());
|
||||
let server = httpmock::MockServer::start_async().await;
|
||||
let config = mock_config(&server.url(""));
|
||||
|
||||
// API returns in different order than requested
|
||||
let body = serde_json::json!({
|
||||
|
|
@ -988,10 +994,10 @@ mod tests {
|
|||
});
|
||||
|
||||
server
|
||||
.mock("POST", "/graphql")
|
||||
.with_status(200)
|
||||
.with_body(body.to_string())
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST").path("/graphql");
|
||||
then.status(200).body(body.to_string());
|
||||
})
|
||||
.await;
|
||||
|
||||
let tracker = LinearTracker::new(config, reqwest::Client::new(), "proj".to_string());
|
||||
|
|
@ -1007,29 +1013,13 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn fetch_issues_by_ids_batching() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let config = mock_config(&server.url());
|
||||
let server = httpmock::MockServer::start_async().await;
|
||||
let config = mock_config(&server.url(""));
|
||||
|
||||
// Create 51 IDs to trigger 2 batches
|
||||
let ids: Vec<String> = (0..51).map(|i| format!("id-{i}")).collect();
|
||||
let id_refs: Vec<&str> = ids.iter().map(|s| s.as_str()).collect();
|
||||
|
||||
// Second batch (ids 50..51) — registered first for LIFO
|
||||
let batch2_node = serde_json::json!({
|
||||
"id": "id-50", "identifier": "T-50", "title": "T50",
|
||||
"state": { "name": "Todo" }, "url": "https://linear.app/t/50",
|
||||
"labels": { "nodes": [] }, "inverseRelations": { "nodes": [] }
|
||||
});
|
||||
let batch2 = serde_json::json!({
|
||||
"data": { "issues": { "nodes": [batch2_node] } }
|
||||
});
|
||||
server
|
||||
.mock("POST", "/graphql")
|
||||
.with_status(200)
|
||||
.with_body(batch2.to_string())
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
// First batch (ids 0..50)
|
||||
let batch1_nodes: Vec<Value> = (0..50)
|
||||
.map(|i| {
|
||||
|
|
@ -1048,10 +1038,26 @@ mod tests {
|
|||
"data": { "issues": { "nodes": batch1_nodes } }
|
||||
});
|
||||
server
|
||||
.mock("POST", "/graphql")
|
||||
.with_status(200)
|
||||
.with_body(batch1.to_string())
|
||||
.create_async()
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST").path("/graphql").body_includes("id-49");
|
||||
then.status(200).body(batch1.to_string());
|
||||
})
|
||||
.await;
|
||||
|
||||
// Second batch (id 50)
|
||||
let batch2_node = serde_json::json!({
|
||||
"id": "id-50", "identifier": "T-50", "title": "T50",
|
||||
"state": { "name": "Todo" }, "url": "https://linear.app/t/50",
|
||||
"labels": { "nodes": [] }, "inverseRelations": { "nodes": [] }
|
||||
});
|
||||
let batch2 = serde_json::json!({
|
||||
"data": { "issues": { "nodes": [batch2_node] } }
|
||||
});
|
||||
server
|
||||
.mock_async(|when, then| {
|
||||
when.method("POST").path("/graphql").body_includes("id-50");
|
||||
then.status(200).body(batch2.to_string());
|
||||
})
|
||||
.await;
|
||||
|
||||
let tracker = LinearTracker::new(config, reqwest::Client::new(), "proj".to_string());
|
||||
|
|
|
|||
|
|
@ -65,7 +65,6 @@ reqwest.workspace = true
|
|||
base64.workspace = true
|
||||
toml.workspace = true
|
||||
fabro-mcp = { path = "../fabro-mcp" }
|
||||
mockito = "1"
|
||||
tokio = { workspace = true, features = ["test-util", "macros"] }
|
||||
tempfile = "3"
|
||||
dotenvy.workspace = true
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue