// 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, cause: Option, context: Option, } #[derive(Debug, Serialize, Deserialize)] struct Group { description: String, schema: Option, enums: Option, types: Option, puncs: Option, tests: Vec, } #[derive(Debug, Serialize, Deserialize)] struct TestCase { description: String, data: Value, valid: bool, action: Option, schema_id: Option, expect_errors: Option>, } 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::(&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 = 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| { 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 }