From 7472749fee861b3cc4db463a29069a6cdc47dbcf Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Thu, 26 Nov 2020 17:27:17 -0500 Subject: [PATCH] add tests for API methods --- Cargo.lock | 1 + sync-server/Cargo.toml | 5 +- sync-server/src/api/add_version.rs | 123 +++++++++++++++++++++++ sync-server/src/api/get_child_version.rs | 78 ++++++++++++++ sync-server/src/api/mod.rs | 11 +- sync-server/src/main.rs | 35 +++++-- sync-server/src/server/mod.rs | 3 + sync-server/src/test.rs | 57 +++++++++++ 8 files changed, 299 insertions(+), 14 deletions(-) create mode 100644 sync-server/src/test.rs diff --git a/Cargo.lock b/Cargo.lock index 4c744d952..103d2e8ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2035,6 +2035,7 @@ dependencies = [ name = "sync-server" version = "0.1.0" dependencies = [ + "actix-rt", "actix-web", "failure", "futures", diff --git a/sync-server/Cargo.toml b/sync-server/Cargo.toml index 3c638aa7c..e9df49ac7 100644 --- a/sync-server/Cargo.toml +++ b/sync-server/Cargo.toml @@ -9,5 +9,8 @@ edition = "2018" [dependencies] actix-web = "3.3.0" failure = "0.1.8" -futures = "0.3.8" taskchampion = { path = "../taskchampion" } +futures = "0.3.8" + +[dev-dependencies] +actix-rt = "1.1.1" diff --git a/sync-server/src/api/add_version.rs b/sync-server/src/api/add_version.rs index 83f010ef5..4dc8393cf 100644 --- a/sync-server/src/api/add_version.rs +++ b/sync-server/src/api/add_version.rs @@ -43,6 +43,10 @@ pub(crate) async fn service( body.extend_from_slice(&chunk); } + if body.is_empty() { + return Err(error::ErrorBadRequest("Empty body")); + } + let result = data .add_version(client_id, parent_version_id, body.to_vec()) .map_err(|e| error::InternalError::new(e, StatusCode::INTERNAL_SERVER_ERROR))?; @@ -55,3 +59,122 @@ pub(crate) async fn service( .body(""), }) } + +#[cfg(test)] +mod test { + use super::*; + use crate::api::ServerState; + use crate::app_scope; + use crate::server::SyncServer; + use crate::test::TestServer; + use actix_web::{test, App}; + use taskchampion::Uuid; + + #[actix_rt::test] + async fn test_success() { + let client_id = Uuid::new_v4(); + let version_id = Uuid::new_v4(); + let parent_version_id = Uuid::new_v4(); + let server_box: Box = Box::new(TestServer { + expected_client_id: client_id, + av_expected_parent_version_id: parent_version_id, + av_expected_history_segment: b"abcd".to_vec(), + av_result: Some(AddVersionResult::Ok(version_id)), + ..Default::default() + }); + let server_state = ServerState::new(server_box); + let mut app = test::init_service(App::new().service(app_scope(server_state))).await; + + let uri = format!("/client/{}/add-version/{}", client_id, parent_version_id); + let req = test::TestRequest::post() + .uri(&uri) + .header( + "Content-Type", + "application/vnd.taskchampion.history-segment", + ) + .set_payload(b"abcd".to_vec()) + .to_request(); + let resp = test::call_service(&mut app, req).await; + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!( + resp.headers().get("X-Version-Id").unwrap(), + &version_id.to_string() + ); + assert_eq!(resp.headers().get("X-Parent-Version-Id"), None); + } + + #[actix_rt::test] + async fn test_conflict() { + let client_id = Uuid::new_v4(); + let version_id = Uuid::new_v4(); + let parent_version_id = Uuid::new_v4(); + let server_box: Box = Box::new(TestServer { + expected_client_id: client_id, + av_expected_parent_version_id: parent_version_id, + av_expected_history_segment: b"abcd".to_vec(), + av_result: Some(AddVersionResult::ExpectedParentVersion(version_id)), + ..Default::default() + }); + let server_state = ServerState::new(server_box); + let mut app = test::init_service(App::new().service(app_scope(server_state))).await; + + let uri = format!("/client/{}/add-version/{}", client_id, parent_version_id); + let req = test::TestRequest::post() + .uri(&uri) + .header( + "Content-Type", + "application/vnd.taskchampion.history-segment", + ) + .set_payload(b"abcd".to_vec()) + .to_request(); + let resp = test::call_service(&mut app, req).await; + assert_eq!(resp.status(), StatusCode::CONFLICT); + assert_eq!(resp.headers().get("X-Version-Id"), None); + assert_eq!( + resp.headers().get("X-Parent-Version-Id").unwrap(), + &version_id.to_string() + ); + } + + #[actix_rt::test] + async fn test_bad_content_type() { + let client_id = Uuid::new_v4(); + let parent_version_id = Uuid::new_v4(); + let server_box: Box = Box::new(TestServer { + ..Default::default() + }); + let server_state = ServerState::new(server_box); + let mut app = test::init_service(App::new().service(app_scope(server_state))).await; + + let uri = format!("/client/{}/add-version/{}", client_id, parent_version_id); + let req = test::TestRequest::post() + .uri(&uri) + .header("Content-Type", "not/correct") + .set_payload(b"abcd".to_vec()) + .to_request(); + let resp = test::call_service(&mut app, req).await; + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + } + + #[actix_rt::test] + async fn test_empty_body() { + let client_id = Uuid::new_v4(); + let parent_version_id = Uuid::new_v4(); + let server_box: Box = Box::new(TestServer { + ..Default::default() + }); + let server_state = ServerState::new(server_box); + let mut app = test::init_service(App::new().service(app_scope(server_state))).await; + + let uri = format!("/client/{}/add-version/{}", client_id, parent_version_id); + let req = test::TestRequest::post() + .uri(&uri) + .header( + "Content-Type", + "application/vnd.taskchampion.history-segment", + ) + .to_request(); + let resp = test::call_service(&mut app, req).await; + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + } +} diff --git a/sync-server/src/api/get_child_version.rs b/sync-server/src/api/get_child_version.rs index 4056408ce..b16f5219b 100644 --- a/sync-server/src/api/get_child_version.rs +++ b/sync-server/src/api/get_child_version.rs @@ -33,3 +33,81 @@ pub(crate) async fn service( Err(error::ErrorNotFound("no such version")) } } + +#[cfg(test)] +mod test { + use super::*; + use crate::api::ServerState; + use crate::app_scope; + use crate::server::{GetVersionResult, SyncServer}; + use crate::test::TestServer; + use actix_web::{test, App}; + use taskchampion::Uuid; + + #[actix_rt::test] + async fn test_success() { + let client_id = Uuid::new_v4(); + let version_id = Uuid::new_v4(); + let parent_version_id = Uuid::new_v4(); + let server_box: Box = Box::new(TestServer { + expected_client_id: client_id, + gcv_expected_parent_version_id: parent_version_id, + gcv_result: Some(GetVersionResult { + version_id: version_id, + parent_version_id: parent_version_id, + history_segment: b"abcd".to_vec(), + }), + ..Default::default() + }); + let server_state = ServerState::new(server_box); + let mut app = test::init_service(App::new().service(app_scope(server_state))).await; + + let uri = format!( + "/client/{}/get-child-version/{}", + client_id, parent_version_id + ); + let req = test::TestRequest::get().uri(&uri).to_request(); + let mut resp = test::call_service(&mut app, req).await; + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!( + resp.headers().get("X-Version-Id").unwrap(), + &version_id.to_string() + ); + assert_eq!( + resp.headers().get("X-Parent-Version-Id").unwrap(), + &parent_version_id.to_string() + ); + assert_eq!( + resp.headers().get("Content-Type").unwrap(), + &"application/vnd.taskchampion.history-segment".to_string() + ); + + use futures::StreamExt; + let (bytes, _) = resp.take_body().into_future().await; + assert_eq!(bytes.unwrap().unwrap().as_ref(), b"abcd"); + } + + #[actix_rt::test] + async fn test_not_found() { + let client_id = Uuid::new_v4(); + let parent_version_id = Uuid::new_v4(); + let server_box: Box = Box::new(TestServer { + expected_client_id: client_id, + gcv_expected_parent_version_id: parent_version_id, + gcv_result: None, + ..Default::default() + }); + let server_state = ServerState::new(server_box); + let mut app = test::init_service(App::new().service(app_scope(server_state))).await; + + let uri = format!( + "/client/{}/get-child-version/{}", + client_id, parent_version_id + ); + let req = test::TestRequest::get().uri(&uri).to_request(); + let resp = test::call_service(&mut app, req).await; + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + assert_eq!(resp.headers().get("X-Version-Id"), None); + assert_eq!(resp.headers().get("X-Parent-Version-Id"), None); + } +} diff --git a/sync-server/src/api/mod.rs b/sync-server/src/api/mod.rs index 9dcca18bc..c58d2420b 100644 --- a/sync-server/src/api/mod.rs +++ b/sync-server/src/api/mod.rs @@ -1,8 +1,9 @@ use crate::server::SyncServer; +use actix_web::{web, Scope}; use std::sync::Arc; -pub(crate) mod add_version; -pub(crate) mod get_child_version; +mod add_version; +mod get_child_version; /// The content-type for history segments (opaque blobs of bytes) pub(crate) const HISTORY_SEGMENT_CONTENT_TYPE: &str = @@ -16,3 +17,9 @@ pub(crate) const PARENT_VERSION_ID_HEADER: &str = "X-Parent-Version-Id"; /// The type containing a reference to the SyncServer object in the Actix state. pub(crate) type ServerState = Arc>; + +pub(crate) fn api_scope() -> Scope { + web::scope("") + .service(get_child_version::service) + .service(add_version::service) +} diff --git a/sync-server/src/main.rs b/sync-server/src/main.rs index a8e4cc9de..3123c80db 100644 --- a/sync-server/src/main.rs +++ b/sync-server/src/main.rs @@ -1,24 +1,37 @@ -use actix_web::{App, HttpServer}; -use api::ServerState; +use actix_web::{get, web, App, HttpServer, Responder, Scope}; +use api::{api_scope, ServerState}; use server::{InMemorySyncServer, SyncServer}; mod api; mod server; +#[cfg(test)] +mod test; + // TODO: use hawk to sign requests +#[get("/")] +async fn index() -> impl Responder { + // TODO: add version here + "TaskChampion sync server" +} + +/// Return a scope defining the URL rules for this server, with access to +/// the given ServerState. +pub(crate) fn app_scope(server_state: ServerState) -> Scope { + web::scope("") + .data(server_state) + .service(index) + .service(api_scope()) +} + #[actix_web::main] async fn main() -> std::io::Result<()> { let server_box: Box = Box::new(InMemorySyncServer::new()); let server_state = ServerState::new(server_box); - HttpServer::new(move || { - App::new() - .data(server_state.clone()) - .service(api::get_child_version::service) - .service(api::add_version::service) - }) - .bind("127.0.0.1:8080")? - .run() - .await + HttpServer::new(move || App::new().service(app_scope(server_state.clone()))) + .bind("127.0.0.1:8080")? + .run() + .await } diff --git a/sync-server/src/server/mod.rs b/sync-server/src/server/mod.rs index 6d8593c03..7768c0c08 100644 --- a/sync-server/src/server/mod.rs +++ b/sync-server/src/server/mod.rs @@ -13,6 +13,7 @@ pub(crate) type ClientId = Uuid; pub(crate) type VersionId = Uuid; /// Response to get_child_version +#[derive(Clone)] pub(crate) struct GetVersionResult { pub(crate) version_id: Uuid, pub(crate) parent_version_id: Uuid, @@ -20,12 +21,14 @@ pub(crate) struct GetVersionResult { } /// Response to add_version +#[derive(Clone)] pub(crate) enum AddVersionResult { /// OK, version added with the given ID Ok(VersionId), /// Rejected; expected a version with the given parent version ExpectedParentVersion(VersionId), } + pub(crate) trait SyncServer: Sync + Send { fn get_child_version( &self, diff --git a/sync-server/src/test.rs b/sync-server/src/test.rs new file mode 100644 index 000000000..b9f68a66d --- /dev/null +++ b/sync-server/src/test.rs @@ -0,0 +1,57 @@ +use crate::api::ServerState; +use crate::app_scope; +use crate::server::{ + AddVersionResult, ClientId, GetVersionResult, HistorySegment, SyncServer, VersionId, +}; +use actix_web::{test, App}; +use failure::Fallible; + +#[derive(Default)] +pub(crate) struct TestServer { + /// test server will panic if this is not given + pub expected_client_id: ClientId, + + pub gcv_expected_parent_version_id: VersionId, + pub gcv_result: Option, + + pub av_expected_parent_version_id: VersionId, + pub av_expected_history_segment: HistorySegment, + pub av_result: Option, +} + +impl SyncServer for TestServer { + fn get_child_version( + &self, + client_id: ClientId, + parent_version_id: VersionId, + ) -> Fallible> { + assert_eq!(client_id, self.expected_client_id); + assert_eq!(parent_version_id, self.gcv_expected_parent_version_id); + Ok(self.gcv_result.clone()) + } + + fn add_version( + &self, + client_id: ClientId, + parent_version_id: VersionId, + history_segment: HistorySegment, + ) -> Fallible { + assert_eq!(client_id, self.expected_client_id); + assert_eq!(parent_version_id, self.av_expected_parent_version_id); + assert_eq!(history_segment, self.av_expected_history_segment); + Ok(self.av_result.clone().unwrap()) + } +} + +#[actix_rt::test] +async fn test_index_get() { + let server_box: Box = Box::new(TestServer { + ..Default::default() + }); + let server_state = ServerState::new(server_box); + let mut app = test::init_service(App::new().service(app_scope(server_state))).await; + + let req = test::TestRequest::get().uri("/").to_request(); + let resp = test::call_service(&mut app, req).await; + assert!(resp.status().is_success()); +}