1//! `jev.on_error` (contract *SQL surface*): under `unsure` a failed 2//! judgment is NULL, so a jev qual skips its row and never passes it; 3//! under `error`, the default, the statement fails. A failure that is not 4//! the remote failing on a row, such as a refused key, raises either way. 5 6use serde_json::{Value, json}; 7use tokio_postgres::error::SqlState; 8use support::mock_jev::{MockJev, Reply}; 9use support::{jev_instance, noul}; 10 11/// Answers 0.95 about ticket 1, and a 500 about ticket 2. 12async fn failing_on_ticket_2() -> MockJev { 13 MockJev::start(|req| { 14 let body: Value = serde_json::from_slice(&req.body).unwrap(); 15 if body["state"]["id"] == 2 { 16 Reply::json(500, json!({ "detail": { "error_type": "server_error", "message": "boom" } })) 17 .header("x-typesafe-request-id", "req-500") 18 } else { 19 Reply::json(200, noul(0.95)) 20 } 21 }) 22 .await 23} 24 25async fn with_ticket_2(client: &tokio_postgres::Client) { 26 client.batch_execute("INSERT INTO tickets VALUES (2, 'Where is my refund?')").await.unwrap(); 27} 28 29#[tokio::test(flavor = "multi_thread")] 30async fn unsure_skips_a_row_whose_judgment_failed() { 31 let mock = failing_on_ticket_2().await; 32 let (_pg, client) = jev_instance(&mock).await; 33 with_ticket_2(&client).await; 34 client.batch_execute("SET jev.on_error = unsure").await.unwrap(); 35 36 // Even a qual every answer passes skips the failed row. 37 let rows = client 38 .query("SELECT id FROM tickets t WHERE jev_prob(t, 'Urgent?') >= 0 ORDER BY id", &[]) 39 .await 40 .expect("a failed judgment does not fail the statement"); 41 let ids: Vec<i32> = rows.iter().map(|r| r.get(0)).collect(); 42 assert_eq!(ids, [1]); 43 44 let rows = client 45 .query("SELECT id, jev_prob(t, 'Urgent?') FROM tickets t ORDER BY id", &[]) 46 .await 47 .unwrap(); 48 let probs: Vec<(i32, Option<f64>)> = rows.iter().map(|r| (r.get(0), r.get(1))).collect(); 49 assert_eq!(probs, [(1, Some(0.95)), (2, None)]); 50 // The 500 was retried as usual before it counted as failed. 51 let failed = mock.requests().iter().filter(|r| serde_json::from_slice::<Value>(&r.body).unwrap()["state"]["id"] == 2).count(); 52 assert_eq!(failed, 6, "three attempts in each statement"); 53 54 // Nothing was stored for it: the next statement asks again. 55 let cached: i64 = client 56 .query_one("SELECT count(*) FROM jev_cache", &[]) 57 .await 58 .unwrap() 59 .get(0); 60 assert_eq!(cached, 1); 61} 62 63#[tokio::test(flavor = "multi_thread")] 64async fn error_fails_the_statement() { 65 let mock = failing_on_ticket_2().await; 66 let (_pg, client) = jev_instance(&mock).await; 67 with_ticket_2(&client).await; 68 let shown: String = client.query_one("SHOW jev.on_error", &[]).await.unwrap().get(0); 69 assert_eq!(shown, "error", "the default"); 70 71 let err = client 72 .query("SELECT id FROM tickets t WHERE jev_prob(t, 'Urgent?') >= 0", &[]) 73 .await 74 .expect_err("fails"); 75 let db = err.as_db_error().expect("an ERROR"); 76 assert_eq!(db.code(), &SqlState::EXTERNAL_ROUTINE_EXCEPTION); 77 assert!(db.message().contains("req-500"), "{}", db.message()); 78} 79 80#[tokio::test(flavor = "multi_thread")] 81async fn unsure_still_raises_a_refused_key() { 82 let mock = MockJev::start(|_| { 83 Reply::json(403, json!({ "detail": { "error_type": "forbidden", "message": "no key" } })) 84 }) 85 .await; 86 let (_pg, client) = jev_instance(&mock).await; 87 client.batch_execute("SET jev.on_error = unsure").await.unwrap(); 88 89 let err = client 90 .query("SELECT id FROM tickets t WHERE jev_prob(t, 'Urgent?') >= 0", &[]) 91 .await 92 .expect_err("every row would fail the same way, so it is not hidden"); 93 assert_eq!(err.as_db_error().expect("an ERROR").code(), &SqlState::INVALID_AUTHORIZATION_SPECIFICATION); 94} 95 96#[tokio::test(flavor = "multi_thread")] 97async fn on_error_takes_only_its_two_values() { 98 let mock = MockJev::start(|_| Reply::json(200, noul(0.95))).await; 99 let (_pg, client) = jev_instance(&mock).await; 100 let err = client.batch_execute("SET jev.on_error = skip").await.expect_err("refused"); 101 assert_eq!(err.as_db_error().expect("an ERROR").code(), &SqlState::INVALID_PARAMETER_VALUE); 102}