use anyhow::{anyhow, Result};
use rekuiper_conf::KuiperConfig;
use rekuiper_core::{RuleManager, StreamBus, StreamManager, TableManager};
use rekuiper_server::routes::{
create_router, load_config_maps, restore_running_rules, test_sse_router, AppState,
};
use serde_json::json;
use std::time::Duration;
use tokio::net::TcpListener;
struct TestServer {
rest_url: String,
data_url: String,
state: AppState,
_handles: Vec<tokio::task::JoinHandle<()>>,
}
impl TestServer {
async fn start() -> Self {
let rest_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let rest_addr = rest_listener.local_addr().unwrap();
let data_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let data_addr = data_listener.local_addr().unwrap();
let mut config = KuiperConfig::default();
config.basic.http_server_ip = "127.0.0.1".to_string();
config.basic.http_server_port = data_addr.port();
let stream_bus = StreamBus::new();
let stream_manager = StreamManager::new();
let table_manager = TableManager::new();
let rule_manager = RuleManager::new(stream_bus.clone());
let state = AppState::new(
"test".to_string(),
config,
stream_manager,
table_manager,
rule_manager,
stream_bus,
);
load_config_maps(&state).await.unwrap();
restore_running_rules(&state).await;
let rest_app = create_router(state.clone());
let rest_handle = tokio::spawn(async move {
let _ = axum::serve(rest_listener, rest_app).await;
});
let data_app = test_sse_router(state.clone());
let data_handle = tokio::spawn(async move {
let _ = axum::serve(data_listener, data_app).await;
});
Self {
rest_url: format!("http://{}", rest_addr),
data_url: format!("http://{}", data_addr),
state,
_handles: vec![rest_handle, data_handle],
}
}
}
#[tokio::test]
async fn test_issue28_httppush_single_and_batch_records() -> Result<()> {
let server = TestServer::start().await;
let client = reqwest::Client::new();
let resp = client
.post(format!("{}/streams", server.rest_url))
.json(&json!({
"sql": "CREATE STREAM h () WITH (DATASOURCE=\"/xp/push\", FORMAT=\"json\", TYPE=\"httppush\")"
}))
.send()
.await?;
assert_eq!(resp.status(), reqwest::StatusCode::CREATED);
let mut sink_rx = server.state.stream_bus.subscribe("h_out");
let resp = client
.post(format!("{}/xp/push", server.data_url))
.json(&json!({"id": 0, "v": "pre_rule"}))
.send()
.await?;
assert_eq!(resp.status(), reqwest::StatusCode::NOT_FOUND);
let resp = client
.post(format!("{}/rules", server.rest_url))
.json(&json!({
"id": "hr",
"sql": "SELECT id, v FROM h",
"actions": [{"memory": {"topic": "h_out"}}]
}))
.send()
.await?;
assert_eq!(resp.status(), reqwest::StatusCode::CREATED);
let resp = client
.post(format!("{}/xp/push", server.data_url))
.json(&json!({"id": 1, "v": "pushed"}))
.send()
.await?;
assert_eq!(resp.status(), reqwest::StatusCode::OK);
let body = resp.text().await?;
assert_eq!(body, "ok");
let rec = tokio::time::timeout(Duration::from_secs(2), sink_rx.recv())
.await?
.ok_or_else(|| anyhow!("sink closed"))?;
assert_eq!(rec.data.get("id"), Some(&json!(1)));
assert_eq!(rec.data.get("v"), Some(&json!("pushed")));
let resp = client
.post(format!("{}/xp/push", server.data_url))
.json(&json!([{"id": 2, "v": "batch2"}, {"id": 3, "v": "batch3"}]))
.send()
.await?;
assert_eq!(resp.status(), reqwest::StatusCode::OK);
assert_eq!(resp.text().await?, "ok");
let rec2 = tokio::time::timeout(Duration::from_secs(2), sink_rx.recv())
.await?
.ok_or_else(|| anyhow!("sink closed"))?;
assert_eq!(rec2.data.get("id"), Some(&json!(2)));
assert_eq!(rec2.data.get("v"), Some(&json!("batch2")));
let rec3 = tokio::time::timeout(Duration::from_secs(2), sink_rx.recv())
.await?
.ok_or_else(|| anyhow!("sink closed"))?;
assert_eq!(rec3.data.get("id"), Some(&json!(3)));
assert_eq!(rec3.data.get("v"), Some(&json!("batch3")));
Ok(())
}
#[tokio::test]
async fn test_issue28_httppush_lifecycle_and_shared_rules() -> Result<()> {
let server = TestServer::start().await;
let client = reqwest::Client::new();
let resp = client
.post(format!("{}/streams", server.rest_url))
.json(&json!({
"sql": "CREATE STREAM s_shared () WITH (DATASOURCE=\"/api/telemetry\", FORMAT=\"json\", TYPE=\"httppush\")"
}))
.send()
.await?;
assert_eq!(resp.status(), reqwest::StatusCode::CREATED);
let mut sink_r1 = server.state.stream_bus.subscribe("out_r1");
let mut sink_r2 = server.state.stream_bus.subscribe("out_r2");
let resp = client
.post(format!("{}/rules", server.rest_url))
.json(&json!({
"id": "r1",
"sql": "SELECT id FROM s_shared",
"actions": [{"memory": {"topic": "out_r1"}}]
}))
.send()
.await?;
assert_eq!(resp.status(), reqwest::StatusCode::CREATED);
let resp = client
.post(format!("{}/rules", server.rest_url))
.json(&json!({
"id": "r2",
"sql": "SELECT id FROM s_shared",
"actions": [{"memory": {"topic": "out_r2"}}]
}))
.send()
.await?;
assert_eq!(resp.status(), reqwest::StatusCode::CREATED);
let resp = client
.post(format!("{}/api/telemetry", server.data_url))
.json(&json!({"id": 100}))
.send()
.await?;
assert_eq!(resp.status(), reqwest::StatusCode::OK);
let rec1 = tokio::time::timeout(Duration::from_secs(2), sink_r1.recv())
.await?
.ok_or_else(|| anyhow!("sink r1 closed"))?;
let rec2 = tokio::time::timeout(Duration::from_secs(2), sink_r2.recv())
.await?
.ok_or_else(|| anyhow!("sink r2 closed"))?;
assert_eq!(rec1.data.get("id"), Some(&json!(100)));
assert_eq!(rec2.data.get("id"), Some(&json!(100)));
let resp = client
.post(format!("{}/rules/r1/stop", server.rest_url))
.send()
.await?;
assert_eq!(resp.status(), reqwest::StatusCode::OK);
let resp = client
.post(format!("{}/api/telemetry", server.data_url))
.json(&json!({"id": 101}))
.send()
.await?;
assert_eq!(resp.status(), reqwest::StatusCode::OK);
let rec2_next = tokio::time::timeout(Duration::from_secs(2), sink_r2.recv())
.await?
.ok_or_else(|| anyhow!("sink r2 closed"))?;
assert_eq!(rec2_next.data.get("id"), Some(&json!(101)));
let resp = client
.post(format!("{}/rules/r2/stop", server.rest_url))
.send()
.await?;
assert_eq!(resp.status(), reqwest::StatusCode::OK);
let resp = client
.post(format!("{}/api/telemetry", server.data_url))
.json(&json!({"id": 102}))
.send()
.await?;
assert_eq!(resp.status(), reqwest::StatusCode::NOT_FOUND);
let resp = client
.post(format!("{}/rules/r2/start", server.rest_url))
.send()
.await?;
assert_eq!(resp.status(), reqwest::StatusCode::OK);
let resp = client
.post(format!("{}/api/telemetry", server.data_url))
.json(&json!({"id": 103}))
.send()
.await?;
assert_eq!(resp.status(), reqwest::StatusCode::OK);
let rec2_resumed = tokio::time::timeout(Duration::from_secs(2), sink_r2.recv())
.await?
.ok_or_else(|| anyhow!("sink r2 closed"))?;
assert_eq!(rec2_resumed.data.get("id"), Some(&json!(103)));
Ok(())
}
#[tokio::test]
async fn test_issue28_httppush_method_and_rest_fallback() -> Result<()> {
let server = TestServer::start().await;
let client = reqwest::Client::new();
let resp = client
.post(format!("{}/streams", server.rest_url))
.json(&json!({
"sql": "CREATE STREAM s_put () WITH (DATASOURCE=\"/put/endpoint\", METHOD=\"PUT\", FORMAT=\"json\", TYPE=\"httppush\")"
}))
.send()
.await?;
assert_eq!(resp.status(), reqwest::StatusCode::CREATED);
let mut sink_rx = server.state.stream_bus.subscribe("put_out");
let resp = client
.post(format!("{}/rules", server.rest_url))
.json(&json!({
"id": "r_put",
"sql": "SELECT v FROM s_put",
"actions": [{"memory": {"topic": "put_out"}}]
}))
.send()
.await?;
assert_eq!(resp.status(), reqwest::StatusCode::CREATED);
let resp = client
.post(format!("{}/put/endpoint", server.data_url))
.json(&json!({"v": 10}))
.send()
.await?;
assert_eq!(resp.status(), reqwest::StatusCode::METHOD_NOT_ALLOWED);
let resp = client
.put(format!("{}/put/endpoint", server.data_url))
.json(&json!({"v": 20}))
.send()
.await?;
assert_eq!(resp.status(), reqwest::StatusCode::OK);
assert_eq!(resp.text().await?, "ok");
let rec = tokio::time::timeout(Duration::from_secs(2), sink_rx.recv())
.await?
.ok_or_else(|| anyhow!("sink closed"))?;
assert_eq!(rec.data.get("v"), Some(&json!(20)));
let resp = client
.put(format!("{}/put/endpoint", server.rest_url))
.json(&json!({"v": 30}))
.send()
.await?;
assert_eq!(resp.status(), reqwest::StatusCode::OK);
assert_eq!(resp.text().await?, "ok");
let rec_rest = tokio::time::timeout(Duration::from_secs(2), sink_rx.recv())
.await?
.ok_or_else(|| anyhow!("sink closed"))?;
assert_eq!(rec_rest.data.get("v"), Some(&json!(30)));
Ok(())
}