diff --git a/Cargo.lock b/Cargo.lock index 3435305b2..4d7f58fee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/lib/crates/fabro-hooks/Cargo.toml b/lib/crates/fabro-hooks/Cargo.toml index c6d10dd68..6e205905f 100644 --- a/lib/crates/fabro-hooks/Cargo.toml +++ b/lib/crates/fabro-hooks/Cargo.toml @@ -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 diff --git a/lib/crates/fabro-hooks/src/executor.rs b/lib/crates/fabro-hooks/src/executor.rs index 9272a5403..1aa43f186 100644 --- a/lib/crates/fabro-hooks/src/executor.rs +++ b/lib/crates/fabro-hooks/src/executor.rs @@ -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, diff --git a/lib/crates/fabro-openai-oauth/Cargo.toml b/lib/crates/fabro-openai-oauth/Cargo.toml index dcb4a14ba..6187ac998 100644 --- a/lib/crates/fabro-openai-oauth/Cargo.toml +++ b/lib/crates/fabro-openai-oauth/Cargo.toml @@ -25,5 +25,5 @@ axum = "0.8" open = "5" [dev-dependencies] -mockito = "1" +httpmock = "0.8" tokio = { workspace = true, features = ["test-util", "macros"] } diff --git a/lib/crates/fabro-openai-oauth/src/lib.rs b/lib/crates/fabro-openai-oauth/src/lib.rs index ad93a3ce5..6ee3c256f 100644 --- a/lib/crates/fabro-openai-oauth/src/lib.rs +++ b/lib/crates/fabro-openai-oauth/src/lib.rs @@ -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(); diff --git a/lib/crates/fabro-tracker/Cargo.toml b/lib/crates/fabro-tracker/Cargo.toml index a162ebe53..b1401ca82 100644 --- a/lib/crates/fabro-tracker/Cargo.toml +++ b/lib/crates/fabro-tracker/Cargo.toml @@ -20,5 +20,5 @@ tracing.workspace = true tokio = { workspace = true } [dev-dependencies] -mockito = "1" +httpmock = "0.8" tokio = { workspace = true, features = ["test-util", "macros"] } \ No newline at end of file diff --git a/lib/crates/fabro-tracker/src/github.rs b/lib/crates/fabro-tracker/src/github.rs index f7e63f6f5..76e9a78e6 100644 --- a/lib/crates/fabro-tracker/src/github.rs +++ b/lib/crates/fabro-tracker/src/github.rs @@ -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 diff --git a/lib/crates/fabro-tracker/src/linear.rs b/lib/crates/fabro-tracker/src/linear.rs index 46dcb9119..05222d68c 100644 --- a/lib/crates/fabro-tracker/src/linear.rs +++ b/lib/crates/fabro-tracker/src/linear.rs @@ -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 = (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 = (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()); diff --git a/lib/crates/fabro-workflows/Cargo.toml b/lib/crates/fabro-workflows/Cargo.toml index ecc0dafc0..b1ab9d329 100644 --- a/lib/crates/fabro-workflows/Cargo.toml +++ b/lib/crates/fabro-workflows/Cargo.toml @@ -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