Skip to content

Commit

Permalink
feature: add integration test
Browse files Browse the repository at this point in the history
  • Loading branch information
tyrchen committed May 4, 2024
1 parent 97917b7 commit 848b6fb
Show file tree
Hide file tree
Showing 15 changed files with 531 additions and 37 deletions.
266 changes: 258 additions & 8 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[workspace]
members = ["chat_server", "chat_core", "notify_server"]
members = ["chat_server", "chat_core", "notify_server", "chat_test"]
resolver = "2"

[workspace.dependencies]
Expand All @@ -14,7 +14,9 @@ axum = { version = "0.7.5", features = [
axum-extra = { version = "0.9.3", features = ["typed-header"] }
chrono = { version = "0.4.38", features = ["serde"] }
chat-core = { path = "./chat_core" }
chat-server = { path = "./chat_server" }
jwt-simple = "0.12.9"
notify-server = { path = "./notify_server" }
serde = { version = "1.0.198", features = ["derive"] }
serde_yaml = "0.9.34"
sqlx = { version = "0.7.4", features = [
Expand Down
10 changes: 6 additions & 4 deletions chat_server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ name = "chat-server"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
default = []
test-util = ["http-body-util", "sqlx-db-tester"]

[dependencies]
anyhow = { workspace = true }
Expand All @@ -13,21 +15,21 @@ axum-extra = { workspace = true }
chrono = { workspace = true }
chat-core = { workspace = true }
hex = "0.4.3"
http-body-util = { version = "0.1.1", optional = true }
jwt-simple = { workspace = true }
mime_guess = "2.0.4"
serde = { workspace = true }
serde_json = "1.0.116"
serde_yaml = { workspace = true }
sha1 = "0.10.6"
sqlx = { workspace = true }
sqlx-db-tester = { version = "0.4.2", optional = true }
thiserror = { workspace = true }
tokio = { workspace = true }
tower = { workspace = true }
tower-http = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }


[dev-dependencies]
http-body-util = "0.1.1"
sqlx-db-tester = "0.4.2"
chat-server = { workspace = true, features = ["test-util"] }
4 changes: 2 additions & 2 deletions chat_server/src/handlers/messages.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use axum::{
extract::{Multipart, Path, Query, State},
http::HeaderMap,
http::{HeaderMap, StatusCode},
response::IntoResponse,
Extension, Json,
};
Expand All @@ -18,7 +18,7 @@ pub(crate) async fn send_message_handler(
) -> Result<impl IntoResponse, AppError> {
let msg = state.create_message(input, id, user.id as _).await?;

Ok(Json(msg))
Ok((StatusCode::CREATED, Json(msg)))
}

pub(crate) async fn list_message_handler(
Expand Down
10 changes: 4 additions & 6 deletions chat_server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,19 @@ use axum::{
pub use config::AppConfig;

#[derive(Debug, Clone)]
pub(crate) struct AppState {
pub struct AppState {
inner: Arc<AppStateInner>,
}

#[allow(unused)]
pub(crate) struct AppStateInner {
pub struct AppStateInner {
pub(crate) config: AppConfig,
pub(crate) dk: DecodingKey,
pub(crate) ek: EncodingKey,
pub(crate) pool: PgPool,
}

pub async fn get_router(config: AppConfig) -> Result<Router, AppError> {
let state = AppState::try_new(config).await?;

pub async fn get_router(state: AppState) -> Result<Router, AppError> {
let chat = Router::new()
.route(
"/:id",
Expand Down Expand Up @@ -118,7 +116,7 @@ impl fmt::Debug for AppStateInner {
}
}

#[cfg(test)]
#[cfg(feature = "test-util")]
mod test_util {
use super::*;
use sqlx::{Executor, PgPool};
Expand Down
5 changes: 3 additions & 2 deletions chat_server/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use anyhow::Result;
use chat_server::{get_router, AppConfig};
use chat_server::{get_router, AppConfig, AppState};
use tokio::net::TcpListener;
use tracing::{info, level_filters::LevelFilter};
use tracing_subscriber::{fmt::Layer, layer::SubscriberExt, util::SubscriberInitExt, Layer as _};
Expand All @@ -12,7 +12,8 @@ async fn main() -> Result<()> {
let config = AppConfig::load()?;
let addr = format!("0.0.0.0:{}", config.server.port);

let app = get_router(config).await?;
let state = AppState::try_new(config).await?;
let app = get_router(state).await?;
let listener = TcpListener::bind(&addr).await?;
info!("Listening on: {}", addr);

Expand Down
24 changes: 24 additions & 0 deletions chat_test/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[package]
name = "chat_test"
version = "0.1.0"
edition = "2021"

[dev-dependencies]
anyhow = { workspace = true }
axum = { workspace = true }
chat-core = { workspace = true }
chat-server = { workspace = true, features = ["test-util"] }
notify-server = { workspace = true }
reqwest = { version = "0.12.4", default-features = false, features = [
"rustls-tls",
"json",
"multipart",
"stream",
] }
reqwest-eventsource = "0.6.0"
serde = { workspace = true }
serde_json = "1.0.116"
tokio = { workspace = true }

[dependencies]
futures = "0.3.30"
13 changes: 13 additions & 0 deletions chat_test/chat.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
server:
port: 6688
db_url: postgres://postgres:postgres@localhost:5432/chat
base_dir: /tmp/chat_server
auth:
sk: |
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIDnxJGEJGoW+mNKHn4vRY1V6BQ3MglSQSuZ8featmyC4
-----END PRIVATE KEY-----
pk: |
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAfM+lwNHj6TRJ3EGP38lIJcOo9Dlt2u2JzcwWMbu7jQY=
-----END PUBLIC KEY-----
8 changes: 8 additions & 0 deletions chat_test/notify.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
server:
port: 6687
db_url: postgres://postgres:postgres@localhost:5432/chat
auth:
pk: |
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAfM+lwNHj6TRJ3EGP38lIJcOo9Dlt2u2JzcwWMbu7jQY=
-----END PUBLIC KEY-----
1 change: 1 addition & 0 deletions chat_test/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// empty
200 changes: 200 additions & 0 deletions chat_test/tests/chat.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
use anyhow::Result;
use chat_core::{Chat, ChatType, Message};
use futures::StreamExt;
use reqwest::{
multipart::{Form, Part},
StatusCode,
};
use reqwest_eventsource::{Event, EventSource};
use serde::Deserialize;
use serde_json::json;
use std::{net::SocketAddr, time::Duration};
use tokio::{net::TcpListener, time::sleep};

/*
test1:
name: user 1 create chat
steps:
- signin
email: [email protected]
password: 123456
- create_chat
name: test
members: [1, 2]
- create_message
chat_id: 1
content: hello
files: [Cargo.toml]
*/

#[derive(Debug, Deserialize)]
struct AuthToken {
token: String,
}

struct ChatServer {
addr: SocketAddr,
token: String,
client: reqwest::Client,
}

struct NotifyServer;

const WILD_ADDR: &str = "0.0.0.0:0";

#[tokio::test]
async fn chat_server_should_work() -> Result<()> {
let (tdb, state) = chat_server::AppState::new_for_test().await?;
let chat_server = ChatServer::new(state).await?;
let db_url = tdb.url();
NotifyServer::new(&db_url, &chat_server.token).await?;
let chat = chat_server.create_chat().await?;
let _msg = chat_server.create_message(chat.id as u64).await?;
sleep(Duration::from_secs(1)).await;
Ok(())
}

impl NotifyServer {
async fn new(db_url: &str, token: &str) -> Result<Self> {
let mut config = notify_server::AppConfig::load()?;
config.server.db_url = db_url.to_string();
let app = notify_server::get_router(config).await?;
let listener = TcpListener::bind(WILD_ADDR).await?;
let addr = listener.local_addr()?;

tokio::spawn(async move {
axum::serve(listener, app.into_make_service())
.await
.unwrap();
});

let mut es = EventSource::get(format!("http://{}/events?access_token={}", addr, token));

tokio::spawn(async move {
while let Some(event) = es.next().await {
match event {
Ok(Event::Open) => println!("Connection Open!"),
Ok(Event::Message(message)) => match message.event.as_str() {
"NewChat" => {
let chat: Chat = serde_json::from_str(&message.data).unwrap();
assert_eq!(chat.name.as_ref().unwrap(), "test");
assert_eq!(chat.members, vec![1, 2]);
assert_eq!(chat.r#type, ChatType::PrivateChannel);
}

"NewMessage" => {
let msg: Message = serde_json::from_str(&message.data).unwrap();
assert_eq!(msg.content, "hello");
assert_eq!(msg.files.len(), 1);
assert_eq!(msg.sender_id, 1);
}
_ => {
panic!("unexpected event: {:?}", message);
}
},
Err(err) => {
println!("Error: {}", err);
es.close();
}
}
}
});

Ok(Self)
}
}

impl ChatServer {
async fn new(state: chat_server::AppState) -> Result<Self> {
let app = chat_server::get_router(state).await?;
let listener = TcpListener::bind(WILD_ADDR).await?;
let addr = listener.local_addr()?;

tokio::spawn(async move {
axum::serve(listener, app.into_make_service())
.await
.unwrap();
});

let client = reqwest::Client::new();

let mut ret = Self {
addr,
client,
token: "".to_string(),
};

ret.token = ret.signin().await?;

Ok(ret)
}

async fn signin(&self) -> Result<String> {
let res = self
.client
.post(&format!("http://{}/api/signin", self.addr))
.header("Content-Type", "application/json")
.body(r#"{"email": "[email protected]","password":"123456"}"#)
.send()
.await?;

assert_eq!(res.status(), 200);
let ret: AuthToken = res.json().await?;
Ok(ret.token)
}

async fn create_chat(&self) -> Result<Chat> {
let res = self
.client
.post(format!("http://{}/api/chats", self.addr))
.header("Authorization", format!("Bearer {}", self.token))
.header("Content-Type", "application/json")
.body(r#"{"name": "test", "members": [1, 2], "public": false}"#);
let res = res.send().await?;
assert_eq!(res.status(), StatusCode::CREATED);
let chat: Chat = res.json().await?;
assert_eq!(chat.name.as_ref().unwrap(), "test");
assert_eq!(chat.members, vec![1, 2]);
assert_eq!(chat.r#type, ChatType::PrivateChannel);

Ok(chat)
}

async fn create_message(&self, chat_id: u64) -> Result<Message> {
// upload file
let data = include_bytes!("../Cargo.toml");
let files = Part::bytes(data)
.file_name("Cargo.toml")
.mime_str("text/plain")?;
let form = Form::new().part("file", files);

let res = self
.client
.post(&format!("http://{}/api/upload", self.addr))
.header("Authorization", format!("Bearer {}", self.token))
.multipart(form)
.send()
.await?;
assert_eq!(res.status(), StatusCode::OK);
let ret: Vec<String> = res.json().await?;

let body = serde_json::to_string(&json!({
"content": "hello",
"files": ret,
}))?;
let res = self
.client
.post(format!("http://{}/api/chats/{}", self.addr, chat_id))
.header("Authorization", format!("Bearer {}", self.token))
.header("Content-Type", "application/json")
.body(body);
let res = res.send().await?;
assert_eq!(res.status(), StatusCode::CREATED);
let message: Message = res.json().await?;
assert_eq!(message.content, "hello");
assert_eq!(message.files, ret);
assert_eq!(message.sender_id, 1);
assert_eq!(message.chat_id, chat_id as i64);
Ok(message)
}
}
Loading

0 comments on commit 848b6fb

Please sign in to comment.