#![allow(clippy::unwrap_used)] #![allow(missing_docs)]
mod common;
use std::sync::{Arc, Mutex};
use fraiseql_core::{
db::{DatabaseAdapter, postgres::PostgresAdapter},
runtime::{Executor, MutationErrorShapeCheck, RuntimeConfig, mutation_error_shape_violations},
schema::CompiledSchema,
};
use serde_json::json;
const SCHEMA: &str = "issue_1425";
async fn provision(adapter: &PostgresAdapter) {
adapter.execute_raw_query("CREATE SCHEMA IF NOT EXISTS app").await.unwrap();
adapter
.execute_raw_query(
"DO $$ BEGIN CREATE TYPE app.mutation_error_class AS ENUM ('validation','conflict',\
'not_found','unauthorized','forbidden','internal','transaction_failed','timeout',\
'rate_limited','service_unavailable'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;",
)
.await
.unwrap();
adapter
.execute_raw_query(
"DO $$ BEGIN CREATE TYPE app.mutation_response AS (succeeded BOOLEAN, \
state_changed BOOLEAN, error_class app.mutation_error_class, status_detail TEXT, \
http_status SMALLINT, message TEXT, entity_id UUID, entity_type TEXT, entity JSONB, \
updated_fields TEXT[], cascade JSONB, error_detail JSONB, metadata JSONB); \
EXCEPTION WHEN duplicate_object THEN NULL; END $$;",
)
.await
.unwrap();
adapter
.execute_raw_query(&format!("DROP SCHEMA IF EXISTS {SCHEMA} CASCADE"))
.await
.unwrap();
adapter.execute_raw_query(&format!("CREATE SCHEMA {SCHEMA}")).await.unwrap();
adapter
.execute_raw_query(&format!(
"CREATE VIEW {SCHEMA}.v_thing AS SELECT NULL::uuid AS id, '{{}}'::jsonb AS data \
WHERE false"
))
.await
.unwrap();
adapter
.execute_raw_query(&format!(
"CREATE FUNCTION {SCHEMA}.fn_fail(p_mode text) \
RETURNS app.mutation_response LANGUAGE plpgsql AS $$ \
DECLARE v_response app.mutation_response; BEGIN \
v_response.succeeded := false; \
v_response.state_changed := false; \
v_response.error_class := 'validation'; \
v_response.message := 'The order cannot be placed'; \
v_response.error_detail := CASE p_mode \
WHEN 'no_errors' THEN NULL \
WHEN 'bad_identifier' THEN '{{\"errors\": [{{\"code\": 422, \
\"identifier\": \"order line_not_found\", \"message\": \"No line\"}}]}}'::jsonb \
ELSE '{{\"errors\": [{{\"code\": 422, \
\"identifier\": \"order_line_not_found\", \"message\": \"No line\"}}]}}'::jsonb \
END; \
RETURN v_response; END; $$"
))
.await
.unwrap();
}
fn schema() -> CompiledSchema {
serde_json::from_value(json!({
"naming_convention": "camelCase",
"types": [{
"name": "Thing",
"sql_source": format!("{SCHEMA}.v_thing"),
"fields": [{ "name": "id", "field_type": "ID" }]
}],
"queries": [],
"mutations": [{
"name": "placeOrder",
"return_type": "Thing",
"sql_source": format!("{SCHEMA}.fn_fail"),
"operation": "Custom",
"arguments": [{ "name": "mode", "arg_type": "String", "nullable": false }]
}]
}))
.expect("schema")
}
#[derive(Clone, Default)]
struct CapturedLog(Arc<Mutex<Vec<u8>>>);
impl std::io::Write for CapturedLog {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0.lock().unwrap().extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl CapturedLog {
fn take(&self) -> String {
let text = String::from_utf8(std::mem::take(&mut *self.0.lock().unwrap())).unwrap();
let mut lines = String::new();
for line in text.lines().filter(|l| l.contains("fraiseql_core::runtime::mutation_result:"))
{
lines.push_str(line);
lines.push('\n');
}
lines
}
}
async fn place(
executor: &Executor,
log: &CapturedLog,
mode: &str,
) -> (serde_json::Value, String, u64) {
log.take();
let before = mutation_error_shape_violations();
let response = executor
.execute(&format!("mutation {{ placeOrder(mode: \"{mode}\") {{ __typename }} }}"), None)
.await
.unwrap();
let counted = mutation_error_shape_violations() - before;
(response, log.take(), counted)
}
#[tokio::test]
async fn a_malformed_failure_is_warned_and_counted_only_when_the_check_is_on() {
let container = common::testcontainer::get_test_container().await;
let adapter = Arc::new(PostgresAdapter::new(&container.connection_string()).await.unwrap());
provision(&adapter).await;
let log = CapturedLog::default();
let writer = log.clone();
let subscriber = tracing_subscriber::fmt()
.with_writer(move || writer.clone())
.with_ansi(false)
.with_max_level(tracing::Level::WARN)
.finish();
let _guard = tracing::subscriber::set_default(subscriber);
let checked = Executor::with_config(
schema(),
Arc::clone(&adapter),
RuntimeConfig {
mutation_error_shape_check: MutationErrorShapeCheck::Warn,
..RuntimeConfig::default()
},
);
let unchecked = Executor::with_config(schema(), Arc::clone(&adapter), RuntimeConfig::default());
for (mode, problem) in [
("no_errors", "no `errors` array"),
("bad_identifier", "order line_not_found"),
] {
let (warned, logged, counted) = place(&checked, &log, mode).await;
assert_eq!(counted, 1, "{mode}: one malformed response, counted once");
assert_eq!(logged.matches(" WARN ").count(), 1, "{mode}: one warning, got {logged:?}");
assert!(
logged.contains("placeOrder") && logged.contains(problem),
"{mode}: the warning names the mutation and the problem: {logged:?}"
);
let (plain, logged, counted) = place(&unchecked, &log, mode).await;
assert_eq!((counted, logged.as_str()), (0, ""), "{mode}: the check is off by default");
assert_eq!(warned, plain, "{mode}: the check does not change the response");
}
let (_, logged, counted) = place(&checked, &log, "well_formed").await;
assert_eq!((counted, logged.as_str()), (0, ""), "a well-formed failure is not flagged");
adapter
.execute_raw_query(&format!("DROP SCHEMA IF EXISTS {SCHEMA} CASCADE"))
.await
.unwrap();
}