mirror of https://github.com/rwf2/Rocket.git
Add tests for 'todo' example.
This commit is contained in:
parent
6a7903a220
commit
614ec1359e
|
@ -14,6 +14,10 @@ diesel = { version = "0.12", features = ["sqlite"] }
|
||||||
diesel_codegen = { version = "0.12", features = ["sqlite"] }
|
diesel_codegen = { version = "0.12", features = ["sqlite"] }
|
||||||
r2d2-diesel = "0.12"
|
r2d2-diesel = "0.12"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
parking_lot = {version = "0.4", features = ["nightly"]}
|
||||||
|
rand = "0.3"
|
||||||
|
|
||||||
[dependencies.rocket_contrib]
|
[dependencies.rocket_contrib]
|
||||||
path = "../../contrib"
|
path = "../../contrib"
|
||||||
default_features = false
|
default_features = false
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
CREATE TABLE tasks (
|
CREATE TABLE tasks (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
description VARCHAR NOT NULL,
|
description VARCHAR NOT NULL,
|
||||||
completed BOOLEAN NOT NULL DEFAULT 0
|
completed BOOLEAN NOT NULL DEFAULT 0
|
||||||
);
|
);
|
||||||
|
|
||||||
INSERT INTO tasks (description) VALUES ("my first task");
|
INSERT INTO tasks (description) VALUES ("demo task");
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
CREATE TABLE tasks (
|
CREATE TABLE tasks (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
description VARCHAR NOT NULL,
|
description VARCHAR NOT NULL,
|
||||||
completed BOOLEAN DEFAULT 0
|
completed BOOLEAN NOT NULL DEFAULT 0
|
||||||
)
|
)
|
||||||
|
|
|
@ -18,11 +18,12 @@ pub fn init_pool() -> Pool {
|
||||||
r2d2::Pool::new(config, manager).expect("db pool")
|
r2d2::Pool::new(config, manager).expect("db pool")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Conn(r2d2::PooledConnection<ConnectionManager<SqliteConnection>>);
|
pub struct Conn(pub r2d2::PooledConnection<ConnectionManager<SqliteConnection>>);
|
||||||
|
|
||||||
impl Deref for Conn {
|
impl Deref for Conn {
|
||||||
type Target = SqliteConnection;
|
type Target = SqliteConnection;
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
&self.0
|
&self.0
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#![feature(plugin, custom_derive)]
|
#![feature(plugin, custom_derive, const_fn)]
|
||||||
#![plugin(rocket_codegen)]
|
#![plugin(rocket_codegen)]
|
||||||
|
|
||||||
extern crate rocket;
|
extern crate rocket;
|
||||||
|
@ -13,12 +13,14 @@ extern crate r2d2_diesel;
|
||||||
mod static_files;
|
mod static_files;
|
||||||
mod task;
|
mod task;
|
||||||
mod db;
|
mod db;
|
||||||
|
#[cfg(test)] mod tests;
|
||||||
|
|
||||||
|
use rocket::Rocket;
|
||||||
use rocket::request::{Form, FlashMessage};
|
use rocket::request::{Form, FlashMessage};
|
||||||
use rocket::response::{Flash, Redirect};
|
use rocket::response::{Flash, Redirect};
|
||||||
use rocket_contrib::Template;
|
use rocket_contrib::Template;
|
||||||
|
|
||||||
use task::Task;
|
use task::{Task, Todo};
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
struct Context<'a, 'b>{ msg: Option<(&'a str, &'b str)>, tasks: Vec<Task> }
|
struct Context<'a, 'b>{ msg: Option<(&'a str, &'b str)>, tasks: Vec<Task> }
|
||||||
|
@ -34,11 +36,11 @@ impl<'a, 'b> Context<'a, 'b> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/", data = "<todo_form>")]
|
#[post("/", data = "<todo_form>")]
|
||||||
fn new(todo_form: Form<Task>, conn: db::Conn) -> Flash<Redirect> {
|
fn new(todo_form: Form<Todo>, conn: db::Conn) -> Flash<Redirect> {
|
||||||
let todo = todo_form.into_inner();
|
let todo = todo_form.into_inner();
|
||||||
if todo.description.is_empty() {
|
if todo.description.is_empty() {
|
||||||
Flash::error(Redirect::to("/"), "Description cannot be empty.")
|
Flash::error(Redirect::to("/"), "Description cannot be empty.")
|
||||||
} else if todo.insert(&conn) {
|
} else if Task::insert(todo, &conn) {
|
||||||
Flash::success(Redirect::to("/"), "Todo successfully added.")
|
Flash::success(Redirect::to("/"), "Todo successfully added.")
|
||||||
} else {
|
} else {
|
||||||
Flash::error(Redirect::to("/"), "Whoops! The server failed.")
|
Flash::error(Redirect::to("/"), "Whoops! The server failed.")
|
||||||
|
@ -71,11 +73,23 @@ fn index(msg: Option<FlashMessage>, conn: db::Conn) -> Template {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn rocket() -> (Rocket, Option<db::Conn>) {
|
||||||
rocket::ignite()
|
let pool = db::init_pool();
|
||||||
|
let conn = if cfg!(test) {
|
||||||
|
Some(db::Conn(pool.get().expect("database connection for testing")))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let rocket = rocket::ignite()
|
||||||
.manage(db::init_pool())
|
.manage(db::init_pool())
|
||||||
.mount("/", routes![index, static_files::all])
|
.mount("/", routes![index, static_files::all])
|
||||||
.mount("/todo/", routes![new, toggle, delete])
|
.mount("/todo/", routes![new, toggle, delete])
|
||||||
.attach(Template::fairing())
|
.attach(Template::fairing());
|
||||||
.launch();
|
|
||||||
|
(rocket, conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
rocket().0.launch();
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,11 +10,16 @@ mod schema {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[table_name = "tasks"]
|
#[table_name = "tasks"]
|
||||||
#[derive(Serialize, Queryable, Insertable, FromForm, Debug, Clone)]
|
#[derive(Serialize, Queryable, Insertable, Debug, Clone)]
|
||||||
pub struct Task {
|
pub struct Task {
|
||||||
id: Option<i32>,
|
pub id: Option<i32>,
|
||||||
|
pub description: String,
|
||||||
|
pub completed: bool
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(FromForm)]
|
||||||
|
pub struct Todo {
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub completed: Option<bool>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Task {
|
impl Task {
|
||||||
|
@ -22,8 +27,9 @@ impl Task {
|
||||||
all_tasks.order(tasks::id.desc()).load::<Task>(conn).unwrap()
|
all_tasks.order(tasks::id.desc()).load::<Task>(conn).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert(&self, conn: &SqliteConnection) -> bool {
|
pub fn insert(todo: Todo, conn: &SqliteConnection) -> bool {
|
||||||
diesel::insert(self).into(tasks::table).execute(conn).is_ok()
|
let t = Task { id: None, description: todo.description, completed: false };
|
||||||
|
diesel::insert(&t).into(tasks::table).execute(conn).is_ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toggle_with_id(id: i32, conn: &SqliteConnection) -> bool {
|
pub fn toggle_with_id(id: i32, conn: &SqliteConnection) -> bool {
|
||||||
|
@ -32,7 +38,7 @@ impl Task {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let new_status = !task.unwrap().completed.unwrap();
|
let new_status = !task.unwrap().completed;
|
||||||
let updated_task = diesel::update(all_tasks.find(id));
|
let updated_task = diesel::update(all_tasks.find(id));
|
||||||
updated_task.set(task_completed.eq(new_status)).execute(conn).is_ok()
|
updated_task.set(task_completed.eq(new_status)).execute(conn).is_ok()
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,144 @@
|
||||||
|
extern crate parking_lot;
|
||||||
|
extern crate rand;
|
||||||
|
|
||||||
|
use super::task::Task;
|
||||||
|
use self::parking_lot::Mutex;
|
||||||
|
use self::rand::{Rng, thread_rng};
|
||||||
|
|
||||||
|
use rocket::testing::MockRequest;
|
||||||
|
use rocket::http::Method::*;
|
||||||
|
use rocket::http::{Status, ContentType};
|
||||||
|
|
||||||
|
// We use a lock to synchronize between tests so DB operations don't collide.
|
||||||
|
// For now. In the future, we'll have a nice way to run each test in a DB
|
||||||
|
// transaction so we can regain concurrency.
|
||||||
|
static DB_LOCK: Mutex<()> = Mutex::new(());
|
||||||
|
|
||||||
|
macro_rules! run_test {
|
||||||
|
(|$rocket:ident, $conn:ident| $block:expr) => ({
|
||||||
|
let _lock = DB_LOCK.lock();
|
||||||
|
let ($rocket, db) = super::rocket();
|
||||||
|
let $conn = db.expect("failed to get database connection for testing");
|
||||||
|
$block
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_insertion_deletion() {
|
||||||
|
run_test!(|rocket, conn| {
|
||||||
|
// Get the tasks before making changes.
|
||||||
|
let init_tasks = Task::all(&conn);
|
||||||
|
|
||||||
|
// Issue a request to insert a new task.
|
||||||
|
let mut req = MockRequest::new(Post, "/todo")
|
||||||
|
.header(ContentType::Form)
|
||||||
|
.body("description=My+first+task");
|
||||||
|
req.dispatch_with(&rocket);
|
||||||
|
|
||||||
|
// Ensure we have one more task in the database.
|
||||||
|
let new_tasks = Task::all(&conn);
|
||||||
|
assert_eq!(new_tasks.len(), init_tasks.len() + 1);
|
||||||
|
|
||||||
|
// Ensure the task is what we expect.
|
||||||
|
assert_eq!(new_tasks[0].description, "My first task");
|
||||||
|
assert_eq!(new_tasks[0].completed, false);
|
||||||
|
|
||||||
|
// Issue a request to delete the task.
|
||||||
|
let id = new_tasks[0].id.unwrap();
|
||||||
|
let mut req = MockRequest::new(Delete, format!("/todo/{}", id));
|
||||||
|
req.dispatch_with(&rocket);
|
||||||
|
|
||||||
|
// Ensure it's gone.
|
||||||
|
let final_tasks = Task::all(&conn);
|
||||||
|
assert_eq!(final_tasks.len(), init_tasks.len());
|
||||||
|
assert_ne!(final_tasks[0].description, "My first task");
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_toggle() {
|
||||||
|
run_test!(|rocket, conn| {
|
||||||
|
// Issue a request to insert a new task; ensure it's not yet completed.
|
||||||
|
let mut req = MockRequest::new(Post, "/todo")
|
||||||
|
.header(ContentType::Form)
|
||||||
|
.body("description=test_for_completion");
|
||||||
|
req.dispatch_with(&rocket);
|
||||||
|
|
||||||
|
let task = Task::all(&conn)[0].clone();
|
||||||
|
assert_eq!(task.completed, false);
|
||||||
|
|
||||||
|
// Issue a request to toggle the task; ensure it is completed.
|
||||||
|
let mut req = MockRequest::new(Put, format!("/todo/{}", task.id.unwrap()));
|
||||||
|
req.dispatch_with(&rocket);
|
||||||
|
assert_eq!(Task::all(&conn)[0].completed, true);
|
||||||
|
|
||||||
|
// Issue a request to toggle the task; ensure it's not completed again.
|
||||||
|
let mut req = MockRequest::new(Put, format!("/todo/{}", task.id.unwrap()));
|
||||||
|
req.dispatch_with(&rocket);
|
||||||
|
assert_eq!(Task::all(&conn)[0].completed, false);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_many_insertions() {
|
||||||
|
const ITER: usize = 100;
|
||||||
|
|
||||||
|
let mut rng = thread_rng();
|
||||||
|
run_test!(|rocket, conn| {
|
||||||
|
// Get the number of tasks initially.
|
||||||
|
let init_num = Task::all(&conn).len();
|
||||||
|
let mut descs = Vec::new();
|
||||||
|
|
||||||
|
for i in 0..ITER {
|
||||||
|
// Issue a request to insert a new task with a random description.
|
||||||
|
let desc: String = rng.gen_ascii_chars().take(12).collect();
|
||||||
|
let mut req = MockRequest::new(Post, "/todo")
|
||||||
|
.header(ContentType::Form)
|
||||||
|
.body(format!("description={}", desc));
|
||||||
|
req.dispatch_with(&rocket);
|
||||||
|
|
||||||
|
// Record the description we choose for this iteration.
|
||||||
|
descs.insert(0, desc);
|
||||||
|
|
||||||
|
// Ensure the task was inserted properly and all other tasks remain.
|
||||||
|
let tasks = Task::all(&conn);
|
||||||
|
assert_eq!(tasks.len(), init_num + i + 1);
|
||||||
|
|
||||||
|
for j in 0..i {
|
||||||
|
assert_eq!(descs[j], tasks[j].description);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bad_form_submissions() {
|
||||||
|
run_test!(|rocket, _conn| {
|
||||||
|
// Submit an empty form. We should get a 422 but no flash error.
|
||||||
|
let mut req = MockRequest::new(Post, "/todo").header(ContentType::Form);
|
||||||
|
let res = req.dispatch_with(&rocket);
|
||||||
|
assert_eq!(res.status(), Status::UnprocessableEntity);
|
||||||
|
let mut cookies = res.headers().get("Set-Cookie");
|
||||||
|
assert!(!cookies.any(|value| value.contains("error")));
|
||||||
|
|
||||||
|
// Submit a form with an empty description.
|
||||||
|
let mut req = MockRequest::new(Post, "/todo")
|
||||||
|
.header(ContentType::Form)
|
||||||
|
.body("description=");
|
||||||
|
|
||||||
|
// We look for 'error' in the cookies which corresponds to flash message
|
||||||
|
// being set as an error.
|
||||||
|
let res = req.dispatch_with(&rocket);
|
||||||
|
let mut cookies = res.headers().get("Set-Cookie");
|
||||||
|
assert!(cookies.any(|value| value.contains("error")));
|
||||||
|
|
||||||
|
// Submit a form without a description. Expect a 422 but no flash error.
|
||||||
|
let mut req = MockRequest::new(Post, "/todo")
|
||||||
|
.header(ContentType::Form)
|
||||||
|
.body("evil=smile");
|
||||||
|
let res = req.dispatch_with(&rocket);
|
||||||
|
assert_eq!(res.status(), Status::UnprocessableEntity);
|
||||||
|
let mut cookies = res.headers().get("Set-Cookie");
|
||||||
|
assert!(!cookies.any(|value| value.contains("error")));
|
||||||
|
})
|
||||||
|
}
|
Loading…
Reference in New Issue