postjevsql.git / tests / on_error.rs
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}