Applications & Application Categories endpoints
Added API endpoints for CRUD actions on applications and application categories
This commit is contained in:
parent
37e64cb9f2
commit
734b704fa2
File diff suppressed because it is too large
Load Diff
|
@ -15,5 +15,8 @@ actix-cors = "0.5.4"
|
||||||
chrono = { version = "0.4.19", features = ["serde"] }
|
chrono = { version = "0.4.19", features = ["serde"] }
|
||||||
serde = { version = "1.0.136", features= [ "derive" ] }
|
serde = { version = "1.0.136", features= [ "derive" ] }
|
||||||
serde_json = "1.0.78"
|
serde_json = "1.0.78"
|
||||||
actix-web = "4.0.0-rc.2"
|
actix-web = "4.0.0-rc.1"
|
||||||
actix-rt = "2.6.0"
|
actix-rt = "2.6.0"
|
||||||
|
tracing-test = "0.2.1"
|
||||||
|
tracing-actix-web = "0.5.0-rc.1"
|
||||||
|
cargo-embed = "0.12.0"
|
||||||
|
|
|
@ -15,7 +15,7 @@ command = "sqlx"
|
||||||
args = ["migrate", "run"]
|
args = ["migrate", "run"]
|
||||||
|
|
||||||
[tasks.resetdb]
|
[tasks.resetdb]
|
||||||
run_task = { name = ["dropdb", "createdb", "migratedb", "entity"] }
|
run_task = { name = ["dropdb", "createdb", "migratedb"] }
|
||||||
|
|
||||||
[tasks.entity]
|
[tasks.entity]
|
||||||
command = "sea-orm-cli"
|
command = "sea-orm-cli"
|
||||||
|
|
|
@ -1,5 +1,27 @@
|
||||||
-- Add migration script here
|
-- Add migration script here
|
||||||
CREATE TABLE application ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, app_name TEXT NOT NULL, url TEXT NOT NULL, description TEXT, active Boolean NOT NULL DEFAULT 1, glyph TEXT, application_category_id INTEGER);
|
CREATE TABLE application (
|
||||||
CREATE TABLE application_category ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, category_name TEXT NOT NULL, active BOOLEAN NOT NULL DEFAULT 1 );
|
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
CREATE TABLE bookmark ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, category_name TEXT NOT NULL, active BOOLEAN NOT NULL DEFAULT 1, glyph TEXT, bookmark_category_id INTEGER);
|
app_name TEXT NOT NULL,
|
||||||
CREATE TABLE bookmark_category ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, category_name TEXT NOT NULL, active BOOLEAN NOT NULL DEFAULT 1, glyph TEXT );
|
url TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
active Boolean NOT NULL DEFAULT 1,
|
||||||
|
glyph TEXT,
|
||||||
|
application_category_id INTEGER
|
||||||
|
);
|
||||||
|
CREATE TABLE application_category (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
category_name TEXT NOT NULL,
|
||||||
|
active BOOLEAN NOT NULL DEFAULT 1
|
||||||
|
);
|
||||||
|
CREATE TABLE bookmark (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
category_name TEXT NOT NULL,
|
||||||
|
active BOOLEAN NOT NULL DEFAULT 1,
|
||||||
|
glyph TEXT, bookmark_category_id INTEGER
|
||||||
|
);
|
||||||
|
CREATE TABLE bookmark_category (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
category_name TEXT NOT NULL,
|
||||||
|
active BOOLEAN NOT NULL DEFAULT 1,
|
||||||
|
glyph TEXT
|
||||||
|
);
|
||||||
|
|
|
@ -0,0 +1,324 @@
|
||||||
|
use tracing::instrument;
|
||||||
|
|
||||||
|
use crate::api::api_prelude::*;
|
||||||
|
use crate::error::{Error, Result};
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
#[get("")]
|
||||||
|
pub async fn list_application_categories(state: web::Data<AppState>) -> Result<HttpResponse> {
|
||||||
|
let cats: Vec<application_category::Model> =
|
||||||
|
ApplicationCategory::find().all(&state.db).await.unwrap();
|
||||||
|
let count = cats.len();
|
||||||
|
Ok(HttpResponse::Ok().json(ListObjects::new(cats, count)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
#[post("")]
|
||||||
|
pub async fn new_application_category(
|
||||||
|
state: web::Data<AppState>,
|
||||||
|
data: web::Json<application_category::Model>,
|
||||||
|
) -> Result<HttpResponse> {
|
||||||
|
let model = application_category::ActiveModel {
|
||||||
|
id: NotSet,
|
||||||
|
category_name: Set(data.0.category_name),
|
||||||
|
active: Set(data.0.active),
|
||||||
|
};
|
||||||
|
let rec = model.insert(&state.db).await?;
|
||||||
|
Ok(HttpResponse::Ok().json(rec))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
#[get("{id}")]
|
||||||
|
pub async fn get_application_category(
|
||||||
|
state: web::Data<AppState>,
|
||||||
|
id: web::Path<i32>,
|
||||||
|
) -> Result<HttpResponse> {
|
||||||
|
let id = id.into_inner();
|
||||||
|
let res: Option<application_category::Model> =
|
||||||
|
ApplicationCategory::find_by_id(id).one(&state.db).await?;
|
||||||
|
match res {
|
||||||
|
Some(rec) => Ok(HttpResponse::Ok().json(rec)),
|
||||||
|
None => Err(Error::not_found()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
#[put("{id}")]
|
||||||
|
pub async fn update_application_category(
|
||||||
|
state: web::Data<AppState>,
|
||||||
|
|
||||||
|
data: web::Json<application_category::Model>,
|
||||||
|
id: web::Path<i32>,
|
||||||
|
) -> Result<HttpResponse> {
|
||||||
|
let id = id.into_inner();
|
||||||
|
let res: Option<application_category::Model> =
|
||||||
|
ApplicationCategory::find_by_id(id).one(&state.db).await?;
|
||||||
|
match res {
|
||||||
|
Some(_rec) => {
|
||||||
|
let data = data.into_inner();
|
||||||
|
let ret = application_category::ActiveModel {
|
||||||
|
id: Set(id),
|
||||||
|
active: Set(data.active),
|
||||||
|
category_name: Set(data.category_name),
|
||||||
|
};
|
||||||
|
let model = ret.update(&state.db).await?;
|
||||||
|
Ok(HttpResponse::Ok().json(model))
|
||||||
|
}
|
||||||
|
None => Err(Error::not_found()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
#[delete("{id}")]
|
||||||
|
pub async fn delete_application_category(
|
||||||
|
state: web::Data<AppState>,
|
||||||
|
id: web::Path<i32>,
|
||||||
|
) -> Result<HttpResponse> {
|
||||||
|
ApplicationCategory::delete_many()
|
||||||
|
.filter(application_category::Column::Id.eq(id.into_inner()))
|
||||||
|
.exec(&state.db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().body(""))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
#[get("{id}/applications")]
|
||||||
|
pub async fn application_category_applications(
|
||||||
|
state: web::Data<AppState>,
|
||||||
|
id: web::Path<i32>,
|
||||||
|
) -> Result<HttpResponse> {
|
||||||
|
let recs: Vec<application::Model> = Application::find()
|
||||||
|
.filter(application::Column::ApplicationCategoryId.eq(id.into_inner()))
|
||||||
|
.all(&state.db)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let count = recs.len();
|
||||||
|
Ok(HttpResponse::Ok().json(ListObjects::new(recs, count)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Routes for the application endpoints. This binds up a scope with all endpoints for applications, to make it easier to add them to the server.
|
||||||
|
pub fn routes() -> Scope {
|
||||||
|
web::scope("/application_categories")
|
||||||
|
.service(application_category_applications)
|
||||||
|
.service(list_application_categories)
|
||||||
|
.service(update_application_category)
|
||||||
|
.service(delete_application_category)
|
||||||
|
.service(new_application_category)
|
||||||
|
.service(get_application_category)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
|
||||||
|
use crate::api::test_prelude::*;
|
||||||
|
use actix_web::http::Method;
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_list_application_categories() -> Result<()> {
|
||||||
|
let state = setup_state().await?;
|
||||||
|
application_category::ActiveModel {
|
||||||
|
category_name: Set("Application 1".into()),
|
||||||
|
active: Set(true),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.insert(&state.db)
|
||||||
|
.await?;
|
||||||
|
application_category::ActiveModel {
|
||||||
|
category_name: Set("Application 2".into()),
|
||||||
|
active: Set(true),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.insert(&state.db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let req = actix_web::test::TestRequest::with_uri("/application_categories")
|
||||||
|
.method(Method::GET)
|
||||||
|
.to_request();
|
||||||
|
let resp = call_endpoint!(req, state);
|
||||||
|
assert_eq!(resp.status(), 200);
|
||||||
|
let data = get_response!(
|
||||||
|
resp,
|
||||||
|
ListObjects<crate::entity::application_category::Model>
|
||||||
|
);
|
||||||
|
assert_eq!(2_usize, data.items.len());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_get_application_categories() -> Result<()> {
|
||||||
|
let state = setup_state().await?;
|
||||||
|
let model = application_category::ActiveModel {
|
||||||
|
category_name: Set("Application 1".into()),
|
||||||
|
active: Set(true),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.insert(&state.db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let req = actix_web::test::TestRequest::with_uri(&format!(
|
||||||
|
"/application_categories/{}",
|
||||||
|
model.id
|
||||||
|
))
|
||||||
|
.method(Method::GET)
|
||||||
|
.to_request();
|
||||||
|
let resp = call_endpoint!(req, state);
|
||||||
|
let status = resp.status();
|
||||||
|
let mut data = get_response!(resp, crate::entity::application_category::Model);
|
||||||
|
data.id = model.id;
|
||||||
|
assert_eq!(model, data);
|
||||||
|
assert_eq!(status, 200);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_new_application_category() -> Result<()> {
|
||||||
|
let model = application_category::Model {
|
||||||
|
id: 0,
|
||||||
|
category_name: "Some name".into(),
|
||||||
|
active: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
let state = setup_state().await?;
|
||||||
|
|
||||||
|
let req = actix_web::test::TestRequest::with_uri("/application_categories")
|
||||||
|
.method(Method::POST)
|
||||||
|
.set_json(model.clone())
|
||||||
|
.to_request();
|
||||||
|
|
||||||
|
let resp = call_endpoint!(req, state);
|
||||||
|
assert_eq!(resp.status(), 200);
|
||||||
|
let data = get_response!(resp, crate::entity::application_category::Model);
|
||||||
|
assert_eq!(model, data);
|
||||||
|
assert_eq!(
|
||||||
|
application_category::Entity::find()
|
||||||
|
.count(&state.db)
|
||||||
|
.await?,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_update_application_category() -> Result<()> {
|
||||||
|
let state = setup_state().await?;
|
||||||
|
application_category::ActiveModel {
|
||||||
|
category_name: Set("Some name".into()),
|
||||||
|
active: Set(true),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.insert(&state.db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut model = application_category::ActiveModel {
|
||||||
|
category_name: Set("Some name".into()),
|
||||||
|
active: Set(true),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.insert(&state.db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
model.category_name = "Another name".into();
|
||||||
|
|
||||||
|
let req = actix_web::test::TestRequest::with_uri(&format!(
|
||||||
|
"/application_categories/{}",
|
||||||
|
model.id
|
||||||
|
))
|
||||||
|
.method(Method::PUT)
|
||||||
|
.set_json(model.clone())
|
||||||
|
.to_request();
|
||||||
|
let resp = call_endpoint!(req, state);
|
||||||
|
assert_eq!(resp.status(), 200);
|
||||||
|
let mut data = get_response!(resp, crate::entity::application_category::Model);
|
||||||
|
data.id = model.id;
|
||||||
|
assert_eq!(model, data, "Check API");
|
||||||
|
|
||||||
|
let db_model = application_category::Entity::find_by_id(model.id)
|
||||||
|
.one(&state.db)
|
||||||
|
.await?
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(db_model, model, "Check DB");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_delete_application_category() -> Result<()> {
|
||||||
|
let state = setup_state().await?;
|
||||||
|
let model = application_category::ActiveModel {
|
||||||
|
category_name: Set("Some name".into()),
|
||||||
|
active: Set(true),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.insert(&state.db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let req = actix_web::test::TestRequest::with_uri(&format!(
|
||||||
|
"/application_categories/{}",
|
||||||
|
model.id
|
||||||
|
))
|
||||||
|
.method(Method::DELETE)
|
||||||
|
.to_request();
|
||||||
|
let resp = call_endpoint!(req, state);
|
||||||
|
assert_eq!(resp.status(), 200);
|
||||||
|
assert_eq!(
|
||||||
|
application_category::Entity::find()
|
||||||
|
.count(&state.db)
|
||||||
|
.await?,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_application_categories_applications() -> Result<()> {
|
||||||
|
let state = setup_state().await?;
|
||||||
|
let category = application_category::ActiveModel {
|
||||||
|
category_name: Set("Application 1".into()),
|
||||||
|
active: Set(true),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.insert(&state.db)
|
||||||
|
.await?;
|
||||||
|
application::ActiveModel {
|
||||||
|
app_name: Set("Application 1".into()),
|
||||||
|
url: Set("http://somewhere/".into()),
|
||||||
|
active: Set(true),
|
||||||
|
application_category_id: Set(Some(category.id)),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.insert(&state.db)
|
||||||
|
.await?;
|
||||||
|
application::ActiveModel {
|
||||||
|
app_name: Set("Application 2".into()),
|
||||||
|
url: Set("http://somewhere/".into()),
|
||||||
|
application_category_id: Set(Some(category.id)),
|
||||||
|
active: Set(true),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.insert(&state.db)
|
||||||
|
.await?;
|
||||||
|
application::ActiveModel {
|
||||||
|
app_name: Set("Application 2".into()),
|
||||||
|
url: Set("http://somewhere/".into()),
|
||||||
|
active: Set(true),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.insert(&state.db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let req = actix_web::test::TestRequest::with_uri(&format!(
|
||||||
|
"/application_categories/{}/applications",
|
||||||
|
category.id
|
||||||
|
))
|
||||||
|
.method(Method::GET)
|
||||||
|
.to_request();
|
||||||
|
let resp = call_endpoint!(req, state);
|
||||||
|
assert_eq!(resp.status(), 200);
|
||||||
|
let data = get_response!(resp, ListObjects<crate::entity::application::Model>);
|
||||||
|
assert_eq!(2_usize, data.items.len());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,33 +1,250 @@
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
use crate::api::api_prelude::*;
|
use crate::api::api_prelude::*;
|
||||||
|
use crate::error::{Error, Result};
|
||||||
|
|
||||||
#[instrument]
|
#[instrument]
|
||||||
#[get("/applications")]
|
#[get("")]
|
||||||
pub async fn list_applications(state: web::Data<AppState>) -> Result<HttpResponse, Error> {
|
pub async fn list_applications(state: web::Data<AppState>) -> Result<HttpResponse> {
|
||||||
let apps: Vec<application::Model> = Application::find().all(&state.db).await.unwrap();
|
let apps: Vec<application::Model> = Application::find().all(&state.db).await.unwrap();
|
||||||
Ok(HttpResponse::Ok().json(apps))
|
let count = apps.len();
|
||||||
|
Ok(HttpResponse::Ok().json(ListObjects::new(apps, count)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
#[post("")]
|
||||||
|
pub async fn new_application(
|
||||||
|
state: web::Data<AppState>,
|
||||||
|
data: web::Json<application::Model>,
|
||||||
|
) -> Result<HttpResponse> {
|
||||||
|
let model = application::ActiveModel {
|
||||||
|
id: NotSet,
|
||||||
|
app_name: Set(data.0.app_name),
|
||||||
|
description: Set(data.0.description),
|
||||||
|
url: Set(data.0.url),
|
||||||
|
active: Set(data.0.active),
|
||||||
|
glyph: Set(data.0.glyph),
|
||||||
|
application_category_id: Set(data.0.application_category_id),
|
||||||
|
};
|
||||||
|
let app = model.insert(&state.db).await?;
|
||||||
|
Ok(HttpResponse::Ok().json(app))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
#[get("{id}")]
|
||||||
|
pub async fn get_applications(
|
||||||
|
state: web::Data<AppState>,
|
||||||
|
id: web::Path<i32>,
|
||||||
|
) -> Result<HttpResponse> {
|
||||||
|
let id = id.into_inner();
|
||||||
|
let res: Option<application::Model> = Application::find_by_id(id).one(&state.db).await?;
|
||||||
|
match res {
|
||||||
|
Some(app) => Ok(HttpResponse::Ok().json(app)),
|
||||||
|
None => Err(Error::not_found()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
#[put("{id}")]
|
||||||
|
pub async fn update_applications(
|
||||||
|
state: web::Data<AppState>,
|
||||||
|
|
||||||
|
data: web::Json<application::Model>,
|
||||||
|
id: web::Path<i32>,
|
||||||
|
) -> Result<HttpResponse> {
|
||||||
|
let id = id.into_inner();
|
||||||
|
let res: Option<application::Model> = Application::find_by_id(id).one(&state.db).await?;
|
||||||
|
match res {
|
||||||
|
Some(_app) => {
|
||||||
|
let data = data.into_inner();
|
||||||
|
let ret = application::ActiveModel {
|
||||||
|
id: Set(id),
|
||||||
|
active: Set(data.active),
|
||||||
|
app_name: Set(data.app_name),
|
||||||
|
description: Set(data.description),
|
||||||
|
url: Set(data.url),
|
||||||
|
application_category_id: Set(data.application_category_id),
|
||||||
|
glyph: Set(data.glyph),
|
||||||
|
};
|
||||||
|
let model = ret.update(&state.db).await?;
|
||||||
|
Ok(HttpResponse::Ok().json(model))
|
||||||
|
}
|
||||||
|
None => Err(Error::not_found()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
#[delete("{id}")]
|
||||||
|
pub async fn delete_application(
|
||||||
|
state: web::Data<AppState>,
|
||||||
|
id: web::Path<i32>,
|
||||||
|
) -> Result<HttpResponse> {
|
||||||
|
Application::delete_many()
|
||||||
|
.filter(application::Column::Id.eq(id.into_inner()))
|
||||||
|
.exec(&state.db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().body(""))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Routes for the application endpoints. This binds up a scope with all endpoints for applications, to make it easier to add them to the server.
|
||||||
|
pub fn routes() -> Scope {
|
||||||
|
web::scope("/applications")
|
||||||
|
.service(list_applications)
|
||||||
|
.service(update_applications)
|
||||||
|
.service(delete_application)
|
||||||
|
.service(new_application)
|
||||||
|
.service(get_applications)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
use crate::api::test_prelude::*;
|
use crate::api::test_prelude::*;
|
||||||
use actix_web::http::Method;
|
use actix_web::http::Method;
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_list_applications() {
|
async fn test_list_applications() -> Result<()> {
|
||||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
let state = setup_state().await?;
|
||||||
.append_query_results(vec![vec![application::Model {
|
application::ActiveModel {
|
||||||
id: 1,
|
app_name: Set("Application 1".into()),
|
||||||
app_name: "Application 1".into(),
|
url: Set("http://somewhere/".into()),
|
||||||
..Default::default()
|
active: Set(true),
|
||||||
}]])
|
..Default::default()
|
||||||
.into_connection();
|
}
|
||||||
|
.insert(&state.db)
|
||||||
|
.await?;
|
||||||
|
application::ActiveModel {
|
||||||
|
app_name: Set("Application 2".into()),
|
||||||
|
url: Set("http://somewhere/".into()),
|
||||||
|
active: Set(true),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.insert(&state.db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let req = actix_web::test::TestRequest::with_uri("/applications")
|
let req = actix_web::test::TestRequest::with_uri("/applications")
|
||||||
.method(Method::GET)
|
.method(Method::GET)
|
||||||
.to_request();
|
.to_request();
|
||||||
let resp = call_endpoint!(req, db);
|
let resp = call_endpoint!(req, state);
|
||||||
assert_eq!(resp.status(), 200);
|
assert_eq!(resp.status(), 200);
|
||||||
|
let data = get_response!(resp, ListObjects<crate::entity::application::Model>);
|
||||||
|
assert_eq!(2_usize, data.items.len());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_get_applications() -> Result<()> {
|
||||||
|
let state = setup_state().await?;
|
||||||
|
let model = application::ActiveModel {
|
||||||
|
app_name: Set("Application 1".into()),
|
||||||
|
url: Set("http://somewhere/".into()),
|
||||||
|
active: Set(true),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.insert(&state.db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let req = actix_web::test::TestRequest::with_uri(&format!("/applications/{}", model.id))
|
||||||
|
.method(Method::GET)
|
||||||
|
.to_request();
|
||||||
|
let resp = call_endpoint!(req, state);
|
||||||
|
let status = resp.status();
|
||||||
|
let mut data = get_response!(resp, crate::entity::application::Model);
|
||||||
|
data.id = model.id;
|
||||||
|
assert_eq!(model, data);
|
||||||
|
assert_eq!(status, 200);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_new_application() -> Result<()> {
|
||||||
|
let model = application::Model {
|
||||||
|
id: 0,
|
||||||
|
app_name: "Application 1".into(),
|
||||||
|
glyph: Some("web".into()),
|
||||||
|
url: "http://example.com".into(),
|
||||||
|
description: Some("Some Application".into()),
|
||||||
|
active: true,
|
||||||
|
application_category_id: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let state = setup_state().await?;
|
||||||
|
|
||||||
|
let req = actix_web::test::TestRequest::with_uri("/applications")
|
||||||
|
.method(Method::POST)
|
||||||
|
.set_json(model.clone())
|
||||||
|
.to_request();
|
||||||
|
|
||||||
|
let resp = call_endpoint!(req, state);
|
||||||
|
assert_eq!(resp.status(), 200);
|
||||||
|
let data = get_response!(resp, crate::entity::application::Model);
|
||||||
|
assert_eq!(model, data);
|
||||||
|
assert_eq!(application::Entity::find().count(&state.db).await?, 1);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_update_application() -> Result<()> {
|
||||||
|
let state = setup_state().await?;
|
||||||
|
application::ActiveModel {
|
||||||
|
app_name: Set("Application 1".into()),
|
||||||
|
url: Set("http://somewhere/".into()),
|
||||||
|
active: Set(true),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.insert(&state.db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut model = application::ActiveModel {
|
||||||
|
app_name: Set("Application 2".into()),
|
||||||
|
url: Set("http://somewhere/".into()),
|
||||||
|
active: Set(true),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.insert(&state.db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
model.url = "http://updated.com".into();
|
||||||
|
|
||||||
|
let req = actix_web::test::TestRequest::with_uri(&format!("/applications/{}", model.id))
|
||||||
|
.method(Method::PUT)
|
||||||
|
.set_json(model.clone())
|
||||||
|
.to_request();
|
||||||
|
let resp = call_endpoint!(req, state);
|
||||||
|
assert_eq!(resp.status(), 200);
|
||||||
|
let mut data = get_response!(resp, crate::entity::application::Model);
|
||||||
|
data.id = model.id;
|
||||||
|
assert_eq!(model, data, "Check API");
|
||||||
|
|
||||||
|
let db_model = application::Entity::find_by_id(model.id)
|
||||||
|
.one(&state.db)
|
||||||
|
.await?
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(db_model, model, "Check DB");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_delete_application() -> Result<()> {
|
||||||
|
let state = setup_state().await?;
|
||||||
|
let model = application::ActiveModel {
|
||||||
|
app_name: Set("Application 1".into()),
|
||||||
|
url: Set("http://somewhere/".into()),
|
||||||
|
active: Set(true),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.insert(&state.db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let req = actix_web::test::TestRequest::with_uri(&format!("/applications/{}", model.id))
|
||||||
|
.method(Method::DELETE)
|
||||||
|
.to_request();
|
||||||
|
let resp = call_endpoint!(req, state);
|
||||||
|
assert_eq!(resp.status(), 200);
|
||||||
|
assert_eq!(application::Entity::find().count(&state.db).await?, 0);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,35 +1,100 @@
|
||||||
#[cfg(test)]
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
|
#[cfg(test)]
|
||||||
macro_rules! call_endpoint {
|
macro_rules! call_endpoint {
|
||||||
($req:ident, $db:ident) => {{
|
($req:ident, $state:ident) => {{
|
||||||
let state = AppState { db: $db };
|
// let subscriber = tracing_subscriber::prelude::__tracing_subscriber_SubscriberExt::with(
|
||||||
|
// tracing_subscriber::registry(),
|
||||||
|
// tracing_subscriber::Layer::with_filter(
|
||||||
|
// tracing_subscriber::fmt::Layer::new()
|
||||||
|
// .pretty()
|
||||||
|
// .with_writer(std::io::stdout)
|
||||||
|
// .with_ansi(true),
|
||||||
|
// tracing_subscriber::filter::LevelFilter::DEBUG,
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// tracing::subscriber::set_global_default(subscriber)
|
||||||
|
// .expect("Unable to set a global collector");
|
||||||
|
|
||||||
let a = App::new()
|
let a = App::new()
|
||||||
.app_data(state)
|
.wrap(tracing_actix_web::TracingLogger::default())
|
||||||
.service(crate::api::applications::list_applications);
|
.app_data($state.clone())
|
||||||
let mut app = actix_web::test::init_service(a).await;
|
.service(crate::api::applications::routes())
|
||||||
let resp = actix_web::test::call_service(&mut app, $req).await;
|
.service(crate::api::application_category::routes());
|
||||||
|
let app = actix_web::test::init_service(a).await;
|
||||||
|
let resp = actix_web::test::call_service(&app, $req).await;
|
||||||
resp
|
resp
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
macro_rules! get_response {
|
||||||
|
($resp: ident, $type:ty) => {{
|
||||||
|
let body = test::read_body($resp).await.to_vec();
|
||||||
|
serde_json::from_slice::<$type>(&body).unwrap()
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod application_category;
|
||||||
pub mod applications;
|
pub mod applications;
|
||||||
|
|
||||||
mod api_prelude {
|
mod api_prelude {
|
||||||
|
pub use super::ListObjects;
|
||||||
pub use crate::entity::prelude::*;
|
pub use crate::entity::prelude::*;
|
||||||
pub use crate::entity::*;
|
pub use crate::entity::*;
|
||||||
pub use crate::AppState;
|
pub use crate::AppState;
|
||||||
pub use actix_web::{get, web, Error, HttpResponse};
|
pub use actix_web::{delete, get, post, put, web, Error, HttpResponse, Scope};
|
||||||
pub use sea_orm::prelude::*;
|
pub use sea_orm::prelude::*;
|
||||||
|
pub use sea_orm::{NotSet, Set};
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test_prelude {
|
mod test_prelude {
|
||||||
|
pub use super::ListObjects;
|
||||||
pub use crate::entity::*;
|
pub use crate::entity::*;
|
||||||
pub use crate::AppState;
|
pub use crate::AppState;
|
||||||
|
|
||||||
|
pub use crate::error::Result;
|
||||||
pub use actix_web::dev::ServiceResponse;
|
pub use actix_web::dev::ServiceResponse;
|
||||||
pub use actix_web::{test, web, App};
|
pub use actix_web::{test, web, App};
|
||||||
|
use sea_orm::sea_query::TableCreateStatement;
|
||||||
|
use sea_orm::ConnectionTrait;
|
||||||
|
use sea_orm::Database;
|
||||||
|
use sea_orm::DbBackend;
|
||||||
|
use sea_orm::Schema;
|
||||||
pub use sea_orm::{
|
pub use sea_orm::{
|
||||||
entity::prelude::*, entity::*, tests_cfg::*, DatabaseBackend, MockDatabase, Transaction,
|
entity::prelude::*, entity::*, tests_cfg::*, DatabaseBackend, MockDatabase, MockExecResult,
|
||||||
|
Transaction,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Sets up a testing state with an in-memory database and creates the scheme.
|
||||||
|
pub async fn setup_state() -> Result<actix_web::web::Data<AppState>> {
|
||||||
|
let db = Database::connect("sqlite::memory:").await?;
|
||||||
|
let schema = Schema::new(DbBackend::Sqlite);
|
||||||
|
|
||||||
|
let stmt: TableCreateStatement = schema.create_table_from_entity(application::Entity);
|
||||||
|
db.execute(db.get_database_backend().build(&stmt)).await?;
|
||||||
|
let stmt: TableCreateStatement =
|
||||||
|
schema.create_table_from_entity(application_category::Entity);
|
||||||
|
db.execute(db.get_database_backend().build(&stmt)).await?;
|
||||||
|
|
||||||
|
Ok(actix_web::web::Data::new(AppState { db }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ListObjects<T>
|
||||||
|
where
|
||||||
|
T: Serialize,
|
||||||
|
{
|
||||||
|
items: Vec<T>,
|
||||||
|
total: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Serialize> ListObjects<T> {
|
||||||
|
pub fn new(items: Vec<T>, total: usize) -> Self {
|
||||||
|
Self { items, total }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,10 @@ use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||||
#[sea_orm(table_name = "application")]
|
#[sea_orm(table_name = "application")]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Model {
|
pub struct Model {
|
||||||
#[sea_orm(primary_key)]
|
#[sea_orm(primary_key)]
|
||||||
|
#[serde(skip_deserializing)]
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub app_name: String,
|
pub app_name: String,
|
||||||
pub url: String,
|
pub url: String,
|
||||||
|
@ -16,16 +18,27 @@ pub struct Model {
|
||||||
pub application_category_id: Option<i32>,
|
pub application_category_id: Option<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter)]
|
||||||
pub enum Relation {
|
pub enum Relation {
|
||||||
#[sea_orm(
|
|
||||||
belongs_to = "super::application_category::Entity",
|
|
||||||
from = "Column::ApplicationCategoryId",
|
|
||||||
to = "super::application_category::Column::Id"
|
|
||||||
)]
|
|
||||||
ApplicationCategory,
|
ApplicationCategory,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl RelationTrait for Relation {
|
||||||
|
fn def(&self) -> RelationDef {
|
||||||
|
match self {
|
||||||
|
Self::ApplicationCategory => Entity::belongs_to(super::application_category::Entity)
|
||||||
|
.from(Column::ApplicationCategoryId)
|
||||||
|
.to(super::application_category::Column::Id)
|
||||||
|
.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::application_category::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::ApplicationCategory.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -5,20 +5,31 @@ use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||||
#[sea_orm(table_name = "application_category")]
|
#[sea_orm(table_name = "application_category")]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Model {
|
pub struct Model {
|
||||||
#[sea_orm(primary_key)]
|
#[sea_orm(primary_key)]
|
||||||
|
#[serde(skip_deserializing)]
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub category_name: String,
|
pub category_name: String,
|
||||||
pub active: bool,
|
pub active: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter)]
|
#[derive(Copy, Clone, Debug, EnumIter)]
|
||||||
pub enum Relation {}
|
pub enum Relation {
|
||||||
|
Application,
|
||||||
|
}
|
||||||
|
|
||||||
impl RelationTrait for Relation {
|
impl RelationTrait for Relation {
|
||||||
fn def(&self) -> RelationDef {
|
fn def(&self) -> RelationDef {
|
||||||
panic!("No RelationDef")
|
match self {
|
||||||
|
Self::Application => Entity::has_many(super::application::Entity).into(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Related<super::application::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::Application.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||||
|
pub enum ErrorCode {
|
||||||
|
NotFound,
|
||||||
|
DatabaseError,
|
||||||
|
Internal,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Error {
|
||||||
|
code: ErrorCode,
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error {
|
||||||
|
pub fn not_found() -> Self {
|
||||||
|
Self {
|
||||||
|
code: ErrorCode::NotFound,
|
||||||
|
message: "Resource not found".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for Error {
|
||||||
|
fn from(s: &str) -> Self {
|
||||||
|
Error {
|
||||||
|
code: ErrorCode::Internal,
|
||||||
|
message: s.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl actix_web::error::ResponseError for Error {}
|
||||||
|
|
||||||
|
impl From<sea_orm::DbErr> for Error {
|
||||||
|
fn from(e: sea_orm::DbErr) -> Self {
|
||||||
|
Self {
|
||||||
|
code: ErrorCode::DatabaseError,
|
||||||
|
message: e.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Error {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.message)
|
||||||
|
}
|
||||||
|
}
|
13
src/main.rs
13
src/main.rs
|
@ -1,13 +1,11 @@
|
||||||
use std::sync::Arc;
|
use actix_web::{web, App, HttpServer};
|
||||||
|
|
||||||
use actix_web::{App, HttpServer};
|
|
||||||
use sea_orm::{Database, DatabaseConnection};
|
use sea_orm::{Database, DatabaseConnection};
|
||||||
use tracing::{instrument, info};
|
use tracing::{info, instrument};
|
||||||
use tracing_subscriber::prelude::*;
|
use tracing_subscriber::prelude::*;
|
||||||
|
|
||||||
mod api;
|
mod api;
|
||||||
|
|
||||||
mod entity;
|
mod entity;
|
||||||
|
mod error;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
|
@ -26,13 +24,14 @@ async fn main() {
|
||||||
tracing::subscriber::set_global_default(subscriber).expect("Unable to set a global collector");
|
tracing::subscriber::set_global_default(subscriber).expect("Unable to set a global collector");
|
||||||
|
|
||||||
let db = setup_database().await.unwrap();
|
let db = setup_database().await.unwrap();
|
||||||
let state = Arc::new(AppState { db });
|
let state = web::Data::new(AppState { db });
|
||||||
|
|
||||||
info!("Starting http server on 8080");
|
info!("Starting http server on 8080");
|
||||||
HttpServer::new(move || {
|
HttpServer::new(move || {
|
||||||
App::new()
|
App::new()
|
||||||
.app_data(state.clone())
|
.app_data(state.clone())
|
||||||
.service(api::applications::list_applications)
|
.service(api::applications::routes())
|
||||||
|
.service(api::application_category::routes())
|
||||||
})
|
})
|
||||||
.bind("127.0.0.1:8080")
|
.bind("127.0.0.1:8080")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
|
Loading…
Reference in New Issue