use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::validation::{Validate, ValidationError};
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct TestPluginConfigRequest {
pub plugin_type: String,
pub config: serde_json::Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub plugin_config_id: Option<Uuid>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub host_id: Option<Uuid>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub test_kind: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub package_identifier: Option<String>,
}
impl TestPluginConfigRequest {
pub fn new(plugin_type: String, config: serde_json::Value) -> Self {
Self {
plugin_type,
config,
plugin_config_id: None,
host_id: None,
test_kind: None,
package_identifier: None,
}
}
#[must_use]
pub fn with_plugin_config_id(mut self, id: Option<uuid::Uuid>) -> Self {
self.plugin_config_id = id;
self
}
#[must_use]
pub fn with_host_id(mut self, id: Option<uuid::Uuid>) -> Self {
self.host_id = id;
self
}
#[must_use]
pub fn with_test_kind(mut self, kind: Option<String>) -> Self {
self.test_kind = kind;
self
}
#[must_use]
pub fn with_package_identifier(mut self, identifier: Option<String>) -> Self {
self.package_identifier = identifier;
self
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct TestPluginConfigResponse {
pub success: bool,
pub test_kind: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub detected_version: Option<String>,
pub duration_ms: u64,
}
impl TestPluginConfigResponse {
pub fn new(success: bool, test_kind: String, duration_ms: u64) -> Self {
Self {
success,
test_kind,
output: None,
error: None,
detected_version: None,
duration_ms,
}
}
}
impl Validate for TestPluginConfigRequest {
fn validate(&self) -> Result<(), ValidationError> {
if self.plugin_type.trim().is_empty() {
return Err(ValidationError {
field: "plugin_type",
message: "plugin_type must not be empty".to_string(),
});
}
Ok(())
}
}
#[cfg(test)]
mod tests {
#![expect(
clippy::assertions_on_result_states,
reason = "test assertions — is_ok/is_err provides readable failure messages"
)]
use super::*;
#[test]
fn validate_empty_plugin_type() {
let req = TestPluginConfigRequest {
plugin_type: "".to_string(),
config: serde_json::json!({}),
plugin_config_id: None,
host_id: None,
test_kind: None,
package_identifier: None,
};
let err = req.validate().unwrap_err();
assert_eq!(err.field, "plugin_type");
}
#[test]
fn validate_whitespace_plugin_type() {
let req = TestPluginConfigRequest {
plugin_type: " ".to_string(),
config: serde_json::json!({}),
plugin_config_id: None,
host_id: None,
test_kind: None,
package_identifier: None,
};
assert!(req.validate().is_err());
}
#[test]
fn validate_valid_request() {
let req = TestPluginConfigRequest {
plugin_type: "generic.shell".to_string(),
config: serde_json::json!({"version_command": "nginx -v"}),
plugin_config_id: None,
host_id: None,
test_kind: None,
package_identifier: None,
};
assert!(req.validate().is_ok());
}
#[test]
fn response_serialization_roundtrip() {
let resp = TestPluginConfigResponse {
success: true,
test_kind: "version_detection".to_string(),
output: Some("nginx version: 1.24.0".to_string()),
error: None,
detected_version: Some("1.24.0".to_string()),
duration_ms: 150,
};
let json = serde_json::to_string(&resp).unwrap();
let de: TestPluginConfigResponse = serde_json::from_str(&json).unwrap();
assert!(de.success);
assert_eq!(de.test_kind, "version_detection");
assert_eq!(de.detected_version.as_deref(), Some("1.24.0"));
assert_eq!(de.duration_ms, 150);
}
#[test]
fn request_serialization_roundtrip() {
let req = TestPluginConfigRequest {
plugin_type: "releases.github".to_string(),
config: serde_json::json!({"api_url": "https://api.github.com"}),
plugin_config_id: None,
host_id: Some(uuid::Uuid::nil()),
test_kind: Some("connectivity".to_string()),
package_identifier: Some("owner/repo".to_string()),
};
let json = serde_json::to_string(&req).unwrap();
let de: TestPluginConfigRequest = serde_json::from_str(&json).unwrap();
assert_eq!(de.plugin_type, "releases.github");
assert_eq!(de.host_id, Some(uuid::Uuid::nil()));
assert_eq!(de.test_kind.as_deref(), Some("connectivity"));
}
#[test]
fn response_omits_none_fields() {
let resp = TestPluginConfigResponse {
success: false,
test_kind: "connectivity".to_string(),
output: None,
error: Some("connection refused".to_string()),
detected_version: None,
duration_ms: 5000,
};
let json = serde_json::to_string(&resp).unwrap();
assert!(!json.contains("output"));
assert!(!json.contains("detected_version"));
assert!(json.contains("error"));
}
}