Vendor dependencies

This commit is contained in:
2026-08-01 16:11:49 +03:00
parent 7f139a0241
commit 6b5e7f0f8b
29706 changed files with 9575646 additions and 0 deletions
@@ -0,0 +1,19 @@
<div align="center">
<h1>
Example REST Todo<br/>
</h1>
<h3>
an example application with tests
</h3>
<br/>
</div>
This is a very simple todo application. It aims to show ...
* How to write some basic tests against end points.
* How to test for some tests to be expecting success, and some to be expecting failure.
* How to take cookies into account (like logging in).
It's primarily to provide some code samples using axum-test.
@@ -0,0 +1,321 @@
//!
//! This is an example Todo Application, wrapped with Shuttle.
//! To show some simple tests when using Shuttle + Axum.
//!
//! ```bash
//! # To run it's tests:
//! cargo test --example=example-shuttle --features shuttle
//! ```
//!
//! The app includes the end points for ...
//!
//! - POST /login ... this takes an email, and returns a session cookie.
//! - PUT /todo ... once logged in, one can store todos.
//! - GET /todo ... once logged in, you can retrieve all todos you have stored.
//!
//! At the bottom of this file are a series of tests for these endpoints.
//!
use anyhow::anyhow;
use anyhow::Result;
use axum::extract::Json;
use axum::extract::State;
use axum::routing::get;
use axum::routing::post;
use axum::routing::put;
use axum::Router;
use axum_extra::extract::cookie::Cookie;
use axum_extra::extract::cookie::CookieJar;
use http::StatusCode;
use serde::Deserialize;
use serde::Serialize;
use serde_email::Email;
use std::collections::HashMap;
use std::result::Result as StdResult;
use std::sync::Arc;
use std::sync::RwLock;
#[cfg(test)]
use axum_test::TestServer;
/// Main to start Shuttle application
#[shuttle_runtime::main]
async fn main() -> ::shuttle_axum::ShuttleAxum {
new_app()
}
/// The Shuttle application itself
fn new_app() -> ::shuttle_axum::ShuttleAxum {
let state = AppState {
user_todos: HashMap::new(),
};
let shared_state = Arc::new(RwLock::new(state));
let app = Router::new()
.route(&"/login", post(route_post_user_login))
.route(&"/todo", get(route_get_user_todos))
.route(&"/todo", put(route_put_user_todos))
.with_state(shared_state);
Ok(app.into())
}
/// A TestServer that runs the Shuttle application
#[cfg(test)]
fn new_test_app() -> TestServer {
TestServer::builder()
// Preserve cookies across requests
// for the session cookie to work.
.save_cookies()
.expect_success_by_default()
.mock_transport()
.build(new_app()) // <- here the application is passed in
.unwrap()
}
const USER_ID_COOKIE_NAME: &'static str = &"example-shuttle-user-id";
type SharedAppState = Arc<RwLock<AppState>>;
// This my poor mans in memory DB.
#[derive(Debug)]
pub struct AppState {
user_todos: HashMap<u32, Vec<Todo>>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct Todo {
name: String,
content: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct LoginRequest {
user: Email,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct AllTodos {
todos: Vec<Todo>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct NumTodos {
num: u32,
}
// Note you should never do something like this in a real application
// for session cookies. It's really bad. Like _seriously_ bad.
//
// This is done like this here to keep the code shorter. That's all.
fn get_user_id_from_cookie(cookies: &CookieJar) -> Result<u32> {
cookies
.get(&USER_ID_COOKIE_NAME)
.map(|c| c.value().to_string().parse::<u32>().ok())
.flatten()
.ok_or_else(|| anyhow!("id not found"))
}
pub async fn route_post_user_login(
State(ref mut state): State<SharedAppState>,
mut cookies: CookieJar,
Json(_body): Json<LoginRequest>,
) -> CookieJar {
let mut lock = state.write().unwrap();
let user_todos = &mut lock.user_todos;
let user_id = user_todos.len() as u32;
user_todos.insert(user_id, vec![]);
let really_insecure_login_cookie = Cookie::new(USER_ID_COOKIE_NAME, user_id.to_string());
cookies = cookies.add(really_insecure_login_cookie);
cookies
}
pub async fn route_put_user_todos(
State(ref mut state): State<SharedAppState>,
mut cookies: CookieJar,
Json(todo): Json<Todo>,
) -> StdResult<Json<u32>, StatusCode> {
let user_id = get_user_id_from_cookie(&mut cookies).map_err(|_| StatusCode::UNAUTHORIZED)?;
let mut lock = state.write().unwrap();
let todos = lock.user_todos.get_mut(&user_id).unwrap();
todos.push(todo);
let num_todos = todos.len() as u32;
Ok(Json(num_todos))
}
pub async fn route_get_user_todos(
State(ref state): State<SharedAppState>,
mut cookies: CookieJar,
) -> StdResult<Json<Vec<Todo>>, StatusCode> {
let user_id = get_user_id_from_cookie(&mut cookies).map_err(|_| StatusCode::UNAUTHORIZED)?;
let lock = state.read().unwrap();
let todos = lock.user_todos[&user_id].clone();
Ok(Json(todos))
}
#[cfg(test)]
mod test_post_login {
use super::*;
use serde_json::json;
#[tokio::test]
async fn it_should_create_session_on_login() {
let server = new_test_app();
let response = server
.post(&"/login")
.json(&json!({
"user": "my-login@example.com",
}))
.await;
let session_cookie = response.cookie(&USER_ID_COOKIE_NAME);
assert_ne!(session_cookie.value(), "");
}
#[tokio::test]
async fn it_should_not_login_using_non_email() {
let server = new_test_app();
let response = server
.post(&"/login")
.json(&json!({
"user": "blah blah blah",
}))
.expect_failure()
.await;
// There should not be a session created.
let cookie = response.maybe_cookie(&USER_ID_COOKIE_NAME);
assert!(cookie.is_none());
}
}
#[cfg(test)]
mod test_route_put_user_todos {
use super::*;
use serde_json::json;
#[tokio::test]
async fn it_should_not_store_todos_without_login() {
let server = new_test_app();
let response = server
.put(&"/todo")
.json(&json!({
"name": "shopping",
"content": "buy eggs",
}))
.expect_failure()
.await;
assert_eq!(response.status_code(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn it_should_return_number_of_todos_as_more_are_pushed() {
let server = new_test_app();
server
.post(&"/login")
.json(&json!({
"user": "my-login@example.com",
}))
.await;
let num_todos = server
.put(&"/todo")
.json(&json!({
"name": "shopping",
"content": "buy eggs",
}))
.await
.json::<u32>();
assert_eq!(num_todos, 1);
let num_todos = server
.put(&"/todo")
.json(&json!({
"name": "afternoon",
"content": "buy shoes",
}))
.await
.json::<u32>();
assert_eq!(num_todos, 2);
}
}
#[cfg(test)]
mod test_route_get_user_todos {
use super::*;
use serde_json::json;
#[tokio::test]
async fn it_should_not_return_todos_if_logged_out() {
let server = new_test_app();
let response = server
.put(&"/todo")
.json(&json!({
"name": "shopping",
"content": "buy eggs",
}))
.expect_failure()
.await;
assert_eq!(response.status_code(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn it_should_return_all_todos_when_logged_in() {
let server = new_test_app();
server
.post(&"/login")
.json(&json!({
"user": "my-login@example.com",
}))
.await;
// Push two todos.
server
.put(&"/todo")
.json(&json!({
"name": "shopping",
"content": "buy eggs",
}))
.await;
server
.put(&"/todo")
.json(&json!({
"name": "afternoon",
"content": "buy shoes",
}))
.await;
// Get all todos out from the server.
let todos = server.get(&"/todo").await.json::<Vec<Todo>>();
let expected_todos: Vec<Todo> = vec![
Todo {
name: "shopping".to_string(),
content: "buy eggs".to_string(),
},
Todo {
name: "afternoon".to_string(),
content: "buy shoes".to_string(),
},
];
assert_eq!(todos, expected_todos)
}
}
@@ -0,0 +1,19 @@
<div align="center">
<h1>
Example REST Todo<br/>
</h1>
<h3>
an example application with tests
</h3>
<br/>
</div>
This is a very simple todo application. It aims to show ...
* How to write some basic tests against end points.
* How to test for some tests to be expecting success, and some to be expecting failure.
* How to take cookies into account (like logging in).
It's primarily to provide some code samples using axum-test.
@@ -0,0 +1,337 @@
//!
//! This is an example Todo Application to show some simple tests.
//!
//! ```bash
//! # To run it's tests:
//! cargo test --example=example-todo
//! ```
//!
//! The app includes the end points for ...
//!
//! - POST /login ... this takes an email, and returns a session cookie.
//! - PUT /todo ... once logged in, one can store todos.
//! - GET /todo ... once logged in, you can retrieve all todos you have stored.
//!
//! At the bottom of this file are a series of tests for these endpoints.
//!
use anyhow::anyhow;
use anyhow::Result;
use axum::extract::Json;
use axum::extract::State;
use axum::routing::get;
use axum::routing::post;
use axum::routing::put;
use axum::serve::serve;
use axum::Router;
use axum_extra::extract::cookie::Cookie;
use axum_extra::extract::cookie::CookieJar;
use http::StatusCode;
use serde::Deserialize;
use serde::Serialize;
use serde_email::Email;
use std::collections::HashMap;
use std::net::IpAddr;
use std::net::Ipv4Addr;
use std::net::SocketAddr;
use std::result::Result as StdResult;
use std::sync::Arc;
use std::sync::RwLock;
use tokio::net::TcpListener;
#[cfg(test)]
use axum_test::TestServer;
const PORT: u16 = 8080;
const USER_ID_COOKIE_NAME: &'static str = &"example-todo-user-id";
#[tokio::main]
async fn main() {
let result: Result<()> = {
let app = new_app();
// Start!
let ip_address = IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0));
let address = SocketAddr::new(ip_address, PORT);
let listener = TcpListener::bind(address).await.unwrap();
serve(listener, app.into_make_service()).await.unwrap();
Ok(())
};
match &result {
Err(err) => eprintln!("{}", err),
_ => {}
};
}
type SharedAppState = Arc<RwLock<AppState>>;
// This my poor mans in memory DB.
#[derive(Debug)]
pub struct AppState {
user_todos: HashMap<u32, Vec<Todo>>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct Todo {
name: String,
content: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct LoginRequest {
user: Email,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct AllTodos {
todos: Vec<Todo>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct NumTodos {
num: u32,
}
// Note you should never do something like this in a real application
// for session cookies. It's really bad. Like _seriously_ bad.
//
// This is done like this here to keep the code shorter. That's all.
fn get_user_id_from_cookie(cookies: &CookieJar) -> Result<u32> {
cookies
.get(&USER_ID_COOKIE_NAME)
.map(|c| c.value().to_string().parse::<u32>().ok())
.flatten()
.ok_or_else(|| anyhow!("id not found"))
}
pub async fn route_post_user_login(
State(ref mut state): State<SharedAppState>,
mut cookies: CookieJar,
Json(_body): Json<LoginRequest>,
) -> CookieJar {
let mut lock = state.write().unwrap();
let user_todos = &mut lock.user_todos;
let user_id = user_todos.len() as u32;
user_todos.insert(user_id, vec![]);
let really_insecure_login_cookie = Cookie::new(USER_ID_COOKIE_NAME, user_id.to_string());
cookies = cookies.add(really_insecure_login_cookie);
cookies
}
pub async fn route_put_user_todos(
State(ref mut state): State<SharedAppState>,
mut cookies: CookieJar,
Json(todo): Json<Todo>,
) -> StdResult<Json<u32>, StatusCode> {
let user_id = get_user_id_from_cookie(&mut cookies).map_err(|_| StatusCode::UNAUTHORIZED)?;
let mut lock = state.write().unwrap();
let todos = lock.user_todos.get_mut(&user_id).unwrap();
todos.push(todo);
let num_todos = todos.len() as u32;
Ok(Json(num_todos))
}
pub async fn route_get_user_todos(
State(ref state): State<SharedAppState>,
mut cookies: CookieJar,
) -> StdResult<Json<Vec<Todo>>, StatusCode> {
let user_id = get_user_id_from_cookie(&mut cookies).map_err(|_| StatusCode::UNAUTHORIZED)?;
let lock = state.read().unwrap();
let todos = lock.user_todos[&user_id].clone();
Ok(Json(todos))
}
pub(crate) fn new_app() -> Router {
let state = AppState {
user_todos: HashMap::new(),
};
let shared_state = Arc::new(RwLock::new(state));
Router::new()
.route(&"/login", post(route_post_user_login))
.route(&"/todo", get(route_get_user_todos))
.route(&"/todo", put(route_put_user_todos))
.with_state(shared_state)
}
#[cfg(test)]
fn new_test_app() -> TestServer {
let app = new_app();
TestServer::builder()
// Preserve cookies across requests
// for the session cookie to work.
.save_cookies()
.expect_success_by_default()
.mock_transport()
.build(app)
.unwrap()
}
#[cfg(test)]
mod test_post_login {
use super::*;
use serde_json::json;
#[tokio::test]
async fn it_should_create_session_on_login() {
let server = new_test_app();
let response = server
.post(&"/login")
.json(&json!({
"user": "my-login@example.com",
}))
.await;
let session_cookie = response.cookie(&USER_ID_COOKIE_NAME);
assert_ne!(session_cookie.value(), "");
}
#[tokio::test]
async fn it_should_not_login_using_non_email() {
let server = new_test_app();
let response = server
.post(&"/login")
.json(&json!({
"user": "blah blah blah",
}))
.expect_failure()
.await;
// There should not be a session created.
let cookie = response.maybe_cookie(&USER_ID_COOKIE_NAME);
assert!(cookie.is_none());
}
}
#[cfg(test)]
mod test_route_put_user_todos {
use super::*;
use serde_json::json;
#[tokio::test]
async fn it_should_not_store_todos_without_login() {
let server = new_test_app();
let response = server
.put(&"/todo")
.json(&json!({
"name": "shopping",
"content": "buy eggs",
}))
.expect_failure()
.await;
assert_eq!(response.status_code(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn it_should_return_number_of_todos_as_more_are_pushed() {
let server = new_test_app();
server
.post(&"/login")
.json(&json!({
"user": "my-login@example.com",
}))
.await;
let num_todos = server
.put(&"/todo")
.json(&json!({
"name": "shopping",
"content": "buy eggs",
}))
.await
.json::<u32>();
assert_eq!(num_todos, 1);
let num_todos = server
.put(&"/todo")
.json(&json!({
"name": "afternoon",
"content": "buy shoes",
}))
.await
.json::<u32>();
assert_eq!(num_todos, 2);
}
}
#[cfg(test)]
mod test_route_get_user_todos {
use super::*;
use serde_json::json;
#[tokio::test]
async fn it_should_not_return_todos_if_logged_out() {
let server = new_test_app();
let response = server
.put(&"/todo")
.json(&json!({
"name": "shopping",
"content": "buy eggs",
}))
.expect_failure()
.await;
assert_eq!(response.status_code(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn it_should_return_all_todos_when_logged_in() {
let server = new_test_app();
server
.post(&"/login")
.json(&json!({
"user": "my-login@example.com",
}))
.await;
// Push two todos.
server
.put(&"/todo")
.json(&json!({
"name": "shopping",
"content": "buy eggs",
}))
.await;
server
.put(&"/todo")
.json(&json!({
"name": "afternoon",
"content": "buy shoes",
}))
.await;
// Get all todos out from the server.
let todos = server.get(&"/todo").await.json::<Vec<Todo>>();
let expected_todos: Vec<Todo> = vec![
Todo {
name: "shopping".to_string(),
content: "buy eggs".to_string(),
},
Todo {
name: "afternoon".to_string(),
content: "buy shoes".to_string(),
},
];
assert_eq!(todos, expected_todos)
}
}
@@ -0,0 +1,18 @@
<div align="center">
<h1>
Example WebSockets Chat<br/>
</h1>
<h3>
a simple chat application with tests
</h3>
<br/>
</div>
This is a very simple application using WebSockets. It aims to show ...
* How to write a very basic chat application,
* and include tests which send and receive data.
It's primarily to provide some code samples using axum-test.
@@ -0,0 +1,242 @@
//!
//! This is an example Todo Application using Web Sockets for communication.
//!
//! At the bottom of this file are a series of tests for using websockets.
//!
//! ```bash
//! # To run it's tests:
//! cargo test --example=example-websocket-chat --features ws
//! ```
//!
use anyhow::Result;
use axum::extract::ws::Message;
use axum::extract::ws::WebSocket;
use axum::extract::Path;
use axum::extract::State;
use axum::extract::WebSocketUpgrade;
use axum::response::Response;
use axum::routing::get;
use axum::serve::serve;
use axum::Router;
use futures_util::SinkExt;
use futures_util::StreamExt;
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;
use std::net::IpAddr;
use std::net::Ipv4Addr;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use tokio::net::TcpListener;
use tokio::sync::RwLock;
#[cfg(test)]
use axum_test::TestServer;
const PORT: u16 = 8080;
#[tokio::main]
async fn main() {
let result: Result<()> = {
let app = new_app();
// Start!
let ip_address = IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0));
let address = SocketAddr::new(ip_address, PORT);
let listener = TcpListener::bind(address).await.unwrap();
serve(listener, app.into_make_service()).await.unwrap();
Ok(())
};
match &result {
Err(err) => eprintln!("{}", err),
_ => {}
};
}
type SharedAppState = Arc<RwLock<AppState>>;
/// This my poor mans chat system.
///
/// It holds a map of User ID to Messages.
#[derive(Debug)]
pub struct AppState {
user_messages: HashMap<String, Vec<ChatReceivedMessage>>,
}
#[derive(Deserialize, Serialize, Debug, PartialEq)]
pub struct ChatSendMessage {
pub to: String,
pub message: String,
}
#[derive(Deserialize, Serialize, Debug, PartialEq)]
pub struct ChatReceivedMessage {
pub from: String,
pub message: String,
}
pub async fn route_get_websocket_chat(
State(state): State<SharedAppState>,
Path(username): Path<String>,
ws: WebSocketUpgrade,
) -> Response {
ws.on_upgrade(move |socket| handle_chat(socket, username, state.clone()))
}
async fn handle_chat(socket: WebSocket, username: String, state: SharedAppState) {
let (mut sender, mut receiver) = socket.split();
// Spawn a task that will push several messages to the client (does not matter what client does)
let send_state = state.clone();
let send_username = username.clone();
let mut send_task = tokio::spawn(async move {
loop {
let mut state_locked = send_state.write().await;
let maybe_messages = state_locked.user_messages.get_mut(&send_username);
if let Some(messages) = maybe_messages {
while let Some(message) = messages.pop() {
let json_text = serde_json::to_string(&message)
.expect("Failed to build JSON message for sending");
sender
.send(Message::Text(json_text.into()))
.await
.expect("Failed to send message to socket");
}
}
::tokio::time::sleep(Duration::from_millis(10)).await;
}
});
// This second task will receive messages from client and print them on server console
let mut recv_task = tokio::spawn(async move {
while let Some(Ok(message)) = receiver.next().await {
let raw_text = message
.into_text()
.expect("Failed to read text from incoming message");
let decoded = serde_json::from_str::<ChatSendMessage>(&raw_text)
.expect("Failed to decode incoming JSON message");
let mut state_locked = state.write().await;
let maybe_messages = state_locked.user_messages.entry(decoded.to);
maybe_messages.or_default().push(ChatReceivedMessage {
from: username.clone(),
message: decoded.message,
});
}
});
// If any one of the tasks exit, abort the other.
tokio::select! {
rv_a = (&mut send_task) => {
match rv_a {
Ok(_) => println!("Messages sent"),
Err(a) => println!("Error sending messages {a:?}")
}
recv_task.abort();
},
rv_b = (&mut recv_task) => {
match rv_b {
Ok(_) => println!("Received messages"),
Err(b) => println!("Error receiving messages {b:?}")
}
send_task.abort();
}
}
}
pub(crate) fn new_app() -> Router {
let state = AppState {
user_messages: HashMap::new(),
};
let shared_state = Arc::new(RwLock::new(state));
Router::new()
.route(&"/ws-chat/{name}", get(route_get_websocket_chat))
.with_state(shared_state)
}
#[cfg(test)]
fn new_test_app() -> TestServer {
let app = new_app();
TestServer::builder()
.http_transport() // Important! It must be a HTTP Transport here.
.build(app)
.unwrap()
}
#[cfg(test)]
mod test_websockets_chat {
use super::*;
#[tokio::test]
async fn it_should_start_a_websocket_connection() {
let server = new_test_app();
let response = server.get_websocket(&"/ws-chat/john").await;
response.assert_status_switching_protocols();
}
#[tokio::test]
async fn it_should_send_messages_back_and_forth() {
let server = new_test_app();
let mut alice_chat = server
.get_websocket(&"/ws-chat/alice")
.await
.into_websocket()
.await;
let mut bob_chat = server
.get_websocket(&"/ws-chat/bob")
.await
.into_websocket()
.await;
bob_chat
.send_json(&ChatSendMessage {
to: "alice".to_string(),
message: "How are you Alice?".to_string(),
})
.await;
alice_chat
.assert_receive_json(&ChatReceivedMessage {
from: "bob".to_string(),
message: "How are you Alice?".to_string(),
})
.await;
alice_chat
.send_json(&ChatSendMessage {
to: "bob".to_string(),
message: "I am good".to_string(),
})
.await;
alice_chat
.send_json(&ChatSendMessage {
to: "bob".to_string(),
message: "How are you?".to_string(),
})
.await;
bob_chat
.assert_receive_json(&ChatReceivedMessage {
from: "alice".to_string(),
message: "I am good".to_string(),
})
.await;
bob_chat
.assert_receive_json(&ChatReceivedMessage {
from: "alice".to_string(),
message: "How are you?".to_string(),
})
.await;
}
}
@@ -0,0 +1,18 @@
<div align="center">
<h1>
Example WebSockets Ping Pong<br/>
</h1>
<h3>
an example websocket application with tests
</h3>
<br/>
</div>
This is a very simple application using WebSockets. It aims to show ...
* How to write a basic test that starts a WebSocket connection.
* A basic ping pong test, where data is pushed up and down.
It's primarily to provide some code samples using axum-test.
@@ -0,0 +1,135 @@
//!
//! This is a simple WebSocket example Application.
//! You send it data, and it will send it back.
//!
//! At the bottom of this file are a series of tests for using websockets.
//!
//! ```bash
//! # To run it's tests:
//! cargo test --example=example-websocket-ping-pong --features ws
//! ```
//!
use anyhow::Result;
use axum::extract::ws::WebSocket;
use axum::extract::WebSocketUpgrade;
use axum::response::Response;
use axum::routing::get;
use axum::serve::serve;
use axum::Router;
use std::net::IpAddr;
use std::net::Ipv4Addr;
use std::net::SocketAddr;
use tokio::net::TcpListener;
#[cfg(test)]
use axum_test::TestServer;
const PORT: u16 = 8080;
#[tokio::main]
async fn main() {
let result: Result<()> = {
let app = new_app();
// Start!
let ip_address = IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0));
let address = SocketAddr::new(ip_address, PORT);
let listener = TcpListener::bind(address).await.unwrap();
serve(listener, app.into_make_service()).await.unwrap();
Ok(())
};
match &result {
Err(err) => eprintln!("{}", err),
_ => {}
};
}
pub async fn route_get_websocket_ping_pong(ws: WebSocketUpgrade) -> Response {
ws.on_upgrade(move |socket| handle_ping_pong(socket))
}
async fn handle_ping_pong(mut socket: WebSocket) {
while let Some(msg) = socket.recv().await {
let msg = if let Ok(msg) = msg {
msg
} else {
// client disconnected
return;
};
if socket.send(msg).await.is_err() {
// client disconnected
return;
}
}
}
pub(crate) fn new_app() -> Router {
Router::new().route(&"/ws-ping-pong", get(route_get_websocket_ping_pong))
}
#[cfg(test)]
fn new_test_app() -> TestServer {
let app = new_app();
TestServer::builder()
.http_transport() // Important! It must be a HTTP Transport here.
.build(app)
.unwrap()
}
#[cfg(test)]
mod test_websockets_ping_pong {
use super::*;
use serde_json::json;
#[tokio::test]
async fn it_should_start_a_websocket_connection() {
let server = new_test_app();
let response = server.get_websocket(&"/ws-ping-pong").await;
response.assert_status_switching_protocols();
}
#[tokio::test]
async fn it_should_ping_pong_text() {
let server = new_test_app();
let mut websocket = server
.get_websocket(&"/ws-ping-pong")
.await
.into_websocket()
.await;
websocket.send_text("Hello!").await;
websocket.assert_receive_text("Hello!").await;
}
#[tokio::test]
async fn it_should_ping_pong_json() {
let server = new_test_app();
let mut websocket = server
.get_websocket(&"/ws-ping-pong")
.await
.into_websocket()
.await;
websocket
.send_json(&json!({
"hello": "world",
"numbers": [1, 2, 3],
}))
.await;
websocket
.assert_receive_json(&json!({
"hello": "world",
"numbers": [1, 2, 3],
}))
.await;
}
}