1use std::collections::HashMap;
49use std::fs::File;
50use std::io::Write;
51use std::path::{Path, PathBuf};
52use std::sync::{Arc, Mutex};
53
54use anyhow::anyhow;
55use async_trait::async_trait;
56use mlua::{Function, Lua, LuaSerdeExt, Table, Value, Variadic};
57use rsa::pkcs1::{DecodeRsaPrivateKey, DecodeRsaPublicKey, EncodeRsaPublicKey, LineEnding};
58use rsa::{Pkcs1v15Sign, RsaPrivateKey, RsaPublicKey};
59use sha2::{Digest, Sha512};
60use tracing::{debug, warn};
61
62use crate::call_chain;
63use crate::catalogue_manager::{CatalogueEntryType, ResolvedCapability, resolve_capability};
64use crate::plugin_manager::lookup_plugin;
65use crate::plugin_models::{
66 PactPluginManifest, PactPluginRpc, PluginInitRequest, PluginInitResponse, PluginInstance,
67};
68use crate::proto::*;
69use crate::proto_v2;
70use crate::utils::{proto_struct_to_json, proto_value_to_json, to_proto_struct, to_proto_value};
71
72pub struct LuaPactPlugin {
81 runtime: Arc<tokio::sync::Mutex<Lua>>,
82 manifest: PactPluginManifest,
83 instance_id: String,
84 plugin_capabilities: Vec<String>,
85}
86
87impl std::fmt::Debug for LuaPactPlugin {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 f.debug_struct("LuaPactPlugin")
90 .field("manifest", &self.manifest)
91 .field("instance_id", &self.instance_id)
92 .field("plugin_capabilities", &self.plugin_capabilities)
93 .finish()
94 }
95}
96
97pub(crate) fn start_lua_plugin(
100 manifest: &PactPluginManifest,
101 instance_id: String,
102) -> anyhow::Result<LuaPactPlugin> {
103 let script_path = resolve_entry_point(manifest)?;
104 debug!("Loading Lua plugin {} from {:?}", manifest.name, script_path);
105
106 let log = Arc::new(LuaPluginLog::open(&manifest.name, &instance_id));
107 let lua = Lua::new();
108 set_package_path(&lua, manifest)?;
109 add_luarocks_path(&lua, manifest)?;
110 register_host_functions(&lua, &manifest.name, &log)?;
111 load_script(&lua, &script_path)?;
112
113 Ok(LuaPactPlugin {
114 runtime: Arc::new(tokio::sync::Mutex::new(lua)),
115 manifest: manifest.clone(),
116 instance_id,
117 plugin_capabilities: vec![],
118 })
119}
120
121impl LuaPactPlugin {
122 pub(crate) fn set_plugin_capabilities(&mut self, capabilities: Vec<String>) {
125 self.plugin_capabilities = capabilities;
126 }
127}
128
129struct LuaPluginLog {
137 file: Mutex<Option<File>>,
138}
139
140impl LuaPluginLog {
141 fn open(plugin_name: &str, instance_id: &str) -> Self {
142 LuaPluginLog {
143 file: Mutex::new(crate::child_process::open_plugin_log_file(plugin_name, instance_id)),
144 }
145 }
146
147 fn write_line(&self, line: &str) {
148 if let Ok(mut guard) = self.file.lock()
149 && let Some(file) = guard.as_mut() {
150 let _ = writeln!(file, "{}", line);
151 let _ = file.flush();
152 }
153 }
154}
155
156fn resolve_entry_point(manifest: &PactPluginManifest) -> anyhow::Result<PathBuf> {
157 let entry_point = PathBuf::from(&manifest.entry_point);
158 let path = if entry_point.is_absolute() && entry_point.exists() {
159 entry_point
160 } else {
161 PathBuf::from(&manifest.plugin_dir).join(&manifest.entry_point)
162 };
163 if !path.exists() {
164 return Err(anyhow!("Lua plugin entry point {:?} does not exist", path));
165 }
166 Ok(path)
167}
168
169fn set_package_path(lua: &Lua, manifest: &PactPluginManifest) -> anyhow::Result<()> {
173 let plugin_dir = PathBuf::from(&manifest.plugin_dir);
174 let package: Table = lua.globals().get("package")?;
175 let existing: String = package.get("path").unwrap_or_default();
176 let new_path = format!(
177 "{}/?.lua;{}/?/init.lua;{}",
178 plugin_dir.to_string_lossy(), plugin_dir.to_string_lossy(), existing
179 );
180 package.set("path", new_path)?;
181 Ok(())
182}
183
184const LUAROCKS_LUA_VERSION: &str = "5.4";
187
188fn add_luarocks_path(lua: &Lua, manifest: &PactPluginManifest) -> anyhow::Result<()> {
198 let configured = manifest.plugin_config.get("luaRocksDir").and_then(|v| v.as_str());
199 let rocks_dir = match configured {
200 Some(dir) => PathBuf::from(dir),
201 None => match home::home_dir() {
202 Some(home) => home.join(".luarocks"),
203 None => return Ok(()),
204 },
205 };
206
207 let lua_dir = rocks_dir.join("share").join("lua").join(LUAROCKS_LUA_VERSION);
208 if !lua_dir.exists() {
209 if configured.is_some() {
210 debug!(
211 "Configured luaRocksDir '{}' does not have a share/lua/{} directory, ignoring",
212 rocks_dir.display(), LUAROCKS_LUA_VERSION
213 );
214 }
215 return Ok(());
216 }
217
218 let package: Table = lua.globals().get("package")?;
219 let existing: String = package.get("path").unwrap_or_default();
220 let new_path = format!(
221 "{}/?.lua;{}/?/init.lua;{}",
222 lua_dir.to_string_lossy(), lua_dir.to_string_lossy(), existing
223 );
224 package.set("path", new_path)?;
225 debug!("Added LuaRocks path {:?} for plugin {}", lua_dir, manifest.name);
226 Ok(())
227}
228
229fn load_script(lua: &Lua, script_path: &Path) -> anyhow::Result<()> {
230 let script = std::fs::read_to_string(script_path)?;
231 lua
232 .load(script)
233 .set_name(script_path.to_string_lossy().to_string())
234 .exec()
235 .map_err(|err| anyhow!("Failed to load Lua plugin script {:?} - {}", script_path, err))?;
236 Ok(())
237}
238
239fn register_host_functions(lua: &Lua, plugin_name: &str, log: &Arc<LuaPluginLog>) -> anyhow::Result<()> {
242 let globals = lua.globals();
243
244 let name = plugin_name.to_string();
245 let logger_log = log.clone();
246 globals.set(
247 "logger",
248 lua.create_function(move |_, message: String| {
249 debug!(plugin = name.as_str(), "{}", message);
250 logger_log.write_line(&message);
251 Ok(())
252 })?,
253 )?;
254
255 let print_log = log.clone();
258 globals.set(
259 "print",
260 lua.create_function(move |lua, args: Variadic<Value>| {
261 let tostring: Function = lua.globals().get("tostring")?;
262 let mut parts = Vec::with_capacity(args.len());
263 for arg in args.iter() {
264 parts.push(tostring.call::<String>(arg.clone())?);
265 }
266 print_log.write_line(&parts.join("\t"));
267 Ok(())
268 })?,
269 )?;
270
271 globals.set(
272 "rsa_sign",
273 lua.create_function(|_, (data, key): (mlua::LuaString, String)| {
274 let private_key = RsaPrivateKey::from_pkcs1_pem(&key).map_err(mlua::Error::external)?;
275 let digest = Sha512::digest(data.as_bytes().as_ref());
276 let signature = private_key
277 .sign(Pkcs1v15Sign::new::<Sha512>(), &digest)
278 .map_err(mlua::Error::external)?;
279 Ok(base64::Engine::encode(
280 &base64::engine::general_purpose::URL_SAFE_NO_PAD,
281 signature,
282 ))
283 })?,
284 )?;
285
286 globals.set(
287 "rsa_public_key",
288 lua.create_function(|_, key: String| {
289 let private_key = RsaPrivateKey::from_pkcs1_pem(&key).map_err(mlua::Error::external)?;
290 let public_key = RsaPublicKey::from(&private_key);
291 let pem = public_key
292 .to_pkcs1_pem(LineEnding::LF)
293 .map_err(mlua::Error::external)?;
294 Ok(pem)
295 })?,
296 )?;
297
298 globals.set(
299 "rsa_validate",
300 lua.create_function(|_, (token_parts, algorithm, key): (Vec<String>, String, String)| {
301 if algorithm != "RS512" {
302 return Err(mlua::Error::RuntimeError(format!(
303 "Unsupported JWT algorithm '{}': only RS512 is supported",
304 algorithm
305 )));
306 }
307 if token_parts.len() != 3 {
308 return Err(mlua::Error::RuntimeError(
309 "Expected a 3 part JWT token (header, payload, signature)".to_string(),
310 ));
311 }
312
313 let public_key = match RsaPublicKey::from_pkcs1_pem(&key) {
314 Ok(key) => key,
315 Err(_) => return Ok(false),
316 };
317 let signature = match decode_base64_lenient(&token_parts[2]) {
318 Ok(bytes) => bytes,
319 Err(_) => return Ok(false),
320 };
321 let base_token = format!("{}.{}", token_parts[0], token_parts[1]);
322 let digest = Sha512::digest(base_token.as_bytes());
323 Ok(
324 public_key
325 .verify(Pkcs1v15Sign::new::<Sha512>(), &digest, &signature)
326 .is_ok(),
327 )
328 })?,
329 )?;
330
331 globals.set(
332 "b64_decode_no_pad",
333 lua.create_function(|lua, data: String| {
334 let bytes = decode_base64_lenient(&data).map_err(mlua::Error::external)?;
335 lua.create_string(&bytes)
336 })?,
337 )?;
338
339 globals.set(
351 "host_compare_contents",
352 lua.create_async_function(move |lua, (entry_key, request): (String, Table)| async move {
353 let request = lua_to_compare_request(&lua, request).map_err(mlua::Error::external)?;
354 let response = call_host_compare_contents(&entry_key, request).await.map_err(mlua::Error::external)?;
355 compare_response_to_lua(&lua, &response)
356 })?,
357 )?;
358
359 globals.set(
360 "host_generate_content",
361 lua.create_async_function(move |lua, (entry_key, contents, generators, test_mode): (String, Value, Option<Table>, Option<String>)| async move {
362 let request = lua_to_generate_request(&lua, contents, generators, test_mode).map_err(mlua::Error::external)?;
363 let response = call_host_generate_content(&entry_key, request).await.map_err(mlua::Error::external)?;
364 body_to_lua(&lua, &response.contents)
365 })?,
366 )?;
367
368 globals.set(
372 "host_match_field",
373 lua.create_async_function(move |lua, (entry_key, request): (String, Table)| async move {
374 let request = lua_to_match_field_request(&lua, request).map_err(mlua::Error::external)?;
375 let response = call_host_match_field(&entry_key, request).await.map_err(mlua::Error::external)?;
376 match_field_response_to_lua(&lua, &response)
377 })?,
378 )?;
379
380 globals.set(
381 "host_generate_field",
382 lua.create_async_function(move |lua, (entry_key, request): (String, Table)| async move {
383 let request = lua_to_generate_field_request(&lua, request).map_err(mlua::Error::external)?;
384 let response = call_host_generate_field(&entry_key, request).await.map_err(mlua::Error::external)?;
385 generate_field_response_to_lua(&lua, &response)
386 })?,
387 )?;
388
389 Ok(())
390}
391
392async fn call_host_compare_contents(
398 entry_key: &str,
399 request: CompareContentsRequest
400) -> anyhow::Result<CompareContentsResponse> {
401 match resolve_capability(entry_key, CatalogueEntryType::CONTENT_MATCHER)? {
402 ResolvedCapability::Core(core_key) => {
403 let handler = crate::core_capabilities::lookup_core_content_matcher(&core_key)
404 .ok_or_else(|| anyhow!("No core content matcher registered for '{}'", core_key))?;
405 handler.compare_contents(request).await
406 }
407 ResolvedCapability::Plugin(manifest) => {
408 let plugin = lookup_plugin(&manifest.as_dependency())
409 .ok_or_else(|| anyhow!("Plugin '{}' for entry '{}' is not currently running", manifest.name, entry_key))?;
410 let chain_id = call_chain::new_call_chain_id();
411 let deadline_ms = call_chain::default_deadline_ms();
412 plugin.compare_contents_with_chain(request, &chain_id, deadline_ms).await
413 }
414 }
415}
416
417async fn call_host_generate_content(
420 entry_key: &str,
421 request: GenerateContentRequest
422) -> anyhow::Result<GenerateContentResponse> {
423 match resolve_capability(entry_key, CatalogueEntryType::CONTENT_GENERATOR)? {
424 ResolvedCapability::Core(core_key) => {
425 let handler = crate::core_capabilities::lookup_core_content_generator(&core_key)
426 .ok_or_else(|| anyhow!("No core content generator registered for '{}'", core_key))?;
427 handler.generate_content(request).await
428 }
429 ResolvedCapability::Plugin(manifest) => {
430 let plugin = lookup_plugin(&manifest.as_dependency())
431 .ok_or_else(|| anyhow!("Plugin '{}' for entry '{}' is not currently running", manifest.name, entry_key))?;
432 let chain_id = call_chain::new_call_chain_id();
433 let deadline_ms = call_chain::default_deadline_ms();
434 plugin.generate_content_with_chain(request, &chain_id, deadline_ms).await
435 }
436 }
437}
438
439async fn call_host_match_field(
442 entry_key: &str,
443 request: proto_v2::MatchFieldRequest
444) -> anyhow::Result<proto_v2::MatchFieldResponse> {
445 match resolve_capability(entry_key, CatalogueEntryType::MATCHER)? {
446 ResolvedCapability::Core(core_key) => {
447 let handler = crate::core_capabilities::lookup_core_field_matcher(&core_key)
448 .ok_or_else(|| anyhow!("No core field matcher registered for '{}'", core_key))?;
449 handler.match_field(request).await
450 }
451 ResolvedCapability::Plugin(manifest) => {
452 let plugin = lookup_plugin(&manifest.as_dependency())
453 .ok_or_else(|| anyhow!("Plugin '{}' for entry '{}' is not currently running", manifest.name, entry_key))?;
454 let chain_id = call_chain::new_call_chain_id();
455 let deadline_ms = call_chain::default_deadline_ms();
456 plugin.match_field_with_chain(request, &chain_id, deadline_ms).await
457 }
458 }
459}
460
461async fn call_host_generate_field(
464 entry_key: &str,
465 request: proto_v2::GenerateFieldRequest
466) -> anyhow::Result<proto_v2::GenerateFieldResponse> {
467 match resolve_capability(entry_key, CatalogueEntryType::GENERATOR)? {
468 ResolvedCapability::Core(core_key) => {
469 let handler = crate::core_capabilities::lookup_core_field_generator(&core_key)
470 .ok_or_else(|| anyhow!("No core field generator registered for '{}'", core_key))?;
471 handler.generate_field(request).await
472 }
473 ResolvedCapability::Plugin(manifest) => {
474 let plugin = lookup_plugin(&manifest.as_dependency())
475 .ok_or_else(|| anyhow!("Plugin '{}' for entry '{}' is not currently running", manifest.name, entry_key))?;
476 let chain_id = call_chain::new_call_chain_id();
477 let deadline_ms = call_chain::default_deadline_ms();
478 plugin.generate_field_with_chain(request, &chain_id, deadline_ms).await
479 }
480 }
481}
482
483fn decode_base64_lenient(data: &str) -> anyhow::Result<Vec<u8>> {
485 use base64::Engine;
486 base64::engine::general_purpose::URL_SAFE
487 .decode(data)
488 .or_else(|_| base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(data))
489 .map_err(|err| anyhow!("Failed to base64 decode value - {}", err))
490}
491
492fn call_init(
493 lua: &Lua,
494 implementation: &str,
495 version: &str,
496) -> anyhow::Result<Vec<CatalogueEntry>> {
497 let init_fn: Function = lua
498 .globals()
499 .get("init")
500 .map_err(|_| anyhow!("Lua plugin does not define a global 'init' function"))?;
501 let result: Table = init_fn
502 .call((implementation.to_string(), version.to_string()))
503 .map_err(|err| anyhow!("Lua init() function failed - {}", err))?;
504 lua_table_to_catalogue_entries(result)
505}
506
507fn lua_table_to_catalogue_entries(table: Table) -> anyhow::Result<Vec<CatalogueEntry>> {
508 let mut entries = vec![];
509 for entry in table.sequence_values::<Table>() {
510 let entry = entry?;
511 let entry_type_str: String = entry.get("entryType")?;
512 let key: String = entry.get("key")?;
513 let values: Option<HashMap<String, String>> = entry.get("values")?;
514 let entry_type = CatalogueEntryType::from_proto_name(&entry_type_str)
515 .ok_or_else(|| anyhow!("Unknown catalogue entry type '{}'", entry_type_str))?;
516 entries.push(CatalogueEntry {
517 r#type: entry_type.to_proto_value(),
518 key,
519 values: values.unwrap_or_default(),
520 });
521 }
522 Ok(entries)
523}
524
525fn content_type_hint_to_str(hint: i32) -> &'static str {
528 match body::ContentTypeHint::try_from(hint).unwrap_or(body::ContentTypeHint::Default) {
529 body::ContentTypeHint::Default => "DEFAULT",
530 body::ContentTypeHint::Text => "TEXT",
531 body::ContentTypeHint::Binary => "BINARY",
532 }
533}
534
535fn str_to_content_type_hint(hint: &str) -> i32 {
536 match hint {
537 "TEXT" => body::ContentTypeHint::Text as i32,
538 "BINARY" => body::ContentTypeHint::Binary as i32,
539 _ => body::ContentTypeHint::Default as i32,
540 }
541}
542
543fn body_to_lua(lua: &Lua, body: &Option<Body>) -> mlua::Result<Value> {
544 match body {
545 None => Ok(Value::Nil),
546 Some(body) => {
547 let table = lua.create_table()?;
548 table.set("content_type", body.content_type.clone())?;
549 match &body.content {
550 Some(bytes) => table.set("contents", lua.create_string(bytes)?)?,
551 None => table.set("contents", Value::Nil)?,
552 }
553 table.set("content_type_hint", content_type_hint_to_str(body.content_type_hint))?;
554 Ok(Value::Table(table))
555 }
556 }
557}
558
559fn lua_to_body(value: Value) -> anyhow::Result<Option<Body>> {
560 match value {
561 Value::Nil => Ok(None),
562 Value::Table(table) => {
563 let content_type: String = table.get("content_type")?;
564 let contents: Option<mlua::LuaString> = table.get("contents")?;
565 let content_type_hint: Option<String> = table.get("content_type_hint")?;
566 Ok(Some(Body {
567 content_type,
568 content: contents.map(|s| s.as_bytes().to_vec()),
569 content_type_hint: content_type_hint
570 .map(|h| str_to_content_type_hint(&h))
571 .unwrap_or(body::ContentTypeHint::Default as i32),
572 }))
573 }
574 _ => Err(anyhow!("Expected a body table or nil from Lua, got {}", value.type_name())),
575 }
576}
577
578fn matching_rules_to_lua(lua: &Lua, rules: &HashMap<String, MatchingRules>) -> mlua::Result<Table> {
581 let table = lua.create_table()?;
582 for (path, rule_list) in rules {
583 let rules_table = lua.create_table()?;
584 for rule in &rule_list.rule {
585 let rule_table = lua.create_table()?;
586 rule_table.set("type", rule.r#type.clone())?;
587 if let Some(values) = &rule.values {
588 rule_table.set("values", lua.to_value(&proto_struct_to_json(values))?)?;
589 }
590 rules_table.push(rule_table)?;
591 }
592 table.set(path.clone(), rules_table)?;
593 }
594 Ok(table)
595}
596
597fn lua_to_matching_rules(lua: &Lua, table: Option<Table>) -> anyhow::Result<HashMap<String, MatchingRules>> {
600 let mut result = HashMap::new();
601 if let Some(table) = table {
602 for pair in table.pairs::<String, Table>() {
603 let (path, rules_table) = pair?;
604 let mut rule = vec![];
605 for rule_value in rules_table.sequence_values::<Table>() {
606 let rule_table = rule_value?;
607 let r#type: String = rule_table.get("type")?;
608 let values: Option<Value> = rule_table.get("values")?;
609 let values = match values {
610 Some(value) => Some(to_proto_struct(&as_json_map(lua.from_value(value)?))),
611 None => None,
612 };
613 rule.push(MatchingRule { r#type, values });
614 }
615 result.insert(path, MatchingRules { rule });
616 }
617 }
618 Ok(result)
619}
620
621fn plugin_configuration_to_lua(lua: &Lua, config: &Option<PluginConfiguration>) -> mlua::Result<Value> {
622 match config {
623 None => Ok(Value::Nil),
624 Some(config) => {
625 let table = lua.create_table()?;
626 if let Some(interaction_configuration) = &config.interaction_configuration {
627 table.set(
628 "interaction_configuration",
629 lua.to_value(&proto_struct_to_json(interaction_configuration))?,
630 )?;
631 }
632 if let Some(pact_configuration) = &config.pact_configuration {
633 table.set(
634 "pact_configuration",
635 lua.to_value(&proto_struct_to_json(pact_configuration))?,
636 )?;
637 }
638 Ok(Value::Table(table))
639 }
640 }
641}
642
643fn lua_to_plugin_configuration(lua: &Lua, value: Option<Value>) -> anyhow::Result<Option<PluginConfiguration>> {
644 match value {
645 None | Some(Value::Nil) => Ok(None),
646 Some(Value::Table(table)) => {
647 let interaction_configuration: Option<serde_json::Value> =
648 table.get::<Option<Value>>("interaction_configuration")?
649 .map(|v| lua.from_value(v))
650 .transpose()?;
651 let pact_configuration: Option<serde_json::Value> =
652 table.get::<Option<Value>>("pact_configuration")?
653 .map(|v| lua.from_value(v))
654 .transpose()?;
655 Ok(Some(PluginConfiguration {
656 interaction_configuration: interaction_configuration.map(|v| to_proto_struct(&as_json_map(v))),
657 pact_configuration: pact_configuration.map(|v| to_proto_struct(&as_json_map(v))),
658 }))
659 }
660 Some(other) => Err(anyhow!("Expected a plugin_config table or nil from Lua, got {}", other.type_name())),
661 }
662}
663
664fn as_json_map(value: serde_json::Value) -> HashMap<String, serde_json::Value> {
665 match value {
666 serde_json::Value::Object(map) => map.into_iter().collect(),
667 _ => HashMap::new(),
668 }
669}
670
671fn compare_request_to_lua(lua: &Lua, request: &CompareContentsRequest) -> mlua::Result<Table> {
674 let table = lua.create_table()?;
675 table.set("expected", body_to_lua(lua, &request.expected)?)?;
676 table.set("actual", body_to_lua(lua, &request.actual)?)?;
677 table.set("allow_unexpected_keys", request.allow_unexpected_keys)?;
678 table.set("rules", matching_rules_to_lua(lua, &request.rules)?)?;
679 table.set(
680 "plugin_configuration",
681 plugin_configuration_to_lua(lua, &request.plugin_configuration)?,
682 )?;
683 Ok(table)
684}
685
686fn lua_to_compare_request(lua: &Lua, table: Table) -> anyhow::Result<CompareContentsRequest> {
690 let expected: Value = table.get("expected")?;
691 let actual: Value = table.get("actual")?;
692 let allow_unexpected_keys: Option<bool> = table.get("allow_unexpected_keys")?;
693 let rules: Option<Table> = table.get("rules")?;
694 let plugin_configuration: Option<Value> = table.get("plugin_configuration")?;
695 Ok(CompareContentsRequest {
696 expected: lua_to_body(expected)?,
697 actual: lua_to_body(actual)?,
698 allow_unexpected_keys: allow_unexpected_keys.unwrap_or(false),
699 rules: lua_to_matching_rules(lua, rules)?,
700 plugin_configuration: lua_to_plugin_configuration(lua, plugin_configuration)?,
701 })
702}
703
704fn lua_to_compare_response(table: Table) -> anyhow::Result<CompareContentsResponse> {
705 let error: Option<String> = table.get("error")?;
706 if let Some(error) = error {
707 return Ok(CompareContentsResponse {
708 error,
709 type_mismatch: None,
710 results: HashMap::new(),
711 });
712 }
713
714 let type_mismatch: Option<Table> = table.get("type-mismatch")?;
715 if let Some(type_mismatch) = type_mismatch {
716 let expected: String = type_mismatch.get("expected")?;
717 let actual: String = type_mismatch.get("actual")?;
718 return Ok(CompareContentsResponse {
719 error: String::new(),
720 type_mismatch: Some(ContentTypeMismatch { expected, actual }),
721 results: HashMap::new(),
722 });
723 }
724
725 let mismatches: Option<Table> = table.get("mismatches")?;
726 let mut results = HashMap::new();
727 if let Some(mismatches) = mismatches {
728 for pair in mismatches.pairs::<String, Value>() {
729 let (path, value) = pair?;
730 let mismatch_list = lua_value_to_content_mismatches(&path, value)?;
731 if !mismatch_list.is_empty() {
732 results.insert(path, ContentMismatches { mismatches: mismatch_list });
733 }
734 }
735 }
736
737 Ok(CompareContentsResponse {
738 error: String::new(),
739 type_mismatch: None,
740 results,
741 })
742}
743
744fn lua_scalar_to_string(value: Value) -> anyhow::Result<Option<String>> {
748 match value {
749 Value::Nil => Ok(None),
750 Value::Boolean(b) => Ok(Some(b.to_string())),
751 Value::Integer(i) => Ok(Some(i.to_string())),
752 Value::Number(n) => Ok(Some(n.to_string())),
753 Value::String(s) => Ok(Some(s.to_str()?.to_string())),
754 other => Err(anyhow!("Expected a scalar mismatch value from Lua, got {}", other.type_name())),
755 }
756}
757
758fn lua_value_to_content_mismatches(path: &str, value: Value) -> anyhow::Result<Vec<ContentMismatch>> {
759 match value {
760 Value::String(s) => Ok(vec![ContentMismatch {
761 expected: None,
762 actual: None,
763 mismatch: s.to_str()?.to_string(),
764 path: path.to_string(),
765 diff: String::new(),
766 mismatch_type: String::new(),
767 }]),
768 Value::Table(table) => {
769 let mismatch_field: Option<String> = table.get("mismatch")?;
771 if let Some(mismatch) = mismatch_field {
772 let expected = lua_scalar_to_string(table.get("expected")?)?;
775 let actual = lua_scalar_to_string(table.get("actual")?)?;
776 let path_override: Option<String> = table.get("path")?;
777 let diff: Option<String> = table.get("diff")?;
778 let mismatch_type: Option<String> = table.get("mismatch_type")?;
779 Ok(vec![ContentMismatch {
780 expected: expected.map(|s| s.into_bytes()),
781 actual: actual.map(|s| s.into_bytes()),
782 mismatch,
783 path: path_override.unwrap_or_else(|| path.to_string()),
784 diff: diff.unwrap_or_default(),
785 mismatch_type: mismatch_type.unwrap_or_default(),
786 }])
787 } else {
788 let mut result = vec![];
789 for entry in table.sequence_values::<Value>() {
790 result.extend(lua_value_to_content_mismatches(path, entry?)?);
791 }
792 Ok(result)
793 }
794 }
795 Value::Nil => Ok(vec![]),
796 other => Err(anyhow!("Expected a mismatch string or table from Lua, got {}", other.type_name())),
797 }
798}
799
800fn content_mismatches_to_lua(lua: &Lua, mismatches: &[ContentMismatch]) -> mlua::Result<Table> {
804 let list = lua.create_table()?;
805 for mismatch in mismatches {
806 let table = lua.create_table()?;
807 table.set("mismatch", mismatch.mismatch.clone())?;
808 if let Some(expected) = &mismatch.expected {
809 table.set("expected", lua.create_string(expected)?)?;
810 }
811 if let Some(actual) = &mismatch.actual {
812 table.set("actual", lua.create_string(actual)?)?;
813 }
814 table.set("path", mismatch.path.clone())?;
815 if !mismatch.diff.is_empty() {
816 table.set("diff", mismatch.diff.clone())?;
817 }
818 if !mismatch.mismatch_type.is_empty() {
819 table.set("mismatch_type", mismatch.mismatch_type.clone())?;
820 }
821 list.push(table)?;
822 }
823 Ok(list)
824}
825
826fn compare_response_to_lua(lua: &Lua, response: &CompareContentsResponse) -> mlua::Result<Table> {
830 let table = lua.create_table()?;
831 if !response.error.is_empty() {
832 table.set("error", response.error.clone())?;
833 return Ok(table);
834 }
835 if let Some(type_mismatch) = &response.type_mismatch {
836 let mismatch_table = lua.create_table()?;
837 mismatch_table.set("expected", type_mismatch.expected.clone())?;
838 mismatch_table.set("actual", type_mismatch.actual.clone())?;
839 table.set("type-mismatch", mismatch_table)?;
840 return Ok(table);
841 }
842 if !response.results.is_empty() {
843 let mismatches_table = lua.create_table()?;
844 for (path, content_mismatches) in &response.results {
845 mismatches_table.set(path.clone(), content_mismatches_to_lua(lua, &content_mismatches.mismatches)?)?;
846 }
847 table.set("mismatches", mismatches_table)?;
848 }
849 Ok(table)
850}
851
852fn lua_to_generate_request(
859 lua: &Lua,
860 contents: Value,
861 generators: Option<Table>,
862 test_mode: Option<String>
863) -> anyhow::Result<GenerateContentRequest> {
864 let mut generator_map = HashMap::new();
865 if let Some(generators) = generators {
866 for pair in generators.pairs::<String, Table>() {
867 let (path, generator_table) = pair?;
868 let r#type: String = generator_table.get("type")?;
869 let values: Option<Value> = generator_table.get("values")?;
870 let values = match values {
871 Some(value) => Some(to_proto_struct(&as_json_map(lua.from_value(value)?))),
872 None => None,
873 };
874 generator_map.insert(path, Generator { r#type, values });
875 }
876 }
877 Ok(GenerateContentRequest {
878 contents: lua_to_body(contents)?,
879 generators: generator_map,
880 test_mode: str_to_test_mode(test_mode.as_deref()),
881 .. GenerateContentRequest::default()
882 })
883}
884
885fn test_mode_to_str(test_mode: i32) -> &'static str {
889 match generate_content_request::TestMode::try_from(test_mode)
890 .unwrap_or(generate_content_request::TestMode::Unknown)
891 {
892 generate_content_request::TestMode::Consumer => "Consumer",
893 generate_content_request::TestMode::Provider => "Provider",
894 generate_content_request::TestMode::Unknown => "Unknown",
895 }
896}
897
898fn str_to_test_mode(test_mode: Option<&str>) -> i32 {
901 match test_mode {
902 Some("Consumer") => generate_content_request::TestMode::Consumer as i32,
903 Some("Provider") => generate_content_request::TestMode::Provider as i32,
904 _ => generate_content_request::TestMode::Unknown as i32,
905 }
906}
907
908fn field_value_to_lua(lua: &Lua, value: &Option<proto_v2::FieldValue>) -> mlua::Result<Value> {
920 use proto_v2::field_value::Value as FieldValue;
921 match value.as_ref().and_then(|value| value.value.as_ref()) {
922 None | Some(FieldValue::NullValue(_)) => Ok(Value::Nil),
923 Some(FieldValue::BooleanValue(value)) => Ok(Value::Boolean(*value)),
924 Some(FieldValue::StringValue(value)) => Ok(Value::String(lua.create_string(value)?)),
925 Some(FieldValue::IntegerValue(value)) => Ok(Value::Integer(*value)),
926 Some(FieldValue::DecimalValue(value)) => Ok(Value::Number(*value)),
927 Some(FieldValue::BinaryValue(bytes)) => {
928 let wrapper = lua.create_table()?;
929 wrapper.set("binary", lua.create_string(bytes)?)?;
930 Ok(Value::Table(wrapper))
931 }
932 Some(FieldValue::StructuredValue(value)) => lua.to_value(&proto_value_to_json(value)),
933 }
934}
935
936fn lua_to_field_value(lua: &Lua, value: Value) -> anyhow::Result<proto_v2::FieldValue> {
939 use proto_v2::field_value::Value as FieldValue;
940 let value = match value {
941 Value::Nil => FieldValue::NullValue(0),
942 Value::Boolean(value) => FieldValue::BooleanValue(value),
943 Value::Integer(value) => FieldValue::IntegerValue(value),
944 Value::Number(value) => FieldValue::DecimalValue(value),
945 Value::String(value) => FieldValue::StringValue(value.to_str()?.to_string()),
946 Value::Table(table) => match table.get::<Option<mlua::LuaString>>("binary")? {
947 Some(binary) => FieldValue::BinaryValue(binary.as_bytes().to_vec()),
948 None => FieldValue::StructuredValue(to_proto_value(&lua.from_value(Value::Table(table))?)),
949 },
950 other => return Err(anyhow!("Expected a field value from Lua, got {}", other.type_name())),
951 };
952 Ok(proto_v2::FieldValue { value: Some(value) })
953}
954
955fn typed_values_to_lua(
960 lua: &Lua,
961 r#type: &str,
962 values: &Option<prost_types::Struct>
963) -> mlua::Result<Table> {
964 let table = lua.create_table()?;
965 table.set("type", r#type.to_string())?;
966 if let Some(values) = values {
967 table.set("values", lua.to_value(&proto_struct_to_json(values))?)?;
968 }
969 Ok(table)
970}
971
972fn lua_to_typed_values(
975 lua: &Lua,
976 table: Option<Table>,
977 what: &str
978) -> anyhow::Result<(String, Option<prost_types::Struct>)> {
979 let table = table.ok_or_else(|| anyhow!("Expected a '{}' table from Lua", what))?;
980 let r#type: String = table.get("type")
981 .map_err(|_| anyhow!("Expected the '{}' table from Lua to have a 'type'", what))?;
982 let values: Option<Value> = table.get("values")?;
983 let values = match values {
984 Some(Value::Nil) | None => None,
985 Some(value) => Some(to_proto_struct(&as_json_map(lua.from_value(value)?))),
986 };
987 Ok((r#type, values))
988}
989
990fn v2_plugin_configuration_to_v1(
995 config: &Option<proto_v2::PluginConfiguration>
996) -> Option<PluginConfiguration> {
997 config.as_ref().map(|config| PluginConfiguration {
998 interaction_configuration: config.interaction_configuration.clone(),
999 pact_configuration: config.pact_configuration.clone(),
1000 })
1001}
1002
1003fn v1_plugin_configuration_to_v2(
1005 config: Option<PluginConfiguration>
1006) -> Option<proto_v2::PluginConfiguration> {
1007 config.map(|config| proto_v2::PluginConfiguration {
1008 interaction_configuration: config.interaction_configuration,
1009 pact_configuration: config.pact_configuration,
1010 })
1011}
1012
1013fn v1_content_mismatch_to_v2(mismatch: ContentMismatch) -> proto_v2::ContentMismatch {
1016 proto_v2::ContentMismatch {
1017 expected: mismatch.expected,
1018 actual: mismatch.actual,
1019 mismatch: mismatch.mismatch,
1020 path: mismatch.path,
1021 diff: mismatch.diff,
1022 mismatch_type: mismatch.mismatch_type,
1023 }
1024}
1025
1026fn v2_content_mismatch_to_v1(mismatch: &proto_v2::ContentMismatch) -> ContentMismatch {
1028 ContentMismatch {
1029 expected: mismatch.expected.clone(),
1030 actual: mismatch.actual.clone(),
1031 mismatch: mismatch.mismatch.clone(),
1032 path: mismatch.path.clone(),
1033 diff: mismatch.diff.clone(),
1034 mismatch_type: mismatch.mismatch_type.clone(),
1035 }
1036}
1037
1038fn match_field_request_to_lua(lua: &Lua, request: &proto_v2::MatchFieldRequest) -> mlua::Result<Table> {
1040 let table = lua.create_table()?;
1041 table.set("key", request.key.clone())?;
1042 let rule = request.rule.clone().unwrap_or_default();
1043 table.set("rule", typed_values_to_lua(lua, &rule.r#type, &rule.values)?)?;
1044 table.set("path", request.path.clone())?;
1045 table.set("mismatch_type", request.mismatch_type.clone())?;
1046 table.set("expected", field_value_to_lua(lua, &request.expected)?)?;
1047 table.set("actual", field_value_to_lua(lua, &request.actual)?)?;
1048 table.set(
1049 "plugin_configuration",
1050 plugin_configuration_to_lua(lua, &v2_plugin_configuration_to_v1(&request.plugin_configuration))?,
1051 )?;
1052 table.set("test_context", struct_to_lua(lua, &request.test_context)?)?;
1053 Ok(table)
1054}
1055
1056fn lua_to_match_field_request(lua: &Lua, table: Table) -> anyhow::Result<proto_v2::MatchFieldRequest> {
1060 let (r#type, values) = lua_to_typed_values(lua, table.get("rule")?, "rule")?;
1061 let plugin_configuration: Option<Value> = table.get("plugin_configuration")?;
1062 let test_context: Option<Value> = table.get("test_context")?;
1063 Ok(proto_v2::MatchFieldRequest {
1064 key: table.get::<Option<String>>("key")?.unwrap_or_default(),
1065 rule: Some(proto_v2::MatchingRule { r#type, values }),
1066 path: table.get::<Option<String>>("path")?.unwrap_or_default(),
1067 mismatch_type: table.get::<Option<String>>("mismatch_type")?.unwrap_or_default(),
1068 expected: Some(lua_to_field_value(lua, table.get("expected")?)?),
1069 actual: Some(lua_to_field_value(lua, table.get("actual")?)?),
1070 plugin_configuration: v1_plugin_configuration_to_v2(
1071 lua_to_plugin_configuration(lua, plugin_configuration)?
1072 ),
1073 test_context: lua_to_struct(lua, test_context)?,
1074 })
1075}
1076
1077fn lua_to_match_field_response(table: Table, path: &str) -> anyhow::Result<proto_v2::MatchFieldResponse> {
1082 let error: Option<String> = table.get("error")?;
1083 if let Some(error) = error {
1084 return Ok(proto_v2::MatchFieldResponse { error, mismatches: vec![] });
1085 }
1086
1087 let mismatches: Option<Value> = table.get("mismatches")?;
1088 let mismatches = match mismatches {
1089 Some(value) => lua_value_to_content_mismatches(path, value)?
1090 .into_iter()
1091 .map(v1_content_mismatch_to_v2)
1092 .collect(),
1093 None => vec![],
1094 };
1095 Ok(proto_v2::MatchFieldResponse { error: String::new(), mismatches })
1096}
1097
1098fn match_field_response_to_lua(lua: &Lua, response: &proto_v2::MatchFieldResponse) -> mlua::Result<Table> {
1101 let table = lua.create_table()?;
1102 if !response.error.is_empty() {
1103 table.set("error", response.error.clone())?;
1104 return Ok(table);
1105 }
1106 let mismatches: Vec<ContentMismatch> = response.mismatches.iter()
1107 .map(v2_content_mismatch_to_v1)
1108 .collect();
1109 table.set("mismatches", content_mismatches_to_lua(lua, &mismatches)?)?;
1110 Ok(table)
1111}
1112
1113fn generate_field_request_to_lua(lua: &Lua, request: &proto_v2::GenerateFieldRequest) -> mlua::Result<Table> {
1115 let table = lua.create_table()?;
1116 table.set("key", request.key.clone())?;
1117 let generator = request.generator.clone().unwrap_or_default();
1118 table.set("generator", typed_values_to_lua(lua, &generator.r#type, &generator.values)?)?;
1119 table.set("path", request.path.clone())?;
1120 table.set("example_value", field_value_to_lua(lua, &request.example_value)?)?;
1121 table.set(
1122 "plugin_configuration",
1123 plugin_configuration_to_lua(lua, &v2_plugin_configuration_to_v1(&request.plugin_configuration))?,
1124 )?;
1125 table.set("test_context", struct_to_lua(lua, &request.test_context)?)?;
1126 table.set("test_mode", test_mode_to_str(request.test_mode))?;
1127 Ok(table)
1128}
1129
1130fn lua_to_generate_field_request(lua: &Lua, table: Table) -> anyhow::Result<proto_v2::GenerateFieldRequest> {
1133 let (r#type, values) = lua_to_typed_values(lua, table.get("generator")?, "generator")?;
1134 let plugin_configuration: Option<Value> = table.get("plugin_configuration")?;
1135 let test_context: Option<Value> = table.get("test_context")?;
1136 let test_mode: Option<String> = table.get("test_mode")?;
1137 Ok(proto_v2::GenerateFieldRequest {
1138 key: table.get::<Option<String>>("key")?.unwrap_or_default(),
1139 generator: Some(proto_v2::Generator { r#type, values }),
1140 path: table.get::<Option<String>>("path")?.unwrap_or_default(),
1141 example_value: Some(lua_to_field_value(lua, table.get("example_value")?)?),
1142 plugin_configuration: v1_plugin_configuration_to_v2(
1143 lua_to_plugin_configuration(lua, plugin_configuration)?
1144 ),
1145 test_context: lua_to_struct(lua, test_context)?,
1146 test_mode: str_to_test_mode(test_mode.as_deref()),
1147 })
1148}
1149
1150fn lua_to_generate_field_response(lua: &Lua, table: Table) -> anyhow::Result<proto_v2::GenerateFieldResponse> {
1153 let error: Option<String> = table.get("error")?;
1154 if let Some(error) = error {
1155 return Ok(proto_v2::GenerateFieldResponse { error, value: None });
1156 }
1157 Ok(proto_v2::GenerateFieldResponse {
1158 error: String::new(),
1159 value: Some(lua_to_field_value(lua, table.get("value")?)?),
1160 })
1161}
1162
1163fn generate_field_response_to_lua(lua: &Lua, response: &proto_v2::GenerateFieldResponse) -> mlua::Result<Table> {
1166 let table = lua.create_table()?;
1167 if !response.error.is_empty() {
1168 table.set("error", response.error.clone())?;
1169 } else {
1170 table.set("value", field_value_to_lua(lua, &response.value)?)?;
1171 }
1172 Ok(table)
1173}
1174
1175fn lua_to_struct(lua: &Lua, value: Option<Value>) -> anyhow::Result<Option<prost_types::Struct>> {
1179 match value {
1180 None | Some(Value::Nil) => Ok(None),
1181 Some(value) => Ok(Some(to_proto_struct(&as_json_map(lua.from_value(value)?)))),
1182 }
1183}
1184
1185fn lua_to_interaction_response(lua: &Lua, table: Table) -> anyhow::Result<InteractionResponse> {
1191 let contents: Option<Value> = table.get("contents")?;
1192 let body = match contents {
1193 Some(value) => lua_to_body(value)?,
1194 None => None,
1195 };
1196 let plugin_config: Option<Value> = table.get("plugin_config")?;
1197 let part_name: Option<String> = table.get("part_name")?;
1198 Ok(InteractionResponse {
1199 contents: body,
1200 rules: HashMap::new(),
1201 generators: HashMap::new(),
1202 message_metadata: None,
1203 plugin_configuration: lua_to_plugin_configuration(lua, plugin_config)?,
1204 interaction_markup: String::new(),
1205 interaction_markup_type: 0,
1206 part_name: part_name.unwrap_or_default(),
1207 metadata_rules: HashMap::new(),
1208 metadata_generators: HashMap::new(),
1209 })
1210}
1211
1212fn lua_to_configure_response(lua: &Lua, table: Table) -> anyhow::Result<ConfigureInteractionResponse> {
1217 let mut interactions = vec![];
1218 let items: Option<Table> = table.get("interactions")?;
1219 if let Some(items) = items {
1220 for entry in items.sequence_values::<Table>() {
1221 interactions.push(lua_to_interaction_response(lua, entry?)?);
1222 }
1223 }
1224
1225 let plugin_config: Option<Value> = table.get("plugin_config")?;
1226 Ok(ConfigureInteractionResponse {
1227 error: String::new(),
1228 interaction: interactions,
1229 plugin_configuration: lua_to_plugin_configuration(lua, plugin_config)?,
1230 })
1231}
1232
1233fn struct_to_lua(lua: &Lua, value: &Option<prost_types::Struct>) -> mlua::Result<Value> {
1237 match value {
1238 Some(value) => lua.to_value(&proto_struct_to_json(value)),
1239 None => Ok(Value::Nil),
1240 }
1241}
1242
1243fn interaction_contents_to_lua(lua: &Lua, contents: &proto_v2::InteractionContents) -> mlua::Result<Table> {
1247 let table = lua.create_table()?;
1248 table.set("interaction_type", contents.interaction_type.clone())?;
1249 table.set("consumer", contents.consumer.clone())?;
1250 table.set("provider", contents.provider.clone())?;
1251 if let Some(plugin_configuration) = &contents.plugin_configuration {
1252 let config_table = lua.create_table()?;
1253 if let Some(interaction_configuration) = &plugin_configuration.interaction_configuration {
1254 config_table.set("interaction_configuration", lua.to_value(&proto_struct_to_json(interaction_configuration))?)?;
1255 }
1256 if let Some(pact_configuration) = &plugin_configuration.pact_configuration {
1257 config_table.set("pact_configuration", lua.to_value(&proto_struct_to_json(pact_configuration))?)?;
1258 }
1259 table.set("plugin_configuration", config_table)?;
1260 }
1261 Ok(table)
1262}
1263
1264fn v2_interaction_data_to_v1(data: &proto_v2::InteractionData) -> anyhow::Result<InteractionData> {
1272 use prost::Message;
1273 InteractionData::decode(data.encode_to_vec().as_slice())
1274 .map_err(|err| anyhow!("Failed to convert V2 InteractionData to V1 - {}", err))
1275}
1276
1277fn metadata_to_lua(lua: &Lua, metadata: &HashMap<String, MetadataValue>) -> mlua::Result<Table> {
1281 let table = lua.create_table()?;
1282 for (key, value) in metadata {
1283 let lua_value = match &value.value {
1284 Some(metadata_value::Value::NonBinaryValue(value)) => lua.to_value(&proto_value_to_json(value))?,
1285 Some(metadata_value::Value::BinaryValue(bytes)) => {
1286 let wrapper = lua.create_table()?;
1287 wrapper.set("binary", lua.create_string(bytes)?)?;
1288 Value::Table(wrapper)
1289 }
1290 None => Value::Nil,
1291 };
1292 table.set(key.clone(), lua_value)?;
1293 }
1294 Ok(table)
1295}
1296
1297fn lua_to_metadata(lua: &Lua, table: Option<Table>) -> anyhow::Result<HashMap<String, MetadataValue>> {
1299 let mut metadata = HashMap::new();
1300 if let Some(table) = table {
1301 for pair in table.pairs::<String, Value>() {
1302 let (key, value) = pair?;
1303 let binary: Option<mlua::LuaString> = match &value {
1304 Value::Table(wrapper) => wrapper.get("binary")?,
1305 _ => None,
1306 };
1307 let metadata_value = if let Some(binary) = binary {
1308 metadata_value::Value::BinaryValue(binary.as_bytes().to_vec())
1309 } else {
1310 let json: serde_json::Value = lua.from_value(value)?;
1311 metadata_value::Value::NonBinaryValue(to_proto_value(&json))
1312 };
1313 metadata.insert(key, MetadataValue { value: Some(metadata_value) });
1314 }
1315 }
1316 Ok(metadata)
1317}
1318
1319fn interaction_data_to_lua(lua: &Lua, data: &Option<InteractionData>) -> mlua::Result<Value> {
1322 match data {
1323 None => Ok(Value::Nil),
1324 Some(data) => {
1325 let table = lua.create_table()?;
1326 table.set("body", body_to_lua(lua, &data.body)?)?;
1327 table.set("metadata", metadata_to_lua(lua, &data.metadata)?)?;
1328 Ok(Value::Table(table))
1329 }
1330 }
1331}
1332
1333fn lua_to_interaction_data(lua: &Lua, value: Option<Value>) -> anyhow::Result<Option<InteractionData>> {
1336 match value {
1337 None | Some(Value::Nil) => Ok(None),
1338 Some(Value::Table(table)) => {
1339 let body: Option<Value> = table.get("body")?;
1340 let body = match body {
1341 Some(value) => lua_to_body(value)?,
1342 None => None,
1343 };
1344 let metadata_table: Option<Table> = table.get("metadata")?;
1345 Ok(Some(InteractionData {
1346 body,
1347 metadata: lua_to_metadata(lua, metadata_table)?,
1348 }))
1349 }
1350 Some(other) => Err(anyhow!("Expected an interaction data table or nil from Lua, got {}", other.type_name())),
1351 }
1352}
1353
1354fn lua_to_start_mock_server_response(table: Table) -> anyhow::Result<StartMockServerResponse> {
1357 let error: Option<String> = table.get("error")?;
1358 if let Some(error) = error {
1359 return Ok(StartMockServerResponse {
1360 response: Some(start_mock_server_response::Response::Error(error)),
1361 });
1362 }
1363
1364 let details: Option<Table> = table.get("details")?;
1365 let details = details.ok_or_else(|| {
1366 anyhow!("Lua start_mock_server() must return either an 'error' or 'details' field")
1367 })?;
1368 Ok(StartMockServerResponse {
1369 response: Some(start_mock_server_response::Response::Details(MockServerDetails {
1370 key: details.get("key")?,
1371 port: details.get("port")?,
1372 address: details.get("address")?,
1373 })),
1374 })
1375}
1376
1377fn lua_to_mock_server_results(table: Table) -> anyhow::Result<MockServerResults> {
1382 let ok: bool = table.get::<Option<bool>>("ok")?.unwrap_or(true);
1383 let mut results = vec![];
1384 let results_table: Option<Table> = table.get("results")?;
1385 if let Some(results_table) = results_table {
1386 for entry in results_table.sequence_values::<Table>() {
1387 let entry = entry?;
1388 let path: String = entry.get::<Option<String>>("path")?.unwrap_or_default();
1389 let error: String = entry.get::<Option<String>>("error")?.unwrap_or_default();
1390 let mismatches_value: Value = entry.get("mismatches")?;
1391 results.push(MockServerResult {
1392 path: path.clone(),
1393 error,
1394 mismatches: lua_value_to_content_mismatches(&path, mismatches_value)?,
1395 });
1396 }
1397 }
1398 Ok(MockServerResults { ok, results })
1399}
1400
1401fn lua_to_verification_preparation_response(
1405 lua: &Lua,
1406 table: Table,
1407) -> anyhow::Result<VerificationPreparationResponse> {
1408 let error: Option<String> = table.get("error")?;
1409 if let Some(error) = error {
1410 return Ok(VerificationPreparationResponse {
1411 response: Some(verification_preparation_response::Response::Error(error)),
1412 });
1413 }
1414
1415 let data: Option<Value> = table.get("interaction_data")?;
1416 let data = data.ok_or_else(|| {
1417 anyhow!("Lua prepare_interaction_for_verification() must return either an 'error' or 'interaction_data' field")
1418 })?;
1419 let interaction_data = lua_to_interaction_data(lua, Some(data))?
1420 .unwrap_or_else(|| InteractionData { body: None, metadata: HashMap::new() });
1421 Ok(VerificationPreparationResponse {
1422 response: Some(verification_preparation_response::Response::InteractionData(interaction_data)),
1423 })
1424}
1425
1426fn lua_to_verification_result_item(value: Value) -> anyhow::Result<VerificationResultItem> {
1429 match value {
1430 Value::String(s) => Ok(VerificationResultItem {
1431 result: Some(verification_result_item::Result::Error(s.to_str()?.to_string())),
1432 }),
1433 Value::Table(table) => {
1434 let mismatch: Option<String> = table.get("mismatch")?;
1435 let path: Option<String> = table.get("path")?;
1436 let expected = lua_scalar_to_string(table.get("expected")?)?;
1437 let actual = lua_scalar_to_string(table.get("actual")?)?;
1438 let diff: Option<String> = table.get("diff")?;
1439 let mismatch_type: Option<String> = table.get("mismatch_type")?;
1440 Ok(VerificationResultItem {
1441 result: Some(verification_result_item::Result::Mismatch(ContentMismatch {
1442 expected: expected.map(|s| s.into_bytes()),
1443 actual: actual.map(|s| s.into_bytes()),
1444 mismatch: mismatch.unwrap_or_default(),
1445 path: path.unwrap_or_default(),
1446 diff: diff.unwrap_or_default(),
1447 mismatch_type: mismatch_type.unwrap_or_default(),
1448 })),
1449 })
1450 }
1451 other => Err(anyhow!("Expected a mismatch string or table from Lua, got {}", other.type_name())),
1452 }
1453}
1454
1455fn lua_to_verify_interaction_response(lua: &Lua, table: Table) -> anyhow::Result<VerifyInteractionResponse> {
1460 let error: Option<String> = table.get("error")?;
1461 if let Some(error) = error {
1462 return Ok(VerifyInteractionResponse {
1463 response: Some(verify_interaction_response::Response::Error(error)),
1464 });
1465 }
1466
1467 let result_table: Option<Table> = table.get("result")?;
1468 let result_table = result_table
1469 .ok_or_else(|| anyhow!("Lua verify_interaction() must return either an 'error' or 'result' field"))?;
1470
1471 let success: bool = result_table.get::<Option<bool>>("success")?.unwrap_or(false);
1472 let response_data: Option<Value> = result_table.get("response_data")?;
1473 let response_data = lua_to_interaction_data(lua, response_data)?;
1474
1475 let mut mismatches = vec![];
1476 let mismatches_value: Option<Value> = result_table.get("mismatches")?;
1477 if let Some(Value::Table(mismatches_table)) = mismatches_value {
1478 for entry in mismatches_table.sequence_values::<Value>() {
1479 mismatches.push(lua_to_verification_result_item(entry?)?);
1480 }
1481 }
1482
1483 let output: Option<Vec<String>> = result_table.get("output")?;
1484
1485 Ok(VerifyInteractionResponse {
1486 response: Some(verify_interaction_response::Response::Result(VerificationResult {
1487 success,
1488 response_data,
1489 mismatches,
1490 output: output.unwrap_or_default(),
1491 })),
1492 })
1493}
1494
1495#[async_trait]
1496impl PactPluginRpc for LuaPactPlugin {
1497 async fn init_plugin(&mut self, request: PluginInitRequest) -> anyhow::Result<PluginInitResponse> {
1498 let lua = self.runtime.lock().await;
1499 let catalogue = call_init(&lua, &request.implementation, &request.version)?;
1500 Ok(PluginInitResponse {
1501 catalogue,
1502 plugin_capabilities: vec![],
1503 })
1504 }
1505}
1506
1507#[async_trait]
1508impl PluginInstance for LuaPactPlugin {
1509 fn manifest(&self) -> &PactPluginManifest {
1510 &self.manifest
1511 }
1512
1513 fn instance_id(&self) -> &str {
1514 &self.instance_id
1515 }
1516
1517 fn has_capability(&self, capability: &str) -> bool {
1518 self.plugin_capabilities.iter().any(|c| c == capability)
1519 }
1520
1521 async fn compare_contents(
1522 &self,
1523 request: CompareContentsRequest,
1524 ) -> anyhow::Result<CompareContentsResponse> {
1525 let lua = self.runtime.lock().await;
1526 let match_fn: Function = lua
1527 .globals()
1528 .get("match_contents")
1529 .map_err(|_| anyhow!("Lua plugin does not define a global 'match_contents' function"))?;
1530 let request_table = compare_request_to_lua(&lua, &request)?;
1531 let result: Table = match_fn
1532 .call_async(request_table)
1533 .await
1534 .map_err(|err| anyhow!("Lua match_contents() function failed - {}", err))?;
1535 lua_to_compare_response(result)
1536 }
1537
1538 async fn configure_interaction(
1539 &self,
1540 request: ConfigureInteractionRequest,
1541 ) -> anyhow::Result<ConfigureInteractionResponse> {
1542 let lua = self.runtime.lock().await;
1543 let configure_fn: Function = lua
1544 .globals()
1545 .get("configure_interaction")
1546 .map_err(|_| anyhow!("Lua plugin does not define a global 'configure_interaction' function"))?;
1547 let config: Value = match &request.contents_config {
1548 Some(config) => lua.to_value(&proto_struct_to_json(config))?,
1549 None => Value::Nil,
1550 };
1551 let result: Table = configure_fn
1552 .call((request.content_type.clone(), config))
1553 .map_err(|err| anyhow!("Lua configure_interaction() function failed - {}", err))?;
1554 lua_to_configure_response(&lua, result)
1555 }
1556
1557 async fn generate_content(
1558 &self,
1559 request: GenerateContentRequest,
1560 ) -> anyhow::Result<GenerateContentResponse> {
1561 let lua = self.runtime.lock().await;
1562 let generate_fn: Option<Function> = lua.globals().get("generate_content")?;
1563 match generate_fn {
1564 None => Ok(GenerateContentResponse {
1565 contents: request.contents,
1566 }),
1567 Some(generate_fn) => {
1568 let contents = body_to_lua(&lua, &request.contents)?;
1569 let generators = lua.create_table()?;
1570 for (path, generator) in &request.generators {
1571 let generator_table = lua.create_table()?;
1572 generator_table.set("type", generator.r#type.clone())?;
1573 if let Some(values) = &generator.values {
1574 generator_table.set("values", lua.to_value(&proto_struct_to_json(values))?)?;
1575 }
1576 generators.set(path.clone(), generator_table)?;
1577 }
1578 let test_mode = test_mode_to_str(request.test_mode);
1579 let result: Value = generate_fn
1580 .call_async((contents, generators, test_mode))
1581 .await
1582 .map_err(|err| anyhow!("Lua generate_content() function failed - {}", err))?;
1583 Ok(GenerateContentResponse {
1584 contents: lua_to_body(result)?,
1585 })
1586 }
1587 }
1588 }
1589
1590 async fn match_field(
1599 &self,
1600 request: proto_v2::MatchFieldRequest,
1601 ) -> anyhow::Result<proto_v2::MatchFieldResponse> {
1602 let lua = self.runtime.lock().await;
1603 let match_fn: Function = lua
1604 .globals()
1605 .get("match_field")
1606 .map_err(|_| anyhow!("Lua plugin does not define a global 'match_field' function"))?;
1607 let path = request.path.clone();
1608 let request_table = match_field_request_to_lua(&lua, &request)?;
1609 let result: Table = match_fn
1610 .call_async(request_table)
1611 .await
1612 .map_err(|err| anyhow!("Lua match_field() function failed - {}", err))?;
1613 lua_to_match_field_response(result, &path)
1614 }
1615
1616 async fn generate_field(
1618 &self,
1619 request: proto_v2::GenerateFieldRequest,
1620 ) -> anyhow::Result<proto_v2::GenerateFieldResponse> {
1621 let lua = self.runtime.lock().await;
1622 let generate_fn: Function = lua
1623 .globals()
1624 .get("generate_field")
1625 .map_err(|_| anyhow!("Lua plugin does not define a global 'generate_field' function"))?;
1626 let request_table = generate_field_request_to_lua(&lua, &request)?;
1627 let result: Table = generate_fn
1628 .call_async(request_table)
1629 .await
1630 .map_err(|err| anyhow!("Lua generate_field() function failed - {}", err))?;
1631 lua_to_generate_field_response(&lua, result)
1632 }
1633
1634 async fn start_mock_server(
1635 &self,
1636 request: StartMockServerRequest,
1637 ) -> anyhow::Result<StartMockServerResponse> {
1638 let lua = self.runtime.lock().await;
1639 let start_fn: Function = lua
1640 .globals()
1641 .get("start_mock_server")
1642 .map_err(|_| anyhow!("Lua plugin does not define a global 'start_mock_server' function"))?;
1643 let request_table = lua.create_table()?;
1644 request_table.set("host_interface", request.host_interface)?;
1645 request_table.set("port", request.port)?;
1646 request_table.set("tls", request.tls)?;
1647 request_table.set("pact", request.pact)?;
1648 request_table.set("test_context", struct_to_lua(&lua, &request.test_context)?)?;
1649 let result: Table = start_fn
1650 .call(request_table)
1651 .map_err(|err| anyhow!("Lua start_mock_server() function failed - {}", err))?;
1652 lua_to_start_mock_server_response(result)
1653 }
1654
1655 async fn start_mock_server_v2(
1656 &self,
1657 request: proto_v2::StartMockServerRequest,
1658 ) -> anyhow::Result<StartMockServerResponse> {
1659 let lua = self.runtime.lock().await;
1660 let start_fn: Function = lua
1661 .globals()
1662 .get("start_mock_server")
1663 .map_err(|_| anyhow!("Lua plugin does not define a global 'start_mock_server' function"))?;
1664 let request_table = lua.create_table()?;
1665 request_table.set("host_interface", request.host_interface)?;
1666 request_table.set("port", request.port)?;
1667 request_table.set("tls", request.tls)?;
1668 let interactions_table = lua.create_table()?;
1669 for interaction in &request.interactions {
1670 interactions_table.push(interaction_contents_to_lua(&lua, interaction)?)?;
1671 }
1672 request_table.set("interactions", interactions_table)?;
1673 request_table.set("test_context", struct_to_lua(&lua, &request.test_context)?)?;
1674 let result: Table = start_fn
1675 .call(request_table)
1676 .map_err(|err| anyhow!("Lua start_mock_server() function failed - {}", err))?;
1677 lua_to_start_mock_server_response(result)
1678 }
1679
1680 async fn shutdown_mock_server(
1681 &self,
1682 request: ShutdownMockServerRequest,
1683 ) -> anyhow::Result<ShutdownMockServerResponse> {
1684 let lua = self.runtime.lock().await;
1685 let shutdown_fn: Function = lua
1686 .globals()
1687 .get("shutdown_mock_server")
1688 .map_err(|_| anyhow!("Lua plugin does not define a global 'shutdown_mock_server' function"))?;
1689 let result: Table = shutdown_fn
1690 .call(request.server_key)
1691 .map_err(|err| anyhow!("Lua shutdown_mock_server() function failed - {}", err))?;
1692 let results = lua_to_mock_server_results(result)?;
1693 Ok(ShutdownMockServerResponse {
1694 ok: results.ok,
1695 results: results.results,
1696 })
1697 }
1698
1699 async fn get_mock_server_results(
1700 &self,
1701 request: MockServerRequest,
1702 ) -> anyhow::Result<MockServerResults> {
1703 let lua = self.runtime.lock().await;
1704 let results_fn: Function = lua
1705 .globals()
1706 .get("get_mock_server_results")
1707 .map_err(|_| anyhow!("Lua plugin does not define a global 'get_mock_server_results' function"))?;
1708 let result: Table = results_fn
1709 .call(request.server_key)
1710 .map_err(|err| anyhow!("Lua get_mock_server_results() function failed - {}", err))?;
1711 lua_to_mock_server_results(result)
1712 }
1713
1714 async fn prepare_interaction_for_verification(
1715 &self,
1716 request: VerificationPreparationRequest,
1717 ) -> anyhow::Result<VerificationPreparationResponse> {
1718 let lua = self.runtime.lock().await;
1719 let prepare_fn: Function = lua.globals().get("prepare_interaction_for_verification").map_err(|_| {
1720 anyhow!("Lua plugin does not define a global 'prepare_interaction_for_verification' function")
1721 })?;
1722 let request_table = lua.create_table()?;
1723 request_table.set("pact", request.pact)?;
1724 request_table.set("interaction_key", request.interaction_key)?;
1725 request_table.set("config", struct_to_lua(&lua, &request.config)?)?;
1726 let result: Table = prepare_fn
1727 .call(request_table)
1728 .map_err(|err| anyhow!("Lua prepare_interaction_for_verification() function failed - {}", err))?;
1729 lua_to_verification_preparation_response(&lua, result)
1730 }
1731
1732 async fn prepare_interaction_for_verification_v2(
1733 &self,
1734 request: proto_v2::VerificationPreparationRequest,
1735 ) -> anyhow::Result<VerificationPreparationResponse> {
1736 let lua = self.runtime.lock().await;
1737 let prepare_fn: Function = lua.globals().get("prepare_interaction_for_verification").map_err(|_| {
1738 anyhow!("Lua plugin does not define a global 'prepare_interaction_for_verification' function")
1739 })?;
1740 let request_table = lua.create_table()?;
1741 if let Some(interaction_contents) = &request.interaction_contents {
1742 request_table.set("interaction_contents", interaction_contents_to_lua(&lua, interaction_contents)?)?;
1743 }
1744 request_table.set("config", struct_to_lua(&lua, &request.config)?)?;
1745 request_table.set("test_context", struct_to_lua(&lua, &request.test_context)?)?;
1746 let result: Table = prepare_fn
1747 .call(request_table)
1748 .map_err(|err| anyhow!("Lua prepare_interaction_for_verification() function failed - {}", err))?;
1749 lua_to_verification_preparation_response(&lua, result)
1750 }
1751
1752 async fn verify_interaction(
1753 &self,
1754 request: VerifyInteractionRequest,
1755 ) -> anyhow::Result<VerifyInteractionResponse> {
1756 let lua = self.runtime.lock().await;
1757 let verify_fn: Function = lua
1758 .globals()
1759 .get("verify_interaction")
1760 .map_err(|_| anyhow!("Lua plugin does not define a global 'verify_interaction' function"))?;
1761 let request_table = lua.create_table()?;
1762 request_table.set("interaction_data", interaction_data_to_lua(&lua, &request.interaction_data)?)?;
1763 request_table.set("config", struct_to_lua(&lua, &request.config)?)?;
1764 request_table.set("pact", request.pact)?;
1765 request_table.set("interaction_key", request.interaction_key)?;
1766 let result: Table = verify_fn
1767 .call(request_table)
1768 .map_err(|err| anyhow!("Lua verify_interaction() function failed - {}", err))?;
1769 lua_to_verify_interaction_response(&lua, result)
1770 }
1771
1772 async fn verify_interaction_v2(
1773 &self,
1774 request: proto_v2::VerifyInteractionRequest,
1775 ) -> anyhow::Result<VerifyInteractionResponse> {
1776 let lua = self.runtime.lock().await;
1777 let verify_fn: Function = lua
1778 .globals()
1779 .get("verify_interaction")
1780 .map_err(|_| anyhow!("Lua plugin does not define a global 'verify_interaction' function"))?;
1781 let request_table = lua.create_table()?;
1782 let interaction_data = request.interaction_data.as_ref()
1783 .map(v2_interaction_data_to_v1)
1784 .transpose()?;
1785 request_table.set("interaction_data", interaction_data_to_lua(&lua, &interaction_data)?)?;
1786 request_table.set("config", struct_to_lua(&lua, &request.config)?)?;
1787 if let Some(interaction_contents) = &request.interaction_contents {
1788 request_table.set("interaction_contents", interaction_contents_to_lua(&lua, interaction_contents)?)?;
1789 }
1790 request_table.set("test_context", struct_to_lua(&lua, &request.test_context)?)?;
1791 let result: Table = verify_fn
1792 .call(request_table)
1793 .map_err(|err| anyhow!("Lua verify_interaction() function failed - {}", err))?;
1794 lua_to_verify_interaction_response(&lua, result)
1795 }
1796
1797 async fn update_catalogue(&self, request: Catalogue) -> anyhow::Result<()> {
1798 let lua = self.runtime.lock().await;
1799 let update_fn: Option<Function> = lua.globals().get("update_catalogue")?;
1800 if let Some(update_fn) = update_fn {
1801 let table = lua.create_table()?;
1802 for entry in &request.catalogue {
1803 let entry_table = lua.create_table()?;
1804 let Some(entry_type) = CatalogueEntryType::from_proto_value(entry.r#type) else {
1807 warn!("Not passing catalogue entry '{}' to the plugin: {} is not a catalogue entry type this driver understands",
1808 entry.key, entry.r#type);
1809 continue;
1810 };
1811 entry_table.set("entryType", entry_type.as_proto_name())?;
1812 entry_table.set("key", entry.key.clone())?;
1813 entry_table.set("values", entry.values.clone())?;
1814 table.push(entry_table)?;
1815 }
1816 update_fn
1817 .call::<()>(table)
1818 .map_err(|err| anyhow!("Lua update_catalogue() function failed - {}", err))?;
1819 }
1820 Ok(())
1821 }
1822}
1823
1824#[cfg(test)]
1825mod tests {
1826 use std::collections::HashMap;
1827
1828 use super::*;
1829
1830 fn jwt_manifest() -> PactPluginManifest {
1831 let plugin_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../../plugins/jwt");
1837 assert!(plugin_dir.exists(), "plugins/jwt directory should exist at {:?}", plugin_dir);
1838 PactPluginManifest {
1839 plugin_dir: plugin_dir.to_string_lossy().to_string(),
1840 plugin_interface_version: 1,
1841 name: "jwt".to_string(),
1842 version: "0.0.0".to_string(),
1843 executable_type: "lua".to_string(),
1844 minimum_required_version: None,
1845 entry_point: "plugin.lua".to_string(),
1846 entry_points: HashMap::new(),
1847 args: None,
1848 dependencies: None,
1849 plugin_config: HashMap::new(),
1850 }
1851 }
1852
1853 const PRIVATE_KEY: &str = include_str!("../tests/fixtures/jwt-test-key.pem");
1854
1855 #[test]
1856 fn loads_pure_lua_packages_from_a_configured_luarocks_directory() {
1857 let rocks_root = tempdir::TempDir::new("luarocks-test").unwrap();
1858 let lua_dir = rocks_root.path().join("share").join("lua").join(LUAROCKS_LUA_VERSION);
1859 std::fs::create_dir_all(&lua_dir).unwrap();
1860 std::fs::write(
1861 lua_dir.join("greeter.lua"),
1862 r#"return { hello = function() return "hello from luarocks" end }"#,
1863 ).unwrap();
1864
1865 let plugin_dir = tempdir::TempDir::new("lua-plugin-test").unwrap();
1866 std::fs::write(
1867 plugin_dir.path().join("entry.lua"),
1868 r#"
1869 local greeter = require "greeter"
1870 GREETER_RESULT = greeter.hello()
1871 "#,
1872 ).unwrap();
1873
1874 let mut plugin_config = HashMap::new();
1875 plugin_config.insert(
1876 "luaRocksDir".to_string(),
1877 serde_json::Value::String(rocks_root.path().to_string_lossy().to_string()),
1878 );
1879
1880 let manifest = PactPluginManifest {
1881 plugin_dir: plugin_dir.path().to_string_lossy().to_string(),
1882 plugin_interface_version: 1,
1883 name: "luarocks-test".to_string(),
1884 version: "0.0.0".to_string(),
1885 executable_type: "lua".to_string(),
1886 minimum_required_version: None,
1887 entry_point: "entry.lua".to_string(),
1888 entry_points: HashMap::new(),
1889 args: None,
1890 dependencies: None,
1891 plugin_config,
1892 };
1893
1894 let plugin = start_lua_plugin(&manifest, "test-instance".to_string()).unwrap();
1895 let lua = plugin.runtime.blocking_lock();
1896 let result: String = lua.globals().get("GREETER_RESULT").unwrap();
1897 assert_eq!(result, "hello from luarocks");
1898 }
1899
1900 #[test]
1901 fn loads_a_vendored_directory_style_module_from_the_plugin_directory() {
1902 let plugin_dir = tempdir::TempDir::new("lua-plugin-test").unwrap();
1903 let module_dir = plugin_dir.path().join("greeter");
1904 std::fs::create_dir_all(&module_dir).unwrap();
1905 std::fs::write(
1906 module_dir.join("init.lua"),
1907 r#"return { hello = function() return "hello from a vendored module" end }"#,
1908 ).unwrap();
1909 std::fs::write(
1910 plugin_dir.path().join("entry.lua"),
1911 r#"
1912 local greeter = require "greeter"
1913 GREETER_RESULT = greeter.hello()
1914 "#,
1915 ).unwrap();
1916
1917 let manifest = PactPluginManifest {
1918 plugin_dir: plugin_dir.path().to_string_lossy().to_string(),
1919 plugin_interface_version: 1,
1920 name: "vendored-module-test".to_string(),
1921 version: "0.0.0".to_string(),
1922 executable_type: "lua".to_string(),
1923 minimum_required_version: None,
1924 entry_point: "entry.lua".to_string(),
1925 entry_points: HashMap::new(),
1926 args: None,
1927 dependencies: None,
1928 plugin_config: HashMap::new(),
1929 };
1930
1931 let plugin = start_lua_plugin(&manifest, "test-instance".to_string()).unwrap();
1932 let lua = plugin.runtime.blocking_lock();
1933 let result: String = lua.globals().get("GREETER_RESULT").unwrap();
1934 assert_eq!(result, "hello from a vendored module");
1935 }
1936
1937 #[test]
1938 fn loads_a_vendored_module_when_the_entry_point_is_in_a_subdirectory() {
1939 let plugin_dir = tempdir::TempDir::new("lua-plugin-test").unwrap();
1943 std::fs::write(
1944 plugin_dir.path().join("greeter.lua"),
1945 r#"return { hello = function() return "hello from the plugin root" end }"#,
1946 ).unwrap();
1947 let src_dir = plugin_dir.path().join("src");
1948 std::fs::create_dir_all(&src_dir).unwrap();
1949 std::fs::write(
1950 src_dir.join("entry.lua"),
1951 r#"
1952 local greeter = require "greeter"
1953 GREETER_RESULT = greeter.hello()
1954 "#,
1955 ).unwrap();
1956
1957 let manifest = PactPluginManifest {
1958 plugin_dir: plugin_dir.path().to_string_lossy().to_string(),
1959 plugin_interface_version: 1,
1960 name: "nested-entry-point-test".to_string(),
1961 version: "0.0.0".to_string(),
1962 executable_type: "lua".to_string(),
1963 minimum_required_version: None,
1964 entry_point: "src/entry.lua".to_string(),
1965 entry_points: HashMap::new(),
1966 args: None,
1967 dependencies: None,
1968 plugin_config: HashMap::new(),
1969 };
1970
1971 let plugin = start_lua_plugin(&manifest, "test-instance".to_string()).unwrap();
1972 let lua = plugin.runtime.blocking_lock();
1973 let result: String = lua.globals().get("GREETER_RESULT").unwrap();
1974 assert_eq!(result, "hello from the plugin root");
1975 }
1976
1977 #[test]
1978 fn ignores_a_missing_luarocks_directory_instead_of_failing() {
1979 let plugin_dir = tempdir::TempDir::new("lua-plugin-test").unwrap();
1980 std::fs::write(plugin_dir.path().join("entry.lua"), "-- no-op").unwrap();
1981
1982 let mut plugin_config = HashMap::new();
1983 plugin_config.insert(
1984 "luaRocksDir".to_string(),
1985 serde_json::Value::String("/no/such/directory".to_string()),
1986 );
1987
1988 let manifest = PactPluginManifest {
1989 plugin_dir: plugin_dir.path().to_string_lossy().to_string(),
1990 plugin_interface_version: 1,
1991 name: "luarocks-test".to_string(),
1992 version: "0.0.0".to_string(),
1993 executable_type: "lua".to_string(),
1994 minimum_required_version: None,
1995 entry_point: "entry.lua".to_string(),
1996 entry_points: HashMap::new(),
1997 args: None,
1998 dependencies: None,
1999 plugin_config,
2000 };
2001
2002 start_lua_plugin(&manifest, "test-instance".to_string()).unwrap();
2003 }
2004
2005 #[test]
2006 fn captures_print_and_logger_output_into_the_per_instance_log_file() {
2007 let output_dir = tempdir::TempDir::new("lua-plugin-log-test").unwrap();
2008 unsafe { std::env::set_var("PACT_OUTPUT_DIR", output_dir.path()); }
2011
2012 let plugin_dir = tempdir::TempDir::new("lua-plugin-test").unwrap();
2013 std::fs::write(
2014 plugin_dir.path().join("entry.lua"),
2015 r#"
2016 print("hello", "world", 42)
2017 logger("a logger message")
2018 "#,
2019 ).unwrap();
2020
2021 let manifest = PactPluginManifest {
2022 plugin_dir: plugin_dir.path().to_string_lossy().to_string(),
2023 plugin_interface_version: 1,
2024 name: "log-test".to_string(),
2025 version: "0.0.0".to_string(),
2026 executable_type: "lua".to_string(),
2027 minimum_required_version: None,
2028 entry_point: "entry.lua".to_string(),
2029 entry_points: HashMap::new(),
2030 args: None,
2031 dependencies: None,
2032 plugin_config: HashMap::new(),
2033 };
2034
2035 start_lua_plugin(&manifest, "test-instance".to_string()).unwrap();
2036 unsafe { std::env::remove_var("PACT_OUTPUT_DIR"); }
2037
2038 let log_path = output_dir.path().join("logs").join("pact-plugin-log-test-test-instance.log");
2039 let contents = std::fs::read_to_string(&log_path)
2040 .unwrap_or_else(|err| panic!("Expected a log file at {:?} - {}", log_path, err));
2041 assert_eq!(contents, "hello\tworld\t42\na logger message\n");
2042 }
2043
2044 #[tokio::test]
2045 async fn loads_the_jwt_plugin_and_runs_the_init_function() {
2046 let manifest = jwt_manifest();
2047 let plugin = start_lua_plugin(&manifest, "test-instance".to_string()).unwrap();
2048 let lua = plugin.runtime.lock().await;
2049 let entries = call_init(&lua, "test", "0.0.0").unwrap();
2050 assert_eq!(entries.len(), 2);
2051 assert_eq!(entries[0].key, "jwt");
2052 assert_eq!(entries[0].r#type, catalogue_entry::EntryType::ContentMatcher as i32);
2053 assert_eq!(entries[1].r#type, catalogue_entry::EntryType::ContentGenerator as i32);
2054 }
2055
2056 #[tokio::test]
2057 async fn init_accepts_matcher_and_generator_catalogue_entries() {
2058 let plugin_dir = tempdir::TempDir::new("lua-plugin-test").unwrap();
2062 std::fs::write(
2063 plugin_dir.path().join("entry.lua"),
2064 r#"
2065 function init(implementation, version)
2066 return {
2067 { entryType = "MATCHER", key = "creditcard", values = { ["config-key"] = "brand" } },
2068 { entryType = "GENERATOR", key = "creditcard" }
2069 }
2070 end
2071 "#,
2072 ).unwrap();
2073
2074 let manifest = lua_manifest(plugin_dir.path(), "field-entries-test");
2075 let plugin = start_lua_plugin(&manifest, "test-instance".to_string()).unwrap();
2076 let lua = plugin.runtime.lock().await;
2077 let entries = call_init(&lua, "test", "0.0.0").unwrap();
2078
2079 assert_eq!(entries.len(), 2);
2080 assert_eq!(entries[0].key, "creditcard");
2081 assert_eq!(entries[0].r#type, CatalogueEntryType::MATCHER.to_proto_value());
2082 assert_eq!(entries[0].values.get("config-key"), Some(&"brand".to_string()));
2083 assert_eq!(entries[1].r#type, CatalogueEntryType::GENERATOR.to_proto_value());
2084 }
2085
2086 #[tokio::test]
2087 async fn init_rejects_an_unknown_catalogue_entry_type() {
2088 let plugin_dir = tempdir::TempDir::new("lua-plugin-test").unwrap();
2089 std::fs::write(
2090 plugin_dir.path().join("entry.lua"),
2091 r#"
2092 function init(implementation, version)
2093 return { { entryType = "NOT_AN_ENTRY_TYPE", key = "nope" } }
2094 end
2095 "#,
2096 ).unwrap();
2097
2098 let manifest = lua_manifest(plugin_dir.path(), "bad-entry-type-test");
2099 let plugin = start_lua_plugin(&manifest, "test-instance".to_string()).unwrap();
2100 let lua = plugin.runtime.lock().await;
2101
2102 let error = call_init(&lua, "test", "0.0.0").unwrap_err().to_string();
2103 assert!(error.contains("NOT_AN_ENTRY_TYPE"), "unexpected error: {}", error);
2104 }
2105
2106 #[tokio::test]
2107 async fn configure_interaction_then_match_contents_round_trip() {
2108 let manifest = jwt_manifest();
2109 let plugin = start_lua_plugin(&manifest, "test-instance".to_string()).unwrap();
2110
2111 let mut config_fields = HashMap::new();
2112 config_fields.insert("private-key".to_string(), serde_json::Value::String(PRIVATE_KEY.to_string()));
2113 config_fields.insert("subject".to_string(), serde_json::Value::String("test-subject".to_string()));
2114 config_fields.insert("issuer".to_string(), serde_json::Value::String("test-issuer".to_string()));
2115 config_fields.insert("audience".to_string(), serde_json::Value::String("test-audience".to_string()));
2116 config_fields.insert("algorithm".to_string(), serde_json::Value::String("RS512".to_string()));
2117
2118 let configure_request = ConfigureInteractionRequest {
2119 content_type: "application/jwt+json".to_string(),
2120 contents_config: Some(to_proto_struct(&config_fields)),
2121 };
2122 let configure_response = plugin.configure_interaction(configure_request).await.unwrap();
2123 assert_eq!(configure_response.error, "");
2124 assert_eq!(configure_response.interaction.len(), 1);
2125
2126 let interaction = &configure_response.interaction[0];
2127 let body = interaction.contents.clone().expect("expected a body");
2128 assert_eq!(body.content_type, "application/jwt+json");
2129 let token = String::from_utf8(body.content.clone().unwrap()).unwrap();
2130 assert_eq!(token.split('.').count(), 3);
2131
2132 let compare_request = CompareContentsRequest {
2133 expected: Some(body.clone()),
2134 actual: Some(body),
2135 allow_unexpected_keys: false,
2136 rules: HashMap::new(),
2137 plugin_configuration: interaction.plugin_configuration.clone(),
2138 };
2139 let compare_response = plugin.compare_contents(compare_request).await.unwrap();
2140 assert_eq!(compare_response.error, "");
2141 assert!(compare_response.type_mismatch.is_none());
2142 assert!(
2143 compare_response.results.is_empty(),
2144 "expected no mismatches, got {:?}",
2145 compare_response.results
2146 );
2147 }
2148
2149 #[tokio::test]
2150 async fn match_contents_detects_a_tampered_token() {
2151 let manifest = jwt_manifest();
2152 let plugin = start_lua_plugin(&manifest, "test-instance".to_string()).unwrap();
2153
2154 let mut config_fields = HashMap::new();
2155 config_fields.insert("private-key".to_string(), serde_json::Value::String(PRIVATE_KEY.to_string()));
2156 config_fields.insert("algorithm".to_string(), serde_json::Value::String("RS512".to_string()));
2157
2158 let configure_request = ConfigureInteractionRequest {
2159 content_type: "application/jwt+json".to_string(),
2160 contents_config: Some(to_proto_struct(&config_fields)),
2161 };
2162 let configure_response = plugin.configure_interaction(configure_request).await.unwrap();
2163 let interaction = &configure_response.interaction[0];
2164 let expected_body = interaction.contents.clone().unwrap();
2165
2166 let mut actual_body = expected_body.clone();
2167 let mut token = String::from_utf8(actual_body.content.clone().unwrap()).unwrap();
2168 token.push('x'); actual_body.content = Some(token.into_bytes());
2170
2171 let compare_request = CompareContentsRequest {
2172 expected: Some(expected_body),
2173 actual: Some(actual_body),
2174 allow_unexpected_keys: false,
2175 rules: HashMap::new(),
2176 plugin_configuration: interaction.plugin_configuration.clone(),
2177 };
2178 let compare_response = plugin.compare_contents(compare_request).await.unwrap();
2179 assert!(!compare_response.results.is_empty(), "expected a mismatch to be detected");
2180 }
2181
2182 #[tokio::test]
2183 async fn compare_contents_handles_non_string_mismatch_values() {
2184 let plugin_dir = tempdir::TempDir::new("lua-plugin-test").unwrap();
2185 std::fs::write(
2186 plugin_dir.path().join("entry.lua"),
2187 r#"
2188 function match_contents(request)
2189 return {
2190 mismatches = {
2191 ["claims:exp"] = { expected = 123, actual = 456, mismatch = "exp differs", path = "claims:exp" },
2192 ["claims:verified"] = { expected = true, actual = false, mismatch = "verified differs", path = "claims:verified" }
2193 }
2194 }
2195 end
2196 "#,
2197 ).unwrap();
2198
2199 let manifest = PactPluginManifest {
2200 plugin_dir: plugin_dir.path().to_string_lossy().to_string(),
2201 plugin_interface_version: 1,
2202 name: "scalar-mismatch-test".to_string(),
2203 version: "0.0.0".to_string(),
2204 executable_type: "lua".to_string(),
2205 minimum_required_version: None,
2206 entry_point: "entry.lua".to_string(),
2207 entry_points: HashMap::new(),
2208 args: None,
2209 dependencies: None,
2210 plugin_config: HashMap::new(),
2211 };
2212 let plugin = start_lua_plugin(&manifest, "test-instance".to_string()).unwrap();
2213
2214 let compare_request = CompareContentsRequest {
2215 expected: None,
2216 actual: None,
2217 allow_unexpected_keys: false,
2218 rules: HashMap::new(),
2219 plugin_configuration: None,
2220 };
2221 let response = plugin.compare_contents(compare_request).await.unwrap();
2222
2223 let exp_mismatch = &response.results["claims:exp"].mismatches[0];
2224 assert_eq!(exp_mismatch.expected.as_deref(), Some("123".as_bytes()));
2225 assert_eq!(exp_mismatch.actual.as_deref(), Some("456".as_bytes()));
2226
2227 let verified_mismatch = &response.results["claims:verified"].mismatches[0];
2228 assert_eq!(verified_mismatch.expected.as_deref(), Some("true".as_bytes()));
2229 assert_eq!(verified_mismatch.actual.as_deref(), Some("false".as_bytes()));
2230 }
2231
2232 fn lua_manifest(plugin_dir: &std::path::Path, name: &str) -> PactPluginManifest {
2233 PactPluginManifest {
2234 plugin_dir: plugin_dir.to_string_lossy().to_string(),
2235 plugin_interface_version: 1,
2236 name: name.to_string(),
2237 version: "0.0.0".to_string(),
2238 executable_type: "lua".to_string(),
2239 minimum_required_version: None,
2240 entry_point: "entry.lua".to_string(),
2241 entry_points: HashMap::new(),
2242 args: None,
2243 dependencies: None,
2244 plugin_config: HashMap::new(),
2245 }
2246 }
2247
2248 fn core_matcher_entry(key: &str) -> crate::catalogue_manager::CatalogueEntry {
2249 crate::catalogue_manager::CatalogueEntry {
2250 entry_type: crate::catalogue_manager::CatalogueEntryType::CONTENT_MATCHER,
2251 provider_type: crate::catalogue_manager::CatalogueEntryProviderType::CORE,
2252 plugin: None,
2253 key: key.to_string(),
2254 values: HashMap::new()
2255 }
2256 }
2257
2258 fn core_generator_entry(key: &str) -> crate::catalogue_manager::CatalogueEntry {
2259 crate::catalogue_manager::CatalogueEntry {
2260 entry_type: crate::catalogue_manager::CatalogueEntryType::CONTENT_GENERATOR,
2261 provider_type: crate::catalogue_manager::CatalogueEntryProviderType::CORE,
2262 plugin: None,
2263 key: key.to_string(),
2264 values: HashMap::new()
2265 }
2266 }
2267
2268 struct FixedErrorCoreMatcher;
2269
2270 #[async_trait]
2271 impl crate::core_capabilities::CoreContentMatcher for FixedErrorCoreMatcher {
2272 async fn compare_contents(&self, _request: CompareContentsRequest) -> anyhow::Result<CompareContentsResponse> {
2273 Ok(CompareContentsResponse {
2274 error: "core matcher says no".to_string(),
2275 type_mismatch: None,
2276 results: HashMap::new(),
2277 })
2278 }
2279 }
2280
2281 #[tokio::test]
2282 async fn match_contents_calls_host_compare_contents_for_a_registered_core_capability() {
2283 let key = "match_contents_calls_host_compare_contents_for_a_registered_core_capability";
2284 crate::catalogue_manager::register_core_entries(&vec![core_matcher_entry(key)]);
2285 crate::core_capabilities::register_core_content_matcher(key, Arc::new(FixedErrorCoreMatcher));
2286
2287 let plugin_dir = tempdir::TempDir::new("lua-plugin-test").unwrap();
2288 std::fs::write(
2289 plugin_dir.path().join("entry.lua"),
2290 format!(r#"
2291 function match_contents(request)
2292 return host_compare_contents("{key}", request)
2293 end
2294 "#, key = key),
2295 ).unwrap();
2296 let manifest = lua_manifest(plugin_dir.path(), "host-compare-contents-test");
2297 let plugin = start_lua_plugin(&manifest, "test-instance".to_string()).unwrap();
2298
2299 let compare_request = CompareContentsRequest {
2300 expected: None,
2301 actual: None,
2302 allow_unexpected_keys: false,
2303 rules: HashMap::new(),
2304 plugin_configuration: None,
2305 };
2306 let response = plugin.compare_contents(compare_request).await.unwrap();
2307
2308 crate::core_capabilities::deregister_core_content_matcher(key);
2309
2310 assert_eq!(response.error, "core matcher says no");
2311 }
2312
2313 #[tokio::test]
2314 async fn match_contents_surfaces_a_clear_error_when_host_compare_contents_targets_an_unregistered_entry() {
2315 let key = "match_contents_surfaces_a_clear_error_when_host_compare_contents_targets_an_unregistered_entry";
2316 let plugin_dir = tempdir::TempDir::new("lua-plugin-test").unwrap();
2317 std::fs::write(
2318 plugin_dir.path().join("entry.lua"),
2319 format!(r#"
2320 function match_contents(request)
2321 return host_compare_contents("{key}", request)
2322 end
2323 "#, key = key),
2324 ).unwrap();
2325 let manifest = lua_manifest(plugin_dir.path(), "host-compare-contents-missing-test");
2326 let plugin = start_lua_plugin(&manifest, "test-instance".to_string()).unwrap();
2327
2328 let compare_request = CompareContentsRequest {
2329 expected: None,
2330 actual: None,
2331 allow_unexpected_keys: false,
2332 rules: HashMap::new(),
2333 plugin_configuration: None,
2334 };
2335 let err = plugin.compare_contents(compare_request).await
2336 .expect_err("expected an error when the target entry is not registered");
2337 assert!(
2338 err.to_string().contains("No catalogue entry found"),
2339 "unexpected error message: {}", err
2340 );
2341 }
2342
2343 struct FixedCoreGenerator;
2344
2345 #[async_trait]
2346 impl crate::core_capabilities::CoreContentGenerator for FixedCoreGenerator {
2347 async fn generate_content(&self, _request: GenerateContentRequest) -> anyhow::Result<GenerateContentResponse> {
2348 Ok(GenerateContentResponse {
2349 contents: Some(Body {
2350 content_type: "text/plain".to_string(),
2351 content: Some(b"generated by the host".to_vec()),
2352 content_type_hint: body::ContentTypeHint::Default as i32,
2353 }),
2354 })
2355 }
2356 }
2357
2358 #[tokio::test]
2359 async fn generate_content_calls_host_generate_content_for_a_registered_core_capability() {
2360 let key = "generate_content_calls_host_generate_content_for_a_registered_core_capability";
2361 crate::catalogue_manager::register_core_entries(&vec![core_generator_entry(key)]);
2362 crate::core_capabilities::register_core_content_generator(key, Arc::new(FixedCoreGenerator));
2363
2364 let plugin_dir = tempdir::TempDir::new("lua-plugin-test").unwrap();
2365 std::fs::write(
2366 plugin_dir.path().join("entry.lua"),
2367 format!(r#"
2368 function generate_content(contents, generators, test_mode)
2369 return host_generate_content("{key}", contents, generators, test_mode)
2370 end
2371 "#, key = key),
2372 ).unwrap();
2373 let manifest = lua_manifest(plugin_dir.path(), "host-generate-content-test");
2374 let plugin = start_lua_plugin(&manifest, "test-instance".to_string()).unwrap();
2375
2376 let request = GenerateContentRequest {
2377 contents: Some(Body {
2378 content_type: "text/plain".to_string(),
2379 content: Some(b"original".to_vec()),
2380 content_type_hint: body::ContentTypeHint::Default as i32,
2381 }),
2382 .. GenerateContentRequest::default()
2383 };
2384 let response = plugin.generate_content(request).await.unwrap();
2385
2386 crate::core_capabilities::deregister_core_content_generator(key);
2387
2388 assert_eq!(response.contents.unwrap().content, Some(b"generated by the host".to_vec()));
2389 }
2390
2391 fn transport_manifest(plugin_dir: &std::path::Path, plugin_interface_version: u8) -> PactPluginManifest {
2392 PactPluginManifest {
2393 plugin_dir: plugin_dir.to_string_lossy().to_string(),
2394 plugin_interface_version,
2395 name: "transport-test".to_string(),
2396 version: "0.0.0".to_string(),
2397 executable_type: "lua".to_string(),
2398 minimum_required_version: None,
2399 entry_point: "entry.lua".to_string(),
2400 entry_points: HashMap::new(),
2401 args: None,
2402 dependencies: None,
2403 plugin_config: HashMap::new(),
2404 }
2405 }
2406
2407 const TRANSPORT_PLUGIN_SCRIPT: &str = r#"
2408 function start_mock_server(request)
2409 START_MOCK_SERVER_REQUEST = request
2410 if request.port == 0 then
2411 return { error = "could not bind a mock server" }
2412 end
2413 return { details = { key = "mock-server-1", port = 12345, address = "127.0.0.1:12345" } }
2414 end
2415
2416 function shutdown_mock_server(server_key)
2417 SHUTDOWN_SERVER_KEY = server_key
2418 return {
2419 ok = false,
2420 results = { { path = "/foo", error = "did not match", mismatches = { "simple string mismatch" } } }
2421 }
2422 end
2423
2424 function get_mock_server_results(server_key)
2425 GET_RESULTS_SERVER_KEY = server_key
2426 return { ok = true, results = {} }
2427 end
2428
2429 function prepare_interaction_for_verification(request)
2430 PREPARE_REQUEST = request
2431 return {
2432 interaction_data = {
2433 body = { content_type = "application/json", contents = "prepared-body", content_type_hint = "TEXT" },
2434 metadata = { path = "/foo", tag = { binary = "raw-bytes" } }
2435 }
2436 }
2437 end
2438
2439 function verify_interaction(request)
2440 VERIFY_REQUEST = request
2441 if request.config ~= nil and request.config.fail == true then
2442 return { error = "verification failed" }
2443 end
2444 return {
2445 result = {
2446 success = true,
2447 response_data = { body = { content_type = "application/json", contents = "response-body" }, metadata = {} },
2448 mismatches = { "a plain mismatch", { mismatch = "a table mismatch", path = "$.foo", expected = 1, actual = 2 } },
2449 output = { "POST /foo", "200 OK" }
2450 }
2451 }
2452 end
2453 "#;
2454
2455 fn start_transport_plugin(plugin_interface_version: u8) -> LuaPactPlugin {
2456 let plugin_dir = tempdir::TempDir::new("lua-transport-plugin-test").unwrap();
2457 std::fs::write(plugin_dir.path().join("entry.lua"), TRANSPORT_PLUGIN_SCRIPT).unwrap();
2458 let manifest = transport_manifest(plugin_dir.path(), plugin_interface_version);
2459 start_lua_plugin(&manifest, "test-instance".to_string()).unwrap()
2462 }
2463
2464 #[tokio::test]
2465 async fn start_mock_server_v1_round_trip() {
2466 let plugin = start_transport_plugin(1);
2467 let response = plugin.start_mock_server(StartMockServerRequest {
2468 host_interface: "127.0.0.1".to_string(),
2469 port: 8080,
2470 tls: false,
2471 pact: "{\"consumer\":{}}".to_string(),
2472 test_context: None,
2473 }).await.unwrap();
2474 match response.response.unwrap() {
2475 start_mock_server_response::Response::Details(details) => {
2476 assert_eq!(details.key, "mock-server-1");
2477 assert_eq!(details.port, 12345);
2478 assert_eq!(details.address, "127.0.0.1:12345");
2479 }
2480 other => panic!("expected mock server details, got {:?}", other),
2481 }
2482 }
2483
2484 #[tokio::test]
2485 async fn start_mock_server_v1_returns_the_lua_error() {
2486 let plugin = start_transport_plugin(1);
2487 let response = plugin.start_mock_server(StartMockServerRequest {
2488 host_interface: "127.0.0.1".to_string(),
2489 port: 0,
2490 tls: false,
2491 pact: "{}".to_string(),
2492 test_context: None,
2493 }).await.unwrap();
2494 match response.response.unwrap() {
2495 start_mock_server_response::Response::Error(err) => assert_eq!(err, "could not bind a mock server"),
2496 other => panic!("expected an error response, got {:?}", other),
2497 }
2498 }
2499
2500 #[tokio::test]
2501 async fn start_mock_server_v2_passes_structured_interactions() {
2502 let plugin = start_transport_plugin(2);
2503 let request = proto_v2::StartMockServerRequest {
2504 host_interface: "127.0.0.1".to_string(),
2505 port: 8080,
2506 tls: false,
2507 interactions: vec![proto_v2::InteractionContents {
2508 interaction_type: "Synchronous/HTTP".to_string(),
2509 plugin_configuration: None,
2510 consumer: "test-consumer".to_string(),
2511 provider: "test-provider".to_string(),
2512 }],
2513 test_context: None,
2514 };
2515 let response = plugin.start_mock_server_v2(request).await.unwrap();
2516 assert!(matches!(response.response.unwrap(), start_mock_server_response::Response::Details(_)));
2517
2518 let lua = plugin.runtime.lock().await;
2519 let captured: Table = lua.globals().get("START_MOCK_SERVER_REQUEST").unwrap();
2520 let interactions: Table = captured.get("interactions").unwrap();
2521 let first: Table = interactions.get(1).unwrap();
2522 assert_eq!(first.get::<String>("interaction_type").unwrap(), "Synchronous/HTTP");
2523 assert_eq!(first.get::<String>("consumer").unwrap(), "test-consumer");
2524 }
2525
2526 #[tokio::test]
2527 async fn shutdown_and_get_mock_server_results_parse_mismatches() {
2528 let plugin = start_transport_plugin(1);
2529
2530 let shutdown_response = plugin.shutdown_mock_server(ShutdownMockServerRequest {
2531 server_key: "mock-server-1".to_string(),
2532 }).await.unwrap();
2533 assert!(!shutdown_response.ok);
2534 assert_eq!(shutdown_response.results.len(), 1);
2535 assert_eq!(shutdown_response.results[0].path, "/foo");
2536 assert_eq!(shutdown_response.results[0].mismatches[0].mismatch, "simple string mismatch");
2537
2538 let results_response = plugin.get_mock_server_results(MockServerRequest {
2539 server_key: "mock-server-1".to_string(),
2540 }).await.unwrap();
2541 assert!(results_response.ok);
2542 assert!(results_response.results.is_empty());
2543 }
2544
2545 #[tokio::test]
2546 async fn prepare_interaction_for_verification_v1_round_trip() {
2547 let plugin = start_transport_plugin(1);
2548 let response = plugin.prepare_interaction_for_verification(VerificationPreparationRequest {
2549 pact: "{}".to_string(),
2550 interaction_key: "interaction-1".to_string(),
2551 config: None,
2552 }).await.unwrap();
2553
2554 match response.response.unwrap() {
2555 verification_preparation_response::Response::InteractionData(data) => {
2556 let body = data.body.unwrap();
2557 assert_eq!(body.content, Some("prepared-body".as_bytes().to_vec()));
2558 let metadata = data.metadata;
2559 assert!(matches!(
2560 metadata["path"].value,
2561 Some(metadata_value::Value::NonBinaryValue(_))
2562 ));
2563 match &metadata["tag"].value {
2564 Some(metadata_value::Value::BinaryValue(bytes)) => assert_eq!(bytes, b"raw-bytes"),
2565 other => panic!("expected a binary metadata value, got {:?}", other),
2566 }
2567 }
2568 other => panic!("expected interaction data, got {:?}", other),
2569 }
2570 }
2571
2572 #[tokio::test]
2573 async fn prepare_interaction_for_verification_v2_passes_interaction_contents() {
2574 let plugin = start_transport_plugin(2);
2575 let request = proto_v2::VerificationPreparationRequest {
2576 interaction_contents: Some(proto_v2::InteractionContents {
2577 interaction_type: "Synchronous/HTTP".to_string(),
2578 plugin_configuration: None,
2579 consumer: "test-consumer".to_string(),
2580 provider: "test-provider".to_string(),
2581 }),
2582 config: None,
2583 test_context: None,
2584 };
2585 let response = plugin.prepare_interaction_for_verification_v2(request).await.unwrap();
2586 assert!(matches!(
2587 response.response.unwrap(),
2588 verification_preparation_response::Response::InteractionData(_)
2589 ));
2590
2591 let lua = plugin.runtime.lock().await;
2592 let captured: Table = lua.globals().get("PREPARE_REQUEST").unwrap();
2593 let interaction_contents: Table = captured.get("interaction_contents").unwrap();
2594 assert_eq!(interaction_contents.get::<String>("provider").unwrap(), "test-provider");
2595 }
2596
2597 #[tokio::test]
2598 async fn verify_interaction_v1_round_trip() {
2599 let plugin = start_transport_plugin(1);
2600 let mut metadata = HashMap::new();
2601 metadata.insert("path".to_string(), MetadataValue {
2602 value: Some(metadata_value::Value::NonBinaryValue(prost_types::Value {
2603 kind: Some(prost_types::value::Kind::StringValue("/foo".to_string())),
2604 })),
2605 });
2606 let response = plugin.verify_interaction(VerifyInteractionRequest {
2607 interaction_data: Some(InteractionData {
2608 body: Some(Body {
2609 content_type: "application/json".to_string(),
2610 content: Some("request-body".as_bytes().to_vec()),
2611 content_type_hint: body::ContentTypeHint::Text as i32,
2612 }),
2613 metadata,
2614 }),
2615 config: None,
2616 pact: "{}".to_string(),
2617 interaction_key: "interaction-1".to_string(),
2618 }).await.unwrap();
2619
2620 match response.response.unwrap() {
2621 verify_interaction_response::Response::Result(result) => {
2622 assert!(result.success);
2623 assert_eq!(result.output, vec!["POST /foo".to_string(), "200 OK".to_string()]);
2624 assert_eq!(result.mismatches.len(), 2);
2625 match &result.mismatches[0].result {
2626 Some(verification_result_item::Result::Error(err)) => assert_eq!(err, "a plain mismatch"),
2627 other => panic!("expected an error mismatch, got {:?}", other),
2628 }
2629 match &result.mismatches[1].result {
2630 Some(verification_result_item::Result::Mismatch(mismatch)) => {
2631 assert_eq!(mismatch.mismatch, "a table mismatch");
2632 assert_eq!(mismatch.expected, Some(b"1".to_vec()));
2633 }
2634 other => panic!("expected a mismatch, got {:?}", other),
2635 }
2636 }
2637 other => panic!("expected a verification result, got {:?}", other),
2638 }
2639
2640 let lua = plugin.runtime.lock().await;
2641 let captured: Table = lua.globals().get("VERIFY_REQUEST").unwrap();
2642 let interaction_data: Table = captured.get("interaction_data").unwrap();
2643 let metadata: Table = interaction_data.get("metadata").unwrap();
2644 assert_eq!(metadata.get::<String>("path").unwrap(), "/foo");
2645 }
2646
2647 #[tokio::test]
2648 async fn verify_interaction_v1_returns_the_lua_error() {
2649 let plugin = start_transport_plugin(1);
2650 let mut config = HashMap::new();
2651 config.insert("fail".to_string(), serde_json::Value::Bool(true));
2652 let response = plugin.verify_interaction(VerifyInteractionRequest {
2653 interaction_data: None,
2654 config: Some(to_proto_struct(&config)),
2655 pact: "{}".to_string(),
2656 interaction_key: "interaction-1".to_string(),
2657 }).await.unwrap();
2658 match response.response.unwrap() {
2659 verify_interaction_response::Response::Error(err) => assert_eq!(err, "verification failed"),
2660 other => panic!("expected an error response, got {:?}", other),
2661 }
2662 }
2663
2664 #[tokio::test]
2665 async fn verify_interaction_v2_converts_the_v2_interaction_data_and_contents() {
2666 let plugin = start_transport_plugin(2);
2667 let request = proto_v2::VerifyInteractionRequest {
2668 interaction_data: Some(proto_v2::InteractionData {
2669 body: Some(proto_v2::Body {
2670 content_type: "application/json".to_string(),
2671 content: Some("request-body".as_bytes().to_vec()),
2672 content_type_hint: 0,
2673 }),
2674 metadata: HashMap::new(),
2675 }),
2676 config: None,
2677 interaction_contents: Some(proto_v2::InteractionContents {
2678 interaction_type: "Synchronous/HTTP".to_string(),
2679 plugin_configuration: None,
2680 consumer: "test-consumer".to_string(),
2681 provider: "test-provider".to_string(),
2682 }),
2683 test_context: None,
2684 };
2685 let response = plugin.verify_interaction_v2(request).await.unwrap();
2686 assert!(matches!(
2687 response.response.unwrap(),
2688 verify_interaction_response::Response::Result(_)
2689 ));
2690
2691 let lua = plugin.runtime.lock().await;
2692 let captured: Table = lua.globals().get("VERIFY_REQUEST").unwrap();
2693 let interaction_data: Table = captured.get("interaction_data").unwrap();
2694 let body: Table = interaction_data.get("body").unwrap();
2695 assert_eq!(body.get::<mlua::LuaString>("contents").unwrap().to_str().unwrap(), "request-body");
2696 let interaction_contents: Table = captured.get("interaction_contents").unwrap();
2697 assert_eq!(interaction_contents.get::<String>("consumer").unwrap(), "test-consumer");
2698 }
2699
2700 #[tokio::test]
2701 async fn shutdown_mock_server_defaults_ok_to_true_when_the_field_is_absent() {
2702 let plugin_dir = tempdir::TempDir::new("lua-transport-plugin-test").unwrap();
2710 std::fs::write(
2711 plugin_dir.path().join("entry.lua"),
2712 r#"
2713 function shutdown_mock_server(server_key)
2714 return { results = {} }
2715 end
2716 "#,
2717 ).unwrap();
2718 let manifest = transport_manifest(plugin_dir.path(), 1);
2719 let plugin = start_lua_plugin(&manifest, "test-instance".to_string()).unwrap();
2720
2721 let response = plugin.shutdown_mock_server(ShutdownMockServerRequest {
2722 server_key: "mock-server-1".to_string(),
2723 }).await.unwrap();
2724 assert!(response.ok, "expected 'ok' to default to true when the Lua script doesn't set it");
2725 }
2726
2727 #[tokio::test]
2728 async fn shutdown_mock_server_errors_on_a_wrong_typed_path_field() {
2729 let plugin_dir = tempdir::TempDir::new("lua-transport-plugin-test").unwrap();
2730 std::fs::write(
2731 plugin_dir.path().join("entry.lua"),
2732 r#"
2733 function shutdown_mock_server(server_key)
2734 return { ok = false, results = { { path = {}, error = "boom", mismatches = {} } } }
2735 end
2736 "#,
2737 ).unwrap();
2738 let manifest = transport_manifest(plugin_dir.path(), 1);
2739 let plugin = start_lua_plugin(&manifest, "test-instance".to_string()).unwrap();
2740
2741 let result = plugin.shutdown_mock_server(ShutdownMockServerRequest {
2742 server_key: "mock-server-1".to_string(),
2743 }).await;
2744 assert!(
2745 result.is_err(),
2746 "expected a wrong-typed 'path' field (a table, not a string) to be a hard error, not silently default, got {:?}",
2747 result
2748 );
2749 }
2750
2751 fn creditcard_manifest() -> PactPluginManifest {
2754 let plugin_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../../plugins/creditcard");
2756 assert!(plugin_dir.exists(), "plugins/creditcard directory should exist at {:?}", plugin_dir);
2757 PactPluginManifest {
2758 plugin_dir: plugin_dir.to_string_lossy().to_string(),
2759 plugin_interface_version: 2,
2760 name: "creditcard".to_string(),
2761 version: "0.0.0".to_string(),
2762 executable_type: "lua".to_string(),
2763 minimum_required_version: None,
2764 entry_point: "plugin.lua".to_string(),
2765 entry_points: HashMap::new(),
2766 args: None,
2767 dependencies: None,
2768 plugin_config: HashMap::new(),
2769 }
2770 }
2771
2772 fn text_field(value: &str) -> proto_v2::FieldValue {
2773 proto_v2::FieldValue {
2774 value: Some(proto_v2::field_value::Value::StringValue(value.to_string()))
2775 }
2776 }
2777
2778 fn creditcard_match_request(brand: Option<&str>, expected: &str, actual: &str) -> proto_v2::MatchFieldRequest {
2780 let values = brand.map(|brand| {
2781 let mut fields = HashMap::new();
2782 fields.insert("brand".to_string(), serde_json::Value::String(brand.to_string()));
2783 to_proto_struct(&fields)
2784 });
2785 proto_v2::MatchFieldRequest {
2786 key: "creditcard".to_string(),
2787 rule: Some(proto_v2::MatchingRule { r#type: "creditcard".to_string(), values }),
2788 path: "$.card.number".to_string(),
2789 mismatch_type: "body".to_string(),
2790 expected: Some(text_field(expected)),
2791 actual: Some(text_field(actual)),
2792 plugin_configuration: None,
2793 test_context: None,
2794 }
2795 }
2796
2797 fn creditcard_generate_request(brand: Option<&str>, example: &str) -> proto_v2::GenerateFieldRequest {
2798 let values = brand.map(|brand| {
2799 let mut fields = HashMap::new();
2800 fields.insert("brand".to_string(), serde_json::Value::String(brand.to_string()));
2801 to_proto_struct(&fields)
2802 });
2803 proto_v2::GenerateFieldRequest {
2804 key: "creditcard".to_string(),
2805 generator: Some(proto_v2::Generator { r#type: "creditcard".to_string(), values }),
2806 path: "$.card.number".to_string(),
2807 example_value: Some(text_field(example)),
2808 plugin_configuration: None,
2809 test_context: None,
2810 test_mode: proto_v2::generate_content_request::TestMode::Consumer as i32,
2811 }
2812 }
2813
2814 #[tokio::test]
2815 async fn creditcard_plugin_registers_a_matcher_and_a_generator_under_the_same_key() {
2816 let manifest = creditcard_manifest();
2817 let plugin = start_lua_plugin(&manifest, "test-instance".to_string()).unwrap();
2818 let lua = plugin.runtime.lock().await;
2819 let entries = call_init(&lua, "test", "0.0.0").unwrap();
2820
2821 assert_eq!(entries.len(), 2);
2822 assert_eq!(entries[0].key, "creditcard");
2823 assert_eq!(entries[0].r#type, proto_v2::catalogue_entry::EntryType::Matcher as i32);
2824 assert_eq!(entries[1].key, "creditcard");
2825 assert_eq!(entries[1].r#type, proto_v2::catalogue_entry::EntryType::Generator as i32);
2826 assert_eq!(entries[0].values.get("config-key"), Some(&"brand".to_string()));
2828 }
2829
2830 #[tokio::test]
2831 async fn creditcard_plugin_accepts_a_valid_card_number() {
2832 let plugin = start_lua_plugin(&creditcard_manifest(), "test-instance".to_string()).unwrap();
2833
2834 let response = plugin
2835 .match_field(creditcard_match_request(Some("visa"), "4111111111111111", "4012888888881881"))
2836 .await
2837 .unwrap();
2838
2839 assert_eq!(response.error, "");
2840 assert!(response.mismatches.is_empty(), "expected no mismatches, got {:?}", response.mismatches);
2841 }
2842
2843 #[tokio::test]
2844 async fn creditcard_plugin_reports_a_number_that_fails_the_luhn_check() {
2845 let plugin = start_lua_plugin(&creditcard_manifest(), "test-instance".to_string()).unwrap();
2846
2847 let response = plugin
2848 .match_field(creditcard_match_request(None, "4111111111111111", "4111111111111112"))
2849 .await
2850 .unwrap();
2851
2852 assert_eq!(response.error, "");
2853 assert_eq!(response.mismatches.len(), 1);
2854 let mismatch = &response.mismatches[0];
2855 assert!(
2856 mismatch.mismatch.contains("Luhn check"),
2857 "unexpected mismatch description: {}", mismatch.mismatch
2858 );
2859 assert_eq!(mismatch.path, "$.card.number");
2861 assert_eq!(mismatch.mismatch_type, "body");
2862 assert_eq!(mismatch.expected.as_deref(), Some("4111111111111111".as_bytes()));
2863 assert_eq!(mismatch.actual.as_deref(), Some("4111111111111112".as_bytes()));
2864 }
2865
2866 #[tokio::test]
2867 async fn creditcard_plugin_reports_a_number_from_the_wrong_brand() {
2868 let plugin = start_lua_plugin(&creditcard_manifest(), "test-instance".to_string()).unwrap();
2869
2870 let response = plugin
2872 .match_field(creditcard_match_request(Some("mastercard"), "5555555555554444", "4012888888881881"))
2873 .await
2874 .unwrap();
2875
2876 assert_eq!(response.mismatches.len(), 1);
2877 assert!(
2878 response.mismatches[0].mismatch.contains("Mastercard"),
2879 "unexpected mismatch description: {}", response.mismatches[0].mismatch
2880 );
2881 }
2882
2883 #[tokio::test]
2884 async fn creditcard_plugin_reports_a_misconfigured_brand_as_an_error_not_a_mismatch() {
2885 let plugin = start_lua_plugin(&creditcard_manifest(), "test-instance".to_string()).unwrap();
2888
2889 let response = plugin
2890 .match_field(creditcard_match_request(Some("amx"), "4111111111111111", "4111111111111111"))
2891 .await
2892 .unwrap();
2893
2894 assert!(
2895 response.error.contains("'amx' is not a credit card brand"),
2896 "unexpected error: {}", response.error
2897 );
2898 assert!(response.mismatches.is_empty());
2899 }
2900
2901 #[tokio::test]
2902 async fn creditcard_plugin_generates_a_number_for_the_configured_brand() {
2903 let plugin = start_lua_plugin(&creditcard_manifest(), "test-instance".to_string()).unwrap();
2904
2905 let response = plugin
2906 .generate_field(creditcard_generate_request(Some("amex"), "4111111111111111"))
2907 .await
2908 .unwrap();
2909
2910 assert_eq!(response.error, "");
2911 let generated = match response.value.and_then(|value| value.value) {
2912 Some(proto_v2::field_value::Value::StringValue(value)) => value,
2913 other => panic!("expected a generated string value, got {:?}", other)
2914 };
2915 assert_eq!(generated.len(), 15, "an Amex number has 15 digits, got '{}'", generated);
2916 assert!(generated.starts_with("34") || generated.starts_with("37"), "got '{}'", generated);
2917
2918 let match_response = plugin
2920 .match_field(creditcard_match_request(Some("amex"), "371449635398431", &generated))
2921 .await
2922 .unwrap();
2923 assert!(
2924 match_response.mismatches.is_empty(),
2925 "the plugin should accept its own generated number, got {:?}", match_response.mismatches
2926 );
2927 }
2928
2929 #[tokio::test]
2930 async fn creditcard_plugin_reports_a_generator_error() {
2931 let plugin = start_lua_plugin(&creditcard_manifest(), "test-instance".to_string()).unwrap();
2932
2933 let response = plugin
2934 .generate_field(creditcard_generate_request(Some("amx"), "4111111111111111"))
2935 .await
2936 .unwrap();
2937
2938 assert!(response.error.contains("amx"), "unexpected error: {}", response.error);
2939 assert!(response.value.is_none());
2940 }
2941
2942 fn start_field_plugin(name: &str, script: &str) -> (tempdir::TempDir, LuaPactPlugin) {
2944 let plugin_dir = tempdir::TempDir::new("lua-plugin-test").unwrap();
2945 std::fs::write(plugin_dir.path().join("entry.lua"), script).unwrap();
2946 let manifest = lua_manifest(plugin_dir.path(), name);
2947 let plugin = start_lua_plugin(&manifest, "test-instance".to_string()).unwrap();
2948 (plugin_dir, plugin)
2950 }
2951
2952 #[tokio::test]
2953 async fn each_field_value_type_survives_the_round_trip_through_lua() {
2954 use proto_v2::field_value::Value as FieldValue;
2955
2956 let (_dir, plugin) = start_field_plugin(
2957 "field-value-round-trip-test",
2958 r#"
2959 function generate_field(request)
2960 return { value = request.example_value }
2961 end
2962 "#,
2963 );
2964
2965 let values = vec![
2966 FieldValue::NullValue(0),
2967 FieldValue::BooleanValue(true),
2968 FieldValue::StringValue("4111111111111111".to_string()),
2969 FieldValue::IntegerValue(100),
2970 FieldValue::DecimalValue(100.5),
2971 FieldValue::BinaryValue(vec![0, 159, 146, 150]),
2972 ];
2973
2974 for value in values {
2975 let request = proto_v2::GenerateFieldRequest {
2976 generator: Some(proto_v2::Generator::default()),
2977 example_value: Some(proto_v2::FieldValue { value: Some(value.clone()) }),
2978 .. proto_v2::GenerateFieldRequest::default()
2979 };
2980 let response = plugin.generate_field(request).await.unwrap();
2981 assert_eq!(
2982 response.value.and_then(|response| response.value), Some(value.clone()),
2983 "{:?} did not survive the round trip through Lua", value
2984 );
2985 }
2986 }
2987
2988 #[tokio::test]
2989 async fn a_whole_number_reaches_lua_as_an_integer_and_a_decimal_as_a_float() {
2990 let (_dir, plugin) = start_field_plugin(
2993 "field-value-lua-type-test",
2994 r#"
2995 function generate_field(request)
2996 return { value = math.type(request.example_value) }
2997 end
2998 "#,
2999 );
3000
3001 let lua_type_of = async |value: proto_v2::field_value::Value| {
3002 let request = proto_v2::GenerateFieldRequest {
3003 example_value: Some(proto_v2::FieldValue { value: Some(value) }),
3004 .. proto_v2::GenerateFieldRequest::default()
3005 };
3006 match plugin.generate_field(request).await.unwrap().value.and_then(|value| value.value) {
3007 Some(proto_v2::field_value::Value::StringValue(value)) => value,
3008 other => panic!("expected math.type() to return a string, got {:?}", other)
3009 }
3010 };
3011
3012 assert_eq!(lua_type_of(proto_v2::field_value::Value::IntegerValue(100)).await, "integer");
3013 assert_eq!(lua_type_of(proto_v2::field_value::Value::DecimalValue(100.0)).await, "float");
3014 }
3015
3016 #[tokio::test]
3017 async fn a_mismatch_that_does_not_place_itself_is_reported_against_the_request_path() {
3018 let (_dir, plugin) = start_field_plugin(
3019 "field-mismatch-path-test",
3020 r#"
3021 function match_field(request)
3022 return { mismatches = { "not a card number" } }
3023 end
3024 "#,
3025 );
3026
3027 let response = plugin
3028 .match_field(creditcard_match_request(None, "4111111111111111", "nope"))
3029 .await
3030 .unwrap();
3031
3032 assert_eq!(response.mismatches.len(), 1);
3033 assert_eq!(response.mismatches[0].mismatch, "not a card number");
3034 assert_eq!(response.mismatches[0].path, "$.card.number");
3035 }
3036
3037 #[tokio::test]
3038 async fn a_plugin_that_does_not_define_the_field_functions_says_so() {
3039 let (_dir, plugin) = start_field_plugin("field-functions-missing-test", "-- nothing here");
3040
3041 let match_error = plugin.match_field(proto_v2::MatchFieldRequest::default()).await
3042 .expect_err("expected an error when the plugin does not define match_field");
3043 assert!(
3044 match_error.to_string().contains("does not define a global 'match_field' function"),
3045 "unexpected error: {}", match_error
3046 );
3047
3048 let generate_error = plugin.generate_field(proto_v2::GenerateFieldRequest::default()).await
3049 .expect_err("expected an error when the plugin does not define generate_field");
3050 assert!(
3051 generate_error.to_string().contains("does not define a global 'generate_field' function"),
3052 "unexpected error: {}", generate_error
3053 );
3054 }
3055
3056 fn core_field_matcher_entry(key: &str) -> crate::catalogue_manager::CatalogueEntry {
3057 crate::catalogue_manager::CatalogueEntry {
3058 entry_type: crate::catalogue_manager::CatalogueEntryType::MATCHER,
3059 provider_type: crate::catalogue_manager::CatalogueEntryProviderType::CORE,
3060 plugin: None,
3061 key: key.to_string(),
3062 values: HashMap::new()
3063 }
3064 }
3065
3066 fn core_field_generator_entry(key: &str) -> crate::catalogue_manager::CatalogueEntry {
3067 crate::catalogue_manager::CatalogueEntry {
3068 entry_type: crate::catalogue_manager::CatalogueEntryType::GENERATOR,
3069 provider_type: crate::catalogue_manager::CatalogueEntryProviderType::CORE,
3070 plugin: None,
3071 key: key.to_string(),
3072 values: HashMap::new()
3073 }
3074 }
3075
3076 struct EchoCoreFieldMatcher;
3079
3080 #[async_trait]
3081 impl crate::core_capabilities::CoreFieldMatcher for EchoCoreFieldMatcher {
3082 async fn match_field(&self, request: proto_v2::MatchFieldRequest) -> anyhow::Result<proto_v2::MatchFieldResponse> {
3083 let rule = request.rule.unwrap_or_default();
3084 Ok(proto_v2::MatchFieldResponse {
3085 error: String::new(),
3086 mismatches: vec![proto_v2::ContentMismatch {
3087 expected: None,
3088 actual: None,
3089 mismatch: format!("core matcher saw rule '{}' at {}", rule.r#type, request.path),
3090 path: request.path,
3091 diff: String::new(),
3092 mismatch_type: request.mismatch_type,
3093 }]
3094 })
3095 }
3096 }
3097
3098 #[tokio::test]
3099 async fn match_field_calls_host_match_field_for_a_registered_core_capability() {
3100 let key = "match_field_calls_host_match_field_for_a_registered_core_capability";
3103 crate::catalogue_manager::register_core_entries(&vec![core_field_matcher_entry(key)]);
3104 crate::core_capabilities::register_core_field_matcher(key, Arc::new(EchoCoreFieldMatcher));
3105
3106 let (_dir, plugin) = start_field_plugin(
3107 "host-match-field-test",
3108 &format!(r#"
3109 function match_field(request)
3110 return host_match_field("{key}", request)
3111 end
3112 "#, key = key),
3113 );
3114
3115 let response = plugin
3116 .match_field(creditcard_match_request(Some("visa"), "4111111111111111", "4012888888881881"))
3117 .await
3118 .unwrap();
3119
3120 crate::core_capabilities::deregister_core_field_matcher(key);
3121
3122 assert_eq!(response.mismatches.len(), 1);
3123 assert_eq!(
3124 response.mismatches[0].mismatch,
3125 "core matcher saw rule 'creditcard' at $.card.number"
3126 );
3127 assert_eq!(response.mismatches[0].mismatch_type, "body");
3128 }
3129
3130 #[tokio::test]
3131 async fn host_match_field_surfaces_a_clear_error_when_the_entry_is_not_registered() {
3132 let key = "host_match_field_surfaces_a_clear_error_when_the_entry_is_not_registered";
3133 let (_dir, plugin) = start_field_plugin(
3134 "host-match-field-missing-test",
3135 &format!(r#"
3136 function match_field(request)
3137 return host_match_field("{key}", request)
3138 end
3139 "#, key = key),
3140 );
3141
3142 let err = plugin.match_field(creditcard_match_request(None, "4111111111111111", "4111111111111111"))
3143 .await
3144 .expect_err("expected an error when the target entry is not registered");
3145 assert!(
3146 err.to_string().contains("No catalogue entry found"),
3147 "unexpected error message: {}", err
3148 );
3149 }
3150
3151 struct FixedCoreFieldGenerator;
3152
3153 #[async_trait]
3154 impl crate::core_capabilities::CoreFieldGenerator for FixedCoreFieldGenerator {
3155 async fn generate_field(&self, _request: proto_v2::GenerateFieldRequest) -> anyhow::Result<proto_v2::GenerateFieldResponse> {
3156 Ok(proto_v2::GenerateFieldResponse {
3157 error: String::new(),
3158 value: Some(text_field("generated by the host"))
3159 })
3160 }
3161 }
3162
3163 #[tokio::test]
3164 async fn generate_field_calls_host_generate_field_for_a_registered_core_capability() {
3165 let key = "generate_field_calls_host_generate_field_for_a_registered_core_capability";
3166 crate::catalogue_manager::register_core_entries(&vec![core_field_generator_entry(key)]);
3167 crate::core_capabilities::register_core_field_generator(key, Arc::new(FixedCoreFieldGenerator));
3168
3169 let (_dir, plugin) = start_field_plugin(
3170 "host-generate-field-test",
3171 &format!(r#"
3172 function generate_field(request)
3173 return host_generate_field("{key}", request)
3174 end
3175 "#, key = key),
3176 );
3177
3178 let response = plugin
3179 .generate_field(creditcard_generate_request(Some("visa"), "4111111111111111"))
3180 .await
3181 .unwrap();
3182
3183 crate::core_capabilities::deregister_core_field_generator(key);
3184
3185 assert_eq!(response.error, "");
3186 assert_eq!(
3187 response.value.and_then(|value| value.value),
3188 Some(proto_v2::field_value::Value::StringValue("generated by the host".to_string()))
3189 );
3190 }
3191}