use pywatt_sdk::{
builder::ModuleBuilder, OrchestratorInit, AnnouncedEndpoint,
communication::ipc_types::{Init, ListenAddress, OrchestratorToModule},
};
use secrecy::SecretString;
use std::collections::HashMap;
use std::io::Cursor;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::pin::Pin;
use std::sync::Once;
use std::task::{Context, Poll};
use tokio::io::{AsyncBufRead, AsyncRead};
struct MockStdin {
cursor: Cursor<Vec<u8>>,
}
impl MockStdin {
#[allow(dead_code)]
fn new(data: &str) -> Self {
let mut bytes = Vec::new();
bytes.extend_from_slice(data.as_bytes());
Self {
cursor: Cursor::new(bytes),
}
}
}
impl AsyncRead for MockStdin {
fn poll_read(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
buf: &mut tokio::io::ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
let this = self.get_mut();
let cursor_buf = this.cursor.get_ref();
let pos = this.cursor.position() as usize;
let remaining = &cursor_buf[pos..];
let amt = std::cmp::min(remaining.len(), buf.remaining());
if amt == 0 {
return Poll::Ready(Ok(()));
}
buf.put_slice(&remaining[..amt]);
this.cursor.set_position((pos + amt) as u64);
Poll::Ready(Ok(()))
}
}
impl AsyncBufRead for MockStdin {
fn poll_fill_buf(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<std::io::Result<&[u8]>> {
let this = self.get_mut();
let buf = this.cursor.get_ref();
let pos = this.cursor.position() as usize;
let remaining = &buf[pos..];
Poll::Ready(Ok(remaining))
}
fn consume(self: Pin<&mut Self>, amt: usize) {
let this = self.get_mut();
let pos = this.cursor.position() as usize;
this.cursor.set_position((pos + amt) as u64);
}
}
static LOGGING_INIT: Once = Once::new();
fn setup_logging() {
LOGGING_INIT.call_once(|| {
});
}
#[tokio::test]
async fn test_init_structure() {
let mut env = HashMap::new();
env.insert("TEST_SECRET".to_string(), "test_value".to_string());
let init = Init::new(
"http://localhost:8000".to_string(),
"test-module".to_string(),
ListenAddress::Tcp(SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
8080,
)),
).with_env(env);
let json = serde_json::to_string(&OrchestratorToModule::Init(init.clone())).unwrap();
let parsed: OrchestratorToModule = serde_json::from_str(&json).unwrap();
match parsed {
OrchestratorToModule::Init(init_blob) => {
assert_eq!(init_blob.orchestrator_api, "http://localhost:8000");
assert_eq!(init_blob.module_id, "test-module");
assert_eq!(init_blob.env.len(), 1);
assert_eq!(init_blob.env.get("TEST_SECRET").unwrap(), "test_value");
}
_ => panic!("Expected Init message"),
}
}
#[tokio::test]
async fn test_init_error_cases() {
let invalid_json = r#"{"invalid json"#;
let result = serde_json::from_str::<Init>(invalid_json);
assert!(result.is_err());
let missing_fields = r#"{"module_id": "test"}"#;
let result = serde_json::from_str::<Init>(missing_fields);
assert!(result.is_err());
}
#[tokio::test]
async fn test_bootstrap_state_builder() {
setup_logging();
let state_builder = |init: &Init, secrets: Vec<secrecy::SecretString>| {
let mut state = HashMap::new();
state.insert("module_id".to_string(), init.module_id.clone());
state.insert("secret_count".to_string(), secrets.len().to_string());
state
};
let _endpoints = [AnnouncedEndpoint {
path: "/api/test".to_string(),
methods: vec!["GET".to_string()],
auth: None,
}];
let mut env = HashMap::new();
env.insert("TEST_SECRET".to_string(), "test_value".to_string());
let init = Init::new(
"http://localhost:8000".to_string(),
"test-module".to_string(),
ListenAddress::Tcp(SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
8080,
)),
).with_env(env);
let secrets = vec![secrecy::SecretString::new("test_secret".to_string().into())];
let state = state_builder(&init, secrets);
assert_eq!(state.get("module_id").unwrap(), "test-module");
assert_eq!(state.get("secret_count").unwrap(), "1");
}
fn create_mock_orchestrator_init() -> OrchestratorInit {
OrchestratorInit::new(
"http://localhost:1234".to_string(),
"test_module".to_string(),
ListenAddress::Tcp("127.0.0.1:0".parse().unwrap()),
)
.with_env(HashMap::new())
}
#[allow(dead_code)]
fn create_mock_secrets() -> Vec<SecretString> {
vec![secrecy::SecretString::new(
"test_secret_value".to_string().into(),
)]
}
#[tokio::test]
async fn test_module_builder_with_state_initialization() {
let init = create_mock_orchestrator_init();
let state_builder =
|_init: &OrchestratorInit, _secrets: Vec<SecretString>| -> HashMap<String, String> {
let mut state = HashMap::new();
state.insert("module_id".to_string(), _init.module_id.clone());
state.insert("secret_count".to_string(), "0".to_string());
state
};
let user_state = state_builder(&init, vec![]);
assert_eq!(user_state.get("module_id").unwrap(), "test_module");
assert_eq!(user_state.get("secret_count").unwrap(), "0");
}
#[tokio::test]
async fn test_module_builder_compiles_with_state() {
let _builder = ModuleBuilder::new().state(
|_init: &OrchestratorInit, _secrets: Vec<SecretString>| -> HashMap<String, String> {
let mut state = HashMap::new();
state.insert("module_id".to_string(), _init.module_id.clone());
state.insert("secret_count".to_string(), _secrets.len().to_string());
state
},
);
}