236 lines
9.3 KiB
Rust
236 lines
9.3 KiB
Rust
// use crate::schema::Schema;
|
|
use crate::registry::REGISTRY;
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_json::{json, Value};
|
|
use pgrx::JsonB;
|
|
use std::{fs, path::Path};
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
struct ExpectedError {
|
|
code: String,
|
|
path: String,
|
|
message_contains: Option<String>,
|
|
cause: Option<Value>,
|
|
context: Option<Value>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
struct Group {
|
|
description: String,
|
|
schema: Option<Value>,
|
|
enums: Option<Value>,
|
|
types: Option<Value>,
|
|
puncs: Option<Value>,
|
|
tests: Vec<TestCase>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
struct TestCase {
|
|
description: String,
|
|
data: Value,
|
|
valid: bool,
|
|
action: Option<String>,
|
|
schema_id: Option<String>,
|
|
expect_errors: Option<Vec<ExpectedError>>,
|
|
}
|
|
|
|
include!("tests.rs");
|
|
|
|
fn load_remotes(dir: &Path, base_url: &str) {
|
|
if !dir.exists() { return; }
|
|
|
|
for entry in fs::read_dir(dir).expect("Failed to read remotes directory") {
|
|
let entry = entry.unwrap();
|
|
let path = entry.path();
|
|
let file_name = path.file_name().unwrap().to_str().unwrap();
|
|
|
|
if path.is_file() && file_name.ends_with(".json") {
|
|
let content = fs::read_to_string(&path).expect("Failed to read remote file");
|
|
if let Ok(schema_value) = serde_json::from_str::<Value>(&content) {
|
|
// Just check if it's a valid JSON value for a schema (object or bool)
|
|
if schema_value.is_object() || schema_value.is_boolean() {
|
|
let schema_id = format!("{}{}", base_url, file_name);
|
|
REGISTRY.insert(schema_id, schema_value);
|
|
}
|
|
}
|
|
} else if path.is_dir() {
|
|
load_remotes(&path, &format!("{}{}/", base_url, file_name));
|
|
}
|
|
}
|
|
|
|
// Mock the meta-schema for testing recursive refs
|
|
let meta_id = "https://json-schema.org/draft/2020-12/schema";
|
|
if REGISTRY.get(meta_id).is_none() {
|
|
// Just mock it as a permissive schema for now so refs resolve
|
|
REGISTRY.insert(meta_id.to_string(), json!({ "$id": meta_id }));
|
|
}
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
fn run_dir(dir: &Path, base_url: Option<&str>) -> (usize, usize) {
|
|
let mut file_count = 0;
|
|
let mut test_count = 0;
|
|
|
|
for entry in fs::read_dir(dir).expect("Failed to read directory") {
|
|
let entry = entry.unwrap();
|
|
let path = entry.path();
|
|
let file_name = path.file_name().unwrap().to_str().unwrap();
|
|
|
|
if path.is_file() && file_name.ends_with(".json") {
|
|
let count = run_file(&path, base_url);
|
|
test_count += count;
|
|
file_count += 1;
|
|
} else if path.is_dir() {
|
|
if !file_name.starts_with('.') && file_name != "optional" {
|
|
let (f, t) = run_dir(&path, base_url);
|
|
file_count += f;
|
|
test_count += t;
|
|
}
|
|
}
|
|
}
|
|
(file_count, test_count)
|
|
}
|
|
|
|
fn run_file(path: &Path, base_url: Option<&str>) -> usize {
|
|
let content = fs::read_to_string(path).expect("Failed to read file");
|
|
let groups: Vec<Group> = serde_json::from_str(&content).expect("Failed to parse JSON");
|
|
let filename = path.file_name().unwrap().to_str().unwrap();
|
|
|
|
let mut test_count = 0;
|
|
|
|
for group in groups {
|
|
// Handle JSPG setup if any JSPG fields are present
|
|
if group.enums.is_some() || group.types.is_some() || group.puncs.is_some() {
|
|
let enums = group.enums.clone().unwrap_or(json!([]));
|
|
let types = group.types.clone().unwrap_or(json!([]));
|
|
let puncs = group.puncs.clone().unwrap_or(json!([]));
|
|
// Use internal helper to register without clearing
|
|
let result = crate::cache_json_schemas(JsonB(enums), JsonB(types), JsonB(puncs));
|
|
if let Some(errors) = result.0.get("errors") {
|
|
// If the group has a test specifically for caching failures, don't panic here
|
|
let has_cache_test = group.tests.iter().any(|t| t.action.as_deref() == Some("cache"));
|
|
if !has_cache_test {
|
|
panic!("FAILED: File: {}, Group: {}\nCache failed: {:?}", filename, group.description, errors);
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut temp_id = "test_root".to_string();
|
|
if let Some(schema_value) = &group.schema {
|
|
temp_id = base_url.map(|b| format!("{}schema.json", b)).unwrap_or_else(|| "test_root".to_string());
|
|
|
|
if schema_value.is_object() || schema_value.is_boolean() {
|
|
REGISTRY.insert(temp_id.clone(), schema_value.clone());
|
|
}
|
|
} else {
|
|
// Fallback for JSPG style tests where the schema is in the puncs/types
|
|
let get_first_id = |items: &Option<Value>| {
|
|
items.as_ref()
|
|
.and_then(|v| v.as_array())
|
|
.and_then(|arr| arr.first())
|
|
.and_then(|item| item.get("schemas"))
|
|
.and_then(|v| v.as_array())
|
|
.and_then(|arr| arr.first())
|
|
.and_then(|sch| sch.get("$id"))
|
|
.and_then(|id| id.as_str())
|
|
.map(|s| s.to_string())
|
|
};
|
|
|
|
if let Some(id) = get_first_id(&group.puncs).or_else(|| get_first_id(&group.types)) {
|
|
temp_id = id;
|
|
}
|
|
}
|
|
|
|
for test in &group.tests {
|
|
test_count += 1;
|
|
let sid = test.schema_id.clone().unwrap_or_else(|| temp_id.clone());
|
|
let action = test.action.as_deref().unwrap_or("validate");
|
|
pgrx::notice!("Starting Test: {}", test.description);
|
|
|
|
let result = if action == "cache" {
|
|
let enums = group.enums.clone().unwrap_or(json!([]));
|
|
let types = group.types.clone().unwrap_or(json!([]));
|
|
let puncs = group.puncs.clone().unwrap_or(json!([]));
|
|
crate::cache_json_schemas(JsonB(enums), JsonB(types), JsonB(puncs))
|
|
} else {
|
|
crate::validate_json_schema(&sid, JsonB(test.data.clone()))
|
|
};
|
|
let is_success = result.0.get("response").is_some();
|
|
pgrx::notice!("TEST: file={}, group={}, test={}, valid={}, outcome={}",
|
|
filename,
|
|
&group.description,
|
|
&test.description,
|
|
test.valid,
|
|
if is_success { "SUCCESS" } else { "ERRORS" }
|
|
);
|
|
|
|
if is_success != test.valid {
|
|
if let Some(errs) = result.0.get("errors") {
|
|
panic!(
|
|
"FAILED: File: {}, Group: {}, Test: {}\nExpected valid: {}, got ERRORS: {:?}",
|
|
filename,
|
|
group.description,
|
|
test.description,
|
|
test.valid,
|
|
errs
|
|
);
|
|
} else {
|
|
panic!(
|
|
"FAILED: File: {}, Group: {}, Test: {}\nExpected invalid, got SUCCESS",
|
|
filename,
|
|
group.description,
|
|
test.description
|
|
);
|
|
}
|
|
}
|
|
|
|
// Perform detailed assertions if present
|
|
if let Some(expectations) = &test.expect_errors {
|
|
let actual_errors = result.0.get("errors").and_then(|e| e.as_array()).expect("Expected errors array in failure response");
|
|
|
|
for expected in expectations {
|
|
let found = actual_errors.iter().any(|e| {
|
|
let code = e["code"].as_str().unwrap_or("");
|
|
let path = e["details"]["path"].as_str().unwrap_or("");
|
|
let message = e["message"].as_str().unwrap_or("");
|
|
|
|
let code_match = code == expected.code;
|
|
let path_match = path == expected.path;
|
|
let msg_match = if let Some(sub) = &expected.message_contains {
|
|
message.contains(sub)
|
|
} else {
|
|
true
|
|
};
|
|
|
|
let matches_cause = if let Some(expected_cause) = &expected.cause {
|
|
e["details"]["cause"] == *expected_cause
|
|
} else {
|
|
true
|
|
};
|
|
|
|
let matches_context = if let Some(expected_context) = &expected.context {
|
|
e["details"]["context"] == *expected_context
|
|
} else {
|
|
true
|
|
};
|
|
|
|
code_match && path_match && msg_match && matches_cause && matches_context
|
|
});
|
|
|
|
if !found {
|
|
panic!(
|
|
"FAILED: File: {}, Group: {}, Test: {}\nMissing expected error: code='{}', path='{}'\nActual errors: {:?}",
|
|
filename,
|
|
group.description,
|
|
test.description,
|
|
expected.code,
|
|
expected.path,
|
|
actual_errors
|
|
);
|
|
}
|
|
}
|
|
}
|
|
} // end of test loop
|
|
} // end of group loop
|
|
test_count
|
|
} |