surrealkit 0.5.8

Manage migrations, seeding and tests for your SurrealDB via CLI
use std::collections::BTreeMap;
use std::path::PathBuf;

use serde::{Deserialize, Serialize};

#[derive(Debug, Clone)]
pub struct TestOpts {
	pub suite: Option<String>,
	pub case: Option<String>,
	pub tags: Vec<String>,
	pub fail_fast: bool,
	pub parallel: usize,
	pub json_out: Option<PathBuf>,
	pub no_setup: bool,
	pub no_sync: bool,
	pub no_seed: bool,
	pub base_url: Option<String>,
	pub timeout_ms: Option<u64>,
	pub keep_db: bool,
}

#[derive(Debug, Clone, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct GlobalTestConfig {
	#[serde(default)]
	pub defaults: GlobalDefaults,
	#[serde(default)]
	pub actors: BTreeMap<String, ActorSpec>,
	#[serde(default)]
	pub fixtures: Vec<FixtureSpec>,
}

#[derive(Debug, Clone, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct GlobalDefaults {
	pub base_url: Option<String>,
	pub timeout_ms: Option<u64>,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SuiteSpec {
	pub name: Option<String>,
	#[serde(default)]
	pub tags: Vec<String>,
	#[serde(default)]
	pub actors: BTreeMap<String, ActorSpec>,
	#[serde(default)]
	pub fixtures: Vec<FixtureSpec>,
	#[serde(default)]
	pub cases: Vec<CaseSpec>,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FixtureSpec {
	pub name: Option<String>,
	pub actor: Option<String>,
	pub sql: Option<String>,
	pub file: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ActorSpec {
	pub kind: ActorKind,
	pub username: Option<String>,
	pub username_env: Option<String>,
	pub password: Option<String>,
	pub password_env: Option<String>,
	pub namespace: Option<String>,
	pub namespace_env: Option<String>,
	pub database: Option<String>,
	pub database_env: Option<String>,
	pub access: Option<String>,
	pub access_env: Option<String>,
	pub signup_params: Option<serde_json::Value>,
	pub signin_params: Option<serde_json::Value>,
	pub params: Option<serde_json::Value>,
	pub token: Option<String>,
	pub token_env: Option<String>,
	#[serde(default)]
	pub headers: BTreeMap<String, String>,
}

#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ActorKind {
	Root,
	Namespace,
	Database,
	Record,
	Token,
	Headers,
}

#[derive(Debug, Clone, Deserialize)]
pub struct CaseSpec {
	pub name: String,
	#[serde(default)]
	pub tags: Vec<String>,
	#[serde(flatten)]
	pub kind: CaseKind,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum CaseKind {
	SqlExpect(SqlExpectCase),
	PermissionsMatrix(PermissionsMatrixCase),
	SchemaMetadata(SchemaMetadataCase),
	SchemaBehavior(SchemaBehaviorCase),
	ApiRequest(ApiRequestCase),
}

impl CaseKind {
	pub fn label(&self) -> &'static str {
		match self {
			Self::SqlExpect(_) => "sql_expect",
			Self::PermissionsMatrix(_) => "permissions_matrix",
			Self::SchemaMetadata(_) => "schema_metadata",
			Self::SchemaBehavior(_) => "schema_behavior",
			Self::ApiRequest(_) => "api_request",
		}
	}
}

#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SqlExpectCase {
	pub actor: Option<String>,
	pub sql: String,
	#[serde(default = "default_true")]
	pub allow: bool,
	pub error_contains: Option<String>,
	pub error_code: Option<String>,
	#[serde(default)]
	pub assertions: Vec<JsonAssertionSpec>,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PermissionsMatrixCase {
	pub actor: Option<String>,
	pub table: String,
	pub record_id: Option<String>,
	#[serde(default)]
	pub rules: Vec<PermissionRuleSpec>,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PermissionRuleSpec {
	pub action: PermissionAction,
	#[serde(default = "default_true")]
	pub allow: bool,
	pub sql: Option<String>,
	pub error_contains: Option<String>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PermissionAction {
	Create,
	Select,
	Update,
	Delete,
	Query,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SchemaMetadataCase {
	pub actor: Option<String>,
	pub table: Option<String>,
	pub sql: Option<String>,
	#[serde(default)]
	pub contains: Vec<String>,
	#[serde(default)]
	pub assertions: Vec<JsonAssertionSpec>,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SchemaBehaviorCase {
	pub actor: Option<String>,
	#[serde(default)]
	pub setup_sql: Vec<String>,
	pub action_sql: String,
	#[serde(default = "default_true")]
	pub expect_success: bool,
	pub expect_error_contains: Option<String>,
	pub verify_sql: Option<String>,
	#[serde(default)]
	pub assertions: Vec<JsonAssertionSpec>,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ApiRequestCase {
	pub actor: Option<String>,
	#[serde(default = "default_get")]
	pub method: String,
	pub path: String,
	pub expected_status: u16,
	#[serde(default)]
	pub headers: BTreeMap<String, String>,
	pub body: Option<serde_json::Value>,
	pub timeout_ms: Option<u64>,
	#[serde(default)]
	pub body_assertions: Vec<JsonAssertionSpec>,
	#[serde(default)]
	pub header_assertions: Vec<HeaderAssertionSpec>,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct JsonAssertionSpec {
	pub path: String,
	pub exists: Option<bool>,
	pub equals: Option<serde_json::Value>,
	pub equals_auth: Option<String>,
	pub contains: Option<String>,
	pub regex: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct HeaderAssertionSpec {
	pub name: String,
	pub exists: Option<bool>,
	pub equals: Option<String>,
	pub contains: Option<String>,
	pub regex: Option<String>,
}

#[derive(Debug, Clone)]
pub struct LoadedSpecs {
	pub global: GlobalTestConfig,
	pub suites: Vec<LoadedSuite>,
}

#[derive(Debug, Clone)]
pub struct LoadedSuite {
	pub path: PathBuf,
	pub spec: SuiteSpec,
}

#[derive(Debug, Clone, Serialize)]
pub struct RunReport {
	pub started_at: String,
	pub finished_at: String,
	pub duration_ms: u128,
	pub suites_total: usize,
	pub suites_failed: usize,
	pub cases_total: usize,
	pub cases_passed: usize,
	pub cases_failed: usize,
	pub suites: Vec<SuiteReport>,
}

#[derive(Debug, Clone, Serialize)]
pub struct SuiteReport {
	pub suite_file: String,
	pub suite_name: String,
	pub namespace: String,
	pub database: String,
	pub duration_ms: u128,
	pub cases_total: usize,
	pub cases_passed: usize,
	pub cases_failed: usize,
	pub cases: Vec<CaseReport>,
}

#[derive(Debug, Clone, Serialize)]
pub struct CaseReport {
	pub name: String,
	pub kind: String,
	pub duration_ms: u128,
	pub passed: bool,
	pub message: Option<String>,
	pub assertions: Vec<AssertionReport>,
}

#[derive(Debug, Clone, Serialize)]
pub struct AssertionReport {
	pub name: String,
	pub passed: bool,
	pub message: String,
}

#[derive(Debug, Clone)]
pub struct FilterInput {
	pub suite_pattern: Option<String>,
	pub case_pattern: Option<String>,
	pub tags: Vec<String>,
}

pub fn default_true() -> bool {
	true
}

fn default_get() -> String {
	"GET".to_string()
}

#[cfg(test)]
mod tests {
	use super::{CaseKind, SuiteSpec};

	#[test]
	fn parses_case_kind_inside_cases_array() {
		let raw = r#"
name = "calendar"
tags = ["security"]

[[cases]]
name = "guest_cannot_create_file"
kind = "sql_expect"
actor = "guest"
sql = "CREATE calendar CONTENT { name: 'x' };"
allow = false
error_contains = "permission"
"#;

		let suite: SuiteSpec = toml::from_str(raw).expect("suite should parse");
		assert_eq!(suite.cases.len(), 1);
		assert!(matches!(suite.cases[0].kind, CaseKind::SqlExpect(_)));
	}
}