Skip to content

Commit 195b772

Browse files
authored
feat: basic integration test (#3)
* feat: basic integration test * feat: ipc integration test * chore: unify the two tests * fix: RV serialization on responses * chore: bump version
1 parent c547ff5 commit 195b772

File tree

9 files changed

+277
-6
lines changed

9 files changed

+277
-6
lines changed

Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description = "Simple, modern, ergonomic JSON-RPC 2.0 router built with tower an
55
keywords = ["json-rpc", "jsonrpc", "json"]
66
categories = ["web-programming::http-server", "web-programming::websocket"]
77

8-
version = "0.1.0"
8+
version = "0.1.1"
99
edition = "2021"
1010
rust-version = "1.81"
1111
authors = ["init4", "James Prestwich"]
@@ -66,3 +66,7 @@ inherits = "dev"
6666
strip = true
6767
debug = false
6868
incremental = false
69+
70+
[dev-dependencies]
71+
tempfile = "3.15.0"
72+
tracing-subscriber = "0.3.19"

src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,9 @@ pub use primitives::{BorrowedRpcObject, MethodId, RpcBorrow, RpcObject, RpcRecv,
139139

140140
#[cfg(feature = "pubsub")]
141141
pub mod pubsub;
142+
#[doc(hidden)] // for tests
143+
#[cfg(feature = "ipc")]
144+
pub use pubsub::ReadJsonStream;
142145

143146
mod routes;
144147
pub(crate) use routes::{BoxedIntoRoute, ErasedIntoRoute, Method, Route};

src/pubsub/ipc.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,8 @@ const CAPACITY: usize = 4096;
9898
/// A stream of JSON-RPC items, read from an [`AsyncRead`] stream.
9999
#[derive(Debug)]
100100
#[pin_project::pin_project]
101-
pub(crate) struct ReadJsonStream<T, Item> {
101+
#[doc(hidden)]
102+
pub struct ReadJsonStream<T, Item> {
102103
/// The underlying reader.
103104
#[pin]
104105
reader: T,

src/pubsub/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@
9090
9191
#[cfg(feature = "ipc")]
9292
mod ipc;
93+
#[cfg(feature = "ipc")]
94+
#[doc(hidden)]
95+
pub use ipc::ReadJsonStream;
9396

9497
mod shared;
9598
pub use shared::{ConnectionId, ServerShutdown, DEFAULT_NOTIFICATION_BUFFER_PER_CLIENT};

src/routes/ctx.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,7 @@ impl HandlerCtx {
6868
/// Notify a client of an event.
6969
pub async fn notify<T: RpcSend>(&self, t: &T) -> Result<(), NotifyError> {
7070
if let Some(notifications) = self.notifications.as_ref() {
71-
let ser = serde_json::to_string(t)?;
72-
let rv = serde_json::value::to_raw_value(&ser)?;
71+
let rv = serde_json::value::to_raw_value(t)?;
7372
notifications.send(rv).await?;
7473
}
7574

src/types/req.rs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,13 +134,31 @@ impl Request {
134134
RawValue::from_string(self.id().to_string()).expect("valid json")
135135
}
136136

137-
/// Return a reference to the serialized method field.
137+
/// Return a reference to the method str, deserialized.
138+
///
139+
/// This is the method without the preceding and trailing quotes. E.g. if
140+
/// the method is `foo`, this will return `&"foo"`.
138141
pub fn method(&self) -> &str {
139-
// SAFETY: `method` is guaranteed to be valid JSON,
142+
// SAFETY: `method` is guaranteed to be valid UTF-8,
140143
// and a valid slice of `bytes`.
141144
unsafe { core::str::from_utf8_unchecked(self.bytes.get_unchecked(self.method.clone())) }
142145
}
143146

147+
/// Return a reference to the raw method str, with preceding and trailing
148+
/// quotes. This is effectively the method as a [`RawValue`].
149+
///
150+
/// E.g. if the method is `foo`, this will return `&r#""foo""#`.
151+
pub fn raw_method(&self) -> &str {
152+
// SAFETY: `params` is guaranteed to be valid JSON,
153+
// and a valid slice of `bytes`.
154+
unsafe {
155+
core::str::from_utf8_unchecked(
156+
self.bytes
157+
.get_unchecked(self.method.start - 1..self.method.end + 1),
158+
)
159+
}
160+
}
161+
144162
/// Return a reference to the serialized params field.
145163
pub fn params(&self) -> &str {
146164
// SAFETY: `params` is guaranteed to be valid JSON,

tests/common/mod.rs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
use ajj::{HandlerCtx, Router};
2+
use serde_json::Value;
3+
use tokio::time;
4+
5+
/// Instantiate a router for testing.
6+
pub fn test_router() -> ajj::Router<()> {
7+
Router::<()>::new()
8+
.route("ping", || async move { Ok::<_, ()>("pong") })
9+
.route(
10+
"double",
11+
|params: usize| async move { Ok::<_, ()>(params * 2) },
12+
)
13+
.route("notify", |ctx: HandlerCtx| async move {
14+
tokio::task::spawn(async move {
15+
time::sleep(time::Duration::from_millis(100)).await;
16+
17+
let _ = ctx
18+
.notify(&serde_json::json!({
19+
"method": "notify",
20+
"result": "notified"
21+
}))
22+
.await;
23+
});
24+
25+
Ok::<_, ()>(())
26+
})
27+
}
28+
29+
/// Test clients
30+
pub trait TestClient {
31+
async fn send<S: serde::Serialize>(&mut self, method: &str, params: &S);
32+
async fn recv<D: serde::de::DeserializeOwned>(&mut self) -> D;
33+
}
34+
35+
/// basic tests of the test router
36+
pub async fn basic_tests<T: TestClient>(mut client: T) {
37+
client.send("ping", &()).await;
38+
39+
let next: Value = client.recv().await;
40+
assert_eq!(
41+
next,
42+
serde_json::json!({"id": 0, "jsonrpc": "2.0", "result": "pong"})
43+
);
44+
45+
client.send("double", &5).await;
46+
let next: Value = client.recv().await;
47+
assert_eq!(
48+
next,
49+
serde_json::json!({"id": 1, "jsonrpc": "2.0", "result": 10})
50+
);
51+
52+
client.send("notify", &()).await;
53+
54+
let now = std::time::Instant::now();
55+
56+
let next: Value = client.recv().await;
57+
assert_eq!(
58+
next,
59+
serde_json::json!({"id": 2, "jsonrpc": "2.0", "result": null})
60+
);
61+
62+
let next: Value = client.recv().await;
63+
assert!(now.elapsed().as_millis() >= 100);
64+
assert_eq!(
65+
next,
66+
serde_json::json!({"method": "notify", "result": "notified"})
67+
);
68+
}

tests/ipc.rs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
mod common;
2+
use common::{test_router, TestClient};
3+
4+
use ajj::pubsub::{Connect, ReadJsonStream, ServerShutdown};
5+
use futures_util::StreamExt;
6+
use interprocess::local_socket::{
7+
self as ls,
8+
tokio::{prelude::LocalSocketStream, RecvHalf, SendHalf},
9+
traits::tokio::Stream,
10+
ListenerOptions,
11+
};
12+
use serde_json::Value;
13+
use tempfile::{NamedTempFile, TempPath};
14+
use tokio::io::AsyncWriteExt;
15+
16+
pub(crate) fn to_name(path: &std::ffi::OsStr) -> std::io::Result<ls::Name<'_>> {
17+
if cfg!(windows) && !path.as_encoded_bytes().starts_with(br"\\.\pipe\") {
18+
ls::ToNsName::to_ns_name::<ls::GenericNamespaced>(path)
19+
} else {
20+
ls::ToFsName::to_fs_name::<ls::GenericFilePath>(path)
21+
}
22+
}
23+
24+
async fn serve_ipc() -> (ServerShutdown, TempPath) {
25+
let router = test_router();
26+
27+
let temp = NamedTempFile::new().unwrap().into_temp_path();
28+
let name = to_name(temp.as_os_str()).unwrap();
29+
30+
dbg!(&name);
31+
dbg!(std::fs::remove_file(&temp).unwrap());
32+
33+
let shutdown = ListenerOptions::new()
34+
.name(name)
35+
.serve(router)
36+
.await
37+
.unwrap();
38+
(shutdown, temp)
39+
}
40+
41+
struct IpcClient {
42+
recv_half: ReadJsonStream<RecvHalf, Value>,
43+
send_half: SendHalf,
44+
id: usize,
45+
}
46+
47+
impl IpcClient {
48+
async fn new(temp: &TempPath) -> Self {
49+
let name = to_name(temp.as_os_str()).unwrap();
50+
let (recv_half, send_half) = LocalSocketStream::connect(name).await.unwrap().split();
51+
Self {
52+
recv_half: recv_half.into(),
53+
send_half,
54+
id: 0,
55+
}
56+
}
57+
58+
async fn send_inner<S: serde::Serialize>(&mut self, msg: &S) {
59+
let s = serde_json::to_string(msg).unwrap();
60+
61+
self.send_half.write_all(s.as_bytes()).await.unwrap();
62+
}
63+
64+
async fn recv_inner(&mut self) -> serde_json::Value {
65+
self.recv_half.next().await.unwrap()
66+
}
67+
68+
fn next_id(&mut self) -> usize {
69+
let id = self.id;
70+
self.id += 1;
71+
id
72+
}
73+
}
74+
75+
impl TestClient for IpcClient {
76+
async fn send<S: serde::Serialize>(&mut self, method: &str, params: &S) {
77+
let id = self.next_id();
78+
self.send_inner(&serde_json::json!({
79+
"jsonrpc": "2.0",
80+
"id": id,
81+
"method": method,
82+
"params": params,
83+
}))
84+
.await;
85+
}
86+
87+
async fn recv<D: serde::de::DeserializeOwned>(&mut self) -> D {
88+
serde_json::from_value(self.recv_inner().await).unwrap()
89+
}
90+
}
91+
92+
#[tokio::test]
93+
async fn basic_ipc() {
94+
let (_server, temp) = serve_ipc().await;
95+
let client = IpcClient::new(&temp).await;
96+
97+
common::basic_tests(client).await;
98+
}

tests/ws.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
mod common;
2+
use common::{test_router, TestClient};
3+
4+
use ajj::pubsub::{Connect, ServerShutdown};
5+
use futures_util::{SinkExt, StreamExt};
6+
use std::net::{Ipv4Addr, SocketAddr};
7+
use tokio_tungstenite::{
8+
tungstenite::{client::IntoClientRequest, Message},
9+
MaybeTlsStream, WebSocketStream,
10+
};
11+
12+
const WS_SOCKET: SocketAddr =
13+
SocketAddr::new(std::net::IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 3383);
14+
const WS_SOCKET_STR: &str = "ws://127.0.0.1:3383";
15+
16+
async fn serve_ws() -> ServerShutdown {
17+
let router = test_router();
18+
WS_SOCKET.serve(router).await.unwrap()
19+
}
20+
21+
struct WsClient {
22+
socket: WebSocketStream<MaybeTlsStream<tokio::net::TcpStream>>,
23+
id: u64,
24+
}
25+
26+
impl WsClient {
27+
async fn send_inner<S: serde::Serialize>(&mut self, msg: &S) {
28+
self.socket
29+
.send(Message::Text(serde_json::to_string(msg).unwrap().into()))
30+
.await
31+
.unwrap();
32+
}
33+
34+
async fn recv_inner<D: serde::de::DeserializeOwned>(&mut self) -> D {
35+
match self.socket.next().await.unwrap().unwrap() {
36+
Message::Text(text) => serde_json::from_str(&text).unwrap(),
37+
_ => panic!("unexpected message type"),
38+
}
39+
}
40+
41+
fn next_id(&mut self) -> u64 {
42+
let id = self.id;
43+
self.id += 1;
44+
id
45+
}
46+
}
47+
48+
impl TestClient for WsClient {
49+
async fn send<S: serde::Serialize>(&mut self, method: &str, params: &S) {
50+
let id = self.next_id();
51+
self.send_inner(&serde_json::json!({
52+
"jsonrpc": "2.0",
53+
"id": id,
54+
"method": method,
55+
"params": params,
56+
}))
57+
.await;
58+
}
59+
60+
async fn recv<D: serde::de::DeserializeOwned>(&mut self) -> D {
61+
self.recv_inner().await
62+
}
63+
}
64+
65+
async fn ws_client() -> WsClient {
66+
let request = WS_SOCKET_STR.into_client_request().unwrap();
67+
let (socket, _) = tokio_tungstenite::connect_async(request).await.unwrap();
68+
69+
WsClient { socket, id: 0 }
70+
}
71+
72+
#[tokio::test]
73+
async fn basic_ws() {
74+
let _server = serve_ws().await;
75+
let client = ws_client().await;
76+
common::basic_tests(client).await;
77+
}

0 commit comments

Comments
 (0)