mirror of
https://github.com/n08i40k/schedule-parser-rusted.git
synced 2025-12-06 17:57:47 +03:00
Регистрация и тесты эндпоинтов
This commit is contained in:
@@ -1,2 +1,3 @@
|
||||
mod schema;
|
||||
pub mod sign_in;
|
||||
mod schema;
|
||||
pub mod sign_up;
|
||||
|
||||
@@ -1,56 +1,109 @@
|
||||
use crate::database::models::User;
|
||||
use serde::{Deserialize, Serialize, Serializer};
|
||||
pub mod sign_in {
|
||||
use crate::database::models::User;
|
||||
use crate::routes::schema::shared::{ErrorToHttpCode, IResponse};
|
||||
use crate::routes::schema::user;
|
||||
use actix_web::http::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SignInDto {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
pub struct SignInResult(Result<SignInOk, SignInErr>);
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SignInOk {
|
||||
id: String,
|
||||
access_token: String,
|
||||
group: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct SignInErr {
|
||||
code: SignInErrCode,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum SignInErrCode {
|
||||
IncorrectCredentials,
|
||||
InvalidVkAccessToken,
|
||||
}
|
||||
|
||||
impl SignInResult {
|
||||
pub fn ok(user: &User) -> Self {
|
||||
Self(Ok(SignInOk {
|
||||
id: user.id.clone(),
|
||||
access_token: user.access_token.clone(),
|
||||
group: user.group.clone(),
|
||||
}))
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct Request {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
pub fn err(code: SignInErrCode) -> SignInResult {
|
||||
Self(Err(SignInErr { code }))
|
||||
}
|
||||
}
|
||||
pub type Response = IResponse<user::ResponseOk, ResponseErr>;
|
||||
|
||||
impl Serialize for SignInResult {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match &self.0 {
|
||||
Ok(ok) => serializer.serialize_some(&ok),
|
||||
Err(err) => serializer.serialize_some(&err),
|
||||
#[derive(Serialize)]
|
||||
pub struct ResponseErr {
|
||||
code: ErrorCode,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum ErrorCode {
|
||||
IncorrectCredentials,
|
||||
InvalidVkAccessToken,
|
||||
}
|
||||
|
||||
pub trait ResponseExt {
|
||||
fn ok(user: &User) -> Self;
|
||||
fn err(code: ErrorCode) -> Response;
|
||||
}
|
||||
|
||||
impl ResponseExt for Response {
|
||||
fn ok(user: &User) -> Self {
|
||||
IResponse(Ok(user::ResponseOk::from_user(&user)))
|
||||
}
|
||||
|
||||
fn err(code: ErrorCode) -> Response {
|
||||
IResponse(Err(ResponseErr { code }))
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrorToHttpCode for ResponseErr {
|
||||
fn to_http_status_code(&self) -> StatusCode {
|
||||
StatusCode::NOT_ACCEPTABLE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod sign_up {
|
||||
use crate::database::models::{User, UserRole};
|
||||
use crate::routes::schema::shared::{ErrorToHttpCode, IResponse};
|
||||
use crate::routes::schema::user;
|
||||
use actix_web::http::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Request {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub group: String,
|
||||
pub role: UserRole,
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
pub type Response = IResponse<user::ResponseOk, ResponseErr>;
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ResponseOk {
|
||||
id: String,
|
||||
access_token: String,
|
||||
group: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ResponseErr {
|
||||
code: ErrorCode,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum ErrorCode {
|
||||
DisallowedRole,
|
||||
InvalidGroupName,
|
||||
UsernameAlreadyExists,
|
||||
}
|
||||
|
||||
pub trait ResponseExt {
|
||||
fn ok(user: &User) -> Self;
|
||||
fn err(code: ErrorCode) -> Self;
|
||||
}
|
||||
|
||||
impl ResponseExt for Response {
|
||||
fn ok(user: &User) -> Self {
|
||||
IResponse(Ok(user::ResponseOk::from_user(&user)))
|
||||
}
|
||||
|
||||
fn err(code: ErrorCode) -> Response {
|
||||
Self(Err(ResponseErr { code }))
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrorToHttpCode for ResponseErr {
|
||||
fn to_http_status_code(&self) -> StatusCode {
|
||||
StatusCode::NOT_ACCEPTABLE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use crate::database::driver;
|
||||
use crate::database::models::User;
|
||||
use crate::routes::auth::schema::SignInErrCode::IncorrectCredentials;
|
||||
use crate::routes::auth::schema::{SignInDto, SignInResult};
|
||||
use crate::routes::auth::schema;
|
||||
use crate::{AppState, utility};
|
||||
use actix_web::{post, web};
|
||||
use diesel::SaveChangesDsl;
|
||||
@@ -9,8 +8,13 @@ use std::ops::DerefMut;
|
||||
use web::Json;
|
||||
|
||||
#[post("/sign-in")]
|
||||
pub async fn sign_in(data: Json<SignInDto>, app_state: web::Data<AppState>) -> Json<SignInResult> {
|
||||
let result = match driver::users::get_by_username(&app_state.database, data.username.clone()) {
|
||||
pub async fn sign_in(
|
||||
data: Json<schema::sign_in::Request>,
|
||||
app_state: web::Data<AppState>,
|
||||
) -> schema::sign_in::Response {
|
||||
use schema::sign_in::*;
|
||||
|
||||
match driver::users::get_by_username(&app_state.database, data.username.clone()) {
|
||||
Ok(mut user) => match bcrypt::verify(&data.password, &user.password) {
|
||||
Ok(true) => {
|
||||
let mut lock = app_state.connection();
|
||||
@@ -21,13 +25,108 @@ pub async fn sign_in(data: Json<SignInDto>, app_state: web::Data<AppState>) -> J
|
||||
user.save_changes::<User>(conn)
|
||||
.expect("Failed to update user");
|
||||
|
||||
SignInResult::ok(&user)
|
||||
Response::ok(&user)
|
||||
}
|
||||
Ok(false) | Err(_) => SignInResult::err(IncorrectCredentials),
|
||||
Ok(false) | Err(_) => Response::err(ErrorCode::IncorrectCredentials),
|
||||
},
|
||||
|
||||
Err(_) => SignInResult::err(IncorrectCredentials),
|
||||
};
|
||||
|
||||
Json(result)
|
||||
Err(_) => Response::err(ErrorCode::IncorrectCredentials),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::app_state::app_state;
|
||||
use crate::database::driver;
|
||||
use crate::database::models::{User, UserRole};
|
||||
use crate::routes::auth::schema;
|
||||
use crate::routes::auth::sign_in::sign_in;
|
||||
use crate::test_env::tests::{static_app_state, test_app, test_env};
|
||||
use crate::utility;
|
||||
use actix_http::StatusCode;
|
||||
use actix_web::dev::ServiceResponse;
|
||||
use actix_web::http::Method;
|
||||
use actix_web::test;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::fmt::Write;
|
||||
|
||||
async fn sign_in_client(data: schema::sign_in::Request) -> ServiceResponse {
|
||||
let app = test_app(app_state(), sign_in).await;
|
||||
|
||||
let req = test::TestRequest::with_uri("/sign-in")
|
||||
.method(Method::POST)
|
||||
.set_json(data)
|
||||
.to_request();
|
||||
|
||||
test::call_service(&app, req).await
|
||||
}
|
||||
|
||||
fn prepare(username: String) {
|
||||
let id = {
|
||||
let mut sha = Sha256::new();
|
||||
sha.update(&username);
|
||||
|
||||
let result = sha.finalize();
|
||||
let bytes = &result[..12];
|
||||
|
||||
let mut hex = String::new();
|
||||
for byte in bytes {
|
||||
write!(&mut hex, "{:02x}", byte).unwrap();
|
||||
}
|
||||
|
||||
hex
|
||||
};
|
||||
|
||||
test_env();
|
||||
|
||||
let app_state = static_app_state();
|
||||
driver::users::insert_or_ignore(
|
||||
&app_state.database,
|
||||
&User {
|
||||
id: id.clone(),
|
||||
username,
|
||||
password: bcrypt::hash("example".to_string(), bcrypt::DEFAULT_COST).unwrap(),
|
||||
vk_id: None,
|
||||
access_token: utility::jwt::encode(&id),
|
||||
group: "ИС-214/23".to_string(),
|
||||
role: UserRole::Student,
|
||||
version: "1.0.0".to_string(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn sign_in_ok() {
|
||||
prepare("test::sign_in_ok".to_string());
|
||||
|
||||
let resp = sign_in_client(schema::sign_in::Request {
|
||||
username: "test::sign_in_ok".to_string(),
|
||||
password: "example".to_string(),
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn sign_in_err() {
|
||||
prepare("test::sign_in_err".to_string());
|
||||
|
||||
let invalid_username = sign_in_client(schema::sign_in::Request {
|
||||
username: "test::sign_in_err::username".to_string(),
|
||||
password: "example".to_string(),
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(invalid_username.status(), StatusCode::NOT_ACCEPTABLE);
|
||||
|
||||
let invalid_password = sign_in_client(schema::sign_in::Request {
|
||||
username: "test::sign_in_err".to_string(),
|
||||
password: "bad_password".to_string(),
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(invalid_password.status(), StatusCode::NOT_ACCEPTABLE);
|
||||
}
|
||||
}
|
||||
|
||||
153
src/routes/auth/sign_up.rs
Normal file
153
src/routes/auth/sign_up.rs
Normal file
@@ -0,0 +1,153 @@
|
||||
use crate::database::driver;
|
||||
use crate::database::models::{User, UserRole};
|
||||
use crate::routes::auth::schema;
|
||||
use crate::{utility, AppState};
|
||||
use actix_web::{post, web};
|
||||
use objectid::ObjectId;
|
||||
use web::Json;
|
||||
|
||||
#[post("/sign-up")]
|
||||
pub async fn sign_up(
|
||||
data: Json<schema::sign_up::Request>,
|
||||
app_state: web::Data<AppState>,
|
||||
) -> schema::sign_up::Response {
|
||||
use schema::sign_up::*;
|
||||
|
||||
if data.role == UserRole::Admin {
|
||||
return Response::err(ErrorCode::DisallowedRole);
|
||||
}
|
||||
|
||||
let schedule_opt = app_state.schedule.lock().unwrap();
|
||||
|
||||
if let Some(schedule) = &*schedule_opt {
|
||||
if !schedule.data.groups.contains_key(&data.group) {
|
||||
return Response::err(ErrorCode::InvalidGroupName);
|
||||
}
|
||||
}
|
||||
|
||||
if driver::users::contains_by_username(&app_state.database, data.username.clone()) {
|
||||
return Response::err(ErrorCode::UsernameAlreadyExists);
|
||||
}
|
||||
|
||||
let id = ObjectId::new().unwrap().to_string();
|
||||
let access_token = utility::jwt::encode(&id);
|
||||
|
||||
let user = User {
|
||||
id,
|
||||
username: data.username.clone(),
|
||||
password: bcrypt::hash(data.password.as_str(), bcrypt::DEFAULT_COST).unwrap(),
|
||||
vk_id: None,
|
||||
access_token,
|
||||
group: data.group.clone(),
|
||||
role: data.role.clone(),
|
||||
version: data.version.clone(),
|
||||
};
|
||||
|
||||
driver::users::insert(&app_state.database, &user).unwrap();
|
||||
|
||||
Response::ok(&user)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::app_state::app_state;
|
||||
use crate::database::driver;
|
||||
use crate::database::models::UserRole;
|
||||
use crate::routes::auth::schema;
|
||||
use crate::routes::auth::sign_up::sign_up;
|
||||
use crate::test_env::tests::{static_app_state, test_app, test_env};
|
||||
use actix_http::StatusCode;
|
||||
use actix_web::dev::ServiceResponse;
|
||||
use actix_web::http::Method;
|
||||
use actix_web::test;
|
||||
|
||||
struct SignUpPartial {
|
||||
username: String,
|
||||
group: String,
|
||||
role: UserRole,
|
||||
}
|
||||
|
||||
async fn sign_up_client(data: SignUpPartial) -> ServiceResponse {
|
||||
let app = test_app(app_state(), sign_up).await;
|
||||
|
||||
let req = test::TestRequest::with_uri("/sign-up")
|
||||
.method(Method::POST)
|
||||
.set_json(schema::sign_up::Request {
|
||||
username: data.username.clone(),
|
||||
password: "example".to_string(),
|
||||
group: data.group.clone(),
|
||||
role: data.role.clone(),
|
||||
version: "1.0.0".to_string(),
|
||||
})
|
||||
.to_request();
|
||||
|
||||
test::call_service(&app, req).await
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn sign_up_valid() {
|
||||
// prepare
|
||||
|
||||
test_env();
|
||||
|
||||
let app_state = static_app_state();
|
||||
driver::users::delete_by_username(&app_state.database, "test::sign_up_valid".to_string());
|
||||
|
||||
// test
|
||||
|
||||
let resp = sign_up_client(SignUpPartial {
|
||||
username: "test::sign_up_valid".to_string(),
|
||||
group: "ИС-214/23".to_string(),
|
||||
role: UserRole::Student,
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn sign_up_multiple() {
|
||||
// prepare
|
||||
|
||||
test_env();
|
||||
|
||||
let app_state = static_app_state();
|
||||
driver::users::delete_by_username(
|
||||
&app_state.database,
|
||||
"test::sign_up_multiple".to_string(),
|
||||
);
|
||||
|
||||
let create = sign_up_client(SignUpPartial {
|
||||
username: "test::sign_up_multiple".to_string(),
|
||||
group: "ИС-214/23".to_string(),
|
||||
role: UserRole::Student,
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(create.status(), StatusCode::OK);
|
||||
|
||||
let resp = sign_up_client(SignUpPartial {
|
||||
username: "test::sign_up_multiple".to_string(),
|
||||
group: "ИС-214/23".to_string(),
|
||||
role: UserRole::Student,
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(resp.status(), StatusCode::NOT_ACCEPTABLE);
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn sign_up_invalid_role() {
|
||||
test_env();
|
||||
|
||||
// test
|
||||
let resp = sign_up_client(SignUpPartial {
|
||||
username: "test::sign_up_invalid_role".to_string(),
|
||||
group: "ИС-214/23".to_string(),
|
||||
role: UserRole::Admin,
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(resp.status(), StatusCode::NOT_ACCEPTABLE);
|
||||
}
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
pub mod auth;
|
||||
pub mod auth;
|
||||
mod schema;
|
||||
|
||||
69
src/routes/schema.rs
Normal file
69
src/routes/schema.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
pub mod shared {
|
||||
use actix_web::body::EitherBody;
|
||||
use actix_web::error::JsonPayloadError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::{HttpRequest, HttpResponse, Responder};
|
||||
use serde::Serialize;
|
||||
|
||||
pub struct IResponse<T: Serialize, E: Serialize>(pub Result<T, E>);
|
||||
|
||||
pub trait ErrorToHttpCode {
|
||||
fn to_http_status_code(&self) -> StatusCode;
|
||||
}
|
||||
|
||||
impl<T: Serialize, E: Serialize + ErrorToHttpCode> Responder for IResponse<T, E> {
|
||||
type Body = EitherBody<String>;
|
||||
|
||||
fn respond_to(self, _: &HttpRequest) -> HttpResponse<Self::Body> {
|
||||
match serde_json::to_string(&self.0) {
|
||||
Ok(body) => {
|
||||
let code = match &self.0 {
|
||||
Ok(_) => StatusCode::OK,
|
||||
Err(e) => e.to_http_status_code(),
|
||||
};
|
||||
|
||||
match HttpResponse::build(code)
|
||||
.content_type(mime::APPLICATION_JSON)
|
||||
.message_body(body)
|
||||
{
|
||||
Ok(res) => res.map_into_left_body(),
|
||||
Err(err) => HttpResponse::from_error(err).map_into_right_body(),
|
||||
}
|
||||
}
|
||||
|
||||
Err(err) => {
|
||||
HttpResponse::from_error(JsonPayloadError::Serialize(err)).map_into_right_body()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod user {
|
||||
use crate::database::models::{User, UserRole};
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ResponseOk {
|
||||
id: String,
|
||||
username: String,
|
||||
group: String,
|
||||
role: UserRole,
|
||||
vk_id: Option<i32>,
|
||||
access_token: String,
|
||||
}
|
||||
|
||||
impl ResponseOk {
|
||||
pub fn from_user(user: &User) -> Self {
|
||||
ResponseOk {
|
||||
id: user.id.clone(),
|
||||
username: user.username.clone(),
|
||||
group: user.group.clone(),
|
||||
role: user.role.clone(),
|
||||
vk_id: user.vk_id.clone(),
|
||||
access_token: user.access_token.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user