mod support;
use ciborium::Value as CborValue;
use serde_json::{Value, json};
use vantage_api_client::{GraphqlApi, GraphqlApiVistaSpec};
use vantage_vista::{Capability, FilterOp, Vista, VistaFactory};
use wiremock::matchers::{body_partial_json, method};
use wiremock::{Mock, MockServer, ResponseTemplate};
fn vista(server: &MockServer, extra: &str) -> Vista {
let yaml = format!(
"name: launches\ncolumns:\n id:\n type: string\n flags: [id]\n name:\n type: string\ngraphql:\n{extra}"
);
let spec: GraphqlApiVistaSpec = serde_yaml_ng::from_str(&yaml).unwrap();
GraphqlApi::builder(server.uri())
.http_client(support::test_client())
.build()
.vista_factory()
.build_from_spec(spec)
.unwrap()
}
fn rows(first: usize, n: usize) -> Value {
let rows: Vec<Value> = (first..first + n)
.map(|i| json!({ "id": i.to_string(), "name": format!("launch {i}") }))
.collect();
json!({ "data": { "launches": rows } })
}
async fn answer(server: &MockServer, expected: Value, response: Value) {
Mock::given(method("POST"))
.and(body_partial_json(expected))
.respond_with(ResponseTemplate::new(200).set_body_json(response))
.mount(server)
.await;
}
#[tokio::test]
async fn fetch_window_sends_the_absolute_offset() {
let server = MockServer::start().await;
answer(
&server,
json!({ "variables": { "limit": 7, "offset": 35 } }),
rows(35, 7),
)
.await;
let vista = vista(&server, " paginate: true\n");
let window = vista.fetch_window(35, 7).await.unwrap();
assert_eq!(window.rows.len(), 7);
assert_eq!(window.rows[0].0, "35");
assert_eq!(window.total, None);
}
#[tokio::test]
async fn fetch_cursor_is_refused() {
let server = MockServer::start().await;
let vista = vista(&server, " paginate: true\n");
let err = vista.fetch_cursor(None, 2).await.unwrap_err();
assert!(err.is_unsupported(), "{err}");
assert_eq!(support::requests(&server).await, 0);
}
#[tokio::test]
async fn hasura_paged_read_orders_by_id_when_no_order_is_set() {
let server = MockServer::start().await;
answer(
&server,
json!({
"query": "query ($limit: Int, $offset: Int) { launches(order_by: [{id: asc}], limit: $limit, offset: $offset) { id name } }"
}),
rows(0, 3),
)
.await;
let vista = vista(&server, " dialect: hasura\n paginate: true\n");
assert_eq!(vista.fetch_window(0, 3).await.unwrap().rows.len(), 3);
}
#[tokio::test]
async fn paging_capabilities_follow_paginate() {
let server = MockServer::start().await;
let caps = *vista(&server, " paginate: true\n").capabilities();
assert!(caps.has(Capability::Window));
assert!(!caps.has(Capability::Cursor));
let off = vista(&server, " dialect: generic\n");
assert!(!off.capabilities().has(Capability::Window));
assert!(off.fetch_window(1, 10).await.unwrap_err().is_unsupported());
assert_eq!(support::requests(&server).await, 0);
}
#[tokio::test]
async fn the_leading_window_without_paging_is_cut_from_the_listing() {
let server = MockServer::start().await;
answer(
&server,
json!({ "query": "query { launches { id name } }" }),
rows(0, 5),
)
.await;
let vista = vista(&server, " dialect: generic\n");
let page = vista.fetch_window(0, 2).await.unwrap();
let ids: Vec<&str> = page.rows.iter().map(|(id, _)| id.as_str()).collect();
assert_eq!(ids, ["0", "1"]);
assert_eq!(page.total, Some(5));
}
#[tokio::test]
async fn preview_posts_the_document_to_the_endpoint() {
let server = MockServer::start().await;
let preview = vista(&server, " dialect: generic\n").preview();
assert_eq!(
preview.query.as_deref(),
Some(format!("POST {}\nquery {{ launches {{ id name }} }}", server.uri()).as_str())
);
assert_eq!(preview.note, None);
}
#[tokio::test]
async fn paging_is_not_advertised_with_filter_false() {
let server = MockServer::start().await;
let vista = vista(&server, " paginate: true\n filter: false\n");
assert!(!vista.capabilities().has(Capability::Window));
assert!(
vista
.fetch_window(5, 10)
.await
.unwrap_err()
.is_unsupported()
);
assert_eq!(support::requests(&server).await, 0);
}
#[tokio::test]
async fn hasura_count_lists_by_default() {
let server = MockServer::start().await;
answer(
&server,
json!({ "query": "query { launches { id name } }" }),
rows(0, 4),
)
.await;
let vista = vista(&server, " dialect: hasura\n");
assert_eq!(vista.count().await.unwrap(), 4);
}
#[tokio::test]
async fn hasura_aggregate_count_applies_the_search() {
let server = MockServer::start().await;
answer(
&server,
json!({
"query": "query { launches_aggregate(where: {name: {_ilike: \"%fal%\"}}) { aggregate { count } } }"
}),
json!({ "data": { "launches_aggregate": { "aggregate": { "count": 5 } } } }),
)
.await;
let mut vista = vista(&server, " dialect: hasura\n count: aggregate\n");
vista.set_search(Some("fal")).unwrap();
assert_eq!(vista.count().await.unwrap(), 5);
}
#[tokio::test]
async fn rejected_aggregate_count_names_count_list() {
let server = MockServer::start().await;
answer(
&server,
json!({ "query": "query { launches_aggregate { aggregate { count } } }" }),
json!({ "errors": [{ "message": "field 'launches_aggregate' not found in type: 'query_root'" }] }),
)
.await;
let vista = vista(&server, " dialect: hasura\n count: aggregate\n");
let err = vista.count().await.unwrap_err().to_string();
assert!(err.contains("count: list"), "{err}");
assert!(err.contains("launches_aggregate"), "{err}");
assert_eq!(support::requests(&server).await, 1, "no fallback listing");
}
#[test]
fn an_unknown_count_mode_is_a_yaml_error() {
let yaml = "name: launches\ncolumns:\n id:\n type: string\n flags: [id]\ngraphql:\n count: exact\n";
assert!(serde_yaml_ng::from_str::<GraphqlApiVistaSpec>(yaml).is_err());
}
#[tokio::test]
async fn hasura_count_runs_the_aggregate_query_with_the_filter() {
let server = MockServer::start().await;
answer(
&server,
json!({
"query": "query { launches_aggregate(where: {name: {_eq: \"FalconSat\"}}) { aggregate { count } } }"
}),
json!({ "data": { "launches_aggregate": { "aggregate": { "count": 42 } } } }),
)
.await;
let mut vista = vista(&server, " dialect: hasura\n count: aggregate\n");
vista
.add_condition("name", FilterOp::Eq, CborValue::Text("FalconSat".into()))
.unwrap();
assert_eq!(vista.count().await.unwrap(), 42);
}
#[tokio::test]
async fn hasura_count_field_is_overridable() {
let server = MockServer::start().await;
answer(
&server,
json!({ "query": "query { launchesStats { aggregate { count } } }" }),
json!({ "data": { "launchesStats": { "aggregate": { "count": 7 } } } }),
)
.await;
let vista = vista(
&server,
" dialect: hasura\n count: aggregate\n count_field: launchesStats\n",
);
assert_eq!(vista.count().await.unwrap(), 7);
}
#[tokio::test]
async fn generic_count_still_lists_and_counts() {
let server = MockServer::start().await;
answer(
&server,
json!({ "query": "query { launches { id name } }" }),
rows(0, 3),
)
.await;
let vista = vista(&server, " dialect: generic\n");
assert_eq!(vista.count().await.unwrap(), 3);
}